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