pygazpar 0.1.21__py3-none-any.whl → 1.3.0__py3-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/__init__.py +10 -3
- pygazpar/__main__.py +89 -58
- pygazpar/api_client.py +228 -0
- pygazpar/client.py +98 -210
- pygazpar/datasource.py +590 -0
- pygazpar/enum.py +31 -8
- pygazpar/excelparser.py +138 -0
- pygazpar/jsonparser.py +53 -0
- pygazpar/resources/daily_data_sample.json +7802 -0
- pygazpar/resources/hourly_data_sample.json +1 -0
- pygazpar/resources/monthly_data_sample.json +146 -0
- pygazpar/resources/weekly_data_sample.json +614 -0
- pygazpar/resources/yearly_data_sample.json +18 -0
- pygazpar/version.py +3 -0
- pygazpar-0.1.21.dist-info/LICENSE.txt → pygazpar-1.3.0.dist-info/LICENSE +21 -21
- pygazpar-1.3.0.dist-info/METADATA +225 -0
- pygazpar-1.3.0.dist-info/RECORD +18 -0
- {pygazpar-0.1.21.dist-info → pygazpar-1.3.0.dist-info}/WHEEL +1 -2
- pygazpar/webdriverwrapper.py +0 -125
- pygazpar/webelementwrapper.py +0 -40
- pygazpar-0.1.21.dist-info/METADATA +0 -149
- pygazpar-0.1.21.dist-info/RECORD +0 -14
- pygazpar-0.1.21.dist-info/entry_points.txt +0 -3
- pygazpar-0.1.21.dist-info/top_level.txt +0 -2
- test/__init__.py +0 -1
- test/test_client.py +0 -50
pygazpar/__init__.py
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
-
from pygazpar.
|
2
|
-
from pygazpar.
|
3
|
-
|
1
|
+
from pygazpar.client import Client # noqa: F401
|
2
|
+
from pygazpar.datasource import ( # noqa: F401
|
3
|
+
ExcelFileDataSource,
|
4
|
+
ExcelWebDataSource,
|
5
|
+
JsonFileDataSource,
|
6
|
+
JsonWebDataSource,
|
7
|
+
TestDataSource,
|
8
|
+
)
|
9
|
+
from pygazpar.enum import Frequency, PropertyName # noqa: F401
|
10
|
+
from pygazpar.version import __version__ # noqa: F401
|
pygazpar/__main__.py
CHANGED
@@ -1,58 +1,89 @@
|
|
1
|
-
import argparse
|
2
|
-
import
|
3
|
-
import
|
4
|
-
import
|
5
|
-
import
|
6
|
-
import
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
parser.add_argument("-
|
17
|
-
|
18
|
-
|
19
|
-
parser.add_argument("-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
1
|
+
import argparse
|
2
|
+
import json
|
3
|
+
import logging
|
4
|
+
import os
|
5
|
+
import sys
|
6
|
+
import traceback
|
7
|
+
|
8
|
+
import pygazpar
|
9
|
+
|
10
|
+
Logger = logging.getLogger(__name__)
|
11
|
+
|
12
|
+
|
13
|
+
def main():
|
14
|
+
"""Main function"""
|
15
|
+
parser = argparse.ArgumentParser()
|
16
|
+
parser.add_argument("-v", "--version", action="version", version=f"PyGazpar {pygazpar.__version__}")
|
17
|
+
parser.add_argument("-u", "--username", required=True, help="GRDF username (email)")
|
18
|
+
parser.add_argument("-p", "--password", required=True, help="GRDF password")
|
19
|
+
parser.add_argument("-c", "--pce", required=True, help="GRDF PCE identifier")
|
20
|
+
parser.add_argument("-t", "--tmpdir", required=False, default="/tmp", help="tmp directory (default is /tmp)")
|
21
|
+
parser.add_argument(
|
22
|
+
"-f",
|
23
|
+
"--frequency",
|
24
|
+
required=False,
|
25
|
+
type=lambda frequency: pygazpar.Frequency[frequency],
|
26
|
+
choices=list(pygazpar.Frequency),
|
27
|
+
default="DAILY",
|
28
|
+
help="Meter reading frequency (DAILY, WEEKLY, MONTHLY, YEARLY)",
|
29
|
+
)
|
30
|
+
parser.add_argument(
|
31
|
+
"-d",
|
32
|
+
"--lastNDays",
|
33
|
+
required=False,
|
34
|
+
type=int,
|
35
|
+
default=365,
|
36
|
+
help="Get only the last N days of records (default: 365 days)",
|
37
|
+
)
|
38
|
+
parser.add_argument("--datasource", required=False, default="json", help="Datasource: json | excel | test")
|
39
|
+
|
40
|
+
args = parser.parse_args()
|
41
|
+
|
42
|
+
print(f"PyGazpar version: {pygazpar.__version__}")
|
43
|
+
print(f"Running on Python version: {sys.version}")
|
44
|
+
|
45
|
+
# We create the tmp directory if not already exists.
|
46
|
+
if not os.path.exists(args.tmpdir):
|
47
|
+
os.mkdir(args.tmpdir)
|
48
|
+
|
49
|
+
# We remove the pygazpar log file.
|
50
|
+
pygazparLogFile = f"{args.tmpdir}/pygazpar.log"
|
51
|
+
if os.path.isfile(pygazparLogFile):
|
52
|
+
os.remove(pygazparLogFile)
|
53
|
+
|
54
|
+
# Setup logging.
|
55
|
+
logging.basicConfig(
|
56
|
+
filename=f"{pygazparLogFile}", level=logging.DEBUG, format="%(asctime)s %(levelname)s [%(name)s] %(message)s"
|
57
|
+
)
|
58
|
+
|
59
|
+
Logger.info(f"PyGazpar version: {pygazpar.__version__}")
|
60
|
+
Logger.info(f"Running on Python version: {sys.version}")
|
61
|
+
Logger.info(f"--tmpdir {args.tmpdir}")
|
62
|
+
Logger.info(f"--frequency {args.frequency}")
|
63
|
+
Logger.info(f"--lastNDays {args.lastNDays}")
|
64
|
+
Logger.info(f"--datasource {bool(args.datasource)}")
|
65
|
+
|
66
|
+
if args.datasource == "json":
|
67
|
+
client = pygazpar.Client(pygazpar.JsonWebDataSource(args.username, args.password))
|
68
|
+
elif args.datasource == "excel":
|
69
|
+
client = pygazpar.Client(pygazpar.ExcelWebDataSource(args.username, args.password, args.tmpdir))
|
70
|
+
elif args.datasource == "test":
|
71
|
+
client = pygazpar.Client(pygazpar.TestDataSource())
|
72
|
+
else:
|
73
|
+
raise ValueError("Invalid datasource: (json | excel | test) is expected")
|
74
|
+
|
75
|
+
try:
|
76
|
+
data = client.load_since(args.pce, int(args.lastNDays), [args.frequency])
|
77
|
+
except BaseException: # pylint: disable=broad-except
|
78
|
+
print("An error occured while querying PyGazpar library : %s", traceback.format_exc())
|
79
|
+
return 1
|
80
|
+
|
81
|
+
Logger.info(f"Data loaded: {len(data)} records")
|
82
|
+
Logger.debug(f"Data: {data}")
|
83
|
+
print(json.dumps(data, indent=2))
|
84
|
+
|
85
|
+
return 0
|
86
|
+
|
87
|
+
|
88
|
+
if __name__ == "__main__":
|
89
|
+
sys.exit(main())
|
pygazpar/api_client.py
ADDED
@@ -0,0 +1,228 @@
|
|
1
|
+
import http.cookiejar
|
2
|
+
import json
|
3
|
+
import logging
|
4
|
+
import time
|
5
|
+
import traceback
|
6
|
+
from datetime import date
|
7
|
+
from enum import Enum
|
8
|
+
from typing import Any
|
9
|
+
|
10
|
+
from requests import Response, Session
|
11
|
+
|
12
|
+
SESSION_TOKEN_URL = "https://connexion.grdf.fr/api/v1/authn"
|
13
|
+
SESSION_TOKEN_PAYLOAD = """{{
|
14
|
+
"username": "{0}",
|
15
|
+
"password": "{1}",
|
16
|
+
"options": {{
|
17
|
+
"multiOptionalFactorEnroll": "false",
|
18
|
+
"warnBeforePasswordExpired": "false"
|
19
|
+
}}
|
20
|
+
}}"""
|
21
|
+
|
22
|
+
AUTH_TOKEN_URL = "https://connexion.grdf.fr/login/sessionCookieRedirect"
|
23
|
+
AUTH_TOKEN_PARAMS = """{{
|
24
|
+
"checkAccountSetupComplete": "true",
|
25
|
+
"token": "{0}",
|
26
|
+
"redirectUrl": "https://monespace.grdf.fr"
|
27
|
+
}}"""
|
28
|
+
|
29
|
+
API_BASE_URL = "https://monespace.grdf.fr/api"
|
30
|
+
|
31
|
+
DATE_FORMAT = "%Y-%m-%d"
|
32
|
+
|
33
|
+
Logger = logging.getLogger(__name__)
|
34
|
+
|
35
|
+
|
36
|
+
# ------------------------------------------------------
|
37
|
+
class ConsumptionType(str, Enum):
|
38
|
+
INFORMATIVE = "informatives"
|
39
|
+
PUBLISHED = "publiees"
|
40
|
+
|
41
|
+
|
42
|
+
# ------------------------------------------------------
|
43
|
+
class Frequency(str, Enum):
|
44
|
+
HOURLY = "Horaire"
|
45
|
+
DAILY = "Journalier"
|
46
|
+
WEEKLY = "Hebdomadaire"
|
47
|
+
MONTHLY = "Mensuel"
|
48
|
+
YEARLY = "Annuel"
|
49
|
+
|
50
|
+
|
51
|
+
# ------------------------------------------------------
|
52
|
+
class ServerError(SystemError):
|
53
|
+
|
54
|
+
def __init__(self, message: str, status_code: int):
|
55
|
+
super().__init__(message)
|
56
|
+
self.status_code = status_code
|
57
|
+
|
58
|
+
|
59
|
+
# ------------------------------------------------------
|
60
|
+
class InternalServerError(ServerError):
|
61
|
+
|
62
|
+
def __init__(self, message: str):
|
63
|
+
super().__init__(message, 500)
|
64
|
+
|
65
|
+
|
66
|
+
# ------------------------------------------------------
|
67
|
+
class APIClient:
|
68
|
+
|
69
|
+
# ------------------------------------------------------
|
70
|
+
def __init__(self, username: str, password: str, retry_count: int = 10):
|
71
|
+
self._username = username
|
72
|
+
self._password = password
|
73
|
+
self._retry_count = retry_count
|
74
|
+
self._session = None
|
75
|
+
|
76
|
+
# ------------------------------------------------------
|
77
|
+
def login(self):
|
78
|
+
if self._session is not None:
|
79
|
+
return
|
80
|
+
|
81
|
+
session = Session()
|
82
|
+
session.headers.update({"domain": "grdf.fr"})
|
83
|
+
session.headers.update({"Content-Type": "application/json"})
|
84
|
+
session.headers.update({"X-Requested-With": "XMLHttpRequest"})
|
85
|
+
|
86
|
+
payload = SESSION_TOKEN_PAYLOAD.format(self._username, self._password)
|
87
|
+
|
88
|
+
response = session.post(SESSION_TOKEN_URL, data=payload)
|
89
|
+
|
90
|
+
if response.status_code != 200:
|
91
|
+
raise ServerError(
|
92
|
+
f"An error occurred while logging in. Status code: {response.status_code} - {response.text}",
|
93
|
+
response.status_code,
|
94
|
+
)
|
95
|
+
|
96
|
+
session_token = response.json().get("sessionToken")
|
97
|
+
|
98
|
+
jar = http.cookiejar.CookieJar()
|
99
|
+
|
100
|
+
self._session = Session() # pylint: disable=attribute-defined-outside-init
|
101
|
+
self._session.headers.update({"Content-Type": "application/json"})
|
102
|
+
self._session.headers.update({"X-Requested-With": "XMLHttpRequest"})
|
103
|
+
|
104
|
+
params = json.loads(AUTH_TOKEN_PARAMS.format(session_token))
|
105
|
+
|
106
|
+
response = self._session.get(AUTH_TOKEN_URL, params=params, allow_redirects=True, cookies=jar) # type: ignore
|
107
|
+
|
108
|
+
if response.status_code != 200:
|
109
|
+
raise ServerError(
|
110
|
+
f"An error occurred while getting the auth token. Status code: {response.status_code} - {response.text}",
|
111
|
+
response.status_code,
|
112
|
+
)
|
113
|
+
|
114
|
+
# ------------------------------------------------------
|
115
|
+
def is_logged_in(self) -> bool:
|
116
|
+
return self._session is not None
|
117
|
+
|
118
|
+
# ------------------------------------------------------
|
119
|
+
def logout(self):
|
120
|
+
if self._session is None:
|
121
|
+
return
|
122
|
+
|
123
|
+
self._session.close()
|
124
|
+
self._session = None
|
125
|
+
|
126
|
+
# ------------------------------------------------------
|
127
|
+
def get(self, endpoint: str, params: dict[str, Any]) -> Response:
|
128
|
+
|
129
|
+
if self._session is None:
|
130
|
+
raise ConnectionError("You must login first")
|
131
|
+
|
132
|
+
retry = self._retry_count
|
133
|
+
while retry > 0:
|
134
|
+
|
135
|
+
try:
|
136
|
+
response = self._session.get(f"{API_BASE_URL}{endpoint}", params=params)
|
137
|
+
|
138
|
+
if "text/html" in response.headers.get("Content-Type"): # type: ignore
|
139
|
+
raise InternalServerError(
|
140
|
+
f"An unknown error occurred. Please check your query parameters (endpoint: {endpoint}): {params}"
|
141
|
+
)
|
142
|
+
|
143
|
+
if response.status_code != 200:
|
144
|
+
raise ServerError(
|
145
|
+
f"HTTP error on enpoint '{endpoint}': Status code: {response.status_code} - {response.text}. Query parameters: {params}",
|
146
|
+
response.status_code,
|
147
|
+
)
|
148
|
+
|
149
|
+
break
|
150
|
+
except InternalServerError as internalServerError: # pylint: disable=broad-exception-caught
|
151
|
+
if retry == 1:
|
152
|
+
Logger.error(f"{internalServerError}. Retry limit reached: {traceback.format_exc()}")
|
153
|
+
raise internalServerError
|
154
|
+
retry -= 1
|
155
|
+
Logger.warning(f"{internalServerError}. Retry in 3 seconds ({retry} retries left)...")
|
156
|
+
time.sleep(3)
|
157
|
+
|
158
|
+
return response
|
159
|
+
|
160
|
+
# ------------------------------------------------------
|
161
|
+
def get_pce_list(self, details: bool = False) -> list[Any]:
|
162
|
+
|
163
|
+
res = self.get("/e-conso/pce", {"details": details}).json()
|
164
|
+
|
165
|
+
if type(res) is not list:
|
166
|
+
raise TypeError(f"Invalid response type: {type(res)} (list expected)")
|
167
|
+
|
168
|
+
return res
|
169
|
+
|
170
|
+
# ------------------------------------------------------
|
171
|
+
def get_pce_consumption(
|
172
|
+
self, consumption_type: ConsumptionType, start_date: date, end_date: date, pce_list: list[str]
|
173
|
+
) -> dict[str, Any]:
|
174
|
+
|
175
|
+
start = start_date.strftime(DATE_FORMAT)
|
176
|
+
end = end_date.strftime(DATE_FORMAT)
|
177
|
+
|
178
|
+
res = self.get(
|
179
|
+
f"/e-conso/pce/consommation/{consumption_type.value}",
|
180
|
+
{"dateDebut": start, "dateFin": end, "pceList[]": ",".join(pce_list)},
|
181
|
+
).json()
|
182
|
+
|
183
|
+
if type(res) is list and len(res) == 0:
|
184
|
+
return dict[str, Any]()
|
185
|
+
|
186
|
+
if type(res) is not dict:
|
187
|
+
raise TypeError(f"Invalid response type: {type(res)} (dict expected)")
|
188
|
+
|
189
|
+
return res
|
190
|
+
|
191
|
+
# ------------------------------------------------------
|
192
|
+
def get_pce_consumption_excelsheet(
|
193
|
+
self,
|
194
|
+
consumption_type: ConsumptionType,
|
195
|
+
start_date: date,
|
196
|
+
end_date: date,
|
197
|
+
frequency: Frequency,
|
198
|
+
pce_list: list[str],
|
199
|
+
) -> dict[str, Any]:
|
200
|
+
|
201
|
+
start = start_date.strftime(DATE_FORMAT)
|
202
|
+
end = end_date.strftime(DATE_FORMAT)
|
203
|
+
|
204
|
+
response = self.get(
|
205
|
+
f"/e-conso/pce/consommation/{consumption_type.value}/telecharger",
|
206
|
+
{"dateDebut": start, "dateFin": end, "frequence": frequency.value, "pceList[]": ",".join(pce_list)},
|
207
|
+
)
|
208
|
+
|
209
|
+
filename = response.headers["Content-Disposition"].split("filename=")[1]
|
210
|
+
|
211
|
+
res = {"filename": filename, "content": response.content}
|
212
|
+
|
213
|
+
return res
|
214
|
+
|
215
|
+
# ------------------------------------------------------
|
216
|
+
def get_pce_meteo(self, end_date: date, days: int, pce: str) -> dict[str, Any]:
|
217
|
+
|
218
|
+
end = end_date.strftime(DATE_FORMAT)
|
219
|
+
|
220
|
+
res = self.get(f"/e-conso/pce/{pce}/meteo", {"dateFinPeriode": end, "nbJours": days}).json()
|
221
|
+
|
222
|
+
if type(res) is list and len(res) == 0:
|
223
|
+
return dict[str, Any]()
|
224
|
+
|
225
|
+
if type(res) is not dict:
|
226
|
+
raise TypeError(f"Invalid response type: {type(res)} (dict expected)")
|
227
|
+
|
228
|
+
return res
|