fraiseql-confiture 0.3.7__cp311-cp311-macosx_11_0_arm64.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 (124) hide show
  1. confiture/__init__.py +48 -0
  2. confiture/_core.cpython-311-darwin.so +0 -0
  3. confiture/cli/__init__.py +0 -0
  4. confiture/cli/dry_run.py +116 -0
  5. confiture/cli/lint_formatter.py +193 -0
  6. confiture/cli/main.py +1893 -0
  7. confiture/config/__init__.py +0 -0
  8. confiture/config/environment.py +263 -0
  9. confiture/core/__init__.py +51 -0
  10. confiture/core/anonymization/__init__.py +0 -0
  11. confiture/core/anonymization/audit.py +485 -0
  12. confiture/core/anonymization/benchmarking.py +372 -0
  13. confiture/core/anonymization/breach_notification.py +652 -0
  14. confiture/core/anonymization/compliance.py +617 -0
  15. confiture/core/anonymization/composer.py +298 -0
  16. confiture/core/anonymization/data_subject_rights.py +669 -0
  17. confiture/core/anonymization/factory.py +319 -0
  18. confiture/core/anonymization/governance.py +737 -0
  19. confiture/core/anonymization/performance.py +1092 -0
  20. confiture/core/anonymization/profile.py +284 -0
  21. confiture/core/anonymization/registry.py +195 -0
  22. confiture/core/anonymization/security/kms_manager.py +547 -0
  23. confiture/core/anonymization/security/lineage.py +888 -0
  24. confiture/core/anonymization/security/token_store.py +686 -0
  25. confiture/core/anonymization/strategies/__init__.py +41 -0
  26. confiture/core/anonymization/strategies/address.py +359 -0
  27. confiture/core/anonymization/strategies/credit_card.py +374 -0
  28. confiture/core/anonymization/strategies/custom.py +161 -0
  29. confiture/core/anonymization/strategies/date.py +218 -0
  30. confiture/core/anonymization/strategies/differential_privacy.py +398 -0
  31. confiture/core/anonymization/strategies/email.py +141 -0
  32. confiture/core/anonymization/strategies/format_preserving_encryption.py +310 -0
  33. confiture/core/anonymization/strategies/hash.py +150 -0
  34. confiture/core/anonymization/strategies/ip_address.py +235 -0
  35. confiture/core/anonymization/strategies/masking_retention.py +252 -0
  36. confiture/core/anonymization/strategies/name.py +298 -0
  37. confiture/core/anonymization/strategies/phone.py +119 -0
  38. confiture/core/anonymization/strategies/preserve.py +85 -0
  39. confiture/core/anonymization/strategies/redact.py +101 -0
  40. confiture/core/anonymization/strategies/salted_hashing.py +322 -0
  41. confiture/core/anonymization/strategies/text_redaction.py +183 -0
  42. confiture/core/anonymization/strategies/tokenization.py +334 -0
  43. confiture/core/anonymization/strategy.py +241 -0
  44. confiture/core/anonymization/syncer_audit.py +357 -0
  45. confiture/core/blue_green.py +683 -0
  46. confiture/core/builder.py +500 -0
  47. confiture/core/checksum.py +358 -0
  48. confiture/core/connection.py +184 -0
  49. confiture/core/differ.py +522 -0
  50. confiture/core/drift.py +564 -0
  51. confiture/core/dry_run.py +182 -0
  52. confiture/core/health.py +313 -0
  53. confiture/core/hooks/__init__.py +87 -0
  54. confiture/core/hooks/base.py +232 -0
  55. confiture/core/hooks/context.py +146 -0
  56. confiture/core/hooks/execution_strategies.py +57 -0
  57. confiture/core/hooks/observability.py +220 -0
  58. confiture/core/hooks/phases.py +53 -0
  59. confiture/core/hooks/registry.py +295 -0
  60. confiture/core/large_tables.py +775 -0
  61. confiture/core/linting/__init__.py +70 -0
  62. confiture/core/linting/composer.py +192 -0
  63. confiture/core/linting/libraries/__init__.py +17 -0
  64. confiture/core/linting/libraries/gdpr.py +168 -0
  65. confiture/core/linting/libraries/general.py +184 -0
  66. confiture/core/linting/libraries/hipaa.py +144 -0
  67. confiture/core/linting/libraries/pci_dss.py +104 -0
  68. confiture/core/linting/libraries/sox.py +120 -0
  69. confiture/core/linting/schema_linter.py +491 -0
  70. confiture/core/linting/versioning.py +151 -0
  71. confiture/core/locking.py +389 -0
  72. confiture/core/migration_generator.py +298 -0
  73. confiture/core/migrator.py +882 -0
  74. confiture/core/observability/__init__.py +44 -0
  75. confiture/core/observability/audit.py +323 -0
  76. confiture/core/observability/logging.py +187 -0
  77. confiture/core/observability/metrics.py +174 -0
  78. confiture/core/observability/tracing.py +192 -0
  79. confiture/core/pg_version.py +418 -0
  80. confiture/core/pool.py +406 -0
  81. confiture/core/risk/__init__.py +39 -0
  82. confiture/core/risk/predictor.py +188 -0
  83. confiture/core/risk/scoring.py +248 -0
  84. confiture/core/rollback_generator.py +388 -0
  85. confiture/core/schema_analyzer.py +769 -0
  86. confiture/core/schema_to_schema.py +590 -0
  87. confiture/core/security/__init__.py +32 -0
  88. confiture/core/security/logging.py +201 -0
  89. confiture/core/security/validation.py +416 -0
  90. confiture/core/signals.py +371 -0
  91. confiture/core/syncer.py +540 -0
  92. confiture/exceptions.py +192 -0
  93. confiture/integrations/__init__.py +0 -0
  94. confiture/models/__init__.py +24 -0
  95. confiture/models/lint.py +193 -0
  96. confiture/models/migration.py +265 -0
  97. confiture/models/schema.py +203 -0
  98. confiture/models/sql_file_migration.py +225 -0
  99. confiture/scenarios/__init__.py +36 -0
  100. confiture/scenarios/compliance.py +586 -0
  101. confiture/scenarios/ecommerce.py +199 -0
  102. confiture/scenarios/financial.py +253 -0
  103. confiture/scenarios/healthcare.py +315 -0
  104. confiture/scenarios/multi_tenant.py +340 -0
  105. confiture/scenarios/saas.py +295 -0
  106. confiture/testing/FRAMEWORK_API.md +722 -0
  107. confiture/testing/__init__.py +100 -0
  108. confiture/testing/fixtures/__init__.py +11 -0
  109. confiture/testing/fixtures/data_validator.py +229 -0
  110. confiture/testing/fixtures/migration_runner.py +167 -0
  111. confiture/testing/fixtures/schema_snapshotter.py +352 -0
  112. confiture/testing/frameworks/__init__.py +10 -0
  113. confiture/testing/frameworks/mutation.py +587 -0
  114. confiture/testing/frameworks/performance.py +479 -0
  115. confiture/testing/loader.py +225 -0
  116. confiture/testing/pytest/__init__.py +38 -0
  117. confiture/testing/pytest_plugin.py +190 -0
  118. confiture/testing/sandbox.py +304 -0
  119. confiture/testing/utils/__init__.py +0 -0
  120. fraiseql_confiture-0.3.7.dist-info/METADATA +438 -0
  121. fraiseql_confiture-0.3.7.dist-info/RECORD +124 -0
  122. fraiseql_confiture-0.3.7.dist-info/WHEEL +4 -0
  123. fraiseql_confiture-0.3.7.dist-info/entry_points.txt +4 -0
  124. fraiseql_confiture-0.3.7.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,44 @@
1
+ """Observability module for Confiture.
2
+
3
+ Provides optional integrations with:
4
+ - OpenTelemetry (tracing)
5
+ - Prometheus (metrics)
6
+ - Structured logging
7
+ - Audit trail
8
+ """
9
+
10
+ from confiture.core.observability.audit import (
11
+ AuditConfig,
12
+ AuditEntry,
13
+ AuditTrail,
14
+ )
15
+ from confiture.core.observability.logging import (
16
+ LoggingConfig,
17
+ StructuredLogger,
18
+ configure_logging,
19
+ )
20
+ from confiture.core.observability.metrics import (
21
+ MetricsConfig,
22
+ MigrationMetrics,
23
+ create_metrics,
24
+ )
25
+ from confiture.core.observability.tracing import (
26
+ MigrationTracer,
27
+ TracingConfig,
28
+ create_tracer,
29
+ )
30
+
31
+ __all__ = [
32
+ "TracingConfig",
33
+ "MigrationTracer",
34
+ "create_tracer",
35
+ "MetricsConfig",
36
+ "MigrationMetrics",
37
+ "create_metrics",
38
+ "LoggingConfig",
39
+ "StructuredLogger",
40
+ "configure_logging",
41
+ "AuditConfig",
42
+ "AuditTrail",
43
+ "AuditEntry",
44
+ ]
@@ -0,0 +1,323 @@
1
+ """Audit trail for migration compliance.
2
+
3
+ Tracks who ran migrations, when, and the outcome for compliance purposes.
4
+ """
5
+
6
+ import getpass
7
+ import json
8
+ import logging
9
+ import os
10
+ import socket
11
+ from dataclasses import dataclass, field
12
+ from datetime import UTC, datetime
13
+ from typing import Any
14
+
15
+ import psycopg
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ @dataclass
21
+ class AuditConfig:
22
+ """Configuration for audit trail."""
23
+
24
+ enabled: bool = True
25
+ table_name: str = "confiture_audit_log"
26
+
27
+
28
+ @dataclass
29
+ class AuditEntry:
30
+ """A single audit log entry."""
31
+
32
+ migration_version: str
33
+ migration_name: str
34
+ action: str # "apply", "rollback", "dry_run"
35
+ status: str # "started", "completed", "failed"
36
+ user: str
37
+ hostname: str
38
+ timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
39
+ duration_ms: int | None = None
40
+ error_message: str | None = None
41
+ metadata: dict[str, Any] = field(default_factory=dict)
42
+
43
+ def to_dict(self) -> dict[str, Any]:
44
+ """Convert to dictionary for JSON serialization."""
45
+ return {
46
+ "migration_version": self.migration_version,
47
+ "migration_name": self.migration_name,
48
+ "action": self.action,
49
+ "status": self.status,
50
+ "user": self.user,
51
+ "hostname": self.hostname,
52
+ "timestamp": self.timestamp.isoformat(),
53
+ "duration_ms": self.duration_ms,
54
+ "error_message": self.error_message,
55
+ "metadata": self.metadata,
56
+ }
57
+
58
+
59
+ class AuditTrail:
60
+ """Records audit trail for compliance.
61
+
62
+ Tracks who ran migrations, when, from which machine, and whether
63
+ they succeeded or failed.
64
+
65
+ Example:
66
+ >>> audit = AuditTrail(conn, AuditConfig(enabled=True))
67
+ >>> audit.initialize()
68
+ >>> entry_id = audit.record_start("001", "create_users", "apply")
69
+ >>> # ... run migration ...
70
+ >>> audit.record_complete(entry_id, duration_ms=1500)
71
+ """
72
+
73
+ def __init__(
74
+ self,
75
+ connection: psycopg.Connection,
76
+ config: AuditConfig | None = None,
77
+ ):
78
+ """Initialize audit trail.
79
+
80
+ Args:
81
+ connection: Database connection
82
+ config: Audit configuration (optional)
83
+ """
84
+ self.connection = connection
85
+ self.config = config or AuditConfig()
86
+ self._initialized = False
87
+
88
+ @property
89
+ def is_enabled(self) -> bool:
90
+ """Check if audit trail is enabled."""
91
+ return self.config.enabled
92
+
93
+ def initialize(self) -> None:
94
+ """Create audit log table if it doesn't exist."""
95
+ if not self.config.enabled:
96
+ return
97
+
98
+ with self.connection.cursor() as cur:
99
+ cur.execute(f"""
100
+ CREATE TABLE IF NOT EXISTS {self.config.table_name} (
101
+ id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
102
+ migration_version VARCHAR(255) NOT NULL,
103
+ migration_name VARCHAR(255) NOT NULL,
104
+ action VARCHAR(50) NOT NULL,
105
+ status VARCHAR(50) NOT NULL,
106
+ username VARCHAR(255) NOT NULL,
107
+ hostname VARCHAR(255) NOT NULL,
108
+ started_at TIMESTAMPTZ NOT NULL,
109
+ completed_at TIMESTAMPTZ,
110
+ duration_ms INTEGER,
111
+ error_message TEXT,
112
+ metadata JSONB DEFAULT '{{}}'::jsonb
113
+ )
114
+ """)
115
+
116
+ cur.execute(f"""
117
+ CREATE INDEX IF NOT EXISTS idx_{self.config.table_name}_version
118
+ ON {self.config.table_name}(migration_version)
119
+ """)
120
+
121
+ cur.execute(f"""
122
+ CREATE INDEX IF NOT EXISTS idx_{self.config.table_name}_started
123
+ ON {self.config.table_name}(started_at DESC)
124
+ """)
125
+
126
+ cur.execute(f"""
127
+ CREATE INDEX IF NOT EXISTS idx_{self.config.table_name}_action
128
+ ON {self.config.table_name}(action)
129
+ """)
130
+
131
+ self.connection.commit()
132
+ self._initialized = True
133
+ logger.info("Audit trail initialized")
134
+
135
+ def record_start(
136
+ self,
137
+ migration_version: str,
138
+ migration_name: str,
139
+ action: str,
140
+ metadata: dict[str, Any] | None = None,
141
+ ) -> int:
142
+ """Record migration start.
143
+
144
+ Args:
145
+ migration_version: Migration version
146
+ migration_name: Migration name
147
+ action: Action type ("apply", "rollback", "dry_run")
148
+ metadata: Additional metadata (optional)
149
+
150
+ Returns:
151
+ Audit entry ID for later completion
152
+ """
153
+ if not self.config.enabled:
154
+ return -1
155
+
156
+ entry = AuditEntry(
157
+ migration_version=migration_version,
158
+ migration_name=migration_name,
159
+ action=action,
160
+ status="started",
161
+ user=self._get_current_user(),
162
+ hostname=self._get_hostname(),
163
+ metadata=metadata or {},
164
+ )
165
+
166
+ with self.connection.cursor() as cur:
167
+ cur.execute(
168
+ f"""
169
+ INSERT INTO {self.config.table_name}
170
+ (migration_version, migration_name, action, status,
171
+ username, hostname, started_at, metadata)
172
+ VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
173
+ RETURNING id
174
+ """,
175
+ (
176
+ entry.migration_version,
177
+ entry.migration_name,
178
+ entry.action,
179
+ entry.status,
180
+ entry.user,
181
+ entry.hostname,
182
+ entry.timestamp,
183
+ json.dumps(entry.metadata),
184
+ ),
185
+ )
186
+ result = cur.fetchone()
187
+ entry_id = result[0] if result else -1
188
+
189
+ self.connection.commit()
190
+ return entry_id
191
+
192
+ def record_complete(
193
+ self,
194
+ entry_id: int,
195
+ duration_ms: int,
196
+ error_message: str | None = None,
197
+ ) -> None:
198
+ """Record migration completion.
199
+
200
+ Args:
201
+ entry_id: Audit entry ID from record_start
202
+ duration_ms: Duration in milliseconds
203
+ error_message: Error message if failed (optional)
204
+ """
205
+ if not self.config.enabled or entry_id < 0:
206
+ return
207
+
208
+ status = "failed" if error_message else "completed"
209
+
210
+ with self.connection.cursor() as cur:
211
+ cur.execute(
212
+ f"""
213
+ UPDATE {self.config.table_name}
214
+ SET status = %s,
215
+ completed_at = %s,
216
+ duration_ms = %s,
217
+ error_message = %s
218
+ WHERE id = %s
219
+ """,
220
+ (
221
+ status,
222
+ datetime.now(UTC),
223
+ duration_ms,
224
+ error_message,
225
+ entry_id,
226
+ ),
227
+ )
228
+
229
+ self.connection.commit()
230
+
231
+ def get_history(
232
+ self,
233
+ migration_version: str | None = None,
234
+ action: str | None = None,
235
+ limit: int = 100,
236
+ ) -> list[dict[str, Any]]:
237
+ """Get audit history.
238
+
239
+ Args:
240
+ migration_version: Filter by version (optional)
241
+ action: Filter by action (optional)
242
+ limit: Maximum number of entries
243
+
244
+ Returns:
245
+ List of audit entries as dictionaries
246
+ """
247
+ if not self.config.enabled:
248
+ return []
249
+
250
+ conditions = []
251
+ params: list[Any] = []
252
+
253
+ if migration_version:
254
+ conditions.append("migration_version = %s")
255
+ params.append(migration_version)
256
+ if action:
257
+ conditions.append("action = %s")
258
+ params.append(action)
259
+
260
+ where_clause = ""
261
+ if conditions:
262
+ where_clause = "WHERE " + " AND ".join(conditions)
263
+
264
+ params.append(limit)
265
+
266
+ with self.connection.cursor() as cur:
267
+ cur.execute(
268
+ f"""
269
+ SELECT id, migration_version, migration_name, action, status,
270
+ username, hostname, started_at, completed_at,
271
+ duration_ms, error_message, metadata
272
+ FROM {self.config.table_name}
273
+ {where_clause}
274
+ ORDER BY started_at DESC
275
+ LIMIT %s
276
+ """,
277
+ params,
278
+ )
279
+
280
+ columns = [desc[0] for desc in cur.description]
281
+ return [dict(zip(columns, row, strict=True)) for row in cur.fetchall()]
282
+
283
+ def get_recent_failures(self, limit: int = 10) -> list[dict[str, Any]]:
284
+ """Get recent failed migrations.
285
+
286
+ Args:
287
+ limit: Maximum number of entries
288
+
289
+ Returns:
290
+ List of failed audit entries
291
+ """
292
+ if not self.config.enabled:
293
+ return []
294
+
295
+ with self.connection.cursor() as cur:
296
+ cur.execute(
297
+ f"""
298
+ SELECT id, migration_version, migration_name, action,
299
+ username, hostname, started_at, error_message
300
+ FROM {self.config.table_name}
301
+ WHERE status = 'failed'
302
+ ORDER BY started_at DESC
303
+ LIMIT %s
304
+ """,
305
+ (limit,),
306
+ )
307
+
308
+ columns = [desc[0] for desc in cur.description]
309
+ return [dict(zip(columns, row, strict=True)) for row in cur.fetchall()]
310
+
311
+ def _get_current_user(self) -> str:
312
+ """Get current username."""
313
+ try:
314
+ return getpass.getuser()
315
+ except Exception:
316
+ return os.environ.get("USER", "unknown")
317
+
318
+ def _get_hostname(self) -> str:
319
+ """Get current hostname."""
320
+ try:
321
+ return socket.gethostname()
322
+ except Exception:
323
+ return "unknown"
@@ -0,0 +1,187 @@
1
+ """Structured JSON logging for migrations.
2
+
3
+ Provides structured logging with correlation IDs for migration operations.
4
+ """
5
+
6
+ import json
7
+ import logging
8
+ import sys
9
+ import uuid
10
+ from dataclasses import dataclass
11
+ from datetime import UTC, datetime
12
+ from typing import Any
13
+
14
+
15
+ @dataclass
16
+ class LoggingConfig:
17
+ """Configuration for structured logging."""
18
+
19
+ enabled: bool = True
20
+ format: str = "json" # "json" or "text"
21
+ level: str = "INFO"
22
+ include_timestamp: bool = True
23
+ include_correlation_id: bool = True
24
+
25
+
26
+ class StructuredFormatter(logging.Formatter):
27
+ """JSON formatter for structured logging."""
28
+
29
+ def __init__(self, config: LoggingConfig):
30
+ """Initialize structured formatter.
31
+
32
+ Args:
33
+ config: Logging configuration
34
+ """
35
+ super().__init__()
36
+ self.config = config
37
+
38
+ def format(self, record: logging.LogRecord) -> str:
39
+ """Format log record as JSON.
40
+
41
+ Args:
42
+ record: Log record to format
43
+
44
+ Returns:
45
+ JSON-formatted log string
46
+ """
47
+ log_data: dict[str, Any] = {
48
+ "level": record.levelname,
49
+ "message": record.getMessage(),
50
+ "logger": record.name,
51
+ }
52
+
53
+ if self.config.include_timestamp:
54
+ log_data["timestamp"] = datetime.now(UTC).isoformat()
55
+
56
+ if self.config.include_correlation_id:
57
+ log_data["correlation_id"] = getattr(record, "correlation_id", str(uuid.uuid4())[:8])
58
+
59
+ # Add extra fields from record
60
+ extra_fields = [
61
+ "migration_version",
62
+ "migration_name",
63
+ "duration_ms",
64
+ "rows_affected",
65
+ "operation",
66
+ "table_name",
67
+ "status",
68
+ ]
69
+ for field in extra_fields:
70
+ if hasattr(record, field):
71
+ log_data[field] = getattr(record, field)
72
+
73
+ # Add exception info
74
+ if record.exc_info:
75
+ log_data["exception"] = self.formatException(record.exc_info)
76
+
77
+ return json.dumps(log_data)
78
+
79
+
80
+ class StructuredLogger:
81
+ """Logger with structured output and correlation IDs.
82
+
83
+ Provides a logging interface that supports correlation IDs
84
+ and structured fields for migration operations.
85
+
86
+ Example:
87
+ >>> logger = StructuredLogger("confiture.migration")
88
+ >>> logger.set_correlation_id("abc123")
89
+ >>> logger.info("Starting migration", migration_version="001")
90
+ """
91
+
92
+ def __init__(self, name: str, config: LoggingConfig | None = None):
93
+ """Initialize structured logger.
94
+
95
+ Args:
96
+ name: Logger name
97
+ config: Logging configuration (optional)
98
+ """
99
+ self.config = config or LoggingConfig()
100
+ self._logger = logging.getLogger(name)
101
+ self._correlation_id: str | None = None
102
+
103
+ def _log(self, level: int, message: str, **kwargs: Any) -> None:
104
+ """Log with extra fields.
105
+
106
+ Args:
107
+ level: Log level
108
+ message: Log message
109
+ **kwargs: Extra fields to include
110
+ """
111
+ extra = {"correlation_id": self._correlation_id or str(uuid.uuid4())[:8]}
112
+ extra.update(kwargs)
113
+ self._logger.log(level, message, extra=extra)
114
+
115
+ def debug(self, message: str, **kwargs: Any) -> None:
116
+ """Log debug message."""
117
+ self._log(logging.DEBUG, message, **kwargs)
118
+
119
+ def info(self, message: str, **kwargs: Any) -> None:
120
+ """Log info message."""
121
+ self._log(logging.INFO, message, **kwargs)
122
+
123
+ def warning(self, message: str, **kwargs: Any) -> None:
124
+ """Log warning message."""
125
+ self._log(logging.WARNING, message, **kwargs)
126
+
127
+ def error(self, message: str, **kwargs: Any) -> None:
128
+ """Log error message."""
129
+ self._log(logging.ERROR, message, **kwargs)
130
+
131
+ def exception(self, message: str, **kwargs: Any) -> None:
132
+ """Log exception with traceback."""
133
+ extra = {"correlation_id": self._correlation_id or str(uuid.uuid4())[:8]}
134
+ extra.update(kwargs)
135
+ self._logger.exception(message, extra=extra)
136
+
137
+ def set_correlation_id(self, correlation_id: str) -> None:
138
+ """Set correlation ID for subsequent logs.
139
+
140
+ Args:
141
+ correlation_id: Correlation ID to use
142
+ """
143
+ self._correlation_id = correlation_id
144
+
145
+ def clear_correlation_id(self) -> None:
146
+ """Clear the correlation ID."""
147
+ self._correlation_id = None
148
+
149
+ def new_correlation_id(self) -> str:
150
+ """Generate and set a new correlation ID.
151
+
152
+ Returns:
153
+ The new correlation ID
154
+ """
155
+ self._correlation_id = str(uuid.uuid4())[:8]
156
+ return self._correlation_id
157
+
158
+
159
+ def configure_logging(config: LoggingConfig | None = None) -> None:
160
+ """Configure structured logging for Confiture.
161
+
162
+ Args:
163
+ config: Logging configuration (optional)
164
+ """
165
+ config = config or LoggingConfig()
166
+
167
+ # Get root logger for confiture
168
+ logger = logging.getLogger("confiture")
169
+ logger.setLevel(getattr(logging, config.level))
170
+
171
+ # Remove existing handlers
172
+ logger.handlers.clear()
173
+
174
+ # Add handler with appropriate formatter
175
+ handler = logging.StreamHandler(sys.stderr)
176
+
177
+ if config.format == "json":
178
+ handler.setFormatter(StructuredFormatter(config))
179
+ else:
180
+ handler.setFormatter(
181
+ logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
182
+ )
183
+
184
+ logger.addHandler(handler)
185
+
186
+ # Prevent propagation to root logger
187
+ logger.propagate = False