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
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
# Oxiliere Utils.
|
|
2
|
+
# Copyright (C) Oxiliere
|
|
3
|
+
# This file is distributed under the same license as the PACKAGE package.
|
|
4
|
+
# Edimedia Mutoke <eddycondor07@gmail.com>, 2025.
|
|
5
|
+
#
|
|
6
|
+
msgid ""
|
|
7
|
+
msgstr ""
|
|
8
|
+
"Project-Id-Version: Oxutils 1.0\n"
|
|
9
|
+
"Report-Msgid-Bugs-To: \n"
|
|
10
|
+
"POT-Creation-Date: 2025-12-02 12:14+0200\n"
|
|
11
|
+
"PO-Revision-Date: 2025-12-02 12:15+0200\n"
|
|
12
|
+
"Last-Translator: Oxiliere Team\n"
|
|
13
|
+
"Language-Team: French\n"
|
|
14
|
+
"Language: fr\n"
|
|
15
|
+
"MIME-Version: 1.0\n"
|
|
16
|
+
"Content-Type: text/plain; charset=UTF-8\n"
|
|
17
|
+
"Content-Transfer-Encoding: 8bit\n"
|
|
18
|
+
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
|
19
|
+
|
|
20
|
+
msgid "Oxiliere Utilities"
|
|
21
|
+
msgstr "Utilitaires Oxiliere"
|
|
22
|
+
|
|
23
|
+
msgid "Oxutils Export"
|
|
24
|
+
msgstr "Export Oxutils"
|
|
25
|
+
|
|
26
|
+
msgid "Failed"
|
|
27
|
+
msgstr "Échoué"
|
|
28
|
+
|
|
29
|
+
msgid "Pending"
|
|
30
|
+
msgstr "En attente"
|
|
31
|
+
|
|
32
|
+
msgid "Success"
|
|
33
|
+
msgstr "Succès"
|
|
34
|
+
|
|
35
|
+
msgid "We encountered an error, please try again later."
|
|
36
|
+
msgstr "Nous avons rencontré une erreur, veuillez réessayer plus tard."
|
|
37
|
+
|
|
38
|
+
msgid "The requested resource does not exist"
|
|
39
|
+
msgstr "La ressource demandée n'existe pas"
|
|
40
|
+
|
|
41
|
+
msgid "Validation error"
|
|
42
|
+
msgstr "Erreur de validation"
|
|
43
|
+
|
|
44
|
+
msgid "The operation conflicts with existing data"
|
|
45
|
+
msgstr "L'opération est en conflit avec les données existantes"
|
|
46
|
+
|
|
47
|
+
msgid "A resource with these details already exists"
|
|
48
|
+
msgstr "Une ressource avec ces détails existe déjà"
|
|
49
|
+
|
|
50
|
+
msgid "You do not have permission to perform this action"
|
|
51
|
+
msgstr "Vous n'avez pas la permission d'effectuer cette action"
|
|
52
|
+
|
|
53
|
+
msgid "Authentication is required"
|
|
54
|
+
msgstr "L'authentification est requise"
|
|
55
|
+
|
|
56
|
+
msgid "Invalid parameter provided"
|
|
57
|
+
msgstr "Paramètre invalide fourni"
|
|
58
|
+
|
|
59
|
+
msgid "Required parameter is missing"
|
|
60
|
+
msgstr "Paramètre requis manquant"
|
|
61
|
+
|
|
62
|
+
msgid "An unexpected error occurred while processing your request"
|
|
63
|
+
msgstr "Une erreur inattendue s'est produite lors du traitement de votre demande"
|
|
64
|
+
|
|
65
|
+
msgid "One or more referenced resources do not exist"
|
|
66
|
+
msgstr "Une ou plusieurs ressources référencées n'existent pas"
|
|
67
|
+
|
|
68
|
+
#, python-brace-format
|
|
69
|
+
msgid "Required parameter {exc} is missing"
|
|
70
|
+
msgstr "Le paramètre requis {exc} est manquant"
|
|
71
|
+
|
|
72
|
+
msgid ""
|
|
73
|
+
"An unexpected error occurred. Please try again later or contact support."
|
|
74
|
+
msgstr "Une erreur inattendue s'est produite. Veuillez réessayer plus tard ou contacter le support."
|
|
75
|
+
|
|
76
|
+
msgid "Credit card"
|
|
77
|
+
msgstr "Carte bancaire"
|
|
78
|
+
|
|
79
|
+
msgid "PayPal"
|
|
80
|
+
msgstr "PayPal"
|
|
81
|
+
|
|
82
|
+
msgid "Bank transfer"
|
|
83
|
+
msgstr "Virement bancaire"
|
|
84
|
+
|
|
85
|
+
msgid "Stripe"
|
|
86
|
+
msgstr "Stripe"
|
|
87
|
+
|
|
88
|
+
msgid "US Dollar"
|
|
89
|
+
msgstr "Dollar américain"
|
|
90
|
+
|
|
91
|
+
msgid "Congolese Franc"
|
|
92
|
+
msgstr "Franc congolais"
|
|
93
|
+
|
|
94
|
+
msgid "billing name"
|
|
95
|
+
msgstr "Nom de facturation"
|
|
96
|
+
|
|
97
|
+
msgid "Full name for billing"
|
|
98
|
+
msgstr "Nom complet pour la facturation"
|
|
99
|
+
|
|
100
|
+
msgid "billing email"
|
|
101
|
+
msgstr "email de facturation"
|
|
102
|
+
|
|
103
|
+
msgid "Email to receive invoices"
|
|
104
|
+
msgstr "Email pour recevoir les factures"
|
|
105
|
+
|
|
106
|
+
msgid "company name"
|
|
107
|
+
msgstr "Nom de l'entreprise"
|
|
108
|
+
|
|
109
|
+
msgid "Company name (optional)"
|
|
110
|
+
msgstr "Nom de l'entreprise (optionnel)"
|
|
111
|
+
|
|
112
|
+
msgid "VAT number"
|
|
113
|
+
msgstr "Numéro de TVA"
|
|
114
|
+
|
|
115
|
+
msgid "VAT or tax identification number"
|
|
116
|
+
msgstr "Numéro de TVA ou d'identification fiscale"
|
|
117
|
+
|
|
118
|
+
msgid "address"
|
|
119
|
+
msgstr "Adresse"
|
|
120
|
+
|
|
121
|
+
msgid "city"
|
|
122
|
+
msgstr "Ville"
|
|
123
|
+
|
|
124
|
+
msgid "postal code"
|
|
125
|
+
msgstr "Code postal"
|
|
126
|
+
|
|
127
|
+
msgid "country"
|
|
128
|
+
msgstr "Pays"
|
|
129
|
+
|
|
130
|
+
msgid "ISO 3166-1 alpha-2 country code"
|
|
131
|
+
msgstr "Code pays ISO 3166-1 alpha-2"
|
|
132
|
+
|
|
133
|
+
msgid "preferred currency"
|
|
134
|
+
msgstr "Devise préférée"
|
|
135
|
+
|
|
136
|
+
msgid "preferred payment method"
|
|
137
|
+
msgstr "Méthode de paiement préférée"
|
|
138
|
+
|
|
139
|
+
msgid "Stripe customer ID"
|
|
140
|
+
msgstr "ID client Stripe"
|
|
141
|
+
|
|
142
|
+
msgid "Stripe customer identifier"
|
|
143
|
+
msgstr "Identifiant client Stripe"
|
|
144
|
+
|
|
145
|
+
msgid "automatic payment"
|
|
146
|
+
msgstr "Paiement automatique"
|
|
147
|
+
|
|
148
|
+
msgid "Enable automatic invoice payment"
|
|
149
|
+
msgstr "Activer le paiement automatique des factures"
|
|
150
|
+
|
|
151
|
+
msgid "billing notes"
|
|
152
|
+
msgstr "Notes de facturation"
|
|
153
|
+
|
|
154
|
+
msgid "Custom notes to include on invoices"
|
|
155
|
+
msgstr "Notes personnalisées à inclure sur les factures"
|
|
156
|
+
|
|
157
|
+
msgid "Billing information"
|
|
158
|
+
msgstr "Informations de facturation"
|
|
159
|
+
|
|
160
|
+
msgid "invoice number"
|
|
161
|
+
msgstr "Numéro de facture"
|
|
162
|
+
|
|
163
|
+
msgid "Unique invoice number"
|
|
164
|
+
msgstr "Numéro unique de la facture"
|
|
165
|
+
|
|
166
|
+
msgid "status"
|
|
167
|
+
msgstr "Statut"
|
|
168
|
+
|
|
169
|
+
msgid "Invoice status"
|
|
170
|
+
msgstr "Statut de la facture"
|
|
171
|
+
|
|
172
|
+
msgid "subtotal"
|
|
173
|
+
msgstr "Sous-total"
|
|
174
|
+
|
|
175
|
+
msgid "Amount excluding taxes"
|
|
176
|
+
msgstr "Montant hors taxes"
|
|
177
|
+
|
|
178
|
+
msgid "tax rate"
|
|
179
|
+
msgstr "Taux de taxe"
|
|
180
|
+
|
|
181
|
+
msgid "Tax rate as percentage"
|
|
182
|
+
msgstr "Taux de taxe en pourcentage"
|
|
183
|
+
|
|
184
|
+
msgid "tax amount"
|
|
185
|
+
msgstr "Montant de la taxe"
|
|
186
|
+
|
|
187
|
+
msgid "Tax amount"
|
|
188
|
+
msgstr "Montant de la taxe"
|
|
189
|
+
|
|
190
|
+
msgid "total"
|
|
191
|
+
msgstr "Total"
|
|
192
|
+
|
|
193
|
+
msgid "Total amount including tax"
|
|
194
|
+
msgstr "Montant total TTC"
|
|
195
|
+
|
|
196
|
+
msgid "currency"
|
|
197
|
+
msgstr "Devise"
|
|
198
|
+
|
|
199
|
+
msgid "ISO currency code (CDF, USD, etc.)"
|
|
200
|
+
msgstr "Code devise ISO (CDF, USD, etc.)"
|
|
201
|
+
|
|
202
|
+
msgid "issue date"
|
|
203
|
+
msgstr "Date d'émission"
|
|
204
|
+
|
|
205
|
+
msgid "Invoice issue date"
|
|
206
|
+
msgstr "Date d'émission de la facture"
|
|
207
|
+
|
|
208
|
+
msgid "due date"
|
|
209
|
+
msgstr "Date d'échéance"
|
|
210
|
+
|
|
211
|
+
msgid "Payment due date"
|
|
212
|
+
msgstr "Date limite de paiement"
|
|
213
|
+
|
|
214
|
+
msgid "payment date"
|
|
215
|
+
msgstr "Date de paiement"
|
|
216
|
+
|
|
217
|
+
msgid "Payment date and time"
|
|
218
|
+
msgstr "Date et heure du paiement"
|
|
219
|
+
|
|
220
|
+
msgid "period start"
|
|
221
|
+
msgstr "Début de période"
|
|
222
|
+
|
|
223
|
+
msgid "Billing period start date"
|
|
224
|
+
msgstr "Date de début de la période facturée"
|
|
225
|
+
|
|
226
|
+
msgid "period end"
|
|
227
|
+
msgstr "Fin de période"
|
|
228
|
+
|
|
229
|
+
msgid "Billing period end date"
|
|
230
|
+
msgstr "Date de fin de la période facturée"
|
|
231
|
+
|
|
232
|
+
msgid "description"
|
|
233
|
+
msgstr "Description"
|
|
234
|
+
|
|
235
|
+
msgid "Detailed invoice description"
|
|
236
|
+
msgstr "Description détaillée de la facture"
|
|
237
|
+
|
|
238
|
+
msgid "notes"
|
|
239
|
+
msgstr "Notes"
|
|
240
|
+
|
|
241
|
+
msgid "Internal notes about the invoice"
|
|
242
|
+
msgstr "Notes internes sur la facture"
|
|
243
|
+
|
|
244
|
+
msgid "payment reference"
|
|
245
|
+
msgstr "Référence de paiement"
|
|
246
|
+
|
|
247
|
+
msgid "External payment system reference (Stripe, etc.)"
|
|
248
|
+
msgstr "Référence du système de paiement externe (Stripe, etc.)"
|
|
249
|
+
|
|
250
|
+
msgid "Invoice"
|
|
251
|
+
msgstr "Facture"
|
|
252
|
+
|
|
253
|
+
msgid "Invoices"
|
|
254
|
+
msgstr "Factures"
|
|
255
|
+
|
|
256
|
+
msgid "service name"
|
|
257
|
+
msgstr "Nom du service"
|
|
258
|
+
|
|
259
|
+
msgid "Name of the billed service or product"
|
|
260
|
+
msgstr "Nom du service ou produit facturé"
|
|
261
|
+
|
|
262
|
+
msgid "Detailed service description"
|
|
263
|
+
msgstr "Description détaillée du service"
|
|
264
|
+
|
|
265
|
+
msgid "quantity"
|
|
266
|
+
msgstr "Quantité"
|
|
267
|
+
|
|
268
|
+
msgid "Service quantity (hours, units, etc.)"
|
|
269
|
+
msgstr "Quantité du service (heures, unités, etc.)"
|
|
270
|
+
|
|
271
|
+
msgid "unit price"
|
|
272
|
+
msgstr "Prix unitaire"
|
|
273
|
+
|
|
274
|
+
msgid "Unit price excluding taxes"
|
|
275
|
+
msgstr "Prix unitaire hors taxes"
|
|
276
|
+
|
|
277
|
+
msgid "total price"
|
|
278
|
+
msgstr "Prix total"
|
|
279
|
+
|
|
280
|
+
msgid "Total price for this item (quantity × unit price)"
|
|
281
|
+
msgstr "Prix total pour cet élément (quantité × prix unitaire)"
|
|
282
|
+
|
|
283
|
+
msgid "metadata"
|
|
284
|
+
msgstr "Métadonnées"
|
|
285
|
+
|
|
286
|
+
msgid "Additional data in JSON format"
|
|
287
|
+
msgstr "Données supplémentaires en format JSON"
|
|
288
|
+
|
|
289
|
+
msgid "Invoice item"
|
|
290
|
+
msgstr "Élément de facture"
|
|
291
|
+
|
|
292
|
+
msgid "Invoice items"
|
|
293
|
+
msgstr "Éléments de facture"
|
|
294
|
+
|
|
295
|
+
msgid "Approved"
|
|
296
|
+
msgstr "Approuvé"
|
|
297
|
+
|
|
298
|
+
msgid "Rejected"
|
|
299
|
+
msgstr "Rejeté"
|
|
300
|
+
|
|
301
|
+
msgid "Processed"
|
|
302
|
+
msgstr "Traité"
|
|
303
|
+
|
|
304
|
+
msgid "Cancelled"
|
|
305
|
+
msgstr "Annulé"
|
|
306
|
+
|
|
307
|
+
msgid "Duplicate payment"
|
|
308
|
+
msgstr "Paiement en double"
|
|
309
|
+
|
|
310
|
+
msgid "Service not received"
|
|
311
|
+
msgstr "Service non reçu"
|
|
312
|
+
|
|
313
|
+
msgid "Billing error"
|
|
314
|
+
msgstr "Erreur de facturation"
|
|
315
|
+
|
|
316
|
+
msgid "Cancellation"
|
|
317
|
+
msgstr "Annulation"
|
|
318
|
+
|
|
319
|
+
msgid "Technical issue"
|
|
320
|
+
msgstr "Problème technique"
|
|
321
|
+
|
|
322
|
+
msgid "Other"
|
|
323
|
+
msgstr "Autre"
|
|
324
|
+
|
|
325
|
+
msgid "Refund request status"
|
|
326
|
+
msgstr "Statut de la demande de remboursement"
|
|
327
|
+
|
|
328
|
+
msgid "reason"
|
|
329
|
+
msgstr "raison"
|
|
330
|
+
|
|
331
|
+
msgid "Reason for refund request"
|
|
332
|
+
msgstr "Raison de la demande de remboursement"
|
|
333
|
+
|
|
334
|
+
msgid "Detailed description of the refund request"
|
|
335
|
+
msgstr "Description détaillée de la demande de remboursement"
|
|
336
|
+
|
|
337
|
+
msgid "requested amount"
|
|
338
|
+
msgstr "Montant demandé"
|
|
339
|
+
|
|
340
|
+
msgid "Requested refund amount"
|
|
341
|
+
msgstr "Montant du remboursement demandé"
|
|
342
|
+
|
|
343
|
+
msgid "approved amount"
|
|
344
|
+
msgstr "Montant approuvé"
|
|
345
|
+
|
|
346
|
+
msgid "Approved refund amount"
|
|
347
|
+
msgstr "Montant du remboursement approuvé"
|
|
348
|
+
|
|
349
|
+
msgid "processing date"
|
|
350
|
+
msgstr "date de traitement"
|
|
351
|
+
|
|
352
|
+
msgid "Refund processing date and time"
|
|
353
|
+
msgstr "Date et heure du traitement du remboursement"
|
|
354
|
+
|
|
355
|
+
msgid "admin notes"
|
|
356
|
+
msgstr "Notes administratives"
|
|
357
|
+
|
|
358
|
+
msgid "Internal administrator notes"
|
|
359
|
+
msgstr "Notes internes de l'administrateur"
|
|
360
|
+
|
|
361
|
+
msgid "refund reference"
|
|
362
|
+
msgstr "référence de remboursement"
|
|
363
|
+
|
|
364
|
+
msgid "Refund request"
|
|
365
|
+
msgstr "Demande de remboursement"
|
|
366
|
+
|
|
367
|
+
msgid "Refund requests"
|
|
368
|
+
msgstr "Demandes de remboursement"
|
|
File without changes
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from django.contrib.sites.shortcuts import RequestSite
|
|
2
|
+
from django.dispatch import receiver
|
|
3
|
+
import structlog
|
|
4
|
+
from django_structlog import signals
|
|
5
|
+
from cid.locals import get_cid
|
|
6
|
+
from oxutils.settings import oxi_settings
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@receiver(signals.bind_extra_request_metadata)
|
|
11
|
+
def bind_domain(request, logger, **kwargs):
|
|
12
|
+
current_site = RequestSite(request)
|
|
13
|
+
structlog.contextvars.bind_contextvars(
|
|
14
|
+
domain=current_site.domain,
|
|
15
|
+
cid=get_cid(),
|
|
16
|
+
user_id=str(request.user.pk),
|
|
17
|
+
service=oxi_settings.service_name
|
|
18
|
+
)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import structlog
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
DJANGO_STRUCTLOG_CELERY_ENABLED = True
|
|
7
|
+
DJANGO_STRUCTLOG_IP_LOGGING_ENABLED = True
|
|
8
|
+
DJANGO_STRUCTLOG_USER_ID_FIELD = 'pk'
|
|
9
|
+
DJANGO_STRUCTLOG_COMMAND_LOGGING_ENABLED = True
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
LOGGING = {
|
|
13
|
+
"version": 1,
|
|
14
|
+
"disable_existing_loggers": False,
|
|
15
|
+
"formatters": {
|
|
16
|
+
"json_formatter": {
|
|
17
|
+
"()": structlog.stdlib.ProcessorFormatter,
|
|
18
|
+
"processor": structlog.processors.JSONRenderer(),
|
|
19
|
+
},
|
|
20
|
+
"plain_console": {
|
|
21
|
+
"()": structlog.stdlib.ProcessorFormatter,
|
|
22
|
+
"processor": structlog.dev.ConsoleRenderer(),
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
"handlers": {
|
|
26
|
+
"console": {
|
|
27
|
+
"class": "logging.StreamHandler",
|
|
28
|
+
"formatter": "plain_console",
|
|
29
|
+
},
|
|
30
|
+
"json_file": {
|
|
31
|
+
"class": "logging.handlers.WatchedFileHandler",
|
|
32
|
+
"filename": "logs/json.log",
|
|
33
|
+
"formatter": "json_formatter",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
"loggers": {
|
|
37
|
+
"django_structlog": {
|
|
38
|
+
"handlers": ["console", "json_file"],
|
|
39
|
+
"level": "INFO",
|
|
40
|
+
},
|
|
41
|
+
"oxiliere_log": {
|
|
42
|
+
"handlers": ["console", "json_file"],
|
|
43
|
+
"level": "INFO",
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
structlog.configure(
|
|
49
|
+
processors=[
|
|
50
|
+
structlog.contextvars.merge_contextvars,
|
|
51
|
+
structlog.stdlib.filter_by_level,
|
|
52
|
+
structlog.processors.TimeStamper(fmt="iso"),
|
|
53
|
+
structlog.stdlib.add_logger_name,
|
|
54
|
+
structlog.stdlib.add_log_level,
|
|
55
|
+
structlog.stdlib.PositionalArgumentsFormatter(),
|
|
56
|
+
structlog.processors.StackInfoRenderer(),
|
|
57
|
+
structlog.processors.format_exc_info,
|
|
58
|
+
structlog.processors.UnicodeDecoder(),
|
|
59
|
+
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
|
|
60
|
+
],
|
|
61
|
+
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
62
|
+
cache_logger_on_first_use=True,
|
|
63
|
+
)
|
|
File without changes
|
oxutils/mixins/base.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
|
|
2
|
+
class DetailDictMixin:
|
|
3
|
+
default_detail: str
|
|
4
|
+
default_code: str
|
|
5
|
+
|
|
6
|
+
def __init__(self, detail=None, code=None) -> None:
|
|
7
|
+
"""
|
|
8
|
+
Builds a detail dictionary for the error to give more information to API
|
|
9
|
+
users.
|
|
10
|
+
"""
|
|
11
|
+
detail_dict = {"detail": self.default_detail, "code": self.default_code}
|
|
12
|
+
|
|
13
|
+
if isinstance(detail, dict):
|
|
14
|
+
detail_dict.update(detail)
|
|
15
|
+
elif detail is not None:
|
|
16
|
+
detail_dict["detail"] = detail
|
|
17
|
+
|
|
18
|
+
if code is not None:
|
|
19
|
+
detail_dict["code"] = code
|
|
20
|
+
|
|
21
|
+
super().__init__(detail_dict)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from typing import Optional, Dict, Any
|
|
2
|
+
from ninja import Schema
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ResponseSchema(Schema):
|
|
7
|
+
"""
|
|
8
|
+
Standardized error response schema matching APIException format.
|
|
9
|
+
Use this for documenting error responses in API endpoints.
|
|
10
|
+
"""
|
|
11
|
+
detail: str
|
|
12
|
+
code: str
|
|
13
|
+
errors: Optional[Dict[str, Any]] = None
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
from logging import Logger
|
|
2
|
+
from django.utils.translation import gettext_lazy as _
|
|
3
|
+
from auditlog.signals import accessed
|
|
4
|
+
from oxutils.settings import oxi_settings
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BaseService:
|
|
11
|
+
"""
|
|
12
|
+
Base service class with exception handling
|
|
13
|
+
|
|
14
|
+
Services should inherit from this class and override inner_exception_handler
|
|
15
|
+
to handle service-specific exceptions.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
logger: Logger | None = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def object_accessed(self, instance_class, instance):
|
|
22
|
+
if oxi_settings.log_access:
|
|
23
|
+
accessed(instance_class, instance)
|
|
24
|
+
|
|
25
|
+
def inner_exception_handler(self, exc: Exception, logger: Logger):
|
|
26
|
+
"""
|
|
27
|
+
Handle service-specific exceptions
|
|
28
|
+
|
|
29
|
+
Override this method in child classes to handle custom exceptions.
|
|
30
|
+
Should raise an APIException subclass if the exception is handled,
|
|
31
|
+
or re-raise the original exception if not handled.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
exc: The exception to handle
|
|
35
|
+
logger: Logger instance for logging
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
APIException: If the exception is handled
|
|
39
|
+
Exception: Re-raises the original exception if not handled
|
|
40
|
+
"""
|
|
41
|
+
# Default implementation does nothing, child classes should override
|
|
42
|
+
raise exc
|
|
43
|
+
|
|
44
|
+
def exception_handler(self, exc: Exception, logger: Logger | None = None):
|
|
45
|
+
"""
|
|
46
|
+
Handle all exceptions with a standardized approach
|
|
47
|
+
|
|
48
|
+
This method first calls inner_exception_handler for service-specific exceptions,
|
|
49
|
+
then handles common Django and Python exceptions.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
exc: The exception to handle
|
|
53
|
+
logger: Optional logger instance
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
APIException: Always raises an appropriate APIException subclass
|
|
57
|
+
"""
|
|
58
|
+
from oxutils.exceptions import (
|
|
59
|
+
NotFoundException,
|
|
60
|
+
ValidationException,
|
|
61
|
+
ConflictException,
|
|
62
|
+
DuplicateEntryException,
|
|
63
|
+
PermissionDeniedException,
|
|
64
|
+
InvalidParameterException,
|
|
65
|
+
MissingParameterException,
|
|
66
|
+
InternalErrorException,
|
|
67
|
+
)
|
|
68
|
+
from django.core.exceptions import ValidationError, ObjectDoesNotExist
|
|
69
|
+
from django.db import IntegrityError
|
|
70
|
+
import logging
|
|
71
|
+
|
|
72
|
+
if self.logger:
|
|
73
|
+
logger = self.logger
|
|
74
|
+
|
|
75
|
+
if logger is None:
|
|
76
|
+
logger = logging.getLogger(__name__)
|
|
77
|
+
|
|
78
|
+
# Log the exception
|
|
79
|
+
logger.error(f"Service exception: {type(exc).__name__} - {str(exc)}")
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
# First, try to handle service-specific exceptions
|
|
83
|
+
self.inner_exception_handler(exc, logger)
|
|
84
|
+
except Exception as inner_exc:
|
|
85
|
+
# If inner_exception_handler raised an APIException, re-raise it
|
|
86
|
+
from oxutils.exceptions import APIException
|
|
87
|
+
if isinstance(inner_exc, APIException):
|
|
88
|
+
raise inner_exc
|
|
89
|
+
|
|
90
|
+
# Otherwise, continue with standard exception handling
|
|
91
|
+
exc = inner_exc
|
|
92
|
+
|
|
93
|
+
# Handle Django validation errors
|
|
94
|
+
if isinstance(exc, ValidationError):
|
|
95
|
+
detail = str(exc)
|
|
96
|
+
if hasattr(exc, 'message_dict'):
|
|
97
|
+
detail = {'detail': str(exc), 'errors': exc.message_dict}
|
|
98
|
+
raise ValidationException(detail=detail) from exc
|
|
99
|
+
|
|
100
|
+
# Handle object not found errors
|
|
101
|
+
if isinstance(exc, ObjectDoesNotExist):
|
|
102
|
+
raise NotFoundException(detail=str(exc) or None) from exc
|
|
103
|
+
|
|
104
|
+
# Handle integrity errors (duplicate entries, foreign key violations)
|
|
105
|
+
if isinstance(exc, IntegrityError):
|
|
106
|
+
error_message = str(exc).lower()
|
|
107
|
+
|
|
108
|
+
if 'unique' in error_message or 'duplicate' in error_message:
|
|
109
|
+
raise DuplicateEntryException() from exc
|
|
110
|
+
|
|
111
|
+
if 'foreign key' in error_message:
|
|
112
|
+
raise InvalidParameterException(
|
|
113
|
+
detail=_('One or more referenced resources do not exist')
|
|
114
|
+
) from exc
|
|
115
|
+
|
|
116
|
+
raise ConflictException() from exc
|
|
117
|
+
|
|
118
|
+
# Handle value errors (invalid parameters)
|
|
119
|
+
if isinstance(exc, ValueError):
|
|
120
|
+
raise InvalidParameterException(detail=str(exc)) from exc
|
|
121
|
+
|
|
122
|
+
# Handle permission errors
|
|
123
|
+
if isinstance(exc, PermissionError):
|
|
124
|
+
raise PermissionDeniedException(detail=str(exc) or None) from exc
|
|
125
|
+
|
|
126
|
+
# Handle type errors (usually programming errors)
|
|
127
|
+
if isinstance(exc, TypeError):
|
|
128
|
+
logger.exception("Type error in service")
|
|
129
|
+
raise InternalErrorException() from exc
|
|
130
|
+
|
|
131
|
+
# Handle key errors (missing required data)
|
|
132
|
+
if isinstance(exc, KeyError):
|
|
133
|
+
raise MissingParameterException(
|
|
134
|
+
detail=_('Required parameter {exc} is missing').format(exc=exc)
|
|
135
|
+
) from exc
|
|
136
|
+
|
|
137
|
+
# Handle attribute errors
|
|
138
|
+
if isinstance(exc, AttributeError):
|
|
139
|
+
logger.exception("Attribute error in service")
|
|
140
|
+
raise InternalErrorException() from exc
|
|
141
|
+
|
|
142
|
+
# Default handler for unknown exceptions
|
|
143
|
+
logger.exception(f"Unhandled exception in service: {type(exc).__name__}")
|
|
144
|
+
raise InternalErrorException(
|
|
145
|
+
detail=_('An unexpected error occurred. Please try again later or contact support.')
|
|
146
|
+
) from exc
|