pygazpar 1.3.0b5__tar.gz → 1.3.1__tar.gz
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-1.3.0b5 → pygazpar-1.3.1}/CHANGELOG.md +7 -1
- {pygazpar-1.3.0b5 → pygazpar-1.3.1}/PKG-INFO +2 -5
- {pygazpar-1.3.0b5 → pygazpar-1.3.1}/README.md +0 -2
- {pygazpar-1.3.0b5 → pygazpar-1.3.1}/pygazpar/api_client.py +60 -34
- {pygazpar-1.3.0b5 → pygazpar-1.3.1}/pyproject.toml +2 -3
- {pygazpar-1.3.0b5 → pygazpar-1.3.1}/LICENSE +0 -0
- {pygazpar-1.3.0b5 → pygazpar-1.3.1}/pygazpar/__init__.py +0 -0
- {pygazpar-1.3.0b5 → pygazpar-1.3.1}/pygazpar/__main__.py +0 -0
- {pygazpar-1.3.0b5 → pygazpar-1.3.1}/pygazpar/client.py +0 -0
- {pygazpar-1.3.0b5 → pygazpar-1.3.1}/pygazpar/datasource.py +0 -0
- {pygazpar-1.3.0b5 → pygazpar-1.3.1}/pygazpar/enum.py +0 -0
- {pygazpar-1.3.0b5 → pygazpar-1.3.1}/pygazpar/excelparser.py +0 -0
- {pygazpar-1.3.0b5 → pygazpar-1.3.1}/pygazpar/jsonparser.py +0 -0
- {pygazpar-1.3.0b5 → pygazpar-1.3.1}/pygazpar/resources/daily_data_sample.json +0 -0
- {pygazpar-1.3.0b5 → pygazpar-1.3.1}/pygazpar/resources/hourly_data_sample.json +0 -0
- {pygazpar-1.3.0b5 → pygazpar-1.3.1}/pygazpar/resources/monthly_data_sample.json +0 -0
- {pygazpar-1.3.0b5 → pygazpar-1.3.1}/pygazpar/resources/weekly_data_sample.json +0 -0
- {pygazpar-1.3.0b5 → pygazpar-1.3.1}/pygazpar/resources/yearly_data_sample.json +0 -0
- {pygazpar-1.3.0b5 → pygazpar-1.3.1}/pygazpar/version.py +0 -0
@@ -4,7 +4,13 @@ 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.
|
7
|
+
## [1.3.1] - 2025-07-22
|
8
|
+
|
9
|
+
### Fixed
|
10
|
+
|
11
|
+
[#88](https://github.com/ssenart/PyGazpar/issues/88) : 500 - NGINX / OpenID Connect login failure.
|
12
|
+
|
13
|
+
## [1.3.0] - 2025-02-15
|
8
14
|
|
9
15
|
### Added
|
10
16
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: pygazpar
|
3
|
-
Version: 1.3.
|
3
|
+
Version: 1.3.1
|
4
4
|
Summary: Python library to download gas consumption from a GrDF (French Gas Company) account
|
5
5
|
License: MIT License
|
6
6
|
|
@@ -24,8 +24,7 @@ License: MIT License
|
|
24
24
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
25
25
|
SOFTWARE.
|
26
26
|
Author: Stéphane Senart
|
27
|
-
Requires-Python: >=3.
|
28
|
-
Classifier: Programming Language :: Python :: 3.9
|
27
|
+
Requires-Python: >=3.10
|
29
28
|
Classifier: Programming Language :: Python :: 3.10
|
30
29
|
Classifier: Programming Language :: Python :: 3.11
|
31
30
|
Classifier: Programming Language :: Python :: 3.12
|
@@ -37,8 +36,6 @@ Description-Content-Type: text/markdown
|
|
37
36
|
|
38
37
|
# PyGazpar
|
39
38
|
|
40
|
-
## $\text{\color{green}{!!! This library is working again. CAPTCHA has been removed !!!}}$
|
41
|
-
|
42
39
|
PyGazpar is a Python library for getting natural gas consumption from GrDF French provider.
|
43
40
|
|
44
41
|
Their natural gas meter is called Gazpar. It is wireless and transmit the gas consumption once per day.
|
@@ -1,7 +1,5 @@
|
|
1
1
|
# PyGazpar
|
2
2
|
|
3
|
-
## $\text{\color{green}{!!! This library is working again. CAPTCHA has been removed !!!}}$
|
4
|
-
|
5
3
|
PyGazpar is a Python library for getting natural gas consumption from GrDF French provider.
|
6
4
|
|
7
5
|
Their natural gas meter is called Gazpar. It is wireless and transmit the gas consumption once per day.
|
@@ -1,6 +1,5 @@
|
|
1
|
-
import http.cookiejar
|
2
|
-
import json
|
3
1
|
import logging
|
2
|
+
import re
|
4
3
|
import time
|
5
4
|
import traceback
|
6
5
|
from datetime import date
|
@@ -9,21 +8,20 @@ from typing import Any
|
|
9
8
|
|
10
9
|
from requests import Response, Session
|
11
10
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
"
|
17
|
-
|
18
|
-
"warnBeforePasswordExpired": "false"
|
19
|
-
}}
|
11
|
+
START_URL = "https://monespace.grdf.fr/"
|
12
|
+
|
13
|
+
MAIL_SESSION_TOKEN_URL = "https://connexion.grdf.fr/idp/idx/identify"
|
14
|
+
MAIL_SESSION_TOKEN_PAYLOAD = """{{
|
15
|
+
"identifier": "{0}",
|
16
|
+
"stateHandle": "{1}"
|
20
17
|
}}"""
|
21
18
|
|
22
|
-
|
23
|
-
|
24
|
-
"
|
25
|
-
|
26
|
-
|
19
|
+
PASSWORD_SESSION_TOKEN_URL = "https://connexion.grdf.fr/idp/idx/challenge/answer"
|
20
|
+
PASSWORD_SESSION_TOKEN_PAYLOAD = """{{
|
21
|
+
"credentials": {{
|
22
|
+
"passcode": "{0}"
|
23
|
+
}},
|
24
|
+
"stateHandle": "{1}"
|
27
25
|
}}"""
|
28
26
|
|
29
27
|
API_BASE_URL = "https://monespace.grdf.fr/api"
|
@@ -71,7 +69,7 @@ class APIClient:
|
|
71
69
|
self._username = username
|
72
70
|
self._password = password
|
73
71
|
self._retry_count = retry_count
|
74
|
-
self._session = None
|
72
|
+
self._session: Session | None = None
|
75
73
|
|
76
74
|
# ------------------------------------------------------
|
77
75
|
def login(self):
|
@@ -79,38 +77,66 @@ class APIClient:
|
|
79
77
|
return
|
80
78
|
|
81
79
|
session = Session()
|
82
|
-
session.headers.update({"
|
83
|
-
session.headers.update({"Content-Type": "application/json"})
|
84
|
-
session.headers.update({"X-Requested-With": "XMLHttpRequest"})
|
80
|
+
session.headers.update({"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"})
|
85
81
|
|
86
|
-
|
82
|
+
start_response = session.get(START_URL)
|
83
|
+
if start_response.status_code != 200:
|
84
|
+
raise ServerError(
|
85
|
+
f"An error occurred while logging in start. Status code: {start_response.status_code} - {start_response.url}",
|
86
|
+
start_response.status_code,
|
87
|
+
)
|
87
88
|
|
88
|
-
|
89
|
+
pattern = r'"stateToken"\s*:\s*"([^"]+)"'
|
90
|
+
match = re.search(pattern, start_response.text)
|
91
|
+
if match:
|
92
|
+
state_token_html = match.group(1)
|
93
|
+
state_token = state_token_html.replace("\\x2D", "-")
|
94
|
+
else:
|
95
|
+
raise ValueError("Cannot retrieve stateToken inside HTML response")
|
96
|
+
|
97
|
+
payload = MAIL_SESSION_TOKEN_PAYLOAD.format(self._username, state_token)
|
98
|
+
session.cookies.set("ln", self._username)
|
99
|
+
|
100
|
+
mail_response = session.post(
|
101
|
+
MAIL_SESSION_TOKEN_URL,
|
102
|
+
data=payload,
|
103
|
+
headers={"Accept": "application/json; okta-version=1.0.0", "Content-Type": "application/json"},
|
104
|
+
)
|
89
105
|
|
90
|
-
if
|
106
|
+
if mail_response.status_code != 200:
|
91
107
|
raise ServerError(
|
92
|
-
f"An error occurred while logging in. Status code: {
|
93
|
-
|
108
|
+
f"An error occurred while logging in mail. Status code: {mail_response.status_code} - {mail_response.text}",
|
109
|
+
mail_response.status_code,
|
94
110
|
)
|
95
111
|
|
96
|
-
|
112
|
+
state_handle = mail_response.json().get("stateHandle")
|
97
113
|
|
98
|
-
|
114
|
+
payload = PASSWORD_SESSION_TOKEN_PAYLOAD.format(self._password, state_handle)
|
99
115
|
|
100
|
-
|
101
|
-
|
102
|
-
|
116
|
+
password_response = session.post(
|
117
|
+
PASSWORD_SESSION_TOKEN_URL,
|
118
|
+
data=payload,
|
119
|
+
headers={"Accept": "application/json; okta-version=1.0.0", "Content-Type": "application/json"},
|
120
|
+
)
|
103
121
|
|
104
|
-
|
122
|
+
if password_response.status_code != 200:
|
123
|
+
raise ServerError(
|
124
|
+
f"An error occurred while logging in password. Status code: {password_response.status_code} - {password_response.text}",
|
125
|
+
password_response.status_code,
|
126
|
+
)
|
105
127
|
|
106
|
-
|
128
|
+
success_url = password_response.json()["success"]["href"]
|
107
129
|
|
108
|
-
|
130
|
+
response_redirect = session.get(success_url)
|
131
|
+
|
132
|
+
if response_redirect.status_code != 200:
|
109
133
|
raise ServerError(
|
110
|
-
f"An error occurred while
|
111
|
-
|
134
|
+
f"An error occurred while logging in response_redirect. Status code: {response_redirect.status_code} - {response_redirect.url}",
|
135
|
+
response_redirect.status_code,
|
112
136
|
)
|
113
137
|
|
138
|
+
self._session = session
|
139
|
+
|
114
140
|
# ------------------------------------------------------
|
115
141
|
def is_logged_in(self) -> bool:
|
116
142
|
return self._session is not None
|
@@ -1,15 +1,14 @@
|
|
1
1
|
[project]
|
2
2
|
name = "pygazpar"
|
3
|
-
version = "1.3.
|
3
|
+
version = "1.3.1"
|
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"
|
7
|
-
requires-python = ">=3.
|
7
|
+
requires-python = ">=3.10"
|
8
8
|
authors = [
|
9
9
|
{ name = "Stéphane Senart" }
|
10
10
|
]
|
11
11
|
classifiers = [
|
12
|
-
"Programming Language :: Python :: 3.9",
|
13
12
|
"Programming Language :: Python :: 3.10",
|
14
13
|
"Programming Language :: Python :: 3.11",
|
15
14
|
"Programming Language :: Python :: 3.12",
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|