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
core_10x/traitable.py ADDED
@@ -0,0 +1,1082 @@
1
+ from __future__ import annotations
2
+
3
+ import functools
4
+ import operator
5
+ import sys
6
+ import warnings
7
+ from abc import ABC, abstractmethod
8
+ from dataclasses import dataclass, field
9
+ from datetime import datetime # noqa: TC003
10
+ from typing import TYPE_CHECKING, Any, get_origin
11
+
12
+ from py10x_core import BTraitable, BTraitableClass, BTraitableProcessor, BTraitFlags
13
+ from typing_extensions import Self, deprecated
14
+
15
+ import core_10x.concrete_traits as concrete_traits
16
+ from core_10x.environment_variables import EnvVars
17
+ from core_10x.global_cache import cache
18
+ from core_10x.nucleus import Nucleus
19
+ from core_10x.package_manifest import PackageManifest
20
+ from core_10x.package_refactoring import PackageRefactoring
21
+ from core_10x.py_class import PyClass
22
+ from core_10x.rc import RC, RC_TRUE
23
+ from core_10x.trait import TRAIT_METHOD, BoundTrait, T, Trait, trait_value
24
+ from core_10x.trait_definition import (
25
+ RT,
26
+ M,
27
+ TraitDefinition,
28
+ TraitModification,
29
+ Ui,
30
+ )
31
+ from core_10x.trait_filter import LE, f
32
+ from core_10x.traitable_id import ID
33
+ from core_10x.ts_store import TS_STORE, TsStore
34
+ from core_10x.xnone import XNone, XNoneType
35
+
36
+ if TYPE_CHECKING:
37
+ from collections.abc import Generator
38
+
39
+ from core_10x.ts_store import TsCollection
40
+
41
+
42
+ class TraitAccessor:
43
+ __slots__ = ('cls', 'obj')
44
+
45
+ def __init__(self, obj: Traitable):
46
+ self.cls = obj.__class__
47
+ self.obj = obj
48
+
49
+ def __getattr__(self, trait_name: str) -> BoundTrait:
50
+ trait = self.cls.trait(trait_name, throw=True)
51
+ return BoundTrait(self.obj, trait)
52
+
53
+ def __call__(self, trait_name: str, throw=True) -> Trait:
54
+ return self.cls.trait(trait_name, throw=True)
55
+
56
+
57
+ class UnboundTraitAccessor:
58
+ __slots__ = ()
59
+
60
+ def __get__(self, instance, owner):
61
+ return TraitAccessor(instance)
62
+
63
+
64
+ COLL_NAME_TAG = '_collection_name'
65
+
66
+
67
+ class StorageHelperDescriptor:
68
+ """Resolves s_storage_helper via __mro__ when in AsOf context; otherwise returns real helper."""
69
+
70
+ def __get__(self, obj: Traitable | None, owner: type[Traitable] | None = None) -> AbstractStorableHelper | Self:
71
+ if owner is None:
72
+ return self
73
+
74
+ if not (helper := owner.s_storage_helper_cached):
75
+ helper_as_of: StorableHelperAsOf = (
76
+ next(
77
+ (
78
+ base_helper
79
+ for base in owner.__mro__
80
+ if issubclass(base, Traitable) and isinstance(base_helper := base.s_storage_helper_cached, StorableHelperAsOf)
81
+ ),
82
+ None,
83
+ )
84
+ if owner.s_history_class
85
+ else None
86
+ )
87
+ if helper_as_of:
88
+ helper = StorableHelperAsOf(owner, helper_as_of.as_of_time)
89
+ else:
90
+ helper = StorableHelper(owner) if owner.is_storable() else NotStorableHelper(owner)
91
+
92
+ owner.s_storage_helper_cached = helper
93
+
94
+ return helper
95
+
96
+
97
+ class TraitableMetaclass(type(BTraitable)):
98
+ def __new__(cls, name, bases, class_dict, **kwargs):
99
+ if '__init__' in class_dict and name != 'Traitable':
100
+ raise TypeError(f'Overriding __init__ is not allowed in {name}. Use __post_init__ instead.')
101
+ return super().__new__(cls, name, bases, class_dict | {'__slots__': ()}, **kwargs)
102
+
103
+
104
+ class Traitable(BTraitable, Nucleus, metaclass=TraitableMetaclass):
105
+ s_dir = {}
106
+ s_default_trait_factory = RT
107
+ s_own_trait_definitions = None
108
+ T = UnboundTraitAccessor()
109
+
110
+ _collection_name: str = XNone
111
+ _rev: int = XNone
112
+
113
+ @classmethod
114
+ @cache
115
+ def rev_trait(cls) -> Trait:
116
+ trait_name = Nucleus.REVISION_TAG()
117
+ return Trait.create(
118
+ trait_name,
119
+ T(0, T.RESERVED, data_type=int),
120
+ )
121
+
122
+ @classmethod
123
+ @cache
124
+ def collection_name_trait(cls) -> Trait:
125
+ return Trait.create(
126
+ COLL_NAME_TAG,
127
+ T(T.RESERVED | T.RUNTIME, data_type=str),
128
+ )
129
+
130
+ def _collection_name_get(self) -> str:
131
+ return self.id().collection_name or XNone
132
+
133
+ def _collection_name_set(self, trait, value) -> RC:
134
+ self.id().collection_name = value
135
+ return RC_TRUE
136
+
137
+ @classmethod
138
+ def own_trait_definitions(cls) -> Generator[tuple[str, TraitDefinition]]:
139
+ class_dict = dict(cls.__dict__)
140
+ own_trait_definitions = class_dict.get('s_own_trait_definitions')
141
+ if own_trait_definitions is not None:
142
+ yield from cls.s_own_trait_definitions.items()
143
+ return
144
+
145
+ module_dict = sys.modules[cls.__module__].__dict__ if cls.__module__ else {}
146
+ type_annotations = cls.__annotations__
147
+ type_annotations |= {k: XNoneType for k, v in class_dict.items() if isinstance(v, TraitModification) and k not in type_annotations}
148
+
149
+ rc = RC(True)
150
+ for trait_name, trait_def in class_dict.items():
151
+ if isinstance(trait_def, TraitDefinition) and trait_name not in type_annotations:
152
+ rc <<= f'{trait_name} = T(...), but the trait is missing a data type annotation. Use `Any` if needed.'
153
+
154
+ for trait_name, dt in type_annotations.items():
155
+ trait_def = class_dict.get(trait_name, class_dict)
156
+ if trait_def is not class_dict and not isinstance(trait_def, TraitDefinition):
157
+ continue
158
+
159
+ old_trait: Trait = cls.s_dir.get(trait_name)
160
+ if not (dt := cls.check_trait_type(trait_name, trait_def, old_trait, dt, class_dict, module_dict, rc)):
161
+ continue
162
+
163
+ if dt is Any:
164
+ dt = XNoneType
165
+
166
+ if trait_def is class_dict: # -- only annotation, not in class_dict
167
+ trait_def = cls.s_default_trait_factory()
168
+
169
+ if isinstance(trait_def, TraitModification):
170
+ if not old_trait:
171
+ rc <<= f'{trait_name} = M(...), but the trait is not defined previously'
172
+ continue
173
+ trait_def = trait_def.apply(old_trait.t_def)
174
+ if dt is not XNoneType: # -- the data type is also being modified
175
+ trait_def.data_type = dt
176
+ else:
177
+ trait_def.data_type = dt
178
+
179
+ yield trait_name, trait_def
180
+
181
+ rc.throw(TypeError)
182
+
183
+ @classmethod
184
+ def check_trait_type(cls, trait_name, trait_def, old_trait, dt, class_dict, module_dict, rc):
185
+ if not dt and old_trait:
186
+ return old_trait.data_type
187
+
188
+ if isinstance(dt, str):
189
+ try:
190
+ dt = eval(dt, class_dict, module_dict)
191
+ except Exception as e:
192
+ rc <<= f'Failed to evaluate type annotation string `{dt}` for `{trait_name}`: {e}'
193
+ return None
194
+ if dt is Self:
195
+ dt = cls
196
+ try:
197
+ dt_valid = isinstance(dt, type) or get_origin(dt) is not None
198
+ except TypeError:
199
+ dt_valid = False
200
+ if not dt_valid:
201
+ rc <<= f'Expected type for `{trait_name}`, but found {dt} of type {type(dt)}.'
202
+ return None
203
+
204
+ if trait_def is class_dict: # -- only annotation, not in class_dict
205
+ if old_trait and dt is not old_trait.data_type:
206
+ rc <<= f'Attempt to implicitly redefine type for previously defined trait `{trait_name}`. Use M() if needed.'
207
+ return None
208
+
209
+ return dt
210
+
211
+ @classmethod
212
+ def inherited_trait_dirs(cls) -> Generator[dict[str, Trait]]:
213
+ return (base.s_dir for base in reversed(cls.__bases__) if issubclass(base, Traitable))
214
+
215
+ @classmethod
216
+ def build_trait_dir(cls):
217
+ class_dict = dict(cls.__dict__)
218
+ module_dict = sys.modules[cls.__module__].__dict__ if cls.__module__ else {}
219
+ type_annotations = cls.__annotations__
220
+ trait_dir = cls.s_dir
221
+ reserved_storable_traits = {
222
+ Nucleus.REVISION_TAG(): cls.rev_trait(),
223
+ COLL_NAME_TAG: cls.collection_name_trait(),
224
+ }
225
+ trait_dir |= reserved_storable_traits
226
+ trait_dir |= functools.reduce(operator.or_, cls.inherited_trait_dirs(), {})
227
+
228
+ rc = RC(True)
229
+ for trait_name, old_trait in trait_dir.items():
230
+ trait_def = class_dict.get(trait_name, class_dict)
231
+ dt = type_annotations.get(trait_name)
232
+ if trait_def is class_dict and any(func_name in class_dict for func_name in Trait.method_defs(trait_name)):
233
+ if cls.check_trait_type(trait_name, trait_def, old_trait, dt, class_dict, module_dict, rc):
234
+ trait_def = old_trait.t_def.copy()
235
+ trait_dir[trait_name] = Trait.create(trait_name, trait_def)
236
+
237
+ for trait_name, trait_def in cls.own_trait_definitions():
238
+ trait_def.name = trait_name
239
+ trait_dir[trait_name] = Trait.create(trait_name, trait_def)
240
+
241
+ if not cls.is_storable():
242
+ for trait_name in reserved_storable_traits:
243
+ del trait_dir[trait_name]
244
+ rc.throw(TypeError)
245
+
246
+ @classmethod
247
+ def traits(cls, flags_on: int | BTraitFlags = 0, flags_off: int | BTraitFlags = 0) -> Generator[Trait, None, None]:
248
+ return (t for t in cls.s_dir.values() if (not flags_on or t.flags_on(flags_on)) and not t.flags_on(flags_off))
249
+
250
+ def __hash__(self):
251
+ return hash(self.id())
252
+
253
+ @classmethod
254
+ def trait(cls, trait_name: str, throw: bool = False) -> Trait:
255
+ trait = cls.s_dir.get(trait_name)
256
+ if trait is None and throw:
257
+ raise TypeError(f'{cls} - unknown trait {trait_name}')
258
+
259
+ return trait
260
+
261
+ @classmethod
262
+ def is_id_endogenous(cls) -> bool:
263
+ return cls.s_bclass.is_id_endogenous()
264
+
265
+ @classmethod
266
+ def is_storable(cls) -> bool:
267
+ return cls.s_bclass.is_storable()
268
+
269
+ # @staticmethod
270
+ # def find_storable_class(class_id: str):
271
+ # traitable_class = PackageRefactoring.find_class(class_id)
272
+ # if not issubclass(traitable_class, Traitable) or not traitable_class.is_storable():
273
+ # raise TypeError(f'{traitable_class} is not a storable Traitable')
274
+ #
275
+ # return traitable_class
276
+
277
+ s_bclass: BTraitableClass = None
278
+ s_custom_collection = False
279
+ s_history_class = XNone # -- will be set in __init__subclass__ for storable traitables unless keep_history = False. affects storage only.
280
+ s_immutable = (
281
+ XNone # -- will be turned on in __init__subclass__ for storable traitables without history unless immutable=False. affects storage only.
282
+ )
283
+ s_direct_subclasses: list[type[Traitable]] = []
284
+ s_storage_helper: AbstractStorableHelper = StorageHelperDescriptor()
285
+ s_storage_helper_cached: AbstractStorableHelper | None = None
286
+
287
+ def __init_subclass__(cls, custom_collection: bool = None, keep_history: bool = None, immutable: bool = None, **kwargs):
288
+ if custom_collection is not None:
289
+ cls.s_custom_collection = custom_collection
290
+
291
+ cls.s_direct_subclasses = []
292
+ for base in cls.__bases__:
293
+ if issubclass(base, Traitable):
294
+ base.s_direct_subclasses.append(cls)
295
+
296
+ cls.s_dir = {}
297
+ cls.s_bclass = BTraitableClass(cls)
298
+
299
+ cls.build_trait_dir() # -- build cls.s_dir from trait definitions in cls.__dict__
300
+
301
+ if cls.is_storable():
302
+ if keep_history is False:
303
+ cls.s_history_class = None
304
+ if cls.s_history_class is not None:
305
+ cls.s_history_class = TraitableHistory.history_class(cls)
306
+
307
+ cls.s_immutable = cls.s_history_class is None if immutable is None else immutable # TODO: review, test.
308
+ else:
309
+ cls.s_history_class = XNone
310
+
311
+ rc = RC(True)
312
+ for trait_name, trait in cls.s_dir.items():
313
+ trait.set_trait_funcs(cls, rc)
314
+ trait.check_integrity(cls, rc)
315
+ setattr(cls, trait_name, trait)
316
+ cls.check_integrity(rc)
317
+ rc.throw()
318
+
319
+ @classmethod
320
+ def check_integrity(cls, rc: RC):
321
+ if cls.is_storable() and (rt_id_trait := next((trait for trait in cls.traits(flags_on=T.ID) if trait.flags_on((T.RUNTIME))), None)):
322
+ rc.add_error(f'{cls}.{rt_id_trait.name} is a RUNTIME ID trait - traitable must not be storable (all traits must be RUNTIME)')
323
+
324
+ # @classmethod
325
+ # def instance_by_id(cls, id_value: str) -> 'Traitable':
326
+ # # return cls.load(id_value)
327
+ # return cls(_id = id_value) #-- TODO: we may not need this method, unless used to enforce loading
328
+
329
+ def __init__(self, _id: ID = None, _collection_name: str = None, _skip_init=False, _replace=False, **trait_values):
330
+ cls = self.__class__
331
+
332
+ if _id is not None:
333
+ assert _collection_name is None, f'{self.__class__}(id_value) may not be invoked with _collection_name'
334
+ assert not trait_values, f'{self.__class__}(id_value) may not be invoked with trait_values'
335
+ super().__init__(cls.s_bclass, _id)
336
+ else:
337
+ super().__init__(cls.s_bclass, ID(collection_name=_collection_name))
338
+ if not _skip_init:
339
+ self.initialize(trait_values, _replace=_replace)
340
+
341
+ self.__post_init__()
342
+
343
+ def __post_init__(self):
344
+ # Default no-op; users can override this in subclasses
345
+ pass
346
+
347
+ @classmethod
348
+ def existing_instance(cls, _collection_name: str = None, _throw: bool = True, **trait_values) -> Traitable | None:
349
+ if not cls.is_storable() and cls.is_id_endogenous(): # runtime endogenous instances are created on the fly
350
+ return cls(_collection_name=_collection_name, **trait_values)
351
+
352
+ obj = cls(_collection_name=_collection_name, _skip_init=True)
353
+ if not obj.accept_existing(trait_values):
354
+ if _throw:
355
+ raise ValueError(f'Instance does not exist: {cls}({trait_values})')
356
+ return None
357
+
358
+ return obj
359
+
360
+ @classmethod
361
+ def existing_instance_by_id(cls, _id: ID = None, _id_value: str = None, _collection_name: str = None, _throw: bool = True) -> Traitable | None:
362
+ if _id is None:
363
+ _id = ID(_id_value, _collection_name)
364
+ obj = cls(_id=_id)
365
+ if obj.id_exists():
366
+ return obj
367
+
368
+ if _throw:
369
+ raise ValueError(f'Instance does not exist: {cls}.{_id_value}')
370
+
371
+ return None
372
+
373
+ @classmethod
374
+ @deprecated('Use create_or_replace instead.')
375
+ def update(cls, **kwargs) -> Traitable:
376
+ return cls(**kwargs, _replace=True)
377
+
378
+ @classmethod
379
+ def new_or_replace(cls, **kwargs) -> Traitable:
380
+ return cls(**kwargs, _replace=True)
381
+
382
+ def set_values(self, _ignore_unknown_traits=True, **trait_values) -> RC:
383
+ return self._set_values(trait_values, _ignore_unknown_traits)
384
+
385
+ def __getitem__(self, item):
386
+ return self.get_value(item)
387
+
388
+ def __setitem__(self, key, value):
389
+ return self.set_value(key, value)
390
+
391
+ # ===================================================================================================================
392
+ # The following methods are available from c++
393
+ #
394
+ # get_value(trait-or-name, *args) -> Any
395
+ # set_value(trait-or_name, value: Any, *args) -> RC
396
+ # raw_value(trait-or_name, value: Any, *args) -> RC
397
+ # invalidate_value(trait-or-name)
398
+ # ===================================================================================================================
399
+
400
+ # ===================================================================================================================
401
+ # Nucleus related methods
402
+ # ===================================================================================================================
403
+
404
+ def serialize(self, embed: bool):
405
+ return self.serialize_nx(embed)
406
+
407
+ @classmethod
408
+ def is_bundle(cls) -> bool:
409
+ return cls.serialize_class_id is not Traitable.serialize_class_id
410
+
411
+ @classmethod
412
+ def serialize_class_id(cls) -> str | None:
413
+ return None
414
+
415
+ @classmethod
416
+ def deserialize_class_id(cls, serialized_class_id: str):
417
+ return cls
418
+
419
+ @classmethod
420
+ def deserialize(cls, serialized_data) -> Traitable:
421
+ return Traitable.deserialize_nx(cls.s_bclass, serialized_data)
422
+
423
+ def to_str(self) -> str:
424
+ return f'{self.id()}'
425
+
426
+ @classmethod
427
+ def from_str(cls, s: str) -> Nucleus:
428
+ # return cls(ID(s)) # collection name?
429
+ return cls.existing_instance_by_id(_id_value=s)
430
+
431
+ @classmethod
432
+ def from_any_xstr(cls, value) -> Nucleus:
433
+ if isinstance(value, dict):
434
+ return cls(**value)
435
+
436
+ raise TypeError(f'{cls}.from_any_xstr() expects a dict, got {value})')
437
+
438
+ @classmethod
439
+ def same_values(cls, value1, value2) -> bool:
440
+ if type(value2) is str:
441
+ return value1.id().value == value2
442
+
443
+ return value1.id() == value2.id()
444
+
445
+ # ===================================================================================================================
446
+ # Storage related methods
447
+ # ===================================================================================================================
448
+
449
+ @staticmethod
450
+ @cache
451
+ def _bound_data_domain(domain):
452
+ from core_10x.backbone.bound_data_domain import BoundDataDomain
453
+
454
+ bb_store = BoundDataDomain.store()
455
+ if not bb_store:
456
+ raise OSError('No Store is available: neither current Store is set nor 10X Backbone host is defined')
457
+
458
+ bbd = BoundDataDomain(domain=domain)
459
+ bbd.reload()
460
+ return bbd
461
+
462
+ @classmethod
463
+ @cache
464
+ def preferred_store(cls) -> TsStore | None:
465
+ rr = PackageManifest.resource_requirements(cls)
466
+ if not rr:
467
+ return None
468
+
469
+ bbd = Traitable._bound_data_domain(rr.domain)
470
+ return bbd.resource(rr.category, throw=False) if bbd else None
471
+
472
+ @staticmethod
473
+ @cache
474
+ def main_store() -> TsStore:
475
+ store_uri = EnvVars.main_ts_store_uri
476
+ return TsStore.instance_from_uri(store_uri) if store_uri else None
477
+
478
+ @classmethod
479
+ @cache
480
+ def store_per_class(cls) -> TsStore:
481
+ store = Traitable.main_store() # -- check if there's XX_MAIN_TS_STORE_URI defining a valid store
482
+ if not store:
483
+ raise OSError('No Traitable Store is specified: neither explicitly, nor via environment variable XX_MAIN_TS_STORE_URI')
484
+
485
+ # -- check if there's a specific store association with this cls
486
+ if EnvVars.use_ts_store_per_class:
487
+ ts_uri = TsClassAssociation.ts_uri(cls)
488
+ if ts_uri:
489
+ store = TsStore.instance_from_uri(ts_uri)
490
+
491
+ return store
492
+
493
+ @classmethod
494
+ def store(cls) -> TsStore:
495
+ store: TsStore = TS_STORE.current_resource() # -- if current TsStore is set, use it!
496
+ if not store:
497
+ store = cls.store_per_class() # -- otherwise, use per class store or main store, if any
498
+
499
+ return store
500
+
501
+ @classmethod
502
+ def collection(cls, _coll_name: str = None) -> TsCollection | None:
503
+ return cls.s_storage_helper.collection(_coll_name)
504
+
505
+ @classmethod
506
+ def exists_in_store(cls, id: ID) -> bool:
507
+ return cls.s_storage_helper.exists_in_store(id)
508
+
509
+ @classmethod
510
+ def load_data(cls, id: ID) -> dict | None:
511
+ return cls.s_storage_helper.load_data(id)
512
+
513
+ @classmethod
514
+ def delete_in_store(cls, id: ID) -> RC:
515
+ return cls.s_storage_helper.delete_in_store(id)
516
+
517
+ @classmethod
518
+ def load(cls, id: ID) -> Traitable | None:
519
+ return cls.s_storage_helper.load(id)
520
+
521
+ @classmethod
522
+ def load_many(cls, query: f = None, _coll_name: str = None, _at_most: int = 0, _order: dict = None, _deserialize=True) -> list[Self]:
523
+ return cls.s_storage_helper.load_many(query, _coll_name, _at_most, _order, _deserialize)
524
+
525
+ @classmethod
526
+ def load_ids(cls, query: f = None, _coll_name: str = None, _at_most: int = 0, _order: dict = None) -> list[ID]:
527
+ return cls.s_storage_helper.load_ids(query, _coll_name, _at_most, _order)
528
+
529
+ @classmethod
530
+ def delete_collection(cls, _coll_name: str = None) -> bool:
531
+ return cls.s_storage_helper.delete_collection(_coll_name)
532
+
533
+ def save(self, save_references=False) -> RC:
534
+ return self.__class__.s_storage_helper.save(self, save_references=save_references)
535
+
536
+ def delete(self) -> RC:
537
+ return self.__class__.s_storage_helper.delete(self)
538
+
539
+ def verify(self) -> RC:
540
+ rc = RC_TRUE
541
+ # TODO: implement
542
+ return rc
543
+
544
+ # TODO: move into storage helper
545
+
546
+ @classmethod
547
+ def as_of(cls, traitable_id: ID, as_of_time: datetime) -> Self:
548
+ history_entry = cls.latest_revision(traitable_id, as_of_time, deserialize=True)
549
+ return history_entry.traitable if history_entry else None
550
+
551
+ @classmethod
552
+ def history(
553
+ cls, _at_most: int = 0, _filter: f = None, _deserialize=False, _collection_name: str = None, _before: datetime = None, **named_filters
554
+ ) -> list:
555
+ """Get history entries for this traitable class."""
556
+ if not cls.s_history_class:
557
+ raise RuntimeError(f'{cls} does not keep history')
558
+
559
+ if cls.s_custom_collection and not _collection_name:
560
+ raise RuntimeError(f'{cls} requires custom _collection_name')
561
+
562
+ if not cls.s_custom_collection and _collection_name:
563
+ raise RuntimeError(f'{cls} does not support custom _collection_name')
564
+
565
+ as_of = {'_at': LE(_before)} if _before else {}
566
+ cursor = cls.s_history_class.load_many(
567
+ f(_filter, **named_filters, **as_of),
568
+ _order=dict(_traitable_id=1, _at=-1),
569
+ _at_most=_at_most,
570
+ _deserialize=_deserialize,
571
+ _coll_name=_collection_name + '#history' if _collection_name else None,
572
+ )
573
+
574
+ return list(cursor)
575
+
576
+ @classmethod
577
+ def latest_revision(cls, traitable_id: ID, timestamp: datetime = None, deserialize: bool = False) -> dict | TraitableHistory | None:
578
+ """Get the latest revision of an entity from history."""
579
+ for entry in cls.history(
580
+ _filter=f(_traitable_id=traitable_id.value),
581
+ _collection_name=traitable_id.collection_name,
582
+ _at_most=1,
583
+ _deserialize=deserialize,
584
+ _before=timestamp,
585
+ ):
586
+ return entry # Return the raw history entry dict
587
+
588
+ return None
589
+
590
+ @classmethod
591
+ def restore(cls, traitable_id, timestamp: datetime = None, save=False) -> bool:
592
+ """Restore a traitable to a specific point in time."""
593
+ history_entry = cls.latest_revision(
594
+ traitable_id,
595
+ timestamp,
596
+ deserialize=True,
597
+ )
598
+ if not history_entry or not history_entry.traitable:
599
+ return False
600
+
601
+ if save:
602
+ return bool(
603
+ cls.collection(
604
+ traitable_id.collection_name,
605
+ ).save_new(
606
+ {'$set': history_entry.serialized_traitable},
607
+ overwrite=True,
608
+ )
609
+ )
610
+ return True
611
+
612
+
613
+ Traitable.s_bclass = BTraitableClass(Traitable)
614
+
615
+
616
+ @dataclass
617
+ class AbstractStorableHelper(ABC):
618
+ traitable_class: type[Traitable]
619
+
620
+ @abstractmethod
621
+ def collection(self, _coll_name: str = None) -> TsCollection | None: ...
622
+
623
+ @abstractmethod
624
+ def exists_in_store(self, id: ID) -> bool: ...
625
+
626
+ @abstractmethod
627
+ def load_data(self, id: ID) -> dict | None: ...
628
+
629
+ @abstractmethod
630
+ def delete_in_store(self, id: ID) -> RC: ...
631
+
632
+ @abstractmethod
633
+ def load(self, id: ID) -> Traitable | None: ...
634
+
635
+ @abstractmethod
636
+ def load_many(self, query: f = None, _coll_name: str = None, _at_most: int = 0, _order: dict = None, _deserialize=True) -> list[Traitable]: ...
637
+
638
+ @abstractmethod
639
+ def load_ids(self, query: f = None, _coll_name: str = None, _at_most: int = 0, _order: dict = None) -> list[ID]: ...
640
+
641
+ @abstractmethod
642
+ def delete_collection(self, _coll_name: str = None) -> bool: ...
643
+
644
+ @abstractmethod
645
+ def save(self, traitable: Traitable, save_references: bool) -> RC: ...
646
+
647
+ @abstractmethod
648
+ def delete(self, traitable: Traitable) -> RC: ...
649
+
650
+
651
+ class NotStorableHelper(AbstractStorableHelper):
652
+ def collection(self, _coll_name: str = None) -> TsCollection | None:
653
+ return None
654
+
655
+ def exists_in_store(self, id: ID) -> bool:
656
+ return False
657
+
658
+ def load_data(self, id: ID) -> dict | None:
659
+ return None
660
+
661
+ def delete_in_store(self, id: ID) -> RC:
662
+ return RC(False, f'{self.traitable_class} is not storable')
663
+
664
+ def load(self, id: ID) -> Traitable | None:
665
+ return None
666
+
667
+ def load_many(self, query: f = None, _coll_name: str = None, _at_most: int = 0, _order: dict = None, _deserialize=True) -> list[Traitable]:
668
+ return []
669
+
670
+ def load_ids(self, query: f = None, _coll_name: str = None, _at_most: int = 0, _order: dict = None) -> list[ID]:
671
+ return []
672
+
673
+ def delete_collection(self, _coll_name: str = None) -> bool:
674
+ return False
675
+
676
+ def save(self, traitable: Traitable, save_references: bool) -> RC:
677
+ return RC(False, f'{self.traitable_class} is not storable')
678
+
679
+ def delete(self, traitable: Traitable) -> RC:
680
+ return RC(False, f'{self.traitable_class} is not storable')
681
+
682
+
683
+ class StorableHelper(AbstractStorableHelper):
684
+ def collection(self, _coll_name: str = None) -> TsCollection:
685
+ cls = self.traitable_class
686
+ cname = _coll_name or PackageRefactoring.find_class_id(cls)
687
+ return cls.store().collection(cname)
688
+
689
+ def exists_in_store(self, id: ID) -> bool:
690
+ return self.traitable_class.collection(_coll_name=id.collection_name).id_exists(id.value)
691
+
692
+ def load_data(self, id: ID) -> dict | None:
693
+ return self.traitable_class.collection(_coll_name=id.collection_name).load(id.value)
694
+
695
+ def delete_in_store(self, id: ID) -> RC:
696
+ cls = self.traitable_class
697
+ coll = cls.collection(_coll_name=id.collection_name)
698
+ if not coll:
699
+ return RC(False, f'{cls} - no store available')
700
+ if not coll.delete(id.value):
701
+ return RC(False, f'{cls} - failed to delete {id.value} from {coll}')
702
+ return RC_TRUE
703
+
704
+ def load(self, id: ID) -> Traitable | None:
705
+ return self.traitable_class.s_bclass.load(id)
706
+
707
+ def _find(self, query: f = None, _coll_name: str = None, _at_most: int = 0, _order: dict = None):
708
+ # TODO FUTURE - current state as history query?
709
+ cls = self.traitable_class
710
+ coll = cls.collection(_coll_name=_coll_name)
711
+ return coll.find(f(query, cls.s_bclass), _at_most=_at_most, _order=_order)
712
+
713
+ def load_many(
714
+ self, query: f = None, _coll_name: str = None, _at_most: int = 0, _order: dict = None, _deserialize: bool = True
715
+ ) -> list[Traitable] | list[dict]:
716
+ cursor = self._find(query=query, _coll_name=_coll_name, _at_most=_at_most, _order=_order)
717
+
718
+ if not _deserialize:
719
+ return list(cursor)
720
+
721
+ f_deserialize = functools.partial(Traitable.deserialize_object, self.traitable_class.s_bclass, _coll_name)
722
+ return [f_deserialize(serialized_data) for serialized_data in cursor]
723
+
724
+ def load_ids(self, query: f = None, _coll_name: str = None, _at_most: int = 0, _order: dict = None) -> list[ID]:
725
+ id_tag = self.traitable_class.collection(_coll_name=_coll_name).s_id_tag # better?
726
+ cursor = self._find(query=query, _coll_name=_coll_name, _at_most=_at_most, _order=_order)
727
+ return [ID(serialized_data.get(id_tag), _coll_name) for serialized_data in cursor]
728
+
729
+ def delete_collection(self, _coll_name: str = None) -> bool:
730
+ cls = self.traitable_class
731
+ store = cls.store()
732
+ if not store:
733
+ return False
734
+ cname = _coll_name or PackageRefactoring.find_class_id(cls)
735
+ return store.delete_collection(collection_name=cname)
736
+
737
+ def save(self, traitable: Traitable, save_references: bool) -> RC:
738
+ rc = traitable.verify()
739
+ if not rc:
740
+ return rc
741
+
742
+ rc = traitable.share(False) # -- not accepting existing traitable values, if any
743
+ if not rc:
744
+ return rc
745
+
746
+ serialized_data = traitable.serialize_object(save_references)
747
+
748
+ if not serialized_data: # -- it's a lazy instance - no reason to load and re-save
749
+ return RC_TRUE
750
+
751
+ coll = self.traitable_class.collection(traitable.id().collection_name)
752
+ if not coll:
753
+ return RC(False, f'{self.__class__} - no store available')
754
+
755
+ try:
756
+ rev = coll.save_new(serialized_data) if traitable.s_immutable else coll.save(serialized_data)
757
+ except Exception as e:
758
+ return RC(False, f'Error saving traitable: {e}')
759
+
760
+ if traitable.get_revision() != rev and self.traitable_class.s_history_class:
761
+ try:
762
+ self.traitable_class.s_history_class(
763
+ serialized_traitable=serialized_data,
764
+ _traitable_rev=rev,
765
+ _collection_name=traitable._collection_name + '#history', # -- add #history suffix
766
+ ).save().throw()
767
+ except Exception as e:
768
+ return RC(False, f'Error saving history: {e}') # TODO: rollback traitable save!!
769
+
770
+ traitable.set_revision(rev)
771
+ return RC_TRUE
772
+
773
+ def delete(self, traitable: Traitable) -> RC:
774
+ rc = self.delete_in_store(traitable.id())
775
+ if rc:
776
+ traitable.set_revision(0)
777
+ return rc
778
+
779
+
780
+ @dataclass
781
+ class StorableHelperAsOf(StorableHelper):
782
+ as_of_time: datetime
783
+
784
+ def _find(self, query: f = None, _coll_name: str = None, _at_most: int = 0, _order: dict = None):
785
+ last_id = None
786
+ for item in self.traitable_class.history(
787
+ _filter=query,
788
+ _before=self.as_of_time,
789
+ _collection_name=_coll_name,
790
+ _deserialize=True,
791
+ ):
792
+ # TODO: optimize by returning one entry per id using a pipeline query
793
+ if last_id != item._traitable_id:
794
+ last_id = item._traitable_id
795
+ yield item.serialized_traitable
796
+
797
+ def exists_in_store(self, id: ID) -> bool:
798
+ history_entry = self.traitable_class.latest_revision(id, self.as_of_time, deserialize=True)
799
+ return bool(history_entry) # TODO: optimize by not downloading the latest revision
800
+
801
+ def load_data(self, id: ID) -> dict | None:
802
+ history_entry = self.traitable_class.latest_revision(id, self.as_of_time, deserialize=True)
803
+ return history_entry.serialized_traitable if history_entry else None
804
+
805
+
806
+ def __getattr__(name):
807
+ if name == 'THIS_CLASS': # -- to use for traits with the same Traitable class type
808
+ warnings.warn('THIS_CLASS is deprecated; use typing.Self instead', DeprecationWarning, stacklevel=2)
809
+ return Self
810
+ raise AttributeError(name)
811
+
812
+
813
+ class TraitableHistory(Traitable, keep_history=False):
814
+ s_traitable_class = None
815
+ s_trait_name_map = dict(_traitable_id='_id', _traitable_rev='_rev')
816
+
817
+ # fmt: off
818
+ traitable: Traitable = RT() // 'original traitable'
819
+ serialized_traitable: dict = RT()
820
+
821
+ _at: datetime = T() // 'time saved'
822
+ _who: str = T() // 'authenticated user, if any'
823
+ _traitable_id: str = T() // 'original traitable id'
824
+ _traitable_rev: int = T() // 'original traitable rev'
825
+ # fmt: on
826
+
827
+ def _traitable_id_get(self) -> str:
828
+ return self.serialized_traitable['_id']
829
+
830
+ def serialize_object(self, save_references: bool = False):
831
+ serialized_data = {**self.serialized_traitable, **super().serialize_object(save_references), '_who': self.store().auth_user()}
832
+ del serialized_data['_at']
833
+ return {
834
+ '$currentDate': {'_at': True},
835
+ '$set': serialized_data,
836
+ }
837
+
838
+ def traitable_get(self):
839
+ return Traitable.deserialize_object(
840
+ self.s_traitable_class.s_bclass,
841
+ self._collection_name.rsplit('#', 1)[0] or None, # -- strip #history suffix
842
+ self.serialized_traitable,
843
+ )
844
+
845
+ def deserialize_traits(self, serialized_data):
846
+ hist_data = {trait.name: serialized_data.pop(trait.name, None) for trait in self.traits(flags_off=T.RUNTIME)}
847
+ self.serialized_traitable = serialized_data | {v: hist_data[k] for k, v in self.s_trait_name_map.items()}
848
+ return super().deserialize_traits(hist_data)
849
+
850
+ @classmethod
851
+ def store(cls):
852
+ return cls.s_traitable_class.store()
853
+
854
+ @classmethod
855
+ def collection(cls, _coll_name: str = None) -> TsCollection | None:
856
+ collection = cls.s_storage_helper.collection(_coll_name)
857
+ collection.create_index('idx_by_traitable_id_time', [('_traitable_id', 1), ('_at', -1)])
858
+ return collection
859
+
860
+ @staticmethod
861
+ @cache
862
+ def history_class(traitable_class: type[Traitable]):
863
+ module_dict = sys.modules[traitable_class.__module__].__dict__
864
+ history_class_name = f'{traitable_class.__name__}#history'
865
+ history_class = type(
866
+ history_class_name,
867
+ (TraitableHistory,),
868
+ dict(
869
+ s_traitable_class=traitable_class,
870
+ s_custom_collection=traitable_class.s_custom_collection,
871
+ __module__=traitable_class.__module__,
872
+ ),
873
+ )
874
+ if traitable_class.__name__ in module_dict:
875
+ assert history_class_name not in module_dict
876
+ module_dict[history_class_name] = history_class
877
+ return history_class
878
+
879
+
880
+ @dataclass
881
+ class AsOfContext:
882
+ """Context manager for time-based traitable loading.
883
+
884
+ Applies to the given traitable_classes and all their subclasses (discovered via
885
+ s_direct_subclasses). Default is [Traitable], so all storable Traitable subclasses
886
+ use AsOf resolution. s_storage_helper is resolved dynamically via __mro__ and
887
+ cached; the cache is invalidated on enter/exit for all relevant subclasses.
888
+ """
889
+
890
+ as_of_time: datetime
891
+ traitable_classes: list[type[Traitable]] = field(default_factory=lambda: [Traitable])
892
+
893
+ _btp: BTraitableProcessor | None = None
894
+
895
+ def __post_init__(self):
896
+ invalid_class = next((traitable_class for traitable_class in self.traitable_classes if not traitable_class.s_history_class), None)
897
+ if invalid_class and invalid_class is not Traitable: # we use Traitable as a special case to cover *all* traitables
898
+ raise ValueError(f'{invalid_class} is not storable or does not keep history')
899
+
900
+ def _reset_storage_helpers(self):
901
+ visited: set[type[Traitable]] = set()
902
+ stack = list(self.traitable_classes)
903
+ while stack:
904
+ traitable_class = stack.pop()
905
+ if traitable_class in visited:
906
+ continue
907
+ visited.add(traitable_class)
908
+ traitable_class.s_storage_helper_cached = None
909
+ for sub in traitable_class.s_direct_subclasses:
910
+ stack.append(sub)
911
+
912
+ def __enter__(self):
913
+ self._reset_storage_helpers()
914
+ for traitable_class in self.traitable_classes:
915
+ traitable_class.s_storage_helper_cached = StorableHelperAsOf(traitable_class, self.as_of_time)
916
+
917
+ btp = BTraitableProcessor.create_root()
918
+ btp.begin_using()
919
+ self._btp = btp
920
+ return self
921
+
922
+ def __exit__(self, exc_type, exc_val, exc_tb):
923
+ if self._btp is not None:
924
+ self._btp.end_using()
925
+ self._btp = None
926
+
927
+ self._reset_storage_helpers()
928
+
929
+
930
+ class traitable_trait(concrete_traits.nucleus_trait, data_type=Traitable, base_class=True):
931
+ def post_ctor(self): ...
932
+
933
+ def check_integrity(self, cls, rc: RC):
934
+ is_anonymous = issubclass(self.data_type, AnonymousTraitable)
935
+ is_runtime = self.flags_on(T.RUNTIME)
936
+ if self.flags_on(T.EMBEDDED):
937
+ if is_runtime:
938
+ rc.add_error(f'{cls.__name__}.{self.name} - cannot be both RUNTIME adn EMBEDDED')
939
+ if not is_anonymous:
940
+ rc.add_error(f'{cls.__name__}.{self.name} - EMBEDDED traitable must be a subclass of AnonymousTraitable')
941
+ else:
942
+ if is_anonymous and not is_runtime:
943
+ rc.add_error(f'{cls.__name__}.{self.name} - may not have a reference to AnonymousTraitable (the trait must be T.EMBEDDED)')
944
+
945
+ def default_value(self):
946
+ def_value = self.default
947
+ if def_value is XNone:
948
+ return def_value
949
+
950
+ if isinstance(def_value, str): # -- id
951
+ return self.data_type.instance_by_id(def_value)
952
+
953
+ if isinstance(def_value, dict): # -- trait values
954
+ return self.data_type(**def_value)
955
+
956
+ raise ValueError(f'{self.data_type} - may not be constructed from {def_value}')
957
+
958
+ def from_str(self, s: str):
959
+ return self.data_type.from_str(s)
960
+
961
+ def from_any_xstr(self, value):
962
+ if not isinstance(value, dict):
963
+ return None
964
+
965
+ return self.data_type(**value)
966
+
967
+
968
+ class NamedTsStore(Traitable):
969
+ # fmt: off
970
+ logical_name: str = T(T.ID)
971
+ uri: str = T()
972
+ # fmt: on
973
+
974
+
975
+ class TsClassAssociation(Traitable):
976
+ # fmt: off
977
+ py_canonical_name: str = T(T.ID)
978
+ ts_logical_name: str = T(Ui.choice('Store Name'))
979
+ # fmt: on
980
+
981
+ def ts_logical_name_choices(self, trait) -> tuple:
982
+ return tuple(nts.logical_name for nts in NamedTsStore.load_many())
983
+
984
+ @classmethod
985
+ @cache
986
+ def store_per_class(cls) -> TsStore:
987
+ return Traitable.main_store()
988
+
989
+ @classmethod
990
+ def ts_uri(cls, traitable_class) -> str:
991
+ # -- 1) Check TsStore association for the class itself, its module and packages
992
+ canonical_name = PyClass.name(traitable_class)
993
+ while True:
994
+ association = cls.existing_instance(py_canonical_name=canonical_name, _throw=False)
995
+ if association:
996
+ named_store = NamedTsStore.existing_instance(logical_name=association.ts_logical_name)
997
+ return named_store.uri
998
+
999
+ parts = canonical_name.rsplit('.', maxsplit=1)
1000
+ name = parts[0]
1001
+ if name == canonical_name: # -- checked all packages bottom up
1002
+ break # -- no asscciation for the class. module, packages
1003
+
1004
+ canonical_name = name
1005
+
1006
+ # -- 2) Check TsStore association for the class' parent Traitables
1007
+ parent_classes = traitable_class.__mro__
1008
+ for pclass in parent_classes[1:]:
1009
+ if issubclass(pclass, Traitable):
1010
+ canonical_name = PyClass.name(pclass)
1011
+ association = cls.existing_instance(py_canonical_name=canonical_name, _throw=False)
1012
+ if association:
1013
+ named_store = NamedTsStore.existing_instance(logical_name=association.ts_logical_name)
1014
+ return named_store.uri
1015
+
1016
+ return ''
1017
+
1018
+
1019
+ class Bundle(Traitable):
1020
+ s_bundle_base = None
1021
+ s_bundle_members: dict = None
1022
+
1023
+ def __init_subclass__(cls, members_known=False, **kwargs):
1024
+ super().__init_subclass__(**kwargs)
1025
+ if not cls.s_bundle_base:
1026
+ cls.s_bundle_base = cls
1027
+ if members_known:
1028
+ cls.s_bundle_members = {}
1029
+ else:
1030
+ assert cls.is_storable(), f'{cls} is not storable'
1031
+ base = cls.s_bundle_base
1032
+ if base:
1033
+ bundle_members = base.s_bundle_members
1034
+ if bundle_members is not None:
1035
+ bundle_members[cls.__name__] = cls
1036
+
1037
+ # cls.collection_name = base.collection_name #TODO: fix
1038
+ cls.collection = base.collection
1039
+ assert cls.s_bundle_base, 'bundle base not defined'
1040
+
1041
+ @classmethod
1042
+ def serialize_class_id(cls) -> str:
1043
+ if cls.s_bundle_members is None: # -- members unknown
1044
+ return PackageRefactoring.find_class_id(cls)
1045
+ else:
1046
+ return cls.__name__
1047
+
1048
+ @classmethod
1049
+ def deserialize_class_id(cls, serialized_class_id: str):
1050
+ if not serialized_class_id:
1051
+ raise ValueError('missing serialized class ID')
1052
+
1053
+ if cls.s_bundle_members is None: # -- members are not known - class_id is a real class_id
1054
+ return PackageRefactoring.find_class(serialized_class_id)
1055
+
1056
+ # -- class_id is a short class name
1057
+ actual_class = cls.s_bundle_members.get(serialized_class_id)
1058
+ if not actual_class:
1059
+ raise ValueError(f'{cls}: unknown bundle member {serialized_class_id}')
1060
+
1061
+ return actual_class
1062
+
1063
+
1064
+ class AnonymousTraitable(Traitable):
1065
+ _me = True
1066
+
1067
+ @classmethod
1068
+ def check_integrity(cls, rc: RC):
1069
+ if cls._me:
1070
+ cls._me = False
1071
+ return
1072
+
1073
+ if cls is not AnonymousTraitable:
1074
+ if not cls.is_storable():
1075
+ rc.add_error(f'{cls} - anonymous traitable must be storable')
1076
+
1077
+ if cls.is_id_endogenous():
1078
+ rc.add_error(f'{cls} - anonymous traitable may not have ID traits')
1079
+
1080
+ @classmethod
1081
+ def collection(cls, _coll_name: str = None):
1082
+ raise AssertionError('AnonymousTraitable may not have a collection')