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,787 @@
1
+ """Tests for TraitableHistory functionality using pytest and real TestStore."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import uuid
6
+ from datetime import date, datetime
7
+
8
+ import pytest
9
+ from py10x_core import BTraitableProcessor
10
+ from typing_extensions import Self
11
+
12
+ from core_10x.exec_control import CACHE_ONLY, GRAPH_OFF, GRAPH_ON, INTERACTIVE
13
+ from core_10x.py_class import PyClass
14
+ from core_10x.rc import RC, RC_TRUE
15
+ from core_10x.traitable import AsOfContext, T, Traitable, TraitableHistory
16
+ from core_10x.traitable_id import ID
17
+ from core_10x.ts_store import TsDuplicateKeyError
18
+
19
+ try:
20
+ from infra_10x.mongodb_store import MongoStore
21
+ except ImportError:
22
+ MongoStore = None
23
+
24
+
25
+ class NameValueTraitableBase(Traitable):
26
+ """Test traitable class for testing."""
27
+
28
+ name: str = T()
29
+ value: int = T()
30
+
31
+
32
+ class NameValueTraitableCustomCollection(NameValueTraitableBase):
33
+ s_custom_collection = True
34
+
35
+
36
+ class PersonTraitableBase(Traitable):
37
+ """Test person class for testing."""
38
+
39
+ name: str = T()
40
+ age: int = T()
41
+ email: str = T()
42
+ dob: date = T()
43
+ spouse: Self = T()
44
+
45
+ def spouse_set(self, trait, spouse) -> RC:
46
+ self.raw_set_value(trait, spouse)
47
+ if spouse:
48
+ spouse.raw_set_value(trait, self)
49
+ return RC_TRUE
50
+
51
+
52
+ NameValueTraitable = type(f'PersonTraitable#{uuid.uuid1().hex}', (NameValueTraitableBase,), {'__module__': __name__})
53
+ PersonTraitable = type(f'PersonTraitable#{uuid.uuid1().hex}', (PersonTraitableBase,), {'__module__': __name__})
54
+
55
+ globals()[NameValueTraitable.__name__] = NameValueTraitable
56
+ globals()[PersonTraitable.__name__] = PersonTraitable
57
+
58
+
59
+ @pytest.fixture
60
+ def test_collection(test_store):
61
+ """Create a test collection for testing."""
62
+ collection_name = f'test_collection_{uuid.uuid1()}'
63
+ yield test_store.collection(collection_name=collection_name)
64
+ test_store.delete_collection(collection_name=collection_name)
65
+ test_store.delete_collection(collection_name=f'{collection_name}#history')
66
+
67
+
68
+ @pytest.fixture
69
+ def test_store(ts_instance):
70
+ store = ts_instance
71
+ store.username = 'test_user'
72
+ store.begin_using()
73
+ yield store
74
+ for cls in [PersonTraitable, NameValueTraitable]:
75
+ store.delete_collection(cls.collection().collection_name())
76
+ store.delete_collection(cls.s_history_class.collection().collection_name())
77
+ store.end_using()
78
+
79
+
80
+ class TestTraitableHistory:
81
+ """Test TraitableHistory functionality."""
82
+
83
+ def test_asof_context_enter_exit(self):
84
+ """Test AsOfContext enter and exit."""
85
+ as_of_time = datetime(2023, 1, 1, 12, 0, 0)
86
+
87
+ with AsOfContext(as_of_time, [NameValueTraitable]) as context:
88
+ assert context.as_of_time == as_of_time
89
+
90
+ def test_asof_context_manager(self):
91
+ """Test AsOfContext as context manager."""
92
+ as_of_time = datetime(2023, 1, 1, 12, 0, 0)
93
+
94
+ with AsOfContext(as_of_time, [NameValueTraitable]) as context:
95
+ assert context.as_of_time == as_of_time
96
+
97
+ def test_traitable_history_collection(self, test_store):
98
+ """Test TraitableHistory collection name generation."""
99
+ # Test with default collection name
100
+ coll = NameValueTraitable.collection()
101
+ history_coll = NameValueTraitable.s_history_class.collection()
102
+ assert coll.collection_name() + '#history' == history_coll.collection_name()
103
+
104
+ def test_traitable_history_save(self, test_store, test_collection):
105
+ serialized_data = {'_id': 'test-123', '_rev': 1, 'name': 'Test Item', 'value': 42}
106
+
107
+ class CustomCollectionHistory(TraitableHistory, custom_collection=True):
108
+ s_traitable_class = Traitable
109
+
110
+ CustomCollectionHistory(
111
+ serialized_traitable=serialized_data,
112
+ _traitable_rev=2,
113
+ _collection_name=test_collection.collection_name(),
114
+ ).save().throw()
115
+
116
+ # Verify that the history entry was saved
117
+ assert test_collection.count() == 1
118
+
119
+ # Get the saved document to verify its structure
120
+ saved_docs = list(test_collection.find())
121
+ assert len(saved_docs) == 1
122
+
123
+ saved_doc = saved_docs[0]
124
+ assert saved_doc['_traitable_id'] == 'test-123'
125
+ assert saved_doc['_traitable_rev'] == 2
126
+ assert saved_doc['_who'] == 'test_user'
127
+ assert saved_doc['name'] == 'Test Item'
128
+ assert saved_doc['value'] == 42
129
+ assert isinstance(saved_doc['_at'], datetime)
130
+
131
+ def test_traitable_class_history_methods(self, test_store, test_collection):
132
+ """Test Traitable class history methods."""
133
+ # Create a test traitable and save it with history
134
+ test_item = NameValueTraitableCustomCollection(_collection_name=test_collection.collection_name())
135
+ test_item.name = 'Test Item'
136
+ test_item.value = 42
137
+
138
+ # Save the traitable (this should create history)
139
+ test_item.save()
140
+
141
+ assert test_item._collection_name == test_collection.collection_name()
142
+
143
+ # Test history method
144
+ history = NameValueTraitableCustomCollection.history(_collection_name=test_item._collection_name)
145
+ assert len(history) == 1
146
+ assert history[0]['_traitable_id'] == test_item.id().value
147
+ assert history[0]['_traitable_rev'] == test_item._rev
148
+ assert history[0]['_who'] == 'test_user'
149
+ assert '_at' in history[0] # Should have timestamp
150
+ assert history[0]['name'] == 'Test Item'
151
+ assert history[0]['value'] == 42
152
+
153
+ ts = datetime.utcnow()
154
+ test_item.value = 43
155
+ test_item.save()
156
+ assert test_item._rev == 2
157
+ assert test_item.value == 43
158
+
159
+ assert NameValueTraitableCustomCollection.restore(test_item.id(), ts)
160
+ assert test_item._rev == 1
161
+ assert test_item.value == 42
162
+
163
+ def test_history_entry_creation(self, test_store):
164
+ """Test that history entries are created when saving traitables."""
165
+ # Create a person
166
+ person = PersonTraitable()
167
+ person.name = 'John Doe'
168
+ person.age = 30
169
+ person.email = 'john@example.com'
170
+
171
+ # Save the person
172
+ person.save()
173
+
174
+ # Check that history was created
175
+ history_collection = test_store.collection(f'{__name__.replace(".", "/")}/{PersonTraitable.__name__}#history')
176
+ assert history_collection.count() == 1
177
+
178
+ # Verify history entry structure
179
+ history_docs = list(history_collection.find())
180
+ history_doc = history_docs[0]
181
+ assert history_doc['_traitable_id'] == person.id().value
182
+ # History entry is created before revision increment, so it should be 0
183
+ assert history_doc['_traitable_rev'] == person._rev
184
+ assert history_doc['_who'] == 'test_user'
185
+ assert '_at' in history_doc
186
+ assert history_doc['name'] == 'John Doe'
187
+ assert history_doc['age'] == 30
188
+ assert history_doc['email'] == 'john@example.com'
189
+
190
+ def test_multiple_history_entries(self, test_store):
191
+ """Test that multiple history entries are created for updates."""
192
+
193
+ # Create a person
194
+ person = PersonTraitable()
195
+ person.name = 'John Doe'
196
+ person.age = 30
197
+ person.email = 'john@example.com'
198
+ person.save()
199
+
200
+ # Update the person
201
+ person.age = 31
202
+ person.save()
203
+
204
+ # Check that two history entries were created
205
+ history_collection = test_store.collection(f'{__name__.replace(".", "/")}/{PersonTraitable.__name__}#history')
206
+ assert history_collection.count() == 2
207
+
208
+ # Verify both history entries
209
+ history_docs = list(history_collection.find())
210
+ assert len(history_docs) == 2
211
+
212
+ # Both should have the same traitable_id but different revs
213
+ traitable_ids = [doc['_traitable_id'] for doc in history_docs]
214
+ assert all(tid == person.id().value for tid in traitable_ids)
215
+
216
+ revs = {doc['_traitable_rev'] for doc in history_docs}
217
+ assert {1, 2} == revs
218
+
219
+ def test_history_method(self, test_store):
220
+ """Test the history method."""
221
+
222
+ # Create a person
223
+ person = PersonTraitable()
224
+ person.name = 'John Doe'
225
+ person.age = 30
226
+ person.email = 'john@example.com'
227
+ person.save()
228
+
229
+ # Update the person
230
+ person.age = 31
231
+ person.save()
232
+
233
+ # Get history
234
+ history = PersonTraitable.history()
235
+
236
+ # Should have 2 history entries
237
+ assert len(history) == 2
238
+
239
+ # Both entries should have the correct traitable_id
240
+ for entry in history:
241
+ assert entry['_traitable_id'] == person.id().value
242
+ assert entry['_who'] == 'test_user'
243
+ assert '_at' in entry
244
+
245
+ def test_latest_revision(self, test_store):
246
+ """Test the latest_revision method."""
247
+
248
+ # Create a person
249
+ person = PersonTraitable()
250
+ person.name = 'John Doe'
251
+ person.age = 30
252
+ person.email = 'john@example.com'
253
+ person.save().throw()
254
+
255
+ # Update the person
256
+ person.age = 31
257
+ person.save().throw()
258
+
259
+ assert person._rev == 2
260
+
261
+ # Get latest revision
262
+ latest = PersonTraitable.latest_revision(person.id())
263
+
264
+ # Should be the latest revision
265
+ assert latest['_traitable_id'] == person.id().value
266
+ assert latest['_traitable_rev'] == person._rev
267
+ assert latest['_who'] == 'test_user'
268
+ assert '_at' in latest
269
+ assert latest['age'] == 31 # Updated age
270
+
271
+ def test_asof_context_manager_ops(self, test_store):
272
+ """Test AsOfContext with real data."""
273
+
274
+ # Create a person
275
+ person = PersonTraitable()
276
+ person.name = 'John Doe'
277
+ person.age = 30
278
+ person.email = 'john@example.com'
279
+ person.save()
280
+
281
+ # Record the time after creation
282
+ query_time = datetime.utcnow()
283
+
284
+ # Update the person
285
+ person.age = 31
286
+ person.save()
287
+
288
+ # Load the person as of the query time (should be the original version)
289
+ with AsOfContext(query_time):
290
+ historical_person = PersonTraitable.load(person.id())
291
+ assert historical_person.age == 30 # Original age
292
+ assert historical_person.name == 'John Doe'
293
+
294
+ def test_as_of_error_cases(self, test_store):
295
+ with BTraitableProcessor.create_root():
296
+ # Create and save a person
297
+ person = PersonTraitable(first_name='Alyssa', last_name='Lees', dob=date(1985, 7, 5), _replace=True)
298
+ person.spouse = PersonTraitable(first_name='James', last_name='Bond', dob=date(1985, 7, 5), _replace=True)
299
+ person.spouse.save().throw()
300
+ person.save().throw()
301
+
302
+ ts = datetime.utcnow()
303
+
304
+ # Update and save again
305
+ person.dob = date(1985, 7, 6)
306
+ person.spouse.dob = date(1985, 7, 6)
307
+ person.spouse.save().throw()
308
+ person.save().throw()
309
+
310
+ person_id = person.id()
311
+ print(person.serialize_object())
312
+
313
+ person.spouse.id()
314
+
315
+ assert PersonTraitable.existing_instance_by_id(person_id, _throw=False)
316
+
317
+ with CACHE_ONLY():
318
+ assert not PersonTraitable.existing_instance_by_id(person_id, _throw=False)
319
+
320
+ for ctx in (
321
+ INTERACTIVE,
322
+ GRAPH_ON,
323
+ GRAPH_OFF,
324
+ ):
325
+ for as_of in (
326
+ ts,
327
+ None,
328
+ ):
329
+ with ctx():
330
+ person1 = PersonTraitable(person_id)
331
+ assert person1.dob == date(1985, 7, 6)
332
+
333
+ person_as_of = PersonTraitable.as_of(person_id, as_of_time=ts)
334
+
335
+ with AsOfContext(traitable_classes=[PersonTraitable], as_of_time=as_of):
336
+ with pytest.raises(RuntimeError, match=r'object not usable - origin cache is not reachable'):
337
+ _ = person1.dob
338
+
339
+ with pytest.raises(RuntimeError, match=r'object not usable - origin cache is not reachable'):
340
+ _ = person_as_of.dob
341
+
342
+ person2 = PersonTraitable(person_id)
343
+ assert person2.dob == (date(1985, 7, 5 + (as_of is None)))
344
+ assert person2.spouse.dob == (date(1985, 7, 5 + (as_of is None)))
345
+
346
+ assert person1.spouse.dob == date(1985, 7, 6)
347
+ assert person_as_of.dob == date(1985, 7, 5), person_as_of.dob
348
+
349
+ assert person_as_of.spouse.dob == date(1985, 7, 6) # note - since no AsOfContext was used, nested objects are not loaded "as_of".
350
+
351
+ with pytest.raises(RuntimeError, match=r'object not usable - origin cache is not reachable'):
352
+ _ = person2.dob
353
+
354
+ def test_load_as_of(self, test_store):
355
+ """Test loading a traitable as of a specific time."""
356
+
357
+ # Create a person
358
+ person = PersonTraitable()
359
+ person.name = 'John Doe'
360
+ person.age = 30
361
+ person.email = 'john@example.com'
362
+ person.save()
363
+
364
+ # Record the time after creation
365
+ initial_time = datetime.utcnow()
366
+
367
+ # Update the person
368
+ person.age = 31
369
+ person.save()
370
+
371
+ # Load the person as of the initial time
372
+ historical_person = PersonTraitable.as_of(person.id(), initial_time)
373
+ assert historical_person.age == 30 # Original age
374
+ assert historical_person.name == 'John Doe'
375
+ assert historical_person == person # person's trait values also get updated due to shared cache
376
+
377
+ def test_load_many_with_as_of(self, test_store):
378
+ """Test loading multiple traitables as of a specific time."""
379
+
380
+ # Create two people
381
+ person1 = PersonTraitable()
382
+ person1.name = 'John Doe'
383
+ person1.age = 30
384
+ person1.email = 'john@example.com'
385
+ person1.save()
386
+
387
+ person2 = PersonTraitable()
388
+ person2.name = 'Jane Smith'
389
+ person2.age = 25
390
+ person2.email = 'jane@example.com'
391
+ person2.save()
392
+
393
+ # Record the time after creation
394
+ query_time = datetime.utcnow()
395
+
396
+ # Update both people
397
+ person1.age = 31
398
+ person1.save()
399
+
400
+ person2.age = 26
401
+ person2.save()
402
+
403
+ with AsOfContext(query_time, [PersonTraitable]):
404
+ historical_people = PersonTraitable.load_many()
405
+
406
+ # Should have 2 people with original ages
407
+ assert len(historical_people) == 2
408
+
409
+ # Find the specific people
410
+ john = next(p for p in historical_people if p.name == 'John Doe')
411
+ jane = next(p for p in historical_people if p.name == 'Jane Smith')
412
+
413
+ assert john.age == 30 # Original age
414
+ assert jane.age == 25 # Original age
415
+
416
+ assert person1.age == 31 # Current age
417
+ assert person2.age == 26 # Current age
418
+
419
+ def test_history_without_keep_history(self, test_store, monkeypatch):
420
+ """Test that history is not created when keep_history is False."""
421
+ monkeypatch.setattr('core_10x.package_refactoring.PackageRefactoring.default_class_id', lambda cls, *args, **kwargs: PyClass.name(cls))
422
+
423
+ # Create a class without history tracking
424
+ class NoHistoryTraitable(Traitable, keep_history=False):
425
+ name: str = T()
426
+
427
+ # Create and save an instance
428
+ item = NoHistoryTraitable()
429
+ item.name = 'Test Item'
430
+ item.save()
431
+
432
+ # Check that no history collection was created
433
+ history_collection = test_store.collection(f'{__name__.replace(".", "/")}/{NoHistoryTraitable.__name__}#history')
434
+ assert history_collection.count() == 0
435
+
436
+ def test_asof_with_keep_history_false_is_noop(self, test_store, monkeypatch):
437
+ """AsOfContext with a class that has keep_history=False is a no-op: current data is used."""
438
+ monkeypatch.setattr('core_10x.package_refactoring.PackageRefactoring.default_class_id', lambda cls, *args, **kwargs: PyClass.name(cls))
439
+
440
+ class NoHistoryTraitable(Traitable, keep_history=False):
441
+ key: str = T(T.ID)
442
+ value: str = T()
443
+
444
+ item = NoHistoryTraitable(key='k1', value='v1', _replace=True)
445
+ item.save()
446
+ original_helper = NoHistoryTraitable.s_storage_helper
447
+ as_of_time = datetime(2020, 1, 1, 12, 0, 0)
448
+
449
+ with pytest.raises(
450
+ ValueError,
451
+ match=r"<class 'core_10x.testlib.traitable_history_tests.TestTraitableHistory.test_asof_with_keep_history_false_is_noop.<locals>.NoHistoryTraitable'> is not storable or does not keep history",
452
+ ):
453
+ with AsOfContext(as_of_time, [NoHistoryTraitable]):
454
+ pass
455
+ assert type(NoHistoryTraitable.s_storage_helper) is type(original_helper)
456
+ assert NoHistoryTraitable.existing_instance(key='k1') == item
457
+
458
+ def test_asof_default_applies_to_all_traitable_subclasses(self, test_store):
459
+ """AsOfContext with default traitable_classes (None) applies to all Traitable subclasses via __mro__."""
460
+ # Create and save a person
461
+ person = PersonTraitable()
462
+ person.name = 'Default AsOf Test'
463
+ person.age = 25
464
+ person.save()
465
+
466
+ query_time = datetime.utcnow()
467
+
468
+ person.age = 26
469
+ person.save()
470
+
471
+ # Default: no traitable_classes => [Traitable]; PersonTraitable is a subclass so gets AsOf
472
+ with AsOfContext(as_of_time=query_time):
473
+ historical = PersonTraitable.load(person.id())
474
+ assert historical is not None
475
+ assert historical.age == 25
476
+ assert historical.name == 'Default AsOf Test'
477
+
478
+ # Outside context: current data
479
+ current = PersonTraitable.load(person.id())
480
+ assert current.age == 26
481
+
482
+ def test_latest_revision_with_timestamp(self, test_store):
483
+ """Test latest_revision with specific timestamp."""
484
+ # Create a person
485
+ person = PersonTraitable()
486
+ person.name = 'Time Test Person'
487
+ person.age = 30
488
+ person.save()
489
+
490
+ # Record time after first save
491
+ query_time = datetime.utcnow()
492
+
493
+ # Update the person
494
+ person.age = 31
495
+ person.save()
496
+
497
+ # Get latest revision as of the query time
498
+ latest = PersonTraitable.latest_revision(person.id(), timestamp=query_time)
499
+
500
+ # Should be the first revision (age 30)
501
+ assert latest['age'] == 30
502
+ assert latest['_traitable_rev'] == 1
503
+
504
+ def test_history_with_empty_collection(self, test_store):
505
+ """Test history method with no history entries."""
506
+ # Create a person but don't save it
507
+ person = PersonTraitable()
508
+ person.name = 'Unsaved Person'
509
+
510
+ # Get history - should be empty
511
+ history = PersonTraitable.history()
512
+ assert len(history) == 0
513
+
514
+ def test_latest_revision_with_nonexistent_id(self, test_store):
515
+ """Test latest_revision with non-existent ID."""
516
+
517
+ # Try to get latest revision for non-existent ID
518
+ latest = PersonTraitable.latest_revision(ID('nonexistent'))
519
+ assert latest is None
520
+
521
+ def test_history_default_at_most_parameter(self, test_store):
522
+ """Test that history method uses default _at_most=0 (no limit)."""
523
+ # Create a person
524
+ person = PersonTraitable()
525
+ person.name = 'Test Person'
526
+ person.age = 30
527
+ person.save()
528
+
529
+ # Update multiple times
530
+ person.age = 31
531
+ person.save()
532
+ person.age = 32
533
+ person.save()
534
+
535
+ # Get all history entries (should get all 3)
536
+ history = PersonTraitable.history()
537
+ assert len(history) == 3
538
+
539
+ # Test with explicit _at_most=2
540
+ history_limited = PersonTraitable.history(_at_most=2)
541
+ assert len(history_limited) == 2
542
+
543
+ def test_latest_revision_with_timestamp_parameter(self, test_store):
544
+ """Test that latest_revision accepts timestamp parameter."""
545
+ # Create a person
546
+ person = PersonTraitable()
547
+ person.name = 'Timestamp Test'
548
+ person.age = 25
549
+ person.save()
550
+
551
+ # Record time after first save
552
+ query_time = datetime.utcnow()
553
+
554
+ # Update the person
555
+ person.age = 26
556
+ person.save()
557
+
558
+ # Test latest_revision with timestamp
559
+ latest = PersonTraitable.latest_revision(person.id(), timestamp=query_time)
560
+ assert latest is not None
561
+ assert latest['age'] == 25 # Should be the first revision
562
+
563
+ # Test latest_revision without timestamp (should get latest)
564
+ latest_no_timestamp = PersonTraitable.latest_revision(person.id())
565
+ assert latest_no_timestamp is not None
566
+ assert latest_no_timestamp['age'] == 26 # Should be the latest revision
567
+
568
+ def test_restore(self, test_store):
569
+ """Test restore method with save parameter."""
570
+ # Create a person
571
+ person = PersonTraitable()
572
+ person.name = 'Restore Test'
573
+ person.age = 30
574
+ person.save()
575
+
576
+ # Record time after first save
577
+ query_time = datetime.utcnow()
578
+
579
+ # Update the person
580
+ person.age = 31
581
+ person.save()
582
+
583
+ # Test restore without save (should not persist)
584
+ result = PersonTraitable.restore(person.id(), timestamp=query_time, save=False)
585
+ assert result is True
586
+ assert person.age == 30
587
+
588
+ # The person should still have the latest age
589
+ person.reload()
590
+ assert person.age == 31
591
+
592
+ # Test restore with save (should persist)
593
+ result = PersonTraitable.restore(person.id(), timestamp=query_time, save=True)
594
+ assert result is True
595
+ assert person.age == 30
596
+ person.reload()
597
+ assert person.age == 30
598
+
599
+ def test_restore_with_nonexistent_timestamp(self, test_store):
600
+ """Test restore method with non-existent timestamp."""
601
+ # Create a person
602
+ person = PersonTraitable()
603
+ person.name = 'Restore Test'
604
+ person.age = 30
605
+ person.save()
606
+
607
+ # Try to restore to a time before the person existed
608
+ past_time = datetime(2020, 1, 1)
609
+ result = PersonTraitable.restore(person.id(), timestamp=past_time, save=False)
610
+ assert result is False
611
+
612
+ def test_restore_with_nonexistent_id(self, test_store):
613
+ """Test restore method with non-existent ID."""
614
+
615
+ # Try to restore non-existent ID
616
+ result = PersonTraitable.restore(ID('nonexistent'), save=False)
617
+ assert result is False
618
+
619
+ def test_traitable_history_deserialize(self, test_store):
620
+ """Test TraitableHistory.deserialize method."""
621
+ # Create a person
622
+ person = PersonTraitable()
623
+ person.name = 'Deserialize Test'
624
+ person.age = 25
625
+ person.save()
626
+
627
+ # Get history entry
628
+ history = PersonTraitable.history(_deserialize=True)
629
+ assert len(history) == 1
630
+
631
+ history_entry = history[0]
632
+
633
+ # Test deserialize
634
+ restored_person = history_entry.traitable
635
+
636
+ assert restored_person.name == 'Deserialize Test'
637
+ assert restored_person.age == 25
638
+ assert restored_person.id().value == person.id().value
639
+
640
+ def test_traitable_history_prepare_to_deserialize(self, test_store):
641
+ """Test TraitableHistory.prepare_to_deserialize method."""
642
+ # Create a person
643
+ person = PersonTraitable()
644
+ person.name = 'Prepare Test'
645
+ person.age = 28
646
+ person.save()
647
+
648
+ # Get history entry
649
+ history = PersonTraitable.history(_deserialize=True)
650
+ history_entry = history[0]
651
+
652
+ # Test prepare_to_deserialize
653
+ prepared_data = history_entry.serialized_traitable
654
+
655
+ # Should have the traitable data without history-specific fields
656
+ assert prepared_data['name'] == 'Prepare Test'
657
+ assert prepared_data['age'] == 28
658
+ assert prepared_data['_id'] == person.id().value
659
+ assert prepared_data['_rev'] == person._rev
660
+ assert '_traitable_id' not in prepared_data
661
+ assert '_traitable_rev' not in prepared_data
662
+ assert '_who' not in prepared_data
663
+ assert '_at' not in prepared_data
664
+
665
+ def test_save_new_with_overwrite_parameter(self, test_store, test_collection):
666
+ """Test TestCollection.save_new with overwrite parameter."""
667
+ # Test save_new with MongoDB-style $set operation
668
+ result = test_collection.save_new({'$set': {'_id': 'new-id', 'name': 'New Person', 'age': 25}})
669
+ assert result == 1 # Should succeed for new document
670
+
671
+ # Fails because the document already exists
672
+ with pytest.raises(TsDuplicateKeyError):
673
+ test_collection.save_new({'$set': {'_id': 'new-id'}})
674
+
675
+ test_collection.save_new({'$set': {'_id': 'new-id'}}, overwrite=True)
676
+
677
+ def test_find_without_filter_parameter(self, test_store, test_collection):
678
+ """Test TestCollection.find without _filter parameter."""
679
+
680
+ # Add documents using the interface method
681
+ test_collection.save_new({'_id': 'doc1', 'name': 'Person 1', 'age': 25})
682
+ test_collection.save_new({'_id': 'doc2', 'name': 'Person 2', 'age': 30})
683
+
684
+ # Test find without _filter parameter
685
+ results = list(test_collection.find())
686
+ assert len(results) == 2 # Should find both persons
687
+
688
+ def test_immutability_of_classes_without_history(self, test_store, test_collection):
689
+ """Test immutability of storable classes that do not keep history."""
690
+
691
+ class NoHistoryImmutableTraitable(NameValueTraitableCustomCollection, keep_history=False): ...
692
+
693
+ # Classes without history should be marked immutable
694
+ assert NoHistoryImmutableTraitable.s_history_class is None
695
+ assert NoHistoryImmutableTraitable.s_immutable is True
696
+
697
+ # First save should succeed and create a single record
698
+ item = NoHistoryImmutableTraitable(_collection_name=test_collection.collection_name())
699
+ item.name = 'First'
700
+ rc1 = item.save()
701
+ assert rc1
702
+ assert item.name == 'First'
703
+ assert item._rev == 1
704
+ assert test_collection.count() == 1
705
+
706
+ # Second save should fail (cannot update immutable records)
707
+ item.name = 'Second'
708
+ rc2 = item.save()
709
+ assert not rc2
710
+ assert item.name == 'Second'
711
+ assert item._rev == 1
712
+
713
+ # After reload, state should still reflect the original save
714
+ item.reload()
715
+ assert item.name == 'First'
716
+ assert item._rev == 1
717
+
718
+ def test_history_entries_are_immutable(self, test_store):
719
+ """Test that individual history entries cannot be modified once written."""
720
+
721
+ # Create and save a person twice to generate multiple history entries
722
+ person = PersonTraitable()
723
+ person.name = 'Immutable History'
724
+ person.age = 30
725
+ person.email = 'immutable@example.com'
726
+ person.save()
727
+
728
+ person.age = 31
729
+ person.save()
730
+
731
+ history_collection = test_store.collection(f'{__name__.replace(".", "/")}/{PersonTraitable.__name__}#history')
732
+ assert history_collection.count() == 2
733
+
734
+ # Load a history entry as TraitableHistory instance
735
+ history_entries = PersonTraitable.history(_deserialize=True)
736
+ assert len(history_entries) == 2
737
+
738
+ entry = history_entries[0]
739
+ original_who = entry._who
740
+
741
+ # Attempting to modify and re-save the history entry should fail
742
+ entry._who = 'someone-else'
743
+ rc = entry.save()
744
+ assert not rc
745
+
746
+ # Reload from the store and verify that the stored entry was not changed
747
+ reloaded_docs = list(history_collection.find())
748
+ assert len(reloaded_docs) == 2
749
+ assert any(doc['_who'] == original_who for doc in reloaded_docs)
750
+ assert all(doc['_who'] != 'someone-else' for doc in reloaded_docs)
751
+
752
+ def test_asof_find_returns_one_record_per_id(self, test_store):
753
+ """Test that the AsOf _find implementation returns at most one record per _id."""
754
+
755
+ # Create two people and multiple history entries for each
756
+ person1 = PersonTraitable()
757
+ person1.name = 'Person One'
758
+ person1.age = 30
759
+ person1.email = 'one@example.com'
760
+ person1.save()
761
+
762
+ person1.age = 31
763
+ person1.save()
764
+
765
+ person2 = PersonTraitable()
766
+ person2.name = 'Person Two'
767
+ person2.age = 25
768
+ person2.email = 'two@example.com'
769
+ person2.save()
770
+
771
+ person2.age = 26
772
+ person2.save()
773
+
774
+ # Sanity check: we indeed have multiple history records per person
775
+ history = PersonTraitable.history()
776
+ assert len(history) >= 4
777
+
778
+ as_of_time = datetime.utcnow()
779
+
780
+ # AsOfContext uses StorableHelperAsOf._find under the hood for load_many
781
+ with AsOfContext(as_of_time, [PersonTraitable]):
782
+ people_as_of = PersonTraitable.load_many()
783
+
784
+ # We should get exactly one record per traitable id
785
+ assert len(people_as_of) == 2
786
+ ids = [p.id().value for p in people_as_of]
787
+ assert len(set(ids)) == len(ids)