pygazpar 0.1.21__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,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