pygazpar 1.3.0a2__py310-none-any.whl → 1.3.0a6__py310-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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