django-activity-audit 1.3.0.dev6__tar.gz → 1.3.0.dev8__tar.gz

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.
Files changed (22) hide show
  1. {django_activity_audit-1.3.0.dev6 → django_activity_audit-1.3.0.dev8}/PKG-INFO +9 -4
  2. {django_activity_audit-1.3.0.dev6 → django_activity_audit-1.3.0.dev8}/README.md +8 -3
  3. {django_activity_audit-1.3.0.dev6 → django_activity_audit-1.3.0.dev8}/README.rst +7 -3
  4. {django_activity_audit-1.3.0.dev6 → django_activity_audit-1.3.0.dev8}/activity_audit/middleware.py +2 -2
  5. {django_activity_audit-1.3.0.dev6 → django_activity_audit-1.3.0.dev8}/activity_audit/protocols.py +7 -7
  6. {django_activity_audit-1.3.0.dev6 → django_activity_audit-1.3.0.dev8}/activity_audit/settings.py +6 -3
  7. {django_activity_audit-1.3.0.dev6 → django_activity_audit-1.3.0.dev8}/activity_audit/signals.py +17 -32
  8. {django_activity_audit-1.3.0.dev6 → django_activity_audit-1.3.0.dev8}/pyproject.toml +1 -1
  9. {django_activity_audit-1.3.0.dev6 → django_activity_audit-1.3.0.dev8}/.gitignore +0 -0
  10. {django_activity_audit-1.3.0.dev6 → django_activity_audit-1.3.0.dev8}/.pre-commit-config.yaml +0 -0
  11. {django_activity_audit-1.3.0.dev6 → django_activity_audit-1.3.0.dev8}/LICENSE +0 -0
  12. {django_activity_audit-1.3.0.dev6 → django_activity_audit-1.3.0.dev8}/MANIFEST.in +0 -0
  13. {django_activity_audit-1.3.0.dev6 → django_activity_audit-1.3.0.dev8}/activity_audit/__init__.py +0 -0
  14. {django_activity_audit-1.3.0.dev6 → django_activity_audit-1.3.0.dev8}/activity_audit/apps.py +0 -0
  15. {django_activity_audit-1.3.0.dev6 → django_activity_audit-1.3.0.dev8}/activity_audit/constants.py +0 -0
  16. {django_activity_audit-1.3.0.dev6 → django_activity_audit-1.3.0.dev8}/activity_audit/formatters.py +0 -0
  17. {django_activity_audit-1.3.0.dev6 → django_activity_audit-1.3.0.dev8}/activity_audit/handlers.py +0 -0
  18. {django_activity_audit-1.3.0.dev6 → django_activity_audit-1.3.0.dev8}/activity_audit/logger_levels.py +0 -0
  19. {django_activity_audit-1.3.0.dev6 → django_activity_audit-1.3.0.dev8}/activity_audit/utils.py +0 -0
  20. {django_activity_audit-1.3.0.dev6 → django_activity_audit-1.3.0.dev8}/hatch +0 -0
  21. {django_activity_audit-1.3.0.dev6 → django_activity_audit-1.3.0.dev8}/pytest.ini +0 -0
  22. {django_activity_audit-1.3.0.dev6 → django_activity_audit-1.3.0.dev8}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-activity-audit
3
- Version: 1.3.0.dev6
3
+ Version: 1.3.0.dev8
4
4
  Summary: A Django package for easy CRUD operation logging and container logs
5
5
  Project-URL: Homepage, https://github.com/shree256/django-activity-audit
6
6
  Project-URL: Repository, https://github.com/shree256/django-activity-audit
@@ -117,7 +117,12 @@ LOGGING = {
117
117
  }
118
118
  ```
119
119
 
120
- 5. For external services logging, extend ```HTTPClient or SFTPClient```
120
+ 5. Configure the service name in `settings.py` (optional, defaults to `"default"`):
121
+ ```python
122
+ AUDIT_SERVICE_NAME = "my_service"
123
+ ```
124
+
125
+ 6. For external services logging, extend ```HTTPClient or SFTPClient```
121
126
  ```python
122
127
  class ExternalService(HTTPClient):
123
128
  def __init__(self):
@@ -128,7 +133,7 @@ class ExternalService(HTTPClient):
128
133
  response = self.get(url) # sample log structure below
129
134
  ```
130
135
 
131
- 7. Create ```audit_logs``` folder in project directory
136
+ 8. Create ```audit_logs``` folder in project directory
132
137
 
133
138
  ## Log Types
134
139
 
@@ -192,7 +197,7 @@ INFO 2025-04-30 08:51:10,403 /app/patients/api/utils.py utils create_patient_wit
192
197
  "level": "API",
193
198
  "name": "audit.request",
194
199
  "message": "Audit Internal Request",
195
- "service_name": "review_board",
200
+ "service_name": "my_service",
196
201
  "request_type": "internal",
197
202
  "protocol": "http",
198
203
  "user_id": "14ab1197-ebdd-4300-a618-5910e0219936",
@@ -77,7 +77,12 @@ LOGGING = {
77
77
  }
78
78
  ```
79
79
 
80
- 5. For external services logging, extend ```HTTPClient or SFTPClient```
80
+ 5. Configure the service name in `settings.py` (optional, defaults to `"default"`):
81
+ ```python
82
+ AUDIT_SERVICE_NAME = "my_service"
83
+ ```
84
+
85
+ 6. For external services logging, extend ```HTTPClient or SFTPClient```
81
86
  ```python
82
87
  class ExternalService(HTTPClient):
83
88
  def __init__(self):
@@ -88,7 +93,7 @@ class ExternalService(HTTPClient):
88
93
  response = self.get(url) # sample log structure below
89
94
  ```
90
95
 
91
- 7. Create ```audit_logs``` folder in project directory
96
+ 8. Create ```audit_logs``` folder in project directory
92
97
 
93
98
  ## Log Types
94
99
 
@@ -152,7 +157,7 @@ INFO 2025-04-30 08:51:10,403 /app/patients/api/utils.py utils create_patient_wit
152
157
  "level": "API",
153
158
  "name": "audit.request",
154
159
  "message": "Audit Internal Request",
155
- "service_name": "review_board",
160
+ "service_name": "my_service",
156
161
  "request_type": "internal",
157
162
  "protocol": "http",
158
163
  "user_id": "14ab1197-ebdd-4300-a618-5910e0219936",
@@ -76,7 +76,11 @@ Installation
76
76
  }
77
77
  }
78
78
 
79
- 5. For external services logging, extend ``HTTPClient`` or ``SFTPClient``::
79
+ 5. Configure the service name in ``settings.py`` (optional, defaults to ``"default"``)::
80
+
81
+ AUDIT_SERVICE_NAME = "my_service"
82
+
83
+ 6. For external services logging, extend ``HTTPClient`` or ``SFTPClient``::
80
84
 
81
85
  class ExternalService(HTTPClient):
82
86
  def __init__(self):
@@ -86,7 +90,7 @@ Installation
86
90
  url = "https://www.sample.com"
87
91
  response = self.get(url) # sample log structure below
88
92
 
89
- 6. Create ``audit_logs`` folder in project directory
93
+ 7. Create ``audit_logs`` folder in project directory
90
94
 
91
95
  Log Types
92
96
  ---------
@@ -158,7 +162,7 @@ Incoming Log Format::
158
162
  "level": "API",
159
163
  "name": "audit.request",
160
164
  "message": "Audit Internal Request",
161
- "service_name": "review_board",
165
+ "service_name": "my_service",
162
166
  "request_type": "internal",
163
167
  "protocol": "http",
164
168
  "user_id": "14ab1197-ebdd-4300-a618-5910e0219936",
@@ -10,7 +10,7 @@ from django.http import HttpResponse
10
10
  from django.utils.deprecation import MiddlewareMixin
11
11
 
12
12
  from .constants import REQUEST_TYPES
13
- from .settings import REGISTERED_URLS, UNREGISTERED_URLS
13
+ from .settings import REGISTERED_URLS, SERVICE_NAME, UNREGISTERED_URLS
14
14
 
15
15
  logger = logging.getLogger("audit.request")
16
16
 
@@ -116,7 +116,7 @@ class AuditLoggingMiddleware(MiddlewareMixin):
116
116
  def __init__(self, get_response):
117
117
  self.get_response = get_response
118
118
  self.log_data = {
119
- "service_name": "review_board",
119
+ "service_name": SERVICE_NAME,
120
120
  "request_type": REQUEST_TYPES[0],
121
121
  "protocol": None,
122
122
  "user_id": "",
@@ -4,8 +4,8 @@ import time
4
4
  from typing import Optional, Tuple
5
5
 
6
6
  import paramiko
7
- from django.conf import settings
8
7
 
8
+ from django.conf import settings
9
9
  from requests.sessions import Session
10
10
 
11
11
  from .constants import REQUEST_TYPES
@@ -218,12 +218,12 @@ class SFTPClient:
218
218
  result = f"{filename} uploaded successfully to {path_to_folder}"
219
219
  logger.info(f"{result}")
220
220
  except Exception as e:
221
- self.log_payload["error_message"] = (
222
- f"File upload failed. Error: {str(e)}"
223
- )
224
- self.log_payload["error_message"] = (
225
- f"Path validation failed. Error: {str(error)}"
226
- )
221
+ self.log_payload[
222
+ "error_message"
223
+ ] = f"File upload failed. Error: {str(e)}"
224
+ self.log_payload[
225
+ "error_message"
226
+ ] = f"Path validation failed. Error: {str(error)}"
227
227
  else:
228
228
  self.log_payload["error_message"] = "Connection not established"
229
229
 
@@ -44,12 +44,12 @@ UNREGISTERED_CLASSES = [
44
44
  SILK_INSTALLED = apps.is_installed("silk")
45
45
  if SILK_INSTALLED:
46
46
  from silk.models import (
47
+ BaseProfile,
48
+ Profile,
47
49
  Request,
48
50
  Response,
49
- SQLQueryManager,
50
51
  SQLQuery,
51
- BaseProfile,
52
- Profile,
52
+ SQLQueryManager,
53
53
  )
54
54
 
55
55
  UNREGISTERED_CLASSES.extend(
@@ -78,3 +78,6 @@ UNREGISTERED_URLS.extend(getattr(settings, "AUDIT_UNREGISTERED_URLS_EXTRA", []))
78
78
 
79
79
  # URL patterns to include in logging (if empty, all URLs are logged)
80
80
  REGISTERED_URLS = getattr(settings, "AUDIT_REGISTERED_URLS", [])
81
+
82
+ # Service name for audit logs
83
+ SERVICE_NAME = getattr(settings, "AUDIT_SERVICE_NAME", "default")
@@ -79,25 +79,7 @@ def get_calling_model() -> Optional[str]:
79
79
  return None
80
80
 
81
81
 
82
- class ModelSignalMixin:
83
- """Mixin to add signal handling capabilities to models."""
84
-
85
- def __init__(self, *args: Any, **kwargs: Any) -> None:
86
- super().__init__(*args, **kwargs)
87
- self._original_m2m = self._get_m2m_state()
88
-
89
- def _get_m2m_state(self) -> dict:
90
- try:
91
- """Get the current state of M2M fields."""
92
- return {
93
- field.name: set(
94
- getattr(self, field.name).all().values_list("id", flat=True)
95
- )
96
- for field in self._meta.many_to_many
97
- }
98
- except Exception as e:
99
- logger.error(f"Error getting M2M state: {e}")
100
- return {}
82
+ _patched_models: set = set()
101
83
 
102
84
 
103
85
  def push_log(
@@ -131,12 +113,15 @@ def push_log(
131
113
  logger.error(f"Failed to prepare audit log: {e}")
132
114
 
133
115
 
116
+ def instance_to_dict(instance: models.Model) -> dict:
117
+ return model_to_dict(instance, fields=[f.name for f in instance._meta.fields])
118
+
119
+
134
120
  def patch_model_event(model_class: type[models.Model]) -> None:
135
121
  """Monkey patch a model to add signal handling capabilities."""
136
122
 
137
- if not issubclass(model_class, ModelSignalMixin):
138
- # Add the mixin to the model's base classes
139
- model_class.__bases__ = (ModelSignalMixin,) + model_class.__bases__
123
+ if model_class not in _patched_models:
124
+ _patched_models.add(model_class)
140
125
 
141
126
  # Store the original methods
142
127
  original_save = model_class.save
@@ -154,7 +139,7 @@ def patch_model_event(model_class: type[models.Model]) -> None:
154
139
  # Log the event
155
140
  event_type = EVENT_TYPES[0] if is_new else EVENT_TYPES[1]
156
141
 
157
- instance_repr = model_to_dict(self)
142
+ instance_repr = instance_to_dict(self)
158
143
 
159
144
  push_log(
160
145
  f"{event_type} event by {model_class.__name__} (id: {self.pk})",
@@ -179,7 +164,7 @@ def patch_model_event(model_class: type[models.Model]) -> None:
179
164
 
180
165
  # For new instances, we might not have a pk yet, so use a placeholder
181
166
  instance_id = str(instance.pk) if instance.pk else "pending"
182
- instance_repr = model_to_dict(instance)
167
+ instance_repr = instance_to_dict(instance)
183
168
 
184
169
  push_log(
185
170
  f"{event_type} event by {model_class.__name__} (id: {instance_id})",
@@ -208,7 +193,7 @@ def patch_model_event(model_class: type[models.Model]) -> None:
208
193
  # Log only if this is the calling model
209
194
  if calling_model == model_class.__name__:
210
195
  first_obj = created_objs[0]
211
- instance_repr = model_to_dict(first_obj)
196
+ instance_repr = instance_to_dict(first_obj)
212
197
 
213
198
  push_log(
214
199
  f"{EVENT_TYPES[3]} event by {model_class.__name__} (id: {first_obj.pk})",
@@ -231,17 +216,17 @@ def patch_model_event(model_class: type[models.Model]) -> None:
231
216
  return original_bulk_update(self, objs, fields, batch_size)
232
217
 
233
218
  # Call the original bulk_update method
234
- original_bulk_update(self, objs, fields, batch_size)
219
+ bulk_update = original_bulk_update(self, objs, fields, batch_size)
235
220
 
236
221
  # Get the calling model
237
222
  calling_model = get_calling_model()
238
223
  if not calling_model:
239
- return original_bulk_update(self, objs, fields, batch_size)
224
+ return bulk_update
240
225
 
241
226
  # Log only if this is the calling model
242
227
  if calling_model == model_class.__name__:
243
228
  first_obj = objs[0]
244
- instance_repr = model_to_dict(first_obj)
229
+ instance_repr = instance_to_dict(first_obj)
245
230
 
246
231
  push_log(
247
232
  f"{EVENT_TYPES[4]} event by {model_class.__name__}",
@@ -268,7 +253,7 @@ def patch_model_event(model_class: type[models.Model]) -> None:
268
253
  if not should_audit(instance):
269
254
  return
270
255
 
271
- instance_repr = model_to_dict(instance)
256
+ instance_repr = instance_to_dict(instance)
272
257
 
273
258
  push_log(
274
259
  f"{EVENT_TYPES[8]} event by {model_class.__name__} (id: {instance.pk})",
@@ -283,7 +268,7 @@ def patch_model_event(model_class: type[models.Model]) -> None:
283
268
  def handle_delete(
284
269
  sender: type[models.Model], instance: models.Model, **kwargs: Any
285
270
  ) -> None:
286
- instance_repr = model_to_dict(instance)
271
+ instance_repr = instance_to_dict(instance)
287
272
 
288
273
  push_log(
289
274
  f"{EVENT_TYPES[2]} event by {model_class.__name__} (id: {instance.pk})",
@@ -308,7 +293,7 @@ def patch_model_event(model_class: type[models.Model]) -> None:
308
293
  return
309
294
 
310
295
  field_name = kwargs.get("model", sender).__name__.lower()
311
- instance_repr = model_to_dict(instance)
296
+ instance_repr = instance_to_dict(instance)
312
297
 
313
298
  push_log(
314
299
  f"M2M {action} event by {model_class.__name__} (id: {instance.pk})",
@@ -330,7 +315,7 @@ def setup_model_signals() -> None:
330
315
  if not should_audit(model):
331
316
  continue
332
317
 
333
- if not issubclass(model, ModelSignalMixin):
318
+ if model not in _patched_models:
334
319
  patch_model_event(model)
335
320
 
336
321
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "django-activity-audit"
3
- version = "1.3.0.dev6"
3
+ version = "1.3.0.dev8"
4
4
  description = "A Django package for easy CRUD operation logging and container logs"
5
5
  authors = [
6
6
  { name = "Shreeshan", email = "shreeshan256@gmail.com" }