e-data 2.0.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.
- e_data-2.0.0b1.dist-info/METADATA +214 -0
- e_data-2.0.0b1.dist-info/RECORD +21 -0
- e_data-2.0.0b1.dist-info/WHEEL +5 -0
- e_data-2.0.0b1.dist-info/licenses/LICENSE +674 -0
- e_data-2.0.0b1.dist-info/top_level.txt +1 -0
- edata/__init__.py +0 -0
- edata/const.py +56 -0
- edata/helpers.py +312 -0
- edata/tests/__init__.py +0 -0
- edata/tests/connectors/__init__.py +0 -0
- edata/tests/connectors/test_datadis_connector.py +178 -0
- edata/tests/connectors/test_redata_connector.py +29 -0
- edata/tests/services/__init__.py +0 -0
- edata/tests/services/test_billing_service.py +1169 -0
- edata/tests/services/test_consumption_service.py +955 -0
- edata/tests/services/test_contract_service.py +216 -0
- edata/tests/services/test_database_service.py +415 -0
- edata/tests/services/test_maximeter_service.py +83 -0
- edata/tests/services/test_supply_service.py +251 -0
- edata/tests/test_helpers.py +612 -0
- edata/utils.py +164 -0
edata/utils.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Utility functions for edata package."""
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import math
|
|
7
|
+
from copy import deepcopy
|
|
8
|
+
from datetime import date, datetime, timedelta
|
|
9
|
+
from json import JSONEncoder
|
|
10
|
+
from typing import Any, Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
import holidays
|
|
13
|
+
|
|
14
|
+
_LOGGER = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
# PVPC tariff constants
|
|
17
|
+
HOURS_P1 = [10, 11, 12, 13, 18, 19, 20, 21]
|
|
18
|
+
HOURS_P2 = [8, 9, 14, 15, 16, 17, 22, 23]
|
|
19
|
+
WEEKDAYS_P3 = [5, 6]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_pvpc_tariff(a_datetime: datetime) -> str:
|
|
23
|
+
"""Evaluate the PVPC tariff for a given datetime.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
a_datetime: The datetime to evaluate
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
The tariff period: "p1", "p2", or "p3"
|
|
30
|
+
"""
|
|
31
|
+
hdays = holidays.country_holidays("ES")
|
|
32
|
+
hour = a_datetime.hour
|
|
33
|
+
weekday = a_datetime.weekday()
|
|
34
|
+
|
|
35
|
+
if weekday in WEEKDAYS_P3 or a_datetime.date() in hdays:
|
|
36
|
+
return "p3"
|
|
37
|
+
elif hour in HOURS_P1:
|
|
38
|
+
return "p1"
|
|
39
|
+
elif hour in HOURS_P2:
|
|
40
|
+
return "p2"
|
|
41
|
+
else:
|
|
42
|
+
return "p3"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def extend_by_key(
|
|
46
|
+
old_lst: List[Dict[str, Any]], new_lst: List[Dict[str, Any]], key: str
|
|
47
|
+
) -> List[Dict[str, Any]]:
|
|
48
|
+
"""Extend a list of dicts by key."""
|
|
49
|
+
lst = deepcopy(old_lst)
|
|
50
|
+
temp_list = []
|
|
51
|
+
for new_element in new_lst:
|
|
52
|
+
for old_element in lst:
|
|
53
|
+
if new_element[key] == old_element[key]:
|
|
54
|
+
for i in old_element:
|
|
55
|
+
old_element[i] = new_element[i]
|
|
56
|
+
break
|
|
57
|
+
else:
|
|
58
|
+
temp_list.append(new_element)
|
|
59
|
+
lst.extend(temp_list)
|
|
60
|
+
return lst
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def extract_dt_ranges(
|
|
64
|
+
lst: List[Dict[str, Any]],
|
|
65
|
+
dt_from: datetime,
|
|
66
|
+
dt_to: datetime,
|
|
67
|
+
gap_interval: timedelta = timedelta(hours=1),
|
|
68
|
+
) -> tuple:
|
|
69
|
+
"""Filter a list of dicts between two datetimes."""
|
|
70
|
+
new_lst = []
|
|
71
|
+
missing = []
|
|
72
|
+
oldest_dt = None
|
|
73
|
+
newest_dt = None
|
|
74
|
+
last_dt = None
|
|
75
|
+
if len(lst) > 0:
|
|
76
|
+
sorted_lst = sorted(lst, key=lambda i: i["datetime"])
|
|
77
|
+
last_dt = dt_from
|
|
78
|
+
for i in sorted_lst:
|
|
79
|
+
if dt_from <= i["datetime"] <= dt_to:
|
|
80
|
+
if (i["datetime"] - last_dt) > gap_interval:
|
|
81
|
+
missing.append({"from": last_dt, "to": i["datetime"]})
|
|
82
|
+
if i.get("value_kWh", 1) > 0:
|
|
83
|
+
if oldest_dt is None or i["datetime"] < oldest_dt:
|
|
84
|
+
oldest_dt = i["datetime"]
|
|
85
|
+
if newest_dt is None or i["datetime"] > newest_dt:
|
|
86
|
+
newest_dt = i["datetime"]
|
|
87
|
+
if i["datetime"] != last_dt: # remove duplicates
|
|
88
|
+
new_lst.append(i)
|
|
89
|
+
last_dt = i["datetime"]
|
|
90
|
+
if dt_to > last_dt:
|
|
91
|
+
missing.append({"from": last_dt, "to": dt_to})
|
|
92
|
+
_LOGGER.debug("found data from %s to %s", oldest_dt, newest_dt)
|
|
93
|
+
else:
|
|
94
|
+
missing.append({"from": dt_from, "to": dt_to})
|
|
95
|
+
return new_lst, missing
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def get_by_key(
|
|
99
|
+
lst: List[Dict[str, Any]], key: str, value: Any
|
|
100
|
+
) -> Optional[Dict[str, Any]]:
|
|
101
|
+
"""Obtain an element of a list of dicts by key=value."""
|
|
102
|
+
for i in lst:
|
|
103
|
+
if i[key] == value:
|
|
104
|
+
return i
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def serialize_dict(data: dict) -> dict:
|
|
109
|
+
"""Serialize dicts as json."""
|
|
110
|
+
|
|
111
|
+
class DateTimeEncoder(JSONEncoder):
|
|
112
|
+
"""Replace datetime objects with ISO strings."""
|
|
113
|
+
|
|
114
|
+
def default(self, o):
|
|
115
|
+
if isinstance(o, (date, datetime)):
|
|
116
|
+
return o.isoformat()
|
|
117
|
+
|
|
118
|
+
return json.loads(json.dumps(data, cls=DateTimeEncoder))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def deserialize_dict(serialized_dict: dict) -> dict:
|
|
122
|
+
"""Deserializes a json replacing ISOTIME strings into datetime."""
|
|
123
|
+
|
|
124
|
+
def datetime_parser(json_dict):
|
|
125
|
+
"""Parse JSON while converting ISO strings into datetime objects."""
|
|
126
|
+
for key, value in json_dict.items():
|
|
127
|
+
if "date" in key:
|
|
128
|
+
with contextlib.suppress(Exception):
|
|
129
|
+
json_dict[key] = datetime.fromisoformat(value)
|
|
130
|
+
return json_dict
|
|
131
|
+
|
|
132
|
+
return json.loads(json.dumps(serialized_dict), object_hook=datetime_parser)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def percentile(N: List, percent: float, key=lambda x: x):
|
|
136
|
+
"""Find the percentile of a list of values."""
|
|
137
|
+
if not N:
|
|
138
|
+
return None
|
|
139
|
+
k = (len(N) - 1) * percent
|
|
140
|
+
f = math.floor(k)
|
|
141
|
+
c = math.ceil(k)
|
|
142
|
+
if f == c:
|
|
143
|
+
return key(N[int(k)])
|
|
144
|
+
d0 = key(N[int(f)]) * (c - k)
|
|
145
|
+
d1 = key(N[int(c)]) * (k - f)
|
|
146
|
+
return d0 + d1
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def extend_and_filter(
|
|
150
|
+
old_lst: List[Dict[str, Any]],
|
|
151
|
+
new_lst: List[Dict[str, Any]],
|
|
152
|
+
key: str,
|
|
153
|
+
dt_from: datetime,
|
|
154
|
+
dt_to: datetime,
|
|
155
|
+
) -> List[Dict[str, Any]]:
|
|
156
|
+
"""Extend and filter data by datetime range."""
|
|
157
|
+
data = extend_by_key(old_lst, new_lst, key)
|
|
158
|
+
data, _ = extract_dt_ranges(
|
|
159
|
+
data,
|
|
160
|
+
dt_from,
|
|
161
|
+
dt_to,
|
|
162
|
+
gap_interval=timedelta(days=365), # trick
|
|
163
|
+
)
|
|
164
|
+
return data
|