oxutils 0.1.0__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.
oxutils/exceptions.py ADDED
@@ -0,0 +1,117 @@
1
+ from django.utils.translation import gettext_lazy as _
2
+ from oxutils.mixins.base import DetailDictMixin
3
+
4
+
5
+
6
+ class OxException(Exception):
7
+ pass
8
+
9
+
10
+ try:
11
+ from ninja_extra import status, exceptions
12
+
13
+ STATUS_CODE = status.HTTP_500_INTERNAL_SERVER_ERROR
14
+ NinjaException = exceptions.APIException
15
+ except:
16
+ STATUS_CODE = 500
17
+ NinjaException = OxException
18
+
19
+
20
+ class ExceptionCode:
21
+ INTERNAL_ERROR = 'internal_error'
22
+ FAILED_ERROR = 'failed_error'
23
+ VALIDATION_ERROR = 'validation_error'
24
+ CONFLICT_ERROR = 'conflict_error'
25
+ NOT_FOUND = 'not_found'
26
+ UNAUTHORIZED = 'unauthorized'
27
+ FORBIDDEN = 'forbidden'
28
+ BAD_REQUEST = 'bad_request'
29
+ AUTHENTICATION_FAILED = 'authentication_failed'
30
+ PERMISSION_DENIED = 'permission_denied'
31
+ METHOD_NOT_ALLOWED = 'method_not_allowed'
32
+ NOT_ACCEPTABLE = 'not_acceptable'
33
+ TIMEOUT = 'timeout'
34
+ RATE_LIMIT_EXCEEDED = 'rate_limit_exceeded'
35
+ SERVICE_UNAVAILABLE = 'service_unavailable'
36
+ INVALID_TOKEN = 'invalid_token'
37
+ INVALID_ORGANIZATION_TOKEN = "invalid_org_token"
38
+ EXPIRED_TOKEN = 'expired_token'
39
+ INVALID_CREDENTIALS = 'invalid_credentials'
40
+ ACCOUNT_DISABLED = 'account_disabled'
41
+ ACCOUNT_NOT_VERIFIED = 'account_not_verified'
42
+ DUPLICATE_ENTRY = 'duplicate_entry'
43
+ RESOURCE_LOCKED = 'resource_locked'
44
+ PAYMENT_REQUIRED = 'payment_required'
45
+ INSUFFICIENT_PERMISSIONS = 'insufficient_permissions'
46
+ QUOTA_EXCEEDED = 'quota_exceeded'
47
+ INVALID_INPUT = 'invalid_input'
48
+ MISSING_PARAMETER = 'missing_parameter'
49
+ INVALID_PARAMETER = 'invalid_parameter'
50
+ OPERATION_NOT_PERMITTED = 'operation_not_permitted'
51
+ RESOURCE_EXHAUSTED = 'resource_exhausted'
52
+ PRECONDITION_FAILED = 'precondition_failed'
53
+
54
+ # Non Error
55
+ SUCCESS = 'success'
56
+
57
+
58
+
59
+ class APIException(DetailDictMixin, NinjaException):
60
+ status_code = STATUS_CODE
61
+ default_code = ExceptionCode.INTERNAL_ERROR
62
+ default_detail = _('We encountered an error, please try again later.')
63
+
64
+
65
+ class NotFoundException(APIException):
66
+ status_code = 404
67
+ default_code = ExceptionCode.NOT_FOUND
68
+ default_detail = _('The requested resource does not exist')
69
+
70
+
71
+ class ValidationException(APIException):
72
+ status_code = 400
73
+ default_code = ExceptionCode.VALIDATION_ERROR
74
+ default_detail = _('Validation error')
75
+
76
+
77
+ class ConflictException(APIException):
78
+ status_code = 409
79
+ default_code = ExceptionCode.CONFLICT_ERROR
80
+ default_detail = _('The operation conflicts with existing data')
81
+
82
+
83
+ class DuplicateEntryException(APIException):
84
+ status_code = 409
85
+ default_code = ExceptionCode.DUPLICATE_ENTRY
86
+ default_detail = _('A resource with these details already exists')
87
+
88
+
89
+ class PermissionDeniedException(APIException):
90
+ status_code = 403
91
+ default_code = ExceptionCode.PERMISSION_DENIED
92
+ default_detail = _('You do not have permission to perform this action')
93
+
94
+
95
+ class UnauthorizedException(APIException):
96
+ status_code = 401
97
+ default_code = ExceptionCode.UNAUTHORIZED
98
+ default_detail = _('Authentication is required')
99
+
100
+
101
+ class InvalidParameterException(APIException):
102
+ status_code = 400
103
+ default_code = ExceptionCode.INVALID_PARAMETER
104
+ default_detail = _('Invalid parameter provided')
105
+
106
+
107
+ class MissingParameterException(APIException):
108
+ status_code = 400
109
+ default_code = ExceptionCode.MISSING_PARAMETER
110
+ default_detail = _('Required parameter is missing')
111
+
112
+
113
+ class InternalErrorException(APIException):
114
+ status_code = 500
115
+ default_code = ExceptionCode.INTERNAL_ERROR
116
+ default_detail = _('An unexpected error occurred while processing your request')
117
+
oxutils/functions.py ADDED
@@ -0,0 +1,99 @@
1
+ from django.conf import settings
2
+ from urllib.parse import urljoin
3
+ from ninja.files import UploadedFile
4
+ from ninja_extra.exceptions import ValidationError
5
+
6
+
7
+
8
+ def get_absolute_url(url: str, request=None):
9
+ if request:
10
+ # Build absolute URL using request
11
+ return request.build_absolute_uri(url)
12
+ else:
13
+ # Fallback: build URL using MEDIA_URL and domain
14
+ base_url = getattr(settings, 'SITE_URL', 'http://localhost:8000')
15
+ return urljoin(base_url, url)
16
+
17
+
18
+ def request_is_bound(request):
19
+ """
20
+ Check if a request is bound (has data), similar to Django Form.is_bound.
21
+ """
22
+
23
+ if not request or not hasattr(request, 'method'):
24
+ return False
25
+
26
+ if hasattr(request, 'data'):
27
+ return (
28
+ request.data is not None or
29
+ bool(getattr(request, 'FILES', {})) or
30
+ request.method in ('POST', 'PUT', 'PATCH')
31
+ )
32
+
33
+ elif hasattr(request, 'POST'):
34
+ return (
35
+ len(getattr(request, 'POST', {})) > 0 or
36
+ len(getattr(request, 'FILES', {})) > 0 or
37
+ request.method in ('POST', 'PUT', 'PATCH')
38
+ )
39
+
40
+ return False
41
+
42
+
43
+ def get_request_data(request):
44
+ """
45
+ Extract data from request, similar to how Django Forms get their data.
46
+
47
+ Args:
48
+ request: Django HttpRequest or DRF Request object
49
+
50
+ Returns:
51
+ dict: Request data (empty dict if no data)
52
+ """
53
+ if not request:
54
+ return {}
55
+
56
+ # DRF Request
57
+ if hasattr(request, 'data'):
58
+ return request.data or {}
59
+
60
+ # Django HttpRequest
61
+ elif hasattr(request, 'POST'):
62
+ return dict(request.POST) if request.POST else {}
63
+
64
+ return {}
65
+
66
+
67
+ def validate_image(image: UploadedFile, size: int = 2):
68
+ """Validate uploaded image file"""
69
+ # Check file size (2MB = 2 * 1024 * 1024 bytes)
70
+ MAX_FILE_SIZE = size * 1024 * 1024 # 2MB
71
+ if image.size > MAX_FILE_SIZE:
72
+ raise ValidationError(f"La taille du fichier ne peut pas dépasser 2MB. Taille actuelle: {image.size / (1024 * 1024):.1f}MB")
73
+
74
+ # Check file type
75
+ ALLOWED_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp']
76
+ if image.content_type not in ALLOWED_TYPES:
77
+ raise ValidationError(f"Type de fichier non supporté. Types autorisés: {', '.join(ALLOWED_TYPES)}")
78
+
79
+ # Check file extension
80
+ ALLOWED_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp']
81
+ file_extension = image.name.lower().split('.')[-1] if '.' in image.name else ''
82
+ if f'.{file_extension}' not in ALLOWED_EXTENSIONS:
83
+ raise ValidationError(f"Extension de fichier non supportée. Extensions autorisées: {', '.join(ALLOWED_EXTENSIONS)}")
84
+
85
+ # Additional validation: check if file is actually an image
86
+ try:
87
+ from PIL import Image
88
+ import io
89
+
90
+ # Reset file pointer to beginning
91
+ image.seek(0)
92
+ image = Image.open(io.BytesIO(image.read()))
93
+ image.verify() # Verify it's a valid image
94
+
95
+ # Reset file pointer again for later use
96
+ image.seek(0)
97
+
98
+ except Exception as e:
99
+ raise ValidationError("Le fichier n'est pas une image valide")
File without changes
oxutils/jwt/auth.py ADDED
@@ -0,0 +1,55 @@
1
+ import os
2
+ from typing import Dict, Any, Optional
3
+ from jwcrypto import jwk
4
+ from django.core.exceptions import ImproperlyConfigured
5
+ from oxutils.settings import oxi_settings
6
+
7
+
8
+
9
+ _public_jwk_cache: Optional[jwk.JWK] = None
10
+
11
+
12
+
13
+ def get_jwks() -> Dict[str, Any]:
14
+ """
15
+ Get JSON Web Key Set (JWKS) for JWT verification.
16
+
17
+ Returns:
18
+ Dict containing the public JWK in JWKS format.
19
+
20
+ Raises:
21
+ ImproperlyConfigured: If jwt_verifying_key is not configured or file doesn't exist.
22
+ """
23
+ global _public_jwk_cache
24
+
25
+ if oxi_settings.jwt_verifying_key is None:
26
+ raise ImproperlyConfigured(
27
+ "JWT verifying key is not configured. Set OXI_JWT_VERIFYING_KEY environment variable."
28
+ )
29
+
30
+ key_path = oxi_settings.jwt_verifying_key
31
+
32
+ if not os.path.exists(key_path):
33
+ raise ImproperlyConfigured(
34
+ f"JWT verifying key file not found at: {key_path}"
35
+ )
36
+
37
+ if _public_jwk_cache is None:
38
+ try:
39
+ with open(key_path, 'r') as f:
40
+ key_data = f.read()
41
+
42
+ _public_jwk_cache = jwk.JWK.from_pem(key_data.encode('utf-8'))
43
+ _public_jwk_cache.update(kid='main')
44
+ except Exception as e:
45
+ raise ImproperlyConfigured(
46
+ f"Failed to load JWT verifying key from {key_path}: {str(e)}"
47
+ )
48
+
49
+ return {"keys": [_public_jwk_cache.export(as_dict=True)]}
50
+
51
+
52
+ def clear_jwk_cache() -> None:
53
+ """Clear the cached JWK. Useful for testing or key rotation."""
54
+ global _public_jwk_cache
55
+ _public_jwk_cache = None
oxutils/jwt/client.py ADDED
@@ -0,0 +1,123 @@
1
+ import jwt
2
+ import requests
3
+ from typing import Dict, Any, Optional
4
+ from datetime import datetime, timedelta
5
+ from django.core.exceptions import ImproperlyConfigured
6
+ from oxutils.settings import oxi_settings
7
+ from .constants import JWT_ALGORITHM
8
+
9
+
10
+ _jwks_cache: Optional[Dict[str, Any]] = None
11
+ _jwks_cache_time: Optional[datetime] = None
12
+ _jwks_cache_ttl = timedelta(hours=1)
13
+
14
+
15
+ def get_jwks_url() -> str:
16
+ """
17
+ Get JWKS URL from settings.
18
+
19
+ Returns:
20
+ The configured JWKS URL.
21
+
22
+ Raises:
23
+ ImproperlyConfigured: If jwt_jwks_url is not configured.
24
+ """
25
+ if not oxi_settings.jwt_jwks_url:
26
+ raise ImproperlyConfigured(
27
+ "JWT JWKS URL is not configured. Set OXI_JWT_JWKS_URL environment variable."
28
+ )
29
+ return oxi_settings.jwt_jwks_url
30
+
31
+
32
+ def fetch_jwks(force_refresh: bool = False) -> Dict[str, Any]:
33
+ """
34
+ Fetch JWKS from the authentication server with caching.
35
+
36
+ Args:
37
+ force_refresh: Force refresh the cache even if not expired.
38
+
39
+ Returns:
40
+ Dict containing the JWKS.
41
+
42
+ Raises:
43
+ ImproperlyConfigured: If JWKS cannot be fetched.
44
+ """
45
+ global _jwks_cache, _jwks_cache_time
46
+
47
+ now = datetime.now()
48
+
49
+ # Return cached JWKS if valid
50
+ if not force_refresh and _jwks_cache is not None and _jwks_cache_time is not None:
51
+ if now - _jwks_cache_time < _jwks_cache_ttl:
52
+ return _jwks_cache
53
+
54
+ # Fetch fresh JWKS
55
+ jwks_url = get_jwks_url()
56
+ try:
57
+ response = requests.get(jwks_url, timeout=10)
58
+ response.raise_for_status()
59
+ _jwks_cache = response.json()
60
+ _jwks_cache_time = now
61
+ return _jwks_cache
62
+ except requests.RequestException as e:
63
+ raise ImproperlyConfigured(
64
+ f"Failed to fetch JWKS from {jwks_url}: {str(e)}"
65
+ )
66
+
67
+
68
+ def get_key(kid: str):
69
+ """
70
+ Get the public key for a given Key ID (kid).
71
+
72
+ Args:
73
+ kid: The Key ID from the JWT header.
74
+
75
+ Returns:
76
+ RSA public key for verification.
77
+
78
+ Raises:
79
+ ValueError: If the kid is not found in JWKS.
80
+ """
81
+ jwks = fetch_jwks()
82
+
83
+ for key in jwks.get("keys", []):
84
+ if key.get("kid") == kid:
85
+ return jwt.algorithms.RSAAlgorithm.from_jwk(key)
86
+
87
+ raise ValueError(f"Unknown Key ID (kid): {kid}")
88
+
89
+
90
+ def verify_token(token: str) -> Dict[str, Any]:
91
+ """
92
+ Verify and decode a JWT token.
93
+
94
+ Args:
95
+ token: The JWT token string to verify.
96
+
97
+ Returns:
98
+ Dict containing the decoded token payload.
99
+
100
+ Raises:
101
+ jwt.InvalidTokenError: If token is invalid or expired.
102
+ ValueError: If kid is not found.
103
+ """
104
+ try:
105
+ headers = jwt.get_unverified_header(token)
106
+ kid = headers.get("kid")
107
+
108
+ if not kid:
109
+ raise ValueError("Token header missing 'kid' field")
110
+
111
+ key = get_key(kid)
112
+ return jwt.decode(token, key=key, algorithms=JWT_ALGORITHM)
113
+ except jwt.InvalidTokenError:
114
+ raise
115
+ except Exception as e:
116
+ raise jwt.InvalidTokenError(f"Token verification failed: {str(e)}")
117
+
118
+
119
+ def clear_jwks_cache() -> None:
120
+ """Clear the cached JWKS. Useful for testing or key rotation."""
121
+ global _jwks_cache, _jwks_cache_time
122
+ _jwks_cache = None
123
+ _jwks_cache_time = None
@@ -0,0 +1 @@
1
+ JWT_ALGORITHM = ["RS256"]
Binary file