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.
Files changed (25) hide show
  1. {e_data-1.2.19/e_data.egg-info → e_data-1.2.21}/PKG-INFO +1 -1
  2. {e_data-1.2.19 → e_data-1.2.21/e_data.egg-info}/PKG-INFO +1 -1
  3. {e_data-1.2.19 → e_data-1.2.21}/edata/connectors/datadis.py +78 -79
  4. {e_data-1.2.19 → e_data-1.2.21}/edata/helpers.py +26 -14
  5. {e_data-1.2.19 → e_data-1.2.21}/setup.py +1 -1
  6. {e_data-1.2.19 → e_data-1.2.21}/LICENSE +0 -0
  7. {e_data-1.2.19 → e_data-1.2.21}/MANIFEST.in +0 -0
  8. {e_data-1.2.19 → e_data-1.2.21}/README.md +0 -0
  9. {e_data-1.2.19 → e_data-1.2.21}/e_data.egg-info/SOURCES.txt +0 -0
  10. {e_data-1.2.19 → e_data-1.2.21}/e_data.egg-info/dependency_links.txt +0 -0
  11. {e_data-1.2.19 → e_data-1.2.21}/e_data.egg-info/requires.txt +0 -0
  12. {e_data-1.2.19 → e_data-1.2.21}/e_data.egg-info/top_level.txt +0 -0
  13. {e_data-1.2.19 → e_data-1.2.21}/edata/__init__.py +0 -0
  14. {e_data-1.2.19 → e_data-1.2.21}/edata/connectors/__init__.py +0 -0
  15. {e_data-1.2.19 → e_data-1.2.21}/edata/connectors/redata.py +0 -0
  16. {e_data-1.2.19 → e_data-1.2.21}/edata/const.py +0 -0
  17. {e_data-1.2.19 → e_data-1.2.21}/edata/definitions.py +0 -0
  18. {e_data-1.2.19 → e_data-1.2.21}/edata/processors/__init__.py +0 -0
  19. {e_data-1.2.19 → e_data-1.2.21}/edata/processors/base.py +0 -0
  20. {e_data-1.2.19 → e_data-1.2.21}/edata/processors/billing.py +0 -0
  21. {e_data-1.2.19 → e_data-1.2.21}/edata/processors/consumption.py +0 -0
  22. {e_data-1.2.19 → e_data-1.2.21}/edata/processors/maximeter.py +0 -0
  23. {e_data-1.2.19 → e_data-1.2.21}/edata/processors/utils.py +0 -0
  24. {e_data-1.2.19 → e_data-1.2.21}/edata/storage.py +0 -0
  25. {e_data-1.2.19 → e_data-1.2.21}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: e-data
3
- Version: 1.2.19
3
+ Version: 1.2.21
4
4
  Summary: Python library for managing spanish energy data from various web providers
5
5
  Home-page: https://github.com/uvejota/python-edata
6
6
  Author: VMG
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: e-data
3
- Version: 1.2.19
3
+ Version: 1.2.21
4
4
  Summary: Python library for managing spanish energy data from various web providers
5
5
  Home-page: https://github.com/uvejota/python-edata
6
6
  Author: VMG
@@ -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
- RECENT_QUERIES_FILENAME = "edata_recent_queries.json"
71
- RECENT_QUERIES_CACHE_FILENAME = "edata_recent_queries_cache.json"
72
- DEFAULT_RECENT_QUERIES_FILE = f"/tmp/{RECENT_QUERIES_FILENAME}"
73
- DEFAULT_RECENT_QUERIES_CACHE = f"/tmp/{RECENT_QUERIES_CACHE_FILENAME}"
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._recent_queries_file = os.path.join(
98
- storage_path, RECENT_QUERIES_FILENAME
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._recent_queries_file = DEFAULT_RECENT_QUERIES_FILE
105
- self._recent_queries_cache_file = DEFAULT_RECENT_QUERIES_CACHE
106
- self._warned_queries = []
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
- with open(self._recent_queries_cache_file, encoding="utf8") as dst_file:
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
- with FileLock(self._recent_queries_file + ".lock"):
128
- self._load_recent_cache()
129
- # identify the query by a md5 hash
130
- hash_query = hashlib.md5(query.encode()).hexdigest()
131
- # prepare key (hash) and values (timestamp and response)
132
- self._recent_queries[hash_query] = datetime.now()
133
- if data is not None:
134
- self._recent_cache[hash_query] = data
135
-
136
- # purge old cache
137
- to_delete = [
138
- x
139
- for x in self._recent_queries
140
- if (datetime.now() - self._recent_queries[x]) > QUERY_LIMIT
141
- ]
142
-
143
- for key in to_delete:
144
- with contextlib.suppress(KeyError):
145
- self._recent_queries.pop(key, None)
146
- self._recent_cache.pop(key, None)
147
-
148
- # dump current cache to disk
149
- try:
150
- with open(self._recent_queries_file, "w", encoding="utf8") as dst_file:
151
- json.dump(utils.serialize_dict(self._recent_queries), dst_file)
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
- if hash_query in self._recent_queries:
167
- return (datetime.now() - self._recent_queries[hash_query]) < QUERY_LIMIT
168
- return False
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
- _LOGGER.info("Serving '%s' from cache", query)
174
- return self._recent_cache.get(hash_query, None)
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.debug("GET %s", url + params)
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 + params)
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 at %s", url + params)
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
- "%s %s at %s",
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
- "%s %s at %s. %s. %s",
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
- x["from"].isoformat() + " - " + x["to"].isoformat()
329
- for x in miss_maxim
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: request consumptions from %s to %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: request maximeter from %s to %s",
379
+ "%s: requesting maximeter from %s to %s",
368
380
  self._scups,
369
381
  start.isoformat(),
370
382
  end.isoformat(),
@@ -20,7 +20,7 @@ URL = "https://github.com/uvejota/python-edata"
20
20
  EMAIL = "vmayorg@outlook.es"
21
21
  AUTHOR = "VMG"
22
22
  REQUIRES_PYTHON = ">=3.6.0"
23
- VERSION = "1.2.19"
23
+ VERSION = "1.2.21"
24
24
 
25
25
  # What packages are required for this module to be executed?
26
26
  REQUIRED = [
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes