dmart 0.1.9__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 (149) hide show
  1. alembic/__init__.py +0 -0
  2. alembic/env.py +91 -0
  3. alembic/scripts/__init__.py +0 -0
  4. alembic/scripts/calculate_checksums.py +77 -0
  5. alembic/scripts/migration_f7a4949eed19.py +28 -0
  6. alembic/versions/0f3d2b1a7c21_add_authz_materialized_views.py +87 -0
  7. alembic/versions/10d2041b94d4_last_checksum_history.py +62 -0
  8. alembic/versions/1cf4e1ee3cb8_ext_permission_with_filter_fields_values.py +33 -0
  9. alembic/versions/26bfe19b49d4_rm_failedloginattempts.py +42 -0
  10. alembic/versions/3c8bca2219cc_add_otp_table.py +38 -0
  11. alembic/versions/6675fd9dfe42_remove_unique_from_sessions_table.py +36 -0
  12. alembic/versions/71bc1df82e6a_adding_user_last_login_at.py +43 -0
  13. alembic/versions/74288ccbd3b5_initial.py +264 -0
  14. alembic/versions/7520a89a8467_rm_activesession_table.py +39 -0
  15. alembic/versions/848b623755a4_make_created_nd_updated_at_required.py +138 -0
  16. alembic/versions/8640dcbebf85_add_notes_to_users.py +32 -0
  17. alembic/versions/91c94250232a_adding_fk_on_owner_shortname.py +104 -0
  18. alembic/versions/98ecd6f56f9a_ext_meta_with_owner_group_shortname.py +66 -0
  19. alembic/versions/9aae9138c4ef_indexing_created_at_updated_at.py +80 -0
  20. alembic/versions/__init__.py +0 -0
  21. alembic/versions/b53f916b3f6d_json_to_jsonb.py +492 -0
  22. alembic/versions/eb5f1ec65156_adding_user_locked_to_device.py +36 -0
  23. alembic/versions/f7a4949eed19_adding_query_policies_to_meta.py +60 -0
  24. api/__init__.py +0 -0
  25. api/info/__init__.py +0 -0
  26. api/info/router.py +109 -0
  27. api/managed/__init__.py +0 -0
  28. api/managed/router.py +1541 -0
  29. api/managed/utils.py +1850 -0
  30. api/public/__init__.py +0 -0
  31. api/public/router.py +758 -0
  32. api/qr/__init__.py +0 -0
  33. api/qr/router.py +108 -0
  34. api/user/__init__.py +0 -0
  35. api/user/model/__init__.py +0 -0
  36. api/user/model/errors.py +14 -0
  37. api/user/model/requests.py +165 -0
  38. api/user/model/responses.py +11 -0
  39. api/user/router.py +1401 -0
  40. api/user/service.py +270 -0
  41. bundler.py +44 -0
  42. config/__init__.py +0 -0
  43. config/channels.json +11 -0
  44. config/notification.json +17 -0
  45. data_adapters/__init__.py +0 -0
  46. data_adapters/adapter.py +16 -0
  47. data_adapters/base_data_adapter.py +467 -0
  48. data_adapters/file/__init__.py +0 -0
  49. data_adapters/file/adapter.py +2043 -0
  50. data_adapters/file/adapter_helpers.py +1013 -0
  51. data_adapters/file/archive.py +150 -0
  52. data_adapters/file/create_index.py +331 -0
  53. data_adapters/file/create_users_folders.py +52 -0
  54. data_adapters/file/custom_validations.py +68 -0
  55. data_adapters/file/drop_index.py +40 -0
  56. data_adapters/file/health_check.py +560 -0
  57. data_adapters/file/redis_services.py +1110 -0
  58. data_adapters/helpers.py +27 -0
  59. data_adapters/sql/__init__.py +0 -0
  60. data_adapters/sql/adapter.py +3210 -0
  61. data_adapters/sql/adapter_helpers.py +491 -0
  62. data_adapters/sql/create_tables.py +451 -0
  63. data_adapters/sql/create_users_folders.py +53 -0
  64. data_adapters/sql/db_to_json_migration.py +482 -0
  65. data_adapters/sql/health_check_sql.py +232 -0
  66. data_adapters/sql/json_to_db_migration.py +454 -0
  67. data_adapters/sql/update_query_policies.py +101 -0
  68. data_generator.py +81 -0
  69. dmart-0.1.9.dist-info/METADATA +64 -0
  70. dmart-0.1.9.dist-info/RECORD +149 -0
  71. dmart-0.1.9.dist-info/WHEEL +5 -0
  72. dmart-0.1.9.dist-info/entry_points.txt +2 -0
  73. dmart-0.1.9.dist-info/top_level.txt +23 -0
  74. dmart.py +513 -0
  75. get_settings.py +7 -0
  76. languages/__init__.py +0 -0
  77. languages/arabic.json +15 -0
  78. languages/english.json +16 -0
  79. languages/kurdish.json +14 -0
  80. languages/loader.py +13 -0
  81. main.py +506 -0
  82. migrate.py +24 -0
  83. models/__init__.py +0 -0
  84. models/api.py +203 -0
  85. models/core.py +597 -0
  86. models/enums.py +255 -0
  87. password_gen.py +8 -0
  88. plugins/__init__.py +0 -0
  89. plugins/action_log/__init__.py +0 -0
  90. plugins/action_log/plugin.py +121 -0
  91. plugins/admin_notification_sender/__init__.py +0 -0
  92. plugins/admin_notification_sender/plugin.py +124 -0
  93. plugins/ldap_manager/__init__.py +0 -0
  94. plugins/ldap_manager/plugin.py +100 -0
  95. plugins/local_notification/__init__.py +0 -0
  96. plugins/local_notification/plugin.py +123 -0
  97. plugins/realtime_updates_notifier/__init__.py +0 -0
  98. plugins/realtime_updates_notifier/plugin.py +58 -0
  99. plugins/redis_db_update/__init__.py +0 -0
  100. plugins/redis_db_update/plugin.py +188 -0
  101. plugins/resource_folders_creation/__init__.py +0 -0
  102. plugins/resource_folders_creation/plugin.py +81 -0
  103. plugins/system_notification_sender/__init__.py +0 -0
  104. plugins/system_notification_sender/plugin.py +188 -0
  105. plugins/update_access_controls/__init__.py +0 -0
  106. plugins/update_access_controls/plugin.py +9 -0
  107. pytests/__init__.py +0 -0
  108. pytests/api_user_models_erros_test.py +16 -0
  109. pytests/api_user_models_requests_test.py +98 -0
  110. pytests/archive_test.py +72 -0
  111. pytests/base_test.py +300 -0
  112. pytests/get_settings_test.py +14 -0
  113. pytests/json_to_db_migration_test.py +237 -0
  114. pytests/service_test.py +26 -0
  115. pytests/test_info.py +55 -0
  116. pytests/test_status.py +15 -0
  117. run_notification_campaign.py +98 -0
  118. scheduled_notification_handler.py +121 -0
  119. schema_migration.py +208 -0
  120. schema_modulate.py +192 -0
  121. set_admin_passwd.py +55 -0
  122. sync.py +202 -0
  123. utils/__init__.py +0 -0
  124. utils/access_control.py +306 -0
  125. utils/async_request.py +8 -0
  126. utils/exporter.py +309 -0
  127. utils/firebase_notifier.py +57 -0
  128. utils/generate_email.py +38 -0
  129. utils/helpers.py +352 -0
  130. utils/hypercorn_config.py +12 -0
  131. utils/internal_error_code.py +60 -0
  132. utils/jwt.py +124 -0
  133. utils/logger.py +167 -0
  134. utils/middleware.py +99 -0
  135. utils/notification.py +75 -0
  136. utils/password_hashing.py +16 -0
  137. utils/plugin_manager.py +215 -0
  138. utils/query_policies_helper.py +112 -0
  139. utils/regex.py +44 -0
  140. utils/repository.py +529 -0
  141. utils/router_helper.py +19 -0
  142. utils/settings.py +165 -0
  143. utils/sms_notifier.py +21 -0
  144. utils/social_sso.py +67 -0
  145. utils/templates/activation.html.j2 +26 -0
  146. utils/templates/reminder.html.j2 +17 -0
  147. utils/ticket_sys_utils.py +203 -0
  148. utils/web_notifier.py +29 -0
  149. websocket.py +231 -0
websocket.py ADDED
@@ -0,0 +1,231 @@
1
+ #!/usr/bin/env -S BACKEND_ENV=config.env python3
2
+ import json
3
+ from contextlib import asynccontextmanager
4
+
5
+ from typing import Any, cast
6
+ from fastapi import Body, FastAPI, WebSocket, WebSocketDisconnect, status
7
+ from utils.jwt import decode_jwt
8
+ import asyncio
9
+ from hypercorn.config import Config
10
+ from utils.logger import changeLogFile, logging_schema
11
+ from utils.settings import settings
12
+ from hypercorn.asyncio import serve
13
+ from models.enums import Status as ResponseStatus
14
+ from fastapi.responses import JSONResponse
15
+ from fastapi.logger import logger
16
+
17
+
18
+ all_MKW = "__ALL__"
19
+ class ConnectionManager:
20
+ def __init__(self) -> None:
21
+ self.active_connections: dict[str, WebSocket] = {}
22
+ # item => channel_name: list_of_subscribed_clients
23
+ self.channels: dict[str, list[str]] = {}
24
+
25
+ async def connect(self, websocket: WebSocket, user_shortname: str):
26
+ await websocket.accept()
27
+ self.active_connections[user_shortname] = websocket
28
+
29
+
30
+ def disconnect(self, user_shortname: str):
31
+ del self.active_connections[user_shortname]
32
+
33
+
34
+ async def send_message(self, message: str, user_shortname: str):
35
+ if user_shortname in self.active_connections:
36
+ await self.active_connections[user_shortname].send_text(message)
37
+ return True
38
+
39
+ return False
40
+
41
+
42
+ async def broadcast_message(self, message: str, channel_name: str):
43
+ if channel_name not in self.channels:
44
+ return False
45
+
46
+ for user_shortname in self.channels[channel_name]:
47
+ await self.send_message(message, user_shortname)
48
+
49
+ return True
50
+
51
+
52
+ def remove_all_subscriptions(self, username: str):
53
+ updated_channels: dict[str, list[str]] = {}
54
+ for channel_name, users in self.channels.items():
55
+ if username in users:
56
+ users.remove(username)
57
+ updated_channels[channel_name] = users
58
+ self.channels = updated_channels
59
+
60
+
61
+ async def channel_unsubscribe(self, websocket: WebSocket):
62
+ connections_usernames = list(self.active_connections.keys())
63
+ connections = list(self.active_connections.values())
64
+ username = connections_usernames[connections.index(websocket)]
65
+ self.remove_all_subscriptions(username)
66
+ subscribed_message = json.dumps({
67
+ "type": "notification_unsubscribe",
68
+ "message": {
69
+ "status": "success"
70
+ }
71
+ })
72
+ await self.send_message(subscribed_message, username)
73
+
74
+
75
+ def generate_channel_name(self, msg: dict):
76
+ if not {"space_name", "subpath"}.issubset(msg):
77
+ return False
78
+ space_name = msg["space_name"]
79
+ subpath = msg["subpath"]
80
+ schema_shortname = msg.get("schema_shortname", all_MKW)
81
+ action_type = msg.get("action_type", all_MKW)
82
+ ticket_state = msg.get("ticket_state", all_MKW)
83
+ return f"{space_name}:{subpath}:{schema_shortname}:{action_type}:{ticket_state}"
84
+
85
+
86
+ async def channel_subscribe(
87
+ self,
88
+ websocket: WebSocket,
89
+ msg_json: dict
90
+ ):
91
+ channel_name = self.generate_channel_name(msg_json)
92
+ if not channel_name:
93
+ return False
94
+
95
+ self.channels.setdefault(channel_name, [])
96
+
97
+ connections_usernames = list(self.active_connections.keys())
98
+ connections = list(self.active_connections.values())
99
+ username = connections_usernames[connections.index(websocket)]
100
+ self.remove_all_subscriptions(username)
101
+ self.channels[channel_name].append(username)
102
+
103
+ subscribed_message = json.dumps({
104
+ "type": "notification_subscription",
105
+ "message": {
106
+ "status": "success"
107
+ }
108
+ })
109
+ await self.send_message(subscribed_message, username)
110
+
111
+
112
+
113
+ websocket_manager = ConnectionManager()
114
+
115
+
116
+ @asynccontextmanager
117
+ async def lifespan(app: FastAPI):
118
+ logger.info("Starting up")
119
+ print('{"stage":"starting up"}')
120
+
121
+ yield
122
+
123
+ logger.info("Application shutting down")
124
+ print('{"stage":"shutting down"}')
125
+
126
+
127
+ app = FastAPI(
128
+ lifespan=lifespan,
129
+ )
130
+
131
+
132
+ @app.websocket("/ws")
133
+ async def websocket_endpoint(websocket: WebSocket, token: str):
134
+ try:
135
+ decoded_token = decode_jwt(token)
136
+ except Exception:
137
+ return status.HTTP_401_UNAUTHORIZED, [], b"Invalid token\n"
138
+
139
+ user_shortname = decoded_token["shortname"]
140
+ try:
141
+ await websocket_manager.connect(websocket, user_shortname)
142
+ except Exception as e:
143
+ return status.HTTP_500_INTERNAL_SERVER_ERROR, [], str(e.__str__()).encode()
144
+
145
+ success_connection_message = json.dumps({
146
+ "type": "connection_response",
147
+ "message": {
148
+ "status": "success"
149
+ }
150
+ })
151
+
152
+ try:
153
+ await websocket_manager.send_message(success_connection_message, user_shortname)
154
+ except Exception as e:
155
+ return status.HTTP_500_INTERNAL_SERVER_ERROR, [], str(e.__str__()).encode()
156
+
157
+ try:
158
+ while True:
159
+ try:
160
+ msg = await websocket.receive_text()
161
+ msg_json = json.loads(msg)
162
+ if "type" in msg_json and msg_json["type"] == "notification_subscription":
163
+ await websocket_manager.channel_subscribe(websocket, msg_json)
164
+ if "type" in msg_json and msg_json["type"] == "notification_unsubscribe":
165
+ await websocket_manager.channel_unsubscribe(websocket)
166
+ except Exception as e:
167
+ logger.error(f"Error while processing message: {e.__str__()}", extra={"user_shortname": user_shortname})
168
+ break
169
+ except WebSocketDisconnect:
170
+ logger.info("WebSocket connection closed", extra={"user_shortname": user_shortname})
171
+ websocket_manager.disconnect(user_shortname)
172
+
173
+
174
+ @app.api_route(path="/send-message/{user_shortname}", methods=["post"])
175
+ async def send_message(user_shortname: str, data: dict = Body(...)):
176
+ formatted_message = json.dumps({
177
+ "type": data["type"],
178
+ "message": data["message"]
179
+ })
180
+ is_sent = await websocket_manager.send_message(formatted_message, user_shortname)
181
+ return JSONResponse(
182
+ status_code=status.HTTP_200_OK,
183
+ content={"status": ResponseStatus.success, "message_sent": is_sent}
184
+ )
185
+
186
+
187
+ @app.api_route(path="/broadcast-to-channels", methods=["post"])
188
+ async def broadcast(data: dict = Body(...)):
189
+ formatted_message = json.dumps({
190
+ "type": data["type"],
191
+ "message": data["message"]
192
+ })
193
+
194
+ is_sent = False
195
+ for channel_name in data["channels"]:
196
+ is_sent = await websocket_manager.broadcast_message(formatted_message, channel_name) or is_sent
197
+
198
+ return JSONResponse(
199
+ status_code=status.HTTP_200_OK,
200
+ content={"status": ResponseStatus.success, "message_sent": is_sent}
201
+ )
202
+
203
+
204
+ @app.api_route(path="/info", methods=["get"])
205
+ async def service_info():
206
+ return JSONResponse(
207
+ status_code=status.HTTP_200_OK,
208
+ content={
209
+ "status": ResponseStatus.success,
210
+ "data": {
211
+ "connected_clients": str(websocket_manager.active_connections),
212
+ "channels": str(websocket_manager.channels)
213
+ }
214
+ }
215
+ )
216
+
217
+
218
+ async def main():
219
+ config = Config()
220
+ config.bind = [f"{settings.listening_host}:{settings.websocket_port}"]
221
+ config.backlog = 200
222
+
223
+ changeLogFile(settings.ws_log_file)
224
+ config.logconfig_dict = logging_schema
225
+ config.errorlog = logger
226
+ config.accesslog = logger
227
+ await serve(cast(Any, app), config)
228
+
229
+ if __name__ == "__main__":
230
+
231
+ asyncio.run(main())