baobab-altered-api 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- baobab_altered_api/__init__.py +33 -0
- baobab_altered_api/auth/__init__.py +5 -0
- baobab_altered_api/auth/altered_auth_factory.py +36 -0
- baobab_altered_api/clients/__init__.py +17 -0
- baobab_altered_api/clients/altered_client.py +137 -0
- baobab_altered_api/clients/api_client_config_alignment.py +35 -0
- baobab_altered_api/clients/async_altered_client.py +141 -0
- baobab_altered_api/clients/transport_port_validator.py +39 -0
- baobab_altered_api/config/__init__.py +17 -0
- baobab_altered_api/config/altered_api_client_config_builder.py +81 -0
- baobab_altered_api/config/altered_api_config.py +193 -0
- baobab_altered_api/config/altered_locale.py +27 -0
- baobab_altered_api/config/altered_network_parameter_validator.py +124 -0
- baobab_altered_api/config/env_loader.py +135 -0
- baobab_altered_api/exceptions/__init__.py +41 -0
- baobab_altered_api/exceptions/altered_api_error.py +44 -0
- baobab_altered_api/exceptions/altered_auth_error.py +29 -0
- baobab_altered_api/exceptions/altered_config_error.py +8 -0
- baobab_altered_api/exceptions/altered_forbidden_error.py +26 -0
- baobab_altered_api/exceptions/altered_http_error.py +13 -0
- baobab_altered_api/exceptions/altered_json_decode_error.py +31 -0
- baobab_altered_api/exceptions/altered_mapping_error.py +29 -0
- baobab_altered_api/exceptions/altered_network_error.py +26 -0
- baobab_altered_api/exceptions/altered_not_found_error.py +26 -0
- baobab_altered_api/exceptions/altered_open_data_download_error.py +26 -0
- baobab_altered_api/exceptions/altered_rate_limit_error.py +38 -0
- baobab_altered_api/exceptions/altered_service_unavailable_error.py +30 -0
- baobab_altered_api/exceptions/altered_timeout_error.py +26 -0
- baobab_altered_api/exceptions/altered_transport_error.py +29 -0
- baobab_altered_api/exceptions/error_mapper.py +136 -0
- baobab_altered_api/internal/__init__.py +1 -0
- baobab_altered_api/internal/altered_api_logger.py +65 -0
- baobab_altered_api/internal/altered_log_redactor.py +144 -0
- baobab_altered_api/internal/open_data_file_io.py +61 -0
- baobab_altered_api/internal/query_params.py +144 -0
- baobab_altered_api/mappers/__init__.py +13 -0
- baobab_altered_api/mappers/altered_cards_json_mapper.py +94 -0
- baobab_altered_api/mappers/altered_common_models_json_mapper.py +321 -0
- baobab_altered_api/mappers/altered_deck_json_mapper.py +62 -0
- baobab_altered_api/mappers/altered_marketplace_offer_json_mapper.py +60 -0
- baobab_altered_api/mappers/altered_open_data_json_mapper.py +132 -0
- baobab_altered_api/mappers/altered_pagination_json_mapper.py +92 -0
- baobab_altered_api/mappers/altered_referentials_json_mapper.py +89 -0
- baobab_altered_api/models/__init__.py +25 -0
- baobab_altered_api/models/altered_card_summary.py +44 -0
- baobab_altered_api/models/altered_card_type_reference.py +16 -0
- baobab_altered_api/models/altered_deck.py +38 -0
- baobab_altered_api/models/altered_faction.py +33 -0
- baobab_altered_api/models/altered_faction_reference.py +18 -0
- baobab_altered_api/models/altered_image.py +29 -0
- baobab_altered_api/models/altered_json_payload.py +19 -0
- baobab_altered_api/models/altered_localized_label.py +29 -0
- baobab_altered_api/models/altered_marketplace_offer.py +29 -0
- baobab_altered_api/models/altered_open_data_card_entry.py +25 -0
- baobab_altered_api/models/altered_open_data_faction_node.py +22 -0
- baobab_altered_api/models/altered_open_data_set_node.py +22 -0
- baobab_altered_api/models/altered_open_data_snapshot.py +37 -0
- baobab_altered_api/models/altered_page.py +29 -0
- baobab_altered_api/models/altered_pagination_params.py +58 -0
- baobab_altered_api/models/altered_rarity.py +29 -0
- baobab_altered_api/models/altered_rarity_reference.py +16 -0
- baobab_altered_api/models/altered_ref.py +27 -0
- baobab_altered_api/models/altered_set.py +33 -0
- baobab_altered_api/models/altered_set_reference.py +25 -0
- baobab_altered_api/py.typed +0 -0
- baobab_altered_api/resources/__init__.py +37 -0
- baobab_altered_api/resources/altered_api_paths.py +33 -0
- baobab_altered_api/resources/async_cards_resource.py +113 -0
- baobab_altered_api/resources/async_decks_resource.py +34 -0
- baobab_altered_api/resources/async_marketplace_resource.py +21 -0
- baobab_altered_api/resources/async_open_data_downloader.py +78 -0
- baobab_altered_api/resources/async_open_data_resource.py +86 -0
- baobab_altered_api/resources/async_referentials_resource.py +62 -0
- baobab_altered_api/resources/card_search_filters.py +65 -0
- baobab_altered_api/resources/cards_resource.py +146 -0
- baobab_altered_api/resources/decks_resource.py +34 -0
- baobab_altered_api/resources/marketplace_resource.py +63 -0
- baobab_altered_api/resources/open_data_access.py +9 -0
- baobab_altered_api/resources/open_data_catalog.py +92 -0
- baobab_altered_api/resources/open_data_downloader.py +99 -0
- baobab_altered_api/resources/open_data_reader.py +37 -0
- baobab_altered_api/resources/open_data_resource.py +89 -0
- baobab_altered_api/resources/referential_list_response_parser.py +43 -0
- baobab_altered_api/resources/referentials_resource.py +62 -0
- baobab_altered_api/transports/__init__.py +13 -0
- baobab_altered_api/transports/altered_async_transport.py +144 -0
- baobab_altered_api/transports/altered_rate_limit_middleware.py +91 -0
- baobab_altered_api/transports/altered_rate_limit_middleware_async.py +88 -0
- baobab_altered_api/transports/altered_sync_transport.py +137 -0
- baobab_altered_api/transports/response_handler.py +92 -0
- baobab_altered_api-1.0.0.dist-info/METADATA +102 -0
- baobab_altered_api-1.0.0.dist-info/RECORD +94 -0
- baobab_altered_api-1.0.0.dist-info/WHEEL +4 -0
- baobab_altered_api-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Point d’entrée public du package ``baobab_altered_api``.
|
|
2
|
+
|
|
3
|
+
L’import de ce module ne doit pas créer de client HTTP, lire la configuration
|
|
4
|
+
depuis l’environnement ni configurer le logging global. Les classes client
|
|
5
|
+
exposées ici ne réalisent aucune requête tant qu’aucune méthode HTTP n’est
|
|
6
|
+
appelée sur ``AlteredClient.http`` ou ``AsyncAlteredClient.http``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from baobab_altered_api.auth import AlteredAuthFactory
|
|
12
|
+
from baobab_altered_api.clients import AlteredClient, AsyncAlteredClient
|
|
13
|
+
from baobab_altered_api.config import (
|
|
14
|
+
AlteredApiConfig,
|
|
15
|
+
AlteredLocale,
|
|
16
|
+
load_config_from_env,
|
|
17
|
+
to_api_client_config,
|
|
18
|
+
)
|
|
19
|
+
from baobab_altered_api.exceptions import AlteredConfigError
|
|
20
|
+
|
|
21
|
+
__version__ = "1.0.0"
|
|
22
|
+
|
|
23
|
+
__all__: tuple[str, ...] = (
|
|
24
|
+
"__version__",
|
|
25
|
+
"AlteredApiConfig",
|
|
26
|
+
"AlteredAuthFactory",
|
|
27
|
+
"AlteredClient",
|
|
28
|
+
"AlteredConfigError",
|
|
29
|
+
"AlteredLocale",
|
|
30
|
+
"AsyncAlteredClient",
|
|
31
|
+
"load_config_from_env",
|
|
32
|
+
"to_api_client_config",
|
|
33
|
+
)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Fabrique d'authentification pour le socle ``baobab-api-call``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from baobab_api_call import ApiKeyAuth, AuthStrategy, BearerAuth
|
|
6
|
+
|
|
7
|
+
from baobab_altered_api.config.altered_api_config import AlteredApiConfig
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AlteredAuthFactory:
|
|
11
|
+
"""Produit une stratégie ``AuthStrategy`` du socle."""
|
|
12
|
+
|
|
13
|
+
def create(self, config: AlteredApiConfig) -> AuthStrategy | None:
|
|
14
|
+
"""Retourne la stratégie d'authentification ou ``None`` si aucune n'est requise.
|
|
15
|
+
|
|
16
|
+
Priorité : ``bearer_token`` puis ``api_key`` (cahier des charges).
|
|
17
|
+
|
|
18
|
+
:param config: Configuration Altered.
|
|
19
|
+
:return: ``BearerAuth``, ``ApiKeyAuth`` ou ``None``.
|
|
20
|
+
"""
|
|
21
|
+
if config.bearer_token is not None:
|
|
22
|
+
return BearerAuth(token=config.bearer_token)
|
|
23
|
+
if config.api_key is not None:
|
|
24
|
+
return ApiKeyAuth(
|
|
25
|
+
api_key=config.api_key,
|
|
26
|
+
header_name=config.api_key_header_name,
|
|
27
|
+
scheme=config.api_key_query_param,
|
|
28
|
+
)
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
def __repr__(self) -> str:
|
|
32
|
+
"""Représentation de débogage sans données sensibles.
|
|
33
|
+
|
|
34
|
+
:return: Identifiant fixe de la fabrique (aucun secret ni état).
|
|
35
|
+
"""
|
|
36
|
+
return "AlteredAuthFactory()"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Clients publics synchrones et asynchrones.
|
|
2
|
+
|
|
3
|
+
Les classes ci-dessous constituent la surface publique de ce sous-package ;
|
|
4
|
+
les modules internes (validateurs, alignement de config) ne sont pas
|
|
5
|
+
ré-exportés ici.
|
|
6
|
+
|
|
7
|
+
Imports supportés :
|
|
8
|
+
|
|
9
|
+
- ``from baobab_altered_api.clients import AlteredClient, AsyncAlteredClient``
|
|
10
|
+
- ``from baobab_altered_api import AlteredClient, AsyncAlteredClient`` (re-export
|
|
11
|
+
racine, voir ``baobab_altered_api.__all__``).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from baobab_altered_api.clients.altered_client import AlteredClient
|
|
15
|
+
from baobab_altered_api.clients.async_altered_client import AsyncAlteredClient
|
|
16
|
+
|
|
17
|
+
__all__: tuple[str, ...] = ("AlteredClient", "AsyncAlteredClient")
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Façade synchrone publique pour les appels HTTP Altered."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from types import TracebackType
|
|
6
|
+
|
|
7
|
+
from baobab_api_call import ApiClientConfig, SyncApiClient
|
|
8
|
+
from baobab_api_call.transports.sync_transport import SyncTransport
|
|
9
|
+
|
|
10
|
+
from baobab_altered_api.clients.api_client_config_alignment import (
|
|
11
|
+
ApiClientConfigAlignment,
|
|
12
|
+
)
|
|
13
|
+
from baobab_altered_api.clients.transport_port_validator import TransportPortValidator
|
|
14
|
+
from baobab_altered_api.config.altered_api_client_config_builder import (
|
|
15
|
+
AlteredApiClientConfigBuilder,
|
|
16
|
+
)
|
|
17
|
+
from baobab_altered_api.config.altered_api_config import AlteredApiConfig
|
|
18
|
+
from baobab_altered_api.resources.cards_resource import CardsResource
|
|
19
|
+
from baobab_altered_api.resources.decks_resource import DecksResource
|
|
20
|
+
from baobab_altered_api.resources.marketplace_resource import MarketplaceResource
|
|
21
|
+
from baobab_altered_api.resources.open_data_resource import OpenDataResource
|
|
22
|
+
from baobab_altered_api.resources.referentials_resource import ReferentialsResource
|
|
23
|
+
from baobab_altered_api.transports.altered_sync_transport import AlteredSyncTransport
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AlteredClient:
|
|
27
|
+
"""Façade synchrone : assemble ``AlteredApiConfig`` et ``SyncApiClient``.
|
|
28
|
+
|
|
29
|
+
Aucune requête réseau n'est effectuée à l'instanciation.
|
|
30
|
+
|
|
31
|
+
:param config: Configuration applicative Altered (immuable) ; sert de
|
|
32
|
+
référence métier et, par défaut, de source pour ``ApiClientConfig``.
|
|
33
|
+
:param transport: Port ``SyncTransport`` avec ``send`` synchrone ; si
|
|
34
|
+
``None``, un
|
|
35
|
+
:class:`~baobab_altered_api.transports.altered_sync_transport.AlteredSyncTransport`
|
|
36
|
+
est créé et détenu par cette façade.
|
|
37
|
+
:param api_client_config: Configuration du socle ``baobab-api-call`` ; si
|
|
38
|
+
fournie, elle remplace celle produite par ``AlteredApiClientConfigBuilder``
|
|
39
|
+
pour le client HTTP. Son ``base_url`` doit coïncider avec
|
|
40
|
+
``config.base_url`` (normalisation par ``rstrip('/')``).
|
|
41
|
+
|
|
42
|
+
**Cycle de vie** : :meth:`close` délègue à :class:`~baobab_api_call.SyncApiClient`,
|
|
43
|
+
qui appelle ``close()`` sur le transport s'il est exposé. Un transport
|
|
44
|
+
**injecté** subit donc aussi ``close()`` : ne pas partager un transport fermé
|
|
45
|
+
entre plusieurs clients sans le recréer. Un transport **créé par défaut**
|
|
46
|
+
est fermé avec la façade.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
config: AlteredApiConfig,
|
|
52
|
+
*,
|
|
53
|
+
transport: SyncTransport | None = None,
|
|
54
|
+
api_client_config: ApiClientConfig | None = None,
|
|
55
|
+
) -> None:
|
|
56
|
+
self._altered_config: AlteredApiConfig = config
|
|
57
|
+
if api_client_config is not None:
|
|
58
|
+
ApiClientConfigAlignment.assert_matching_base_url(config, api_client_config)
|
|
59
|
+
api_config = api_client_config
|
|
60
|
+
else:
|
|
61
|
+
api_config = AlteredApiClientConfigBuilder.build(config)
|
|
62
|
+
self._transport: SyncTransport = (
|
|
63
|
+
AlteredSyncTransport(
|
|
64
|
+
default_timeout_seconds=float(config.timeout_seconds),
|
|
65
|
+
)
|
|
66
|
+
if transport is None
|
|
67
|
+
else transport
|
|
68
|
+
)
|
|
69
|
+
TransportPortValidator.verify_sync_send(self._transport)
|
|
70
|
+
self._client: SyncApiClient = SyncApiClient(api_config, self._transport)
|
|
71
|
+
self._cards: CardsResource | None = None
|
|
72
|
+
self._decks: DecksResource | None = None
|
|
73
|
+
self._referentials: ReferentialsResource | None = None
|
|
74
|
+
self._marketplace: MarketplaceResource | None = None
|
|
75
|
+
self._open_data: OpenDataResource | None = None
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def config(self) -> AlteredApiConfig:
|
|
79
|
+
"""Configuration Altered (lecture seule)."""
|
|
80
|
+
return self._altered_config
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def http(self) -> SyncApiClient:
|
|
84
|
+
"""Client HTTP du socle (accès direct, en attendant les ressources métier)."""
|
|
85
|
+
return self._client
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def cards(self) -> CardsResource:
|
|
89
|
+
"""Ressource cartes (lecture)."""
|
|
90
|
+
if self._cards is None:
|
|
91
|
+
self._cards = CardsResource(self._client)
|
|
92
|
+
return self._cards
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def decks(self) -> DecksResource:
|
|
96
|
+
"""Ressource decks (lecture)."""
|
|
97
|
+
if self._decks is None:
|
|
98
|
+
self._decks = DecksResource(self._client)
|
|
99
|
+
return self._decks
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def referentials(self) -> ReferentialsResource:
|
|
103
|
+
"""Référentiels (sets, factions, etc.)."""
|
|
104
|
+
if self._referentials is None:
|
|
105
|
+
self._referentials = ReferentialsResource(self._client)
|
|
106
|
+
return self._referentials
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def marketplace(self) -> MarketplaceResource:
|
|
110
|
+
"""Marketplace lecture seule."""
|
|
111
|
+
if self._marketplace is None:
|
|
112
|
+
self._marketplace = MarketplaceResource(self._client)
|
|
113
|
+
return self._marketplace
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def open_data(self) -> OpenDataResource:
|
|
117
|
+
"""Open Data : téléchargement explicite et lecture locale."""
|
|
118
|
+
if self._open_data is None:
|
|
119
|
+
self._open_data = OpenDataResource(self._altered_config, self._client)
|
|
120
|
+
return self._open_data
|
|
121
|
+
|
|
122
|
+
def close(self) -> None:
|
|
123
|
+
"""Ferme le client du socle et le transport sous-jacent si applicable."""
|
|
124
|
+
self._client.close()
|
|
125
|
+
|
|
126
|
+
def __enter__(self) -> AlteredClient:
|
|
127
|
+
"""Context manager synchrone : retourne ``self``."""
|
|
128
|
+
return self
|
|
129
|
+
|
|
130
|
+
def __exit__(
|
|
131
|
+
self,
|
|
132
|
+
exc_type: type[BaseException] | None,
|
|
133
|
+
exc_val: BaseException | None,
|
|
134
|
+
exc_tb: TracebackType | None,
|
|
135
|
+
) -> None:
|
|
136
|
+
"""Ferme la façade à la sortie du bloc ``with``."""
|
|
137
|
+
self.close()
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Alignement entre ``AlteredApiConfig`` et ``ApiClientConfig`` du socle."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from baobab_api_call import ApiClientConfig
|
|
6
|
+
|
|
7
|
+
from baobab_altered_api.config.altered_api_config import AlteredApiConfig
|
|
8
|
+
from baobab_altered_api.exceptions.altered_config_error import AlteredConfigError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ApiClientConfigAlignment:
|
|
12
|
+
"""Vérifie la cohérence minimale entre ``AlteredApiConfig`` et ``ApiClientConfig``.
|
|
13
|
+
|
|
14
|
+
Utilisé lorsque l'appelant injecte une configuration du socle via
|
|
15
|
+
``api_client_config`` sur les façades publiques.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def assert_matching_base_url(
|
|
20
|
+
altered: AlteredApiConfig,
|
|
21
|
+
api_cfg: ApiClientConfig,
|
|
22
|
+
) -> None:
|
|
23
|
+
"""Vérifie que les URL de base HTTP concordent après normalisation.
|
|
24
|
+
|
|
25
|
+
:param altered: Configuration applicative Altered (référence métier).
|
|
26
|
+
:param api_cfg: Configuration ``baobab-api-call`` injectée.
|
|
27
|
+
:raises AlteredConfigError: Si ``base_url`` diffère après ``rstrip('/')``.
|
|
28
|
+
"""
|
|
29
|
+
left = altered.base_url.rstrip("/")
|
|
30
|
+
right = api_cfg.base_url.rstrip("/")
|
|
31
|
+
if left != right:
|
|
32
|
+
raise AlteredConfigError(
|
|
33
|
+
"api_client_config.base_url must match config.base_url "
|
|
34
|
+
f"(got {right!r} vs config {left!r})",
|
|
35
|
+
)
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Façade asynchrone publique pour les appels HTTP Altered."""
|
|
2
|
+
|
|
3
|
+
# pylint: disable=duplicate-code
|
|
4
|
+
# Parallèle volontaire avec ``altered_client`` (FEAT-004 / BL-004-002).
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from types import TracebackType
|
|
9
|
+
|
|
10
|
+
from baobab_api_call import ApiClientConfig, AsyncApiClient
|
|
11
|
+
from baobab_api_call.transports.async_transport import AsyncTransport
|
|
12
|
+
|
|
13
|
+
from baobab_altered_api.clients.api_client_config_alignment import (
|
|
14
|
+
ApiClientConfigAlignment,
|
|
15
|
+
)
|
|
16
|
+
from baobab_altered_api.clients.transport_port_validator import TransportPortValidator
|
|
17
|
+
from baobab_altered_api.config.altered_api_client_config_builder import (
|
|
18
|
+
AlteredApiClientConfigBuilder,
|
|
19
|
+
)
|
|
20
|
+
from baobab_altered_api.config.altered_api_config import AlteredApiConfig
|
|
21
|
+
from baobab_altered_api.resources.async_cards_resource import AsyncCardsResource
|
|
22
|
+
from baobab_altered_api.resources.async_decks_resource import AsyncDecksResource
|
|
23
|
+
from baobab_altered_api.resources.async_marketplace_resource import (
|
|
24
|
+
AsyncMarketplaceResource,
|
|
25
|
+
)
|
|
26
|
+
from baobab_altered_api.resources.async_open_data_resource import AsyncOpenDataResource
|
|
27
|
+
from baobab_altered_api.resources.async_referentials_resource import (
|
|
28
|
+
AsyncReferentialsResource,
|
|
29
|
+
)
|
|
30
|
+
from baobab_altered_api.transports.altered_async_transport import AlteredAsyncTransport
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AsyncAlteredClient:
|
|
34
|
+
"""Façade asynchrone : assemble ``AlteredApiConfig`` et ``AsyncApiClient``.
|
|
35
|
+
|
|
36
|
+
Aucune requête réseau n'est effectuée à l'instanciation.
|
|
37
|
+
|
|
38
|
+
:param config: Configuration applicative Altered (immuable) ; sert de
|
|
39
|
+
référence métier et, par défaut, de source pour ``ApiClientConfig``.
|
|
40
|
+
:param transport: Port ``AsyncTransport`` avec ``send`` asynchrone ; si
|
|
41
|
+
``None``, un
|
|
42
|
+
:class:`~baobab_altered_api.transports.altered_async_transport.AlteredAsyncTransport`
|
|
43
|
+
est créé et détenu par cette façade.
|
|
44
|
+
:param api_client_config: Configuration du socle ; si fournie, elle remplace
|
|
45
|
+
celle du builder. Son ``base_url`` doit coïncider avec ``config.base_url``.
|
|
46
|
+
|
|
47
|
+
**Cycle de vie** : :meth:`aclose` délègue à :class:`~baobab_api_call.AsyncApiClient`
|
|
48
|
+
qui appelle ``aclose()`` sur le transport s'il est exposé. Un transport
|
|
49
|
+
**injecté** est fermé par cette chaîne : ne pas réutiliser un transport fermé
|
|
50
|
+
sans le recréer.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
config: AlteredApiConfig,
|
|
56
|
+
*,
|
|
57
|
+
transport: AsyncTransport | None = None,
|
|
58
|
+
api_client_config: ApiClientConfig | None = None,
|
|
59
|
+
) -> None:
|
|
60
|
+
self._altered_config: AlteredApiConfig = config
|
|
61
|
+
if api_client_config is not None:
|
|
62
|
+
ApiClientConfigAlignment.assert_matching_base_url(config, api_client_config)
|
|
63
|
+
api_config = api_client_config
|
|
64
|
+
else:
|
|
65
|
+
api_config = AlteredApiClientConfigBuilder.build(config)
|
|
66
|
+
self._transport: AsyncTransport = (
|
|
67
|
+
AlteredAsyncTransport(
|
|
68
|
+
default_timeout_seconds=float(config.timeout_seconds),
|
|
69
|
+
)
|
|
70
|
+
if transport is None
|
|
71
|
+
else transport
|
|
72
|
+
)
|
|
73
|
+
TransportPortValidator.verify_async_send(self._transport)
|
|
74
|
+
self._client: AsyncApiClient = AsyncApiClient(api_config, self._transport)
|
|
75
|
+
self._cards: AsyncCardsResource | None = None
|
|
76
|
+
self._decks: AsyncDecksResource | None = None
|
|
77
|
+
self._referentials: AsyncReferentialsResource | None = None
|
|
78
|
+
self._marketplace: AsyncMarketplaceResource | None = None
|
|
79
|
+
self._open_data: AsyncOpenDataResource | None = None
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def config(self) -> AlteredApiConfig:
|
|
83
|
+
"""Configuration Altered (lecture seule)."""
|
|
84
|
+
return self._altered_config
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def http(self) -> AsyncApiClient:
|
|
88
|
+
"""Client HTTP async du socle (accès direct, en attendant les ressources)."""
|
|
89
|
+
return self._client
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def cards(self) -> AsyncCardsResource:
|
|
93
|
+
"""Ressource cartes async."""
|
|
94
|
+
if self._cards is None:
|
|
95
|
+
self._cards = AsyncCardsResource(self._client)
|
|
96
|
+
return self._cards
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def decks(self) -> AsyncDecksResource:
|
|
100
|
+
"""Ressource decks async."""
|
|
101
|
+
if self._decks is None:
|
|
102
|
+
self._decks = AsyncDecksResource(self._client)
|
|
103
|
+
return self._decks
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def referentials(self) -> AsyncReferentialsResource:
|
|
107
|
+
"""Référentiels async."""
|
|
108
|
+
if self._referentials is None:
|
|
109
|
+
self._referentials = AsyncReferentialsResource(self._client)
|
|
110
|
+
return self._referentials
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def marketplace(self) -> AsyncMarketplaceResource:
|
|
114
|
+
"""Marketplace async (lecture seule)."""
|
|
115
|
+
if self._marketplace is None:
|
|
116
|
+
self._marketplace = AsyncMarketplaceResource(self._client)
|
|
117
|
+
return self._marketplace
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def open_data(self) -> AsyncOpenDataResource:
|
|
121
|
+
"""Open Data async : téléchargement explicite et lecture locale."""
|
|
122
|
+
if self._open_data is None:
|
|
123
|
+
self._open_data = AsyncOpenDataResource(self._altered_config, self._client)
|
|
124
|
+
return self._open_data
|
|
125
|
+
|
|
126
|
+
async def aclose(self) -> None:
|
|
127
|
+
"""Ferme le client du socle et le transport sous-jacent si applicable."""
|
|
128
|
+
await self._client.aclose()
|
|
129
|
+
|
|
130
|
+
async def __aenter__(self) -> AsyncAlteredClient:
|
|
131
|
+
"""Context manager async : retourne ``self``."""
|
|
132
|
+
return self
|
|
133
|
+
|
|
134
|
+
async def __aexit__(
|
|
135
|
+
self,
|
|
136
|
+
exc_type: type[BaseException] | None,
|
|
137
|
+
exc_val: BaseException | None,
|
|
138
|
+
exc_tb: TracebackType | None,
|
|
139
|
+
) -> None:
|
|
140
|
+
"""Ferme la façade à la sortie du bloc ``async with``."""
|
|
141
|
+
await self.aclose()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Vérifications runtime des ports transport pour les façades clients."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TransportPortValidator:
|
|
10
|
+
"""Rejette les transports dont ``send`` ne correspond pas au mode sync/async.
|
|
11
|
+
|
|
12
|
+
Les protocoles ``SyncTransport`` / ``AsyncTransport`` du socle ne sont pas
|
|
13
|
+
vérifiables statiquement à l'exécution : ces garde-fous évitent les erreurs
|
|
14
|
+
tardives lorsqu'un fake ou un adaptateur incorrect est injecté.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def verify_sync_send(transport: Any) -> None:
|
|
19
|
+
"""Lève si ``send`` est une coroutine (transport async sur client sync)."""
|
|
20
|
+
send = getattr(transport, "send", None)
|
|
21
|
+
if send is None or not callable(send):
|
|
22
|
+
raise TypeError("transport must expose a callable send() method")
|
|
23
|
+
if inspect.iscoroutinefunction(send):
|
|
24
|
+
raise TypeError(
|
|
25
|
+
"This transport uses async send(); use AsyncAlteredClient instead "
|
|
26
|
+
"of AlteredClient."
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def verify_async_send(transport: Any) -> None:
|
|
31
|
+
"""Lève si ``send`` n'est pas une coroutine (transport sync sur async)."""
|
|
32
|
+
send = getattr(transport, "send", None)
|
|
33
|
+
if send is None or not callable(send):
|
|
34
|
+
raise TypeError("transport must expose a callable send() method")
|
|
35
|
+
if not inspect.iscoroutinefunction(send):
|
|
36
|
+
raise TypeError(
|
|
37
|
+
"This transport uses synchronous send(); use AlteredClient instead "
|
|
38
|
+
"of AsyncAlteredClient."
|
|
39
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Configuration Altered et pont vers ``baobab-api-call``."""
|
|
2
|
+
|
|
3
|
+
from baobab_altered_api.config.altered_api_config import AlteredApiConfig
|
|
4
|
+
from baobab_altered_api.config.altered_api_client_config_builder import (
|
|
5
|
+
AlteredApiClientConfigBuilder,
|
|
6
|
+
to_api_client_config,
|
|
7
|
+
)
|
|
8
|
+
from baobab_altered_api.config.altered_locale import AlteredLocale
|
|
9
|
+
from baobab_altered_api.config.env_loader import load_config_from_env
|
|
10
|
+
|
|
11
|
+
__all__ = (
|
|
12
|
+
"AlteredApiClientConfigBuilder",
|
|
13
|
+
"AlteredApiConfig",
|
|
14
|
+
"AlteredLocale",
|
|
15
|
+
"load_config_from_env",
|
|
16
|
+
"to_api_client_config",
|
|
17
|
+
)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Conversion ``AlteredApiConfig`` vers ``ApiClientConfig`` (baobab-api-call)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from baobab_api_call import ApiClientConfig, RetryPolicy
|
|
6
|
+
from baobab_api_call.middleware import Middleware, MiddlewareAsync
|
|
7
|
+
|
|
8
|
+
from baobab_altered_api.config.altered_api_config import AlteredApiConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AlteredApiClientConfigBuilder:
|
|
12
|
+
"""Construit un ``ApiClientConfig`` à partir d'un ``AlteredApiConfig``."""
|
|
13
|
+
|
|
14
|
+
@staticmethod
|
|
15
|
+
def build(config: AlteredApiConfig) -> ApiClientConfig:
|
|
16
|
+
"""Produit la configuration du socle ``baobab-api-call``.
|
|
17
|
+
|
|
18
|
+
Les en-têtes par défaut incluent ``User-Agent``, ``Accept`` et
|
|
19
|
+
``Accept-Language``. ``timeout_seconds`` et la :class:`RetryPolicy`
|
|
20
|
+
(tentatives, backoff initial, jitter) sont alignés sur le socle.
|
|
21
|
+
Si ``rate_limit_per_second`` est défini, des middlewares sync et async
|
|
22
|
+
espacent les envois côté client (pacing local ; voir
|
|
23
|
+
``altered_rate_limit_middleware``). Les réponses ``429`` serveur restent
|
|
24
|
+
traitées par le socle et le mapping d'exceptions Altered.
|
|
25
|
+
|
|
26
|
+
:param config: Source Altered validée.
|
|
27
|
+
:return: Instance immuable du socle.
|
|
28
|
+
"""
|
|
29
|
+
default_headers: dict[str, str] = {
|
|
30
|
+
"User-Agent": config.user_agent,
|
|
31
|
+
"Accept": "application/json",
|
|
32
|
+
"Accept-Language": config.locale.accept_language_value(),
|
|
33
|
+
}
|
|
34
|
+
retry_policy = RetryPolicy(
|
|
35
|
+
max_attempts=int(config.retry_max_attempts),
|
|
36
|
+
backoff_initial=float(config.retry_backoff_seconds),
|
|
37
|
+
jitter=bool(config.retry_jitter),
|
|
38
|
+
)
|
|
39
|
+
# Import local pour éviter un cycle config → auth → config.
|
|
40
|
+
# pylint: disable-next=import-outside-toplevel
|
|
41
|
+
from baobab_altered_api.auth.altered_auth_factory import (
|
|
42
|
+
AlteredAuthFactory,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Import local : middlewares dépendent du transport, pas de l'auth.
|
|
46
|
+
# pylint: disable-next=import-outside-toplevel
|
|
47
|
+
from baobab_altered_api.transports.altered_rate_limit_middleware import (
|
|
48
|
+
AlteredRateLimitMiddleware,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# pylint: disable-next=import-outside-toplevel
|
|
52
|
+
from baobab_altered_api.transports.altered_rate_limit_middleware_async import (
|
|
53
|
+
AlteredRateLimitMiddlewareAsync,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
auth_strategy = AlteredAuthFactory().create(config)
|
|
57
|
+
sync_middlewares: tuple[Middleware, ...] = ()
|
|
58
|
+
async_middlewares: tuple[MiddlewareAsync, ...] = ()
|
|
59
|
+
if config.rate_limit_per_second is not None:
|
|
60
|
+
rps = float(config.rate_limit_per_second)
|
|
61
|
+
sync_middlewares = (AlteredRateLimitMiddleware(max_per_second=rps),)
|
|
62
|
+
async_middlewares = (AlteredRateLimitMiddlewareAsync(max_per_second=rps),)
|
|
63
|
+
|
|
64
|
+
return ApiClientConfig(
|
|
65
|
+
base_url=config.base_url,
|
|
66
|
+
default_headers=default_headers,
|
|
67
|
+
timeout_seconds=float(config.timeout_seconds),
|
|
68
|
+
retry_policy=retry_policy,
|
|
69
|
+
auth_strategy=auth_strategy,
|
|
70
|
+
middlewares=sync_middlewares,
|
|
71
|
+
async_middlewares=async_middlewares,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def to_api_client_config(config: AlteredApiConfig) -> ApiClientConfig:
|
|
76
|
+
"""Convertit une configuration Altered en :class:`ApiClientConfig`.
|
|
77
|
+
|
|
78
|
+
:param config: Configuration applicative Altered.
|
|
79
|
+
:return: Configuration du client HTTP générique.
|
|
80
|
+
"""
|
|
81
|
+
return AlteredApiClientConfigBuilder.build(config)
|