oxutils 0.1.4__tar.gz → 0.1.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. {oxutils-0.1.4 → oxutils-0.1.6}/PKG-INFO +81 -4
  2. {oxutils-0.1.4 → oxutils-0.1.6}/README.md +73 -2
  3. {oxutils-0.1.4 → oxutils-0.1.6}/pyproject.toml +18 -2
  4. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/__init__.py +1 -1
  5. oxutils-0.1.6/src/oxutils/constants.py +2 -0
  6. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/currency/controllers.py +3 -2
  7. oxutils-0.1.6/src/oxutils/currency/utils.py +69 -0
  8. oxutils-0.1.6/src/oxutils/oxiliere/admin.py +3 -0
  9. oxutils-0.1.6/src/oxutils/oxiliere/apps.py +6 -0
  10. oxutils-0.1.6/src/oxutils/oxiliere/cacheops.py +7 -0
  11. oxutils-0.1.6/src/oxutils/oxiliere/caches.py +33 -0
  12. oxutils-0.1.6/src/oxutils/oxiliere/controllers.py +36 -0
  13. oxutils-0.1.6/src/oxutils/oxiliere/enums.py +10 -0
  14. oxutils-0.1.6/src/oxutils/oxiliere/management/commands/__init__.py +0 -0
  15. oxutils-0.1.6/src/oxutils/oxiliere/management/commands/init_oxiliere_system.py +86 -0
  16. oxutils-0.1.6/src/oxutils/oxiliere/middleware.py +97 -0
  17. oxutils-0.1.6/src/oxutils/oxiliere/migrations/__init__.py +0 -0
  18. oxutils-0.1.6/src/oxutils/oxiliere/models.py +55 -0
  19. oxutils-0.1.6/src/oxutils/oxiliere/permissions.py +104 -0
  20. oxutils-0.1.6/src/oxutils/oxiliere/schemas.py +65 -0
  21. oxutils-0.1.6/src/oxutils/oxiliere/settings.py +17 -0
  22. oxutils-0.1.6/src/oxutils/oxiliere/tests.py +3 -0
  23. oxutils-0.1.6/src/oxutils/oxiliere/utils.py +76 -0
  24. oxutils-0.1.6/src/oxutils/pdf/__init__.py +10 -0
  25. oxutils-0.1.6/src/oxutils/pdf/printer.py +81 -0
  26. oxutils-0.1.6/src/oxutils/pdf/utils.py +94 -0
  27. oxutils-0.1.6/src/oxutils/pdf/views.py +208 -0
  28. oxutils-0.1.6/src/oxutils/py.typed +0 -0
  29. oxutils-0.1.6/src/oxutils/s3/__init__.py +0 -0
  30. oxutils-0.1.6/src/oxutils/users/__init__.py +0 -0
  31. oxutils-0.1.6/src/oxutils/users/admin.py +3 -0
  32. oxutils-0.1.6/src/oxutils/users/apps.py +6 -0
  33. oxutils-0.1.6/src/oxutils/users/migrations/__init__.py +0 -0
  34. oxutils-0.1.6/src/oxutils/users/models.py +88 -0
  35. oxutils-0.1.6/src/oxutils/users/tests.py +3 -0
  36. oxutils-0.1.6/src/oxutils/users/utils.py +15 -0
  37. oxutils-0.1.4/src/oxutils/currency/utils.py +0 -31
  38. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/apps.py +0 -0
  39. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/audit/__init__.py +0 -0
  40. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/audit/apps.py +0 -0
  41. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/audit/export.py +0 -0
  42. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/audit/masks.py +0 -0
  43. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/audit/migrations/0001_initial.py +0 -0
  44. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/audit/migrations/__init__.py +0 -0
  45. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/audit/models.py +0 -0
  46. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/audit/settings.py +0 -0
  47. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/audit/utils.py +0 -0
  48. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/celery/__init__.py +0 -0
  49. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/celery/base.py +0 -0
  50. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/celery/settings.py +0 -0
  51. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/conf.py +0 -0
  52. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/context/__init__.py +0 -0
  53. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/context/site_name_processor.py +0 -0
  54. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/currency/__init__.py +0 -0
  55. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/currency/admin.py +0 -0
  56. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/currency/apps.py +0 -0
  57. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/currency/enums.py +0 -0
  58. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/currency/migrations/0001_initial.py +0 -0
  59. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/currency/migrations/__init__.py +0 -0
  60. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/currency/models.py +0 -0
  61. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/currency/schemas.py +0 -0
  62. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/currency/tests.py +0 -0
  63. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/enums/__init__.py +0 -0
  64. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/enums/audit.py +0 -0
  65. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/enums/invoices.py +0 -0
  66. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/exceptions.py +0 -0
  67. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/functions.py +0 -0
  68. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/jwt/__init__.py +0 -0
  69. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/jwt/auth.py +0 -0
  70. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/jwt/client.py +0 -0
  71. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/jwt/constants.py +0 -0
  72. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/locale/fr/LC_MESSAGES/django.po +0 -0
  73. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/logger/__init__.py +0 -0
  74. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/logger/receivers.py +0 -0
  75. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/logger/settings.py +0 -0
  76. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/mixins/__init__.py +0 -0
  77. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/mixins/base.py +0 -0
  78. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/mixins/schemas.py +0 -0
  79. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/mixins/services.py +0 -0
  80. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/models/__init__.py +0 -0
  81. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/models/base.py +0 -0
  82. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/models/billing.py +0 -0
  83. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/models/invoice.py +0 -0
  84. {oxutils-0.1.4/src/oxutils/s3 → oxutils-0.1.6/src/oxutils/oxiliere}/__init__.py +0 -0
  85. /oxutils-0.1.4/src/oxutils/py.typed → /oxutils-0.1.6/src/oxutils/oxiliere/management/__init__.py +0 -0
  86. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/s3/settings.py +0 -0
  87. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/s3/storages.py +0 -0
  88. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/settings.py +0 -0
  89. {oxutils-0.1.4 → oxutils-0.1.6}/src/oxutils/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oxutils
3
- Version: 0.1.4
3
+ Version: 0.1.6
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
@@ -16,7 +16,6 @@ Classifier: Programming Language :: Python :: 3
16
16
  Classifier: Programming Language :: Python :: 3.12
17
17
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
18
  Classifier: Topic :: Internet :: WWW/HTTP
19
- Requires-Dist: bcc-rates>=1.1.0
20
19
  Requires-Dist: boto3>=1.41.5
21
20
  Requires-Dist: celery>=5.5.3
22
21
  Requires-Dist: cryptography>=46.0.3
@@ -31,12 +30,19 @@ Requires-Dist: jwcrypto>=1.5.6
31
30
  Requires-Dist: pydantic-settings>=2.12.0
32
31
  Requires-Dist: pyjwt>=2.10.1
33
32
  Requires-Dist: requests>=2.32.5
33
+ Requires-Dist: bcc-rates>=1.1.0 ; extra == 'currency'
34
+ Requires-Dist: django-cacheops>=7.2 ; extra == 'oxiliere'
35
+ Requires-Dist: django-tenants>=3.9.0 ; extra == 'oxiliere'
36
+ Requires-Dist: weasyprint>=67.0 ; extra == 'pdf'
34
37
  Requires-Python: >=3.12
35
38
  Project-URL: Changelog, https://github.com/oxiliere/oxutils/blob/main/CHANGELOG.md
36
39
  Project-URL: Documentation, https://github.com/oxiliere/oxutils/tree/main/docs
37
40
  Project-URL: Homepage, https://github.com/oxiliere/oxutils
38
41
  Project-URL: Issues, https://github.com/oxiliere/oxutils/issues
39
42
  Project-URL: Repository, https://github.com/oxiliere/oxutils
43
+ Provides-Extra: currency
44
+ Provides-Extra: oxiliere
45
+ Provides-Extra: pdf
40
46
  Description-Content-Type: text/markdown
41
47
 
42
48
  # OxUtils
@@ -46,7 +52,7 @@ Description-Content-Type: text/markdown
46
52
  [![PyPI version](https://img.shields.io/pypi/v/oxutils.svg)](https://pypi.org/project/oxutils/)
47
53
  [![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/)
48
54
  [![Django 5.0+](https://img.shields.io/badge/django-5.0+-green.svg)](https://www.djangoproject.com/)
49
- [![Tests](https://img.shields.io/badge/tests-145%20passed-success.svg)](tests/)
55
+ [![Tests](https://img.shields.io/badge/tests-201%20passed-success.svg)](tests/)
50
56
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
51
57
  [![Code style: ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)](https://github.com/astral-sh/ruff)
52
58
 
@@ -60,6 +66,9 @@ Description-Content-Type: text/markdown
60
66
  - 🛠️ **Django Mixins** - UUID, timestamps, user tracking
61
67
  - ⚡ **Custom Exceptions** - Standardized API errors
62
68
  - 🎨 **Context Processors** - Site name and domain for templates
69
+ - 💱 **Currency Module** - Multi-source exchange rates (BCC/OXR)
70
+ - 📄 **PDF Generation** - WeasyPrint integration for Django
71
+ - 🏢 **Multi-Tenant** - PostgreSQL schema-based isolation
63
72
 
64
73
  ---
65
74
 
@@ -141,6 +150,7 @@ TEMPLATES = [{
141
150
 
142
151
  ## Documentation
143
152
 
153
+ ### Core Modules
144
154
  - **[Settings](docs/settings.md)** - Configuration reference
145
155
  - **[JWT](docs/jwt.md)** - Authentication
146
156
  - **[S3](docs/s3.md)** - Storage backends
@@ -149,6 +159,11 @@ TEMPLATES = [{
149
159
  - **[Mixins](docs/mixins.md)** - Model/service mixins
150
160
  - **[Celery](docs/celery.md)** - Task processing
151
161
 
162
+ ### Additional Modules
163
+ - **[Currency](docs/currency.md)** - Exchange rates management
164
+ - **[PDF](docs/pdf.md)** - PDF generation with WeasyPrint
165
+ - **[Oxiliere](docs/oxiliere.md)** - Multi-tenant architecture
166
+
152
167
  ## Requirements
153
168
 
154
169
  - Python 3.12+
@@ -161,7 +176,7 @@ TEMPLATES = [{
161
176
  git clone https://github.com/oxiliere/oxutils.git
162
177
  cd oxutils
163
178
  uv sync
164
- uv run pytest # 145 tests
179
+ uv run pytest # 201 tests passing, 4 skipped
165
180
  ```
166
181
 
167
182
  ### Creating Migrations
@@ -176,6 +191,19 @@ uv run make_migrations.py
176
191
 
177
192
  See [MIGRATIONS.md](MIGRATIONS.md) for detailed documentation.
178
193
 
194
+ ## Optional Dependencies
195
+
196
+ ```bash
197
+ # Multi-tenant support
198
+ uv add oxutils[oxiliere]
199
+
200
+ # PDF generation
201
+ uv add oxutils[pdf]
202
+
203
+ # Development tools
204
+ uv add oxutils[dev]
205
+ ```
206
+
179
207
  ## Advanced Examples
180
208
 
181
209
  ### JWT with Django Ninja
@@ -210,6 +238,55 @@ export = export_logs_from_date(from_date=from_date)
210
238
  print(f"Exported to {export.data.url}")
211
239
  ```
212
240
 
241
+ ### Currency Exchange Rates
242
+
243
+ ```python
244
+ from oxutils.currency.models import CurrencyState
245
+
246
+ # Sync rates from BCC (with OXR fallback)
247
+ state = CurrencyState.sync()
248
+
249
+ # Get latest rates
250
+ latest = CurrencyState.objects.latest()
251
+ usd_rate = latest.currencies.get(code='USD').rate
252
+ eur_rate = latest.currencies.get(code='EUR').rate
253
+ ```
254
+
255
+ ### PDF Generation
256
+
257
+ ```python
258
+ from oxutils.pdf.printer import Printer
259
+ from oxutils.pdf.views import WeasyTemplateView
260
+
261
+ # Standalone PDF generation
262
+ printer = Printer(
263
+ template_name='invoice.html',
264
+ context={'invoice': invoice},
265
+ stylesheets=['css/invoice.css']
266
+ )
267
+ pdf_bytes = printer.write_pdf()
268
+
269
+ # Class-based view
270
+ class InvoicePDFView(WeasyTemplateView):
271
+ template_name = 'invoice.html'
272
+ pdf_filename = 'invoice.pdf'
273
+ pdf_stylesheets = ['css/invoice.css']
274
+ ```
275
+
276
+ ### Multi-Tenant Setup
277
+
278
+ ```python
279
+ # settings.py
280
+ TENANT_MODEL = "oxiliere.Tenant"
281
+ MIDDLEWARE = [
282
+ 'oxutils.oxiliere.middleware.TenantMainMiddleware', # First!
283
+ # other middleware...
284
+ ]
285
+
286
+ # All requests must include X-Organization-ID header
287
+ # Data is automatically isolated per tenant schema
288
+ ```
289
+
213
290
  ## License
214
291
 
215
292
  Apache 2.0 License - see [LICENSE](LICENSE)
@@ -5,7 +5,7 @@
5
5
  [![PyPI version](https://img.shields.io/pypi/v/oxutils.svg)](https://pypi.org/project/oxutils/)
6
6
  [![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/)
7
7
  [![Django 5.0+](https://img.shields.io/badge/django-5.0+-green.svg)](https://www.djangoproject.com/)
8
- [![Tests](https://img.shields.io/badge/tests-145%20passed-success.svg)](tests/)
8
+ [![Tests](https://img.shields.io/badge/tests-201%20passed-success.svg)](tests/)
9
9
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
10
10
  [![Code style: ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)](https://github.com/astral-sh/ruff)
11
11
 
@@ -19,6 +19,9 @@
19
19
  - 🛠️ **Django Mixins** - UUID, timestamps, user tracking
20
20
  - ⚡ **Custom Exceptions** - Standardized API errors
21
21
  - 🎨 **Context Processors** - Site name and domain for templates
22
+ - 💱 **Currency Module** - Multi-source exchange rates (BCC/OXR)
23
+ - 📄 **PDF Generation** - WeasyPrint integration for Django
24
+ - 🏢 **Multi-Tenant** - PostgreSQL schema-based isolation
22
25
 
23
26
  ---
24
27
 
@@ -100,6 +103,7 @@ TEMPLATES = [{
100
103
 
101
104
  ## Documentation
102
105
 
106
+ ### Core Modules
103
107
  - **[Settings](docs/settings.md)** - Configuration reference
104
108
  - **[JWT](docs/jwt.md)** - Authentication
105
109
  - **[S3](docs/s3.md)** - Storage backends
@@ -108,6 +112,11 @@ TEMPLATES = [{
108
112
  - **[Mixins](docs/mixins.md)** - Model/service mixins
109
113
  - **[Celery](docs/celery.md)** - Task processing
110
114
 
115
+ ### Additional Modules
116
+ - **[Currency](docs/currency.md)** - Exchange rates management
117
+ - **[PDF](docs/pdf.md)** - PDF generation with WeasyPrint
118
+ - **[Oxiliere](docs/oxiliere.md)** - Multi-tenant architecture
119
+
111
120
  ## Requirements
112
121
 
113
122
  - Python 3.12+
@@ -120,7 +129,7 @@ TEMPLATES = [{
120
129
  git clone https://github.com/oxiliere/oxutils.git
121
130
  cd oxutils
122
131
  uv sync
123
- uv run pytest # 145 tests
132
+ uv run pytest # 201 tests passing, 4 skipped
124
133
  ```
125
134
 
126
135
  ### Creating Migrations
@@ -135,6 +144,19 @@ uv run make_migrations.py
135
144
 
136
145
  See [MIGRATIONS.md](MIGRATIONS.md) for detailed documentation.
137
146
 
147
+ ## Optional Dependencies
148
+
149
+ ```bash
150
+ # Multi-tenant support
151
+ uv add oxutils[oxiliere]
152
+
153
+ # PDF generation
154
+ uv add oxutils[pdf]
155
+
156
+ # Development tools
157
+ uv add oxutils[dev]
158
+ ```
159
+
138
160
  ## Advanced Examples
139
161
 
140
162
  ### JWT with Django Ninja
@@ -169,6 +191,55 @@ export = export_logs_from_date(from_date=from_date)
169
191
  print(f"Exported to {export.data.url}")
170
192
  ```
171
193
 
194
+ ### Currency Exchange Rates
195
+
196
+ ```python
197
+ from oxutils.currency.models import CurrencyState
198
+
199
+ # Sync rates from BCC (with OXR fallback)
200
+ state = CurrencyState.sync()
201
+
202
+ # Get latest rates
203
+ latest = CurrencyState.objects.latest()
204
+ usd_rate = latest.currencies.get(code='USD').rate
205
+ eur_rate = latest.currencies.get(code='EUR').rate
206
+ ```
207
+
208
+ ### PDF Generation
209
+
210
+ ```python
211
+ from oxutils.pdf.printer import Printer
212
+ from oxutils.pdf.views import WeasyTemplateView
213
+
214
+ # Standalone PDF generation
215
+ printer = Printer(
216
+ template_name='invoice.html',
217
+ context={'invoice': invoice},
218
+ stylesheets=['css/invoice.css']
219
+ )
220
+ pdf_bytes = printer.write_pdf()
221
+
222
+ # Class-based view
223
+ class InvoicePDFView(WeasyTemplateView):
224
+ template_name = 'invoice.html'
225
+ pdf_filename = 'invoice.pdf'
226
+ pdf_stylesheets = ['css/invoice.css']
227
+ ```
228
+
229
+ ### Multi-Tenant Setup
230
+
231
+ ```python
232
+ # settings.py
233
+ TENANT_MODEL = "oxiliere.Tenant"
234
+ MIDDLEWARE = [
235
+ 'oxutils.oxiliere.middleware.TenantMainMiddleware', # First!
236
+ # other middleware...
237
+ ]
238
+
239
+ # All requests must include X-Organization-ID header
240
+ # Data is automatically isolated per tenant schema
241
+ ```
242
+
172
243
  ## License
173
244
 
174
245
  Apache 2.0 License - see [LICENSE](LICENSE)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "oxutils"
3
- version = "0.1.4"
3
+ version = "0.1.6"
4
4
  description = "Production-ready utilities for Django applications in the Oxiliere ecosystem"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -22,7 +22,6 @@ classifiers = [
22
22
  "Topic :: Internet :: WWW/HTTP",
23
23
  ]
24
24
  dependencies = [
25
- "bcc-rates>=1.1.0",
26
25
  "boto3>=1.41.5",
27
26
  "celery>=5.5.3",
28
27
  "cryptography>=46.0.3",
@@ -55,6 +54,22 @@ members = [
55
54
  ".",
56
55
  ]
57
56
 
57
+ [tool.uv.sources]
58
+ oxutils = { workspace = true }
59
+
60
+ [project.optional-dependencies]
61
+ currency = [
62
+ "bcc-rates>=1.1.0",
63
+ ]
64
+
65
+ oxiliere = [
66
+ "django-cacheops>=7.2",
67
+ "django-tenants>=3.9.0",
68
+ ]
69
+ pdf = [
70
+ "weasyprint>=67.0",
71
+ ]
72
+
58
73
  [dependency-groups]
59
74
  dev = [
60
75
  "pytest>=8.0.0",
@@ -66,6 +81,7 @@ dev = [
66
81
  "ruff>=0.8.0",
67
82
  ]
68
83
 
84
+
69
85
  [tool.ruff]
70
86
  line-length = 100
71
87
  target-version = "py312"
@@ -10,7 +10,7 @@ This package provides:
10
10
  - Custom exceptions
11
11
  """
12
12
 
13
- __version__ = "0.1.4"
13
+ __version__ = "0.1.6"
14
14
 
15
15
  from oxutils.settings import oxi_settings
16
16
  from oxutils.conf import UTILS_APPS, AUDIT_MIDDLEWARE
@@ -0,0 +1,2 @@
1
+ ORGANIZATION_QUERY_KEY = 'organization_id'
2
+ ORGANIZATION_HEADER_KEY = 'X-Organization-ID'
@@ -11,12 +11,13 @@ from ninja_extra.pagination import (
11
11
  from ninja.errors import HttpError
12
12
  from uuid import UUID
13
13
  import structlog
14
- from currency.models import CurrencyState
15
- from currency.schemas import (
14
+ from oxutils.currency.models import CurrencyState
15
+ from oxutils.currency.schemas import (
16
16
  CurrencyStateSchema,
17
17
  CurrencyStateDetailSchema,
18
18
  )
19
19
 
20
+
20
21
  logger = structlog.get_logger(__name__)
21
22
 
22
23
 
@@ -0,0 +1,69 @@
1
+ import time
2
+ from django.conf import settings
3
+ from django.db import transaction
4
+ from bcc_rates import BCCBankSource, OXRBankSource, SourceValue
5
+ from oxutils.currency.enums import CurrencySource
6
+ from oxutils.currency.schemas import CurrencyStateDetailSchema
7
+ import structlog
8
+
9
+ logger = structlog.get_logger(__name__)
10
+
11
+
12
+ def load_rates() -> tuple[list[SourceValue], CurrencySource]:
13
+ max_retries = 3
14
+ retry_count = 0
15
+
16
+ while retry_count < max_retries:
17
+ try:
18
+ bcc_source = BCCBankSource()
19
+ rates = bcc_source.sync(cache=True)
20
+ return rates, CurrencySource.BCC
21
+ except Exception as e:
22
+ retry_count += 1
23
+ if retry_count < max_retries:
24
+ time.sleep(1)
25
+ else:
26
+ if not getattr(settings, 'OXI_BCC_FALLBACK_ON_OXR', False):
27
+ raise Exception(f"Failed to load rates from BCC: {str(e)}")
28
+ break
29
+
30
+ try:
31
+ oxr_source = OXRBankSource()
32
+ rates = oxr_source.sync(cache=True)
33
+ return rates, CurrencySource.OXR
34
+ except Exception as e:
35
+ raise Exception(f"Failed to load rates from both BCC and OXR: {str(e)}")
36
+
37
+ @transaction.atomic
38
+ def update_rates(state: CurrencyStateDetailSchema):
39
+ from oxutils.currency.models import CurrencyState, Currency
40
+
41
+ if CurrencyState.objects.filter(id=state.id).exists():
42
+ logger.info("currency_state_exists", id=state.id)
43
+ return
44
+
45
+ if not len(state.currencies.keys()):
46
+ logger.info("currency_state_no_currencies", id=state.id)
47
+ return
48
+
49
+ _state = CurrencyState.objects.create(
50
+ id=state.id,
51
+ source=state.source,
52
+ created_at=state.created_at,
53
+ updated_at=state.updated_at,
54
+ )
55
+
56
+ currencies = []
57
+
58
+ for key, value in state.currencies.items():
59
+ currencies.append(
60
+ Currency(
61
+ code=key,
62
+ rate=value,
63
+ state=_state
64
+ )
65
+ )
66
+
67
+ Currency.objects.bulk_create(currencies)
68
+
69
+ logger.info("currency_state_updated", id=state.id)
@@ -0,0 +1,3 @@
1
+ from django.contrib import admin
2
+
3
+ # Register your models here.
@@ -0,0 +1,6 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class OxiliereConfig(AppConfig):
5
+ default_auto_field = 'django.db.models.BigAutoField'
6
+ name = 'oxiliere'
@@ -0,0 +1,7 @@
1
+ from django.db import connection
2
+
3
+
4
+ def cacheops_prefix(query):
5
+ if connection.schema_name:
6
+ return '%s:' % connection.schema_name
7
+ return 'default:'
@@ -0,0 +1,33 @@
1
+ from cacheops import cached_as, cached
2
+ from oxutils.oxiliere.models import Tenant, TenantUser
3
+ from django_tenants.utils import get_tenant_model
4
+
5
+
6
+ TenantModel = get_tenant_model()
7
+
8
+
9
+ @cached_as(TenantModel, timeout=60*15)
10
+ def get_tenant_by_oxi_id(oxi_id: str):
11
+ return TenantModel.objects.get(oxi_id=oxi_id)
12
+
13
+
14
+ @cached_as(TenantModel, timeout=60*15)
15
+ def get_tenant_by_schema_name(schema_name: str) -> Tenant:
16
+ return TenantModel.objects.get(schema_name=schema_name)
17
+
18
+
19
+ @cached_as(TenantUser, timeout=60*15)
20
+ def get_tenant_user(oxi_org_id: str, oxi_user_id: str):
21
+ return TenantUser.objects.get(
22
+ tenant__oxi_id=oxi_org_id,
23
+ user__oxi_id=oxi_user_id
24
+ )
25
+
26
+ @cached(timeout=60*15)
27
+ def get_system_tenant():
28
+ from oxutils.oxiliere.utils import oxid_to_schema_name
29
+
30
+ system_schema_name = oxid_to_schema_name(
31
+ getattr(settings, 'OXI_SYSTEM_TENANT', 'tenant_oxisystem')
32
+ )
33
+ return get_tenant_model().objects.get(schema_name=system_schema_name)
@@ -0,0 +1,36 @@
1
+ from ninja_extra import (
2
+ api_controller,
3
+ ControllerBase,
4
+ http_post,
5
+ )
6
+ from .permissions import OxiliereServicePermission
7
+ from .schemas import CreateTenantSchema
8
+ from oxutils.mixins.schemas import ResponseSchema
9
+
10
+
11
+ @api_controller(
12
+ '/setup',
13
+ tags=['Setup'],
14
+ auth=None,
15
+ permissions=[
16
+ OxiliereServicePermission(),
17
+ ]
18
+ )
19
+ class SetupController(ControllerBase):
20
+
21
+ @http_post(
22
+ '/init',
23
+ response=ResponseSchema,
24
+ )
25
+ def init(self, payload: CreateTenantSchema):
26
+ try:
27
+ payload.create_tenant()
28
+ except Exception as e:
29
+ return ResponseSchema(
30
+ code='initialization_failed',
31
+ detail=str(e)
32
+ )
33
+ return ResponseSchema(
34
+ code='success',
35
+ detail='Tenant initialized successfully'
36
+ )
@@ -0,0 +1,10 @@
1
+ from django.db.models import TextChoices
2
+
3
+
4
+
5
+ class TenantStatus(TextChoices):
6
+ ACTIVE = 'active'
7
+ INACTIVE = 'inactive'
8
+ SUSPENDED = 'suspended'
9
+ DELETED = 'deleted'
10
+ REMOVED = 'removed'
@@ -0,0 +1,86 @@
1
+ import uuid
2
+ from django.core.management.base import BaseCommand
3
+ from django.conf import settings
4
+ from django.db import transaction
5
+ from django.contrib.auth import get_user_model
6
+ from django_tenants.utils import get_tenant_model
7
+ from oxutils.oxiliere.models import Domain, TenantUser
8
+ from oxutils.oxiliere.utils import oxid_to_schema_name
9
+
10
+
11
+
12
+ class Command(BaseCommand):
13
+ help = 'Initialise le tenant système Oxiliere'
14
+
15
+ @transaction.atomic
16
+ def handle(self, *args, **options):
17
+ TenantModel = get_tenant_model()
18
+ UserModel = get_user_model()
19
+
20
+ # Configuration du tenant système depuis settings
21
+ system_slug = getattr(settings, 'OXI_SYSTEM_TENANT', 'tenant_oxisystem')
22
+ schema_name = oxid_to_schema_name(system_slug)
23
+ system_domain = getattr(settings, 'OXI_SYSTEM_DOMAIN', 'system.oxiliere.com')
24
+ owner_email = getattr(settings, 'OXI_SYSTEM_OWNER_EMAIL', 'dev@oxiliere.com')
25
+ owner_oxi_id = uuid.uuid4()
26
+
27
+ self.stdout.write(self.style.WARNING(f'Initialisation du tenant système...'))
28
+
29
+ # Vérifier si le tenant système existe déjà
30
+ if TenantModel.objects.filter(schema_name=schema_name).exists():
31
+ self.stdout.write(self.style.ERROR(f'Le tenant système "{schema_name}" existe déjà!'))
32
+ return
33
+
34
+ # Créer le tenant système
35
+ self.stdout.write(f'Création du tenant système: {schema_name}')
36
+ tenant = TenantModel.objects.create(
37
+ name='Oxiliere System',
38
+ schema_name=schema_name,
39
+ oxi_id=system_slug,
40
+ subscription_plan='system',
41
+ subscription_status='active',
42
+ )
43
+ self.stdout.write(self.style.SUCCESS(f'✓ Tenant système créé: {tenant.name} ({tenant.schema_name})'))
44
+
45
+ # Créer le domaine pour le tenant système
46
+ self.stdout.write(f'Création du domaine: {system_domain}')
47
+ domain = Domain.objects.create(
48
+ domain=system_domain,
49
+ tenant=tenant,
50
+ is_primary=True
51
+ )
52
+ self.stdout.write(self.style.SUCCESS(f'✓ Domaine créé: {domain.domain}'))
53
+
54
+ self.stdout.write(f'Création du superuser: {owner_email}')
55
+ try:
56
+ superuser = UserModel.objects.get(email=owner_email)
57
+ self.stdout.write(self.style.WARNING(f'⚠ Superuser existe déjà: {superuser.email}'))
58
+ except UserModel.DoesNotExist:
59
+ superuser = UserModel.objects.create_superuser(
60
+ email=owner_email,
61
+ oxi_id=owner_oxi_id,
62
+ first_name='System',
63
+ last_name='Admin'
64
+ )
65
+ self.stdout.write(self.style.SUCCESS(f'✓ Superuser créé: {superuser.email}'))
66
+
67
+ # Lier le superuser au tenant système
68
+ self.stdout.write('Liaison du superuser au tenant système')
69
+ tenant_user, created = TenantUser.objects.get_or_create(
70
+ tenant=tenant,
71
+ user=superuser,
72
+ defaults={
73
+ 'is_owner': True,
74
+ 'is_admin': True,
75
+ }
76
+ )
77
+ if created:
78
+ self.stdout.write(self.style.SUCCESS(f'✓ Superuser lié au tenant système'))
79
+ else:
80
+ self.stdout.write(self.style.WARNING(f'⚠ Liaison existe déjà'))
81
+
82
+ self.stdout.write(self.style.SUCCESS('\n=== Initialisation terminée avec succès ==='))
83
+ self.stdout.write(f'Tenant: {tenant.name}')
84
+ self.stdout.write(f'Schema: {tenant.schema_name}')
85
+ self.stdout.write(f'Domain: {domain.domain}')
86
+ self.stdout.write(f'Superuser: {owner_email}')