cardo-python-utils 0.4.2__tar.gz → 0.5.dev0__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 (32) hide show
  1. {cardo_python_utils-0.4.2/cardo_python_utils.egg-info → cardo_python_utils-0.5.dev0}/PKG-INFO +29 -22
  2. {cardo_python_utils-0.4.2 → cardo_python_utils-0.5.dev0}/README.rst +2 -4
  3. {cardo_python_utils-0.4.2 → cardo_python_utils-0.5.dev0/cardo_python_utils.egg-info}/PKG-INFO +29 -22
  4. {cardo_python_utils-0.4.2 → cardo_python_utils-0.5.dev0}/cardo_python_utils.egg-info/SOURCES.txt +7 -6
  5. cardo_python_utils-0.5.dev0/cardo_python_utils.egg-info/requires.txt +26 -0
  6. {cardo_python_utils-0.4.2 → cardo_python_utils-0.5.dev0}/cardo_python_utils.egg-info/top_level.txt +1 -0
  7. cardo_python_utils-0.5.dev0/pyproject.toml +81 -0
  8. {cardo_python_utils-0.4.2 → cardo_python_utils-0.5.dev0}/python_utils/choices.py +1 -1
  9. cardo_python_utils-0.5.dev0/python_utils/django/auth/__init__.py +0 -0
  10. cardo_python_utils-0.5.dev0/python_utils/django/auth/admin.py +49 -0
  11. cardo_python_utils-0.5.dev0/python_utils/django/auth/drf.py +117 -0
  12. cardo_python_utils-0.5.dev0/python_utils/django/auth/ninja.py +128 -0
  13. cardo_python_utils-0.4.2/python_utils/django_utils.py → cardo_python_utils-0.5.dev0/python_utils/django/utils.py +0 -38
  14. {cardo_python_utils-0.4.2 → cardo_python_utils-0.5.dev0}/python_utils/text.py +5 -4
  15. cardo_python_utils-0.5.dev0/setup.cfg +4 -0
  16. cardo_python_utils-0.4.2/cardo_python_utils.egg-info/requires.txt +0 -18
  17. cardo_python_utils-0.4.2/python_utils/pandas_utils.py +0 -143
  18. cardo_python_utils-0.4.2/python_utils/rest.py +0 -37
  19. cardo_python_utils-0.4.2/setup.cfg +0 -44
  20. cardo_python_utils-0.4.2/setup.py +0 -7
  21. {cardo_python_utils-0.4.2 → cardo_python_utils-0.5.dev0}/LICENSE +0 -0
  22. {cardo_python_utils-0.4.2 → cardo_python_utils-0.5.dev0}/MANIFEST.in +0 -0
  23. {cardo_python_utils-0.4.2 → cardo_python_utils-0.5.dev0}/cardo_python_utils.egg-info/dependency_links.txt +0 -0
  24. {cardo_python_utils-0.4.2 → cardo_python_utils-0.5.dev0}/python_utils/__init__.py +0 -0
  25. {cardo_python_utils-0.4.2 → cardo_python_utils-0.5.dev0}/python_utils/data_structures.py +0 -0
  26. {cardo_python_utils-0.4.2 → cardo_python_utils-0.5.dev0}/python_utils/db.py +0 -0
  27. {cardo_python_utils-0.4.2 → cardo_python_utils-0.5.dev0}/python_utils/esma_choices.py +0 -0
  28. {cardo_python_utils-0.4.2 → cardo_python_utils-0.5.dev0}/python_utils/exceptions.py +0 -0
  29. {cardo_python_utils-0.4.2 → cardo_python_utils-0.5.dev0}/python_utils/imports.py +0 -0
  30. {cardo_python_utils-0.4.2 → cardo_python_utils-0.5.dev0}/python_utils/math.py +0 -0
  31. {cardo_python_utils-0.4.2 → cardo_python_utils-0.5.dev0}/python_utils/time.py +0 -0
  32. {cardo_python_utils-0.4.2 → cardo_python_utils-0.5.dev0}/python_utils/types_hinting.py +0 -0
@@ -1,40 +1,49 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cardo-python-utils
3
- Version: 0.4.2
3
+ Version: 0.5.dev0
4
4
  Summary: Python library enhanced with a wide range of functions for different scenarios.
5
- Home-page: https://github.com/CardoAI/cardo-python-utils
6
- Author: Kristi Kotini
7
- Author-email: hello@cardoai.com
8
- License: MIT (X11)
5
+ Author-email: Kristi Kotini <hello@cardoai.com>, Klajdi Çaushi <hello@cardoai.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/CardoAI/cardo-python-utils
8
+ Project-URL: Repository, https://github.com/CardoAI/cardo-python-utils.git
9
+ Project-URL: Issues, https://github.com/CardoAI/cardo-python-utils/issues
10
+ Keywords: utilities,helpers,django
9
11
  Classifier: Environment :: Web Environment
10
12
  Classifier: Framework :: Django
11
13
  Classifier: Intended Audience :: Developers
12
- Classifier: License :: OSI Approved :: BSD License
14
+ Classifier: License :: OSI Approved :: MIT License
13
15
  Classifier: Operating System :: OS Independent
14
16
  Classifier: Programming Language :: Python
15
17
  Classifier: Programming Language :: Python :: 3
16
18
  Classifier: Programming Language :: Python :: 3 :: Only
17
- Classifier: Programming Language :: Python :: 3.8
18
- Classifier: Programming Language :: Python :: 3.9
19
19
  Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
20
22
  Classifier: Topic :: Internet :: WWW/HTTP
21
23
  Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
22
24
  Requires-Python: >=3.8
25
+ Description-Content-Type: text/x-rst
23
26
  License-File: LICENSE
24
- Provides-Extra: pandas
25
- Requires-Dist: pandas>=1.4.0; extra == "pandas"
26
27
  Provides-Extra: django
27
28
  Requires-Dist: Django; extra == "django"
28
- Requires-Dist: django-model-utils==4.2.0; extra == "django"
29
- Provides-Extra: rest
30
- Requires-Dist: djangorestframework; extra == "rest"
31
- Requires-Dist: requests; extra == "rest"
29
+ Provides-Extra: django-ninja
30
+ Requires-Dist: Django; extra == "django-ninja"
31
+ Requires-Dist: django-ninja; extra == "django-ninja"
32
+ Requires-Dist: PyJWT; extra == "django-ninja"
33
+ Provides-Extra: drf
34
+ Requires-Dist: Django; extra == "drf"
35
+ Requires-Dist: djangorestframework; extra == "drf"
36
+ Requires-Dist: PyJWT; extra == "drf"
37
+ Provides-Extra: django-admin-auth
38
+ Requires-Dist: Django; extra == "django-admin-auth"
39
+ Requires-Dist: mozilla-django-oidc>=4.0.1; extra == "django-admin-auth"
32
40
  Provides-Extra: all
33
41
  Requires-Dist: Django; extra == "all"
34
- Requires-Dist: pandas>=1.4.0; extra == "all"
35
- Requires-Dist: django-model-utils>=4.2.0; extra == "all"
36
- Requires-Dist: djangorestframework; extra == "all"
37
- Requires-Dist: requests; extra == "all"
42
+ Provides-Extra: dev
43
+ Requires-Dist: pytest>=7.0; extra == "dev"
44
+ Requires-Dist: pytest-django>=4.5; extra == "dev"
45
+ Requires-Dist: coverage>=6.0; extra == "dev"
46
+ Requires-Dist: tox>=3.25; extra == "dev"
38
47
  Dynamic: license-file
39
48
 
40
49
  ============================
@@ -50,9 +59,7 @@ Main utils:
50
59
  * data_structures
51
60
  * db
52
61
  * django
53
- * django_rest
54
62
  * math
55
- * pandas
56
63
  * exception
57
64
  * choices
58
65
 
@@ -64,7 +71,7 @@ Quick start
64
71
  from python_utils.time import date_range
65
72
  date_range(start_date, end_date)
66
73
 
67
- Although the library provides some utility functions related to other libraries like django and pandas, it does not install any dependencies automatically.
74
+ Although the library provides some utility functions related to other libraries like django, it does not install any dependencies automatically.
68
75
  This means, you can install the library even if you do not use these libraries, but keep in mind that in this case you cannot use the
69
76
  functions that depend on them.
70
77
 
@@ -74,7 +81,7 @@ You can also chose to install the dependencies alongside the library, including
74
81
 
75
82
  Tests
76
83
  -----
77
- The library has a 100% coverage by tests. If you want to see tests in action:
84
+ The library has a high coverage by tests. If you want to see tests in action:
78
85
 
79
86
  1. Inside venv, run ``pip install -r tests/requirements.txt``
80
87
 
@@ -11,9 +11,7 @@ Main utils:
11
11
  * data_structures
12
12
  * db
13
13
  * django
14
- * django_rest
15
14
  * math
16
- * pandas
17
15
  * exception
18
16
  * choices
19
17
 
@@ -25,7 +23,7 @@ Quick start
25
23
  from python_utils.time import date_range
26
24
  date_range(start_date, end_date)
27
25
 
28
- Although the library provides some utility functions related to other libraries like django and pandas, it does not install any dependencies automatically.
26
+ Although the library provides some utility functions related to other libraries like django, it does not install any dependencies automatically.
29
27
  This means, you can install the library even if you do not use these libraries, but keep in mind that in this case you cannot use the
30
28
  functions that depend on them.
31
29
 
@@ -35,7 +33,7 @@ You can also chose to install the dependencies alongside the library, including
35
33
 
36
34
  Tests
37
35
  -----
38
- The library has a 100% coverage by tests. If you want to see tests in action:
36
+ The library has a high coverage by tests. If you want to see tests in action:
39
37
 
40
38
  1. Inside venv, run ``pip install -r tests/requirements.txt``
41
39
 
@@ -1,40 +1,49 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cardo-python-utils
3
- Version: 0.4.2
3
+ Version: 0.5.dev0
4
4
  Summary: Python library enhanced with a wide range of functions for different scenarios.
5
- Home-page: https://github.com/CardoAI/cardo-python-utils
6
- Author: Kristi Kotini
7
- Author-email: hello@cardoai.com
8
- License: MIT (X11)
5
+ Author-email: Kristi Kotini <hello@cardoai.com>, Klajdi Çaushi <hello@cardoai.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/CardoAI/cardo-python-utils
8
+ Project-URL: Repository, https://github.com/CardoAI/cardo-python-utils.git
9
+ Project-URL: Issues, https://github.com/CardoAI/cardo-python-utils/issues
10
+ Keywords: utilities,helpers,django
9
11
  Classifier: Environment :: Web Environment
10
12
  Classifier: Framework :: Django
11
13
  Classifier: Intended Audience :: Developers
12
- Classifier: License :: OSI Approved :: BSD License
14
+ Classifier: License :: OSI Approved :: MIT License
13
15
  Classifier: Operating System :: OS Independent
14
16
  Classifier: Programming Language :: Python
15
17
  Classifier: Programming Language :: Python :: 3
16
18
  Classifier: Programming Language :: Python :: 3 :: Only
17
- Classifier: Programming Language :: Python :: 3.8
18
- Classifier: Programming Language :: Python :: 3.9
19
19
  Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
20
22
  Classifier: Topic :: Internet :: WWW/HTTP
21
23
  Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
22
24
  Requires-Python: >=3.8
25
+ Description-Content-Type: text/x-rst
23
26
  License-File: LICENSE
24
- Provides-Extra: pandas
25
- Requires-Dist: pandas>=1.4.0; extra == "pandas"
26
27
  Provides-Extra: django
27
28
  Requires-Dist: Django; extra == "django"
28
- Requires-Dist: django-model-utils==4.2.0; extra == "django"
29
- Provides-Extra: rest
30
- Requires-Dist: djangorestframework; extra == "rest"
31
- Requires-Dist: requests; extra == "rest"
29
+ Provides-Extra: django-ninja
30
+ Requires-Dist: Django; extra == "django-ninja"
31
+ Requires-Dist: django-ninja; extra == "django-ninja"
32
+ Requires-Dist: PyJWT; extra == "django-ninja"
33
+ Provides-Extra: drf
34
+ Requires-Dist: Django; extra == "drf"
35
+ Requires-Dist: djangorestframework; extra == "drf"
36
+ Requires-Dist: PyJWT; extra == "drf"
37
+ Provides-Extra: django-admin-auth
38
+ Requires-Dist: Django; extra == "django-admin-auth"
39
+ Requires-Dist: mozilla-django-oidc>=4.0.1; extra == "django-admin-auth"
32
40
  Provides-Extra: all
33
41
  Requires-Dist: Django; extra == "all"
34
- Requires-Dist: pandas>=1.4.0; extra == "all"
35
- Requires-Dist: django-model-utils>=4.2.0; extra == "all"
36
- Requires-Dist: djangorestframework; extra == "all"
37
- Requires-Dist: requests; extra == "all"
42
+ Provides-Extra: dev
43
+ Requires-Dist: pytest>=7.0; extra == "dev"
44
+ Requires-Dist: pytest-django>=4.5; extra == "dev"
45
+ Requires-Dist: coverage>=6.0; extra == "dev"
46
+ Requires-Dist: tox>=3.25; extra == "dev"
38
47
  Dynamic: license-file
39
48
 
40
49
  ============================
@@ -50,9 +59,7 @@ Main utils:
50
59
  * data_structures
51
60
  * db
52
61
  * django
53
- * django_rest
54
62
  * math
55
- * pandas
56
63
  * exception
57
64
  * choices
58
65
 
@@ -64,7 +71,7 @@ Quick start
64
71
  from python_utils.time import date_range
65
72
  date_range(start_date, end_date)
66
73
 
67
- Although the library provides some utility functions related to other libraries like django and pandas, it does not install any dependencies automatically.
74
+ Although the library provides some utility functions related to other libraries like django, it does not install any dependencies automatically.
68
75
  This means, you can install the library even if you do not use these libraries, but keep in mind that in this case you cannot use the
69
76
  functions that depend on them.
70
77
 
@@ -74,7 +81,7 @@ You can also chose to install the dependencies alongside the library, including
74
81
 
75
82
  Tests
76
83
  -----
77
- The library has a 100% coverage by tests. If you want to see tests in action:
84
+ The library has a high coverage by tests. If you want to see tests in action:
78
85
 
79
86
  1. Inside venv, run ``pip install -r tests/requirements.txt``
80
87
 
@@ -1,8 +1,7 @@
1
1
  LICENSE
2
2
  MANIFEST.in
3
3
  README.rst
4
- setup.cfg
5
- setup.py
4
+ pyproject.toml
6
5
  cardo_python_utils.egg-info/PKG-INFO
7
6
  cardo_python_utils.egg-info/SOURCES.txt
8
7
  cardo_python_utils.egg-info/dependency_links.txt
@@ -12,13 +11,15 @@ python_utils/__init__.py
12
11
  python_utils/choices.py
13
12
  python_utils/data_structures.py
14
13
  python_utils/db.py
15
- python_utils/django_utils.py
16
14
  python_utils/esma_choices.py
17
15
  python_utils/exceptions.py
18
16
  python_utils/imports.py
19
17
  python_utils/math.py
20
- python_utils/pandas_utils.py
21
- python_utils/rest.py
22
18
  python_utils/text.py
23
19
  python_utils/time.py
24
- python_utils/types_hinting.py
20
+ python_utils/types_hinting.py
21
+ python_utils/django/utils.py
22
+ python_utils/django/auth/__init__.py
23
+ python_utils/django/auth/admin.py
24
+ python_utils/django/auth/drf.py
25
+ python_utils/django/auth/ninja.py
@@ -0,0 +1,26 @@
1
+
2
+ [all]
3
+ Django
4
+
5
+ [dev]
6
+ pytest>=7.0
7
+ pytest-django>=4.5
8
+ coverage>=6.0
9
+ tox>=3.25
10
+
11
+ [django]
12
+ Django
13
+
14
+ [django-admin-auth]
15
+ Django
16
+ mozilla-django-oidc>=4.0.1
17
+
18
+ [django-ninja]
19
+ Django
20
+ django-ninja
21
+ PyJWT
22
+
23
+ [drf]
24
+ Django
25
+ djangorestframework
26
+ PyJWT
@@ -0,0 +1,81 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "cardo-python-utils"
7
+ version = "0.5.dev0"
8
+ description = "Python library enhanced with a wide range of functions for different scenarios."
9
+ readme = "README.rst"
10
+ requires-python = ">=3.8"
11
+ license = {text = "MIT"}
12
+ authors = [
13
+ {name = "Kristi Kotini", email = "hello@cardoai.com"},
14
+ {name = "Klajdi Çaushi", email = "hello@cardoai.com"}
15
+ ]
16
+ keywords = ["utilities", "helpers", "django"]
17
+ classifiers = [
18
+ "Environment :: Web Environment",
19
+ "Framework :: Django",
20
+ "Intended Audience :: Developers",
21
+ "License :: OSI Approved :: MIT License",
22
+ "Operating System :: OS Independent",
23
+ "Programming Language :: Python",
24
+ "Programming Language :: Python :: 3",
25
+ "Programming Language :: Python :: 3 :: Only",
26
+ "Programming Language :: Python :: 3.10",
27
+ "Programming Language :: Python :: 3.11",
28
+ "Programming Language :: Python :: 3.12",
29
+ "Topic :: Internet :: WWW/HTTP",
30
+ "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
31
+ ]
32
+ dependencies = []
33
+
34
+ [project.optional-dependencies]
35
+ django = [
36
+ "Django",
37
+ ]
38
+ django-ninja = [
39
+ "Django",
40
+ "django-ninja",
41
+ "PyJWT",
42
+ ]
43
+ drf = [
44
+ "Django",
45
+ "djangorestframework",
46
+ "PyJWT",
47
+ ]
48
+ django-admin-auth = [
49
+ "Django",
50
+ "mozilla-django-oidc>=4.0.1",
51
+ ]
52
+ all = [
53
+ "Django",
54
+ ]
55
+ dev = [
56
+ "pytest>=7.0",
57
+ "pytest-django>=4.5",
58
+ "coverage>=6.0",
59
+ "tox>=3.25",
60
+ ]
61
+
62
+ [project.urls]
63
+ Homepage = "https://github.com/CardoAI/cardo-python-utils"
64
+ Repository = "https://github.com/CardoAI/cardo-python-utils.git"
65
+ Issues = "https://github.com/CardoAI/cardo-python-utils/issues"
66
+
67
+ [tool.setuptools]
68
+ include-package-data = true
69
+
70
+ [tool.setuptools.packages.find]
71
+ where = ["."]
72
+ exclude = ["tests", "tests.*"]
73
+
74
+ [tool.pytest.ini_options]
75
+ DJANGO_SETTINGS_MODULE = "tests.settings"
76
+ django_find_project = false
77
+ python_files = ["tests.py", "test_*.py", "*_tests.py"]
78
+ addopts = "--strict-markers --no-migrations --reuse-db --log-cli-level=INFO --doctest-modules"
79
+
80
+ [tool.tox]
81
+ legacy_tox_ini = "tox.ini"
@@ -76,7 +76,7 @@ class ChoiceEnum(Enum, metaclass=ChoiceEnumMeta):
76
76
 
77
77
  @classmethod
78
78
  def get_by_value(cls, value: str | int):
79
- value_index = 0 if type(value) == int else 1
79
+ value_index = 0 if isinstance(value, int) else 1
80
80
  return next((v for v in cls.__members__.values() if v.value[value_index] == value), None)
81
81
 
82
82
  @classmethod
@@ -0,0 +1,49 @@
1
+ from django.conf import settings
2
+ from mozilla_django_oidc.auth import OIDCAuthenticationBackend
3
+
4
+
5
+ class OIDCCustomAuthenticationBackend(OIDCAuthenticationBackend):
6
+ def _get_user_data(self, claims) -> dict:
7
+ client_roles = (
8
+ claims.get("resource_access", {})
9
+ .get(getattr(settings, "OIDC_RP_CLIENT_ID", ""), {})
10
+ .get("roles", [])
11
+ )
12
+ is_superuser = "Admin" in client_roles
13
+
14
+ return {
15
+ "username": claims.get("preferred_username"),
16
+ "email": claims.get("email"),
17
+ "first_name": claims.get("given_name", ""),
18
+ "last_name": claims.get("family_name", ""),
19
+ "is_staff": claims.get("is_staff", False),
20
+ "is_superuser": is_superuser,
21
+ }
22
+
23
+ def filter_users_by_claims(self, claims):
24
+ username = claims.get("preferred_username")
25
+ if not username:
26
+ return self.UserModel.objects.none()
27
+ return self.UserModel.objects.filter(username=username)
28
+
29
+ def create_user(self, claims):
30
+ return self.UserModel.objects.create_user(**self._get_user_data(claims))
31
+
32
+ def update_user(self, user, claims):
33
+ save_needed = False
34
+
35
+ for attr, value in self._get_user_data(claims).items():
36
+ if getattr(user, attr) != value:
37
+ setattr(user, attr, value)
38
+ save_needed = True
39
+
40
+ if save_needed:
41
+ user.save()
42
+
43
+ return user
44
+
45
+
46
+ def has_permission(request):
47
+ # The user does not need to be staff to access the admin site
48
+ # Only superusers will have access to do anything in the admin site
49
+ return request.user.is_active and request.user.is_superuser
@@ -0,0 +1,117 @@
1
+ import jwt
2
+
3
+ from django.conf import settings
4
+ from django.contrib.auth import get_user_model
5
+ from jwt import PyJWKClient
6
+ from jwt.exceptions import InvalidTokenError
7
+
8
+ from rest_framework import authentication
9
+ from rest_framework.exceptions import AuthenticationFailed
10
+ from rest_framework.permissions import BasePermission
11
+
12
+
13
+ jwks_client = PyJWKClient(getattr(settings, "JWKS_URL", ""))
14
+
15
+
16
+ class AuthenticationBackend(authentication.TokenAuthentication):
17
+ keyword = "Bearer"
18
+
19
+ def authenticate_credentials(self, token: str):
20
+ signing_key = jwks_client.get_signing_key_from_jwt(token)
21
+
22
+ try:
23
+ payload = jwt.decode(
24
+ token,
25
+ signing_key.key,
26
+ algorithms=["RS256"],
27
+ audience=getattr(settings, "JWT_AUDIENCE", None),
28
+ )
29
+ except InvalidTokenError as e:
30
+ raise AuthenticationFailed(f"Invalid token: {str(e)}") from e
31
+
32
+ try:
33
+ username = payload["preferred_username"]
34
+ except KeyError as e:
35
+ raise AuthenticationFailed(
36
+ "Invalid token: preferred_username not present."
37
+ ) from e
38
+
39
+ user = self._get_user(username, payload)
40
+ return user, payload
41
+
42
+ def _get_user(self, username: str, payload: dict):
43
+ """
44
+ Get or create a user based on the JWT payload.
45
+ If the user exists, update their details.
46
+ """
47
+ user_model = get_user_model()
48
+ user_data = {
49
+ "first_name": payload.get("given_name") or "",
50
+ "last_name": payload.get("family_name") or "",
51
+ "email": payload.get("email") or "",
52
+ "is_staff": payload.get("is_staff", False),
53
+ }
54
+ if hasattr(user_model, "is_demo"):
55
+ user_data["is_demo"] = payload.get("is_demo", False)
56
+
57
+ user = user_model.objects.filter(username=username).first()
58
+ if user:
59
+ update_needed = False
60
+
61
+ for field, value in user_data.items():
62
+ if getattr(user, field) != value:
63
+ setattr(user, field, value)
64
+ update_needed = True
65
+
66
+ if update_needed:
67
+ user.save(update_fields=list(user_data.keys()))
68
+
69
+ return user
70
+ else:
71
+ return user_model.objects.create(
72
+ username=username,
73
+ **user_data,
74
+ )
75
+
76
+
77
+ class HasScope(BasePermission):
78
+ """
79
+ Permission class to check for allowed scopes in the access token.
80
+
81
+ This permission class checks if any of the scopes defined in the `allowed_scopes`
82
+ attribute of the view are present in the 'scope' claim of the access token.
83
+
84
+ Example Usage in a View:
85
+
86
+ class MyApiView(APIView):
87
+ permission_classes = [IsAuthenticated, HasScope]
88
+ allowed_scopes = ["jobs"]
89
+ ...
90
+
91
+ If no particular scope is required, you can set `allowed_scopes = "*"`
92
+ to allow access without scope checks.
93
+ """
94
+
95
+ def has_permission(self, request, view):
96
+ allowed_scopes = getattr(view, "allowed_scopes", [])
97
+
98
+ if not allowed_scopes:
99
+ raise Exception(
100
+ f"No allowed_scopes defined on the view '{view.__class__.__name__}'. "
101
+ "Define allowed_scopes or set it to '*' to allow any scope."
102
+ )
103
+
104
+ if allowed_scopes == "*":
105
+ return True
106
+
107
+ if not request.auth or "scope" not in request.auth:
108
+ return False
109
+
110
+ token_scopes_str = request.auth.get("scope", "")
111
+ token_scopes = set(token_scopes_str.split())
112
+
113
+ for scope in allowed_scopes:
114
+ if f"{getattr(settings, 'JWT_SCOPE_PREFIX', '')}:{scope}" in token_scopes:
115
+ return True
116
+
117
+ return False
@@ -0,0 +1,128 @@
1
+ from typing import Literal
2
+
3
+ from jwt import PyJWKClient, decode as jwt_decode
4
+ from jwt.exceptions import InvalidTokenError
5
+
6
+ from django.conf import settings
7
+ from django.contrib.auth import get_user_model
8
+ from ninja.security import HttpBearer
9
+ from ninja.errors import AuthenticationError, HttpError
10
+
11
+ jwks_client = PyJWKClient(getattr(settings, "JWKS_URL", ""))
12
+
13
+
14
+ class AuthBearer(HttpBearer):
15
+ def authenticate(self, request, token):
16
+ signing_key = jwks_client.get_signing_key_from_jwt(token)
17
+
18
+ try:
19
+ payload = jwt_decode(
20
+ token,
21
+ signing_key.key,
22
+ algorithms=["RS256"],
23
+ audience=getattr(settings, "JWT_AUDIENCE", None),
24
+ )
25
+ except InvalidTokenError as e:
26
+ raise AuthenticationError(f"Invalid token: {str(e)}") from e
27
+
28
+ try:
29
+ username = payload["preferred_username"]
30
+ except KeyError as e:
31
+ raise AuthenticationError(
32
+ "Invalid token: preferred_username not present."
33
+ ) from e
34
+
35
+ user = self._get_user(username, payload)
36
+
37
+ self._verify_scopes(request, payload)
38
+
39
+ return user
40
+
41
+ def _get_user(self, username: str, payload: dict):
42
+ """
43
+ Get or create a user based on the JWT payload.
44
+ If the user exists, update their details.
45
+ """
46
+ user_model = get_user_model()
47
+ user_data = {
48
+ "first_name": payload.get("given_name") or "",
49
+ "last_name": payload.get("family_name") or "",
50
+ "email": payload.get("email") or "",
51
+ "is_staff": payload.get("is_staff", False),
52
+ }
53
+ if hasattr(user_model, "is_demo"):
54
+ user_data["is_demo"] = payload.get("is_demo", False)
55
+
56
+ user = user_model.objects.filter(username=username).first()
57
+ if user:
58
+ update_needed = False
59
+
60
+ for field, value in user_data.items():
61
+ if getattr(user, field) != value:
62
+ setattr(user, field, value)
63
+ update_needed = True
64
+
65
+ if update_needed:
66
+ user.save(update_fields=list(user_data.keys()))
67
+
68
+ return user
69
+ else:
70
+ return user_model.objects.create(
71
+ username=username,
72
+ **user_data,
73
+ )
74
+
75
+ def _verify_scopes(self, request, token_payload):
76
+ allowed_scopes = self._get_view_allowed_scopes(request)
77
+
78
+ if allowed_scopes == "*":
79
+ return
80
+
81
+ token_scopes_str = token_payload.get("scope", "")
82
+ token_scopes = set(token_scopes_str.split())
83
+
84
+ for scope in allowed_scopes:
85
+ if f"{getattr(settings, 'JWT_SCOPE_PREFIX', '')}:{scope}" in token_scopes:
86
+ return
87
+
88
+ raise HttpError(403, "You are not allowed to access this resource.")
89
+
90
+ def _get_view_allowed_scopes(self, request):
91
+ view_function = self._get_view_function(request)
92
+ scopes = getattr(view_function, "_allowed_scopes", None)
93
+
94
+ if scopes is None:
95
+ raise Exception(
96
+ f"No allowed_scopes defined on the view {view_function.__name__}."
97
+ "Add the decorator @allowed_scopes([...]) or @allowed_scopes('*') to the view."
98
+ )
99
+
100
+ return scopes
101
+
102
+ def _get_view_function(self, request):
103
+ view_func = request.resolver_match.func.__self__
104
+ method = request.method.upper()
105
+ for operation in view_func.operations:
106
+ if operation.methods and method in operation.methods:
107
+ return operation.view_func
108
+
109
+ raise Exception(
110
+ f"Could not determine the view function for {request.method} {request.path}."
111
+ )
112
+
113
+
114
+ def allowed_scopes(scopes: list[str] | Literal["*"]):
115
+ """
116
+ A decorator that attaches a list of required scopes to a view function
117
+ in the attribute `_allowed_scopes`.
118
+ This is used by a global authenticator to perform authorization checks.
119
+ """
120
+
121
+ def decorator(view_func):
122
+ if not isinstance(scopes, list) and scopes != "*":
123
+ raise ValueError("scopes must be a list of strings or '*'")
124
+
125
+ setattr(view_func, "_allowed_scopes", scopes)
126
+ return view_func
127
+
128
+ return decorator
@@ -64,25 +64,6 @@ def record_to_dict(record: Model_T, exclude: List = None) -> Dict:
64
64
  return initial_data
65
65
 
66
66
 
67
- def get_choices_list_and_dict(choices) -> Tuple[List, Dict]:
68
- """
69
- From a Choices object return a list and dict of these choices.
70
- Args:
71
- choices: Choices object to get the representations from
72
- Returns:
73
- tuple of list with human_value and dict-> {human_value: value}
74
- Examples:
75
- >>> from model_utils import Choices
76
- >>> get_choices_list_and_dict(Choices((1, 'hello', 'HELLO')))
77
- ([('HELLO', 'HELLO')], {'HELLO': 1})
78
- """
79
- import_optional_dependency('model_utils', dependency='django-model-utils')
80
-
81
- choices_list = [(human_value, human_value) for _, human_value in choices]
82
- choices_dict = {human_value: value for value, human_value in choices}
83
- return choices_list, choices_dict
84
-
85
-
86
67
  def perform_query(
87
68
  sql_query: str,
88
69
  params: Optional[List] = None,
@@ -236,25 +217,6 @@ def update_record(record: Model_T, save=True, **data) -> Model_T:
236
217
  return record
237
218
 
238
219
 
239
- def get_choice_value(choices, human_string: str) -> Optional[Union[int, str]]:
240
- """
241
- Get database representation of a choice for a human-readable value
242
-
243
- Args:
244
- choices: model_utils Choice object to get the db value from
245
- human_string: string representing the human-readable value
246
- Returns:
247
- db representation of choice field
248
- """
249
-
250
- import_optional_dependency('model_utils', dependency='django-model-utils')
251
-
252
- for value, representation in choices:
253
- if representation == human_string:
254
- return value
255
- return None
256
-
257
-
258
220
  def get_or_none(records: models.QuerySet, *args, **kwargs) -> Optional[Model_T]:
259
221
  """
260
222
  Wrapper around queryset.get to return None if no record is found
@@ -1,6 +1,5 @@
1
1
  import hashlib
2
2
  import re
3
- from decimal import Decimal
4
3
  from typing import Optional, List, Any
5
4
 
6
5
  from python_utils.types_hinting import NumberIFD
@@ -19,6 +18,7 @@ def format_percent(value: Optional[NumberIFD]):
19
18
  '1300%'
20
19
  >>> format_percent(0.13)
21
20
  '13.0%'
21
+ >>> from decimal import Decimal
22
22
  >>> format_percent(Decimal('0.123456'))
23
23
  '12.35%'
24
24
  """
@@ -37,6 +37,7 @@ def format_currency(value: Optional[NumberIFD], symbol: str) -> Optional[str]:
37
37
  Examples:
38
38
  >>> format_currency(120.1234, 'EUR')
39
39
  'EUR120.12'
40
+ >>> from decimal import Decimal
40
41
  >>> format_currency(Decimal('120.1234'), 'EUR')
41
42
  'EUR120.12'
42
43
  >>> format_currency(None, 'EUR') is None
@@ -63,7 +64,7 @@ def human_string(text: str) -> str:
63
64
  """
64
65
  if not text:
65
66
  return ""
66
- return ' '.join(word.title() for word in text.split('_'))
67
+ return " ".join(word.title() for word in text.split("_"))
67
68
 
68
69
 
69
70
  camel_case_pattern = re.compile(r"(?<!^)(?=[A-Z])")
@@ -141,5 +142,5 @@ def create_hash(data: List[Any]) -> str:
141
142
  >>> create_hash(['test', 'hash', 1])
142
143
  '5ca1b365312e58a36bb985fc770a490b'
143
144
  """
144
- merged_data = '-'.join([str(rec) for rec in data])
145
- return hashlib.md5(merged_data.encode('utf-8')).hexdigest()
145
+ merged_data = "-".join([str(rec) for rec in data])
146
+ return hashlib.md5(merged_data.encode("utf-8")).hexdigest()
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -1,18 +0,0 @@
1
-
2
- [all]
3
- Django
4
- pandas>=1.4.0
5
- django-model-utils>=4.2.0
6
- djangorestframework
7
- requests
8
-
9
- [django]
10
- Django
11
- django-model-utils==4.2.0
12
-
13
- [pandas]
14
- pandas>=1.4.0
15
-
16
- [rest]
17
- djangorestframework
18
- requests
@@ -1,143 +0,0 @@
1
- from datetime import date, datetime
2
- from typing import List, Dict, Union, Literal
3
-
4
- import pandas as pd
5
-
6
- from python_utils.time import get_date, last_day_of_month
7
-
8
-
9
- def unique_not_null_values(df: pd.DataFrame, column: str) -> List:
10
- """
11
- Get a list with unique values from the values of a dataframe column
12
- 1. Filter the df to keep rows with notnull values in the given column
13
- 2. Return a list with all the values of the column appearing only once
14
-
15
- Args:
16
- df: dataframe object we are getting the unique values from
17
- column: the column name on which we are looking for the unique values
18
- Returns:
19
- list with all unique values
20
- Examples:
21
- >>> test_df = pd.DataFrame({'col1': [1, None, 1, 3, 2], 'col2': [3, 7, 4, 1, 3]})
22
- >>> unique_not_null_values(test_df, 'col1')
23
- [1.0, 3.0, 2.0]
24
- """
25
- return df[df[column].notnull()][column].unique().tolist()
26
-
27
-
28
- def rename_and_replace_column_information(df: pd.DataFrame, data: Dict) -> pd.DataFrame:
29
- """
30
- We use this mostly when we serialize the data from postgres.
31
- It replaces the column values with a dictionary and also renames the column names.
32
- Performs a rename of the columns
33
-
34
- Args:
35
- df: Pandas Data Frame
36
- data: data that maps columns and replacement/mapping as in example:
37
- {
38
- 'column_in_pandas': {
39
- 'replace_to': 'new_column_name',
40
- 'map': {
41
- 1: 2,
42
- 2: 3
43
- }
44
- }
45
- Returns:
46
- New Modified Pandas Dataframe.
47
- Examples:
48
- >>> test_df = pd.DataFrame({'col1': ['a', 'b'], 'col2': [1, 2]})
49
- >>> rename_and_replace_column_information(test_df, {'col1': {'replace_to': 'new', 'map': {'a': 1}}})
50
- new col2
51
- 0 1 1
52
- 1 b 2
53
- >>> test_df = pd.DataFrame({'col1': ['a', 'b'], 'col2': [1, 2]})
54
- >>> rename_and_replace_column_information(test_df, {})
55
- col1 col2
56
- 0 a 1
57
- 1 b 2
58
- """
59
- map_data = {key: value.get('map') for key, value in data.items()}
60
- if map_data:
61
- df.replace(map_data, inplace=True)
62
-
63
- rename_columns = {key: value.get('replace_to') for key, value in data.items()}
64
- if rename_columns:
65
- df.rename(columns=rename_columns, inplace=True)
66
- return df
67
-
68
-
69
- def get_dates_between(
70
- start_date: Union[date, datetime, str],
71
- end_date: Union[date, datetime, str],
72
- inclusive: Literal["both", "neither", "left", "right"] = "both"
73
- ) -> List[date]:
74
- """
75
- 1. Give start and end dates.
76
- 2. Set inclusive if you want, by default the value is both
77
- 3. Return list of dates
78
- Args:
79
- start_date (date, datetime, str): The left bound for the date generation by pandas
80
- end_date (date, datetime, str): The right bound for the date generation by pandas
81
- inclusive: Include boundaries; Whether to set each bound as closed or open
82
- Returns:
83
- list of date obj representing the dates in between the start and end
84
- Raises:
85
- ValueError: if one of passed dates is str and not well formatted to be converted to date obj
86
- Examples:
87
- >>> get_dates_between(date(2021, 2, 1), date(2021, 2, 3))
88
- [datetime.date(2021, 2, 1), datetime.date(2021, 2, 2), datetime.date(2021, 2, 3)]
89
- >>> get_dates_between(datetime(2021, 2, 1, 0, 0), datetime(2021, 2, 3, 0, 0))
90
- [datetime.date(2021, 2, 1), datetime.date(2021, 2, 2), datetime.date(2021, 2, 3)]
91
- >>> get_dates_between("2021-2-1", "2021-2-3")
92
- [datetime.date(2021, 2, 1), datetime.date(2021, 2, 2), datetime.date(2021, 2, 3)]
93
- >>> get_dates_between(date(2021, 2, 1), date(2021, 2, 3), inclusive="right")
94
- [datetime.date(2021, 2, 2), datetime.date(2021, 2, 3)]
95
- >>> get_dates_between("2021-2-1-1-1", "2021-3-1-1-1-1")
96
- Traceback (most recent call last):
97
- ...
98
- ValueError: Invalid date 2021-2-1-1-1
99
- """
100
- start_date = get_date(start_date, raise_error=True)
101
- end_date = get_date(end_date, raise_error=True)
102
- dates_range = pd.date_range(start=start_date, end=end_date, inclusive=inclusive).tolist()
103
- return [date(d.year, d.month, d.day) for d in dates_range]
104
-
105
-
106
- def get_months_between_dates(
107
- start_date: Union[date, datetime, str],
108
- end_date: Union[date, datetime, str],
109
- inclusive: Literal["both", "neither", "left", "right"] = "both"
110
- ) -> List[date]:
111
- """
112
- Get a timestamp for each month between the two dates.
113
- The start_date is appended because the pandas list begins from the next month
114
-
115
- Args:
116
- start_date (date, datetime, str): The left bound for the date generation by pandas
117
- end_date (date, datetime, str): The right bound for the date generation by pandas
118
- inclusive: Include boundaries; Whether to set each bound as closed or open
119
- Returns:
120
- list of date obj representing the end date of each month in between
121
- Raises:
122
- ValueError: if one of passed dates is str and not well formatted to be converted to date obj
123
- Examples:
124
- >>> get_months_between_dates(date(2021, 2, 1), date(2021, 3, 1))
125
- [datetime.date(2021, 2, 28), datetime.date(2021, 3, 31)]
126
- >>> get_months_between_dates(datetime(2021, 2, 1, 0, 0), datetime(2021, 3, 1, 1, 1))
127
- [datetime.date(2021, 2, 28), datetime.date(2021, 3, 31)]
128
- >>> get_months_between_dates("2021-2-1", "2021-3-1")
129
- [datetime.date(2021, 2, 28), datetime.date(2021, 3, 31)]
130
- >>> get_months_between_dates(date(2021, 2, 1), date(2021, 3, 1), inclusive="right")
131
- [datetime.date(2021, 3, 31)]
132
- >>> get_months_between_dates("2021-2-1-1-1", "2021-3-1-1-1-1")
133
- Traceback (most recent call last):
134
- ...
135
- ValueError: Invalid date 2021-2-1-1-1
136
- """
137
- start_date = get_date(start_date, raise_error=True)
138
- end_date = get_date(end_date, raise_error=True)
139
- dates_range = pd.date_range(start_date, end_date, freq="MS", inclusive=inclusive).to_list()
140
- return [
141
- last_day_of_month(year=cur_month.year, month=cur_month.month)
142
- for cur_month in dates_range
143
- ]
@@ -1,37 +0,0 @@
1
- from urllib.parse import urlencode
2
-
3
- from rest_framework.response import Response
4
- from rest_framework.status import HTTP_400_BAD_REQUEST, HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN, \
5
- HTTP_502_BAD_GATEWAY
6
-
7
-
8
- def bad_request(message="Bad request."): # pragma no cover
9
- return Response(data={"message": message}, status=HTTP_400_BAD_REQUEST)
10
-
11
-
12
- def unauthorized(message="Unauthorized."): # pragma no cover
13
- return Response(data={"message": message}, status=HTTP_401_UNAUTHORIZED)
14
-
15
-
16
- def forbidden(message="Forbidden."): # pragma no cover
17
- return Response(data={"message": message}, status=HTTP_403_FORBIDDEN)
18
-
19
-
20
- def bad_gateway(message="Bad Gateway."): # pragma no cover
21
- return Response(data={"message": message}, status=HTTP_502_BAD_GATEWAY)
22
-
23
-
24
- def add_params_to_url(url: str, **kwargs) -> str:
25
- """
26
- Join the url and params ready for making requests
27
-
28
- Args:
29
- url: string representing the url to be called
30
- **kwargs: all the params to join the url
31
- Returns:
32
- Final url ready for request
33
- Examples:
34
- >>> add_params_to_url('https://test.com', p1='20', p2=30, p3=None, p4='test', p5='<html/>')
35
- 'https://test.com?p1=20&p2=30&p3=None&p4=test&p5=%3Chtml%2F%3E'
36
- """
37
- return url if not kwargs else f"{url}?{urlencode(kwargs)}"
@@ -1,44 +0,0 @@
1
- [metadata]
2
- name = cardo-python-utils
3
- version = 0.4.2
4
- description = Python library enhanced with a wide range of functions for different scenarios.
5
- long_description = file: README.rst
6
- url = https://github.com/CardoAI/cardo-python-utils
7
- author = Kristi Kotini
8
- author_email = hello@cardoai.com
9
- license = MIT (X11)
10
- classifiers =
11
- Environment :: Web Environment
12
- Framework :: Django
13
- Intended Audience :: Developers
14
- License :: OSI Approved :: BSD License
15
- Operating System :: OS Independent
16
- Programming Language :: Python
17
- Programming Language :: Python :: 3
18
- Programming Language :: Python :: 3 :: Only
19
- Programming Language :: Python :: 3.8
20
- Programming Language :: Python :: 3.9
21
- Programming Language :: Python :: 3.10
22
- Topic :: Internet :: WWW/HTTP
23
- Topic :: Internet :: WWW/HTTP :: Dynamic Content
24
-
25
- [options]
26
- include_package_data = true
27
- packages = find:
28
- python_requires = >=3.8
29
-
30
- [options.extras_require]
31
- pandas = pandas>=1.4.0
32
- django = Django; django-model-utils==4.2.0
33
- rest = djangorestframework; requests
34
- all =
35
- Django
36
- pandas>=1.4.0
37
- django-model-utils>=4.2.0
38
- djangorestframework
39
- requests
40
-
41
- [egg_info]
42
- tag_build =
43
- tag_date = 0
44
-
@@ -1,7 +0,0 @@
1
- from setuptools import setup, find_packages
2
-
3
- setup(
4
- name="cardo-python-utils",
5
- packages=find_packages(exclude=["tests.*", "tests"]),
6
- include_package_data=True
7
- )