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
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())
|