py10x-universe 0.1.3__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 (214) hide show
  1. core_10x/__init__.py +42 -0
  2. core_10x/backbone/__init__.py +0 -0
  3. core_10x/backbone/backbone_store.py +59 -0
  4. core_10x/backbone/backbone_traitable.py +30 -0
  5. core_10x/backbone/backbone_user.py +66 -0
  6. core_10x/backbone/bound_data_domain.py +49 -0
  7. core_10x/backbone/namespace.py +101 -0
  8. core_10x/backbone/vault.py +38 -0
  9. core_10x/code_samples/__init__.py +0 -0
  10. core_10x/code_samples/_package_manifest.py +3 -0
  11. core_10x/code_samples/directories.py +181 -0
  12. core_10x/code_samples/person.py +76 -0
  13. core_10x/concrete_traits.py +356 -0
  14. core_10x/conftest.py +12 -0
  15. core_10x/curve.py +321 -0
  16. core_10x/data_domain.py +48 -0
  17. core_10x/data_domain_binder.py +45 -0
  18. core_10x/directory.py +250 -0
  19. core_10x/entity.py +8 -0
  20. core_10x/entity_filter.py +5 -0
  21. core_10x/environment_variables.py +147 -0
  22. core_10x/exec_control.py +84 -0
  23. core_10x/experimental/__init__.py +0 -0
  24. core_10x/experimental/data_protocol_ex.py +34 -0
  25. core_10x/global_cache.py +121 -0
  26. core_10x/manual_tests/__init__.py +0 -0
  27. core_10x/manual_tests/calendar_test.py +35 -0
  28. core_10x/manual_tests/ctor_update_bug.py +58 -0
  29. core_10x/manual_tests/debug_graph_on.py +17 -0
  30. core_10x/manual_tests/debug_graphoff_inside_graph_on.py +28 -0
  31. core_10x/manual_tests/enum_bits_test.py +17 -0
  32. core_10x/manual_tests/env_vars_trivial_test.py +12 -0
  33. core_10x/manual_tests/existing_traitable.py +33 -0
  34. core_10x/manual_tests/k10x_test1.py +13 -0
  35. core_10x/manual_tests/named_constant_test.py +121 -0
  36. core_10x/manual_tests/nucleus_trivial_test.py +42 -0
  37. core_10x/manual_tests/polars_test.py +14 -0
  38. core_10x/manual_tests/py_class_test.py +4 -0
  39. core_10x/manual_tests/rc_test.py +42 -0
  40. core_10x/manual_tests/rdate_test.py +12 -0
  41. core_10x/manual_tests/reference_serialization_bug.py +19 -0
  42. core_10x/manual_tests/resource_trivial_test.py +10 -0
  43. core_10x/manual_tests/store_uri_test.py +6 -0
  44. core_10x/manual_tests/trait_definition_test.py +19 -0
  45. core_10x/manual_tests/trait_filter_test.py +15 -0
  46. core_10x/manual_tests/trait_flag_modification_test.py +42 -0
  47. core_10x/manual_tests/trait_modification_bug.py +26 -0
  48. core_10x/manual_tests/traitable_as_of_test.py +82 -0
  49. core_10x/manual_tests/traitable_heir_test.py +39 -0
  50. core_10x/manual_tests/traitable_history_test.py +41 -0
  51. core_10x/manual_tests/traitable_serialization_test.py +54 -0
  52. core_10x/manual_tests/traitable_trivial_test.py +71 -0
  53. core_10x/manual_tests/trivial_graph_test.py +16 -0
  54. core_10x/manual_tests/ts_class_association_test.py +64 -0
  55. core_10x/manual_tests/ts_trivial_test.py +35 -0
  56. core_10x/named_constant.py +425 -0
  57. core_10x/nucleus.py +81 -0
  58. core_10x/package_manifest.py +85 -0
  59. core_10x/package_refactoring.py +153 -0
  60. core_10x/py_class.py +431 -0
  61. core_10x/rc.py +155 -0
  62. core_10x/rdate.py +339 -0
  63. core_10x/resource.py +189 -0
  64. core_10x/roman_number.py +67 -0
  65. core_10x/testlib/__init__.py +0 -0
  66. core_10x/testlib/test_store.py +240 -0
  67. core_10x/testlib/traitable_history_tests.py +787 -0
  68. core_10x/testlib/ts_tests.py +280 -0
  69. core_10x/trait.py +377 -0
  70. core_10x/trait_definition.py +176 -0
  71. core_10x/trait_filter.py +205 -0
  72. core_10x/trait_method_error.py +36 -0
  73. core_10x/traitable.py +1082 -0
  74. core_10x/traitable_cli.py +153 -0
  75. core_10x/traitable_heir.py +33 -0
  76. core_10x/traitable_id.py +31 -0
  77. core_10x/ts_store.py +172 -0
  78. core_10x/ts_store_type.py +26 -0
  79. core_10x/ts_union.py +147 -0
  80. core_10x/ui_hint.py +153 -0
  81. core_10x/unit_tests/test_concrete_traits.py +156 -0
  82. core_10x/unit_tests/test_converters.py +51 -0
  83. core_10x/unit_tests/test_curve.py +157 -0
  84. core_10x/unit_tests/test_directory.py +54 -0
  85. core_10x/unit_tests/test_documentation.py +172 -0
  86. core_10x/unit_tests/test_environment_variables.py +15 -0
  87. core_10x/unit_tests/test_filters.py +239 -0
  88. core_10x/unit_tests/test_graph.py +348 -0
  89. core_10x/unit_tests/test_named_constant.py +98 -0
  90. core_10x/unit_tests/test_rc.py +11 -0
  91. core_10x/unit_tests/test_rdate.py +484 -0
  92. core_10x/unit_tests/test_trait_method_error.py +80 -0
  93. core_10x/unit_tests/test_trait_modification.py +19 -0
  94. core_10x/unit_tests/test_traitable.py +959 -0
  95. core_10x/unit_tests/test_traitable_history.py +1 -0
  96. core_10x/unit_tests/test_ts_store.py +1 -0
  97. core_10x/unit_tests/test_ts_union.py +369 -0
  98. core_10x/unit_tests/test_ui_nodes.py +81 -0
  99. core_10x/unit_tests/test_xxcalendar.py +471 -0
  100. core_10x/vault/__init__.py +0 -0
  101. core_10x/vault/sec_keys.py +133 -0
  102. core_10x/vault/security_keys_old.py +168 -0
  103. core_10x/vault/vault.py +56 -0
  104. core_10x/vault/vault_traitable.py +56 -0
  105. core_10x/vault/vault_user.py +70 -0
  106. core_10x/xdate_time.py +136 -0
  107. core_10x/xnone.py +71 -0
  108. core_10x/xxcalendar.py +228 -0
  109. infra_10x/__init__.py +0 -0
  110. infra_10x/manual_tests/__init__.py +0 -0
  111. infra_10x/manual_tests/test_misc.py +16 -0
  112. infra_10x/manual_tests/test_prepare_filter_and_pipeline.py +25 -0
  113. infra_10x/mongodb_admin.py +111 -0
  114. infra_10x/mongodb_store.py +346 -0
  115. infra_10x/mongodb_utils.py +129 -0
  116. infra_10x/unit_tests/conftest.py +13 -0
  117. infra_10x/unit_tests/test_mongo_db.py +36 -0
  118. infra_10x/unit_tests/test_mongo_history.py +1 -0
  119. py10x_universe-0.1.3.dist-info/METADATA +406 -0
  120. py10x_universe-0.1.3.dist-info/RECORD +214 -0
  121. py10x_universe-0.1.3.dist-info/WHEEL +4 -0
  122. py10x_universe-0.1.3.dist-info/licenses/LICENSE +21 -0
  123. ui_10x/__init__.py +0 -0
  124. ui_10x/apps/__init__.py +0 -0
  125. ui_10x/apps/collection_editor_app.py +100 -0
  126. ui_10x/choice.py +212 -0
  127. ui_10x/collection_editor.py +135 -0
  128. ui_10x/concrete_trait_widgets.py +220 -0
  129. ui_10x/conftest.py +8 -0
  130. ui_10x/entity_stocker.py +173 -0
  131. ui_10x/examples/__init__.py +0 -0
  132. ui_10x/examples/_guess_word_data.py +14076 -0
  133. ui_10x/examples/collection_editor.py +17 -0
  134. ui_10x/examples/date_selector.py +14 -0
  135. ui_10x/examples/entity_stocker.py +18 -0
  136. ui_10x/examples/guess_word.py +392 -0
  137. ui_10x/examples/message_box.py +20 -0
  138. ui_10x/examples/multi_choice.py +17 -0
  139. ui_10x/examples/py_data_browser.py +66 -0
  140. ui_10x/examples/radiobox.py +29 -0
  141. ui_10x/examples/single_choice.py +31 -0
  142. ui_10x/examples/style_sheet.py +47 -0
  143. ui_10x/examples/trivial_entity_editor.py +18 -0
  144. ui_10x/platform.py +20 -0
  145. ui_10x/platform_interface.py +517 -0
  146. ui_10x/py_data_browser.py +249 -0
  147. ui_10x/qt6/__init__.py +0 -0
  148. ui_10x/qt6/conftest.py +8 -0
  149. ui_10x/qt6/manual_tests/__init__.py +0 -0
  150. ui_10x/qt6/manual_tests/basic_test.py +35 -0
  151. ui_10x/qt6/platform_implementation.py +275 -0
  152. ui_10x/qt6/utils.py +665 -0
  153. ui_10x/rio/__init__.py +0 -0
  154. ui_10x/rio/apps/examples/examples/__init__.py +22 -0
  155. ui_10x/rio/apps/examples/examples/components/__init__.py +3 -0
  156. ui_10x/rio/apps/examples/examples/components/collection_editor.py +15 -0
  157. ui_10x/rio/apps/examples/examples/pages/collection_editor.py +21 -0
  158. ui_10x/rio/apps/examples/examples/pages/login_page.py +88 -0
  159. ui_10x/rio/apps/examples/examples/pages/style_sheet.py +21 -0
  160. ui_10x/rio/apps/examples/rio.toml +14 -0
  161. ui_10x/rio/component_builder.py +497 -0
  162. ui_10x/rio/components/__init__.py +9 -0
  163. ui_10x/rio/components/group_box.py +31 -0
  164. ui_10x/rio/components/labeled_checkbox.py +18 -0
  165. ui_10x/rio/components/line_edit.py +37 -0
  166. ui_10x/rio/components/radio_button.py +32 -0
  167. ui_10x/rio/components/separator.py +24 -0
  168. ui_10x/rio/components/splitter.py +121 -0
  169. ui_10x/rio/components/tree_view.py +75 -0
  170. ui_10x/rio/conftest.py +35 -0
  171. ui_10x/rio/internals/__init__.py +0 -0
  172. ui_10x/rio/internals/app.py +192 -0
  173. ui_10x/rio/manual_tests/__init__.py +0 -0
  174. ui_10x/rio/manual_tests/basic_test.py +24 -0
  175. ui_10x/rio/manual_tests/splitter.py +27 -0
  176. ui_10x/rio/platform_implementation.py +91 -0
  177. ui_10x/rio/style_sheet.py +53 -0
  178. ui_10x/rio/unit_tests/test_collection_editor.py +68 -0
  179. ui_10x/rio/unit_tests/test_internals.py +630 -0
  180. ui_10x/rio/unit_tests/test_style_sheet.py +37 -0
  181. ui_10x/rio/widgets/__init__.py +46 -0
  182. ui_10x/rio/widgets/application.py +109 -0
  183. ui_10x/rio/widgets/button.py +48 -0
  184. ui_10x/rio/widgets/button_group.py +60 -0
  185. ui_10x/rio/widgets/calendar.py +23 -0
  186. ui_10x/rio/widgets/checkbox.py +24 -0
  187. ui_10x/rio/widgets/dialog.py +137 -0
  188. ui_10x/rio/widgets/group_box.py +27 -0
  189. ui_10x/rio/widgets/layout.py +34 -0
  190. ui_10x/rio/widgets/line_edit.py +37 -0
  191. ui_10x/rio/widgets/list.py +105 -0
  192. ui_10x/rio/widgets/message_box.py +70 -0
  193. ui_10x/rio/widgets/scroll_area.py +31 -0
  194. ui_10x/rio/widgets/spacer.py +6 -0
  195. ui_10x/rio/widgets/splitter.py +45 -0
  196. ui_10x/rio/widgets/text_edit.py +28 -0
  197. ui_10x/rio/widgets/tree.py +89 -0
  198. ui_10x/rio/widgets/unit_tests/test_button.py +101 -0
  199. ui_10x/rio/widgets/unit_tests/test_button_group.py +33 -0
  200. ui_10x/rio/widgets/unit_tests/test_calendar.py +114 -0
  201. ui_10x/rio/widgets/unit_tests/test_checkbox.py +109 -0
  202. ui_10x/rio/widgets/unit_tests/test_group_box.py +158 -0
  203. ui_10x/rio/widgets/unit_tests/test_label.py +43 -0
  204. ui_10x/rio/widgets/unit_tests/test_line_edit.py +140 -0
  205. ui_10x/rio/widgets/unit_tests/test_list.py +146 -0
  206. ui_10x/table_header_view.py +305 -0
  207. ui_10x/table_view.py +174 -0
  208. ui_10x/trait_editor.py +189 -0
  209. ui_10x/trait_widget.py +131 -0
  210. ui_10x/traitable_editor.py +200 -0
  211. ui_10x/traitable_view.py +131 -0
  212. ui_10x/unit_tests/conftest.py +8 -0
  213. ui_10x/unit_tests/test_platform.py +9 -0
  214. ui_10x/utils.py +661 -0
@@ -0,0 +1,471 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import date
4
+
5
+ import pytest
6
+ from core_10x.exec_control import CACHE_ONLY
7
+ from core_10x.xxcalendar import Calendar, CalendarAdjustment, CalendarNameParser
8
+
9
+
10
+ class TestCalendarNameParser:
11
+ """Unit tests for CalendarNameParser class."""
12
+
13
+ def test_combo_name(self):
14
+ """Test combo_name method."""
15
+ assert CalendarNameParser.combo_name('CAL1|CAL2') is True
16
+ assert CalendarNameParser.combo_name('CAL1&CAL2') is True
17
+ assert CalendarNameParser.combo_name('SIMPLE_CAL') is False
18
+
19
+ def test_operation_repr_with_calendar_objects(self):
20
+ """Test operation_repr with calendar objects."""
21
+
22
+ # Create mock calendar objects for testing
23
+ class MockCalendar:
24
+ def __init__(self, name):
25
+ self.name = name
26
+
27
+ cal1 = MockCalendar('CAL1')
28
+ cal2 = MockCalendar('CAL2')
29
+ cal3 = MockCalendar('CAL3')
30
+
31
+ result = CalendarNameParser.operation_repr(MockCalendar, CalendarNameParser.OR_CHAR, cal1, cal2, cal3)
32
+ # The result includes a trailing comma before the operation and a newline at the end
33
+ expected = 'CAL1,CAL2,CAL3,|3\n'
34
+ assert result == expected
35
+
36
+ result = CalendarNameParser.operation_repr(MockCalendar, CalendarNameParser.AND_CHAR, cal1, cal2)
37
+ expected = 'CAL1,CAL2,&2\n'
38
+ assert result == expected
39
+
40
+ def test_operation_repr_with_strings(self):
41
+ """Test operation_repr with string names."""
42
+ # This would require existing calendars in the database
43
+ # We'll test the assertion failures instead
44
+ pass
45
+
46
+ def test_operation_repr_invalid_op(self):
47
+ """Test operation_repr with invalid operation character."""
48
+
49
+ class MockCalendar:
50
+ def __init__(self, name):
51
+ self.name = name
52
+
53
+ cal1 = MockCalendar('CAL1')
54
+ cal2 = MockCalendar('CAL2')
55
+
56
+ with pytest.raises(AssertionError, match='Unknown op char = \\*'):
57
+ CalendarNameParser.operation_repr(MockCalendar, '*', cal1, cal2)
58
+
59
+ def test_operation_repr_too_few_calendars(self):
60
+ """Test operation_repr with too few calendars."""
61
+
62
+ class MockCalendar:
63
+ def __init__(self, name):
64
+ self.name = name
65
+
66
+ cal1 = MockCalendar('CAL1')
67
+
68
+ with pytest.raises(AssertionError, match='there must be at least 2 calendars'):
69
+ CalendarNameParser.operation_repr(MockCalendar, CalendarNameParser.OR_CHAR, cal1)
70
+
71
+ def test_parse_simple_calendar(self, ts_instance):
72
+ """Test parse method with simple calendar name."""
73
+ from datetime import date
74
+
75
+ with ts_instance:
76
+ holidays = {date(2024, 1, 1), date(2024, 12, 25)}
77
+ cal = Calendar(
78
+ name='TEST_SIMPLE_CAL',
79
+ description='Simple calendar for parsing',
80
+ non_working_days=sorted(holidays),
81
+ _replace=True,
82
+ )
83
+ assert cal.save()
84
+
85
+ parsed_days = CalendarNameParser.parse(Calendar, cal.name)
86
+ assert parsed_days == holidays
87
+
88
+ def test_parse_combined_calendar(self, ts_instance):
89
+ """Test parse method with combined calendar operations."""
90
+ from datetime import date
91
+
92
+ with ts_instance:
93
+ holidays_a = {date(2024, 1, 1), date(2024, 1, 2)}
94
+ holidays_b = {date(2024, 1, 2), date(2024, 1, 3)}
95
+
96
+ cal_a = Calendar(
97
+ name='TEST_CAL_A',
98
+ description='Calendar A for combined parsing',
99
+ non_working_days=sorted(holidays_a),
100
+ _replace=True,
101
+ )
102
+ cal_b = Calendar(
103
+ name='TEST_CAL_B',
104
+ description='Calendar B for combined parsing',
105
+ non_working_days=sorted(holidays_b),
106
+ _replace=True,
107
+ )
108
+
109
+ assert cal_a.save()
110
+ assert cal_b.save()
111
+
112
+ # OR operation: union of non-working days
113
+ union_name = CalendarNameParser.operation_repr(Calendar, CalendarNameParser.OR_CHAR, cal_a, cal_b)
114
+ union_days = CalendarNameParser.parse(Calendar, union_name)
115
+ assert union_days == holidays_a | holidays_b
116
+
117
+ # AND operation: intersection of non-working days
118
+ intersection_name = CalendarNameParser.operation_repr(Calendar, CalendarNameParser.AND_CHAR, cal_a, cal_b)
119
+ intersection_days = CalendarNameParser.parse(Calendar, intersection_name)
120
+ assert intersection_days == holidays_a & holidays_b
121
+
122
+
123
+ class TestCalendarAdjustment:
124
+ """Unit tests for CalendarAdjustment class."""
125
+
126
+ def test_creation(self):
127
+ """Test CalendarAdjustment can be created."""
128
+ with CACHE_ONLY():
129
+ # CalendarAdjustment requires name (which has T.ID flag)
130
+ ca = CalendarAdjustment(name='TEST_ADJ', _replace=True)
131
+ assert ca.name == 'TEST_ADJ'
132
+
133
+ def test_calendar_adjustment_is_storable(self):
134
+ """Test that CalendarAdjustment is storable."""
135
+ assert CalendarAdjustment.is_storable()
136
+ assert CalendarAdjustment.trait('name') # Has ID trait
137
+
138
+ def test_calendar_adjustment_with_adjusted_for(self, ts_instance):
139
+ """Test Calendar with adjusted_for parameter applies CalendarAdjustment."""
140
+ with ts_instance:
141
+ # Create base calendar
142
+ base_cal = Calendar(
143
+ _replace=True,
144
+ name='BASE',
145
+ description='Base calendar',
146
+ non_working_days=[date(2025, 1, 1), date(2025, 12, 25)],
147
+ )
148
+ base_cal.save()
149
+
150
+ # Create adjustment that adds and removes days
151
+ adj = CalendarAdjustment(
152
+ _replace=True,
153
+ name='TEST_ADJ',
154
+ add_days=[date(2025, 1, 15)],
155
+ remove_days=[date(2025, 1, 1)],
156
+ )
157
+ adj.save()
158
+
159
+ # Create adjusted calendar
160
+ adjusted_cal = Calendar(
161
+ _replace=True,
162
+ name='BASE',
163
+ adjusted_for='TEST_ADJ',
164
+ )
165
+ adjusted_cal.save()
166
+
167
+ # Adjusted calendar should have base days + add_days - remove_days
168
+ nwds = set(adjusted_cal.non_working_days)
169
+ assert date(2025, 1, 1) not in nwds # removed
170
+ assert date(2025, 1, 15) in nwds # added
171
+ assert date(2025, 12, 25) in nwds # kept from base
172
+
173
+
174
+ class TestCalendar:
175
+ """Unit tests for Calendar class."""
176
+
177
+ def setup_method(self):
178
+ """Set up test fixtures."""
179
+ # Create test calendar with some holidays
180
+ self.test_holidays = [
181
+ date(2023, 1, 1), # New Year's Day
182
+ date(2023, 12, 25), # Christmas
183
+ date(2023, 7, 4), # Independence Day
184
+ ]
185
+
186
+ def test_add_days(self):
187
+ """Test add_days class method."""
188
+ days = {date(2023, 1, 1), date(2023, 1, 2)}
189
+
190
+ # Add new days
191
+ result = Calendar.add_days(days, date(2023, 1, 3), date(2023, 1, 4))
192
+ assert result is True
193
+ assert days == {date(2023, 1, 1), date(2023, 1, 2), date(2023, 1, 3), date(2023, 1, 4)}
194
+
195
+ # Add existing day (no change)
196
+ result = Calendar.add_days(days, date(2023, 1, 1))
197
+ assert result is False
198
+
199
+ # Add no days
200
+ result = Calendar.add_days(days)
201
+ assert result is False
202
+
203
+ def test_add_days_invalid_type(self):
204
+ """Test add_days with invalid types raises TypeError."""
205
+ days = set()
206
+ with pytest.raises(TypeError, match='Every day to add must be a date'):
207
+ Calendar.add_days(days, '2023-01-01')
208
+
209
+ def test_remove_days(self):
210
+ """Test remove_days class method."""
211
+ days = {date(2023, 1, 1), date(2023, 1, 2), date(2023, 1, 3)}
212
+
213
+ # Remove existing days
214
+ result = Calendar.remove_days(days, date(2023, 1, 1), date(2023, 1, 2))
215
+ assert result is True
216
+ assert days == {date(2023, 1, 3)}
217
+
218
+ # Remove non-existing day (no change)
219
+ result = Calendar.remove_days(days, date(2023, 1, 4))
220
+ assert result is False
221
+
222
+ # Remove no days
223
+ result = Calendar.remove_days(days)
224
+ assert result is False
225
+
226
+ def test_remove_days_invalid_type(self):
227
+ """Test remove_days with invalid types raises TypeError."""
228
+ days = {date(2023, 1, 1)}
229
+ with pytest.raises(TypeError, match='Every day to remove must be a date'):
230
+ Calendar.remove_days(days, '2023-01-01')
231
+
232
+ def test_is_bizday(self):
233
+ """Test is_bizday method using proper Traitable calendar."""
234
+ with CACHE_ONLY():
235
+ # Create a proper Calendar instance with non-working days
236
+ cal = Calendar(
237
+ name='TEST_CAL_HOLIDAYS', description='Calendar with holidays', non_working_days=[date(2023, 1, 1), date(2023, 12, 25)], _replace=True
238
+ )
239
+
240
+ # Test business days
241
+ assert cal.is_bizday(date(2023, 1, 2)) is True
242
+ assert cal.is_bizday(date(2023, 2, 1)) is True
243
+
244
+ # Test non-business days
245
+ assert cal.is_bizday(date(2023, 1, 1)) is False
246
+ assert cal.is_bizday(date(2023, 12, 25)) is False
247
+
248
+ def test_next_bizday(self):
249
+ """Test next_bizday method using proper Traitable calendar."""
250
+ with CACHE_ONLY():
251
+ # Create a calendar with consecutive holidays
252
+ cal = Calendar(
253
+ name='TEST_CAL_CONSECUTIVE',
254
+ description='Calendar with consecutive holidays',
255
+ non_working_days=[
256
+ date(2023, 1, 1), # Sunday
257
+ date(2023, 1, 2), # Monday
258
+ date(2023, 1, 3), # Tuesday
259
+ ],
260
+ _replace=True,
261
+ )
262
+
263
+ # Test from a business day
264
+ result = cal.next_bizday(date(2023, 1, 4)) # Wednesday
265
+ assert result == date(2023, 1, 5) # Thursday
266
+
267
+ # Test from a non-business day
268
+ result = cal.next_bizday(date(2023, 1, 1)) # Sunday (holiday)
269
+ assert result == date(2023, 1, 4) # Wednesday
270
+
271
+ # Test from a non-business day in middle of holidays
272
+ result = cal.next_bizday(date(2023, 1, 2)) # Monday (holiday)
273
+ assert result == date(2023, 1, 4) # Wednesday
274
+
275
+ def test_prev_bizday(self):
276
+ """Test prev_bizday method using proper Traitable calendar."""
277
+ with CACHE_ONLY():
278
+ # Create a calendar with consecutive holidays
279
+ cal = Calendar(
280
+ name='TEST_CAL_PREV',
281
+ description='Calendar for prev_bizday test',
282
+ non_working_days=[
283
+ date(2023, 1, 1), # Sunday
284
+ date(2023, 1, 2), # Monday
285
+ date(2023, 1, 3), # Tuesday
286
+ ],
287
+ _replace=True,
288
+ )
289
+
290
+ # Test from a business day
291
+ result = cal.prev_bizday(date(2023, 1, 4)) # Wednesday
292
+ assert result == date(2022, 12, 31) # Saturday (goes back one day from Wednesday)
293
+
294
+ # Test from a non-business day
295
+ result = cal.prev_bizday(date(2023, 1, 1)) # Sunday (holiday)
296
+ assert result == date(2022, 12, 31) # Saturday
297
+
298
+ def test_advance_bizdays_forward(self):
299
+ """Test advance_bizdays with positive count using proper Traitable calendar."""
300
+ with CACHE_ONLY():
301
+ # Create a calendar with weekends and holidays
302
+ cal = Calendar(
303
+ name='TEST_CAL_ADVANCE',
304
+ description='Calendar for advance_bizdays test',
305
+ non_working_days=[
306
+ date(2023, 1, 1), # New Year
307
+ date(2023, 1, 7), # Saturday
308
+ date(2023, 1, 8), # Sunday
309
+ date(2023, 1, 14), # Saturday
310
+ date(2023, 1, 15), # Sunday
311
+ ],
312
+ _replace=True,
313
+ )
314
+
315
+ # Start from Friday Jan 6, 2023
316
+ start_date = date(2023, 1, 6) # Friday
317
+
318
+ # Advance 1 business day (skip weekend)
319
+ result = cal.advance_bizdays(start_date, 1)
320
+ assert result == date(2023, 1, 9) # Monday
321
+
322
+ # Advance 3 business days
323
+ result = cal.advance_bizdays(start_date, 3)
324
+ assert result == date(2023, 1, 11) # Wednesday
325
+
326
+ # Advance 0 business days (no change)
327
+ result = cal.advance_bizdays(start_date, 0)
328
+ assert result == start_date
329
+
330
+ def test_advance_bizdays_backward(self):
331
+ """Test advance_bizdays with negative count using proper Traitable calendar."""
332
+ with CACHE_ONLY():
333
+ # Create a calendar with weekends and holidays (same as forward test)
334
+ cal = Calendar(
335
+ name='TEST_CAL_BACKWARD',
336
+ description='Calendar for backward advance_bizdays test',
337
+ non_working_days=[
338
+ date(2023, 1, 1), # New Year
339
+ date(2023, 1, 7), # Saturday
340
+ date(2023, 1, 8), # Sunday
341
+ date(2023, 1, 14), # Saturday
342
+ date(2023, 1, 15), # Sunday
343
+ ],
344
+ _replace=True,
345
+ )
346
+
347
+ # Start from Wednesday Jan 11, 2023
348
+ start_date = date(2023, 1, 11) # Wednesday
349
+
350
+ # Go back 1 business day (skip weekend)
351
+ result = cal.advance_bizdays(start_date, -1)
352
+ assert result == date(2023, 1, 10) # Tuesday
353
+
354
+ # Go back 3 business days
355
+ result = cal.advance_bizdays(start_date, -3)
356
+ assert result == date(2023, 1, 6) # Friday
357
+
358
+ def test_and(self):
359
+ """Test AND class method."""
360
+ # This would require actual calendar instances
361
+ # Test the None case
362
+ result = Calendar.AND()
363
+ assert result is None
364
+
365
+ def test_or(self):
366
+ """Test OR class method."""
367
+ # This would require actual calendar instances
368
+ # Test the None case
369
+ result = Calendar.OR()
370
+ assert result is None
371
+
372
+ def test_union(self):
373
+ """Test union class method."""
374
+ # Test with no calendars
375
+ with pytest.raises(AssertionError, match='At least one calendar is required for union'):
376
+ Calendar.union()
377
+
378
+ def test_calendar_union_and_intersection_real_calendars(self, ts_instance):
379
+ """Test Calendar.union and Calendar.intersection on real stored calendars."""
380
+ with ts_instance:
381
+ us_holidays = {
382
+ date(2025, 1, 1),
383
+ date(2025, 1, 20),
384
+ date(2025, 2, 17),
385
+ date(2025, 5, 26),
386
+ date(2025, 7, 4),
387
+ date(2025, 9, 1),
388
+ date(2025, 10, 13),
389
+ date(2025, 11, 11),
390
+ date(2025, 11, 27),
391
+ date(2025, 12, 25),
392
+ }
393
+ uk_holidays = {
394
+ date(2025, 1, 1),
395
+ date(2025, 4, 18),
396
+ date(2025, 5, 5),
397
+ date(2025, 5, 26),
398
+ date(2025, 8, 25),
399
+ date(2025, 12, 25),
400
+ date(2025, 12, 26),
401
+ }
402
+
403
+ us_c = Calendar(
404
+ _replace=True,
405
+ name='US',
406
+ description='US calendar',
407
+ non_working_days=sorted(us_holidays),
408
+ )
409
+ uk_c = Calendar(
410
+ _replace=True,
411
+ name='UK',
412
+ description='UK calendar',
413
+ non_working_days=sorted(uk_holidays),
414
+ )
415
+
416
+ assert us_c.save()
417
+ assert uk_c.save()
418
+
419
+ union_cal = Calendar.union('US', 'UK')
420
+ assert isinstance(union_cal, Calendar)
421
+ assert set(union_cal.non_working_days) == us_holidays | uk_holidays
422
+
423
+ intersection_cal = Calendar.intersection('US', 'UK')
424
+ assert isinstance(intersection_cal, Calendar)
425
+ assert set(intersection_cal.non_working_days) == us_holidays & uk_holidays
426
+
427
+ def test_intersection_alias(self):
428
+ """Test that intersection is an alias for AND."""
429
+ assert Calendar.intersection == Calendar.AND
430
+
431
+ def test_add_non_working_days(self):
432
+ """Test add_non_working_days instance method."""
433
+ # Test the class method that add_non_working_days would use
434
+ days = {date(2023, 1, 1)}
435
+ Calendar.add_days(days, date(2023, 12, 25), date(2023, 7, 4))
436
+ assert days == {date(2023, 1, 1), date(2023, 12, 25), date(2023, 7, 4)}
437
+
438
+ def test_remove_non_working_days(self):
439
+ """Test remove_non_working_days instance method."""
440
+ # Test the class method that remove_non_working_days would use
441
+ days = {date(2023, 1, 1), date(2023, 12, 25), date(2023, 7, 4)}
442
+ Calendar.remove_days(days, date(2023, 12, 25))
443
+ assert days == {date(2023, 1, 1), date(2023, 7, 4)}
444
+
445
+ def test_non_working_days_get(self):
446
+ """Test non_working_days_get method."""
447
+ # This method requires database access, so we'll skip detailed testing
448
+ # The method parses calendar names and gets holidays from database
449
+ pass
450
+
451
+ def test_non_working_days_trait_get(self):
452
+ """Test _non_working_days_get trait method."""
453
+ # Test the logic that _non_working_days_get would use
454
+ non_working_days = [date(2023, 1, 1), date(2023, 12, 25)]
455
+ result = set(non_working_days)
456
+ assert result == {date(2023, 1, 1), date(2023, 12, 25)}
457
+
458
+ def test_calendar_instantiation(self):
459
+ """Test proper Calendar instantiation following Traitable patterns."""
460
+ # Calendar is storable, so it needs proper trait values like other storable classes
461
+ with CACHE_ONLY():
462
+ # Calendar requires name (which has T.ID flag)
463
+ cal = Calendar(name='TEST_CAL', description='Test Calendar', _replace=True)
464
+ assert cal.name == 'TEST_CAL'
465
+ assert cal.description == 'Test Calendar'
466
+
467
+ def test_calendar_is_storable(self):
468
+ """Test that Calendar is storable like other Traitable subclasses."""
469
+ assert Calendar.is_storable()
470
+ assert Calendar.trait('name') # Has ID trait
471
+ assert Calendar.trait('_collection_name') # Has collection name trait
File without changes
@@ -0,0 +1,133 @@
1
+ import secrets
2
+
3
+ from cryptography.hazmat.backends import default_backend
4
+ from cryptography.hazmat.primitives import hashes, serialization
5
+ from cryptography.hazmat.primitives.asymmetric import padding, rsa
6
+ from cryptography.hazmat.primitives.serialization import load_pem_private_key, load_pem_public_key
7
+
8
+ # fmt: off
9
+ PUBLIC_EXP = 65537
10
+ KEY_SIZE = 2048
11
+ PWD_SIZE = 24
12
+ ENCODING = 'utf-8'
13
+ # fmt: on
14
+
15
+
16
+ class SecKeys:
17
+ @classmethod
18
+ def generate_password(cls, length=PWD_SIZE) -> str:
19
+ return secrets.token_urlsafe(length)
20
+
21
+ @classmethod
22
+ def generate_keys(cls, pwd=None) -> tuple:
23
+ private_key = rsa.generate_private_key(public_exponent=PUBLIC_EXP, key_size=KEY_SIZE, backend=default_backend())
24
+ public_key = private_key.public_key()
25
+
26
+ if pwd:
27
+ format = serialization.PrivateFormat.PKCS8
28
+ algo = serialization.BestAvailableEncryption(bytes(pwd, encoding=ENCODING))
29
+ else:
30
+ format = serialization.PrivateFormat.TraditionalOpenSSL
31
+ algo = serialization.NoEncryption()
32
+
33
+ private_key_pem = private_key.private_bytes(encoding=serialization.Encoding.PEM, format=format, encryption_algorithm=algo)
34
+ public_key_pem = public_key.public_bytes(encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo)
35
+
36
+ return (private_key_pem, public_key_pem)
37
+
38
+ @classmethod
39
+ def encrypt(cls, message, public_key_pem: bytes) -> bytes:
40
+ if type(message) is str:
41
+ message = bytes(message, encoding=ENCODING)
42
+
43
+ public_key = load_pem_public_key(public_key_pem)
44
+ # fmt: off
45
+ return public_key.encrypt(
46
+ message,
47
+ padding.OAEP(
48
+ mgf = padding.MGF1(algorithm = hashes.SHA256()),
49
+ algorithm = hashes.SHA256(),
50
+ label = None
51
+ )
52
+ )
53
+ # fmt: on
54
+
55
+ @classmethod
56
+ def decrypt(cls, encrypted_message: bytes, private_key_pem: bytes, to_str=True):
57
+ private_key = load_pem_private_key(private_key_pem, password=None)
58
+ # fmt: off
59
+ res = private_key.decrypt(
60
+ encrypted_message,
61
+ padding.OAEP(
62
+ mgf = padding.MGF1(algorithm = hashes.SHA256()),
63
+ algorithm = hashes.SHA256(),
64
+ label = None
65
+ )
66
+ )
67
+ # fmt: on
68
+
69
+ if to_str:
70
+ res = res.decode(encoding=ENCODING)
71
+
72
+ return res
73
+
74
+ @classmethod
75
+ def encrypt_private_key(cls, private_key_pem: bytes, password) -> bytes:
76
+ if type(password) is str:
77
+ password = bytes(password, encoding=ENCODING)
78
+
79
+ private_key = load_pem_private_key(private_key_pem, password=None)
80
+ # fmt: off
81
+ return private_key.private_bytes(
82
+ encoding = serialization.Encoding.PEM,
83
+ format = serialization.PrivateFormat.PKCS8,
84
+ encryption_algorithm = serialization.BestAvailableEncryption(password),
85
+ )
86
+ # fmt: on
87
+
88
+ @classmethod
89
+ def decrypt_private_key(cls, private_key_with_password, password) -> bytes:
90
+ if type(password) is str:
91
+ password = bytes(password, encoding=ENCODING)
92
+
93
+ pk = load_pem_private_key(private_key_with_password, password=password)
94
+ # fmt: off
95
+ return pk.private_bytes(
96
+ encoding = serialization.Encoding.PEM,
97
+ format = serialization.PrivateFormat.TraditionalOpenSSL,
98
+ encryption_algorithm = serialization.NoEncryption(),
99
+ )
100
+ # fmt: on
101
+
102
+ def __init__(self, private_key_with_password: bytes, public_key_pem: bytes, password):
103
+ if type(password) is str:
104
+ password = bytes(password, encoding=ENCODING)
105
+
106
+ self.private_key = load_pem_private_key(private_key_with_password, password=password)
107
+ self.public_key = load_pem_public_key(public_key_pem)
108
+
109
+ def encrypt_text(self, text: str) -> bytes:
110
+ message = bytes(text, encoding=ENCODING)
111
+ # fmt: off
112
+ return self.public_key.encrypt(
113
+ message,
114
+ padding.OAEP(
115
+ mgf = padding.MGF1(algorithm = hashes.SHA256()),
116
+ algorithm = hashes.SHA256(),
117
+ label = None
118
+ )
119
+ )
120
+
121
+ def decrypt_text(self, encrypted_message: bytes) -> str:
122
+ # fmt: off
123
+ res = self.private_key.decrypt(
124
+ encrypted_message,
125
+ padding.OAEP(
126
+ mgf = padding.MGF1(algorithm = hashes.SHA256()),
127
+ algorithm = hashes.SHA256(),
128
+ label = None
129
+ )
130
+ )
131
+ # fmt: on
132
+
133
+ return res.decode(encoding=ENCODING)