oxutils 0.1.2__tar.gz → 0.1.4__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.
- {oxutils-0.1.2 → oxutils-0.1.4}/PKG-INFO +22 -11
- {oxutils-0.1.2 → oxutils-0.1.4}/README.md +19 -7
- {oxutils-0.1.2 → oxutils-0.1.4}/pyproject.toml +4 -5
- {oxutils-0.1.2 → oxutils-0.1.4}/src/oxutils/__init__.py +1 -1
- oxutils-0.1.4/src/oxutils/audit/settings.py +4 -0
- oxutils-0.1.4/src/oxutils/audit/utils.py +22 -0
- {oxutils-0.1.2 → oxutils-0.1.4}/src/oxutils/conf.py +1 -3
- oxutils-0.1.4/src/oxutils/context/site_name_processor.py +11 -0
- oxutils-0.1.4/src/oxutils/currency/admin.py +57 -0
- oxutils-0.1.4/src/oxutils/currency/apps.py +7 -0
- oxutils-0.1.4/src/oxutils/currency/controllers.py +78 -0
- oxutils-0.1.4/src/oxutils/currency/enums.py +7 -0
- oxutils-0.1.4/src/oxutils/currency/migrations/0001_initial.py +41 -0
- oxutils-0.1.4/src/oxutils/currency/models.py +100 -0
- oxutils-0.1.4/src/oxutils/currency/schemas.py +38 -0
- oxutils-0.1.4/src/oxutils/currency/tests.py +3 -0
- oxutils-0.1.4/src/oxutils/currency/utils.py +31 -0
- {oxutils-0.1.2 → oxutils-0.1.4}/src/oxutils/functions.py +5 -2
- {oxutils-0.1.2 → oxutils-0.1.4}/src/oxutils/logger/receivers.py +0 -2
- oxutils-0.1.4/src/oxutils/mixins/__init__.py +0 -0
- oxutils-0.1.4/src/oxutils/py.typed +0 -0
- oxutils-0.1.4/src/oxutils/s3/__init__.py +0 -0
- {oxutils-0.1.2 → oxutils-0.1.4}/src/oxutils/settings.py +2 -0
- oxutils-0.1.2/src/oxutils/audit/settings.py +0 -19
- oxutils-0.1.2/src/oxutils/locale/fr/LC_MESSAGES/django.mo +0 -0
- {oxutils-0.1.2 → oxutils-0.1.4}/src/oxutils/apps.py +0 -0
- {oxutils-0.1.2 → oxutils-0.1.4}/src/oxutils/audit/__init__.py +0 -0
- {oxutils-0.1.2 → oxutils-0.1.4}/src/oxutils/audit/apps.py +0 -0
- {oxutils-0.1.2 → oxutils-0.1.4}/src/oxutils/audit/export.py +0 -0
- {oxutils-0.1.2 → oxutils-0.1.4}/src/oxutils/audit/masks.py +0 -0
- {oxutils-0.1.2 → oxutils-0.1.4}/src/oxutils/audit/migrations/0001_initial.py +0 -0
- {oxutils-0.1.2 → oxutils-0.1.4}/src/oxutils/audit/migrations/__init__.py +0 -0
- {oxutils-0.1.2 → oxutils-0.1.4}/src/oxutils/audit/models.py +0 -0
- {oxutils-0.1.2 → oxutils-0.1.4}/src/oxutils/celery/__init__.py +0 -0
- {oxutils-0.1.2 → oxutils-0.1.4}/src/oxutils/celery/base.py +0 -0
- {oxutils-0.1.2 → oxutils-0.1.4}/src/oxutils/celery/settings.py +0 -0
- {oxutils-0.1.2/src/oxutils/jwt → oxutils-0.1.4/src/oxutils/context}/__init__.py +0 -0
- {oxutils-0.1.2/src/oxutils/logger → oxutils-0.1.4/src/oxutils/currency}/__init__.py +0 -0
- {oxutils-0.1.2/src/oxutils/mixins → oxutils-0.1.4/src/oxutils/currency/migrations}/__init__.py +0 -0
- {oxutils-0.1.2 → oxutils-0.1.4}/src/oxutils/enums/__init__.py +0 -0
- {oxutils-0.1.2 → oxutils-0.1.4}/src/oxutils/enums/audit.py +0 -0
- {oxutils-0.1.2 → oxutils-0.1.4}/src/oxutils/enums/invoices.py +0 -0
- {oxutils-0.1.2 → oxutils-0.1.4}/src/oxutils/exceptions.py +0 -0
- {oxutils-0.1.2/src/oxutils/s3 → oxutils-0.1.4/src/oxutils/jwt}/__init__.py +0 -0
- {oxutils-0.1.2 → oxutils-0.1.4}/src/oxutils/jwt/auth.py +0 -0
- {oxutils-0.1.2 → oxutils-0.1.4}/src/oxutils/jwt/client.py +0 -0
- {oxutils-0.1.2 → oxutils-0.1.4}/src/oxutils/jwt/constants.py +0 -0
- {oxutils-0.1.2 → oxutils-0.1.4}/src/oxutils/locale/fr/LC_MESSAGES/django.po +0 -0
- /oxutils-0.1.2/src/oxutils/py.typed → /oxutils-0.1.4/src/oxutils/logger/__init__.py +0 -0
- {oxutils-0.1.2 → oxutils-0.1.4}/src/oxutils/logger/settings.py +0 -0
- {oxutils-0.1.2 → oxutils-0.1.4}/src/oxutils/mixins/base.py +0 -0
- {oxutils-0.1.2 → oxutils-0.1.4}/src/oxutils/mixins/schemas.py +0 -0
- {oxutils-0.1.2 → oxutils-0.1.4}/src/oxutils/mixins/services.py +0 -0
- {oxutils-0.1.2 → oxutils-0.1.4}/src/oxutils/models/__init__.py +0 -0
- {oxutils-0.1.2 → oxutils-0.1.4}/src/oxutils/models/base.py +0 -0
- {oxutils-0.1.2 → oxutils-0.1.4}/src/oxutils/models/billing.py +0 -0
- {oxutils-0.1.2 → oxutils-0.1.4}/src/oxutils/models/invoice.py +0 -0
- {oxutils-0.1.2 → oxutils-0.1.4}/src/oxutils/s3/settings.py +0 -0
- {oxutils-0.1.2 → oxutils-0.1.4}/src/oxutils/s3/storages.py +0 -0
- {oxutils-0.1.2 → oxutils-0.1.4}/src/oxutils/types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: oxutils
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Production-ready utilities for Django applications in the Oxiliere ecosystem
|
|
5
5
|
Keywords: django,utilities,jwt,s3,audit,logging,celery,structlog
|
|
6
6
|
Author: Edimedia Mutoke
|
|
@@ -13,16 +13,15 @@ Classifier: Intended Audience :: Developers
|
|
|
13
13
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
14
|
Classifier: Operating System :: OS Independent
|
|
15
15
|
Classifier: Programming Language :: Python :: 3
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
17
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
18
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
19
|
+
Requires-Dist: bcc-rates>=1.1.0
|
|
20
20
|
Requires-Dist: boto3>=1.41.5
|
|
21
21
|
Requires-Dist: celery>=5.5.3
|
|
22
22
|
Requires-Dist: cryptography>=46.0.3
|
|
23
23
|
Requires-Dist: django-auditlog>=3.3.0
|
|
24
24
|
Requires-Dist: django-celery-results>=2.6.0
|
|
25
|
-
Requires-Dist: django-cid>=3.0
|
|
26
25
|
Requires-Dist: django-extensions>=4.1
|
|
27
26
|
Requires-Dist: django-ninja>=1.5.0
|
|
28
27
|
Requires-Dist: django-ninja-extra>=0.30.6
|
|
@@ -32,7 +31,7 @@ Requires-Dist: jwcrypto>=1.5.6
|
|
|
32
31
|
Requires-Dist: pydantic-settings>=2.12.0
|
|
33
32
|
Requires-Dist: pyjwt>=2.10.1
|
|
34
33
|
Requires-Dist: requests>=2.32.5
|
|
35
|
-
Requires-Python: >=3.
|
|
34
|
+
Requires-Python: >=3.12
|
|
36
35
|
Project-URL: Changelog, https://github.com/oxiliere/oxutils/blob/main/CHANGELOG.md
|
|
37
36
|
Project-URL: Documentation, https://github.com/oxiliere/oxutils/tree/main/docs
|
|
38
37
|
Project-URL: Homepage, https://github.com/oxiliere/oxutils
|
|
@@ -45,9 +44,9 @@ Description-Content-Type: text/markdown
|
|
|
45
44
|
**Production-ready utilities for Django applications in the Oxiliere ecosystem.**
|
|
46
45
|
|
|
47
46
|
[](https://pypi.org/project/oxutils/)
|
|
48
|
-
[](https://www.python.org/)
|
|
49
48
|
[](https://www.djangoproject.com/)
|
|
50
|
-
[](tests/)
|
|
51
50
|
[](LICENSE)
|
|
52
51
|
[](https://github.com/astral-sh/ruff)
|
|
53
52
|
|
|
@@ -55,11 +54,12 @@ Description-Content-Type: text/markdown
|
|
|
55
54
|
|
|
56
55
|
- 🔐 **JWT Authentication** - RS256 with JWKS caching
|
|
57
56
|
- 📦 **S3 Storage** - Static, media, private, and log backends
|
|
58
|
-
- 📝 **Structured Logging** - JSON logs with
|
|
57
|
+
- 📝 **Structured Logging** - JSON logs with automatic request tracking
|
|
59
58
|
- 🔍 **Audit System** - Change tracking with S3 export
|
|
60
59
|
- ⚙️ **Celery Integration** - Pre-configured task processing
|
|
61
60
|
- 🛠️ **Django Mixins** - UUID, timestamps, user tracking
|
|
62
61
|
- ⚡ **Custom Exceptions** - Standardized API errors
|
|
62
|
+
- 🎨 **Context Processors** - Site name and domain for templates
|
|
63
63
|
|
|
64
64
|
---
|
|
65
65
|
|
|
@@ -82,12 +82,12 @@ uv add oxutils
|
|
|
82
82
|
from oxutils.conf import UTILS_APPS, AUDIT_MIDDLEWARE
|
|
83
83
|
|
|
84
84
|
INSTALLED_APPS = [
|
|
85
|
-
*UTILS_APPS, # structlog, auditlog,
|
|
85
|
+
*UTILS_APPS, # structlog, auditlog, celery_results
|
|
86
86
|
# your apps...
|
|
87
87
|
]
|
|
88
88
|
|
|
89
89
|
MIDDLEWARE = [
|
|
90
|
-
*AUDIT_MIDDLEWARE, #
|
|
90
|
+
*AUDIT_MIDDLEWARE, # RequestMiddleware, Auditlog
|
|
91
91
|
# your middleware...
|
|
92
92
|
]
|
|
93
93
|
```
|
|
@@ -126,6 +126,17 @@ class Product(BaseModelMixin): # UUID + timestamps + is_active
|
|
|
126
126
|
# Custom Exceptions
|
|
127
127
|
from oxutils.exceptions import NotFoundException
|
|
128
128
|
raise NotFoundException(detail="User not found")
|
|
129
|
+
|
|
130
|
+
# Context Processors
|
|
131
|
+
# settings.py
|
|
132
|
+
TEMPLATES = [{
|
|
133
|
+
'OPTIONS': {
|
|
134
|
+
'context_processors': [
|
|
135
|
+
'oxutils.context.site_name_processor.site_name',
|
|
136
|
+
],
|
|
137
|
+
},
|
|
138
|
+
}]
|
|
139
|
+
# Now {{ site_name }} and {{ site_domain }} are available in templates
|
|
129
140
|
```
|
|
130
141
|
|
|
131
142
|
## Documentation
|
|
@@ -140,7 +151,7 @@ raise NotFoundException(detail="User not found")
|
|
|
140
151
|
|
|
141
152
|
## Requirements
|
|
142
153
|
|
|
143
|
-
- Python 3.
|
|
154
|
+
- Python 3.12+
|
|
144
155
|
- Django 5.0+
|
|
145
156
|
- PostgreSQL (recommended)
|
|
146
157
|
|
|
@@ -150,7 +161,7 @@ raise NotFoundException(detail="User not found")
|
|
|
150
161
|
git clone https://github.com/oxiliere/oxutils.git
|
|
151
162
|
cd oxutils
|
|
152
163
|
uv sync
|
|
153
|
-
uv run pytest #
|
|
164
|
+
uv run pytest # 145 tests
|
|
154
165
|
```
|
|
155
166
|
|
|
156
167
|
### Creating Migrations
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
**Production-ready utilities for Django applications in the Oxiliere ecosystem.**
|
|
4
4
|
|
|
5
5
|
[](https://pypi.org/project/oxutils/)
|
|
6
|
-
[](https://www.python.org/)
|
|
7
7
|
[](https://www.djangoproject.com/)
|
|
8
|
-
[](tests/)
|
|
9
9
|
[](LICENSE)
|
|
10
10
|
[](https://github.com/astral-sh/ruff)
|
|
11
11
|
|
|
@@ -13,11 +13,12 @@
|
|
|
13
13
|
|
|
14
14
|
- 🔐 **JWT Authentication** - RS256 with JWKS caching
|
|
15
15
|
- 📦 **S3 Storage** - Static, media, private, and log backends
|
|
16
|
-
- 📝 **Structured Logging** - JSON logs with
|
|
16
|
+
- 📝 **Structured Logging** - JSON logs with automatic request tracking
|
|
17
17
|
- 🔍 **Audit System** - Change tracking with S3 export
|
|
18
18
|
- ⚙️ **Celery Integration** - Pre-configured task processing
|
|
19
19
|
- 🛠️ **Django Mixins** - UUID, timestamps, user tracking
|
|
20
20
|
- ⚡ **Custom Exceptions** - Standardized API errors
|
|
21
|
+
- 🎨 **Context Processors** - Site name and domain for templates
|
|
21
22
|
|
|
22
23
|
---
|
|
23
24
|
|
|
@@ -40,12 +41,12 @@ uv add oxutils
|
|
|
40
41
|
from oxutils.conf import UTILS_APPS, AUDIT_MIDDLEWARE
|
|
41
42
|
|
|
42
43
|
INSTALLED_APPS = [
|
|
43
|
-
*UTILS_APPS, # structlog, auditlog,
|
|
44
|
+
*UTILS_APPS, # structlog, auditlog, celery_results
|
|
44
45
|
# your apps...
|
|
45
46
|
]
|
|
46
47
|
|
|
47
48
|
MIDDLEWARE = [
|
|
48
|
-
*AUDIT_MIDDLEWARE, #
|
|
49
|
+
*AUDIT_MIDDLEWARE, # RequestMiddleware, Auditlog
|
|
49
50
|
# your middleware...
|
|
50
51
|
]
|
|
51
52
|
```
|
|
@@ -84,6 +85,17 @@ class Product(BaseModelMixin): # UUID + timestamps + is_active
|
|
|
84
85
|
# Custom Exceptions
|
|
85
86
|
from oxutils.exceptions import NotFoundException
|
|
86
87
|
raise NotFoundException(detail="User not found")
|
|
88
|
+
|
|
89
|
+
# Context Processors
|
|
90
|
+
# settings.py
|
|
91
|
+
TEMPLATES = [{
|
|
92
|
+
'OPTIONS': {
|
|
93
|
+
'context_processors': [
|
|
94
|
+
'oxutils.context.site_name_processor.site_name',
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
}]
|
|
98
|
+
# Now {{ site_name }} and {{ site_domain }} are available in templates
|
|
87
99
|
```
|
|
88
100
|
|
|
89
101
|
## Documentation
|
|
@@ -98,7 +110,7 @@ raise NotFoundException(detail="User not found")
|
|
|
98
110
|
|
|
99
111
|
## Requirements
|
|
100
112
|
|
|
101
|
-
- Python 3.
|
|
113
|
+
- Python 3.12+
|
|
102
114
|
- Django 5.0+
|
|
103
115
|
- PostgreSQL (recommended)
|
|
104
116
|
|
|
@@ -108,7 +120,7 @@ raise NotFoundException(detail="User not found")
|
|
|
108
120
|
git clone https://github.com/oxiliere/oxutils.git
|
|
109
121
|
cd oxutils
|
|
110
122
|
uv sync
|
|
111
|
-
uv run pytest #
|
|
123
|
+
uv run pytest # 145 tests
|
|
112
124
|
```
|
|
113
125
|
|
|
114
126
|
### Creating Migrations
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "oxutils"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.4"
|
|
4
4
|
description = "Production-ready utilities for Django applications in the Oxiliere ecosystem"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
7
7
|
authors = [
|
|
8
8
|
{ name = "Edimedia Mutoke", email = "eddycondor07@gmail.com" }
|
|
9
9
|
]
|
|
10
|
-
requires-python = ">=3.
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
11
|
keywords = ["django", "utilities", "jwt", "s3", "audit", "logging", "celery", "structlog"]
|
|
12
12
|
classifiers = [
|
|
13
13
|
"Development Status :: 4 - Beta",
|
|
@@ -17,18 +17,17 @@ classifiers = [
|
|
|
17
17
|
"License :: OSI Approved :: Apache Software License",
|
|
18
18
|
"Operating System :: OS Independent",
|
|
19
19
|
"Programming Language :: Python :: 3",
|
|
20
|
-
"Programming Language :: Python :: 3.11",
|
|
21
20
|
"Programming Language :: Python :: 3.12",
|
|
22
21
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
23
22
|
"Topic :: Internet :: WWW/HTTP",
|
|
24
23
|
]
|
|
25
24
|
dependencies = [
|
|
25
|
+
"bcc-rates>=1.1.0",
|
|
26
26
|
"boto3>=1.41.5",
|
|
27
27
|
"celery>=5.5.3",
|
|
28
28
|
"cryptography>=46.0.3",
|
|
29
29
|
"django-auditlog>=3.3.0",
|
|
30
30
|
"django-celery-results>=2.6.0",
|
|
31
|
-
"django-cid>=3.0",
|
|
32
31
|
"django-extensions>=4.1",
|
|
33
32
|
"django-ninja>=1.5.0",
|
|
34
33
|
"django-ninja-extra>=0.30.6",
|
|
@@ -69,7 +68,7 @@ dev = [
|
|
|
69
68
|
|
|
70
69
|
[tool.ruff]
|
|
71
70
|
line-length = 100
|
|
72
|
-
target-version = "
|
|
71
|
+
target-version = "py312"
|
|
73
72
|
exclude = [
|
|
74
73
|
".git",
|
|
75
74
|
".venv",
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions for audit logging.
|
|
3
|
+
"""
|
|
4
|
+
import structlog
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_request_id():
|
|
8
|
+
"""
|
|
9
|
+
Get the request_id from django-structlog context.
|
|
10
|
+
|
|
11
|
+
This function retrieves the request_id that was set by
|
|
12
|
+
django-structlog's RequestMiddleware and returns it for use
|
|
13
|
+
in auditlog's correlation ID field.
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
str: The request_id from the current request context, or None if not available.
|
|
17
|
+
"""
|
|
18
|
+
try:
|
|
19
|
+
context = structlog.contextvars.get_contextvars()
|
|
20
|
+
return context.get('request_id')
|
|
21
|
+
except Exception:
|
|
22
|
+
return None
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
UTILS_APPS = (
|
|
2
2
|
'django_structlog',
|
|
3
3
|
'auditlog',
|
|
4
|
-
'cid.apps.CidAppConfig',
|
|
5
4
|
'django_celery_results',
|
|
6
5
|
'oxutils.audit',
|
|
7
6
|
)
|
|
8
7
|
|
|
9
8
|
AUDIT_MIDDLEWARE = (
|
|
10
|
-
'cid.middleware.CidMiddleware',
|
|
11
|
-
'auditlog.middleware.AuditlogMiddleware',
|
|
12
9
|
'django_structlog.middlewares.RequestMiddleware',
|
|
10
|
+
'auditlog.middleware.AuditlogMiddleware',
|
|
13
11
|
)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from django.contrib import admin
|
|
2
|
+
from .models import CurrencyState, Currency
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class CurrencyInline(admin.TabularInline):
|
|
6
|
+
model = Currency
|
|
7
|
+
extra = 0
|
|
8
|
+
readonly_fields = ('id', 'code', 'rate')
|
|
9
|
+
can_delete = False
|
|
10
|
+
|
|
11
|
+
def has_add_permission(self, request, obj=None):
|
|
12
|
+
return False
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@admin.register(CurrencyState)
|
|
16
|
+
class CurrencyStateAdmin(admin.ModelAdmin):
|
|
17
|
+
list_display = ('id', 'source', 'currency_count', 'created_at', 'updated_at')
|
|
18
|
+
list_filter = ('source', 'created_at')
|
|
19
|
+
readonly_fields = ('id', 'created_at', 'updated_at')
|
|
20
|
+
search_fields = ('id', 'source')
|
|
21
|
+
ordering = ('-created_at',)
|
|
22
|
+
inlines = [CurrencyInline]
|
|
23
|
+
|
|
24
|
+
def currency_count(self, obj):
|
|
25
|
+
return obj.currencies.count()
|
|
26
|
+
currency_count.short_description = 'Currencies'
|
|
27
|
+
|
|
28
|
+
def has_add_permission(self, request):
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
def has_delete_permission(self, request, obj=None):
|
|
32
|
+
return request.user.is_superuser
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@admin.register(Currency)
|
|
36
|
+
class CurrencyAdmin(admin.ModelAdmin):
|
|
37
|
+
list_display = ('id', 'code', 'rate', 'state_source', 'state_created_at')
|
|
38
|
+
list_filter = ('code', 'state__source', 'state__created_at')
|
|
39
|
+
readonly_fields = ('id', 'code', 'rate', 'state')
|
|
40
|
+
search_fields = ('code', 'state__id')
|
|
41
|
+
ordering = ('code',)
|
|
42
|
+
|
|
43
|
+
def state_source(self, obj):
|
|
44
|
+
return obj.state.source
|
|
45
|
+
state_source.short_description = 'Source'
|
|
46
|
+
state_source.admin_order_field = 'state__source'
|
|
47
|
+
|
|
48
|
+
def state_created_at(self, obj):
|
|
49
|
+
return obj.state.created_at
|
|
50
|
+
state_created_at.short_description = 'State Created'
|
|
51
|
+
state_created_at.admin_order_field = 'state__created_at'
|
|
52
|
+
|
|
53
|
+
def has_add_permission(self, request):
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
def has_delete_permission(self, request, obj=None):
|
|
57
|
+
return request.user.is_superuser
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from django.http import HttpRequest
|
|
2
|
+
from django.core.exceptions import ObjectDoesNotExist
|
|
3
|
+
from ninja_extra import (
|
|
4
|
+
ControllerBase,
|
|
5
|
+
api_controller,
|
|
6
|
+
http_get,
|
|
7
|
+
)
|
|
8
|
+
from ninja_extra.pagination import (
|
|
9
|
+
paginate, PageNumberPaginationExtra, PaginatedResponseSchema
|
|
10
|
+
)
|
|
11
|
+
from ninja.errors import HttpError
|
|
12
|
+
from uuid import UUID
|
|
13
|
+
import structlog
|
|
14
|
+
from currency.models import CurrencyState
|
|
15
|
+
from currency.schemas import (
|
|
16
|
+
CurrencyStateSchema,
|
|
17
|
+
CurrencyStateDetailSchema,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
logger = structlog.get_logger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@api_controller('/currency', tags=['Currency'], auth=None)
|
|
24
|
+
class CurrencyController(ControllerBase):
|
|
25
|
+
|
|
26
|
+
@http_get('/states', response=PaginatedResponseSchema[CurrencyStateSchema])
|
|
27
|
+
@paginate(PageNumberPaginationExtra, page_size=20)
|
|
28
|
+
def list_states(self, request: HttpRequest):
|
|
29
|
+
return CurrencyState.objects.all().order_by('-created_at')
|
|
30
|
+
|
|
31
|
+
@http_get('/states/latest', response=CurrencyStateDetailSchema)
|
|
32
|
+
def get_latest_state(self, request: HttpRequest):
|
|
33
|
+
try:
|
|
34
|
+
state = CurrencyState.objects.latest()
|
|
35
|
+
except ObjectDoesNotExist:
|
|
36
|
+
logger.error("currency_state_not_found", message="No currency state found in database")
|
|
37
|
+
raise HttpError(404, "No currency state found in database")
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
'id': state.id,
|
|
41
|
+
'source': state.source,
|
|
42
|
+
'created_at': state.created_at,
|
|
43
|
+
'updated_at': state.updated_at,
|
|
44
|
+
'currencies': {c.code: float(c.rate) for c in state.currencies.all()}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@http_get('/states/{state_id}', response=CurrencyStateDetailSchema)
|
|
48
|
+
def get_state(self, request: HttpRequest, state_id: UUID):
|
|
49
|
+
state = CurrencyState.objects.prefetch_related('currencies').get(id=state_id)
|
|
50
|
+
return {
|
|
51
|
+
'id': state.id,
|
|
52
|
+
'source': state.source,
|
|
53
|
+
'created_at': state.created_at,
|
|
54
|
+
'updated_at': state.updated_at,
|
|
55
|
+
'currencies': {c.code: float(c.rate) for c in state.currencies.all()}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@http_get('/rates', response=dict[str, float])
|
|
59
|
+
def get_current_rates(self, request: HttpRequest):
|
|
60
|
+
try:
|
|
61
|
+
state = CurrencyState.objects.latest()
|
|
62
|
+
except ObjectDoesNotExist:
|
|
63
|
+
logger.error("currency_state_not_found", message="No currency state found in database")
|
|
64
|
+
raise HttpError(404, "No currency rates available")
|
|
65
|
+
|
|
66
|
+
currencies = state.currencies.all()
|
|
67
|
+
return {c.code: float(c.rate) for c in currencies}
|
|
68
|
+
|
|
69
|
+
@http_get('/rates/{code}', response=dict[str, float])
|
|
70
|
+
def get_rate_by_code(self, request: HttpRequest, code: str):
|
|
71
|
+
try:
|
|
72
|
+
state = CurrencyState.objects.latest()
|
|
73
|
+
currency = state.currencies.get(code=code.upper())
|
|
74
|
+
except ObjectDoesNotExist:
|
|
75
|
+
logger.error("currency_rate_not_found", code=code.upper())
|
|
76
|
+
raise HttpError(404, f"Currency rate for {code.upper()} not found")
|
|
77
|
+
|
|
78
|
+
return {currency.code: float(currency.rate)}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Generated by Django 5.2.9 on 2025-12-19 14:34
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
import uuid
|
|
5
|
+
from django.db import migrations, models
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Migration(migrations.Migration):
|
|
9
|
+
|
|
10
|
+
initial = True
|
|
11
|
+
|
|
12
|
+
dependencies = [
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
operations = [
|
|
16
|
+
migrations.CreateModel(
|
|
17
|
+
name='CurrencyState',
|
|
18
|
+
fields=[
|
|
19
|
+
('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='Unique identifier for this record', primary_key=True, serialize=False)),
|
|
20
|
+
('created_at', models.DateTimeField(auto_now_add=True, help_text='Date and time when this record was created')),
|
|
21
|
+
('updated_at', models.DateTimeField(auto_now=True, help_text='Date and time when this record was last updated')),
|
|
22
|
+
('source', models.CharField(choices=[('bcc', 'BCC'), ('oxr', 'Open Exchange Rates')], max_length=10)),
|
|
23
|
+
],
|
|
24
|
+
options={
|
|
25
|
+
'abstract': False,
|
|
26
|
+
},
|
|
27
|
+
),
|
|
28
|
+
migrations.CreateModel(
|
|
29
|
+
name='Currency',
|
|
30
|
+
fields=[
|
|
31
|
+
('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='Unique identifier for this record', primary_key=True, serialize=False)),
|
|
32
|
+
('code', models.CharField(max_length=10)),
|
|
33
|
+
('rate', models.DecimalField(decimal_places=4, max_digits=10)),
|
|
34
|
+
('state', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='currencies', to='currency.currencystate')),
|
|
35
|
+
],
|
|
36
|
+
options={
|
|
37
|
+
'ordering': ['code'],
|
|
38
|
+
'indexes': [models.Index(fields=['code', 'state'], name='currency_cu_code_c68344_idx')],
|
|
39
|
+
},
|
|
40
|
+
),
|
|
41
|
+
]
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
from django.db import models
|
|
3
|
+
from django.db import transaction
|
|
4
|
+
import structlog
|
|
5
|
+
from oxutils.models import (
|
|
6
|
+
UUIDPrimaryKeyMixin,
|
|
7
|
+
TimestampMixin,
|
|
8
|
+
)
|
|
9
|
+
from .enums import CurrencySource
|
|
10
|
+
from .utils import load_rates
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
logger = structlog.get_logger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
AVAILABLES_CURRENCIES = [
|
|
18
|
+
"AOA",
|
|
19
|
+
"AUD",
|
|
20
|
+
"BIF",
|
|
21
|
+
"CAD",
|
|
22
|
+
"CHF",
|
|
23
|
+
"CNY",
|
|
24
|
+
"EUR",
|
|
25
|
+
"GBP",
|
|
26
|
+
"JPY",
|
|
27
|
+
"RWF",
|
|
28
|
+
"TZS",
|
|
29
|
+
"UGX",
|
|
30
|
+
"USD",
|
|
31
|
+
"XAF",
|
|
32
|
+
"XDR",
|
|
33
|
+
"ZAR",
|
|
34
|
+
"ZMW"
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class CurrencyStateManager(models.Manager):
|
|
39
|
+
def latest(self):
|
|
40
|
+
return self.get_queryset().prefetch_related("currencies").latest("created_at")
|
|
41
|
+
|
|
42
|
+
class CurrencyState(UUIDPrimaryKeyMixin, TimestampMixin):
|
|
43
|
+
source = models.CharField(max_length=10, choices=CurrencySource.choices)
|
|
44
|
+
objects = CurrencyStateManager()
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def sync(cls) -> Optional['CurrencyState']:
|
|
48
|
+
rates, source = load_rates()
|
|
49
|
+
currencies = []
|
|
50
|
+
|
|
51
|
+
if not rates:
|
|
52
|
+
logger.error("currency_state_sync_failed", source=source)
|
|
53
|
+
raise ValueError("No rates found")
|
|
54
|
+
|
|
55
|
+
with transaction.atomic():
|
|
56
|
+
state = cls.objects.create(source=source)
|
|
57
|
+
|
|
58
|
+
for rate in rates:
|
|
59
|
+
currency = Currency(
|
|
60
|
+
code=rate.currency,
|
|
61
|
+
rate=rate.amount,
|
|
62
|
+
state=state
|
|
63
|
+
)
|
|
64
|
+
currencies.append(currency)
|
|
65
|
+
|
|
66
|
+
Currency.objects.bulk_create(currencies)
|
|
67
|
+
|
|
68
|
+
logger.info("currency_state_synced", state=state.id, source=source)
|
|
69
|
+
|
|
70
|
+
return state
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class Currency(UUIDPrimaryKeyMixin):
|
|
74
|
+
code = models.CharField(max_length=10)
|
|
75
|
+
rate = models.DecimalField(max_digits=10, decimal_places=4)
|
|
76
|
+
state = models.ForeignKey(
|
|
77
|
+
CurrencyState,
|
|
78
|
+
on_delete=models.CASCADE,
|
|
79
|
+
related_name="currencies"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
class Meta:
|
|
83
|
+
ordering = ['code']
|
|
84
|
+
indexes = [
|
|
85
|
+
models.Index(fields=['code', 'state']),
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
def __str__(self):
|
|
89
|
+
return f"{self.code} - {self.rate}"
|
|
90
|
+
|
|
91
|
+
def clean(self):
|
|
92
|
+
if self.code not in AVAILABLES_CURRENCIES:
|
|
93
|
+
raise ValueError(f"Invalid currency code: {self.code}")
|
|
94
|
+
|
|
95
|
+
if self.rate <= 0:
|
|
96
|
+
raise ValueError(f"Invalid currency rate: {self.rate}")
|
|
97
|
+
|
|
98
|
+
def save(self, *args, **kwargs):
|
|
99
|
+
self.clean()
|
|
100
|
+
super().save(*args, **kwargs)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from ninja import Schema
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
from decimal import Decimal
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CurrencySchema(Schema):
|
|
8
|
+
code: str
|
|
9
|
+
rate: Decimal
|
|
10
|
+
|
|
11
|
+
class Config:
|
|
12
|
+
from_attributes = True
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CurrencyStateSchema(Schema):
|
|
16
|
+
id: UUID
|
|
17
|
+
source: str
|
|
18
|
+
created_at: datetime
|
|
19
|
+
updated_at: datetime
|
|
20
|
+
|
|
21
|
+
class Config:
|
|
22
|
+
from_attributes = True
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CurrencyStateDetailSchema(Schema):
|
|
26
|
+
id: UUID
|
|
27
|
+
source: str
|
|
28
|
+
created_at: datetime
|
|
29
|
+
updated_at: datetime
|
|
30
|
+
currencies: dict[str, float]
|
|
31
|
+
|
|
32
|
+
class Config:
|
|
33
|
+
from_attributes = True
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class CurrencyRateSchema(Schema):
|
|
37
|
+
code: str
|
|
38
|
+
rate: Decimal
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from django.conf import settings
|
|
3
|
+
from bcc_rates import BCCBankSource, OXRBankSource, SourceValue
|
|
4
|
+
from oxutils.currency.enums import CurrencySource
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def load_rates() -> tuple[list[SourceValue], CurrencySource]:
|
|
9
|
+
max_retries = 3
|
|
10
|
+
retry_count = 0
|
|
11
|
+
|
|
12
|
+
while retry_count < max_retries:
|
|
13
|
+
try:
|
|
14
|
+
bcc_source = BCCBankSource()
|
|
15
|
+
rates = bcc_source.sync(cache=True)
|
|
16
|
+
return rates, CurrencySource.BCC
|
|
17
|
+
except Exception as e:
|
|
18
|
+
retry_count += 1
|
|
19
|
+
if retry_count < max_retries:
|
|
20
|
+
time.sleep(1)
|
|
21
|
+
else:
|
|
22
|
+
if not getattr(settings, 'OXI_BCC_FALLBACK_ON_OXR', False):
|
|
23
|
+
raise Exception(f"Failed to load rates from BCC: {str(e)}")
|
|
24
|
+
break
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
oxr_source = OXRBankSource()
|
|
28
|
+
rates = oxr_source.sync(cache=True)
|
|
29
|
+
return rates, CurrencySource.OXR
|
|
30
|
+
except Exception as e:
|
|
31
|
+
raise Exception(f"Failed to load rates from both BCC and OXR: {str(e)}")
|
|
@@ -6,12 +6,15 @@ from ninja_extra.exceptions import ValidationError
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def get_absolute_url(url: str, request=None):
|
|
9
|
+
if url.startswith('http'):
|
|
10
|
+
return url
|
|
11
|
+
|
|
9
12
|
if request:
|
|
10
13
|
# Build absolute URL using request
|
|
11
14
|
return request.build_absolute_uri(url)
|
|
12
15
|
else:
|
|
13
|
-
# Fallback: build URL using
|
|
14
|
-
base_url = getattr(settings, '
|
|
16
|
+
# Fallback: build URL using SITE_DOMAIN and domain
|
|
17
|
+
base_url = getattr(settings, 'SITE_DOMAIN', 'http://localhost:8000')
|
|
15
18
|
return urljoin(base_url, url)
|
|
16
19
|
|
|
17
20
|
|
|
@@ -2,7 +2,6 @@ from django.contrib.sites.shortcuts import RequestSite
|
|
|
2
2
|
from django.dispatch import receiver
|
|
3
3
|
import structlog
|
|
4
4
|
from django_structlog import signals
|
|
5
|
-
from cid.locals import get_cid
|
|
6
5
|
from oxutils.settings import oxi_settings
|
|
7
6
|
|
|
8
7
|
|
|
@@ -12,7 +11,6 @@ def bind_domain(request, logger, **kwargs):
|
|
|
12
11
|
current_site = RequestSite(request)
|
|
13
12
|
structlog.contextvars.bind_contextvars(
|
|
14
13
|
domain=current_site.domain,
|
|
15
|
-
cid=get_cid(),
|
|
16
14
|
user_id=str(request.user.pk),
|
|
17
15
|
service=oxi_settings.service_name
|
|
18
16
|
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -21,6 +21,8 @@ class OxUtilsSettings(BaseSettings):
|
|
|
21
21
|
|
|
22
22
|
# Service
|
|
23
23
|
service_name: Optional[str] = 'Oxutils'
|
|
24
|
+
site_name: Optional[str] = 'Oxiliere'
|
|
25
|
+
site_domain: Optional[str] = 'oxiliere.com'
|
|
24
26
|
|
|
25
27
|
# Auth JWT Settings (JWT_SIGNING_KEY)
|
|
26
28
|
jwt_signing_key: Optional[str] = None
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
# Oxiliere Audit settings
|
|
2
|
-
|
|
3
|
-
AUDITLOG_DISABLE_REMOTE_ADDR = False
|
|
4
|
-
AUDITLOG_MASK_TRACKING_FIELDS = (
|
|
5
|
-
"password",
|
|
6
|
-
"api_key",
|
|
7
|
-
"secret_token",
|
|
8
|
-
"token",
|
|
9
|
-
)
|
|
10
|
-
|
|
11
|
-
AUDITLOG_EXCLUDE_TRACKING_FIELDS = (
|
|
12
|
-
"created_at",
|
|
13
|
-
"updated_at",
|
|
14
|
-
)
|
|
15
|
-
|
|
16
|
-
CID_GENERATE = False
|
|
17
|
-
|
|
18
|
-
AUDITLOG_CID_GETTER = "cid.locals.get_cid"
|
|
19
|
-
AUDITLOG_LOGENTRY_MODEL = "auditlog.LogEntry"
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{oxutils-0.1.2/src/oxutils/mixins → oxutils-0.1.4/src/oxutils/currency/migrations}/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|