cardo-python-utils 0.5.dev50__py3-none-any.whl → 0.5.dev53__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.dev50
3
+ Version: 0.5.dev53
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.dev50.dist-info/licenses/LICENSE,sha256=N-YtxDy8n5A1Mo7JKKItNIlboiK_pMOZ48ojx76jo3g,1046
1
+ cardo_python_utils-0.5.dev53.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
@@ -11,7 +11,7 @@ python_utils/math.py,sha256=p_v8a9nVSe9426nR8H_SM8hOQrkzESVpCnn3gntw7TA,5603
11
11
  python_utils/text.py,sha256=pw9CZeM_Lcw-6k4GyR-4D1Wix8A7F_V1u1IIZTIazW4,3792
12
12
  python_utils/time.py,sha256=7Wei3uJ02Bk-BFRf-e1axoG418XQOhrXPvTwNZgTdnw,9614
13
13
  python_utils/types_hinting.py,sha256=QVWzmXRgNxhvln14tEX_FbQYryuVYhjWJ0dVOnlF6G4,120
14
- python_utils/django/README.md,sha256=1_I6P3sB2_nBZ9hb0kUrTI4W4pDOxRAkfSO1iekfEVE,6750
14
+ python_utils/django/README.md,sha256=J9CEKaFiMGOnIRsoIpDbQPlno4O6vgymMJLQHmeSnVg,7236
15
15
  python_utils/django/__init__.py,sha256=uXyqF-_5gZAlSIKoQkUTedAeBjnUHqh6lR6SzA1DEOM,64
16
16
  python_utils/django/apps.py,sha256=vH2Ov8XgavTGKFLSjbH1kvuG7RWQCjeJepw6BSp2o3E,126
17
17
  python_utils/django/oidc_settings.py,sha256=JqF-qOfW23JhmmVciN1B7ZV-KI7qrdn5VigTE7E2k_0,4367
@@ -24,22 +24,23 @@ python_utils/django/admin/views.py,sha256=D4Ez-RkZa_c7O8AN1ljosBBJWieSodrY2TAZ7-
24
24
  python_utils/django/admin/templates/__init__.py,sha256=LxCKcnJ1Ty48CFDJ8XtAZYTW45xDw5o7H4GLsRo6iQk,53
25
25
  python_utils/django/admin/templates/user_groups_changelist.html,sha256=KbO6bsH-nh3DDYCq4UB8j25NdjLP_nh_GuGZ4lYSarM,182
26
26
  python_utils/django/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
- python_utils/django/api/drf.py,sha256=a61gp-fSHV760FYfRv_IxnykuB14sSSI6tS1dUB1POk,2957
28
- python_utils/django/api/ninja.py,sha256=r0-tlS8TlCp_q1_3wdplyZmYMjeUwv8YuzWjRTtijBw,4765
27
+ python_utils/django/api/drf.py,sha256=ar94uAVIaHDxzi3K7ubNSJDuysUWjV_xv0sFXtkJEuc,3101
28
+ python_utils/django/api/ninja.py,sha256=Rz7331hYmKo4jyxtuvEKzOaNUjdmuCpXeHYkWGf0NsA,4863
29
29
  python_utils/django/api/utils.py,sha256=ycpSnTtGcfdGP1_Hk0P2c8ZNId70xOYtjx1m0nAUWRM,3465
30
30
  python_utils/django/auth/service.py,sha256=9gMURteyVnfjpZLyrvjAvgglfi2NPwrdia2LDVPfxBs,7475
31
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
- python_utils/django/celery/tenant_aware_task.py,sha256=RS-6etWP16vYka0nlY1UbEdriqF8ouW5OML05fBF-LA,2944
33
+ python_utils/django/celery/tenant_aware_task.py,sha256=kjcAfAAOUAveOKaoOBe-yJLdhXIT7CMgNKOPOFKjEbQ,4065
34
34
  python_utils/django/db/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
35
35
  python_utils/django/db/alias.py,sha256=sybxhZsShJdnz13hqEyYLxuaRert9zpRPPi-bAayyM4,574
36
36
  python_utils/django/db/routers.py,sha256=Q4YAvr37wgEgL04miPKSREhzFB5LDZrXulqStbqBZeA,252
37
37
  python_utils/django/db/transaction.py,sha256=0d4Yn98qxjn1LOJfaOTsd5qpPqB2fCqxsqG16StfcSg,921
38
- python_utils/django/db/utils.py,sha256=DMrFHTjXOkeDsUTdYOCAnZOU_PHKh0zvx-WXsyn7Z-A,2029
38
+ python_utils/django/db/utils.py,sha256=RtaoaLUQF8bOS0nNBCiFsyoj4zs-iqKstoM65pxWOls,2010
39
39
  python_utils/django/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
40
40
  python_utils/django/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
41
  python_utils/django/management/commands/migrateall.py,sha256=j1AA29FOGr5Rwe6Fjm4RMFA1-qtb66z_zRi4e7ocHV0,783
42
42
  python_utils/django/management/commands/shell.py,sha256=fGHJ1i-p-TQ9lFLOz0j4dm_HEH0kgTO4FGmtYMNqqK4,967
43
+ python_utils/django/management/commands/showmigrations.py,sha256=mwNnL2EYAqX3iZ5-YI86X1-9YhbHvmMMG_wX3wNykKc,1260
43
44
  python_utils/django/management/commands/tenant_aware_command.py,sha256=MBI7N0BEHZAhK183QmFjjEdDecM060O0nsxg0spH9Mc,2440
44
45
  python_utils/django/middleware/__init__.py,sha256=GyhuzaOtaXW13iB9GL0XF7OvEEzKoJzorJzYX597idA,117
45
46
  python_utils/django/middleware/tenant_aware_http_middleware.py,sha256=W8U3-nR90b8m_wz5xDJTuImUrPEXvESzYh3VITYMIJU,3825
@@ -63,7 +64,7 @@ python_utils/django/storage/__init__.py,sha256=mNn2YmD7pkXhBLHMM1444BLsCMq78YdYx
63
64
  python_utils/django/storage/tenant_aware_storage.py,sha256=5dDes6xLv7_R8hIBbFIzRvPL7HL9K_RM-G6LI8qUSxM,2550
64
65
  python_utils/django/tests/__init__.py,sha256=Nkt0a7LEHyjLvuEBZ7113VjjAWJlyZlMy-H-JZ5tNcs,252
65
66
  python_utils/django/tests/conftest.py,sha256=KozXmXUWVcDLbkVAb7Aq4sDydGLh2YZkbRa4tkA8Z6U,3167
66
- cardo_python_utils-0.5.dev50.dist-info/METADATA,sha256=BMJmbLgqqHr5HtaBkeZ-OSFIz-I3o15trwrPqpW71ug,3007
67
- cardo_python_utils-0.5.dev50.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
68
- cardo_python_utils-0.5.dev50.dist-info/top_level.txt,sha256=zAx6OfEsjJs8BEW3okSiG_j9gpkI69xWShzum6oBgKI,13
69
- cardo_python_utils-0.5.dev50.dist-info/RECORD,,
67
+ cardo_python_utils-0.5.dev53.dist-info/METADATA,sha256=OQENj0UZFzNZWi0xazB6ZH6qv5wc9GsaapXXXocNJWQ,3007
68
+ cardo_python_utils-0.5.dev53.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
69
+ cardo_python_utils-0.5.dev53.dist-info/top_level.txt,sha256=zAx6OfEsjJs8BEW3okSiG_j9gpkI69xWShzum6oBgKI,13
70
+ cardo_python_utils-0.5.dev53.dist-info/RECORD,,
@@ -157,14 +157,28 @@ If using `django-ninja`, apart from the settings configured above, auth utils ar
157
157
 
158
158
  ## Atomic Transactions
159
159
 
160
- Django's `transaction.atomic` uses the default database. To make it tenant-aware, use `tenant_atomic`
160
+ Django's `transaction.atomic` uses the default database. To make it tenant-aware, use `tenant_atomic`.
161
+ If `transaction.on_commit` is used, make sure to pass the tenant as DB alias as well:
161
162
 
162
163
  ```python3
163
164
  from python_utils.django.db.transaction import tenant_atomic
164
165
 
165
166
  @tenant_atomic
166
167
  def my_function():
167
- ...
168
+ # Some logic
169
+
170
+ transaction.on_commit(do_smth, using=TenantContext.get())
171
+ ```
172
+
173
+ ## Explicit database connection
174
+
175
+ If using django.db.connection anywhere in the code, you need to change that to get a tenant-aware connection:
176
+
177
+ ```python3
178
+ from python_utils.django.db.utils import get_connection
179
+ from python_utils.django.tenant_context import TenantContext
180
+
181
+ connection = get_connection(TenantContext.get())
168
182
  ```
169
183
 
170
184
  ## Django Shell
@@ -1,8 +1,8 @@
1
1
  from django.conf import settings
2
- from jwt.exceptions import InvalidTokenError, PyJWKClientError
2
+ from jwt.exceptions import ExpiredSignatureError, InvalidTokenError, PyJWKClientError
3
3
 
4
4
  from rest_framework import authentication
5
- from rest_framework.exceptions import AuthenticationFailed
5
+ from rest_framework.exceptions import AuthenticationFailed, PermissionDenied
6
6
  from rest_framework.permissions import BasePermission
7
7
 
8
8
  from .utils import create_or_update_user, decode_jwt
@@ -14,13 +14,15 @@ class AuthenticationBackend(authentication.TokenAuthentication):
14
14
  def authenticate_credentials(self, token: str):
15
15
  try:
16
16
  payload = decode_jwt(token, audience=self._get_audience())
17
+ except ExpiredSignatureError as e:
18
+ raise AuthenticationFailed("Token has expired.") from e
17
19
  except (InvalidTokenError, PyJWKClientError) as e:
18
- raise AuthenticationFailed(f"Invalid token: {str(e)}") from e
20
+ raise PermissionDenied(f"Invalid token: {str(e)}") from e
19
21
 
20
22
  try:
21
23
  username = payload["preferred_username"]
22
24
  except KeyError as e:
23
- raise AuthenticationFailed(
25
+ raise PermissionDenied(
24
26
  "Invalid token: preferred_username not present."
25
27
  ) from e
26
28
 
@@ -1,12 +1,12 @@
1
1
  import logging
2
2
  from typing import Literal, Optional, Union
3
3
 
4
- from jwt.exceptions import InvalidTokenError, PyJWKClientError
4
+ from jwt.exceptions import ExpiredSignatureError, InvalidTokenError, PyJWKClientError
5
5
 
6
6
  from django.conf import settings
7
7
  from django.http import HttpRequest
8
8
  from ninja.security import HttpBearer
9
- from ninja.errors import AuthenticationError, HttpError
9
+ from ninja.errors import AuthenticationError, AuthorizationError, HttpError
10
10
 
11
11
  from .utils import (
12
12
  acreate_or_update_user,
@@ -42,7 +42,7 @@ class AuthBearer(HttpBearer):
42
42
 
43
43
  def _get_token(self, request: HttpRequest) -> Optional[str]:
44
44
  """
45
- This part of the token validation is similar to what
45
+ This part of the token validation is similar to what
46
46
  django-ninja is doing in HttpBearer.__call__
47
47
  """
48
48
  headers = request.headers
@@ -61,16 +61,16 @@ class AuthBearer(HttpBearer):
61
61
  def _decode_token(self, token: str) -> TokenPayload:
62
62
  try:
63
63
  return decode_jwt(token)
64
+ except ExpiredSignatureError as e:
65
+ raise AuthenticationError("Token has expired.") from e
64
66
  except (InvalidTokenError, PyJWKClientError) as e:
65
- raise AuthenticationError(f"Invalid token: {str(e)}") from e
67
+ raise AuthorizationError(f"Invalid token: {str(e)}") from e
66
68
 
67
69
  def _get_username(self, payload: TokenPayload) -> str:
68
70
  try:
69
71
  return payload["preferred_username"]
70
72
  except KeyError as e:
71
- raise AuthenticationError(
72
- "Invalid token: 'preferred_username' claim not present."
73
- ) from e
73
+ raise AuthorizationError("Invalid token: 'preferred_username' claim not present.") from e
74
74
 
75
75
  def _verify_scopes(self, request, token_payload):
76
76
  allowed_scopes = self._get_view_allowed_scopes(request)
@@ -106,9 +106,7 @@ class AuthBearer(HttpBearer):
106
106
  if operation.methods and method in operation.methods:
107
107
  return operation.view_func
108
108
 
109
- raise Exception(
110
- f"Could not determine the view function for {request.method} {request.path}."
111
- )
109
+ raise Exception(f"Could not determine the view function for {request.method} {request.path}.")
112
110
 
113
111
 
114
112
  class AuthBearerAsync(AuthBearer):
@@ -27,8 +27,49 @@ class TenantAwareTask(TaskClass):
27
27
 
28
28
  once = {"graceful": True, "unlock_before_run": False}
29
29
 
30
+ def apply(
31
+ self,
32
+ args=None,
33
+ kwargs=None,
34
+ link=None,
35
+ link_error=None,
36
+ task_id=None,
37
+ retries=None,
38
+ throw=None,
39
+ logfile=None,
40
+ loglevel=None,
41
+ headers=None,
42
+ **options,
43
+ ):
44
+ """Pass the tenant name from the context in the kwargs."""
45
+
46
+ if kwargs is None:
47
+ kwargs = {}
48
+
49
+ if TENANT_KEY not in kwargs:
50
+ tenant = TenantContext.get()
51
+ kwargs[TENANT_KEY] = tenant
52
+
53
+ return super().apply(
54
+ args, kwargs, link, link_error, task_id, retries, throw, logfile, loglevel, headers, **options
55
+ )
56
+
57
+ def apply_async(
58
+ self, args=None, kwargs=None, task_id=None, producer=None, link=None, link_error=None, shadow=None, **options
59
+ ):
60
+ """Pass the tenant name from the context in the kwargs."""
61
+
62
+ if kwargs is None:
63
+ kwargs = {}
64
+
65
+ if TENANT_KEY not in kwargs:
66
+ tenant = TenantContext.get()
67
+ kwargs[TENANT_KEY] = tenant
68
+
69
+ return super().apply_async(args, kwargs, task_id, producer, link, link_error, shadow, **options)
70
+
30
71
  def __call__(self, *args, **kwargs):
31
- """Override the __call__ method to set the tenant name in the thread namespace."""
72
+ """Use the tenant name from the kwargs to update the context."""
32
73
 
33
74
  # Only clear the lock before the task's execution if the
34
75
  # "unlock_before_run" option is True
@@ -43,7 +84,8 @@ class TenantAwareTask(TaskClass):
43
84
  return self.run(*args, **kwargs)
44
85
 
45
86
  def after_return(self, status, retval, task_id, args, kwargs, einfo):
46
- """Clear the tenant from the thread namespace after the task has returned."""
87
+ """Clear the tenant from the context after the task has returned."""
88
+
47
89
  TenantContext.clear()
48
90
  super().after_return(status, retval, task_id, args, kwargs, einfo)
49
91
 
@@ -67,7 +109,7 @@ class TenantAwareTask(TaskClass):
67
109
  tenant_kwarg = {TENANT_KEY: tenant}
68
110
  task_call_args = super()._get_call_args(args, _kwargs)
69
111
 
70
- # Add the tenant kwarg back to the return value
112
+ # Add the tenant kwarg back to the return value
71
113
  # since this value is being used to create the key for the lock.
72
114
  # We want to lock the task for the tenant that is running it.
73
115
  return task_call_args | tenant_kwarg
@@ -1,7 +1,7 @@
1
1
  from functools import reduce
2
2
  import json
3
3
  import os
4
- from typing import NotRequired, TypedDict
4
+ from typing import TypedDict
5
5
  from django.db import connections
6
6
 
7
7
 
@@ -10,7 +10,7 @@ class DatabaseConfigData(TypedDict):
10
10
  name: str
11
11
  user: str
12
12
  password: str
13
- port: NotRequired[int]
13
+ port: int | None
14
14
 
15
15
 
16
16
  def get_connection(tenant: str = None):
@@ -0,0 +1,37 @@
1
+ from django.core.management.commands.showmigrations import Command as ShowMigrationsCommand
2
+
3
+ from ...settings import TENANT_DATABASES
4
+ from ...tenant_context import TenantContext
5
+
6
+
7
+ class Command(ShowMigrationsCommand):
8
+ help = "Shows all available migrations for a specific tenant."
9
+
10
+ def add_arguments(self, parser):
11
+ super().add_arguments(parser)
12
+
13
+ parser.add_argument(
14
+ "--tenant",
15
+ action="store",
16
+ dest="tenant",
17
+ help="Specify the tenant to show migrations for, either a single tenant name or 'all'.",
18
+ required=True,
19
+ type=str,
20
+ )
21
+
22
+ def handle(self, *args, **options):
23
+ tenant = options["tenant"]
24
+
25
+ if tenant.lower() == "all":
26
+ tenants_to_use = list(TENANT_DATABASES)
27
+ else:
28
+ if tenant not in TENANT_DATABASES:
29
+ self.stdout.write(self.style.ERROR(f"Tenant '{tenant}' not found in DATABASES settings."))
30
+ return
31
+ tenants_to_use = [tenant]
32
+
33
+ for t in tenants_to_use:
34
+ self.stdout.write(self.style.MIGRATE_LABEL(f"\n=== Tenant: {t} ==="))
35
+ options["database"] = t
36
+ with TenantContext(t):
37
+ super().handle(*args, **options)