pygazpar 1.3.0a2__py39-none-any.whl → 1.3.0a9__py39-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)
87
88
 
88
- if "status" in loginData and "error" in loginData and loginData["status"] >= 400:
89
- raise Exception(f"{loginData['error']} ({loginData['status']})")
89
+ jar = http.cookiejar.CookieJar()
90
90
 
91
- if "state" in loginData and loginData["state"] != "SUCCESS":
92
- raise Exception(loginData["error"])
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}")
101
+
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
 
@@ -132,7 +144,10 @@ class ExcelWebDataSource(WebDataSource):
132
144
  file_list = glob.glob(data_file_path_pattern)
133
145
  for filename in file_list:
134
146
  if os.path.isfile(filename):
135
- os.remove(filename)
147
+ try:
148
+ os.remove(filename)
149
+ except PermissionError:
150
+ pass
136
151
 
137
152
  if frequencies is None:
138
153
  # Transform Enum in List.
@@ -145,11 +160,31 @@ class ExcelWebDataSource(WebDataSource):
145
160
  # Inject parameters.
146
161
  downloadUrl = ExcelWebDataSource.DATA_URL.format(startDate.strftime(ExcelWebDataSource.DATE_FORMAT), endDate.strftime(ExcelWebDataSource.DATE_FORMAT), pceIdentifier, ExcelWebDataSource.FREQUENCY_VALUES[frequency])
147
162
 
148
- session.get(downloadUrl) # First request does not return anything : strange...
149
-
150
163
  Logger.debug(f"Loading data of frequency {ExcelWebDataSource.FREQUENCY_VALUES[frequency]} from {startDate.strftime(ExcelWebDataSource.DATE_FORMAT)} to {endDate.strftime(ExcelWebDataSource.DATE_FORMAT)}")
151
164
 
152
- self.__downloadFile(session, downloadUrl, self.__tmpDirectory)
165
+ # Retry mechanism.
166
+ retry = 10
167
+ while retry > 0:
168
+
169
+ # Create a session.
170
+ session = Session()
171
+ session.headers.update({"Host": "monespace.grdf.fr"})
172
+ session.headers.update({"Domain": "grdf.fr"})
173
+ session.headers.update({"X-Requested-With": "XMLHttpRequest"})
174
+ session.headers.update({"Accept": "application/json"})
175
+ session.cookies.set("auth_token", auth_token, domain="monespace.grdf.fr")
176
+
177
+ try:
178
+ self.__downloadFile(session, downloadUrl, self.__tmpDirectory)
179
+ break
180
+ except Exception as e:
181
+
182
+ if retry == 1:
183
+ raise e
184
+
185
+ Logger.error("An error occurred while loading data. Retry in 3 seconds.")
186
+ time.sleep(3)
187
+ retry -= 1
153
188
 
154
189
  # Load the XLSX file into the data structure
155
190
  file_list = glob.glob(data_file_path_pattern)
@@ -159,7 +194,11 @@ class ExcelWebDataSource(WebDataSource):
159
194
 
160
195
  for filename in file_list:
161
196
  res[frequency.value] = ExcelParser.parse(filename, frequency if frequency != Frequency.YEARLY else Frequency.DAILY)
162
- os.remove(filename)
197
+ try:
198
+ # openpyxl does not close the file properly.
199
+ os.remove(filename)
200
+ except PermissionError:
201
+ pass
163
202
 
164
203
  # We compute yearly from daily data.
165
204
  if frequency == Frequency.YEARLY:
@@ -172,6 +211,12 @@ class ExcelWebDataSource(WebDataSource):
172
211
 
173
212
  response = session.get(url)
174
213
 
214
+ if "text/html" in response.headers.get("Content-Type"):
215
+ raise Exception("An error occurred while loading data. Please check your credentials.")
216
+
217
+ if response.status_code != 200:
218
+ raise Exception(f"An error occurred while loading data. Status code: {response.status_code} - {response.text}")
219
+
175
220
  response.raise_for_status()
176
221
 
177
222
  filename = response.headers["Content-Disposition"].split("filename=")[1]
@@ -210,7 +255,7 @@ class ExcelFileDataSource(IDataSource):
210
255
  # ------------------------------------------------------------------------------------------------------------
211
256
  class JsonWebDataSource(WebDataSource):
212
257
 
213
- DATA_URL = "https://monespace.grdf.fr/api/e-conso/pce/consommation/informatives?dateDebut={0}&dateFin={1}&pceList%5B%5D={2}"
258
+ DATA_URL = "https://monespace.grdf.fr/api/e-conso/pce/consommation/informatives?dateDebut={0}&dateFin={1}&pceList[]={2}"
214
259
 
215
260
  TEMPERATURES_URL = "https://monespace.grdf.fr/api/e-conso/pce/{0}/meteo?dateFinPeriode={1}&nbJours={2}"
216
261
 
@@ -222,7 +267,7 @@ class JsonWebDataSource(WebDataSource):
222
267
 
223
268
  super().__init__(username, password)
224
269
 
225
- def _loadFromSession(self, session: Session, pceIdentifier: str, startDate: date, endDate: date, frequencies: Optional[List[Frequency]] = None) -> MeterReadingsByFrequency:
270
+ def _loadFromSession(self, auth_token: str, pceIdentifier: str, startDate: date, endDate: date, frequencies: Optional[List[Frequency]] = None) -> MeterReadingsByFrequency:
226
271
 
227
272
  res = {}
228
273
 
@@ -237,11 +282,38 @@ class JsonWebDataSource(WebDataSource):
237
282
  # Data URL: Inject parameters.
238
283
  downloadUrl = JsonWebDataSource.DATA_URL.format(startDate.strftime(JsonWebDataSource.INPUT_DATE_FORMAT), endDate.strftime(JsonWebDataSource.INPUT_DATE_FORMAT), pceIdentifier)
239
284
 
240
- # First request never returns data.
241
- session.get(downloadUrl)
285
+ # Retry mechanism.
286
+ retry = 10
287
+ while retry > 0:
288
+
289
+ # Create a session.
290
+ session = Session()
291
+ session.headers.update({"Host": "monespace.grdf.fr"})
292
+ session.headers.update({"Domain": "grdf.fr"})
293
+ session.headers.update({"X-Requested-With": "XMLHttpRequest"})
294
+ session.headers.update({"Accept": "application/json"})
295
+ session.cookies.set("auth_token", auth_token, domain="monespace.grdf.fr")
296
+
297
+ try:
298
+ response = session.get(downloadUrl)
299
+
300
+ if "text/html" in response.headers.get("Content-Type"):
301
+ raise Exception("An error occurred while loading data. Please check your credentials.")
302
+
303
+ if response.status_code != 200:
304
+ raise Exception(f"An error occurred while loading data. Status code: {response.status_code} - {response.text}")
305
+
306
+ break
307
+ except Exception as e:
308
+
309
+ if retry == 1:
310
+ raise e
311
+
312
+ Logger.error("An error occurred while loading data. Retry in 3 seconds.")
313
+ time.sleep(3)
314
+ retry -= 1
242
315
 
243
- # Get consumption data.
244
- data = session.get(downloadUrl).text
316
+ data = response.text
245
317
 
246
318
  # Temperatures URL: Inject parameters.
247
319
  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.0a9"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pygazpar
3
- Version: 1.3.0a2
3
+ Version: 1.3.0a9
4
4
  Summary: Retrieve gas consumption from GrDF web site (French Gas Company)
5
5
  Home-page: https://github.com/ssenart/pygazpar
6
6
  Author: Stephane Senart
@@ -25,11 +25,16 @@ 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
33
+
34
+ ## <span style="color:green">!!! This library is working again. CAPTCHA has been removed !!!</span>
35
+
36
+ ## <span style="color:red">~~!!! This library is broken since CAPTCHA is mandatory on GrDF site !!!~~</span>
37
+
33
38
  PyGazpar is a Python library for getting natural gas consumption from GrDF French provider.
34
39
 
35
40
  Their natural gas meter is called Gazpar. It is wireless and transmit the gas consumption once per day.
@@ -214,9 +219,16 @@ All notable changes to this project will be documented in this file.
214
219
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
215
220
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
216
221
 
217
- ## [1.2.1](https://github.com/ssenart/PyGazpar/compare/1.2.0...1.2.1) - 2022-12-28
222
+ ## [1.2.2](https://github.com/ssenart/PyGazpar/compare/1.2.1...1.2.2) - 2024-05-08
218
223
 
219
224
  ### Fixed
225
+ - [#65](https://github.com/ssenart/PyGazpar/issues/65): [Bug] PermissionError happens when loading data from Excel file.
226
+
227
+ ## [1.2.1](https://github.com/ssenart/PyGazpar/compare/1.2.0...1.2.1) - 2024-05-04
228
+
229
+ ### Fixed
230
+ - [#64](https://github.com/ssenart/PyGazpar/issues/64): [Issue] Captcha failed issue.
231
+
220
232
  - [#63](https://github.com/ssenart/PyGazpar/issues/63): [Bug] If the latest received consumption is Sunday, then the last weekly period is duplicated.
221
233
 
222
234
  ## [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=nlIWxZ6SNSHf09BVBFIChSHf4dN05lC3LCxUUDRTylg,21470
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=M68dz_mPRENIQzrCD_i1UgDy4ON37VxutlMfPrsxFpY,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=DVGGubMPMM56LvQW-4_pCouvG9qTCbVZVPoRTBGuCU4,5111
20
20
  tests/test_datafileparser.py,sha256=nAeUpOHtelblMpmbrrnf-2GuMjK5ai65veDoymceprE,818
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=bN196PaZrGluxX8Xfko-8jmn6i_dqPeHp0fynh0qzG8,17682
24
- pygazpar-1.3.0a2.dist-info/WHEEL,sha256=ns_9KNZvwSNZtRgVV_clzMUG_fXjGc5Z8Tx4hxQ0gkw,93
25
- pygazpar-1.3.0a2.dist-info/entry_points.txt,sha256=c_FMZPYlRv1w9EqfgWhlkdJOoje7FcglI0UMm2oRLoI,53
26
- pygazpar-1.3.0a2.dist-info/top_level.txt,sha256=P7qn-XtanDPBLQsTvjvLV71wH8RK0DYbx8tzN_rDS70,23
27
- pygazpar-1.3.0a2.dist-info/RECORD,,
21
+ tests/test_datasource.py,sha256=Fkn9BOGVKITAgrx9XipR1_ykT7rdvPyt_PAg3ftjfSU,5983
22
+ pygazpar-1.3.0a9.dist-info/LICENSE.md,sha256=XsCJx_7_BC9tvmE0ZxS1cTNR7ekurog_ea9ybdZ-8tc,1073
23
+ pygazpar-1.3.0a9.dist-info/METADATA,sha256=BAd0ZNGUkpdCRe0CxbqHc-9X-1wa5MwrBFLVy6AEjvg,18193
24
+ pygazpar-1.3.0a9.dist-info/WHEEL,sha256=kG0f_S63jJ569yg8d_OdUrqQSrnKsZBVKN5Kb1RSnpA,93
25
+ pygazpar-1.3.0a9.dist-info/entry_points.txt,sha256=c_FMZPYlRv1w9EqfgWhlkdJOoje7FcglI0UMm2oRLoI,53
26
+ pygazpar-1.3.0a9.dist-info/top_level.txt,sha256=P7qn-XtanDPBLQsTvjvLV71wH8RK0DYbx8tzN_rDS70,23
27
+ pygazpar-1.3.0a9.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: py39-none-any
5
5
 
tests/test_client.py CHANGED
@@ -72,10 +72,10 @@ class TestClient:
72
72
 
73
73
  data = client.loadSince(self.__pceIdentifier, 365, [Frequency.MONTHLY])
74
74
 
75
- assert (len(data[Frequency.MONTHLY.value]) >= 12 and len(data[Frequency.MONTHLY.value]) <= 13)
75
+ assert (len(data[Frequency.MONTHLY.value]) >= 11 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
 
tests/test_datasource.py CHANGED
@@ -143,7 +143,7 @@ class TestAllDataSource:
143
143
 
144
144
  assert (len(data[Frequency.WEEKLY.value]) >= 51 and len(data[Frequency.WEEKLY.value]) <= 54)
145
145
 
146
- assert (len(data[Frequency.MONTHLY.value]) >= 12 and len(data[Frequency.MONTHLY.value]) <= 13)
146
+ assert (len(data[Frequency.MONTHLY.value]) >= 11 and len(data[Frequency.MONTHLY.value]) <= 13)
147
147
 
148
148
  assert (len(data[Frequency.YEARLY.value]) == 1)
149
149