py10x-universe 0.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. core_10x/__init__.py +42 -0
  2. core_10x/backbone/__init__.py +0 -0
  3. core_10x/backbone/backbone_store.py +59 -0
  4. core_10x/backbone/backbone_traitable.py +30 -0
  5. core_10x/backbone/backbone_user.py +66 -0
  6. core_10x/backbone/bound_data_domain.py +49 -0
  7. core_10x/backbone/namespace.py +101 -0
  8. core_10x/backbone/vault.py +38 -0
  9. core_10x/code_samples/__init__.py +0 -0
  10. core_10x/code_samples/_package_manifest.py +3 -0
  11. core_10x/code_samples/directories.py +181 -0
  12. core_10x/code_samples/person.py +76 -0
  13. core_10x/concrete_traits.py +356 -0
  14. core_10x/conftest.py +12 -0
  15. core_10x/curve.py +321 -0
  16. core_10x/data_domain.py +48 -0
  17. core_10x/data_domain_binder.py +45 -0
  18. core_10x/directory.py +250 -0
  19. core_10x/entity.py +8 -0
  20. core_10x/entity_filter.py +5 -0
  21. core_10x/environment_variables.py +147 -0
  22. core_10x/exec_control.py +84 -0
  23. core_10x/experimental/__init__.py +0 -0
  24. core_10x/experimental/data_protocol_ex.py +34 -0
  25. core_10x/global_cache.py +121 -0
  26. core_10x/manual_tests/__init__.py +0 -0
  27. core_10x/manual_tests/calendar_test.py +35 -0
  28. core_10x/manual_tests/ctor_update_bug.py +58 -0
  29. core_10x/manual_tests/debug_graph_on.py +17 -0
  30. core_10x/manual_tests/debug_graphoff_inside_graph_on.py +28 -0
  31. core_10x/manual_tests/enum_bits_test.py +17 -0
  32. core_10x/manual_tests/env_vars_trivial_test.py +12 -0
  33. core_10x/manual_tests/existing_traitable.py +33 -0
  34. core_10x/manual_tests/k10x_test1.py +13 -0
  35. core_10x/manual_tests/named_constant_test.py +121 -0
  36. core_10x/manual_tests/nucleus_trivial_test.py +42 -0
  37. core_10x/manual_tests/polars_test.py +14 -0
  38. core_10x/manual_tests/py_class_test.py +4 -0
  39. core_10x/manual_tests/rc_test.py +42 -0
  40. core_10x/manual_tests/rdate_test.py +12 -0
  41. core_10x/manual_tests/reference_serialization_bug.py +19 -0
  42. core_10x/manual_tests/resource_trivial_test.py +10 -0
  43. core_10x/manual_tests/store_uri_test.py +6 -0
  44. core_10x/manual_tests/trait_definition_test.py +19 -0
  45. core_10x/manual_tests/trait_filter_test.py +15 -0
  46. core_10x/manual_tests/trait_flag_modification_test.py +42 -0
  47. core_10x/manual_tests/trait_modification_bug.py +26 -0
  48. core_10x/manual_tests/traitable_as_of_test.py +82 -0
  49. core_10x/manual_tests/traitable_heir_test.py +39 -0
  50. core_10x/manual_tests/traitable_history_test.py +41 -0
  51. core_10x/manual_tests/traitable_serialization_test.py +54 -0
  52. core_10x/manual_tests/traitable_trivial_test.py +71 -0
  53. core_10x/manual_tests/trivial_graph_test.py +16 -0
  54. core_10x/manual_tests/ts_class_association_test.py +64 -0
  55. core_10x/manual_tests/ts_trivial_test.py +35 -0
  56. core_10x/named_constant.py +425 -0
  57. core_10x/nucleus.py +81 -0
  58. core_10x/package_manifest.py +85 -0
  59. core_10x/package_refactoring.py +153 -0
  60. core_10x/py_class.py +431 -0
  61. core_10x/rc.py +155 -0
  62. core_10x/rdate.py +339 -0
  63. core_10x/resource.py +189 -0
  64. core_10x/roman_number.py +67 -0
  65. core_10x/testlib/__init__.py +0 -0
  66. core_10x/testlib/test_store.py +240 -0
  67. core_10x/testlib/traitable_history_tests.py +787 -0
  68. core_10x/testlib/ts_tests.py +280 -0
  69. core_10x/trait.py +377 -0
  70. core_10x/trait_definition.py +176 -0
  71. core_10x/trait_filter.py +205 -0
  72. core_10x/trait_method_error.py +36 -0
  73. core_10x/traitable.py +1082 -0
  74. core_10x/traitable_cli.py +153 -0
  75. core_10x/traitable_heir.py +33 -0
  76. core_10x/traitable_id.py +31 -0
  77. core_10x/ts_store.py +172 -0
  78. core_10x/ts_store_type.py +26 -0
  79. core_10x/ts_union.py +147 -0
  80. core_10x/ui_hint.py +153 -0
  81. core_10x/unit_tests/test_concrete_traits.py +156 -0
  82. core_10x/unit_tests/test_converters.py +51 -0
  83. core_10x/unit_tests/test_curve.py +157 -0
  84. core_10x/unit_tests/test_directory.py +54 -0
  85. core_10x/unit_tests/test_documentation.py +172 -0
  86. core_10x/unit_tests/test_environment_variables.py +15 -0
  87. core_10x/unit_tests/test_filters.py +239 -0
  88. core_10x/unit_tests/test_graph.py +348 -0
  89. core_10x/unit_tests/test_named_constant.py +98 -0
  90. core_10x/unit_tests/test_rc.py +11 -0
  91. core_10x/unit_tests/test_rdate.py +484 -0
  92. core_10x/unit_tests/test_trait_method_error.py +80 -0
  93. core_10x/unit_tests/test_trait_modification.py +19 -0
  94. core_10x/unit_tests/test_traitable.py +959 -0
  95. core_10x/unit_tests/test_traitable_history.py +1 -0
  96. core_10x/unit_tests/test_ts_store.py +1 -0
  97. core_10x/unit_tests/test_ts_union.py +369 -0
  98. core_10x/unit_tests/test_ui_nodes.py +81 -0
  99. core_10x/unit_tests/test_xxcalendar.py +471 -0
  100. core_10x/vault/__init__.py +0 -0
  101. core_10x/vault/sec_keys.py +133 -0
  102. core_10x/vault/security_keys_old.py +168 -0
  103. core_10x/vault/vault.py +56 -0
  104. core_10x/vault/vault_traitable.py +56 -0
  105. core_10x/vault/vault_user.py +70 -0
  106. core_10x/xdate_time.py +136 -0
  107. core_10x/xnone.py +71 -0
  108. core_10x/xxcalendar.py +228 -0
  109. infra_10x/__init__.py +0 -0
  110. infra_10x/manual_tests/__init__.py +0 -0
  111. infra_10x/manual_tests/test_misc.py +16 -0
  112. infra_10x/manual_tests/test_prepare_filter_and_pipeline.py +25 -0
  113. infra_10x/mongodb_admin.py +111 -0
  114. infra_10x/mongodb_store.py +346 -0
  115. infra_10x/mongodb_utils.py +129 -0
  116. infra_10x/unit_tests/conftest.py +13 -0
  117. infra_10x/unit_tests/test_mongo_db.py +36 -0
  118. infra_10x/unit_tests/test_mongo_history.py +1 -0
  119. py10x_universe-0.1.3.dist-info/METADATA +406 -0
  120. py10x_universe-0.1.3.dist-info/RECORD +214 -0
  121. py10x_universe-0.1.3.dist-info/WHEEL +4 -0
  122. py10x_universe-0.1.3.dist-info/licenses/LICENSE +21 -0
  123. ui_10x/__init__.py +0 -0
  124. ui_10x/apps/__init__.py +0 -0
  125. ui_10x/apps/collection_editor_app.py +100 -0
  126. ui_10x/choice.py +212 -0
  127. ui_10x/collection_editor.py +135 -0
  128. ui_10x/concrete_trait_widgets.py +220 -0
  129. ui_10x/conftest.py +8 -0
  130. ui_10x/entity_stocker.py +173 -0
  131. ui_10x/examples/__init__.py +0 -0
  132. ui_10x/examples/_guess_word_data.py +14076 -0
  133. ui_10x/examples/collection_editor.py +17 -0
  134. ui_10x/examples/date_selector.py +14 -0
  135. ui_10x/examples/entity_stocker.py +18 -0
  136. ui_10x/examples/guess_word.py +392 -0
  137. ui_10x/examples/message_box.py +20 -0
  138. ui_10x/examples/multi_choice.py +17 -0
  139. ui_10x/examples/py_data_browser.py +66 -0
  140. ui_10x/examples/radiobox.py +29 -0
  141. ui_10x/examples/single_choice.py +31 -0
  142. ui_10x/examples/style_sheet.py +47 -0
  143. ui_10x/examples/trivial_entity_editor.py +18 -0
  144. ui_10x/platform.py +20 -0
  145. ui_10x/platform_interface.py +517 -0
  146. ui_10x/py_data_browser.py +249 -0
  147. ui_10x/qt6/__init__.py +0 -0
  148. ui_10x/qt6/conftest.py +8 -0
  149. ui_10x/qt6/manual_tests/__init__.py +0 -0
  150. ui_10x/qt6/manual_tests/basic_test.py +35 -0
  151. ui_10x/qt6/platform_implementation.py +275 -0
  152. ui_10x/qt6/utils.py +665 -0
  153. ui_10x/rio/__init__.py +0 -0
  154. ui_10x/rio/apps/examples/examples/__init__.py +22 -0
  155. ui_10x/rio/apps/examples/examples/components/__init__.py +3 -0
  156. ui_10x/rio/apps/examples/examples/components/collection_editor.py +15 -0
  157. ui_10x/rio/apps/examples/examples/pages/collection_editor.py +21 -0
  158. ui_10x/rio/apps/examples/examples/pages/login_page.py +88 -0
  159. ui_10x/rio/apps/examples/examples/pages/style_sheet.py +21 -0
  160. ui_10x/rio/apps/examples/rio.toml +14 -0
  161. ui_10x/rio/component_builder.py +497 -0
  162. ui_10x/rio/components/__init__.py +9 -0
  163. ui_10x/rio/components/group_box.py +31 -0
  164. ui_10x/rio/components/labeled_checkbox.py +18 -0
  165. ui_10x/rio/components/line_edit.py +37 -0
  166. ui_10x/rio/components/radio_button.py +32 -0
  167. ui_10x/rio/components/separator.py +24 -0
  168. ui_10x/rio/components/splitter.py +121 -0
  169. ui_10x/rio/components/tree_view.py +75 -0
  170. ui_10x/rio/conftest.py +35 -0
  171. ui_10x/rio/internals/__init__.py +0 -0
  172. ui_10x/rio/internals/app.py +192 -0
  173. ui_10x/rio/manual_tests/__init__.py +0 -0
  174. ui_10x/rio/manual_tests/basic_test.py +24 -0
  175. ui_10x/rio/manual_tests/splitter.py +27 -0
  176. ui_10x/rio/platform_implementation.py +91 -0
  177. ui_10x/rio/style_sheet.py +53 -0
  178. ui_10x/rio/unit_tests/test_collection_editor.py +68 -0
  179. ui_10x/rio/unit_tests/test_internals.py +630 -0
  180. ui_10x/rio/unit_tests/test_style_sheet.py +37 -0
  181. ui_10x/rio/widgets/__init__.py +46 -0
  182. ui_10x/rio/widgets/application.py +109 -0
  183. ui_10x/rio/widgets/button.py +48 -0
  184. ui_10x/rio/widgets/button_group.py +60 -0
  185. ui_10x/rio/widgets/calendar.py +23 -0
  186. ui_10x/rio/widgets/checkbox.py +24 -0
  187. ui_10x/rio/widgets/dialog.py +137 -0
  188. ui_10x/rio/widgets/group_box.py +27 -0
  189. ui_10x/rio/widgets/layout.py +34 -0
  190. ui_10x/rio/widgets/line_edit.py +37 -0
  191. ui_10x/rio/widgets/list.py +105 -0
  192. ui_10x/rio/widgets/message_box.py +70 -0
  193. ui_10x/rio/widgets/scroll_area.py +31 -0
  194. ui_10x/rio/widgets/spacer.py +6 -0
  195. ui_10x/rio/widgets/splitter.py +45 -0
  196. ui_10x/rio/widgets/text_edit.py +28 -0
  197. ui_10x/rio/widgets/tree.py +89 -0
  198. ui_10x/rio/widgets/unit_tests/test_button.py +101 -0
  199. ui_10x/rio/widgets/unit_tests/test_button_group.py +33 -0
  200. ui_10x/rio/widgets/unit_tests/test_calendar.py +114 -0
  201. ui_10x/rio/widgets/unit_tests/test_checkbox.py +109 -0
  202. ui_10x/rio/widgets/unit_tests/test_group_box.py +158 -0
  203. ui_10x/rio/widgets/unit_tests/test_label.py +43 -0
  204. ui_10x/rio/widgets/unit_tests/test_line_edit.py +140 -0
  205. ui_10x/rio/widgets/unit_tests/test_list.py +146 -0
  206. ui_10x/table_header_view.py +305 -0
  207. ui_10x/table_view.py +174 -0
  208. ui_10x/trait_editor.py +189 -0
  209. ui_10x/trait_widget.py +131 -0
  210. ui_10x/traitable_editor.py +200 -0
  211. ui_10x/traitable_view.py +131 -0
  212. ui_10x/unit_tests/conftest.py +8 -0
  213. ui_10x/unit_tests/test_platform.py +9 -0
  214. ui_10x/utils.py +661 -0
@@ -0,0 +1,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