pygazpar 1.3.0b2__tar.gz → 1.3.0b3__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
@@ -4,7 +4,11 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
- ## [1.3.0] - 2025-02-08
7
+ ## [1.3.0] - 2025-02-09
8
+
9
+ ### Added
10
+
11
+ [#84](https://github.com/ssenart/PyGazpar/issues/84) : Add a function to retrieve list of PCE ids and labels from account.
8
12
 
9
13
  ### Changed
10
14
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pygazpar
3
- Version: 1.3.0b2
3
+ Version: 1.3.0b3
4
4
  Summary: Python library to download gas consumption from a GrDF (French Gas Company) account
5
5
  License: MIT License
6
6
 
@@ -113,8 +113,12 @@ client = pygazpar.Client(pygazpar.JsonWebDataSource(
113
113
  password='your password')
114
114
  )
115
115
 
116
- data = client.loadSince(pceIdentifier='your PCE identifier',
117
- lastNDays=60,
116
+ # Returns the list of your PCE identifiers attached to your account.
117
+ pce_identifiers = client.get_pce_identifiers()
118
+
119
+ # Returns the daily and monthly consumptions for the last 60 days on your PCE identifier.
120
+ data = client.load_since(pce_identifier='your PCE identifier',
121
+ last_n_days=60,
118
122
  frequencies=[pygazpar.Frequency.DAILY, pygazpar.Frequency.MONTHLY])
119
123
  ```
120
124
  See [samples/jsonSample.py](samples/jsonSample.py) file for the full example.
@@ -129,8 +133,12 @@ client = pygazpar.Client(pygazpar.ExcelWebDataSource(
129
133
  password='your password')
130
134
  )
131
135
 
132
- data = client.loadSince(pceIdentifier='your PCE identifier',
133
- lastNDays=60,
136
+ # Returns the list of your PCE identifiers attached to your account.
137
+ pce_identifiers = client.get_pce_identifiers()
138
+
139
+ # Returns the daily and monthly consumptions for the last 60 days on your PCE identifier.
140
+ data = client.load_since(pce_identifier='your PCE identifier',
141
+ last_n_days=60,
134
142
  frequencies=[pygazpar.Frequency.DAILY, pygazpar.Frequency.MONTHLY])
135
143
  ```
136
144
  See [samples/excelSample.py](samples/jsonSample.py) file for the full example.
@@ -142,8 +150,8 @@ import pygazpar
142
150
 
143
151
  client = pygazpar.Client(pygazpar.TestDataSource())
144
152
 
145
- data = client.loadSince(pceIdentifier='your PCE identifier',
146
- lastNDays=10,
153
+ data = client.load_since(pce_identifier='your PCE identifier',
154
+ last_n_days=10,
147
155
  frequencies=[pygazpar.Frequency.DAILY, Frequency.MONTHLY])
148
156
  ```
149
157
  See [samples/testSample.py](samples/jsonSample.py) file for the full example.
@@ -76,8 +76,12 @@ client = pygazpar.Client(pygazpar.JsonWebDataSource(
76
76
  password='your password')
77
77
  )
78
78
 
79
- data = client.loadSince(pceIdentifier='your PCE identifier',
80
- lastNDays=60,
79
+ # Returns the list of your PCE identifiers attached to your account.
80
+ pce_identifiers = client.get_pce_identifiers()
81
+
82
+ # Returns the daily and monthly consumptions for the last 60 days on your PCE identifier.
83
+ data = client.load_since(pce_identifier='your PCE identifier',
84
+ last_n_days=60,
81
85
  frequencies=[pygazpar.Frequency.DAILY, pygazpar.Frequency.MONTHLY])
82
86
  ```
83
87
  See [samples/jsonSample.py](samples/jsonSample.py) file for the full example.
@@ -92,8 +96,12 @@ client = pygazpar.Client(pygazpar.ExcelWebDataSource(
92
96
  password='your password')
93
97
  )
94
98
 
95
- data = client.loadSince(pceIdentifier='your PCE identifier',
96
- lastNDays=60,
99
+ # Returns the list of your PCE identifiers attached to your account.
100
+ pce_identifiers = client.get_pce_identifiers()
101
+
102
+ # Returns the daily and monthly consumptions for the last 60 days on your PCE identifier.
103
+ data = client.load_since(pce_identifier='your PCE identifier',
104
+ last_n_days=60,
97
105
  frequencies=[pygazpar.Frequency.DAILY, pygazpar.Frequency.MONTHLY])
98
106
  ```
99
107
  See [samples/excelSample.py](samples/jsonSample.py) file for the full example.
@@ -105,8 +113,8 @@ import pygazpar
105
113
 
106
114
  client = pygazpar.Client(pygazpar.TestDataSource())
107
115
 
108
- data = client.loadSince(pceIdentifier='your PCE identifier',
109
- lastNDays=10,
116
+ data = client.load_since(pce_identifier='your PCE identifier',
117
+ last_n_days=10,
110
118
  frequencies=[pygazpar.Frequency.DAILY, Frequency.MONTHLY])
111
119
  ```
112
120
  See [samples/testSample.py](samples/jsonSample.py) file for the full example.
@@ -67,7 +67,7 @@ def main():
67
67
  raise ValueError("Invalid datasource: (json | excel | test) is expected")
68
68
 
69
69
  try:
70
- data = client.loadSince(args.pce, int(args.lastNDays), [args.frequency])
70
+ data = client.load_since(args.pce, int(args.lastNDays), [args.frequency])
71
71
  except BaseException: # pylint: disable=broad-except
72
72
  print("An error occured while querying PyGazpar library : %s", traceback.format_exc())
73
73
  return 1
@@ -0,0 +1,228 @@
1
+ import http.cookiejar
2
+ import json
3
+ import logging
4
+ import time
5
+ import traceback
6
+ from datetime import date
7
+ from enum import Enum
8
+ from typing import Any
9
+
10
+ from requests import Response, Session
11
+
12
+ SESSION_TOKEN_URL = "https://connexion.grdf.fr/api/v1/authn"
13
+ SESSION_TOKEN_PAYLOAD = """{{
14
+ "username": "{0}",
15
+ "password": "{1}",
16
+ "options": {{
17
+ "multiOptionalFactorEnroll": "false",
18
+ "warnBeforePasswordExpired": "false"
19
+ }}
20
+ }}"""
21
+
22
+ AUTH_TOKEN_URL = "https://connexion.grdf.fr/login/sessionCookieRedirect"
23
+ AUTH_TOKEN_PARAMS = """{{
24
+ "checkAccountSetupComplete": "true",
25
+ "token": "{0}",
26
+ "redirectUrl": "https://monespace.grdf.fr"
27
+ }}"""
28
+
29
+ API_BASE_URL = "https://monespace.grdf.fr/api"
30
+
31
+ DATE_FORMAT = "%Y-%m-%d"
32
+
33
+ Logger = logging.getLogger(__name__)
34
+
35
+
36
+ # ------------------------------------------------------
37
+ class ConsumptionType(str, Enum):
38
+ INFORMATIVE = "informatives"
39
+ PUBLISHED = "publiees"
40
+
41
+
42
+ # ------------------------------------------------------
43
+ class Frequency(str, Enum):
44
+ HOURLY = "Horaire"
45
+ DAILY = "Journalier"
46
+ WEEKLY = "Hebdomadaire"
47
+ MONTHLY = "Mensuel"
48
+ YEARLY = "Annuel"
49
+
50
+
51
+ # ------------------------------------------------------
52
+ class ServerError(SystemError):
53
+
54
+ def __init__(self, message: str, status_code: int):
55
+ super().__init__(message)
56
+ self.status_code = status_code
57
+
58
+
59
+ # ------------------------------------------------------
60
+ class InternalServerError(ServerError):
61
+
62
+ def __init__(self, message: str):
63
+ super().__init__(message, 500)
64
+
65
+
66
+ # ------------------------------------------------------
67
+ class APIClient:
68
+
69
+ # ------------------------------------------------------
70
+ def __init__(self, username: str, password: str, retry_count: int = 10):
71
+ self._username = username
72
+ self._password = password
73
+ self._retry_count = retry_count
74
+ self._session = None
75
+
76
+ # ------------------------------------------------------
77
+ def login(self):
78
+ if self._session is not None:
79
+ return
80
+
81
+ session = Session()
82
+ session.headers.update({"domain": "grdf.fr"})
83
+ session.headers.update({"Content-Type": "application/json"})
84
+ session.headers.update({"X-Requested-With": "XMLHttpRequest"})
85
+
86
+ payload = SESSION_TOKEN_PAYLOAD.format(self._username, self._password)
87
+
88
+ response = session.post(SESSION_TOKEN_URL, data=payload)
89
+
90
+ if response.status_code != 200:
91
+ raise ServerError(
92
+ f"An error occurred while logging in. Status code: {response.status_code} - {response.text}",
93
+ response.status_code,
94
+ )
95
+
96
+ session_token = response.json().get("sessionToken")
97
+
98
+ jar = http.cookiejar.CookieJar()
99
+
100
+ self._session = Session() # pylint: disable=attribute-defined-outside-init
101
+ self._session.headers.update({"Content-Type": "application/json"})
102
+ self._session.headers.update({"X-Requested-With": "XMLHttpRequest"})
103
+
104
+ params = json.loads(AUTH_TOKEN_PARAMS.format(session_token))
105
+
106
+ response = self._session.get(AUTH_TOKEN_URL, params=params, allow_redirects=True, cookies=jar) # type: ignore
107
+
108
+ if response.status_code != 200:
109
+ raise ServerError(
110
+ f"An error occurred while getting the auth token. Status code: {response.status_code} - {response.text}",
111
+ response.status_code,
112
+ )
113
+
114
+ # ------------------------------------------------------
115
+ def is_logged_in(self) -> bool:
116
+ return self._session is not None
117
+
118
+ # ------------------------------------------------------
119
+ def logout(self):
120
+ if self._session is None:
121
+ return
122
+
123
+ self._session.close()
124
+ self._session = None
125
+
126
+ # ------------------------------------------------------
127
+ def get(self, endpoint: str, params: dict[str, Any]) -> Response:
128
+
129
+ if self._session is None:
130
+ raise ConnectionError("You must login first")
131
+
132
+ retry = self._retry_count
133
+ while retry > 0:
134
+
135
+ try:
136
+ response = self._session.get(f"{API_BASE_URL}{endpoint}", params=params)
137
+
138
+ if "text/html" in response.headers.get("Content-Type"): # type: ignore
139
+ raise InternalServerError(
140
+ f"An unknown error occurred. Please check your query parameters (endpoint: {endpoint}): {params}"
141
+ )
142
+
143
+ if response.status_code != 200:
144
+ raise ServerError(
145
+ f"HTTP error on enpoint '{endpoint}': Status code: {response.status_code} - {response.text}. Query parameters: {params}",
146
+ response.status_code,
147
+ )
148
+
149
+ break
150
+ except InternalServerError as internalServerError: # pylint: disable=broad-exception-caught
151
+ if retry == 1:
152
+ Logger.error(f"{internalServerError}. Retry limit reached: {traceback.format_exc()}")
153
+ raise internalServerError
154
+ retry -= 1
155
+ Logger.warning(f"{internalServerError}. Retry in 3 seconds ({retry} retries left)...")
156
+ time.sleep(3)
157
+
158
+ return response
159
+
160
+ # ------------------------------------------------------
161
+ def get_pce_list(self, details: bool = False) -> list[Any]:
162
+
163
+ res = self.get("/e-conso/pce", {"details": details}).json()
164
+
165
+ if type(res) is not list:
166
+ raise TypeError(f"Invalid response type: {type(res)} (list expected)")
167
+
168
+ return res
169
+
170
+ # ------------------------------------------------------
171
+ def get_pce_consumption(
172
+ self, consumption_type: ConsumptionType, start_date: date, end_date: date, pce_list: list[str]
173
+ ) -> dict[str, Any]:
174
+
175
+ start = start_date.strftime(DATE_FORMAT)
176
+ end = end_date.strftime(DATE_FORMAT)
177
+
178
+ res = self.get(
179
+ f"/e-conso/pce/consommation/{consumption_type.value}",
180
+ {"dateDebut": start, "dateFin": end, "pceList[]": ",".join(pce_list)},
181
+ ).json()
182
+
183
+ if type(res) is list and len(res) == 0:
184
+ return dict[str, Any]()
185
+
186
+ if type(res) is not dict:
187
+ raise TypeError(f"Invalid response type: {type(res)} (dict expected)")
188
+
189
+ return res
190
+
191
+ # ------------------------------------------------------
192
+ def get_pce_consumption_excelsheet(
193
+ self,
194
+ consumption_type: ConsumptionType,
195
+ start_date: date,
196
+ end_date: date,
197
+ frequency: Frequency,
198
+ pce_list: list[str],
199
+ ) -> dict[str, Any]:
200
+
201
+ start = start_date.strftime(DATE_FORMAT)
202
+ end = end_date.strftime(DATE_FORMAT)
203
+
204
+ response = self.get(
205
+ f"/e-conso/pce/consommation/{consumption_type.value}/telecharger",
206
+ {"dateDebut": start, "dateFin": end, "frequence": frequency.value, "pceList[]": ",".join(pce_list)},
207
+ )
208
+
209
+ filename = response.headers["Content-Disposition"].split("filename=")[1]
210
+
211
+ res = {"filename": filename, "content": response.content}
212
+
213
+ return res
214
+
215
+ # ------------------------------------------------------
216
+ def get_pce_meteo(self, end_date: date, days: int, pce: str) -> dict[str, Any]:
217
+
218
+ end = end_date.strftime(DATE_FORMAT)
219
+
220
+ res = self.get(f"/e-conso/pce/{pce}/meteo", {"dateFinPeriode": end, "nbJours": days}).json()
221
+
222
+ if type(res) is list and len(res) == 0:
223
+ return dict[str, Any]()
224
+
225
+ if type(res) is not dict:
226
+ raise TypeError(f"Invalid response type: {type(res)} (dict expected)")
227
+
228
+ return res
@@ -0,0 +1,98 @@
1
+ import logging
2
+ import warnings
3
+ from datetime import date, timedelta
4
+ from typing import Optional
5
+
6
+ from pygazpar.datasource import IDataSource, MeterReadingsByFrequency
7
+ from pygazpar.enum import Frequency
8
+
9
+ DEFAULT_LAST_N_DAYS = 365
10
+
11
+
12
+ Logger = logging.getLogger(__name__)
13
+
14
+
15
+ # ------------------------------------------------------------------------------------------------------------
16
+ class Client:
17
+
18
+ # ------------------------------------------------------
19
+ def __init__(self, dataSource: IDataSource):
20
+ self.__dataSource = dataSource
21
+
22
+ # ------------------------------------------------------
23
+ def login(self):
24
+
25
+ try:
26
+ self.__dataSource.login()
27
+ except Exception:
28
+ Logger.error("An unexpected error occured while login", exc_info=True)
29
+ raise
30
+
31
+ # ------------------------------------------------------
32
+ def logout(self):
33
+
34
+ try:
35
+ self.__dataSource.logout()
36
+ except Exception:
37
+ Logger.error("An unexpected error occured while logout", exc_info=True)
38
+ raise
39
+
40
+ # ------------------------------------------------------
41
+ def get_pce_identifiers(self) -> list[str]:
42
+
43
+ try:
44
+ res = self.__dataSource.get_pce_identifiers()
45
+ except Exception:
46
+ Logger.error("An unexpected error occured while getting the PCE identifiers", exc_info=True)
47
+ raise
48
+
49
+ return res
50
+
51
+ # ------------------------------------------------------
52
+ def load_since(
53
+ self, pce_identifier: str, last_n_days: int = DEFAULT_LAST_N_DAYS, frequencies: Optional[list[Frequency]] = None
54
+ ) -> MeterReadingsByFrequency:
55
+
56
+ end_date = date.today()
57
+ start_date = end_date + timedelta(days=-last_n_days)
58
+
59
+ res = self.load_date_range(pce_identifier, start_date, end_date, frequencies)
60
+
61
+ return res
62
+
63
+ # ------------------------------------------------------
64
+ def load_date_range(
65
+ self, pce_identifier: str, start_date: date, end_date: date, frequencies: Optional[list[Frequency]] = None
66
+ ) -> MeterReadingsByFrequency:
67
+
68
+ Logger.debug("Start loading the data...")
69
+
70
+ try:
71
+ res = self.__dataSource.load(pce_identifier, start_date, end_date, frequencies)
72
+
73
+ Logger.debug("The data load terminates normally")
74
+ except Exception:
75
+ Logger.error("An unexpected error occured while loading the data", exc_info=True)
76
+ raise
77
+
78
+ return res
79
+
80
+ # ------------------------------------------------------
81
+ def loadSince(
82
+ self, pceIdentifier: str, lastNDays: int = DEFAULT_LAST_N_DAYS, frequencies: Optional[list[Frequency]] = None
83
+ ) -> MeterReadingsByFrequency:
84
+ warnings.warn(
85
+ "Client.loadSince() method will be removed in 2026-01-01. Please migrate to Client.load_since() method",
86
+ DeprecationWarning,
87
+ )
88
+ return self.load_since(pceIdentifier, lastNDays, frequencies)
89
+
90
+ # ------------------------------------------------------
91
+ def loadDateRange(
92
+ self, pceIdentifier: str, startDate: date, endDate: date, frequencies: Optional[list[Frequency]] = None
93
+ ) -> MeterReadingsByFrequency:
94
+ warnings.warn(
95
+ "Client.loadDateRange() method will be removed in 2026-01-01. Please migrate to Client.load_date_range() method",
96
+ DeprecationWarning,
97
+ )
98
+ return self.load_date_range(pceIdentifier, startDate, endDate, frequencies)
@@ -1,52 +1,46 @@
1
1
  import glob
2
- import http.cookiejar
3
2
  import json
4
3
  import logging
5
4
  import os
6
- import time
7
5
  from abc import ABC, abstractmethod
8
6
  from datetime import date, timedelta
9
- from typing import Any, Dict, List, Optional, cast
7
+ from typing import Any, Optional, cast
10
8
 
11
9
  import pandas as pd
12
- from requests import Session
13
10
 
11
+ from pygazpar.api_client import APIClient, ConsumptionType
12
+ from pygazpar.api_client import Frequency as APIClientFrequency
14
13
  from pygazpar.enum import Frequency, PropertyName
15
14
  from pygazpar.excelparser import ExcelParser
16
15
  from pygazpar.jsonparser import JsonParser
17
16
 
18
- SESSION_TOKEN_URL = "https://connexion.grdf.fr/api/v1/authn"
19
- SESSION_TOKEN_PAYLOAD = """{{
20
- "username": "{0}",
21
- "password": "{1}",
22
- "options": {{
23
- "multiOptionalFactorEnroll": "false",
24
- "warnBeforePasswordExpired": "false"
25
- }}
26
- }}"""
27
-
28
- AUTH_TOKEN_URL = "https://connexion.grdf.fr/login/sessionCookieRedirect"
29
- AUTH_TOKEN_PARAMS = """{{
30
- "checkAccountSetupComplete": "true",
31
- "token": "{0}",
32
- "redirectUrl": "https://monespace.grdf.fr"
33
- }}"""
34
-
35
17
  Logger = logging.getLogger(__name__)
36
18
 
37
- MeterReading = Dict[str, Any]
19
+ MeterReading = dict[str, Any]
38
20
 
39
- MeterReadings = List[MeterReading]
21
+ MeterReadings = list[MeterReading]
40
22
 
41
- MeterReadingsByFrequency = Dict[str, MeterReadings]
23
+ MeterReadingsByFrequency = dict[str, MeterReadings]
42
24
 
43
25
 
44
26
  # ------------------------------------------------------------------------------------------------------------
45
27
  class IDataSource(ABC): # pylint: disable=too-few-public-methods
46
28
 
29
+ @abstractmethod
30
+ def login(self):
31
+ pass
32
+
33
+ @abstractmethod
34
+ def logout(self):
35
+ pass
36
+
37
+ @abstractmethod
38
+ def get_pce_identifiers(self) -> list[str]:
39
+ pass
40
+
47
41
  @abstractmethod
48
42
  def load(
49
- self, pceIdentifier: str, startDate: date, endDate: date, frequencies: Optional[List[Frequency]] = None
43
+ self, pceIdentifier: str, startDate: date, endDate: date, frequencies: Optional[list[Frequency]] = None
50
44
  ) -> MeterReadingsByFrequency:
51
45
  pass
52
46
 
@@ -57,65 +51,50 @@ class WebDataSource(IDataSource): # pylint: disable=too-few-public-methods
57
51
  # ------------------------------------------------------
58
52
  def __init__(self, username: str, password: str):
59
53
 
60
- self.__username = username
61
- self.__password = password
54
+ self._api_client = APIClient(username, password)
62
55
 
63
56
  # ------------------------------------------------------
64
- def load(
65
- self, pceIdentifier: str, startDate: date, endDate: date, frequencies: Optional[List[Frequency]] = None
66
- ) -> MeterReadingsByFrequency:
57
+ def login(self):
67
58
 
68
- self._login(self.__username, self.__password) # We ignore the return value.
69
-
70
- res = self._loadFromSession(pceIdentifier, startDate, endDate, frequencies)
71
-
72
- Logger.debug("The data update terminates normally")
73
-
74
- return res
59
+ if not self._api_client.is_logged_in():
60
+ self._api_client.login()
75
61
 
76
62
  # ------------------------------------------------------
77
- def _login(self, username: str, password: str) -> str:
63
+ def logout(self):
78
64
 
79
- session = Session()
80
- session.headers.update({"domain": "grdf.fr"})
81
- session.headers.update({"Content-Type": "application/json"})
82
- session.headers.update({"X-Requested-With": "XMLHttpRequest"})
65
+ if self._api_client.is_logged_in():
66
+ self._api_client.logout()
83
67
 
84
- payload = SESSION_TOKEN_PAYLOAD.format(username, password)
85
-
86
- response = session.post(SESSION_TOKEN_URL, data=payload)
87
-
88
- if response.status_code != 200:
89
- raise ValueError(
90
- f"An error occurred while logging in. Status code: {response.status_code} - {response.text}"
91
- )
68
+ # ------------------------------------------------------
69
+ def get_pce_identifiers(self) -> list[str]:
92
70
 
93
- session_token = response.json().get("sessionToken")
71
+ if not self._api_client.is_logged_in():
72
+ self._api_client.login()
94
73
 
95
- Logger.debug("Session token: %s", session_token)
74
+ pce_list = self._api_client.get_pce_list()
96
75
 
97
- jar = http.cookiejar.CookieJar()
76
+ if pce_list is None:
77
+ return []
98
78
 
99
- self._session = Session() # pylint: disable=attribute-defined-outside-init
100
- self._session.headers.update({"Content-Type": "application/json"})
101
- self._session.headers.update({"X-Requested-With": "XMLHttpRequest"})
79
+ return [pce["idObject"] for pce in pce_list]
102
80
 
103
- params = json.loads(AUTH_TOKEN_PARAMS.format(session_token))
81
+ # ------------------------------------------------------
82
+ def load(
83
+ self, pceIdentifier: str, startDate: date, endDate: date, frequencies: Optional[list[Frequency]] = None
84
+ ) -> MeterReadingsByFrequency:
104
85
 
105
- response = self._session.get(AUTH_TOKEN_URL, params=params, allow_redirects=True, cookies=jar) # type: ignore
86
+ if not self._api_client.is_logged_in():
87
+ self._api_client.login()
106
88
 
107
- if response.status_code != 200:
108
- raise ValueError(
109
- f"An error occurred while getting the auth token. Status code: {response.status_code} - {response.text}"
110
- )
89
+ res = self._loadFromSession(pceIdentifier, startDate, endDate, frequencies)
111
90
 
112
- auth_token = self._session.cookies.get("auth_token", domain="monespace.grdf.fr")
91
+ Logger.debug("The data update terminates normally")
113
92
 
114
- return auth_token # type: ignore
93
+ return res
115
94
 
116
95
  @abstractmethod
117
96
  def _loadFromSession(
118
- self, pceIdentifier: str, startDate: date, endDate: date, frequencies: Optional[List[Frequency]] = None
97
+ self, pceIdentifier: str, startDate: date, endDate: date, frequencies: Optional[list[Frequency]] = None
119
98
  ) -> MeterReadingsByFrequency:
120
99
  pass
121
100
 
@@ -123,8 +102,6 @@ class WebDataSource(IDataSource): # pylint: disable=too-few-public-methods
123
102
  # ------------------------------------------------------------------------------------------------------------
124
103
  class ExcelWebDataSource(WebDataSource): # pylint: disable=too-few-public-methods
125
104
 
126
- DATA_URL = "https://monespace.grdf.fr/api/e-conso/pce/consommation/informatives/telecharger?dateDebut={0}&dateFin={1}&frequence={3}&pceList[]={2}"
127
-
128
105
  DATE_FORMAT = "%Y-%m-%d"
129
106
 
130
107
  FREQUENCY_VALUES = {
@@ -146,7 +123,7 @@ class ExcelWebDataSource(WebDataSource): # pylint: disable=too-few-public-metho
146
123
 
147
124
  # ------------------------------------------------------
148
125
  def _loadFromSession( # pylint: disable=too-many-branches
149
- self, pceIdentifier: str, startDate: date, endDate: date, frequencies: Optional[List[Frequency]] = None
126
+ self, pceIdentifier: str, startDate: date, endDate: date, frequencies: Optional[list[Frequency]] = None
150
127
  ) -> MeterReadingsByFrequency: # pylint: disable=too-many-branches
151
128
 
152
129
  res = {}
@@ -171,33 +148,24 @@ class ExcelWebDataSource(WebDataSource): # pylint: disable=too-few-public-metho
171
148
  frequencyList = list(set(frequencies))
172
149
 
173
150
  for frequency in frequencyList:
174
- # Inject parameters.
175
- downloadUrl = ExcelWebDataSource.DATA_URL.format(
176
- startDate.strftime(ExcelWebDataSource.DATE_FORMAT),
177
- endDate.strftime(ExcelWebDataSource.DATE_FORMAT),
178
- pceIdentifier,
179
- ExcelWebDataSource.FREQUENCY_VALUES[frequency],
180
- )
181
151
 
182
152
  Logger.debug(
183
153
  f"Loading data of frequency {ExcelWebDataSource.FREQUENCY_VALUES[frequency]} from {startDate.strftime(ExcelWebDataSource.DATE_FORMAT)} to {endDate.strftime(ExcelWebDataSource.DATE_FORMAT)}"
184
154
  )
185
155
 
186
- # Retry mechanism.
187
- retry = 10
188
- while retry > 0:
189
-
190
- try:
191
- self.__downloadFile(self._session, downloadUrl, self.__tmpDirectory)
192
- break
193
- except Exception as e: # pylint: disable=broad-exception-caught
156
+ response = self._api_client.get_pce_consumption_excelsheet(
157
+ ConsumptionType.INFORMATIVE,
158
+ startDate,
159
+ endDate,
160
+ APIClientFrequency(ExcelWebDataSource.FREQUENCY_VALUES[frequency]),
161
+ [pceIdentifier],
162
+ )
194
163
 
195
- if retry == 1:
196
- raise e
164
+ filename = response["filename"]
165
+ content = response["content"]
197
166
 
198
- Logger.error("An error occurred while loading data. Retry in 3 seconds.")
199
- time.sleep(3)
200
- retry -= 1
167
+ with open(f"{self.__tmpDirectory}/{filename}", "wb") as file:
168
+ file.write(content)
201
169
 
202
170
  # Load the XLSX file into the data structure
203
171
  file_list = glob.glob(data_file_path_pattern)
@@ -221,26 +189,6 @@ class ExcelWebDataSource(WebDataSource): # pylint: disable=too-few-public-metho
221
189
 
222
190
  return res
223
191
 
224
- # ------------------------------------------------------
225
- def __downloadFile(self, session: Session, url: str, path: str):
226
-
227
- response = session.get(url)
228
-
229
- if "text/html" in response.headers.get("Content-Type"): # type: ignore
230
- raise ValueError("An error occurred while loading data. Please check your credentials.")
231
-
232
- if response.status_code != 200:
233
- raise ValueError(
234
- f"An error occurred while loading data. Status code: {response.status_code} - {response.text}"
235
- )
236
-
237
- response.raise_for_status()
238
-
239
- filename = response.headers["Content-Disposition"].split("filename=")[1]
240
-
241
- with open(f"{path}/{filename}", "wb") as file:
242
- file.write(response.content)
243
-
244
192
 
245
193
  # ------------------------------------------------------------------------------------------------------------
246
194
  class ExcelFileDataSource(IDataSource): # pylint: disable=too-few-public-methods
@@ -249,8 +197,22 @@ class ExcelFileDataSource(IDataSource): # pylint: disable=too-few-public-method
249
197
 
250
198
  self.__excelFile = excelFile
251
199
 
200
+ # ------------------------------------------------------
201
+ def login(self):
202
+ pass
203
+
204
+ # ------------------------------------------------------
205
+ def logout(self):
206
+ pass
207
+
208
+ # ------------------------------------------------------
209
+ def get_pce_identifiers(self) -> list[str]:
210
+
211
+ return ["0123456789"]
212
+
213
+ # ------------------------------------------------------
252
214
  def load(
253
- self, pceIdentifier: str, startDate: date, endDate: date, frequencies: Optional[List[Frequency]] = None
215
+ self, pceIdentifier: str, startDate: date, endDate: date, frequencies: Optional[list[Frequency]] = None
254
216
  ) -> MeterReadingsByFrequency:
255
217
 
256
218
  res = {}
@@ -275,21 +237,16 @@ class ExcelFileDataSource(IDataSource): # pylint: disable=too-few-public-method
275
237
  # ------------------------------------------------------------------------------------------------------------
276
238
  class JsonWebDataSource(WebDataSource): # pylint: disable=too-few-public-methods
277
239
 
278
- DATA_URL = (
279
- "https://monespace.grdf.fr/api/e-conso/pce/consommation/informatives?dateDebut={0}&dateFin={1}&pceList[]={2}"
280
- )
281
-
282
- TEMPERATURES_URL = "https://monespace.grdf.fr/api/e-conso/pce/{0}/meteo?dateFinPeriode={1}&nbJours={2}"
283
-
284
240
  INPUT_DATE_FORMAT = "%Y-%m-%d"
285
241
 
286
242
  OUTPUT_DATE_FORMAT = "%d/%m/%Y"
287
243
 
244
+ # ------------------------------------------------------
288
245
  def _loadFromSession(
289
- self, pceIdentifier: str, startDate: date, endDate: date, frequencies: Optional[List[Frequency]] = None
246
+ self, pceIdentifier: str, startDate: date, endDate: date, frequencies: Optional[list[Frequency]] = None
290
247
  ) -> MeterReadingsByFrequency:
291
248
 
292
- res = {}
249
+ res = dict[str, Any]()
293
250
 
294
251
  computeByFrequency = {
295
252
  Frequency.HOURLY: FrequencyConverter.computeHourly,
@@ -299,56 +256,30 @@ class JsonWebDataSource(WebDataSource): # pylint: disable=too-few-public-method
299
256
  Frequency.YEARLY: FrequencyConverter.computeYearly,
300
257
  }
301
258
 
302
- # Data URL: Inject parameters.
303
- downloadUrl = JsonWebDataSource.DATA_URL.format(
304
- startDate.strftime(JsonWebDataSource.INPUT_DATE_FORMAT),
305
- endDate.strftime(JsonWebDataSource.INPUT_DATE_FORMAT),
306
- pceIdentifier,
307
- )
308
-
309
- # Retry mechanism.
310
- retry = 10
311
- while retry > 0:
312
-
313
- try:
314
- response = self._session.get(downloadUrl)
315
-
316
- if "text/html" in response.headers.get("Content-Type"): # type: ignore
317
- raise ValueError("An error occurred while loading data. Please check your credentials.")
318
-
319
- if response.status_code != 200:
320
- raise ValueError(
321
- f"An error occurred while loading data. Status code: {response.status_code} - {response.text}"
322
- )
323
-
324
- break
325
- except Exception as e: # pylint: disable=broad-exception-caught
326
-
327
- if retry == 1:
328
- raise e
329
-
330
- Logger.error("An error occurred while loading data. Retry in 3 seconds.")
331
- time.sleep(3)
332
- retry -= 1
333
-
334
- data = response.text
259
+ data = self._api_client.get_pce_consumption(ConsumptionType.INFORMATIVE, startDate, endDate, [pceIdentifier])
335
260
 
336
261
  Logger.debug("Json meter data: %s", data)
337
262
 
338
263
  # Temperatures URL: Inject parameters.
339
264
  endDate = date.today() - timedelta(days=1) if endDate >= date.today() else endDate
340
- days = min((endDate - startDate).days, 730)
341
- temperaturesUrl = JsonWebDataSource.TEMPERATURES_URL.format(
342
- pceIdentifier, endDate.strftime(JsonWebDataSource.INPUT_DATE_FORMAT), days
343
- )
265
+ days = max(
266
+ min((endDate - startDate).days, 730), 10
267
+ ) # At least 10 days, at most 730 days, to avoid HTTP 500 error.
344
268
 
345
269
  # Get weather data.
346
- temperatures = self._session.get(temperaturesUrl).text
270
+ try:
271
+ temperatures = self._api_client.get_pce_meteo(endDate, days, pceIdentifier)
272
+ except Exception: # pylint: disable=broad-except
273
+ # Not a blocking error.
274
+ temperatures = None
347
275
 
348
276
  Logger.debug("Json temperature data: %s", temperatures)
349
277
 
350
278
  # Transform all the data into the target structure.
351
- daily = JsonParser.parse(data, temperatures, pceIdentifier)
279
+ if data is None or len(data) == 0:
280
+ return res
281
+
282
+ daily = JsonParser.parse(json.dumps(data), json.dumps(temperatures), pceIdentifier)
352
283
 
353
284
  Logger.debug("Processed daily data: %s", daily)
354
285
 
@@ -368,13 +299,28 @@ class JsonWebDataSource(WebDataSource): # pylint: disable=too-few-public-method
368
299
  # ------------------------------------------------------------------------------------------------------------
369
300
  class JsonFileDataSource(IDataSource): # pylint: disable=too-few-public-methods
370
301
 
302
+ # ------------------------------------------------------
371
303
  def __init__(self, consumptionJsonFile: str, temperatureJsonFile):
372
304
 
373
305
  self.__consumptionJsonFile = consumptionJsonFile
374
306
  self.__temperatureJsonFile = temperatureJsonFile
375
307
 
308
+ # ------------------------------------------------------
309
+ def login(self):
310
+ pass
311
+
312
+ # ------------------------------------------------------
313
+ def logout(self):
314
+ pass
315
+
316
+ # ------------------------------------------------------
317
+ def get_pce_identifiers(self) -> list[str]:
318
+
319
+ return ["0123456789"]
320
+
321
+ # ------------------------------------------------------
376
322
  def load(
377
- self, pceIdentifier: str, startDate: date, endDate: date, frequencies: Optional[List[Frequency]] = None
323
+ self, pceIdentifier: str, startDate: date, endDate: date, frequencies: Optional[list[Frequency]] = None
378
324
  ) -> MeterReadingsByFrequency:
379
325
 
380
326
  res = {}
@@ -409,12 +355,27 @@ class TestDataSource(IDataSource): # pylint: disable=too-few-public-methods
409
355
 
410
356
  __test__ = False # Will not be discovered as a test
411
357
 
358
+ # ------------------------------------------------------
412
359
  def __init__(self):
413
360
 
414
361
  pass
415
362
 
363
+ # ------------------------------------------------------
364
+ def login(self):
365
+ pass
366
+
367
+ # ------------------------------------------------------
368
+ def logout(self):
369
+ pass
370
+
371
+ # ------------------------------------------------------
372
+ def get_pce_identifiers(self) -> list[str]:
373
+
374
+ return ["0123456789"]
375
+
376
+ # ------------------------------------------------------
416
377
  def load(
417
- self, pceIdentifier: str, startDate: date, endDate: date, frequencies: Optional[List[Frequency]] = None
378
+ self, pceIdentifier: str, startDate: date, endDate: date, frequencies: Optional[list[Frequency]] = None
418
379
  ) -> MeterReadingsByFrequency:
419
380
 
420
381
  res = dict[str, Any]()
@@ -440,7 +401,7 @@ class TestDataSource(IDataSource): # pylint: disable=too-few-public-methods
440
401
  )
441
402
 
442
403
  with open(dataSampleFilename, mode="r", encoding="utf-8") as jsonFile:
443
- res[frequency.value] = cast(List[Dict[PropertyName, Any]], json.load(jsonFile))
404
+ res[frequency.value] = cast(list[dict[PropertyName, Any]], json.load(jsonFile))
444
405
 
445
406
  return res
446
407
 
@@ -465,19 +426,19 @@ class FrequencyConverter:
465
426
 
466
427
  # ------------------------------------------------------
467
428
  @staticmethod
468
- def computeHourly(daily: List[Dict[str, Any]]) -> List[Dict[str, Any]]: # pylint: disable=unused-argument
429
+ def computeHourly(daily: list[dict[str, Any]]) -> list[dict[str, Any]]: # pylint: disable=unused-argument
469
430
 
470
431
  return []
471
432
 
472
433
  # ------------------------------------------------------
473
434
  @staticmethod
474
- def computeDaily(daily: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
435
+ def computeDaily(daily: list[dict[str, Any]]) -> list[dict[str, Any]]:
475
436
 
476
437
  return daily
477
438
 
478
439
  # ------------------------------------------------------
479
440
  @staticmethod
480
- def computeWeekly(daily: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
441
+ def computeWeekly(daily: list[dict[str, Any]]) -> list[dict[str, Any]]:
481
442
 
482
443
  df = pd.DataFrame(daily)
483
444
 
@@ -533,13 +494,13 @@ class FrequencyConverter:
533
494
  # Select target columns.
534
495
  df = df[["time_period", "start_index_m3", "end_index_m3", "volume_m3", "energy_kwh", "timestamp"]]
535
496
 
536
- res = cast(List[Dict[str, Any]], df.to_dict("records"))
497
+ res = cast(list[dict[str, Any]], df.to_dict("records"))
537
498
 
538
499
  return res
539
500
 
540
501
  # ------------------------------------------------------
541
502
  @staticmethod
542
- def computeMonthly(daily: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
503
+ def computeMonthly(daily: list[dict[str, Any]]) -> list[dict[str, Any]]:
543
504
 
544
505
  df = pd.DataFrame(daily)
545
506
 
@@ -581,13 +542,13 @@ class FrequencyConverter:
581
542
  # Select target columns.
582
543
  df = df[["time_period", "start_index_m3", "end_index_m3", "volume_m3", "energy_kwh", "timestamp"]]
583
544
 
584
- res = cast(List[Dict[str, Any]], df.to_dict("records"))
545
+ res = cast(list[dict[str, Any]], df.to_dict("records"))
585
546
 
586
547
  return res
587
548
 
588
549
  # ------------------------------------------------------
589
550
  @staticmethod
590
- def computeYearly(daily: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
551
+ def computeYearly(daily: list[dict[str, Any]]) -> list[dict[str, Any]]:
591
552
 
592
553
  df = pd.DataFrame(daily)
593
554
 
@@ -624,6 +585,6 @@ class FrequencyConverter:
624
585
  # Select target columns.
625
586
  df = df[["time_period", "start_index_m3", "end_index_m3", "volume_m3", "energy_kwh", "timestamp"]]
626
587
 
627
- res = cast(List[Dict[str, Any]], df.to_dict("records"))
588
+ res = cast(list[dict[str, Any]], df.to_dict("records"))
628
589
 
629
590
  return res
@@ -1,6 +1,6 @@
1
1
  import logging
2
2
  from datetime import datetime
3
- from typing import Any, Dict, List
3
+ from typing import Any
4
4
 
5
5
  from openpyxl import load_workbook
6
6
  from openpyxl.cell.cell import Cell
@@ -18,7 +18,7 @@ class ExcelParser: # pylint: disable=too-few-public-methods
18
18
 
19
19
  # ------------------------------------------------------
20
20
  @staticmethod
21
- def parse(dataFilename: str, dataReadingFrequency: Frequency) -> List[Dict[str, Any]]:
21
+ def parse(dataFilename: str, dataReadingFrequency: Frequency) -> list[dict[str, Any]]:
22
22
 
23
23
  parseByFrequency = {
24
24
  Frequency.HOURLY: ExcelParser.__parseHourly,
@@ -43,7 +43,7 @@ class ExcelParser: # pylint: disable=too-few-public-methods
43
43
 
44
44
  # ------------------------------------------------------
45
45
  @staticmethod
46
- def __fillRow(row: Dict, propertyName: str, cell: Cell, isNumber: bool):
46
+ def __fillRow(row: dict, propertyName: str, cell: Cell, isNumber: bool):
47
47
 
48
48
  if cell.value is not None:
49
49
  if isNumber:
@@ -57,12 +57,12 @@ class ExcelParser: # pylint: disable=too-few-public-methods
57
57
 
58
58
  # ------------------------------------------------------
59
59
  @staticmethod
60
- def __parseHourly(worksheet: Worksheet) -> List[Dict[str, Any]]: # pylint: disable=unused-argument
60
+ def __parseHourly(worksheet: Worksheet) -> list[dict[str, Any]]: # pylint: disable=unused-argument
61
61
  return []
62
62
 
63
63
  # ------------------------------------------------------
64
64
  @staticmethod
65
- def __parseDaily(worksheet: Worksheet) -> List[Dict[str, Any]]:
65
+ def __parseDaily(worksheet: Worksheet) -> list[dict[str, Any]]:
66
66
 
67
67
  res = []
68
68
 
@@ -91,7 +91,7 @@ class ExcelParser: # pylint: disable=too-few-public-methods
91
91
 
92
92
  # ------------------------------------------------------
93
93
  @staticmethod
94
- def __parseWeekly(worksheet: Worksheet) -> List[Dict[str, Any]]:
94
+ def __parseWeekly(worksheet: Worksheet) -> list[dict[str, Any]]:
95
95
 
96
96
  res = []
97
97
 
@@ -115,7 +115,7 @@ class ExcelParser: # pylint: disable=too-few-public-methods
115
115
 
116
116
  # ------------------------------------------------------
117
117
  @staticmethod
118
- def __parseMonthly(worksheet: Worksheet) -> List[Dict[str, Any]]:
118
+ def __parseMonthly(worksheet: Worksheet) -> list[dict[str, Any]]:
119
119
 
120
120
  res = []
121
121
 
@@ -1,7 +1,7 @@
1
1
  import json
2
2
  import logging
3
3
  from datetime import datetime
4
- from typing import Any, Dict, List
4
+ from typing import Any
5
5
 
6
6
  from pygazpar.enum import PropertyName
7
7
 
@@ -17,7 +17,7 @@ class JsonParser: # pylint: disable=too-few-public-methods
17
17
 
18
18
  # ------------------------------------------------------
19
19
  @staticmethod
20
- def parse(jsonStr: str, temperaturesStr: str, pceIdentifier: str) -> List[Dict[str, Any]]:
20
+ def parse(jsonStr: str, temperaturesStr: str, pceIdentifier: str) -> list[dict[str, Any]]:
21
21
 
22
22
  res = []
23
23
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pygazpar"
3
- version = "1.3.0b2"
3
+ version = "1.3.0b3"
4
4
  description = "Python library to download gas consumption from a GrDF (French Gas Company) account"
5
5
  license = { file = "LICENSE" }
6
6
  readme = "README.md"
@@ -1,64 +0,0 @@
1
- import logging
2
- from datetime import date, timedelta
3
- from typing import List, Optional
4
-
5
- from pygazpar.datasource import IDataSource, MeterReadingsByFrequency
6
- from pygazpar.enum import Frequency
7
-
8
- AUTH_NONCE_URL = "https://monespace.grdf.fr/client/particulier/accueil"
9
- LOGIN_URL = "https://login.monespace.grdf.fr/sofit-account-api/api/v1/auth"
10
- LOGIN_HEADER = {"domain": "grdf.fr"}
11
- LOGIN_PAYLOAD = """{{
12
- "email": "{0}",
13
- "password": "{1}",
14
- "capp": "meg",
15
- "goto": "https://sofa-connexion.grdf.fr:443/openam/oauth2/externeGrdf/authorize?response_type=code&scope=openid%20profile%20email%20infotravaux%20%2Fv1%2Faccreditation%20%2Fv1%2Faccreditations%20%2Fdigiconso%2Fv1%20%2Fdigiconso%2Fv1%2Fconsommations%20new_meg&client_id=prod_espaceclient&state=0&redirect_uri=https%3A%2F%2Fmonespace.grdf.fr%2F_codexch&nonce={2}&by_pass_okta=1&capp=meg"}}"""
16
- DATA_URL = "https://monespace.grdf.fr/api/e-conso/pce/consommation/informatives/telecharger?dateDebut={1}&dateFin={2}&frequence={0}&pceList%5B%5D={3}"
17
- DATA_FILENAME = "Donnees_informatives_*.xlsx"
18
-
19
- DEFAULT_TMP_DIRECTORY = "/tmp"
20
- DEFAULT_LAST_N_DAYS = 365
21
-
22
-
23
- Logger = logging.getLogger(__name__)
24
-
25
-
26
- # ------------------------------------------------------------------------------------------------------------
27
- class Client:
28
-
29
- # ------------------------------------------------------
30
- def __init__(self, dataSource: IDataSource):
31
- self.__dataSource = dataSource
32
-
33
- # ------------------------------------------------------
34
- def loadSince(
35
- self, pceIdentifier: str, lastNDays: int = DEFAULT_LAST_N_DAYS, frequencies: Optional[List[Frequency]] = None
36
- ) -> MeterReadingsByFrequency:
37
-
38
- try:
39
- endDate = date.today()
40
- startDate = endDate + timedelta(days=-lastNDays)
41
-
42
- res = self.loadDateRange(pceIdentifier, startDate, endDate, frequencies)
43
- except Exception:
44
- Logger.error("An unexpected error occured while loading the data", exc_info=True)
45
- raise
46
-
47
- return res
48
-
49
- # ------------------------------------------------------
50
- def loadDateRange(
51
- self, pceIdentifier: str, startDate: date, endDate: date, frequencies: Optional[List[Frequency]] = None
52
- ) -> MeterReadingsByFrequency:
53
-
54
- Logger.debug("Start loading the data...")
55
-
56
- try:
57
- res = self.__dataSource.load(pceIdentifier, startDate, endDate, frequencies)
58
-
59
- Logger.debug("The data load terminates normally")
60
- except Exception:
61
- Logger.error("An unexpected error occured while loading the data", exc_info=True)
62
- raise
63
-
64
- return res
File without changes
File without changes