oxutils 0.1.12__py3-none-any.whl → 0.1.15__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.
@@ -387,41 +387,156 @@ def check(
387
387
  # Vérifier l'existence d'un grant correspondant
388
388
  return Grant.objects.filter(grant_filter).exists()
389
389
 
390
- def str_check(user: AbstractBaseUser, perm: str, **context: Any) -> bool:
390
+
391
+ def any_action_check(
392
+ user: AbstractBaseUser,
393
+ scope: str,
394
+ required: list[str],
395
+ group: Optional[str] = None,
396
+ **context: Any
397
+ ) -> bool:
391
398
  """
392
- Vérifie si un utilisateur possède les permissions requises à partir d'une chaîne formatée.
399
+ Vérifie si un utilisateur possède au moins une des actions requises pour un scope donné.
400
+
401
+ Cette fonction utilise une seule requête optimisée avec des conditions OR pour vérifier
402
+ si l'utilisateur possède au moins une des actions dans la liste.
393
403
 
394
404
  Args:
395
405
  user: L'utilisateur dont on vérifie les permissions
396
- perm: Chaîne de permission au format "<scope>:<actions>:<group>?key=value&key2=value2"
397
- - scope: Le scope à vérifier (ex: 'articles')
398
- - actions: Actions requises (ex: 'rw', 'r', 'rwdx')
399
- - group: (Optionnel) Slug du groupe
400
- - query params: (Optionnel) Contexte sous forme de query parameters
401
- **context: Contexte additionnel pour filtrer les grants (fusionné avec les query params)
406
+ scope: Le scope à vérifier (ex: 'articles', 'invoices')
407
+ required: Liste des actions dont au moins une est requise (ex: ['r', 'w'], ['d'])
408
+ group: Slug du groupe optionnel pour filtrer les grants par groupe
409
+ **context: Contexte additionnel pour filtrer les grants (clés JSON)
402
410
 
403
411
  Returns:
404
- True si l'utilisateur possède les permissions requises, False sinon
412
+ True si l'utilisateur possède au moins une des actions requises, False sinon
405
413
 
406
414
  Example:
407
- >>> # Vérifier lecture sur articles
408
- >>> str_check(user, 'articles:r')
409
- True
410
- >>> # Vérifier écriture sur articles dans le groupe staff
411
- >>> str_check(user, 'articles:w:staff')
415
+ >>> # Vérifier si l'utilisateur peut lire OU écrire les articles
416
+ >>> any_action_check(user, 'articles', ['r', 'w'])
412
417
  True
413
- >>> # Avec contexte via query params
414
- >>> str_check(user, 'articles:w?tenant_id=123&status=published')
418
+ >>> # Vérifier avec contexte
419
+ >>> any_action_check(user, 'articles', ['w', 'd'], tenant_id=123)
415
420
  False
416
- >>> # Avec groupe et contexte
417
- >>> str_check(user, 'articles:w:staff?tenant_id=123')
421
+ >>> # Vérifier dans le contexte d'un groupe spécifique
422
+ >>> any_action_check(user, 'articles', ['r', 'w'], group='staff')
418
423
  True
419
- >>> # Contexte mixte (query params + kwargs)
420
- >>> str_check(user, 'articles:w?tenant_id=123', level=2)
424
+
425
+ Note:
426
+ Les actions sont automatiquement expandées lors de la création du grant,
427
+ donc si un grant contient ['w'], il contient aussi ['r'] implicitement.
428
+ Cette fonction vérifie si AU MOINS UNE des actions requises est présente.
429
+ """
430
+ # Construire le filtre de base pour l'utilisateur et le scope
431
+ grant_filter = Q(user__pk=user.pk, scope=scope)
432
+
433
+ # Filtrer par groupe si spécifié
434
+ if group:
435
+ grant_filter &= Q(user_group__group__slug=group)
436
+
437
+ # Ajouter les filtres de contexte si fournis
438
+ if context:
439
+ grant_filter &= Q(context__contains=context)
440
+
441
+ # Vérifier si au moins une des actions requises est présente dans le grant
442
+ # Utilise l'opérateur overlap (&&) pour une requête optimale
443
+ grant_filter &= Q(actions__overlap=required)
444
+
445
+ # Vérifier l'existence d'un grant correspondant
446
+ return Grant.objects.filter(grant_filter).exists()
447
+
448
+
449
+ def any_permission_check(user: AbstractBaseUser, *str_perms: str) -> bool:
450
+ """
451
+ Vérifie si un utilisateur possède au moins une des permissions fournies.
452
+
453
+ Cette fonction parse toutes les permissions fournies et effectue une seule requête
454
+ optimisée avec des conditions OR pour vérifier si l'utilisateur possède au moins
455
+ une des permissions.
456
+
457
+ Args:
458
+ user: L'utilisateur dont on vérifie les permissions
459
+ *str_perms: Liste de chaînes de permissions au format standard
460
+ (ex: 'articles:r', 'invoices:w:staff', 'users:d?tenant_id=123')
461
+
462
+ Returns:
463
+ True si l'utilisateur possède au moins une des permissions, False sinon
464
+
465
+ Example:
466
+ >>> # Vérifier si l'utilisateur peut lire les articles OU écrire les factures
467
+ >>> any_permission_check(user, 'articles:r', 'invoices:w')
468
+ True
469
+ >>> # Avec différents groupes et contextes
470
+ >>> any_permission_check(
471
+ ... user,
472
+ ... 'articles:w:staff',
473
+ ... 'invoices:r:admin',
474
+ ... 'users:d?tenant_id=123'
475
+ ... )
421
476
  False
477
+
478
+ Note:
479
+ Toute la vérification se fait au niveau de la base de données avec une seule
480
+ requête utilisant des conditions OR pour optimiser les performances.
422
481
  """
423
- from .caches import cache_check
482
+ if not str_perms:
483
+ return False
484
+
485
+ # Construire le filtre de base pour l'utilisateur
486
+ base_filter = Q(user__pk=user.pk)
487
+
488
+ # Construire les conditions OR pour chaque permission
489
+ permission_filters = Q()
490
+
491
+ for perm in str_perms:
492
+ # Parser la permission
493
+ scope, actions, group, context = parse_permission(perm)
494
+
495
+ # Construire le filtre pour cette permission spécifique
496
+ perm_filter = Q(scope=scope, actions__overlap=actions)
497
+
498
+ # Ajouter le filtre de groupe si spécifié
499
+ if group:
500
+ perm_filter &= Q(user_group__group__slug=group)
501
+
502
+ # Ajouter le filtre de contexte si fourni
503
+ if context:
504
+ perm_filter &= Q(context__contains=context)
505
+
506
+ # Ajouter cette permission aux conditions OR
507
+ permission_filters |= perm_filter
508
+
509
+ # Combiner le filtre de base avec les conditions OR et vérifier l'existence
510
+ return Grant.objects.filter(base_filter & permission_filters).exists()
511
+
424
512
 
513
+ def parse_permission(perm: str) -> tuple[str, list[str], Optional[str], dict[str, Any]]:
514
+ """
515
+ Parse une chaîne de permission et retourne ses composants.
516
+
517
+ Args:
518
+ perm: Chaîne de permission au format "<scope>:<actions>:<group>?key=value&key2=value2"
519
+ - scope: Le scope (ex: 'articles')
520
+ - actions: Actions requises (ex: 'rw', 'r', 'rwdx')
521
+ - group: (Optionnel) Slug du groupe
522
+ - query params: (Optionnel) Contexte sous forme de query parameters
523
+
524
+ Returns:
525
+ Tuple contenant (scope, actions_list, group, context_dict)
526
+
527
+ Raises:
528
+ ValueError: Si le format de la permission est invalide
529
+
530
+ Example:
531
+ >>> parse_permission('articles:rw')
532
+ ('articles', ['r', 'w'], None, {})
533
+ >>> parse_permission('articles:w:staff')
534
+ ('articles', ['w'], 'staff', {})
535
+ >>> parse_permission('articles:rw?tenant_id=123&status=published')
536
+ ('articles', ['r', 'w'], None, {'tenant_id': 123, 'status': 'published'})
537
+ >>> parse_permission('articles:w:staff?tenant_id=123')
538
+ ('articles', ['w'], 'staff', {'tenant_id': 123})
539
+ """
425
540
  # Séparer la partie principale des query params
426
541
  if '?' in perm:
427
542
  from urllib.parse import parse_qs
@@ -455,7 +570,48 @@ def str_check(user: AbstractBaseUser, perm: str, **context: Any) -> bool:
455
570
 
456
571
  # Convertir la chaîne d'actions en liste
457
572
  # 'rwd' -> ['r', 'w', 'd']
458
- required = list(actions_str)
573
+ actions_list = list(actions_str)
574
+
575
+ return scope, actions_list, group, query_context
576
+
577
+
578
+ def str_check(user: AbstractBaseUser, perm: str, **context: Any) -> bool:
579
+ """
580
+ Vérifie si un utilisateur possède les permissions requises à partir d'une chaîne formatée.
581
+
582
+ Args:
583
+ user: L'utilisateur dont on vérifie les permissions
584
+ perm: Chaîne de permission au format "<scope>:<actions>:<group>?key=value&key2=value2"
585
+ - scope: Le scope à vérifier (ex: 'articles')
586
+ - actions: Actions requises (ex: 'rw', 'r', 'rwdx')
587
+ - group: (Optionnel) Slug du groupe
588
+ - query params: (Optionnel) Contexte sous forme de query parameters
589
+ **context: Contexte additionnel pour filtrer les grants (fusionné avec les query params)
590
+
591
+ Returns:
592
+ True si l'utilisateur possède les permissions requises, False sinon
593
+
594
+ Example:
595
+ >>> # Vérifier lecture sur articles
596
+ >>> str_check(user, 'articles:r')
597
+ True
598
+ >>> # Vérifier écriture sur articles dans le groupe staff
599
+ >>> str_check(user, 'articles:w:staff')
600
+ True
601
+ >>> # Avec contexte via query params
602
+ >>> str_check(user, 'articles:w?tenant_id=123&status=published')
603
+ False
604
+ >>> # Avec groupe et contexte
605
+ >>> str_check(user, 'articles:w:staff?tenant_id=123')
606
+ True
607
+ >>> # Contexte mixte (query params + kwargs)
608
+ >>> str_check(user, 'articles:w?tenant_id=123', level=2)
609
+ False
610
+ """
611
+ from .caches import cache_check
612
+
613
+ # Parser la chaîne de permission
614
+ scope, required, group, query_context = parse_permission(perm)
459
615
 
460
616
  # Fusionner les contextes (kwargs ont priorité sur query params)
461
617
  final_context = {**query_context, **context}
@@ -0,0 +1,18 @@
1
+ # Generated by Django 5.2.9 on 2026-02-02 13:03
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('users', '0002_alter_user_first_name_alter_user_last_name'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name='user',
15
+ name='photo',
16
+ field=models.ImageField(blank=True, null=True, upload_to='users/'),
17
+ ),
18
+ ]
oxutils/users/models.py CHANGED
@@ -59,6 +59,7 @@ class User(AbstractUser, SafeDeleteModel, BaseModelMixin):
59
59
  email = models.EmailField(unique=True)
60
60
  first_name = models.CharField(max_length=255, blank=True, null=True)
61
61
  last_name = models.CharField(max_length=255, blank=True, null=True)
62
+ photo = models.ImageField(upload_to='users/', blank=True, null=True)
62
63
  is_active = models.BooleanField(default=True)
63
64
  subscription_plan = models.CharField(max_length=255, null=True, blank=True)
64
65
  subscription_status = models.CharField(max_length=255, null=True, blank=True)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oxutils
3
- Version: 0.1.12
3
+ Version: 0.1.15
4
4
  Summary: Production-ready utilities for Django applications in the Oxiliere ecosystem
5
5
  Keywords: django,utilities,jwt,audit,logging,celery,structlog
6
6
  Author: Edimedia Mutoke
@@ -1,4 +1,4 @@
1
- oxutils/__init__.py,sha256=yiRt0OGkiw1AUyOxAYBpNisZ4VD2AsqkuKSz73VBocQ,508
1
+ oxutils/__init__.py,sha256=DvkIn9bebbDFzHSk08CPBnzHDrEdursLTV4nkRrTGTI,508
2
2
  oxutils/apps.py,sha256=8pO8eXUZeKYn8fPo0rkoytmHACwDNuTNhdRcpkPTxGM,347
3
3
  oxutils/audit/__init__.py,sha256=uonc00G73Xm7RwRHVWD-wBn8lJYNCq3iBgnRGMWAEWs,583
4
4
  oxutils/audit/apps.py,sha256=xvnmB5Z6nLV7ejzhSeQbesTkwRoFygoPFob8H5QTHgU,304
@@ -33,9 +33,10 @@ oxutils/enums/invoices.py,sha256=E33QGQeutZUqvlovJY0VGDxWUb0i_kdfhEiir1ARKuQ,201
33
33
  oxutils/exceptions.py,sha256=CCjENOD0of6_noif2ajrpfbBLoG16DWa46iB9_uEe3M,3592
34
34
  oxutils/functions.py,sha256=4stHj94VebWX0s1XeWshubMD2v8w8QztTWppbkTE_Gg,3246
35
35
  oxutils/jwt/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
- oxutils/jwt/auth.py,sha256=h3rm7nSEweMgyzy5HBRwqC1gPvZ-EZuwdJISSvnltXY,6349
37
- oxutils/jwt/models.py,sha256=Q0zRnWpK0trFoPDv5ZEY2ROCRaNn83W-K8SbrmSg1E8,2122
38
- oxutils/jwt/tokens.py,sha256=kWgtPl4XxV0xHkjbhd5QteQy8Wv5MsvyLcLVyO-gzuo,1822
36
+ oxutils/jwt/auth.py,sha256=3OACNYR6Mp5J57QUvu0iCuRuYiT5C49urUuCXqspNLA,7437
37
+ oxutils/jwt/middleware.py,sha256=m81lrpl9fJZ5gGHV_ysPxppd8pJklddFHDmxXRUpyvM,14285
38
+ oxutils/jwt/models.py,sha256=SG4qM2Ix7coopfIXUJ2KCkuC2bMAOuBnIwNhB5MVK14,5026
39
+ oxutils/jwt/tokens.py,sha256=l20XgPK-gUJcVwH8cWFSyYhfAQD70iqbzriRJkrPczo,2268
39
40
  oxutils/jwt/utils.py,sha256=Wuy-PnCcUw6MpY6z1Isy2vOx-_u1o6LjUfRJgf_cqbY,1202
40
41
  oxutils/locale/fr/LC_MESSAGES/django.po,sha256=APXt_8R99seCWjJyS5ELOawvRLvUqqBT32O252BaG5s,7971
41
42
  oxutils/logger/__init__.py,sha256=lhPCC8G1aHZTt-FWRqTWptVrqONln-ty9ufVDeOBHYs,183
@@ -66,11 +67,11 @@ oxutils/oxiliere/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMp
66
67
  oxutils/oxiliere/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
67
68
  oxutils/oxiliere/management/commands/grant_tenant_owners.py,sha256=U0tc-b677kEFA7KC6xah3Ufbg6qYkW21nRikC0FJRQI,774
68
69
  oxutils/oxiliere/management/commands/init_oxiliere_system.py,sha256=7ZmKOwL2TOIaPYBpGEoqcw2XslpG1VikUnTJwpu84Lo,4247
69
- oxutils/oxiliere/middleware.py,sha256=c0C1aalshhYNfe4SBglJkWfUo4Ct-d391GoWP_NhPOw,6412
70
+ oxutils/oxiliere/middleware.py,sha256=opwBbYMIyTMnytLQbQiKpcS5VzLblJci3iFc9r6X5aU,8142
70
71
  oxutils/oxiliere/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
71
72
  oxutils/oxiliere/models.py,sha256=dN3q-8W2gUcUra49b3R33o0ZNn3stc-pfkoVMAKR4gE,6260
72
- oxutils/oxiliere/permissions.py,sha256=6lJ_43a0-Z5O0B9cttA2cQFjZTGBucJk7w5rgtfd-lQ,3122
73
- oxutils/oxiliere/schemas.py,sha256=eV9MzkIpHFHmZFtv8Ck2xaGoVUDJXr-yl7w50U-Tj_8,2260
73
+ oxutils/oxiliere/permissions.py,sha256=Cz1GfwACxPMayQRVwGIMl_PlWM_f2ukP5VHeTlkmDM0,3566
74
+ oxutils/oxiliere/schemas.py,sha256=mjGyYTwQokAlMiZ0gKa5os_9HqFMp4NLaWpYxCb0yc0,3087
74
75
  oxutils/oxiliere/settings.py,sha256=ZuKppEyrucWxvvYC2-wLap4RzKfaEfaRdjJnsNZzpuY,440
75
76
  oxutils/oxiliere/signals.py,sha256=il6twTzbmv14SxukLx7dLw2QzuNDyVAsIgHmqbYspjw,97
76
77
  oxutils/oxiliere/tests.py,sha256=mrbGGRNg5jwbTJtWWa7zSKdDyeB4vmgZCRc2nk6VY-g,60
@@ -85,7 +86,7 @@ oxutils/permissions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSu
85
86
  oxutils/permissions/actions.py,sha256=YmwiKOxhHl6GJ9YxrCzftWfd1ddrYR2GSZx4VeLtv5g,1273
86
87
  oxutils/permissions/admin.py,sha256=suMo4x8I3JBxAFBVIdE-5qnqZ6JAZV0FESABHOSc-vg,63
87
88
  oxutils/permissions/apps.py,sha256=nuTziz75_t-GToyZpJv2uloEDxzoz-v8ZBglzzdQeG8,272
88
- oxutils/permissions/caches.py,sha256=05fvTBc9pisWBwUEnnxCtiJU13PB0b55WalR9mzK24I,565
89
+ oxutils/permissions/caches.py,sha256=OVkbi3oQGXyotFVqTxt0jHn7zXncZFCX-gnDNbB4uec,1262
89
90
  oxutils/permissions/checks.py,sha256=eNnm2RF0IcZwzdpQqsoEdY9684tQ0r4I839DIFoQfRQ,6590
90
91
  oxutils/permissions/constants.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
91
92
  oxutils/permissions/controllers.py,sha256=W9iOsjWPPXUX9c1ZR_v3aOEpT_ZEztQbICQ_KcXQU70,9966
@@ -98,12 +99,12 @@ oxutils/permissions/migrations/0002_alter_grant_role.py,sha256=uTWAEgCYszHGw3fKZ
98
99
  oxutils/permissions/migrations/0003_alter_grant_options_alter_group_options_and_more.py,sha256=W5QoxO2klWnze9p0yV9WUbLPIodZoc9CRcrihg6c6RI,893
99
100
  oxutils/permissions/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
100
101
  oxutils/permissions/models.py,sha256=FzTsecGcBdlZU6SPlT-aZmbocFDxJ0pWsHVTOvtQvUo,4474
101
- oxutils/permissions/perms.py,sha256=yVX7OGxCo1uoc4KDHvLy8b55i6PcS3WGgjr1uzzWtk8,3153
102
+ oxutils/permissions/perms.py,sha256=HUrXoVIC4vry1J4tnOdEVSD7AvXovylFR3BBSVMhH48,6991
102
103
  oxutils/permissions/queryset.py,sha256=c_iYIO5ZX4jp3lWP9WucloAr2RPA72XuI_OM4SNhKpw,3173
103
104
  oxutils/permissions/schemas.py,sha256=Y6lwP9FkxSmklhwq5_VL6zORxvTmUXShsClm_FM2B-w,6122
104
105
  oxutils/permissions/services.py,sha256=d0oCeluzU_7YHpllphMymm-6gY_joKK-gxNDfJ_rD-Q,22140
105
106
  oxutils/permissions/tests.py,sha256=mrbGGRNg5jwbTJtWWa7zSKdDyeB4vmgZCRc2nk6VY-g,60
106
- oxutils/permissions/utils.py,sha256=n-naz4mlUPihC8WnBAQnqQaVRpYg1ooVwxGAI-EKJLc,22049
107
+ oxutils/permissions/utils.py,sha256=nyAvWOmkEo4XHVSzd0EEoSoOr-PX0oHD_w7J7fmGkZU,28085
107
108
  oxutils/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
108
109
  oxutils/settings.py,sha256=EZOHxvlj-RCLlble7il0SUcuOAbPlmxMe8cRgu74xfA,2404
109
110
  oxutils/types.py,sha256=DIz8YK8xMpLc7FYbf88yEElyLsYN_-rbvaZXvENQkOQ,234
@@ -112,11 +113,12 @@ oxutils/users/admin.py,sha256=suMo4x8I3JBxAFBVIdE-5qnqZ6JAZV0FESABHOSc-vg,63
112
113
  oxutils/users/apps.py,sha256=zfWHq8f0DIh8skbnqskDSoHG9nrvVrCegSz22Mw4BGI,150
113
114
  oxutils/users/migrations/0001_initial.py,sha256=7l5xgJnms2D8Nnazh38iBQ7I1W9NgNTF228-og_tOVw,3136
114
115
  oxutils/users/migrations/0002_alter_user_first_name_alter_user_last_name.py,sha256=o65pUPXu5m9tX_pHkeyKreRUPrByzQRU803Xz4D8v94,577
116
+ oxutils/users/migrations/0003_user_photo.py,sha256=_YiKl0EDKuWXX28yYYqGqWHaZtidjrR0ddADjMjJMxA,432
115
117
  oxutils/users/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
116
- oxutils/users/models.py,sha256=y5SF5tSqri11LRqRBDorSv5IYRa7noBHjFqQUJ5Jkkg,3105
118
+ oxutils/users/models.py,sha256=RmPwsTbjhnDb0qALQrz_t-ODSj05NfISE9JHxcfv2z4,3178
117
119
  oxutils/users/tests.py,sha256=mrbGGRNg5jwbTJtWWa7zSKdDyeB4vmgZCRc2nk6VY-g,60
118
120
  oxutils/users/utils.py,sha256=jY-zL8vLT5U3E2FV3DqCvrPORjKLutbkPZTQ-z96dCw,376
119
121
  oxutils/utils.py,sha256=6yGX2d1ajU5RqgfqiaS4McYm7ip2KEgADABo3M-yA3U,595
120
- oxutils-0.1.12.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
121
- oxutils-0.1.12.dist-info/METADATA,sha256=M79Xi3iC54CkegN9QcU68qIRJgAQdnYt_2YmK19hE28,8389
122
- oxutils-0.1.12.dist-info/RECORD,,
122
+ oxutils-0.1.15.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
123
+ oxutils-0.1.15.dist-info/METADATA,sha256=_EDraKohaULS5QhUbtjTRhkka7-cFy-Jny-G-WMZr4E,8389
124
+ oxutils-0.1.15.dist-info/RECORD,,