dmart 0.1.4__py3-none-any.whl → 0.1.8__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 (49) hide show
  1. alembic/scripts/__init__.py +0 -0
  2. alembic/scripts/calculate_checksums.py +77 -0
  3. alembic/scripts/migration_f7a4949eed19.py +28 -0
  4. alembic/versions/0f3d2b1a7c21_add_authz_materialized_views.py +87 -0
  5. alembic/versions/10d2041b94d4_last_checksum_history.py +62 -0
  6. alembic/versions/1cf4e1ee3cb8_ext_permission_with_filter_fields_values.py +33 -0
  7. alembic/versions/26bfe19b49d4_rm_failedloginattempts.py +42 -0
  8. alembic/versions/3c8bca2219cc_add_otp_table.py +38 -0
  9. alembic/versions/6675fd9dfe42_remove_unique_from_sessions_table.py +36 -0
  10. alembic/versions/71bc1df82e6a_adding_user_last_login_at.py +43 -0
  11. alembic/versions/74288ccbd3b5_initial.py +264 -0
  12. alembic/versions/7520a89a8467_rm_activesession_table.py +39 -0
  13. alembic/versions/848b623755a4_make_created_nd_updated_at_required.py +138 -0
  14. alembic/versions/8640dcbebf85_add_notes_to_users.py +32 -0
  15. alembic/versions/91c94250232a_adding_fk_on_owner_shortname.py +104 -0
  16. alembic/versions/98ecd6f56f9a_ext_meta_with_owner_group_shortname.py +66 -0
  17. alembic/versions/9aae9138c4ef_indexing_created_at_updated_at.py +80 -0
  18. alembic/versions/__init__.py +0 -0
  19. alembic/versions/b53f916b3f6d_json_to_jsonb.py +492 -0
  20. alembic/versions/eb5f1ec65156_adding_user_locked_to_device.py +36 -0
  21. alembic/versions/f7a4949eed19_adding_query_policies_to_meta.py +60 -0
  22. api/user/model/__init__.py +0 -0
  23. api/user/model/errors.py +14 -0
  24. api/user/model/requests.py +165 -0
  25. api/user/model/responses.py +11 -0
  26. dmart-0.1.8.dist-info/METADATA +64 -0
  27. {dmart-0.1.4.dist-info → dmart-0.1.8.dist-info}/RECORD +48 -5
  28. plugins/action_log/__init__.py +0 -0
  29. plugins/action_log/plugin.py +121 -0
  30. plugins/admin_notification_sender/__init__.py +0 -0
  31. plugins/admin_notification_sender/plugin.py +124 -0
  32. plugins/ldap_manager/__init__.py +0 -0
  33. plugins/ldap_manager/plugin.py +100 -0
  34. plugins/local_notification/__init__.py +0 -0
  35. plugins/local_notification/plugin.py +123 -0
  36. plugins/realtime_updates_notifier/__init__.py +0 -0
  37. plugins/realtime_updates_notifier/plugin.py +58 -0
  38. plugins/redis_db_update/__init__.py +0 -0
  39. plugins/redis_db_update/plugin.py +188 -0
  40. plugins/resource_folders_creation/__init__.py +0 -0
  41. plugins/resource_folders_creation/plugin.py +81 -0
  42. plugins/system_notification_sender/__init__.py +0 -0
  43. plugins/system_notification_sender/plugin.py +188 -0
  44. plugins/update_access_controls/__init__.py +0 -0
  45. plugins/update_access_controls/plugin.py +9 -0
  46. dmart-0.1.4.dist-info/METADATA +0 -9
  47. {dmart-0.1.4.dist-info → dmart-0.1.8.dist-info}/WHEEL +0 -0
  48. {dmart-0.1.4.dist-info → dmart-0.1.8.dist-info}/entry_points.txt +0 -0
  49. {dmart-0.1.4.dist-info → dmart-0.1.8.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,100 @@
1
+ from fastapi.logger import logger
2
+ from models.core import Event, PluginBase, User
3
+ from models.enums import ActionType
4
+ from utils.settings import settings
5
+ from data_adapters.adapter import data_adapter as db
6
+
7
+ from ldap3 import AUTO_BIND_NO_TLS, MODIFY_REPLACE, Server, Connection, ALL
8
+
9
+
10
+ class Plugin(PluginBase):
11
+
12
+ def __init__(self) -> None:
13
+ super().__init__()
14
+ try:
15
+ server = Server(settings.ldap_url, get_info=ALL)
16
+ self.conn = Connection(
17
+ server,
18
+ user=settings.ldap_admin_dn,
19
+ password=settings.ldap_pass,
20
+ auto_bind=AUTO_BIND_NO_TLS
21
+ )
22
+ except Exception:
23
+ logger.error(
24
+ "Failed to connect to LDAP"
25
+ )
26
+
27
+
28
+
29
+ async def hook(self, data: Event):
30
+ if not hasattr(self, "conn"):
31
+ return
32
+
33
+ # Type narrowing for PyRight
34
+ if not isinstance(data.shortname, str):
35
+ logger.warning(
36
+ "data.shortname is None and str is required at ldap_manager"
37
+ )
38
+ return
39
+
40
+ if data.action_type == ActionType.delete:
41
+ self.delete(data.shortname)
42
+ return
43
+
44
+ user_model: User = await db.load(
45
+ space_name=settings.management_space,
46
+ subpath=data.subpath,
47
+ shortname=data.shortname,
48
+ class_type=User
49
+ )
50
+
51
+ if data.action_type == ActionType.create:
52
+ self.add(data.shortname, user_model)
53
+
54
+ elif data.action_type == ActionType.update:
55
+ self.modify(data.shortname, user_model)
56
+
57
+
58
+ elif data.action_type == ActionType.move and "src_shortname" in data.attributes:
59
+ self.delete(data.attributes['src_shortname'])
60
+ self.add(data.shortname, user_model)
61
+
62
+
63
+
64
+ def delete(self, shortname: str):
65
+ self.conn.delete(f"cn={shortname},{settings.ldap_root_dn}")
66
+
67
+ def add(
68
+ self,
69
+ shortname: str,
70
+ user_model: User
71
+ ):
72
+ self.conn.add(
73
+ f"cn={shortname},{settings.ldap_root_dn}",
74
+ 'dmartUser',
75
+ {
76
+ "cn": shortname.encode(),
77
+ "sn": shortname.encode(),
78
+ "gn": str(getattr(user_model, "displayname", "")).encode(),
79
+ "userPassword": getattr(user_model, "password", "").encode()
80
+ }
81
+ )
82
+
83
+ def modify(
84
+ self,
85
+ shortname: str,
86
+ user_model: User
87
+ ):
88
+ self.conn.modify(
89
+ f"cn={shortname},{settings.ldap_root_dn}",
90
+ {
91
+ "gn": [(
92
+ MODIFY_REPLACE,
93
+ [str(getattr(user_model, "displayname", "")).encode()]
94
+ )],
95
+ "userPassword": [(
96
+ MODIFY_REPLACE,
97
+ [getattr(user_model, "password", "").encode()]
98
+ )],
99
+ }
100
+ )
File without changes
@@ -0,0 +1,123 @@
1
+ import sys
2
+ from models.core import Content, Payload, PluginBase, Event, Reaction, Comment, Relationship, Locator
3
+ from models.enums import ContentType
4
+ from utils.helpers import camel_case
5
+ from data_adapters.adapter import data_adapter as db
6
+ from uuid import uuid4
7
+ from fastapi.logger import logger
8
+ from utils.async_request import AsyncRequest
9
+ from utils.settings import settings
10
+
11
+
12
+ class Plugin(PluginBase):
13
+ async def hook(self, data: Event):
14
+ if not isinstance(data.shortname, str):
15
+ logger.error("data.shortname is None and str is required at local_notification")
16
+ return
17
+
18
+ if data.resource_type is None:
19
+ return
20
+
21
+ resource_type = data.resource_type
22
+ try:
23
+ class_type = getattr(sys.modules["models.core"], camel_case(resource_type))
24
+ except AttributeError:
25
+ logger.warning(
26
+ f"local_notification: unsupported resource_type={resource_type!s} (no model class found)"
27
+ )
28
+ return
29
+ parent_subpath, parent_shortname, parent_owner = None, None, None
30
+
31
+ entry = await db.load(
32
+ space_name=data.space_name,
33
+ subpath=data.subpath,
34
+ shortname=data.shortname,
35
+ class_type=class_type,
36
+ user_shortname=data.user_shortname
37
+ )
38
+ relationship = None
39
+ if class_type in [Reaction, Comment]:
40
+ parent_subpath, parent_shortname = data.subpath.rsplit("/", 1)
41
+ parent = await db.load(
42
+ space_name=data.space_name,
43
+ subpath=parent_subpath,
44
+ shortname=parent_shortname,
45
+ class_type=Content,
46
+ user_shortname=data.user_shortname
47
+ )
48
+ if parent.owner_shortname == data.user_shortname:
49
+ return
50
+
51
+ parent_owner = parent.owner_shortname
52
+
53
+ relationship = Relationship(
54
+ related_to=Locator(
55
+ type=resource_type,
56
+ space_name=data.space_name,
57
+ subpath=parent_subpath,
58
+ shortname=parent_shortname
59
+ ),
60
+ attributes={
61
+ "parent_owner": parent_owner
62
+ }
63
+ )
64
+
65
+ else:
66
+ if not entry.owner_shortname or entry.owner_shortname == data.user_shortname:
67
+ return
68
+ parent_owner = entry.owner_shortname
69
+
70
+ uuid = uuid4()
71
+
72
+ meta_obj = Content(
73
+ uuid=uuid,
74
+ shortname=str(uuid)[:8],
75
+ owner_shortname=data.user_shortname,
76
+ is_active=True,
77
+ payload=Payload(
78
+ content_type=ContentType.json,
79
+ schema_shortname="notification",
80
+ body=f"{str(uuid)[:8]}.json"
81
+ ),
82
+ )
83
+
84
+ if relationship is not None:
85
+ meta_obj.relationships = [relationship]
86
+
87
+ await db.save(
88
+ "personal",
89
+ f"people/{parent_owner}/notifications",
90
+ meta_obj,
91
+ )
92
+
93
+ notification_obj = {
94
+ "entry_space": data.space_name,
95
+ "entry_subpath": data.subpath,
96
+ "entry_shortname": data.shortname,
97
+
98
+ "action_by": data.user_shortname,
99
+ "action_type": data.action_type,
100
+ "resource_type": data.resource_type,
101
+ "is_read": "no"
102
+ }
103
+ await db.save_payload_from_json(
104
+ "personal",
105
+ f"people/{parent_owner}/notifications",
106
+ meta_obj,
107
+ notification_obj,
108
+ )
109
+
110
+ if not settings.websocket_url:
111
+ return
112
+
113
+ async with AsyncRequest() as client:
114
+ await client.post(
115
+ f"{settings.websocket_url}/send-message/{parent_owner}",
116
+ json={
117
+ "type": "notification",
118
+ "channels": [
119
+ f"{data.space_name}:__ALL__:__ALL__:__ALL__:__ALL__",
120
+ ],
121
+ "message": notification_obj
122
+ }
123
+ )
File without changes
@@ -0,0 +1,58 @@
1
+ from models.core import PluginBase, Event
2
+ from utils.async_request import AsyncRequest
3
+ from utils.settings import settings
4
+
5
+
6
+ class Plugin(PluginBase):
7
+
8
+ async def hook(self, data: Event):
9
+ all_MKW = "__ALL__"
10
+
11
+ state = data.attributes.get("state", all_MKW)
12
+
13
+ # if subpath = parent/child
14
+ # send to channels with subpaths "parent" and "parent/child"
15
+ channels = []
16
+ subpath = ""
17
+ for subpath_part in data.subpath.split("/"):
18
+ if not subpath_part:
19
+ continue
20
+ subpath += subpath_part
21
+
22
+ # Consider channels with __ALL__ magic word
23
+ if not subpath.startswith("/"):
24
+ subpath = "/" + subpath
25
+ channels.extend([
26
+ f"{data.space_name}:{subpath}:{all_MKW}:{data.action_type}:{state}",
27
+ f"{data.space_name}:{subpath}:{all_MKW}:{all_MKW}:{state}",
28
+ f"{data.space_name}:{subpath}:{all_MKW}:{data.action_type}:{all_MKW}",
29
+ f"{data.space_name}:{subpath}:{all_MKW}:{all_MKW}:{all_MKW}",
30
+ ])
31
+ if data.schema_shortname:
32
+ channels.extend([
33
+ f"{data.space_name}:{subpath}:{data.schema_shortname}:{data.action_type}:{state}",
34
+ f"{data.space_name}:{subpath}:{data.schema_shortname}:{all_MKW}:{state}",
35
+ f"{data.space_name}:{subpath}:{data.schema_shortname}:{data.action_type}:{all_MKW}",
36
+ f"{data.space_name}:{subpath}:{data.schema_shortname}:{all_MKW}:{all_MKW}",
37
+ ])
38
+ subpath += "/"
39
+ if not settings.websocket_url:
40
+ return
41
+
42
+ async with AsyncRequest() as client:
43
+ await client.post(
44
+ f"{settings.websocket_url}/broadcast-to-channels",
45
+ json={
46
+ "type": "notification_subscription",
47
+ "channels": [*set(channels)],
48
+ "message": {
49
+ "title": "updated",
50
+ "subpath": data.subpath,
51
+ "space": data.space_name,
52
+ "shortname": data.shortname,
53
+ "action_type": data.action_type,
54
+ "owner_shortname": data.user_shortname
55
+ }
56
+ }
57
+ )
58
+
File without changes
@@ -0,0 +1,188 @@
1
+ import sys
2
+ from models.core import ActionType, Attachment, PluginBase, Event, Space
3
+ from utils.helpers import camel_case
4
+ from data_adapters.file.adapter_helpers import generate_payload_string
5
+ from data_adapters.adapter import data_adapter as db
6
+ from models import core
7
+ from models.enums import ContentType, ResourceType
8
+ from data_adapters.file.redis_services import RedisServices
9
+ from fastapi.logger import logger
10
+ from data_adapters.file.create_index import main as reload_redis
11
+ from utils.settings import settings
12
+ from typing import Any
13
+
14
+
15
+ class Plugin(PluginBase):
16
+ async def hook(self, data: Event):
17
+ if settings.active_data_db == "sql":
18
+ return
19
+
20
+ self.data = data
21
+ # Type narrowing for PyRight
22
+ if (
23
+ not isinstance(data.shortname, str)
24
+ or not isinstance(data.resource_type, ResourceType)
25
+ or not isinstance(data.attributes, dict)
26
+ ):
27
+ logger.error("invalid data at redis_db_update")
28
+ return
29
+
30
+ spaces = await db.get_spaces()
31
+ if (
32
+ data.space_name not in spaces
33
+ or not Space.model_validate_json(spaces[data.space_name]).indexing_enabled
34
+ ):
35
+ return
36
+
37
+ class_type = getattr(
38
+ sys.modules["models.core"],
39
+ camel_case(core.ResourceType(data.resource_type)),
40
+ )
41
+ if issubclass(class_type, Attachment):
42
+ await self.update_parent_entry_payload_string()
43
+ return
44
+
45
+ async with RedisServices() as redis_services:
46
+ if data.resource_type == ResourceType.folder and data.action_type in [
47
+ ActionType.delete,
48
+ ActionType.move,
49
+ ]:
50
+ await reload_redis(for_space=data.space_name)
51
+ return
52
+
53
+ if data.action_type == ActionType.delete:
54
+ doc_id = redis_services.generate_doc_id(
55
+ data.space_name,
56
+ "meta",
57
+ data.shortname,
58
+ data.subpath,
59
+ )
60
+ meta_doc = await redis_services.get_doc_by_id(doc_id)
61
+ # Delete meta doc
62
+ await redis_services.delete_doc(
63
+ data.space_name,
64
+ "meta",
65
+ data.shortname,
66
+ data.subpath,
67
+ )
68
+ # Delete payload doc
69
+ await redis_services.delete_doc(
70
+ data.space_name,
71
+ (
72
+ meta_doc.get("payload", {}).get("schema_shortname", "meta")
73
+ if meta_doc
74
+ else "meta"
75
+ ),
76
+ data.shortname,
77
+ data.subpath,
78
+ )
79
+ return
80
+ try:
81
+ meta = await db.load(
82
+ space_name=data.space_name,
83
+ subpath=data.subpath,
84
+ shortname=data.shortname,
85
+ class_type=class_type,
86
+ user_shortname=data.user_shortname,
87
+ )
88
+ except Exception as _:
89
+ return
90
+
91
+ if data.action_type in [
92
+ ActionType.create,
93
+ ActionType.update,
94
+ ActionType.progress_ticket,
95
+ ]:
96
+ meta_doc_id, meta_json = redis_services.prepare_meta_doc(
97
+ data.space_name, data.subpath, meta
98
+ )
99
+ payload: dict[str, Any] = {}
100
+ if (
101
+ meta.payload
102
+ and meta.payload.content_type == ContentType.json
103
+ and meta.payload.body is not None
104
+ ):
105
+ mypayload = await db.load_resource_payload(
106
+ space_name=data.space_name,
107
+ subpath=data.subpath,
108
+ filename=str(meta.payload.body),
109
+ class_type=class_type,
110
+ )
111
+ payload = mypayload if mypayload else {}
112
+
113
+ meta_json["payload_string"] = await generate_payload_string(
114
+ db,
115
+ space_name=data.space_name,
116
+ subpath=meta_json["subpath"],
117
+ shortname=meta_json["shortname"],
118
+ payload=payload,
119
+ ) if settings.store_payload_string else ""
120
+
121
+ await redis_services.save_doc(meta_doc_id, meta_json)
122
+ if meta.payload:
123
+ payload.update(meta_json)
124
+ await redis_services.save_payload_doc(
125
+ data.space_name,
126
+ data.subpath,
127
+ meta,
128
+ payload,
129
+ data.resource_type,
130
+ )
131
+
132
+ elif data.action_type == ActionType.move:
133
+ await redis_services.move_meta_doc(
134
+ data.space_name,
135
+ data.attributes["src_shortname"],
136
+ data.attributes["src_subpath"],
137
+ data.subpath,
138
+ meta,
139
+ )
140
+ if meta.payload and meta.payload.schema_shortname:
141
+ await redis_services.move_payload_doc(
142
+ data.space_name,
143
+ meta.payload.schema_shortname,
144
+ data.attributes["src_shortname"],
145
+ data.attributes["src_subpath"],
146
+ meta.shortname,
147
+ data.subpath,
148
+ )
149
+
150
+ async def update_parent_entry_payload_string(self) -> None:
151
+ async with RedisServices() as redis_services:
152
+ # get the parent meta doc
153
+ subpath_parts = self.data.subpath.strip("/").split("/")
154
+ if len(subpath_parts) <= 1:
155
+ return
156
+ parent_subpath, parent_shortname = (
157
+ "/".join(subpath_parts[:-1]),
158
+ subpath_parts[-1],
159
+ )
160
+ doc_id = redis_services.generate_doc_id(
161
+ self.data.space_name,
162
+ "meta",
163
+ parent_shortname,
164
+ parent_subpath,
165
+ )
166
+ meta_doc: dict = await redis_services.get_doc_by_id(doc_id)
167
+
168
+ if meta_doc is None:
169
+ raise Exception("Meta doc not found")
170
+
171
+ payload = {}
172
+ if meta_doc.get("payload_doc_id"):
173
+ payload_doc = await redis_services.get_doc_by_id(
174
+ meta_doc["payload_doc_id"]
175
+ )
176
+ payload = {k: v for k, v in payload_doc.items() if k not in meta_doc}
177
+
178
+ # generate the payload string
179
+ meta_doc["payload_string"] = await generate_payload_string(
180
+ db,
181
+ space_name=self.data.space_name,
182
+ subpath=parent_subpath,
183
+ shortname=parent_shortname,
184
+ payload=payload,
185
+ ) if settings.store_payload_string else ""
186
+
187
+ # update parent meta doc
188
+ await redis_services.save_doc(doc_id, meta_doc)
File without changes
@@ -0,0 +1,81 @@
1
+ from models.core import Folder, PluginBase, Event
2
+ from models.enums import ResourceType
3
+ from data_adapters.file.redis_services import RedisServices
4
+ from fastapi.logger import logger
5
+ from data_adapters.adapter import data_adapter as db
6
+ from utils.settings import settings
7
+
8
+
9
+ class Plugin(PluginBase):
10
+ async def hook(self, data: Event):
11
+ # Type narrowing for PyRight
12
+ if (
13
+ not isinstance(data.shortname, str)
14
+ or not isinstance(data.resource_type, ResourceType)
15
+ or not isinstance(data.attributes, dict)
16
+ ):
17
+ logger.error("invalid data at resource_folders_creation")
18
+ return
19
+
20
+ folders = []
21
+ if data.resource_type == ResourceType.user:
22
+ folders = [
23
+ ("personal", "people", data.shortname),
24
+ ("personal", f"people/{data.shortname}", "notifications"),
25
+ ("personal", f"people/{data.shortname}", "private"),
26
+ ("personal", f"people/{data.shortname}", "protected"),
27
+ ("personal", f"people/{data.shortname}", "public"),
28
+ ("personal", f"people/{data.shortname}", "inbox"),
29
+ ]
30
+ elif data.resource_type == ResourceType.space:
31
+ # sys_schemas = ["meta_schema", "folder_rendering"]
32
+ # for schema_name in sys_schemas:
33
+ # await clone(
34
+ # src_space=settings.management_space,
35
+ # src_subpath="schema",
36
+ # src_shortname=schema_name,
37
+ # dest_space=data.shortname,
38
+ # dest_subpath="schema",
39
+ # dest_shortname=schema_name,
40
+ # class_type=Schema,
41
+ # )
42
+ if settings.active_data_db == "file":
43
+ async with RedisServices() as redis_services:
44
+ await redis_services.create_indices(
45
+ for_space=data.shortname,
46
+ # for_schemas=sys_schemas,
47
+ for_custom_indices=False,
48
+ del_docs=False,
49
+ )
50
+
51
+ # redis_update_plugin = RedisUpdatePlugin()
52
+ # for schema_name in sys_schemas:
53
+ # await redis_update_plugin.hook(Event(
54
+ # space_name=data.shortname,
55
+ # subpath="schema",
56
+ # shortname=schema_name,
57
+ # action_type=ActionType.create,
58
+ # resource_type=ResourceType.schema,
59
+ # user_shortname=data.user_shortname
60
+ # ))
61
+
62
+ folders = [(data.shortname, "/", "schema")]
63
+
64
+ for folder in folders:
65
+ existing_folder = await db.load_or_none(
66
+ space_name=folder[0],
67
+ subpath=folder[1],
68
+ shortname=folder[2],
69
+ class_type=Folder,
70
+ user_shortname=data.user_shortname,
71
+ )
72
+ if existing_folder is None:
73
+ await db.internal_save_model(
74
+ space_name=folder[0],
75
+ subpath=folder[1],
76
+ meta=Folder(
77
+ shortname=folder[2],
78
+ is_active=True,
79
+ owner_shortname=data.user_shortname,
80
+ ),
81
+ )
File without changes