drf-iam 0.2.2__py3-none-any.whl → 0.2.4__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.
drf_iam/apps.py CHANGED
@@ -1,19 +1,75 @@
1
+ """Configuration for the drf_iam Django app."""
2
+
1
3
  import logging
2
4
 
3
5
  from django.apps import AppConfig
6
+ from django.conf import settings
4
7
  from django.db.models.signals import post_migrate
5
8
 
6
- logger = logging.getLogger(__name__)
9
+ # Attempt to use the colorful logger setup, fallback to standard logger
10
+ try:
11
+ from drf_iam.utils.logging_utils import setup_colorful_logger
12
+ logger = setup_colorful_logger(__name__)
13
+ except ImportError:
14
+ logger = logging.getLogger(__name__)
15
+ # Basic configuration if colorful logger isn't available or during early app loading
16
+ if not logger.handlers:
17
+ handler = logging.StreamHandler()
18
+ formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
19
+ handler.setFormatter(formatter)
20
+ logger.addHandler(handler)
21
+ logger.setLevel(logging.INFO) # Default level
22
+
7
23
 
8
24
  class DrfIamConfig(AppConfig):
25
+ """Django AppConfig for drf_iam."""
9
26
  default_auto_field = "django.db.models.BigAutoField"
10
27
  name = "drf_iam"
28
+ verbose_name = "DRF IAM"
29
+
30
+ _already_run_ready = False # Class attribute to track if ready() logic has run
31
+
32
+ def ready(self) -> None:
33
+ """Performs initialization tasks when the app is ready.
34
+
35
+ Connects the permission loading utility to the post_migrate signal
36
+ if DRF_IAM_AUTO_LOAD_PERMISSIONS setting is True.
37
+ Ensures this setup runs only once.
38
+ """
39
+ if DrfIamConfig._already_run_ready:
40
+ return
11
41
 
12
- def ready(self):
13
- if not hasattr(self, 'already_loaded'):
42
+ # Check Django settings for whether to auto-load permissions
43
+ auto_load_enabled = getattr(settings, 'DRF_IAM_AUTO_LOAD_PERMISSIONS', True)
44
+
45
+ if auto_load_enabled:
14
46
  from drf_iam.utils.load_viewset_permissions import load_permissions_from_urls
47
+
48
+ def _robust_load_permissions(sender: AppConfig, **kwargs: Any) -> None:
49
+ """Signal receiver to load permissions, with error handling."""
50
+ try:
51
+ logger.info("🚀 Attempting to load/synchronize DRF-IAM permissions post-migrate...")
52
+ load_permissions_from_urls()
53
+ logger.info("🎉 Successfully loaded/synchronized DRF-IAM permissions.")
54
+ except Exception as e:
55
+ logger.error(
56
+ f"❌ Error during DRF-IAM permission loading post-migrate: {e}",
57
+ exc_info=True
58
+ )
59
+ # Decide if you want to re-raise. For post_migrate, often it's better
60
+ # to log and continue, rather than breaking the migration process.
61
+
15
62
  post_migrate.connect(
16
- load_permissions_from_urls,
17
- dispatch_uid="drf_iam.utils.load_permissions_from_urls",
63
+ _robust_load_permissions,
64
+ sender=self, # Connect to migrations for this app only
65
+ dispatch_uid="drf_iam.utils._robust_load_permissions",
66
+ )
67
+ logger.info(
68
+ f"{self.verbose_name}: 🔌 Permission auto-loading signal connected (DRF_IAM_AUTO_LOAD_PERMISSIONS=True)."
69
+ )
70
+ else:
71
+ logger.info(
72
+ f"{self.verbose_name}: 🚫 Permission auto-loading disabled via DRF_IAM_AUTO_LOAD_PERMISSIONS setting."
18
73
  )
19
- self.already_loaded = True
74
+
75
+ DrfIamConfig._already_run_ready = True
drf_iam/decorators.py ADDED
@@ -0,0 +1,27 @@
1
+ import functools
2
+
3
+ def action_permissions_config(policy_name=None,policy_description=None,exclude_from_iam=False):
4
+ """
5
+ A decorator for viewset actions to attach custom arguments and keyword arguments.
6
+ This data can be retrieved by permission-loading utilities to implement custom logic.
7
+
8
+ Usage:
9
+ from drf_iam.decorators import action_permissions_config
10
+
11
+ class MyViewSet(ViewSet):
12
+ @action_permissions_config('can_read_special_data', project_level=True)
13
+ def my_action(self, request):
14
+ # ...
15
+ pass
16
+ """
17
+ def decorator(func_to_decorate):
18
+ @functools.wraps(func_to_decorate)
19
+ def wrapper_func(*view_args, **view_kwargs):
20
+ return func_to_decorate(*view_args, **view_kwargs)
21
+
22
+ setattr(wrapper_func, 'policy_name', policy_name)
23
+ setattr(wrapper_func, 'exclude_from_iam', exclude_from_iam)
24
+ setattr(wrapper_func, 'policy_description', policy_description)
25
+
26
+ return wrapper_func
27
+ return decorator
@@ -0,0 +1,16 @@
1
+ from django.db import migrations, models
2
+
3
+
4
+ class Migration(migrations.Migration):
5
+
6
+ dependencies = [
7
+ ("drf_iam", "0001_initial"),
8
+ ]
9
+
10
+ operations = [
11
+ migrations.AddField(
12
+ model_name="policy",
13
+ name="policy_name",
14
+ field=models.CharField(blank=True, max_length=100, null=True),
15
+ ),
16
+ ]
@@ -1,69 +1,253 @@
1
1
  import inspect
2
+ import logging
3
+ from dataclasses import dataclass
4
+ from typing import List, Dict, Any, Set, Tuple, Iterator, Type, Optional
2
5
 
3
- from django.template.base import kwarg_re
4
- from django.urls.resolvers import URLPattern, URLResolver
6
+ from django.conf import settings
7
+ from django.urls import get_resolver, URLPattern, URLResolver
5
8
  from rest_framework.viewsets import ViewSetMixin
6
- from rest_framework.decorators import action
7
9
 
8
- from django.urls import get_resolver
9
- from ..models import Policy
10
+ from drf_iam.models import Policy
11
+ from .logging_utils import setup_colorful_logger
10
12
 
13
+ logger = setup_colorful_logger("drf_iam.permissions", level=logging.INFO)
11
14
 
12
- def is_viewset(cls):
15
+
16
+ @dataclass
17
+ class PolicyDetail:
18
+ """Represents the details of a policy to be created or updated."""
19
+ action: str
20
+ resource_type: str
21
+ policy_name: str
22
+ description: str
23
+ id: Optional[int] = None # For existing policies that might be updated
24
+
25
+ def __hash__(self):
26
+ return hash((self.action, self.resource_type))
27
+
28
+ def __eq__(self, other):
29
+ if not isinstance(other, PolicyDetail):
30
+ return NotImplemented
31
+ return (self.action, self.resource_type) == (other.action, other.resource_type)
32
+
33
+
34
+ def is_viewset(cls: Any) -> bool:
35
+ """Checks if the given class is a DRF ViewSet."""
13
36
  return isinstance(cls, type) and issubclass(cls, ViewSetMixin)
14
37
 
15
38
 
16
- def get_viewset_actions(viewset_cls):
17
- actions = set()
39
+ def get_viewset_actions(viewset_cls: Type[ViewSetMixin]) -> Set[str]:
40
+ """Extracts all actions (default and custom) from a ViewSet class.
41
+
42
+ Checks Django settings for 'DRF_IAM_DEFAULT_VIEWSET_ACTIONS'.
43
+ """
44
+ actions: Set[str] = set()
45
+
46
+ fallback_default_actions = {
47
+ 'list', 'retrieve', 'create', 'update', 'partial_update', 'destroy'
48
+ }
49
+ default_actions = getattr(settings, 'DRF_IAM_DEFAULT_VIEWSET_ACTIONS', fallback_default_actions)
18
50
 
19
- # Default ViewSet methods
20
- default_actions = {'list', 'retrieve', 'create', 'update', 'partial_update', 'destroy'}
21
51
  for action_name in default_actions:
22
52
  if hasattr(viewset_cls, action_name):
23
53
  actions.add(action_name)
24
-
25
- # Custom @action methods
26
54
  for name, method in inspect.getmembers(viewset_cls, predicate=inspect.isfunction):
27
55
  if hasattr(method, 'mapping'):
28
- for http_method in method.mapping:
29
- actions.add(f"{name}")
30
-
56
+ actions.add(name)
31
57
  return actions
32
58
 
33
59
 
34
- def extract_viewsets_from_urlpatterns(urlpatterns, prefix=''):
35
- for pattern in urlpatterns:
36
- if isinstance(pattern, URLResolver):
37
- # Recurse into included URLConfs
38
- yield from extract_viewsets_from_urlpatterns(pattern.url_patterns, prefix + str(pattern.pattern))
39
- elif isinstance(pattern, URLPattern):
40
- callback = pattern.callback
41
- cls = getattr(callback, 'cls', None)
42
- if is_viewset(cls):
43
- yield {
44
- 'prefix': prefix,
45
- 'pattern': pattern.pattern,
46
- 'viewset': cls,
47
- 'callback': callback
48
- }
49
-
50
-
51
- def load_permissions_from_urls(**kwargs):
52
- urlpatterns = get_resolver().url_patterns
53
- for entry in extract_viewsets_from_urlpatterns(urlpatterns):
54
- viewset_cls = entry['viewset']
55
- basename = getattr(viewset_cls,"iam_policy_name") or viewset_cls.__name__.lower().replace('viewset', '')
56
- exclude_from_permissions = getattr(viewset_cls, "drf_iam_exclude_from_permissions", False)
57
- if exclude_from_permissions:
58
- continue
59
- actions = get_viewset_actions(viewset_cls)
60
-
61
- for action in actions:
62
- full_action = f"{basename}:{action}"
63
- Policy.objects.get_or_create(
64
- action=full_action,
65
- resource_type=basename,
66
- defaults={
67
- 'description': f"Permission for {full_action} on {basename}"
68
- }
60
+ class PermissionLoader:
61
+ """Handles the synchronization of permissions from URL patterns to the database."""
62
+
63
+ def __init__(self):
64
+ self.urlpatterns: List[Any] = get_resolver().url_patterns
65
+ self.desired_policies: List[PolicyDetail] = []
66
+ self.current_db_policies: List[PolicyDetail] = []
67
+
68
+ def _extract_viewsets_from_urlpatterns(
69
+ self,
70
+ urlpatterns: List[Any],
71
+ prefix: str = ''
72
+ ) -> Iterator[Dict[str, Any]]:
73
+ """Recursively extracts ViewSet details from Django URL patterns."""
74
+ for pattern in urlpatterns:
75
+ if isinstance(pattern, URLResolver):
76
+ yield from self._extract_viewsets_from_urlpatterns(
77
+ pattern.url_patterns, prefix + str(pattern.pattern)
78
+ )
79
+ elif isinstance(pattern, URLPattern):
80
+ callback = pattern.callback
81
+ viewset_class = getattr(callback, 'cls', None)
82
+ if is_viewset(viewset_class):
83
+ yield {
84
+ 'prefix': prefix,
85
+ 'pattern': pattern.pattern,
86
+ 'viewset': viewset_class,
87
+ 'callback': callback,
88
+ }
89
+
90
+ def _generate_policy_details_from_viewsets(self) -> None:
91
+ """Generates a list of desired PolicyDetail objects from URL patterns."""
92
+ raw_policy_details: List[Dict[str, Any]] = []
93
+ for entry in self._extract_viewsets_from_urlpatterns(self.urlpatterns):
94
+ viewset_cls: Type[ViewSetMixin] = entry['viewset']
95
+ basename = getattr(viewset_cls, "iam_policy_name", None) or \
96
+ viewset_cls.__name__.lower().replace('viewset', '')
97
+
98
+ exclude_from_permissions = getattr(viewset_cls, "drf_iam_exclude_from_permissions", False)
99
+ if exclude_from_permissions:
100
+ continue
101
+
102
+ actions = get_viewset_actions(viewset_cls)
103
+
104
+ for action_name in actions:
105
+ action_method = getattr(viewset_cls, action_name, None)
106
+ policy_name_override = action_name
107
+ description = f"Permission for {action_name} on {basename}"
108
+
109
+ if action_method:
110
+ if getattr(action_method, 'exclude_from_iam', False):
111
+ continue
112
+ policy_name_override = getattr(action_method, 'policy_name', policy_name_override)
113
+ description = getattr(action_method, 'policy_description', description)
114
+
115
+ raw_policy_details.append({
116
+ 'action': f"{basename}:{action_name}",
117
+ 'resource_type': basename,
118
+ 'policy_name': policy_name_override,
119
+ 'description': description
120
+ })
121
+
122
+ seen_policies: Set[Tuple[str, str]] = set()
123
+ unique_policy_details: List[PolicyDetail] = []
124
+ for detail_dict in raw_policy_details:
125
+ key = (detail_dict['action'], detail_dict['resource_type'])
126
+ if key not in seen_policies:
127
+ seen_policies.add(key)
128
+ unique_policy_details.append(
129
+ PolicyDetail(
130
+ action=detail_dict['action'],
131
+ resource_type=detail_dict['resource_type'],
132
+ policy_name=detail_dict['policy_name'],
133
+ description=detail_dict['description'],
134
+ )
135
+ )
136
+ self.desired_policies = unique_policy_details
137
+
138
+ def _get_current_policies_from_db(self) -> None:
139
+ """Fetches all current policies from the database."""
140
+ self.current_db_policies = [
141
+ PolicyDetail(
142
+ id=p.id,
143
+ action=p.action,
144
+ resource_type=p.resource_type,
145
+ policy_name=p.policy_name,
146
+ description=p.description,
69
147
  )
148
+ for p in Policy.objects.all()
149
+ ]
150
+
151
+ def _calculate_policy_changes(
152
+ self
153
+ ) -> Tuple[List[PolicyDetail], List[PolicyDetail], List[PolicyDetail]]:
154
+ """Compares desired and current policies to determine changes."""
155
+ desired_set = set(self.desired_policies)
156
+ current_set = set(self.current_db_policies)
157
+
158
+ to_create = list(desired_set - current_set)
159
+
160
+ policies_to_delete_with_id: List[PolicyDetail] = []
161
+ current_map_for_delete = {(p.action, p.resource_type): p.id for p in self.current_db_policies if p.id is not None}
162
+ policies_to_delete_stubs = current_set - desired_set
163
+ for policy_stub in policies_to_delete_stubs:
164
+ policy_id = current_map_for_delete.get((policy_stub.action, policy_stub.resource_type))
165
+ if policy_id is not None:
166
+ policies_to_delete_with_id.append(
167
+ PolicyDetail(action=policy_stub.action, resource_type=policy_stub.resource_type, policy_name='',
168
+ description='', id=policy_id))
169
+
170
+ to_update: List[PolicyDetail] = []
171
+ current_map_for_update = {(p.action, p.resource_type): p for p in self.current_db_policies}
172
+
173
+ for desired_policy in desired_set & current_set:
174
+ current_policy_match = current_map_for_update.get((desired_policy.action, desired_policy.resource_type))
175
+ if current_policy_match and (
176
+ current_policy_match.policy_name != desired_policy.policy_name or \
177
+ current_policy_match.description != desired_policy.description
178
+ ):
179
+ to_update.append(PolicyDetail(
180
+ id=current_policy_match.id,
181
+ action=desired_policy.action,
182
+ resource_type=desired_policy.resource_type,
183
+ policy_name=desired_policy.policy_name,
184
+ description=desired_policy.description
185
+ ))
186
+ return to_create, policies_to_delete_with_id, to_update
187
+
188
+ def _apply_policy_changes(
189
+ self,
190
+ policies_to_create: List[PolicyDetail],
191
+ policies_to_delete: List[PolicyDetail],
192
+ policies_to_update: List[PolicyDetail],
193
+ ) -> None:
194
+ """Applies the calculated policy changes to the database."""
195
+ if policies_to_delete:
196
+ delete_ids = [p.id for p in policies_to_delete if p.id is not None]
197
+ if delete_ids:
198
+ Policy.objects.filter(id__in=delete_ids).delete()
199
+ logger.info(f"🗑️ Deleted {len(delete_ids)} policies.")
200
+
201
+ if policies_to_create:
202
+ Policy.objects.bulk_create([
203
+ Policy(
204
+ action=p.action,
205
+ resource_type=p.resource_type,
206
+ policy_name=p.policy_name,
207
+ description=p.description
208
+ ) for p in policies_to_create
209
+ ])
210
+ logger.info(f"✨ Created {len(policies_to_create)} new policies.")
211
+
212
+ if policies_to_update:
213
+ update_batch = []
214
+ for p_update in policies_to_update:
215
+ if p_update.id is not None:
216
+ policy_obj = Policy(
217
+ id=p_update.id,
218
+ action=p_update.action,
219
+ resource_type=p_update.resource_type,
220
+ policy_name=p_update.policy_name,
221
+ description=p_update.description
222
+ )
223
+ update_batch.append(policy_obj)
224
+
225
+ if update_batch:
226
+ Policy.objects.bulk_update(update_batch, fields=['policy_name', 'description'])
227
+ logger.info(f"🔄 Updated {len(update_batch)} policies.")
228
+
229
+ def sync_permissions(self) -> None:
230
+ """Main method to synchronize permissions."""
231
+ logger.info("🚀 Starting permission synchronization...")
232
+
233
+ self._generate_policy_details_from_viewsets()
234
+ logger.info(f"🔍 Discovered {len(self.desired_policies)} desired policies from URL patterns.")
235
+
236
+ self._get_current_policies_from_db()
237
+ logger.info(f"💾 Found {len(self.current_db_policies)} existing policies in the database.")
238
+
239
+ to_create, to_delete, to_update = self._calculate_policy_changes()
240
+ logger.info(f"➕ Policies to create: {len(to_create)}")
241
+ logger.info(f"➖ Policies to delete: {len(to_delete)}")
242
+ logger.info(f"🔧 Policies to update: {len(to_update)}")
243
+
244
+ self._apply_policy_changes(to_create, to_delete, to_update)
245
+ logger.info("🎉 Successfully finished permission synchronization.")
246
+
247
+
248
+ def load_permissions_from_urls(**kwargs: Any) -> None:
249
+ """Loads permissions from URL patterns and synchronizes them with the database.
250
+ This function instantiates and uses the PermissionLoader class.
251
+ """
252
+ loader = PermissionLoader()
253
+ loader.sync_permissions()
@@ -0,0 +1,56 @@
1
+ import logging
2
+
3
+ class Colors:
4
+ RESET = "\033[0m"
5
+ BOLD = "\033[1m"
6
+ RED = "\033[91m"
7
+ GREEN = "\033[92m"
8
+ YELLOW = "\033[93m"
9
+ BLUE = "\033[94m"
10
+ MAGENTA = "\033[95m"
11
+ CYAN = "\033[96m"
12
+ WHITE = "\033[97m"
13
+
14
+ LOG_LEVEL_COLORS = {
15
+ logging.DEBUG: Colors.BLUE,
16
+ logging.INFO: Colors.GREEN,
17
+ logging.WARNING: Colors.YELLOW,
18
+ logging.ERROR: Colors.RED,
19
+ logging.CRITICAL: Colors.BOLD + Colors.RED,
20
+ }
21
+
22
+ class ColorfulFormatter(logging.Formatter):
23
+ def __init__(self, datefmt='%H:%M:%S'):
24
+ super().__init__(datefmt=datefmt)
25
+
26
+ def format(self, record):
27
+ level_color = LOG_LEVEL_COLORS.get(record.levelno, Colors.RESET)
28
+
29
+ msg_body_color = Colors.WHITE # Default for message body
30
+ if record.levelno == logging.INFO:
31
+ msg_body_color = Colors.GREEN
32
+ elif record.levelno == logging.DEBUG:
33
+ msg_body_color = Colors.BLUE
34
+ elif record.levelno >= logging.WARNING:
35
+ msg_body_color = level_color # Errors, Warnings use their level's color for message body
36
+
37
+ asctime_str = f"{Colors.CYAN}{self.formatTime(record, self.datefmt)}{Colors.RESET}"
38
+ name_str = f"{Colors.MAGENTA}[{record.name}]{Colors.RESET}"
39
+ levelname_str = f"{level_color}{record.levelname.center(8)}{Colors.RESET}" # Padded levelname
40
+ message_str = f"{msg_body_color}{record.getMessage()}{Colors.RESET}"
41
+
42
+ return f"{asctime_str} {name_str} {levelname_str} : {message_str}"
43
+
44
+ def setup_colorful_logger(name: str, level: int = logging.INFO) -> logging.Logger:
45
+ """Sets up and returns a logger instance with colorful formatting."""
46
+ logger = logging.getLogger(name)
47
+ logger.setLevel(level)
48
+
49
+ if not logger.handlers: # Avoid adding multiple handlers if called multiple times
50
+ ch = logging.StreamHandler()
51
+ ch.setLevel(level)
52
+ formatter = ColorfulFormatter()
53
+ ch.setFormatter(formatter)
54
+ logger.addHandler(ch)
55
+ logger.propagate = False # Prevent double logging if root logger is configured
56
+ return logger
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: drf-iam
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: IAM-style roles and permissions for Django Rest Framework
5
5
  Home-page: https://github.com/tushar1328/drf-iam.git
6
6
  Author: Tushar Patel
@@ -0,0 +1,17 @@
1
+ drf_iam/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ drf_iam/admin.py,sha256=xH4T6xGKuylRCTwqLPJAx3Eb07Mb0Qb43iYAWX_cSN8,788
3
+ drf_iam/apps.py,sha256=IbQP1ZN8RnHj2tOxVpplqc_M9K8rMvnlCfL-8Bp8KnI,3161
4
+ drf_iam/decorators.py,sha256=11NEWGQ4cOZiH7gj-clLKi9M929gGtl1ZnaqEbVvhOc,1040
5
+ drf_iam/models.py,sha256=3K0f0SKWiiMFKbiLQ_SKrwV-B5jzzm-HH2gO8nOGs54,1296
6
+ drf_iam/permissions.py,sha256=kUhB7V48sb4v3uZI2lWv40hvI85nE61rL-amh6OY8mg,958
7
+ drf_iam/tests.py,sha256=mrbGGRNg5jwbTJtWWa7zSKdDyeB4vmgZCRc2nk6VY-g,60
8
+ drf_iam/migrations/0001_initial.py,sha256=y_4jXnr7gjU4UXxVrgVrStTSFu3h1ZrmjEZDL4FtZa4,3086
9
+ drf_iam/migrations/0002_add_policy_name.py,sha256=WxQczamrefP6lUxtOU425CK1FwLzcm4lapL-2aYSE6c,353
10
+ drf_iam/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ drf_iam/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ drf_iam/utils/load_viewset_permissions.py,sha256=NRjTekNgtuCiDbBH1hyttcW6nbchaBCD0fQnHALgYtI,10769
13
+ drf_iam/utils/logging_utils.py,sha256=9I9hhSVgOVW5xdKXSKofbjgl6rMYbmAI40euiB5WNlM,2074
14
+ drf_iam-0.2.4.dist-info/METADATA,sha256=aU6oVnf9ydBNsFdPiYYuSUlYlRVzlCYoPY8mrU-Js8A,2763
15
+ drf_iam-0.2.4.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
16
+ drf_iam-0.2.4.dist-info/top_level.txt,sha256=daz6AaQ9e_cfCjLk2aRoLb_PCOoFofYUX4DU85VwHSM,8
17
+ drf_iam-0.2.4.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.8.0)
2
+ Generator: setuptools (80.7.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,14 +0,0 @@
1
- drf_iam/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- drf_iam/admin.py,sha256=xH4T6xGKuylRCTwqLPJAx3Eb07Mb0Qb43iYAWX_cSN8,788
3
- drf_iam/apps.py,sha256=HUJatT0Wf-6ft19lWNkSqI7fnsnAjoLKQrjPUnVAcn8,611
4
- drf_iam/models.py,sha256=3K0f0SKWiiMFKbiLQ_SKrwV-B5jzzm-HH2gO8nOGs54,1296
5
- drf_iam/permissions.py,sha256=kUhB7V48sb4v3uZI2lWv40hvI85nE61rL-amh6OY8mg,958
6
- drf_iam/tests.py,sha256=mrbGGRNg5jwbTJtWWa7zSKdDyeB4vmgZCRc2nk6VY-g,60
7
- drf_iam/migrations/0001_initial.py,sha256=y_4jXnr7gjU4UXxVrgVrStTSFu3h1ZrmjEZDL4FtZa4,3086
8
- drf_iam/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- drf_iam/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- drf_iam/utils/load_viewset_permissions.py,sha256=PF7W4enw9SwzyeiLi632QY-VWhj-2qL6AG9JYdL4rxU,2457
11
- drf_iam-0.2.2.dist-info/METADATA,sha256=D090_EbQMpGViOHP-rNeXNnoOWrOQ_9ag8yaKRnp5bc,2763
12
- drf_iam-0.2.2.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
13
- drf_iam-0.2.2.dist-info/top_level.txt,sha256=daz6AaQ9e_cfCjLk2aRoLb_PCOoFofYUX4DU85VwHSM,8
14
- drf_iam-0.2.2.dist-info/RECORD,,