e-data 1.2.19__tar.gz → 1.2.21__tar.gz
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.
- {e_data-1.2.19/e_data.egg-info → e_data-1.2.21}/PKG-INFO +1 -1
- {e_data-1.2.19 → e_data-1.2.21/e_data.egg-info}/PKG-INFO +1 -1
- {e_data-1.2.19 → e_data-1.2.21}/edata/connectors/datadis.py +78 -79
- {e_data-1.2.19 → e_data-1.2.21}/edata/helpers.py +26 -14
- {e_data-1.2.19 → e_data-1.2.21}/setup.py +1 -1
- {e_data-1.2.19 → e_data-1.2.21}/LICENSE +0 -0
- {e_data-1.2.19 → e_data-1.2.21}/MANIFEST.in +0 -0
- {e_data-1.2.19 → e_data-1.2.21}/README.md +0 -0
- {e_data-1.2.19 → e_data-1.2.21}/e_data.egg-info/SOURCES.txt +0 -0
- {e_data-1.2.19 → e_data-1.2.21}/e_data.egg-info/dependency_links.txt +0 -0
- {e_data-1.2.19 → e_data-1.2.21}/e_data.egg-info/requires.txt +0 -0
- {e_data-1.2.19 → e_data-1.2.21}/e_data.egg-info/top_level.txt +0 -0
- {e_data-1.2.19 → e_data-1.2.21}/edata/__init__.py +0 -0
- {e_data-1.2.19 → e_data-1.2.21}/edata/connectors/__init__.py +0 -0
- {e_data-1.2.19 → e_data-1.2.21}/edata/connectors/redata.py +0 -0
- {e_data-1.2.19 → e_data-1.2.21}/edata/const.py +0 -0
- {e_data-1.2.19 → e_data-1.2.21}/edata/definitions.py +0 -0
- {e_data-1.2.19 → e_data-1.2.21}/edata/processors/__init__.py +0 -0
- {e_data-1.2.19 → e_data-1.2.21}/edata/processors/base.py +0 -0
- {e_data-1.2.19 → e_data-1.2.21}/edata/processors/billing.py +0 -0
- {e_data-1.2.19 → e_data-1.2.21}/edata/processors/consumption.py +0 -0
- {e_data-1.2.19 → e_data-1.2.21}/edata/processors/maximeter.py +0 -0
- {e_data-1.2.19 → e_data-1.2.21}/edata/processors/utils.py +0 -0
- {e_data-1.2.19 → e_data-1.2.21}/edata/storage.py +0 -0
- {e_data-1.2.19 → e_data-1.2.21}/setup.cfg +0 -0
|
@@ -8,13 +8,14 @@ There a few issues that are workarounded:
|
|
|
8
8
|
|
|
9
9
|
import contextlib
|
|
10
10
|
from datetime import datetime, timedelta
|
|
11
|
+
import glob
|
|
11
12
|
import hashlib
|
|
12
13
|
import json
|
|
13
14
|
import logging
|
|
14
15
|
import os
|
|
16
|
+
import tempfile
|
|
15
17
|
|
|
16
18
|
from dateutil.relativedelta import relativedelta
|
|
17
|
-
from filelock import FileLock
|
|
18
19
|
import requests
|
|
19
20
|
|
|
20
21
|
from ..definitions import ConsumptionData, ContractData, MaxPowerData, SupplyData
|
|
@@ -67,10 +68,15 @@ TIMEOUT = 3 * 60 # requests timeout
|
|
|
67
68
|
QUERY_LIMIT = timedelta(hours=24) # a datadis limitation, again...
|
|
68
69
|
|
|
69
70
|
# Cache-related constants
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
RECENT_CACHE_SUBDIR = "cache"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def migrate_storage(storage_dir):
|
|
75
|
+
"""Migrate storage from older versions."""
|
|
76
|
+
|
|
77
|
+
with contextlib.suppress(FileNotFoundError):
|
|
78
|
+
os.remove(os.path.join(storage_dir, "edata_recent_queries.json"))
|
|
79
|
+
os.remove(os.path.join(storage_dir, "edata_recent_queries_cache.json"))
|
|
74
80
|
|
|
75
81
|
|
|
76
82
|
class DatadisConnector:
|
|
@@ -93,85 +99,69 @@ class DatadisConnector:
|
|
|
93
99
|
self._smart_fetch = enable_smart_fetch
|
|
94
100
|
self._recent_queries = {}
|
|
95
101
|
self._recent_cache = {}
|
|
102
|
+
self._warned_queries = []
|
|
96
103
|
if storage_path is not None:
|
|
97
|
-
self.
|
|
98
|
-
|
|
99
|
-
)
|
|
100
|
-
self._recent_queries_cache_file = os.path.join(
|
|
101
|
-
storage_path, RECENT_QUERIES_CACHE_FILENAME
|
|
102
|
-
)
|
|
104
|
+
self._recent_cache_dir = os.path.join(storage_path, RECENT_CACHE_SUBDIR)
|
|
105
|
+
migrate_storage(storage_path)
|
|
103
106
|
else:
|
|
104
|
-
self.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
self._load_recent_cache()
|
|
108
|
-
|
|
109
|
-
def _load_recent_cache(self) -> None:
|
|
110
|
-
"""Load caches."""
|
|
111
|
-
|
|
112
|
-
with contextlib.suppress(FileNotFoundError):
|
|
113
|
-
with open(self._recent_queries_file, encoding="utf8") as dst_file:
|
|
114
|
-
recent_queries = json.load(dst_file)
|
|
115
|
-
for query in recent_queries:
|
|
116
|
-
recent_queries[query] = datetime.fromisoformat(
|
|
117
|
-
recent_queries[query]
|
|
118
|
-
)
|
|
119
|
-
self._recent_queries.update(recent_queries)
|
|
107
|
+
self._recent_cache_dir = os.path.join(
|
|
108
|
+
tempfile.gettempdir(), RECENT_CACHE_SUBDIR
|
|
109
|
+
)
|
|
120
110
|
|
|
121
|
-
|
|
122
|
-
self._recent_cache.update(json.load(dst_file))
|
|
111
|
+
os.makedirs(self._recent_cache_dir, exist_ok=True)
|
|
123
112
|
|
|
124
113
|
def _update_recent_queries(self, query: str, data: dict | None = None) -> None:
|
|
125
114
|
"""Cache a successful query to avoid exceeding query limits."""
|
|
126
115
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if data is not None:
|
|
153
|
-
with open(
|
|
154
|
-
self._recent_queries_cache_file, "w", encoding="utf8"
|
|
155
|
-
) as dst_file:
|
|
156
|
-
json.dump(self._recent_cache, dst_file)
|
|
157
|
-
_LOGGER.debug("Local cache updated")
|
|
158
|
-
|
|
159
|
-
except Exception as e:
|
|
160
|
-
_LOGGER.warning("Unknown error while updating cache: %s", e)
|
|
116
|
+
# identify the query by a md5 hash
|
|
117
|
+
hash_query = hashlib.md5(query.encode()).hexdigest()
|
|
118
|
+
|
|
119
|
+
# remove expired cache files
|
|
120
|
+
with contextlib.suppress(FileNotFoundError):
|
|
121
|
+
for cache_file in glob.glob(os.path.join(self._recent_cache_dir, "*")):
|
|
122
|
+
if (
|
|
123
|
+
datetime.now()
|
|
124
|
+
- datetime.fromtimestamp(os.path.getmtime(cache_file))
|
|
125
|
+
) > QUERY_LIMIT:
|
|
126
|
+
_LOGGER.info("Removing cache item '%s'", cache_file)
|
|
127
|
+
os.remove(cache_file)
|
|
128
|
+
|
|
129
|
+
# dump current cache to disk
|
|
130
|
+
try:
|
|
131
|
+
with open(
|
|
132
|
+
os.path.join(self._recent_cache_dir, hash_query),
|
|
133
|
+
"w",
|
|
134
|
+
encoding="utf8",
|
|
135
|
+
) as dst_file:
|
|
136
|
+
json.dump(data, dst_file)
|
|
137
|
+
_LOGGER.info("Updating cache item '%s'", hash_query)
|
|
138
|
+
|
|
139
|
+
except Exception as e:
|
|
140
|
+
_LOGGER.warning("Unknown error while updating cache: %s", e)
|
|
161
141
|
|
|
162
142
|
def _is_recent_query(self, query: str) -> bool:
|
|
163
143
|
"""Check if a query has been done recently to avoid exceeding query limits."""
|
|
144
|
+
|
|
164
145
|
hash_query = hashlib.md5(query.encode()).hexdigest()
|
|
146
|
+
cache_file = os.path.join(self._recent_cache_dir, hash_query)
|
|
165
147
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
148
|
+
return (
|
|
149
|
+
os.path.exists(cache_file)
|
|
150
|
+
and (datetime.now() - datetime.fromtimestamp(os.path.getmtime(cache_file)))
|
|
151
|
+
< QUERY_LIMIT
|
|
152
|
+
)
|
|
169
153
|
|
|
170
|
-
def _get_cache_for_query(self, query: str) -> dict:
|
|
154
|
+
def _get_cache_for_query(self, query: str) -> dict | None:
|
|
171
155
|
"""Return cached response for a query."""
|
|
156
|
+
|
|
172
157
|
hash_query = hashlib.md5(query.encode()).hexdigest()
|
|
173
|
-
|
|
174
|
-
|
|
158
|
+
cache_file = os.path.join(self._recent_cache_dir, hash_query)
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
with open(cache_file, encoding="utf8") as cache:
|
|
162
|
+
return json.load(cache)
|
|
163
|
+
except FileNotFoundError:
|
|
164
|
+
return None
|
|
175
165
|
|
|
176
166
|
def _get_token(self):
|
|
177
167
|
"""Private method that fetches a new token if needed."""
|
|
@@ -227,39 +217,50 @@ class DatadisConnector:
|
|
|
227
217
|
key = param
|
|
228
218
|
value = data[param]
|
|
229
219
|
params = params + f"{key}={value}&"
|
|
220
|
+
anonym_params = "?" if len(data) > 0 else ""
|
|
221
|
+
|
|
222
|
+
# build anonymized params for logging
|
|
223
|
+
for anonym_param in data:
|
|
224
|
+
key = anonym_param
|
|
225
|
+
if key == "cups":
|
|
226
|
+
value = "xxxx" + data[anonym_param][-5:]
|
|
227
|
+
elif key == "authorizedNif":
|
|
228
|
+
value = "xxxx"
|
|
229
|
+
else:
|
|
230
|
+
value = data[anonym_param]
|
|
231
|
+
anonym_params = anonym_params + f"{key}={value}&"
|
|
230
232
|
|
|
231
233
|
# check if query is already in cache
|
|
232
234
|
if not ignore_recent_queries and self._is_recent_query(url + params):
|
|
233
235
|
_cache = self._get_cache_for_query(url + params)
|
|
234
236
|
if _cache is not None:
|
|
237
|
+
_LOGGER.info("Returning cached data")
|
|
235
238
|
return _cache
|
|
236
239
|
return []
|
|
237
240
|
|
|
238
241
|
# run the query
|
|
239
242
|
try:
|
|
240
|
-
_LOGGER.
|
|
243
|
+
_LOGGER.info("GET %s", url + anonym_params)
|
|
241
244
|
reply = self._session.get(
|
|
242
245
|
url + params,
|
|
243
246
|
headers={"Accept-Encoding": "identity"},
|
|
244
247
|
timeout=TIMEOUT,
|
|
245
248
|
)
|
|
246
249
|
except requests.exceptions.Timeout:
|
|
247
|
-
_LOGGER.warning("Timeout at %s", url +
|
|
250
|
+
_LOGGER.warning("Timeout at %s", url + anonym_params)
|
|
248
251
|
return []
|
|
249
252
|
|
|
250
253
|
# eval response
|
|
251
254
|
if reply.status_code == 200:
|
|
252
255
|
# we're here if reply seems valid
|
|
253
|
-
_LOGGER.info("Got 200 OK
|
|
256
|
+
_LOGGER.info("Got 200 OK")
|
|
254
257
|
if reply.json():
|
|
255
258
|
response = reply.json()
|
|
256
259
|
if not ignore_recent_queries:
|
|
257
260
|
self._update_recent_queries(url + params, response)
|
|
258
261
|
else:
|
|
259
262
|
# this mostly happens when datadis provides an empty response
|
|
260
|
-
_LOGGER.info(
|
|
261
|
-
"Datadis returned an empty response at %s", url + params
|
|
262
|
-
)
|
|
263
|
+
_LOGGER.info("Got an empty response")
|
|
263
264
|
if not ignore_recent_queries:
|
|
264
265
|
self._update_recent_queries(url + params)
|
|
265
266
|
elif reply.status_code == 401 and not refresh_token:
|
|
@@ -273,10 +274,9 @@ class DatadisConnector:
|
|
|
273
274
|
elif reply.status_code == 429:
|
|
274
275
|
# we're here if we exceeded datadis API rates (24h)
|
|
275
276
|
_LOGGER.warning(
|
|
276
|
-
"
|
|
277
|
+
"Got status code '%s' with message '%s'",
|
|
277
278
|
reply.status_code,
|
|
278
279
|
reply.text,
|
|
279
|
-
url + params,
|
|
280
280
|
)
|
|
281
281
|
if not ignore_recent_queries:
|
|
282
282
|
self._update_recent_queries(url + params)
|
|
@@ -284,10 +284,9 @@ class DatadisConnector:
|
|
|
284
284
|
# otherwise, if this was a retried request... warn the user
|
|
285
285
|
if (url + params) not in self._warned_queries:
|
|
286
286
|
_LOGGER.warning(
|
|
287
|
-
"
|
|
287
|
+
"Got status code '%s' with message '%s'. %s. %s",
|
|
288
288
|
reply.status_code,
|
|
289
289
|
reply.text,
|
|
290
|
-
url + params,
|
|
291
290
|
"Query temporary disabled",
|
|
292
291
|
"Future 500 code errors for this query will be silenced until restart",
|
|
293
292
|
)
|
|
@@ -212,6 +212,8 @@ class EdataHelper:
|
|
|
212
212
|
self.data["consumptions"], consumptions, "datetime"
|
|
213
213
|
)
|
|
214
214
|
self.last_update["consumptions"] = datetime.now()
|
|
215
|
+
else:
|
|
216
|
+
_LOGGER.info("%s: consumptions are up to date", self._scups)
|
|
215
217
|
else:
|
|
216
218
|
_LOGGER.info("%s: consumptions are already updated (skipping)", self._scups)
|
|
217
219
|
|
|
@@ -236,6 +238,8 @@ class EdataHelper:
|
|
|
236
238
|
self.data["maximeter"], maximeter, "datetime"
|
|
237
239
|
)
|
|
238
240
|
self.last_update["maximeter"] = datetime.now()
|
|
241
|
+
else:
|
|
242
|
+
_LOGGER.info("%s: maximeter is up to date", self._scups)
|
|
239
243
|
else:
|
|
240
244
|
_LOGGER.info("%s: maximeter is already updated (skipping)", self._scups)
|
|
241
245
|
|
|
@@ -285,6 +289,9 @@ class EdataHelper:
|
|
|
285
289
|
_LOGGER.info(
|
|
286
290
|
"%s: CUPS start date is %s", self._scups, supply_date_start.isoformat()
|
|
287
291
|
)
|
|
292
|
+
_LOGGER.info(
|
|
293
|
+
"%s: CUPS end date is %s", self._scups, supply["date_end"].isoformat()
|
|
294
|
+
)
|
|
288
295
|
|
|
289
296
|
# update contracts to get valid periods
|
|
290
297
|
self.update_contracts(cups, distributor_code)
|
|
@@ -293,7 +300,7 @@ class EdataHelper:
|
|
|
293
300
|
"%s: contracts update failed or no contracts found in the provided account",
|
|
294
301
|
self._scups,
|
|
295
302
|
)
|
|
296
|
-
return False
|
|
303
|
+
# return False
|
|
297
304
|
|
|
298
305
|
# filter consumptions and maximeter, and log gaps
|
|
299
306
|
def sort_and_filter(dt_from, dt_to):
|
|
@@ -313,25 +320,20 @@ class EdataHelper:
|
|
|
313
320
|
|
|
314
321
|
miss_cons, miss_maxim = sort_and_filter(date_from, date_to)
|
|
315
322
|
|
|
323
|
+
# update consumptions
|
|
316
324
|
_LOGGER.info(
|
|
317
325
|
"%s: missing consumptions: %s",
|
|
318
326
|
self._scups,
|
|
319
|
-
", ".join(
|
|
320
|
-
[x["from"].isoformat() + " - " + x["to"].isoformat() for x in miss_cons]
|
|
321
|
-
),
|
|
322
|
-
)
|
|
323
|
-
_LOGGER.info(
|
|
324
|
-
"%s: missing maximeter: %s",
|
|
325
|
-
self._scups,
|
|
326
327
|
", ".join(
|
|
327
328
|
[
|
|
328
|
-
|
|
329
|
-
|
|
329
|
+
"from "
|
|
330
|
+
+ (x["from"] + timedelta(hours=1)).isoformat()
|
|
331
|
+
+ " to "
|
|
332
|
+
+ x["to"].isoformat()
|
|
333
|
+
for x in miss_cons
|
|
330
334
|
]
|
|
331
335
|
),
|
|
332
336
|
)
|
|
333
|
-
|
|
334
|
-
# update consumptions
|
|
335
337
|
for gap in miss_cons:
|
|
336
338
|
if not (
|
|
337
339
|
gap["to"] < supply["date_start"] or gap["from"] > supply["date_end"]
|
|
@@ -340,7 +342,7 @@ class EdataHelper:
|
|
|
340
342
|
start = max([gap["from"] + timedelta(hours=1), supply["date_start"]])
|
|
341
343
|
end = min([gap["to"], supply["date_end"]])
|
|
342
344
|
_LOGGER.info(
|
|
343
|
-
"%s:
|
|
345
|
+
"%s: requesting consumptions from %s to %s",
|
|
344
346
|
self._scups,
|
|
345
347
|
start.isoformat(),
|
|
346
348
|
end.isoformat(),
|
|
@@ -355,6 +357,16 @@ class EdataHelper:
|
|
|
355
357
|
)
|
|
356
358
|
|
|
357
359
|
# update maximeter
|
|
360
|
+
_LOGGER.info(
|
|
361
|
+
"%s: missing maximeter: %s",
|
|
362
|
+
self._scups,
|
|
363
|
+
", ".join(
|
|
364
|
+
[
|
|
365
|
+
"from " + x["from"].isoformat() + " to " + x["to"].isoformat()
|
|
366
|
+
for x in miss_maxim
|
|
367
|
+
]
|
|
368
|
+
),
|
|
369
|
+
)
|
|
358
370
|
for gap in miss_maxim:
|
|
359
371
|
if not (date_to < supply["date_start"] or date_from > supply["date_end"]):
|
|
360
372
|
# fetch maximeter for each maximeter gap in valid periods
|
|
@@ -364,7 +376,7 @@ class EdataHelper:
|
|
|
364
376
|
end = min([gap["to"], supply["date_end"]])
|
|
365
377
|
start = min([start, end])
|
|
366
378
|
_LOGGER.info(
|
|
367
|
-
"%s:
|
|
379
|
+
"%s: requesting maximeter from %s to %s",
|
|
368
380
|
self._scups,
|
|
369
381
|
start.isoformat(),
|
|
370
382
|
end.isoformat(),
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|