pydpm_xl 0.1.10__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.
- py_dpm/AST/ASTConstructor.py +503 -0
- py_dpm/AST/ASTObjects.py +827 -0
- py_dpm/AST/ASTTemplate.py +101 -0
- py_dpm/AST/ASTVisitor.py +13 -0
- py_dpm/AST/MLGeneration.py +588 -0
- py_dpm/AST/ModuleAnalyzer.py +79 -0
- py_dpm/AST/ModuleDependencies.py +203 -0
- py_dpm/AST/WhereClauseChecker.py +12 -0
- py_dpm/AST/__init__.py +0 -0
- py_dpm/AST/check_operands.py +302 -0
- py_dpm/DataTypes/ScalarTypes.py +324 -0
- py_dpm/DataTypes/TimeClasses.py +370 -0
- py_dpm/DataTypes/TypePromotion.py +195 -0
- py_dpm/DataTypes/__init__.py +0 -0
- py_dpm/Exceptions/__init__.py +0 -0
- py_dpm/Exceptions/exceptions.py +84 -0
- py_dpm/Exceptions/messages.py +114 -0
- py_dpm/OperationScopes/OperationScopeService.py +247 -0
- py_dpm/OperationScopes/__init__.py +0 -0
- py_dpm/Operators/AggregateOperators.py +138 -0
- py_dpm/Operators/BooleanOperators.py +30 -0
- py_dpm/Operators/ClauseOperators.py +159 -0
- py_dpm/Operators/ComparisonOperators.py +69 -0
- py_dpm/Operators/ConditionalOperators.py +362 -0
- py_dpm/Operators/NumericOperators.py +101 -0
- py_dpm/Operators/Operator.py +388 -0
- py_dpm/Operators/StringOperators.py +27 -0
- py_dpm/Operators/TimeOperators.py +53 -0
- py_dpm/Operators/__init__.py +0 -0
- py_dpm/Utils/ValidationsGenerationUtils.py +429 -0
- py_dpm/Utils/__init__.py +0 -0
- py_dpm/Utils/operands_mapping.py +73 -0
- py_dpm/Utils/operator_mapping.py +89 -0
- py_dpm/Utils/tokens.py +172 -0
- py_dpm/Utils/utils.py +2 -0
- py_dpm/ValidationsGeneration/PropertiesConstraintsProcessor.py +190 -0
- py_dpm/ValidationsGeneration/Utils.py +364 -0
- py_dpm/ValidationsGeneration/VariantsProcessor.py +265 -0
- py_dpm/ValidationsGeneration/__init__.py +0 -0
- py_dpm/ValidationsGeneration/auxiliary_functions.py +98 -0
- py_dpm/__init__.py +61 -0
- py_dpm/api/__init__.py +140 -0
- py_dpm/api/ast_generator.py +438 -0
- py_dpm/api/complete_ast.py +241 -0
- py_dpm/api/data_dictionary_validation.py +577 -0
- py_dpm/api/migration.py +77 -0
- py_dpm/api/semantic.py +224 -0
- py_dpm/api/syntax.py +182 -0
- py_dpm/client.py +106 -0
- py_dpm/data_handlers.py +99 -0
- py_dpm/db_utils.py +117 -0
- py_dpm/grammar/__init__.py +0 -0
- py_dpm/grammar/dist/__init__.py +0 -0
- py_dpm/grammar/dist/dpm_xlLexer.interp +428 -0
- py_dpm/grammar/dist/dpm_xlLexer.py +804 -0
- py_dpm/grammar/dist/dpm_xlLexer.tokens +106 -0
- py_dpm/grammar/dist/dpm_xlParser.interp +249 -0
- py_dpm/grammar/dist/dpm_xlParser.py +5224 -0
- py_dpm/grammar/dist/dpm_xlParser.tokens +106 -0
- py_dpm/grammar/dist/dpm_xlParserListener.py +742 -0
- py_dpm/grammar/dist/dpm_xlParserVisitor.py +419 -0
- py_dpm/grammar/dist/listeners.py +10 -0
- py_dpm/grammar/dpm_xlLexer.g4 +435 -0
- py_dpm/grammar/dpm_xlParser.g4 +260 -0
- py_dpm/migration.py +282 -0
- py_dpm/models.py +2139 -0
- py_dpm/semantics/DAG/DAGAnalyzer.py +158 -0
- py_dpm/semantics/DAG/__init__.py +0 -0
- py_dpm/semantics/SemanticAnalyzer.py +320 -0
- py_dpm/semantics/Symbols.py +223 -0
- py_dpm/semantics/__init__.py +0 -0
- py_dpm/utils/__init__.py +0 -0
- py_dpm/utils/ast_serialization.py +481 -0
- py_dpm/views/data_types.sql +12 -0
- py_dpm/views/datapoints.sql +65 -0
- py_dpm/views/hierarchy_operand_reference.sql +11 -0
- py_dpm/views/hierarchy_preconditions.sql +13 -0
- py_dpm/views/hierarchy_variables.sql +26 -0
- py_dpm/views/hierarchy_variables_context.sql +14 -0
- py_dpm/views/key_components.sql +18 -0
- py_dpm/views/module_from_table.sql +11 -0
- py_dpm/views/open_keys.sql +13 -0
- py_dpm/views/operation_info.sql +27 -0
- py_dpm/views/operation_list.sql +18 -0
- py_dpm/views/operations_versions_from_module_version.sql +30 -0
- py_dpm/views/precondition_info.sql +17 -0
- py_dpm/views/report_type_operand_reference_info.sql +18 -0
- py_dpm/views/subcategory_info.sql +17 -0
- py_dpm/views/table_info.sql +19 -0
- pydpm_xl-0.1.10.dist-info/LICENSE +674 -0
- pydpm_xl-0.1.10.dist-info/METADATA +50 -0
- pydpm_xl-0.1.10.dist-info/RECORD +94 -0
- pydpm_xl-0.1.10.dist-info/WHEEL +4 -0
- pydpm_xl-0.1.10.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
|
|
4
|
+
from py_dpm.DataTypes.TimeClasses import timeParser, timePeriodParser
|
|
5
|
+
from py_dpm.Exceptions.exceptions import DataTypeError, SemanticError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ScalarType:
|
|
9
|
+
"""
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
default = None
|
|
13
|
+
|
|
14
|
+
def __repr__(self) -> str:
|
|
15
|
+
return f"{self.__class__.__name__}"
|
|
16
|
+
|
|
17
|
+
def strictly_same_class(self, obj) -> bool:
|
|
18
|
+
if not isinstance(obj, ScalarType):
|
|
19
|
+
raise Exception("Not use strictly_same_class")
|
|
20
|
+
return self.__class__ == obj.__class__
|
|
21
|
+
|
|
22
|
+
def __eq__(self, other):
|
|
23
|
+
return self.__class__.__name__ == other.__class__.__name__
|
|
24
|
+
|
|
25
|
+
def is_included(self, set_: set) -> bool:
|
|
26
|
+
return self.__class__ in set_
|
|
27
|
+
|
|
28
|
+
def is_subtype(self, obj) -> bool:
|
|
29
|
+
if not isinstance(obj, ScalarType):
|
|
30
|
+
raise Exception("Not use is_subtype")
|
|
31
|
+
return issubclass(self.__class__, obj.__class__)
|
|
32
|
+
|
|
33
|
+
def is_null_type(self) -> bool:
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
def set_interval(self, interval: bool):
|
|
37
|
+
raise SemanticError("3-4", operand_type=self.__class__.__name__)
|
|
38
|
+
|
|
39
|
+
__str__ = __repr__
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class String(ScalarType):
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
"""
|
|
46
|
+
default = ""
|
|
47
|
+
|
|
48
|
+
def __init__(self):
|
|
49
|
+
super().__init__()
|
|
50
|
+
|
|
51
|
+
def check_type(self, value): # Not needed for semantic, but can be util later
|
|
52
|
+
if isinstance(value, str):
|
|
53
|
+
return True
|
|
54
|
+
raise DataTypeError(value, String)
|
|
55
|
+
|
|
56
|
+
def cast(self, value):
|
|
57
|
+
return str(value)
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def dtype(self):
|
|
61
|
+
return 'string'
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class Number(ScalarType):
|
|
65
|
+
"""
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(self, interval=False):
|
|
69
|
+
super().__init__()
|
|
70
|
+
self.interval: bool = interval
|
|
71
|
+
|
|
72
|
+
def check_type(self, value):
|
|
73
|
+
if isinstance(value, float):
|
|
74
|
+
return True
|
|
75
|
+
|
|
76
|
+
raise DataTypeError(value, Number)
|
|
77
|
+
|
|
78
|
+
def cast(self, value):
|
|
79
|
+
return float(value)
|
|
80
|
+
|
|
81
|
+
def set_interval(self, interval: bool):
|
|
82
|
+
self.interval = interval
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def dtype(self):
|
|
86
|
+
return 'Float64'
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class Integer(Number):
|
|
90
|
+
"""
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
def __init__(self, interval=False):
|
|
94
|
+
super().__init__(interval)
|
|
95
|
+
|
|
96
|
+
def check_type(self, value):
|
|
97
|
+
if isinstance(value, int):
|
|
98
|
+
return True
|
|
99
|
+
|
|
100
|
+
raise DataTypeError(value, Integer)
|
|
101
|
+
|
|
102
|
+
def cast(self, value):
|
|
103
|
+
return int(round(float(value), 0))
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def dtype(self):
|
|
107
|
+
return 'Int64'
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class TimeInterval(ScalarType):
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
"""
|
|
114
|
+
default = pd.NA
|
|
115
|
+
|
|
116
|
+
def __init__(self):
|
|
117
|
+
super().__init__()
|
|
118
|
+
|
|
119
|
+
def check_type(self, value):
|
|
120
|
+
if isinstance(value, str):
|
|
121
|
+
return True
|
|
122
|
+
|
|
123
|
+
raise DataTypeError(value, TimeInterval)
|
|
124
|
+
|
|
125
|
+
def cast(self, value):
|
|
126
|
+
return timeParser(value)
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def dtype(self):
|
|
130
|
+
return 'string'
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class Date(TimeInterval):
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
"""
|
|
137
|
+
default = np.nan
|
|
138
|
+
|
|
139
|
+
def __init__(self):
|
|
140
|
+
super().__init__()
|
|
141
|
+
|
|
142
|
+
def check_type(self, value):
|
|
143
|
+
pass
|
|
144
|
+
|
|
145
|
+
def cast(self, value):
|
|
146
|
+
return str(value)
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def dtype(self):
|
|
150
|
+
return 'string'
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class TimePeriod(TimeInterval):
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
"""
|
|
157
|
+
default = pd.NA
|
|
158
|
+
|
|
159
|
+
def __init__(self):
|
|
160
|
+
super().__init__()
|
|
161
|
+
|
|
162
|
+
def check_type(self, value):
|
|
163
|
+
pass
|
|
164
|
+
|
|
165
|
+
def cast(self, value):
|
|
166
|
+
return timePeriodParser(value)
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def dtype(self):
|
|
170
|
+
return 'string'
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class Duration(ScalarType):
|
|
174
|
+
pass
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class Boolean(ScalarType):
|
|
178
|
+
"""
|
|
179
|
+
"""
|
|
180
|
+
default = np.nan
|
|
181
|
+
|
|
182
|
+
def __init__(self):
|
|
183
|
+
super().__init__()
|
|
184
|
+
|
|
185
|
+
def check_type(self, value):
|
|
186
|
+
if isinstance(value, bool):
|
|
187
|
+
return True
|
|
188
|
+
|
|
189
|
+
def cast(self, value):
|
|
190
|
+
if isinstance(value, str):
|
|
191
|
+
if value.lower() == "true":
|
|
192
|
+
return True
|
|
193
|
+
elif value.lower() == "false":
|
|
194
|
+
return False
|
|
195
|
+
elif value.lower() == "1":
|
|
196
|
+
return True
|
|
197
|
+
elif value.lower() == "0":
|
|
198
|
+
return False
|
|
199
|
+
else:
|
|
200
|
+
return np.nan
|
|
201
|
+
if isinstance(value, int):
|
|
202
|
+
if value != 0:
|
|
203
|
+
return True
|
|
204
|
+
else:
|
|
205
|
+
return False
|
|
206
|
+
if isinstance(value, float):
|
|
207
|
+
if value != 0.0:
|
|
208
|
+
return True
|
|
209
|
+
else:
|
|
210
|
+
return False
|
|
211
|
+
if isinstance(value, bool):
|
|
212
|
+
return value
|
|
213
|
+
if isinstance(value, np.bool_):
|
|
214
|
+
return np.bool_(value)
|
|
215
|
+
if pd.isnull(value):
|
|
216
|
+
return np.nan
|
|
217
|
+
return np.nan
|
|
218
|
+
|
|
219
|
+
@property
|
|
220
|
+
def dtype(self):
|
|
221
|
+
return 'boolean'
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class Null(ScalarType): # I think it is needed
|
|
225
|
+
"""
|
|
226
|
+
All the Data Types are assumed to contain the conventional value null, which means “no value”, or “absence of known value” or “missing value”.
|
|
227
|
+
Note that the null value, therefore, is the only value of multiple different types.
|
|
228
|
+
"""
|
|
229
|
+
default = None
|
|
230
|
+
|
|
231
|
+
def __init__(self):
|
|
232
|
+
super().__init__()
|
|
233
|
+
|
|
234
|
+
def is_null_type(self) -> bool:
|
|
235
|
+
return True
|
|
236
|
+
|
|
237
|
+
def cast(self, value):
|
|
238
|
+
return type(None)()
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class Mixed(ScalarType):
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
def __init__(self):
|
|
247
|
+
super().__init__()
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class Item(ScalarType):
|
|
251
|
+
default = ""
|
|
252
|
+
|
|
253
|
+
def __init__(self):
|
|
254
|
+
super().__init__()
|
|
255
|
+
|
|
256
|
+
def cast(self, value):
|
|
257
|
+
return str(value)
|
|
258
|
+
|
|
259
|
+
@property
|
|
260
|
+
def dtype(self):
|
|
261
|
+
return 'string'
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class Subcategory(ScalarType):
|
|
265
|
+
pass
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class ScalarFactory:
|
|
269
|
+
types_dict = {
|
|
270
|
+
"String": String,
|
|
271
|
+
"Number": Number,
|
|
272
|
+
"Integer": Integer,
|
|
273
|
+
"TimeInterval": TimeInterval,
|
|
274
|
+
"Date": Date,
|
|
275
|
+
"TimePeriod": TimePeriod,
|
|
276
|
+
"Duration": Duration,
|
|
277
|
+
"Boolean": Boolean,
|
|
278
|
+
"Item": Item,
|
|
279
|
+
"Subcategory": Subcategory,
|
|
280
|
+
"Null": Null,
|
|
281
|
+
"Mixed": Mixed
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
database_types = {
|
|
285
|
+
"URI": String,
|
|
286
|
+
"PER": Number,
|
|
287
|
+
"ENU": Item,
|
|
288
|
+
"DAT": TimeInterval,
|
|
289
|
+
"STR": String,
|
|
290
|
+
"INT": Integer,
|
|
291
|
+
"MON": Number,
|
|
292
|
+
"BOO": Boolean,
|
|
293
|
+
"TRU": Boolean,
|
|
294
|
+
"DEC": Number,
|
|
295
|
+
"b": Boolean,
|
|
296
|
+
"d": TimeInterval,
|
|
297
|
+
"i": Integer,
|
|
298
|
+
"m": Number,
|
|
299
|
+
"p": Number,
|
|
300
|
+
"e": Item,
|
|
301
|
+
"s": String,
|
|
302
|
+
"es": String,
|
|
303
|
+
"r": Number,
|
|
304
|
+
"t": Boolean
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
def scalar_factory(self, code=None, interval=None):
|
|
308
|
+
if code in ("Number", "Integer"):
|
|
309
|
+
return self.types_dict[code](interval)
|
|
310
|
+
if code in self.types_dict:
|
|
311
|
+
return self.types_dict[code]()
|
|
312
|
+
return Null()
|
|
313
|
+
|
|
314
|
+
def database_types_mapping(self, code):
|
|
315
|
+
return self.database_types[code]
|
|
316
|
+
|
|
317
|
+
def all_types(self):
|
|
318
|
+
return (v for v in self.types_dict.values())
|
|
319
|
+
|
|
320
|
+
def from_database_to_scalar_types(self, code, interval):
|
|
321
|
+
scalar_type = self.database_types_mapping(code)
|
|
322
|
+
if isinstance(scalar_type(), Number):
|
|
323
|
+
return scalar_type(interval)
|
|
324
|
+
return scalar_type()
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import calendar
|
|
2
|
+
import operator
|
|
3
|
+
from datetime import date, datetime
|
|
4
|
+
from typing import Union
|
|
5
|
+
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from pandas._libs.missing import NAType
|
|
8
|
+
|
|
9
|
+
duration_mapping = {
|
|
10
|
+
"A": 6,
|
|
11
|
+
"S": 5,
|
|
12
|
+
"Q": 4,
|
|
13
|
+
"M": 3,
|
|
14
|
+
"W": 2,
|
|
15
|
+
"D": 1
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
duration_mapping_reversed = {
|
|
19
|
+
6: "A",
|
|
20
|
+
5: "S",
|
|
21
|
+
4: "Q",
|
|
22
|
+
3: "M",
|
|
23
|
+
2: "W",
|
|
24
|
+
1: "D"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TimePeriod:
|
|
29
|
+
_year: int
|
|
30
|
+
_period_indicator: str
|
|
31
|
+
_period_number: int
|
|
32
|
+
|
|
33
|
+
def __init__(self, period: str):
|
|
34
|
+
self.year = int(period[:4])
|
|
35
|
+
if len(period) > 4:
|
|
36
|
+
self.period_indicator = period[4]
|
|
37
|
+
else:
|
|
38
|
+
self.period_indicator = 'A'
|
|
39
|
+
if len(period) > 5:
|
|
40
|
+
self.period_number = int(period[5:])
|
|
41
|
+
else:
|
|
42
|
+
self.period_number = 1
|
|
43
|
+
|
|
44
|
+
def __str__(self):
|
|
45
|
+
if self.period_indicator == 'A':
|
|
46
|
+
return f"{self.year}{self.period_indicator}"
|
|
47
|
+
return f"{self.year}{self.period_indicator}{self.period_number}"
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def _check_year(year: int):
|
|
51
|
+
if year < 1900 or year > 9999:
|
|
52
|
+
raise ValueError(f'Invalid year {year}, must be between 1900 and 9999.')
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def year(self) -> int:
|
|
56
|
+
return self._year
|
|
57
|
+
|
|
58
|
+
@year.setter
|
|
59
|
+
def year(self, value: int):
|
|
60
|
+
self._check_year(value)
|
|
61
|
+
self._year = value
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def period_indicator(self) -> str:
|
|
65
|
+
return self._period_indicator
|
|
66
|
+
|
|
67
|
+
@period_indicator.setter
|
|
68
|
+
def period_indicator(self, value: str):
|
|
69
|
+
if value not in PeriodDuration():
|
|
70
|
+
raise ValueError(
|
|
71
|
+
f'Cannot set period indicator as {value}. Possible values: {PeriodDuration().member_names}')
|
|
72
|
+
self._period_indicator = value
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def period_number(self) -> int:
|
|
76
|
+
return self._period_number
|
|
77
|
+
|
|
78
|
+
@period_number.setter
|
|
79
|
+
def period_number(self, value: int):
|
|
80
|
+
if not PeriodDuration.check_period_range(self.period_indicator, value):
|
|
81
|
+
raise ValueError(f'Period Number must be between 1 and '
|
|
82
|
+
f'{PeriodDuration.periods[self.period_indicator]} '
|
|
83
|
+
f'for period indicator {self.period_indicator}.')
|
|
84
|
+
self._period_number = value
|
|
85
|
+
|
|
86
|
+
def _meta_comparison(self, other, py_op) -> bool:
|
|
87
|
+
return py_op(duration_mapping[self.period_indicator],
|
|
88
|
+
duration_mapping[other.period_indicator])
|
|
89
|
+
|
|
90
|
+
def start_date(self, as_date=False) -> Union[date, str]:
|
|
91
|
+
"""
|
|
92
|
+
Gets the starting date of the Period
|
|
93
|
+
"""
|
|
94
|
+
date_value = period_to_date(year=self.year,
|
|
95
|
+
period_indicator=self.period_indicator,
|
|
96
|
+
period_number=self.period_number,
|
|
97
|
+
start=True)
|
|
98
|
+
if as_date:
|
|
99
|
+
return date_value
|
|
100
|
+
return date_value.isoformat()
|
|
101
|
+
|
|
102
|
+
def end_date(self, as_date=False) -> Union[date, str]:
|
|
103
|
+
"""
|
|
104
|
+
Gets the ending date of the Period
|
|
105
|
+
"""
|
|
106
|
+
date_value = period_to_date(year=self.year,
|
|
107
|
+
period_indicator=self.period_indicator,
|
|
108
|
+
period_number=self.period_number,
|
|
109
|
+
start=False)
|
|
110
|
+
if as_date:
|
|
111
|
+
return date_value
|
|
112
|
+
return date_value.isoformat()
|
|
113
|
+
|
|
114
|
+
def __eq__(self, other) -> bool:
|
|
115
|
+
return self._meta_comparison(other, operator.eq)
|
|
116
|
+
|
|
117
|
+
def __ne__(self, other) -> bool:
|
|
118
|
+
return not self._meta_comparison(other, operator.eq)
|
|
119
|
+
|
|
120
|
+
def __lt__(self, other) -> bool:
|
|
121
|
+
return self._meta_comparison(other, operator.lt)
|
|
122
|
+
|
|
123
|
+
def __le__(self, other) -> bool:
|
|
124
|
+
return self._meta_comparison(other, operator.le)
|
|
125
|
+
|
|
126
|
+
def __gt__(self, other) -> bool:
|
|
127
|
+
return self._meta_comparison(other, operator.gt)
|
|
128
|
+
|
|
129
|
+
def __ge__(self, other) -> bool:
|
|
130
|
+
return self._meta_comparison(other, operator.ge)
|
|
131
|
+
|
|
132
|
+
def change_indicator(self, new_indicator):
|
|
133
|
+
if self.period_indicator == new_indicator:
|
|
134
|
+
return
|
|
135
|
+
date_value = period_to_date(self.year, self.period_indicator, self.period_number)
|
|
136
|
+
self.period_indicator = new_indicator
|
|
137
|
+
self.period_number = date_to_period(date_value, period_indicator=new_indicator).period_number
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class Time:
|
|
141
|
+
_date1: str = '0'
|
|
142
|
+
_date2: str = 'Z'
|
|
143
|
+
|
|
144
|
+
def __init__(self, date1: str, date2: str):
|
|
145
|
+
self.date1 = date1
|
|
146
|
+
self.date2 = date2
|
|
147
|
+
if date1 > date2:
|
|
148
|
+
raise ValueError(f'Invalid Time with duration less than 0 ({self.length} days)')
|
|
149
|
+
|
|
150
|
+
@classmethod
|
|
151
|
+
def from_dates(cls, date1: date, date2: date):
|
|
152
|
+
return cls(date1.isoformat(), date2.isoformat())
|
|
153
|
+
|
|
154
|
+
@classmethod
|
|
155
|
+
def from_iso_format(cls, dates: str):
|
|
156
|
+
return cls(*dates.split('/', maxsplit=1))
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def date1(self, as_date=False) -> Union[date, str]:
|
|
160
|
+
if as_date:
|
|
161
|
+
return date.fromisoformat(self._date1)
|
|
162
|
+
return self._date1
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def date2(self) -> Union[date, str]:
|
|
166
|
+
return self._date2
|
|
167
|
+
|
|
168
|
+
@date1.setter
|
|
169
|
+
def date1(self, value: str):
|
|
170
|
+
date.fromisoformat(value)
|
|
171
|
+
if value > self.date2:
|
|
172
|
+
raise ValueError(f"({value} > {self.date2}). Cannot set date1 with a value greater than date2.")
|
|
173
|
+
self._date1 = value
|
|
174
|
+
|
|
175
|
+
def date1_asdate(self):
|
|
176
|
+
return date.fromisoformat(self._date1)
|
|
177
|
+
|
|
178
|
+
def date2_asdate(self):
|
|
179
|
+
return date.fromisoformat(self._date2)
|
|
180
|
+
|
|
181
|
+
@date2.setter
|
|
182
|
+
def date2(self, value: str):
|
|
183
|
+
date.fromisoformat(value)
|
|
184
|
+
if value < self.date1:
|
|
185
|
+
raise ValueError(f"({value} < {self.date1}). Cannot set date2 with a value lower than date1.")
|
|
186
|
+
self._date2 = value
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def length(self) -> int:
|
|
190
|
+
date_left = date.fromisoformat(self.date1)
|
|
191
|
+
date_right = date.fromisoformat(self.date2)
|
|
192
|
+
return (date_right - date_left).days
|
|
193
|
+
|
|
194
|
+
__len__ = length
|
|
195
|
+
|
|
196
|
+
def __str__(self):
|
|
197
|
+
return f"{self.date1}/{self.date2}"
|
|
198
|
+
|
|
199
|
+
__repr__ = __str__
|
|
200
|
+
|
|
201
|
+
def _meta_comparison(self, other, py_op):
|
|
202
|
+
return py_op(self.length, other.length)
|
|
203
|
+
|
|
204
|
+
def __eq__(self, other) -> bool:
|
|
205
|
+
return self._meta_comparison(other, operator.eq)
|
|
206
|
+
|
|
207
|
+
def __ne__(self, other) -> bool:
|
|
208
|
+
return self._meta_comparison(other, operator.ne)
|
|
209
|
+
|
|
210
|
+
def __lt__(self, other) -> bool:
|
|
211
|
+
return self._meta_comparison(other, operator.lt)
|
|
212
|
+
|
|
213
|
+
def __le__(self, other) -> bool:
|
|
214
|
+
return self._meta_comparison(other, operator.le)
|
|
215
|
+
|
|
216
|
+
def __gt__(self, other) -> bool:
|
|
217
|
+
return self._meta_comparison(other, operator.gt)
|
|
218
|
+
|
|
219
|
+
def __ge__(self, other) -> bool:
|
|
220
|
+
return self._meta_comparison(other, operator.ge)
|
|
221
|
+
|
|
222
|
+
@classmethod
|
|
223
|
+
def from_time_period(cls, value: TimePeriod):
|
|
224
|
+
date1 = period_to_date(value.year, value.period_indicator, value.period_number, start=True)
|
|
225
|
+
date2 = period_to_date(value.year, value.period_indicator, value.period_number, start=False)
|
|
226
|
+
return cls.from_dates(date1, date2)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def timePeriodParser(str_: str) -> Union[TimePeriod, NAType]:
|
|
230
|
+
"""
|
|
231
|
+
Examples: 2020, 2019A, 2018Q3, 2011M12 2023S2.
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
if pd.isnull(str_) or len(str_) == 0:
|
|
236
|
+
return pd.NA
|
|
237
|
+
return TimePeriod(str_)
|
|
238
|
+
|
|
239
|
+
except ValueError:
|
|
240
|
+
# DATAMODEL_DATASET.13
|
|
241
|
+
raise ValueError('Not a valid time period format {}'.format(str_))
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def timeParser(str_: str) -> Union[NAType, Time]:
|
|
245
|
+
"""
|
|
246
|
+
Example: 2000-01-01/2009-12-31
|
|
247
|
+
"""
|
|
248
|
+
try:
|
|
249
|
+
if pd.isnull(str_) or len(str_) == 0:
|
|
250
|
+
return pd.NA
|
|
251
|
+
return Time.from_iso_format(str_)
|
|
252
|
+
|
|
253
|
+
except ValueError:
|
|
254
|
+
# DATAMODEL_DATASET.10
|
|
255
|
+
raise ValueError('Not a valid time format {}'.format(str_))
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def date_to_period(date_value: date, period_indicator):
|
|
259
|
+
if period_indicator == "A":
|
|
260
|
+
return TimePeriod(f"{date_value.year}A")
|
|
261
|
+
elif period_indicator == "S":
|
|
262
|
+
return TimePeriod(f"{date_value.year}S{((date_value.month - 1) // 6) + 1}")
|
|
263
|
+
elif period_indicator == "Q":
|
|
264
|
+
return TimePeriod(f"{date_value.year}Q{((date_value.month - 1) // 3) + 1}")
|
|
265
|
+
elif period_indicator == "M":
|
|
266
|
+
return TimePeriod(f"{date_value.year}M{date_value.month}")
|
|
267
|
+
elif period_indicator == "W":
|
|
268
|
+
cal = date_value.isocalendar()
|
|
269
|
+
return TimePeriod(f"{cal[0]}W{cal[1]}")
|
|
270
|
+
elif period_indicator == "D": # Extract day of the year
|
|
271
|
+
return TimePeriod(f"{date_value.year}D{date_value.timetuple().tm_yday}")
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def period_to_date(year, period_indicator, period_number, start=False):
|
|
275
|
+
if period_indicator == 'A':
|
|
276
|
+
if start:
|
|
277
|
+
return date(year, 1, 1)
|
|
278
|
+
else:
|
|
279
|
+
return date(year, 12, 31)
|
|
280
|
+
if period_indicator == 'S':
|
|
281
|
+
if period_number == 1:
|
|
282
|
+
if start:
|
|
283
|
+
return date(year, 1, 1)
|
|
284
|
+
else:
|
|
285
|
+
return date(year, 6, 30)
|
|
286
|
+
else:
|
|
287
|
+
if start:
|
|
288
|
+
return date(year, 7, 1)
|
|
289
|
+
else:
|
|
290
|
+
return date(year, 12, 31)
|
|
291
|
+
if period_indicator == 'Q':
|
|
292
|
+
if period_number == 1:
|
|
293
|
+
if start:
|
|
294
|
+
return date(year, 1, 1)
|
|
295
|
+
else:
|
|
296
|
+
return date(year, 3, 31)
|
|
297
|
+
elif period_number == 2:
|
|
298
|
+
if start:
|
|
299
|
+
return date(year, 4, 1)
|
|
300
|
+
else:
|
|
301
|
+
return date(year, 6, 30)
|
|
302
|
+
elif period_number == 3:
|
|
303
|
+
if start:
|
|
304
|
+
return date(year, 7, 1)
|
|
305
|
+
else:
|
|
306
|
+
return date(year, 9, 30)
|
|
307
|
+
else:
|
|
308
|
+
if start:
|
|
309
|
+
return date(year, 10, 1)
|
|
310
|
+
else:
|
|
311
|
+
return date(year, 12, 31)
|
|
312
|
+
if period_indicator == "M":
|
|
313
|
+
if start:
|
|
314
|
+
return date(year, period_number, 1)
|
|
315
|
+
else:
|
|
316
|
+
day = int(calendar.monthrange(year, period_number)[1])
|
|
317
|
+
return date(year, period_number, day)
|
|
318
|
+
if period_indicator == "W": # 0 for Sunday, 1 for Monday in %w
|
|
319
|
+
if start:
|
|
320
|
+
return datetime.strptime(f"{year}-W{period_number}-1", "%G-W%V-%w").date()
|
|
321
|
+
else:
|
|
322
|
+
return datetime.strptime(f"{year}-W{period_number}-0", "%G-W%V-%w").date()
|
|
323
|
+
if period_indicator == "D":
|
|
324
|
+
return datetime.strptime(f"{year}-D{period_number}", "%Y-D%j").date()
|
|
325
|
+
|
|
326
|
+
raise ValueError(f'Invalid Period Indicator {period_indicator}')
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
class SingletonMeta(type):
|
|
330
|
+
"""
|
|
331
|
+
The Singleton class can be implemented in different ways in Python. Some
|
|
332
|
+
possible methods include: base class, decorator, metaclass. We will use the
|
|
333
|
+
metaclass because it is best suited for this purpose.
|
|
334
|
+
"""
|
|
335
|
+
|
|
336
|
+
_instances = {}
|
|
337
|
+
|
|
338
|
+
def __call__(cls, *args, **kwargs):
|
|
339
|
+
"""
|
|
340
|
+
Possible changes to the value of the `__init__` argument do not affect
|
|
341
|
+
the returned instance.
|
|
342
|
+
"""
|
|
343
|
+
if cls not in cls._instances:
|
|
344
|
+
instance = super().__call__(*args, **kwargs)
|
|
345
|
+
cls._instances[cls] = instance
|
|
346
|
+
return cls._instances[cls]
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
class PeriodDuration(metaclass=SingletonMeta):
|
|
350
|
+
periods = {
|
|
351
|
+
'D': 365,
|
|
352
|
+
'W': 53,
|
|
353
|
+
'M': 12,
|
|
354
|
+
'Q': 4,
|
|
355
|
+
'S': 2,
|
|
356
|
+
'A': 1
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
def __contains__(self, item):
|
|
360
|
+
return item in self.periods
|
|
361
|
+
|
|
362
|
+
@property
|
|
363
|
+
def member_names(self):
|
|
364
|
+
return list(self.periods.keys())
|
|
365
|
+
|
|
366
|
+
@classmethod
|
|
367
|
+
def check_period_range(cls, letter, value):
|
|
368
|
+
if letter == 'A':
|
|
369
|
+
return True
|
|
370
|
+
return value in range(1, cls.periods[letter] + 1)
|