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.
Files changed (94) hide show
  1. py_dpm/AST/ASTConstructor.py +503 -0
  2. py_dpm/AST/ASTObjects.py +827 -0
  3. py_dpm/AST/ASTTemplate.py +101 -0
  4. py_dpm/AST/ASTVisitor.py +13 -0
  5. py_dpm/AST/MLGeneration.py +588 -0
  6. py_dpm/AST/ModuleAnalyzer.py +79 -0
  7. py_dpm/AST/ModuleDependencies.py +203 -0
  8. py_dpm/AST/WhereClauseChecker.py +12 -0
  9. py_dpm/AST/__init__.py +0 -0
  10. py_dpm/AST/check_operands.py +302 -0
  11. py_dpm/DataTypes/ScalarTypes.py +324 -0
  12. py_dpm/DataTypes/TimeClasses.py +370 -0
  13. py_dpm/DataTypes/TypePromotion.py +195 -0
  14. py_dpm/DataTypes/__init__.py +0 -0
  15. py_dpm/Exceptions/__init__.py +0 -0
  16. py_dpm/Exceptions/exceptions.py +84 -0
  17. py_dpm/Exceptions/messages.py +114 -0
  18. py_dpm/OperationScopes/OperationScopeService.py +247 -0
  19. py_dpm/OperationScopes/__init__.py +0 -0
  20. py_dpm/Operators/AggregateOperators.py +138 -0
  21. py_dpm/Operators/BooleanOperators.py +30 -0
  22. py_dpm/Operators/ClauseOperators.py +159 -0
  23. py_dpm/Operators/ComparisonOperators.py +69 -0
  24. py_dpm/Operators/ConditionalOperators.py +362 -0
  25. py_dpm/Operators/NumericOperators.py +101 -0
  26. py_dpm/Operators/Operator.py +388 -0
  27. py_dpm/Operators/StringOperators.py +27 -0
  28. py_dpm/Operators/TimeOperators.py +53 -0
  29. py_dpm/Operators/__init__.py +0 -0
  30. py_dpm/Utils/ValidationsGenerationUtils.py +429 -0
  31. py_dpm/Utils/__init__.py +0 -0
  32. py_dpm/Utils/operands_mapping.py +73 -0
  33. py_dpm/Utils/operator_mapping.py +89 -0
  34. py_dpm/Utils/tokens.py +172 -0
  35. py_dpm/Utils/utils.py +2 -0
  36. py_dpm/ValidationsGeneration/PropertiesConstraintsProcessor.py +190 -0
  37. py_dpm/ValidationsGeneration/Utils.py +364 -0
  38. py_dpm/ValidationsGeneration/VariantsProcessor.py +265 -0
  39. py_dpm/ValidationsGeneration/__init__.py +0 -0
  40. py_dpm/ValidationsGeneration/auxiliary_functions.py +98 -0
  41. py_dpm/__init__.py +61 -0
  42. py_dpm/api/__init__.py +140 -0
  43. py_dpm/api/ast_generator.py +438 -0
  44. py_dpm/api/complete_ast.py +241 -0
  45. py_dpm/api/data_dictionary_validation.py +577 -0
  46. py_dpm/api/migration.py +77 -0
  47. py_dpm/api/semantic.py +224 -0
  48. py_dpm/api/syntax.py +182 -0
  49. py_dpm/client.py +106 -0
  50. py_dpm/data_handlers.py +99 -0
  51. py_dpm/db_utils.py +117 -0
  52. py_dpm/grammar/__init__.py +0 -0
  53. py_dpm/grammar/dist/__init__.py +0 -0
  54. py_dpm/grammar/dist/dpm_xlLexer.interp +428 -0
  55. py_dpm/grammar/dist/dpm_xlLexer.py +804 -0
  56. py_dpm/grammar/dist/dpm_xlLexer.tokens +106 -0
  57. py_dpm/grammar/dist/dpm_xlParser.interp +249 -0
  58. py_dpm/grammar/dist/dpm_xlParser.py +5224 -0
  59. py_dpm/grammar/dist/dpm_xlParser.tokens +106 -0
  60. py_dpm/grammar/dist/dpm_xlParserListener.py +742 -0
  61. py_dpm/grammar/dist/dpm_xlParserVisitor.py +419 -0
  62. py_dpm/grammar/dist/listeners.py +10 -0
  63. py_dpm/grammar/dpm_xlLexer.g4 +435 -0
  64. py_dpm/grammar/dpm_xlParser.g4 +260 -0
  65. py_dpm/migration.py +282 -0
  66. py_dpm/models.py +2139 -0
  67. py_dpm/semantics/DAG/DAGAnalyzer.py +158 -0
  68. py_dpm/semantics/DAG/__init__.py +0 -0
  69. py_dpm/semantics/SemanticAnalyzer.py +320 -0
  70. py_dpm/semantics/Symbols.py +223 -0
  71. py_dpm/semantics/__init__.py +0 -0
  72. py_dpm/utils/__init__.py +0 -0
  73. py_dpm/utils/ast_serialization.py +481 -0
  74. py_dpm/views/data_types.sql +12 -0
  75. py_dpm/views/datapoints.sql +65 -0
  76. py_dpm/views/hierarchy_operand_reference.sql +11 -0
  77. py_dpm/views/hierarchy_preconditions.sql +13 -0
  78. py_dpm/views/hierarchy_variables.sql +26 -0
  79. py_dpm/views/hierarchy_variables_context.sql +14 -0
  80. py_dpm/views/key_components.sql +18 -0
  81. py_dpm/views/module_from_table.sql +11 -0
  82. py_dpm/views/open_keys.sql +13 -0
  83. py_dpm/views/operation_info.sql +27 -0
  84. py_dpm/views/operation_list.sql +18 -0
  85. py_dpm/views/operations_versions_from_module_version.sql +30 -0
  86. py_dpm/views/precondition_info.sql +17 -0
  87. py_dpm/views/report_type_operand_reference_info.sql +18 -0
  88. py_dpm/views/subcategory_info.sql +17 -0
  89. py_dpm/views/table_info.sql +19 -0
  90. pydpm_xl-0.1.10.dist-info/LICENSE +674 -0
  91. pydpm_xl-0.1.10.dist-info/METADATA +50 -0
  92. pydpm_xl-0.1.10.dist-info/RECORD +94 -0
  93. pydpm_xl-0.1.10.dist-info/WHEEL +4 -0
  94. 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)