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 +119 -47
- pygazpar/version.py +1 -1
- {pygazpar-1.3.0a2.dist-info → pygazpar-1.3.0a9.dist-info}/METADATA +16 -4
- {pygazpar-1.3.0a2.dist-info → pygazpar-1.3.0a9.dist-info}/RECORD +10 -10
- {pygazpar-1.3.0a2.dist-info → pygazpar-1.3.0a9.dist-info}/WHEEL +1 -1
- tests/test_client.py +2 -2
- tests/test_datasource.py +1 -1
- {pygazpar-1.3.0a2.dist-info → pygazpar-1.3.0a9.dist-info}/LICENSE.md +0 -0
- {pygazpar-1.3.0a2.dist-info → pygazpar-1.3.0a9.dist-info}/entry_points.txt +0 -0
- {pygazpar-1.3.0a2.dist-info → pygazpar-1.3.0a9.dist-info}/top_level.txt +0 -0
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
|
-
|
15
|
-
|
16
|
-
|
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
|
-
"
|
21
|
-
|
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
|
-
|
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(
|
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,
|
71
|
+
def _login(self, username: str, password: str) -> str:
|
67
72
|
|
68
|
-
|
69
|
-
session.
|
70
|
-
|
71
|
-
|
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
|
-
|
75
|
-
payload = LOGIN_PAYLOAD.format(username, password, auth_nonce)
|
78
|
+
payload = SESSION_TOKEN_PAYLOAD.format(username, password)
|
76
79
|
|
77
|
-
|
78
|
-
data = json.loads(payload)
|
80
|
+
response = session.post(SESSION_TOKEN_URL, data=payload)
|
79
81
|
|
80
|
-
|
81
|
-
|
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
|
-
|
84
|
-
loginData = response.json()
|
85
|
+
session_token = response.json().get("sessionToken")
|
85
86
|
|
86
|
-
|
87
|
+
Logger.debug("Session token: %s", session_token)
|
87
88
|
|
88
|
-
|
89
|
-
raise Exception(f"{loginData['error']} ({loginData['status']})")
|
89
|
+
jar = http.cookiejar.CookieJar()
|
90
90
|
|
91
|
-
|
92
|
-
|
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,
|
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
|
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,
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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,
|
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
|
-
#
|
241
|
-
|
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
|
-
|
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.
|
1
|
+
__version__ = "1.3.0a9"
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: pygazpar
|
3
|
-
Version: 1.3.
|
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
|
29
|
-
Requires-Dist: requests
|
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.
|
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=
|
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=
|
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=
|
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=
|
22
|
-
pygazpar-1.3.
|
23
|
-
pygazpar-1.3.
|
24
|
-
pygazpar-1.3.
|
25
|
-
pygazpar-1.3.
|
26
|
-
pygazpar-1.3.
|
27
|
-
pygazpar-1.3.
|
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,,
|
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]) >=
|
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(
|
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]) >=
|
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
|
|
File without changes
|
File without changes
|
File without changes
|