pygazpar 1.3.0a2__py310-none-any.whl → 1.3.0a6__py310-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.
pygazpar/datasource.py CHANGED
@@ -2,7 +2,9 @@ import logging
2
2
  import glob
3
3
  import os
4
4
  import json
5
+ import time
5
6
  import pandas as pd
7
+ import http.cookiejar
6
8
  from abc import ABC, abstractmethod
7
9
  from typing import Any, List, Dict, cast, Optional
8
10
  from requests import Session
@@ -11,15 +13,22 @@ from pygazpar.enum import Frequency, PropertyName
11
13
  from pygazpar.excelparser import ExcelParser
12
14
  from pygazpar.jsonparser import JsonParser
13
15
 
14
- AUTH_NONCE_URL = "https://monespace.grdf.fr/client/particulier/accueil"
15
- LOGIN_URL = "https://login.monespace.grdf.fr/sofit-account-api/api/v1/auth"
16
- LOGIN_HEADER = {"domain": "grdf.fr"}
17
- LOGIN_PAYLOAD = """{{
18
- "email": "{0}",
16
+ SESSION_TOKEN_URL = "https://connexion.grdf.fr/api/v1/authn"
17
+ SESSION_TOKEN_PAYLOAD = """{{
18
+ "username": "{0}",
19
19
  "password": "{1}",
20
- "capp": "meg",
21
- "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"}}"""
22
-
20
+ "options": {{
21
+ "multiOptionalFactorEnroll": "false",
22
+ "warnBeforePasswordExpired": "false"
23
+ }}
24
+ }}"""
25
+
26
+ AUTH_TOKEN_URL = "https://connexion.grdf.fr/login/sessionCookieRedirect"
27
+ AUTH_TOKEN_PARAMS = """{{
28
+ "checkAccountSetupComplete": "true",
29
+ "token": "{0}",
30
+ "redirectUrl": "https://monespace.grdf.fr"
31
+ }}"""
23
32
 
24
33
  Logger = logging.getLogger(__name__)
25
34
 
@@ -50,56 +59,59 @@ class WebDataSource(IDataSource):
50
59
  # ------------------------------------------------------
51
60
  def load(self, pceIdentifier: str, startDate: date, endDate: date, frequencies: Optional[List[Frequency]] = None) -> MeterReadingsByFrequency:
52
61
 
53
- session = Session()
54
-
55
- session.headers.update(LOGIN_HEADER)
56
-
57
- self._login(session, self.__username, self.__password)
62
+ auth_token = self._login(self.__username, self.__password)
58
63
 
59
- res = self._loadFromSession(session, pceIdentifier, startDate, endDate, frequencies)
64
+ res = self._loadFromSession(auth_token, pceIdentifier, startDate, endDate, frequencies)
60
65
 
61
66
  Logger.debug("The data update terminates normally")
62
67
 
63
68
  return res
64
69
 
65
70
  # ------------------------------------------------------
66
- def _login(self, session: Session, username: str, password: str):
71
+ def _login(self, username: str, password: str) -> str:
67
72
 
68
- # Get auth_nonce token.
69
- session.get(AUTH_NONCE_URL)
70
- if "auth_nonce" not in session.cookies:
71
- raise Exception("Login error: Cannot get auth_nonce token")
72
- auth_nonce = session.cookies.get("auth_nonce")
73
+ session = Session()
74
+ session.headers.update({"domain": "grdf.fr"})
75
+ session.headers.update({"Content-Type": "application/json"})
76
+ session.headers.update({"X-Requested-With": "XMLHttpRequest"})
73
77
 
74
- # Build the login payload as a json string.
75
- payload = LOGIN_PAYLOAD.format(username, password, auth_nonce)
78
+ payload = SESSION_TOKEN_PAYLOAD.format(username, password)
76
79
 
77
- # Build the login payload as a python object.
78
- data = json.loads(payload)
80
+ response = session.post(SESSION_TOKEN_URL, data=payload)
79
81
 
80
- # Send the login command.
81
- response = session.post(LOGIN_URL, data=data)
82
+ if response.status_code != 200:
83
+ raise Exception(f"An error occurred while logging in. Status code: {response.status_code} - {response.text}")
82
84
 
83
- # Check login result.
84
- loginData = response.json()
85
+ session_token = response.json().get("sessionToken")
85
86
 
86
- response.raise_for_status()
87
+ Logger.debug("Session token: %s", session_token)
88
+
89
+ jar = http.cookiejar.CookieJar()
87
90
 
88
- if "status" in loginData and "error" in loginData and loginData["status"] >= 400:
89
- raise Exception(f"{loginData['error']} ({loginData['status']})")
91
+ session = Session()
92
+ session.headers.update({"Content-Type": "application/json"})
93
+ session.headers.update({"X-Requested-With": "XMLHttpRequest"})
94
+
95
+ params = json.loads(AUTH_TOKEN_PARAMS.format(session_token))
96
+
97
+ response = session.get(AUTH_TOKEN_URL, params=params, allow_redirects=True, cookies=jar)
98
+
99
+ if response.status_code != 200:
100
+ raise Exception(f"An error occurred while getting the auth token. Status code: {response.status_code} - {response.text}")
90
101
 
91
- if "state" in loginData and loginData["state"] != "SUCCESS":
92
- raise Exception(loginData["error"])
102
+ auth_token = session.cookies.get("auth_token", domain="monespace.grdf.fr")
103
+
104
+ return auth_token
93
105
 
94
106
  @abstractmethod
95
- def _loadFromSession(self, session: Session, pceIdentifier: str, startDate: date, endDate: date, frequencies: Optional[List[Frequency]] = None) -> MeterReadingsByFrequency:
107
+ def _loadFromSession(self, auth_token: str, pceIdentifier: str, startDate: date, endDate: date, frequencies: Optional[List[Frequency]] = None) -> MeterReadingsByFrequency:
96
108
  pass
97
109
 
98
110
 
99
111
  # ------------------------------------------------------------------------------------------------------------
100
112
  class ExcelWebDataSource(WebDataSource):
101
113
 
102
- DATA_URL = "https://monespace.grdf.fr/api/e-conso/pce/consommation/informatives/telecharger?dateDebut={0}&dateFin={1}&frequence={3}&pceList%5B%5D={2}"
114
+ DATA_URL = "https://monespace.grdf.fr/api/e-conso/pce/consommation/informatives/telecharger?dateDebut={0}&dateFin={1}&frequence={3}&pceList[]={2}"
103
115
 
104
116
  DATE_FORMAT = "%Y-%m-%d"
105
117
 
@@ -121,7 +133,7 @@ class ExcelWebDataSource(WebDataSource):
121
133
  self.__tmpDirectory = tmpDirectory
122
134
 
123
135
  # ------------------------------------------------------
124
- def _loadFromSession(self, session: Session, pceIdentifier: str, startDate: date, endDate: date, frequencies: Optional[List[Frequency]] = None) -> MeterReadingsByFrequency:
136
+ def _loadFromSession(self, auth_token: str, pceIdentifier: str, startDate: date, endDate: date, frequencies: Optional[List[Frequency]] = None) -> MeterReadingsByFrequency:
125
137
 
126
138
  res = {}
127
139
 
@@ -145,11 +157,31 @@ class ExcelWebDataSource(WebDataSource):
145
157
  # Inject parameters.
146
158
  downloadUrl = ExcelWebDataSource.DATA_URL.format(startDate.strftime(ExcelWebDataSource.DATE_FORMAT), endDate.strftime(ExcelWebDataSource.DATE_FORMAT), pceIdentifier, ExcelWebDataSource.FREQUENCY_VALUES[frequency])
147
159
 
148
- session.get(downloadUrl) # First request does not return anything : strange...
149
-
150
160
  Logger.debug(f"Loading data of frequency {ExcelWebDataSource.FREQUENCY_VALUES[frequency]} from {startDate.strftime(ExcelWebDataSource.DATE_FORMAT)} to {endDate.strftime(ExcelWebDataSource.DATE_FORMAT)}")
151
161
 
152
- self.__downloadFile(session, downloadUrl, self.__tmpDirectory)
162
+ # Retry mechanism.
163
+ retry = 10
164
+ while retry > 0:
165
+
166
+ # Create a session.
167
+ session = Session()
168
+ session.headers.update({"Host": "monespace.grdf.fr"})
169
+ session.headers.update({"Domain": "grdf.fr"})
170
+ session.headers.update({"X-Requested-With": "XMLHttpRequest"})
171
+ session.headers.update({"Accept": "application/json"})
172
+ session.cookies.set("auth_token", auth_token, domain="monespace.grdf.fr")
173
+
174
+ try:
175
+ self.__downloadFile(session, downloadUrl, self.__tmpDirectory)
176
+ break
177
+ except Exception as e:
178
+
179
+ if retry == 1:
180
+ raise e
181
+
182
+ Logger.error("An error occurred while loading data. Retry in 3 seconds.")
183
+ time.sleep(3)
184
+ retry -= 1
153
185
 
154
186
  # Load the XLSX file into the data structure
155
187
  file_list = glob.glob(data_file_path_pattern)
@@ -159,7 +191,11 @@ class ExcelWebDataSource(WebDataSource):
159
191
 
160
192
  for filename in file_list:
161
193
  res[frequency.value] = ExcelParser.parse(filename, frequency if frequency != Frequency.YEARLY else Frequency.DAILY)
162
- os.remove(filename)
194
+ try:
195
+ # openpyxl does not close the file properly.
196
+ os.remove(filename)
197
+ except Exception:
198
+ pass
163
199
 
164
200
  # We compute yearly from daily data.
165
201
  if frequency == Frequency.YEARLY:
@@ -172,6 +208,12 @@ class ExcelWebDataSource(WebDataSource):
172
208
 
173
209
  response = session.get(url)
174
210
 
211
+ if "text/html" in response.headers.get("Content-Type"):
212
+ raise Exception("An error occurred while loading data. Please check your credentials.")
213
+
214
+ if response.status_code != 200:
215
+ raise Exception(f"An error occurred while loading data. Status code: {response.status_code} - {response.text}")
216
+
175
217
  response.raise_for_status()
176
218
 
177
219
  filename = response.headers["Content-Disposition"].split("filename=")[1]
@@ -210,7 +252,7 @@ class ExcelFileDataSource(IDataSource):
210
252
  # ------------------------------------------------------------------------------------------------------------
211
253
  class JsonWebDataSource(WebDataSource):
212
254
 
213
- DATA_URL = "https://monespace.grdf.fr/api/e-conso/pce/consommation/informatives?dateDebut={0}&dateFin={1}&pceList%5B%5D={2}"
255
+ DATA_URL = "https://monespace.grdf.fr/api/e-conso/pce/consommation/informatives?dateDebut={0}&dateFin={1}&pceList[]={2}"
214
256
 
215
257
  TEMPERATURES_URL = "https://monespace.grdf.fr/api/e-conso/pce/{0}/meteo?dateFinPeriode={1}&nbJours={2}"
216
258
 
@@ -222,7 +264,7 @@ class JsonWebDataSource(WebDataSource):
222
264
 
223
265
  super().__init__(username, password)
224
266
 
225
- def _loadFromSession(self, session: Session, pceIdentifier: str, startDate: date, endDate: date, frequencies: Optional[List[Frequency]] = None) -> MeterReadingsByFrequency:
267
+ def _loadFromSession(self, auth_token: str, pceIdentifier: str, startDate: date, endDate: date, frequencies: Optional[List[Frequency]] = None) -> MeterReadingsByFrequency:
226
268
 
227
269
  res = {}
228
270
 
@@ -237,11 +279,38 @@ class JsonWebDataSource(WebDataSource):
237
279
  # Data URL: Inject parameters.
238
280
  downloadUrl = JsonWebDataSource.DATA_URL.format(startDate.strftime(JsonWebDataSource.INPUT_DATE_FORMAT), endDate.strftime(JsonWebDataSource.INPUT_DATE_FORMAT), pceIdentifier)
239
281
 
240
- # First request never returns data.
241
- session.get(downloadUrl)
282
+ # Retry mechanism.
283
+ retry = 10
284
+ while retry > 0:
285
+
286
+ # Create a session.
287
+ session = Session()
288
+ session.headers.update({"Host": "monespace.grdf.fr"})
289
+ session.headers.update({"Domain": "grdf.fr"})
290
+ session.headers.update({"X-Requested-With": "XMLHttpRequest"})
291
+ session.headers.update({"Accept": "application/json"})
292
+ session.cookies.set("auth_token", auth_token, domain="monespace.grdf.fr")
293
+
294
+ try:
295
+ response = session.get(downloadUrl)
296
+
297
+ if "text/html" in response.headers.get("Content-Type"):
298
+ raise Exception("An error occurred while loading data. Please check your credentials.")
299
+
300
+ if response.status_code != 200:
301
+ raise Exception(f"An error occurred while loading data. Status code: {response.status_code} - {response.text}")
302
+
303
+ break
304
+ except Exception as e:
305
+
306
+ if retry == 1:
307
+ raise e
308
+
309
+ Logger.error("An error occurred while loading data. Retry in 3 seconds.")
310
+ time.sleep(3)
311
+ retry -= 1
242
312
 
243
- # Get consumption data.
244
- data = session.get(downloadUrl).text
313
+ data = response.text
245
314
 
246
315
  # Temperatures URL: Inject parameters.
247
316
  endDate = date.today() - timedelta(days=1) if endDate >= date.today() else endDate
pygazpar/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "1.3.0a2"
1
+ __version__ = "1.3.0a6"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pygazpar
3
- Version: 1.3.0a2
3
+ Version: 1.3.0a6
4
4
  Summary: Retrieve gas consumption from GrDF web site (French Gas Company)
5
5
  Home-page: https://github.com/ssenart/pygazpar
6
6
  Download-URL: https://github.com/ssenart/pygazpar/releases
@@ -25,8 +25,8 @@ Classifier: Programming Language :: Python :: 3.11
25
25
  Requires-Python: >=3.7
26
26
  Description-Content-Type: text/markdown
27
27
  License-File: LICENSE.md
28
- Requires-Dist: openpyxl (>=2.6.3)
29
- Requires-Dist: requests (>=2.26.0)
28
+ Requires-Dist: openpyxl >=2.6.3
29
+ Requires-Dist: requests >=2.26.0
30
30
  Requires-Dist: pandas
31
31
 
32
32
  # PyGazpar
@@ -214,9 +214,12 @@ All notable changes to this project will be documented in this file.
214
214
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
215
215
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
216
216
 
217
- ## [1.2.1](https://github.com/ssenart/PyGazpar/compare/1.2.0...1.2.1) - 2022-12-28
217
+
218
+ ## [1.2.1](https://github.com/ssenart/PyGazpar/compare/1.2.0...1.2.1) - 2024-05-04
218
219
 
219
220
  ### Fixed
221
+ - [#64](https://github.com/ssenart/PyGazpar/issues/64): [Issue] Captcha failed issue.
222
+
220
223
  - [#63](https://github.com/ssenart/PyGazpar/issues/63): [Bug] If the latest received consumption is Sunday, then the last weekly period is duplicated.
221
224
 
222
225
  ## [1.2.0](https://github.com/ssenart/PyGazpar/compare/1.1.6...1.2.0) - 2022-12-16
@@ -1,11 +1,11 @@
1
1
  pygazpar/__init__.py,sha256=qshO_XZbDA2Wrt80ABDs0MoScqJytClAuIJjAnILglk,309
2
2
  pygazpar/__main__.py,sha256=Pt3PInX7QiWcs0aBKZN90NTaU8KFnrQiZ5Hsow1eR5U,3177
3
3
  pygazpar/client.py,sha256=JdVm0jZbeibwtTumcRbUSFadfXnCUClPMjL95_J6p5Y,2595
4
- pygazpar/datasource.py,sha256=B2mXUGpgq7MafYKZ1mrfq1z-U9fTgwLdTEaagTIihEU,18957
4
+ pygazpar/datasource.py,sha256=gqOREo9OQaBmqWgaDIgShJrP1dZNVFJoIxUu9Rfxmec,21374
5
5
  pygazpar/enum.py,sha256=3ZCk4SziXF6pxgP3MuQ1qxYfqB3X5DOV8Rtd0GHsK9w,898
6
6
  pygazpar/excelparser.py,sha256=glWlbj22pxYjHGKurOFmhzcVAoWCvfOHn7_Y6GgHUPo,5915
7
7
  pygazpar/jsonparser.py,sha256=AWdU3h7UohsOov8HpeP8GNuqcnDmM4r3I7-CI_crDvA,1804
8
- pygazpar/version.py,sha256=0Q-MEvpw9O5iUPwcmISVzmckFYWXa-XeZODFDjKaSvE,24
8
+ pygazpar/version.py,sha256=rcyBYFWxUArZ5y56YkLypabOiwSZfCl48nXHK0ebB20,24
9
9
  pygazpar/resources/daily_data_sample.json,sha256=YJovtrNUMs257magTfyxiewLmecySFypcelbGFUUeT8,199583
10
10
  pygazpar/resources/hourly_data_sample.json,sha256=N1F-Xz3GaBn2H1p7uKzhkhKCQV8QVR0t76XD6wmFtXA,3
11
11
  pygazpar/resources/monthly_data_sample.json,sha256=yrr4SqrB2MubeVU2HX_FRDZKHIhC0LXCqkO1iqnFWcg,3351
@@ -16,12 +16,12 @@ samples/excelSample.py,sha256=ltAl-bBz9-U9YI802JpcIswra-vDS7tR_KL5VNdxJ5c,765
16
16
  samples/jsonSample.py,sha256=sYAIusdEJhZdwDAMgHqoWcwDR0FA2eWhSt_2gL_mJRk,736
17
17
  samples/testSample.py,sha256=UeirdEtezHwfZDv_75oxul17YzGWn5yZuHfJYTF3Ez0,387
18
18
  tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
- tests/test_client.py,sha256=zpviS6C8h5PR-jP2RZNHUZnbVTAKXl1bjF-ku3l9EEg,5134
19
+ tests/test_client.py,sha256=OwYBeNC66WiykU1IUEf4eZacAU49xQJXsFQY6kYiXCQ,5111
20
20
  tests/test_datafileparser.py,sha256=nAeUpOHtelblMpmbrrnf-2GuMjK5ai65veDoymceprE,818
21
21
  tests/test_datasource.py,sha256=2BCrnUh9ZbR_dAdvKT9498u5a-jGnC4aZafP7V9iZn8,5983
22
- pygazpar-1.3.0a2.dist-info/LICENSE.md,sha256=XsCJx_7_BC9tvmE0ZxS1cTNR7ekurog_ea9ybdZ-8tc,1073
23
- pygazpar-1.3.0a2.dist-info/METADATA,sha256=Fu8S-HbGGmPIefWD3p4GBoryM6L-CNf_byVUBw_Q7nk,17680
24
- pygazpar-1.3.0a2.dist-info/WHEEL,sha256=xdKEXCSA3z67AuZ0U8YY67qtEq4KdeUP34QFIQ6Frx0,94
25
- pygazpar-1.3.0a2.dist-info/entry_points.txt,sha256=dNJjC6RYY3FeJIe0hZL9Kcr6vKEx1qkZ71e6Slocb7I,52
26
- pygazpar-1.3.0a2.dist-info/top_level.txt,sha256=P7qn-XtanDPBLQsTvjvLV71wH8RK0DYbx8tzN_rDS70,23
27
- pygazpar-1.3.0a2.dist-info/RECORD,,
22
+ pygazpar-1.3.0a6.dist-info/LICENSE.md,sha256=XsCJx_7_BC9tvmE0ZxS1cTNR7ekurog_ea9ybdZ-8tc,1073
23
+ pygazpar-1.3.0a6.dist-info/METADATA,sha256=RMMJQhnVA9M0vDyqxi3tXz50SeahGId-MXGAzLtbMfE,17764
24
+ pygazpar-1.3.0a6.dist-info/WHEEL,sha256=zivVvCKG3z2j-gmh6yOhs4fqAlXtQ7W3cw4l2J-wg1c,94
25
+ pygazpar-1.3.0a6.dist-info/entry_points.txt,sha256=dNJjC6RYY3FeJIe0hZL9Kcr6vKEx1qkZ71e6Slocb7I,52
26
+ pygazpar-1.3.0a6.dist-info/top_level.txt,sha256=P7qn-XtanDPBLQsTvjvLV71wH8RK0DYbx8tzN_rDS70,23
27
+ pygazpar-1.3.0a6.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.38.4)
2
+ Generator: bdist_wheel (0.43.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py310-none-any
5
5
 
tests/test_client.py CHANGED
@@ -75,7 +75,7 @@ class TestClient:
75
75
  assert (len(data[Frequency.MONTHLY.value]) >= 12 and len(data[Frequency.MONTHLY.value]) <= 13)
76
76
 
77
77
  def test_yearly_jsonweb(self):
78
- client = Client(ExcelWebDataSource(self.__username, self.__password, self.__tmp_directory))
78
+ client = Client(JsonWebDataSource(self.__username, self.__password))
79
79
 
80
80
  data = client.loadSince(self.__pceIdentifier, 365, [Frequency.YEARLY])
81
81