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
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')
|