cardo-python-utils 0.5.dev31__tar.gz → 0.5.dev33__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 (69) hide show
  1. cardo_python_utils-0.5.dev33/MANIFEST.in +5 -0
  2. {cardo_python_utils-0.5.dev31/cardo_python_utils.egg-info → cardo_python_utils-0.5.dev33}/PKG-INFO +3 -1
  3. {cardo_python_utils-0.5.dev31 → cardo_python_utils-0.5.dev33/cardo_python_utils.egg-info}/PKG-INFO +3 -1
  4. cardo_python_utils-0.5.dev33/cardo_python_utils.egg-info/SOURCES.txt +58 -0
  5. {cardo_python_utils-0.5.dev31 → cardo_python_utils-0.5.dev33}/cardo_python_utils.egg-info/requires.txt +2 -0
  6. {cardo_python_utils-0.5.dev31 → cardo_python_utils-0.5.dev33}/pyproject.toml +3 -1
  7. cardo_python_utils-0.5.dev33/python_utils/django/README.md +125 -0
  8. cardo_python_utils-0.5.dev33/python_utils/django/__init__.py +1 -0
  9. {cardo_python_utils-0.5.dev31/python_utils/keycloak → cardo_python_utils-0.5.dev33/python_utils}/django/admin/auth.py +33 -2
  10. cardo_python_utils-0.5.dev33/python_utils/django/admin/templates/__init__.py +3 -0
  11. {cardo_python_utils-0.5.dev31/python_utils/keycloak → cardo_python_utils-0.5.dev33/python_utils}/django/admin/user_group.py +1 -1
  12. cardo_python_utils-0.5.dev33/python_utils/django/admin/views.py +63 -0
  13. {cardo_python_utils-0.5.dev31/python_utils/keycloak → cardo_python_utils-0.5.dev33/python_utils}/django/api/utils.py +17 -1
  14. cardo_python_utils-0.5.dev33/python_utils/django/apps.py +5 -0
  15. {cardo_python_utils-0.5.dev31/python_utils/keycloak/django → cardo_python_utils-0.5.dev33/python_utils/django/auth}/service.py +8 -6
  16. cardo_python_utils-0.5.dev33/python_utils/django/middleware/__init__.py +6 -0
  17. cardo_python_utils-0.5.dev31/python_utils/multi_tenancy/middleware/tenant_http_middleware.py → cardo_python_utils-0.5.dev33/python_utils/django/middleware/tenant_aware_http_middleware.py +7 -24
  18. cardo_python_utils-0.5.dev33/python_utils/django/oidc_settings.py +85 -0
  19. cardo_python_utils-0.5.dev33/python_utils/django/redis/__init__.py +4 -0
  20. {cardo_python_utils-0.5.dev31/python_utils/multi_tenancy → cardo_python_utils-0.5.dev33/python_utils/django}/redis/key_function.py +1 -1
  21. {cardo_python_utils-0.5.dev31/python_utils/multi_tenancy → cardo_python_utils-0.5.dev33/python_utils/django}/settings.py +8 -2
  22. {cardo_python_utils-0.5.dev31/python_utils/multi_tenancy → cardo_python_utils-0.5.dev33/python_utils/django}/tenant_context.py +2 -2
  23. cardo_python_utils-0.5.dev31/MANIFEST.in +0 -5
  24. cardo_python_utils-0.5.dev31/cardo_python_utils.egg-info/SOURCES.txt +0 -57
  25. cardo_python_utils-0.5.dev31/python_utils/keycloak/utils.py +0 -51
  26. cardo_python_utils-0.5.dev31/python_utils/multi_tenancy/README.md +0 -62
  27. cardo_python_utils-0.5.dev31/python_utils/multi_tenancy/__init__.py +0 -1
  28. cardo_python_utils-0.5.dev31/python_utils/multi_tenancy/apps.py +0 -7
  29. cardo_python_utils-0.5.dev31/python_utils/multi_tenancy/management/commands/__init__.py +0 -0
  30. cardo_python_utils-0.5.dev31/python_utils/multi_tenancy/middleware/__init__.py +0 -6
  31. cardo_python_utils-0.5.dev31/python_utils/multi_tenancy/redis/__init__.py +0 -4
  32. {cardo_python_utils-0.5.dev31 → cardo_python_utils-0.5.dev33}/LICENSE +0 -0
  33. {cardo_python_utils-0.5.dev31 → cardo_python_utils-0.5.dev33}/README.rst +0 -0
  34. {cardo_python_utils-0.5.dev31 → cardo_python_utils-0.5.dev33}/cardo_python_utils.egg-info/dependency_links.txt +0 -0
  35. {cardo_python_utils-0.5.dev31 → cardo_python_utils-0.5.dev33}/cardo_python_utils.egg-info/top_level.txt +0 -0
  36. {cardo_python_utils-0.5.dev31 → cardo_python_utils-0.5.dev33}/python_utils/__init__.py +0 -0
  37. {cardo_python_utils-0.5.dev31 → cardo_python_utils-0.5.dev33}/python_utils/choices.py +0 -0
  38. {cardo_python_utils-0.5.dev31 → cardo_python_utils-0.5.dev33}/python_utils/data_structures.py +0 -0
  39. {cardo_python_utils-0.5.dev31 → cardo_python_utils-0.5.dev33}/python_utils/db.py +0 -0
  40. {cardo_python_utils-0.5.dev31/python_utils/keycloak/django → cardo_python_utils-0.5.dev33/python_utils/django/admin}/__init__.py +0 -0
  41. {cardo_python_utils-0.5.dev31/python_utils/keycloak/django/admin → cardo_python_utils-0.5.dev33/python_utils/django/admin/templates}/user_groups_changelist.html +0 -0
  42. {cardo_python_utils-0.5.dev31/python_utils/keycloak/django/admin → cardo_python_utils-0.5.dev33/python_utils/django/api}/__init__.py +0 -0
  43. {cardo_python_utils-0.5.dev31/python_utils/keycloak → cardo_python_utils-0.5.dev33/python_utils}/django/api/drf.py +0 -0
  44. {cardo_python_utils-0.5.dev31/python_utils/keycloak → cardo_python_utils-0.5.dev33/python_utils}/django/api/ninja.py +0 -0
  45. {cardo_python_utils-0.5.dev31/python_utils/multi_tenancy → cardo_python_utils-0.5.dev33/python_utils/django}/celery/__init__.py +0 -0
  46. {cardo_python_utils-0.5.dev31/python_utils/multi_tenancy → cardo_python_utils-0.5.dev33/python_utils/django}/celery/tenant_aware_task.py +0 -0
  47. {cardo_python_utils-0.5.dev31/python_utils/keycloak/django/api → cardo_python_utils-0.5.dev33/python_utils/django/db}/__init__.py +0 -0
  48. {cardo_python_utils-0.5.dev31/python_utils/multi_tenancy → cardo_python_utils-0.5.dev33/python_utils/django}/db/routers.py +0 -0
  49. {cardo_python_utils-0.5.dev31/python_utils/multi_tenancy → cardo_python_utils-0.5.dev33/python_utils/django}/db/transaction.py +0 -0
  50. {cardo_python_utils-0.5.dev31/python_utils/multi_tenancy → cardo_python_utils-0.5.dev33/python_utils/django}/db/utils.py +0 -0
  51. {cardo_python_utils-0.5.dev31/python_utils/keycloak/django/models → cardo_python_utils-0.5.dev33/python_utils/django/management}/__init__.py +0 -0
  52. {cardo_python_utils-0.5.dev31/python_utils/multi_tenancy/db → cardo_python_utils-0.5.dev33/python_utils/django/management/commands}/__init__.py +0 -0
  53. {cardo_python_utils-0.5.dev31/python_utils/multi_tenancy → cardo_python_utils-0.5.dev33/python_utils/django}/management/commands/migrateall.py +0 -0
  54. {cardo_python_utils-0.5.dev31/python_utils/multi_tenancy → cardo_python_utils-0.5.dev33/python_utils/django}/management/commands/tenant_aware_command.py +0 -0
  55. {cardo_python_utils-0.5.dev31/python_utils/multi_tenancy/management → cardo_python_utils-0.5.dev33/python_utils/django/models}/__init__.py +0 -0
  56. {cardo_python_utils-0.5.dev31/python_utils/keycloak → cardo_python_utils-0.5.dev33/python_utils}/django/models/user_group.py +0 -0
  57. {cardo_python_utils-0.5.dev31/python_utils/multi_tenancy → cardo_python_utils-0.5.dev33/python_utils/django}/storage/__init__.py +0 -0
  58. {cardo_python_utils-0.5.dev31/python_utils/multi_tenancy → cardo_python_utils-0.5.dev33/python_utils/django}/storage/tenant_aware_storage.py +0 -0
  59. {cardo_python_utils-0.5.dev31/python_utils/keycloak → cardo_python_utils-0.5.dev33/python_utils}/django/tests/__init__.py +0 -0
  60. {cardo_python_utils-0.5.dev31/python_utils/keycloak → cardo_python_utils-0.5.dev33/python_utils}/django/tests/conftest.py +0 -0
  61. {cardo_python_utils-0.5.dev31 → cardo_python_utils-0.5.dev33}/python_utils/django_utils.py +0 -0
  62. {cardo_python_utils-0.5.dev31 → cardo_python_utils-0.5.dev33}/python_utils/esma_choices.py +0 -0
  63. {cardo_python_utils-0.5.dev31 → cardo_python_utils-0.5.dev33}/python_utils/exceptions.py +0 -0
  64. {cardo_python_utils-0.5.dev31 → cardo_python_utils-0.5.dev33}/python_utils/imports.py +0 -0
  65. {cardo_python_utils-0.5.dev31 → cardo_python_utils-0.5.dev33}/python_utils/math.py +0 -0
  66. {cardo_python_utils-0.5.dev31 → cardo_python_utils-0.5.dev33}/python_utils/text.py +0 -0
  67. {cardo_python_utils-0.5.dev31 → cardo_python_utils-0.5.dev33}/python_utils/time.py +0 -0
  68. {cardo_python_utils-0.5.dev31 → cardo_python_utils-0.5.dev33}/python_utils/types_hinting.py +0 -0
  69. {cardo_python_utils-0.5.dev31 → cardo_python_utils-0.5.dev33}/setup.cfg +0 -0
@@ -0,0 +1,5 @@
1
+ include LICENSE
2
+ include README.rst
3
+ include python_utils/django/admin/templates/user_groups_changelist.html
4
+ include python_utils/django/README.md
5
+ recursive-exclude tests *
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cardo-python-utils
3
- Version: 0.5.dev31
3
+ Version: 0.5.dev33
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
@@ -27,12 +27,14 @@ License-File: LICENSE
27
27
  Provides-Extra: django-keycloak
28
28
  Requires-Dist: PyJWT>=2.10.1; extra == "django-keycloak"
29
29
  Requires-Dist: mozilla-django-oidc>=4.0.1; extra == "django-keycloak"
30
+ Requires-Dist: requests; extra == "django-keycloak"
30
31
  Provides-Extra: django-keycloak-groups
31
32
  Requires-Dist: python-keycloak>=5.8.1; extra == "django-keycloak-groups"
32
33
  Provides-Extra: all
33
34
  Requires-Dist: PyJWT>=2.10.1; extra == "all"
34
35
  Requires-Dist: mozilla-django-oidc>=4.0.1; extra == "all"
35
36
  Requires-Dist: python-keycloak>=5.8.1; extra == "all"
37
+ Requires-Dist: requests; extra == "all"
36
38
  Provides-Extra: dev
37
39
  Requires-Dist: pytest>=7.0; extra == "dev"
38
40
  Requires-Dist: pytest-django>=4.5; extra == "dev"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cardo-python-utils
3
- Version: 0.5.dev31
3
+ Version: 0.5.dev33
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
@@ -27,12 +27,14 @@ License-File: LICENSE
27
27
  Provides-Extra: django-keycloak
28
28
  Requires-Dist: PyJWT>=2.10.1; extra == "django-keycloak"
29
29
  Requires-Dist: mozilla-django-oidc>=4.0.1; extra == "django-keycloak"
30
+ Requires-Dist: requests; extra == "django-keycloak"
30
31
  Provides-Extra: django-keycloak-groups
31
32
  Requires-Dist: python-keycloak>=5.8.1; extra == "django-keycloak-groups"
32
33
  Provides-Extra: all
33
34
  Requires-Dist: PyJWT>=2.10.1; extra == "all"
34
35
  Requires-Dist: mozilla-django-oidc>=4.0.1; extra == "all"
35
36
  Requires-Dist: python-keycloak>=5.8.1; extra == "all"
37
+ Requires-Dist: requests; extra == "all"
36
38
  Provides-Extra: dev
37
39
  Requires-Dist: pytest>=7.0; extra == "dev"
38
40
  Requires-Dist: pytest-django>=4.5; extra == "dev"
@@ -0,0 +1,58 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.rst
4
+ pyproject.toml
5
+ cardo_python_utils.egg-info/PKG-INFO
6
+ cardo_python_utils.egg-info/SOURCES.txt
7
+ cardo_python_utils.egg-info/dependency_links.txt
8
+ cardo_python_utils.egg-info/requires.txt
9
+ cardo_python_utils.egg-info/top_level.txt
10
+ python_utils/__init__.py
11
+ python_utils/choices.py
12
+ python_utils/data_structures.py
13
+ python_utils/db.py
14
+ python_utils/django_utils.py
15
+ python_utils/esma_choices.py
16
+ python_utils/exceptions.py
17
+ python_utils/imports.py
18
+ python_utils/math.py
19
+ python_utils/text.py
20
+ python_utils/time.py
21
+ python_utils/types_hinting.py
22
+ python_utils/django/README.md
23
+ python_utils/django/__init__.py
24
+ python_utils/django/apps.py
25
+ python_utils/django/oidc_settings.py
26
+ python_utils/django/settings.py
27
+ python_utils/django/tenant_context.py
28
+ python_utils/django/admin/__init__.py
29
+ python_utils/django/admin/auth.py
30
+ python_utils/django/admin/user_group.py
31
+ python_utils/django/admin/views.py
32
+ python_utils/django/admin/templates/__init__.py
33
+ python_utils/django/admin/templates/user_groups_changelist.html
34
+ python_utils/django/api/__init__.py
35
+ python_utils/django/api/drf.py
36
+ python_utils/django/api/ninja.py
37
+ python_utils/django/api/utils.py
38
+ python_utils/django/auth/service.py
39
+ python_utils/django/celery/__init__.py
40
+ python_utils/django/celery/tenant_aware_task.py
41
+ python_utils/django/db/__init__.py
42
+ python_utils/django/db/routers.py
43
+ python_utils/django/db/transaction.py
44
+ python_utils/django/db/utils.py
45
+ python_utils/django/management/__init__.py
46
+ python_utils/django/management/commands/__init__.py
47
+ python_utils/django/management/commands/migrateall.py
48
+ python_utils/django/management/commands/tenant_aware_command.py
49
+ python_utils/django/middleware/__init__.py
50
+ python_utils/django/middleware/tenant_aware_http_middleware.py
51
+ python_utils/django/models/__init__.py
52
+ python_utils/django/models/user_group.py
53
+ python_utils/django/redis/__init__.py
54
+ python_utils/django/redis/key_function.py
55
+ python_utils/django/storage/__init__.py
56
+ python_utils/django/storage/tenant_aware_storage.py
57
+ python_utils/django/tests/__init__.py
58
+ python_utils/django/tests/conftest.py
@@ -3,6 +3,7 @@
3
3
  PyJWT>=2.10.1
4
4
  mozilla-django-oidc>=4.0.1
5
5
  python-keycloak>=5.8.1
6
+ requests
6
7
 
7
8
  [dev]
8
9
  pytest>=7.0
@@ -13,6 +14,7 @@ tox>=3.25
13
14
  [django-keycloak]
14
15
  PyJWT>=2.10.1
15
16
  mozilla-django-oidc>=4.0.1
17
+ requests
16
18
 
17
19
  [django-keycloak-groups]
18
20
  python-keycloak>=5.8.1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cardo-python-utils"
7
- version = "0.5.dev31"
7
+ version = "0.5.dev33"
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"
@@ -34,6 +34,7 @@ dependencies = []
34
34
  django-keycloak = [
35
35
  "PyJWT>=2.10.1",
36
36
  "mozilla-django-oidc>=4.0.1",
37
+ "requests",
37
38
  ]
38
39
  django-keycloak-groups = [
39
40
  "python-keycloak>=5.8.1",
@@ -42,6 +43,7 @@ all = [
42
43
  "PyJWT>=2.10.1",
43
44
  "mozilla-django-oidc>=4.0.1",
44
45
  "python-keycloak>=5.8.1",
46
+ "requests",
45
47
  ]
46
48
  dev = [
47
49
  "pytest>=7.0",
@@ -0,0 +1,125 @@
1
+ This package provides utilities for facilitating IDP communication and multi-tenancy support.
2
+
3
+ # Usage
4
+
5
+ ## Environment variables to set
6
+
7
+ - AWS_STORAGE_TENANT_BUCKET_NAMES
8
+ - _This variable should be set if separate tenant buckets are needed._
9
+ - A JSON dictionary where each key is the tenant name and the value is the bucket name.
10
+ - DATABASE_CONFIG
11
+ - A JSON dictionary where each key is the tenant name and the value is a dict with the datase config.
12
+ - If multiple 'DATABASE_CONFIG'-prefixed variables are set, they will be merged into a single dictionary.
13
+ - KEYCLOAK_SERVER_URL
14
+ - The URL of the Keycloak deployment
15
+ - KEYCLOAK_CONFIDENTIAL_CLIENT_ID
16
+ - The id of the confidential client of the backend service
17
+ - KEYCLOAK_CONFIDENTIAL_CLIENT_SERVICE_ACCOUNT_TOKEN_FILE_PATHS
18
+ - A JSON dictionary where each key is the tenant name and the value is the file path of the service account token for the confidential client of that tenant
19
+
20
+ ## settings.py file
21
+
22
+ ### For multi-tenancy
23
+
24
+ ```python3
25
+ INSTALLED_APPS = [
26
+ ...
27
+ "python_utils.django",
28
+ ...
29
+ ]
30
+
31
+ MIDDLEWARE = [
32
+ "python_utils.django.middleware.TenantAwareHttpMiddleware",
33
+ ...
34
+ ]
35
+
36
+ # Include the database configuration for each tenant in the DATABASES setting.
37
+ # You can use the get_database_configs() function from python_utils.django.db.utils as a helper.
38
+ DATABASES = {
39
+ 'tenant1': { ... },
40
+ 'tenant2': { ... },
41
+ ...
42
+ }
43
+
44
+ # If you want to override the database alias to use for local development (when DEBUG is True).
45
+ # By default, the first database defined in DATABASES is used.
46
+ DEVELOPMENT_TENANT = "development"
47
+
48
+ # This is required to use the tenant context when routing database queries
49
+ DATABASE_ROUTERS = [
50
+ "python_utils.django.db.routers.TenantAwareRouter"
51
+ ]
52
+
53
+ # If using celery, set the task class to TenantAwareTask:
54
+ CELERY_TASK_CLS = "python_utils.django.celery.TenantAwareTask"
55
+
56
+ # If using Redis caching, configure the cache backend as follows:
57
+ CACHES = {
58
+ "default": {
59
+ "BACKEND": "django_redis.cache.RedisCache",
60
+ "LOCATION": REDIS_LOCATION,
61
+ "KEY_FUNCTION": "python_utils.django.redis.make_tenant_aware_key",
62
+ **OPTIONS,
63
+ }
64
+ }
65
+
66
+ # If using Django Storages with S3, and separate tenant buckets are needed,
67
+ # configure the storage backends as follows:
68
+ STORAGES = {
69
+ "default": {
70
+ "BACKEND": "python_utils.django.storage.TenantAwarePrivateS3Storage",
71
+ },
72
+ }
73
+
74
+ # If you want to exclude certain paths from tenant processing, use TENANT_AWARE_EXCLUDED_PATHS:
75
+ # They are considered as prefixes, so all paths starting with the given strings will be excluded.
76
+ TENANT_AWARE_EXCLUDED_PATHS = ("/some/path",)
77
+ ```
78
+
79
+ ### For OIDC auth
80
+
81
+ ```python3
82
+ from python_utils.django.admin.templates import TEMPLATE_PATH
83
+
84
+ INSTALLED_APPS.append("mozilla_django_oidc")
85
+ TEMPLATES[0]["DIRS"].append(TEMPLATE_PATH)
86
+
87
+ JWT_AUDIENCE = "myapp"
88
+ JWT_SCOPE_PREFIX = "myapp"
89
+
90
+ # If using DRF
91
+ REST_FRAMEWORK.update(
92
+ {
93
+ "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
94
+ "DEFAULT_AUTHENTICATION_CLASSES": ("python_utils.django.api.drf.AuthenticationBackend",),
95
+ "DEFAULT_PERMISSION_CLASSES": (
96
+ "rest_framework.permissions.IsAuthenticated",
97
+ "python_utils.django.api.drf.HasScope",
98
+ ),
99
+ }
100
+ )
101
+
102
+ # Admin auth backends
103
+ AUTHENTICATION_BACKENDS = [
104
+ "django.contrib.auth.backends.ModelBackend",
105
+ "python_utils.django.admin.auth.AdminAuthenticationBackend",
106
+ ]
107
+
108
+ # If user groups are used for Row Level Security (RLS)
109
+ KEYCLOAK_USER_GROUP_MODEL = "myapp.UserGroup"
110
+
111
+ KEYCLOAK_CONFIDENTIAL_CLIENT_ID = os.getenv("KEYCLOAK_CONFIDENTIAL_CLIENT_ID", f"{JWT_AUDIENCE}_confidential")
112
+
113
+ OIDC_RP_CLIENT_ID = KEYCLOAK_CONFIDENTIAL_CLIENT_ID
114
+ OIDC_RP_CLIENT_SECRET = None
115
+ OIDC_RP_SIGN_ALGO = "RS256"
116
+ OIDC_CREATE_USER = True
117
+
118
+ LOGIN_REDIRECT_URL = "/admin"
119
+ SESSION_COOKIE_AGE = 60 * 30 # 30 minutes
120
+ SESSION_SAVE_EVERY_REQUEST = True # Extend session on each request
121
+ ```
122
+
123
+ ## With django-ninja
124
+
125
+ If using `django-ninja`, apart from the settings configured above, auth utils are provided in the django/api/ninja.py module.
@@ -0,0 +1 @@
1
+ default_app_config = "python_utils.django.apps.DjangoAppConfig"
@@ -1,17 +1,48 @@
1
1
  from django.conf import settings
2
2
  from mozilla_django_oidc.auth import OIDCAuthenticationBackend
3
3
 
4
- from ...utils import get_keycloak_confidential_client_token
4
+ from ..oidc_settings import (
5
+ get_oidc_confidential_client_token,
6
+ get_oidc_op_token_endpoint,
7
+ get_oidc_op_user_endpoint,
8
+ get_oidc_op_jwks_endpoint,
9
+ )
5
10
 
6
11
 
7
12
  class AdminAuthenticationBackend(OIDCAuthenticationBackend):
13
+ """
14
+ Tenant-aware OIDC authentication backend for Django admin.
15
+
16
+ This backend dynamically resolves OIDC endpoints based on the current
17
+ tenant context, allowing each tenant to authenticate against their
18
+ own Keycloak realm.
19
+ """
20
+
21
+ @property
22
+ def OIDC_OP_TOKEN_ENDPOINT(self):
23
+ """Dynamically get the token endpoint for the current tenant."""
24
+
25
+ return get_oidc_op_token_endpoint()
26
+
27
+ @property
28
+ def OIDC_OP_USER_ENDPOINT(self):
29
+ """Dynamically get the userinfo endpoint for the current tenant."""
30
+
31
+ return get_oidc_op_user_endpoint()
32
+
33
+ @property
34
+ def OIDC_OP_JWKS_ENDPOINT(self):
35
+ """Dynamically get the JWKS endpoint for the current tenant."""
36
+
37
+ return get_oidc_op_jwks_endpoint()
38
+
8
39
  def get_token(self, payload):
9
40
  # Instead of passing client_id and client_secret,
10
41
  # client_assertion with service account token will be used
11
42
  payload.pop("client_id", None)
12
43
  payload.pop("client_secret", None)
13
44
 
14
- return get_keycloak_confidential_client_token(**payload)
45
+ return get_oidc_confidential_client_token(**payload)
15
46
 
16
47
  def _get_user_data(self, claims) -> dict:
17
48
  client_roles = (
@@ -0,0 +1,3 @@
1
+ import os
2
+
3
+ TEMPLATE_PATH = os.path.dirname(__file__)
@@ -5,7 +5,7 @@ from django.db.models import Count
5
5
  from django.urls import path
6
6
  from django.shortcuts import redirect
7
7
 
8
- from ..service import KeycloakService
8
+ from ..auth.service import KeycloakService
9
9
 
10
10
 
11
11
  class UserGroupAdminMetaclass(forms.MediaDefiningClass):
@@ -0,0 +1,63 @@
1
+ """
2
+ Tenant-aware OIDC views for mozilla-django-oidc.
3
+
4
+ These views override the default mozilla-django-oidc views to dynamically
5
+ resolve OIDC endpoints based on the current tenant context.
6
+ """
7
+
8
+ from mozilla_django_oidc.views import (
9
+ OIDCAuthenticationCallbackView,
10
+ OIDCAuthenticationRequestView,
11
+ OIDCLogoutView,
12
+ )
13
+
14
+ from ..oidc_settings import (
15
+ get_oidc_op_authorization_endpoint,
16
+ get_oidc_op_logout_endpoint,
17
+ )
18
+
19
+
20
+ class TenantAwareOIDCAuthenticationRequestView(OIDCAuthenticationRequestView):
21
+ """
22
+ Tenant-aware OIDC authentication request view.
23
+
24
+ Dynamically resolves the authorization endpoint based on the current tenant.
25
+ """
26
+
27
+ @property
28
+ def OIDC_OP_AUTH_ENDPOINT(self):
29
+ """Dynamically get the authorization endpoint for the current tenant."""
30
+ return get_oidc_op_authorization_endpoint()
31
+
32
+ def get_settings(self, attr, *args):
33
+ if attr == "OIDC_OP_AUTHORIZATION_ENDPOINT":
34
+ return self.OIDC_OP_AUTH_ENDPOINT
35
+ return super().get_settings(attr, *args)
36
+
37
+
38
+ class TenantAwareOIDCAuthenticationCallbackView(OIDCAuthenticationCallbackView):
39
+ """
40
+ Tenant-aware OIDC authentication callback view.
41
+
42
+ Uses the tenant-aware authentication backend.
43
+ """
44
+
45
+ pass
46
+
47
+
48
+ class TenantAwareOIDCLogoutView(OIDCLogoutView):
49
+ """
50
+ Tenant-aware OIDC logout view.
51
+
52
+ Dynamically resolves the logout endpoint based on the current tenant.
53
+ """
54
+
55
+ @property
56
+ def OIDC_OP_LOGOUT_ENDPOINT(self):
57
+ """Dynamically get the logout endpoint for the current tenant."""
58
+ return get_oidc_op_logout_endpoint()
59
+
60
+ def get_settings(self, attr, *args):
61
+ if attr == "OIDC_OP_LOGOUT_ENDPOINT":
62
+ return self.OIDC_OP_LOGOUT_ENDPOINT
63
+ return super().get_settings(attr, *args)
@@ -4,10 +4,25 @@ from django.conf import settings
4
4
  from django.contrib.auth import get_user_model
5
5
  from jwt import decode, PyJWKClient
6
6
 
7
- jwks_client = PyJWKClient(getattr(settings, "JWKS_URL", ""))
7
+ from ..oidc_settings import get_oidc_op_jwks_endpoint
8
+ from ..tenant_context import TenantContext
9
+
10
+ JWKS_CLIENTS = {} # Cache for PyJWKClient instances
11
+
12
+
13
+ def get_jwks_client():
14
+ tenant = TenantContext.get()
15
+
16
+ if tenant not in JWKS_CLIENTS:
17
+ jwks_client = PyJWKClient(get_oidc_op_jwks_endpoint())
18
+ JWKS_CLIENTS[tenant] = jwks_client
19
+
20
+ return JWKS_CLIENTS[tenant]
21
+
8
22
 
9
23
  User = get_user_model()
10
24
 
25
+
11
26
  class TokenPayload(TypedDict, total=False):
12
27
  exp: int
13
28
  iat: int
@@ -35,6 +50,7 @@ def decode_jwt(token: str, audience: Optional[str] = None) -> TokenPayload:
35
50
  jwt.exceptions.PyJWKClientError: If there is an error fetching the signing key.
36
51
  jwt.exceptions.InvalidTokenError: If the token is invalid or cannot be decoded.
37
52
  """
53
+ jwks_client = get_jwks_client()
38
54
  signing_key = jwks_client.get_signing_key_from_jwt(token)
39
55
 
40
56
  if audience is None:
@@ -0,0 +1,5 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class DjangoAppConfig(AppConfig):
5
+ name = "python_utils.django"
@@ -5,12 +5,12 @@ from keycloak import KeycloakAdmin
5
5
  from keycloak import KeycloakOpenIDConnection
6
6
  from keycloak.exceptions import KeycloakGetError
7
7
 
8
- from ..utils import (
8
+ from ..oidc_settings import (
9
9
  KEYCLOAK_SERVER_URL,
10
- KEYCLOAK_REALM,
11
10
  KEYCLOAK_CONFIDENTIAL_CLIENT_ID,
12
- get_keycloak_confidential_client_token,
11
+ get_oidc_confidential_client_token,
13
12
  )
13
+ from ..tenant_context import TenantContext
14
14
 
15
15
 
16
16
  def get_user_group_model():
@@ -67,12 +67,14 @@ class KeycloakService:
67
67
  deleted_groups.delete()
68
68
 
69
69
  def _get_keycloak_admin(self):
70
+ tenant = TenantContext.get()
71
+
70
72
  keycloak_connection = KeycloakOpenIDConnection(
71
73
  server_url=KEYCLOAK_SERVER_URL,
72
- realm_name=KEYCLOAK_REALM,
73
- user_realm_name=KEYCLOAK_REALM,
74
+ realm_name=tenant,
75
+ user_realm_name=tenant,
74
76
  client_id=KEYCLOAK_CONFIDENTIAL_CLIENT_ID,
75
- token=get_keycloak_confidential_client_token(),
77
+ token=get_oidc_confidential_client_token(),
76
78
  verify=True,
77
79
  )
78
80
  return KeycloakAdmin(connection=keycloak_connection)
@@ -0,0 +1,6 @@
1
+ from .tenant_aware_http_middleware import TenantAwareHttpMiddleware
2
+
3
+
4
+ __all__ = [
5
+ "TenantAwareHttpMiddleware",
6
+ ]
@@ -5,7 +5,7 @@ from django.conf import settings
5
5
  from django.core.handlers.wsgi import WSGIRequest
6
6
  from django.http import HttpResponse
7
7
 
8
- from ..settings import DEVELOPMENT_TENANT, TENANT_AWARE_EXCLUDED_PATHS, TENANT_DATABASES
8
+ from ..settings import DEVELOPMENT_TENANT, TENANT_AWARE_EXCLUDED_PATHS
9
9
  from ..tenant_context import TenantContext
10
10
 
11
11
  logger = logging.getLogger(__name__)
@@ -51,9 +51,6 @@ class TenantAwareHttpMiddleware:
51
51
  if self._is_excluded_path(request.path):
52
52
  return self.get_response(request)
53
53
 
54
- if TenantContext.is_set():
55
- raise Exception("Tenant context already set")
56
-
57
54
  # In DEBUG mode, use DEVELOPMENT_TENANT directly
58
55
  # In production, extract tenant from subdomain
59
56
  if settings.DEBUG:
@@ -64,10 +61,6 @@ class TenantAwareHttpMiddleware:
64
61
  if tenant is None:
65
62
  raise Exception(f"Could not determine tenant from subdomain. Host: {request.get_host()}")
66
63
 
67
- # Validate tenant exists in configured databases
68
- if not self._is_valid_tenant(tenant):
69
- raise Exception(f"Unknown tenant: {tenant}")
70
-
71
64
  # Call the next middleware in the chain until the response is returned.
72
65
  # After that, the database alias is removed from the thread local variable.
73
66
  with TenantContext(tenant):
@@ -94,27 +87,17 @@ class TenantAwareHttpMiddleware:
94
87
  - <app>.tenant-internal.domain.com -> tenant (strips -internal suffix)
95
88
  """
96
89
  host = request.get_host().split(":")[0] # Remove port if present
90
+
91
+ if host == "testserver":
92
+ logger.debug("Using 'default' tenant for testserver host.")
93
+ return "default"
94
+
97
95
  parts = host.split(".")
98
96
 
99
97
  # Need at least 3 parts: <app>.<tenant>.<domain>
100
98
  if len(parts) >= 3:
101
- tenant = self._normalize_tenant(parts[1])
99
+ tenant = parts[1].replace("-internal", "")
102
100
  logger.debug(f"Tenant '{tenant}' extracted from subdomain: {host}")
103
101
  return tenant
104
102
 
105
103
  return None
106
-
107
- @staticmethod
108
- def _normalize_tenant(tenant: str) -> str:
109
- # Remove -internal suffix if present
110
- if tenant.endswith("-internal"):
111
- return tenant[: -len("-internal")]
112
- return tenant
113
-
114
- @staticmethod
115
- def _is_valid_tenant(tenant: str) -> bool:
116
- """
117
- Validate that the tenant exists in configured databases.
118
- Skip 'default' as it's typically a placeholder.
119
- """
120
- return tenant in TENANT_DATABASES
@@ -0,0 +1,85 @@
1
+ """
2
+ This module provides dynamic OIDC configuration based on the current tenant context.
3
+ Each tenant has its own Keycloak realm, and the OIDC endpoints are computed
4
+ dynamically based on the tenant.
5
+ """
6
+
7
+ import json
8
+ import os
9
+ import requests
10
+
11
+ from .tenant_context import TenantContext
12
+
13
+
14
+ KEYCLOAK_SERVER_URL = os.getenv("KEYCLOAK_SERVER_URL", None)
15
+ KEYCLOAK_CONFIDENTIAL_CLIENT_ID = os.getenv("KEYCLOAK_CONFIDENTIAL_CLIENT_ID", None)
16
+ KEYCLOAK_CONFIDENTIAL_CLIENT_SERVICE_ACCOUNT_TOKEN_FILE_PATHS: dict[str, str] = json.loads(
17
+ os.getenv("KEYCLOAK_CONFIDENTIAL_CLIENT_SERVICE_ACCOUNT_TOKEN_FILE_PATHS", "{}")
18
+ )
19
+ KEYCLOAK_CLIENT_CREDENTIALS_GRANT_TYPE = "client_credentials"
20
+ KEYCLOAK_CLIENT_ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
21
+
22
+
23
+ def get_oidc_op_base_url() -> str:
24
+ """Get the base URL for the OIDC provider (Keycloak realm URL)."""
25
+
26
+ realm = TenantContext.get()
27
+ return f"{KEYCLOAK_SERVER_URL}/realms/{realm}"
28
+
29
+
30
+ def get_oidc_op_authorization_endpoint() -> str:
31
+ return f"{get_oidc_op_base_url()}/protocol/openid-connect/auth"
32
+
33
+
34
+ def get_oidc_op_token_endpoint() -> str:
35
+ return f"{get_oidc_op_base_url()}/protocol/openid-connect/token"
36
+
37
+
38
+ def get_oidc_op_user_endpoint() -> str:
39
+ return f"{get_oidc_op_base_url()}/protocol/openid-connect/userinfo"
40
+
41
+
42
+ def get_oidc_op_jwks_endpoint() -> str:
43
+ return f"{get_oidc_op_base_url()}/protocol/openid-connect/certs"
44
+
45
+
46
+ def get_oidc_op_logout_endpoint() -> str:
47
+ return f"{get_oidc_op_base_url()}/protocol/openid-connect/logout"
48
+
49
+
50
+ def get_confidential_client_service_account_token() -> str:
51
+ """
52
+ Reads the Keycloak confidential client service account token for the current tenant from a file.
53
+ """
54
+ tenant = TenantContext.get()
55
+ token_file_path = KEYCLOAK_CONFIDENTIAL_CLIENT_SERVICE_ACCOUNT_TOKEN_FILE_PATHS.get(tenant)
56
+ if not token_file_path or not os.path.isfile(token_file_path):
57
+ raise FileNotFoundError(f"Keycloak service account token file for tenant {tenant} not found: {token_file_path}")
58
+
59
+ with open(token_file_path, "r") as f:
60
+ token = f.read().strip()
61
+
62
+ if not token:
63
+ raise ValueError(f"Keycloak service account token for tenant {tenant} is empty.")
64
+
65
+ return token
66
+
67
+
68
+ def get_oidc_confidential_client_token(**kwargs) -> dict:
69
+ """
70
+ Obtains token for an OIDC confidential client with the client credentials grant,
71
+ using a service account token for authentication.
72
+ """
73
+
74
+ response = requests.post(
75
+ get_oidc_op_token_endpoint(),
76
+ data={
77
+ "grant_type": KEYCLOAK_CLIENT_CREDENTIALS_GRANT_TYPE,
78
+ "client_assertion_type": KEYCLOAK_CLIENT_ASSERTION_TYPE,
79
+ "client_assertion": get_confidential_client_service_account_token(),
80
+ **kwargs,
81
+ },
82
+ )
83
+ response.raise_for_status()
84
+
85
+ return response.json()
@@ -0,0 +1,4 @@
1
+ from .key_function import make_tenant_aware_key
2
+
3
+
4
+ __all__ = ["make_tenant_aware_key"]
@@ -1,5 +1,5 @@
1
1
  from ..tenant_context import TenantContext
2
2
 
3
3
 
4
- def make_key(key, key_prefix, version):
4
+ def make_tenant_aware_key(key, key_prefix, version):
5
5
  return ":".join([TenantContext.get(), key_prefix, str(version), key])
@@ -1,7 +1,8 @@
1
1
  from django.conf import settings
2
2
 
3
- TENANT_DATABASES = set(settings.DATABASES.keys()) - {"default"}
4
3
  TENANT_KEY = "tenant"
4
+ DATABASES = settings.DATABASES
5
+ TENANT_DATABASES = set(settings.DATABASES.keys()) - {"default"}
5
6
 
6
7
  TENANT_AWARE_EXCLUDED_PATHS = getattr(settings, "TENANT_AWARE_EXCLUDED_PATHS", ())
7
8
  TENANT_AWARE_EXCLUDED_PATHS = (
@@ -14,4 +15,9 @@ TENANT_AWARE_EXCLUDED_PATHS = (
14
15
  "/mediafiles",
15
16
  )
16
17
 
17
- DEVELOPMENT_TENANT = getattr(settings, "DEVELOPMENT_TENANT", list(TENANT_DATABASES)[0])
18
+ DEVELOPMENT_TENANT = getattr(settings, "DEVELOPMENT_TENANT")
19
+ if DEVELOPMENT_TENANT is None:
20
+ if TENANT_DATABASES:
21
+ DEVELOPMENT_TENANT = list(TENANT_DATABASES)[0]
22
+ else:
23
+ DEVELOPMENT_TENANT = list(DATABASES.keys())[0]
@@ -4,7 +4,7 @@ from contextlib import ContextDecorator
4
4
  from threading import local
5
5
  from typing import Optional
6
6
 
7
- from .settings import TENANT_DATABASES
7
+ from .settings import DATABASES
8
8
 
9
9
  logger = logging.getLogger(__name__)
10
10
  thread_namespace = local()
@@ -75,7 +75,7 @@ class TenantContext(ContextDecorator):
75
75
  logger.info("Tenant context already set to %s", tenant)
76
76
  return
77
77
 
78
- if tenant not in TENANT_DATABASES:
78
+ if tenant not in DATABASES:
79
79
  logger.error(f"Tenant '{tenant}' not found in DATABASES settings.")
80
80
  return sys.exit(1)
81
81
 
@@ -1,5 +0,0 @@
1
- include LICENSE
2
- include README.rst
3
- include python_utils/keycloak/django/admin/user_groups_changelist.html
4
- include python_utils/multi_tenancy/README.md
5
- recursive-exclude tests *
@@ -1,57 +0,0 @@
1
- LICENSE
2
- MANIFEST.in
3
- README.rst
4
- pyproject.toml
5
- cardo_python_utils.egg-info/PKG-INFO
6
- cardo_python_utils.egg-info/SOURCES.txt
7
- cardo_python_utils.egg-info/dependency_links.txt
8
- cardo_python_utils.egg-info/requires.txt
9
- cardo_python_utils.egg-info/top_level.txt
10
- python_utils/__init__.py
11
- python_utils/choices.py
12
- python_utils/data_structures.py
13
- python_utils/db.py
14
- python_utils/django_utils.py
15
- python_utils/esma_choices.py
16
- python_utils/exceptions.py
17
- python_utils/imports.py
18
- python_utils/math.py
19
- python_utils/text.py
20
- python_utils/time.py
21
- python_utils/types_hinting.py
22
- python_utils/keycloak/utils.py
23
- python_utils/keycloak/django/__init__.py
24
- python_utils/keycloak/django/service.py
25
- python_utils/keycloak/django/admin/__init__.py
26
- python_utils/keycloak/django/admin/auth.py
27
- python_utils/keycloak/django/admin/user_group.py
28
- python_utils/keycloak/django/admin/user_groups_changelist.html
29
- python_utils/keycloak/django/api/__init__.py
30
- python_utils/keycloak/django/api/drf.py
31
- python_utils/keycloak/django/api/ninja.py
32
- python_utils/keycloak/django/api/utils.py
33
- python_utils/keycloak/django/models/__init__.py
34
- python_utils/keycloak/django/models/user_group.py
35
- python_utils/keycloak/django/tests/__init__.py
36
- python_utils/keycloak/django/tests/conftest.py
37
- python_utils/multi_tenancy/README.md
38
- python_utils/multi_tenancy/__init__.py
39
- python_utils/multi_tenancy/apps.py
40
- python_utils/multi_tenancy/settings.py
41
- python_utils/multi_tenancy/tenant_context.py
42
- python_utils/multi_tenancy/celery/__init__.py
43
- python_utils/multi_tenancy/celery/tenant_aware_task.py
44
- python_utils/multi_tenancy/db/__init__.py
45
- python_utils/multi_tenancy/db/routers.py
46
- python_utils/multi_tenancy/db/transaction.py
47
- python_utils/multi_tenancy/db/utils.py
48
- python_utils/multi_tenancy/management/__init__.py
49
- python_utils/multi_tenancy/management/commands/__init__.py
50
- python_utils/multi_tenancy/management/commands/migrateall.py
51
- python_utils/multi_tenancy/management/commands/tenant_aware_command.py
52
- python_utils/multi_tenancy/middleware/__init__.py
53
- python_utils/multi_tenancy/middleware/tenant_http_middleware.py
54
- python_utils/multi_tenancy/redis/__init__.py
55
- python_utils/multi_tenancy/redis/key_function.py
56
- python_utils/multi_tenancy/storage/__init__.py
57
- python_utils/multi_tenancy/storage/tenant_aware_storage.py
@@ -1,51 +0,0 @@
1
- import os
2
- import requests
3
-
4
- KEYCLOAK_SERVER_URL = os.getenv("KEYCLOAK_SERVER_URL")
5
- KEYCLOAK_REALM = os.getenv("KEYCLOAK_REALM")
6
- KEYCLOAK_REALM_URL = f"{KEYCLOAK_SERVER_URL}/realms/{KEYCLOAK_REALM}"
7
- KEYCLOAK_TOKEN_ENDPOINT = f"{KEYCLOAK_REALM_URL}/protocol/openid-connect/token"
8
-
9
- KEYCLOAK_CONFIDENTIAL_CLIENT_ID = os.getenv("KEYCLOAK_CONFIDENTIAL_CLIENT_ID")
10
- KEYCLOAK_CONFIDENTIAL_CLIENT_SERVICE_ACCOUNT_TOKEN_FILE_PATH = os.getenv(
11
- "KEYCLOAK_CONFIDENTIAL_CLIENT_SERVICE_ACCOUNT_TOKEN_FILE_PATH"
12
- )
13
- KEYCLOAK_CLIENT_CREDENTIALS_GRANT_TYPE = "client_credentials"
14
- KEYCLOAK_CLIENT_ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
15
-
16
-
17
- def get_confidential_client_service_account_token() -> str:
18
- """
19
- Reads the Keycloak confidential client service account token from a file.
20
- """
21
- token_file_path = KEYCLOAK_CONFIDENTIAL_CLIENT_SERVICE_ACCOUNT_TOKEN_FILE_PATH
22
- if not token_file_path or not os.path.isfile(token_file_path):
23
- raise FileNotFoundError(f"Keycloak service account token file not found: {token_file_path}")
24
-
25
- with open(token_file_path, "r") as f:
26
- token = f.read().strip()
27
-
28
- if not token:
29
- raise ValueError("Keycloak service account token is empty.")
30
-
31
- return token
32
-
33
-
34
- def get_keycloak_confidential_client_token(**kwargs) -> dict:
35
- """
36
- Obtains token for a Keycloak confidential client with the client credentials grant,
37
- using a service account token for authentication.
38
- """
39
-
40
- response = requests.post(
41
- KEYCLOAK_TOKEN_ENDPOINT,
42
- data={
43
- "grant_type": KEYCLOAK_CLIENT_CREDENTIALS_GRANT_TYPE,
44
- "client_assertion_type": KEYCLOAK_CLIENT_ASSERTION_TYPE,
45
- "client_assertion": get_confidential_client_service_account_token(),
46
- **kwargs,
47
- },
48
- )
49
- response.raise_for_status()
50
-
51
- return response.json()
@@ -1,62 +0,0 @@
1
- This package adds multi-tenancy support to a Django application, with a separate database for each tenant.
2
- Several components are provided to facilitate this, including: database routing, middleware, celery tasks etc.
3
-
4
- It is heavily inspired by the implementation of multi-tenancy in the Mercury app by Klement Omeri.
5
-
6
- # Usage
7
-
8
- To use this package, add the following to your Django settings.py file:
9
-
10
- ```python3
11
- INSTALLED_APPS = [
12
- ...
13
- "python_utils.multi_tenancy",
14
- ...
15
- ]
16
-
17
- MIDDLEWARE = [
18
- "python_utils.multi_tenancy.middleware.TenantAwareHttpMiddleware",
19
- ...
20
- ]
21
-
22
- # Include the database configuration for each tenant in the DATABASES setting.
23
- # You can use the get_database_configs() function from python_utils.multi_tenancy.db.utils as a helper.
24
- DATABASES = {
25
- 'tenant1': { ... },
26
- 'tenant2': { ... },
27
- ...
28
- }
29
-
30
- # If you want to override the database alias to use for local development (when DEBUG is True).
31
- # By default, the first database defined in DATABASES is used.
32
- DEVELOPMENT_TENANT = "development"
33
-
34
- # This is required to use the tenant context when routing database queries
35
- DATABASE_ROUTERS = [
36
- "python_utils.multi_tenancy.db.routers.TenantAwareRouter"
37
- ]
38
-
39
- # If using celery, set the task class to TenantAwareTask:
40
- CELERY_TASK_CLS = "python_utils.multi_tenancy.celery.TenantAwareTask"
41
-
42
- # If using Redis caching, configure the cache backend as follows:
43
- CACHES = {
44
- "default": {
45
- "BACKEND": "django_redis.cache.RedisCache",
46
- "LOCATION": REDIS_LOCATION,
47
- "KEY_FUNCTION": "python_utils.multi_tenancy.redis.make_key",
48
- **OPTIONS,
49
- }
50
- }
51
-
52
- # If using Django Storages with S3, configure the storage backends as follows:
53
- STORAGES = {
54
- "default": {
55
- "BACKEND": "python_utils.multi_tenancy.storage.TenantAwarePrivateS3Storage",
56
- },
57
- }
58
-
59
- # If you want to exclude certain paths from tenant processing, use TENANT_AWARE_EXCLUDED_PATHS:
60
- # They are considered as prefixes, so all paths starting with the given strings will be excluded.
61
- TENANT_AWARE_EXCLUDED_PATHS = ("/some/path",)
62
- ```
@@ -1 +0,0 @@
1
- default_app_config = "python_utils.multi_tenancy.apps.MultiTenancyConfig"
@@ -1,7 +0,0 @@
1
- from django.apps import AppConfig
2
-
3
-
4
- class MultiTenancyConfig(AppConfig):
5
- name = "python_utils.multi_tenancy"
6
- label = "multi_tenancy"
7
- verbose_name = "Multi-Tenancy"
@@ -1,6 +0,0 @@
1
- from .tenant_http_middleware import TenantAwareHttpMiddleware
2
-
3
-
4
- __all__ = [
5
- "TenantAwareHttpMiddleware",
6
- ]
@@ -1,4 +0,0 @@
1
- from .key_function import make_key
2
-
3
-
4
- __all__ = ["make_key"]