cardo-python-utils 0.5.dev38__py3-none-any.whl → 0.5.dev40__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cardo-python-utils
3
- Version: 0.5.dev38
3
+ Version: 0.5.dev40
4
4
  Summary: Python library enhanced with a wide range of functions for different scenarios.
5
5
  Author-email: CardoAI <hello@cardoai.com>
6
6
  License: MIT
@@ -1,4 +1,4 @@
1
- cardo_python_utils-0.5.dev38.dist-info/licenses/LICENSE,sha256=N-YtxDy8n5A1Mo7JKKItNIlboiK_pMOZ48ojx76jo3g,1046
1
+ cardo_python_utils-0.5.dev40.dist-info/licenses/LICENSE,sha256=N-YtxDy8n5A1Mo7JKKItNIlboiK_pMOZ48ojx76jo3g,1046
2
2
  python_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  python_utils/choices.py,sha256=_sLNkSnQqhg55gGKNRsOQCJ75W6gnz8J8Q00528MEYk,2548
4
4
  python_utils/data_structures.py,sha256=ZqkZYPy20zyGYOVhwb9qst4vF_P7X2A9z5E36rMUC6I,16820
@@ -28,7 +28,7 @@ python_utils/django/api/drf.py,sha256=a61gp-fSHV760FYfRv_IxnykuB14sSSI6tS1dUB1PO
28
28
  python_utils/django/api/ninja.py,sha256=r0-tlS8TlCp_q1_3wdplyZmYMjeUwv8YuzWjRTtijBw,4765
29
29
  python_utils/django/api/utils.py,sha256=DHY7qmVaN5sQsgjLevFWpkRVQiGQ7DmkAq4bIRicEOE,3423
30
30
  python_utils/django/auth/service.py,sha256=9gMURteyVnfjpZLyrvjAvgglfi2NPwrdia2LDVPfxBs,7475
31
- python_utils/django/celery/__init__.py,sha256=ryR-M8H6x0lpBpocIOvQ9HICqQZS59nbZN9dJ7vy-S8,185
31
+ python_utils/django/celery/__init__.py,sha256=eqKpBqhClH-7oK-kD1SUEpzt4Gqu7VWLWmhFUktee0A,79
32
32
  python_utils/django/celery/tenant_aware_database_scheduler.py,sha256=Qcz8mFhfFcX8Opzb75qvuF9jxwuY8Nsn0gxIBKFaU8w,8087
33
33
  python_utils/django/celery/tenant_aware_task.py,sha256=pKKuLczhI-1N5-ccFPeB1YV-n8Uu8LVOAvPVrB1zHeU,1316
34
34
  python_utils/django/db/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -41,6 +41,7 @@ python_utils/django/management/commands/migrateall.py,sha256=j1AA29FOGr5Rwe6Fjm4
41
41
  python_utils/django/management/commands/tenant_aware_command.py,sha256=MBI7N0BEHZAhK183QmFjjEdDecM060O0nsxg0spH9Mc,2440
42
42
  python_utils/django/middleware/__init__.py,sha256=GyhuzaOtaXW13iB9GL0XF7OvEEzKoJzorJzYX597idA,117
43
43
  python_utils/django/middleware/tenant_aware_http_middleware.py,sha256=W8U3-nR90b8m_wz5xDJTuImUrPEXvESzYh3VITYMIJU,3825
44
+ python_utils/django/middleware/tenant_aware_websocket_middleware.py,sha256=k6bn4pS4uZcTPafF7Yz_8VAj0vchEfJSXM4Bxb-Od-k,4100
44
45
  python_utils/django/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
46
  python_utils/django/models/user_group.py,sha256=WU_KClfj-pboDIKUDHZQfQuhAPIPXgP-LrEvg9XJjmA,504
46
47
  python_utils/django/redis/__init__.py,sha256=upioQc5-PwmdB9FB4or4C3RnCCxsrOIUrA3iTvo-tk8,86
@@ -49,7 +50,7 @@ python_utils/django/storage/__init__.py,sha256=mNn2YmD7pkXhBLHMM1444BLsCMq78YdYx
49
50
  python_utils/django/storage/tenant_aware_storage.py,sha256=5dDes6xLv7_R8hIBbFIzRvPL7HL9K_RM-G6LI8qUSxM,2550
50
51
  python_utils/django/tests/__init__.py,sha256=Nkt0a7LEHyjLvuEBZ7113VjjAWJlyZlMy-H-JZ5tNcs,252
51
52
  python_utils/django/tests/conftest.py,sha256=KozXmXUWVcDLbkVAb7Aq4sDydGLh2YZkbRa4tkA8Z6U,3167
52
- cardo_python_utils-0.5.dev38.dist-info/METADATA,sha256=wvbabrJ5i_E5PL4rdl79mzB8fPbSNx0x5u6bmrwzi14,3007
53
- cardo_python_utils-0.5.dev38.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
54
- cardo_python_utils-0.5.dev38.dist-info/top_level.txt,sha256=zAx6OfEsjJs8BEW3okSiG_j9gpkI69xWShzum6oBgKI,13
55
- cardo_python_utils-0.5.dev38.dist-info/RECORD,,
53
+ cardo_python_utils-0.5.dev40.dist-info/METADATA,sha256=vytll5InRIiggtoRpU8pVYg2GMZmxs2pLJoSP7BZ9k0,3007
54
+ cardo_python_utils-0.5.dev40.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
55
+ cardo_python_utils-0.5.dev40.dist-info/top_level.txt,sha256=zAx6OfEsjJs8BEW3okSiG_j9gpkI69xWShzum6oBgKI,13
56
+ cardo_python_utils-0.5.dev40.dist-info/RECORD,,
@@ -1,5 +1,4 @@
1
1
  from .tenant_aware_task import TenantAwareTask
2
- from .tenant_aware_database_scheduler import TenantAwareDatabaseScheduler
3
2
 
4
3
 
5
- __all__ = ["TenantAwareTask", "TenantAwareDatabaseScheduler"]
4
+ __all__ = ["TenantAwareTask"]
@@ -0,0 +1,129 @@
1
+ import logging
2
+ from urllib.parse import parse_qs
3
+
4
+ from django.conf import settings
5
+ from django.contrib.auth import get_user_model
6
+
7
+ from ..api.utils import decode_jwt, TokenPayload
8
+ from ..settings import DEVELOPMENT_TENANT
9
+ from ..tenant_context import TenantContext
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def parse_query_params_from_scope(scope):
15
+ """
16
+ Parse query params from scope
17
+
18
+ Parameters:
19
+ scope (dict): scope from consumer
20
+
21
+ Returns:
22
+ dict: query params
23
+ """
24
+ return parse_qs(scope["query_string"].decode("utf-8"))
25
+
26
+
27
+ class TenantAwareWebsocketMiddleware:
28
+ def __init__(self, app):
29
+ self.app = app
30
+
31
+ async def __call__(self, scope, receive, send):
32
+ """
33
+ Extract authentication token from request query params and authenticate user,
34
+ used by django channels.
35
+
36
+ Also sets the tenant context from the subdomain, mirroring
37
+ the TenantAwareHttpMiddleware logic for HTTP requests.
38
+
39
+ Parameters:
40
+ scope (dict): ASGI scope
41
+ receive (callable): ASGI receive callable
42
+ send (callable): ASGI send callable
43
+ """
44
+ query_params = parse_query_params_from_scope(scope)
45
+ access_token = query_params.get("authorization", [None])[0]
46
+ if not access_token:
47
+ await self._reject_connection(send, "Authorization token is missing")
48
+ return
49
+
50
+ tenant = self._get_tenant(scope)
51
+
52
+ async with TenantContext(tenant):
53
+ token_payload: TokenPayload = decode_jwt(access_token)
54
+ username = token_payload.get("preferred_username")
55
+ if not username:
56
+ await self._reject_connection(send, "Username cannot be extracted from the token")
57
+ return
58
+
59
+ exp_timestamp = token_payload.get("exp")
60
+ if not exp_timestamp:
61
+ await self._reject_connection(send, "Token does not have an expiration time")
62
+ return
63
+
64
+ user = await get_user_model().objects.filter(username=username).afirst()
65
+ if not user:
66
+ await self._reject_connection(send, "User not found.")
67
+ return
68
+
69
+ scope["user"] = user
70
+ scope["auth"] = token_payload
71
+ scope["exp_timestamp"] = exp_timestamp
72
+
73
+ return await self.app(scope, receive, send)
74
+
75
+ @staticmethod
76
+ async def _reject_connection(send, reason):
77
+ """
78
+ Reject the WebSocket connection with a specific reason.
79
+
80
+ Parameters:
81
+ send (callable): ASGI send callable
82
+ reason (str): Reason for rejecting the connection
83
+
84
+ Returns:
85
+ None
86
+ """
87
+ await send(
88
+ {"type": "websocket.close", "code": 4000, "reason": reason}
89
+ ) # Custom close code for rejection
90
+
91
+ def _get_tenant(self, scope) -> str:
92
+ """
93
+ Determine the tenant from the websocket scope.
94
+
95
+ In DEBUG mode, uses DEVELOPMENT_TENANT directly.
96
+ In production, extracts tenant from the Host header subdomain.
97
+ """
98
+ if settings.DEBUG:
99
+ return DEVELOPMENT_TENANT
100
+
101
+ host = self._get_host_from_scope(scope)
102
+
103
+ if host == "testserver":
104
+ return "default"
105
+
106
+ parts = host.split(".")
107
+
108
+ if len(parts) >= 3:
109
+ tenant = parts[1].replace("-internal", "")
110
+ logger.debug(f"Tenant '{tenant}' extracted from websocket host: {host}")
111
+ return tenant
112
+
113
+ raise Exception(
114
+ f"Could not determine tenant from websocket subdomain. Host: {host}"
115
+ )
116
+
117
+ @staticmethod
118
+ def _get_host_from_scope(scope) -> str:
119
+ """Extract the host from ASGI scope headers."""
120
+ for header_name, header_value in scope.get("headers", []):
121
+ if header_name == b"host":
122
+ return header_value.decode("utf-8").split(":")[0]
123
+
124
+ # Fallback to scope["server"] if no Host header
125
+ server = scope.get("server")
126
+ if server:
127
+ return server[0]
128
+
129
+ raise Exception(f"Could not determine host from websocket scope: {scope}")