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 CHANGED
@@ -1,3 +1,10 @@
1
- from pygazpar.enum import PropertyNameEnum
2
- from pygazpar.client import Client
3
- from pygazpar.client import LoginError
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 sys
3
- import json
4
- import traceback
5
- import os
6
- import logging
7
-
8
- from pygazpar.client import Client
9
-
10
- def main():
11
- """Main function"""
12
- parser = argparse.ArgumentParser()
13
- parser.add_argument("-u", "--username",
14
- required=True,
15
- help="GRDF username (email)")
16
- parser.add_argument("-p", "--password",
17
- required=True,
18
- help="GRDF password")
19
- parser.add_argument("-w", "--webdriver",
20
- required=True,
21
- help="Firefox webdriver executable (geckodriver)")
22
- parser.add_argument("-s", "--wait_time",
23
- required=False,
24
- type=int,
25
- default=30,
26
- help="Wait time in seconds (see https://selenium-python.readthedocs.io/waits.html for details)")
27
- parser.add_argument("-t", "--tmpdir",
28
- required=False,
29
- default="/tmp",
30
- help="tmp directory (default is /tmp)")
31
- parser.add_argument("-l", "--lastNRows",
32
- required=False,
33
- type=int,
34
- default=0,
35
- help="Get only the last N rows (default is 0: it means all rows are retrieved)")
36
-
37
- args = parser.parse_args()
38
-
39
- # We remove the pygazpar log file.
40
- geckodriverLogFile = f"{args.tmpdir}/pygazpar.log"
41
- if os.path.isfile(geckodriverLogFile):
42
- os.remove(geckodriverLogFile)
43
-
44
- # Setup logging.
45
- logging.basicConfig(filename=f"{args.tmpdir}/pygazpar.log", level=logging.DEBUG, format="%(asctime)s %(levelname)s [%(name)s] %(message)s")
46
-
47
- client = Client(args.username, args.password, args.webdriver, int(args.wait_time), args.tmpdir, int(args.lastNRows))
48
-
49
- try:
50
- client.update()
51
- except BaseException:
52
- print('An error occured while querying PyGazpar library : %s', traceback.format_exc())
53
- return 1
54
-
55
- print(json.dumps(client.data(), indent=2))
56
-
57
- if __name__ == '__main__':
58
- sys.exit(main())
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