squirrels 0.1.0__py3-none-any.whl → 0.6.0.post0__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 (127) hide show
  1. dateutils/__init__.py +6 -0
  2. dateutils/_enums.py +25 -0
  3. squirrels/dateutils.py → dateutils/_implementation.py +409 -380
  4. dateutils/types.py +6 -0
  5. squirrels/__init__.py +21 -18
  6. squirrels/_api_routes/__init__.py +5 -0
  7. squirrels/_api_routes/auth.py +337 -0
  8. squirrels/_api_routes/base.py +196 -0
  9. squirrels/_api_routes/dashboards.py +156 -0
  10. squirrels/_api_routes/data_management.py +148 -0
  11. squirrels/_api_routes/datasets.py +220 -0
  12. squirrels/_api_routes/project.py +289 -0
  13. squirrels/_api_server.py +552 -134
  14. squirrels/_arguments/__init__.py +0 -0
  15. squirrels/_arguments/init_time_args.py +83 -0
  16. squirrels/_arguments/run_time_args.py +111 -0
  17. squirrels/_auth.py +777 -0
  18. squirrels/_command_line.py +239 -107
  19. squirrels/_compile_prompts.py +147 -0
  20. squirrels/_connection_set.py +94 -0
  21. squirrels/_constants.py +141 -64
  22. squirrels/_dashboards.py +179 -0
  23. squirrels/_data_sources.py +570 -0
  24. squirrels/_dataset_types.py +91 -0
  25. squirrels/_env_vars.py +209 -0
  26. squirrels/_exceptions.py +29 -0
  27. squirrels/_http_error_responses.py +52 -0
  28. squirrels/_initializer.py +319 -110
  29. squirrels/_logging.py +121 -0
  30. squirrels/_manifest.py +357 -187
  31. squirrels/_mcp_server.py +578 -0
  32. squirrels/_model_builder.py +69 -0
  33. squirrels/_model_configs.py +74 -0
  34. squirrels/_model_queries.py +52 -0
  35. squirrels/_models.py +1201 -0
  36. squirrels/_package_data/base_project/.env +7 -0
  37. squirrels/_package_data/base_project/.env.example +44 -0
  38. squirrels/_package_data/base_project/connections.yml +16 -0
  39. squirrels/_package_data/base_project/dashboards/dashboard_example.py +40 -0
  40. squirrels/_package_data/base_project/dashboards/dashboard_example.yml +22 -0
  41. squirrels/_package_data/base_project/docker/.dockerignore +16 -0
  42. squirrels/_package_data/base_project/docker/Dockerfile +16 -0
  43. squirrels/_package_data/base_project/docker/compose.yml +7 -0
  44. squirrels/_package_data/base_project/duckdb_init.sql +10 -0
  45. squirrels/_package_data/base_project/gitignore +13 -0
  46. squirrels/_package_data/base_project/macros/macros_example.sql +17 -0
  47. squirrels/_package_data/base_project/models/builds/build_example.py +26 -0
  48. squirrels/_package_data/base_project/models/builds/build_example.sql +16 -0
  49. squirrels/_package_data/base_project/models/builds/build_example.yml +57 -0
  50. squirrels/_package_data/base_project/models/dbviews/dbview_example.sql +17 -0
  51. squirrels/_package_data/base_project/models/dbviews/dbview_example.yml +32 -0
  52. squirrels/_package_data/base_project/models/federates/federate_example.py +51 -0
  53. squirrels/_package_data/base_project/models/federates/federate_example.sql +21 -0
  54. squirrels/_package_data/base_project/models/federates/federate_example.yml +65 -0
  55. squirrels/_package_data/base_project/models/sources.yml +38 -0
  56. squirrels/_package_data/base_project/parameters.yml +142 -0
  57. squirrels/_package_data/base_project/pyconfigs/connections.py +19 -0
  58. squirrels/_package_data/base_project/pyconfigs/context.py +96 -0
  59. squirrels/_package_data/base_project/pyconfigs/parameters.py +141 -0
  60. squirrels/_package_data/base_project/pyconfigs/user.py +56 -0
  61. squirrels/_package_data/base_project/resources/expenses.db +0 -0
  62. squirrels/_package_data/base_project/resources/public/.gitkeep +0 -0
  63. squirrels/_package_data/base_project/resources/weather.db +0 -0
  64. squirrels/_package_data/base_project/seeds/seed_categories.csv +6 -0
  65. squirrels/_package_data/base_project/seeds/seed_categories.yml +15 -0
  66. squirrels/_package_data/base_project/seeds/seed_subcategories.csv +15 -0
  67. squirrels/_package_data/base_project/seeds/seed_subcategories.yml +21 -0
  68. squirrels/_package_data/base_project/squirrels.yml.j2 +61 -0
  69. squirrels/_package_data/base_project/tmp/.gitignore +2 -0
  70. squirrels/_package_data/templates/login_successful.html +53 -0
  71. squirrels/_package_data/templates/squirrels_studio.html +22 -0
  72. squirrels/_package_loader.py +29 -0
  73. squirrels/_parameter_configs.py +592 -0
  74. squirrels/_parameter_options.py +348 -0
  75. squirrels/_parameter_sets.py +207 -0
  76. squirrels/_parameters.py +1703 -0
  77. squirrels/_project.py +796 -0
  78. squirrels/_py_module.py +122 -0
  79. squirrels/_request_context.py +33 -0
  80. squirrels/_schemas/__init__.py +0 -0
  81. squirrels/_schemas/auth_models.py +83 -0
  82. squirrels/_schemas/query_param_models.py +70 -0
  83. squirrels/_schemas/request_models.py +26 -0
  84. squirrels/_schemas/response_models.py +286 -0
  85. squirrels/_seeds.py +97 -0
  86. squirrels/_sources.py +112 -0
  87. squirrels/_utils.py +540 -149
  88. squirrels/_version.py +1 -3
  89. squirrels/arguments.py +7 -0
  90. squirrels/auth.py +4 -0
  91. squirrels/connections.py +3 -0
  92. squirrels/dashboards.py +3 -0
  93. squirrels/data_sources.py +14 -282
  94. squirrels/parameter_options.py +13 -189
  95. squirrels/parameters.py +14 -801
  96. squirrels/types.py +18 -0
  97. squirrels-0.6.0.post0.dist-info/METADATA +148 -0
  98. squirrels-0.6.0.post0.dist-info/RECORD +101 -0
  99. {squirrels-0.1.0.dist-info → squirrels-0.6.0.post0.dist-info}/WHEEL +1 -2
  100. {squirrels-0.1.0.dist-info → squirrels-0.6.0.post0.dist-info}/entry_points.txt +1 -0
  101. squirrels-0.6.0.post0.dist-info/licenses/LICENSE +201 -0
  102. squirrels/_credentials_manager.py +0 -87
  103. squirrels/_module_loader.py +0 -37
  104. squirrels/_parameter_set.py +0 -151
  105. squirrels/_renderer.py +0 -286
  106. squirrels/_timed_imports.py +0 -37
  107. squirrels/connection_set.py +0 -126
  108. squirrels/package_data/base_project/.gitignore +0 -4
  109. squirrels/package_data/base_project/connections.py +0 -21
  110. squirrels/package_data/base_project/database/sample_database.db +0 -0
  111. squirrels/package_data/base_project/database/seattle_weather.db +0 -0
  112. squirrels/package_data/base_project/datasets/sample_dataset/context.py +0 -8
  113. squirrels/package_data/base_project/datasets/sample_dataset/database_view1.py +0 -23
  114. squirrels/package_data/base_project/datasets/sample_dataset/database_view1.sql.j2 +0 -7
  115. squirrels/package_data/base_project/datasets/sample_dataset/final_view.py +0 -10
  116. squirrels/package_data/base_project/datasets/sample_dataset/final_view.sql.j2 +0 -2
  117. squirrels/package_data/base_project/datasets/sample_dataset/parameters.py +0 -30
  118. squirrels/package_data/base_project/datasets/sample_dataset/selections.cfg +0 -6
  119. squirrels/package_data/base_project/squirrels.yaml +0 -26
  120. squirrels/package_data/static/favicon.ico +0 -0
  121. squirrels/package_data/static/script.js +0 -234
  122. squirrels/package_data/static/style.css +0 -110
  123. squirrels/package_data/templates/index.html +0 -32
  124. squirrels-0.1.0.dist-info/LICENSE +0 -22
  125. squirrels-0.1.0.dist-info/METADATA +0 -67
  126. squirrels-0.1.0.dist-info/RECORD +0 -40
  127. squirrels-0.1.0.dist-info/top_level.txt +0 -1
@@ -1,380 +1,409 @@
1
- from typing import Sequence
2
- from dataclasses import dataclass
3
- from datetime import datetime
4
- from dateutil.relativedelta import relativedelta
5
- from enum import Enum
6
-
7
- from squirrels import _utils
8
-
9
-
10
- class DayOfWeek(Enum):
11
- Sunday = 0
12
- Monday = 1
13
- Tuesday = 2
14
- Wednesday = 3
15
- Thursday = 4
16
- Friday = 5
17
- Saturday = 6
18
-
19
- class Month(Enum):
20
- January = 1
21
- February = 2
22
- March = 3
23
- April = 4
24
- May = 5
25
- June = 6
26
- July = 7
27
- August = 8
28
- September = 9
29
- October = 10
30
- November = 11
31
- December = 12
32
-
33
-
34
- @dataclass
35
- class DateModifier:
36
- """
37
- Interface for all Date modification classes, and declares a "modify" method
38
- """
39
-
40
- def modify(self, date: datetime) -> datetime:
41
- """
42
- Method to be overwritten, modifies the input date
43
-
44
- Parameters:
45
- date: The input date to modify.
46
-
47
- Returns:
48
- The modified date.
49
- """
50
- raise _utils.AbstractMethodCallError(self.__class__, "modify")
51
-
52
-
53
- @dataclass
54
- class _DayIdxOfCalendarUnit(DateModifier):
55
- """
56
- Interface for adjusting a date to some day of calendar unit
57
- """
58
- idx: int
59
-
60
- def __post_init__(self):
61
- if self.idx == 0:
62
- raise _utils.ConfigurationError("The idx attribute of any DateModifier object cannot be zero")
63
- self.incr = self.idx - 1 if self.idx > 0 else self.idx
64
-
65
-
66
- @dataclass
67
- class DayIdxOfMonthsCycle(_DayIdxOfCalendarUnit):
68
- """
69
- DateModifier class to get the idx-th day of a cycle of months for an input date
70
-
71
- Attributes:
72
- idx: 1 for first, 2 for second, etc. Or, -1 for last, -2 for second last, etc. Must not be 0
73
- num_months_in_cycle: 2 for one 6th of year, 3 for Quarter, 4 for one 3rd of year, 6 for half year, 12 for full year. Must fit evenly in 12
74
- first_month_of_cycle: The first month of months cycle of year. Default is January
75
- """
76
- num_months_in_cycle: int
77
- first_month_of_cycle: Month = Month.January
78
-
79
- def __post_init__(self):
80
- super().__post_init__()
81
- if 12 % self.num_months_in_cycle != 0:
82
- raise _utils.ConfigurationError("Value X must fit evenly in 12")
83
- self.first_month_of_first_cycle = (self.first_month_of_cycle.value - 1) % self.num_months_in_cycle + 1
84
-
85
- def modify(self, date: datetime) -> datetime:
86
- current_cycle = (date.month - self.first_month_of_first_cycle) % 12 // self.num_months_in_cycle
87
- first_month_of_curr_cycle = current_cycle * self.num_months_in_cycle + self.first_month_of_first_cycle
88
- year = date.year if date.month >= first_month_of_curr_cycle else date.year - 1
89
- first_day = datetime(year, first_month_of_curr_cycle, 1)
90
- ref_date = first_day if self.idx > 0 else first_day + relativedelta(months=self.num_months_in_cycle)
91
- return ref_date + relativedelta(days=self.incr)
92
-
93
-
94
- @dataclass
95
- class DayIdxOfYear(DayIdxOfMonthsCycle):
96
- """
97
- DateModifier class to get the idx-th day of year of an input date
98
-
99
- Attributes:
100
- idx: 1 for first, 2 for second, etc. Or, -1 for last, -2 for second last, etc. Must not be 0
101
- first_month_of_year: The first month of year. Default is January
102
- """
103
-
104
- def __init__(self, idx: int, first_month_of_year: Month = Month.January):
105
- super().__init__(idx, num_months_in_cycle=12, first_month_of_cycle=first_month_of_year)
106
-
107
-
108
- @dataclass
109
- class DayIdxOfQuarter(DayIdxOfMonthsCycle):
110
- """
111
- DateModifier class to get the idx-th day of quarter of an input date
112
-
113
- Attributes:
114
- idx: 1 for first, 2 for second, etc. Or, -1 for last, -2 for second last, etc. Must not be 0
115
- first_month_of_quarter: The first month of first quarter. Default is January
116
- """
117
-
118
- def __init__(self, idx: int, first_month_of_quarter: Month = Month.January):
119
- super().__init__(idx, num_months_in_cycle=3, first_month_of_cycle=first_month_of_quarter)
120
-
121
-
122
- @dataclass
123
- class DayIdxOfMonth(_DayIdxOfCalendarUnit):
124
- """
125
- DateModifier class to get the idx-th day of month of an input date
126
-
127
- Attributes:
128
- idx: 1 for first, 2 for second, etc. Or, -1 for last, -2 for second last, etc. Must not be 0
129
- """
130
-
131
- def modify(self, date: datetime) -> datetime:
132
- first_day = datetime(date.year, date.month, 1)
133
- ref_date = first_day if self.idx > 0 else first_day + relativedelta(months=1)
134
- return ref_date + relativedelta(days=self.incr)
135
-
136
-
137
- @dataclass
138
- class DayIdxOfWeek(_DayIdxOfCalendarUnit):
139
- """
140
- DateModifier class to get the idx-th day of week of an input date
141
-
142
- Attributes:
143
- idx: 1 for first, 2 for second, etc. Or, -1 for last, -2 for second last, etc. Must not be 0
144
- first_day_of_week: The day of week identified as the "first". Default is Monday
145
- """
146
- first_day_of_week: DayOfWeek = DayOfWeek.Monday
147
-
148
- def __post_init__(self):
149
- super().__post_init__()
150
- self.first_dow_num = self.first_day_of_week.value
151
-
152
- def modify(self, date: datetime) -> datetime:
153
- distance_from_first_day = (1 + date.weekday() - self.first_dow_num) % 7
154
- total_incr = -distance_from_first_day + (7 if self.idx < 0 else 0) + self.incr
155
- return date + relativedelta(days=total_incr)
156
-
157
-
158
- @dataclass
159
- class _OffsetUnits(DateModifier):
160
- """
161
- Abstract DateModifier class to offset an input date by some number of some calendar unit
162
- """
163
- offset: int
164
-
165
-
166
- @dataclass
167
- class OffsetYears(_OffsetUnits):
168
- """
169
- DateModifier class to offset an input date by some number of years
170
-
171
- Attributes:
172
- offset: The number of years to offset the input date.
173
- """
174
-
175
- def modify(self, date: datetime) -> datetime:
176
- return date + relativedelta(years=self.offset)
177
-
178
-
179
- @dataclass
180
- class OffsetMonths(_OffsetUnits):
181
- """
182
- DateModifier class to offset an input date by some number of months
183
-
184
- Attributes:
185
- offset: The number of months to offset the input date.
186
- """
187
-
188
- def modify(self, date: datetime) -> datetime:
189
- return date + relativedelta(months=self.offset)
190
-
191
-
192
- @dataclass
193
- class OffsetWeeks(_OffsetUnits):
194
- """
195
- DateModifier class to offset an input date by some number of weeks
196
-
197
- Attributes:
198
- offset: The number of weeks to offset the input date.
199
- """
200
-
201
- def modify(self, date: datetime) -> datetime:
202
- return date + relativedelta(weeks=self.offset)
203
-
204
-
205
- @dataclass
206
- class OffsetDays(_OffsetUnits):
207
- """
208
- DateModifier class to offset an input date by some number of days
209
-
210
- Attributes:
211
- offset: The number of days to offset the input date.
212
- """
213
-
214
- def modify(self, date: datetime) -> datetime:
215
- return date + relativedelta(days=self.offset)
216
-
217
-
218
- @dataclass
219
- class DateModPipeline(DateModifier):
220
- """
221
- DateModifier class to apply a list of date modifiers to an input date
222
-
223
- Attributes:
224
- modifiers: The list of DateModifier's to apply in sequence.
225
- """
226
- modifiers: Sequence[DateModifier]
227
-
228
- def __post_init__(self):
229
- self.modifiers = tuple(self.modifiers)
230
-
231
- def modify(self, date: datetime) -> datetime:
232
- for modifier in self.modifiers:
233
- date = modifier.modify(date)
234
- return date
235
-
236
-
237
- @dataclass
238
- class _DateRepresentationModifier:
239
- """
240
- Abstract class for modifying other representations of dates (such as string or unix timestemp)
241
- """
242
- def __init__(self, date_modifiers: Sequence[DateModifier]):
243
- self.date_modifier = DateModPipeline(date_modifiers)
244
-
245
- def _get_joined_modifiers(self, date_modifiers: Sequence[DateModifier]) -> Sequence[DateModifier]:
246
- joined_modifiers = self.date_modifier.modifiers + tuple(date_modifiers)
247
- return joined_modifiers
248
-
249
- def with_more_modifiers(self, date_modifiers: Sequence[DateModifier]):
250
- raise _utils.AbstractMethodCallError(self.__class__, "with_more_modifiers")
251
-
252
- def get_date_list(self, curr_date: datetime, step: DateModifier) -> Sequence[datetime]:
253
- modified_date = self.date_modifier.modify(curr_date)
254
- curr_date, end_date = min(curr_date, modified_date), max(curr_date, modified_date)
255
- distance = None
256
- output = []
257
- while curr_date <= end_date:
258
- if distance is not None and (end_date - curr_date) >= distance:
259
- raise _utils.ConfigurationError("The step must increment forward in time")
260
- distance = end_date - curr_date
261
- output.append(curr_date)
262
- curr_date = step.modify(curr_date)
263
- return output
264
-
265
-
266
- @dataclass
267
- class DateStringModifier(_DateRepresentationModifier):
268
- """
269
- Class to modify a string representation of a date given a DateModifier
270
-
271
- Attributes:
272
- date_modifier: The DateModifier to apply on datetime objects
273
- date_format: Format of the output date string. Default is '%Y-%m-%d'
274
- """
275
- def __init__(self, date_modifiers: Sequence[DateModifier], date_format: str = '%Y-%m-%d'):
276
- super().__init__(date_modifiers)
277
- self.date_format = date_format
278
-
279
- def with_more_modifiers(self, date_modifiers: Sequence[DateModifier]):
280
- """
281
- Create a new DateStringModifier with more date modifiers
282
-
283
- Parameters:
284
- date_modifiers: The additional date modifiers to add
285
-
286
- Returns:
287
- A new DateStringModifier
288
- """
289
- joined_modifiers = self._get_joined_modifiers(date_modifiers)
290
- return DateStringModifier(joined_modifiers, self.date_format)
291
-
292
- def _get_input_date_obj(self, date_str: str, input_format: str = None) -> datetime:
293
- input_format = self.date_format if input_format is None else input_format
294
- return datetime.strptime(date_str, input_format)
295
-
296
- def modify(self, date_str: str, input_format: str = None) -> str:
297
- """
298
- Modifies the input date string with the date modifiers
299
-
300
- Parameters:
301
- date_str: The input date string
302
- input_format: The input date format. Defaults to the same as output date format
303
-
304
- Returns:
305
- The resulting date string
306
- """
307
- date_obj = self._get_input_date_obj(date_str, input_format)
308
- return self.date_modifier.modify(date_obj).strftime(self.date_format)
309
-
310
- def get_date_list(self, date_str: str, step: DateModifier, input_format: str = None) -> Sequence[str]:
311
- """
312
- This method modifies the input date string, and returns all dates from the earlier to later date,
313
- incremented by a DateModifier step, until the last date is less than or equal to the later date.
314
-
315
- Parameters:
316
- date_str: The input date string, usually the first date in the output list
317
- step: The increment to take (specified as a DateModifier). It must increment forward in time
318
- input_format: The input date format. Defaults to the same as output date format
319
-
320
- Returns:
321
- A list of date strings
322
- """
323
- curr_date = self._get_input_date_obj(date_str, input_format)
324
- output = super().get_date_list(curr_date, step)
325
- return [x.strftime(self.date_format) for x in output]
326
-
327
-
328
- @dataclass
329
- class TimestampModifier(_DateRepresentationModifier):
330
- """
331
- Class to modify a numeric representation of a date (as Unix/Epoch/POSIX timestamp) given a DateModifier
332
-
333
- Attributes:
334
- date_modifier: The DateModifier to apply on datetime objects
335
- date_format: Format of the date string. Default is '%Y-%m-%d'
336
- """
337
- def __init__(self, date_modifiers: Sequence[DateModifier]):
338
- super().__init__(date_modifiers)
339
-
340
- def with_more_modifiers(self, date_modifiers: Sequence[DateModifier]):
341
- """
342
- Create a new TimestampModifier with more date modifiers
343
-
344
- Parameters:
345
- date_modifiers: The additional date modifiers to add
346
-
347
- Returns:
348
- A new TimestampModifier
349
- """
350
- joined_modifiers = self._get_joined_modifiers(date_modifiers)
351
- return TimestampModifier(joined_modifiers)
352
-
353
- def modify(self, timestamp: float) -> float:
354
- """
355
- Modifies the input timestamp with the date modifiers
356
-
357
- Parameters:
358
- timestamp: The input timestamp as float
359
-
360
- Returns:
361
- The resulting timestamp
362
- """
363
- date_obj = datetime.fromtimestamp(timestamp)
364
- return self.date_modifier.modify(date_obj).timestamp()
365
-
366
- def get_date_list(self, timestamp: float, step: DateModifier) -> Sequence[float]:
367
- """
368
- This method modifies the input date string, and returns all dates from the earlier to later date,
369
- incremented by a DateModifier step, until the last date is less than or equal to the later date.
370
-
371
- Parameters:
372
- timestamp: The input timestamp as float, usually the first date in the output list
373
- step: The increment to take (specified as a DateModifier). It must increment forward in time
374
-
375
- Returns:
376
- A list of timestamp as floats
377
- """
378
- curr_date = datetime.fromtimestamp(timestamp)
379
- output = super().get_date_list(curr_date, step)
380
- return [x.timestamp() for x in output]
1
+ from typing import Sequence, Type
2
+ from dataclasses import dataclass
3
+ from datetime import date as Date, datetime
4
+ from dateutil.relativedelta import relativedelta
5
+ from abc import ABCMeta, abstractmethod
6
+
7
+ from ._enums import DayOfWeekEnum, MonthEnum
8
+
9
+
10
+ class DateModifier(metaclass=ABCMeta):
11
+ """
12
+ Interface for all Date modification classes, and declares a "modify" method
13
+ """
14
+
15
+ @abstractmethod
16
+ def modify(self, date: Date) -> Date:
17
+ """
18
+ Method to be overwritten, modifies the input date
19
+
20
+ Arguments:
21
+ date: The input date to modify.
22
+
23
+ Returns:
24
+ The modified date.
25
+ """
26
+ pass
27
+
28
+ def _get_date(self, datetype: Type, year: int, month: int, day: int) -> Date:
29
+ return datetype(year, month, day)
30
+
31
+
32
+ @dataclass
33
+ class DayIdxOfCalendarUnit(DateModifier):
34
+ """
35
+ Interface for adjusting a date to some day of calendar unit
36
+ """
37
+ idx: int
38
+
39
+ def __post_init__(self) -> None:
40
+ if self.idx == 0:
41
+ raise ValueError(f"For constructors of class names that start with DayIdxOf_, idx cannot be zero")
42
+ self.incr = self.idx - 1 if self.idx > 0 else self.idx
43
+
44
+
45
+ @dataclass
46
+ class DayIdxOfMonthsCycle(DayIdxOfCalendarUnit):
47
+ """
48
+ DateModifier class to get the idx-th day of a cycle of months for an input date
49
+
50
+ Attributes:
51
+ idx: 1 for first, 2 for second, etc. Or, -1 for last, -2 for second last, etc. Must not be 0
52
+ num_months_in_cycle: 2 for one 6th of year, 3 for Quarter, 4 for one 3rd of year, 6 for half year, 12 for full year. Must fit evenly in 12
53
+ first_month_of_cycle: The first month of months cycle of year. Default is January
54
+ """
55
+ num_months_in_cycle: int
56
+ first_month_of_cycle: MonthEnum = MonthEnum.January
57
+
58
+ def __post_init__(self) -> None:
59
+ super().__post_init__()
60
+ if 12 % self.num_months_in_cycle != 0:
61
+ raise ValueError(f"Argument 'num_months_in_cycle' must fit evenly in 12")
62
+ self.first_month_of_first_cycle = (self.first_month_of_cycle.value - 1) % self.num_months_in_cycle + 1
63
+
64
+ def modify(self, date: Date) -> Date:
65
+ current_cycle = (date.month - self.first_month_of_first_cycle) % 12 // self.num_months_in_cycle
66
+ first_month_of_curr_cycle = current_cycle * self.num_months_in_cycle + self.first_month_of_first_cycle
67
+ year = date.year if date.month >= first_month_of_curr_cycle else date.year - 1
68
+ first_day = self._get_date(type(date), year, first_month_of_curr_cycle, 1)
69
+ ref_date = first_day if self.idx > 0 else first_day + relativedelta(months=self.num_months_in_cycle)
70
+ return ref_date + relativedelta(days=self.incr)
71
+
72
+
73
+ @dataclass
74
+ class DayIdxOfYear(DayIdxOfMonthsCycle):
75
+ """
76
+ DateModifier class to get the idx-th day of year of an input date
77
+
78
+ Attributes:
79
+ idx: 1 for first, 2 for second, etc. Or, -1 for last, -2 for second last, etc. Must not be 0
80
+ first_month_of_year: The first month of year. Default is January
81
+ """
82
+
83
+ def __init__(self, idx: int, first_month_of_year: MonthEnum = MonthEnum.January):
84
+ super().__init__(idx, num_months_in_cycle=12, first_month_of_cycle=first_month_of_year)
85
+
86
+
87
+ @dataclass
88
+ class DayIdxOfQuarter(DayIdxOfMonthsCycle):
89
+ """
90
+ DateModifier class to get the idx-th day of quarter of an input date
91
+
92
+ Attributes:
93
+ idx: 1 for first, 2 for second, etc. Or, -1 for last, -2 for second last, etc. Must not be 0
94
+ first_month_of_quarter: The first month of first quarter. Default is January
95
+ """
96
+
97
+ def __init__(self, idx: int, first_month_of_quarter: MonthEnum = MonthEnum.January):
98
+ super().__init__(idx, num_months_in_cycle=3, first_month_of_cycle=first_month_of_quarter)
99
+
100
+
101
+ @dataclass
102
+ class DayIdxOfMonth(DayIdxOfCalendarUnit):
103
+ """
104
+ DateModifier class to get the idx-th day of month of an input date
105
+
106
+ Attributes:
107
+ idx: 1 for first, 2 for second, etc. Or, -1 for last, -2 for second last, etc. Must not be 0
108
+ """
109
+
110
+ def modify(self, date: Date) -> Date:
111
+ first_day = self._get_date(type(date), date.year, date.month, 1)
112
+ ref_date = first_day if self.idx > 0 else first_day + relativedelta(months=1)
113
+ return ref_date + relativedelta(days=self.incr)
114
+
115
+
116
+ @dataclass
117
+ class DayIdxOfWeek(DayIdxOfCalendarUnit):
118
+ """
119
+ DateModifier class to get the idx-th day of week of an input date
120
+
121
+ Attributes:
122
+ idx: 1 for first, 2 for second, etc. Or, -1 for last, -2 for second last, etc. Must not be 0
123
+ first_day_of_week: The day of week identified as the "first". Default is Monday
124
+ """
125
+ first_day_of_week: DayOfWeekEnum = DayOfWeekEnum.Monday
126
+
127
+ def __post_init__(self) -> None:
128
+ super().__post_init__()
129
+ self.first_dow_num = self.first_day_of_week.value
130
+
131
+ def modify(self, date: Date) -> Date:
132
+ distance_from_first_day = (1 + date.weekday() - self.first_dow_num) % 7
133
+ total_incr = -distance_from_first_day + (7 if self.idx < 0 else 0) + self.incr
134
+ return date + relativedelta(days=total_incr)
135
+
136
+
137
+ @dataclass
138
+ class OffsetUnits(DateModifier):
139
+ """
140
+ Abstract DateModifier class to offset an input date by some number of some calendar unit
141
+ """
142
+ offset: int
143
+
144
+
145
+ @dataclass
146
+ class OffsetYears(OffsetUnits):
147
+ """
148
+ DateModifier class to offset an input date by some number of years
149
+
150
+ Attributes:
151
+ offset: The number of years to offset the input date.
152
+ """
153
+
154
+ def modify(self, date: Date) -> Date:
155
+ return date + relativedelta(years=self.offset)
156
+
157
+
158
+ @dataclass
159
+ class OffsetMonths(OffsetUnits):
160
+ """
161
+ DateModifier class to offset an input date by some number of months
162
+
163
+ Attributes:
164
+ offset: The number of months to offset the input date.
165
+ """
166
+
167
+ def modify(self, date: Date) -> Date:
168
+ return date + relativedelta(months=self.offset)
169
+
170
+
171
+ @dataclass
172
+ class OffsetWeeks(OffsetUnits):
173
+ """
174
+ DateModifier class to offset an input date by some number of weeks
175
+
176
+ Attributes:
177
+ offset: The number of weeks to offset the input date.
178
+ """
179
+
180
+ def modify(self, date: Date) -> Date:
181
+ return date + relativedelta(weeks=self.offset)
182
+
183
+
184
+ @dataclass
185
+ class OffsetDays(OffsetUnits):
186
+ """
187
+ DateModifier class to offset an input date by some number of days
188
+
189
+ Attributes:
190
+ offset: The number of days to offset the input date.
191
+ """
192
+
193
+ def modify(self, date: Date) -> Date:
194
+ return date + relativedelta(days=self.offset)
195
+
196
+
197
+ @dataclass
198
+ class DateModPipeline(DateModifier):
199
+ """
200
+ DateModifier class to apply a list of date modifiers to an input date
201
+
202
+ Attributes:
203
+ modifiers: The list of DateModifier's to apply in sequence.
204
+ """
205
+ date_modifiers: Sequence[DateModifier]
206
+
207
+ def modify(self, date: Date) -> Date:
208
+ for modifier in self.date_modifiers:
209
+ date = modifier.modify(date)
210
+ return date
211
+
212
+ def get_joined_modifiers(self, date_modifiers: Sequence[DateModifier]) -> Sequence[DateModifier]:
213
+ """
214
+ Create a new sequence of DateModifier by joining the date modifiers in this class
215
+ with the input date_modifiers
216
+
217
+ Arguments:
218
+ date_modifiers: The new date modifier sequence to join
219
+
220
+ Returns:
221
+ A new sequence of DateModifier
222
+ """
223
+ joined_modifiers = tuple(self.date_modifiers) + tuple(date_modifiers)
224
+ return joined_modifiers
225
+
226
+ def with_more_modifiers(self, date_modifiers: Sequence[DateModifier]):
227
+ """
228
+ Create a new DateModPipeline with more date modifiers
229
+
230
+ Arguments:
231
+ date_modifiers: The additional date modifiers to add
232
+
233
+ Returns:
234
+ A new DateModPipeline
235
+ """
236
+ joined_modifiers = self.get_joined_modifiers(date_modifiers)
237
+ return DateModPipeline(joined_modifiers)
238
+
239
+ def get_date_list(self, start_date: Date, step: DateModifier) -> Sequence[Date]:
240
+ """
241
+ This method modifies the input date, and returns all dates from the input date to the modified date,
242
+ incremented by a DateModifier step.
243
+
244
+ If the step is positive and start date is less than end date, then it'll return an increasing list of
245
+ dates starting from the start date. If the step is negative and start date is greater than end date,
246
+ then it'll return a decreasing list of dates starting from the start date. Otherwise, an empty list
247
+ is returned.
248
+
249
+ Arguments:
250
+ start_date: The input date (it's the first date in the output list if step moves towards end date)
251
+ step: The increment to take (specified as an offset DateModifier). Offset cannot be zero
252
+
253
+ Returns:
254
+ A list of datetime objects
255
+ """
256
+ assert isinstance(step, OffsetUnits)
257
+ if step.offset == 0:
258
+ raise ValueError(f"The length of 'step' must not be zero")
259
+
260
+ output: Sequence[Date] = []
261
+ end_date = self.modify(start_date)
262
+ curr_date = start_date
263
+ is_not_done_positive_step = lambda: curr_date <= end_date and step.offset > 0
264
+ is_not_done_negative_step = lambda: curr_date >= end_date and step.offset < 0
265
+ while is_not_done_positive_step() or is_not_done_negative_step():
266
+ output.append(curr_date)
267
+ curr_date = step.modify(curr_date)
268
+ return output
269
+
270
+
271
+ @dataclass
272
+ class DateRepresentationModifier(metaclass=ABCMeta):
273
+ """
274
+ Abstract class for modifying other representations of dates (such as string or unix timestemp)
275
+ """
276
+ date_modifiers: Sequence[DateModifier]
277
+
278
+ def __post_init__(self) -> None:
279
+ self.date_mod_pipeline = DateModPipeline(self.date_modifiers)
280
+
281
+ @abstractmethod
282
+ def with_more_modifiers(self, date_modifiers: Sequence[DateModifier]):
283
+ pass
284
+
285
+
286
+ @dataclass
287
+ class DateStringModifier(DateRepresentationModifier):
288
+ """
289
+ Class to modify a string representation of a date given a DateModifier
290
+
291
+ Attributes:
292
+ date_modifier: The DateModifier to apply on datetime objects
293
+ date_format: Format of the output date string. Default is '%Y-%m-%d'
294
+ """
295
+ date_format: str = '%Y-%m-%d'
296
+
297
+ def with_more_modifiers(self, date_modifiers: Sequence[DateModifier]):
298
+ """
299
+ Create a new DateStringModifier with more date modifiers
300
+
301
+ Arguments:
302
+ date_modifiers: The additional date modifiers to add
303
+
304
+ Returns:
305
+ A new DateStringModifier
306
+ """
307
+ joined_modifiers = self.date_mod_pipeline.get_joined_modifiers(date_modifiers)
308
+ return DateStringModifier(joined_modifiers, self.date_format)
309
+
310
+ def _get_input_date_obj(self, date_str: str, input_format: str | None = None) -> Date:
311
+ input_format = self.date_format if input_format is None else input_format
312
+ return datetime.strptime(date_str, input_format).date()
313
+
314
+ def modify(self, date_str: str, input_format: str | None = None) -> str:
315
+ """
316
+ Modifies the input date string with the date modifiers
317
+
318
+ Arguments:
319
+ date_str: The input date string
320
+ input_format: The input date format. Defaults to the same as output date format
321
+
322
+ Returns:
323
+ The resulting date string
324
+ """
325
+ date_obj = self._get_input_date_obj(date_str, input_format)
326
+ return self.date_mod_pipeline.modify(date_obj).strftime(self.date_format)
327
+
328
+ def get_date_list(self, start_date_str: str, step: DateModifier, input_format: str | None = None) -> Sequence[str]:
329
+ """
330
+ This method modifies the input date string, and returns all dates as strings from the input date
331
+ to the modified date, incremented by a DateModifier step.
332
+
333
+ If the step is positive and start date is less than end date, then it'll return an increasing list of
334
+ dates starting from the start date. If the step is negative and start date is greater than end date,
335
+ then it'll return a decreasing list of dates starting from the start date. Otherwise, an empty list
336
+ is returned.
337
+
338
+ Arguments:
339
+ start_date_str: The input date string (it's the first date in the output list if step moves towards end date)
340
+ step: The increment to take (specified as an offset DateModifier). Offset cannot be zero
341
+ input_format: The input date format. Defaults to the same as output date format
342
+
343
+ Returns:
344
+ A list of date strings
345
+ """
346
+ assert isinstance(step, OffsetUnits)
347
+ curr_date = self._get_input_date_obj(start_date_str, input_format)
348
+ output = self.date_mod_pipeline.get_date_list(curr_date, step)
349
+ return [x.strftime(self.date_format) for x in output]
350
+
351
+
352
+ @dataclass
353
+ class TimestampModifier(DateRepresentationModifier):
354
+ """
355
+ Class to modify a numeric representation of a date (as Unix/Epoch/POSIX timestamp) given a DateModifier
356
+
357
+ Attributes:
358
+ date_modifier: The DateModifier to apply on datetime objects
359
+ date_format: Format of the date string. Default is '%Y-%m-%d'
360
+ """
361
+
362
+ def with_more_modifiers(self, date_modifiers: Sequence[DateModifier]):
363
+ """
364
+ Create a new TimestampModifier with more date modifiers
365
+
366
+ Arguments:
367
+ date_modifiers: The additional date modifiers to add
368
+
369
+ Returns:
370
+ A new TimestampModifier
371
+ """
372
+ joined_modifiers = self.date_mod_pipeline.get_joined_modifiers(date_modifiers)
373
+ return TimestampModifier(joined_modifiers)
374
+
375
+ def modify(self, timestamp: float) -> float:
376
+ """
377
+ Modifies the input timestamp with the date modifiers
378
+
379
+ Arguments:
380
+ timestamp: The input timestamp as float
381
+
382
+ Returns:
383
+ The resulting timestamp
384
+ """
385
+ date_obj = datetime.fromtimestamp(timestamp).date()
386
+ modified_date = self.date_mod_pipeline.modify(date_obj)
387
+ modified_datetime = datetime.combine(modified_date, datetime.min.time())
388
+ return modified_datetime.timestamp()
389
+
390
+ def get_date_list(self, start_timestamp: float, step: DateModifier) -> Sequence[float]:
391
+ """
392
+ This method modifies the input timestamp, and returns all dates as timestampes/floats from the input date
393
+ to the modified date, incremented by a DateModifier step.
394
+
395
+ If the step is positive and start date is less than end date, then it'll return an increasing list of
396
+ dates starting from the start date. If the step is negative and start date is greater than end date,
397
+ then it'll return a decreasing list of dates starting from the start date. Otherwise, an empty list
398
+ is returned.
399
+
400
+ Arguments:
401
+ start_timestamp: The input timestamp as float (it's the first date in the output list if step moves towards end date)
402
+ step: The increment to take (specified as an offset DateModifier). Offset cannot be zero
403
+
404
+ Returns:
405
+ A list of timestamp as floats
406
+ """
407
+ curr_date = datetime.fromtimestamp(start_timestamp).date()
408
+ output = self.date_mod_pipeline.get_date_list(curr_date, step)
409
+ return [datetime.combine(x, datetime.min.time()).timestamp() for x in output]