thorcast_api_client 0.1.4.2__tar.gz → 0.2.0__tar.gz

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.
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.4
2
+ Name: thorcast_api_client
3
+ Version: 0.2.0
4
+ Summary: Un client Python pour interagir avec l'API ThorCast
5
+ Author: JBCAYRE
6
+ Author-email: jcayre@mint.eco
7
+ Requires-Python: >=3.8,<4.0
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.8
10
+ Classifier: Programming Language :: Python :: 3.9
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Programming Language :: Python :: 3.15
17
+ Requires-Dist: requests (>=2.26.0,<3.0.0)
@@ -1,14 +1,13 @@
1
- [tool.poetry]
2
- name = "thorcast_api_client"
3
- version = "0.1.4.2"
4
- description = "Un client Python pour interagir avec l'API ThorCast"
5
- authors = ["JBCAYRE <jcayre@mint.eco>"]
6
- readme = "README.md"
7
-
8
- [tool.poetry.dependencies]
9
- python = "^3.8"
10
- requests = "^2.26.0"
11
-
12
- [build-system]
13
- requires = ["poetry-core>=1.0.0"]
14
- build-backend = "poetry.core.masonry.api"
1
+ [tool.poetry]
2
+ name = "thorcast_api_client"
3
+ version = "0.2.0"
4
+ description = "Un client Python pour interagir avec l'API ThorCast"
5
+ authors = ["JBCAYRE <jcayre@mint.eco>"]
6
+
7
+ [tool.poetry.dependencies]
8
+ python = "^3.8"
9
+ requests = "^2.26.0"
10
+
11
+ [build-system]
12
+ requires = ["poetry-core>=1.0.0"]
13
+ build-backend = "poetry.core.masonry.api"
@@ -14,6 +14,7 @@ class AuthManager:
14
14
  """Récupère un token JWT"""
15
15
  url = f"{API_BASE_URL}/backend/auth/token/"
16
16
  payload = {"email": self.username, "password": self.password}
17
+
17
18
  response = requests.post(url, json=payload)
18
19
  if response.status_code == 200:
19
20
  data = response.json()
@@ -0,0 +1,98 @@
1
+ import time
2
+
3
+ import requests
4
+ from thorcast_api_client.auth import AuthManager
5
+ from thorcast_api_client.config import API_BASE_URL, DEFAULT_RETRY_AFTER, MAX_REQUESTS_PER_MINUTE
6
+ from thorcast_api_client.exceptions import APIClientError, AuthenticationError, RateLimitExceeded, ServerError
7
+ from thorcast_api_client.rate_limit import SlidingWindowRateLimiter
8
+
9
+
10
+ def _parse_retry_after(response):
11
+ """Délai Retry-After en secondes, None s'il est absent ou non numérique."""
12
+ value = response.headers.get("Retry-After")
13
+ try:
14
+ return max(float(value), 0.0)
15
+ except (TypeError, ValueError):
16
+ return None
17
+
18
+
19
+ class ThorCastClient:
20
+ """Client pour interagir avec l'API ThorCast.
21
+
22
+ - max_requests_per_minute : plafond d'appels par minute glissante, appliqué
23
+ côté client et partagé entre threads (None = pas de limitation). Par défaut,
24
+ le quota documenté par l'API ; à baisser si d'autres applications utilisent
25
+ le même compte.
26
+ - max_retries : nombre de nouvelles tentatives sur une réponse 429, après
27
+ l'attente indiquée par l'en-tête Retry-After.
28
+ - timeout : timeout HTTP en secondes passé à requests (None = aucun).
29
+ """
30
+
31
+ def __init__(self, username: str, password: str,
32
+ max_requests_per_minute: int = MAX_REQUESTS_PER_MINUTE,
33
+ max_retries: int = 3, timeout: float = None):
34
+ self.auth = AuthManager(username, password)
35
+ self.auth.authenticate()
36
+ self.max_retries = max_retries
37
+ self.timeout = timeout
38
+ self._rate_limiter = (SlidingWindowRateLimiter(max_requests_per_minute)
39
+ if max_requests_per_minute else None)
40
+
41
+ def _send(self, method: str, url: str, **kwargs):
42
+ if self._rate_limiter:
43
+ self._rate_limiter.acquire()
44
+ return requests.request(method, url, headers=self.auth.get_headers(),
45
+ timeout=self.timeout, **kwargs)
46
+
47
+ def _request(self, method: str, endpoint: str, **kwargs):
48
+ """Méthode générique pour effectuer une requête.
49
+
50
+ Un 429 est retenté jusqu'à max_retries fois après l'attente Retry-After."""
51
+ url = f"{API_BASE_URL}{endpoint}"
52
+
53
+ response = self._send(method, url, **kwargs)
54
+
55
+ retries = 0
56
+ while response.status_code == 429 and retries < self.max_retries:
57
+ retries += 1
58
+ time.sleep(_parse_retry_after(response) or DEFAULT_RETRY_AFTER)
59
+ response = self._send(method, url, **kwargs)
60
+
61
+ if response.status_code == 401:
62
+ raise AuthenticationError(401, "Echec d'authentification, token invalide.")
63
+ elif response.status_code == 429:
64
+ raise RateLimitExceeded(429, "Limite de requêtes dépassée.",
65
+ retry_after=_parse_retry_after(response))
66
+ elif response.status_code >= 500:
67
+ raise ServerError(response.status_code, "Erreur interne du serveur.")
68
+ elif not response.ok:
69
+ raise APIClientError(response.status_code, "Erreur inconnue.")
70
+
71
+ return response.json()
72
+
73
+ def get_lt_curves_versions(self, prms: list):
74
+ """Liste les versions disponibles des courbes de charge LT."""
75
+ endpoint = f"/backend/thorcast/v1/lt_curves/versions/?prms={','.join(prms)}"
76
+ return self._request("GET", endpoint)
77
+
78
+ def get_lt_curves(self, prms: list, sd: str, ed: str, versions: list = None, tz: str = "UTC"):
79
+ """Récupère les courbes de charge LT."""
80
+ endpoint = f"/backend/thorcast/v1/lt_curves/?prms={','.join(prms)}&sd={sd}&ed={ed}&tz={tz}"
81
+ if versions:
82
+ endpoint += f"&versions={','.join(versions)}"
83
+ return self._request("GET", endpoint)
84
+
85
+ def get_historical_curves(self, prms: list, sd: str, ed: str, tz: str = "UTC"):
86
+ """Récupère les courbes de charge historiques."""
87
+ endpoint = f"/backend/thorcast/v1/historical_load_curves/?prms={','.join(prms)}&sd={sd}&ed={ed}&tz={tz}"
88
+ return self._request("GET", endpoint)
89
+
90
+ def get_catalog_versions(self, catalogs: list, sd: str, ed: str):
91
+ """Liste les versions disponibles des courbes de charge LT."""
92
+ endpoint = f"/backend/thorcast/v1/catalog_curves/versions/?catalogs={','.join(catalogs)}&sd={sd}&ed={ed}"
93
+ return self._request("GET", endpoint)
94
+
95
+ def get_catalog_curves(self, ids: list, tz: str = "UTC"):
96
+ """Récupère les courbes de charge catalog."""
97
+ endpoint = f"/backend/thorcast/v1/catalog_curves/?ids={','.join(ids)}&tz={tz}"
98
+ return self._request("GET", endpoint)
@@ -0,0 +1,11 @@
1
+ API_BASE_URL = "https://profilage-api.holmium-consulting.com"
2
+ HEADERS = {
3
+ "Accept": "application/json",
4
+ "Content-Type": "application/json",
5
+ }
6
+
7
+ # Quota de l'API : 60 requêtes par minute glissante (documentation v1.8, §5)
8
+ MAX_REQUESTS_PER_MINUTE = 60
9
+ # Attente avant nouvelle tentative sur 429 quand l'en-tête Retry-After manque :
10
+ # une fenêtre glissante complète
11
+ DEFAULT_RETRY_AFTER = 60.0
@@ -11,8 +11,14 @@ class AuthenticationError(APIClientError):
11
11
  pass
12
12
 
13
13
  class RateLimitExceeded(APIClientError):
14
- """Erreur lorsque la limite de requêtes est dépassée."""
15
- pass
14
+ """Erreur lorsque la limite de requêtes est dépassée.
15
+
16
+ `retry_after` : délai d'attente en secondes indiqué par l'API (en-tête
17
+ Retry-After), None s'il est absent ou illisible."""
18
+
19
+ def __init__(self, status_code, message, retry_after=None):
20
+ super().__init__(status_code, message)
21
+ self.retry_after = retry_after
16
22
 
17
23
  class ServerError(APIClientError):
18
24
  """Erreur interne du serveur API."""
@@ -0,0 +1,36 @@
1
+ import threading
2
+ import time
3
+ from collections import deque
4
+
5
+
6
+ class SlidingWindowRateLimiter:
7
+ """Limite le nombre d'appels sur une fenêtre glissante (thread-safe).
8
+
9
+ L'API ThorCast renvoie 429 au-delà de 60 requêtes par minute glissante
10
+ (documentation de l'API v1.8, §5) : on attend une place libre côté client
11
+ plutôt que de consommer le quota et d'encaisser des 429.
12
+ """
13
+
14
+ def __init__(self, max_calls: int, period: float = 60.0,
15
+ clock=time.monotonic, sleep=time.sleep):
16
+ if max_calls < 1:
17
+ raise ValueError("max_calls doit être >= 1")
18
+ self.max_calls = max_calls
19
+ self.period = period
20
+ self._clock = clock
21
+ self._sleep = sleep
22
+ self._calls = deque()
23
+ self._lock = threading.Lock()
24
+
25
+ def acquire(self):
26
+ """Bloque jusqu'à ce qu'un appel soit autorisé, puis l'enregistre."""
27
+ while True:
28
+ with self._lock:
29
+ now = self._clock()
30
+ while self._calls and now - self._calls[0] >= self.period:
31
+ self._calls.popleft()
32
+ if len(self._calls) < self.max_calls:
33
+ self._calls.append(now)
34
+ return
35
+ wait = self.period - (now - self._calls[0])
36
+ self._sleep(wait)
@@ -1,78 +0,0 @@
1
- Metadata-Version: 2.3
2
- Name: thorcast_api_client
3
- Version: 0.1.4.2
4
- Summary: Un client Python pour interagir avec l'API ThorCast
5
- Author: JBCAYRE
6
- Author-email: jcayre@mint.eco
7
- Requires-Python: >=3.8,<4.0
8
- Classifier: Programming Language :: Python :: 3
9
- Classifier: Programming Language :: Python :: 3.8
10
- Classifier: Programming Language :: Python :: 3.9
11
- Classifier: Programming Language :: Python :: 3.10
12
- Classifier: Programming Language :: Python :: 3.11
13
- Classifier: Programming Language :: Python :: 3.12
14
- Classifier: Programming Language :: Python :: 3.13
15
- Requires-Dist: requests (>=2.26.0,<3.0.0)
16
- Description-Content-Type: text/markdown
17
-
18
- # ThorCast API Client V0.1
19
-
20
- ## 📌 Description (je suis nul en Markdown , j'espère que ce sera lisible)
21
-
22
- `thorcast_api_client` est un client Python permettant d'interagir avec l'API **ThorCast** mise en place par Holmium pour MINT.
23
-
24
- Il prend en charge (normalement) :
25
- - L'**authentification JWT**
26
- - La récupération des **courbes de charge prévues**
27
- - La récupération des **courbes de charge historiques**
28
- - La gestion des **erreurs API**
29
-
30
- ## 🚀 Installation
31
-
32
- ### Via `pip`
33
- ```sh
34
- pip install thorcast_api_client
35
- ```
36
-
37
- ### Via `poetry` pour les amateurs de Baudelaire
38
- ```sh
39
- poetry add thorcast_api_client
40
- ```
41
-
42
- ## 📜 Utilisation
43
-
44
- ### 🔒 Authentification :
45
-
46
- L'authentification passe par la requête d'un token JWS en injectant un MDP/Username :
47
-
48
- ```sh
49
- from thorcast_api_client.client import ThorCastClient
50
-
51
- client = ThorCastClient("mon_user", "mon_mdp")
52
- ```
53
- #### Lister les versions disponibles pour une ou plusieurs courbes LT
54
-
55
- ```sh
56
- versions = client.get_lt_curves_versions(["prm1", "prm2"])
57
- print(versions)
58
- ```
59
- #### Récupérer les courbes de charge LT
60
-
61
- ```sh
62
- courbes = client.get_lt_curves(["prm1", "prm2"], "2023-01-01T00:00:00Z", "2025-01-01T00:00:00Z")
63
- print(courbes)
64
- ```
65
-
66
- #### Récupérer les versions des courbes de catalogue 🤷 (rien compris à ce que ça remonte)
67
-
68
- ```sh
69
- version_catalogues = client.get_catalog_versions(["catalogue1", "catalogue2"], "2023-01-01T00:00:00Z", "2025-01-01T00:00:00Z")
70
- print(version_catalogues)
71
- ```
72
- #### Récupérer les courbes de charge historiques
73
-
74
- ```sh
75
- courbes_catalogue = client.get_catalog_curves(["id_catalogue1", "id_catalogue2"])
76
- print(courbes_catalogue)
77
- ```
78
-
@@ -1,60 +0,0 @@
1
- # ThorCast API Client V0.1
2
-
3
- ## 📌 Description (je suis nul en Markdown , j'espère que ce sera lisible)
4
-
5
- `thorcast_api_client` est un client Python permettant d'interagir avec l'API **ThorCast** mise en place par Holmium pour MINT.
6
-
7
- Il prend en charge (normalement) :
8
- - L'**authentification JWT**
9
- - La récupération des **courbes de charge prévues**
10
- - La récupération des **courbes de charge historiques**
11
- - La gestion des **erreurs API**
12
-
13
- ## 🚀 Installation
14
-
15
- ### Via `pip`
16
- ```sh
17
- pip install thorcast_api_client
18
- ```
19
-
20
- ### Via `poetry` pour les amateurs de Baudelaire
21
- ```sh
22
- poetry add thorcast_api_client
23
- ```
24
-
25
- ## 📜 Utilisation
26
-
27
- ### 🔒 Authentification :
28
-
29
- L'authentification passe par la requête d'un token JWS en injectant un MDP/Username :
30
-
31
- ```sh
32
- from thorcast_api_client.client import ThorCastClient
33
-
34
- client = ThorCastClient("mon_user", "mon_mdp")
35
- ```
36
- #### Lister les versions disponibles pour une ou plusieurs courbes LT
37
-
38
- ```sh
39
- versions = client.get_lt_curves_versions(["prm1", "prm2"])
40
- print(versions)
41
- ```
42
- #### Récupérer les courbes de charge LT
43
-
44
- ```sh
45
- courbes = client.get_lt_curves(["prm1", "prm2"], "2023-01-01T00:00:00Z", "2025-01-01T00:00:00Z")
46
- print(courbes)
47
- ```
48
-
49
- #### Récupérer les versions des courbes de catalogue 🤷 (rien compris à ce que ça remonte)
50
-
51
- ```sh
52
- version_catalogues = client.get_catalog_versions(["catalogue1", "catalogue2"], "2023-01-01T00:00:00Z", "2025-01-01T00:00:00Z")
53
- print(version_catalogues)
54
- ```
55
- #### Récupérer les courbes de charge historiques
56
-
57
- ```sh
58
- courbes_catalogue = client.get_catalog_curves(["id_catalogue1", "id_catalogue2"])
59
- print(courbes_catalogue)
60
- ```
@@ -1,56 +0,0 @@
1
- import requests
2
- from thorcast_api_client.auth import AuthManager
3
- from thorcast_api_client.config import API_BASE_URL
4
- from thorcast_api_client.exceptions import APIClientError, AuthenticationError, RateLimitExceeded, ServerError
5
-
6
- class ThorCastClient:
7
- """Client pour interagir avec l'API ThorCast."""
8
-
9
- def __init__(self, username: str, password: str):
10
- self.auth = AuthManager(username, password)
11
- self.auth.authenticate()
12
-
13
- def _request(self, method: str, endpoint: str, **kwargs):
14
- """Méthode générique pour effectuer une requête."""
15
- url = f"{API_BASE_URL}{endpoint}"
16
- headers = self.auth.get_headers()
17
-
18
- response = requests.request(method, url, headers=headers, **kwargs)
19
-
20
- if response.status_code == 401:
21
- raise AuthenticationError(401, "Echec d'authentification, token invalide.")
22
- elif response.status_code == 429:
23
- raise RateLimitExceeded(429, "Limite de requêtes dépassée.")
24
- elif response.status_code >= 500:
25
- raise ServerError(response.status_code, "Erreur interne du serveur.")
26
- elif not response.ok:
27
- raise APIClientError(response.status_code, "Erreur inconnue.")
28
-
29
- return response.json()
30
-
31
- def get_lt_curves_versions(self, prms: list):
32
- """Liste les versions disponibles des courbes de charge LT."""
33
- endpoint = f"/backend/thorcast/v1/lt_curves/versions/?prms={','.join(prms)}"
34
- return self._request("GET", endpoint)
35
-
36
- def get_lt_curves(self, prms: list, sd: str, ed: str, versions: list = None, tz: str = "UTC"):
37
- """Récupère les courbes de charge LT."""
38
- endpoint = f"/backend/thorcast/v1/lt_curves/?prms={','.join(prms)}&sd={sd}&ed={ed}&tz={tz}"
39
- if versions:
40
- endpoint += f"&versions={','.join(versions)}"
41
- return self._request("GET", endpoint)
42
-
43
- def get_historical_curves(self, prms: list, sd: str, ed: str, tz: str = "UTC"):
44
- """Récupère les courbes de charge historiques."""
45
- endpoint = f"/backend/thorcast/v1/historical_load_curves/?prms={','.join(prms)}&sd={sd}&ed={ed}&tz={tz}"
46
- return self._request("GET", endpoint)
47
-
48
- def get_catalog_versions(self, catalogs: list, sd: str, ed: str):
49
- """Liste les versions disponibles des courbes de charge LT."""
50
- endpoint = f"/backend/thorcast/v1/catalog_curves/versions/?catalogs={','.join(catalogs)}&sd={sd}&ed={ed}"
51
- return self._request("GET", endpoint)
52
-
53
- def get_catalog_curves(self, ids: list, tz: str = "UTC"):
54
- """Récupère les courbes de charge catalog."""
55
- endpoint = f"/backend/thorcast/v1/catalog_curves/?ids={','.join(ids)}&tz={tz}"
56
- return self._request("GET", endpoint)
@@ -1,5 +0,0 @@
1
- API_BASE_URL = "https://profilage-api.holmium-consulting.com"
2
- HEADERS = {
3
- "Accept": "application/json",
4
- "Content-Type": "application/json",
5
- }