universal-audit-logs 1.0.2__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.
Files changed (39) hide show
  1. audit_sdk/__init__.py +67 -0
  2. audit_sdk/audit.py +544 -0
  3. audit_sdk/config/__init__.py +10 -0
  4. audit_sdk/config/settings.py +224 -0
  5. audit_sdk/core/__init__.py +12 -0
  6. audit_sdk/core/event_bus.py +130 -0
  7. audit_sdk/core/exceptions.py +209 -0
  8. audit_sdk/core/interfaces.py +251 -0
  9. audit_sdk/middleware/__init__.py +38 -0
  10. audit_sdk/middleware/auto_audit.py +781 -0
  11. audit_sdk/middleware/base.py +11 -0
  12. audit_sdk/middleware/enhanced.py +673 -0
  13. audit_sdk/middleware/fastapi.py +782 -0
  14. audit_sdk/models/__init__.py +23 -0
  15. audit_sdk/models/audit_event.py +545 -0
  16. audit_sdk/models/enums.py +76 -0
  17. audit_sdk/plugins/__init__.py +28 -0
  18. audit_sdk/plugins/base.py +11 -0
  19. audit_sdk/plugins/registry.py +183 -0
  20. audit_sdk/py.typed +0 -0
  21. audit_sdk/security/__init__.py +25 -0
  22. audit_sdk/security/encryption.py +138 -0
  23. audit_sdk/security/signing.py +133 -0
  24. audit_sdk/transport/__init__.py +21 -0
  25. audit_sdk/transport/base.py +11 -0
  26. audit_sdk/transport/database.py +425 -0
  27. audit_sdk/transport/kafka.py +130 -0
  28. audit_sdk/transport/mongodb.py +240 -0
  29. audit_sdk/transport/rest.py +274 -0
  30. audit_sdk/transport/simple_database.py +200 -0
  31. audit_sdk/utils/__init__.py +12 -0
  32. audit_sdk/utils/logger.py +130 -0
  33. audit_sdk/utils/retry.py +117 -0
  34. audit_sdk/utils/retry_manager.py +485 -0
  35. audit_sdk/utils/serializer.py +177 -0
  36. universal_audit_logs-1.0.2.dist-info/METADATA +225 -0
  37. universal_audit_logs-1.0.2.dist-info/RECORD +39 -0
  38. universal_audit_logs-1.0.2.dist-info/WHEEL +4 -0
  39. universal_audit_logs-1.0.2.dist-info/licenses/LICENSE +21 -0
audit_sdk/__init__.py ADDED
@@ -0,0 +1,67 @@
1
+ """
2
+ Universal Audit Framework (UAF) SDK
3
+ ====================================
4
+
5
+ A production-ready, framework-agnostic audit logging SDK for Python 3.12+.
6
+
7
+ Quick-start — REST transport::
8
+
9
+ from audit_sdk import AuditClient, AuditSettings
10
+
11
+ settings = AuditSettings(endpoint="https://audit.example.com/api/v1/events")
12
+ audit = AuditClient(settings=settings)
13
+
14
+ Quick-start — Database transport::
15
+
16
+ from audit_sdk import AuditClient
17
+ from audit_sdk.transport.database import DatabaseTransport
18
+
19
+ transport = DatabaseTransport.for_postgres(
20
+ host="localhost", database="audit_db",
21
+ username="user", password="pass",
22
+ )
23
+ audit = AuditClient(transport=transport)
24
+
25
+ Quick-start — FastAPI middleware::
26
+
27
+ from audit_sdk.middleware.fastapi import AuditMiddleware, AuditMiddlewareConfig
28
+
29
+ app.add_middleware(AuditMiddleware, audit_client=audit, config=AuditMiddlewareConfig(...))
30
+ """
31
+
32
+ from audit_sdk.audit import AuditClient
33
+ from audit_sdk.config.settings import AuditSettings
34
+ from audit_sdk.core.exceptions import QueueFullError, TransportError
35
+ from audit_sdk.models.audit_event import AuditEvent
36
+ from audit_sdk.utils.retry_manager import RetryManager, RetryMetrics
37
+
38
+ __all__: list[str] = [
39
+ # Core
40
+ "AuditClient",
41
+ "AuditSettings",
42
+ "AuditEvent",
43
+ # Retry
44
+ "RetryManager",
45
+ "RetryMetrics",
46
+ # Exceptions
47
+ "QueueFullError",
48
+ "TransportError",
49
+ ]
50
+
51
+ # Optional: FastAPI middleware (requires `pip install universal-audit-logs[fastapi]`)
52
+ try:
53
+ from audit_sdk.middleware.fastapi import AuditMiddleware, AuditMiddlewareConfig
54
+
55
+ __all__ += ["AuditMiddleware", "AuditMiddlewareConfig"]
56
+ except ImportError:
57
+ pass
58
+
59
+ # Optional: Database transport (requires `pip install universal-audit-logs[postgres]` or `[sqlite]`)
60
+ try:
61
+ from audit_sdk.transport.database import DatabaseTransport
62
+
63
+ __all__ += ["DatabaseTransport"]
64
+ except ImportError:
65
+ pass
66
+
67
+ __version__: str = "1.0.2"
audit_sdk/audit.py ADDED
@@ -0,0 +1,544 @@
1
+ """
2
+ audit — Public-facing AuditClient: the SDK's primary entry point.
3
+
4
+ :class:`AuditClient` is the single object developers interact with.
5
+ It orchestrates the full audit event lifecycle:
6
+
7
+ 1. **Build** — assemble an :class:`~audit_sdk.models.audit_event.AuditEvent`
8
+ from caller-supplied parameters (with auto-populated ``event_id`` / ``timestamp``).
9
+ 2. **Validate** — Pydantic v2 validates the event; invalid events raise
10
+ :class:`~audit_sdk.core.exceptions.ValidationError` immediately.
11
+ 3. **Enrich** — ``on_before_send`` plugin hooks may add metadata.
12
+ 4. **Secure** — signing and/or encryption are applied when enabled.
13
+ 5. **Dispatch** — the transport delivers the event (with retry).
14
+ 6. **Observe** — ``on_after_send`` plugin hooks are called with the result.
15
+
16
+ Design:
17
+ - No global state. Every AuditClient holds its own configuration,
18
+ transport, serializer, plugin registry, and optional security components.
19
+ - AuditClient is an async context manager for clean resource management.
20
+ - All dependencies are injected; the client does not create its own transport
21
+ unless a factory helper (``AuditClient.create()``) is used.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import logging
27
+ from typing import Any
28
+
29
+ from audit_sdk.config.settings import AuditSettings
30
+ from audit_sdk.core.event_bus import AuditEventBus
31
+ from audit_sdk.core.exceptions import ConfigurationError, TransportError, ValidationError
32
+ from audit_sdk.core.interfaces import (
33
+ AbstractEncryptor,
34
+ AbstractSerializer,
35
+ AbstractSigner,
36
+ AbstractTransport,
37
+ )
38
+ from audit_sdk.models.audit_event import AuditEvent
39
+ from audit_sdk.plugins.registry import PluginRegistry
40
+ from audit_sdk.utils.retry_manager import RetryManager
41
+
42
+ log = logging.getLogger(__name__)
43
+
44
+
45
+ class AuditClient:
46
+ """The primary SDK entry point for emitting structured audit events.
47
+
48
+ Args:
49
+ settings: SDK configuration object (endpoint, API key, retry settings, etc.).
50
+ transport: The delivery transport (REST, Kafka, etc.). If ``None``, a
51
+ :class:`~audit_sdk.transport.rest.RestTransport` is created automatically.
52
+ serializer: Event serializer. Defaults to
53
+ :class:`~audit_sdk.utils.serializer.AuditEventSerializer`.
54
+ signer: Optional HMAC signer. Created automatically if
55
+ ``settings.signing_enabled`` is ``True``.
56
+ encryptor: Optional field encryptor. Created automatically if
57
+ ``settings.encryption_enabled`` is ``True``.
58
+ plugin_registry: Plugin registry. A fresh empty registry is used if not supplied.
59
+ event_bus: Internal event bus. A fresh bus is used if not supplied.
60
+
61
+ Example::
62
+
63
+ from audit_sdk import AuditClient, AuditSettings
64
+
65
+ settings = AuditSettings(endpoint="https://audit.example.com/api/v1/events")
66
+ audit = AuditClient(settings=settings)
67
+
68
+ await audit.log(
69
+ action="CREATE",
70
+ entity="Employee",
71
+ entity_id="100",
72
+ user={"id": "1", "name": "Admin"},
73
+ old_value=None,
74
+ new_value={"name": "John"},
75
+ )
76
+ """
77
+
78
+ def __init__(
79
+ self,
80
+ settings: AuditSettings | None = None,
81
+ transport: AbstractTransport | None = None,
82
+ serializer: AbstractSerializer | None = None,
83
+ signer: AbstractSigner | None = None,
84
+ encryptor: AbstractEncryptor | None = None,
85
+ plugin_registry: PluginRegistry | None = None,
86
+ event_bus: AuditEventBus | None = None,
87
+ retry_manager: RetryManager | None = None,
88
+ ) -> None:
89
+ # settings is optional when a transport is supplied directly
90
+ # (e.g. DatabaseTransport / MongoDBTransport — no endpoint needed).
91
+ if settings is None and transport is None:
92
+ raise ConfigurationError(
93
+ "AuditClient requires either 'settings' (for HTTP/REST transport) "
94
+ "or 'transport' (for direct DB/MongoDB transport)."
95
+ )
96
+ self._settings = settings
97
+ self._transport = transport # lazy-initialized in _get_transport()
98
+ self._serializer = serializer # lazy-initialized in _get_serializer()
99
+ self._signer = signer
100
+ self._encryptor = encryptor
101
+ self._plugin_registry = plugin_registry or PluginRegistry()
102
+ self._event_bus = event_bus or AuditEventBus()
103
+ self._retry_manager = retry_manager
104
+ self._closed = False
105
+
106
+ # ------------------------------------------------------------------
107
+ # Primary public API
108
+ # ------------------------------------------------------------------
109
+
110
+ async def log(
111
+ self,
112
+ action: str,
113
+ entity: str,
114
+ entity_id: str,
115
+ user: dict[str, Any],
116
+ old_value: Any = None,
117
+ new_value: Any = None,
118
+ metadata: dict[str, Any] | None = None,
119
+ tenant_id: str | None = None,
120
+ application: str | None = None,
121
+ module: str | None = None,
122
+ service: str | None = None,
123
+ event_type: str | None = None,
124
+ status: str | None = None,
125
+ request: dict[str, Any] | None = None,
126
+ device: dict[str, Any] | None = None,
127
+ ) -> AuditEvent:
128
+ """Build and validate a single structured audit event.
129
+
130
+ Constructs an :class:`~audit_sdk.models.audit_event.AuditEvent` from
131
+ the supplied parameters, merging caller-provided values with context
132
+ drawn from :attr:`settings` (e.g. ``service_name``). Pydantic v2
133
+ validates every field at construction time.
134
+
135
+ The event is returned immediately — no transport, retry, or plugin
136
+ hooks are invoked at this stage.
137
+
138
+ Args:
139
+ action: The action performed (e.g. ``"CREATE"``, ``"UPDATE"``).
140
+ Accepts any :class:`~audit_sdk.models.enums.AuditAction` value
141
+ or a custom non-empty string.
142
+ entity: Entity type acted upon (e.g. ``"Employee"``, ``"Invoice"``).
143
+ entity_id: Unique identifier of the specific entity instance.
144
+ user: Actor who performed the action. Must contain at least
145
+ ``"id"`` and ``"name"`` keys.
146
+ old_value: State of the entity *before* the action.
147
+ Pass ``None`` for CREATE operations.
148
+ new_value: State of the entity *after* the action.
149
+ Pass ``None`` for DELETE operations.
150
+ metadata: Arbitrary key-value pairs for custom enrichment
151
+ (tags, batch IDs, feature flags, etc.).
152
+ tenant_id: Tenant / organisation identifier for multi-tenant
153
+ deployments (e.g. ``"tenant-acme"``).
154
+ application: Top-level application name (e.g. ``"HRPortal"``).
155
+ module: Sub-module or feature area (e.g. ``"Payroll"``).
156
+ service: Emitting service name. Falls back to
157
+ ``settings.service_name`` when not supplied.
158
+ event_type: High-level event category. Use
159
+ :class:`~audit_sdk.models.audit_event.EventType` values or any
160
+ custom string. Defaults to ``EventType.GENERIC`` when omitted.
161
+ status: Outcome of the audited operation. Use
162
+ :class:`~audit_sdk.models.audit_event.AuditOutcome` values or
163
+ any custom string. Defaults to ``AuditOutcome.SUCCESS`` when
164
+ omitted.
165
+ request: HTTP request context dict. Accepted keys match
166
+ :class:`~audit_sdk.models.audit_event.RequestModel`.
167
+ device: Client device context dict. Accepted keys match
168
+ :class:`~audit_sdk.models.audit_event.DeviceModel`.
169
+
170
+ Returns:
171
+ A fully validated :class:`~audit_sdk.models.audit_event.AuditEvent`
172
+ instance. ``correlation_id`` and ``timestamp`` are auto-populated.
173
+
174
+ Raises:
175
+ ValidationError: If any field fails Pydantic v2 validation.
176
+ AuditSDKError: For any other SDK-level error.
177
+
178
+ Example::
179
+
180
+ event = await audit.log(
181
+ action="UPDATE",
182
+ entity="Invoice",
183
+ entity_id="INV-9999",
184
+ user={"id": "42", "name": "Finance Bot"},
185
+ tenant_id="tenant-001",
186
+ application="FinanceSystem",
187
+ module="Invoicing",
188
+ event_type="BUSINESS",
189
+ status="SUCCESS",
190
+ old_value={"status": "draft"},
191
+ new_value={"status": "approved"},
192
+ metadata={"approved_by": "cfo@example.com"},
193
+ )
194
+ """
195
+ return self._build_event(
196
+ action=action,
197
+ entity=entity,
198
+ entity_id=entity_id,
199
+ user=user,
200
+ old_value=old_value,
201
+ new_value=new_value,
202
+ metadata=metadata,
203
+ tenant_id=tenant_id,
204
+ application=application,
205
+ module=module,
206
+ service=service,
207
+ event_type=event_type,
208
+ status=status,
209
+ request=request,
210
+ device=device,
211
+ )
212
+
213
+ async def log_event(self, event: AuditEvent) -> None:
214
+ """Dispatch a pre-built :class:`AuditEvent` directly.
215
+
216
+ Attempts to deliver *event* via the configured transport. On a
217
+ retryable failure (network error, timeout, HTTP 5xx/429) the event
218
+ is handed off to the :class:`~audit_sdk.utils.retry_manager.RetryManager`
219
+ if one is configured and ``settings.retry_enabled`` is ``True``.
220
+ On a non-retryable failure (HTTP 4xx) the :class:`TransportError`
221
+ is re-raised immediately.
222
+
223
+ Args:
224
+ event: A fully validated :class:`~audit_sdk.models.audit_event.AuditEvent`.
225
+
226
+ Raises:
227
+ ConfigurationError: If no transport has been injected.
228
+ TransportError: For non-retryable HTTP errors, or retryable errors
229
+ when no :class:`RetryManager` is configured.
230
+ QueueFullError: If the retry queue is at capacity.
231
+ """
232
+ if self._transport is None:
233
+ raise ConfigurationError(
234
+ "No transport configured. "
235
+ "Pass transport= when constructing AuditClient."
236
+ )
237
+
238
+ try:
239
+ await self._transport.send(event)
240
+
241
+ except TransportError as exc:
242
+ is_retryable = (
243
+ exc.status_code is None # network / timeout
244
+ or exc.status_code in RetryManager.RETRYABLE_STATUS_CODES
245
+ )
246
+
247
+ retry_enabled = self._settings.retry_enabled if self._settings is not None else True
248
+ if is_retryable and self._retry_manager is not None and retry_enabled:
249
+ log.warning(
250
+ "Transport delivery failed (retryable). "
251
+ "Enqueuing event for retry. "
252
+ "correlation_id=%s, status=%s",
253
+ event.correlation_id,
254
+ exc.status_code,
255
+ )
256
+ await self._retry_manager.enqueue(event)
257
+ return # caller is not blocked; retry proceeds in background
258
+
259
+ raise # non-retryable or no retry manager
260
+
261
+ async def publish(
262
+ self,
263
+ action: str,
264
+ entity: str,
265
+ entity_id: str,
266
+ user: dict[str, Any],
267
+ old_value: Any = None,
268
+ new_value: Any = None,
269
+ metadata: dict[str, Any] | None = None,
270
+ tenant_id: str | None = None,
271
+ application: str | None = None,
272
+ module: str | None = None,
273
+ service: str | None = None,
274
+ event_type: str | None = None,
275
+ status: str | None = None,
276
+ request: dict[str, Any] | None = None,
277
+ device: dict[str, Any] | None = None,
278
+ ) -> AuditEvent:
279
+ """Build, validate, and dispatch a single audit event in one call.
280
+
281
+ Convenience method that combines :meth:`log` (build + validate) and
282
+ :meth:`log_event` (transport dispatch + retry hand-off).
283
+
284
+ On a **retryable** transport failure the event is enqueued in the
285
+ :class:`~audit_sdk.utils.retry_manager.RetryManager` (if configured)
286
+ and this method returns normally — the caller is not blocked.
287
+
288
+ On a **non-retryable** failure the :class:`TransportError` is
289
+ propagated immediately.
290
+
291
+ Args:
292
+ action: Action verb (e.g. ``"CREATE"``, ``"UPDATE"``).
293
+ entity: Entity type acted upon (e.g. ``"Employee"``).
294
+ entity_id: Unique identifier of the entity instance.
295
+ user: Actor dict — must contain ``"id"`` and ``"name"``.
296
+ old_value: Entity state *before* the action (``None`` for CREATE).
297
+ new_value: Entity state *after* the action (``None`` for DELETE).
298
+ metadata: Arbitrary key-value enrichment.
299
+ tenant_id: Tenant / organisation identifier.
300
+ application: Top-level application name.
301
+ module: Sub-module or feature area.
302
+ service: Emitting service name (falls back to
303
+ ``settings.service_name``).
304
+ event_type: High-level event category (default: ``GENERIC``).
305
+ status: Outcome of the audited operation (default: ``SUCCESS``).
306
+ request: HTTP request context dict.
307
+ device: Client device context dict.
308
+
309
+ Returns:
310
+ The fully validated :class:`~audit_sdk.models.audit_event.AuditEvent`
311
+ that was dispatched (or queued for retry).
312
+
313
+ Raises:
314
+ ValidationError: If the event parameters fail Pydantic validation.
315
+ ConfigurationError: If no transport has been injected.
316
+ TransportError: For non-retryable delivery failures.
317
+ QueueFullError: If the retry queue is at capacity.
318
+
319
+ Example::
320
+
321
+ event = await audit.publish(
322
+ action="CREATE",
323
+ entity="Employee",
324
+ entity_id="EMP-001",
325
+ user={"id": "u1", "name": "Alice"},
326
+ tenant_id="tenant-acme",
327
+ application="HRPortal",
328
+ new_value={"name": "Alice", "department": "Engineering"},
329
+ )
330
+ """
331
+ event = await self.log(
332
+ action=action,
333
+ entity=entity,
334
+ entity_id=entity_id,
335
+ user=user,
336
+ old_value=old_value,
337
+ new_value=new_value,
338
+ metadata=metadata,
339
+ tenant_id=tenant_id,
340
+ application=application,
341
+ module=module,
342
+ service=service,
343
+ event_type=event_type,
344
+ status=status,
345
+ request=request,
346
+ device=device,
347
+ )
348
+ await self.log_event(event)
349
+ return event
350
+
351
+ # ------------------------------------------------------------------
352
+ # Lifecycle
353
+ # ------------------------------------------------------------------
354
+
355
+ async def close(self) -> None:
356
+ """Flush pending retries, then release all held resources.
357
+
358
+ Stops the :class:`~audit_sdk.utils.retry_manager.RetryManager`
359
+ (waiting for in-flight retry tasks to complete) before closing the
360
+ transport connection.
361
+
362
+ After calling this method, the client must not be used.
363
+
364
+ This method is idempotent: calling it multiple times is safe.
365
+ """
366
+ if self._closed:
367
+ return
368
+ self._closed = True
369
+ if self._retry_manager is not None:
370
+ await self._retry_manager.stop()
371
+ if self._transport is not None:
372
+ await self._transport.close()
373
+ log.debug("AuditClient closed.")
374
+
375
+ async def __aenter__(self) -> AuditClient:
376
+ return self
377
+
378
+ async def __aexit__(self, *_: Any) -> None:
379
+ await self.close()
380
+
381
+ # ------------------------------------------------------------------
382
+ # Lazy component initialization (stubs)
383
+ # ------------------------------------------------------------------
384
+
385
+ def _get_transport(self) -> AbstractTransport:
386
+ """Return the configured transport, creating a default RestTransport if needed.
387
+
388
+ Returns:
389
+ The active :class:`~audit_sdk.core.interfaces.AbstractTransport`.
390
+
391
+ Raises:
392
+ NotImplementedError: Until factory logic is implemented.
393
+ """
394
+ # TODO: Auto-create RestTransport / KafkaTransport based on settings.transport.
395
+ raise NotImplementedError("AuditClient._get_transport() is not yet implemented.")
396
+
397
+ def _get_serializer(self) -> AbstractSerializer:
398
+ """Return the configured serializer, defaulting to AuditEventSerializer.
399
+
400
+ Returns:
401
+ The active :class:`~audit_sdk.core.interfaces.AbstractSerializer`.
402
+
403
+ Raises:
404
+ NotImplementedError: Until factory logic is implemented.
405
+ """
406
+ # TODO: Return self._serializer or instantiate AuditEventSerializer().
407
+ raise NotImplementedError("AuditClient._get_serializer() is not yet implemented.")
408
+
409
+ def _build_event(
410
+ self,
411
+ action: str,
412
+ entity: str,
413
+ entity_id: str,
414
+ user: Any,
415
+ old_value: Any,
416
+ new_value: Any,
417
+ metadata: dict[str, Any] | None,
418
+ tenant_id: str | None,
419
+ application: str | None,
420
+ module: str | None,
421
+ service: str | None,
422
+ event_type: str | None,
423
+ status: str | None,
424
+ request: Any,
425
+ device: Any,
426
+ ) -> AuditEvent:
427
+ """Construct a validated :class:`AuditEvent` from individual parameters.
428
+
429
+ Merges caller-supplied values with context drawn from :attr:`_settings`:
430
+
431
+ - ``service`` falls back to ``settings.service_name`` when not supplied.
432
+ - All other optional fields fall back to :class:`AuditEvent`'s own
433
+ defaults (``None``, ``EventType.GENERIC``, ``AuditOutcome.SUCCESS``)
434
+ when the caller passes ``None``.
435
+
436
+ Only non-``None`` optional values are forwarded to the model, keeping
437
+ AuditEvent's own defaults intact for anything the caller did not supply.
438
+
439
+ Args:
440
+ action: The action verb.
441
+ entity: Entity type name.
442
+ entity_id: Entity instance ID.
443
+ user: Actor dict or :class:`~audit_sdk.models.audit_event.ActorModel`.
444
+ old_value: Pre-action entity state (``None`` for CREATE).
445
+ new_value: Post-action entity state (``None`` for DELETE).
446
+ metadata: Arbitrary key-value enrichment dict.
447
+ tenant_id: Tenant / organisation identifier.
448
+ application: Top-level application name.
449
+ module: Sub-module or feature area name.
450
+ service: Emitting service name (falls back to ``settings.service_name``).
451
+ event_type: High-level event category string.
452
+ status: Outcome string of the audited operation.
453
+ request: HTTP request context (dict or RequestModel).
454
+ device: Client device context (dict or DeviceModel).
455
+
456
+ Returns:
457
+ A fully validated :class:`~audit_sdk.models.audit_event.AuditEvent`.
458
+
459
+ Raises:
460
+ ValidationError: Wraps any Pydantic v2 ``ValidationError`` raised
461
+ during model construction so callers only need to handle SDK
462
+ exception types.
463
+ """
464
+ from pydantic import ValidationError as PydanticValidationError
465
+
466
+ from audit_sdk.core.exceptions import ValidationError as SDKValidationError
467
+
468
+ # Required fields — always forwarded.
469
+ kwargs: dict[str, Any] = {
470
+ "action": action,
471
+ "entity": entity,
472
+ "entity_id": entity_id,
473
+ "user": user,
474
+ "old_value": old_value,
475
+ "new_value": new_value,
476
+ # Fall back to the settings service_name when the caller does not
477
+ # explicitly pass a service name.
478
+ "service": service if service is not None else (
479
+ self._settings.service_name if self._settings is not None else "unknown-service"
480
+ ),
481
+ }
482
+
483
+ # Optional fields — only injected when the caller provided a value,
484
+ # so AuditEvent's own defaults remain in effect for omitted fields.
485
+ if tenant_id is not None:
486
+ kwargs["tenant_id"] = tenant_id
487
+ if application is not None:
488
+ kwargs["application"] = application
489
+ if module is not None:
490
+ kwargs["module"] = module
491
+ if event_type is not None:
492
+ kwargs["event_type"] = event_type
493
+ if status is not None:
494
+ kwargs["status"] = status
495
+ if metadata is not None:
496
+ kwargs["metadata"] = metadata
497
+ if request is not None:
498
+ kwargs["request"] = request
499
+ if device is not None:
500
+ kwargs["device"] = device
501
+
502
+ try:
503
+ return AuditEvent(**kwargs)
504
+ except PydanticValidationError as exc:
505
+ raise SDKValidationError(
506
+ f"AuditEvent validation failed: {exc}",
507
+ errors=exc.errors(),
508
+ ) from exc
509
+
510
+ # ------------------------------------------------------------------
511
+ # Properties
512
+ # ------------------------------------------------------------------
513
+
514
+ @property
515
+ def settings(self) -> AuditSettings | None:
516
+ """The SDK configuration object (``None`` when using direct transport)."""
517
+ return self._settings
518
+
519
+ @property
520
+ def plugin_registry(self) -> PluginRegistry:
521
+ """The plugin registry for this client instance."""
522
+ return self._plugin_registry
523
+
524
+ @property
525
+ def is_closed(self) -> bool:
526
+ """Return ``True`` if the client has been closed."""
527
+ return self._closed
528
+
529
+ @property
530
+ def retry_manager(self) -> RetryManager | None:
531
+ """The injected :class:`~audit_sdk.utils.retry_manager.RetryManager`, if any."""
532
+ return self._retry_manager
533
+
534
+ def __repr__(self) -> str:
535
+ svc = self._settings.service_name if self._settings else "direct-transport"
536
+ env = self._settings.environment if self._settings else "n/a"
537
+ tpt = str(self._settings.transport) if self._settings else type(self._transport).__name__
538
+ return (
539
+ f"AuditClient(service={svc!r}, "
540
+ f"env={env!r}, "
541
+ f"transport={tpt!r}, "
542
+ f"retry={'enabled' if self._retry_manager else 'disabled'}, "
543
+ f"closed={self._closed})"
544
+ )
@@ -0,0 +1,10 @@
1
+ """
2
+ config — SDK-wide configuration management.
3
+
4
+ All configuration is expressed as a single :class:`~audit_sdk.config.settings.AuditSettings`
5
+ Pydantic model that reads values from environment variables with the ``AUDIT_``
6
+ prefix (e.g. ``AUDIT_ENDPOINT``, ``AUDIT_API_KEY``).
7
+
8
+ No defaults require external services; the SDK is always safe to import even
9
+ when environment variables are absent (except for required fields like ``endpoint``).
10
+ """