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/client.py CHANGED
@@ -1,210 +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
- try:
62
- # id=btn_accept_banner
63
- accept_button = driver.find_element_by_id("btn_accept_banner", "Privacy Conditions accept button", False)
64
- accept_button.click()
65
- except:
66
- # Do nothing, because the Pop up may not appear.
67
- pass
68
-
69
-
70
- # ------------------------------------------------------
71
- def closeEventualPopup(self, driver: WebDriverWrapper):
72
-
73
- # Accept an eventual Privacy Conditions popup.
74
- self.acceptPrivacyConditions(driver)
75
-
76
- # Eventually, click Accept in the lower banner to accept cookies from the site.
77
- self.acceptCookies(driver)
78
-
79
- # Eventually, close Advertisement Popup Windows.
80
- try:
81
- advertisement_popup_element = driver.find_element_by_xpath("/html/body/abtasty-modal/div/div[1]", "Advertisement close button", False)
82
- advertisement_popup_element.click()
83
- except:
84
- # Do nothing, because the Pop up may not appear.
85
- pass
86
-
87
- # Eventually, close Survey Popup Windows : /html/body/div[12]/div[2] or //*[@id="mfbIframeClose"]
88
- try:
89
- survey_popup_element = driver.find_element_by_xpath("//*[@id='mfbIframeClose']", "Survey close button", False)
90
- survey_popup_element.click()
91
- except:
92
- # Do nothing, because the Pop up may not appear.
93
- pass
94
-
95
-
96
- # ------------------------------------------------------
97
- def update(self):
98
-
99
- Client.logger.debug("Start updating the data...")
100
-
101
- # XLSX is in the TMP directory
102
- data_file_path_pattern = self.__tmp_directory + '/' + DATA_FILENAME
103
-
104
- # We remove an eventual existing data file (from a previous run that has not deleted it)
105
- file_list = glob.glob(data_file_path_pattern)
106
- for filename in file_list:
107
- if os.path.isfile(filename):
108
- os.remove(filename)
109
-
110
- # Create the WebDriver with the ability to log and take screenshot for debugging.
111
- driver = WebDriverWrapper(self.__firefox_webdriver_executable, self.__wait_time, self.__tmp_directory)
112
-
113
- try:
114
-
115
- ## Login URL
116
- driver.get(LOGIN_URL, "Go to login page")
117
-
118
- # Fill login form
119
- email_element = driver.find_element_by_id("_EspacePerso_WAR_EPportlet_:seConnecterForm:email", "Login page: Email text field")
120
- password_element = driver.find_element_by_id("_EspacePerso_WAR_EPportlet_:seConnecterForm:passwordSecretSeConnecter", "Login page: Password text field")
121
-
122
- email_element.send_keys(self.__username)
123
- password_element.send_keys(self.__password)
124
-
125
- # Submit the login form.
126
- submit_button_element = driver.find_element_by_id("_EspacePerso_WAR_EPportlet_:seConnecterForm:meConnecter", "Login page: 'Me connecter' button")
127
- submit_button_element.click()
128
-
129
- # Close eventual popup Windows or Assistant appearing.
130
- self.closeEventualPopup(driver)
131
-
132
- # Once we find the 'Acceder' button from the main page, we are logged on successfully.
133
- try:
134
- driver.find_element_by_xpath("//div[2]/div[2]/div/a/div", "Welcome page: 'Acceder' button of 'Suivi de consommation'")
135
- except:
136
- # Perhaps, login has failed.
137
- if driver.current_url() == WELCOME_URL:
138
- # We're good.
139
- pass
140
- elif driver.current_url() == LOGIN_URL:
141
- raise LoginError("GrDF sign in has failed, please check your username/password")
142
- else:
143
- raise
144
-
145
- # Page 'votre consommation'
146
- driver.get(DATA_URL, "Go to 'Consommations' page")
147
-
148
- # Wait for the data page to load completely.
149
- time.sleep(self.__wait_time)
150
-
151
- # Eventually, close TokyWoky assistant which may hide the Download button.
152
- try:
153
- tokyWoky_close_button = driver.find_element_by_xpath("//div[@id='toky_container']/div/div", "TokyWoky assistant close button")
154
- tokyWoky_close_button.click()
155
- except:
156
- # Do nothing, because the Pop up may not appear.
157
- pass
158
-
159
- # Select daily consumption
160
- daily_consumption_element = driver.find_element_by_xpath("//table[@id='_eConsoconsoDetaille_WAR_eConsoportlet_:idFormConsoDetaille:panelTypeGranularite1']/tbody/tr/td[3]/label", "Daily consumption button")
161
- daily_consumption_element.click()
162
-
163
- # Download file
164
- # xpath=//button[@id='_eConsoconsoDetaille_WAR_eConsoportlet_:idFormConsoDetaille:telechargerDonnees']/span
165
- download_button_element = driver.find_element_by_xpath("//button[@onclick=\"envoieGATelechargerConsoDetaille('particulier', 'jour_kwh');\"]/span", "Download button")
166
- download_button_element.click()
167
-
168
- # Timestamp of the data.
169
- data_timestamp = datetime.now().isoformat()
170
-
171
- # Wait a few for the download to complete
172
- time.sleep(self.__wait_time)
173
-
174
- # Load the XLSX file into the data structure
175
- file_list = glob.glob(data_file_path_pattern)
176
-
177
- for filename in file_list:
178
-
179
- Client.logger.debug(f"Loading Excel data file '{filename}'...")
180
- wb = load_workbook(filename = filename)
181
- ws = wb['Historique par jour']
182
- minRowNum = max(8, len(ws['B'])+1-self.__lastNRows) if self.__lastNRows > 0 else 8
183
- maxRowNum = len(ws['B'])
184
- for rownum in range(minRowNum, maxRowNum + 1):
185
- row = {}
186
- if ws.cell(column=2, row=rownum).value != None:
187
- row[PropertyNameEnum.DATE.value] = ws.cell(column=2, row=rownum).value
188
- row[PropertyNameEnum.START_INDEX_M3.value] = ws.cell(column=3, row=rownum).value
189
- row[PropertyNameEnum.END_INDEX_M3.value] = ws.cell(column=4, row=rownum).value
190
- row[PropertyNameEnum.VOLUME_M3.value] = ws.cell(column=5, row=rownum).value
191
- row[PropertyNameEnum.ENERGY_KWH.value] = ws.cell(column=6, row=rownum).value
192
- row[PropertyNameEnum.CONVERTER_FACTOR.value] = ws.cell(column=7, row=rownum).value
193
- row[PropertyNameEnum.LOCAL_TEMPERATURE.value] = ws.cell(column=8, row=rownum).value
194
- row[PropertyNameEnum.TYPE.value] = ws.cell(column=9, row=rownum).value
195
- row[PropertyNameEnum.TIMESTAMP.value] = data_timestamp
196
- self.__data.append(row)
197
- wb.close()
198
- Client.logger.debug(f"Data read successfully between row #{minRowNum} and row #{maxRowNum}")
199
-
200
- os.remove(filename)
201
-
202
- Client.logger.debug("The data update terminates normally")
203
-
204
-
205
- except Exception:
206
- WebDriverWrapper.logger.error(f"An unexpected error occured while updating the data", exc_info=True)
207
- finally:
208
- # Quit the driver
209
- driver.quit()
210
-
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)