dmart 0.1.0__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.
- alembic/__init__.py +0 -0
- alembic/env.py +91 -0
- api/__init__.py +0 -0
- api/info/__init__.py +0 -0
- api/info/router.py +109 -0
- api/managed/__init__.py +0 -0
- api/managed/router.py +1541 -0
- api/managed/utils.py +1850 -0
- api/public/__init__.py +0 -0
- api/public/router.py +758 -0
- api/qr/__init__.py +0 -0
- api/qr/router.py +108 -0
- api/user/__init__.py +0 -0
- api/user/router.py +1401 -0
- api/user/service.py +270 -0
- bundler.py +44 -0
- config/__init__.py +0 -0
- config/channels.json +11 -0
- config/notification.json +17 -0
- data_adapters/__init__.py +0 -0
- data_adapters/adapter.py +16 -0
- data_adapters/base_data_adapter.py +467 -0
- data_adapters/file/__init__.py +0 -0
- data_adapters/file/adapter.py +2043 -0
- data_adapters/file/adapter_helpers.py +1013 -0
- data_adapters/file/archive.py +150 -0
- data_adapters/file/create_index.py +331 -0
- data_adapters/file/create_users_folders.py +52 -0
- data_adapters/file/custom_validations.py +68 -0
- data_adapters/file/drop_index.py +40 -0
- data_adapters/file/health_check.py +560 -0
- data_adapters/file/redis_services.py +1110 -0
- data_adapters/helpers.py +27 -0
- data_adapters/sql/__init__.py +0 -0
- data_adapters/sql/adapter.py +3210 -0
- data_adapters/sql/adapter_helpers.py +491 -0
- data_adapters/sql/create_tables.py +451 -0
- data_adapters/sql/create_users_folders.py +53 -0
- data_adapters/sql/db_to_json_migration.py +482 -0
- data_adapters/sql/health_check_sql.py +232 -0
- data_adapters/sql/json_to_db_migration.py +454 -0
- data_adapters/sql/update_query_policies.py +101 -0
- data_generator.py +81 -0
- dmart-0.1.0.dist-info/METADATA +27 -0
- dmart-0.1.0.dist-info/RECORD +106 -0
- dmart-0.1.0.dist-info/WHEEL +5 -0
- dmart-0.1.0.dist-info/entry_points.txt +2 -0
- dmart-0.1.0.dist-info/top_level.txt +23 -0
- dmart.py +513 -0
- get_settings.py +7 -0
- languages/__init__.py +0 -0
- languages/arabic.json +15 -0
- languages/english.json +16 -0
- languages/kurdish.json +14 -0
- languages/loader.py +13 -0
- main.py +506 -0
- migrate.py +24 -0
- models/__init__.py +0 -0
- models/api.py +203 -0
- models/core.py +597 -0
- models/enums.py +255 -0
- password_gen.py +8 -0
- plugins/__init__.py +0 -0
- pytests/__init__.py +0 -0
- pytests/api_user_models_erros_test.py +16 -0
- pytests/api_user_models_requests_test.py +98 -0
- pytests/archive_test.py +72 -0
- pytests/base_test.py +300 -0
- pytests/get_settings_test.py +14 -0
- pytests/json_to_db_migration_test.py +237 -0
- pytests/service_test.py +26 -0
- pytests/test_info.py +55 -0
- pytests/test_status.py +15 -0
- run_notification_campaign.py +98 -0
- scheduled_notification_handler.py +121 -0
- schema_migration.py +208 -0
- schema_modulate.py +192 -0
- set_admin_passwd.py +55 -0
- sync.py +202 -0
- utils/__init__.py +0 -0
- utils/access_control.py +306 -0
- utils/async_request.py +8 -0
- utils/exporter.py +309 -0
- utils/firebase_notifier.py +57 -0
- utils/generate_email.py +38 -0
- utils/helpers.py +352 -0
- utils/hypercorn_config.py +12 -0
- utils/internal_error_code.py +60 -0
- utils/jwt.py +124 -0
- utils/logger.py +167 -0
- utils/middleware.py +99 -0
- utils/notification.py +75 -0
- utils/password_hashing.py +16 -0
- utils/plugin_manager.py +215 -0
- utils/query_policies_helper.py +112 -0
- utils/regex.py +44 -0
- utils/repository.py +529 -0
- utils/router_helper.py +19 -0
- utils/settings.py +165 -0
- utils/sms_notifier.py +21 -0
- utils/social_sso.py +67 -0
- utils/templates/activation.html.j2 +26 -0
- utils/templates/reminder.html.j2 +17 -0
- utils/ticket_sys_utils.py +203 -0
- utils/web_notifier.py +29 -0
- websocket.py +231 -0
models/core.py
ADDED
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import json
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from pydantic import BaseModel, ConfigDict
|
|
5
|
+
from typing import Any, Dict, TypeVar
|
|
6
|
+
from pydantic.types import UUID4 as UUID
|
|
7
|
+
from uuid import uuid4
|
|
8
|
+
from pydantic import Field
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
import sys
|
|
11
|
+
from models.enums import (
|
|
12
|
+
ActionType,
|
|
13
|
+
ContentType,
|
|
14
|
+
Language,
|
|
15
|
+
NotificationPriority,
|
|
16
|
+
NotificationType,
|
|
17
|
+
ResourceType,
|
|
18
|
+
UserType,
|
|
19
|
+
ConditionType,
|
|
20
|
+
PluginType,
|
|
21
|
+
EventListenTime,
|
|
22
|
+
)
|
|
23
|
+
from utils.helpers import camel_case, remove_none_dict, snake_case
|
|
24
|
+
import utils.regex as regex
|
|
25
|
+
from utils.settings import settings
|
|
26
|
+
import utils.password_hashing as password_hashing
|
|
27
|
+
from hashlib import sha1 as hashlib_sha1
|
|
28
|
+
|
|
29
|
+
KeyType = TypeVar('KeyType')
|
|
30
|
+
def deep_update(mapping: Dict[KeyType, Any], *updating_mappings: Dict[KeyType, Any]) -> Dict[KeyType, Any]:
|
|
31
|
+
updated_mapping = mapping.copy()
|
|
32
|
+
for updating_mapping in updating_mappings:
|
|
33
|
+
for k, v in updating_mapping.items():
|
|
34
|
+
if k in updated_mapping and isinstance(updated_mapping[k], dict) and isinstance(v, dict):
|
|
35
|
+
updated_mapping[k] = deep_update(updated_mapping[k], v)
|
|
36
|
+
else:
|
|
37
|
+
updated_mapping[k] = v
|
|
38
|
+
return updated_mapping
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# class MoveModel(BaseModel):
|
|
42
|
+
# resource_type: ResourceType
|
|
43
|
+
# src_shortname: str = Field(regex=regex.SHORTNAME)
|
|
44
|
+
# src_subpath: str = Field(regex=regex.SUBPATH)
|
|
45
|
+
# dist_shortname: str = Field(default=None, regex=regex.SHORTNAME)
|
|
46
|
+
# dist_subpath: str = Field(default=None, regex=regex.SUBPATH)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Resource(BaseModel):
|
|
50
|
+
model_config = ConfigDict(use_enum_values=True, arbitrary_types_allowed=True)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Payload(Resource):
|
|
54
|
+
content_type: ContentType
|
|
55
|
+
content_sub_type: str | None = (
|
|
56
|
+
None # FIXME change to proper content type static hashmap
|
|
57
|
+
)
|
|
58
|
+
schema_shortname: str | None = None
|
|
59
|
+
client_checksum: str | None = None
|
|
60
|
+
checksum: str | None = None
|
|
61
|
+
body: str | dict[str, Any] | None
|
|
62
|
+
|
|
63
|
+
def __init__(self, **data):
|
|
64
|
+
BaseModel.__init__(self, **data)
|
|
65
|
+
|
|
66
|
+
if not self.checksum and self.body:
|
|
67
|
+
sha1 = hashlib_sha1()
|
|
68
|
+
|
|
69
|
+
if isinstance(self.body, dict):
|
|
70
|
+
sha1.update(json.dumps(self.body).encode('utf-8'))
|
|
71
|
+
else:
|
|
72
|
+
sha1.update(self.body.encode('utf-8'))
|
|
73
|
+
|
|
74
|
+
self.checksum = sha1.hexdigest()
|
|
75
|
+
|
|
76
|
+
def update(
|
|
77
|
+
self, payload: dict, old_body: dict | None = None, replace: bool = False
|
|
78
|
+
) -> dict | None:
|
|
79
|
+
|
|
80
|
+
if payload.get("body", None) is None:
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
if isinstance(payload["body"], dict):
|
|
84
|
+
if old_body and not replace:
|
|
85
|
+
separate_payload_body = dict(
|
|
86
|
+
remove_none_dict(
|
|
87
|
+
deep_update(
|
|
88
|
+
old_body,
|
|
89
|
+
payload["body"],
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
else:
|
|
94
|
+
separate_payload_body = payload["body"]
|
|
95
|
+
|
|
96
|
+
if "schema_shortname" in payload:
|
|
97
|
+
self.schema_shortname = payload["schema_shortname"]
|
|
98
|
+
|
|
99
|
+
return separate_payload_body
|
|
100
|
+
|
|
101
|
+
else:
|
|
102
|
+
self.body = payload["body"]
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class Record(BaseModel):
|
|
107
|
+
resource_type: ResourceType
|
|
108
|
+
uuid: UUID | None = None
|
|
109
|
+
shortname: str = Field(pattern=regex.SHORTNAME)
|
|
110
|
+
subpath: str = Field(pattern=regex.SUBPATH)
|
|
111
|
+
attributes: dict[str, Any]
|
|
112
|
+
attachments: dict[ResourceType, list[Any]] | None = None
|
|
113
|
+
retrieve_lock_status: bool = False
|
|
114
|
+
|
|
115
|
+
def __init__(self, **data):
|
|
116
|
+
BaseModel.__init__(self, **data)
|
|
117
|
+
if self.subpath != "/":
|
|
118
|
+
self.subpath = self.subpath.strip("/")
|
|
119
|
+
|
|
120
|
+
def to_dict(self):
|
|
121
|
+
return self.model_dump(exclude_none=True, warnings="error")
|
|
122
|
+
|
|
123
|
+
def __eq__(self, other):
|
|
124
|
+
return (
|
|
125
|
+
isinstance(other, Record)
|
|
126
|
+
and self.shortname == other.shortname
|
|
127
|
+
and self.subpath == other.subpath
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
model_config = ConfigDict(
|
|
131
|
+
extra= "forbid",
|
|
132
|
+
json_schema_extra= { "examples": [
|
|
133
|
+
{
|
|
134
|
+
"resource_type": "content",
|
|
135
|
+
"shortname": "auto",
|
|
136
|
+
"subpath": "/users",
|
|
137
|
+
"attributes": {
|
|
138
|
+
"is_active": True,
|
|
139
|
+
"slug": None,
|
|
140
|
+
"displayname": {
|
|
141
|
+
"en": "name en",
|
|
142
|
+
"ar": "name ar",
|
|
143
|
+
"ku": "name ku",
|
|
144
|
+
},
|
|
145
|
+
"description": {
|
|
146
|
+
"en": "desc en",
|
|
147
|
+
"ar": "desc ar",
|
|
148
|
+
"ku": "desc ku",
|
|
149
|
+
},
|
|
150
|
+
"tags": [],
|
|
151
|
+
"payload": {
|
|
152
|
+
"content_type": "json",
|
|
153
|
+
"schema_shortname": "user",
|
|
154
|
+
"body": {
|
|
155
|
+
"email": "myname@gmail.com",
|
|
156
|
+
"first_name": "John",
|
|
157
|
+
"language": "en",
|
|
158
|
+
"last_name": "Doo",
|
|
159
|
+
"mobile": "7999311703",
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
}
|
|
164
|
+
]
|
|
165
|
+
}
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
class Translation(Resource):
|
|
169
|
+
en: str | None = None
|
|
170
|
+
ar: str | None = None
|
|
171
|
+
ku: str | None = None
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class Locator(Resource):
|
|
175
|
+
uuid: UUID | None = None
|
|
176
|
+
domain: str | None = None
|
|
177
|
+
type: ResourceType
|
|
178
|
+
space_name: str
|
|
179
|
+
subpath: str
|
|
180
|
+
shortname: str
|
|
181
|
+
schema_shortname: str | None = None
|
|
182
|
+
displayname: Translation | None = None
|
|
183
|
+
description: Translation | None = None
|
|
184
|
+
tags: list[str] | None = None
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class Relationship(Resource):
|
|
188
|
+
related_to: Locator | dict[str, Any]
|
|
189
|
+
attributes: dict[str, Any]
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class ACL(BaseModel):
|
|
193
|
+
user_shortname: str
|
|
194
|
+
allowed_actions: list[ActionType]
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class Meta(Resource):
|
|
198
|
+
uuid: UUID = Field(default_factory=uuid4)
|
|
199
|
+
shortname: str = Field(pattern=regex.SHORTNAME)
|
|
200
|
+
slug: str | None = Field(default=None, pattern=regex.SLUG)
|
|
201
|
+
is_active: bool = False
|
|
202
|
+
displayname: Translation | None = None
|
|
203
|
+
description: Translation | None = None
|
|
204
|
+
tags: list[str] = []
|
|
205
|
+
created_at: datetime = Field(default_factory=datetime.now)
|
|
206
|
+
updated_at: datetime = Field(default_factory=datetime.now)
|
|
207
|
+
owner_shortname: str
|
|
208
|
+
owner_group_shortname: str | None = None
|
|
209
|
+
payload: Payload | None = None
|
|
210
|
+
relationships: list[Relationship] | list[dict[str, Any]] | None = None
|
|
211
|
+
acl: list[ACL] | None = None
|
|
212
|
+
last_checksum_history: str | None = Field(default=None)
|
|
213
|
+
|
|
214
|
+
model_config = ConfigDict(validate_assignment=True)
|
|
215
|
+
|
|
216
|
+
@staticmethod
|
|
217
|
+
def from_record(record: Record, owner_shortname: str):
|
|
218
|
+
if record.shortname == settings.auto_uuid_rule:
|
|
219
|
+
record.uuid = uuid4()
|
|
220
|
+
record.shortname = str(record.uuid)[:8]
|
|
221
|
+
record.attributes["uuid"] = record.uuid
|
|
222
|
+
|
|
223
|
+
meta_class = getattr(
|
|
224
|
+
sys.modules["models.core"], camel_case(record.resource_type)
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
if issubclass(meta_class, User) and "password" in record.attributes:
|
|
228
|
+
hashed_pass = password_hashing.hash_password(record.attributes["password"])
|
|
229
|
+
record.attributes["password"] = hashed_pass
|
|
230
|
+
|
|
231
|
+
record.attributes["owner_shortname"] = owner_shortname
|
|
232
|
+
record.attributes["shortname"] = record.shortname
|
|
233
|
+
return meta_class(**remove_none_dict(record.attributes))
|
|
234
|
+
|
|
235
|
+
@staticmethod
|
|
236
|
+
def check_record(record: Record, owner_shortname: str):
|
|
237
|
+
meta_class = getattr(
|
|
238
|
+
sys.modules["models.core"], camel_case(record.resource_type)
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
meta_obj = meta_class(
|
|
242
|
+
owner_shortname=owner_shortname,
|
|
243
|
+
shortname=record.shortname,
|
|
244
|
+
**record.attributes,
|
|
245
|
+
)
|
|
246
|
+
return meta_obj
|
|
247
|
+
|
|
248
|
+
def update_from_record(
|
|
249
|
+
self, record: Record, old_body: dict | None = None, replace: bool = False
|
|
250
|
+
) -> dict | None:
|
|
251
|
+
restricted_fields = [
|
|
252
|
+
"uuid",
|
|
253
|
+
"shortname",
|
|
254
|
+
"created_at",
|
|
255
|
+
"updated_at",
|
|
256
|
+
"owner_shortname",
|
|
257
|
+
"payload",
|
|
258
|
+
"acl",
|
|
259
|
+
]
|
|
260
|
+
for field_name, _ in self.__dict__.items():
|
|
261
|
+
if field_name in record.attributes and field_name not in restricted_fields:
|
|
262
|
+
if isinstance(self, User) and field_name == "password":
|
|
263
|
+
self.__setattr__(
|
|
264
|
+
field_name,
|
|
265
|
+
password_hashing.hash_password(record.attributes[field_name]),
|
|
266
|
+
)
|
|
267
|
+
continue
|
|
268
|
+
|
|
269
|
+
self.__setattr__(field_name, record.attributes[field_name])
|
|
270
|
+
|
|
271
|
+
if (
|
|
272
|
+
not self.payload
|
|
273
|
+
and "payload" in record.attributes
|
|
274
|
+
and record.attributes["payload"] is not None
|
|
275
|
+
and "content_type" in record.attributes["payload"]
|
|
276
|
+
):
|
|
277
|
+
self.payload = Payload(
|
|
278
|
+
content_type=ContentType(record.attributes["payload"]["content_type"]),
|
|
279
|
+
schema_shortname=record.attributes["payload"].get("schema_shortname"),
|
|
280
|
+
body=f"{record.shortname}.json",
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
if self.payload and "payload" in record.attributes:
|
|
284
|
+
return self.payload.update(
|
|
285
|
+
payload=record.attributes["payload"], old_body=old_body, replace=replace
|
|
286
|
+
)
|
|
287
|
+
return None
|
|
288
|
+
|
|
289
|
+
def to_record(
|
|
290
|
+
self,
|
|
291
|
+
subpath: str,
|
|
292
|
+
shortname: str,
|
|
293
|
+
include: list[str] = [],
|
|
294
|
+
) -> Record:
|
|
295
|
+
# Sanity check
|
|
296
|
+
|
|
297
|
+
if self.shortname != shortname:
|
|
298
|
+
raise Exception(
|
|
299
|
+
f"shortname in meta({subpath}/{self.shortname}) should be same as body({subpath}/{shortname})"
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
record_fields = {
|
|
303
|
+
"resource_type": snake_case(type(self).__name__),
|
|
304
|
+
"uuid": self.uuid,
|
|
305
|
+
"shortname": self.shortname,
|
|
306
|
+
"subpath": subpath,
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
attributes = {}
|
|
310
|
+
for key, value in self.__dict__.items():
|
|
311
|
+
if key == '_sa_instance_state':
|
|
312
|
+
continue
|
|
313
|
+
if (not include or key in include) and key not in record_fields:
|
|
314
|
+
attributes[key] = copy.deepcopy(value)
|
|
315
|
+
|
|
316
|
+
record_fields["attributes"] = attributes
|
|
317
|
+
|
|
318
|
+
return Record(**record_fields)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
class Space(Meta):
|
|
322
|
+
root_registration_signature: str = ""
|
|
323
|
+
primary_website: str = ""
|
|
324
|
+
indexing_enabled: bool = False
|
|
325
|
+
capture_misses: bool = False
|
|
326
|
+
check_health: bool = False
|
|
327
|
+
languages: list[Language] = [Language.en]
|
|
328
|
+
icon: str = ""
|
|
329
|
+
mirrors: list[str] = []
|
|
330
|
+
hide_folders: list[str] = []
|
|
331
|
+
hide_space: bool | None = None
|
|
332
|
+
active_plugins: list[str] = []
|
|
333
|
+
ordinal: int | None = None
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
class Actor(Meta):
|
|
337
|
+
pass
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class User(Actor):
|
|
341
|
+
password: str | None = None
|
|
342
|
+
email: str | None = None
|
|
343
|
+
msisdn: str | None = Field(default=None, pattern=regex.MSISDN)
|
|
344
|
+
locked_to_device: bool = False
|
|
345
|
+
is_email_verified: bool = False
|
|
346
|
+
is_msisdn_verified: bool = False
|
|
347
|
+
force_password_change: bool = False
|
|
348
|
+
type: UserType = UserType.web
|
|
349
|
+
roles: list[str] = []
|
|
350
|
+
groups: list[str] = []
|
|
351
|
+
firebase_token: str | None = None
|
|
352
|
+
language: Language = Language.ar
|
|
353
|
+
google_id: str | None = None
|
|
354
|
+
facebook_id: str | None = None
|
|
355
|
+
social_avatar_url: str | None = None
|
|
356
|
+
last_login: dict | None = None
|
|
357
|
+
notes: str | None = None
|
|
358
|
+
|
|
359
|
+
@staticmethod
|
|
360
|
+
def invitation_url_template() -> str:
|
|
361
|
+
return (
|
|
362
|
+
"{url}/auth/invitation?invitation={token}&lang={lang}&user-type={user_type}"
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
class Group(Meta):
|
|
367
|
+
roles: list[str] = [] # list of entitled roles
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
class Attachment(Meta):
|
|
371
|
+
author_locator: Locator | None = None
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
class DataAsset(Attachment):
|
|
375
|
+
pass
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
class Json(Attachment):
|
|
379
|
+
pass
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
class Jsonl(DataAsset):
|
|
383
|
+
pass
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
class Parquet(DataAsset):
|
|
387
|
+
pass
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
class Sqlite(DataAsset):
|
|
391
|
+
pass
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
class Duckdb(DataAsset):
|
|
395
|
+
pass
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
class Csv(DataAsset):
|
|
399
|
+
pass
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
class Share(Attachment):
|
|
403
|
+
pass
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
class Reaction(Attachment):
|
|
407
|
+
pass
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
class Reply(Attachment):
|
|
411
|
+
pass
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
class Comment(Attachment):
|
|
415
|
+
pass
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
class Lock(Attachment):
|
|
419
|
+
pass
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
class Media(Attachment):
|
|
423
|
+
pass
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
class Alteration(Attachment):
|
|
427
|
+
requested_update: dict
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
class Action(Resource):
|
|
431
|
+
resource: Locator
|
|
432
|
+
user_shortname: str
|
|
433
|
+
request: ActionType
|
|
434
|
+
timestamp: datetime
|
|
435
|
+
attributes: dict[str, Any] | None
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
class History(Meta):
|
|
439
|
+
timestamp: datetime
|
|
440
|
+
request_headers: dict[str, Any]
|
|
441
|
+
diff: dict[str, Any]
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
class Schema(Meta):
|
|
445
|
+
pass
|
|
446
|
+
|
|
447
|
+
# USE meta_schema TO VALIDATE ANY SCHEMA
|
|
448
|
+
def __init__(self, **data):
|
|
449
|
+
Meta.__init__(self, **data)
|
|
450
|
+
if self.payload is not None and self.shortname != "meta_schema":
|
|
451
|
+
self.payload.schema_shortname = "meta_schema"
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
class Content(Meta):
|
|
455
|
+
pass
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
class Log(Meta):
|
|
459
|
+
pass
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
class Folder(Meta):
|
|
463
|
+
pass
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
class Permission(Meta):
|
|
467
|
+
subpaths: dict[str, list[str]] # {"space_name": ["subpath_one", "subpath_two"]}
|
|
468
|
+
resource_types: list[ResourceType]
|
|
469
|
+
actions: list[ActionType]
|
|
470
|
+
conditions: list[ConditionType] = list()
|
|
471
|
+
restricted_fields: list[str] = []
|
|
472
|
+
allowed_fields_values: dict[str, list[str] | list[list[str]]] = {}
|
|
473
|
+
filter_fields_values: str | None = None
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
class Role(Meta):
|
|
477
|
+
permissions: list[str] # list of permissions_shortnames
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
# class Collabolator(Resource):
|
|
481
|
+
# role: str | None = "" # a free-text role-name e.g. developer, qa, admin, ...
|
|
482
|
+
# shortname: str
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
class Reporter(Resource):
|
|
486
|
+
type: str | None = None
|
|
487
|
+
name: str | None = None
|
|
488
|
+
channel: str | None = None
|
|
489
|
+
distributor: str | None = None
|
|
490
|
+
governorate: str | None = None
|
|
491
|
+
msisdn: str | None = None
|
|
492
|
+
channel_address: dict | None = None
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
class Ticket(Meta):
|
|
496
|
+
state: str
|
|
497
|
+
is_open: bool = True
|
|
498
|
+
reporter: Reporter | None = None
|
|
499
|
+
workflow_shortname: str
|
|
500
|
+
collaborators: dict[str, str] | None = None # list[Collabolator] | None = None
|
|
501
|
+
resolution_reason: str | None = None
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
class Post(Content):
|
|
505
|
+
pass
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
class Event(BaseModel):
|
|
509
|
+
space_name: str
|
|
510
|
+
subpath: str = Field(pattern=regex.SUBPATH)
|
|
511
|
+
shortname: str | None = Field(default=None, pattern=regex.SHORTNAME)
|
|
512
|
+
action_type: ActionType
|
|
513
|
+
resource_type: ResourceType | None = None
|
|
514
|
+
schema_shortname: str | None = None
|
|
515
|
+
attributes: dict = {}
|
|
516
|
+
user_shortname: str
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
class PluginBase(ABC):
|
|
520
|
+
@abstractmethod
|
|
521
|
+
async def hook(self, data: Event) -> None:
|
|
522
|
+
pass
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
class EventFilter(BaseModel):
|
|
526
|
+
subpaths: list
|
|
527
|
+
resource_types: list
|
|
528
|
+
schema_shortnames: list
|
|
529
|
+
actions: list[ActionType]
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
class PluginWrapper(Resource):
|
|
533
|
+
shortname: str = Field(pattern=regex.SHORTNAME)
|
|
534
|
+
is_active: bool = False
|
|
535
|
+
filters: EventFilter | None = None
|
|
536
|
+
listen_time: EventListenTime | None = None
|
|
537
|
+
type: PluginType | None = None
|
|
538
|
+
ordinal: int = 9999
|
|
539
|
+
object: PluginBase | None = None
|
|
540
|
+
dependencies: list = []
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
class NotificationData(Resource):
|
|
544
|
+
receiver: dict
|
|
545
|
+
title: Translation
|
|
546
|
+
body: Translation
|
|
547
|
+
image_urls: Translation | None = None
|
|
548
|
+
deep_link: dict = {}
|
|
549
|
+
entry_id: str | None = None
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
class Notification(Meta):
|
|
553
|
+
"""
|
|
554
|
+
title: use the displayname attribute inside Meta
|
|
555
|
+
description: use the description attribute inside Meta
|
|
556
|
+
"""
|
|
557
|
+
|
|
558
|
+
type: NotificationType
|
|
559
|
+
is_read: bool = False
|
|
560
|
+
priority: NotificationPriority
|
|
561
|
+
entry: Locator | None = None
|
|
562
|
+
|
|
563
|
+
@staticmethod
|
|
564
|
+
async def from_request(notification_req: dict, entry: dict = {}):
|
|
565
|
+
if (
|
|
566
|
+
notification_req["payload"]["schema_shortname"]
|
|
567
|
+
== "admin_notification_request"
|
|
568
|
+
):
|
|
569
|
+
notification_type = NotificationType.admin
|
|
570
|
+
elif notification_req["payload"]["schema_shortname"] == "system_notification_request":
|
|
571
|
+
notification_type = NotificationType.system
|
|
572
|
+
else:
|
|
573
|
+
notification_type = NotificationType.admin
|
|
574
|
+
|
|
575
|
+
entry_locator = None
|
|
576
|
+
if entry:
|
|
577
|
+
entry_locator = Locator(
|
|
578
|
+
space_name=entry["space_name"],
|
|
579
|
+
type=ResourceType(entry["resource_type"]),
|
|
580
|
+
schema_shortname=entry["payload"]["schema_shortname"],
|
|
581
|
+
subpath=entry["subpath"],
|
|
582
|
+
shortname=entry["shortname"],
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
uuid = uuid4()
|
|
586
|
+
return Notification(
|
|
587
|
+
uuid=uuid,
|
|
588
|
+
shortname=str(uuid)[:8],
|
|
589
|
+
is_active=True,
|
|
590
|
+
displayname=notification_req["displayname"],
|
|
591
|
+
description=notification_req["description"],
|
|
592
|
+
owner_shortname=notification_req["owner_shortname"],
|
|
593
|
+
type=notification_type,
|
|
594
|
+
is_read=False,
|
|
595
|
+
priority=notification_req["payload"]["body"]["priority"],
|
|
596
|
+
entry=entry_locator,
|
|
597
|
+
)
|