e-data 1.2.19__tar.gz → 1.2.20__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.20}/PKG-INFO +1 -1
  2. {e_data-1.2.19 → e_data-1.2.20/e_data.egg-info}/PKG-INFO +1 -1
  3. {e_data-1.2.19 → e_data-1.2.20}/edata/connectors/datadis.py +78 -79
  4. {e_data-1.2.19 → e_data-1.2.20}/edata/helpers.py +22 -14
  5. {e_data-1.2.19 → e_data-1.2.20}/setup.py +1 -1
  6. {e_data-1.2.19 → e_data-1.2.20}/LICENSE +0 -0
  7. {e_data-1.2.19 → e_data-1.2.20}/MANIFEST.in +0 -0
  8. {e_data-1.2.19 → e_data-1.2.20}/README.md +0 -0
  9. {e_data-1.2.19 → e_data-1.2.20}/e_data.egg-info/SOURCES.txt +0 -0
  10. {e_data-1.2.19 → e_data-1.2.20}/e_data.egg-info/dependency_links.txt +0 -0
  11. {e_data-1.2.19 → e_data-1.2.20}/e_data.egg-info/requires.txt +0 -0
  12. {e_data-1.2.19 → e_data-1.2.20}/e_data.egg-info/top_level.txt +0 -0
  13. {e_data-1.2.19 → e_data-1.2.20}/edata/__init__.py +0 -0
  14. {e_data-1.2.19 → e_data-1.2.20}/edata/connectors/__init__.py +0 -0
  15. {e_data-1.2.19 → e_data-1.2.20}/edata/connectors/redata.py +0 -0
  16. {e_data-1.2.19 → e_data-1.2.20}/edata/const.py +0 -0
  17. {e_data-1.2.19 → e_data-1.2.20}/edata/definitions.py +0 -0
  18. {e_data-1.2.19 → e_data-1.2.20}/edata/processors/__init__.py +0 -0
  19. {e_data-1.2.19 → e_data-1.2.20}/edata/processors/base.py +0 -0
  20. {e_data-1.2.19 → e_data-1.2.20}/edata/processors/billing.py +0 -0
  21. {e_data-1.2.19 → e_data-1.2.20}/edata/processors/consumption.py +0 -0
  22. {e_data-1.2.19 → e_data-1.2.20}/edata/processors/maximeter.py +0 -0
  23. {e_data-1.2.19 → e_data-1.2.20}/edata/processors/utils.py +0 -0
  24. {e_data-1.2.19 → e_data-1.2.20}/edata/storage.py +0 -0
  25. {e_data-1.2.19 → e_data-1.2.20}/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.20
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.20
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(storage_dir, "edata_recent_queries.json")
79
+ os.remove(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
  )
@@ -285,6 +285,9 @@ class EdataHelper:
285
285
  _LOGGER.info(
286
286
  "%s: CUPS start date is %s", self._scups, supply_date_start.isoformat()
287
287
  )
288
+ _LOGGER.info(
289
+ "%s: CUPS end date is %s", self._scups, supply["date_end"].isoformat()
290
+ )
288
291
 
289
292
  # update contracts to get valid periods
290
293
  self.update_contracts(cups, distributor_code)
@@ -293,7 +296,7 @@ class EdataHelper:
293
296
  "%s: contracts update failed or no contracts found in the provided account",
294
297
  self._scups,
295
298
  )
296
- return False
299
+ # return False
297
300
 
298
301
  # filter consumptions and maximeter, and log gaps
299
302
  def sort_and_filter(dt_from, dt_to):
@@ -313,25 +316,20 @@ class EdataHelper:
313
316
 
314
317
  miss_cons, miss_maxim = sort_and_filter(date_from, date_to)
315
318
 
319
+ # update consumptions
316
320
  _LOGGER.info(
317
321
  "%s: missing consumptions: %s",
318
322
  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
323
  ", ".join(
327
324
  [
328
- x["from"].isoformat() + " - " + x["to"].isoformat()
329
- for x in miss_maxim
325
+ "from "
326
+ + (x["from"] + timedelta(hours=1)).isoformat()
327
+ + " to "
328
+ + x["to"].isoformat()
329
+ for x in miss_cons
330
330
  ]
331
331
  ),
332
332
  )
333
-
334
- # update consumptions
335
333
  for gap in miss_cons:
336
334
  if not (
337
335
  gap["to"] < supply["date_start"] or gap["from"] > supply["date_end"]
@@ -340,7 +338,7 @@ class EdataHelper:
340
338
  start = max([gap["from"] + timedelta(hours=1), supply["date_start"]])
341
339
  end = min([gap["to"], supply["date_end"]])
342
340
  _LOGGER.info(
343
- "%s: request consumptions from %s to %s",
341
+ "%s: requesting consumptions from %s to %s",
344
342
  self._scups,
345
343
  start.isoformat(),
346
344
  end.isoformat(),
@@ -355,6 +353,16 @@ class EdataHelper:
355
353
  )
356
354
 
357
355
  # update maximeter
356
+ _LOGGER.info(
357
+ "%s: missing maximeter: %s",
358
+ self._scups,
359
+ ", ".join(
360
+ [
361
+ "from " + x["from"].isoformat() + " to " + x["to"].isoformat()
362
+ for x in miss_maxim
363
+ ]
364
+ ),
365
+ )
358
366
  for gap in miss_maxim:
359
367
  if not (date_to < supply["date_start"] or date_from > supply["date_end"]):
360
368
  # fetch maximeter for each maximeter gap in valid periods
@@ -364,7 +372,7 @@ class EdataHelper:
364
372
  end = min([gap["to"], supply["date_end"]])
365
373
  start = min([start, end])
366
374
  _LOGGER.info(
367
- "%s: request maximeter from %s to %s",
375
+ "%s: requesting maximeter from %s to %s",
368
376
  self._scups,
369
377
  start.isoformat(),
370
378
  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.20"
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