diff --git a/libs/mangacollec/src/mangacollec/__init__.py b/libs/mangacollec/src/mangacollec/__init__.py index 3d313e7..e966154 100644 --- a/libs/mangacollec/src/mangacollec/__init__.py +++ b/libs/mangacollec/src/mangacollec/__init__.py @@ -1 +1,5 @@ """Client library for MangaCollec.""" + +from .mangacollec import Mangacollec + +__all__ = ["Mangacollec"] diff --git a/libs/mangacollec/src/mangacollec/dto/__init__.py b/libs/mangacollec/src/mangacollec/dto/__init__.py index 59095b8..7151957 100644 --- a/libs/mangacollec/src/mangacollec/dto/__init__.py +++ b/libs/mangacollec/src/mangacollec/dto/__init__.py @@ -3,98 +3,3 @@ # \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / # ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / # /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ -from mangacollec.dto.author_response import AuthorResponse -from mangacollec.dto.authors_response import AuthorsResponse -from mangacollec.dto.borrower_create_request import BorrowerCreateRequest -from mangacollec.dto.borrower_create_response import BorrowerCreateResponse -from mangacollec.dto.box_response import BoxResponse -from mangacollec.dto.create_box_possessions_multiple_request import ( - CreateBoxesPossessionsMultipleRequest, - CreateBoxPossessionMultipleRequest, -) -from mangacollec.dto.create_box_possessions_multiple_response import ( - CreateBoxPossessionsMultipleResponse, -) -from mangacollec.dto.create_volume_possessions_multiple_request import ( - CreateVolumePossessionMultipleRequest, - CreateVolumesPossessionsMultipleRequest, -) -from mangacollec.dto.create_volume_possessions_multiple_response import ( - CreateVolumePossessionsMultipleResponse, -) -from mangacollec.dto.delete_box_possessions_multiple_request import ( - DeleteBoxPossessionMultipleRequest, - DeleteBoxPossessionsMultipleRequest, - DeletePossessionMultipleRequest, -) -from mangacollec.dto.delete_box_possessions_multiple_response import ( - DeleteBoxPossessionsMultipleResponse, -) -from mangacollec.dto.delete_box_possessions_multiple_response import ( - DeletedResource as DeletedBoxResource, -) -from mangacollec.dto.delete_volume_possessions_multiple_response import ( - DeletedResource, - DeleteVolumePossessionsMultipleResponse, -) -from mangacollec.dto.edition_response import EditionResponse -from mangacollec.dto.follow_edition_create_request import FollowEditionCreateRequest -from mangacollec.dto.kinds_response import KindsResponse -from mangacollec.dto.loans_create_request import LoanCreateRequest, LoansCreateRequest -from mangacollec.dto.loans_create_response import LoanCreateError, LoansCreateResponse -from mangacollec.dto.loans_delete_request import LoanDeleteRequest, LoansDeleteRequest -from mangacollec.dto.loans_delete_response import LoansDeleteResponse -from mangacollec.dto.news_response import NewsResponse -from mangacollec.dto.publisher_response import PublisherResponse -from mangacollec.dto.publishers_response import PublishersResponse -from mangacollec.dto.reads_create_request import ReadCreateRequest, ReadsCreateRequest -from mangacollec.dto.reads_create_response import ReadsCreateResponse -from mangacollec.dto.reads_delete_request import ReadDeleteRequest, ReadsDeleteRequest -from mangacollec.dto.reads_delete_response import ReadDeleteError, ReadsDeleteResponse -from mangacollec.dto.serie_response import SerieResponse -from mangacollec.dto.series_response import SeriesResponse -from 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/catalogue/__init__.py b/libs/mangacollec/src/mangacollec/dto/catalogue/__init__.py new file mode 100644 index 0000000..024f255 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/dto/catalogue/__init__.py @@ -0,0 +1,25 @@ +from mangacollec.dto.news_response import NewsResponse +from mangacollec.dto.catalogue.box_response import BoxResponse +from mangacollec.dto.catalogue.edition_response import EditionResponse +from mangacollec.dto.catalogue.kinds_response import KindsResponse +from mangacollec.dto.catalogue.publisher_response import PublisherResponse +from mangacollec.dto.catalogue.publishers_response import PublishersResponse +from mangacollec.dto.catalogue.author_response import AuthorResponse +from mangacollec.dto.catalogue.authors_response import AuthorsResponse +from mangacollec.dto.catalogue.serie_response import SerieResponse +from mangacollec.dto.catalogue.series_response import SeriesResponse +from mangacollec.dto.catalogue.volume_response import VolumeResponse + +__all__ = [ + "AuthorResponse", + "AuthorsResponse", + "BoxResponse", + "NewsResponse", + "PublisherResponse", + "PublishersResponse", + "EditionResponse", + "KindsResponse", + "SerieResponse", + "SeriesResponse", + "VolumeResponse", +] diff --git a/libs/mangacollec/src/mangacollec/dto/author_response.py b/libs/mangacollec/src/mangacollec/dto/catalogue/author_response.py similarity index 100% rename from libs/mangacollec/src/mangacollec/dto/author_response.py rename to libs/mangacollec/src/mangacollec/dto/catalogue/author_response.py diff --git a/libs/mangacollec/src/mangacollec/dto/authors_response.py b/libs/mangacollec/src/mangacollec/dto/catalogue/authors_response.py similarity index 100% rename from libs/mangacollec/src/mangacollec/dto/authors_response.py rename to libs/mangacollec/src/mangacollec/dto/catalogue/authors_response.py diff --git a/libs/mangacollec/src/mangacollec/dto/box_response.py b/libs/mangacollec/src/mangacollec/dto/catalogue/box_response.py similarity index 100% rename from libs/mangacollec/src/mangacollec/dto/box_response.py rename to libs/mangacollec/src/mangacollec/dto/catalogue/box_response.py diff --git a/libs/mangacollec/src/mangacollec/dto/edition_response.py b/libs/mangacollec/src/mangacollec/dto/catalogue/edition_response.py similarity index 100% rename from libs/mangacollec/src/mangacollec/dto/edition_response.py rename to libs/mangacollec/src/mangacollec/dto/catalogue/edition_response.py diff --git a/libs/mangacollec/src/mangacollec/dto/kinds_response.py b/libs/mangacollec/src/mangacollec/dto/catalogue/kinds_response.py similarity index 100% rename from libs/mangacollec/src/mangacollec/dto/kinds_response.py rename to libs/mangacollec/src/mangacollec/dto/catalogue/kinds_response.py diff --git a/libs/mangacollec/src/mangacollec/dto/publisher_response.py b/libs/mangacollec/src/mangacollec/dto/catalogue/publisher_response.py similarity index 100% rename from libs/mangacollec/src/mangacollec/dto/publisher_response.py rename to libs/mangacollec/src/mangacollec/dto/catalogue/publisher_response.py diff --git a/libs/mangacollec/src/mangacollec/dto/publishers_response.py b/libs/mangacollec/src/mangacollec/dto/catalogue/publishers_response.py similarity index 100% rename from libs/mangacollec/src/mangacollec/dto/publishers_response.py rename to libs/mangacollec/src/mangacollec/dto/catalogue/publishers_response.py diff --git a/libs/mangacollec/src/mangacollec/dto/serie_response.py b/libs/mangacollec/src/mangacollec/dto/catalogue/serie_response.py similarity index 100% rename from libs/mangacollec/src/mangacollec/dto/serie_response.py rename to libs/mangacollec/src/mangacollec/dto/catalogue/serie_response.py diff --git a/libs/mangacollec/src/mangacollec/dto/series_response.py b/libs/mangacollec/src/mangacollec/dto/catalogue/series_response.py similarity index 100% rename from libs/mangacollec/src/mangacollec/dto/series_response.py rename to libs/mangacollec/src/mangacollec/dto/catalogue/series_response.py diff --git a/libs/mangacollec/src/mangacollec/dto/volume_response.py b/libs/mangacollec/src/mangacollec/dto/catalogue/volume_response.py similarity index 88% rename from libs/mangacollec/src/mangacollec/dto/volume_response.py rename to libs/mangacollec/src/mangacollec/dto/catalogue/volume_response.py index b318990..5f26e71 100644 --- a/libs/mangacollec/src/mangacollec/dto/volume_response.py +++ b/libs/mangacollec/src/mangacollec/dto/catalogue/volume_response.py @@ -25,6 +25,6 @@ class VolumeResponse(BaseModel): publishers: list[Publisher] series: list[Serie] types: list[SerieType] - box_volumes: list[BoxVolume] | None - boxes: list[Boxe] | None - box_editions: list[BoxEdition] | None + box_volumes: list[BoxVolume] | None = None + boxes: list[Boxe] | None = None + box_editions: list[BoxEdition] | None = None diff --git a/libs/mangacollec/src/mangacollec/dto/follows/__init__.py b/libs/mangacollec/src/mangacollec/dto/follows/__init__.py new file mode 100644 index 0000000..144b16d --- /dev/null +++ b/libs/mangacollec/src/mangacollec/dto/follows/__init__.py @@ -0,0 +1,5 @@ +from mangacollec.dto.follows.follow_edition_create_request import FollowEditionCreateRequest + +__all__ = [ + "FollowEditionCreateRequest" +] \ No newline at end of file diff --git a/libs/mangacollec/src/mangacollec/dto/follow_edition_create_request.py b/libs/mangacollec/src/mangacollec/dto/follows/follow_edition_create_request.py similarity index 100% rename from libs/mangacollec/src/mangacollec/dto/follow_edition_create_request.py rename to libs/mangacollec/src/mangacollec/dto/follows/follow_edition_create_request.py diff --git a/libs/mangacollec/src/mangacollec/dto/loans/__init__.py b/libs/mangacollec/src/mangacollec/dto/loans/__init__.py new file mode 100644 index 0000000..be3b779 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/dto/loans/__init__.py @@ -0,0 +1,18 @@ +from mangacollec.dto.loans.borrower_create_request import BorrowerCreateRequest +from mangacollec.dto.loans.borrower_create_response import BorrowerCreateResponse +from mangacollec.dto.loans.loans_create_request import LoanCreateRequest, LoansCreateRequest +from mangacollec.dto.loans.loans_create_response import LoanCreateError, LoansCreateResponse +from mangacollec.dto.loans.loans_delete_request import LoanDeleteRequest, LoansDeleteRequest +from mangacollec.dto.loans.loans_delete_response import LoansDeleteResponse + +__all__ = [ + "BorrowerCreateRequest", + "BorrowerCreateResponse", + "LoanCreateError", + "LoanCreateRequest", + "LoanDeleteRequest", + "LoansCreateRequest", + "LoansCreateResponse", + "LoansDeleteRequest", + "LoansDeleteResponse" +] \ No newline at end of file diff --git a/libs/mangacollec/src/mangacollec/dto/borrower_create_request.py b/libs/mangacollec/src/mangacollec/dto/loans/borrower_create_request.py similarity index 100% rename from libs/mangacollec/src/mangacollec/dto/borrower_create_request.py rename to libs/mangacollec/src/mangacollec/dto/loans/borrower_create_request.py diff --git a/libs/mangacollec/src/mangacollec/dto/borrower_create_response.py b/libs/mangacollec/src/mangacollec/dto/loans/borrower_create_response.py similarity index 100% rename from libs/mangacollec/src/mangacollec/dto/borrower_create_response.py rename to libs/mangacollec/src/mangacollec/dto/loans/borrower_create_response.py diff --git a/libs/mangacollec/src/mangacollec/dto/loans_create_request.py b/libs/mangacollec/src/mangacollec/dto/loans/loans_create_request.py similarity index 86% rename from libs/mangacollec/src/mangacollec/dto/loans_create_request.py rename to libs/mangacollec/src/mangacollec/dto/loans/loans_create_request.py index e240f40..b9837c4 100644 --- a/libs/mangacollec/src/mangacollec/dto/loans_create_request.py +++ b/libs/mangacollec/src/mangacollec/dto/loans/loans_create_request.py @@ -11,14 +11,14 @@ from mangacollec.value_object import ID class LoanCreateRequest(BaseModel): - """Association entre un emprunteur et une possession à prêter.""" + """Association entre un emprunteur et une possessions à prêter.""" borrower_id: ID possession_id: ID class LoansCreateRequest(BaseModel): - """Liste des associations emprunteur-possession à créer.""" + """Liste des associations emprunteur-possessions à créer.""" loans: list[LoanCreateRequest] diff --git a/libs/mangacollec/src/mangacollec/dto/loans_create_response.py b/libs/mangacollec/src/mangacollec/dto/loans/loans_create_response.py similarity index 100% rename from libs/mangacollec/src/mangacollec/dto/loans_create_response.py rename to libs/mangacollec/src/mangacollec/dto/loans/loans_create_response.py diff --git a/libs/mangacollec/src/mangacollec/dto/loans_delete_request.py b/libs/mangacollec/src/mangacollec/dto/loans/loans_delete_request.py similarity index 100% rename from libs/mangacollec/src/mangacollec/dto/loans_delete_request.py rename to libs/mangacollec/src/mangacollec/dto/loans/loans_delete_request.py diff --git a/libs/mangacollec/src/mangacollec/dto/loans_delete_response.py b/libs/mangacollec/src/mangacollec/dto/loans/loans_delete_response.py similarity index 100% rename from libs/mangacollec/src/mangacollec/dto/loans_delete_response.py rename to libs/mangacollec/src/mangacollec/dto/loans/loans_delete_response.py diff --git a/libs/mangacollec/src/mangacollec/dto/possessions/__init__.py b/libs/mangacollec/src/mangacollec/dto/possessions/__init__.py new file mode 100644 index 0000000..c8bb820 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/dto/possessions/__init__.py @@ -0,0 +1,45 @@ +from mangacollec.dto.possessions.create_box_possessions_multiple_request import ( + CreateBoxesPossessionsMultipleRequest, + CreateBoxPossessionMultipleRequest, +) +from mangacollec.dto.possessions.create_box_possessions_multiple_response import ( + CreateBoxPossessionsMultipleResponse, +) +from mangacollec.dto.possessions.create_volume_possessions_multiple_request import ( + CreateVolumePossessionMultipleRequest, + CreateVolumesPossessionsMultipleRequest, +) +from mangacollec.dto.possessions.create_volume_possessions_multiple_response import ( + CreateVolumePossessionsMultipleResponse, +) +from mangacollec.dto.possessions.delete_box_possessions_multiple_request import ( + DeleteBoxPossessionMultipleRequest, + DeleteBoxPossessionsMultipleRequest, + DeletePossessionMultipleRequest, +) +from mangacollec.dto.possessions.delete_box_possessions_multiple_response import ( + DeleteBoxPossessionsMultipleResponse, +) +from mangacollec.dto.possessions.delete_box_possessions_multiple_response import ( + DeletedResource as DeletedBoxResource, +) +from mangacollec.dto.possessions.delete_volume_possessions_multiple_response import ( + DeletedResource, + DeleteVolumePossessionsMultipleResponse, +) + +__all__ = [ + "CreateBoxPossessionMultipleRequest", + "CreateBoxPossessionsMultipleResponse", + "CreateBoxesPossessionsMultipleRequest", + "CreateVolumePossessionMultipleRequest", + "CreateVolumePossessionsMultipleResponse", + "CreateVolumesPossessionsMultipleRequest", + "DeleteBoxPossessionMultipleRequest", + "DeleteBoxPossessionsMultipleRequest", + "DeleteBoxPossessionsMultipleResponse", + "DeletePossessionMultipleRequest", + "DeleteVolumePossessionsMultipleResponse", + "DeletedBoxResource", + "DeletedResource" +] \ No newline at end of file diff --git a/libs/mangacollec/src/mangacollec/dto/create_box_possessions_multiple_request.py b/libs/mangacollec/src/mangacollec/dto/possessions/create_box_possessions_multiple_request.py similarity index 100% rename from libs/mangacollec/src/mangacollec/dto/create_box_possessions_multiple_request.py rename to libs/mangacollec/src/mangacollec/dto/possessions/create_box_possessions_multiple_request.py diff --git a/libs/mangacollec/src/mangacollec/dto/create_box_possessions_multiple_response.py b/libs/mangacollec/src/mangacollec/dto/possessions/create_box_possessions_multiple_response.py similarity index 100% rename from libs/mangacollec/src/mangacollec/dto/create_box_possessions_multiple_response.py rename to libs/mangacollec/src/mangacollec/dto/possessions/create_box_possessions_multiple_response.py diff --git a/libs/mangacollec/src/mangacollec/dto/create_volume_possessions_multiple_request.py b/libs/mangacollec/src/mangacollec/dto/possessions/create_volume_possessions_multiple_request.py similarity index 100% rename from libs/mangacollec/src/mangacollec/dto/create_volume_possessions_multiple_request.py rename to libs/mangacollec/src/mangacollec/dto/possessions/create_volume_possessions_multiple_request.py diff --git a/libs/mangacollec/src/mangacollec/dto/create_volume_possessions_multiple_response.py b/libs/mangacollec/src/mangacollec/dto/possessions/create_volume_possessions_multiple_response.py similarity index 100% rename from libs/mangacollec/src/mangacollec/dto/create_volume_possessions_multiple_response.py rename to libs/mangacollec/src/mangacollec/dto/possessions/create_volume_possessions_multiple_response.py diff --git a/libs/mangacollec/src/mangacollec/dto/delete_box_possessions_multiple_request.py b/libs/mangacollec/src/mangacollec/dto/possessions/delete_box_possessions_multiple_request.py similarity index 97% rename from libs/mangacollec/src/mangacollec/dto/delete_box_possessions_multiple_request.py rename to libs/mangacollec/src/mangacollec/dto/possessions/delete_box_possessions_multiple_request.py index 5b50f4d..53e1d95 100644 --- a/libs/mangacollec/src/mangacollec/dto/delete_box_possessions_multiple_request.py +++ b/libs/mangacollec/src/mangacollec/dto/possessions/delete_box_possessions_multiple_request.py @@ -11,7 +11,7 @@ from mangacollec.value_object import ID class DeleteBoxPossessionMultipleRequest(BaseModel): - """Box possession à supprimer.""" + """Box possessions à supprimer.""" id: ID diff --git a/libs/mangacollec/src/mangacollec/dto/delete_box_possessions_multiple_response.py b/libs/mangacollec/src/mangacollec/dto/possessions/delete_box_possessions_multiple_response.py similarity index 100% rename from libs/mangacollec/src/mangacollec/dto/delete_box_possessions_multiple_response.py rename to libs/mangacollec/src/mangacollec/dto/possessions/delete_box_possessions_multiple_response.py diff --git a/libs/mangacollec/src/mangacollec/dto/delete_volume_possessions_multiple_response.py b/libs/mangacollec/src/mangacollec/dto/possessions/delete_volume_possessions_multiple_response.py similarity index 100% rename from libs/mangacollec/src/mangacollec/dto/delete_volume_possessions_multiple_response.py rename to libs/mangacollec/src/mangacollec/dto/possessions/delete_volume_possessions_multiple_response.py diff --git a/libs/mangacollec/src/mangacollec/dto/reads/__init__.py b/libs/mangacollec/src/mangacollec/dto/reads/__init__.py new file mode 100644 index 0000000..d900901 --- /dev/null +++ b/libs/mangacollec/src/mangacollec/dto/reads/__init__.py @@ -0,0 +1,15 @@ + +from mangacollec.dto.reads.reads_create_request import ReadCreateRequest, ReadsCreateRequest +from mangacollec.dto.reads.reads_create_response import ReadsCreateResponse +from mangacollec.dto.reads.reads_delete_request import ReadDeleteRequest, ReadsDeleteRequest +from mangacollec.dto.reads.reads_delete_response import ReadDeleteError, ReadsDeleteResponse + +__all__ = [ + "ReadCreateRequest", + "ReadDeleteError", + "ReadDeleteRequest", + "ReadsCreateRequest", + "ReadsCreateResponse", + "ReadsDeleteRequest", + "ReadsDeleteResponse" +] \ No newline at end of file diff --git a/libs/mangacollec/src/mangacollec/dto/reads_create_request.py b/libs/mangacollec/src/mangacollec/dto/reads/reads_create_request.py similarity index 100% rename from libs/mangacollec/src/mangacollec/dto/reads_create_request.py rename to libs/mangacollec/src/mangacollec/dto/reads/reads_create_request.py diff --git a/libs/mangacollec/src/mangacollec/dto/reads_create_response.py b/libs/mangacollec/src/mangacollec/dto/reads/reads_create_response.py similarity index 100% rename from libs/mangacollec/src/mangacollec/dto/reads_create_response.py rename to libs/mangacollec/src/mangacollec/dto/reads/reads_create_response.py diff --git a/libs/mangacollec/src/mangacollec/dto/reads_delete_request.py b/libs/mangacollec/src/mangacollec/dto/reads/reads_delete_request.py similarity index 100% rename from libs/mangacollec/src/mangacollec/dto/reads_delete_request.py rename to libs/mangacollec/src/mangacollec/dto/reads/reads_delete_request.py diff --git a/libs/mangacollec/src/mangacollec/dto/reads_delete_response.py b/libs/mangacollec/src/mangacollec/dto/reads/reads_delete_response.py similarity index 100% rename from libs/mangacollec/src/mangacollec/dto/reads_delete_response.py rename to libs/mangacollec/src/mangacollec/dto/reads/reads_delete_response.py diff --git a/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_author.py b/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_author.py index 847882e..9f95361 100644 --- a/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_author.py +++ b/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_author.py @@ -3,8 +3,8 @@ # \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / # ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / # /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ -from mangacollec.dto import AuthorResponse -from mangacollec.interface.endpoint_interface import IEndpoint +from mangacollec.dto.catalogue import AuthorResponse +from mangacollec.interface import IEndpoint from mangacollec.value_object import ID diff --git a/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_authors.py b/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_authors.py index 9dd55dc..25c7d9f 100644 --- a/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_authors.py +++ b/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_authors.py @@ -4,8 +4,8 @@ # ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / # /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ -from mangacollec.dto.authors_response import AuthorsResponse -from mangacollec.interface.endpoint_interface import IEndpoint +from mangacollec.dto.catalogue import AuthorsResponse +from mangacollec.interface import IEndpoint class CallAuthorsEndpoint(IEndpoint): diff --git a/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_boxes.py b/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_boxes.py index 4a7e75c..97d2817 100644 --- a/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_boxes.py +++ b/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_boxes.py @@ -4,8 +4,8 @@ # ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / # /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ -from mangacollec.dto import BoxResponse -from mangacollec.interface.endpoint_interface import IEndpoint +from mangacollec.dto.catalogue import BoxResponse +from mangacollec.interface import IEndpoint from mangacollec.value_object import ID diff --git a/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_edition.py b/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_edition.py index 60819ce..db0f0a2 100644 --- a/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_edition.py +++ b/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_edition.py @@ -3,8 +3,8 @@ # \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / # ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / # /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ -from mangacollec.dto import EditionResponse -from mangacollec.interface.endpoint_interface import IEndpoint +from mangacollec.dto.catalogue import EditionResponse +from mangacollec.interface import IEndpoint from mangacollec.value_object import ID diff --git a/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_kinds.py b/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_kinds.py index 34ef6bc..32e07c5 100644 --- a/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_kinds.py +++ b/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_kinds.py @@ -3,8 +3,8 @@ # \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / # ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / # /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ -from mangacollec.dto.kinds_response import KindsResponse -from mangacollec.interface.endpoint_interface import IEndpoint +from mangacollec.dto.catalogue.kinds_response import KindsResponse +from mangacollec.interface import IEndpoint class CallKindsEndpoint(IEndpoint): diff --git a/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_news.py b/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_news.py index bf79033..9d1f62a 100644 --- a/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_news.py +++ b/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_news.py @@ -4,7 +4,7 @@ # ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / # /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ from mangacollec.dto.news_response import NewsResponse -from mangacollec.interface.endpoint_interface import IEndpoint +from mangacollec.interface import IEndpoint class CallNewsEndpoint(IEndpoint): diff --git a/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_publisher.py b/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_publisher.py index 77f57d6..8fe7416 100644 --- a/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_publisher.py +++ b/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_publisher.py @@ -3,8 +3,8 @@ # \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / # ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / # /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ -from mangacollec.dto import PublisherResponse -from mangacollec.interface.endpoint_interface import IEndpoint +from mangacollec.dto.catalogue import PublisherResponse +from mangacollec.interface import IEndpoint from mangacollec.value_object import ID diff --git a/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_publishers.py b/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_publishers.py index ed4bc76..29b08ea 100644 --- a/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_publishers.py +++ b/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_publishers.py @@ -3,8 +3,8 @@ # \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / # ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / # /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ -from mangacollec.dto.publishers_response import PublishersResponse -from mangacollec.interface.endpoint_interface import IEndpoint +from mangacollec.dto.catalogue.publishers_response import PublishersResponse +from mangacollec.interface import IEndpoint class CallPublishesEndpoint(IEndpoint): diff --git a/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_serie.py b/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_serie.py index c107e3f..e5c7eb7 100644 --- a/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_serie.py +++ b/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_serie.py @@ -3,8 +3,8 @@ # \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / # ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / # /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ -from mangacollec.dto import SerieResponse -from mangacollec.interface.endpoint_interface import IEndpoint +from mangacollec.dto.catalogue import SerieResponse +from mangacollec.interface import IEndpoint from mangacollec.value_object import ID diff --git a/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_series.py b/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_series.py index 07daa9e..cc187fd 100644 --- a/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_series.py +++ b/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_series.py @@ -3,8 +3,8 @@ # \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / # ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / # /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ -from mangacollec.dto.series_response import SeriesResponse -from mangacollec.interface.endpoint_interface import IEndpoint +from mangacollec.dto.catalogue.series_response import SeriesResponse +from mangacollec.interface import IEndpoint class CallSeriesEndpoint(IEndpoint): diff --git a/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_volume.py b/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_volume.py index e652e5f..2b38bb6 100644 --- a/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_volume.py +++ b/libs/mangacollec/src/mangacollec/endpoints/catalogue/call_volume.py @@ -3,8 +3,8 @@ # \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / # ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / # /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ -from mangacollec.dto.volume_response import VolumeResponse -from mangacollec.interface.endpoint_interface import IEndpoint +from mangacollec.dto.catalogue.volume_response import VolumeResponse +from mangacollec.interface import IEndpoint from mangacollec.value_object import ID 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 index bd67c82..1cc27e7 100644 --- a/libs/mangacollec/src/mangacollec/endpoints/follows/call_create_follow_edition.py +++ b/libs/mangacollec/src/mangacollec/endpoints/follows/call_create_follow_edition.py @@ -5,8 +5,8 @@ # /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ """Wrapper de l'endpoint de création d'un suivi d'édition.""" -from mangacollec.dto import FollowEditionCreateRequest -from mangacollec.interface.endpoint_interface import IEndpoint +from mangacollec.dto.follows import FollowEditionCreateRequest +from mangacollec.interface import IEndpoint from mangacollec.model.possessions import VolumeFollowEdition from mangacollec.value_object import ID 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 index 84a476f..3059654 100644 --- a/libs/mangacollec/src/mangacollec/endpoints/follows/call_delete_follow_edition.py +++ b/libs/mangacollec/src/mangacollec/endpoints/follows/call_delete_follow_edition.py @@ -5,7 +5,7 @@ # /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ """Wrapper de suppression d'un suivi d'édition.""" -from mangacollec.interface.endpoint_interface import IEndpoint +from mangacollec.interface import IEndpoint from mangacollec.value_object import ID diff --git a/libs/mangacollec/src/mangacollec/endpoints/loans/call_create_borrower.py b/libs/mangacollec/src/mangacollec/endpoints/loans/call_create_borrower.py index 44d461c..a718708 100644 --- a/libs/mangacollec/src/mangacollec/endpoints/loans/call_create_borrower.py +++ b/libs/mangacollec/src/mangacollec/endpoints/loans/call_create_borrower.py @@ -7,8 +7,8 @@ from typing import Literal -from mangacollec.dto import BorrowerCreateRequest, BorrowerCreateResponse -from mangacollec.interface.mutation_client_interface import IPostClient +from mangacollec.dto.loans import BorrowerCreateRequest, BorrowerCreateResponse +from mangacollec.interface import IPostClient class CallCreateBorrowerEndpoint: 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 index 1542573..43ae94d 100644 --- a/libs/mangacollec/src/mangacollec/endpoints/loans/call_create_loans_multiple.py +++ b/libs/mangacollec/src/mangacollec/endpoints/loans/call_create_loans_multiple.py @@ -5,8 +5,8 @@ # /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ """Endpoint de création groupée de prêts.""" -from mangacollec.dto import LoanCreateRequest, LoansCreateRequest, LoansCreateResponse -from mangacollec.interface.mutation_client_interface import IPostClient +from mangacollec.dto.loans import LoanCreateRequest, LoansCreateRequest, LoansCreateResponse +from mangacollec.interface import IPostClient class CallCreateLoansMultipleEndpoint: diff --git a/libs/mangacollec/src/mangacollec/endpoints/loans/call_delete_borrower.py b/libs/mangacollec/src/mangacollec/endpoints/loans/call_delete_borrower.py index 37b0f3c..cfdcfe4 100644 --- a/libs/mangacollec/src/mangacollec/endpoints/loans/call_delete_borrower.py +++ b/libs/mangacollec/src/mangacollec/endpoints/loans/call_delete_borrower.py @@ -5,7 +5,7 @@ # /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ """Wrapper de suppression d'un emprunteur.""" -from mangacollec.interface.mutation_client_interface import IDeleteClient +from mangacollec.interface import IDeleteClient from mangacollec.value_object import 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 index 40a705f..47dba56 100644 --- a/libs/mangacollec/src/mangacollec/endpoints/loans/call_delete_loans_multiple.py +++ b/libs/mangacollec/src/mangacollec/endpoints/loans/call_delete_loans_multiple.py @@ -5,9 +5,9 @@ # /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ """Wrapper de suppression groupée de prêts.""" -from mangacollec.dto import LoanDeleteRequest, LoansDeleteRequest, LoansDeleteResponse +from mangacollec.dto.loans import LoanDeleteRequest, LoansDeleteRequest, LoansDeleteResponse from mangacollec.exceptions.exceptions import DomainValidationError -from mangacollec.interface.mutation_client_interface import IDeleteClient +from mangacollec.interface import IDeleteClient from mangacollec.value_object import ID 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 index a258475..34c73ab 100644 --- 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 @@ -5,13 +5,13 @@ # /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ """Endpoint de création groupée de possessions.""" -from mangacollec.dto import ( +from mangacollec.dto.possessions import ( CreateBoxesPossessionsMultipleRequest, CreateBoxPossessionMultipleRequest, CreateBoxPossessionsMultipleResponse, ) from mangacollec.exceptions.exceptions import DomainValidationError -from mangacollec.interface.endpoint_interface import IEndpoint +from mangacollec.interface import IEndpoint from mangacollec.value_object import ID 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 index 8c8a2a9..f2a2a1f 100644 --- 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 @@ -5,13 +5,13 @@ # /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ """Endpoint de création groupée de possessions.""" -from mangacollec.dto import ( +from mangacollec.dto.possessions import ( CreateVolumePossessionMultipleRequest, CreateVolumePossessionsMultipleResponse, CreateVolumesPossessionsMultipleRequest, ) from mangacollec.exceptions.exceptions import DomainValidationError -from mangacollec.interface.endpoint_interface import IEndpoint +from mangacollec.interface import IEndpoint from mangacollec.value_object import ID 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 index a65f2bf..5c94bee 100644 --- 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 @@ -5,14 +5,14 @@ # /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ """Endpoint de suppression groupée de possessions de boxes.""" -from mangacollec.dto import ( +from mangacollec.dto.possessions import ( DeleteBoxPossessionMultipleRequest, DeleteBoxPossessionsMultipleRequest, DeleteBoxPossessionsMultipleResponse, DeletePossessionMultipleRequest, ) from mangacollec.exceptions.exceptions import DomainValidationError -from mangacollec.interface.endpoint_interface import IEndpoint +from mangacollec.interface import IEndpoint from mangacollec.value_object import ID 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 index 89369de..9d2fda9 100644 --- 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 @@ -7,11 +7,11 @@ from pydantic import BaseModel -from mangacollec.dto import ( +from mangacollec.dto.possessions import ( DeleteVolumePossessionsMultipleResponse, ) from mangacollec.exceptions.exceptions import DomainValidationError -from mangacollec.interface.endpoint_interface import IEndpoint +from mangacollec.interface import IEndpoint from mangacollec.value_object import ID 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 index c339887..6fbe7d2 100644 --- a/libs/mangacollec/src/mangacollec/endpoints/reads/call_create_reads_multiple.py +++ b/libs/mangacollec/src/mangacollec/endpoints/reads/call_create_reads_multiple.py @@ -7,10 +7,10 @@ from typing import TYPE_CHECKING -from mangacollec.dto import ReadCreateRequest, ReadsCreateRequest, ReadsCreateResponse +from mangacollec.dto.reads import ReadCreateRequest, ReadsCreateRequest, ReadsCreateResponse if TYPE_CHECKING: - from mangacollec.interface.mutation_client_interface import IPostClient + from mangacollec.interface import IPostClient from mangacollec.value_object import ID 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 index 3f361c2..d2231a1 100644 --- a/libs/mangacollec/src/mangacollec/endpoints/reads/call_delete_reads_multiple.py +++ b/libs/mangacollec/src/mangacollec/endpoints/reads/call_delete_reads_multiple.py @@ -5,9 +5,9 @@ # /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ """Wrapper de suppression groupée de lectures.""" -from mangacollec.dto import ReadDeleteRequest, ReadsDeleteRequest, ReadsDeleteResponse +from mangacollec.dto.reads import ReadDeleteRequest, ReadsDeleteRequest, ReadsDeleteResponse from mangacollec.exceptions.exceptions import DomainValidationError -from mangacollec.interface.mutation_client_interface import IDeleteClient +from mangacollec.interface import IDeleteClient from mangacollec.value_object import ID diff --git a/libs/mangacollec/src/mangacollec/mangacollec.py b/libs/mangacollec/src/mangacollec/mangacollec.py index 15f302b..c14aa01 100644 --- a/libs/mangacollec/src/mangacollec/mangacollec.py +++ b/libs/mangacollec/src/mangacollec/mangacollec.py @@ -29,6 +29,7 @@ class Mangacollec: _AUTHENTICATION_PATH: str = "oauth/token" _DEFAULT_REQUEST_TIMEOUT: float = 30.0 _TOKEN_SAFETY_MARGIN: int = 30 * 60 + _RATE_LIMIT_STATUS: int = 429 _SERVER_ERROR_MIN: int = 500 _SERVER_ERROR_MAX: int = 600 _NO_CONTENT_STATUS: int = 204 @@ -37,17 +38,21 @@ class Mangacollec: _oauth_client: httpx.AsyncClient _is_auth: bool - _MANGACOLLEC_CLIENT_ID: str - _MANGACOLLEC_CLIENT_SECRET: str + _client_id: str + _client_secret: str - def __init__(self, proxy: str | None = None) -> None: - """Configure HTTP clients with environment-provided credentials. + def __init__(self, client_id: str, client_secret: str, proxy: str | None = None) -> None: + """Configure HTTP clients with explicitly 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() + if not client_id or not client_secret: + raise ConfigurationError("Les identifiants MangaCollec sont requis.") + + self._client_id = client_id + self._client_secret = client_secret self._headers = { "Accept": "application/json", @@ -104,19 +109,6 @@ class Mangacollec: 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.""" @@ -167,13 +159,17 @@ class Mangacollec: 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): + retry_after: str | None = None 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 response.status_code == self._RATE_LIMIT_STATUS or ( + self._SERVER_ERROR_MIN <= response.status_code < self._SERVER_ERROR_MAX + ): if not retryable or retry_count == RetryPolicy.max_retries: response.raise_for_status() + retry_after = response.headers.get("Retry-After") else: response.raise_for_status() if response.status_code == self._NO_CONTENT_STATUS: @@ -194,7 +190,16 @@ class Mangacollec: if not retryable or retry_count == RetryPolicy.max_retries: raise - await asyncio.sleep(RetryPolicy.delay_for_retry(retry_count + 1)) + delay_seconds = RetryPolicy.delay_for_retry(retry_count + 1, retry_after=retry_after) + logger.warning( + "%s %s : reprise %d/%d dans %s secondes.", + method.upper(), + url, + retry_count + 1, + RetryPolicy.max_retries, + delay_seconds, + ) + await asyncio.sleep(delay_seconds) raise RemoteRequestError("La requête a échoué après cinq reprises.") @@ -229,8 +234,8 @@ class Mangacollec: 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, + client_id=self._client_id, + client_secret=self._client_secret, grant_type="client_credentials", ) await self._authenticate(credential) @@ -238,8 +243,8 @@ class Mangacollec: 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, + client_id=self._client_id, + client_secret=self._client_secret, grant_type="password", username=username, password=password, @@ -310,8 +315,8 @@ class Mangacollec: 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, + client_id=self._client_id, + client_secret=self._client_secret, grant_type="refresh_token", refresh_token=previous_refresh_token, ) diff --git a/libs/mangacollec/src/mangacollec/model/loans/loan.py b/libs/mangacollec/src/mangacollec/model/loans/loan.py index 439d7cb..b651532 100644 --- a/libs/mangacollec/src/mangacollec/model/loans/loan.py +++ b/libs/mangacollec/src/mangacollec/model/loans/loan.py @@ -3,7 +3,7 @@ # \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / # ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / # /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ -"""Modèle d'un prêt de possession à un emprunteur.""" +"""Modèle d'un prêt de possessions à un emprunteur.""" from datetime import datetime diff --git a/libs/mangacollec/src/mangacollec/retry_policy.py b/libs/mangacollec/src/mangacollec/retry_policy.py index 327c0e5..6754728 100644 --- a/libs/mangacollec/src/mangacollec/retry_policy.py +++ b/libs/mangacollec/src/mangacollec/retry_policy.py @@ -4,23 +4,33 @@ # ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / # /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ -"""Fixed retry policy shared by MangaCollec transports.""" +"""Retry delays shared by MangaCollec transports.""" +import math from typing import ClassVar class RetryPolicy: - """Define the bounded exponential delay used before retrying MangaCollec requests.""" + """Prefer Retry-After seconds with a margin over bounded exponential delays.""" max_retries: ClassVar[int] = 5 initial_delay_seconds: ClassVar[float] = 0.5 maximum_delay_seconds: ClassVar[float] = 8.0 + retry_after_multiplier: ClassVar[float] = 1.05 @classmethod - def delay_for_retry(cls, retry_count: int) -> float: + def delay_for_retry(cls, retry_count: int, *, retry_after: str | None = None) -> 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.") + + if retry_after is not None: + seconds = retry_after.strip() + if seconds.isascii() and seconds.isdecimal(): + retry_after_delay = float(seconds) * cls.retry_after_multiplier + if math.isfinite(retry_after_delay): + return retry_after_delay + delay_seconds: float = cls.initial_delay_seconds * 2 ** (retry_count - 1) if delay_seconds >= cls.maximum_delay_seconds: return cls.maximum_delay_seconds diff --git a/libs/mangacollec/tests/conftest.py b/libs/mangacollec/tests/conftest.py new file mode 100644 index 0000000..7b4f7e8 --- /dev/null +++ b/libs/mangacollec/tests/conftest.py @@ -0,0 +1,69 @@ +"""Fixtures for MangaCollec requests without network access or real waiting.""" + +import asyncio +from collections.abc import Iterator +from unittest.mock import AsyncMock + +import httpx +import pytest +from mangacollec.mangacollec import Mangacollec +from pytest_mock import MockerFixture + + +@pytest.fixture +def responses() -> list[httpx.Response | httpx.RequestError]: + """Return the sequence of HTTP responses and transport errors to simulate.""" + return [] + + +@pytest.fixture +def sent_requests() -> list[httpx.Request]: + """Record requests made through the mock transport.""" + return [] + + +@pytest.fixture +def client( + responses: list[httpx.Response | httpx.RequestError], + sent_requests: list[httpx.Request], + monkeypatch: pytest.MonkeyPatch, +) -> Iterator[Mangacollec]: + """Provide an authenticated client with mocked resource and OAuth transports.""" + monkeypatch.setenv("BOOKSYNC_REQUEST_TIMEOUT", "30") + client = Mangacollec("test-client", "test-secret") + asyncio.run(client.close()) + + def handle_request(request: httpx.Request) -> httpx.Response: + """Consume the next simulated response and record its request.""" + sent_requests.append(request) + response = responses.pop(0) + if isinstance(response, httpx.RequestError): + raise response + return response + + transport = httpx.MockTransport(handle_request) + client._client = httpx.AsyncClient( + base_url="https://api.mangacollec.com/", transport=transport, trust_env=False, + ) + client._oauth_client = httpx.AsyncClient( + base_url="https://api.mangacollec.com/", transport=transport, trust_env=False, + ) + client._set_token({ + "access_token": "test-token", + "token_type": "Bearer", + "expires_in": 86400, + "created_at": 0, + }) + + try: + yield client + finally: + asyncio.run(client.close()) + + +@pytest.fixture +def sleep_mock(mocker: MockerFixture) -> AsyncMock: + """Record asynchronous waits while completing them immediately.""" + sleep = AsyncMock(return_value=None) + mocker.patch("mangacollec.mangacollec.asyncio.sleep", new=sleep) + return sleep diff --git a/libs/mangacollec/tests/test_mangacollec.py b/libs/mangacollec/tests/test_mangacollec.py new file mode 100644 index 0000000..7e9c567 --- /dev/null +++ b/libs/mangacollec/tests/test_mangacollec.py @@ -0,0 +1,201 @@ +"""Tests for HTTP retry behavior through simulated MangaCollec transports.""" + +import asyncio +from unittest.mock import AsyncMock, call + +import httpx +import pytest +from mangacollec.mangacollec import Mangacollec + + +class TestRequest: + """Exercise HTTP retries, transport errors and retry eligibility.""" + + @pytest.mark.parametrize("status_code", [429, 500, 503, 599]) + @pytest.mark.parametrize("retry_after, expected_delay", [ + ("120", 126.0), ("0", 0.0), (None, 0.5), ("invalid", 0.5), + ]) + def test_retry_then_success( + self, + client: Mangacollec, + responses: list[httpx.Response | httpx.RequestError], + sent_requests: list[httpx.Request], + sleep_mock: AsyncMock, + status_code: int, + retry_after: str | None, + expected_delay: float, + ) -> None: + """Retry rate limits and server errors using the current response's delay.""" + headers = {} if retry_after is None else {"retry-after": retry_after} + responses.extend([ + httpx.Response(status_code, headers=headers), + httpx.Response(200, json={"series": []}), + ]) + + payload = asyncio.run(client.get("series")) + + assert payload == {"series": []} + assert len(sent_requests) == 2 + sleep_mock.assert_awaited_once_with(expected_delay) + + @pytest.mark.parametrize("status_code", [429, 503]) + @pytest.mark.parametrize("retry_after", [None, "120"]) + def test_retry_limit( + self, + client: Mangacollec, + responses: list[httpx.Response | httpx.RequestError], + sent_requests: list[httpx.Request], + sleep_mock: AsyncMock, + status_code: int, + retry_after: str | None, + ) -> None: + """Raise the final HTTP error after five waits and six attempts.""" + headers = {} if retry_after is None else {"Retry-After": retry_after} + responses.extend(httpx.Response(status_code, headers=headers) for _ in range(6)) + final_response = responses[-1] + + with pytest.raises(httpx.HTTPStatusError) as error: + asyncio.run(client.get("series")) + + assert error.value.response is final_response + assert len(sent_requests) == 6 + expected_delays = [0.5, 1.0, 2.0, 4.0, 8.0] if retry_after is None else [126.0] * 5 + assert sleep_mock.await_args_list == [call(delay) for delay in expected_delays] + + @pytest.mark.parametrize("error_type", [ + httpx.ReadTimeout, httpx.ConnectError, httpx.RemoteProtocolError, + ]) + def test_network_error_resets_header( + self, + client: Mangacollec, + responses: list[httpx.Response | httpx.RequestError], + sent_requests: list[httpx.Request], + sleep_mock: AsyncMock, + error_type: type[httpx.RequestError], + ) -> None: + """Use exponential fallback after a transport error rather than stale headers.""" + responses.extend([ + httpx.Response(429, headers={"Retry-After": "120"}), + error_type("Simulated network error"), + httpx.Response(200, json={}), + ]) + + assert asyncio.run(client.get("series")) == {} + assert len(sent_requests) == 3 + assert sleep_mock.await_args_list == [call(126.0), call(1.0)] + + def test_network_error_limit( + self, + client: Mangacollec, + responses: list[httpx.Response | httpx.RequestError], + sent_requests: list[httpx.Request], + sleep_mock: AsyncMock, + ) -> None: + """Propagate the final transport error without an additional wait.""" + responses.extend(httpx.ConnectError("Simulated network error") for _ in range(6)) + final_error = responses[-1] + + with pytest.raises(httpx.ConnectError) as error: + asyncio.run(client.get("series")) + + assert error.value is final_error + assert len(sent_requests) == 6 + assert sleep_mock.await_args_list == [call(delay) for delay in [0.5, 1.0, 2.0, 4.0, 8.0]] + + @pytest.mark.parametrize("method", ["POST", "PUT", "DELETE"]) + @pytest.mark.parametrize("status_code", [429, 503]) + def test_mutation_not_retried( + self, + client: Mangacollec, + responses: list[httpx.Response | httpx.RequestError], + sent_requests: list[httpx.Request], + sleep_mock: AsyncMock, + method: str, + status_code: int, + ) -> None: + """Preserve immediate errors for resource mutations despite Retry-After.""" + responses.append(httpx.Response(status_code, headers={"Retry-After": "120"})) + + with pytest.raises(httpx.HTTPStatusError): + asyncio.run(client._request(method, "possessions", data={})) + + assert len(sent_requests) == 1 + sleep_mock.assert_not_awaited() + + @pytest.mark.parametrize("status_code", [400, 401, 403, 404]) + def test_other_client_errors_not_retried( + self, + client: Mangacollec, + responses: list[httpx.Response | httpx.RequestError], + sent_requests: list[httpx.Request], + sleep_mock: AsyncMock, + status_code: int, + ) -> None: + """Do not expand retries to other client errors carrying the header.""" + responses.append(httpx.Response(status_code, headers={"Retry-After": "120"})) + + with pytest.raises(httpx.HTTPStatusError): + asyncio.run(client.get("series")) + + assert len(sent_requests) == 1 + sleep_mock.assert_not_awaited() + + @pytest.mark.parametrize("status_code", [429, 503]) + def test_oauth_retry( + self, + client: Mangacollec, + responses: list[httpx.Response | httpx.RequestError], + sent_requests: list[httpx.Request], + sleep_mock: AsyncMock, + status_code: int, + ) -> None: + """Apply the server delay to retriable OAuth POST requests.""" + responses.extend([ + httpx.Response(status_code, headers={"Retry-After": "120"}), + httpx.Response(200, json={ + "access_token": "new-test-token", + "token_type": "Bearer", + "expires_in": 86400, + "created_at": 0, + }), + ]) + + asyncio.run(client.login()) + + assert client.authorization_header == "Bearer new-test-token" + assert len(sent_requests) == 2 + assert all(request.method == "POST" for request in sent_requests) + assert all(request.url.path == "/oauth/token" for request in sent_requests) + sleep_mock.assert_awaited_once_with(126.0) + + @pytest.mark.parametrize("status_code", [200, 204]) + def test_success_does_not_wait( + self, + client: Mangacollec, + responses: list[httpx.Response | httpx.RequestError], + sleep_mock: AsyncMock, + status_code: int, + ) -> None: + """Return successful responses immediately even if they have Retry-After.""" + responses.append(httpx.Response(status_code, headers={"Retry-After": "120"}, json={})) + + assert asyncio.run(client.get("series")) == {} + sleep_mock.assert_not_awaited() + + def test_logs_scheduled_wait( + self, + client: Mangacollec, + responses: list[httpx.Response | httpx.RequestError], + sleep_mock: AsyncMock, + caplog: pytest.LogCaptureFixture, + ) -> None: + """Log the retry number and the actual wait duration.""" + responses.extend([ + httpx.Response(429, headers={"Retry-After": "120"}), + httpx.Response(200, json={}), + ]) + + asyncio.run(client.get("series")) + + assert any("reprise 1/5 dans 126.0 secondes" in message for message in caplog.messages) + sleep_mock.assert_awaited_once_with(126.0) diff --git a/libs/mangacollec/tests/test_retry_policy.py b/libs/mangacollec/tests/test_retry_policy.py new file mode 100644 index 0000000..5e95fd0 --- /dev/null +++ b/libs/mangacollec/tests/test_retry_policy.py @@ -0,0 +1,55 @@ +"""Tests for server-directed waits and exponential fallback delays.""" + +import pytest +from mangacollec.retry_policy import RetryPolicy + + +class TestDelayForRetry: + """Verify the delay policy independently of HTTP requests.""" + + @pytest.mark.parametrize("retry_count", [1, 5]) + @pytest.mark.parametrize("retry_after, expected", [ + ("120", 126.0), + ("8", 8.4), + ("1", 1.05), + ("0", 0.0), + (" 120 ", 126.0), + ("00120", 126.0), + ]) + def test_server_delay(self, retry_count: int, retry_after: str, expected: float) -> None: + """Use server seconds plus five percent, without the fallback's cap.""" + delay = RetryPolicy.delay_for_retry(retry_count, retry_after=retry_after) + + assert delay == pytest.approx(expected) + + @pytest.mark.parametrize("retry_after", [ + None, "", " ", "-1", "+1", "1.5", "1e2", "invalid", "NaN", "Infinity", + "١٢٠", "120, 60", "Fri, 31 Dec 1999 23:59:59 GMT", + ]) + def test_invalid_header(self, retry_after: str | None) -> None: + """Fall back for missing headers and values outside the seconds format.""" + assert RetryPolicy.delay_for_retry(1, retry_after=retry_after) == 0.5 + assert RetryPolicy.delay_for_retry(5, retry_after=retry_after) == 8.0 + + @pytest.mark.parametrize("retry_after", [ + pytest.param("9" * 400, id="non-finite-seconds"), + pytest.param("179" + "0" * 306, id="non-finite-margin"), + pytest.param("9" * 5000, id="oversized-number"), + ]) + def test_non_finite_delay(self, retry_after: str) -> None: + """Avoid passing infinite delays to the asynchronous wait.""" + assert RetryPolicy.delay_for_retry(1, retry_after=retry_after) == 0.5 + + @pytest.mark.parametrize("retry_count, expected", [ + (1, 0.5), (2, 1.0), (3, 2.0), (4, 4.0), (5, 8.0), (6, 8.0), + ]) + def test_exponential_fallback(self, retry_count: int, expected: float) -> None: + """Keep the existing exponential delays and maximum intact.""" + assert RetryPolicy.delay_for_retry(retry_count) == expected + + @pytest.mark.parametrize("retry_count", [0, -1]) + @pytest.mark.parametrize("retry_after", [None, "120"]) + def test_invalid_retry_count(self, retry_count: int, retry_after: str | None) -> None: + """Validate the retry number even when the server supplies a delay.""" + with pytest.raises(ValueError): + RetryPolicy.delay_for_retry(retry_count, retry_after=retry_after)