From 2581d4f1fc22f31cf3ff0b59e3dea5a5ce3da4c8 Mon Sep 17 00:00:00 2001 From: shooter-dev Date: Wed, 7 Oct 2026 00:38:38 +0200 Subject: [PATCH] init mangacollec --- libs/mangacollec/pyproject.toml | 14 + libs/mangacollec/src/mangacollec/__init__.py | 1 + .../src/mangacollec/dto/__init__.py | 100 +++++ .../src/mangacollec/dto/author_response.py | 24 ++ .../src/mangacollec/dto/authors_response.py | 15 + .../dto/borrower_create_request.py | 21 ++ .../dto/borrower_create_response.py | 16 + .../src/mangacollec/dto/box_response.py | 31 ++ ...create_box_possessions_multiple_request.py | 26 ++ ...reate_box_possessions_multiple_response.py | 22 ++ ...ate_volume_possessions_multiple_request.py | 26 ++ ...te_volume_possessions_multiple_response.py | 18 + ...delete_box_possessions_multiple_request.py | 38 ++ ...elete_box_possessions_multiple_response.py | 26 ++ ...te_volume_possessions_multiple_response.py | 25 ++ .../src/mangacollec/dto/edition_response.py | 22 ++ .../dto/follow_edition_create_request.py | 21 ++ .../src/mangacollec/dto/kinds_response.py | 14 + .../mangacollec/dto/loans_create_request.py | 27 ++ .../mangacollec/dto/loans_create_response.py | 22 ++ .../mangacollec/dto/loans_delete_request.py | 26 ++ .../mangacollec/dto/loans_delete_response.py | 16 + .../src/mangacollec/dto/news_response.py | 30 ++ .../src/mangacollec/dto/publisher_response.py | 28 ++ .../mangacollec/dto/publishers_response.py | 14 + .../mangacollec/dto/reads_create_request.py | 38 ++ .../mangacollec/dto/reads_create_response.py | 17 + .../mangacollec/dto/reads_delete_request.py | 26 ++ .../mangacollec/dto/reads_delete_response.py | 23 ++ .../src/mangacollec/dto/serie_response.py | 36 ++ .../src/mangacollec/dto/series_response.py | 15 + .../src/mangacollec/dto/volume_response.py | 30 ++ .../src/mangacollec/endpoints/__init__.py | 5 + .../endpoints/catalogue/__init__.py | 31 ++ .../endpoints/catalogue/call_author.py | 17 + .../endpoints/catalogue/call_authors.py | 17 + .../endpoints/catalogue/call_boxes.py | 21 ++ .../endpoints/catalogue/call_edition.py | 17 + .../endpoints/catalogue/call_kinds.py | 16 + .../endpoints/catalogue/call_news.py | 16 + .../endpoints/catalogue/call_publisher.py | 17 + .../endpoints/catalogue/call_publishers.py | 16 + .../endpoints/catalogue/call_serie.py | 17 + .../endpoints/catalogue/call_series.py | 16 + .../endpoints/catalogue/call_volume.py | 17 + .../mangacollec/endpoints/follows/__init__.py | 18 + .../follows/call_create_follow_edition.py | 24 ++ .../follows/call_delete_follow_edition.py | 17 + .../mangacollec/endpoints/loans/__init__.py | 16 + .../endpoints/loans/call_create_borrower.py | 24 ++ .../loans/call_create_loans_multiple.py | 22 ++ .../endpoints/loans/call_delete_borrower.py | 21 ++ .../loans/call_delete_loans_multiple.py | 33 ++ .../endpoints/possessions/__init__.py | 33 ++ .../call_create_box_multiple_possessions.py | 38 ++ ...call_create_volume_multiple_possessions.py | 38 ++ .../call_delete_box_multiple_possessions.py | 52 +++ ...call_delete_volume_multiple_possessions.py | 54 +++ .../mangacollec/endpoints/reads/__init__.py | 18 + .../reads/call_create_reads_multiple.py | 27 ++ .../reads/call_delete_reads_multiple.py | 29 ++ .../src/mangacollec/exceptions/__init__.py | 0 .../src/mangacollec/exceptions/exceptions.py | 26 ++ .../src/mangacollec/interface/__init__.py | 14 + .../interface/endpoint_interface.py | 18 + .../interface/mutation_client_interface.py | 24 ++ .../src/mangacollec/mangacollec.py | 341 ++++++++++++++++++ .../src/mangacollec/model/__init__.py | 9 + .../mangacollec/src/mangacollec/model/base.py | 14 + .../mangacollec/model/catalogue/__init__.py | 40 ++ .../src/mangacollec/model/catalogue/author.py | 15 + .../model/catalogue/box_edition.py | 18 + .../mangacollec/model/catalogue/box_volume.py | 16 + .../src/mangacollec/model/catalogue/boxe.py | 42 +++ .../mangacollec/model/catalogue/edition.py | 21 ++ .../src/mangacollec/model/catalogue/job.py | 13 + .../src/mangacollec/model/catalogue/kind.py | 15 + .../catalogue/native_ad_volume_home_first.py | 18 + .../src/mangacollec/model/catalogue/news.py | 34 ++ .../mangacollec/model/catalogue/publisher.py | 16 + .../src/mangacollec/model/catalogue/serie.py | 19 + .../mangacollec/model/catalogue/serie_type.py | 14 + .../src/mangacollec/model/catalogue/task.py | 14 + .../src/mangacollec/model/catalogue/volume.py | 41 +++ .../src/mangacollec/model/credential.py | 19 + .../src/mangacollec/model/deleted_item.py | 14 + .../src/mangacollec/model/loans/__init__.py | 10 + .../src/mangacollec/model/loans/borrower.py | 20 + .../src/mangacollec/model/loans/loan.py | 19 + .../model/loans/loan_delete_result.py | 16 + .../mangacollec/model/possessions/__init__.py | 22 ++ .../model/possessions/box_follow_edition.py | 18 + .../model/possessions/box_possession.py | 17 + .../possessions/volume_follow_edition.py | 19 + .../model/possessions/volume_possession.py | 17 + .../src/mangacollec/model/reads/__init__.py | 10 + .../src/mangacollec/model/reads/read.py | 19 + .../mangacollec/model/reads/read_edition.py | 24 ++ .../src/mangacollec/model/token.py | 16 + .../src/mangacollec/retry_policy.py | 27 ++ .../src/mangacollec/value_object/__init__.py | 11 + .../src/mangacollec/value_object/asin.py | 19 + .../src/mangacollec/value_object/base.py | 35 ++ .../src/mangacollec/value_object/id.py | 22 ++ .../src/mangacollec/value_object/isbn.py | 18 + libs/mangacollec/tests/__init__.py | 1 + 106 files changed, 2670 insertions(+) create mode 100644 libs/mangacollec/pyproject.toml create mode 100644 libs/mangacollec/src/mangacollec/__init__.py create mode 100644 libs/mangacollec/src/mangacollec/dto/__init__.py create mode 100644 libs/mangacollec/src/mangacollec/dto/author_response.py create mode 100644 libs/mangacollec/src/mangacollec/dto/authors_response.py create mode 100644 libs/mangacollec/src/mangacollec/dto/borrower_create_request.py create mode 100644 libs/mangacollec/src/mangacollec/dto/borrower_create_response.py create mode 100644 libs/mangacollec/src/mangacollec/dto/box_response.py create mode 100644 libs/mangacollec/src/mangacollec/dto/create_box_possessions_multiple_request.py create mode 100644 libs/mangacollec/src/mangacollec/dto/create_box_possessions_multiple_response.py create mode 100644 libs/mangacollec/src/mangacollec/dto/create_volume_possessions_multiple_request.py create mode 100644 libs/mangacollec/src/mangacollec/dto/create_volume_possessions_multiple_response.py create mode 100644 libs/mangacollec/src/mangacollec/dto/delete_box_possessions_multiple_request.py create mode 100644 libs/mangacollec/src/mangacollec/dto/delete_box_possessions_multiple_response.py create mode 100644 libs/mangacollec/src/mangacollec/dto/delete_volume_possessions_multiple_response.py create mode 100644 libs/mangacollec/src/mangacollec/dto/edition_response.py create mode 100644 libs/mangacollec/src/mangacollec/dto/follow_edition_create_request.py create mode 100644 libs/mangacollec/src/mangacollec/dto/kinds_response.py create mode 100644 libs/mangacollec/src/mangacollec/dto/loans_create_request.py create mode 100644 libs/mangacollec/src/mangacollec/dto/loans_create_response.py create mode 100644 libs/mangacollec/src/mangacollec/dto/loans_delete_request.py create mode 100644 libs/mangacollec/src/mangacollec/dto/loans_delete_response.py create mode 100644 libs/mangacollec/src/mangacollec/dto/news_response.py create mode 100644 libs/mangacollec/src/mangacollec/dto/publisher_response.py create mode 100644 libs/mangacollec/src/mangacollec/dto/publishers_response.py create mode 100644 libs/mangacollec/src/mangacollec/dto/reads_create_request.py create mode 100644 libs/mangacollec/src/mangacollec/dto/reads_create_response.py create mode 100644 libs/mangacollec/src/mangacollec/dto/reads_delete_request.py create mode 100644 libs/mangacollec/src/mangacollec/dto/reads_delete_response.py create mode 100644 libs/mangacollec/src/mangacollec/dto/serie_response.py create mode 100644 libs/mangacollec/src/mangacollec/dto/series_response.py create mode 100644 libs/mangacollec/src/mangacollec/dto/volume_response.py create mode 100644 libs/mangacollec/src/mangacollec/endpoints/__init__.py create mode 100644 libs/mangacollec/src/mangacollec/endpoints/catalogue/__init__.py create mode 100644 libs/mangacollec/src/mangacollec/endpoints/catalogue/call_author.py create mode 100644 libs/mangacollec/src/mangacollec/endpoints/catalogue/call_authors.py create mode 100644 libs/mangacollec/src/mangacollec/endpoints/catalogue/call_boxes.py create mode 100644 libs/mangacollec/src/mangacollec/endpoints/catalogue/call_edition.py create mode 100644 libs/mangacollec/src/mangacollec/endpoints/catalogue/call_kinds.py create mode 100644 libs/mangacollec/src/mangacollec/endpoints/catalogue/call_news.py create mode 100644 libs/mangacollec/src/mangacollec/endpoints/catalogue/call_publisher.py create mode 100644 libs/mangacollec/src/mangacollec/endpoints/catalogue/call_publishers.py create mode 100644 libs/mangacollec/src/mangacollec/endpoints/catalogue/call_serie.py create mode 100644 libs/mangacollec/src/mangacollec/endpoints/catalogue/call_series.py create mode 100644 libs/mangacollec/src/mangacollec/endpoints/catalogue/call_volume.py create mode 100644 libs/mangacollec/src/mangacollec/endpoints/follows/__init__.py create mode 100644 libs/mangacollec/src/mangacollec/endpoints/follows/call_create_follow_edition.py create mode 100644 libs/mangacollec/src/mangacollec/endpoints/follows/call_delete_follow_edition.py create mode 100644 libs/mangacollec/src/mangacollec/endpoints/loans/__init__.py create mode 100644 libs/mangacollec/src/mangacollec/endpoints/loans/call_create_borrower.py create mode 100644 libs/mangacollec/src/mangacollec/endpoints/loans/call_create_loans_multiple.py create mode 100644 libs/mangacollec/src/mangacollec/endpoints/loans/call_delete_borrower.py create mode 100644 libs/mangacollec/src/mangacollec/endpoints/loans/call_delete_loans_multiple.py create mode 100644 libs/mangacollec/src/mangacollec/endpoints/possessions/__init__.py create mode 100644 libs/mangacollec/src/mangacollec/endpoints/possessions/call_create_box_multiple_possessions.py create mode 100644 libs/mangacollec/src/mangacollec/endpoints/possessions/call_create_volume_multiple_possessions.py create mode 100644 libs/mangacollec/src/mangacollec/endpoints/possessions/call_delete_box_multiple_possessions.py create mode 100644 libs/mangacollec/src/mangacollec/endpoints/possessions/call_delete_volume_multiple_possessions.py create mode 100644 libs/mangacollec/src/mangacollec/endpoints/reads/__init__.py create mode 100644 libs/mangacollec/src/mangacollec/endpoints/reads/call_create_reads_multiple.py create mode 100644 libs/mangacollec/src/mangacollec/endpoints/reads/call_delete_reads_multiple.py create mode 100644 libs/mangacollec/src/mangacollec/exceptions/__init__.py create mode 100644 libs/mangacollec/src/mangacollec/exceptions/exceptions.py create mode 100644 libs/mangacollec/src/mangacollec/interface/__init__.py create mode 100644 libs/mangacollec/src/mangacollec/interface/endpoint_interface.py create mode 100644 libs/mangacollec/src/mangacollec/interface/mutation_client_interface.py create mode 100644 libs/mangacollec/src/mangacollec/mangacollec.py create mode 100644 libs/mangacollec/src/mangacollec/model/__init__.py create mode 100644 libs/mangacollec/src/mangacollec/model/base.py create mode 100644 libs/mangacollec/src/mangacollec/model/catalogue/__init__.py create mode 100644 libs/mangacollec/src/mangacollec/model/catalogue/author.py create mode 100644 libs/mangacollec/src/mangacollec/model/catalogue/box_edition.py create mode 100644 libs/mangacollec/src/mangacollec/model/catalogue/box_volume.py create mode 100644 libs/mangacollec/src/mangacollec/model/catalogue/boxe.py create mode 100644 libs/mangacollec/src/mangacollec/model/catalogue/edition.py create mode 100644 libs/mangacollec/src/mangacollec/model/catalogue/job.py create mode 100644 libs/mangacollec/src/mangacollec/model/catalogue/kind.py create mode 100644 libs/mangacollec/src/mangacollec/model/catalogue/native_ad_volume_home_first.py create mode 100644 libs/mangacollec/src/mangacollec/model/catalogue/news.py create mode 100644 libs/mangacollec/src/mangacollec/model/catalogue/publisher.py create mode 100644 libs/mangacollec/src/mangacollec/model/catalogue/serie.py create mode 100644 libs/mangacollec/src/mangacollec/model/catalogue/serie_type.py create mode 100644 libs/mangacollec/src/mangacollec/model/catalogue/task.py create mode 100644 libs/mangacollec/src/mangacollec/model/catalogue/volume.py create mode 100644 libs/mangacollec/src/mangacollec/model/credential.py create mode 100644 libs/mangacollec/src/mangacollec/model/deleted_item.py create mode 100644 libs/mangacollec/src/mangacollec/model/loans/__init__.py create mode 100644 libs/mangacollec/src/mangacollec/model/loans/borrower.py create mode 100644 libs/mangacollec/src/mangacollec/model/loans/loan.py create mode 100644 libs/mangacollec/src/mangacollec/model/loans/loan_delete_result.py create mode 100644 libs/mangacollec/src/mangacollec/model/possessions/__init__.py create mode 100644 libs/mangacollec/src/mangacollec/model/possessions/box_follow_edition.py create mode 100644 libs/mangacollec/src/mangacollec/model/possessions/box_possession.py create mode 100644 libs/mangacollec/src/mangacollec/model/possessions/volume_follow_edition.py create mode 100644 libs/mangacollec/src/mangacollec/model/possessions/volume_possession.py create mode 100644 libs/mangacollec/src/mangacollec/model/reads/__init__.py create mode 100644 libs/mangacollec/src/mangacollec/model/reads/read.py create mode 100644 libs/mangacollec/src/mangacollec/model/reads/read_edition.py create mode 100644 libs/mangacollec/src/mangacollec/model/token.py create mode 100644 libs/mangacollec/src/mangacollec/retry_policy.py create mode 100644 libs/mangacollec/src/mangacollec/value_object/__init__.py create mode 100644 libs/mangacollec/src/mangacollec/value_object/asin.py create mode 100644 libs/mangacollec/src/mangacollec/value_object/base.py create mode 100644 libs/mangacollec/src/mangacollec/value_object/id.py create mode 100644 libs/mangacollec/src/mangacollec/value_object/isbn.py create mode 100644 libs/mangacollec/tests/__init__.py diff --git a/libs/mangacollec/pyproject.toml b/libs/mangacollec/pyproject.toml new file mode 100644 index 0000000..0a7dfe6 --- /dev/null +++ b/libs/mangacollec/pyproject.toml @@ -0,0 +1,14 @@ +[build-system] +requires = ["setuptools>=61"] +build-backend = "setuptools.build_meta" + +[project] +name = "mangacollec" +version = "0.1.0" +description = "Client library for MangaCollec." +requires-python = ">=3.12" +dependencies = [] + +[tool.setuptools.packages.find] +where = ["src"] +include = ["mangacollec*"] diff --git a/libs/mangacollec/src/mangacollec/__init__.py b/libs/mangacollec/src/mangacollec/__init__.py new file mode 100644 index 0000000..3d313e7 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/__init__.py @@ -0,0 +1 @@ +"""Client library for MangaCollec.""" diff --git a/libs/mangacollec/src/mangacollec/dto/__init__.py b/libs/mangacollec/src/mangacollec/dto/__init__.py new file mode 100644 index 0000000..b7e8409 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/dto/__init__.py @@ -0,0 +1,100 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +from libs.mangacollec.dto.author_response import AuthorResponse +from libs.mangacollec.dto.authors_response import AuthorsResponse +from libs.mangacollec.dto.borrower_create_request import BorrowerCreateRequest +from libs.mangacollec.dto.borrower_create_response import BorrowerCreateResponse +from libs.mangacollec.dto.box_response import BoxResponse +from libs.mangacollec.dto.create_box_possessions_multiple_request import ( + CreateBoxesPossessionsMultipleRequest, + CreateBoxPossessionMultipleRequest, +) +from libs.mangacollec.dto.create_box_possessions_multiple_response import ( + CreateBoxPossessionsMultipleResponse, +) +from libs.mangacollec.dto.create_volume_possessions_multiple_request import ( + CreateVolumePossessionMultipleRequest, + CreateVolumesPossessionsMultipleRequest, +) +from libs.mangacollec.dto.create_volume_possessions_multiple_response import ( + CreateVolumePossessionsMultipleResponse, +) +from libs.mangacollec.dto.delete_box_possessions_multiple_request import ( + DeleteBoxPossessionMultipleRequest, + DeleteBoxPossessionsMultipleRequest, + DeletePossessionMultipleRequest, +) +from libs.mangacollec.dto.delete_box_possessions_multiple_response import ( + DeleteBoxPossessionsMultipleResponse, +) +from libs.mangacollec.dto.delete_box_possessions_multiple_response import ( + DeletedResource as DeletedBoxResource, +) +from libs.mangacollec.dto.delete_volume_possessions_multiple_response import ( + DeletedResource, + DeleteVolumePossessionsMultipleResponse, +) +from libs.mangacollec.dto.edition_response import EditionResponse +from libs.mangacollec.dto.follow_edition_create_request import FollowEditionCreateRequest +from libs.mangacollec.dto.kinds_response import KindsResponse +from libs.mangacollec.dto.loans_create_request import LoanCreateRequest, LoansCreateRequest +from libs.mangacollec.dto.loans_create_response import LoanCreateError, LoansCreateResponse +from libs.mangacollec.dto.loans_delete_request import LoanDeleteRequest, LoansDeleteRequest +from libs.mangacollec.dto.loans_delete_response import LoansDeleteResponse +from libs.mangacollec.dto.news_response import NewsResponse +from libs.mangacollec.dto.publisher_response import PublisherResponse +from libs.mangacollec.dto.publishers_response import PublishersResponse +from libs.mangacollec.dto.reads_create_request import ReadCreateRequest, ReadsCreateRequest +from libs.mangacollec.dto.reads_create_response import ReadsCreateResponse +from libs.mangacollec.dto.reads_delete_request import ReadDeleteRequest, ReadsDeleteRequest +from libs.mangacollec.dto.reads_delete_response import ReadDeleteError, ReadsDeleteResponse +from libs.mangacollec.dto.serie_response import SerieResponse +from libs.mangacollec.dto.series_response import SeriesResponse +from libs.mangacollec.dto.volume_response import VolumeResponse + +__all__ = [ + "AuthorResponse", + "AuthorsResponse", + "BorrowerCreateRequest", + "BorrowerCreateResponse", + "BoxResponse", + "CreateBoxPossessionMultipleRequest", + "CreateBoxPossessionsMultipleResponse", + "CreateBoxesPossessionsMultipleRequest", + "CreateVolumePossessionMultipleRequest", + "CreateVolumePossessionsMultipleResponse", + "CreateVolumesPossessionsMultipleRequest", + "DeleteBoxPossessionMultipleRequest", + "DeleteBoxPossessionsMultipleRequest", + "DeleteBoxPossessionsMultipleResponse", + "DeletePossessionMultipleRequest", + "DeleteVolumePossessionsMultipleResponse", + "DeletedBoxResource", + "DeletedResource", + "EditionResponse", + "FollowEditionCreateRequest", + "KindsResponse", + "LoanCreateError", + "LoanCreateRequest", + "LoanDeleteRequest", + "LoansCreateRequest", + "LoansCreateResponse", + "LoansDeleteRequest", + "LoansDeleteResponse", + "NewsResponse", + "PublisherResponse", + "PublishersResponse", + "ReadCreateRequest", + "ReadDeleteError", + "ReadDeleteRequest", + "ReadsCreateRequest", + "ReadsCreateResponse", + "ReadsDeleteRequest", + "ReadsDeleteResponse", + "SerieResponse", + "SeriesResponse", + "VolumeResponse", +] diff --git a/libs/mangacollec/src/mangacollec/dto/author_response.py b/libs/mangacollec/src/mangacollec/dto/author_response.py new file mode 100644 index 0000000..694de4a --- /dev/null +++ b/libs/mangacollec/src/mangacollec/dto/author_response.py @@ -0,0 +1,24 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +from pydantic import BaseModel + +from libs.mangacollec.model.catalogue.author import Author +from libs.mangacollec.model.catalogue.edition import Edition +from libs.mangacollec.model.catalogue.job import Job +from libs.mangacollec.model.catalogue.serie import Serie +from libs.mangacollec.model.catalogue.task import Task +from libs.mangacollec.model.catalogue.volume import Volume + + +class AuthorResponse(BaseModel): + """Réponse détaillée d'un auteur et de ses ressources associées.""" + + authors: list[Author] + tasks: list[Task] + jobs: list[Job] + series: list[Serie] + editions: list[Edition] + volumes: list[Volume] diff --git a/libs/mangacollec/src/mangacollec/dto/authors_response.py b/libs/mangacollec/src/mangacollec/dto/authors_response.py new file mode 100644 index 0000000..faefabb --- /dev/null +++ b/libs/mangacollec/src/mangacollec/dto/authors_response.py @@ -0,0 +1,15 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ + +from pydantic import BaseModel + +from libs.mangacollec.model.catalogue.author import Author + + +class AuthorsResponse(BaseModel): + """Réponse de l'endpoint listant les auteurs du catalogue.""" + + authors: list[Author] diff --git a/libs/mangacollec/src/mangacollec/dto/borrower_create_request.py b/libs/mangacollec/src/mangacollec/dto/borrower_create_request.py new file mode 100644 index 0000000..c759770 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/dto/borrower_create_request.py @@ -0,0 +1,21 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +"""DTO de création d'un emprunteur ou d'un lieu de stockage.""" + +from typing import Literal + +from pydantic import BaseModel + + +class BorrowerCreateRequest(BaseModel): + """Corps JSON attendu par POST /v2/borrowers.""" + + category: Literal["person", "storage"] + title: str + + def to_api_payload(self) -> dict[str, object]: + """Sérialiser les champs de création dans la forme attendue par l'API.""" + return self.model_dump(mode="json") diff --git a/libs/mangacollec/src/mangacollec/dto/borrower_create_response.py b/libs/mangacollec/src/mangacollec/dto/borrower_create_response.py new file mode 100644 index 0000000..0c6bde7 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/dto/borrower_create_response.py @@ -0,0 +1,16 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +"""DTO de l'emprunteur retourné par POST /v2/borrowers.""" + +from datetime import datetime + +from libs.mangacollec.model.loans.borrower import Borrower + + +class BorrowerCreateResponse(Borrower): + """Emprunteur créé avec la date de mise à jour propre à la réponse POST.""" + + updated_at: datetime diff --git a/libs/mangacollec/src/mangacollec/dto/box_response.py b/libs/mangacollec/src/mangacollec/dto/box_response.py new file mode 100644 index 0000000..bd14c02 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/dto/box_response.py @@ -0,0 +1,31 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ + +from pydantic import BaseModel + +from libs.mangacollec.model.catalogue import ( + Boxe, + BoxEdition, + BoxVolume, + Edition, + Publisher, + Serie, + SerieType, + Volume, +) + + +class BoxResponse(BaseModel): + """Réponse détaillée d'un coffret et de ses ressources associées.""" + + boxes: list[Boxe] + box_editions: list[BoxEdition] + publishers: list[Publisher] + box_volumes: list[BoxVolume] + volumes: list[Volume] + editions: list[Edition] + series: list[Serie] + types: list[SerieType] diff --git a/libs/mangacollec/src/mangacollec/dto/create_box_possessions_multiple_request.py b/libs/mangacollec/src/mangacollec/dto/create_box_possessions_multiple_request.py new file mode 100644 index 0000000..6af16ca --- /dev/null +++ b/libs/mangacollec/src/mangacollec/dto/create_box_possessions_multiple_request.py @@ -0,0 +1,26 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +"""DTO de création groupée de possessions.""" + +from pydantic import BaseModel + +from libs.mangacollec.value_object.id import ID + + +class CreateBoxPossessionMultipleRequest(BaseModel): + """box à ajouter à la collection.""" + + box_id: ID + + +class CreateBoxesPossessionsMultipleRequest(BaseModel): + """Liste des boxes à ajouter.""" + + possessions: list[CreateBoxPossessionMultipleRequest] + + def to_api_payload(self) -> list[dict[str, str]]: + """Convertir le DTO vers le tableau JSON attendu par l'API.""" + return [item.model_dump(mode="json") for item in self.possessions] diff --git a/libs/mangacollec/src/mangacollec/dto/create_box_possessions_multiple_response.py b/libs/mangacollec/src/mangacollec/dto/create_box_possessions_multiple_response.py new file mode 100644 index 0000000..811aeb2 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/dto/create_box_possessions_multiple_response.py @@ -0,0 +1,22 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +"""DTO de réponse de création groupée de possessions.""" + +from pydantic import BaseModel + +from libs.mangacollec.model.possessions.box_follow_edition import BoxFollowEdition +from libs.mangacollec.model.possessions.box_possession import BoxPossession +from libs.mangacollec.model.possessions.volume_follow_edition import VolumeFollowEdition +from libs.mangacollec.model.possessions.volume_possession import VolumePossession + + +class CreateBoxPossessionsMultipleResponse(BaseModel): + """Réponse de l'endpoint de boxe possessions multiples.""" + + box_possessions: list[BoxPossession] + box_follow_editions: list[BoxFollowEdition] + possessions: list[VolumePossession] + follow_editions: list[VolumeFollowEdition] diff --git a/libs/mangacollec/src/mangacollec/dto/create_volume_possessions_multiple_request.py b/libs/mangacollec/src/mangacollec/dto/create_volume_possessions_multiple_request.py new file mode 100644 index 0000000..88b61ca --- /dev/null +++ b/libs/mangacollec/src/mangacollec/dto/create_volume_possessions_multiple_request.py @@ -0,0 +1,26 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +"""DTO de création groupée de possessions.""" + +from pydantic import BaseModel + +from libs.mangacollec.value_object.id import ID + + +class CreateVolumePossessionMultipleRequest(BaseModel): + """Volume à ajouter à la collection.""" + + volume_id: ID + + +class CreateVolumesPossessionsMultipleRequest(BaseModel): + """Liste des volumes à ajouter.""" + + possessions: list[CreateVolumePossessionMultipleRequest] + + def to_api_payload(self) -> list[dict[str, str]]: + """Convertir le DTO vers le tableau JSON attendu par l'API.""" + return [item.model_dump(mode="json") for item in self.possessions] diff --git a/libs/mangacollec/src/mangacollec/dto/create_volume_possessions_multiple_response.py b/libs/mangacollec/src/mangacollec/dto/create_volume_possessions_multiple_response.py new file mode 100644 index 0000000..15370c4 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/dto/create_volume_possessions_multiple_response.py @@ -0,0 +1,18 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +"""DTO de réponse de création groupée de possessions.""" + +from pydantic import BaseModel + +from libs.mangacollec.model.possessions import VolumePossession +from libs.mangacollec.model.possessions.volume_follow_edition import VolumeFollowEdition + + +class CreateVolumePossessionsMultipleResponse(BaseModel): + """Réponse de l'endpoint de volume possessions multiples.""" + + possessions: list[VolumePossession] + follow_editions: list[VolumeFollowEdition] diff --git a/libs/mangacollec/src/mangacollec/dto/delete_box_possessions_multiple_request.py b/libs/mangacollec/src/mangacollec/dto/delete_box_possessions_multiple_request.py new file mode 100644 index 0000000..a0ec1ed --- /dev/null +++ b/libs/mangacollec/src/mangacollec/dto/delete_box_possessions_multiple_request.py @@ -0,0 +1,38 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +"""DTO de suppression groupée de possessions de boxes.""" + +from pydantic import BaseModel + +from libs.mangacollec.value_object.id import ID + + +class DeleteBoxPossessionMultipleRequest(BaseModel): + """Box possession à supprimer.""" + + id: ID + + +class DeletePossessionMultipleRequest(BaseModel): + """Possession de volume à supprimer.""" + + id: ID + + +class DeleteBoxPossessionsMultipleRequest(BaseModel): + """Objet de requête de suppression groupée de box possessions.""" + + box_possessions: list[DeleteBoxPossessionMultipleRequest] + possessions: list[DeletePossessionMultipleRequest] | None = None + + def to_api_payload(self) -> dict[str, object]: + """Convertir le DTO vers l'objet JSON attendu par l'API.""" + payload: dict[str, object] = { + "box_possessions": [item.model_dump(mode="json") for item in self.box_possessions] + } + if self.possessions is not None: + payload["possessions"] = [item.model_dump(mode="json") for item in self.possessions] + return payload diff --git a/libs/mangacollec/src/mangacollec/dto/delete_box_possessions_multiple_response.py b/libs/mangacollec/src/mangacollec/dto/delete_box_possessions_multiple_response.py new file mode 100644 index 0000000..90d679d --- /dev/null +++ b/libs/mangacollec/src/mangacollec/dto/delete_box_possessions_multiple_response.py @@ -0,0 +1,26 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +"""DTO de réponse de suppression groupée de possessions de boxes.""" + +from pydantic import BaseModel + +from libs.mangacollec.value_object.id import ID + + +class DeletedResource(BaseModel): + """Ressource supprimée par l'API.""" + + id: ID + deleted: bool + + +class DeleteBoxPossessionsMultipleResponse(BaseModel): + """Réponse de l'endpoint de suppression groupée de box possessions.""" + + box_possessions: list[DeletedResource] + box_follow_editions: list[DeletedResource] + possessions: list[DeletedResource] + follow_editions: list[DeletedResource] diff --git a/libs/mangacollec/src/mangacollec/dto/delete_volume_possessions_multiple_response.py b/libs/mangacollec/src/mangacollec/dto/delete_volume_possessions_multiple_response.py new file mode 100644 index 0000000..65e295f --- /dev/null +++ b/libs/mangacollec/src/mangacollec/dto/delete_volume_possessions_multiple_response.py @@ -0,0 +1,25 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +"""DTO de réponse de suppression groupée de possessions.""" + +from pydantic import BaseModel + +from libs.mangacollec.value_object.id import ID + + +class DeletedResource(BaseModel): + """Ressource supprimée par l'API.""" + + id: ID + deleted: bool + + +class DeleteVolumePossessionsMultipleResponse(BaseModel): + """Réponse de l'endpoint de suppression groupée.""" + + possessions: list[DeletedResource] + follow_editions: list[DeletedResource] + loans: list[DeletedResource] | None = None diff --git a/libs/mangacollec/src/mangacollec/dto/edition_response.py b/libs/mangacollec/src/mangacollec/dto/edition_response.py new file mode 100644 index 0000000..0f78459 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/dto/edition_response.py @@ -0,0 +1,22 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +from pydantic import BaseModel + +from libs.mangacollec.model.catalogue.edition import Edition +from libs.mangacollec.model.catalogue.publisher import Publisher +from libs.mangacollec.model.catalogue.serie import Serie +from libs.mangacollec.model.catalogue.serie_type import SerieType +from libs.mangacollec.model.catalogue.volume import Volume + + +class EditionResponse(BaseModel): + """Réponse détaillée de l'endpoint d'une édition.""" + + editions: list[Edition] + publishers: list[Publisher] + series: list[Serie] + types: list[SerieType] + volumes: list[Volume] diff --git a/libs/mangacollec/src/mangacollec/dto/follow_edition_create_request.py b/libs/mangacollec/src/mangacollec/dto/follow_edition_create_request.py new file mode 100644 index 0000000..e8413b6 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/dto/follow_edition_create_request.py @@ -0,0 +1,21 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +"""DTO de création ou d'activation d'un suivi d'édition.""" + +from pydantic import BaseModel + +from libs.mangacollec.value_object.id import ID + + +class FollowEditionCreateRequest(BaseModel): + """Corps JSON attendu par POST /v1/follow_editions.""" + + edition_id: ID + following: bool + + def to_api_payload(self) -> dict[str, object]: + """Sérialiser la requête dans la forme JSON attendue par l'API.""" + return self.model_dump(mode="json") diff --git a/libs/mangacollec/src/mangacollec/dto/kinds_response.py b/libs/mangacollec/src/mangacollec/dto/kinds_response.py new file mode 100644 index 0000000..8642898 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/dto/kinds_response.py @@ -0,0 +1,14 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +from pydantic import BaseModel + +from libs.mangacollec.model.catalogue.kind import Kind + + +class KindsResponse(BaseModel): + """Réponse contenant les genres.""" + + kinds: list[Kind] diff --git a/libs/mangacollec/src/mangacollec/dto/loans_create_request.py b/libs/mangacollec/src/mangacollec/dto/loans_create_request.py new file mode 100644 index 0000000..50d7273 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/dto/loans_create_request.py @@ -0,0 +1,27 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +"""DTO de création groupée de prêts.""" + +from pydantic import BaseModel + +from libs.mangacollec.value_object.id import ID + + +class LoanCreateRequest(BaseModel): + """Association entre un emprunteur et une possession à prêter.""" + + borrower_id: ID + possession_id: ID + + +class LoansCreateRequest(BaseModel): + """Liste des associations emprunteur-possession à créer.""" + + loans: list[LoanCreateRequest] + + def to_api_payload(self) -> dict[str, object]: + """Convertir les associations vers l'objet attendu par l'API.""" + return {"loans": [loan.model_dump(mode="json") for loan in self.loans]} diff --git a/libs/mangacollec/src/mangacollec/dto/loans_create_response.py b/libs/mangacollec/src/mangacollec/dto/loans_create_response.py new file mode 100644 index 0000000..578cea4 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/dto/loans_create_response.py @@ -0,0 +1,22 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +"""DTO des résultats de création groupée de prêts.""" + +from pydantic import BaseModel + +from libs.mangacollec.model.loans.loan import Loan + + +class LoanCreateError(BaseModel): + """Erreurs métier d'un prêt refusé, renvoyées sous un statut HTTP 200.""" + + errors: dict[str, list[str]] + + +class LoansCreateResponse(BaseModel): + """Prêts créés et erreurs métier propres aux éléments du lot.""" + + loans: list[Loan | LoanCreateError] diff --git a/libs/mangacollec/src/mangacollec/dto/loans_delete_request.py b/libs/mangacollec/src/mangacollec/dto/loans_delete_request.py new file mode 100644 index 0000000..fd5323f --- /dev/null +++ b/libs/mangacollec/src/mangacollec/dto/loans_delete_request.py @@ -0,0 +1,26 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +"""DTO de requête de suppression groupée de prêts.""" + +from pydantic import BaseModel + +from libs.mangacollec.value_object.id import ID + + +class LoanDeleteRequest(BaseModel): + """Prêt à supprimer, identifié par l'ID reçu à sa création.""" + + id: ID + + +class LoansDeleteRequest(BaseModel): + """Prêts à sérialiser dans le tableau loans du corps de requête.""" + + loans: list[LoanDeleteRequest] + + def to_api_payload(self) -> dict[str, object]: + """Convertir les identifiants validés vers l'objet attendu par l'API.""" + return {"loans": [{"id": str(loan.id)} for loan in self.loans]} diff --git a/libs/mangacollec/src/mangacollec/dto/loans_delete_response.py b/libs/mangacollec/src/mangacollec/dto/loans_delete_response.py new file mode 100644 index 0000000..e3995c5 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/dto/loans_delete_response.py @@ -0,0 +1,16 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +"""DTO de réponse de suppression groupée de prêts.""" + +from pydantic import BaseModel + +from libs.mangacollec.model.loans.loan_delete_result import LoanDeleteResult + + +class LoansDeleteResponse(BaseModel): + """Résultats individuels de suppression, incluant les erreurs métier.""" + + loans: list[LoanDeleteResult] diff --git a/libs/mangacollec/src/mangacollec/dto/news_response.py b/libs/mangacollec/src/mangacollec/dto/news_response.py new file mode 100644 index 0000000..6fa4f50 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/dto/news_response.py @@ -0,0 +1,30 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +from pydantic import BaseModel + +from libs.mangacollec.model.catalogue import ( + Boxe, + BoxEdition, + BoxVolume, + Edition, + NativeAdVolumeHomeFirst, + Serie, + SerieType, + Volume, +) + + +class NewsResponse(BaseModel): + """Réponse des dernières sorties de volumes et de coffrets.""" + + volumes: list[Volume] + editions: list[Edition] + series: list[Serie] + types: list[SerieType] + boxes: list[Boxe] + box_editions: list[BoxEdition] + box_volumes: list[BoxVolume] + native_ad_volume_home_first: NativeAdVolumeHomeFirst | None = None diff --git a/libs/mangacollec/src/mangacollec/dto/publisher_response.py b/libs/mangacollec/src/mangacollec/dto/publisher_response.py new file mode 100644 index 0000000..f2c3512 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/dto/publisher_response.py @@ -0,0 +1,28 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +from pydantic import BaseModel + +from libs.mangacollec.model.catalogue import ( + Boxe, + BoxEdition, + Edition, + Publisher, + Serie, + SerieType, + Volume, +) + + +class PublisherResponse(BaseModel): + """PlublisherResponse.""" + + box_editions: list[BoxEdition] + boxes: list[Boxe] + editions: list[Edition] + publishers: list[Publisher] + series: list[Serie] + types: list[SerieType] + volumes: list[Volume] diff --git a/libs/mangacollec/src/mangacollec/dto/publishers_response.py b/libs/mangacollec/src/mangacollec/dto/publishers_response.py new file mode 100644 index 0000000..eabaeab --- /dev/null +++ b/libs/mangacollec/src/mangacollec/dto/publishers_response.py @@ -0,0 +1,14 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +from pydantic import BaseModel + +from libs.mangacollec.model.catalogue import Publisher + + +class PublishersResponse(BaseModel): + """PlublishersResponse.""" + + publishers: list[Publisher] diff --git a/libs/mangacollec/src/mangacollec/dto/reads_create_request.py b/libs/mangacollec/src/mangacollec/dto/reads_create_request.py new file mode 100644 index 0000000..42dfb48 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/dto/reads_create_request.py @@ -0,0 +1,38 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +"""DTO de création groupée de lectures.""" + +from pydantic import BaseModel, field_validator + +from libs.mangacollec.exceptions import DomainValidationError +from libs.mangacollec.value_object.id import ID + + +class ReadCreateRequest(BaseModel): + """Volume à marquer comme lu.""" + + volume_id: ID + + +class ReadsCreateRequest(BaseModel): + """Volumes à marquer lus, avec garde-fous locaux de non-vacuité et d'unicité.""" + + reads: list[ReadCreateRequest] + + @field_validator("reads") + @classmethod + def validate_reads(cls, reads: list[ReadCreateRequest]) -> list[ReadCreateRequest]: + """Refuser localement les listes vides et les volumes répétés.""" + if not reads: + raise DomainValidationError("La liste des volumes ne peut pas être vide.") + volume_ids = [read.volume_id for read in reads] + if len(set(volume_ids)) != len(volume_ids): + raise DomainValidationError("Les identifiants de volumes doivent être distincts.") + return reads + + def to_api_payload(self) -> list[dict[str, str]]: + """Sérialiser le tableau racine attendu par POST /v1/reads_multiple.""" + return [{"volume_id": str(read.volume_id)} for read in self.reads] diff --git a/libs/mangacollec/src/mangacollec/dto/reads_create_response.py b/libs/mangacollec/src/mangacollec/dto/reads_create_response.py new file mode 100644 index 0000000..ad550b5 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/dto/reads_create_response.py @@ -0,0 +1,17 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +"""DTO de réponse de création groupée de lectures.""" + +from pydantic import BaseModel + +from libs.mangacollec.model.reads import Read, ReadEdition + + +class ReadsCreateResponse(BaseModel): + """Lectures et états des éditions renvoyés par POST /v1/reads_multiple.""" + + reads: list[Read] + read_editions: list[ReadEdition] diff --git a/libs/mangacollec/src/mangacollec/dto/reads_delete_request.py b/libs/mangacollec/src/mangacollec/dto/reads_delete_request.py new file mode 100644 index 0000000..70d314c --- /dev/null +++ b/libs/mangacollec/src/mangacollec/dto/reads_delete_request.py @@ -0,0 +1,26 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +"""DTO de requête de suppression groupée de lectures.""" + +from pydantic import BaseModel + +from libs.mangacollec.value_object.id import ID + + +class ReadDeleteRequest(BaseModel): + """Lecture à supprimer, identifiée par l'ID reçu à sa création.""" + + id: ID + + +class ReadsDeleteRequest(BaseModel): + """Lectures à sérialiser sous forme de tableau à la racine du corps JSON.""" + + reads: list[ReadDeleteRequest] + + def to_api_payload(self) -> list[dict[str, str]]: + """Convertir les identifiants validés vers le corps attendu par l'API.""" + return [{"id": str(read.id)} for read in self.reads] diff --git a/libs/mangacollec/src/mangacollec/dto/reads_delete_response.py b/libs/mangacollec/src/mangacollec/dto/reads_delete_response.py new file mode 100644 index 0000000..c16558b --- /dev/null +++ b/libs/mangacollec/src/mangacollec/dto/reads_delete_response.py @@ -0,0 +1,23 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +"""DTO de réponse de suppression groupée de lectures.""" + +from pydantic import BaseModel + +from libs.mangacollec.model.deleted_item import DeletedItem + + +class ReadDeleteError(BaseModel): + """Erreur métier de suppression, renvoyée dans reads même sous HTTP 200.""" + + errors: list[str] + + +class ReadsDeleteResponse(BaseModel): + """Suppressions et erreurs métier des lectures, avec leurs suivis supprimés.""" + + reads: list[DeletedItem | ReadDeleteError] + read_editions: list[DeletedItem] diff --git a/libs/mangacollec/src/mangacollec/dto/serie_response.py b/libs/mangacollec/src/mangacollec/dto/serie_response.py new file mode 100644 index 0000000..f9eb858 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/dto/serie_response.py @@ -0,0 +1,36 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +from pydantic import BaseModel + +from libs.mangacollec.model.catalogue.author import Author +from libs.mangacollec.model.catalogue.box_edition import BoxEdition +from libs.mangacollec.model.catalogue.box_volume import BoxVolume +from libs.mangacollec.model.catalogue.boxe import Boxe +from libs.mangacollec.model.catalogue.edition import Edition +from libs.mangacollec.model.catalogue.job import Job +from libs.mangacollec.model.catalogue.kind import Kind +from libs.mangacollec.model.catalogue.publisher import Publisher +from libs.mangacollec.model.catalogue.serie import Serie +from libs.mangacollec.model.catalogue.serie_type import SerieType +from libs.mangacollec.model.catalogue.task import Task +from libs.mangacollec.model.catalogue.volume import Volume + + +class SerieResponse(BaseModel): + """SerieResponse.""" + + series: list[Serie] + types: list[SerieType] + kinds: list[Kind] + tasks: list[Task] + jobs: list[Job] + authors: list[Author] + editions: list[Edition] + publishers: list[Publisher] + volumes: list[Volume] + box_editions: list[BoxEdition] + boxes: list[Boxe] + box_volumes: list[BoxVolume] diff --git a/libs/mangacollec/src/mangacollec/dto/series_response.py b/libs/mangacollec/src/mangacollec/dto/series_response.py new file mode 100644 index 0000000..f8bbf7d --- /dev/null +++ b/libs/mangacollec/src/mangacollec/dto/series_response.py @@ -0,0 +1,15 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +from pydantic import BaseModel + +from libs.mangacollec.model.catalogue import Serie, SerieType + + +class SeriesResponse(BaseModel): + """Réponse contenant les séries et leurs types.""" + + series: list[Serie] + types: list[SerieType] diff --git a/libs/mangacollec/src/mangacollec/dto/volume_response.py b/libs/mangacollec/src/mangacollec/dto/volume_response.py new file mode 100644 index 0000000..1e18781 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/dto/volume_response.py @@ -0,0 +1,30 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +from pydantic import BaseModel + +from libs.mangacollec.model.catalogue import ( + BoxEdition, + Edition, + Publisher, + Serie, + Volume, +) +from libs.mangacollec.model.catalogue.box_volume import BoxVolume +from libs.mangacollec.model.catalogue.boxe import Boxe +from libs.mangacollec.model.catalogue.serie_type import SerieType + + +class VolumeResponse(BaseModel): + """Réponse détaillée d'un volume et de ses relations de catalogue.""" + + volumes: list[Volume] + editions: list[Edition] + publishers: list[Publisher] + series: list[Serie] + types: list[SerieType] + box_volumes: list[BoxVolume] | None + boxes: list[Boxe] | None + box_editions: list[BoxEdition] | None diff --git a/libs/mangacollec/src/mangacollec/endpoints/__init__.py b/libs/mangacollec/src/mangacollec/endpoints/__init__.py new file mode 100644 index 0000000..7151957 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/endpoints/__init__.py @@ -0,0 +1,5 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ diff --git a/libs/mangacollec/src/mangacollec/endpoints/catalogue/__init__.py b/libs/mangacollec/src/mangacollec/endpoints/catalogue/__init__.py new file mode 100644 index 0000000..b623b94 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/endpoints/catalogue/__init__.py @@ -0,0 +1,31 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ + +from .call_author import CallAuthorEndpoint +from .call_authors import CallAuthorsEndpoint +from .call_boxes import CallBoxesEndpoint +from .call_edition import CallEditionEndpoint +from .call_kinds import CallKindsEndpoint +from .call_news import CallNewsEndpoint +from .call_publisher import CallPublisherEndpoint +from .call_publishers import CallPublishesEndpoint +from .call_serie import CallSerieEndpoint +from .call_series import CallSeriesEndpoint +from .call_volume import CallVolumeEndpoint + +__all__ = [ + "CallAuthorEndpoint", + "CallAuthorsEndpoint", + "CallBoxesEndpoint", + "CallEditionEndpoint", + "CallKindsEndpoint", + "CallNewsEndpoint", + "CallPublisherEndpoint", + "CallPublishesEndpoint", + "CallSerieEndpoint", + "CallSeriesEndpoint", + "CallVolumeEndpoint", +] diff --git a/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_author.py b/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_author.py new file mode 100644 index 0000000..46b29c2 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_author.py @@ -0,0 +1,17 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +from libs.mangacollec.dto import AuthorResponse +from libs.mangacollec.interface.endpoint_interface import IEndpoint +from libs.mangacollec.value_object.id import ID + + +class CallAuthorEndpoint(IEndpoint): + """Appeler l'endpoint de détail d'un auteur.""" + + async def execute(self, id_author: ID) -> AuthorResponse: + """Récupérer et valider le détail de l'auteur identifié.""" + response = await self.client.get(f"v2/authors/{id_author}") + return AuthorResponse.model_validate(response) diff --git a/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_authors.py b/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_authors.py new file mode 100644 index 0000000..38c731f --- /dev/null +++ b/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_authors.py @@ -0,0 +1,17 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ + +from libs.mangacollec.dto.authors_response import AuthorsResponse +from libs.mangacollec.interface.endpoint_interface import IEndpoint + + +class CallAuthorsEndpoint(IEndpoint): + """Appeler l'endpoint de liste des auteurs.""" + + async def execute(self) -> AuthorsResponse: + """Récupérer et valider les auteurs du catalogue.""" + response = await self.client.get("v2/authors") + return AuthorsResponse.model_validate(response) diff --git a/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_boxes.py b/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_boxes.py new file mode 100644 index 0000000..fc0e3b0 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_boxes.py @@ -0,0 +1,21 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ + +from libs.mangacollec.dto import BoxResponse +from libs.mangacollec.interface.endpoint_interface import IEndpoint +from libs.mangacollec.value_object.id import ID + + +class CallBoxesEndpoint(IEndpoint): + """Appeler GET /v2/boxes/{id}.""" + + async def execute(self, id_box: ID) -> BoxResponse: + """Récupérer et valider le détail du coffret indiqué.""" + response = await self.client.get(f"v2/boxes/{id_box}") + return BoxResponse.model_validate(response) + + +CallBoxEndpoint = CallBoxesEndpoint diff --git a/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_edition.py b/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_edition.py new file mode 100644 index 0000000..d2a2b39 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_edition.py @@ -0,0 +1,17 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +from libs.mangacollec.dto import EditionResponse +from libs.mangacollec.interface.endpoint_interface import IEndpoint +from libs.mangacollec.value_object.id import ID + + +class CallEditionEndpoint(IEndpoint): + """Appeler GET /v2/editions/{id}.""" + + async def execute(self, id_edition: ID) -> EditionResponse: + """Récupérer et valider le détail d'une édition.""" + response = await self.client.get(f"v2/editions/{id_edition}") + return EditionResponse.model_validate(response) diff --git a/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_kinds.py b/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_kinds.py new file mode 100644 index 0000000..2375d09 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_kinds.py @@ -0,0 +1,16 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +from libs.mangacollec.dto.kinds_response import KindsResponse +from libs.mangacollec.interface.endpoint_interface import IEndpoint + + +class CallKindsEndpoint(IEndpoint): + """Appeler l'endpoint de récupération des genres.""" + + async def execute(self) -> KindsResponse: + """Récupérer les genres depuis MangaCollec.""" + response = await self.client.get("v2/kinds") + return KindsResponse.model_validate(response) diff --git a/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_news.py b/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_news.py new file mode 100644 index 0000000..b422c00 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_news.py @@ -0,0 +1,16 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +from libs.mangacollec.dto.news_response import NewsResponse +from libs.mangacollec.interface.endpoint_interface import IEndpoint + + +class CallNewsEndpoint(IEndpoint): + """Appeler l'endpoint des dernières sorties du catalogue.""" + + async def execute(self) -> NewsResponse: + """Récupérer et valider les dernières sorties.""" + response = await self.client.get("v2/volumes/news") + return NewsResponse.model_validate(response) diff --git a/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_publisher.py b/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_publisher.py new file mode 100644 index 0000000..370e50e --- /dev/null +++ b/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_publisher.py @@ -0,0 +1,17 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +from libs.mangacollec.dto import PublisherResponse +from libs.mangacollec.interface.endpoint_interface import IEndpoint +from libs.mangacollec.value_object.id import ID + + +class CallPublisherEndpoint(IEndpoint): + """Appeler l'endpoint de récupération d'un éditeur.""" + + async def execute(self, id_publisher: ID) -> PublisherResponse: + """Récupérer un éditeur depuis MangaCollec.""" + response = await self.client.get(f"v2/publishers/{id_publisher}") + return PublisherResponse.model_validate(response) diff --git a/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_publishers.py b/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_publishers.py new file mode 100644 index 0000000..e88d90e --- /dev/null +++ b/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_publishers.py @@ -0,0 +1,16 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +from libs.mangacollec.dto.publishers_response import PublishersResponse +from libs.mangacollec.interface.endpoint_interface import IEndpoint + + +class CallPublishesEndpoint(IEndpoint): + """Call Publishers Class.""" + + async def execute(self) -> PublishersResponse: + """.""" + response = await self.client.get("v2/publishers") + return PublishersResponse.model_validate(response) diff --git a/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_serie.py b/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_serie.py new file mode 100644 index 0000000..2afdd18 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_serie.py @@ -0,0 +1,17 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +from libs.mangacollec.dto import SerieResponse +from libs.mangacollec.interface.endpoint_interface import IEndpoint +from libs.mangacollec.value_object.id import ID + + +class CallSerieEndpoint(IEndpoint): + """callSerie Class.""" + + async def execute(self, id_serie: ID) -> SerieResponse: + """.""" + response = await self.client.get(f"v2/series/{id_serie}") + return SerieResponse.model_validate(response) diff --git a/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_series.py b/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_series.py new file mode 100644 index 0000000..f3bf486 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_series.py @@ -0,0 +1,16 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +from libs.mangacollec.dto.series_response import SeriesResponse +from libs.mangacollec.interface.endpoint_interface import IEndpoint + + +class CallSeriesEndpoint(IEndpoint): + """Appeler l'endpoint de récupération des séries.""" + + async def execute(self) -> SeriesResponse: + """Récupérer les séries depuis MangaCollec.""" + response = await self.client.get("v2/series") + return SeriesResponse.model_validate(response) diff --git a/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_volume.py b/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_volume.py new file mode 100644 index 0000000..489292c --- /dev/null +++ b/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_volume.py @@ -0,0 +1,17 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +from libs.mangacollec.dto.volume_response import VolumeResponse +from libs.mangacollec.interface.endpoint_interface import IEndpoint +from libs.mangacollec.value_object.id import ID + + +class CallVolumeEndpoint(IEndpoint): + """Appeler l'endpoint de détail d'un volume.""" + + async def execute(self, id_volume: ID) -> VolumeResponse: + """Récupérer et valider le détail du volume indiqué.""" + response = await self.client.get(f"v2/volumes/{id_volume}") + return VolumeResponse.model_validate(response) diff --git a/libs/mangacollec/src/mangacollec/endpoints/follows/__init__.py b/libs/mangacollec/src/mangacollec/endpoints/follows/__init__.py new file mode 100644 index 0000000..44c9a3a --- /dev/null +++ b/libs/mangacollec/src/mangacollec/endpoints/follows/__init__.py @@ -0,0 +1,18 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +"""Endpoints de suivi MangaCollec.""" + +from libs.mangacollec.endpoints.follows.call_create_follow_edition import ( + CallCreateFollowEditionEndpoint, +) +from libs.mangacollec.endpoints.follows.call_delete_follow_edition import ( + CallDeleteFollowEditionEndpoint, +) + +__all__ = [ + "CallCreateFollowEditionEndpoint", + "CallDeleteFollowEditionEndpoint", +] diff --git a/libs/mangacollec/src/mangacollec/endpoints/follows/call_create_follow_edition.py b/libs/mangacollec/src/mangacollec/endpoints/follows/call_create_follow_edition.py new file mode 100644 index 0000000..d95b0a2 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/endpoints/follows/call_create_follow_edition.py @@ -0,0 +1,24 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +"""Wrapper de l'endpoint de création d'un suivi d'édition.""" + +from libs.mangacollec.dto import FollowEditionCreateRequest +from libs.mangacollec.interface.endpoint_interface import IEndpoint +from libs.mangacollec.model.possessions.volume_follow_edition import VolumeFollowEdition +from libs.mangacollec.value_object.id import ID + + +class CallCreateFollowEditionEndpoint(IEndpoint): + """Appel de POST /v1/follow_editions.""" + + async def execute(self, edition_id: ID, *, following: bool) -> VolumeFollowEdition: + """Créer ou activer le suivi de l'édition indiquée.""" + request = FollowEditionCreateRequest(edition_id=edition_id, following=following) + response = await self.client.post( + "v1/follow_editions", + data=request.to_api_payload(), + ) + return VolumeFollowEdition.model_validate(response) diff --git a/libs/mangacollec/src/mangacollec/endpoints/follows/call_delete_follow_edition.py b/libs/mangacollec/src/mangacollec/endpoints/follows/call_delete_follow_edition.py new file mode 100644 index 0000000..52e60c1 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/endpoints/follows/call_delete_follow_edition.py @@ -0,0 +1,17 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +"""Wrapper de suppression d'un suivi d'édition.""" + +from libs.mangacollec.interface.endpoint_interface import IEndpoint +from libs.mangacollec.value_object.id import ID + + +class CallDeleteFollowEditionEndpoint(IEndpoint): + """Appeler DELETE /v1/follow_editions/{follow_edition_id}.""" + + async def execute(self, follow_edition_id: ID) -> None: + """Supprimer le suivi d'édition identifié pour l'utilisateur courant.""" + await self.client.delete(f"v1/follow_editions/{follow_edition_id}") diff --git a/libs/mangacollec/src/mangacollec/endpoints/loans/__init__.py b/libs/mangacollec/src/mangacollec/endpoints/loans/__init__.py new file mode 100644 index 0000000..7106fef --- /dev/null +++ b/libs/mangacollec/src/mangacollec/endpoints/loans/__init__.py @@ -0,0 +1,16 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +from libs.mangacollec.endpoints.loans.call_create_borrower import CallCreateBorrowerEndpoint +from libs.mangacollec.endpoints.loans.call_create_loans_multiple import CallCreateLoansMultipleEndpoint +from libs.mangacollec.endpoints.loans.call_delete_borrower import CallDeleteBorrowerEndpoint +from libs.mangacollec.endpoints.loans.call_delete_loans_multiple import CallDeleteLoansMultipleEndpoint + +__all__ = [ + "CallCreateBorrowerEndpoint", + "CallCreateLoansMultipleEndpoint", + "CallDeleteBorrowerEndpoint", + "CallDeleteLoansMultipleEndpoint", +] diff --git a/libs/mangacollec/src/mangacollec/endpoints/loans/call_create_borrower.py b/libs/mangacollec/src/mangacollec/endpoints/loans/call_create_borrower.py new file mode 100644 index 0000000..6cb3332 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/endpoints/loans/call_create_borrower.py @@ -0,0 +1,24 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +"""Endpoint de création d'un emprunteur ou d'un lieu de stockage.""" + +from typing import Literal + +from libs.mangacollec.dto import BorrowerCreateRequest, BorrowerCreateResponse +from libs.mangacollec.interface.mutation_client_interface import IPostClient + + +class CallCreateBorrowerEndpoint: + """Appel de POST /v2/borrowers via un transport injecté.""" + + def __init__(self, client: IPostClient) -> None: + self.client = client + + async def execute(self, *, category: Literal["person", "storage"], title: str) -> BorrowerCreateResponse: + """Créer un emprunteur après validation des paramètres de la requête.""" + request = BorrowerCreateRequest(category=category, title=title) + response = await self.client.post("v2/borrowers", data=request.to_api_payload()) + return BorrowerCreateResponse.model_validate(response) diff --git a/libs/mangacollec/src/mangacollec/endpoints/loans/call_create_loans_multiple.py b/libs/mangacollec/src/mangacollec/endpoints/loans/call_create_loans_multiple.py new file mode 100644 index 0000000..4a5fc80 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/endpoints/loans/call_create_loans_multiple.py @@ -0,0 +1,22 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +"""Endpoint de création groupée de prêts.""" + +from libs.mangacollec.dto import LoanCreateRequest, LoansCreateRequest, LoansCreateResponse +from libs.mangacollec.interface.mutation_client_interface import IPostClient + + +class CallCreateLoansMultipleEndpoint: + """Appel de création de prêts pour les emprunteurs et possessions indiqués.""" + + def __init__(self, client: IPostClient) -> None: + self.client = client + + async def execute(self, loans: list[LoanCreateRequest]) -> LoansCreateResponse: + """Créer le lot et conserver les erreurs métier éventuelles dans la réponse.""" + request = LoansCreateRequest(loans=loans) + response = await self.client.post("v1/loans_multiple", data=request.to_api_payload()) + return LoansCreateResponse.model_validate(response) diff --git a/libs/mangacollec/src/mangacollec/endpoints/loans/call_delete_borrower.py b/libs/mangacollec/src/mangacollec/endpoints/loans/call_delete_borrower.py new file mode 100644 index 0000000..4de2f95 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/endpoints/loans/call_delete_borrower.py @@ -0,0 +1,21 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +"""Wrapper de suppression d'un emprunteur.""" + +from libs.mangacollec.interface.mutation_client_interface import IDeleteClient +from libs.mangacollec.value_object.id import ID + + +class CallDeleteBorrowerEndpoint: + """Appeler DELETE /v2/borrowers/{borrower_id} via un client injectable.""" + + def __init__(self, client: IDeleteClient) -> None: + self.client = client + + async def execute(self, borrower_id: ID) -> None: + """Supprimer l'emprunteur identifié après validation de son UUID.""" + validated_id = ID(borrower_id) + await self.client.delete(f"v2/borrowers/{validated_id}") diff --git a/libs/mangacollec/src/mangacollec/endpoints/loans/call_delete_loans_multiple.py b/libs/mangacollec/src/mangacollec/endpoints/loans/call_delete_loans_multiple.py new file mode 100644 index 0000000..71230fc --- /dev/null +++ b/libs/mangacollec/src/mangacollec/endpoints/loans/call_delete_loans_multiple.py @@ -0,0 +1,33 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +"""Wrapper de suppression groupée de prêts.""" + +from libs.mangacollec.dto import LoanDeleteRequest, LoansDeleteRequest, LoansDeleteResponse +from libs.mangacollec.exceptions import DomainValidationError +from libs.mangacollec.interface.mutation_client_interface import IDeleteClient +from libs.mangacollec.value_object.id import ID + + +class CallDeleteLoansMultipleEndpoint: + """Appeler DELETE /v1/loans_multiple via un client injectable.""" + + def __init__(self, client: IDeleteClient) -> None: + self.client = client + + async def execute(self, loan_ids: list[ID]) -> LoansDeleteResponse: + """Supprimer les prêts indiqués et conserver leurs erreurs métier. + + Le client exige une liste non vide et des identifiants distincts ; ces + garde-fous locaux ne constituent pas des contraintes documentées de l'API. + """ + if not loan_ids: + raise DomainValidationError("La liste des prêts ne peut pas être vide.") + if len(set(loan_ids)) != len(loan_ids): + raise DomainValidationError("Les identifiants de prêts doivent être distincts.") + + request = LoansDeleteRequest(loans=[LoanDeleteRequest(id=loan_id) for loan_id in loan_ids]) + response = await self.client.delete("v1/loans_multiple", data=request.to_api_payload()) + return LoansDeleteResponse.model_validate(response) diff --git a/libs/mangacollec/src/mangacollec/endpoints/possessions/__init__.py b/libs/mangacollec/src/mangacollec/endpoints/possessions/__init__.py new file mode 100644 index 0000000..e3864fb --- /dev/null +++ b/libs/mangacollec/src/mangacollec/endpoints/possessions/__init__.py @@ -0,0 +1,33 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +# __project__: app_booksync +# __author__: ShooterDev +# __filename__: __init__.py.py +# __directory__: core/mangacollec/possessions + +from libs.mangacollec.endpoints.possessions.call_create_box_multiple_possessions import ( + CallCreateBoxMultiplePossessionsEndpoint, +) +from libs.mangacollec.endpoints.possessions.call_create_volume_multiple_possessions import ( + CallCreateVolumeMultiplePossessionsEndpoint, +) +from libs.mangacollec.endpoints.possessions.call_delete_box_multiple_possessions import ( + CallDeleteBoxMultiplePossessionsEndpoint, +) +from libs.mangacollec.endpoints.possessions.call_delete_volume_multiple_possessions import ( + CallDeleteMultiplePossessionsEndpoint, + DeletePossessionsMultipleRequest, + DeleteVolumePossessionMultipleRequest, +) + +__all__ = [ + "CallCreateBoxMultiplePossessionsEndpoint", + "CallCreateVolumeMultiplePossessionsEndpoint", + "CallDeleteBoxMultiplePossessionsEndpoint", + "CallDeleteMultiplePossessionsEndpoint", + "DeletePossessionsMultipleRequest", + "DeleteVolumePossessionMultipleRequest", +] diff --git a/libs/mangacollec/src/mangacollec/endpoints/possessions/call_create_box_multiple_possessions.py b/libs/mangacollec/src/mangacollec/endpoints/possessions/call_create_box_multiple_possessions.py new file mode 100644 index 0000000..d980085 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/endpoints/possessions/call_create_box_multiple_possessions.py @@ -0,0 +1,38 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +"""Endpoint de création groupée de possessions.""" + +from libs.mangacollec.dto import ( + CreateBoxesPossessionsMultipleRequest, + CreateBoxPossessionMultipleRequest, + CreateBoxPossessionsMultipleResponse, +) +from libs.mangacollec.exceptions import DomainValidationError +from libs.mangacollec.interface.endpoint_interface import IEndpoint +from libs.mangacollec.value_object.id import ID + + +class CallCreateBoxMultiplePossessionsEndpoint(IEndpoint): + """Appel de l'endpoint de création groupée de possessions.""" + + async def execute( + self, + box_ids: list[ID], + ) -> CreateBoxPossessionsMultipleResponse: + """Créer les possessions correspondant aux boxes indiqués.""" + if not box_ids: + raise DomainValidationError("La liste des boxes ne peut pas être vide.") + if len(set(box_ids)) != len(box_ids): + raise DomainValidationError("Les identifiants de boxes doivent être distincts.") + + request = CreateBoxesPossessionsMultipleRequest( + possessions=[CreateBoxPossessionMultipleRequest(box_id=box_id) for box_id in box_ids] + ) + response = await self.client.post( + "v1/box_possessions_multiple", + data=request.to_api_payload(), + ) + return CreateBoxPossessionsMultipleResponse.model_validate(response) diff --git a/libs/mangacollec/src/mangacollec/endpoints/possessions/call_create_volume_multiple_possessions.py b/libs/mangacollec/src/mangacollec/endpoints/possessions/call_create_volume_multiple_possessions.py new file mode 100644 index 0000000..201406d --- /dev/null +++ b/libs/mangacollec/src/mangacollec/endpoints/possessions/call_create_volume_multiple_possessions.py @@ -0,0 +1,38 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +"""Endpoint de création groupée de possessions.""" + +from libs.mangacollec.dto import ( + CreateVolumePossessionMultipleRequest, + CreateVolumePossessionsMultipleResponse, + CreateVolumesPossessionsMultipleRequest, +) +from libs.mangacollec.exceptions import DomainValidationError +from libs.mangacollec.interface.endpoint_interface import IEndpoint +from libs.mangacollec.value_object.id import ID + + +class CallCreateVolumeMultiplePossessionsEndpoint(IEndpoint): + """Appel de l'endpoint de création groupée de possessions.""" + + async def execute( + self, + volume_ids: list[ID], + ) -> CreateVolumePossessionsMultipleResponse: + """Créer les possessions correspondant aux volumes indiqués.""" + if not volume_ids: + raise DomainValidationError("La liste des volumes ne peut pas être vide.") + if len(set(volume_ids)) != len(volume_ids): + raise DomainValidationError("Les identifiants de volumes doivent être distincts.") + + request = CreateVolumesPossessionsMultipleRequest( + possessions=[CreateVolumePossessionMultipleRequest(volume_id=volume_id) for volume_id in volume_ids] + ) + response = await self.client.post( + "v1/possessions_multiple", + data=request.to_api_payload(), + ) + return CreateVolumePossessionsMultipleResponse.model_validate(response) diff --git a/libs/mangacollec/src/mangacollec/endpoints/possessions/call_delete_box_multiple_possessions.py b/libs/mangacollec/src/mangacollec/endpoints/possessions/call_delete_box_multiple_possessions.py new file mode 100644 index 0000000..1a9e52f --- /dev/null +++ b/libs/mangacollec/src/mangacollec/endpoints/possessions/call_delete_box_multiple_possessions.py @@ -0,0 +1,52 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +"""Endpoint de suppression groupée de possessions de boxes.""" + +from libs.mangacollec.dto import ( + DeleteBoxPossessionMultipleRequest, + DeleteBoxPossessionsMultipleRequest, + DeleteBoxPossessionsMultipleResponse, + DeletePossessionMultipleRequest, +) +from libs.mangacollec.exceptions import DomainValidationError +from libs.mangacollec.interface.endpoint_interface import IEndpoint +from libs.mangacollec.value_object.id import ID + + +class CallDeleteBoxMultiplePossessionsEndpoint(IEndpoint): + """Appel de l'endpoint de suppression groupée de box possessions.""" + + async def execute( + self, + box_possession_ids: list[ID], + possession_ids: list[ID] | None = None, + ) -> DeleteBoxPossessionsMultipleResponse: + """Supprimer les box possessions et possessions indiquées.""" + if not box_possession_ids: + raise DomainValidationError("La liste des box possessions ne peut pas être vide.") + if len(set(box_possession_ids)) != len(box_possession_ids): + raise DomainValidationError("Les identifiants de box possessions doivent être distincts.") + if possession_ids is not None: + if not possession_ids: + raise DomainValidationError("La liste des possessions ne peut pas être vide.") + if len(set(possession_ids)) != len(possession_ids): + raise DomainValidationError("Les identifiants de possessions doivent être distincts.") + + request = DeleteBoxPossessionsMultipleRequest( + box_possessions=[ + DeleteBoxPossessionMultipleRequest(id=box_possession_id) for box_possession_id in box_possession_ids + ], + possessions=( + [DeletePossessionMultipleRequest(id=possession_id) for possession_id in possession_ids] + if possession_ids is not None + else None + ), + ) + response = await self.client.delete( + "v1/box_possessions_multiple", + data=request.to_api_payload(), + ) + return DeleteBoxPossessionsMultipleResponse.model_validate(response) diff --git a/libs/mangacollec/src/mangacollec/endpoints/possessions/call_delete_volume_multiple_possessions.py b/libs/mangacollec/src/mangacollec/endpoints/possessions/call_delete_volume_multiple_possessions.py new file mode 100644 index 0000000..9dbc735 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/endpoints/possessions/call_delete_volume_multiple_possessions.py @@ -0,0 +1,54 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +"""Endpoint de suppression groupée de possessions.""" + +from pydantic import BaseModel + +from libs.mangacollec.dto import ( + DeleteVolumePossessionsMultipleResponse, +) +from libs.mangacollec.exceptions import DomainValidationError +from libs.mangacollec.interface.endpoint_interface import IEndpoint +from libs.mangacollec.value_object.id import ID + + +class DeleteVolumePossessionMultipleRequest(BaseModel): + """VolumePossession à supprimer.""" + + id: ID + + +class DeletePossessionsMultipleRequest(BaseModel): + """Liste des possessions à supprimer.""" + + possessions: list[DeleteVolumePossessionMultipleRequest] + + def to_api_payload(self) -> list[dict[str, str]]: + """Convertir la requête vers le tableau JSON de l'API.""" + return [item.model_dump(mode="json") for item in self.possessions] + + +class CallDeleteMultiplePossessionsEndpoint(IEndpoint): + """Appel de l'endpoint de suppression groupée de possessions.""" + + async def execute( + self, + possession_ids: list[ID], + ) -> DeleteVolumePossessionsMultipleResponse: + """Supprimer les possessions indiquées.""" + if not possession_ids: + raise DomainValidationError("La liste des possessions ne peut pas être vide.") + if len(set(possession_ids)) != len(possession_ids): + raise DomainValidationError("Les identifiants de possessions doivent être distincts.") + + request = DeletePossessionsMultipleRequest( + possessions=[DeleteVolumePossessionMultipleRequest(id=possession_id) for possession_id in possession_ids] + ) + response = await self.client.delete( + "v1/possessions_multiple", + data=request.to_api_payload(), + ) + return DeleteVolumePossessionsMultipleResponse.model_validate(response) diff --git a/libs/mangacollec/src/mangacollec/endpoints/reads/__init__.py b/libs/mangacollec/src/mangacollec/endpoints/reads/__init__.py new file mode 100644 index 0000000..0e4e519 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/endpoints/reads/__init__.py @@ -0,0 +1,18 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +"""Endpoints de lectures MangaCollec.""" + +from libs.mangacollec.endpoints.reads.call_create_reads_multiple import ( + CallCreateReadsMultipleEndpoint, +) +from libs.mangacollec.endpoints.reads.call_delete_reads_multiple import ( + CallDeleteReadsMultipleEndpoint, +) + +__all__ = [ + "CallCreateReadsMultipleEndpoint", + "CallDeleteReadsMultipleEndpoint", +] diff --git a/libs/mangacollec/src/mangacollec/endpoints/reads/call_create_reads_multiple.py b/libs/mangacollec/src/mangacollec/endpoints/reads/call_create_reads_multiple.py new file mode 100644 index 0000000..1d35587 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/endpoints/reads/call_create_reads_multiple.py @@ -0,0 +1,27 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +"""Wrapper de création groupée de lectures.""" + +from typing import TYPE_CHECKING + +from libs.mangacollec.dto import ReadCreateRequest, ReadsCreateRequest, ReadsCreateResponse + +if TYPE_CHECKING: + from libs.mangacollec.interface.mutation_client_interface import IPostClient + from libs.mangacollec.value_object.id import ID + + +class CallCreateReadsMultipleEndpoint: + """Appel de POST /v1/reads_multiple avec un client HTTP injectable.""" + + def __init__(self, client: "IPostClient") -> None: + self.client = client + + async def execute(self, volume_ids: "list[ID]") -> ReadsCreateResponse: + """Marquer les volumes lus et valider les lectures créées.""" + request = ReadsCreateRequest(reads=[ReadCreateRequest(volume_id=volume_id) for volume_id in volume_ids]) + response = await self.client.post("v1/reads_multiple", data=request.to_api_payload()) + return ReadsCreateResponse.model_validate(response) diff --git a/libs/mangacollec/src/mangacollec/endpoints/reads/call_delete_reads_multiple.py b/libs/mangacollec/src/mangacollec/endpoints/reads/call_delete_reads_multiple.py new file mode 100644 index 0000000..ceaa8cc --- /dev/null +++ b/libs/mangacollec/src/mangacollec/endpoints/reads/call_delete_reads_multiple.py @@ -0,0 +1,29 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +"""Wrapper de suppression groupée de lectures.""" + +from libs.mangacollec.dto import ReadDeleteRequest, ReadsDeleteRequest, ReadsDeleteResponse +from libs.mangacollec.exceptions import DomainValidationError +from libs.mangacollec.interface.mutation_client_interface import IDeleteClient +from libs.mangacollec.value_object.id import ID + + +class CallDeleteReadsMultipleEndpoint: + """Appeler DELETE /v1/reads_multiple via un client injectable.""" + + def __init__(self, client: IDeleteClient) -> None: + self.client = client + + async def execute(self, read_ids: list[ID]) -> ReadsDeleteResponse: + """Supprimer les lectures indiquées et conserver les erreurs métier.""" + if not read_ids: + raise DomainValidationError("La liste des lectures ne peut pas être vide.") + if len(set(read_ids)) != len(read_ids): + raise DomainValidationError("Les identifiants de lectures doivent être distincts.") + + request = ReadsDeleteRequest(reads=[ReadDeleteRequest(id=read_id) for read_id in read_ids]) + response = await self.client.delete("v1/reads_multiple", data=request.to_api_payload()) + return ReadsDeleteResponse.model_validate(response) diff --git a/libs/mangacollec/src/mangacollec/exceptions/__init__.py b/libs/mangacollec/src/mangacollec/exceptions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/libs/mangacollec/src/mangacollec/exceptions/exceptions.py b/libs/mangacollec/src/mangacollec/exceptions/exceptions.py new file mode 100644 index 0000000..7fe0aad --- /dev/null +++ b/libs/mangacollec/src/mangacollec/exceptions/exceptions.py @@ -0,0 +1,26 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +"""Exceptions explicites utilisées par le domaine et ses adaptateurs.""" + + +class BooksyncError(Exception): + """Base commune des erreurs attendues par Booksync.""" + + +class DomainValidationError(ValueError, BooksyncError): + """Indiquer qu'une donnée métier ou une commande est invalide.""" + + +class ConfigurationError(ValueError, BooksyncError): + """Indiquer qu'une configuration requise est absente ou invalide.""" + + +class AuthenticationError(RuntimeError, BooksyncError): + """Indiquer qu'une authentification ou un renouvellement a échoué.""" + + +class RemoteRequestError(RuntimeError, BooksyncError): + """Indiquer qu'une requête distante ne peut pas aboutir.""" diff --git a/libs/mangacollec/src/mangacollec/interface/__init__.py b/libs/mangacollec/src/mangacollec/interface/__init__.py new file mode 100644 index 0000000..884a355 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/interface/__init__.py @@ -0,0 +1,14 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +# __project__: app_booksync +# __author__: ShooterDev +# __filename__: __init__.py.py +# __directory__: core/mangacollec/interface + +from libs.mangacollec.interface.endpoint_interface import IEndpoint +from libs.mangacollec.interface.mutation_client_interface import IDeleteClient, IPostClient + +__all__ = ["IDeleteClient", "IEndpoint", "IPostClient"] diff --git a/libs/mangacollec/src/mangacollec/interface/endpoint_interface.py b/libs/mangacollec/src/mangacollec/interface/endpoint_interface.py new file mode 100644 index 0000000..e918d37 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/interface/endpoint_interface.py @@ -0,0 +1,18 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +from typing import Protocol + +from libs.mangacollec.core.mangacollec import Mangacollec + + +class IEndpoint(Protocol): + """Définir le contrat commun des endpoints.""" + + client: Mangacollec + + def __init__(self, client: Mangacollec) -> None: + """Initialize the endpoint with a MangaCollec client.""" + self.client = client diff --git a/libs/mangacollec/src/mangacollec/interface/mutation_client_interface.py b/libs/mangacollec/src/mangacollec/interface/mutation_client_interface.py new file mode 100644 index 0000000..f843237 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/interface/mutation_client_interface.py @@ -0,0 +1,24 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +"""Ports HTTP des endpoints de mutation, substituables par des doubles de test.""" + +from typing import Protocol + + +class IPostClient(Protocol): + """Transport requis par un endpoint de création.""" + + async def post(self, url: str, data: dict[str, object] | list[dict[str, str]]) -> dict[str, object]: + """Envoyer le corps JSON et retourner la réponse décodée.""" + ... + + +class IDeleteClient(Protocol): + """Transport requis par un endpoint de suppression.""" + + async def delete(self, url: str, data: dict[str, object] | list[dict[str, str]] | None = None) -> dict[str, object]: + """Supprimer la ressource ; une réponse sans contenu devient un objet vide.""" + ... diff --git a/libs/mangacollec/src/mangacollec/mangacollec.py b/libs/mangacollec/src/mangacollec/mangacollec.py new file mode 100644 index 0000000..fb12a94 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/mangacollec.py @@ -0,0 +1,341 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +import asyncio +import logging +import math +import os +import time + +import httpx + +from libs.mangacollec.core.retry_policy import RetryPolicy +from libs.mangacollec.exceptions import AuthenticationError, ConfigurationError, RemoteRequestError +from libs.mangacollec.model.credential import Credential +from libs.mangacollec.model.token import Token + +logger = logging.getLogger(__name__) + + +class Mangacollec: + """Client asynchrone de l'API MangaCollec.""" + + _AUTHENTICATION_PATH: str = "oauth/token" + _DEFAULT_REQUEST_TIMEOUT: float = 30.0 + _TOKEN_SAFETY_MARGIN: int = 30 * 60 + _SERVER_ERROR_MIN: int = 500 + _SERVER_ERROR_MAX: int = 600 + _NO_CONTENT_STATUS: int = 204 + _headers: dict[str, str] + _client: httpx.AsyncClient + _oauth_client: httpx.AsyncClient + _is_auth: bool + + _MANGACOLLEC_CLIENT_ID: str + _MANGACOLLEC_CLIENT_SECRET: str + + def __init__(self, proxy: str | None = None) -> None: + """Configure HTTP clients with environment-provided credentials. + + Les requêtes vers les ressources peuvent passer par ``proxy``. Les échanges + OAuth restent toujours directs afin de ne pas transmettre les identifiants + de l'application à un proxy tiers. + """ + self.validate_mangacollec_credentials() + + self._headers = { + "Accept": "application/json", + } + + self._token: Token | None = None + + self._credential: Credential | None = None + + self._is_auth = False + + self._token_received_at: float | None = None + + self._token_refresh_lock = asyncio.Lock() + + validated_proxy = self._validate_proxy_url(proxy) + request_timeout = self._request_timeout_from_environment() + self._client = httpx.AsyncClient( + base_url="https://api.mangacollec.com/", + timeout=request_timeout, + headers=self._headers, + proxy=validated_proxy, + trust_env=False, + verify=True, + ) + self._oauth_client = httpx.AsyncClient( + base_url="https://api.mangacollec.com/", + timeout=request_timeout, + headers=self._headers, + proxy=None, + trust_env=False, + verify=True, + ) + + @staticmethod + def _validate_proxy_url(proxy: str | None) -> str | None: + """Validate an HTTP(S) proxy URL that HTTPX can use.""" + if proxy is None: + return None + + try: + proxy_url = httpx.URL(proxy) + except (httpx.InvalidURL, TypeError) as error: + raise ConfigurationError("L'URL du proxy MangaCollec doit être une URL HTTP(S) valide.") from error + + if ( + proxy_url.scheme not in {"http", "https"} + or not proxy_url.host + or proxy_url.path not in {"", "/"} + or proxy_url.query + or proxy_url.fragment + ): + raise ConfigurationError("L'URL du proxy MangaCollec doit être une URL HTTP(S) valide.") + + return str(proxy_url) + + def validate_mangacollec_credentials(self) -> None: + """Charger et valider les identifiants MangaCollec depuis l'environnement.""" + client_id = os.environ.get("MANGACOLLEC_CLIENT_ID", None) + + client_secret = os.environ.get("MANGACOLLEC_CLIENT_SECRET", None) + + if client_id is None or client_secret is None: + raise ConfigurationError("Les identifiants MangaCollec sont requis.") + + self._MANGACOLLEC_CLIENT_ID = client_id + + self._MANGACOLLEC_CLIENT_SECRET = client_secret + + @classmethod + def _request_timeout_from_environment(cls) -> float: + """Read a positive finite HTTP timeout from the local environment.""" + raw_timeout = os.environ.get("BOOKSYNC_REQUEST_TIMEOUT", str(cls._DEFAULT_REQUEST_TIMEOUT)) + try: + timeout = float(raw_timeout) + except ValueError as error: + raise ConfigurationError("BOOKSYNC_REQUEST_TIMEOUT doit être un nombre positif.") from error + if not math.isfinite(timeout) or timeout <= 0: + raise ConfigurationError("BOOKSYNC_REQUEST_TIMEOUT doit être un nombre positif.") + return timeout + + @property + def is_auth(self) -> bool: + """Indiquer si le token courant provient d'une authentification utilisateur.""" + return self._is_auth + + @property + def authorization_header(self) -> str: + """Retourner l'en-tête d'autorisation du token courant.""" + authorization_header = self._client.headers.get("Authorization") + if not isinstance(authorization_header, str): + raise AuthenticationError("Le client MangaCollec n'est pas authentifié.") + return authorization_header + + async def get_authorization_header(self, *, force_refresh: bool = False) -> str: + """Return a valid authorization header, refreshing it when requested.""" + expected_access_token = None if self._token is None else self._token.access_token + await self._ensure_token_valid( + force_refresh=force_refresh, + expected_access_token=expected_access_token, + ) + return self.authorization_header + + async def _request( + self, + method: str, + url: str, + param: dict[str, str] | None = None, + data: dict[str, object] | list[dict[str, str]] | None = None, + *, + is_token_required: bool = True, + ) -> dict[str, object]: + """Execute a request with up to five delayed retries.""" + if is_token_required: + await self._ensure_token_valid() + + retryable = method.upper() == "GET" or url == self._AUTHENTICATION_PATH + request_client = self._client if is_token_required else self._oauth_client + for retry_count in range(RetryPolicy.max_retries + 1): + try: + response = await request_client.request(method=method, url=url, params=param, json=data) + logger.info("%s | %s | %s", method.upper(), response.url, response.status_code) + + if self._SERVER_ERROR_MIN <= response.status_code < self._SERVER_ERROR_MAX: + if not retryable or retry_count == RetryPolicy.max_retries: + response.raise_for_status() + else: + response.raise_for_status() + if response.status_code == self._NO_CONTENT_STATUS: + return {} + payload = response.json() + if not isinstance(payload, dict): + raise RemoteRequestError("La réponse HTTP doit être un objet JSON.") + return payload + except (httpx.TimeoutException, httpx.NetworkError, httpx.RemoteProtocolError) as error: + logger.warning( + "%s %s : échec réseau à la tentative %d/%d (%s).", + method.upper(), + url, + retry_count + 1, + RetryPolicy.max_retries + 1, + type(error).__name__, + ) + if not retryable or retry_count == RetryPolicy.max_retries: + raise + + await asyncio.sleep(RetryPolicy.delay_for_retry(retry_count + 1)) + + raise RemoteRequestError("La requête a échoué après cinq reprises.") + + async def get(self, url: str) -> dict[str, object]: + """Effectuer une requête GET.""" + return await self._request(method="GET", url=url) + + async def post( + self, + url: str, + data: dict[str, object] | list[dict[str, str]], + ) -> dict[str, object]: + """Effectuer une requête POST.""" + return await self._request(method="POST", url=url, data=data) + + async def put( + self, + url: str, + data: dict[str, object] | None = None, + ) -> dict[str, object]: + """Effectuer une requête PUT.""" + return await self._request(method="PUT", url=url, data=data) + + async def delete( + self, + url: str, + data: dict[str, object] | list[dict[str, str]] | None = None, + ) -> dict[str, object]: + """Effectuer une requête DELETE.""" + return await self._request(method="DELETE", url=url, data=data) + + async def login(self) -> None: + """Obtenir et installer un token OAuth avec les identifiants du client.""" + credential = Credential( + client_id=self._MANGACOLLEC_CLIENT_ID, + client_secret=self._MANGACOLLEC_CLIENT_SECRET, + grant_type="client_credentials", + ) + await self._authenticate(credential) + + async def login_password(self, username: str, password: str) -> None: + """Obtenir et installer un token OAuth avec un compte utilisateur.""" + credential = Credential( + client_id=self._MANGACOLLEC_CLIENT_ID, + client_secret=self._MANGACOLLEC_CLIENT_SECRET, + grant_type="password", + username=username, + password=password, + ) + await self._authenticate(credential) + + async def _authenticate(self, credential: Credential) -> None: + """Obtenir un token avec les identifiants fournis et le mémoriser.""" + logger.info("Récupération du token MangaCollec en cours (grant_type=%s).", credential.grant_type) + response = await self._request( + method="POST", + url=self._AUTHENTICATION_PATH, + data=credential.model_dump(exclude_none=True), + is_token_required=False, + ) + self._credential = credential + self._is_auth = credential.grant_type == "password" + self._set_token(response) + logger.info("Récupération du token MangaCollec réussie.") + + async def _ensure_token_valid( + self, + *, + force_refresh: bool = False, + expected_access_token: str | None = None, + ) -> None: + """Renouveler le token avant son expiration effective ou sur demande.""" + if self._token is None: + async with self._token_refresh_lock: + if self._token is None: + await self.login() + return + if not force_refresh and not self._token_is_expiring(): + return + + async with self._token_refresh_lock: + if self._token is None: + return + if ( + force_refresh + and expected_access_token is not None + and self._token.access_token != expected_access_token + ): + return + if not force_refresh and not self._token_is_expiring(): + return + + logger.debug("Le token MangaCollec arrive dans sa fenêtre de renouvellement.") + if self._token.refresh_token is not None: + await self._refresh_token() + elif self._credential is not None: + await self._authenticate(self._credential) + else: + raise AuthenticationError("Impossible de renouveler le token sans identifiants.") + + def _token_is_expiring(self) -> bool: + """Indiquer si le token est dans sa fenêtre de renouvellement.""" + if self._token is None or self._token_received_at is None: + return False + renewal_delay = max(0, self._token.expires_in - self._TOKEN_SAFETY_MARGIN) + return time.monotonic() >= self._token_received_at + renewal_delay + + async def _refresh_token(self) -> None: + """Renouveler le token avec le refresh token OAuth courant.""" + if self._token is None or self._token.refresh_token is None: + raise AuthenticationError("Aucun refresh token disponible.") + + logger.info("Renouvellement du token MangaCollec en cours.") + previous_refresh_token = self._token.refresh_token + credential = Credential( + client_id=self._MANGACOLLEC_CLIENT_ID, + client_secret=self._MANGACOLLEC_CLIENT_SECRET, + grant_type="refresh_token", + refresh_token=previous_refresh_token, + ) + response = await self._request( + "POST", + url=self._AUTHENTICATION_PATH, + data=credential.model_dump(exclude_none=True), + is_token_required=False, + ) + if response.get("refresh_token") is None: + response["refresh_token"] = previous_refresh_token + self._set_token(response) + logger.info("Renouvellement du token MangaCollec réussi.") + + def _set_token(self, response: dict[str, object]) -> None: + """Installe un token pour les requêtes suivantes.""" + self._token = Token.model_validate(response) + self._token_received_at = time.monotonic() + self._client.headers["Authorization"] = f"Bearer {self._token.access_token}" + logger.debug( + "Token MangaCollec installé (expires_in=%s, is_auth=%s).", + self._token.expires_in, + self.is_auth, + ) + + async def close(self) -> None: + """Fermer les clients HTTP des ressources et d'authentification.""" + try: + await self._client.aclose() + finally: + await self._oauth_client.aclose() diff --git a/libs/mangacollec/src/mangacollec/model/__init__.py b/libs/mangacollec/src/mangacollec/model/__init__.py new file mode 100644 index 0000000..a80a135 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/model/__init__.py @@ -0,0 +1,9 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ + +__all__ = ["Base"] + +from libs.mangacollec.model.base import Base diff --git a/libs/mangacollec/src/mangacollec/model/base.py b/libs/mangacollec/src/mangacollec/model/base.py new file mode 100644 index 0000000..1f71b24 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/model/base.py @@ -0,0 +1,14 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +from pydantic import BaseModel + +from libs.mangacollec.value_object.id import ID + + +class Base(BaseModel): + """Modèle de base portant un identifiant.""" + + id: ID diff --git a/libs/mangacollec/src/mangacollec/model/catalogue/__init__.py b/libs/mangacollec/src/mangacollec/model/catalogue/__init__.py new file mode 100644 index 0000000..12011a9 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/model/catalogue/__init__.py @@ -0,0 +1,40 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +"""Modèles du catalogue.""" + +from .author import Author +from .box_edition import BoxEdition +from .box_volume import BoxVolume +from .boxe import Boxe +from .edition import Edition +from .job import Job +from .kind import Kind +from .native_ad_volume_home_first import NativeAdVolumeHomeFirst +from .news import News, NewsBox, NewsVolume +from .publisher import Publisher +from .serie import Serie +from .serie_type import SerieType +from .task import Task +from .volume import Volume + +__all__ = [ + "Author", + "BoxEdition", + "BoxVolume", + "Boxe", + "Edition", + "Job", + "Kind", + "NativeAdVolumeHomeFirst", + "News", + "NewsBox", + "NewsVolume", + "Publisher", + "Serie", + "SerieType", + "Task", + "Volume", +] diff --git a/libs/mangacollec/src/mangacollec/model/catalogue/author.py b/libs/mangacollec/src/mangacollec/model/catalogue/author.py new file mode 100644 index 0000000..84f4a59 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/model/catalogue/author.py @@ -0,0 +1,15 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ + +from libs.mangacollec.model import Base + + +class Author(Base): + """Auteur associé à une série.""" + + name: str + first_name: str | None + tasks_count: int diff --git a/libs/mangacollec/src/mangacollec/model/catalogue/box_edition.py b/libs/mangacollec/src/mangacollec/model/catalogue/box_edition.py new file mode 100644 index 0000000..e06e7cd --- /dev/null +++ b/libs/mangacollec/src/mangacollec/model/catalogue/box_edition.py @@ -0,0 +1,18 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ + +from libs.mangacollec.model import Base +from libs.mangacollec.value_object import ID + + +class BoxEdition(Base): + """Édition commerciale d'un coffret.""" + + title: str + publisher_id: ID + boxes_count: int + adult_content: bool + box_follow_editions_count: int diff --git a/libs/mangacollec/src/mangacollec/model/catalogue/box_volume.py b/libs/mangacollec/src/mangacollec/model/catalogue/box_volume.py new file mode 100644 index 0000000..55c8c5c --- /dev/null +++ b/libs/mangacollec/src/mangacollec/model/catalogue/box_volume.py @@ -0,0 +1,16 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ + +from libs.mangacollec.model import Base +from libs.mangacollec.value_object import ID + + +class BoxVolume(Base): + """Association entre un coffret et un volume.""" + + box_id: ID + volume_id: ID + included: bool diff --git a/libs/mangacollec/src/mangacollec/model/catalogue/boxe.py b/libs/mangacollec/src/mangacollec/model/catalogue/boxe.py new file mode 100644 index 0000000..9bee939 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/model/catalogue/boxe.py @@ -0,0 +1,42 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ + +from datetime import date + +from pydantic import field_validator + +from libs.mangacollec.model import Base +from libs.mangacollec.value_object.asin import ASIN +from libs.mangacollec.value_object.id import ID +from libs.mangacollec.value_object.isbn import ISBN + + +class Boxe(Base): + """Coffret regroupant des volumes.""" + + id: ID + title: str + number: int + release_date: date | None + isbn: ISBN | None + asin: ASIN | None + commercial_stop: bool + box_edition_id: ID + box_possessions_count: int + image_url: str | None + content: str | None = None + + @field_validator("isbn", mode="before") + @classmethod + def invalid_isbn_is_missing(cls, value: object) -> object: + """Convertir un ISBN externe malformé en valeur absente.""" + if value is None: + return None + try: + ISBN(str(value)) + except ValueError: + return None + return value diff --git a/libs/mangacollec/src/mangacollec/model/catalogue/edition.py b/libs/mangacollec/src/mangacollec/model/catalogue/edition.py new file mode 100644 index 0000000..e9120be --- /dev/null +++ b/libs/mangacollec/src/mangacollec/model/catalogue/edition.py @@ -0,0 +1,21 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +from libs.mangacollec.model import Base +from libs.mangacollec.value_object.id import ID + + +class Edition(Base): + """Représenter une édition de série.""" + + title: str | None + series_id: ID + publisher_id: ID + parent_edition_id: ID | None + volumes_count: int + last_volume_number: int | None + commercial_stop: bool + not_finished: bool + follow_editions_count: int diff --git a/libs/mangacollec/src/mangacollec/model/catalogue/job.py b/libs/mangacollec/src/mangacollec/model/catalogue/job.py new file mode 100644 index 0000000..e516e8e --- /dev/null +++ b/libs/mangacollec/src/mangacollec/model/catalogue/job.py @@ -0,0 +1,13 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ + +from libs.mangacollec.model import Base + + +class Job(Base): + """Métier exercé par un auteur.""" + + title: str diff --git a/libs/mangacollec/src/mangacollec/model/catalogue/kind.py b/libs/mangacollec/src/mangacollec/model/catalogue/kind.py new file mode 100644 index 0000000..1915373 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/model/catalogue/kind.py @@ -0,0 +1,15 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ + +from libs.mangacollec.model import Base +from libs.mangacollec.value_object import ID + + +class Kind(Base): + """Genre associé à une série.""" + + title: str + series_ids: list[ID] | None = None diff --git a/libs/mangacollec/src/mangacollec/model/catalogue/native_ad_volume_home_first.py b/libs/mangacollec/src/mangacollec/model/catalogue/native_ad_volume_home_first.py new file mode 100644 index 0000000..843dd26 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/model/catalogue/native_ad_volume_home_first.py @@ -0,0 +1,18 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +from datetime import date + +from libs.mangacollec.model.base import Base +from libs.mangacollec.value_object.id import ID + + +class NativeAdVolumeHomeFirst(Base): + """Publicité native associée au volume mis en avant sur l'accueil.""" + + volume_id: ID + title: str + start_date: date + end_date: date diff --git a/libs/mangacollec/src/mangacollec/model/catalogue/news.py b/libs/mangacollec/src/mangacollec/model/catalogue/news.py new file mode 100644 index 0000000..80b2fda --- /dev/null +++ b/libs/mangacollec/src/mangacollec/model/catalogue/news.py @@ -0,0 +1,34 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +from pydantic import BaseModel + +from libs.mangacollec.model.catalogue.boxe import Boxe +from libs.mangacollec.model.catalogue.volume import Volume +from libs.mangacollec.value_object.id import ID + + +class NewsVolume(BaseModel): + """Volume de nouveauté avec les relations utiles à sa présentation.""" + + volume: Volume + publisher_id: ID | None + publisher_title: str | None + series_title: str | None + + +class NewsBox(BaseModel): + """Coffret de nouveauté avec les relations utiles à sa présentation.""" + + box: Boxe + publisher_id: ID | None + publisher_title: str | None + + +class News(BaseModel): + """Nouveautés résolues depuis l'index du store local.""" + + volumes: list[NewsVolume] + boxes: list[NewsBox] diff --git a/libs/mangacollec/src/mangacollec/model/catalogue/publisher.py b/libs/mangacollec/src/mangacollec/model/catalogue/publisher.py new file mode 100644 index 0000000..d016884 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/model/catalogue/publisher.py @@ -0,0 +1,16 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ + +from libs.mangacollec.model import Base + + +class Publisher(Base): + """Éditeur et ses métadonnées.""" + + title: str + closed: bool + editions_count: int + no_amazon: bool diff --git a/libs/mangacollec/src/mangacollec/model/catalogue/serie.py b/libs/mangacollec/src/mangacollec/model/catalogue/serie.py new file mode 100644 index 0000000..0a03e59 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/model/catalogue/serie.py @@ -0,0 +1,19 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ + +from libs.mangacollec.model import Base +from libs.mangacollec.value_object import ID + + +class Serie(Base): + """Représenter une série du catalogue.""" + + title: str + type_id: ID | None = None + kinds_ids: list[ID] | None = None + adult_content: bool | None = None + editions_count: int | None = None + tasks_count: int | None = None diff --git a/libs/mangacollec/src/mangacollec/model/catalogue/serie_type.py b/libs/mangacollec/src/mangacollec/model/catalogue/serie_type.py new file mode 100644 index 0000000..cc64bf7 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/model/catalogue/serie_type.py @@ -0,0 +1,14 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ + +from libs.mangacollec.model.base import Base + + +class SerieType(Base): + """Représenter un type de série.""" + + title: str + to_display: bool | None = None diff --git a/libs/mangacollec/src/mangacollec/model/catalogue/task.py b/libs/mangacollec/src/mangacollec/model/catalogue/task.py new file mode 100644 index 0000000..d8fe341 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/model/catalogue/task.py @@ -0,0 +1,14 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +from libs.mangacollec.model import Base + + +class Task(Base): + """Représenter une tâche de série.""" + + series_id: str + author_id: str | None = None + job_id: str | None = None diff --git a/libs/mangacollec/src/mangacollec/model/catalogue/volume.py b/libs/mangacollec/src/mangacollec/model/catalogue/volume.py new file mode 100644 index 0000000..3511381 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/model/catalogue/volume.py @@ -0,0 +1,41 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +from datetime import date + +from pydantic import field_validator + +from libs.mangacollec.model import Base +from libs.mangacollec.value_object.asin import ASIN +from libs.mangacollec.value_object.id import ID +from libs.mangacollec.value_object.isbn import ISBN + + +class Volume(Base): + """Représenter un volume d'édition.""" + + title: str | None + number: int + release_date: date | None + isbn: ISBN | None + asin: ASIN | None + edition_id: ID + possessions_count: int + not_sold: bool + image_url: str | None + nb_pages: int | None = None + content: str | None = None + + @field_validator("isbn", mode="before") + @classmethod + def invalid_isbn_is_missing(cls, value: object) -> object: + """Convertir un ISBN externe malformé en valeur absente.""" + if value is None: + return None + try: + ISBN(str(value)) + except ValueError: + return None + return value diff --git a/libs/mangacollec/src/mangacollec/model/credential.py b/libs/mangacollec/src/mangacollec/model/credential.py new file mode 100644 index 0000000..1cb8917 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/model/credential.py @@ -0,0 +1,19 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +from typing import Literal + +from pydantic import BaseModel + + +class Credential(BaseModel): + """Identifiants transmis à l'endpoint OAuth.""" + + client_id: str + client_secret: str + grant_type: Literal["password", "client_credentials", "refresh_token"] = "client_credentials" + username: str | None = None + password: str | None = None + refresh_token: str | None = None diff --git a/libs/mangacollec/src/mangacollec/model/deleted_item.py b/libs/mangacollec/src/mangacollec/model/deleted_item.py new file mode 100644 index 0000000..2e71835 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/model/deleted_item.py @@ -0,0 +1,14 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +"""Modèle technique commun d'une ressource supprimée par l'API.""" + +from libs.mangacollec.model.base import Base + + +class DeletedItem(Base): + """Ressource identifiée dont la suppression a réussi.""" + + deleted: bool diff --git a/libs/mangacollec/src/mangacollec/model/loans/__init__.py b/libs/mangacollec/src/mangacollec/model/loans/__init__.py new file mode 100644 index 0000000..83422e7 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/model/loans/__init__.py @@ -0,0 +1,10 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +from .borrower import Borrower +from .loan import Loan +from .loan_delete_result import LoanDeleteResult + +__all__ = ["Borrower", "Loan", "LoanDeleteResult"] diff --git a/libs/mangacollec/src/mangacollec/model/loans/borrower.py b/libs/mangacollec/src/mangacollec/model/loans/borrower.py new file mode 100644 index 0000000..ad6b55a --- /dev/null +++ b/libs/mangacollec/src/mangacollec/model/loans/borrower.py @@ -0,0 +1,20 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +"""Projection d'un emprunteur ou d'un lieu de stockage dans la collection.""" + +from datetime import datetime + +from libs.mangacollec.model.base import Base +from libs.mangacollec.value_object.id import ID + + +class Borrower(Base): + """Emprunteur appartenant à un utilisateur, sans date de mise à jour.""" + + user_id: ID + title: str + category: str + created_at: datetime diff --git a/libs/mangacollec/src/mangacollec/model/loans/loan.py b/libs/mangacollec/src/mangacollec/model/loans/loan.py new file mode 100644 index 0000000..c831c4c --- /dev/null +++ b/libs/mangacollec/src/mangacollec/model/loans/loan.py @@ -0,0 +1,19 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +"""Modèle d'un prêt de possession à un emprunteur.""" + +from datetime import datetime + +from libs.mangacollec.model.base import Base +from libs.mangacollec.value_object.id import ID + + +class Loan(Base): + """Prêt créé avec ses relations et sa date de création.""" + + borrower_id: ID + possession_id: ID + created_at: datetime diff --git a/libs/mangacollec/src/mangacollec/model/loans/loan_delete_result.py b/libs/mangacollec/src/mangacollec/model/loans/loan_delete_result.py new file mode 100644 index 0000000..4fbc0be --- /dev/null +++ b/libs/mangacollec/src/mangacollec/model/loans/loan_delete_result.py @@ -0,0 +1,16 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +"""Résultat individuel de suppression d'un prêt.""" + +from pydantic import Field + +from libs.mangacollec.model.base import Base + + +class LoanDeleteResult(Base): + """Identifiant traité et erreurs métier éventuelles, même sous HTTP 200.""" + + errors: list[str] = Field(default_factory=list) diff --git a/libs/mangacollec/src/mangacollec/model/possessions/__init__.py b/libs/mangacollec/src/mangacollec/model/possessions/__init__.py new file mode 100644 index 0000000..bdbdc8a --- /dev/null +++ b/libs/mangacollec/src/mangacollec/model/possessions/__init__.py @@ -0,0 +1,22 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +# __project__: app_booksync +# __author__: ShooterDev +# __filename__: __init__.py +# __directory__: core/model/possessions +"""Modèles liés aux possessions de l'utilisateur.""" + +from .box_follow_edition import BoxFollowEdition +from .box_possession import BoxPossession +from .volume_follow_edition import VolumeFollowEdition +from .volume_possession import VolumePossession + +__all__ = [ + "BoxFollowEdition", + "BoxPossession", + "VolumeFollowEdition", + "VolumePossession", +] diff --git a/libs/mangacollec/src/mangacollec/model/possessions/box_follow_edition.py b/libs/mangacollec/src/mangacollec/model/possessions/box_follow_edition.py new file mode 100644 index 0000000..22feac6 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/model/possessions/box_follow_edition.py @@ -0,0 +1,18 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +from datetime import datetime + +from libs.mangacollec.model.base import Base +from libs.mangacollec.value_object.id import ID + + +class BoxFollowEdition(Base): + """Suivi d'édition créé implicitement par l'API.""" + + user_id: ID + box_edition_id: ID + following: bool + created_at: datetime diff --git a/libs/mangacollec/src/mangacollec/model/possessions/box_possession.py b/libs/mangacollec/src/mangacollec/model/possessions/box_possession.py new file mode 100644 index 0000000..c147e54 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/model/possessions/box_possession.py @@ -0,0 +1,17 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +from datetime import datetime + +from libs.mangacollec.model import Base +from libs.mangacollec.value_object import ID + + +class BoxPossession(Base): + """VolumePossession de box créée.""" + + user_id: ID + box_id: ID + created_at: datetime diff --git a/libs/mangacollec/src/mangacollec/model/possessions/volume_follow_edition.py b/libs/mangacollec/src/mangacollec/model/possessions/volume_follow_edition.py new file mode 100644 index 0000000..7a121ec --- /dev/null +++ b/libs/mangacollec/src/mangacollec/model/possessions/volume_follow_edition.py @@ -0,0 +1,19 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +from datetime import datetime + +from libs.mangacollec.model.base import Base +from libs.mangacollec.value_object.id import ID + + +class VolumeFollowEdition(Base): + """Suivi d'édition créé implicitement par l'API.""" + + user_id: ID + edition_id: ID + following: bool + created_at: datetime + updated_at: datetime | None = None diff --git a/libs/mangacollec/src/mangacollec/model/possessions/volume_possession.py b/libs/mangacollec/src/mangacollec/model/possessions/volume_possession.py new file mode 100644 index 0000000..047d8b5 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/model/possessions/volume_possession.py @@ -0,0 +1,17 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +from datetime import datetime + +from libs.mangacollec.model import Base +from libs.mangacollec.value_object.id import ID + + +class VolumePossession(Base): + """VolumePossession de volume créée.""" + + user_id: ID + volume_id: ID + created_at: datetime diff --git a/libs/mangacollec/src/mangacollec/model/reads/__init__.py b/libs/mangacollec/src/mangacollec/model/reads/__init__.py new file mode 100644 index 0000000..2c16940 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/model/reads/__init__.py @@ -0,0 +1,10 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ + +from .read import Read +from .read_edition import ReadEdition + +__all__ = ["Read", "ReadEdition"] diff --git a/libs/mangacollec/src/mangacollec/model/reads/read.py b/libs/mangacollec/src/mangacollec/model/reads/read.py new file mode 100644 index 0000000..7cb0c49 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/model/reads/read.py @@ -0,0 +1,19 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +"""Lecture d'un volume par un utilisateur.""" + +from datetime import datetime + +from libs.mangacollec.model.base import Base +from libs.mangacollec.value_object.id import ID + + +class Read(Base): + """Lecture observée dans les collections et la création groupée.""" + + user_id: ID + volume_id: ID + created_at: datetime diff --git a/libs/mangacollec/src/mangacollec/model/reads/read_edition.py b/libs/mangacollec/src/mangacollec/model/reads/read_edition.py new file mode 100644 index 0000000..a0ac4fe --- /dev/null +++ b/libs/mangacollec/src/mangacollec/model/reads/read_edition.py @@ -0,0 +1,24 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +"""Projection de création de l'état de lecture d'une édition.""" + +from datetime import datetime + +from libs.mangacollec.model.base import Base +from libs.mangacollec.value_object.id import ID + + +class ReadEdition(Base): + """État renvoyé par POST /v1/reads_multiple, sans champ updated_at. + + La variante de collection expose updated_at ; sa nullabilité n'est pas + documentée. Cette projection de création ne l'ajoute donc pas. + """ + + user_id: ID + edition_id: ID + reading: bool + created_at: datetime diff --git a/libs/mangacollec/src/mangacollec/model/token.py b/libs/mangacollec/src/mangacollec/model/token.py new file mode 100644 index 0000000..9bddff9 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/model/token.py @@ -0,0 +1,16 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +from pydantic import BaseModel + + +class Token(BaseModel): + """Représenter un token OAuth2 MangaCollec.""" + + access_token: str + token_type: str + expires_in: int + refresh_token: str | None = None + created_at: int diff --git a/libs/mangacollec/src/mangacollec/retry_policy.py b/libs/mangacollec/src/mangacollec/retry_policy.py new file mode 100644 index 0000000..327c0e5 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/retry_policy.py @@ -0,0 +1,27 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ + +"""Fixed retry policy shared by MangaCollec transports.""" + +from typing import ClassVar + + +class RetryPolicy: + """Define the bounded exponential delay used before retrying MangaCollec requests.""" + + max_retries: ClassVar[int] = 5 + initial_delay_seconds: ClassVar[float] = 0.5 + maximum_delay_seconds: ClassVar[float] = 8.0 + + @classmethod + def delay_for_retry(cls, retry_count: int) -> float: + """Return the delay for a scheduled retry, where the first retry is one.""" + if retry_count < 1: + raise ValueError("Le numéro de reprise doit être supérieur ou égal à un.") + delay_seconds: float = cls.initial_delay_seconds * 2 ** (retry_count - 1) + if delay_seconds >= cls.maximum_delay_seconds: + return cls.maximum_delay_seconds + return delay_seconds diff --git a/libs/mangacollec/src/mangacollec/value_object/__init__.py b/libs/mangacollec/src/mangacollec/value_object/__init__.py new file mode 100644 index 0000000..abecb55 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/value_object/__init__.py @@ -0,0 +1,11 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +from libs.mangacollec.value_object.asin import ASIN +from libs.mangacollec.value_object.base import ValueObject +from libs.mangacollec.value_object.id import ID +from libs.mangacollec.value_object.isbn import ISBN + +__all__ = ["ASIN", "ID", "ISBN", "ValueObject"] diff --git a/libs/mangacollec/src/mangacollec/value_object/asin.py b/libs/mangacollec/src/mangacollec/value_object/asin.py new file mode 100644 index 0000000..12f6afe --- /dev/null +++ b/libs/mangacollec/src/mangacollec/value_object/asin.py @@ -0,0 +1,19 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +from libs.mangacollec.exceptions import DomainValidationError +from libs.mangacollec.value_object.base import ValueObject + + +class ASIN(ValueObject): + """Valider un identifiant ASIN.""" + + _LENGTH = 10 + + @classmethod + def _validate(cls, value: str) -> str: + if len(value) != cls._LENGTH or not value.isalnum(): + raise DomainValidationError(f"'{value}' n'est pas un ASIN valide gg") + return value diff --git a/libs/mangacollec/src/mangacollec/value_object/base.py b/libs/mangacollec/src/mangacollec/value_object/base.py new file mode 100644 index 0000000..4335b19 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/value_object/base.py @@ -0,0 +1,35 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +from typing import Any, Self + +from pydantic_core import core_schema + + +class ValueObject(str): + """Base class for validated string value objects. + + Cette classe se comporte comme une chaîne et centralise l'intégration Pydantic. + """ + + __slots__ = () + + def __new__(cls, value: Any) -> Self: + """Construire une valeur après validation.""" + validated = cls._validate(str(value)) + return super().__new__(cls, validated) + + @classmethod + def _validate(cls, value: str) -> str: + """À surcharger dans chaque sous-classe : lève ValueError si invalide.""" + return value + + @classmethod + def __get_pydantic_core_schema__(cls, source_type: Any, handler: Any) -> Any: + """Provide the Pydantic schema for the value object.""" + return core_schema.no_info_plain_validator_function( + cls, + serialization=core_schema.plain_serializer_function_ser_schema(str), + ) diff --git a/libs/mangacollec/src/mangacollec/value_object/id.py b/libs/mangacollec/src/mangacollec/value_object/id.py new file mode 100644 index 0000000..34aed1b --- /dev/null +++ b/libs/mangacollec/src/mangacollec/value_object/id.py @@ -0,0 +1,22 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +from uuid import UUID + +from libs.mangacollec.exceptions import DomainValidationError +from libs.mangacollec.value_object.base import ValueObject + + +class ID(ValueObject): + """Valider un identifiant UUID de version 4.""" + + _UUID_VERSION = 4 + + @classmethod + def _validate(cls, value: str) -> str: + u = UUID(value) + if u.version != cls._UUID_VERSION: + raise DomainValidationError(f"'{value}' n'est pas un UUID v4") + return str(u) diff --git a/libs/mangacollec/src/mangacollec/value_object/isbn.py b/libs/mangacollec/src/mangacollec/value_object/isbn.py new file mode 100644 index 0000000..79216ca --- /dev/null +++ b/libs/mangacollec/src/mangacollec/value_object/isbn.py @@ -0,0 +1,18 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +from libs.mangacollec.exceptions import DomainValidationError +from libs.mangacollec.value_object.base import ValueObject + + +class ISBN(ValueObject): + """Valider un identifiant ISBN.""" + + @classmethod + def _validate(cls, value: str) -> str: + clean = value.replace("-", "").replace(" ", "") + if len(clean) not in (8, 13): + raise DomainValidationError(f"'{value}' n'est pas un ISBN valide") + return value diff --git a/libs/mangacollec/tests/__init__.py b/libs/mangacollec/tests/__init__.py new file mode 100644 index 0000000..6638f6b --- /dev/null +++ b/libs/mangacollec/tests/__init__.py @@ -0,0 +1 @@ +"""Tests for the mangacollec library."""