update mangacollec
This commit is contained in:
1 parent
e4865224cd
commit
4f564e2962
64 files changed
+531
-174
No files matched your search
@@ -1 +1,5 @@
|
|||||||
"""Client library for MangaCollec."""
|
"""Client library for MangaCollec."""
|
||||||
|
|
||||||
|
from .mangacollec import Mangacollec
|
||||||
|
|
||||||
|
__all__ = ["Mangacollec"]
|
||||||
@@ -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",
|
|
||||||
]
|
|
||||||
@@ -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",
|
||||||
|
]
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+3
-3
@@ -25,6 +25,6 @@ class VolumeResponse(BaseModel):
|
|||||||
publishers: list[Publisher]
|
publishers: list[Publisher]
|
||||||
series: list[Serie]
|
series: list[Serie]
|
||||||
types: list[SerieType]
|
types: list[SerieType]
|
||||||
box_volumes: list[BoxVolume] | None
|
box_volumes: list[BoxVolume] | None = None
|
||||||
boxes: list[Boxe] | None
|
boxes: list[Boxe] | None = None
|
||||||
box_editions: list[BoxEdition] | None
|
box_editions: list[BoxEdition] | None = None
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from mangacollec.dto.follows.follow_edition_create_request import FollowEditionCreateRequest
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"FollowEditionCreateRequest"
|
||||||
|
]
|
||||||
File renamed without changes.
@@ -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"
|
||||||
|
]
|
||||||
File renamed without changes.
File renamed without changes.
+2
-2
@@ -11,14 +11,14 @@ from mangacollec.value_object import ID
|
|||||||
|
|
||||||
|
|
||||||
class LoanCreateRequest(BaseModel):
|
class LoanCreateRequest(BaseModel):
|
||||||
"""Association entre un emprunteur et une possession à prêter."""
|
"""Association entre un emprunteur et une possessions à prêter."""
|
||||||
|
|
||||||
borrower_id: ID
|
borrower_id: ID
|
||||||
possession_id: ID
|
possession_id: ID
|
||||||
|
|
||||||
|
|
||||||
class LoansCreateRequest(BaseModel):
|
class LoansCreateRequest(BaseModel):
|
||||||
"""Liste des associations emprunteur-possession à créer."""
|
"""Liste des associations emprunteur-possessions à créer."""
|
||||||
|
|
||||||
loans: list[LoanCreateRequest]
|
loans: list[LoanCreateRequest]
|
||||||
|
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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"
|
||||||
|
]
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+1
-1
@@ -11,7 +11,7 @@ from mangacollec.value_object import ID
|
|||||||
|
|
||||||
|
|
||||||
class DeleteBoxPossessionMultipleRequest(BaseModel):
|
class DeleteBoxPossessionMultipleRequest(BaseModel):
|
||||||
"""Box possession à supprimer."""
|
"""Box possessions à supprimer."""
|
||||||
|
|
||||||
id: ID
|
id: ID
|
||||||
|
|
||||||
File renamed without changes.
File renamed without changes.
@@ -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"
|
||||||
|
]
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -3,8 +3,8 @@
|
|||||||
# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / /
|
# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / /
|
||||||
# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ /
|
# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ /
|
||||||
# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/
|
# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/
|
||||||
from mangacollec.dto import AuthorResponse
|
from mangacollec.dto.catalogue import AuthorResponse
|
||||||
from mangacollec.interface.endpoint_interface import IEndpoint
|
from mangacollec.interface import IEndpoint
|
||||||
from mangacollec.value_object import ID
|
from mangacollec.value_object import ID
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ /
|
# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ /
|
||||||
# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/
|
# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/
|
||||||
|
|
||||||
from mangacollec.dto.authors_response import AuthorsResponse
|
from mangacollec.dto.catalogue import AuthorsResponse
|
||||||
from mangacollec.interface.endpoint_interface import IEndpoint
|
from mangacollec.interface import IEndpoint
|
||||||
|
|
||||||
|
|
||||||
class CallAuthorsEndpoint(IEndpoint):
|
class CallAuthorsEndpoint(IEndpoint):
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ /
|
# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ /
|
||||||
# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/
|
# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/
|
||||||
|
|
||||||
from mangacollec.dto import BoxResponse
|
from mangacollec.dto.catalogue import BoxResponse
|
||||||
from mangacollec.interface.endpoint_interface import IEndpoint
|
from mangacollec.interface import IEndpoint
|
||||||
from mangacollec.value_object import ID
|
from mangacollec.value_object import ID
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / /
|
# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / /
|
||||||
# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ /
|
# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ /
|
||||||
# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/
|
# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/
|
||||||
from mangacollec.dto import EditionResponse
|
from mangacollec.dto.catalogue import EditionResponse
|
||||||
from mangacollec.interface.endpoint_interface import IEndpoint
|
from mangacollec.interface import IEndpoint
|
||||||
from mangacollec.value_object import ID
|
from mangacollec.value_object import ID
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / /
|
# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / /
|
||||||
# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ /
|
# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ /
|
||||||
# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/
|
# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/
|
||||||
from mangacollec.dto.kinds_response import KindsResponse
|
from mangacollec.dto.catalogue.kinds_response import KindsResponse
|
||||||
from mangacollec.interface.endpoint_interface import IEndpoint
|
from mangacollec.interface import IEndpoint
|
||||||
|
|
||||||
|
|
||||||
class CallKindsEndpoint(IEndpoint):
|
class CallKindsEndpoint(IEndpoint):
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ /
|
# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ /
|
||||||
# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/
|
# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/
|
||||||
from mangacollec.dto.news_response import NewsResponse
|
from mangacollec.dto.news_response import NewsResponse
|
||||||
from mangacollec.interface.endpoint_interface import IEndpoint
|
from mangacollec.interface import IEndpoint
|
||||||
|
|
||||||
|
|
||||||
class CallNewsEndpoint(IEndpoint):
|
class CallNewsEndpoint(IEndpoint):
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / /
|
# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / /
|
||||||
# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ /
|
# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ /
|
||||||
# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/
|
# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/
|
||||||
from mangacollec.dto import PublisherResponse
|
from mangacollec.dto.catalogue import PublisherResponse
|
||||||
from mangacollec.interface.endpoint_interface import IEndpoint
|
from mangacollec.interface import IEndpoint
|
||||||
from mangacollec.value_object import ID
|
from mangacollec.value_object import ID
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / /
|
# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / /
|
||||||
# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ /
|
# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ /
|
||||||
# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/
|
# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/
|
||||||
from mangacollec.dto.publishers_response import PublishersResponse
|
from mangacollec.dto.catalogue.publishers_response import PublishersResponse
|
||||||
from mangacollec.interface.endpoint_interface import IEndpoint
|
from mangacollec.interface import IEndpoint
|
||||||
|
|
||||||
|
|
||||||
class CallPublishesEndpoint(IEndpoint):
|
class CallPublishesEndpoint(IEndpoint):
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / /
|
# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / /
|
||||||
# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ /
|
# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ /
|
||||||
# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/
|
# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/
|
||||||
from mangacollec.dto import SerieResponse
|
from mangacollec.dto.catalogue import SerieResponse
|
||||||
from mangacollec.interface.endpoint_interface import IEndpoint
|
from mangacollec.interface import IEndpoint
|
||||||
from mangacollec.value_object import ID
|
from mangacollec.value_object import ID
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / /
|
# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / /
|
||||||
# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ /
|
# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ /
|
||||||
# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/
|
# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/
|
||||||
from mangacollec.dto.series_response import SeriesResponse
|
from mangacollec.dto.catalogue.series_response import SeriesResponse
|
||||||
from mangacollec.interface.endpoint_interface import IEndpoint
|
from mangacollec.interface import IEndpoint
|
||||||
|
|
||||||
|
|
||||||
class CallSeriesEndpoint(IEndpoint):
|
class CallSeriesEndpoint(IEndpoint):
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / /
|
# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / /
|
||||||
# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ /
|
# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ /
|
||||||
# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/
|
# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/
|
||||||
from mangacollec.dto.volume_response import VolumeResponse
|
from mangacollec.dto.catalogue.volume_response import VolumeResponse
|
||||||
from mangacollec.interface.endpoint_interface import IEndpoint
|
from mangacollec.interface import IEndpoint
|
||||||
from mangacollec.value_object import ID
|
from mangacollec.value_object import ID
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/
|
# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/
|
||||||
"""Wrapper de l'endpoint de création d'un suivi d'édition."""
|
"""Wrapper de l'endpoint de création d'un suivi d'édition."""
|
||||||
|
|
||||||
from mangacollec.dto import FollowEditionCreateRequest
|
from mangacollec.dto.follows import FollowEditionCreateRequest
|
||||||
from mangacollec.interface.endpoint_interface import IEndpoint
|
from mangacollec.interface import IEndpoint
|
||||||
from mangacollec.model.possessions import VolumeFollowEdition
|
from mangacollec.model.possessions import VolumeFollowEdition
|
||||||
from mangacollec.value_object import ID
|
from mangacollec.value_object import ID
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/
|
# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/
|
||||||
"""Wrapper de suppression d'un suivi d'édition."""
|
"""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
|
from mangacollec.value_object import ID
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
|
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
from mangacollec.dto import BorrowerCreateRequest, BorrowerCreateResponse
|
from mangacollec.dto.loans import BorrowerCreateRequest, BorrowerCreateResponse
|
||||||
from mangacollec.interface.mutation_client_interface import IPostClient
|
from mangacollec.interface import IPostClient
|
||||||
|
|
||||||
|
|
||||||
class CallCreateBorrowerEndpoint:
|
class CallCreateBorrowerEndpoint:
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/
|
# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/
|
||||||
"""Endpoint de création groupée de prêts."""
|
"""Endpoint de création groupée de prêts."""
|
||||||
|
|
||||||
from mangacollec.dto import LoanCreateRequest, LoansCreateRequest, LoansCreateResponse
|
from mangacollec.dto.loans import LoanCreateRequest, LoansCreateRequest, LoansCreateResponse
|
||||||
from mangacollec.interface.mutation_client_interface import IPostClient
|
from mangacollec.interface import IPostClient
|
||||||
|
|
||||||
|
|
||||||
class CallCreateLoansMultipleEndpoint:
|
class CallCreateLoansMultipleEndpoint:
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/
|
# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/
|
||||||
"""Wrapper de suppression d'un emprunteur."""
|
"""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
|
from mangacollec.value_object import ID
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/
|
# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/
|
||||||
"""Wrapper de suppression groupée de prêts."""
|
"""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.exceptions.exceptions import DomainValidationError
|
||||||
from mangacollec.interface.mutation_client_interface import IDeleteClient
|
from mangacollec.interface import IDeleteClient
|
||||||
from mangacollec.value_object import ID
|
from mangacollec.value_object import ID
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -5,13 +5,13 @@
|
|||||||
# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/
|
# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/
|
||||||
"""Endpoint de création groupée de possessions."""
|
"""Endpoint de création groupée de possessions."""
|
||||||
|
|
||||||
from mangacollec.dto import (
|
from mangacollec.dto.possessions import (
|
||||||
CreateBoxesPossessionsMultipleRequest,
|
CreateBoxesPossessionsMultipleRequest,
|
||||||
CreateBoxPossessionMultipleRequest,
|
CreateBoxPossessionMultipleRequest,
|
||||||
CreateBoxPossessionsMultipleResponse,
|
CreateBoxPossessionsMultipleResponse,
|
||||||
)
|
)
|
||||||
from mangacollec.exceptions.exceptions import DomainValidationError
|
from mangacollec.exceptions.exceptions import DomainValidationError
|
||||||
from mangacollec.interface.endpoint_interface import IEndpoint
|
from mangacollec.interface import IEndpoint
|
||||||
from mangacollec.value_object import ID
|
from mangacollec.value_object import ID
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -5,13 +5,13 @@
|
|||||||
# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/
|
# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/
|
||||||
"""Endpoint de création groupée de possessions."""
|
"""Endpoint de création groupée de possessions."""
|
||||||
|
|
||||||
from mangacollec.dto import (
|
from mangacollec.dto.possessions import (
|
||||||
CreateVolumePossessionMultipleRequest,
|
CreateVolumePossessionMultipleRequest,
|
||||||
CreateVolumePossessionsMultipleResponse,
|
CreateVolumePossessionsMultipleResponse,
|
||||||
CreateVolumesPossessionsMultipleRequest,
|
CreateVolumesPossessionsMultipleRequest,
|
||||||
)
|
)
|
||||||
from mangacollec.exceptions.exceptions import DomainValidationError
|
from mangacollec.exceptions.exceptions import DomainValidationError
|
||||||
from mangacollec.interface.endpoint_interface import IEndpoint
|
from mangacollec.interface import IEndpoint
|
||||||
from mangacollec.value_object import ID
|
from mangacollec.value_object import ID
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -5,14 +5,14 @@
|
|||||||
# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/
|
# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/
|
||||||
"""Endpoint de suppression groupée de possessions de boxes."""
|
"""Endpoint de suppression groupée de possessions de boxes."""
|
||||||
|
|
||||||
from mangacollec.dto import (
|
from mangacollec.dto.possessions import (
|
||||||
DeleteBoxPossessionMultipleRequest,
|
DeleteBoxPossessionMultipleRequest,
|
||||||
DeleteBoxPossessionsMultipleRequest,
|
DeleteBoxPossessionsMultipleRequest,
|
||||||
DeleteBoxPossessionsMultipleResponse,
|
DeleteBoxPossessionsMultipleResponse,
|
||||||
DeletePossessionMultipleRequest,
|
DeletePossessionMultipleRequest,
|
||||||
)
|
)
|
||||||
from mangacollec.exceptions.exceptions import DomainValidationError
|
from mangacollec.exceptions.exceptions import DomainValidationError
|
||||||
from mangacollec.interface.endpoint_interface import IEndpoint
|
from mangacollec.interface import IEndpoint
|
||||||
from mangacollec.value_object import ID
|
from mangacollec.value_object import ID
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -7,11 +7,11 @@
|
|||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from mangacollec.dto import (
|
from mangacollec.dto.possessions import (
|
||||||
DeleteVolumePossessionsMultipleResponse,
|
DeleteVolumePossessionsMultipleResponse,
|
||||||
)
|
)
|
||||||
from mangacollec.exceptions.exceptions import DomainValidationError
|
from mangacollec.exceptions.exceptions import DomainValidationError
|
||||||
from mangacollec.interface.endpoint_interface import IEndpoint
|
from mangacollec.interface import IEndpoint
|
||||||
from mangacollec.value_object import ID
|
from mangacollec.value_object import ID
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,10 +7,10 @@
|
|||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from mangacollec.dto import ReadCreateRequest, ReadsCreateRequest, ReadsCreateResponse
|
from mangacollec.dto.reads import ReadCreateRequest, ReadsCreateRequest, ReadsCreateResponse
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from mangacollec.interface.mutation_client_interface import IPostClient
|
from mangacollec.interface import IPostClient
|
||||||
from mangacollec.value_object import ID
|
from mangacollec.value_object import ID
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/
|
# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/
|
||||||
"""Wrapper de suppression groupée de lectures."""
|
"""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.exceptions.exceptions import DomainValidationError
|
||||||
from mangacollec.interface.mutation_client_interface import IDeleteClient
|
from mangacollec.interface import IDeleteClient
|
||||||
from mangacollec.value_object import ID
|
from mangacollec.value_object import ID
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ class Mangacollec:
|
|||||||
_AUTHENTICATION_PATH: str = "oauth/token"
|
_AUTHENTICATION_PATH: str = "oauth/token"
|
||||||
_DEFAULT_REQUEST_TIMEOUT: float = 30.0
|
_DEFAULT_REQUEST_TIMEOUT: float = 30.0
|
||||||
_TOKEN_SAFETY_MARGIN: int = 30 * 60
|
_TOKEN_SAFETY_MARGIN: int = 30 * 60
|
||||||
|
_RATE_LIMIT_STATUS: int = 429
|
||||||
_SERVER_ERROR_MIN: int = 500
|
_SERVER_ERROR_MIN: int = 500
|
||||||
_SERVER_ERROR_MAX: int = 600
|
_SERVER_ERROR_MAX: int = 600
|
||||||
_NO_CONTENT_STATUS: int = 204
|
_NO_CONTENT_STATUS: int = 204
|
||||||
@@ -37,17 +38,21 @@ class Mangacollec:
|
|||||||
_oauth_client: httpx.AsyncClient
|
_oauth_client: httpx.AsyncClient
|
||||||
_is_auth: bool
|
_is_auth: bool
|
||||||
|
|
||||||
_MANGACOLLEC_CLIENT_ID: str
|
_client_id: str
|
||||||
_MANGACOLLEC_CLIENT_SECRET: str
|
_client_secret: str
|
||||||
|
|
||||||
def __init__(self, proxy: str | None = None) -> None:
|
def __init__(self, client_id: str, client_secret: str, proxy: str | None = None) -> None:
|
||||||
"""Configure HTTP clients with environment-provided credentials.
|
"""Configure HTTP clients with explicitly provided credentials.
|
||||||
|
|
||||||
Les requêtes vers les ressources peuvent passer par ``proxy``. Les échanges
|
Les requêtes vers les ressources peuvent passer par ``proxy``. Les échanges
|
||||||
OAuth restent toujours directs afin de ne pas transmettre les identifiants
|
OAuth restent toujours directs afin de ne pas transmettre les identifiants
|
||||||
de l'application à un proxy tiers.
|
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 = {
|
self._headers = {
|
||||||
"Accept": "application/json",
|
"Accept": "application/json",
|
||||||
@@ -104,19 +109,6 @@ class Mangacollec:
|
|||||||
|
|
||||||
return str(proxy_url)
|
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
|
@classmethod
|
||||||
def _request_timeout_from_environment(cls) -> float:
|
def _request_timeout_from_environment(cls) -> float:
|
||||||
"""Read a positive finite HTTP timeout from the local environment."""
|
"""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
|
retryable = method.upper() == "GET" or url == self._AUTHENTICATION_PATH
|
||||||
request_client = self._client if is_token_required else self._oauth_client
|
request_client = self._client if is_token_required else self._oauth_client
|
||||||
for retry_count in range(RetryPolicy.max_retries + 1):
|
for retry_count in range(RetryPolicy.max_retries + 1):
|
||||||
|
retry_after: str | None = None
|
||||||
try:
|
try:
|
||||||
response = await request_client.request(method=method, url=url, params=param, json=data)
|
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)
|
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:
|
if not retryable or retry_count == RetryPolicy.max_retries:
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
retry_after = response.headers.get("Retry-After")
|
||||||
else:
|
else:
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
if response.status_code == self._NO_CONTENT_STATUS:
|
if response.status_code == self._NO_CONTENT_STATUS:
|
||||||
@@ -194,7 +190,16 @@ class Mangacollec:
|
|||||||
if not retryable or retry_count == RetryPolicy.max_retries:
|
if not retryable or retry_count == RetryPolicy.max_retries:
|
||||||
raise
|
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.")
|
raise RemoteRequestError("La requête a échoué après cinq reprises.")
|
||||||
|
|
||||||
@@ -229,8 +234,8 @@ class Mangacollec:
|
|||||||
async def login(self) -> None:
|
async def login(self) -> None:
|
||||||
"""Obtenir et installer un token OAuth avec les identifiants du client."""
|
"""Obtenir et installer un token OAuth avec les identifiants du client."""
|
||||||
credential = Credential(
|
credential = Credential(
|
||||||
client_id=self._MANGACOLLEC_CLIENT_ID,
|
client_id=self._client_id,
|
||||||
client_secret=self._MANGACOLLEC_CLIENT_SECRET,
|
client_secret=self._client_secret,
|
||||||
grant_type="client_credentials",
|
grant_type="client_credentials",
|
||||||
)
|
)
|
||||||
await self._authenticate(credential)
|
await self._authenticate(credential)
|
||||||
@@ -238,8 +243,8 @@ class Mangacollec:
|
|||||||
async def login_password(self, username: str, password: str) -> None:
|
async def login_password(self, username: str, password: str) -> None:
|
||||||
"""Obtenir et installer un token OAuth avec un compte utilisateur."""
|
"""Obtenir et installer un token OAuth avec un compte utilisateur."""
|
||||||
credential = Credential(
|
credential = Credential(
|
||||||
client_id=self._MANGACOLLEC_CLIENT_ID,
|
client_id=self._client_id,
|
||||||
client_secret=self._MANGACOLLEC_CLIENT_SECRET,
|
client_secret=self._client_secret,
|
||||||
grant_type="password",
|
grant_type="password",
|
||||||
username=username,
|
username=username,
|
||||||
password=password,
|
password=password,
|
||||||
@@ -310,8 +315,8 @@ class Mangacollec:
|
|||||||
logger.info("Renouvellement du token MangaCollec en cours.")
|
logger.info("Renouvellement du token MangaCollec en cours.")
|
||||||
previous_refresh_token = self._token.refresh_token
|
previous_refresh_token = self._token.refresh_token
|
||||||
credential = Credential(
|
credential = Credential(
|
||||||
client_id=self._MANGACOLLEC_CLIENT_ID,
|
client_id=self._client_id,
|
||||||
client_secret=self._MANGACOLLEC_CLIENT_SECRET,
|
client_secret=self._client_secret,
|
||||||
grant_type="refresh_token",
|
grant_type="refresh_token",
|
||||||
refresh_token=previous_refresh_token,
|
refresh_token=previous_refresh_token,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
from datetime import datetime
|
||||||
|
|
||||||
|
|||||||
@@ -4,23 +4,33 @@
|
|||||||
# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ /
|
# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ /
|
||||||
# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/
|
# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/
|
||||||
|
|
||||||
"""Fixed retry policy shared by MangaCollec transports."""
|
"""Retry delays shared by MangaCollec transports."""
|
||||||
|
|
||||||
|
import math
|
||||||
from typing import ClassVar
|
from typing import ClassVar
|
||||||
|
|
||||||
|
|
||||||
class RetryPolicy:
|
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
|
max_retries: ClassVar[int] = 5
|
||||||
initial_delay_seconds: ClassVar[float] = 0.5
|
initial_delay_seconds: ClassVar[float] = 0.5
|
||||||
maximum_delay_seconds: ClassVar[float] = 8.0
|
maximum_delay_seconds: ClassVar[float] = 8.0
|
||||||
|
retry_after_multiplier: ClassVar[float] = 1.05
|
||||||
|
|
||||||
@classmethod
|
@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."""
|
"""Return the delay for a scheduled retry, where the first retry is one."""
|
||||||
if retry_count < 1:
|
if retry_count < 1:
|
||||||
raise ValueError("Le numéro de reprise doit être supérieur ou égal à un.")
|
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)
|
delay_seconds: float = cls.initial_delay_seconds * 2 ** (retry_count - 1)
|
||||||
if delay_seconds >= cls.maximum_delay_seconds:
|
if delay_seconds >= cls.maximum_delay_seconds:
|
||||||
return cls.maximum_delay_seconds
|
return cls.maximum_delay_seconds
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
Reference in new issue
Block a user