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,346 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from core_10x.global_cache import cache
|
|
6
|
+
from core_10x.nucleus import Nucleus
|
|
7
|
+
from core_10x.ts_store import TsCollection, TsDuplicateKeyError, TsStore, standard_key
|
|
8
|
+
from py10x_infra import MongoCollectionHelper
|
|
9
|
+
from pymongo import MongoClient, errors
|
|
10
|
+
from pymongo.errors import DuplicateKeyError
|
|
11
|
+
from pymongo.uri_parser import parse_uri as pymongo_parse_uri
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from collections.abc import Iterable
|
|
15
|
+
|
|
16
|
+
from core_10x.ts_store import f
|
|
17
|
+
from pymongo.collection import Collection
|
|
18
|
+
from pymongo.database import Database
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MongoCollection(TsCollection):
|
|
22
|
+
s_id_tag = '_id'
|
|
23
|
+
|
|
24
|
+
assert Nucleus.ID_TAG() == s_id_tag, f"Nucleus.ID_TAG() must be '{s_id_tag}'"
|
|
25
|
+
|
|
26
|
+
def __init__(self, db, collection_name: str):
|
|
27
|
+
self.coll: Collection = db[collection_name]
|
|
28
|
+
|
|
29
|
+
def collection_name(self) -> str:
|
|
30
|
+
return self.coll.name
|
|
31
|
+
|
|
32
|
+
def id_exists(self, id_value: str) -> bool:
|
|
33
|
+
return self.coll.count_documents({self.s_id_tag: id_value}) > 0
|
|
34
|
+
|
|
35
|
+
def find(self, query: f = None, _at_most: int = 0, _order: dict = None) -> Iterable:
|
|
36
|
+
cursor = self.coll.find(query.prefix_notation()) if query else self.coll.find()
|
|
37
|
+
if _order:
|
|
38
|
+
cursor = cursor.sort(list(_order.items()))
|
|
39
|
+
if _at_most:
|
|
40
|
+
cursor = cursor.limit(_at_most)
|
|
41
|
+
return cursor
|
|
42
|
+
|
|
43
|
+
def count(self, query: f = None) -> int:
|
|
44
|
+
return self.coll.count_documents(query.prefix_notation()) if query else self.coll.count_documents({})
|
|
45
|
+
|
|
46
|
+
def save_new(self, serialized_traitable: dict, overwrite: bool = False) -> int:
|
|
47
|
+
needs_upsert = (set_values := serialized_traitable.get('$set')) or overwrite
|
|
48
|
+
id_tag = self.s_id_tag
|
|
49
|
+
id_value = (set_values or serialized_traitable)[id_tag]
|
|
50
|
+
e = None
|
|
51
|
+
|
|
52
|
+
# TODO: overwrite via save(), not save_new() so that revision is incremented rather than reset
|
|
53
|
+
(set_values or serialized_traitable)[Nucleus.REVISION_TAG()] = 1
|
|
54
|
+
|
|
55
|
+
if not needs_upsert:
|
|
56
|
+
try:
|
|
57
|
+
res = self.coll.insert_one(serialized_traitable)
|
|
58
|
+
except DuplicateKeyError:
|
|
59
|
+
ack, cnt = False, 1
|
|
60
|
+
else:
|
|
61
|
+
ack, cnt = res.acknowledged, 0
|
|
62
|
+
else:
|
|
63
|
+
res = self.coll.update_one({id_tag: id_value}, serialized_traitable if set_values else {'$set': serialized_traitable}, upsert=True)
|
|
64
|
+
ack, cnt = res.acknowledged, res.matched_count
|
|
65
|
+
|
|
66
|
+
if cnt and not overwrite: # -- e.g. this id/revision already existed
|
|
67
|
+
raise TsDuplicateKeyError(self.collection_name(), {id_tag: id_value}) from e
|
|
68
|
+
|
|
69
|
+
return int(ack)
|
|
70
|
+
|
|
71
|
+
def save(self, serialized_traitable: dict) -> int:
|
|
72
|
+
rev_tag = Nucleus.REVISION_TAG()
|
|
73
|
+
id_tag = self.s_id_tag
|
|
74
|
+
|
|
75
|
+
revision = serialized_traitable.get(rev_tag, -1)
|
|
76
|
+
assert revision >= 0, 'revision must be >= 0'
|
|
77
|
+
|
|
78
|
+
if revision == 0:
|
|
79
|
+
return self.save_new(serialized_traitable)
|
|
80
|
+
|
|
81
|
+
id_value = serialized_traitable.get(id_tag)
|
|
82
|
+
|
|
83
|
+
filter = {}
|
|
84
|
+
pipeline = []
|
|
85
|
+
serialized_traitable = dict(serialized_traitable) # -- copy to avoid modifying the input
|
|
86
|
+
MongoCollectionHelper.prepare_filter_and_pipeline(serialized_traitable, filter, pipeline)
|
|
87
|
+
# self.filter_and_pipeline(serialized_traitable, filter, pipeline)
|
|
88
|
+
|
|
89
|
+
res = self.coll.update_one(filter, pipeline)
|
|
90
|
+
if not res.acknowledged:
|
|
91
|
+
return revision
|
|
92
|
+
|
|
93
|
+
if not res.matched_count: # -- e.g. restore from deleted
|
|
94
|
+
raise AssertionError(f'{self.coll} {id_value} has been most probably inappropriately restored from deleted')
|
|
95
|
+
|
|
96
|
+
if res.matched_count != 1:
|
|
97
|
+
return revision
|
|
98
|
+
|
|
99
|
+
return revision if res.modified_count != 1 else revision + 1
|
|
100
|
+
|
|
101
|
+
@classmethod
|
|
102
|
+
def filter_and_pipeline(cls, serialized_traitable, filter, pipeline):
|
|
103
|
+
rev_tag = Nucleus.REVISION_TAG()
|
|
104
|
+
id_tag = cls.s_id_tag
|
|
105
|
+
for key in (rev_tag, id_tag):
|
|
106
|
+
filter[key] = serialized_traitable.pop(key)
|
|
107
|
+
|
|
108
|
+
rev_condition = {'$and': [{'$eq': ['$' + name, {'$literal': value}]} for name, value in serialized_traitable.items()]}
|
|
109
|
+
|
|
110
|
+
# fmt: off
|
|
111
|
+
update_revision = {
|
|
112
|
+
'$cond': [
|
|
113
|
+
rev_condition, #-- if each field is equal to its prev value
|
|
114
|
+
filter[rev_tag], # then, keep the revision as is
|
|
115
|
+
filter[rev_tag] + 1 # else, increment it
|
|
116
|
+
]
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
pipeline.append(
|
|
120
|
+
{
|
|
121
|
+
'$replaceRoot': {
|
|
122
|
+
'newRoot': {
|
|
123
|
+
id_tag: filter[id_tag],
|
|
124
|
+
rev_tag: update_revision,
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
# fmt: on
|
|
130
|
+
|
|
131
|
+
pipeline.extend(
|
|
132
|
+
{'$replaceWith': {'$setField': dict(field=field, input='$$ROOT', value={'$literal': value})}}
|
|
133
|
+
for field, value in serialized_traitable.items()
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# #---- TODO: move to C++
|
|
137
|
+
# def save(self, serialized_traitable: dict) -> int:
|
|
138
|
+
# """
|
|
139
|
+
# Updates (and inc _rev) only if at least one traits has been changed
|
|
140
|
+
# :returns new revision if successful, otherwise the old one
|
|
141
|
+
# """
|
|
142
|
+
#
|
|
143
|
+
# rev_tag = Nucleus.REVISION_TAG
|
|
144
|
+
# id_tag = self.s_id_tag
|
|
145
|
+
#
|
|
146
|
+
# revision = serialized_traitable.get(rev_tag, -1)
|
|
147
|
+
# assert revision >= 0, 'revision must be >= 0'
|
|
148
|
+
#
|
|
149
|
+
# if revision == 0:
|
|
150
|
+
# return self.save_new(serialized_traitable)
|
|
151
|
+
#
|
|
152
|
+
# id_value = serialized_traitable.get(id_tag)
|
|
153
|
+
# del serialized_traitable[id_tag]
|
|
154
|
+
# del serialized_traitable[rev_tag]
|
|
155
|
+
#
|
|
156
|
+
# filter = {
|
|
157
|
+
# id_tag: id_value,
|
|
158
|
+
# rev_tag: revision,
|
|
159
|
+
# }
|
|
160
|
+
#
|
|
161
|
+
# rev_condition = {
|
|
162
|
+
# '$and': [ {'$eq': ['$' + name, {'$literal': value}] } for name, value in serialized_traitable.items() ]
|
|
163
|
+
# }
|
|
164
|
+
#
|
|
165
|
+
# update_revision = {
|
|
166
|
+
# '$cond': [
|
|
167
|
+
# rev_condition, #-- if each field is equal to its prev value
|
|
168
|
+
# revision, # then, keep the revision as is
|
|
169
|
+
# revision + 1 # else, increment it
|
|
170
|
+
# ]
|
|
171
|
+
# }
|
|
172
|
+
#
|
|
173
|
+
# cmds = [
|
|
174
|
+
# {
|
|
175
|
+
# '$replaceRoot': {
|
|
176
|
+
# 'newRoot': {
|
|
177
|
+
# id_tag: id_value,
|
|
178
|
+
# rev_tag: update_revision,
|
|
179
|
+
# }
|
|
180
|
+
# }
|
|
181
|
+
# }
|
|
182
|
+
# ]
|
|
183
|
+
#
|
|
184
|
+
# cmds.extend(
|
|
185
|
+
# {
|
|
186
|
+
# '$replaceWith': {
|
|
187
|
+
# '$setField': dict(field = field, input = '$$ROOT', value = {'$literal': value})
|
|
188
|
+
# }
|
|
189
|
+
# }
|
|
190
|
+
# for field, value in serialized_traitable.items()
|
|
191
|
+
# )
|
|
192
|
+
#
|
|
193
|
+
# res = self.coll.update_one(filter, cmds)
|
|
194
|
+
# if not res.acknowledged:
|
|
195
|
+
# return revision
|
|
196
|
+
#
|
|
197
|
+
# if not res.matched_count: # -- e.g. restore from deleted
|
|
198
|
+
# raise AssertionError(f'{self.coll} {id_value} has been most probably inapropriately restored from deleted')
|
|
199
|
+
#
|
|
200
|
+
# if res.matched_count != 1:
|
|
201
|
+
# return revision
|
|
202
|
+
#
|
|
203
|
+
# return revision if res.modified_count != 1 else revision + 1
|
|
204
|
+
|
|
205
|
+
def delete(self, id_value: str) -> bool:
|
|
206
|
+
q = {self.s_id_tag: id_value}
|
|
207
|
+
return self.coll.delete_one(q).acknowledged
|
|
208
|
+
|
|
209
|
+
def create_index(self, name: str, trait_name: str | list[tuple[str, int]], **index_args) -> str | None:
|
|
210
|
+
index_info = self.coll.index_information()
|
|
211
|
+
if name in index_info:
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
return self.coll.create_index(trait_name, name=name, **index_args)
|
|
215
|
+
|
|
216
|
+
def max(self, trait_name: str, filter: f = None) -> dict | None:
|
|
217
|
+
if filter:
|
|
218
|
+
cur = self.coll.find(filter.prefix_notation()).sort({trait_name: -1}).limit(1)
|
|
219
|
+
else:
|
|
220
|
+
cur = self.coll.find().sort({trait_name: -1}).limit(1)
|
|
221
|
+
for data in cur:
|
|
222
|
+
return data
|
|
223
|
+
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
def min(self, trait_name: str, filter: f = None) -> dict | None:
|
|
227
|
+
if filter:
|
|
228
|
+
cur = self.coll.find(filter.prefix_notation()).sort({trait_name: 1}).limit(1)
|
|
229
|
+
else:
|
|
230
|
+
cur = self.coll.find().sort({trait_name: 1}).limit(1)
|
|
231
|
+
for data in cur:
|
|
232
|
+
return data
|
|
233
|
+
|
|
234
|
+
return None
|
|
235
|
+
|
|
236
|
+
def load(self, id_value: str) -> dict | None:
|
|
237
|
+
for data in self.coll.find({self.s_id_tag: id_value}):
|
|
238
|
+
return data
|
|
239
|
+
|
|
240
|
+
return None
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class MongoStore(TsStore, resource_name='MONGO_DB'):
|
|
244
|
+
PROTOCOL = 'mongodb'
|
|
245
|
+
|
|
246
|
+
ADMIN = 'admin'
|
|
247
|
+
|
|
248
|
+
# fmt: off
|
|
249
|
+
s_instance_kwargs_map = dict(
|
|
250
|
+
port = ('port', 27017),
|
|
251
|
+
ssl = ('ssl', False),
|
|
252
|
+
sst = ('serverSelectionTimeoutMS', 10000),
|
|
253
|
+
)
|
|
254
|
+
# fmt: on
|
|
255
|
+
|
|
256
|
+
s_cached_connections: dict[tuple, MongoClient] = {}
|
|
257
|
+
|
|
258
|
+
@classmethod
|
|
259
|
+
def connect(cls, hostname: str, username: str, password: str, _cache: bool = True, _throw: bool = True, **kwargs) -> MongoClient:
|
|
260
|
+
connection_key = standard_key((hostname, username), kwargs) if _cache else None
|
|
261
|
+
client = cls.s_cached_connections.get(connection_key)
|
|
262
|
+
if not client:
|
|
263
|
+
client = MongoClient(hostname, username=username, password=password, **kwargs)
|
|
264
|
+
try:
|
|
265
|
+
client.server_info()
|
|
266
|
+
except Exception:
|
|
267
|
+
client.close()
|
|
268
|
+
if _throw:
|
|
269
|
+
raise
|
|
270
|
+
client = None
|
|
271
|
+
if client and connection_key:
|
|
272
|
+
cls.s_cached_connections[connection_key] = client
|
|
273
|
+
|
|
274
|
+
return client
|
|
275
|
+
|
|
276
|
+
@classmethod
|
|
277
|
+
def uncache_connection(cls, hostname: str, username: str, password: str, **kwargs):
|
|
278
|
+
connection_key = standard_key((hostname, username), kwargs)
|
|
279
|
+
client = cls.s_cached_connections.pop(connection_key, None)
|
|
280
|
+
if client:
|
|
281
|
+
client.close()
|
|
282
|
+
|
|
283
|
+
# noinspection PyMethodOverriding
|
|
284
|
+
@classmethod
|
|
285
|
+
def new_instance(cls, hostname: str, dbname: str, username: str, password: str, **kwargs) -> TsStore:
|
|
286
|
+
client = cls.connect(hostname, username, password, **kwargs)
|
|
287
|
+
return cls(client, client[dbname], username)
|
|
288
|
+
|
|
289
|
+
@classmethod
|
|
290
|
+
def parse_uri(cls, uri: str) -> dict:
|
|
291
|
+
params = pymongo_parse_uri(uri)
|
|
292
|
+
try:
|
|
293
|
+
# fmt: off
|
|
294
|
+
hostname, port = params['nodelist'][0]
|
|
295
|
+
kwargs = params['options']
|
|
296
|
+
kwargs[cls.PORT_TAG] = port
|
|
297
|
+
args = {
|
|
298
|
+
cls.HOSTNAME_TAG: hostname,
|
|
299
|
+
cls.DBNAME_TAG: params['database'],
|
|
300
|
+
cls.USERNAME_TAG: params['username'],
|
|
301
|
+
cls.PASSWORD_TAG: params['password'],
|
|
302
|
+
}
|
|
303
|
+
# fmt: on
|
|
304
|
+
args.update(kwargs)
|
|
305
|
+
return args
|
|
306
|
+
|
|
307
|
+
except Exception as e:
|
|
308
|
+
raise ValueError(f'Invalid URI = {uri}') from e
|
|
309
|
+
|
|
310
|
+
def __init__(self, client: MongoClient, db: Database, username: str):
|
|
311
|
+
self.client = client
|
|
312
|
+
self.db: Database = db
|
|
313
|
+
self.username = username
|
|
314
|
+
|
|
315
|
+
def collection_names(self, regexp: str = None) -> list:
|
|
316
|
+
filter = dict(name={'$regex': regexp}) if regexp else None
|
|
317
|
+
return self.db.list_collection_names(filter=filter)
|
|
318
|
+
|
|
319
|
+
def collection(self, collection_name: str) -> TsCollection:
|
|
320
|
+
return MongoCollection(self.db, collection_name)
|
|
321
|
+
|
|
322
|
+
def delete_collection(self, collection_name: str) -> bool:
|
|
323
|
+
self.db.drop_collection(collection_name)
|
|
324
|
+
return True
|
|
325
|
+
|
|
326
|
+
@classmethod
|
|
327
|
+
@cache
|
|
328
|
+
def is_running_with_auth(cls, host_name: str) -> tuple: # -- (is_running, with_auth)
|
|
329
|
+
client = cls.connect(host_name, '', '', _cache=False, _throw=False)
|
|
330
|
+
if not client:
|
|
331
|
+
return False, False
|
|
332
|
+
|
|
333
|
+
admin_db = client[cls.ADMIN]
|
|
334
|
+
try:
|
|
335
|
+
res = admin_db.command('getCmdLineOpts')
|
|
336
|
+
auth = any(r == '--auth' for r in res['argv'][1:])
|
|
337
|
+
return True, auth
|
|
338
|
+
|
|
339
|
+
except errors.OperationFailure: # -- auth is required
|
|
340
|
+
return True, True
|
|
341
|
+
|
|
342
|
+
finally:
|
|
343
|
+
client.close()
|
|
344
|
+
|
|
345
|
+
def auth_user(self) -> str | None:
|
|
346
|
+
return self.username
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import secrets
|
|
3
|
+
import string
|
|
4
|
+
import subprocess
|
|
5
|
+
from getpass import getpass
|
|
6
|
+
|
|
7
|
+
import keyring
|
|
8
|
+
import requests
|
|
9
|
+
from core_10x.backbone.namespace import AUTO_PASSWORD_LENGTH, USER_ROLE
|
|
10
|
+
from core_10x.global_cache import cache
|
|
11
|
+
from core_10x.rc import RC
|
|
12
|
+
|
|
13
|
+
from infra_10x.mongodb_admin import MongodbAdmin
|
|
14
|
+
from infra_10x.mongodb_store import MongoStore
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@cache
|
|
18
|
+
def create_xx_role(admin: MongodbAdmin):
|
|
19
|
+
admin.db.command(
|
|
20
|
+
'updateRole' if admin.role_exists('xxUser') else 'createRole',
|
|
21
|
+
'xxUser',
|
|
22
|
+
privileges=[
|
|
23
|
+
{
|
|
24
|
+
'resource': {'anyResource': True},
|
|
25
|
+
'actions': USER_ROLE.WORKER.value,
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
roles=[],
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def create_xx_user(admin: MongodbAdmin, xx_user: str, new_password: str) -> None:
|
|
33
|
+
password = keyring.get_password('xxuser', xx_user) or new_password
|
|
34
|
+
admin.update_user(xx_user, password, 'xxUser', keep_current_roles=False)
|
|
35
|
+
if password == new_password:
|
|
36
|
+
keyring.set_password('xxuser', xx_user, password)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def create_xx_admin(admin: MongodbAdmin, xx_admin: str, new_password: str) -> None:
|
|
40
|
+
if xx_admin == admin.username:
|
|
41
|
+
raise ValueError('cannot create self!')
|
|
42
|
+
password = keyring.get_password('xxadmin', xx_admin) or new_password
|
|
43
|
+
admin.update_user(xx_admin, password, 'userAdminAnyDatabase', 'readWriteAnyDatabase')
|
|
44
|
+
if password == new_password:
|
|
45
|
+
keyring.set_password('xxadmin', xx_admin, password)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_xx_admin(user: str):
|
|
49
|
+
return f'{user}-admin'
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@cache
|
|
53
|
+
def git_user():
|
|
54
|
+
return requests.get('https://api.github.com/user', headers={'Authorization': f'token {git_token()}'}).json()['login'] or input('Username:')
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@cache
|
|
58
|
+
def git_token():
|
|
59
|
+
return subprocess.getoutput("git credential fill <<< 'protocol=https\nhost=github.com\n' | grep password | cut -d= -f2") or input('Token:')
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@cache
|
|
63
|
+
def all_git_users():
|
|
64
|
+
git_org = re.search(r'github\.com[/:]([\w\-\.]+)/', subprocess.check_output(['git', 'remote', 'get-url', 'origin'], text=True).strip()).group(1)
|
|
65
|
+
return [
|
|
66
|
+
user['login']
|
|
67
|
+
for user in requests.get(f'https://api.github.com/orgs/{git_org}/members', headers={'Authorization': f'token {git_token()}'}).json()
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def generate_password():
|
|
72
|
+
return ''.join(secrets.choice(string.ascii_letters + string.digits + '!@#$%^&*') for _ in range(AUTO_PASSWORD_LENGTH))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def create_users(hostname):
|
|
76
|
+
admin = MongodbAdmin(hostname=hostname, **get_credentials('xxadmin', hostname))
|
|
77
|
+
create_xx_role(admin)
|
|
78
|
+
for xx_user in all_git_users():
|
|
79
|
+
xx_admin = get_xx_admin(xx_user)
|
|
80
|
+
if xx_admin != admin.username: # -- do not break current working account!
|
|
81
|
+
create_xx_admin(admin, xx_admin, generate_password())
|
|
82
|
+
create_xx_user(admin, xx_user, generate_password())
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def clear_vault():
|
|
86
|
+
for xx_user in all_git_users():
|
|
87
|
+
if keyring.get_password('xxuser', xx_user):
|
|
88
|
+
keyring.delete_password('xxuser', xx_user)
|
|
89
|
+
if xx_user != git_user(): # -- do not break own admin account!
|
|
90
|
+
xx_admin = get_xx_admin(xx_user)
|
|
91
|
+
if keyring.get_password('xxadmin', xx_admin):
|
|
92
|
+
keyring.delete_password('xxadmin', xx_admin)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def drop_users(hostname):
|
|
96
|
+
admin = MongodbAdmin(hostname=hostname, **get_credentials('xxadmin', hostname))
|
|
97
|
+
for xx_user in all_git_users():
|
|
98
|
+
xx_admin = get_xx_admin(xx_user)
|
|
99
|
+
if xx_admin != admin.username: # -- do not break current working account!
|
|
100
|
+
admin.delete_user(xx_admin)
|
|
101
|
+
admin.delete_user(xx_user)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def get_credentials(service, hostname):
|
|
105
|
+
if hostname == 'localhost':
|
|
106
|
+
return dict(username='', password='')
|
|
107
|
+
username = git_user()
|
|
108
|
+
if service == 'xxadmin':
|
|
109
|
+
username = get_xx_admin(username)
|
|
110
|
+
password = keyring.get_password(service, username)
|
|
111
|
+
if not password:
|
|
112
|
+
password = getpass(f"{username}'s password for `{service}`:")
|
|
113
|
+
keyring.set_password(service, username, password)
|
|
114
|
+
return dict(username=username, password=password)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def copy_db(from_host: str, to_host: str, dbname: str, overwrite=False) -> RC:
|
|
118
|
+
from_store = MongoStore.instance(hostname=from_host, dbname=dbname, **get_credentials('xxuser', from_host))
|
|
119
|
+
to_store = MongoStore.instance(hostname=to_host, dbname=dbname, **get_credentials('xxuser', to_host))
|
|
120
|
+
return from_store.copy_to(to_store, overwrite=overwrite)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
if __name__ == '__main__':
|
|
124
|
+
host = 'mongo10x.eastus2.cloudapp.azure.com'
|
|
125
|
+
# drop_users(host)
|
|
126
|
+
# clear_vault()
|
|
127
|
+
# create_users(host)
|
|
128
|
+
|
|
129
|
+
copy_db(from_host='localhost', to_host=host, dbname='mkt_data', overwrite=False).throw()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from infra_10x.mongodb_store import MongoStore
|
|
3
|
+
|
|
4
|
+
MONGO_URL = 'mongodb://localhost:27017/'
|
|
5
|
+
# MONGO_URL="mongodb+srv://dev.qbultu3.ts_storedb.net/?authMechanism=MONGODB-X509&authSource=%24external&tls=true&tlsCertificateKeyFile=%2FUsers%2Fiap%2FDownloads%2FX509-cert-590074097809994161.pem"
|
|
6
|
+
TEST_DB = 'test_db'
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.fixture(scope='module')
|
|
10
|
+
def ts_instance():
|
|
11
|
+
instance = MongoStore.instance(hostname=MONGO_URL, dbname=TEST_DB)
|
|
12
|
+
instance.username = 'test_user'
|
|
13
|
+
return instance
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from core_10x.testlib.ts_tests import TestTSStore, ts_setup
|
|
2
|
+
from infra_10x.mongodb_store import MongoCollection, MongoStore
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_mongo_parse_uri_round_trip():
|
|
6
|
+
uri = 'mongodb://user:pass@localhost:27017/testdb?ssl=false&serverSelectionTimeoutMS=5000'
|
|
7
|
+
args = MongoStore.parse_uri(uri)
|
|
8
|
+
|
|
9
|
+
assert args[MongoStore.HOSTNAME_TAG] == 'localhost'
|
|
10
|
+
assert args[MongoStore.DBNAME_TAG] == 'testdb'
|
|
11
|
+
assert args[MongoStore.USERNAME_TAG] == 'user'
|
|
12
|
+
assert args[MongoStore.PASSWORD_TAG] == 'pass'
|
|
13
|
+
assert args['port'] == 27017
|
|
14
|
+
# Options are driver-dependent; we only require that custom options are propagated.
|
|
15
|
+
assert 'serverSelectionTimeoutMS' in args
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_filter_and_pipeline_equivalence():
|
|
19
|
+
# This test mirrors infra_10x/manual_tests/test_prepare_filter_and_pipeline.py
|
|
20
|
+
from py10x_infra import MongoCollectionHelper
|
|
21
|
+
|
|
22
|
+
# The helper only constructs filter/pipeline; no need for a live Mongo instance here.
|
|
23
|
+
serialized_traitable = dict(_id='AAAA', _rev=10, name='test', age=60)
|
|
24
|
+
|
|
25
|
+
data1 = dict(serialized_traitable)
|
|
26
|
+
pipeline1: list = []
|
|
27
|
+
filter1: dict = {}
|
|
28
|
+
MongoCollection.filter_and_pipeline(data1, filter1, pipeline1)
|
|
29
|
+
|
|
30
|
+
data2 = dict(serialized_traitable)
|
|
31
|
+
pipeline2: list = []
|
|
32
|
+
filter2: dict = {}
|
|
33
|
+
MongoCollectionHelper.prepare_filter_and_pipeline(data2, filter2, pipeline2)
|
|
34
|
+
|
|
35
|
+
assert filter1 == filter2
|
|
36
|
+
assert pipeline1 == pipeline2
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from core_10x.testlib.traitable_history_tests import TestTraitableHistory, test_collection, test_store
|