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/__init__.py +23 -0
- oxutils/apps.py +14 -0
- oxutils/audit/__init__.py +0 -0
- oxutils/audit/apps.py +12 -0
- oxutils/audit/export.py +229 -0
- oxutils/audit/masks.py +97 -0
- oxutils/audit/models.py +75 -0
- oxutils/audit/settings.py +19 -0
- oxutils/celery/__init__.py +1 -0
- oxutils/celery/base.py +98 -0
- oxutils/celery/settings.py +1 -0
- oxutils/conf.py +12 -0
- oxutils/enums/__init__.py +1 -0
- oxutils/enums/audit.py +8 -0
- oxutils/enums/invoices.py +11 -0
- oxutils/exceptions.py +117 -0
- oxutils/functions.py +99 -0
- oxutils/jwt/__init__.py +0 -0
- oxutils/jwt/auth.py +55 -0
- oxutils/jwt/client.py +123 -0
- oxutils/jwt/constants.py +1 -0
- oxutils/locale/fr/LC_MESSAGES/django.mo +0 -0
- oxutils/locale/fr/LC_MESSAGES/django.po +368 -0
- oxutils/logger/__init__.py +0 -0
- oxutils/logger/receivers.py +18 -0
- oxutils/logger/settings.py +63 -0
- oxutils/mixins/__init__.py +0 -0
- oxutils/mixins/base.py +21 -0
- oxutils/mixins/schemas.py +13 -0
- oxutils/mixins/services.py +146 -0
- oxutils/models/__init__.py +3 -0
- oxutils/models/base.py +116 -0
- oxutils/models/billing.py +140 -0
- oxutils/models/invoice.py +467 -0
- oxutils/py.typed +0 -0
- oxutils/s3/__init__.py +0 -0
- oxutils/s3/settings.py +34 -0
- oxutils/s3/storages.py +130 -0
- oxutils/settings.py +254 -0
- oxutils/types.py +13 -0
- oxutils-0.1.0.dist-info/METADATA +201 -0
- oxutils-0.1.0.dist-info/RECORD +43 -0
- oxutils-0.1.0.dist-info/WHEEL +4 -0
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,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
|
+
[](https://pypi.org/project/oxutils/)
|
|
48
|
+
[](https://www.python.org/)
|
|
49
|
+
[](https://www.djangoproject.com/)
|
|
50
|
+
[](tests/)
|
|
51
|
+
[](LICENSE)
|
|
52
|
+
[](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,,
|