init mangacollec

This commit is contained in:
shooter-dev committed 2026-10-07 00:38:38 +02:00
commit 2581d4f1fc
106 files changed
+2670

No files matched your search

+14
View File
@@ -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",
]
@@ -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)
@@ -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)
@@ -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)
@@ -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