py10x-universe 0.1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- core_10x/__init__.py +42 -0
- core_10x/backbone/__init__.py +0 -0
- core_10x/backbone/backbone_store.py +59 -0
- core_10x/backbone/backbone_traitable.py +30 -0
- core_10x/backbone/backbone_user.py +66 -0
- core_10x/backbone/bound_data_domain.py +49 -0
- core_10x/backbone/namespace.py +101 -0
- core_10x/backbone/vault.py +38 -0
- core_10x/code_samples/__init__.py +0 -0
- core_10x/code_samples/_package_manifest.py +3 -0
- core_10x/code_samples/directories.py +181 -0
- core_10x/code_samples/person.py +76 -0
- core_10x/concrete_traits.py +356 -0
- core_10x/conftest.py +12 -0
- core_10x/curve.py +321 -0
- core_10x/data_domain.py +48 -0
- core_10x/data_domain_binder.py +45 -0
- core_10x/directory.py +250 -0
- core_10x/entity.py +8 -0
- core_10x/entity_filter.py +5 -0
- core_10x/environment_variables.py +147 -0
- core_10x/exec_control.py +84 -0
- core_10x/experimental/__init__.py +0 -0
- core_10x/experimental/data_protocol_ex.py +34 -0
- core_10x/global_cache.py +121 -0
- core_10x/manual_tests/__init__.py +0 -0
- core_10x/manual_tests/calendar_test.py +35 -0
- core_10x/manual_tests/ctor_update_bug.py +58 -0
- core_10x/manual_tests/debug_graph_on.py +17 -0
- core_10x/manual_tests/debug_graphoff_inside_graph_on.py +28 -0
- core_10x/manual_tests/enum_bits_test.py +17 -0
- core_10x/manual_tests/env_vars_trivial_test.py +12 -0
- core_10x/manual_tests/existing_traitable.py +33 -0
- core_10x/manual_tests/k10x_test1.py +13 -0
- core_10x/manual_tests/named_constant_test.py +121 -0
- core_10x/manual_tests/nucleus_trivial_test.py +42 -0
- core_10x/manual_tests/polars_test.py +14 -0
- core_10x/manual_tests/py_class_test.py +4 -0
- core_10x/manual_tests/rc_test.py +42 -0
- core_10x/manual_tests/rdate_test.py +12 -0
- core_10x/manual_tests/reference_serialization_bug.py +19 -0
- core_10x/manual_tests/resource_trivial_test.py +10 -0
- core_10x/manual_tests/store_uri_test.py +6 -0
- core_10x/manual_tests/trait_definition_test.py +19 -0
- core_10x/manual_tests/trait_filter_test.py +15 -0
- core_10x/manual_tests/trait_flag_modification_test.py +42 -0
- core_10x/manual_tests/trait_modification_bug.py +26 -0
- core_10x/manual_tests/traitable_as_of_test.py +82 -0
- core_10x/manual_tests/traitable_heir_test.py +39 -0
- core_10x/manual_tests/traitable_history_test.py +41 -0
- core_10x/manual_tests/traitable_serialization_test.py +54 -0
- core_10x/manual_tests/traitable_trivial_test.py +71 -0
- core_10x/manual_tests/trivial_graph_test.py +16 -0
- core_10x/manual_tests/ts_class_association_test.py +64 -0
- core_10x/manual_tests/ts_trivial_test.py +35 -0
- core_10x/named_constant.py +425 -0
- core_10x/nucleus.py +81 -0
- core_10x/package_manifest.py +85 -0
- core_10x/package_refactoring.py +153 -0
- core_10x/py_class.py +431 -0
- core_10x/rc.py +155 -0
- core_10x/rdate.py +339 -0
- core_10x/resource.py +189 -0
- core_10x/roman_number.py +67 -0
- core_10x/testlib/__init__.py +0 -0
- core_10x/testlib/test_store.py +240 -0
- core_10x/testlib/traitable_history_tests.py +787 -0
- core_10x/testlib/ts_tests.py +280 -0
- core_10x/trait.py +377 -0
- core_10x/trait_definition.py +176 -0
- core_10x/trait_filter.py +205 -0
- core_10x/trait_method_error.py +36 -0
- core_10x/traitable.py +1082 -0
- core_10x/traitable_cli.py +153 -0
- core_10x/traitable_heir.py +33 -0
- core_10x/traitable_id.py +31 -0
- core_10x/ts_store.py +172 -0
- core_10x/ts_store_type.py +26 -0
- core_10x/ts_union.py +147 -0
- core_10x/ui_hint.py +153 -0
- core_10x/unit_tests/test_concrete_traits.py +156 -0
- core_10x/unit_tests/test_converters.py +51 -0
- core_10x/unit_tests/test_curve.py +157 -0
- core_10x/unit_tests/test_directory.py +54 -0
- core_10x/unit_tests/test_documentation.py +172 -0
- core_10x/unit_tests/test_environment_variables.py +15 -0
- core_10x/unit_tests/test_filters.py +239 -0
- core_10x/unit_tests/test_graph.py +348 -0
- core_10x/unit_tests/test_named_constant.py +98 -0
- core_10x/unit_tests/test_rc.py +11 -0
- core_10x/unit_tests/test_rdate.py +484 -0
- core_10x/unit_tests/test_trait_method_error.py +80 -0
- core_10x/unit_tests/test_trait_modification.py +19 -0
- core_10x/unit_tests/test_traitable.py +959 -0
- core_10x/unit_tests/test_traitable_history.py +1 -0
- core_10x/unit_tests/test_ts_store.py +1 -0
- core_10x/unit_tests/test_ts_union.py +369 -0
- core_10x/unit_tests/test_ui_nodes.py +81 -0
- core_10x/unit_tests/test_xxcalendar.py +471 -0
- core_10x/vault/__init__.py +0 -0
- core_10x/vault/sec_keys.py +133 -0
- core_10x/vault/security_keys_old.py +168 -0
- core_10x/vault/vault.py +56 -0
- core_10x/vault/vault_traitable.py +56 -0
- core_10x/vault/vault_user.py +70 -0
- core_10x/xdate_time.py +136 -0
- core_10x/xnone.py +71 -0
- core_10x/xxcalendar.py +228 -0
- infra_10x/__init__.py +0 -0
- infra_10x/manual_tests/__init__.py +0 -0
- infra_10x/manual_tests/test_misc.py +16 -0
- infra_10x/manual_tests/test_prepare_filter_and_pipeline.py +25 -0
- infra_10x/mongodb_admin.py +111 -0
- infra_10x/mongodb_store.py +346 -0
- infra_10x/mongodb_utils.py +129 -0
- infra_10x/unit_tests/conftest.py +13 -0
- infra_10x/unit_tests/test_mongo_db.py +36 -0
- infra_10x/unit_tests/test_mongo_history.py +1 -0
- py10x_universe-0.1.3.dist-info/METADATA +406 -0
- py10x_universe-0.1.3.dist-info/RECORD +214 -0
- py10x_universe-0.1.3.dist-info/WHEEL +4 -0
- py10x_universe-0.1.3.dist-info/licenses/LICENSE +21 -0
- ui_10x/__init__.py +0 -0
- ui_10x/apps/__init__.py +0 -0
- ui_10x/apps/collection_editor_app.py +100 -0
- ui_10x/choice.py +212 -0
- ui_10x/collection_editor.py +135 -0
- ui_10x/concrete_trait_widgets.py +220 -0
- ui_10x/conftest.py +8 -0
- ui_10x/entity_stocker.py +173 -0
- ui_10x/examples/__init__.py +0 -0
- ui_10x/examples/_guess_word_data.py +14076 -0
- ui_10x/examples/collection_editor.py +17 -0
- ui_10x/examples/date_selector.py +14 -0
- ui_10x/examples/entity_stocker.py +18 -0
- ui_10x/examples/guess_word.py +392 -0
- ui_10x/examples/message_box.py +20 -0
- ui_10x/examples/multi_choice.py +17 -0
- ui_10x/examples/py_data_browser.py +66 -0
- ui_10x/examples/radiobox.py +29 -0
- ui_10x/examples/single_choice.py +31 -0
- ui_10x/examples/style_sheet.py +47 -0
- ui_10x/examples/trivial_entity_editor.py +18 -0
- ui_10x/platform.py +20 -0
- ui_10x/platform_interface.py +517 -0
- ui_10x/py_data_browser.py +249 -0
- ui_10x/qt6/__init__.py +0 -0
- ui_10x/qt6/conftest.py +8 -0
- ui_10x/qt6/manual_tests/__init__.py +0 -0
- ui_10x/qt6/manual_tests/basic_test.py +35 -0
- ui_10x/qt6/platform_implementation.py +275 -0
- ui_10x/qt6/utils.py +665 -0
- ui_10x/rio/__init__.py +0 -0
- ui_10x/rio/apps/examples/examples/__init__.py +22 -0
- ui_10x/rio/apps/examples/examples/components/__init__.py +3 -0
- ui_10x/rio/apps/examples/examples/components/collection_editor.py +15 -0
- ui_10x/rio/apps/examples/examples/pages/collection_editor.py +21 -0
- ui_10x/rio/apps/examples/examples/pages/login_page.py +88 -0
- ui_10x/rio/apps/examples/examples/pages/style_sheet.py +21 -0
- ui_10x/rio/apps/examples/rio.toml +14 -0
- ui_10x/rio/component_builder.py +497 -0
- ui_10x/rio/components/__init__.py +9 -0
- ui_10x/rio/components/group_box.py +31 -0
- ui_10x/rio/components/labeled_checkbox.py +18 -0
- ui_10x/rio/components/line_edit.py +37 -0
- ui_10x/rio/components/radio_button.py +32 -0
- ui_10x/rio/components/separator.py +24 -0
- ui_10x/rio/components/splitter.py +121 -0
- ui_10x/rio/components/tree_view.py +75 -0
- ui_10x/rio/conftest.py +35 -0
- ui_10x/rio/internals/__init__.py +0 -0
- ui_10x/rio/internals/app.py +192 -0
- ui_10x/rio/manual_tests/__init__.py +0 -0
- ui_10x/rio/manual_tests/basic_test.py +24 -0
- ui_10x/rio/manual_tests/splitter.py +27 -0
- ui_10x/rio/platform_implementation.py +91 -0
- ui_10x/rio/style_sheet.py +53 -0
- ui_10x/rio/unit_tests/test_collection_editor.py +68 -0
- ui_10x/rio/unit_tests/test_internals.py +630 -0
- ui_10x/rio/unit_tests/test_style_sheet.py +37 -0
- ui_10x/rio/widgets/__init__.py +46 -0
- ui_10x/rio/widgets/application.py +109 -0
- ui_10x/rio/widgets/button.py +48 -0
- ui_10x/rio/widgets/button_group.py +60 -0
- ui_10x/rio/widgets/calendar.py +23 -0
- ui_10x/rio/widgets/checkbox.py +24 -0
- ui_10x/rio/widgets/dialog.py +137 -0
- ui_10x/rio/widgets/group_box.py +27 -0
- ui_10x/rio/widgets/layout.py +34 -0
- ui_10x/rio/widgets/line_edit.py +37 -0
- ui_10x/rio/widgets/list.py +105 -0
- ui_10x/rio/widgets/message_box.py +70 -0
- ui_10x/rio/widgets/scroll_area.py +31 -0
- ui_10x/rio/widgets/spacer.py +6 -0
- ui_10x/rio/widgets/splitter.py +45 -0
- ui_10x/rio/widgets/text_edit.py +28 -0
- ui_10x/rio/widgets/tree.py +89 -0
- ui_10x/rio/widgets/unit_tests/test_button.py +101 -0
- ui_10x/rio/widgets/unit_tests/test_button_group.py +33 -0
- ui_10x/rio/widgets/unit_tests/test_calendar.py +114 -0
- ui_10x/rio/widgets/unit_tests/test_checkbox.py +109 -0
- ui_10x/rio/widgets/unit_tests/test_group_box.py +158 -0
- ui_10x/rio/widgets/unit_tests/test_label.py +43 -0
- ui_10x/rio/widgets/unit_tests/test_line_edit.py +140 -0
- ui_10x/rio/widgets/unit_tests/test_list.py +146 -0
- ui_10x/table_header_view.py +305 -0
- ui_10x/table_view.py +174 -0
- ui_10x/trait_editor.py +189 -0
- ui_10x/trait_widget.py +131 -0
- ui_10x/traitable_editor.py +200 -0
- ui_10x/traitable_view.py +131 -0
- ui_10x/unit_tests/conftest.py +8 -0
- ui_10x/unit_tests/test_platform.py +9 -0
- ui_10x/utils.py +661 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
from core_10x.traitable import RC, RC_TRUE, Traitable
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TraitableCli(Traitable):
|
|
7
|
+
s_master = None
|
|
8
|
+
s_switch = {}
|
|
9
|
+
|
|
10
|
+
def __init_subclass__(cls, _command: str = None, **kwargs):
|
|
11
|
+
cls.s_switch = {}
|
|
12
|
+
if cls.s_master is None: # -- master parser
|
|
13
|
+
assert _command is None, 'master may not have a command'
|
|
14
|
+
cls.s_master = cls
|
|
15
|
+
else: # -- subordinate parser
|
|
16
|
+
assert _command and _command.isidentifier(), 'command must be a valid identifier'
|
|
17
|
+
cls.s_master.s_switch[_command] = cls
|
|
18
|
+
cls.s_master = cls
|
|
19
|
+
|
|
20
|
+
super().__init_subclass__(**kwargs)
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def from_command_line(cls) -> tuple: # -- (RC, Traitable)
|
|
24
|
+
"""
|
|
25
|
+
Creates an instance of target traitable parsing positional args to obtain the right target class and then taking trait values
|
|
26
|
+
in the name = value pairs form.
|
|
27
|
+
One can use any spacing around symbol '=', i.e.: name=value name= value name =value or name = value
|
|
28
|
+
|
|
29
|
+
:return: (rc, traitable) - RC and a traitable according to args and trait values parsed from sys.argv.
|
|
30
|
+
rc holds error message(s) if instantiation fails for any reason (traitable is set to None)
|
|
31
|
+
"""
|
|
32
|
+
_script, *args = sys.argv
|
|
33
|
+
return cls.instance_from_args(args)
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def instance_from_args(cls, input_args: tuple) -> tuple:
|
|
37
|
+
args = []
|
|
38
|
+
trait_values = {}
|
|
39
|
+
rc = cls.parse(input_args, args, trait_values)
|
|
40
|
+
if not rc:
|
|
41
|
+
return rc, None
|
|
42
|
+
|
|
43
|
+
return cls.instantiate(args, trait_values)
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def instantiate(cls, args, trait_values: dict) -> tuple: # -- (RC, target_traitable)
|
|
47
|
+
if not args:
|
|
48
|
+
try:
|
|
49
|
+
res = cls()
|
|
50
|
+
for name, value in trait_values.items():
|
|
51
|
+
trait = cls.trait(name)
|
|
52
|
+
if not trait:
|
|
53
|
+
return RC(False, f'unknown attribute {name}\nValid attributes: {", ".join(cls.s_dir)}'), None
|
|
54
|
+
|
|
55
|
+
rc = res.set_value(trait, value)
|
|
56
|
+
if not rc:
|
|
57
|
+
return RC(False, f'{name}: {rc.err()}'), None
|
|
58
|
+
|
|
59
|
+
return RC_TRUE, res
|
|
60
|
+
|
|
61
|
+
except Exception as ex:
|
|
62
|
+
return RC(False, str(ex)), None
|
|
63
|
+
|
|
64
|
+
command, *args = args
|
|
65
|
+
parser = cls.s_switch.get(command)
|
|
66
|
+
if not parser or not issubclass(parser, TraitableCli):
|
|
67
|
+
return RC(False, f'Unknown argument {command}'), None
|
|
68
|
+
|
|
69
|
+
return parser.instantiate(args, trait_values)
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def parse(cls, input_args: tuple, args: list, trait_values: dict) -> RC: # noqa: C901
|
|
73
|
+
if not input_args:
|
|
74
|
+
return RC_TRUE
|
|
75
|
+
|
|
76
|
+
trait_name = None
|
|
77
|
+
deal_with_args = True
|
|
78
|
+
new_vp = True
|
|
79
|
+
eq_expected = False
|
|
80
|
+
for i, arg in enumerate(input_args):
|
|
81
|
+
if deal_with_args:
|
|
82
|
+
parts = arg.split('=', 1)
|
|
83
|
+
n = len(parts)
|
|
84
|
+
if n == 1:
|
|
85
|
+
args.append(arg)
|
|
86
|
+
else:
|
|
87
|
+
deal_with_args = False
|
|
88
|
+
eq_expected = False
|
|
89
|
+
first, second = parts
|
|
90
|
+
if not first and not second: # -- just '='
|
|
91
|
+
if i:
|
|
92
|
+
trait_name = args.pop(-1)
|
|
93
|
+
new_vp = False
|
|
94
|
+
else:
|
|
95
|
+
return RC(False, 'May not start with "="')
|
|
96
|
+
|
|
97
|
+
elif first:
|
|
98
|
+
if not second: # -- xxx=
|
|
99
|
+
trait_name = first
|
|
100
|
+
new_vp = False
|
|
101
|
+
else: # -- xxx=yyy
|
|
102
|
+
trait_values[first] = second
|
|
103
|
+
new_vp = True
|
|
104
|
+
else: # -- =yyy
|
|
105
|
+
if i:
|
|
106
|
+
trait_name = args.pop(-1)
|
|
107
|
+
trait_values[trait_name] = second
|
|
108
|
+
new_vp = True
|
|
109
|
+
else:
|
|
110
|
+
return RC(False, 'May not start with "="')
|
|
111
|
+
|
|
112
|
+
else:
|
|
113
|
+
parts = arg.split('=', 1)
|
|
114
|
+
n = len(parts)
|
|
115
|
+
if new_vp:
|
|
116
|
+
trait_name = parts[0]
|
|
117
|
+
if n == 1:
|
|
118
|
+
eq_expected = True
|
|
119
|
+
new_vp = False
|
|
120
|
+
else:
|
|
121
|
+
value = parts[1]
|
|
122
|
+
if value:
|
|
123
|
+
trait_values[trait_name] = value
|
|
124
|
+
new_vp = True
|
|
125
|
+
else:
|
|
126
|
+
eq_expected = False
|
|
127
|
+
new_vp = False
|
|
128
|
+
else:
|
|
129
|
+
if not eq_expected:
|
|
130
|
+
if n == 2:
|
|
131
|
+
return RC(False, f'Invalid "=": {input_args[i - 1]} {arg}')
|
|
132
|
+
|
|
133
|
+
trait_values[trait_name] = parts[0]
|
|
134
|
+
new_vp = True
|
|
135
|
+
|
|
136
|
+
else:
|
|
137
|
+
if n == 1:
|
|
138
|
+
return RC(False, f'"=" is expected before {arg}')
|
|
139
|
+
|
|
140
|
+
if parts[0]:
|
|
141
|
+
return RC(False, f'{input_args[i - 1]} {arg}')
|
|
142
|
+
|
|
143
|
+
if parts[1]:
|
|
144
|
+
trait_values[trait_name] = parts[1]
|
|
145
|
+
new_vp = True
|
|
146
|
+
|
|
147
|
+
else:
|
|
148
|
+
eq_expected = False
|
|
149
|
+
|
|
150
|
+
if not new_vp:
|
|
151
|
+
return RC(False, f'Value is missing for {trait_name} = ')
|
|
152
|
+
|
|
153
|
+
return RC_TRUE
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from core_10x.traitable import Traitable, Trait, T, RT, RC, XNone
|
|
2
|
+
|
|
3
|
+
class TraitableHeir(Traitable):
|
|
4
|
+
_grantor: Traitable = T()
|
|
5
|
+
|
|
6
|
+
s_grantor_traits = {}
|
|
7
|
+
def __init_subclass__(cls, **kwargs):
|
|
8
|
+
super().__init_subclass__(**kwargs)
|
|
9
|
+
|
|
10
|
+
cls.s_grantor_traits = dir = {}
|
|
11
|
+
grantor_trait = cls.trait('_grantor')
|
|
12
|
+
trait: Trait
|
|
13
|
+
for trait in cls.traits():
|
|
14
|
+
if trait is grantor_trait or trait.flags_on(T.RESERVED):
|
|
15
|
+
continue
|
|
16
|
+
if not trait.has_custom_getter() and trait.default is XNone: #-- trait has neither getter nor default value
|
|
17
|
+
trait.set_f_get(lambda self, trait_name = trait.name: self.heir_getter(trait_name), False)
|
|
18
|
+
dir[trait.name] = trait
|
|
19
|
+
|
|
20
|
+
def heir_getter(self, trait_name: str):
|
|
21
|
+
grantor = self._grantor
|
|
22
|
+
if not grantor:
|
|
23
|
+
return XNone
|
|
24
|
+
trait = grantor.__class__.trait(trait_name)
|
|
25
|
+
return grantor.get_value(trait) if trait else XNone
|
|
26
|
+
|
|
27
|
+
def serialize_object(self, save_references = False) -> dict:
|
|
28
|
+
serialized_data = super().serialize_object(save_references = save_references)
|
|
29
|
+
for name, trait in self.__class__.s_grantor_traits.items():
|
|
30
|
+
if not self.is_set(trait):
|
|
31
|
+
del serialized_data[name]
|
|
32
|
+
|
|
33
|
+
return serialized_data
|
core_10x/traitable_id.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from functools import total_ordering
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@total_ordering
|
|
7
|
+
class ID:
|
|
8
|
+
__slots__ = ('collection_name', 'value')
|
|
9
|
+
|
|
10
|
+
def __init__(self, id_value: str = None, collection_name: str = None):
|
|
11
|
+
self.value = id_value
|
|
12
|
+
self.collection_name = collection_name
|
|
13
|
+
|
|
14
|
+
def __eq__(self, other: ID):
|
|
15
|
+
if not isinstance(other, ID):
|
|
16
|
+
return NotImplemented
|
|
17
|
+
return self.value == other.value and self.collection_name == other.collection_name
|
|
18
|
+
|
|
19
|
+
def __bool__(self):
|
|
20
|
+
return bool(self.value)
|
|
21
|
+
|
|
22
|
+
def __repr__(self):
|
|
23
|
+
return f'{self.value}' if not self.collection_name else f'{self.collection_name}/{self.value}'
|
|
24
|
+
|
|
25
|
+
def __hash__(self):
|
|
26
|
+
return hash((self.value, self.collection_name))
|
|
27
|
+
|
|
28
|
+
def __lt__(self, other: ID) -> bool:
|
|
29
|
+
if not isinstance(other, ID):
|
|
30
|
+
return NotImplemented
|
|
31
|
+
return (self.collection_name, self.value) < (other.collection_name, other.value)
|
core_10x/ts_store.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import abc
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from core_10x.exec_control import ProcessContext
|
|
7
|
+
from core_10x.global_cache import standard_key
|
|
8
|
+
from core_10x.py_class import PyClass
|
|
9
|
+
from core_10x.rc import RC
|
|
10
|
+
from core_10x.resource import TS_STORE, Resource, ResourceSpec
|
|
11
|
+
from core_10x.trait_filter import f
|
|
12
|
+
from core_10x.ts_store_type import TS_STORE_TYPE
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from collections.abc import Iterable
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TsDuplicateKeyError(Exception):
|
|
19
|
+
"""Raised when attempting to insert a document with a duplicate key."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, collection_name: str, duplicate_key: dict):
|
|
22
|
+
super().__init__(f'Duplicate key error collection {collection_name} dup key: {duplicate_key} was found while insert was attempted.')
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TsCollection(abc.ABC):
|
|
26
|
+
s_id_tag: str = None
|
|
27
|
+
|
|
28
|
+
@abc.abstractmethod
|
|
29
|
+
def collection_name(self) -> str: ...
|
|
30
|
+
@abc.abstractmethod
|
|
31
|
+
def id_exists(self, id_value: str) -> bool: ...
|
|
32
|
+
@abc.abstractmethod
|
|
33
|
+
def find(self, query: f = None, _at_most: int = 0, _order: dict = None) -> Iterable: ...
|
|
34
|
+
@abc.abstractmethod
|
|
35
|
+
def count(self, query: f = None) -> int: ...
|
|
36
|
+
@abc.abstractmethod
|
|
37
|
+
def save_new(self, serialized_traitable: dict, overwrite: bool = False) -> int: ...
|
|
38
|
+
@abc.abstractmethod
|
|
39
|
+
def save(self, serialized_traitable: dict) -> int: ...
|
|
40
|
+
@abc.abstractmethod
|
|
41
|
+
def delete(self, id_value: str) -> bool: ...
|
|
42
|
+
@abc.abstractmethod
|
|
43
|
+
def create_index(self, name: str, trait_name: str | list[tuple[str, int]], **index_args) -> str: ...
|
|
44
|
+
@abc.abstractmethod
|
|
45
|
+
def max(self, trait_name: str, filter: f = None) -> dict: ...
|
|
46
|
+
@abc.abstractmethod
|
|
47
|
+
def min(self, trait_name: str, filter: f = None) -> dict: ...
|
|
48
|
+
|
|
49
|
+
def exists(self, query: f) -> bool:
|
|
50
|
+
return self.count(query) > 0
|
|
51
|
+
|
|
52
|
+
def load(self, id_value: str) -> dict | None:
|
|
53
|
+
for data in self.find(f(**{self.s_id_tag: id_value})):
|
|
54
|
+
return data
|
|
55
|
+
|
|
56
|
+
def copy_to(self, to_coll: TsCollection, overwrite: bool = False) -> RC:
|
|
57
|
+
"""Copy all documents from this collection to another collection."""
|
|
58
|
+
rc = RC(True)
|
|
59
|
+
|
|
60
|
+
for doc in self.find():
|
|
61
|
+
try:
|
|
62
|
+
if not to_coll.save_new(doc, overwrite=overwrite):
|
|
63
|
+
rc.add_error(f'Failed to save {doc.get(to_coll.s_id_tag)} to {to_coll.collection_name()}')
|
|
64
|
+
except TsDuplicateKeyError:
|
|
65
|
+
if overwrite:
|
|
66
|
+
raise # -- we do not expect an exception in case of overwrite, so raise
|
|
67
|
+
|
|
68
|
+
return rc
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class TsStore(Resource, resource_type=TS_STORE):
|
|
72
|
+
PROTOCOL = ''
|
|
73
|
+
|
|
74
|
+
def __init_subclass__(cls, **kwargs):
|
|
75
|
+
super().__init_subclass__(**kwargs)
|
|
76
|
+
assert cls.__mro__[1] is TsStore, 'TsStore must be the first base class'
|
|
77
|
+
cls.s_instance_kwargs_map = {**TsStore.s_instance_kwargs_map, **cls.s_instance_kwargs_map}
|
|
78
|
+
|
|
79
|
+
@staticmethod
|
|
80
|
+
def store_class(store_class_name: str):
|
|
81
|
+
cls = PyClass.find(store_class_name, TsStore)
|
|
82
|
+
assert cls, f'Unknown TsStore class {store_class_name}'
|
|
83
|
+
return cls
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def standard_key(cls, *args, **kwargs) -> tuple:
|
|
87
|
+
return standard_key(args, kwargs)
|
|
88
|
+
|
|
89
|
+
s_instances = {}
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def spec_from_uri(cls, uri: str) -> ResourceSpec:
|
|
93
|
+
parts = uri.split(':', maxsplit=1)
|
|
94
|
+
protocol = parts[0]
|
|
95
|
+
ts_class = TS_STORE_TYPE.ts_store_class(protocol)
|
|
96
|
+
return ResourceSpec(ts_class, ts_class.parse_uri(uri))
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def instance(cls, *args, password: str = '', _cache: bool = True, **kwargs) -> TsStore:
|
|
100
|
+
translated_kwargs = cls.translate_kwargs(kwargs)
|
|
101
|
+
try:
|
|
102
|
+
if not _cache:
|
|
103
|
+
return cls.new_instance(*args, password=password, **translated_kwargs)
|
|
104
|
+
|
|
105
|
+
instance_key = cls.standard_key(*args, **kwargs)
|
|
106
|
+
store = cls.s_instances.get(instance_key)
|
|
107
|
+
if not store:
|
|
108
|
+
store = cls.new_instance(*args, password=password, **translated_kwargs)
|
|
109
|
+
cls.s_instances[instance_key] = store
|
|
110
|
+
|
|
111
|
+
return store
|
|
112
|
+
|
|
113
|
+
except Exception as e:
|
|
114
|
+
raise OSError(f'Failed to connect to {cls.s_driver_name}({args}, {translated_kwargs})\nOriginal Exception:\n{e!s}') from e
|
|
115
|
+
|
|
116
|
+
# fmt: off
|
|
117
|
+
s_instance_kwargs_map = {
|
|
118
|
+
Resource.HOSTNAME_TAG: (Resource.HOSTNAME_TAG, None),
|
|
119
|
+
Resource.USERNAME_TAG: (Resource.USERNAME_TAG, None),
|
|
120
|
+
Resource.DBNAME_TAG: (Resource.DBNAME_TAG, None),
|
|
121
|
+
Resource.PORT_TAG: (Resource.PORT_TAG, None),
|
|
122
|
+
Resource.SSL_TAG: (Resource.SSL_TAG, True),
|
|
123
|
+
'sst': ('sst', 1000),
|
|
124
|
+
}
|
|
125
|
+
# fmt: on
|
|
126
|
+
|
|
127
|
+
@classmethod
|
|
128
|
+
def translate_kwargs(cls, kwargs: dict) -> dict:
|
|
129
|
+
kwargs_map = cls.s_instance_kwargs_map
|
|
130
|
+
def_kwargs = {name: def_value for name, (real_name, def_value) in kwargs_map.items()}
|
|
131
|
+
def_kwargs.update(kwargs)
|
|
132
|
+
return {kwargs_map[name][0]: value for name, value in def_kwargs.items()}
|
|
133
|
+
|
|
134
|
+
@classmethod
|
|
135
|
+
def new_instance(cls, *args, password: str, **kwargs) -> TsStore:
|
|
136
|
+
raise NotImplementedError
|
|
137
|
+
|
|
138
|
+
@classmethod
|
|
139
|
+
def parse_uri(cls, uri: str) -> dict:
|
|
140
|
+
raise NotImplementedError
|
|
141
|
+
|
|
142
|
+
def on_enter(self):
|
|
143
|
+
self.bpc_flags = ProcessContext.reset_flags(ProcessContext.CACHE_ONLY)
|
|
144
|
+
|
|
145
|
+
def on_exit(self):
|
|
146
|
+
ProcessContext.replace_flags(self.bpc_flags)
|
|
147
|
+
|
|
148
|
+
@abc.abstractmethod
|
|
149
|
+
def collection_names(self, regexp: str = None) -> list: ...
|
|
150
|
+
|
|
151
|
+
@abc.abstractmethod
|
|
152
|
+
def collection(self, collection_name: str) -> TsCollection: ...
|
|
153
|
+
|
|
154
|
+
@abc.abstractmethod
|
|
155
|
+
def delete_collection(self, collection_name: str) -> bool: ...
|
|
156
|
+
|
|
157
|
+
@classmethod
|
|
158
|
+
def is_running_with_auth(cls, host_name: str) -> tuple: ... # -- (is_running, with_auth)
|
|
159
|
+
|
|
160
|
+
@abc.abstractmethod
|
|
161
|
+
def auth_user(self) -> str | None: ...
|
|
162
|
+
|
|
163
|
+
def copy_to(self, to_store: TsStore, overwrite: bool = False) -> RC:
|
|
164
|
+
"""Copy all collections from this store to another store."""
|
|
165
|
+
rc = RC(True)
|
|
166
|
+
|
|
167
|
+
for collection_name in self.collection_names():
|
|
168
|
+
from_coll = self.collection(collection_name)
|
|
169
|
+
to_coll = to_store.collection(collection_name)
|
|
170
|
+
rc += from_coll.copy_to(to_coll, overwrite=overwrite)
|
|
171
|
+
|
|
172
|
+
return rc
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from core_10x.named_constant import NamedConstant
|
|
2
|
+
from core_10x.py_class import PyClass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TS_STORE_TYPE(NamedConstant):
|
|
6
|
+
"""
|
|
7
|
+
All known TsStore subclasses must be "registered" here.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
MONGODB = 'infra_10x.mongodb_store.MongoStore'
|
|
11
|
+
...
|
|
12
|
+
|
|
13
|
+
@classmethod
|
|
14
|
+
def ts_store_class(cls, uri_protocol: str):
|
|
15
|
+
"""
|
|
16
|
+
example: TS_STORE_TYPE.ts_store_class('mongodb')
|
|
17
|
+
"""
|
|
18
|
+
symbol = cls.s_dir.get(uri_protocol.upper())
|
|
19
|
+
if symbol is None:
|
|
20
|
+
raise ValueError(f'Unknown URI protocol: {uri_protocol}')
|
|
21
|
+
|
|
22
|
+
ts_class = PyClass.find(symbol.value)
|
|
23
|
+
if not ts_class:
|
|
24
|
+
raise TypeError(f'{symbol.value} - unknown TsStore class')
|
|
25
|
+
|
|
26
|
+
return ts_class
|
core_10x/ts_union.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import heapq
|
|
4
|
+
import itertools
|
|
5
|
+
import operator
|
|
6
|
+
from itertools import zip_longest
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from core_10x.nucleus import Nucleus
|
|
10
|
+
from core_10x.trait_filter import EQ, f
|
|
11
|
+
from core_10x.ts_store import TsCollection, TsStore
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from collections.abc import Iterable
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _OrderKey:
|
|
18
|
+
__slots__ = ('reverse', 'value')
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def _dict_cmp(cls, d: dict, od: dict) -> int:
|
|
22
|
+
assert isinstance(od, dict), 'can only compare dict to dict'
|
|
23
|
+
|
|
24
|
+
for i, oi in zip_longest(d.items(), od.items()):
|
|
25
|
+
if i == oi:
|
|
26
|
+
continue
|
|
27
|
+
if i is None:
|
|
28
|
+
return -1 # all match, d is shorter
|
|
29
|
+
if oi is None:
|
|
30
|
+
return 1 # all match d is longer
|
|
31
|
+
k, v = i
|
|
32
|
+
ok, ov = oi
|
|
33
|
+
if k != ok:
|
|
34
|
+
return -1 if k < ok else 1 if k > ok else 0 # keys do not match
|
|
35
|
+
if isinstance(v, dict) and isinstance(ov, dict):
|
|
36
|
+
return cls._dict_cmp(v, ov)
|
|
37
|
+
# TODO: handle mismatching types..
|
|
38
|
+
return -1 if v < ov else 1 if v > ov else 0
|
|
39
|
+
return 0 # equals..
|
|
40
|
+
|
|
41
|
+
def __init__(self, value, reverse: bool):
|
|
42
|
+
self.value = value
|
|
43
|
+
self.reverse = reverse
|
|
44
|
+
|
|
45
|
+
def __lt__(self, other):
|
|
46
|
+
assert isinstance(other, _OrderKey), 'can only compare to order key'
|
|
47
|
+
v = self.value
|
|
48
|
+
ov = other.value
|
|
49
|
+
if isinstance(v, dict):
|
|
50
|
+
return self._dict_cmp(v, ov) < 0 if self.reverse else self._dict_cmp(ov, v) > 0
|
|
51
|
+
return ov < v if self.reverse else v < ov
|
|
52
|
+
|
|
53
|
+
def __eq__(self, other):
|
|
54
|
+
assert isinstance(other, _OrderKey), 'can only compare to order key'
|
|
55
|
+
v = self.value
|
|
56
|
+
ov = other.value
|
|
57
|
+
return not self._dict_cmp(v, ov) if isinstance(v, dict) else v == ov
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def key(cls, item, order):
|
|
61
|
+
return tuple(cls(value=item.get(k), reverse=v < 0) for k, v in order)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class TsUnionCollection(TsCollection):
|
|
65
|
+
def __init__(self, *collections: TsCollection):
|
|
66
|
+
self.collections = collections
|
|
67
|
+
|
|
68
|
+
def collection_name(self) -> str:
|
|
69
|
+
return self.collections[0].collection_name() if self.collections else ''
|
|
70
|
+
|
|
71
|
+
def find(self, query: f = None, _at_most: int = 0, _order: dict = None) -> Iterable:
|
|
72
|
+
order = tuple((_order or {'_id': 1}).items()) # FIX: assumes _id is always there!
|
|
73
|
+
order_key = _OrderKey.key
|
|
74
|
+
iterables = (collection.find(query, _at_most=_at_most, _order=_order) for collection in self.collections)
|
|
75
|
+
keyed_iterables = (((order_key(item, order), item) for item in iterable) for iterable in iterables if iterable is not None)
|
|
76
|
+
results = (item for _, item in heapq.merge(*keyed_iterables, key=operator.itemgetter(0)))
|
|
77
|
+
return (item for i, item in enumerate(results) if i < _at_most) if _at_most else results
|
|
78
|
+
|
|
79
|
+
def save_new(self, serialized_traitable: dict, overwrite: bool = False) -> int:
|
|
80
|
+
return self.collections[0].save_new(serialized_traitable, overwrite=overwrite)
|
|
81
|
+
|
|
82
|
+
def save(self, serialized_traitable):
|
|
83
|
+
# if serialized_traitable was not loaded from the union head, we need to call save_new
|
|
84
|
+
id_tag = Nucleus.ID_TAG()
|
|
85
|
+
if not self.collections[0].exists(f(**{id_tag: EQ(serialized_traitable[id_tag])})):
|
|
86
|
+
# TODO: optimize by introducing save with overwrite flag to bypass the "inappropriate restore from deleted" check
|
|
87
|
+
return self.collections[0].save_new(serialized_traitable)
|
|
88
|
+
return self.collections[0].save(serialized_traitable)
|
|
89
|
+
|
|
90
|
+
def delete(self, id_value):
|
|
91
|
+
# if id_value exists in the union tail, return False as the object wasn't fully deleted
|
|
92
|
+
return self.collections[0].delete(id_value) and not self.exists(f(**{Nucleus.ID_TAG(): EQ(id_value)}))
|
|
93
|
+
|
|
94
|
+
def create_index(self, name: str, trait_name: str | list[tuple[str, int]], **index_args) -> str:
|
|
95
|
+
# TODO: verify that index exists in the union tail
|
|
96
|
+
return self.collections[0].create_index(name, trait_name, **index_args)
|
|
97
|
+
|
|
98
|
+
def max(self, trait_name: str, filter: f = None) -> dict:
|
|
99
|
+
results = (result for result in (collection.max(trait_name, filter) for collection in self.collections) if result is not None)
|
|
100
|
+
return max(results, key=operator.itemgetter(trait_name), default=None)
|
|
101
|
+
|
|
102
|
+
def min(self, trait_name: str, filter: f = None) -> dict:
|
|
103
|
+
results = (result for result in (collection.min(trait_name, filter) for collection in self.collections) if result is not None)
|
|
104
|
+
return min(results, key=operator.itemgetter(trait_name), default=None)
|
|
105
|
+
|
|
106
|
+
def id_exists(self, id_value: str) -> bool:
|
|
107
|
+
return any(collection.id_exists(id_value) for collection in self.collections)
|
|
108
|
+
|
|
109
|
+
def count(self, query: f = None) -> int:
|
|
110
|
+
return sum(collection.count(query) for collection in self.collections)
|
|
111
|
+
|
|
112
|
+
def load(self, id_value: str) -> dict | None:
|
|
113
|
+
for collection in self.collections:
|
|
114
|
+
data = collection.load(id_value)
|
|
115
|
+
if data is not None:
|
|
116
|
+
return data
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class TsUnion(TsStore, resource_name='TS_UNION'):
|
|
121
|
+
@classmethod
|
|
122
|
+
def is_running_with_auth(cls, host_name: str) -> tuple: # -- (is_running, with_auth)
|
|
123
|
+
raise NotImplementedError
|
|
124
|
+
|
|
125
|
+
@classmethod
|
|
126
|
+
def standard_key(cls, *args) -> tuple:
|
|
127
|
+
return tuple(cls.s_resource_type.resource_driver(kw['driver_name']).standard_key(**kw) for kw in args)
|
|
128
|
+
|
|
129
|
+
@classmethod
|
|
130
|
+
def new_instance(cls, *args, **kwargs) -> TsUnion:
|
|
131
|
+
stores = [cls.s_resource_type.resource_driver(kw.pop('driver_name')).instance(**kw) for kw in args]
|
|
132
|
+
return TsUnion(*stores)
|
|
133
|
+
|
|
134
|
+
def __init__(self, *stores: TsStore):
|
|
135
|
+
self.stores = stores
|
|
136
|
+
|
|
137
|
+
def collection_names(self, regexp: str = None) -> list:
|
|
138
|
+
return list(set(itertools.chain(*(store.collection_names(regexp) for store in self.stores))))
|
|
139
|
+
|
|
140
|
+
def collection(self, collection_name):
|
|
141
|
+
return TsUnionCollection(*(store.collection(collection_name) for store in self.stores))
|
|
142
|
+
|
|
143
|
+
def delete_collection(self, collection_name: str) -> bool:
|
|
144
|
+
return self.stores[0].delete_collection(collection_name) if self.stores else False
|
|
145
|
+
|
|
146
|
+
def auth_user(self) -> str | None:
|
|
147
|
+
return self.stores[0].auth_user() if self.stores else None
|