pygazpar 0.1.21__py3-none-any.whl → 1.3.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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