drf-iam 0.2.3__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 +62 -6
- drf_iam/decorators.py +27 -0
- drf_iam/migrations/0002_add_policy_name.py +16 -0
- drf_iam/utils/load_viewset_permissions.py +234 -50
- drf_iam/utils/logging_utils.py +56 -0
- {drf_iam-0.2.3.dist-info → drf_iam-0.2.4.dist-info}/METADATA +2 -2
- drf_iam-0.2.4.dist-info/RECORD +17 -0
- {drf_iam-0.2.3.dist-info → drf_iam-0.2.4.dist-info}/WHEEL +1 -1
- drf_iam-0.2.3.dist-info/RECORD +0 -14
- {drf_iam-0.2.3.dist-info → drf_iam-0.2.4.dist-info}/top_level.txt +0 -0
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
|
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
|
-
|
13
|
-
|
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
|
-
|
17
|
-
|
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
|
-
|
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.
|
4
|
-
from django.urls
|
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
|
9
|
-
from
|
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
|
-
|
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
|
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
|
-
|
29
|
-
actions.add(f"{name}")
|
30
|
-
|
56
|
+
actions.add(name)
|
31
57
|
return actions
|
32
58
|
|
33
59
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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
|
@@ -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,,
|
drf_iam-0.2.3.dist-info/RECORD
DELETED
@@ -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=hQSjX7nGb3bfdcweJQzz7Ktj5FnlStEkW8_iX7xaCKg,2463
|
11
|
-
drf_iam-0.2.3.dist-info/METADATA,sha256=BDbby2aBZDN3KWY3ZjJufJTancmgDHwXOTLgiUb3JfI,2763
|
12
|
-
drf_iam-0.2.3.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
13
|
-
drf_iam-0.2.3.dist-info/top_level.txt,sha256=daz6AaQ9e_cfCjLk2aRoLb_PCOoFofYUX4DU85VwHSM,8
|
14
|
-
drf_iam-0.2.3.dist-info/RECORD,,
|
File without changes
|