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.
- core_10x/__init__.py +42 -0
- core_10x/backbone/__init__.py +0 -0
- core_10x/backbone/backbone_store.py +59 -0
- core_10x/backbone/backbone_traitable.py +30 -0
- core_10x/backbone/backbone_user.py +66 -0
- core_10x/backbone/bound_data_domain.py +49 -0
- core_10x/backbone/namespace.py +101 -0
- core_10x/backbone/vault.py +38 -0
- core_10x/code_samples/__init__.py +0 -0
- core_10x/code_samples/_package_manifest.py +3 -0
- core_10x/code_samples/directories.py +181 -0
- core_10x/code_samples/person.py +76 -0
- core_10x/concrete_traits.py +356 -0
- core_10x/conftest.py +12 -0
- core_10x/curve.py +321 -0
- core_10x/data_domain.py +48 -0
- core_10x/data_domain_binder.py +45 -0
- core_10x/directory.py +250 -0
- core_10x/entity.py +8 -0
- core_10x/entity_filter.py +5 -0
- core_10x/environment_variables.py +147 -0
- core_10x/exec_control.py +84 -0
- core_10x/experimental/__init__.py +0 -0
- core_10x/experimental/data_protocol_ex.py +34 -0
- core_10x/global_cache.py +121 -0
- core_10x/manual_tests/__init__.py +0 -0
- core_10x/manual_tests/calendar_test.py +35 -0
- core_10x/manual_tests/ctor_update_bug.py +58 -0
- core_10x/manual_tests/debug_graph_on.py +17 -0
- core_10x/manual_tests/debug_graphoff_inside_graph_on.py +28 -0
- core_10x/manual_tests/enum_bits_test.py +17 -0
- core_10x/manual_tests/env_vars_trivial_test.py +12 -0
- core_10x/manual_tests/existing_traitable.py +33 -0
- core_10x/manual_tests/k10x_test1.py +13 -0
- core_10x/manual_tests/named_constant_test.py +121 -0
- core_10x/manual_tests/nucleus_trivial_test.py +42 -0
- core_10x/manual_tests/polars_test.py +14 -0
- core_10x/manual_tests/py_class_test.py +4 -0
- core_10x/manual_tests/rc_test.py +42 -0
- core_10x/manual_tests/rdate_test.py +12 -0
- core_10x/manual_tests/reference_serialization_bug.py +19 -0
- core_10x/manual_tests/resource_trivial_test.py +10 -0
- core_10x/manual_tests/store_uri_test.py +6 -0
- core_10x/manual_tests/trait_definition_test.py +19 -0
- core_10x/manual_tests/trait_filter_test.py +15 -0
- core_10x/manual_tests/trait_flag_modification_test.py +42 -0
- core_10x/manual_tests/trait_modification_bug.py +26 -0
- core_10x/manual_tests/traitable_as_of_test.py +82 -0
- core_10x/manual_tests/traitable_heir_test.py +39 -0
- core_10x/manual_tests/traitable_history_test.py +41 -0
- core_10x/manual_tests/traitable_serialization_test.py +54 -0
- core_10x/manual_tests/traitable_trivial_test.py +71 -0
- core_10x/manual_tests/trivial_graph_test.py +16 -0
- core_10x/manual_tests/ts_class_association_test.py +64 -0
- core_10x/manual_tests/ts_trivial_test.py +35 -0
- core_10x/named_constant.py +425 -0
- core_10x/nucleus.py +81 -0
- core_10x/package_manifest.py +85 -0
- core_10x/package_refactoring.py +153 -0
- core_10x/py_class.py +431 -0
- core_10x/rc.py +155 -0
- core_10x/rdate.py +339 -0
- core_10x/resource.py +189 -0
- core_10x/roman_number.py +67 -0
- core_10x/testlib/__init__.py +0 -0
- core_10x/testlib/test_store.py +240 -0
- core_10x/testlib/traitable_history_tests.py +787 -0
- core_10x/testlib/ts_tests.py +280 -0
- core_10x/trait.py +377 -0
- core_10x/trait_definition.py +176 -0
- core_10x/trait_filter.py +205 -0
- core_10x/trait_method_error.py +36 -0
- core_10x/traitable.py +1082 -0
- core_10x/traitable_cli.py +153 -0
- core_10x/traitable_heir.py +33 -0
- core_10x/traitable_id.py +31 -0
- core_10x/ts_store.py +172 -0
- core_10x/ts_store_type.py +26 -0
- core_10x/ts_union.py +147 -0
- core_10x/ui_hint.py +153 -0
- core_10x/unit_tests/test_concrete_traits.py +156 -0
- core_10x/unit_tests/test_converters.py +51 -0
- core_10x/unit_tests/test_curve.py +157 -0
- core_10x/unit_tests/test_directory.py +54 -0
- core_10x/unit_tests/test_documentation.py +172 -0
- core_10x/unit_tests/test_environment_variables.py +15 -0
- core_10x/unit_tests/test_filters.py +239 -0
- core_10x/unit_tests/test_graph.py +348 -0
- core_10x/unit_tests/test_named_constant.py +98 -0
- core_10x/unit_tests/test_rc.py +11 -0
- core_10x/unit_tests/test_rdate.py +484 -0
- core_10x/unit_tests/test_trait_method_error.py +80 -0
- core_10x/unit_tests/test_trait_modification.py +19 -0
- core_10x/unit_tests/test_traitable.py +959 -0
- core_10x/unit_tests/test_traitable_history.py +1 -0
- core_10x/unit_tests/test_ts_store.py +1 -0
- core_10x/unit_tests/test_ts_union.py +369 -0
- core_10x/unit_tests/test_ui_nodes.py +81 -0
- core_10x/unit_tests/test_xxcalendar.py +471 -0
- core_10x/vault/__init__.py +0 -0
- core_10x/vault/sec_keys.py +133 -0
- core_10x/vault/security_keys_old.py +168 -0
- core_10x/vault/vault.py +56 -0
- core_10x/vault/vault_traitable.py +56 -0
- core_10x/vault/vault_user.py +70 -0
- core_10x/xdate_time.py +136 -0
- core_10x/xnone.py +71 -0
- core_10x/xxcalendar.py +228 -0
- infra_10x/__init__.py +0 -0
- infra_10x/manual_tests/__init__.py +0 -0
- infra_10x/manual_tests/test_misc.py +16 -0
- infra_10x/manual_tests/test_prepare_filter_and_pipeline.py +25 -0
- infra_10x/mongodb_admin.py +111 -0
- infra_10x/mongodb_store.py +346 -0
- infra_10x/mongodb_utils.py +129 -0
- infra_10x/unit_tests/conftest.py +13 -0
- infra_10x/unit_tests/test_mongo_db.py +36 -0
- infra_10x/unit_tests/test_mongo_history.py +1 -0
- py10x_universe-0.1.3.dist-info/METADATA +406 -0
- py10x_universe-0.1.3.dist-info/RECORD +214 -0
- py10x_universe-0.1.3.dist-info/WHEEL +4 -0
- py10x_universe-0.1.3.dist-info/licenses/LICENSE +21 -0
- ui_10x/__init__.py +0 -0
- ui_10x/apps/__init__.py +0 -0
- ui_10x/apps/collection_editor_app.py +100 -0
- ui_10x/choice.py +212 -0
- ui_10x/collection_editor.py +135 -0
- ui_10x/concrete_trait_widgets.py +220 -0
- ui_10x/conftest.py +8 -0
- ui_10x/entity_stocker.py +173 -0
- ui_10x/examples/__init__.py +0 -0
- ui_10x/examples/_guess_word_data.py +14076 -0
- ui_10x/examples/collection_editor.py +17 -0
- ui_10x/examples/date_selector.py +14 -0
- ui_10x/examples/entity_stocker.py +18 -0
- ui_10x/examples/guess_word.py +392 -0
- ui_10x/examples/message_box.py +20 -0
- ui_10x/examples/multi_choice.py +17 -0
- ui_10x/examples/py_data_browser.py +66 -0
- ui_10x/examples/radiobox.py +29 -0
- ui_10x/examples/single_choice.py +31 -0
- ui_10x/examples/style_sheet.py +47 -0
- ui_10x/examples/trivial_entity_editor.py +18 -0
- ui_10x/platform.py +20 -0
- ui_10x/platform_interface.py +517 -0
- ui_10x/py_data_browser.py +249 -0
- ui_10x/qt6/__init__.py +0 -0
- ui_10x/qt6/conftest.py +8 -0
- ui_10x/qt6/manual_tests/__init__.py +0 -0
- ui_10x/qt6/manual_tests/basic_test.py +35 -0
- ui_10x/qt6/platform_implementation.py +275 -0
- ui_10x/qt6/utils.py +665 -0
- ui_10x/rio/__init__.py +0 -0
- ui_10x/rio/apps/examples/examples/__init__.py +22 -0
- ui_10x/rio/apps/examples/examples/components/__init__.py +3 -0
- ui_10x/rio/apps/examples/examples/components/collection_editor.py +15 -0
- ui_10x/rio/apps/examples/examples/pages/collection_editor.py +21 -0
- ui_10x/rio/apps/examples/examples/pages/login_page.py +88 -0
- ui_10x/rio/apps/examples/examples/pages/style_sheet.py +21 -0
- ui_10x/rio/apps/examples/rio.toml +14 -0
- ui_10x/rio/component_builder.py +497 -0
- ui_10x/rio/components/__init__.py +9 -0
- ui_10x/rio/components/group_box.py +31 -0
- ui_10x/rio/components/labeled_checkbox.py +18 -0
- ui_10x/rio/components/line_edit.py +37 -0
- ui_10x/rio/components/radio_button.py +32 -0
- ui_10x/rio/components/separator.py +24 -0
- ui_10x/rio/components/splitter.py +121 -0
- ui_10x/rio/components/tree_view.py +75 -0
- ui_10x/rio/conftest.py +35 -0
- ui_10x/rio/internals/__init__.py +0 -0
- ui_10x/rio/internals/app.py +192 -0
- ui_10x/rio/manual_tests/__init__.py +0 -0
- ui_10x/rio/manual_tests/basic_test.py +24 -0
- ui_10x/rio/manual_tests/splitter.py +27 -0
- ui_10x/rio/platform_implementation.py +91 -0
- ui_10x/rio/style_sheet.py +53 -0
- ui_10x/rio/unit_tests/test_collection_editor.py +68 -0
- ui_10x/rio/unit_tests/test_internals.py +630 -0
- ui_10x/rio/unit_tests/test_style_sheet.py +37 -0
- ui_10x/rio/widgets/__init__.py +46 -0
- ui_10x/rio/widgets/application.py +109 -0
- ui_10x/rio/widgets/button.py +48 -0
- ui_10x/rio/widgets/button_group.py +60 -0
- ui_10x/rio/widgets/calendar.py +23 -0
- ui_10x/rio/widgets/checkbox.py +24 -0
- ui_10x/rio/widgets/dialog.py +137 -0
- ui_10x/rio/widgets/group_box.py +27 -0
- ui_10x/rio/widgets/layout.py +34 -0
- ui_10x/rio/widgets/line_edit.py +37 -0
- ui_10x/rio/widgets/list.py +105 -0
- ui_10x/rio/widgets/message_box.py +70 -0
- ui_10x/rio/widgets/scroll_area.py +31 -0
- ui_10x/rio/widgets/spacer.py +6 -0
- ui_10x/rio/widgets/splitter.py +45 -0
- ui_10x/rio/widgets/text_edit.py +28 -0
- ui_10x/rio/widgets/tree.py +89 -0
- ui_10x/rio/widgets/unit_tests/test_button.py +101 -0
- ui_10x/rio/widgets/unit_tests/test_button_group.py +33 -0
- ui_10x/rio/widgets/unit_tests/test_calendar.py +114 -0
- ui_10x/rio/widgets/unit_tests/test_checkbox.py +109 -0
- ui_10x/rio/widgets/unit_tests/test_group_box.py +158 -0
- ui_10x/rio/widgets/unit_tests/test_label.py +43 -0
- ui_10x/rio/widgets/unit_tests/test_line_edit.py +140 -0
- ui_10x/rio/widgets/unit_tests/test_list.py +146 -0
- ui_10x/table_header_view.py +305 -0
- ui_10x/table_view.py +174 -0
- ui_10x/trait_editor.py +189 -0
- ui_10x/trait_widget.py +131 -0
- ui_10x/traitable_editor.py +200 -0
- ui_10x/traitable_view.py +131 -0
- ui_10x/unit_tests/conftest.py +8 -0
- ui_10x/unit_tests/test_platform.py +9 -0
- 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)
|