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/py.typed ADDED
File without changes
oxutils/s3/__init__.py ADDED
File without changes
oxutils/s3/settings.py ADDED
@@ -0,0 +1,34 @@
1
+ import os
2
+
3
+ USE_S3 = os.getenv('USE_S3') == 'TRUE'
4
+
5
+ if USE_S3:
6
+ # aws settings
7
+ AWS_ACCESS_KEY_ID = os.getenv('AWS_ACCESS_KEY_ID')
8
+ AWS_SECRET_ACCESS_KEY = os.getenv('AWS_SECRET_ACCESS_KEY')
9
+ AWS_STORAGE_BUCKET_NAME = os.getenv('AWS_STORAGE_BUCKET_NAME')
10
+ AWS_DEFAULT_ACL = 'public-read'
11
+ AWS_S3_CUSTOM_DOMAIN = f'{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com'
12
+ AWS_S3_OBJECT_PARAMETERS = {'CacheControl': 'max-age=86400'}
13
+
14
+ # s3 static settings
15
+ STATIC_LOCATION = 'static'
16
+ STATIC_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{STATIC_LOCATION}/'
17
+ STATICFILES_STORAGE = 'oxutils.s3.storages.StaticStorage'
18
+
19
+ # s3 public media settings
20
+ PUBLIC_MEDIA_LOCATION = 'media'
21
+ MEDIA_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{PUBLIC_MEDIA_LOCATION}/'
22
+ DEFAULT_FILE_STORAGE = 'oxutils.s3.storages.PublicMediaStorage'
23
+
24
+ # s3 private media settings
25
+ PRIVATE_MEDIA_LOCATION = 'private'
26
+ PRIVATE_FILE_STORAGE = 'oxutils.s3.storages.PrivateMediaStorage'
27
+ else:
28
+ STATIC_URL = '/static/'
29
+ STATIC_ROOT = os.path.join(BASE_DIR, 'static')
30
+
31
+ STATICFILES_DIRS = (os.path.join(BASE_DIR, 'static'),)
32
+
33
+ MEDIA_URL = '/media/'
34
+ MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
oxutils/s3/storages.py ADDED
@@ -0,0 +1,130 @@
1
+ from storages.backends.s3boto3 import S3Boto3Storage
2
+ from oxutils.settings import oxi_settings
3
+ from django.core.exceptions import ImproperlyConfigured
4
+
5
+
6
+ class StaticStorage(S3Boto3Storage):
7
+ def __init__(self, *args, **kwargs):
8
+ if not oxi_settings.use_static_s3:
9
+ raise ImproperlyConfigured(
10
+ "StaticStorage requires OXI_USE_STATIC_S3=True"
11
+ )
12
+
13
+ self.access_key = oxi_settings.static_access_key_id
14
+ self.secret_key = oxi_settings.static_secret_access_key
15
+ self.bucket_name = oxi_settings.static_storage_bucket_name
16
+ self.custom_domain = oxi_settings.static_s3_custom_domain
17
+ self.location = oxi_settings.static_location
18
+ self.default_acl = oxi_settings.static_default_acl
19
+ self.file_overwrite = False
20
+
21
+ self._validate_required_fields('StaticStorage', {
22
+ 'access_key': self.access_key,
23
+ 'secret_key': self.secret_key,
24
+ 'bucket_name': self.bucket_name,
25
+ 'custom_domain': self.custom_domain,
26
+ })
27
+
28
+ super().__init__(*args, **kwargs)
29
+
30
+ @staticmethod
31
+ def _validate_required_fields(storage_name: str, fields: dict):
32
+ """Validate that all required fields are present."""
33
+ missing = [name for name, value in fields.items() if not value]
34
+ if missing:
35
+ raise ImproperlyConfigured(
36
+ f"{storage_name} is missing required configuration: {', '.join(missing)}"
37
+ )
38
+
39
+
40
+ class PublicMediaStorage(S3Boto3Storage):
41
+ def __init__(self, *args, **kwargs):
42
+ if not oxi_settings.use_default_s3:
43
+ raise ImproperlyConfigured(
44
+ "PublicMediaStorage requires OXI_USE_DEFAULT_S3=True"
45
+ )
46
+
47
+ if oxi_settings.use_static_s3_as_default:
48
+ self.access_key = oxi_settings.static_access_key_id
49
+ self.secret_key = oxi_settings.static_secret_access_key
50
+ self.bucket_name = oxi_settings.static_storage_bucket_name
51
+ self.custom_domain = oxi_settings.static_s3_custom_domain
52
+ else:
53
+ self.access_key = oxi_settings.default_s3_access_key_id
54
+ self.secret_key = oxi_settings.default_s3_secret_access_key
55
+ self.bucket_name = oxi_settings.default_s3_storage_bucket_name
56
+ self.custom_domain = oxi_settings.default_s3_s3_custom_domain
57
+
58
+ self.location = oxi_settings.default_s3_location
59
+ self.default_acl = oxi_settings.default_s3_default_acl
60
+ self.file_overwrite = False
61
+
62
+ StaticStorage._validate_required_fields('PublicMediaStorage', {
63
+ 'access_key': self.access_key,
64
+ 'secret_key': self.secret_key,
65
+ 'bucket_name': self.bucket_name,
66
+ 'custom_domain': self.custom_domain,
67
+ })
68
+
69
+ super().__init__(*args, **kwargs)
70
+
71
+
72
+ class PrivateMediaStorage(S3Boto3Storage):
73
+ def __init__(self, *args, **kwargs):
74
+ if not oxi_settings.use_private_s3:
75
+ raise ImproperlyConfigured(
76
+ "PrivateMediaStorage requires OXI_USE_PRIVATE_S3=True"
77
+ )
78
+
79
+ self.access_key = oxi_settings.private_s3_access_key_id
80
+ self.secret_key = oxi_settings.private_s3_secret_access_key
81
+ self.bucket_name = oxi_settings.private_s3_storage_bucket_name
82
+ self.custom_domain = oxi_settings.private_s3_s3_custom_domain
83
+ self.location = oxi_settings.private_s3_location
84
+ self.default_acl = oxi_settings.private_s3_default_acl
85
+ self.file_overwrite = False
86
+ self.querystring_auth = True
87
+ self.querystring_expire = 3600
88
+
89
+ StaticStorage._validate_required_fields('PrivateMediaStorage', {
90
+ 'access_key': self.access_key,
91
+ 'secret_key': self.secret_key,
92
+ 'bucket_name': self.bucket_name,
93
+ 'custom_domain': self.custom_domain,
94
+ })
95
+
96
+ super().__init__(*args, **kwargs)
97
+
98
+
99
+ class LogStorage(S3Boto3Storage):
100
+ def __init__(self, *args, **kwargs):
101
+ if not oxi_settings.use_log_s3:
102
+ raise ImproperlyConfigured(
103
+ "LogStorage requires OXI_USE_LOG_S3=True"
104
+ )
105
+
106
+ if oxi_settings.use_private_s3_as_log:
107
+ self.access_key = oxi_settings.private_s3_access_key_id
108
+ self.secret_key = oxi_settings.private_s3_secret_access_key
109
+ self.bucket_name = oxi_settings.private_s3_storage_bucket_name
110
+ self.custom_domain = oxi_settings.private_s3_s3_custom_domain
111
+ else:
112
+ self.access_key = oxi_settings.log_s3_access_key_id
113
+ self.secret_key = oxi_settings.log_s3_secret_access_key
114
+ self.bucket_name = oxi_settings.log_s3_storage_bucket_name
115
+ self.custom_domain = oxi_settings.log_s3_s3_custom_domain
116
+
117
+ self.location = f'{oxi_settings.log_s3_location}/{oxi_settings.service_name}'
118
+ self.default_acl = oxi_settings.log_s3_default_acl
119
+ self.file_overwrite = False
120
+ self.querystring_auth = True
121
+ self.querystring_expire = 3600
122
+
123
+ StaticStorage._validate_required_fields('LogStorage', {
124
+ 'access_key': self.access_key,
125
+ 'secret_key': self.secret_key,
126
+ 'bucket_name': self.bucket_name,
127
+ 'custom_domain': self.custom_domain,
128
+ })
129
+
130
+ super().__init__(*args, **kwargs)
oxutils/settings.py ADDED
@@ -0,0 +1,254 @@
1
+ from typing import Optional
2
+ from pydantic import Field, model_validator
3
+ from pydantic_settings import BaseSettings, SettingsConfigDict
4
+ from django.core.exceptions import ImproperlyConfigured
5
+
6
+
7
+
8
+
9
+ __all__ = [
10
+ 'oxi_settings',
11
+ ]
12
+
13
+
14
+
15
+ class OxUtilsSettings(BaseSettings):
16
+ model_config = SettingsConfigDict(
17
+ validate_assignment=True,
18
+ extra="ignore",
19
+ env_prefix='OXI_'
20
+ )
21
+
22
+ # Service
23
+ service_name: Optional[str] = 'Oxutils'
24
+
25
+ # Auth JWT Settings (JWT_SIGNING_KEY)
26
+ jwt_signing_key: Optional[str] = None
27
+ jwt_verifying_key: Optional[str] = None
28
+ jwt_jwks_url: Optional[str] = None
29
+ jwt_access_token_key: str = Field('access_token')
30
+ jwt_org_access_token_key: str = Field('org_access_token')
31
+
32
+
33
+ # AuditLog
34
+ log_access: bool = Field(False)
35
+ retention_delay: int = Field(7) # one week
36
+
37
+
38
+ # Static S3
39
+ use_static_s3: bool = Field(False)
40
+ static_access_key_id: Optional[str] = None
41
+ static_secret_access_key: Optional[str] = None
42
+ static_storage_bucket_name: Optional[str] = None
43
+ static_default_acl: str = Field('public-read')
44
+ static_s3_custom_domain: Optional[str] = None
45
+ static_location: str = Field('static')
46
+ static_storage: str = Field('oxutils.s3.storages.StaticStorage')
47
+
48
+ # Default S3 for media
49
+ use_default_s3: bool = Field(False)
50
+ use_static_s3_as_default: bool = Field(False)
51
+ default_s3_access_key_id: Optional[str] = None
52
+ default_s3_secret_access_key: Optional[str] = None
53
+ default_s3_storage_bucket_name: Optional[str] = None
54
+ default_s3_default_acl: str = Field('public-read')
55
+ default_s3_s3_custom_domain: Optional[str] = None
56
+ default_s3_location: str = Field('media')
57
+ default_s3_storage: str = Field('oxutils.s3.storages.PublicMediaStorage')
58
+
59
+ # Private S3 for sensible data
60
+ use_private_s3: bool = Field(False)
61
+ private_s3_access_key_id: Optional[str] = None
62
+ private_s3_secret_access_key: Optional[str] = None
63
+ private_s3_storage_bucket_name: Optional[str] = None
64
+ private_s3_default_acl: str = Field('private')
65
+ private_s3_s3_custom_domain: Optional[str] = None
66
+ private_s3_location: str = Field('private')
67
+ private_s3_storage: str = Field('oxutils.s3.storages.PrivateMediaStorage')
68
+
69
+ # Log S3
70
+ use_log_s3: bool = Field(False)
71
+ use_private_s3_as_log: bool = Field(False)
72
+ log_s3_access_key_id: Optional[str] = None
73
+ log_s3_secret_access_key: Optional[str] = None
74
+ log_s3_storage_bucket_name: Optional[str] = None
75
+ log_s3_default_acl: str = Field('private')
76
+ log_s3_s3_custom_domain: Optional[str] = None
77
+ log_s3_location: str = Field('oxi_logs')
78
+ log_s3_storage: str = Field('oxutils.s3.storages.LogStorage')
79
+
80
+
81
+ @model_validator(mode='after')
82
+ def validate_s3_configurations(self):
83
+ """Validate S3 and JWT configurations when enabled."""
84
+ # Validate JWT keys if present
85
+ self._validate_jwt_keys()
86
+
87
+ # Validate static S3
88
+ if self.use_static_s3:
89
+ self._validate_s3_config(
90
+ 'static',
91
+ self.static_access_key_id,
92
+ self.static_secret_access_key,
93
+ self.static_storage_bucket_name,
94
+ self.static_s3_custom_domain
95
+ )
96
+
97
+ # Validate default S3
98
+ if self.use_default_s3:
99
+ if not self.use_static_s3_as_default:
100
+ self._validate_s3_config(
101
+ 'default',
102
+ self.default_s3_access_key_id,
103
+ self.default_s3_secret_access_key,
104
+ self.default_s3_storage_bucket_name,
105
+ self.default_s3_s3_custom_domain
106
+ )
107
+ elif not self.use_static_s3:
108
+ raise ValueError(
109
+ "OXI_USE_STATIC_S3_AS_DEFAULT requires OXI_USE_STATIC_S3 to be True"
110
+ )
111
+
112
+ # Validate private S3
113
+ if self.use_private_s3:
114
+ self._validate_s3_config(
115
+ 'private',
116
+ self.private_s3_access_key_id,
117
+ self.private_s3_secret_access_key,
118
+ self.private_s3_storage_bucket_name,
119
+ self.private_s3_s3_custom_domain
120
+ )
121
+
122
+ # Validate log S3
123
+ if self.use_log_s3:
124
+ if not self.use_private_s3_as_log:
125
+ self._validate_s3_config(
126
+ 'log',
127
+ self.log_s3_access_key_id,
128
+ self.log_s3_secret_access_key,
129
+ self.log_s3_storage_bucket_name,
130
+ self.log_s3_s3_custom_domain
131
+ )
132
+ elif not self.use_private_s3:
133
+ raise ValueError(
134
+ "OXI_USE_PRIVATE_S3_AS_LOG requires OXI_USE_PRIVATE_S3 to be True"
135
+ )
136
+
137
+ return self
138
+
139
+ def _validate_jwt_keys(self):
140
+ """Validate JWT key files if configured."""
141
+ import os
142
+
143
+ if self.jwt_signing_key:
144
+ if not os.path.exists(self.jwt_signing_key):
145
+ raise ValueError(
146
+ f"JWT signing key file not found at: {self.jwt_signing_key}"
147
+ )
148
+ if not os.path.isfile(self.jwt_signing_key):
149
+ raise ValueError(
150
+ f"JWT signing key path is not a file: {self.jwt_signing_key}"
151
+ )
152
+
153
+ if self.jwt_verifying_key:
154
+ if not os.path.exists(self.jwt_verifying_key):
155
+ raise ValueError(
156
+ f"JWT verifying key file not found at: {self.jwt_verifying_key}"
157
+ )
158
+ if not os.path.isfile(self.jwt_verifying_key):
159
+ raise ValueError(
160
+ f"JWT verifying key path is not a file: {self.jwt_verifying_key}"
161
+ )
162
+
163
+ def _validate_s3_config(self, name: str, access_key: Optional[str],
164
+ secret_key: Optional[str], bucket: Optional[str],
165
+ domain: Optional[str]):
166
+ """Validate required S3 configuration fields."""
167
+ missing_fields = []
168
+ if not access_key:
169
+ missing_fields.append(f'OXI_{name.upper()}_ACCESS_KEY_ID')
170
+ if not secret_key:
171
+ missing_fields.append(f'OXI_{name.upper()}_SECRET_ACCESS_KEY')
172
+ if not bucket:
173
+ missing_fields.append(f'OXI_{name.upper()}_STORAGE_BUCKET_NAME')
174
+ if not domain:
175
+ missing_fields.append(f'OXI_{name.upper()}_S3_CUSTOM_DOMAIN')
176
+
177
+ if missing_fields:
178
+ raise ValueError(
179
+ f"Missing required {name} S3 configuration: {', '.join(missing_fields)}"
180
+ )
181
+
182
+ def get_static_storage_url(self) -> str:
183
+ """Get static storage URL."""
184
+ if not self.use_static_s3:
185
+ raise ImproperlyConfigured(
186
+ "Static S3 is not enabled. Set OXI_USE_STATIC_S3=True."
187
+ )
188
+ return f'https://{self.static_s3_custom_domain}/{self.static_location}/'
189
+
190
+ def get_default_storage_url(self) -> str:
191
+ """Get default storage URL."""
192
+ if self.use_default_s3:
193
+ if self.use_static_s3_as_default:
194
+ # Use static S3 credentials but keep default_s3 specific values (location, etc.)
195
+ domain = self.static_s3_custom_domain
196
+ else:
197
+ domain = self.default_s3_s3_custom_domain
198
+ return f'https://{domain}/{self.default_s3_location}/'
199
+
200
+ raise ImproperlyConfigured(
201
+ "Default S3 is not enabled. Set OXI_USE_DEFAULT_S3=True."
202
+ )
203
+
204
+ def get_private_storage_url(self) -> str:
205
+ """Get private storage URL."""
206
+ if not self.use_private_s3:
207
+ raise ImproperlyConfigured(
208
+ "Private S3 is not enabled. Set OXI_USE_PRIVATE_S3=True."
209
+ )
210
+ return f'https://{self.private_s3_s3_custom_domain}/{self.private_s3_location}/'
211
+
212
+ def get_log_storage_url(self) -> str:
213
+ """Get log storage URL."""
214
+ if not self.use_log_s3:
215
+ raise ImproperlyConfigured(
216
+ "Log S3 is not enabled. Set OXI_USE_LOG_S3=True."
217
+ )
218
+ if self.use_private_s3_as_log:
219
+ # Use private S3 credentials but keep log_s3 specific values (location, etc.)
220
+ domain = self.private_s3_s3_custom_domain
221
+ else:
222
+ domain = self.log_s3_s3_custom_domain
223
+ return f'https://{domain}/{self.log_s3_location}/{self.service_name}/'
224
+
225
+
226
+ def write_django_settings(self, django_settings_module):
227
+ """
228
+ Configure Django settings for S3 storages if enabled.
229
+
230
+ Sets:
231
+ 1. STATIC_URL & STATICFILES_STORAGE (if use_static_s3)
232
+ 2. MEDIA_URL & DEFAULT_FILE_STORAGE (if use_default_s3)
233
+ 3. PRIVATE_MEDIA_LOCATION & PRIVATE_FILE_STORAGE (if use_private_s3)
234
+
235
+ Args:
236
+ django_settings_module: The Django settings module to update.
237
+ """
238
+ # Configure static storage
239
+ if self.use_static_s3:
240
+ django_settings_module.STATIC_URL = self.get_static_storage_url()
241
+ django_settings_module.STATICFILES_STORAGE = self.static_storage
242
+
243
+ # Configure default/media storage
244
+ if self.use_default_s3:
245
+ django_settings_module.MEDIA_URL = self.get_default_storage_url()
246
+ django_settings_module.DEFAULT_FILE_STORAGE = self.default_s3_storage
247
+
248
+ # Configure private storage
249
+ if self.use_private_s3:
250
+ django_settings_module.PRIVATE_MEDIA_LOCATION = self.private_s3_location
251
+ django_settings_module.PRIVATE_FILE_STORAGE = self.private_s3_storage
252
+
253
+
254
+ oxi_settings = OxUtilsSettings()
oxutils/types.py ADDED
@@ -0,0 +1,13 @@
1
+ from typing import Annotated
2
+ from pydantic import BeforeValidator, EmailStr
3
+
4
+
5
+
6
+ def strip_and_lower(v: object) -> str:
7
+ return str(v).strip().lower()
8
+
9
+
10
+ EmailApiType = Annotated[
11
+ EmailStr,
12
+ BeforeValidator(strip_and_lower),
13
+ ]
@@ -0,0 +1,201 @@
1
+ Metadata-Version: 2.4
2
+ Name: oxutils
3
+ Version: 0.1.0
4
+ Summary: Production-ready utilities for Django applications in the Oxiliere ecosystem
5
+ Keywords: django,utilities,jwt,s3,audit,logging,celery,structlog
6
+ Author: Edimedia Mutoke
7
+ Author-email: Edimedia Mutoke <eddycondor07@gmail.com>
8
+ License-Expression: Apache-2.0
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Framework :: Django
11
+ Classifier: Framework :: Django :: 5.0
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: Apache Software License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: Topic :: Internet :: WWW/HTTP
20
+ Requires-Dist: boto3>=1.41.5
21
+ Requires-Dist: celery>=5.5.3
22
+ Requires-Dist: cryptography>=46.0.3
23
+ Requires-Dist: django-auditlog>=3.3.0
24
+ Requires-Dist: django-celery-results>=2.6.0
25
+ Requires-Dist: django-cid>=3.0
26
+ Requires-Dist: django-extensions>=4.1
27
+ Requires-Dist: django-ninja>=1.5.0
28
+ Requires-Dist: django-ninja-extra>=0.30.6
29
+ Requires-Dist: django-storages[s3]>=1.14.6
30
+ Requires-Dist: django-structlog[celery]>=10.0.0
31
+ Requires-Dist: jwcrypto>=1.5.6
32
+ Requires-Dist: pydantic-settings>=2.12.0
33
+ Requires-Dist: pyjwt>=2.10.1
34
+ Requires-Dist: requests>=2.32.5
35
+ Requires-Python: >=3.11
36
+ Project-URL: Changelog, https://github.com/oxiliere/oxutils/blob/main/CHANGELOG.md
37
+ Project-URL: Documentation, https://github.com/oxiliere/oxutils/tree/main/docs
38
+ Project-URL: Homepage, https://github.com/oxiliere/oxutils
39
+ Project-URL: Issues, https://github.com/oxiliere/oxutils/issues
40
+ Project-URL: Repository, https://github.com/oxiliere/oxutils
41
+ Description-Content-Type: text/markdown
42
+
43
+ # OxUtils
44
+
45
+ **Production-ready utilities for Django applications in the Oxiliere ecosystem.**
46
+
47
+ [![PyPI version](https://img.shields.io/pypi/v/oxutils.svg)](https://pypi.org/project/oxutils/)
48
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/)
49
+ [![Django 5.0+](https://img.shields.io/badge/django-5.0+-green.svg)](https://www.djangoproject.com/)
50
+ [![Tests](https://img.shields.io/badge/tests-126%20passed-success.svg)](tests/)
51
+ [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
52
+ [![Code style: ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)](https://github.com/astral-sh/ruff)
53
+
54
+ ## Features
55
+
56
+ - 🔐 **JWT Authentication** - RS256 with JWKS caching
57
+ - 📦 **S3 Storage** - Static, media, private, and log backends
58
+ - 📝 **Structured Logging** - JSON logs with correlation IDs
59
+ - 🔍 **Audit System** - Change tracking with S3 export
60
+ - ⚙️ **Celery Integration** - Pre-configured task processing
61
+ - 🛠️ **Django Mixins** - UUID, timestamps, user tracking
62
+ - ⚡ **Custom Exceptions** - Standardized API errors
63
+
64
+ ---
65
+
66
+ ## Installation
67
+
68
+ ```bash
69
+ pip install oxutils
70
+ ```
71
+
72
+ ```bash
73
+ uv add oxutils
74
+ ```
75
+
76
+ ## Quick Start
77
+
78
+ ### 1. Configure Django Settings
79
+
80
+ ```python
81
+ # settings.py
82
+ from oxutils.conf import UTILS_APPS, AUDIT_MIDDLEWARE
83
+
84
+ INSTALLED_APPS = [
85
+ *UTILS_APPS, # structlog, auditlog, cid, celery_results
86
+ # your apps...
87
+ ]
88
+
89
+ MIDDLEWARE = [
90
+ *AUDIT_MIDDLEWARE, # CID, Auditlog, RequestMiddleware
91
+ # your middleware...
92
+ ]
93
+ ```
94
+
95
+ ### 2. Environment Variables
96
+
97
+ ```bash
98
+ OXI_SERVICE_NAME=my-service
99
+ OXI_JWT_JWKS_URL=https://auth.example.com/.well-known/jwks.json
100
+ OXI_USE_STATIC_S3=True
101
+ OXI_STATIC_STORAGE_BUCKET_NAME=my-bucket
102
+ ```
103
+
104
+ ### 3. Usage Examples
105
+
106
+ ```python
107
+ # JWT Authentication
108
+ from oxutils.jwt.client import verify_token
109
+ payload = verify_token(token)
110
+
111
+ # Structured Logging
112
+ import structlog
113
+ logger = structlog.get_logger(__name__)
114
+ logger.info("user_action", user_id=user_id)
115
+
116
+ # S3 Storage
117
+ from oxutils.s3.storages import PrivateMediaStorage
118
+ class Document(models.Model):
119
+ file = models.FileField(storage=PrivateMediaStorage())
120
+
121
+ # Model Mixins
122
+ from oxutils.models.base import BaseModelMixin
123
+ class Product(BaseModelMixin): # UUID + timestamps + is_active
124
+ name = models.CharField(max_length=255)
125
+
126
+ # Custom Exceptions
127
+ from oxutils.exceptions import NotFoundException
128
+ raise NotFoundException(detail="User not found")
129
+ ```
130
+
131
+ ## Documentation
132
+
133
+ - **[Settings](docs/settings.md)** - Configuration reference
134
+ - **[JWT](docs/jwt.md)** - Authentication
135
+ - **[S3](docs/s3.md)** - Storage backends
136
+ - **[Audit](docs/audit.md)** - Change tracking
137
+ - **[Logging](docs/logger.md)** - Structured logs
138
+ - **[Mixins](docs/mixins.md)** - Model/service mixins
139
+ - **[Celery](docs/celery.md)** - Task processing
140
+
141
+ ## Requirements
142
+
143
+ - Python 3.11+
144
+ - Django 5.0+
145
+ - PostgreSQL (recommended)
146
+
147
+ ## Development
148
+
149
+ ```bash
150
+ git clone https://github.com/oxiliere/oxutils.git
151
+ cd oxutils
152
+ uv sync
153
+ uv run pytest # 126 tests
154
+ ```
155
+
156
+ ## Advanced Examples
157
+
158
+ ### JWT with Django Ninja
159
+
160
+ ```python
161
+ from ninja import NinjaAPI
162
+ from ninja.security import HttpBearer
163
+ from oxutils.jwt.client import verify_token
164
+
165
+ class JWTAuth(HttpBearer):
166
+ def authenticate(self, request, token):
167
+ try:
168
+ return verify_token(token)
169
+ except:
170
+ return None
171
+
172
+ api = NinjaAPI(auth=JWTAuth())
173
+
174
+ @api.get("/protected")
175
+ def protected(request):
176
+ return {"user_id": request.auth['sub']}
177
+ ```
178
+
179
+ ### Audit Log Export
180
+
181
+ ```python
182
+ from oxutils.audit.export import export_logs_from_date
183
+ from datetime import datetime, timedelta
184
+
185
+ from_date = datetime.now() - timedelta(days=7)
186
+ export = export_logs_from_date(from_date=from_date)
187
+ print(f"Exported to {export.data.url}")
188
+ ```
189
+
190
+ ## License
191
+
192
+ MIT License - see [LICENSE](LICENSE)
193
+
194
+ ## Support
195
+
196
+ - **Issues**: [GitHub Issues](https://github.com/oxiliere/oxutils/issues)
197
+ - **Email**: dev@oxiliere.com
198
+
199
+ ---
200
+
201
+ **Made with ❤️ by Oxiliere**
@@ -0,0 +1,43 @@
1
+ oxutils/__init__.py,sha256=P25yalCx6B4hRjjbABZP9zpGc6WLaErSr62EtbkKXMw,536
2
+ oxutils/apps.py,sha256=8pO8eXUZeKYn8fPo0rkoytmHACwDNuTNhdRcpkPTxGM,347
3
+ oxutils/audit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ oxutils/audit/apps.py,sha256=oT6yoZiFsrg__xSTwr1YXPh0rBhgxcXkuWLIx_fv5rA,301
5
+ oxutils/audit/export.py,sha256=MVf2RhLzXatBaGK7bIEvSY1VTwEojrPEKwYMvH1stwE,7992
6
+ oxutils/audit/masks.py,sha256=BRCz2m8dbaLgqn5umxpWCwn9mT6Z_ww_WIedl36AmPM,2345
7
+ oxutils/audit/models.py,sha256=NWzZjwgRGB212PeSb0E_aSxSComrTSR0knt2aBCiWhg,2100
8
+ oxutils/audit/settings.py,sha256=E4AoTpbvL1svDogux-OTjAdccD5LzyJ1G-lBeCZTbDU,353
9
+ oxutils/celery/__init__.py,sha256=29jo4DfdvOoThX-jfL0ZiDjsy3-Z_fNhwHVJaLO5rsk,29
10
+ oxutils/celery/base.py,sha256=qLlBU2XvT2zj8qszy8togqH7hM_wUYyWWA3JAQPPJx0,3378
11
+ oxutils/celery/settings.py,sha256=njhHBErpcFczV2e23NCPX_Jxs015jr4dIig4Is_wbgE,33
12
+ oxutils/conf.py,sha256=Www0b7XdmsRK_wcKFmp9sHMLKApSA4JVgM-dVvdB0UM,275
13
+ oxutils/enums/__init__.py,sha256=gFhZG8ER6ArGZO5agWhdfs7NiD2h9FzrzfQRHq18dD0,40
14
+ oxutils/enums/audit.py,sha256=ju2Z9CrtdyPziRQ7oOe4Ygw85t9sW3jynO_1DkgZoAM,126
15
+ oxutils/enums/invoices.py,sha256=E33QGQeutZUqvlovJY0VGDxWUb0i_kdfhEiir1ARKuQ,201
16
+ oxutils/exceptions.py,sha256=CCjENOD0of6_noif2ajrpfbBLoG16DWa46iB9_uEe3M,3592
17
+ oxutils/functions.py,sha256=p4SycMykom6H2AoryX-X4W_Acphf2_tx-LY2WxFD0Xc,3190
18
+ oxutils/jwt/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
+ oxutils/jwt/auth.py,sha256=rO-xWNfug9Ok6zA7EIPvVkpD8TBUdq05CdrnMrL-t9Q,1597
20
+ oxutils/jwt/client.py,sha256=bskLpmSBrehi_snbo3Qbq1m99Kbfg2GP7jqfcXKHvys,3341
21
+ oxutils/jwt/constants.py,sha256=MUahZjm8plTYpHjLOMQCuH0H18lkIwS45EtRm617wq8,26
22
+ oxutils/locale/fr/LC_MESSAGES/django.mo,sha256=Qk5R90E-JqJX2JxSnveKXxecKVHR-a76uFlwkHTF7f0,8114
23
+ oxutils/locale/fr/LC_MESSAGES/django.po,sha256=APXt_8R99seCWjJyS5ELOawvRLvUqqBT32O252BaG5s,7971
24
+ oxutils/logger/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
+ oxutils/logger/receivers.py,sha256=U8JVjHRK1zUaCRnPn6p1qsm6FFXgTkXp67PpJ1LnjgU,542
26
+ oxutils/logger/settings.py,sha256=aiKiJqNNkw1g5vQgjk3Zfh6UgY7jx-lbmcFgATiXrGI,1805
27
+ oxutils/mixins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
+ oxutils/mixins/base.py,sha256=0cGY4mGKhL-hJTEBsbETYiaKMVuUgio5DISCv5iYtGI,589
29
+ oxutils/mixins/schemas.py,sha256=DGW0GQBJc9K7hwOJLX-fEryi7KCoY1QaCLny8fjtQMI,319
30
+ oxutils/mixins/services.py,sha256=i4MrkCE3y1W7Xrrz6YeMMaY9xMtCQs4WgBIpM7hrDTI,5399
31
+ oxutils/models/__init__.py,sha256=mR9hhncZMGKefovg9xSPBtb9Yu2AssAdta26ihcNvI4,77
32
+ oxutils/models/base.py,sha256=1HGHB8MnhPpaa1SpnUVgiknCSHTwajz5LxprB2zicbc,2884
33
+ oxutils/models/billing.py,sha256=aCDZcMx4CUyAwh3wgJGypAJl_fSEuWrL27-cSYv3gCs,3323
34
+ oxutils/models/invoice.py,sha256=nqphkhlBhssODm2H4dBYyb1cOmHS29fToER40UN0cXo,13216
35
+ oxutils/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
+ oxutils/s3/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
+ oxutils/s3/settings.py,sha256=NIlVhaOzWdsepOgCpxdTTJRHfM0tM5EcAoy4kaFC1R8,1190
38
+ oxutils/s3/storages.py,sha256=vW8BlxLwkqMgVJPf9z2rkjmlrL_RArlN3YA6y4BJ7Zw,5414
39
+ oxutils/settings.py,sha256=iOVRgkurkv6UtlEvY0dkO71oxxgPAIdyXgHhf3RJaTU,9673
40
+ oxutils/types.py,sha256=DIz8YK8xMpLc7FYbf88yEElyLsYN_-rbvaZXvENQkOQ,234
41
+ oxutils-0.1.0.dist-info/WHEEL,sha256=4n27za1eEkOnA7dNjN6C5-O2rUiw6iapszm14Uj-Qmk,79
42
+ oxutils-0.1.0.dist-info/METADATA,sha256=q6Jww1jWZILdwarNnXtg0SkqKcvvuqYVCCyZaIs9CII,5725
43
+ oxutils-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.8.13
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any