pygazpar 0.1.20__py3-none-any.whl → 1.3.0b1__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 +10 -3
- pygazpar/__main__.py +81 -58
- pygazpar/client.py +64 -253
- pygazpar/datasource.py +629 -0
- pygazpar/enum.py +31 -8
- pygazpar/excelparser.py +138 -0
- pygazpar/jsonparser.py +53 -0
- pygazpar/resources/daily_data_sample.json +7802 -0
- pygazpar/resources/hourly_data_sample.json +1 -0
- pygazpar/resources/monthly_data_sample.json +146 -0
- pygazpar/resources/weekly_data_sample.json +614 -0
- pygazpar/resources/yearly_data_sample.json +18 -0
- pygazpar/version.py +3 -0
- pygazpar-0.1.20.dist-info/LICENSE.txt → pygazpar-1.3.0b1.dist-info/LICENSE +21 -21
- pygazpar-1.3.0b1.dist-info/METADATA +220 -0
- pygazpar-1.3.0b1.dist-info/RECORD +17 -0
- {pygazpar-0.1.20.dist-info → pygazpar-1.3.0b1.dist-info}/WHEEL +1 -2
- pygazpar/webdriverwrapper.py +0 -125
- pygazpar/webelementwrapper.py +0 -40
- pygazpar-0.1.20.dist-info/METADATA +0 -149
- pygazpar-0.1.20.dist-info/RECORD +0 -14
- pygazpar-0.1.20.dist-info/entry_points.txt +0 -3
- pygazpar-0.1.20.dist-info/top_level.txt +0 -2
- test/__init__.py +0 -1
- test/test_client.py +0 -50
pygazpar/__init__.py
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
-
from pygazpar.
|
2
|
-
from pygazpar.
|
3
|
-
|
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
|
3
|
-
import
|
4
|
-
import
|
5
|
-
import
|
6
|
-
import
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
parser.
|
14
|
-
|
15
|
-
|
16
|
-
parser.add_argument("-p", "--password",
|
17
|
-
|
18
|
-
|
19
|
-
parser.add_argument(
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
if os.path.
|
42
|
-
os.
|
43
|
-
|
44
|
-
#
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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
|
2
|
-
import
|
3
|
-
import
|
4
|
-
|
5
|
-
from
|
6
|
-
from
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
# ------------------------------------------------------------------------------------------------------------
|
27
|
-
class Client
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
self
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
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
|