py10x-universe 0.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. core_10x/__init__.py +42 -0
  2. core_10x/backbone/__init__.py +0 -0
  3. core_10x/backbone/backbone_store.py +59 -0
  4. core_10x/backbone/backbone_traitable.py +30 -0
  5. core_10x/backbone/backbone_user.py +66 -0
  6. core_10x/backbone/bound_data_domain.py +49 -0
  7. core_10x/backbone/namespace.py +101 -0
  8. core_10x/backbone/vault.py +38 -0
  9. core_10x/code_samples/__init__.py +0 -0
  10. core_10x/code_samples/_package_manifest.py +3 -0
  11. core_10x/code_samples/directories.py +181 -0
  12. core_10x/code_samples/person.py +76 -0
  13. core_10x/concrete_traits.py +356 -0
  14. core_10x/conftest.py +12 -0
  15. core_10x/curve.py +321 -0
  16. core_10x/data_domain.py +48 -0
  17. core_10x/data_domain_binder.py +45 -0
  18. core_10x/directory.py +250 -0
  19. core_10x/entity.py +8 -0
  20. core_10x/entity_filter.py +5 -0
  21. core_10x/environment_variables.py +147 -0
  22. core_10x/exec_control.py +84 -0
  23. core_10x/experimental/__init__.py +0 -0
  24. core_10x/experimental/data_protocol_ex.py +34 -0
  25. core_10x/global_cache.py +121 -0
  26. core_10x/manual_tests/__init__.py +0 -0
  27. core_10x/manual_tests/calendar_test.py +35 -0
  28. core_10x/manual_tests/ctor_update_bug.py +58 -0
  29. core_10x/manual_tests/debug_graph_on.py +17 -0
  30. core_10x/manual_tests/debug_graphoff_inside_graph_on.py +28 -0
  31. core_10x/manual_tests/enum_bits_test.py +17 -0
  32. core_10x/manual_tests/env_vars_trivial_test.py +12 -0
  33. core_10x/manual_tests/existing_traitable.py +33 -0
  34. core_10x/manual_tests/k10x_test1.py +13 -0
  35. core_10x/manual_tests/named_constant_test.py +121 -0
  36. core_10x/manual_tests/nucleus_trivial_test.py +42 -0
  37. core_10x/manual_tests/polars_test.py +14 -0
  38. core_10x/manual_tests/py_class_test.py +4 -0
  39. core_10x/manual_tests/rc_test.py +42 -0
  40. core_10x/manual_tests/rdate_test.py +12 -0
  41. core_10x/manual_tests/reference_serialization_bug.py +19 -0
  42. core_10x/manual_tests/resource_trivial_test.py +10 -0
  43. core_10x/manual_tests/store_uri_test.py +6 -0
  44. core_10x/manual_tests/trait_definition_test.py +19 -0
  45. core_10x/manual_tests/trait_filter_test.py +15 -0
  46. core_10x/manual_tests/trait_flag_modification_test.py +42 -0
  47. core_10x/manual_tests/trait_modification_bug.py +26 -0
  48. core_10x/manual_tests/traitable_as_of_test.py +82 -0
  49. core_10x/manual_tests/traitable_heir_test.py +39 -0
  50. core_10x/manual_tests/traitable_history_test.py +41 -0
  51. core_10x/manual_tests/traitable_serialization_test.py +54 -0
  52. core_10x/manual_tests/traitable_trivial_test.py +71 -0
  53. core_10x/manual_tests/trivial_graph_test.py +16 -0
  54. core_10x/manual_tests/ts_class_association_test.py +64 -0
  55. core_10x/manual_tests/ts_trivial_test.py +35 -0
  56. core_10x/named_constant.py +425 -0
  57. core_10x/nucleus.py +81 -0
  58. core_10x/package_manifest.py +85 -0
  59. core_10x/package_refactoring.py +153 -0
  60. core_10x/py_class.py +431 -0
  61. core_10x/rc.py +155 -0
  62. core_10x/rdate.py +339 -0
  63. core_10x/resource.py +189 -0
  64. core_10x/roman_number.py +67 -0
  65. core_10x/testlib/__init__.py +0 -0
  66. core_10x/testlib/test_store.py +240 -0
  67. core_10x/testlib/traitable_history_tests.py +787 -0
  68. core_10x/testlib/ts_tests.py +280 -0
  69. core_10x/trait.py +377 -0
  70. core_10x/trait_definition.py +176 -0
  71. core_10x/trait_filter.py +205 -0
  72. core_10x/trait_method_error.py +36 -0
  73. core_10x/traitable.py +1082 -0
  74. core_10x/traitable_cli.py +153 -0
  75. core_10x/traitable_heir.py +33 -0
  76. core_10x/traitable_id.py +31 -0
  77. core_10x/ts_store.py +172 -0
  78. core_10x/ts_store_type.py +26 -0
  79. core_10x/ts_union.py +147 -0
  80. core_10x/ui_hint.py +153 -0
  81. core_10x/unit_tests/test_concrete_traits.py +156 -0
  82. core_10x/unit_tests/test_converters.py +51 -0
  83. core_10x/unit_tests/test_curve.py +157 -0
  84. core_10x/unit_tests/test_directory.py +54 -0
  85. core_10x/unit_tests/test_documentation.py +172 -0
  86. core_10x/unit_tests/test_environment_variables.py +15 -0
  87. core_10x/unit_tests/test_filters.py +239 -0
  88. core_10x/unit_tests/test_graph.py +348 -0
  89. core_10x/unit_tests/test_named_constant.py +98 -0
  90. core_10x/unit_tests/test_rc.py +11 -0
  91. core_10x/unit_tests/test_rdate.py +484 -0
  92. core_10x/unit_tests/test_trait_method_error.py +80 -0
  93. core_10x/unit_tests/test_trait_modification.py +19 -0
  94. core_10x/unit_tests/test_traitable.py +959 -0
  95. core_10x/unit_tests/test_traitable_history.py +1 -0
  96. core_10x/unit_tests/test_ts_store.py +1 -0
  97. core_10x/unit_tests/test_ts_union.py +369 -0
  98. core_10x/unit_tests/test_ui_nodes.py +81 -0
  99. core_10x/unit_tests/test_xxcalendar.py +471 -0
  100. core_10x/vault/__init__.py +0 -0
  101. core_10x/vault/sec_keys.py +133 -0
  102. core_10x/vault/security_keys_old.py +168 -0
  103. core_10x/vault/vault.py +56 -0
  104. core_10x/vault/vault_traitable.py +56 -0
  105. core_10x/vault/vault_user.py +70 -0
  106. core_10x/xdate_time.py +136 -0
  107. core_10x/xnone.py +71 -0
  108. core_10x/xxcalendar.py +228 -0
  109. infra_10x/__init__.py +0 -0
  110. infra_10x/manual_tests/__init__.py +0 -0
  111. infra_10x/manual_tests/test_misc.py +16 -0
  112. infra_10x/manual_tests/test_prepare_filter_and_pipeline.py +25 -0
  113. infra_10x/mongodb_admin.py +111 -0
  114. infra_10x/mongodb_store.py +346 -0
  115. infra_10x/mongodb_utils.py +129 -0
  116. infra_10x/unit_tests/conftest.py +13 -0
  117. infra_10x/unit_tests/test_mongo_db.py +36 -0
  118. infra_10x/unit_tests/test_mongo_history.py +1 -0
  119. py10x_universe-0.1.3.dist-info/METADATA +406 -0
  120. py10x_universe-0.1.3.dist-info/RECORD +214 -0
  121. py10x_universe-0.1.3.dist-info/WHEEL +4 -0
  122. py10x_universe-0.1.3.dist-info/licenses/LICENSE +21 -0
  123. ui_10x/__init__.py +0 -0
  124. ui_10x/apps/__init__.py +0 -0
  125. ui_10x/apps/collection_editor_app.py +100 -0
  126. ui_10x/choice.py +212 -0
  127. ui_10x/collection_editor.py +135 -0
  128. ui_10x/concrete_trait_widgets.py +220 -0
  129. ui_10x/conftest.py +8 -0
  130. ui_10x/entity_stocker.py +173 -0
  131. ui_10x/examples/__init__.py +0 -0
  132. ui_10x/examples/_guess_word_data.py +14076 -0
  133. ui_10x/examples/collection_editor.py +17 -0
  134. ui_10x/examples/date_selector.py +14 -0
  135. ui_10x/examples/entity_stocker.py +18 -0
  136. ui_10x/examples/guess_word.py +392 -0
  137. ui_10x/examples/message_box.py +20 -0
  138. ui_10x/examples/multi_choice.py +17 -0
  139. ui_10x/examples/py_data_browser.py +66 -0
  140. ui_10x/examples/radiobox.py +29 -0
  141. ui_10x/examples/single_choice.py +31 -0
  142. ui_10x/examples/style_sheet.py +47 -0
  143. ui_10x/examples/trivial_entity_editor.py +18 -0
  144. ui_10x/platform.py +20 -0
  145. ui_10x/platform_interface.py +517 -0
  146. ui_10x/py_data_browser.py +249 -0
  147. ui_10x/qt6/__init__.py +0 -0
  148. ui_10x/qt6/conftest.py +8 -0
  149. ui_10x/qt6/manual_tests/__init__.py +0 -0
  150. ui_10x/qt6/manual_tests/basic_test.py +35 -0
  151. ui_10x/qt6/platform_implementation.py +275 -0
  152. ui_10x/qt6/utils.py +665 -0
  153. ui_10x/rio/__init__.py +0 -0
  154. ui_10x/rio/apps/examples/examples/__init__.py +22 -0
  155. ui_10x/rio/apps/examples/examples/components/__init__.py +3 -0
  156. ui_10x/rio/apps/examples/examples/components/collection_editor.py +15 -0
  157. ui_10x/rio/apps/examples/examples/pages/collection_editor.py +21 -0
  158. ui_10x/rio/apps/examples/examples/pages/login_page.py +88 -0
  159. ui_10x/rio/apps/examples/examples/pages/style_sheet.py +21 -0
  160. ui_10x/rio/apps/examples/rio.toml +14 -0
  161. ui_10x/rio/component_builder.py +497 -0
  162. ui_10x/rio/components/__init__.py +9 -0
  163. ui_10x/rio/components/group_box.py +31 -0
  164. ui_10x/rio/components/labeled_checkbox.py +18 -0
  165. ui_10x/rio/components/line_edit.py +37 -0
  166. ui_10x/rio/components/radio_button.py +32 -0
  167. ui_10x/rio/components/separator.py +24 -0
  168. ui_10x/rio/components/splitter.py +121 -0
  169. ui_10x/rio/components/tree_view.py +75 -0
  170. ui_10x/rio/conftest.py +35 -0
  171. ui_10x/rio/internals/__init__.py +0 -0
  172. ui_10x/rio/internals/app.py +192 -0
  173. ui_10x/rio/manual_tests/__init__.py +0 -0
  174. ui_10x/rio/manual_tests/basic_test.py +24 -0
  175. ui_10x/rio/manual_tests/splitter.py +27 -0
  176. ui_10x/rio/platform_implementation.py +91 -0
  177. ui_10x/rio/style_sheet.py +53 -0
  178. ui_10x/rio/unit_tests/test_collection_editor.py +68 -0
  179. ui_10x/rio/unit_tests/test_internals.py +630 -0
  180. ui_10x/rio/unit_tests/test_style_sheet.py +37 -0
  181. ui_10x/rio/widgets/__init__.py +46 -0
  182. ui_10x/rio/widgets/application.py +109 -0
  183. ui_10x/rio/widgets/button.py +48 -0
  184. ui_10x/rio/widgets/button_group.py +60 -0
  185. ui_10x/rio/widgets/calendar.py +23 -0
  186. ui_10x/rio/widgets/checkbox.py +24 -0
  187. ui_10x/rio/widgets/dialog.py +137 -0
  188. ui_10x/rio/widgets/group_box.py +27 -0
  189. ui_10x/rio/widgets/layout.py +34 -0
  190. ui_10x/rio/widgets/line_edit.py +37 -0
  191. ui_10x/rio/widgets/list.py +105 -0
  192. ui_10x/rio/widgets/message_box.py +70 -0
  193. ui_10x/rio/widgets/scroll_area.py +31 -0
  194. ui_10x/rio/widgets/spacer.py +6 -0
  195. ui_10x/rio/widgets/splitter.py +45 -0
  196. ui_10x/rio/widgets/text_edit.py +28 -0
  197. ui_10x/rio/widgets/tree.py +89 -0
  198. ui_10x/rio/widgets/unit_tests/test_button.py +101 -0
  199. ui_10x/rio/widgets/unit_tests/test_button_group.py +33 -0
  200. ui_10x/rio/widgets/unit_tests/test_calendar.py +114 -0
  201. ui_10x/rio/widgets/unit_tests/test_checkbox.py +109 -0
  202. ui_10x/rio/widgets/unit_tests/test_group_box.py +158 -0
  203. ui_10x/rio/widgets/unit_tests/test_label.py +43 -0
  204. ui_10x/rio/widgets/unit_tests/test_line_edit.py +140 -0
  205. ui_10x/rio/widgets/unit_tests/test_list.py +146 -0
  206. ui_10x/table_header_view.py +305 -0
  207. ui_10x/table_view.py +174 -0
  208. ui_10x/trait_editor.py +189 -0
  209. ui_10x/trait_widget.py +131 -0
  210. ui_10x/traitable_editor.py +200 -0
  211. ui_10x/traitable_view.py +131 -0
  212. ui_10x/unit_tests/conftest.py +8 -0
  213. ui_10x/unit_tests/test_platform.py +9 -0
  214. ui_10x/utils.py +661 -0
core_10x/xxcalendar.py ADDED
@@ -0,0 +1,228 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import deque
4
+ from datetime import date, timedelta
5
+
6
+ from core_10x.traitable import RT, T, Traitable
7
+
8
+
9
+ class CalendarNameParser:
10
+ """
11
+ calendar_name := just_name | operation_repr_list
12
+ operation_repr_list := operation_repr | operation_repr OP_CHAR operation_repr_list
13
+ operation_repr := op num_args | name_list op num_args
14
+ name_list := just_name MORE_CHAR just_name MORE_CHAR | just_name MORE_CHAR name_list
15
+ op := OR_CHAR | AND_CHAR
16
+ """
17
+
18
+ # fmt: off
19
+ MORE_CHAR = ','
20
+ OR_CHAR = '|'
21
+ AND_CHAR = '&'
22
+ OP_CHAR = '\n'
23
+
24
+ s_ops = {
25
+ OR_CHAR: set.update,
26
+ AND_CHAR: set.intersection_update
27
+ }
28
+ # fmt: on
29
+
30
+ @classmethod
31
+ def combo_name(cls, name: str) -> bool:
32
+ return any(sym in name for sym in cls.s_ops.keys())
33
+
34
+ @classmethod
35
+ def operation_repr(cls, calendar_cls, op_char: str, *calendars) -> str:
36
+ assert len(calendars) > 1, 'there must be at least 2 calendars'
37
+ assert op_char in cls.s_ops, f'Unknown op char = {op_char}'
38
+
39
+ cal_names = []
40
+ for cal_or_name in calendars:
41
+ if isinstance(cal_or_name, calendar_cls):
42
+ cname = cal_or_name.name
43
+ elif isinstance(cal_or_name, str):
44
+ cal = calendar_cls.existing_instance(name=cal_or_name)
45
+ assert cal, f"Unknown calendar '{cal_or_name}'"
46
+ cname = cal_or_name
47
+ else:
48
+ raise NameError(f"Invalid calendar/name '{cal_or_name}'")
49
+
50
+ cal_names.append(cname)
51
+
52
+ return f'{cls.MORE_CHAR.join(sorted(cal_names))}{cls.MORE_CHAR}{op_char}{len(cal_names)}{cls.OP_CHAR}'
53
+
54
+ def __init__(self):
55
+ self.stack = deque()
56
+ self.non_working_days = set()
57
+
58
+ @classmethod
59
+ def parse(cls, calendar_cls, name: str) -> set:
60
+ parser = cls()
61
+ non_working_days = parser.non_working_days
62
+ stack = parser.stack
63
+
64
+ operation_repr_list = name.split(cls.OP_CHAR)
65
+ if len(operation_repr_list) == 1: # -- regular (stored) calendar
66
+ cal = calendar_cls.existing_instance(name=name)
67
+ return cal._non_working_days
68
+
69
+ for operation_repr in operation_repr_list:
70
+ if not operation_repr:
71
+ continue
72
+
73
+ name_list_op_num_args = operation_repr.split(cls.MORE_CHAR)
74
+ if len(name_list_op_num_args) > 1: # -- name list followed by op with num_args
75
+ for cname in name_list_op_num_args[:-1]:
76
+ if cname:
77
+ cal = calendar_cls.existing_instance(name=cname)
78
+ assert cal, f"Unknown calendar '{cname}"
79
+ stack.append(cal._non_working_days)
80
+
81
+ op_with_num_args = name_list_op_num_args[-1]
82
+ op_char = op_with_num_args[0]
83
+ op = cls.s_ops.get(op_char)
84
+ assert op, f'Unknown op char {op_char}'
85
+ try:
86
+ num_args = int(op_with_num_args[1:])
87
+ except Exception as e:
88
+ raise RuntimeError(f'Invalid num_args = {op_with_num_args[1:]}') from e
89
+
90
+ for _ in range(num_args):
91
+ _non_working_days = stack.pop()
92
+ assert isinstance(_non_working_days, set)
93
+
94
+ if not non_working_days:
95
+ non_working_days.update(_non_working_days)
96
+ else:
97
+ op(non_working_days, _non_working_days)
98
+
99
+ stack.append(non_working_days) # -- push set of non_working_days of an intermediate calendar
100
+
101
+ return non_working_days
102
+
103
+
104
+ class CalendarAdjustment(Traitable):
105
+ # fmt: off
106
+ name: str = T(T.ID)
107
+ add_days: list = T()
108
+ remove_days: list = T()
109
+ # fmt: on
110
+
111
+
112
+ # -- TODO: _keep_history=True, _default_cache = True:
113
+ class Calendar(Traitable):
114
+ # fmt: off
115
+ name: str = T(T.ID)
116
+ adjusted_for: str = T(T.ID, default = '') // 'Name of a specific adjustment to this calendar, if any'
117
+ description: str = T(T.NOT_EMPTY) // 'Calendar Description'
118
+ non_working_days: list = T() // 'Non-Working Days'
119
+
120
+ _non_working_days: set = RT()
121
+ # fmt: on
122
+
123
+ @classmethod
124
+ def AND(cls, *calendars) -> Calendar: # noqa: N802
125
+ if not calendars:
126
+ return None
127
+
128
+ name = CalendarNameParser.operation_repr(cls, CalendarNameParser.AND_CHAR, *calendars)
129
+ return cls(name=name)
130
+
131
+ intersection = AND
132
+
133
+ @classmethod
134
+ def OR(cls, *calendars) -> Calendar: # noqa: N802
135
+ if not calendars:
136
+ return None
137
+
138
+ name = CalendarNameParser.operation_repr(cls, CalendarNameParser.OR_CHAR, *calendars)
139
+ return cls(name=name)
140
+
141
+ @classmethod
142
+ def union(cls, *calendars) -> Calendar:
143
+ if len(calendars) > 1:
144
+ return cls.OR(*calendars)
145
+ assert calendars, 'At least one calendar is required for union()'
146
+ return cls(name=calendars[0])
147
+
148
+ def non_working_days_get(self) -> list:
149
+ non_working_days = CalendarNameParser.parse(self.__class__, self.name)
150
+ adjusted_for = self.adjusted_for
151
+ if adjusted_for:
152
+ ca = CalendarAdjustment.existing_instance_by_id(_id_value=adjusted_for, _throw=False)
153
+ if ca:
154
+ self.add_days(non_working_days, *ca.add_days)
155
+ self.remove_days(non_working_days, *ca.remove_days)
156
+
157
+ return sorted(non_working_days) if non_working_days else []
158
+
159
+ def _non_working_days_get(self) -> set:
160
+ return set(self.non_working_days)
161
+
162
+ @classmethod
163
+ def add_days(cls, days: set, *days_to_add) -> bool:
164
+ if not days_to_add:
165
+ return False
166
+
167
+ all_dates = all(type(hd) is date for hd in days_to_add)
168
+ if not all_dates:
169
+ raise TypeError('Every day to add must be a date')
170
+
171
+ ndays = len(days)
172
+ days.update(days_to_add)
173
+ return len(days) > ndays
174
+
175
+ @classmethod
176
+ def remove_days(cls, days: set, *days_to_remove) -> bool:
177
+ if not days_to_remove:
178
+ return False
179
+
180
+ all_dates = all(type(hd) is date for hd in days_to_remove)
181
+ if not all_dates:
182
+ raise TypeError('Every day to remove must be a date')
183
+
184
+ ndays = len(days)
185
+ days.difference_update(days_to_remove)
186
+ return len(days) < ndays
187
+
188
+ def add_non_working_days(self, *days_to_add):
189
+ non_working_days = set(self.non_working_days)
190
+ if self.add_days(non_working_days, *days_to_add):
191
+ self.non_working_days = list(non_working_days)
192
+
193
+ def remove_non_working_days(self, *days_to_remove):
194
+ non_working_days = set(self.non_working_days)
195
+ if self.remove_days(non_working_days, *days_to_remove):
196
+ self.non_working_days = list(non_working_days)
197
+
198
+ def is_bizday(self, d: date) -> bool:
199
+ return d not in self._non_working_days
200
+
201
+ def next_bizday(self, d: date) -> date:
202
+ dt = timedelta(days=1)
203
+ d += dt
204
+ while not self.is_bizday(d):
205
+ d += dt
206
+
207
+ return d
208
+
209
+ def prev_bizday(self, d: date) -> date:
210
+ dt = timedelta(days=-1)
211
+ d += dt
212
+ while not self.is_bizday(d):
213
+ d += dt
214
+
215
+ return d # -- TODO: if the cal has ALL days as holidays... :-)
216
+
217
+ def advance_bizdays(self, d: date, biz_days: int) -> date:
218
+ if not biz_days:
219
+ return d
220
+
221
+ if biz_days > 0:
222
+ for _ in range(biz_days):
223
+ d = self.next_bizday(d)
224
+ else:
225
+ for _ in range(-biz_days):
226
+ d = self.prev_bizday(d)
227
+
228
+ return d
infra_10x/__init__.py ADDED
File without changes
File without changes
@@ -0,0 +1,16 @@
1
+ from datetime import date
2
+
3
+ from core_10x.code_samples.person import Person
4
+ from core_10x.exec_control import GRAPH_ON
5
+
6
+ from infra_10x.mongodb_store import MongoStore
7
+
8
+ if __name__ == '__main__':
9
+ with MongoStore.instance(hostname='localhost', dbname='test', username='', password=''):
10
+ p = Person(first_name='Ilya', last_name='Pevzner')
11
+ p.dob = date(1971, 7, 1)
12
+ p.save()
13
+ p.dob = date(1971, 7, 3)
14
+ assert p.dob == date(1971, 7, 3)
15
+ with GRAPH_ON():
16
+ assert p.dob == date(1971, 7, 3) # FIX: fails here!
@@ -0,0 +1,25 @@
1
+ if __name__ == '__main__':
2
+ from infra_10x.mongodb_store import MongoCollectionHelper, MongoStore
3
+
4
+ store = MongoStore.instance('localhost', 'test', '', '')
5
+
6
+ id_value = 'AAAA'
7
+ rev = 10
8
+
9
+ serialized_traitable = dict(_id=id_value, _rev=rev, name='test', age=60)
10
+
11
+ coll = store.collection('test')
12
+
13
+ data1 = dict(serialized_traitable)
14
+ pipeline1 = []
15
+ filter1 = {}
16
+ coll.filter_and_pipeline('_id', '_rev', id_value, rev, data1, filter1, pipeline1)
17
+
18
+ data2 = dict(serialized_traitable)
19
+ pipeline2 = []
20
+ filter2 = {}
21
+
22
+ MongoCollectionHelper.prepare_filter_and_pipeline(data2, filter2, pipeline2)
23
+
24
+ assert filter1 == filter2
25
+ assert pipeline1 == pipeline2
@@ -0,0 +1,111 @@
1
+ from infra_10x.mongodb_store import MongoStore
2
+
3
+
4
+ class MongodbAdmin:
5
+ def __init__(self, hostname: str, username: str, password: str):
6
+ self.client = MongoStore.connect(hostname=hostname, username=username, password=password, _cache=False)
7
+ self.db = self.client[MongoStore.ADMIN]
8
+ self.hostname = hostname
9
+ self.username = username
10
+
11
+ def user_exists(self, username: str) -> bool:
12
+ res = self.db.command('usersInfo', {'user': username, 'db': MongoStore.ADMIN})
13
+ return bool(res.get('users'))
14
+
15
+ def update_user(self, username: str, password: str, *role_names, keep_current_roles=True):
16
+ create = not self.user_exists(username)
17
+ if create:
18
+ if not password:
19
+ raise RuntimeError(f'User {username} does not exist and requires a non-empty password')
20
+
21
+ cmd = dict(createUser=username, pwd=password, roles=[*role_names])
22
+
23
+ else:
24
+ if not role_names and keep_current_roles:
25
+ role_names = self.user_role_names(username)
26
+
27
+ cmd = dict(updateUser=username)
28
+ if role_names:
29
+ cmd.update(roles=[*role_names])
30
+ if password:
31
+ cmd.update(pwd=password)
32
+
33
+ self.db.command(cmd)
34
+
35
+ def change_own_password(self, password: str):
36
+ self.db.command(dict(updateUser=self.username, pwd=password))
37
+ self.client = MongoStore.connect(hostname=self.hostname, username=self.username, password=password, _cache=False)
38
+ self.db = self.client[MongoStore.ADMIN]
39
+
40
+ # # Discover which user/db you're authenticated as (handy to avoid hardcoding)
41
+ # status = client.admin.command("connectionStatus", showPrivileges = False)
42
+ # auth = status[ "authInfo" ][ "authenticatedUsers" ]
43
+ # if not auth:
44
+ # raise RuntimeError("Not authenticated. Connect with your current username/password first.")
45
+ #
46
+ # username = auth[0]['user']
47
+ # user_db = auth[0]['db']
48
+ #
49
+ # # Change your password (roles remain unchanged because we don't pass 'roles')
50
+ # client[user_db].command("updateUser", username, pwd = "<NEW_STRONG_PASSWORD>")
51
+ #
52
+ # # Reconnect with the new password (recommended)
53
+ # client = MongoClient(f"mongodb://{username}:<NEW_STRONG_PASSWORD>@localhost:27017/?authSource={user_db}")
54
+ # client.admin.command("ping")
55
+ # print("Password changed and re-authenticated successfully.")
56
+
57
+ def update_role(self, role_name: str, **permissions_per_db):
58
+ """
59
+ :param role_name: e.g., 'VISITOR'
60
+ :param permissions_per_db: a dict: each key is either a db name or db_name/collection_name, value is PERMISSION, e.g.
61
+ { 'catalog': PERMISSION.READ_ONLY, 'logs/errors': PERMISSION.READ_WRITE }
62
+ :return:
63
+ """
64
+ privileges = [
65
+ dict(resource=dict(db=resource_parts[0], collection=resource_parts[1] if len(resource_parts) == 2 else ''), actions=permission)
66
+ for db_name, permission in permissions_per_db.items()
67
+ if (resource_parts := db_name.split('/'))
68
+ ]
69
+ method = 'createRole' if not self.role_exists(role_name) else 'updateRole'
70
+ self.db.command(method, role_name, privileges=privileges, roles=[])
71
+
72
+ def all_user_names(self) -> list:
73
+ listing = self.db.command('usersInfo')
74
+ return [doc['user'] for doc in listing['users']] if listing['ok'] else []
75
+
76
+ def all_role_names(self) -> list:
77
+ listing = self.db.command('rolesInfo')
78
+ return [doc['role'] for doc in listing['roles']] if listing['ok'] else []
79
+
80
+ def role_exists(self, role_name: str) -> bool:
81
+ res = self.db.command('rolesInfo', dict(role=role_name, db=MongoStore.ADMIN))
82
+ return bool(res['roles'])
83
+
84
+ def user_role_names(self, username: str) -> list:
85
+ res = self.db.command(dict(usersInfo=username))
86
+ if not res:
87
+ return []
88
+
89
+ return [r['role'] for r in res['users'][0]['roles']]
90
+
91
+ def delete_all_roles(self):
92
+ db = self.db
93
+ for role_name in self.all_role_names():
94
+ db.command('dropRole', role_name)
95
+
96
+ def delete_role(self, role_name: str):
97
+ if self.role_exists(role_name):
98
+ self.db.command('dropRole', role_name)
99
+
100
+ def delete_all_users(self):
101
+ db = self.db
102
+ me = self.username
103
+ for username in self.all_user_names():
104
+ if username != me:
105
+ db.command('dropUser', username)
106
+
107
+ def delete_user(self, username: str):
108
+ me = self.username
109
+ if me != username:
110
+ if self.user_exists(username):
111
+ self.db.command('dropUser', username)