hestia-earth-utils 0.16.12__tar.gz → 0.16.14__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.
- {hestia_earth_utils-0.16.12/hestia_earth_utils.egg-info → hestia_earth_utils-0.16.14}/PKG-INFO +1 -1
- hestia_earth_utils-0.16.14/hestia_earth/utils/date.py +525 -0
- hestia_earth_utils-0.16.14/hestia_earth/utils/pivot/_shared.py +110 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/hestia_earth/utils/pivot/pivot_csv.py +3 -22
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/hestia_earth/utils/pivot/pivot_json.py +55 -21
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/hestia_earth/utils/tools.py +69 -3
- hestia_earth_utils-0.16.14/hestia_earth/utils/version.py +1 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14/hestia_earth_utils.egg-info}/PKG-INFO +1 -1
- hestia_earth_utils-0.16.14/tests/test_date.py +496 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/tests/test_tools.py +121 -0
- hestia_earth_utils-0.16.12/hestia_earth/utils/date.py +0 -86
- hestia_earth_utils-0.16.12/hestia_earth/utils/pivot/_shared.py +0 -55
- hestia_earth_utils-0.16.12/hestia_earth/utils/version.py +0 -1
- hestia_earth_utils-0.16.12/tests/test_date.py +0 -17
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/MANIFEST.in +0 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/README.md +0 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/bin/hestia-format-upload +0 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/bin/hestia-pivot-csv +0 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/hestia_earth/utils/__init__.py +0 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/hestia_earth/utils/api.py +0 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/hestia_earth/utils/blank_node.py +0 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/hestia_earth/utils/calculation_status.py +0 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/hestia_earth/utils/cycle.py +0 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/hestia_earth/utils/descriptive_stats.py +0 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/hestia_earth/utils/emission.py +0 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/hestia_earth/utils/lookup.py +0 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/hestia_earth/utils/lookup_utils.py +0 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/hestia_earth/utils/model.py +0 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/hestia_earth/utils/pipeline.py +0 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/hestia_earth/utils/pivot/__init__.py +0 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/hestia_earth/utils/request.py +0 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/hestia_earth/utils/stats.py +0 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/hestia_earth/utils/storage/__init__.py +0 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/hestia_earth/utils/storage/_azure_client.py +0 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/hestia_earth/utils/storage/_local_client.py +0 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/hestia_earth/utils/storage/_s3_client.py +0 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/hestia_earth/utils/storage/_sns_client.py +0 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/hestia_earth/utils/table.py +0 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/hestia_earth/utils/term.py +0 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/hestia_earth_utils.egg-info/SOURCES.txt +0 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/hestia_earth_utils.egg-info/dependency_links.txt +0 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/hestia_earth_utils.egg-info/requires.txt +0 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/hestia_earth_utils.egg-info/top_level.txt +0 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/setup.cfg +0 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/setup.py +0 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/tests/test_api.py +0 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/tests/test_blank_node.py +0 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/tests/test_calculation_status.py +0 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/tests/test_cycle.py +0 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/tests/test_descriptive_stats.py +0 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/tests/test_emission.py +0 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/tests/test_lookup.py +0 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/tests/test_lookup_utils.py +0 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/tests/test_model.py +0 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/tests/test_pipeline.py +0 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/tests/test_request.py +0 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/tests/test_stats.py +0 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/tests/test_table.py +0 -0
- {hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/tests/test_term.py +0 -0
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
from calendar import monthrange
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from dateutil.relativedelta import relativedelta
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from math import floor
|
|
6
|
+
from typing import Any, Callable, Literal, Optional, Union
|
|
7
|
+
|
|
8
|
+
from dateutil.parser import parse
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
from .tools import is_list_like, safe_parse_date
|
|
12
|
+
|
|
13
|
+
SECOND = 1
|
|
14
|
+
MINUTE = 60 * SECOND
|
|
15
|
+
HOUR = 60 * MINUTE
|
|
16
|
+
DAY = 24 * HOUR
|
|
17
|
+
YEAR = 365.2425
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def diff_in_days(from_date: str, to_date: str) -> float:
|
|
21
|
+
"""
|
|
22
|
+
Return the difference in days between two dates.
|
|
23
|
+
|
|
24
|
+
Deprecated, use `diff_in` function with `unit = TimeUnit.DAY` instead.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
from_date : str
|
|
29
|
+
Date in string format
|
|
30
|
+
to_date
|
|
31
|
+
Date in string format
|
|
32
|
+
|
|
33
|
+
Returns
|
|
34
|
+
-------
|
|
35
|
+
float
|
|
36
|
+
The difference of days between from and to dates with a precision of 1.
|
|
37
|
+
"""
|
|
38
|
+
difference = parse(to_date) - parse(from_date)
|
|
39
|
+
return round(difference.days + difference.seconds / DAY, 1)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def diff_in_years(from_date: str, to_date: str) -> float:
|
|
43
|
+
"""
|
|
44
|
+
Return the difference in years between two dates.
|
|
45
|
+
|
|
46
|
+
Deprecated, use `diff_in` function with `unit = TimeUnit.YEAR` instead.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
from_date : str
|
|
51
|
+
Date in string format
|
|
52
|
+
to_date
|
|
53
|
+
Date in string format
|
|
54
|
+
|
|
55
|
+
Returns
|
|
56
|
+
-------
|
|
57
|
+
float
|
|
58
|
+
The difference of years between from and to dates with a precision of 1.
|
|
59
|
+
"""
|
|
60
|
+
return round(diff_in_days(from_date, to_date) / YEAR, 1)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def is_in_days(date: str) -> bool:
|
|
64
|
+
"""
|
|
65
|
+
Check if the date as a string contains year, month and day.
|
|
66
|
+
|
|
67
|
+
Deprecated, use `validate_datestr_format` with `valid_format = DatestrFormat.YEAR_MONTH_DAY` instead.
|
|
68
|
+
|
|
69
|
+
Parameters
|
|
70
|
+
----------
|
|
71
|
+
date : str
|
|
72
|
+
Date in string format
|
|
73
|
+
|
|
74
|
+
Returns
|
|
75
|
+
-------
|
|
76
|
+
bool
|
|
77
|
+
True if the date contains the year, month and day.
|
|
78
|
+
"""
|
|
79
|
+
return (
|
|
80
|
+
date is not None
|
|
81
|
+
and re.compile(r"^[\d]{4}\-[\d]{2}\-[\d]{2}").match(date) is not None
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def is_in_months(date: str) -> bool:
|
|
86
|
+
"""
|
|
87
|
+
Check if the date as a string contains year, month but no day.
|
|
88
|
+
|
|
89
|
+
Deprecated, use `validate_datestr_format` with `valid_format = DatestrFormat.YEAR_MONTH` instead.
|
|
90
|
+
|
|
91
|
+
Parameters
|
|
92
|
+
----------
|
|
93
|
+
date : str
|
|
94
|
+
Date in string format
|
|
95
|
+
|
|
96
|
+
Returns
|
|
97
|
+
-------
|
|
98
|
+
bool
|
|
99
|
+
True if the date contains the year, month but no day.
|
|
100
|
+
"""
|
|
101
|
+
return (
|
|
102
|
+
date is not None and re.compile(r"^[\d]{4}\-[\d]{2}$").match(date) is not None
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
OLDEST_DATE = "1800"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class TimeUnit(Enum):
|
|
110
|
+
YEAR = "year"
|
|
111
|
+
MONTH = "month"
|
|
112
|
+
DAY = "day"
|
|
113
|
+
HOUR = "hour"
|
|
114
|
+
MINUTE = "minute"
|
|
115
|
+
SECOND = "second"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class DatestrFormat(Enum):
|
|
119
|
+
"""
|
|
120
|
+
Enum representing ISO date formats permitted by HESTIA.
|
|
121
|
+
|
|
122
|
+
See: https://en.wikipedia.org/wiki/ISO_8601
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
YEAR = r"%Y"
|
|
126
|
+
YEAR_MONTH = r"%Y-%m"
|
|
127
|
+
YEAR_MONTH_DAY = r"%Y-%m-%d"
|
|
128
|
+
YEAR_MONTH_DAY_HOUR_MINUTE_SECOND = r"%Y-%m-%dT%H:%M:%S"
|
|
129
|
+
MONTH = r"--%m"
|
|
130
|
+
MONTH_DAY = r"--%m-%d"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
DatestrGapfillMode = Literal["start", "middle", "end"]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
DATESTR_FORMAT_TO_EXPECTED_LENGTH = {
|
|
137
|
+
DatestrFormat.YEAR: len("2001"),
|
|
138
|
+
DatestrFormat.YEAR_MONTH: len("2001-01"),
|
|
139
|
+
DatestrFormat.YEAR_MONTH_DAY: len("2001-01-01"),
|
|
140
|
+
DatestrFormat.YEAR_MONTH_DAY_HOUR_MINUTE_SECOND: len("2001-01-01T00:00:00"),
|
|
141
|
+
DatestrFormat.MONTH: len("--01"),
|
|
142
|
+
DatestrFormat.MONTH_DAY: len("--01-01"),
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
TIME_UNIT_TO_DATESTR_FORMAT = {
|
|
147
|
+
TimeUnit.YEAR: DatestrFormat.YEAR,
|
|
148
|
+
TimeUnit.MONTH: DatestrFormat.YEAR_MONTH,
|
|
149
|
+
TimeUnit.DAY: DatestrFormat.YEAR_MONTH_DAY,
|
|
150
|
+
TimeUnit.HOUR: DatestrFormat.YEAR_MONTH_DAY_HOUR_MINUTE_SECOND,
|
|
151
|
+
TimeUnit.MINUTE: DatestrFormat.YEAR_MONTH_DAY_HOUR_MINUTE_SECOND,
|
|
152
|
+
TimeUnit.SECOND: DatestrFormat.YEAR_MONTH_DAY_HOUR_MINUTE_SECOND,
|
|
153
|
+
}
|
|
154
|
+
"""
|
|
155
|
+
Minimum Datestr format required to express DatetimeUnit.
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
_SECONDS_IN_MINUTE = 60 # 60 seconds in a minute
|
|
160
|
+
_MINUTES_IN_HOUR = 60 # 60 minutes in an hour
|
|
161
|
+
_HOURS_IN_DAY = 24 # 24 hours in a day
|
|
162
|
+
_MONTHS_IN_YEAR = 12 # 12 months in a year
|
|
163
|
+
|
|
164
|
+
_DAYS_IN_YEAR = YEAR # average days in a year (365.2425)
|
|
165
|
+
_DAYS_IN_MONTH = (
|
|
166
|
+
_DAYS_IN_YEAR / _MONTHS_IN_YEAR
|
|
167
|
+
) # average days in a month (365.2425/12)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
DATETIME_UNIT_CONVERSIONS: dict[str, dict[str, float]] = {
|
|
171
|
+
TimeUnit.YEAR.value: {
|
|
172
|
+
TimeUnit.MONTH.value: _MONTHS_IN_YEAR,
|
|
173
|
+
TimeUnit.DAY.value: _DAYS_IN_YEAR,
|
|
174
|
+
TimeUnit.HOUR.value: _DAYS_IN_YEAR * _HOURS_IN_DAY,
|
|
175
|
+
TimeUnit.MINUTE.value: _DAYS_IN_YEAR * _HOURS_IN_DAY * _MINUTES_IN_HOUR,
|
|
176
|
+
TimeUnit.SECOND.value: _DAYS_IN_YEAR
|
|
177
|
+
* _HOURS_IN_DAY
|
|
178
|
+
* _MINUTES_IN_HOUR
|
|
179
|
+
* _SECONDS_IN_MINUTE,
|
|
180
|
+
},
|
|
181
|
+
TimeUnit.MONTH.value: {
|
|
182
|
+
TimeUnit.YEAR.value: 1 / _MONTHS_IN_YEAR,
|
|
183
|
+
TimeUnit.DAY.value: _DAYS_IN_MONTH,
|
|
184
|
+
TimeUnit.HOUR.value: _DAYS_IN_MONTH * _HOURS_IN_DAY,
|
|
185
|
+
TimeUnit.MINUTE.value: _DAYS_IN_MONTH * _HOURS_IN_DAY * _MINUTES_IN_HOUR,
|
|
186
|
+
TimeUnit.SECOND.value: _DAYS_IN_MONTH
|
|
187
|
+
* _HOURS_IN_DAY
|
|
188
|
+
* _MINUTES_IN_HOUR
|
|
189
|
+
* _SECONDS_IN_MINUTE,
|
|
190
|
+
},
|
|
191
|
+
TimeUnit.DAY.value: {
|
|
192
|
+
TimeUnit.YEAR.value: 1 / _DAYS_IN_YEAR,
|
|
193
|
+
TimeUnit.MONTH.value: 1 / _DAYS_IN_MONTH,
|
|
194
|
+
TimeUnit.HOUR.value: _HOURS_IN_DAY,
|
|
195
|
+
TimeUnit.MINUTE.value: _HOURS_IN_DAY * _MINUTES_IN_HOUR,
|
|
196
|
+
TimeUnit.SECOND.value: _HOURS_IN_DAY * _MINUTES_IN_HOUR * _SECONDS_IN_MINUTE,
|
|
197
|
+
},
|
|
198
|
+
TimeUnit.HOUR.value: {
|
|
199
|
+
TimeUnit.YEAR.value: 1 / (_HOURS_IN_DAY * _DAYS_IN_YEAR),
|
|
200
|
+
TimeUnit.MONTH.value: 1 / (_HOURS_IN_DAY * _DAYS_IN_MONTH),
|
|
201
|
+
TimeUnit.DAY.value: 1 / (_HOURS_IN_DAY),
|
|
202
|
+
TimeUnit.MINUTE.value: _MINUTES_IN_HOUR,
|
|
203
|
+
TimeUnit.SECOND.value: _MINUTES_IN_HOUR * _SECONDS_IN_MINUTE,
|
|
204
|
+
},
|
|
205
|
+
TimeUnit.MINUTE.value: {
|
|
206
|
+
TimeUnit.YEAR.value: 1 / (_MINUTES_IN_HOUR * _HOURS_IN_DAY * _DAYS_IN_YEAR),
|
|
207
|
+
TimeUnit.MONTH.value: 1 / (_MINUTES_IN_HOUR * _HOURS_IN_DAY * _DAYS_IN_MONTH),
|
|
208
|
+
TimeUnit.DAY.value: 1 / (_MINUTES_IN_HOUR * _HOURS_IN_DAY),
|
|
209
|
+
TimeUnit.HOUR.value: 1 / _MINUTES_IN_HOUR,
|
|
210
|
+
TimeUnit.SECOND.value: _SECONDS_IN_MINUTE,
|
|
211
|
+
},
|
|
212
|
+
TimeUnit.SECOND.value: {
|
|
213
|
+
TimeUnit.YEAR.value: 1
|
|
214
|
+
/ (_SECONDS_IN_MINUTE * _MINUTES_IN_HOUR * _HOURS_IN_DAY * _DAYS_IN_YEAR),
|
|
215
|
+
TimeUnit.MONTH.value: 1
|
|
216
|
+
/ (_SECONDS_IN_MINUTE * _MINUTES_IN_HOUR * _HOURS_IN_DAY * _DAYS_IN_MONTH),
|
|
217
|
+
TimeUnit.DAY.value: 1 / (_SECONDS_IN_MINUTE * _MINUTES_IN_HOUR * _HOURS_IN_DAY),
|
|
218
|
+
TimeUnit.HOUR.value: 1 / (_SECONDS_IN_MINUTE * _MINUTES_IN_HOUR),
|
|
219
|
+
TimeUnit.MINUTE.value: 1 / _SECONDS_IN_MINUTE,
|
|
220
|
+
},
|
|
221
|
+
}
|
|
222
|
+
"""
|
|
223
|
+
A dict of TimeUnit conversion factors with format:
|
|
224
|
+
```
|
|
225
|
+
{
|
|
226
|
+
source (str): {
|
|
227
|
+
dest (str): conversion_factor (float)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _get_time_unit_conversion(
|
|
235
|
+
src_unit: TimeUnit, dest_unit: TimeUnit, default_value: float = 1
|
|
236
|
+
):
|
|
237
|
+
src_key = src_unit if isinstance(src_unit, str) else src_unit.value
|
|
238
|
+
dest_key = dest_unit if isinstance(dest_unit, str) else dest_unit.value
|
|
239
|
+
return DATETIME_UNIT_CONVERSIONS.get(src_key, {}).get(dest_key, default_value)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def convert_duration(
|
|
243
|
+
duration: float,
|
|
244
|
+
src_unit: TimeUnit,
|
|
245
|
+
dest_unit: TimeUnit,
|
|
246
|
+
default_conversion_factor: float = 1,
|
|
247
|
+
):
|
|
248
|
+
conversion_factor = _get_time_unit_conversion(
|
|
249
|
+
src_unit, dest_unit, default_conversion_factor
|
|
250
|
+
)
|
|
251
|
+
return duration * conversion_factor
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _check_datestr_format(datestr: str, format: DatestrFormat) -> bool:
|
|
255
|
+
"""
|
|
256
|
+
Use `datetime.strptime` to determine if a datestr is in a particular ISO format.
|
|
257
|
+
"""
|
|
258
|
+
try:
|
|
259
|
+
expected_length = DATESTR_FORMAT_TO_EXPECTED_LENGTH.get(format, 0)
|
|
260
|
+
format_str = format.value
|
|
261
|
+
return len(datestr) == expected_length and bool(
|
|
262
|
+
datetime.strptime(datestr, format_str)
|
|
263
|
+
)
|
|
264
|
+
except ValueError:
|
|
265
|
+
return False
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _get_datestr_format(
|
|
269
|
+
datestr: str, default: Optional[Any] = None
|
|
270
|
+
) -> Union[DatestrFormat, Any, None]:
|
|
271
|
+
"""
|
|
272
|
+
Check a datestr against each ISO format permitted by the HESTIA schema and
|
|
273
|
+
return the matching format.
|
|
274
|
+
"""
|
|
275
|
+
return next(
|
|
276
|
+
(
|
|
277
|
+
date_format
|
|
278
|
+
for date_format in DatestrFormat
|
|
279
|
+
if _check_datestr_format(str(datestr), date_format)
|
|
280
|
+
),
|
|
281
|
+
default,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def validate_datestr_format(
|
|
286
|
+
datestr: str,
|
|
287
|
+
valid_format: Union[DatestrFormat, list[DatestrFormat]] = [
|
|
288
|
+
DatestrFormat.YEAR,
|
|
289
|
+
DatestrFormat.YEAR_MONTH,
|
|
290
|
+
DatestrFormat.YEAR_MONTH_DAY,
|
|
291
|
+
],
|
|
292
|
+
):
|
|
293
|
+
valid_formats = valid_format if is_list_like(valid_format) else [valid_format]
|
|
294
|
+
format_ = _get_datestr_format(datestr)
|
|
295
|
+
return format_ in valid_formats
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _gapfill_datestr_start(datestr: str, *_) -> str:
|
|
299
|
+
"""
|
|
300
|
+
Gapfill an incomplete datestr with the earliest possible date and time.
|
|
301
|
+
|
|
302
|
+
Datestr will snap to the start of the year/month/day as appropriate.
|
|
303
|
+
"""
|
|
304
|
+
return datestr + "YYYY-01-01T00:00:00"[len(datestr) :]
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _days_in_month(datestr: str) -> int:
|
|
308
|
+
"""
|
|
309
|
+
Get the number of days in the datestr's month. If datestr invalid, return minimum value of 28.
|
|
310
|
+
"""
|
|
311
|
+
datetime = safe_parse_date(datestr)
|
|
312
|
+
return monthrange(datetime.year, datetime.month)[1] if datetime else 28
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _gapfill_datestr_end(datestr: str, format: DatestrFormat) -> str:
|
|
316
|
+
"""
|
|
317
|
+
Gapfill an incomplete datestr with the latest possible date and time.
|
|
318
|
+
|
|
319
|
+
Datestr will snap to the end of the year/month/day as appropriate.
|
|
320
|
+
"""
|
|
321
|
+
days = _days_in_month(datestr) if format == DatestrFormat.YEAR_MONTH else 31
|
|
322
|
+
completion_str = f"YYYY-12-{days}T23:59:59"
|
|
323
|
+
return datestr + completion_str[len(datestr) :]
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _gapfill_datestr_middle(datestr: str, format: DatestrFormat) -> str:
|
|
327
|
+
"""
|
|
328
|
+
Gap-fill an incomplete datestr with the middle value, halfway between the latest and earliest values.
|
|
329
|
+
"""
|
|
330
|
+
start_date_obj = datetime.strptime(
|
|
331
|
+
_gapfill_datestr_start(datestr),
|
|
332
|
+
DatestrFormat.YEAR_MONTH_DAY_HOUR_MINUTE_SECOND.value,
|
|
333
|
+
)
|
|
334
|
+
end_date_obj = datetime.strptime(
|
|
335
|
+
_gapfill_datestr_end(datestr, format=format),
|
|
336
|
+
DatestrFormat.YEAR_MONTH_DAY_HOUR_MINUTE_SECOND.value,
|
|
337
|
+
)
|
|
338
|
+
middle_date = start_date_obj + (end_date_obj - start_date_obj) / 2
|
|
339
|
+
return datetime.strftime(
|
|
340
|
+
middle_date, DatestrFormat.YEAR_MONTH_DAY_HOUR_MINUTE_SECOND.value
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
DATESTR_GAPFILL_MODE_TO_GAPFILL_FUNCTION: dict[DatestrGapfillMode, Callable] = {
|
|
345
|
+
"start": _gapfill_datestr_start,
|
|
346
|
+
"middle": _gapfill_datestr_middle,
|
|
347
|
+
"end": _gapfill_datestr_end,
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
_VALID_GAPFILL_DATE_FORMATS = {
|
|
351
|
+
DatestrFormat.YEAR,
|
|
352
|
+
DatestrFormat.YEAR_MONTH,
|
|
353
|
+
DatestrFormat.YEAR_MONTH_DAY,
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def gapfill_datestr(datestr: str, mode: DatestrGapfillMode = "start") -> str:
|
|
358
|
+
"""
|
|
359
|
+
Gapfill incomplete datestrs and returns them in the format `YYYY-MM-DDTHH:mm:ss`.
|
|
360
|
+
"""
|
|
361
|
+
datestr_ = str(datestr)
|
|
362
|
+
format_ = _get_datestr_format(datestr_)
|
|
363
|
+
should_run = format_ in _VALID_GAPFILL_DATE_FORMATS
|
|
364
|
+
return (
|
|
365
|
+
None
|
|
366
|
+
if datestr is None
|
|
367
|
+
else (
|
|
368
|
+
DATESTR_GAPFILL_MODE_TO_GAPFILL_FUNCTION[mode](datestr_, format_)
|
|
369
|
+
if should_run
|
|
370
|
+
else datestr_
|
|
371
|
+
)
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def convert_datestr(
|
|
376
|
+
datestr: str,
|
|
377
|
+
target_format: DatestrFormat,
|
|
378
|
+
gapfill_mode: DatestrGapfillMode = "start",
|
|
379
|
+
) -> str:
|
|
380
|
+
should_run = validate_datestr_format(datestr, _VALID_GAPFILL_DATE_FORMATS)
|
|
381
|
+
return (
|
|
382
|
+
datetime.strptime(
|
|
383
|
+
gapfill_datestr(datestr, gapfill_mode),
|
|
384
|
+
DatestrFormat.YEAR_MONTH_DAY_HOUR_MINUTE_SECOND.value,
|
|
385
|
+
).strftime(target_format.value)
|
|
386
|
+
if should_run
|
|
387
|
+
else datestr
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def parse_gapfilled_datestr(
|
|
392
|
+
datestr: str, gapfill_mode: DatestrGapfillMode = "start", default: Any = None
|
|
393
|
+
):
|
|
394
|
+
return safe_parse_date(gapfill_datestr(datestr, mode=gapfill_mode), default=default)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def datestrs_match(
|
|
398
|
+
datestr_a: str, datestr_b: str, mode: DatestrGapfillMode = "start"
|
|
399
|
+
) -> bool:
|
|
400
|
+
"""
|
|
401
|
+
Comparison of non-gap-filled string dates.
|
|
402
|
+
example: For end dates, '2010' would match '2010-12-31', but not '2010-01-01'
|
|
403
|
+
"""
|
|
404
|
+
return gapfill_datestr(datestr=datestr_a, mode=mode) == gapfill_datestr(
|
|
405
|
+
datestr=datestr_b, mode=mode
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _diff_in_years_calendar(a: datetime, b: datetime, *, add_second: bool, **_) -> int:
|
|
410
|
+
reverse = a > b
|
|
411
|
+
b_ = (
|
|
412
|
+
b
|
|
413
|
+
if not add_second
|
|
414
|
+
else b - relativedelta(seconds=1) if reverse else b + relativedelta(seconds=1)
|
|
415
|
+
)
|
|
416
|
+
diff = relativedelta(b_, a)
|
|
417
|
+
return diff.years
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _diff_in_months_calendar(a: datetime, b: datetime, *, add_second: bool, **_) -> int:
|
|
421
|
+
reverse = a > b
|
|
422
|
+
b_ = (
|
|
423
|
+
b
|
|
424
|
+
if not add_second
|
|
425
|
+
else b - relativedelta(seconds=1) if reverse else b + relativedelta(seconds=1)
|
|
426
|
+
)
|
|
427
|
+
diff = relativedelta(b_, a)
|
|
428
|
+
return diff.years * 12 + diff.months
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _diff(
|
|
432
|
+
a: datetime, b: datetime, *, unit: TimeUnit, add_second: bool, complete_only: bool
|
|
433
|
+
) -> Union[float, int]:
|
|
434
|
+
reverse = a > b
|
|
435
|
+
b_ = (
|
|
436
|
+
b
|
|
437
|
+
if not add_second
|
|
438
|
+
else b - relativedelta(seconds=1) if reverse else b + relativedelta(seconds=1)
|
|
439
|
+
)
|
|
440
|
+
diff = convert_duration((b_ - a).total_seconds(), TimeUnit.SECOND, unit)
|
|
441
|
+
return floor(diff) if complete_only else diff
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
DIFF_FUNCTION = {
|
|
445
|
+
(TimeUnit.YEAR, True): _diff_in_years_calendar,
|
|
446
|
+
(TimeUnit.MONTH, True): _diff_in_months_calendar,
|
|
447
|
+
}
|
|
448
|
+
"""
|
|
449
|
+
(unit: TimeUnit, calendar: bool): Callable
|
|
450
|
+
"""
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def diff_in(
|
|
454
|
+
a: Union[datetime, str],
|
|
455
|
+
b: Union[datetime, str],
|
|
456
|
+
unit: TimeUnit,
|
|
457
|
+
add_second=False,
|
|
458
|
+
calendar=False,
|
|
459
|
+
gapfill_mode: DatestrGapfillMode = "start",
|
|
460
|
+
):
|
|
461
|
+
"""
|
|
462
|
+
Calculate the difference between two dates.
|
|
463
|
+
|
|
464
|
+
This function does NOT return the absolute difference. If `b` is before `a` the function will return a negative
|
|
465
|
+
value.
|
|
466
|
+
|
|
467
|
+
If dates are passed as datestrings, they will be parsed into datetime objects. Caution is advised when using
|
|
468
|
+
datestrings with formats `--MM` and `--MM-DD` as these might be parsed in unexpected ways.
|
|
469
|
+
|
|
470
|
+
Parameters
|
|
471
|
+
----------
|
|
472
|
+
a : datetime | str
|
|
473
|
+
The first date.
|
|
474
|
+
|
|
475
|
+
b: datetime | str
|
|
476
|
+
The second date.
|
|
477
|
+
|
|
478
|
+
unit : TimeUnit
|
|
479
|
+
The time unit to calculate the diff in.
|
|
480
|
+
|
|
481
|
+
add_second : bool, optional, default = `False`
|
|
482
|
+
A flag to determine whether to add one second to diff results.
|
|
483
|
+
|
|
484
|
+
Set to `True` in cases where you are calculating the duration of nodes with incomplete datestrings.
|
|
485
|
+
|
|
486
|
+
For example, a node with `"startDate"` = `"2000"` and `"endDate"` = `"2001"` will ordinarily be assumed to take
|
|
487
|
+
place over the entirety of 2000 and 2001 (i.e., from `"2000-01-01T00-00-00"` to `"2001-12-31T23-59-59"`).
|
|
488
|
+
However, If `add_second` = `False`, the diff in days will be slightly less than 731 because the final second of
|
|
489
|
+
2001-12-31 is not accounted for. If `True` the diff will be exactly 731.
|
|
490
|
+
|
|
491
|
+
calendar : bool, optional, default = `False`
|
|
492
|
+
A flag to determine whether to use calendar time units.
|
|
493
|
+
|
|
494
|
+
If `True` the diff in years between `"2000"` and `"2001"` will be exactly 1, if `False` the diff will be
|
|
495
|
+
slightly over 1 because a leap year is longer than the average year.
|
|
496
|
+
|
|
497
|
+
If `True` the diff in months between `"2000-02"` and `"2000-03"` will be exactly 1, if `False` the diff will be
|
|
498
|
+
approximately 0.95 because February is shorter than the average month.
|
|
499
|
+
|
|
500
|
+
For all units, if `True`, only complete units will be counted, For example, the diff in days between
|
|
501
|
+
`"2000-01-01:00:00:00"` and `"2000-01-01:12:00:00"` will be 0. If `False` the diff will be 0.5.
|
|
502
|
+
|
|
503
|
+
gapfill_mode : DatestrGapfillMode, optional, default = `"start"`
|
|
504
|
+
How to gapfill incomplete datestrings (`"start"`, `"middle"` or `"end"`).
|
|
505
|
+
|
|
506
|
+
Returns
|
|
507
|
+
-------
|
|
508
|
+
diff : float | int
|
|
509
|
+
The difference between the dates in the selected units.
|
|
510
|
+
"""
|
|
511
|
+
a_, b_ = (
|
|
512
|
+
(
|
|
513
|
+
d
|
|
514
|
+
if isinstance(d, datetime)
|
|
515
|
+
else parse_gapfilled_datestr(d, gapfill_mode=gapfill_mode)
|
|
516
|
+
)
|
|
517
|
+
for d in (a, b)
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
diff_func = DIFF_FUNCTION.get(
|
|
521
|
+
(unit, calendar),
|
|
522
|
+
lambda *_, **kwargs: _diff(a_, b_, **kwargs, complete_only=calendar),
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
return diff_func(a_, b_, unit=unit, add_second=add_second)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from hestia_earth.schema import SCHEMA_TYPES, NODE_TYPES, EmissionMethodTier
|
|
5
|
+
from flatten_json import flatten as flatten_json
|
|
6
|
+
|
|
7
|
+
from ..tools import list_sum
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
EXCLUDE_FIELDS = ["@type", "type", "@context"]
|
|
11
|
+
EXCLUDE_PRIVATE_FIELDS = [
|
|
12
|
+
"added",
|
|
13
|
+
"addedVersion",
|
|
14
|
+
"updated",
|
|
15
|
+
"updatedVersion",
|
|
16
|
+
"aggregatedVersion",
|
|
17
|
+
"_cache",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# assuming column labels always camelCase
|
|
22
|
+
def _get_node_type_label(node_type):
|
|
23
|
+
return node_type[0].lower() + node_type[1:]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _get_node_type_from_label(node_type):
|
|
27
|
+
return node_type[0].upper() + node_type[1:]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _is_blank_node(data: dict):
|
|
31
|
+
node_type = data.get("@type") or data.get("type")
|
|
32
|
+
return node_type in SCHEMA_TYPES and node_type not in NODE_TYPES
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _with_csv_formatting(dct):
|
|
36
|
+
"""
|
|
37
|
+
Use as object_hook when parsing a JSON node: json.loads(node, object_hook=_with_csv_formatting).
|
|
38
|
+
Ensures parsed JSON has field values formatted according to hestia csv conventions.
|
|
39
|
+
"""
|
|
40
|
+
if "boundary" in dct:
|
|
41
|
+
dct["boundary"] = json.dumps(dct["boundary"])
|
|
42
|
+
for key, value in dct.items():
|
|
43
|
+
if _is_scalar_list(value):
|
|
44
|
+
dct[key] = ";".join([str(el) for el in value])
|
|
45
|
+
return dct
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _is_scalar_list(value):
|
|
49
|
+
if not isinstance(value, list):
|
|
50
|
+
return False
|
|
51
|
+
all_scalar = True
|
|
52
|
+
for element in value:
|
|
53
|
+
if not np.isscalar(element):
|
|
54
|
+
all_scalar = False
|
|
55
|
+
break
|
|
56
|
+
return all_scalar
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _filter_not_relevant(blank_node: dict):
|
|
60
|
+
return blank_node.get("methodTier") != EmissionMethodTier.NOT_RELEVANT.value
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _filter_emissions_not_relevant(node: dict):
|
|
64
|
+
"""
|
|
65
|
+
Ignore all emissions where `methodTier=not relevant` to save space.
|
|
66
|
+
"""
|
|
67
|
+
return node | (
|
|
68
|
+
{
|
|
69
|
+
key: list(filter(_filter_not_relevant, node[key]))
|
|
70
|
+
for key in ["emissions", "emissionsResourceUse"]
|
|
71
|
+
if key in node
|
|
72
|
+
}
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _filter_zero_value(blank_node: dict):
|
|
77
|
+
value = blank_node.get("value")
|
|
78
|
+
value = (
|
|
79
|
+
list_sum(blank_node.get("value"), default=-1)
|
|
80
|
+
if isinstance(value, list)
|
|
81
|
+
else value
|
|
82
|
+
)
|
|
83
|
+
return value != 0
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _filter_zero_values(node: dict):
|
|
87
|
+
"""
|
|
88
|
+
Ignore all blank nodes where `value=0` to save space.
|
|
89
|
+
"""
|
|
90
|
+
return node | (
|
|
91
|
+
{
|
|
92
|
+
key: list(filter(_filter_zero_value, value))
|
|
93
|
+
for key, value in node.items()
|
|
94
|
+
if isinstance(value, list)
|
|
95
|
+
and isinstance(value[0], dict)
|
|
96
|
+
and _is_blank_node(value[0])
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def nodes_to_df(nodes: list[dict]):
|
|
102
|
+
nodes_flattened = [
|
|
103
|
+
flatten_json(
|
|
104
|
+
dict([(_get_node_type_label(node.get("@type", node.get("type"))), node)]),
|
|
105
|
+
".",
|
|
106
|
+
)
|
|
107
|
+
for node in nodes
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
return pd.json_normalize(nodes_flattened)
|
{hestia_earth_utils-0.16.12 → hestia_earth_utils-0.16.14}/hestia_earth/utils/pivot/pivot_csv.py
RENAMED
|
@@ -5,7 +5,6 @@ import numpy as np
|
|
|
5
5
|
import pandas as pd
|
|
6
6
|
from hestia_earth.schema import UNIQUENESS_FIELDS, Term, NODE_TYPES
|
|
7
7
|
from hestia_earth.schema.utils.sort import get_sort_key, SORT_CONFIG
|
|
8
|
-
from flatten_json import flatten as flatten_json
|
|
9
8
|
|
|
10
9
|
# __package__ = "hestia_earth.utils" # required to run interactively in vscode
|
|
11
10
|
from ..api import find_term_ids_by_names
|
|
@@ -14,6 +13,9 @@ from ._shared import (
|
|
|
14
13
|
EXCLUDE_PRIVATE_FIELDS,
|
|
15
14
|
_with_csv_formatting,
|
|
16
15
|
_filter_emissions_not_relevant,
|
|
16
|
+
_get_node_type_label,
|
|
17
|
+
_get_node_type_from_label,
|
|
18
|
+
nodes_to_df,
|
|
17
19
|
)
|
|
18
20
|
|
|
19
21
|
|
|
@@ -36,15 +38,6 @@ def _get_blank_node_uniqueness_fields():
|
|
|
36
38
|
BLANK_NODE_UNIQUENESS_FIELDS = _get_blank_node_uniqueness_fields()
|
|
37
39
|
|
|
38
40
|
|
|
39
|
-
# assuming column labels always camelCase
|
|
40
|
-
def _get_node_type_label(node_type):
|
|
41
|
-
return node_type[0].lower() + node_type[1:]
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def _get_node_type_from_label(node_type):
|
|
45
|
-
return node_type[0].upper() + node_type[1:]
|
|
46
|
-
|
|
47
|
-
|
|
48
41
|
def _get_names(df):
|
|
49
42
|
names = []
|
|
50
43
|
for node_type, array_fields in BLANK_NODE_UNIQUENESS_FIELDS.items():
|
|
@@ -283,18 +276,6 @@ def _format_and_pivot(df_in):
|
|
|
283
276
|
return df_out
|
|
284
277
|
|
|
285
278
|
|
|
286
|
-
def nodes_to_df(nodes: list[dict]):
|
|
287
|
-
nodes_flattened = [
|
|
288
|
-
flatten_json(
|
|
289
|
-
dict([(_get_node_type_label(node.get("@type", node.get("type"))), node)]),
|
|
290
|
-
".",
|
|
291
|
-
)
|
|
292
|
-
for node in nodes
|
|
293
|
-
]
|
|
294
|
-
|
|
295
|
-
return pd.json_normalize(nodes_flattened)
|
|
296
|
-
|
|
297
|
-
|
|
298
279
|
def pivot_nodes(nodes: list[dict]):
|
|
299
280
|
"""
|
|
300
281
|
Pivot array of nodes in dict format (e.g under the 'nodes' key of a .hestia file)
|