ffbb-data-client 2.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.
Files changed (207) hide show
  1. ffbb_api_client_v3/__init__.py +25 -0
  2. ffbb_data_client/__init__.py +175 -0
  3. ffbb_data_client/clients/__init__.py +13 -0
  4. ffbb_data_client/clients/api_ffbb_app_client.py +2475 -0
  5. ffbb_data_client/clients/ffbb_data_client.py +2789 -0
  6. ffbb_data_client/clients/meilisearch_client.py +218 -0
  7. ffbb_data_client/clients/meilisearch_ffbb_client.py +647 -0
  8. ffbb_data_client/config.py +153 -0
  9. ffbb_data_client/data/__init__.py +25 -0
  10. ffbb_data_client/data/collections.json +1364 -0
  11. ffbb_data_client/data/endpoint_discovery.json +1875 -0
  12. ffbb_data_client/data/indexes.json +501 -0
  13. ffbb_data_client/data/openapi.json +35713 -0
  14. ffbb_data_client/data/openapi_full.json +37622 -0
  15. ffbb_data_client/helpers/__init__.py +27 -0
  16. ffbb_data_client/helpers/http_requests_helper.py +73 -0
  17. ffbb_data_client/helpers/http_requests_utils.py +502 -0
  18. ffbb_data_client/helpers/meilisearch_client_extension.py +153 -0
  19. ffbb_data_client/helpers/multi_search_query_helper.py +35 -0
  20. ffbb_data_client/models/__init__.py +241 -0
  21. ffbb_data_client/models/affiche.py +45 -0
  22. ffbb_data_client/models/cartographie.py +82 -0
  23. ffbb_data_client/models/categorie.py +55 -0
  24. ffbb_data_client/models/categorie_type.py +42 -0
  25. ffbb_data_client/models/clock.py +38 -0
  26. ffbb_data_client/models/club_contacts.py +77 -0
  27. ffbb_data_client/models/code.py +7 -0
  28. ffbb_data_client/models/commune.py +66 -0
  29. ffbb_data_client/models/competition_fields.py +309 -0
  30. ffbb_data_client/models/competition_id.py +116 -0
  31. ffbb_data_client/models/competition_id_categorie.py +31 -0
  32. ffbb_data_client/models/competition_id_sexe.py +31 -0
  33. ffbb_data_client/models/competition_id_type_competition.py +27 -0
  34. ffbb_data_client/models/competition_id_type_competition_generique.py +24 -0
  35. ffbb_data_client/models/competition_origine.py +69 -0
  36. ffbb_data_client/models/competition_origine_categorie.py +23 -0
  37. ffbb_data_client/models/competition_origine_type_competition.py +14 -0
  38. ffbb_data_client/models/competition_origine_type_competition_generique.py +24 -0
  39. ffbb_data_client/models/competition_type.py +6 -0
  40. ffbb_data_client/models/competitions_facet_distribution.py +65 -0
  41. ffbb_data_client/models/competitions_facet_stats.py +14 -0
  42. ffbb_data_client/models/competitions_hit.py +232 -0
  43. ffbb_data_client/models/competitions_multi_search_query.py +40 -0
  44. ffbb_data_client/models/competitions_query.py +11 -0
  45. ffbb_data_client/models/configuration_models.py +5 -0
  46. ffbb_data_client/models/contact_info.py +18 -0
  47. ffbb_data_client/models/content_multi_search_query.py +93 -0
  48. ffbb_data_client/models/coordonnees.py +27 -0
  49. ffbb_data_client/models/coordonnees_type.py +5 -0
  50. ffbb_data_client/models/document_flyer.py +205 -0
  51. ffbb_data_client/models/document_flyer_type.py +6 -0
  52. ffbb_data_client/models/engagement_contacts.py +97 -0
  53. ffbb_data_client/models/engagements_facet_distribution.py +59 -0
  54. ffbb_data_client/models/engagements_facet_stats.py +14 -0
  55. ffbb_data_client/models/engagements_hit.py +192 -0
  56. ffbb_data_client/models/engagements_multi_search_query.py +41 -0
  57. ffbb_data_client/models/etat.py +6 -0
  58. ffbb_data_client/models/external_competition_id.py +42 -0
  59. ffbb_data_client/models/external_id.py +72 -0
  60. ffbb_data_client/models/facet_distribution.py +13 -0
  61. ffbb_data_client/models/facet_stats.py +13 -0
  62. ffbb_data_client/models/field_set.py +10 -0
  63. ffbb_data_client/models/folder.py +35 -0
  64. ffbb_data_client/models/formation_session.py +60 -0
  65. ffbb_data_client/models/formations_facet_distribution.py +61 -0
  66. ffbb_data_client/models/formations_facet_stats.py +14 -0
  67. ffbb_data_client/models/formations_hit.py +277 -0
  68. ffbb_data_client/models/formations_multi_search_query.py +41 -0
  69. ffbb_data_client/models/game_stats_model.py +57 -0
  70. ffbb_data_client/models/game_stats_models.py +5 -0
  71. ffbb_data_client/models/generic_search.py +92 -0
  72. ffbb_data_client/models/geo.py +27 -0
  73. ffbb_data_client/models/geo_sort_order.py +6 -0
  74. ffbb_data_client/models/get_commune_response.py +18 -0
  75. ffbb_data_client/models/get_competition_response.py +523 -0
  76. ffbb_data_client/models/get_configuration_response.py +45 -0
  77. ffbb_data_client/models/get_engagement_response.py +23 -0
  78. ffbb_data_client/models/get_entraineur_response.py +18 -0
  79. ffbb_data_client/models/get_formation_response.py +28 -0
  80. ffbb_data_client/models/get_officiel_response.py +18 -0
  81. ffbb_data_client/models/get_organisme_response.py +476 -0
  82. ffbb_data_client/models/get_poule_response.py +68 -0
  83. ffbb_data_client/models/get_pratique_response.py +18 -0
  84. ffbb_data_client/models/get_rencontre_response.py +93 -0
  85. ffbb_data_client/models/get_saisons_response.py +56 -0
  86. ffbb_data_client/models/get_salle_response.py +20 -0
  87. ffbb_data_client/models/get_terrain_response.py +16 -0
  88. ffbb_data_client/models/get_tournoi_response.py +16 -0
  89. ffbb_data_client/models/gradient_color.py +27 -0
  90. ffbb_data_client/models/hit.py +16 -0
  91. ffbb_data_client/models/id_engagement_equipe.py +32 -0
  92. ffbb_data_client/models/id_organisme_equipe.py +51 -0
  93. ffbb_data_client/models/id_organisme_equipe1_logo.py +28 -0
  94. ffbb_data_client/models/id_poule.py +27 -0
  95. ffbb_data_client/models/jour.py +11 -0
  96. ffbb_data_client/models/label.py +15 -0
  97. ffbb_data_client/models/labellisation.py +30 -0
  98. ffbb_data_client/models/live.py +192 -0
  99. ffbb_data_client/models/lives.py +6 -0
  100. ffbb_data_client/models/logo.py +28 -0
  101. ffbb_data_client/models/multi_search_queries.py +24 -0
  102. ffbb_data_client/models/multi_search_query.py +96 -0
  103. ffbb_data_client/models/multi_search_result_competitions.py +14 -0
  104. ffbb_data_client/models/multi_search_result_engagements.py +14 -0
  105. ffbb_data_client/models/multi_search_result_formations.py +12 -0
  106. ffbb_data_client/models/multi_search_result_organismes.py +12 -0
  107. ffbb_data_client/models/multi_search_result_pratiques.py +12 -0
  108. ffbb_data_client/models/multi_search_result_rencontres.py +12 -0
  109. ffbb_data_client/models/multi_search_result_salles.py +12 -0
  110. ffbb_data_client/models/multi_search_result_terrains.py +12 -0
  111. ffbb_data_client/models/multi_search_result_tournois.py +12 -0
  112. ffbb_data_client/models/multi_search_results.py +103 -0
  113. ffbb_data_client/models/multi_search_results_class.py +96 -0
  114. ffbb_data_client/models/nature_sol.py +57 -0
  115. ffbb_data_client/models/niveau.py +10 -0
  116. ffbb_data_client/models/niveau_class.py +27 -0
  117. ffbb_data_client/models/niveau_extractor.py +214 -0
  118. ffbb_data_client/models/niveau_info.py +64 -0
  119. ffbb_data_client/models/niveau_models.py +14 -0
  120. ffbb_data_client/models/niveau_type.py +10 -0
  121. ffbb_data_client/models/objectif.py +7 -0
  122. ffbb_data_client/models/organisateur.py +197 -0
  123. ffbb_data_client/models/organisateur_type.py +6 -0
  124. ffbb_data_client/models/organisme_fields.py +327 -0
  125. ffbb_data_client/models/organisme_id_pere.py +177 -0
  126. ffbb_data_client/models/organismes_facet_distribution.py +46 -0
  127. ffbb_data_client/models/organismes_facet_stats.py +14 -0
  128. ffbb_data_client/models/organismes_hit.py +196 -0
  129. ffbb_data_client/models/organismes_multi_search_query.py +41 -0
  130. ffbb_data_client/models/organismes_query.py +8 -0
  131. ffbb_data_client/models/phase_code.py +23 -0
  132. ffbb_data_client/models/poule.py +35 -0
  133. ffbb_data_client/models/poule_fields.py +261 -0
  134. ffbb_data_client/models/poule_rencontre_item_model.py +69 -0
  135. ffbb_data_client/models/poules_models.py +6 -0
  136. ffbb_data_client/models/poules_query.py +9 -0
  137. ffbb_data_client/models/pratique.py +7 -0
  138. ffbb_data_client/models/pratiques_facet_distribution.py +29 -0
  139. ffbb_data_client/models/pratiques_facet_stats.py +14 -0
  140. ffbb_data_client/models/pratiques_hit.py +310 -0
  141. ffbb_data_client/models/pratiques_hit_type.py +9 -0
  142. ffbb_data_client/models/pratiques_multi_search_query.py +41 -0
  143. ffbb_data_client/models/pratiques_type_class.py +45 -0
  144. ffbb_data_client/models/publication_internet.py +6 -0
  145. ffbb_data_client/models/purple_logo.py +24 -0
  146. ffbb_data_client/models/query_fields_manager.py +75 -0
  147. ffbb_data_client/models/ranking_engagement.py +41 -0
  148. ffbb_data_client/models/rankings_models.py +6 -0
  149. ffbb_data_client/models/rencontres_engagement.py +23 -0
  150. ffbb_data_client/models/rencontres_facet_distribution.py +65 -0
  151. ffbb_data_client/models/rencontres_facet_stats.py +14 -0
  152. ffbb_data_client/models/rencontres_hit.py +271 -0
  153. ffbb_data_client/models/rencontres_multi_search_query.py +41 -0
  154. ffbb_data_client/models/saison.py +23 -0
  155. ffbb_data_client/models/saison_fields.py +36 -0
  156. ffbb_data_client/models/saisons_models.py +6 -0
  157. ffbb_data_client/models/saisons_query.py +9 -0
  158. ffbb_data_client/models/salle.py +56 -0
  159. ffbb_data_client/models/salles_facet_distribution.py +14 -0
  160. ffbb_data_client/models/salles_facet_stats.py +14 -0
  161. ffbb_data_client/models/salles_hit.py +153 -0
  162. ffbb_data_client/models/salles_multi_search_query.py +40 -0
  163. ffbb_data_client/models/sexe.py +9 -0
  164. ffbb_data_client/models/sexe_class.py +31 -0
  165. ffbb_data_client/models/source.py +5 -0
  166. ffbb_data_client/models/status.py +5 -0
  167. ffbb_data_client/models/team_engagement.py +56 -0
  168. ffbb_data_client/models/team_ranking.py +108 -0
  169. ffbb_data_client/models/terrains_categorie_championnat_3x3_libelle.py +5 -0
  170. ffbb_data_client/models/terrains_facet_distribution.py +41 -0
  171. ffbb_data_client/models/terrains_facet_stats.py +14 -0
  172. ffbb_data_client/models/terrains_hit.py +223 -0
  173. ffbb_data_client/models/terrains_multi_search_query.py +42 -0
  174. ffbb_data_client/models/terrains_name.py +5 -0
  175. ffbb_data_client/models/terrains_sexe_enum.py +7 -0
  176. ffbb_data_client/models/terrains_storage.py +5 -0
  177. ffbb_data_client/models/tournoi_type_class.py +35 -0
  178. ffbb_data_client/models/tournoi_type_enum.py +7 -0
  179. ffbb_data_client/models/tournoi_types_3x3.py +43 -0
  180. ffbb_data_client/models/tournoi_types_3x3_libelle.py +60 -0
  181. ffbb_data_client/models/tournoi_types_3x3_libelle_enum.py +10 -0
  182. ffbb_data_client/models/tournois_facet_distribution.py +41 -0
  183. ffbb_data_client/models/tournois_facet_stats.py +14 -0
  184. ffbb_data_client/models/tournois_hit.py +132 -0
  185. ffbb_data_client/models/tournois_hit_type.py +5 -0
  186. ffbb_data_client/models/tournois_libelle.py +7 -0
  187. ffbb_data_client/models/tournois_multi_search_query.py +40 -0
  188. ffbb_data_client/models/type_association.py +23 -0
  189. ffbb_data_client/models/type_association_libelle.py +30 -0
  190. ffbb_data_client/models/type_class.py +23 -0
  191. ffbb_data_client/models/type_competition.py +8 -0
  192. ffbb_data_client/models/type_competition_generique.py +31 -0
  193. ffbb_data_client/models/type_enum.py +7 -0
  194. ffbb_data_client/models/type_league.py +6 -0
  195. ffbb_data_client/py.typed +0 -0
  196. ffbb_data_client/utils/__init__.py +27 -0
  197. ffbb_data_client/utils/cache_manager.py +393 -0
  198. ffbb_data_client/utils/converter_utils.py +329 -0
  199. ffbb_data_client/utils/input_validation.py +360 -0
  200. ffbb_data_client/utils/retry_utils.py +478 -0
  201. ffbb_data_client/utils/secure_logging.py +153 -0
  202. ffbb_data_client/utils/token_manager.py +115 -0
  203. ffbb_data_client-2.0.0.dist-info/METADATA +339 -0
  204. ffbb_data_client-2.0.0.dist-info/RECORD +207 -0
  205. ffbb_data_client-2.0.0.dist-info/WHEEL +5 -0
  206. ffbb_data_client-2.0.0.dist-info/licenses/LICENSE.txt +201 -0
  207. ffbb_data_client-2.0.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ from ..config import MEILISEARCH_FACETS_TOURNOIS, MEILISEARCH_INDEX_TOURNOIS
4
+ from .multi_search_query import MultiSearchQuery
5
+ from .multi_search_result_tournois import TournoisMultiSearchResult
6
+ from .multi_search_results import MultiSearchResult
7
+ from .tournois_facet_distribution import TournoisFacetDistribution
8
+ from .tournois_facet_stats import TournoisFacetStats
9
+
10
+
11
+ class TournoisMultiSearchQuery(MultiSearchQuery):
12
+ def __init__(
13
+ self,
14
+ q: str | None,
15
+ limit: int | None = 10,
16
+ offset: int | None = 0,
17
+ filter: list[str] | None = None,
18
+ sort: list[str] | None = None,
19
+ ):
20
+ super().__init__(
21
+ index_uid=MEILISEARCH_INDEX_TOURNOIS,
22
+ q=q,
23
+ facets=MEILISEARCH_FACETS_TOURNOIS,
24
+ limit=limit,
25
+ offset=offset,
26
+ filter=filter,
27
+ )
28
+
29
+ def is_valid_result(self, result: MultiSearchResult):
30
+ return result and (
31
+ isinstance(result, TournoisMultiSearchResult)
32
+ and (
33
+ result.facet_distribution is None
34
+ or isinstance(result.facet_distribution, TournoisFacetDistribution)
35
+ )
36
+ and (
37
+ result.facet_stats is None
38
+ or isinstance(result.facet_stats, TournoisFacetStats)
39
+ )
40
+ )
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+ from ..utils.converter_utils import from_str
7
+
8
+
9
+ @dataclass
10
+ class TypeAssociation:
11
+ libelle: str | None = None
12
+
13
+ @staticmethod
14
+ def from_dict(obj: Any) -> TypeAssociation:
15
+ assert isinstance(obj, dict)
16
+ libelle = from_str(obj, "libelle")
17
+ return TypeAssociation(libelle=libelle)
18
+
19
+ def to_dict(self) -> dict:
20
+ result: dict = {}
21
+ if self.libelle is not None:
22
+ result["libelle"] = self.libelle
23
+ return result
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+ from ..utils.converter_utils import from_int
7
+
8
+
9
+ @dataclass
10
+ class TypeAssociationLibelle:
11
+ club: int | None = None
12
+ coopération_territoriale_club: int | None = None
13
+
14
+ @staticmethod
15
+ def from_dict(obj: Any) -> TypeAssociationLibelle:
16
+ assert isinstance(obj, dict)
17
+ club = from_int(obj, "Club")
18
+ coopération_territoriale_club = from_int(obj, "Coopération Territoriale Club")
19
+ return TypeAssociationLibelle(
20
+ club=club,
21
+ coopération_territoriale_club=coopération_territoriale_club,
22
+ )
23
+
24
+ def to_dict(self) -> dict:
25
+ result: dict = {}
26
+ if self.club is not None:
27
+ result["Club"] = self.club
28
+ if self.coopération_territoriale_club is not None:
29
+ result["Coopération Territoriale Club"] = self.coopération_territoriale_club
30
+ return result
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+ from ..utils.converter_utils import from_int
7
+
8
+
9
+ @dataclass
10
+ class TypeClass:
11
+ groupement: int | None = None
12
+
13
+ @staticmethod
14
+ def from_dict(obj: Any) -> TypeClass:
15
+ assert isinstance(obj, dict)
16
+ groupement = from_int(obj, "Groupement")
17
+ return TypeClass(groupement=groupement)
18
+
19
+ def to_dict(self) -> dict:
20
+ result: dict = {}
21
+ if self.groupement is not None:
22
+ result["Groupement"] = self.groupement
23
+ return result
@@ -0,0 +1,8 @@
1
+ from enum import Enum
2
+
3
+
4
+ class TypeCompetition(Enum):
5
+ CHAMPIONNAT = "Championnat"
6
+ CHAMPIONNAT_3_X3 = "Championnat 3x3"
7
+ COUPE = "Coupe"
8
+ PLATEAU = "Plateau"
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+ from ..utils.converter_utils import from_obj, from_str
7
+ from .logo import Logo
8
+
9
+
10
+ @dataclass
11
+ class TypeCompetitionGenerique:
12
+ type_competition_generique_id: str | None = None
13
+ logo: Logo | None = None
14
+
15
+ @staticmethod
16
+ def from_dict(obj: Any) -> TypeCompetitionGenerique:
17
+ assert isinstance(obj, dict)
18
+ type_competition_generique_id = from_str(obj, "id")
19
+ logo = from_obj(Logo.from_dict, obj, "logo")
20
+ return TypeCompetitionGenerique(
21
+ type_competition_generique_id=type_competition_generique_id,
22
+ logo=logo,
23
+ )
24
+
25
+ def to_dict(self) -> dict:
26
+ result: dict = {}
27
+ if self.type_competition_generique_id is not None:
28
+ result["id"] = self.type_competition_generique_id
29
+ if self.logo is not None:
30
+ result["logo"] = self.logo.to_dict()
31
+ return result
@@ -0,0 +1,7 @@
1
+ from enum import Enum
2
+
3
+
4
+ class TypeEnum(Enum):
5
+ COMITÉ = "Comité"
6
+ FÉDÉRATION = "Fédération"
7
+ LIGUE = "Ligue"
@@ -0,0 +1,6 @@
1
+ from enum import Enum
2
+
3
+
4
+ class TypeLeague(Enum):
5
+ JUNIOR = "junior"
6
+ SENIOR = "senior"
File without changes
@@ -0,0 +1,27 @@
1
+ """Utility modules for FFBB API client."""
2
+
3
+ from .converter_utils import (
4
+ from_bool,
5
+ from_datetime,
6
+ from_enum,
7
+ from_float,
8
+ from_int,
9
+ from_list,
10
+ from_obj,
11
+ from_officiels_list,
12
+ from_str,
13
+ from_uuid,
14
+ )
15
+
16
+ __all__ = [
17
+ "from_bool",
18
+ "from_datetime",
19
+ "from_enum",
20
+ "from_float",
21
+ "from_int",
22
+ "from_list",
23
+ "from_obj",
24
+ "from_officiels_list",
25
+ "from_str",
26
+ "from_uuid",
27
+ ]
@@ -0,0 +1,393 @@
1
+ """
2
+ Advanced cache management for FFBB Data Client.
3
+
4
+ This module provides sophisticated caching strategies including:
5
+ - Multi-level caching (memory, disk, Redis)
6
+ - Configurable cache policies
7
+ - Cache performance metrics
8
+ - Intelligent cache invalidation
9
+ - Cache warming capabilities
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import hashlib
15
+ import threading
16
+ from dataclasses import dataclass
17
+ from typing import Any, cast
18
+
19
+ import hishel
20
+ import hishel.httpx
21
+ import httpx
22
+
23
+
24
+ @dataclass
25
+ class CacheMetrics:
26
+ """Cache performance metrics."""
27
+
28
+ hits: int = 0
29
+ misses: int = 0
30
+ evictions: int = 0
31
+ sets: int = 0
32
+ errors: int = 0
33
+
34
+ @property
35
+ def hit_rate(self) -> float:
36
+ """Calculate cache hit rate."""
37
+ total = self.hits + self.misses
38
+ return self.hits / total if total > 0 else 0.0
39
+
40
+ def reset(self) -> None:
41
+ """Reset all metrics."""
42
+ self.hits = 0
43
+ self.misses = 0
44
+ self.evictions = 0
45
+ self.sets = 0
46
+ self.errors = 0
47
+
48
+
49
+ class CacheConfig:
50
+ """
51
+ Configuration for cache behavior.
52
+
53
+ Attributes:
54
+ enabled: Whether caching is enabled.
55
+ backend: Cache backend ('memory', 'sqlite', 'redis').
56
+ expire_after: Default expiration time in seconds.
57
+ max_size: Maximum cache size (for memory backend).
58
+ redis_url: Redis URL for Redis backend.
59
+ key_prefix: Prefix for cache keys.
60
+ compression: Whether to compress cached data.
61
+ transport_retries: Low-level HTTP transport retries.
62
+ """
63
+
64
+ def __init__(
65
+ self,
66
+ enabled: bool = True,
67
+ backend: str = "sqlite",
68
+ expire_after: int = 1800, # 30 minutes
69
+ max_size: int = 1000,
70
+ redis_url: str | None = None,
71
+ key_prefix: str = "ffbb_api",
72
+ compression: bool = False,
73
+ transport_retries: int = 0,
74
+ ) -> None:
75
+ """
76
+ Initialize cache configuration.
77
+
78
+ Args:
79
+ enabled: Whether caching is enabled.
80
+ backend: Cache backend type.
81
+ expire_after: Default expiration time.
82
+ max_size: Maximum cache size for memory backend.
83
+ redis_url: Redis connection URL.
84
+ key_prefix: Prefix for cache keys.
85
+ compression: Whether to compress cached data.
86
+ transport_retries: Low-level HTTP transport retries.
87
+ """
88
+ self.enabled = enabled
89
+ self.backend = backend
90
+ self.expire_after = expire_after
91
+ self.max_size = max_size
92
+ self.redis_url = redis_url
93
+ self.key_prefix = key_prefix
94
+ self.compression = compression
95
+ self.transport_retries = transport_retries
96
+
97
+
98
+ class CacheManager:
99
+ """
100
+ Thread-safe singleton cache manager for API requests.
101
+
102
+ This class implements the singleton pattern with double-check locking
103
+ to ensure thread safety during instance creation.
104
+
105
+ Attributes:
106
+ config: Cache configuration settings.
107
+ metrics: Cache performance metrics.
108
+ """
109
+
110
+ _instance: CacheManager | None = None
111
+ _lock: threading.Lock = threading.Lock()
112
+ _initialized: bool = False
113
+
114
+ def __new__(cls, config: CacheConfig | None = None) -> CacheManager:
115
+ """
116
+ Create or return the singleton instance.
117
+
118
+ Uses double-check locking for thread safety.
119
+
120
+ Args:
121
+ config: Optional configuration for first instantiation.
122
+
123
+ Returns:
124
+ The singleton CacheManager instance.
125
+ """
126
+ if cls._instance is None:
127
+ with cls._lock:
128
+ if cls._instance is None:
129
+ instance = super().__new__(cls)
130
+ cls._instance = instance
131
+ return cls._instance
132
+
133
+ def __init__(self, config: CacheConfig | None = None) -> None:
134
+ """
135
+ Initialize the cache manager instance.
136
+
137
+ Only initializes on first call to avoid re-initialization.
138
+
139
+ Args:
140
+ config: Cache configuration settings.
141
+ """
142
+ if CacheManager._initialized:
143
+ return
144
+
145
+ with CacheManager._lock:
146
+ if CacheManager._initialized:
147
+ return
148
+
149
+ self.config = config or CacheConfig()
150
+ self.metrics = CacheMetrics()
151
+ self._memory_cache: dict[str, dict[str, Any]] = {}
152
+ self._client: httpx.Client | None = None
153
+ self._async_client: httpx.AsyncClient | None = None
154
+
155
+ if self.config.enabled:
156
+ self._initialize_cache()
157
+
158
+ CacheManager._initialized = True
159
+
160
+ def _initialize_cache(self) -> None:
161
+ """Initialize the cache backend."""
162
+ policy = hishel.FilterPolicy()
163
+ policy.use_body_key = True
164
+ # ── AJOUT : force le cache indépendamment des headers HTTP ──────────
165
+ # hishel respecte RFC 7234 par défaut → ne cache pas si le serveur
166
+ # ne renvoie pas de Cache-Control/ETag. FFBB n'en envoie pas.
167
+ policy.cacheable_methods = ["GET", "POST"]
168
+ policy.cacheable_status_codes = [200, 203, 204, 206, 300, 301, 308]
169
+ # ────────────────────────────────────────────────────────────────────
170
+
171
+ if self.config.backend == "memory":
172
+ import sqlite3
173
+
174
+ # Use separate in-memory SQLite connections for sync and async:
175
+ # sharing one connection across threads (sync vs asyncio) is not safe.
176
+ sync_conn = sqlite3.connect(":memory:", check_same_thread=False)
177
+ async_conn = sqlite3.connect(":memory:", check_same_thread=False)
178
+ storage = hishel.SyncSqliteStorage(
179
+ connection=sync_conn, default_ttl=self.config.expire_after
180
+ )
181
+ self._client = hishel.httpx.SyncCacheClient(
182
+ storage=storage,
183
+ policy=policy,
184
+ transport=httpx.HTTPTransport(retries=self.config.transport_retries),
185
+ )
186
+ # Async version uses its own connection
187
+ async_storage = hishel.AsyncSqliteStorage(
188
+ connection=async_conn, default_ttl=self.config.expire_after
189
+ )
190
+ self._async_client = hishel.httpx.AsyncCacheClient(
191
+ storage=async_storage,
192
+ policy=policy,
193
+ transport=httpx.AsyncHTTPTransport(
194
+ retries=self.config.transport_retries
195
+ ),
196
+ )
197
+ elif self.config.backend == "sqlite":
198
+ storage = hishel.SyncSqliteStorage(
199
+ database_path="http_cache.db", default_ttl=self.config.expire_after
200
+ )
201
+ self._client = hishel.httpx.SyncCacheClient(
202
+ storage=storage,
203
+ policy=policy,
204
+ transport=httpx.HTTPTransport(retries=self.config.transport_retries),
205
+ )
206
+ # Async version
207
+ async_storage = hishel.AsyncSqliteStorage(
208
+ database_path="http_cache.db", default_ttl=self.config.expire_after
209
+ )
210
+ self._async_client = hishel.httpx.AsyncCacheClient(
211
+ storage=async_storage,
212
+ policy=policy,
213
+ transport=httpx.AsyncHTTPTransport(
214
+ retries=self.config.transport_retries
215
+ ),
216
+ )
217
+ else:
218
+ raise ValueError(f"Unsupported cache backend: {self.config.backend}")
219
+
220
+ def create_cache_key(self, request: httpx.Request, **_kwargs: Any) -> str:
221
+ """
222
+ Create a cache key from the request.
223
+
224
+ Args:
225
+ request: httpx Request.
226
+ **_kwargs: Additional arguments (ignored).
227
+
228
+ Returns:
229
+ Cache key string.
230
+ """
231
+ key_parts = [
232
+ request.method or "GET",
233
+ str(request.url) or "",
234
+ ]
235
+
236
+ if request.headers:
237
+ auth_header = request.headers.get("Authorization", "")
238
+ if auth_header:
239
+ key_parts.append("auth_masked")
240
+
241
+ if request.method == "POST" and request.content:
242
+ content_bytes = (
243
+ request.content
244
+ if isinstance(request.content, bytes)
245
+ else str(request.content).encode("utf-8")
246
+ )
247
+ body_hash = hashlib.md5(content_bytes).hexdigest()
248
+ key_parts.append(body_hash)
249
+
250
+ key_string = "|".join(key_parts)
251
+ return (
252
+ f"{self.config.key_prefix}:{hashlib.md5(key_string.encode()).hexdigest()}"
253
+ )
254
+
255
+ @property
256
+ def session(self) -> httpx.Client | None:
257
+ """Get the cached session."""
258
+ return self._client if self.config.enabled else None
259
+
260
+ @property
261
+ def async_session(self) -> httpx.AsyncClient | None:
262
+ """Get the cached async session."""
263
+ return self._async_client if self.config.enabled else None
264
+
265
+ def get_session(
266
+ self, async_mode: bool = False
267
+ ) -> httpx.Client | httpx.AsyncClient | None:
268
+ """
269
+ Get the cached session.
270
+
271
+ Args:
272
+ async_mode: Whether to return the async session.
273
+
274
+ Returns:
275
+ The cached session or None if caching is disabled.
276
+ """
277
+ if async_mode:
278
+ return self.async_session
279
+ return self.session
280
+
281
+ def is_enabled(self) -> bool:
282
+ """
283
+ Check if caching is enabled.
284
+
285
+ Returns:
286
+ True if caching is enabled.
287
+ """
288
+ return self.config.enabled and self._client is not None
289
+
290
+ def clear_cache(self) -> bool:
291
+ """
292
+ Clear all cached data.
293
+
294
+ Returns:
295
+ True if cache was cleared successfully, False otherwise.
296
+ """
297
+ if self._client is None:
298
+ return False
299
+ try:
300
+ storage = getattr(self._client, "_storage", None)
301
+ clear_fn = getattr(storage, "clear", None)
302
+ if callable(clear_fn):
303
+ clear_fn()
304
+ self.metrics.evictions = 0
305
+ return True
306
+ return False
307
+ except (OSError, RuntimeError, AttributeError):
308
+ self.metrics.errors += 1
309
+ return False
310
+
311
+ def get_cache_size(self) -> int:
312
+ """
313
+ Get the current cache size.
314
+
315
+ Returns:
316
+ Number of cached items.
317
+ """
318
+ if self._client is None:
319
+ return 0
320
+ try:
321
+ storage = getattr(self._client, "_storage", None)
322
+ count_method = getattr(storage, "count", None)
323
+ if count_method is not None:
324
+ return cast(int, count_method())
325
+ except (OSError, RuntimeError, AttributeError):
326
+ self.metrics.errors += 1
327
+ return 0
328
+
329
+ def get_metrics(self) -> CacheMetrics:
330
+ """
331
+ Get cache performance metrics.
332
+
333
+ Returns:
334
+ Current cache metrics.
335
+ """
336
+ return self.metrics
337
+
338
+ def warm_cache(self, urls: list[str], headers: dict[str, str] | None = None) -> int:
339
+ """
340
+ Warm the cache by pre-fetching specified URLs.
341
+
342
+ Args:
343
+ urls: List of URLs to cache.
344
+ headers: Headers to use for requests.
345
+
346
+ Returns:
347
+ Number of URLs successfully cached.
348
+ """
349
+ if not self.is_enabled() or self._client is None:
350
+ return 0
351
+
352
+ headers = headers or {}
353
+ count = 0
354
+ for url in urls:
355
+ try:
356
+ self._client.get(url, headers=headers, timeout=10)
357
+ count += 1
358
+ except (OSError, ConnectionError, TimeoutError, ValueError):
359
+ pass
360
+ return count
361
+
362
+ def invalidate_pattern(self, pattern: str) -> int:
363
+ """
364
+ Invalidate cache entries matching a pattern.
365
+
366
+ Args:
367
+ pattern: Pattern to match for invalidation.
368
+
369
+ Returns:
370
+ Number of entries invalidated.
371
+ """
372
+ if not self.is_enabled() or self._client is None:
373
+ return 0
374
+
375
+ try:
376
+ getattr(self._client, "_storage", None)
377
+ # hishel storage doesn't generally support pattern invalidation exposing cache_dict,
378
+ # so we'd need to iterate visually, but for now we skip or implement if needed
379
+ return 0
380
+ except (OSError, RuntimeError, AttributeError, KeyError):
381
+ self.metrics.errors += 1
382
+ return 0
383
+
384
+ @classmethod
385
+ def reset_instance(cls) -> None:
386
+ """Reset the singleton instance (for testing purposes)."""
387
+ with cls._lock:
388
+ if cls._instance is not None:
389
+ client = getattr(cls._instance, "_client", None)
390
+ if client is not None:
391
+ client.close()
392
+ cls._instance = None
393
+ cls._initialized = False