pygazpar 0.1.20__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/client.py CHANGED
@@ -1,253 +1,98 @@
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
+ import warnings
3
+ from datetime import date, timedelta
4
+ from typing import Optional
5
+
6
+ from pygazpar.datasource import IDataSource, MeterReadingsByFrequency
7
+ from pygazpar.enum import Frequency
8
+
9
+ DEFAULT_LAST_N_DAYS = 365
10
+
11
+
12
+ Logger = logging.getLogger(__name__)
13
+
14
+
15
+ # ------------------------------------------------------------------------------------------------------------
16
+ class Client:
17
+
18
+ # ------------------------------------------------------
19
+ def __init__(self, dataSource: IDataSource):
20
+ self.__dataSource = dataSource
21
+
22
+ # ------------------------------------------------------
23
+ def login(self):
24
+
25
+ try:
26
+ self.__dataSource.login()
27
+ except Exception:
28
+ Logger.error("An unexpected error occured while login", exc_info=True)
29
+ raise
30
+
31
+ # ------------------------------------------------------
32
+ def logout(self):
33
+
34
+ try:
35
+ self.__dataSource.logout()
36
+ except Exception:
37
+ Logger.error("An unexpected error occured while logout", exc_info=True)
38
+ raise
39
+
40
+ # ------------------------------------------------------
41
+ def get_pce_identifiers(self) -> list[str]:
42
+
43
+ try:
44
+ res = self.__dataSource.get_pce_identifiers()
45
+ except Exception:
46
+ Logger.error("An unexpected error occured while getting the PCE identifiers", exc_info=True)
47
+ raise
48
+
49
+ return res
50
+
51
+ # ------------------------------------------------------
52
+ def load_since(
53
+ self, pce_identifier: str, last_n_days: int = DEFAULT_LAST_N_DAYS, frequencies: Optional[list[Frequency]] = None
54
+ ) -> MeterReadingsByFrequency:
55
+
56
+ end_date = date.today()
57
+ start_date = end_date + timedelta(days=-last_n_days)
58
+
59
+ res = self.load_date_range(pce_identifier, start_date, end_date, frequencies)
60
+
61
+ return res
62
+
63
+ # ------------------------------------------------------
64
+ def load_date_range(
65
+ self, pce_identifier: str, start_date: date, end_date: date, frequencies: Optional[list[Frequency]] = None
66
+ ) -> MeterReadingsByFrequency:
67
+
68
+ Logger.debug("Start loading the data...")
69
+
70
+ try:
71
+ res = self.__dataSource.load(pce_identifier, start_date, end_date, frequencies)
72
+
73
+ Logger.debug("The data load terminates normally")
74
+ except Exception:
75
+ Logger.error("An unexpected error occured while loading the data", exc_info=True)
76
+ raise
77
+
78
+ return res
79
+
80
+ # ------------------------------------------------------
81
+ def loadSince(
82
+ self, pceIdentifier: str, lastNDays: int = DEFAULT_LAST_N_DAYS, frequencies: Optional[list[Frequency]] = None
83
+ ) -> MeterReadingsByFrequency:
84
+ warnings.warn(
85
+ "Client.loadSince() method will be removed in 2026-01-01. Please migrate to Client.load_since() method",
86
+ DeprecationWarning,
87
+ )
88
+ return self.load_since(pceIdentifier, lastNDays, frequencies)
89
+
90
+ # ------------------------------------------------------
91
+ def loadDateRange(
92
+ self, pceIdentifier: str, startDate: date, endDate: date, frequencies: Optional[list[Frequency]] = None
93
+ ) -> MeterReadingsByFrequency:
94
+ warnings.warn(
95
+ "Client.loadDateRange() method will be removed in 2026-01-01. Please migrate to Client.load_date_range() method",
96
+ DeprecationWarning,
97
+ )
98
+ return self.load_date_range(pceIdentifier, startDate, endDate, frequencies)