pygazpar 0.1.21__py3-none-any.whl → 1.3.0b2__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,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,210 +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
- 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
+ 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