pygazpar 0.1.20__py3-none-any.whl → 1.3.0b1__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,81 @@
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
+
11
+ def main():
12
+ """Main function"""
13
+ parser = argparse.ArgumentParser()
14
+ parser.add_argument("-v", "--version", action="version", version=f"PyGazpar {pygazpar.__version__}")
15
+ parser.add_argument("-u", "--username", required=True, help="GRDF username (email)")
16
+ parser.add_argument("-p", "--password", required=True, help="GRDF password")
17
+ parser.add_argument("-c", "--pce", required=True, help="GRDF PCE identifier")
18
+ parser.add_argument("-t", "--tmpdir", required=False, default="/tmp", help="tmp directory (default is /tmp)")
19
+ parser.add_argument(
20
+ "-f",
21
+ "--frequency",
22
+ required=False,
23
+ type=lambda frequency: pygazpar.Frequency[frequency],
24
+ choices=list(pygazpar.Frequency),
25
+ default="DAILY",
26
+ help="Meter reading frequency (DAILY, WEEKLY, MONTHLY, YEARLY)",
27
+ )
28
+ parser.add_argument(
29
+ "-d",
30
+ "--lastNDays",
31
+ required=False,
32
+ type=int,
33
+ default=365,
34
+ help="Get only the last N days of records (default: 365 days)",
35
+ )
36
+ parser.add_argument("--datasource", required=False, default="json", help="Datasource: json | excel | test")
37
+
38
+ args = parser.parse_args()
39
+
40
+ # We create the tmp directory if not already exists.
41
+ if not os.path.exists(args.tmpdir):
42
+ os.mkdir(args.tmpdir)
43
+
44
+ # We remove the pygazpar log file.
45
+ pygazparLogFile = f"{args.tmpdir}/pygazpar.log"
46
+ if os.path.isfile(pygazparLogFile):
47
+ os.remove(pygazparLogFile)
48
+
49
+ # Setup logging.
50
+ logging.basicConfig(
51
+ filename=f"{pygazparLogFile}", level=logging.DEBUG, format="%(asctime)s %(levelname)s [%(name)s] %(message)s"
52
+ )
53
+
54
+ logging.info(f"PyGazpar {pygazpar.__version__}")
55
+ logging.info(f"--tmpdir {args.tmpdir}")
56
+ logging.info(f"--frequency {args.frequency}")
57
+ logging.info(f"--lastNDays {args.lastNDays}")
58
+ logging.info(f"--datasource {bool(args.datasource)}")
59
+
60
+ if args.datasource == "json":
61
+ client = pygazpar.Client(pygazpar.JsonWebDataSource(args.username, args.password))
62
+ elif args.datasource == "excel":
63
+ client = pygazpar.Client(pygazpar.ExcelWebDataSource(args.username, args.password, args.tmpdir))
64
+ elif args.datasource == "test":
65
+ client = pygazpar.Client(pygazpar.TestDataSource())
66
+ else:
67
+ raise ValueError("Invalid datasource: (json | excel | test) is expected")
68
+
69
+ try:
70
+ data = client.loadSince(args.pce, int(args.lastNDays), [args.frequency])
71
+ except BaseException: # pylint: disable=broad-except
72
+ print("An error occured while querying PyGazpar library : %s", traceback.format_exc())
73
+ return 1
74
+
75
+ print(json.dumps(data, indent=2))
76
+
77
+ return 0
78
+
79
+
80
+ if __name__ == "__main__":
81
+ sys.exit(main())
pygazpar/client.py CHANGED
@@ -1,253 +1,64 @@
1
- import os
2
- import time
3
- import glob
4
- import logging
5
- from datetime import datetime
6
- from openpyxl import load_workbook
7
- from pygazpar.enum import PropertyNameEnum
8
- from .webdriverwrapper import WebDriverWrapper
9
-
10
- HOME_URL = 'https://monespace.grdf.fr'
11
- LOGIN_URL = HOME_URL + '/monespace/connexion'
12
- WELCOME_URL = HOME_URL + '/monespace/particulier/accueil'
13
- DATA_URL = HOME_URL + '/monespace/particulier/consommation/consommations'
14
- DATA_FILENAME = 'Consommations gaz_*.xlsx'
15
-
16
- DEFAULT_TMP_DIRECTORY = '/tmp'
17
- DEFAULT_FIREFOX_WEBDRIVER = os.getcwd() + '/geckodriver'
18
- DEFAULT_WAIT_TIME = 30
19
-
20
- # ------------------------------------------------------------------------------------------------------------
21
- class LoginError(Exception):
22
- """ Client has failed to login in GrDF Web site (check username/password)"""
23
- pass
24
-
25
-
26
- # ------------------------------------------------------------------------------------------------------------
27
- class Client(object):
28
-
29
- logger = logging.getLogger(__name__)
30
-
31
- # ------------------------------------------------------
32
- def __init__(self, username: str, password: str, firefox_webdriver_executable: str = DEFAULT_FIREFOX_WEBDRIVER, wait_time: int = DEFAULT_WAIT_TIME, tmp_directory: str = DEFAULT_TMP_DIRECTORY, lastNRows: int = 0):
33
- self.__username = username
34
- self.__password = password
35
- self.__firefox_webdriver_executable = firefox_webdriver_executable
36
- self.__wait_time = wait_time
37
- self.__tmp_directory = tmp_directory
38
- self.__data = []
39
- self.__lastNRows = lastNRows
40
-
41
-
42
- # ------------------------------------------------------
43
- def data(self):
44
- return self.__data
45
-
46
-
47
- # ------------------------------------------------------
48
- def acceptCookies(self, driver: WebDriverWrapper):
49
-
50
- try:
51
- cookies_accept_button = driver.find_element_by_xpath("//a[@id='_EPcommonPage_WAR_EPportlet_:formBandeauCnil:j_idt12']", "Cookies accept button", False)
52
- cookies_accept_button.click()
53
- except:
54
- # Do nothing, because the Pop up may not appear.
55
- pass
56
-
57
-
58
- # ------------------------------------------------------
59
- def acceptPrivacyConditions(self, driver: WebDriverWrapper):
60
-
61
- # linkText=Tout accepter
62
- # css=#btn_accept_banner
63
-
64
- try:
65
- # id=btn_accept_banner
66
- accept_button = driver.find_element_by_id("btn_accept_banner", "Privacy Conditions accept button", False)
67
- accept_button.click()
68
- except:
69
- # Do nothing, because the Pop up may not appear.
70
- pass
71
-
72
- try:
73
- # xpath=//a[contains(text(),'Tout accepter')]
74
- accept_button = driver.find_element_by_xpath("//a[contains(text(),'Tout accepter')]", "Privacy Conditions accept button", False)
75
- accept_button.click()
76
- except:
77
- # Do nothing, because the Pop up may not appear.
78
- pass
79
-
80
- try:
81
- # xpath=//a[@id='btn_accept_banner']
82
- accept_button = driver.find_element_by_xpath("//a[@id='btn_accept_banner']", "Privacy Conditions accept button", False)
83
- accept_button.click()
84
- except:
85
- # Do nothing, because the Pop up may not appear.
86
- pass
87
-
88
- try:
89
- # xpath=//div[@id='ckieBnr_banner']/div[2]/a[2]
90
- accept_button = driver.find_element_by_xpath("//div[@id='ckieBnr_banner']/div[2]/a[2]", "Privacy Conditions accept button", False)
91
- accept_button.click()
92
- except:
93
- # Do nothing, because the Pop up may not appear.
94
- pass
95
-
96
- try:
97
- # xpath=//div[9]/div[2]/a[2]
98
- accept_button = driver.find_element_by_xpath("//div[9]/div[2]/a[2]", "Privacy Conditions accept button", False)
99
- accept_button.click()
100
- except:
101
- # Do nothing, because the Pop up may not appear.
102
- pass
103
-
104
- try:
105
- # xpath=//a[contains(.,'Tout accepter')]
106
- accept_button = driver.find_element_by_xpath("//a[contains(.,'Tout accepter')]", "Privacy Conditions accept button", False)
107
- accept_button.click()
108
- except:
109
- # Do nothing, because the Pop up may not appear.
110
- pass
111
-
112
-
113
- # ------------------------------------------------------
114
- def closeEventualPopup(self, driver: WebDriverWrapper):
115
-
116
- # Accept an eventual Privacy Conditions popup.
117
- self.acceptPrivacyConditions(driver)
118
-
119
- # Eventually, click Accept in the lower banner to accept cookies from the site.
120
- self.acceptCookies(driver)
121
-
122
- # Eventually, close Advertisement Popup Windows.
123
- try:
124
- advertisement_popup_element = driver.find_element_by_xpath("/html/body/abtasty-modal/div/div[1]", "Advertisement close button", False)
125
- advertisement_popup_element.click()
126
- except:
127
- # Do nothing, because the Pop up may not appear.
128
- pass
129
-
130
- # Eventually, close Survey Popup Windows : /html/body/div[12]/div[2] or //*[@id="mfbIframeClose"]
131
- try:
132
- survey_popup_element = driver.find_element_by_xpath("//*[@id='mfbIframeClose']", "Survey close button", False)
133
- survey_popup_element.click()
134
- except:
135
- # Do nothing, because the Pop up may not appear.
136
- pass
137
-
138
-
139
- # ------------------------------------------------------
140
- def update(self):
141
-
142
- Client.logger.debug("Start updating the data...")
143
-
144
- # XLSX is in the TMP directory
145
- data_file_path_pattern = self.__tmp_directory + '/' + DATA_FILENAME
146
-
147
- # We remove an eventual existing data file (from a previous run that has not deleted it)
148
- file_list = glob.glob(data_file_path_pattern)
149
- for filename in file_list:
150
- if os.path.isfile(filename):
151
- os.remove(filename)
152
-
153
- # Create the WebDriver with the ability to log and take screenshot for debugging.
154
- driver = WebDriverWrapper(self.__firefox_webdriver_executable, self.__wait_time, self.__tmp_directory)
155
-
156
- try:
157
-
158
- ## Login URL
159
- driver.get(LOGIN_URL, "Go to login page")
160
-
161
- # Fill login form
162
- email_element = driver.find_element_by_id("_EspacePerso_WAR_EPportlet_:seConnecterForm:email", "Login page: Email text field")
163
- password_element = driver.find_element_by_id("_EspacePerso_WAR_EPportlet_:seConnecterForm:passwordSecretSeConnecter", "Login page: Password text field")
164
-
165
- email_element.send_keys(self.__username)
166
- password_element.send_keys(self.__password)
167
-
168
- # Submit the login form.
169
- submit_button_element = driver.find_element_by_id("_EspacePerso_WAR_EPportlet_:seConnecterForm:meConnecter", "Login page: 'Me connecter' button")
170
- submit_button_element.click()
171
-
172
- # Close eventual popup Windows or Assistant appearing.
173
- self.closeEventualPopup(driver)
174
-
175
- # Once we find the 'Acceder' button from the main page, we are logged on successfully.
176
- try:
177
- driver.find_element_by_xpath("//div[2]/div[2]/div/a/div", "Welcome page: 'Acceder' button of 'Suivi de consommation'")
178
- except:
179
- # Perhaps, login has failed.
180
- if driver.current_url() == WELCOME_URL:
181
- # We're good.
182
- pass
183
- elif driver.current_url() == LOGIN_URL:
184
- raise LoginError("GrDF sign in has failed, please check your username/password")
185
- else:
186
- raise
187
-
188
- # Page 'votre consommation'
189
- driver.get(DATA_URL, "Go to 'Consommations' page")
190
-
191
- # Wait for the data page to load completely.
192
- time.sleep(self.__wait_time)
193
-
194
- # Eventually, close TokyWoky assistant which may hide the Download button.
195
- try:
196
- tokyWoky_close_button = driver.find_element_by_xpath("//div[@id='toky_container']/div/div", "TokyWoky assistant close button")
197
- tokyWoky_close_button.click()
198
- except:
199
- # Do nothing, because the Pop up may not appear.
200
- pass
201
-
202
- # Select daily consumption
203
- daily_consumption_element = driver.find_element_by_xpath("//table[@id='_eConsoconsoDetaille_WAR_eConsoportlet_:idFormConsoDetaille:panelTypeGranularite1']/tbody/tr/td[3]/label", "Daily consumption button")
204
- daily_consumption_element.click()
205
-
206
- # Download file
207
- # xpath=//button[@id='_eConsoconsoDetaille_WAR_eConsoportlet_:idFormConsoDetaille:telechargerDonnees']/span
208
- download_button_element = driver.find_element_by_xpath("//button[@onclick=\"envoieGATelechargerConsoDetaille('particulier', 'jour_kwh');\"]/span", "Download button")
209
- download_button_element.click()
210
-
211
- # Timestamp of the data.
212
- data_timestamp = datetime.now().isoformat()
213
-
214
- # Wait a few for the download to complete
215
- time.sleep(self.__wait_time)
216
-
217
- # Load the XLSX file into the data structure
218
- file_list = glob.glob(data_file_path_pattern)
219
-
220
- for filename in file_list:
221
-
222
- Client.logger.debug(f"Loading Excel data file '{filename}'...")
223
- wb = load_workbook(filename = filename)
224
- ws = wb['Historique par jour']
225
- minRowNum = max(8, len(ws['B'])+1-self.__lastNRows) if self.__lastNRows > 0 else 8
226
- maxRowNum = len(ws['B'])
227
- for rownum in range(minRowNum, maxRowNum + 1):
228
- row = {}
229
- if ws.cell(column=2, row=rownum).value != None:
230
- row[PropertyNameEnum.DATE.value] = ws.cell(column=2, row=rownum).value
231
- row[PropertyNameEnum.START_INDEX_M3.value] = ws.cell(column=3, row=rownum).value
232
- row[PropertyNameEnum.END_INDEX_M3.value] = ws.cell(column=4, row=rownum).value
233
- row[PropertyNameEnum.VOLUME_M3.value] = ws.cell(column=5, row=rownum).value
234
- row[PropertyNameEnum.ENERGY_KWH.value] = ws.cell(column=6, row=rownum).value
235
- row[PropertyNameEnum.CONVERTER_FACTOR.value] = ws.cell(column=7, row=rownum).value
236
- row[PropertyNameEnum.LOCAL_TEMPERATURE.value] = ws.cell(column=8, row=rownum).value
237
- row[PropertyNameEnum.TYPE.value] = ws.cell(column=9, row=rownum).value
238
- row[PropertyNameEnum.TIMESTAMP.value] = data_timestamp
239
- self.__data.append(row)
240
- wb.close()
241
- Client.logger.debug(f"Data read successfully between row #{minRowNum} and row #{maxRowNum}")
242
-
243
- os.remove(filename)
244
-
245
- Client.logger.debug("The data update terminates normally")
246
-
247
-
248
- except Exception:
249
- WebDriverWrapper.logger.error(f"An unexpected error occured while updating the data", exc_info=True)
250
- finally:
251
- # Quit the driver
252
- driver.quit()
253
-
1
+ import logging
2
+ from datetime import date, timedelta
3
+ from typing import List, Optional
4
+
5
+ from pygazpar.datasource import IDataSource, MeterReadingsByFrequency
6
+ from pygazpar.enum import Frequency
7
+
8
+ AUTH_NONCE_URL = "https://monespace.grdf.fr/client/particulier/accueil"
9
+ LOGIN_URL = "https://login.monespace.grdf.fr/sofit-account-api/api/v1/auth"
10
+ LOGIN_HEADER = {"domain": "grdf.fr"}
11
+ LOGIN_PAYLOAD = """{{
12
+ "email": "{0}",
13
+ "password": "{1}",
14
+ "capp": "meg",
15
+ "goto": "https://sofa-connexion.grdf.fr:443/openam/oauth2/externeGrdf/authorize?response_type=code&scope=openid%20profile%20email%20infotravaux%20%2Fv1%2Faccreditation%20%2Fv1%2Faccreditations%20%2Fdigiconso%2Fv1%20%2Fdigiconso%2Fv1%2Fconsommations%20new_meg&client_id=prod_espaceclient&state=0&redirect_uri=https%3A%2F%2Fmonespace.grdf.fr%2F_codexch&nonce={2}&by_pass_okta=1&capp=meg"}}"""
16
+ DATA_URL = "https://monespace.grdf.fr/api/e-conso/pce/consommation/informatives/telecharger?dateDebut={1}&dateFin={2}&frequence={0}&pceList%5B%5D={3}"
17
+ DATA_FILENAME = "Donnees_informatives_*.xlsx"
18
+
19
+ DEFAULT_TMP_DIRECTORY = "/tmp"
20
+ DEFAULT_LAST_N_DAYS = 365
21
+
22
+
23
+ Logger = logging.getLogger(__name__)
24
+
25
+
26
+ # ------------------------------------------------------------------------------------------------------------
27
+ class Client:
28
+
29
+ # ------------------------------------------------------
30
+ def __init__(self, dataSource: IDataSource):
31
+ self.__dataSource = dataSource
32
+
33
+ # ------------------------------------------------------
34
+ def loadSince(
35
+ self, pceIdentifier: str, lastNDays: int = DEFAULT_LAST_N_DAYS, frequencies: Optional[List[Frequency]] = None
36
+ ) -> MeterReadingsByFrequency:
37
+
38
+ try:
39
+ endDate = date.today()
40
+ startDate = endDate + timedelta(days=-lastNDays)
41
+
42
+ res = self.loadDateRange(pceIdentifier, startDate, endDate, frequencies)
43
+ except Exception:
44
+ Logger.error("An unexpected error occured while loading the data", exc_info=True)
45
+ raise
46
+
47
+ return res
48
+
49
+ # ------------------------------------------------------
50
+ def loadDateRange(
51
+ self, pceIdentifier: str, startDate: date, endDate: date, frequencies: Optional[List[Frequency]] = None
52
+ ) -> MeterReadingsByFrequency:
53
+
54
+ Logger.debug("Start loading the data...")
55
+
56
+ try:
57
+ res = self.__dataSource.load(pceIdentifier, startDate, endDate, frequencies)
58
+
59
+ Logger.debug("The data load terminates normally")
60
+ except Exception:
61
+ Logger.error("An unexpected error occured while loading the data", exc_info=True)
62
+ raise
63
+
64
+ return res