cardo-python-utils 0.5.dev39__tar.gz → 0.5.dev40__tar.gz

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 (62) hide show
  1. {cardo_python_utils-0.5.dev39/cardo_python_utils.egg-info → cardo_python_utils-0.5.dev40}/PKG-INFO +1 -1
  2. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40/cardo_python_utils.egg-info}/PKG-INFO +1 -1
  3. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/cardo_python_utils.egg-info/SOURCES.txt +1 -0
  4. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/pyproject.toml +1 -1
  5. cardo_python_utils-0.5.dev40/python_utils/django/middleware/tenant_aware_websocket_middleware.py +129 -0
  6. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/LICENSE +0 -0
  7. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/MANIFEST.in +0 -0
  8. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/README.rst +0 -0
  9. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/cardo_python_utils.egg-info/dependency_links.txt +0 -0
  10. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/cardo_python_utils.egg-info/requires.txt +0 -0
  11. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/cardo_python_utils.egg-info/top_level.txt +0 -0
  12. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/__init__.py +0 -0
  13. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/choices.py +0 -0
  14. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/data_structures.py +0 -0
  15. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/db.py +0 -0
  16. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/django/README.md +0 -0
  17. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/django/__init__.py +0 -0
  18. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/django/admin/__init__.py +0 -0
  19. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/django/admin/auth.py +0 -0
  20. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/django/admin/templates/__init__.py +0 -0
  21. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/django/admin/templates/user_groups_changelist.html +0 -0
  22. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/django/admin/user_group.py +0 -0
  23. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/django/admin/views.py +0 -0
  24. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/django/api/__init__.py +0 -0
  25. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/django/api/drf.py +0 -0
  26. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/django/api/ninja.py +0 -0
  27. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/django/api/utils.py +0 -0
  28. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/django/apps.py +0 -0
  29. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/django/auth/service.py +0 -0
  30. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/django/celery/__init__.py +0 -0
  31. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/django/celery/tenant_aware_database_scheduler.py +0 -0
  32. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/django/celery/tenant_aware_task.py +0 -0
  33. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/django/db/__init__.py +0 -0
  34. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/django/db/routers.py +0 -0
  35. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/django/db/transaction.py +0 -0
  36. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/django/db/utils.py +0 -0
  37. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/django/management/__init__.py +0 -0
  38. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/django/management/commands/__init__.py +0 -0
  39. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/django/management/commands/migrateall.py +0 -0
  40. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/django/management/commands/tenant_aware_command.py +0 -0
  41. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/django/middleware/__init__.py +0 -0
  42. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/django/middleware/tenant_aware_http_middleware.py +0 -0
  43. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/django/models/__init__.py +0 -0
  44. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/django/models/user_group.py +0 -0
  45. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/django/oidc_settings.py +0 -0
  46. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/django/redis/__init__.py +0 -0
  47. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/django/redis/key_function.py +0 -0
  48. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/django/settings.py +0 -0
  49. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/django/storage/__init__.py +0 -0
  50. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/django/storage/tenant_aware_storage.py +0 -0
  51. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/django/tenant_context.py +0 -0
  52. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/django/tests/__init__.py +0 -0
  53. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/django/tests/conftest.py +0 -0
  54. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/django_utils.py +0 -0
  55. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/esma_choices.py +0 -0
  56. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/exceptions.py +0 -0
  57. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/imports.py +0 -0
  58. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/math.py +0 -0
  59. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/text.py +0 -0
  60. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/time.py +0 -0
  61. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/python_utils/types_hinting.py +0 -0
  62. {cardo_python_utils-0.5.dev39 → cardo_python_utils-0.5.dev40}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cardo-python-utils
3
- Version: 0.5.dev39
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cardo-python-utils
3
- Version: 0.5.dev39
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
@@ -49,6 +49,7 @@ python_utils/django/management/commands/migrateall.py
49
49
  python_utils/django/management/commands/tenant_aware_command.py
50
50
  python_utils/django/middleware/__init__.py
51
51
  python_utils/django/middleware/tenant_aware_http_middleware.py
52
+ python_utils/django/middleware/tenant_aware_websocket_middleware.py
52
53
  python_utils/django/models/__init__.py
53
54
  python_utils/django/models/user_group.py
54
55
  python_utils/django/redis/__init__.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cardo-python-utils"
7
- version = "0.5.dev39"
7
+ version = "0.5.dev40"
8
8
  description = "Python library enhanced with a wide range of functions for different scenarios."
9
9
  readme = "README.rst"
10
10
  requires-python = ">=3.8"
@@ -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}")