oxutils 0.1.12__py3-none-any.whl → 0.1.14__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 CHANGED
@@ -10,7 +10,7 @@ This package provides:
10
10
  - Permission management
11
11
  """
12
12
 
13
- __version__ = "0.1.12"
13
+ __version__ = "0.1.14"
14
14
 
15
15
  from oxutils.settings import oxi_settings
16
16
  from oxutils.conf import UTILS_APPS, AUDIT_MIDDLEWARE
@@ -7,13 +7,27 @@ CACHE_CHECK_PERMISSION = getattr(settings, 'CACHE_CHECK_PERMISSION', False)
7
7
  if CACHE_CHECK_PERMISSION:
8
8
  from cacheops import cached_as
9
9
  from .models import Grant
10
- from .utils import check
10
+ from .utils import check, any_action_check, any_permission_check
11
11
 
12
- @cached_as(Grant, timeout=60*5)
12
+ @cached_as(Grant, timeout=60*15)
13
13
  def cache_check(user, scope, actions, group = None, **context):
14
14
  return check(user, scope, actions, group, **context)
15
+
16
+ @cached_as(Grant, timeout=60*15)
17
+ def cache_any_action_check(user, scope, required, group = None, **context):
18
+ return any_action_check(user, scope, required, group, **context)
19
+
20
+ @cached_as(Grant, timeout=60*15)
21
+ def cache_any_permission_check(user, *str_perms):
22
+ return any_permission_check(user, *str_perms)
15
23
  else:
16
- from .utils import check
24
+ from .utils import check, any_action_check, any_permission_check
17
25
 
18
26
  def cache_check(user, scope, actions, group = None, **context):
19
27
  return check(user, scope, actions, group, **context)
28
+
29
+ def cache_any_action_check(user, scope, required, group = None, **context):
30
+ return any_action_check(user, scope, required, group, **context)
31
+
32
+ def cache_any_permission_check(user, *str_perms):
33
+ return any_permission_check(user, *str_perms)
@@ -45,6 +45,112 @@ class ScopePermission(BasePermission):
45
45
  return str_check(request.user, self.perm, **self.ctx)
46
46
 
47
47
 
48
+ class ScopeAnyPermission(BasePermission):
49
+ """
50
+ Permission class for checking if user has at least one of multiple permissions.
51
+
52
+ Vérifie si l'utilisateur possède au moins une des permissions fournies.
53
+ Utilise any_permission_check pour une vérification optimisée en une seule requête.
54
+
55
+ Example:
56
+ @api_controller('/articles', permissions=[
57
+ ScopeAnyPermission('articles:r', 'articles:w:staff', 'articles:d:admin')
58
+ ])
59
+ class ArticleController:
60
+ # User needs either read access, OR staff write access, OR admin delete access
61
+ pass
62
+ """
63
+
64
+ def __init__(self, *perms: str):
65
+ """
66
+ Initialize the permission checker with multiple permission strings.
67
+
68
+ Args:
69
+ *perms: Variable number of permission strings in format "<scope>:<actions>:<group>?context"
70
+ """
71
+ if not perms:
72
+ raise ValueError("At least one permission string must be provided")
73
+ self.perms = perms
74
+
75
+ def has_permission(self, request: HttpRequest, controller: ControllerBase) -> bool:
76
+ """
77
+ Check if the user has at least one of the required permissions.
78
+
79
+ Args:
80
+ request: HTTP request object
81
+ controller: Controller instance
82
+
83
+ Returns:
84
+ True if user has at least one permission, False otherwise
85
+ """
86
+ from oxutils.permissions.caches import cache_any_permission_check
87
+ return cache_any_permission_check(request.user, *self.perms)
88
+
89
+
90
+ class ScopeAnyActionPermission(BasePermission):
91
+ """
92
+ Permission class for checking if user has at least one of multiple actions on a scope.
93
+
94
+ Vérifie si l'utilisateur possède au moins une des actions requises pour un scope donné.
95
+ La chaîne d'actions contient plusieurs actions dont au moins une est requise.
96
+
97
+ Example:
98
+ @api_controller('/articles', permissions=[
99
+ ScopeAnyActionPermission('articles:rwd:staff')
100
+ ])
101
+ class ArticleController:
102
+ # User needs read OR write OR delete access on articles in staff group
103
+ pass
104
+
105
+ @api_controller('/invoices', permissions=[
106
+ ScopeAnyActionPermission('invoices:rw?tenant_id=123')
107
+ ])
108
+ class InvoiceController:
109
+ # User needs read OR write access on invoices with tenant_id=123
110
+ pass
111
+ """
112
+
113
+ def __init__(self, perm: str, ctx: Optional[dict] = None):
114
+ """
115
+ Initialize the permission checker with a permission string.
116
+
117
+ Args:
118
+ perm: Permission string in format "<scope>:<actions>:<group>?context"
119
+ where actions contains multiple characters (e.g., 'rwd' for read OR write OR delete)
120
+ ctx: Optional additional context dict
121
+ """
122
+ if not perm:
123
+ raise ValueError("Permission string must be provided")
124
+
125
+ self.perm = perm
126
+ self.ctx = ctx if ctx else dict()
127
+
128
+ def has_permission(self, request: HttpRequest, controller: ControllerBase) -> bool:
129
+ """
130
+ Check if the user has at least one of the required actions.
131
+
132
+ Args:
133
+ request: HTTP request object
134
+ controller: Controller instance
135
+
136
+ Returns:
137
+ True if user has at least one action, False otherwise
138
+ """
139
+ from oxutils.permissions.caches import cache_any_action_check
140
+ from oxutils.permissions.utils import parse_permission
141
+
142
+ scope, actions, group, query_context = parse_permission(self.perm)
143
+ final_context = {**query_context, **self.ctx}
144
+
145
+ return cache_any_action_check(
146
+ request.user,
147
+ scope,
148
+ actions,
149
+ group,
150
+ **final_context
151
+ )
152
+
153
+
48
154
  def access_manager(actions: str):
49
155
  """
50
156
  Factory function for creating ScopePermission instances for access manager.
@@ -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.14
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=4VMJ48vLu5PsZMfhvnI84bZ3Bvx7f1p2lQKUz6Boyuc,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
@@ -85,7 +85,7 @@ oxutils/permissions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSu
85
85
  oxutils/permissions/actions.py,sha256=YmwiKOxhHl6GJ9YxrCzftWfd1ddrYR2GSZx4VeLtv5g,1273
86
86
  oxutils/permissions/admin.py,sha256=suMo4x8I3JBxAFBVIdE-5qnqZ6JAZV0FESABHOSc-vg,63
87
87
  oxutils/permissions/apps.py,sha256=nuTziz75_t-GToyZpJv2uloEDxzoz-v8ZBglzzdQeG8,272
88
- oxutils/permissions/caches.py,sha256=05fvTBc9pisWBwUEnnxCtiJU13PB0b55WalR9mzK24I,565
88
+ oxutils/permissions/caches.py,sha256=OVkbi3oQGXyotFVqTxt0jHn7zXncZFCX-gnDNbB4uec,1262
89
89
  oxutils/permissions/checks.py,sha256=eNnm2RF0IcZwzdpQqsoEdY9684tQ0r4I839DIFoQfRQ,6590
90
90
  oxutils/permissions/constants.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
91
91
  oxutils/permissions/controllers.py,sha256=W9iOsjWPPXUX9c1ZR_v3aOEpT_ZEztQbICQ_KcXQU70,9966
@@ -98,12 +98,12 @@ oxutils/permissions/migrations/0002_alter_grant_role.py,sha256=uTWAEgCYszHGw3fKZ
98
98
  oxutils/permissions/migrations/0003_alter_grant_options_alter_group_options_and_more.py,sha256=W5QoxO2klWnze9p0yV9WUbLPIodZoc9CRcrihg6c6RI,893
99
99
  oxutils/permissions/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
100
100
  oxutils/permissions/models.py,sha256=FzTsecGcBdlZU6SPlT-aZmbocFDxJ0pWsHVTOvtQvUo,4474
101
- oxutils/permissions/perms.py,sha256=yVX7OGxCo1uoc4KDHvLy8b55i6PcS3WGgjr1uzzWtk8,3153
101
+ oxutils/permissions/perms.py,sha256=HUrXoVIC4vry1J4tnOdEVSD7AvXovylFR3BBSVMhH48,6991
102
102
  oxutils/permissions/queryset.py,sha256=c_iYIO5ZX4jp3lWP9WucloAr2RPA72XuI_OM4SNhKpw,3173
103
103
  oxutils/permissions/schemas.py,sha256=Y6lwP9FkxSmklhwq5_VL6zORxvTmUXShsClm_FM2B-w,6122
104
104
  oxutils/permissions/services.py,sha256=d0oCeluzU_7YHpllphMymm-6gY_joKK-gxNDfJ_rD-Q,22140
105
105
  oxutils/permissions/tests.py,sha256=mrbGGRNg5jwbTJtWWa7zSKdDyeB4vmgZCRc2nk6VY-g,60
106
- oxutils/permissions/utils.py,sha256=n-naz4mlUPihC8WnBAQnqQaVRpYg1ooVwxGAI-EKJLc,22049
106
+ oxutils/permissions/utils.py,sha256=nyAvWOmkEo4XHVSzd0EEoSoOr-PX0oHD_w7J7fmGkZU,28085
107
107
  oxutils/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
108
108
  oxutils/settings.py,sha256=EZOHxvlj-RCLlble7il0SUcuOAbPlmxMe8cRgu74xfA,2404
109
109
  oxutils/types.py,sha256=DIz8YK8xMpLc7FYbf88yEElyLsYN_-rbvaZXvENQkOQ,234
@@ -112,11 +112,12 @@ oxutils/users/admin.py,sha256=suMo4x8I3JBxAFBVIdE-5qnqZ6JAZV0FESABHOSc-vg,63
112
112
  oxutils/users/apps.py,sha256=zfWHq8f0DIh8skbnqskDSoHG9nrvVrCegSz22Mw4BGI,150
113
113
  oxutils/users/migrations/0001_initial.py,sha256=7l5xgJnms2D8Nnazh38iBQ7I1W9NgNTF228-og_tOVw,3136
114
114
  oxutils/users/migrations/0002_alter_user_first_name_alter_user_last_name.py,sha256=o65pUPXu5m9tX_pHkeyKreRUPrByzQRU803Xz4D8v94,577
115
+ oxutils/users/migrations/0003_user_photo.py,sha256=_YiKl0EDKuWXX28yYYqGqWHaZtidjrR0ddADjMjJMxA,432
115
116
  oxutils/users/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
116
- oxutils/users/models.py,sha256=y5SF5tSqri11LRqRBDorSv5IYRa7noBHjFqQUJ5Jkkg,3105
117
+ oxutils/users/models.py,sha256=RmPwsTbjhnDb0qALQrz_t-ODSj05NfISE9JHxcfv2z4,3178
117
118
  oxutils/users/tests.py,sha256=mrbGGRNg5jwbTJtWWa7zSKdDyeB4vmgZCRc2nk6VY-g,60
118
119
  oxutils/users/utils.py,sha256=jY-zL8vLT5U3E2FV3DqCvrPORjKLutbkPZTQ-z96dCw,376
119
120
  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,,
121
+ oxutils-0.1.14.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
122
+ oxutils-0.1.14.dist-info/METADATA,sha256=UXHNH7j-hUIfeM_6Ti9VUJTY0BDlUC5k4HmiR3PE7_0,8389
123
+ oxutils-0.1.14.dist-info/RECORD,,