mdb-engine 0.1.6__py3-none-any.whl → 0.2.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.
- mdb_engine/__init__.py +104 -11
- mdb_engine/auth/ARCHITECTURE.md +112 -0
- mdb_engine/auth/README.md +648 -11
- mdb_engine/auth/__init__.py +136 -29
- mdb_engine/auth/audit.py +592 -0
- mdb_engine/auth/base.py +252 -0
- mdb_engine/auth/casbin_factory.py +264 -69
- mdb_engine/auth/config_helpers.py +7 -6
- mdb_engine/auth/cookie_utils.py +3 -7
- mdb_engine/auth/csrf.py +373 -0
- mdb_engine/auth/decorators.py +3 -10
- mdb_engine/auth/dependencies.py +47 -50
- mdb_engine/auth/helpers.py +3 -3
- mdb_engine/auth/integration.py +53 -80
- mdb_engine/auth/jwt.py +2 -6
- mdb_engine/auth/middleware.py +77 -34
- mdb_engine/auth/oso_factory.py +18 -38
- mdb_engine/auth/provider.py +270 -171
- mdb_engine/auth/rate_limiter.py +504 -0
- mdb_engine/auth/restrictions.py +8 -24
- mdb_engine/auth/session_manager.py +14 -29
- mdb_engine/auth/shared_middleware.py +600 -0
- mdb_engine/auth/shared_users.py +759 -0
- mdb_engine/auth/token_store.py +14 -28
- mdb_engine/auth/users.py +54 -113
- mdb_engine/auth/utils.py +213 -15
- mdb_engine/cli/commands/generate.py +545 -9
- mdb_engine/cli/commands/validate.py +3 -7
- mdb_engine/cli/utils.py +3 -3
- mdb_engine/config.py +7 -21
- mdb_engine/constants.py +65 -0
- mdb_engine/core/README.md +117 -6
- mdb_engine/core/__init__.py +39 -7
- mdb_engine/core/app_registration.py +22 -41
- mdb_engine/core/app_secrets.py +290 -0
- mdb_engine/core/connection.py +18 -9
- mdb_engine/core/encryption.py +223 -0
- mdb_engine/core/engine.py +1057 -93
- mdb_engine/core/index_management.py +12 -16
- mdb_engine/core/manifest.py +459 -150
- mdb_engine/core/ray_integration.py +435 -0
- mdb_engine/core/seeding.py +10 -18
- mdb_engine/core/service_initialization.py +12 -23
- mdb_engine/core/types.py +2 -5
- mdb_engine/database/README.md +140 -17
- mdb_engine/database/__init__.py +17 -6
- mdb_engine/database/abstraction.py +25 -37
- mdb_engine/database/connection.py +11 -18
- mdb_engine/database/query_validator.py +367 -0
- mdb_engine/database/resource_limiter.py +204 -0
- mdb_engine/database/scoped_wrapper.py +713 -196
- mdb_engine/dependencies.py +426 -0
- mdb_engine/di/__init__.py +34 -0
- mdb_engine/di/container.py +248 -0
- mdb_engine/di/providers.py +205 -0
- mdb_engine/di/scopes.py +139 -0
- mdb_engine/embeddings/README.md +54 -24
- mdb_engine/embeddings/__init__.py +31 -24
- mdb_engine/embeddings/dependencies.py +37 -154
- mdb_engine/embeddings/service.py +11 -25
- mdb_engine/exceptions.py +92 -0
- mdb_engine/indexes/README.md +30 -13
- mdb_engine/indexes/__init__.py +1 -0
- mdb_engine/indexes/helpers.py +1 -1
- mdb_engine/indexes/manager.py +50 -114
- mdb_engine/memory/README.md +2 -2
- mdb_engine/memory/__init__.py +1 -2
- mdb_engine/memory/service.py +30 -87
- mdb_engine/observability/README.md +4 -2
- mdb_engine/observability/__init__.py +26 -9
- mdb_engine/observability/health.py +8 -9
- mdb_engine/observability/metrics.py +32 -12
- mdb_engine/repositories/__init__.py +34 -0
- mdb_engine/repositories/base.py +325 -0
- mdb_engine/repositories/mongo.py +233 -0
- mdb_engine/repositories/unit_of_work.py +166 -0
- mdb_engine/routing/README.md +1 -1
- mdb_engine/routing/__init__.py +1 -3
- mdb_engine/routing/websockets.py +25 -60
- mdb_engine-0.2.0.dist-info/METADATA +313 -0
- mdb_engine-0.2.0.dist-info/RECORD +96 -0
- mdb_engine-0.1.6.dist-info/METADATA +0 -213
- mdb_engine-0.1.6.dist-info/RECORD +0 -75
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.2.0.dist-info}/WHEEL +0 -0
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.2.0.dist-info}/entry_points.txt +0 -0
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.2.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit of Work Pattern
|
|
3
|
+
|
|
4
|
+
Manages repository access and provides a clean interface for data operations.
|
|
5
|
+
The UnitOfWork acts as a factory for repositories and manages their lifecycle.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Any, Dict, Generic, Optional, Type, TypeVar
|
|
10
|
+
|
|
11
|
+
from .base import Entity, Repository
|
|
12
|
+
from .mongo import MongoRepository
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
T = TypeVar("T", bound=Entity)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class UnitOfWork:
|
|
20
|
+
"""
|
|
21
|
+
Unit of Work for managing repository access.
|
|
22
|
+
|
|
23
|
+
Provides a clean interface for accessing repositories through
|
|
24
|
+
attribute access (e.g., uow.users, uow.orders).
|
|
25
|
+
|
|
26
|
+
The UnitOfWork is request-scoped - one instance per HTTP request.
|
|
27
|
+
Repositories are created lazily and cached for the duration of the request.
|
|
28
|
+
|
|
29
|
+
Usage:
|
|
30
|
+
# In a route handler
|
|
31
|
+
@app.get("/users/{user_id}")
|
|
32
|
+
async def get_user(user_id: str, ctx: RequestContext = Depends()):
|
|
33
|
+
# Access repository through UnitOfWork
|
|
34
|
+
user = await ctx.uow.users.get(user_id)
|
|
35
|
+
return user
|
|
36
|
+
|
|
37
|
+
# With explicit repository method
|
|
38
|
+
@app.get("/orders")
|
|
39
|
+
async def list_orders(ctx: RequestContext = Depends()):
|
|
40
|
+
repo = ctx.uow.repository("orders", Order)
|
|
41
|
+
return await repo.find({"status": "pending"})
|
|
42
|
+
|
|
43
|
+
Repository Naming Convention:
|
|
44
|
+
- Attribute access uses collection name: uow.users -> users collection
|
|
45
|
+
- The entity class defaults to Entity if not registered
|
|
46
|
+
- Register entity classes for type-safe repositories
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
db: Any, # ScopedMongoWrapper - avoid import cycle
|
|
52
|
+
entity_registry: Optional[Dict[str, Type[Entity]]] = None,
|
|
53
|
+
):
|
|
54
|
+
"""
|
|
55
|
+
Initialize the Unit of Work.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
db: ScopedMongoWrapper for database access
|
|
59
|
+
entity_registry: Optional mapping of collection names to entity classes
|
|
60
|
+
"""
|
|
61
|
+
self._db = db
|
|
62
|
+
self._repositories: Dict[str, Repository] = {}
|
|
63
|
+
self._entity_registry: Dict[str, Type[Entity]] = entity_registry or {}
|
|
64
|
+
|
|
65
|
+
def register_entity(self, collection_name: str, entity_class: Type[Entity]) -> None:
|
|
66
|
+
"""
|
|
67
|
+
Register an entity class for a collection.
|
|
68
|
+
|
|
69
|
+
This enables type-safe repository access.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
collection_name: Name of the collection
|
|
73
|
+
entity_class: Entity subclass for this collection
|
|
74
|
+
"""
|
|
75
|
+
self._entity_registry[collection_name] = entity_class
|
|
76
|
+
|
|
77
|
+
def repository(
|
|
78
|
+
self,
|
|
79
|
+
name: str,
|
|
80
|
+
entity_class: Optional[Type[T]] = None,
|
|
81
|
+
) -> Repository[T]:
|
|
82
|
+
"""
|
|
83
|
+
Get or create a repository for a collection.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
name: Collection name
|
|
87
|
+
entity_class: Optional entity class override
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Repository instance for the collection
|
|
91
|
+
"""
|
|
92
|
+
if name in self._repositories:
|
|
93
|
+
return self._repositories[name]
|
|
94
|
+
|
|
95
|
+
# Determine entity class
|
|
96
|
+
if entity_class is None:
|
|
97
|
+
entity_class = self._entity_registry.get(name, Entity)
|
|
98
|
+
|
|
99
|
+
# Get collection from db wrapper
|
|
100
|
+
collection = getattr(self._db, name)
|
|
101
|
+
|
|
102
|
+
# Create repository
|
|
103
|
+
repo = MongoRepository(collection, entity_class)
|
|
104
|
+
self._repositories[name] = repo
|
|
105
|
+
|
|
106
|
+
logger.debug(f"Created repository for '{name}' with entity {entity_class.__name__}")
|
|
107
|
+
return repo
|
|
108
|
+
|
|
109
|
+
def __getattr__(self, name: str) -> Repository:
|
|
110
|
+
"""
|
|
111
|
+
Access repositories via attribute syntax.
|
|
112
|
+
|
|
113
|
+
Example:
|
|
114
|
+
uow.users # Returns Repository for 'users' collection
|
|
115
|
+
uow.orders # Returns Repository for 'orders' collection
|
|
116
|
+
"""
|
|
117
|
+
# Prevent recursion on private attributes
|
|
118
|
+
if name.startswith("_"):
|
|
119
|
+
raise AttributeError(f"'{type(self).__name__}' has no attribute '{name}'")
|
|
120
|
+
|
|
121
|
+
return self.repository(name)
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def db(self) -> Any:
|
|
125
|
+
"""
|
|
126
|
+
Direct access to the underlying ScopedMongoWrapper.
|
|
127
|
+
|
|
128
|
+
Use this for operations not covered by the Repository interface,
|
|
129
|
+
like complex aggregations or raw queries.
|
|
130
|
+
|
|
131
|
+
Example:
|
|
132
|
+
# Complex aggregation
|
|
133
|
+
pipeline = [{"$match": {...}}, {"$group": {...}}]
|
|
134
|
+
results = await ctx.uow.db.users.aggregate(pipeline).to_list(None)
|
|
135
|
+
"""
|
|
136
|
+
return self._db
|
|
137
|
+
|
|
138
|
+
def dispose(self) -> None:
|
|
139
|
+
"""
|
|
140
|
+
Dispose of the UnitOfWork and clear cached repositories.
|
|
141
|
+
|
|
142
|
+
Called automatically at the end of a request scope.
|
|
143
|
+
"""
|
|
144
|
+
self._repositories.clear()
|
|
145
|
+
logger.debug("UnitOfWork disposed")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class TypedUnitOfWork(UnitOfWork, Generic[T]):
|
|
149
|
+
"""
|
|
150
|
+
Generic typed UnitOfWork for better IDE support.
|
|
151
|
+
|
|
152
|
+
This is a convenience class that provides type hints for specific
|
|
153
|
+
repository types.
|
|
154
|
+
|
|
155
|
+
Usage:
|
|
156
|
+
class MyUnitOfWork(TypedUnitOfWork):
|
|
157
|
+
@property
|
|
158
|
+
def users(self) -> Repository[User]:
|
|
159
|
+
return self.repository("users", User)
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def orders(self) -> Repository[Order]:
|
|
163
|
+
return self.repository("orders", Order)
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
pass
|
mdb_engine/routing/README.md
CHANGED
|
@@ -286,7 +286,7 @@ async def safe_message_handler(websocket, message):
|
|
|
286
286
|
"type": "action_result",
|
|
287
287
|
"result": result
|
|
288
288
|
})
|
|
289
|
-
except
|
|
289
|
+
except (ValueError, TypeError, KeyError) as e:
|
|
290
290
|
logger.error(f"Error handling message: {e}")
|
|
291
291
|
# Send error to client
|
|
292
292
|
manager = await get_websocket_manager("my_app")
|
mdb_engine/routing/__init__.py
CHANGED
|
@@ -23,9 +23,7 @@ def _check_websockets_available():
|
|
|
23
23
|
try:
|
|
24
24
|
from fastapi import WebSocket
|
|
25
25
|
|
|
26
|
-
_websockets_module = __import__(
|
|
27
|
-
".websockets", fromlist=[""], package=__name__
|
|
28
|
-
)
|
|
26
|
+
_websockets_module = __import__(".websockets", fromlist=[""], package=__name__)
|
|
29
27
|
_websockets_available = True
|
|
30
28
|
except (ImportError, AttributeError):
|
|
31
29
|
_websockets_available = False
|
mdb_engine/routing/websockets.py
CHANGED
|
@@ -72,9 +72,7 @@ class WebSocketConnectionManager:
|
|
|
72
72
|
app_slug: App slug for scoping connections (ensures isolation)
|
|
73
73
|
"""
|
|
74
74
|
self.app_slug = app_slug
|
|
75
|
-
self.active_connections: List[WebSocketConnection] =
|
|
76
|
-
[]
|
|
77
|
-
) # List of connection metadata
|
|
75
|
+
self.active_connections: List[WebSocketConnection] = [] # List of connection metadata
|
|
78
76
|
self._lock = asyncio.Lock()
|
|
79
77
|
logger.debug(f"Initialized WebSocket manager for app: {app_slug}")
|
|
80
78
|
|
|
@@ -97,10 +95,7 @@ class WebSocketConnectionManager:
|
|
|
97
95
|
"""
|
|
98
96
|
# Note: websocket should already be accepted by the endpoint handler
|
|
99
97
|
# This is just for tracking - don't accept again
|
|
100
|
-
if (
|
|
101
|
-
hasattr(websocket, "client_state")
|
|
102
|
-
and websocket.client_state.name != "CONNECTED"
|
|
103
|
-
):
|
|
98
|
+
if hasattr(websocket, "client_state") and websocket.client_state.name != "CONNECTED":
|
|
104
99
|
await websocket.accept()
|
|
105
100
|
connection = WebSocketConnection(
|
|
106
101
|
websocket=websocket,
|
|
@@ -130,9 +125,7 @@ class WebSocketConnectionManager:
|
|
|
130
125
|
async def _disconnect():
|
|
131
126
|
async with self._lock:
|
|
132
127
|
self.active_connections = [
|
|
133
|
-
conn
|
|
134
|
-
for conn in self.active_connections
|
|
135
|
-
if conn.websocket is not websocket
|
|
128
|
+
conn for conn in self.active_connections if conn.websocket is not websocket
|
|
136
129
|
]
|
|
137
130
|
logger.info(
|
|
138
131
|
f"WebSocket disconnected for app '{self.app_slug}'. "
|
|
@@ -141,9 +134,7 @@ class WebSocketConnectionManager:
|
|
|
141
134
|
|
|
142
135
|
asyncio.create_task(_disconnect())
|
|
143
136
|
|
|
144
|
-
async def broadcast(
|
|
145
|
-
self, message: Dict[str, Any], filter_by_user: Optional[str] = None
|
|
146
|
-
) -> int:
|
|
137
|
+
async def broadcast(self, message: Dict[str, Any], filter_by_user: Optional[str] = None) -> int:
|
|
147
138
|
"""
|
|
148
139
|
Broadcast a message to all connected clients for this app.
|
|
149
140
|
|
|
@@ -276,9 +267,7 @@ _manager_lock = asyncio.Lock()
|
|
|
276
267
|
|
|
277
268
|
# Global registry of message handlers per app (for listening to client messages)
|
|
278
269
|
# Note: Registration happens synchronously during app startup, so no lock needed
|
|
279
|
-
_message_handlers: Dict[
|
|
280
|
-
str, Dict[str, Callable[[Any, Dict[str, Any]], Awaitable[None]]]
|
|
281
|
-
] = {}
|
|
270
|
+
_message_handlers: Dict[str, Dict[str, Callable[[Any, Dict[str, Any]], Awaitable[None]]]] = {}
|
|
282
271
|
|
|
283
272
|
|
|
284
273
|
async def get_websocket_manager(app_slug: str) -> WebSocketConnectionManager:
|
|
@@ -367,22 +356,16 @@ async def authenticate_websocket(
|
|
|
367
356
|
else:
|
|
368
357
|
logger.info(f"WebSocket query_params is empty for app '{app_slug}'")
|
|
369
358
|
else:
|
|
370
|
-
logger.warning(
|
|
371
|
-
f"WebSocket has no query_params attribute for app '{app_slug}'"
|
|
372
|
-
)
|
|
359
|
+
logger.warning(f"WebSocket has no query_params attribute for app '{app_slug}'")
|
|
373
360
|
|
|
374
361
|
# If no token in query, try to get from cookies (if available)
|
|
375
362
|
# Check both ws_token (non-httponly, for JS access) and token (httponly)
|
|
376
363
|
if not token:
|
|
377
364
|
if hasattr(websocket, "cookies"):
|
|
378
|
-
cookie_token = websocket.cookies.get(
|
|
379
|
-
"ws_token"
|
|
380
|
-
) or websocket.cookies.get("token")
|
|
365
|
+
cookie_token = websocket.cookies.get("ws_token") or websocket.cookies.get("token")
|
|
381
366
|
if cookie_token:
|
|
382
367
|
token = cookie_token
|
|
383
|
-
logger.debug(
|
|
384
|
-
f"WebSocket token found in cookies for app '{app_slug}'"
|
|
385
|
-
)
|
|
368
|
+
logger.debug(f"WebSocket token found in cookies for app '{app_slug}'")
|
|
386
369
|
else:
|
|
387
370
|
logger.debug(f"WebSocket has no cookies attribute for app '{app_slug}'")
|
|
388
371
|
|
|
@@ -404,6 +387,8 @@ async def authenticate_websocket(
|
|
|
404
387
|
return None, None # Signal auth failure
|
|
405
388
|
return None, None
|
|
406
389
|
|
|
390
|
+
import jwt
|
|
391
|
+
|
|
407
392
|
from ..auth.dependencies import SECRET_KEY
|
|
408
393
|
from ..auth.jwt import decode_jwt_token
|
|
409
394
|
|
|
@@ -412,22 +397,16 @@ async def authenticate_websocket(
|
|
|
412
397
|
user_id = payload.get("sub") or payload.get("user_id")
|
|
413
398
|
user_email = payload.get("email")
|
|
414
399
|
|
|
415
|
-
logger.info(
|
|
416
|
-
f"WebSocket authenticated successfully for app '{app_slug}': {user_email}"
|
|
417
|
-
)
|
|
400
|
+
logger.info(f"WebSocket authenticated successfully for app '{app_slug}': {user_email}")
|
|
418
401
|
return user_id, user_email
|
|
419
|
-
except
|
|
420
|
-
logger.error(
|
|
421
|
-
f"JWT decode error for app '{app_slug}': {decode_error}", exc_info=True
|
|
422
|
-
)
|
|
402
|
+
except (jwt.ExpiredSignatureError, jwt.InvalidTokenError) as decode_error:
|
|
403
|
+
logger.error(f"JWT decode error for app '{app_slug}': {decode_error}", exc_info=True)
|
|
423
404
|
raise
|
|
424
405
|
|
|
425
406
|
except WebSocketDisconnect:
|
|
426
407
|
raise
|
|
427
408
|
except (ValueError, TypeError, AttributeError, KeyError, RuntimeError) as e:
|
|
428
|
-
logger.error(
|
|
429
|
-
f"WebSocket authentication failed for app '{app_slug}': {e}", exc_info=True
|
|
430
|
-
)
|
|
409
|
+
logger.error(f"WebSocket authentication failed for app '{app_slug}': {e}", exc_info=True)
|
|
431
410
|
if require_auth:
|
|
432
411
|
# Don't close before accepting - return error info instead
|
|
433
412
|
return None, None # Signal auth failure
|
|
@@ -469,9 +448,7 @@ def register_message_handler(
|
|
|
469
448
|
if app_slug not in _message_handlers:
|
|
470
449
|
_message_handlers[app_slug] = {}
|
|
471
450
|
_message_handlers[app_slug][endpoint_name] = handler
|
|
472
|
-
logger.info(
|
|
473
|
-
f"Registered message handler for app '{app_slug}', endpoint '{endpoint_name}'"
|
|
474
|
-
)
|
|
451
|
+
logger.info(f"Registered message handler for app '{app_slug}', endpoint '{endpoint_name}'")
|
|
475
452
|
|
|
476
453
|
|
|
477
454
|
def get_message_handler(
|
|
@@ -497,7 +474,7 @@ async def _accept_websocket_connection(websocket: Any, app_slug: str) -> None:
|
|
|
497
474
|
await websocket.accept()
|
|
498
475
|
print(f"✅ [WEBSOCKET ACCEPTED] App: '{app_slug}'")
|
|
499
476
|
logger.info(f"✅ WebSocket accepted for app '{app_slug}'")
|
|
500
|
-
except
|
|
477
|
+
except (RuntimeError, ConnectionError, OSError) as accept_error:
|
|
501
478
|
print(f"❌ [WEBSOCKET ACCEPT FAILED] App: '{app_slug}', Error: {accept_error}")
|
|
502
479
|
logger.error(
|
|
503
480
|
f"❌ Failed to accept WebSocket for app '{app_slug}': {accept_error}",
|
|
@@ -511,9 +488,7 @@ async def _authenticate_websocket_connection(
|
|
|
511
488
|
) -> tuple:
|
|
512
489
|
"""Authenticate WebSocket connection and return (user_id, user_email)."""
|
|
513
490
|
try:
|
|
514
|
-
user_id, user_email = await authenticate_websocket(
|
|
515
|
-
websocket, app_slug, require_auth
|
|
516
|
-
)
|
|
491
|
+
user_id, user_email = await authenticate_websocket(websocket, app_slug, require_auth)
|
|
517
492
|
|
|
518
493
|
if require_auth and not user_id:
|
|
519
494
|
logger.warning(
|
|
@@ -522,9 +497,7 @@ async def _authenticate_websocket_connection(
|
|
|
522
497
|
try:
|
|
523
498
|
await websocket.close(code=1008, reason="Authentication required")
|
|
524
499
|
except (WebSocketDisconnect, RuntimeError, OSError) as e:
|
|
525
|
-
logger.debug(
|
|
526
|
-
f"WebSocket already closed during auth failure cleanup: {e}"
|
|
527
|
-
)
|
|
500
|
+
logger.debug(f"WebSocket already closed during auth failure cleanup: {e}")
|
|
528
501
|
raise WebSocketDisconnect(code=1008)
|
|
529
502
|
|
|
530
503
|
return user_id, user_email
|
|
@@ -547,12 +520,10 @@ async def _authenticate_websocket_connection(
|
|
|
547
520
|
exc_info=True,
|
|
548
521
|
)
|
|
549
522
|
try:
|
|
550
|
-
await websocket.close(
|
|
551
|
-
|
|
552
|
-
)
|
|
553
|
-
|
|
554
|
-
logger.debug(f"WebSocket already closed during auth error cleanup: {e}")
|
|
555
|
-
raise WebSocketDisconnect(code=1011)
|
|
523
|
+
await websocket.close(code=1011, reason="Internal server error during authentication")
|
|
524
|
+
except (WebSocketDisconnect, RuntimeError, OSError) as close_error:
|
|
525
|
+
logger.debug(f"WebSocket already closed during auth error cleanup: {close_error}")
|
|
526
|
+
raise WebSocketDisconnect(code=1011) from None
|
|
556
527
|
|
|
557
528
|
|
|
558
529
|
async def _handle_websocket_message(
|
|
@@ -656,9 +627,7 @@ def create_websocket_endpoint(
|
|
|
656
627
|
file=sys.stderr,
|
|
657
628
|
flush=True,
|
|
658
629
|
)
|
|
659
|
-
print(
|
|
660
|
-
f"🔌 [WEBSOCKET HANDLER CALLED] App: '{app_slug}', Path: {path}", flush=True
|
|
661
|
-
)
|
|
630
|
+
print(f"🔌 [WEBSOCKET HANDLER CALLED] App: '{app_slug}', Path: {path}", flush=True)
|
|
662
631
|
logger.info(f"🔌 [WEBSOCKET HANDLER CALLED] App: '{app_slug}', Path: {path}")
|
|
663
632
|
connection = None
|
|
664
633
|
try:
|
|
@@ -693,9 +662,7 @@ def create_websocket_endpoint(
|
|
|
693
662
|
)
|
|
694
663
|
|
|
695
664
|
# Connect with metadata (websocket already accepted)
|
|
696
|
-
connection = await manager.connect(
|
|
697
|
-
websocket, user_id=user_id, user_email=user_email
|
|
698
|
-
)
|
|
665
|
+
connection = await manager.connect(websocket, user_id=user_id, user_email=user_email)
|
|
699
666
|
|
|
700
667
|
# Send initial connection confirmation
|
|
701
668
|
await manager.send_to_connection(
|
|
@@ -765,9 +732,7 @@ def create_websocket_endpoint(
|
|
|
765
732
|
TypeError,
|
|
766
733
|
AttributeError,
|
|
767
734
|
) as e:
|
|
768
|
-
logger.error(
|
|
769
|
-
f"WebSocket connection error for app '{app_slug}': {e}", exc_info=True
|
|
770
|
-
)
|
|
735
|
+
logger.error(f"WebSocket connection error for app '{app_slug}': {e}", exc_info=True)
|
|
771
736
|
finally:
|
|
772
737
|
if connection:
|
|
773
738
|
manager.disconnect(websocket)
|