fraiseql-confiture 0.3.4__cp311-cp311-win_amd64.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 (119) hide show
  1. confiture/__init__.py +48 -0
  2. confiture/_core.cp311-win_amd64.pyd +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 +1656 -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 +132 -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 +793 -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 +0 -0
  95. confiture/models/lint.py +193 -0
  96. confiture/models/migration.py +180 -0
  97. confiture/models/schema.py +203 -0
  98. confiture/scenarios/__init__.py +36 -0
  99. confiture/scenarios/compliance.py +586 -0
  100. confiture/scenarios/ecommerce.py +199 -0
  101. confiture/scenarios/financial.py +253 -0
  102. confiture/scenarios/healthcare.py +315 -0
  103. confiture/scenarios/multi_tenant.py +340 -0
  104. confiture/scenarios/saas.py +295 -0
  105. confiture/testing/FRAMEWORK_API.md +722 -0
  106. confiture/testing/__init__.py +38 -0
  107. confiture/testing/fixtures/__init__.py +11 -0
  108. confiture/testing/fixtures/data_validator.py +229 -0
  109. confiture/testing/fixtures/migration_runner.py +167 -0
  110. confiture/testing/fixtures/schema_snapshotter.py +352 -0
  111. confiture/testing/frameworks/__init__.py +10 -0
  112. confiture/testing/frameworks/mutation.py +587 -0
  113. confiture/testing/frameworks/performance.py +479 -0
  114. confiture/testing/utils/__init__.py +0 -0
  115. fraiseql_confiture-0.3.4.dist-info/METADATA +438 -0
  116. fraiseql_confiture-0.3.4.dist-info/RECORD +119 -0
  117. fraiseql_confiture-0.3.4.dist-info/WHEEL +4 -0
  118. fraiseql_confiture-0.3.4.dist-info/entry_points.txt +2 -0
  119. fraiseql_confiture-0.3.4.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,669 @@
1
+ """Data subject rights automation.
2
+
3
+ Provides automated fulfillment of data subject rights required by modern
4
+ data protection regulations. These include access, erasure, portability,
5
+ and rectification rights.
6
+
7
+ Supported Rights:
8
+ - Right to Access: Get all data held about a person
9
+ - Right to Erasure: Delete all data about a person (GDPR "right to be forgotten")
10
+ - Right to Rectification: Correct inaccurate data
11
+ - Right to Portability: Get data in portable format
12
+ - Right to Restrict Processing: Limit how data is used
13
+ - Right to Object: Oppose processing for certain purposes
14
+
15
+ Regulations:
16
+ - GDPR: Articles 15-21 (EU)
17
+ - CCPA: Sections 1798.100, 1798.105, 1798.110 (USA)
18
+ - LGPD: Articles 18-23 (Brazil)
19
+ - Other modern regulations
20
+
21
+ Example:
22
+ >>> from confiture.core.anonymization.data_subject_rights import (
23
+ ... DataSubjectRightsManager, RequestType
24
+ ... )
25
+ >>>
26
+ >>> manager = DataSubjectRightsManager(conn, storage_path)
27
+ >>>
28
+ >>> # Process access request
29
+ >>> request = manager.create_request(
30
+ ... request_type=RequestType.ACCESS,
31
+ ... data_subject_id="user@example.com",
32
+ ... contact_email="user@example.com",
33
+ ... reason="Subject access request"
34
+ ... )
35
+ >>>
36
+ >>> # Verify identity and fulfill request
37
+ >>> data = manager.fulfill_access_request(request)
38
+ >>> data_path = manager.export_to_portable_format(data)
39
+ >>> manager.send_to_subject(request, data_path)
40
+ >>>
41
+ >>> # Process deletion request
42
+ >>> del_request = manager.create_request(
43
+ ... request_type=RequestType.ERASURE,
44
+ ... data_subject_id="user@example.com"
45
+ ... )
46
+ >>> manager.fulfill_erasure_request(del_request)
47
+ """
48
+
49
+ import json
50
+ import logging
51
+ from dataclasses import dataclass
52
+ from datetime import datetime, timedelta
53
+ from enum import Enum
54
+ from pathlib import Path
55
+ from typing import Any
56
+ from uuid import UUID, uuid4
57
+
58
+ import psycopg
59
+
60
+ logger = logging.getLogger(__name__)
61
+
62
+
63
+ class RequestType(Enum):
64
+ """Types of data subject rights requests."""
65
+
66
+ ACCESS = "access"
67
+ """Right to access all data held about the subject."""
68
+
69
+ ERASURE = "erasure"
70
+ """Right to be forgotten (delete all data)."""
71
+
72
+ RECTIFICATION = "rectification"
73
+ """Right to correct inaccurate data."""
74
+
75
+ PORTABILITY = "portability"
76
+ """Right to receive data in portable format."""
77
+
78
+ RESTRICT = "restrict"
79
+ """Right to restrict processing."""
80
+
81
+ OBJECT = "object"
82
+ """Right to object to processing."""
83
+
84
+
85
+ class RequestStatus(Enum):
86
+ """Status of a data subject rights request."""
87
+
88
+ RECEIVED = "received"
89
+ """Request received but not yet processed."""
90
+
91
+ VERIFYING = "verifying"
92
+ """Identity verification in progress."""
93
+
94
+ VERIFIED = "verified"
95
+ """Identity verified, ready to process."""
96
+
97
+ PROCESSING = "processing"
98
+ """Request being fulfilled."""
99
+
100
+ FULFILLED = "fulfilled"
101
+ """Request completed successfully."""
102
+
103
+ REJECTED = "rejected"
104
+ """Request rejected (illegal, unverifiable, etc.)."""
105
+
106
+ PARTIAL = "partial"
107
+ """Partially fulfilled (some data exempt from right)."""
108
+
109
+
110
+ @dataclass
111
+ class DataSubjectRequest:
112
+ """Request to exercise data subject rights."""
113
+
114
+ id: UUID
115
+ """Unique request ID."""
116
+
117
+ request_type: RequestType
118
+ """Type of request (access, erasure, etc.)."""
119
+
120
+ data_subject_id: str
121
+ """Identifier for subject (email, ID, hash)."""
122
+
123
+ contact_email: str
124
+ """Email for sending response."""
125
+
126
+ status: RequestStatus = RequestStatus.RECEIVED
127
+ """Current request status."""
128
+
129
+ created_at: datetime | None = None
130
+ """When request was created."""
131
+
132
+ verified_at: datetime | None = None
133
+ """When identity was verified."""
134
+
135
+ deadline: datetime | None = None
136
+ """Regulatory deadline (usually 30 days)."""
137
+
138
+ fulfilled_at: datetime | None = None
139
+ """When request was fulfilled."""
140
+
141
+ reason: str | None = None
142
+ """Reason for request (optional)."""
143
+
144
+ verification_method: str | None = None
145
+ """How identity was verified (email, phone, document)."""
146
+
147
+ rejection_reason: str | None = None
148
+ """If rejected, why."""
149
+
150
+ data_location: Path | None = None
151
+ """Path to exported data (for access/portability)."""
152
+
153
+ record_count: int = 0
154
+ """Number of records affected."""
155
+
156
+ processing_notes: str | None = None
157
+ """Notes about processing."""
158
+
159
+ def __post_init__(self):
160
+ """Initialize default values."""
161
+ if self.created_at is None:
162
+ self.created_at = datetime.now()
163
+ if self.deadline is None:
164
+ self.deadline = self.created_at + timedelta(days=30)
165
+
166
+
167
+ class DataSubjectRightsManager:
168
+ """Manage and fulfill data subject rights requests.
169
+
170
+ Automates fulfillment of data subject rights as required by modern
171
+ data protection regulations (GDPR, CCPA, LGPD, etc.).
172
+
173
+ Features:
174
+ - Request tracking and status management
175
+ - Identity verification
176
+ - Data collection and export
177
+ - Erasure with audit trail
178
+ - Deadline tracking
179
+ - Legal compliance reporting
180
+
181
+ Workflow:
182
+ 1. Subject submits request (access, erasure, etc.)
183
+ 2. System receives and logs request
184
+ 3. Identity verification (email confirmation, etc.)
185
+ 4. Process request according to type:
186
+ - ACCESS: Collect all data, export portable format
187
+ - ERASURE: Delete data, verify deletion, document
188
+ - RECTIFICATION: Correct inaccurate data, document
189
+ - PORTABILITY: Export in standard format (JSON, CSV)
190
+ - RESTRICT: Flag for limited processing
191
+ - OBJECT: Document objection, stop processing
192
+ 5. Send response to subject
193
+ 6. Log completion for audit trail
194
+
195
+ Regulations:
196
+ - GDPR (EU): 30-day deadline, some exemptions
197
+ - CCPA (USA): 45-day deadline
198
+ - LGPD (Brazil): 15-day deadline
199
+ - PIPL (China): Specific requirements
200
+ """
201
+
202
+ def __init__(
203
+ self,
204
+ conn: psycopg.Connection,
205
+ storage_path: Path | None = None,
206
+ ):
207
+ """Initialize data subject rights manager.
208
+
209
+ Args:
210
+ conn: Database connection
211
+ storage_path: Path for storing exported data
212
+ """
213
+ self.conn = conn
214
+ self.storage_path = storage_path or Path("/tmp/data_subject_exports")
215
+ self.storage_path.mkdir(parents=True, exist_ok=True)
216
+ self._ensure_request_table()
217
+
218
+ def _ensure_request_table(self) -> None:
219
+ """Create request tracking table if not exists."""
220
+ with self.conn.cursor() as cursor:
221
+ cursor.execute(
222
+ """
223
+ CREATE TABLE IF NOT EXISTS confiture_data_subject_requests (
224
+ id UUID PRIMARY KEY,
225
+ request_type TEXT NOT NULL,
226
+ data_subject_id TEXT NOT NULL,
227
+ contact_email TEXT NOT NULL,
228
+ status TEXT NOT NULL,
229
+ created_at TIMESTAMPTZ NOT NULL,
230
+ verified_at TIMESTAMPTZ,
231
+ deadline TIMESTAMPTZ NOT NULL,
232
+ fulfilled_at TIMESTAMPTZ,
233
+ reason TEXT,
234
+ verification_method TEXT,
235
+ rejection_reason TEXT,
236
+ record_count INTEGER DEFAULT 0,
237
+ processing_notes TEXT,
238
+ created_at_idx TIMESTAMPTZ DEFAULT NOW()
239
+ );
240
+
241
+ CREATE INDEX IF NOT EXISTS idx_dsr_status
242
+ ON confiture_data_subject_requests(status);
243
+ CREATE INDEX IF NOT EXISTS idx_dsr_deadline
244
+ ON confiture_data_subject_requests(deadline);
245
+ CREATE INDEX IF NOT EXISTS idx_dsr_subject
246
+ ON confiture_data_subject_requests(data_subject_id);
247
+ """
248
+ )
249
+ self.conn.commit()
250
+
251
+ def create_request(
252
+ self,
253
+ request_type: RequestType,
254
+ data_subject_id: str,
255
+ contact_email: str,
256
+ reason: str | None = None,
257
+ ) -> DataSubjectRequest:
258
+ """Create a new data subject rights request.
259
+
260
+ Args:
261
+ request_type: Type of request
262
+ data_subject_id: Subject identifier (email, ID, etc.)
263
+ contact_email: Email for response
264
+ reason: Optional reason for request
265
+
266
+ Returns:
267
+ DataSubjectRequest instance
268
+ """
269
+ request = DataSubjectRequest(
270
+ id=uuid4(),
271
+ request_type=request_type,
272
+ data_subject_id=data_subject_id,
273
+ contact_email=contact_email,
274
+ reason=reason,
275
+ )
276
+
277
+ # Store in database
278
+ with self.conn.cursor() as cursor:
279
+ cursor.execute(
280
+ """
281
+ INSERT INTO confiture_data_subject_requests (
282
+ id, request_type, data_subject_id, contact_email,
283
+ status, created_at, deadline, reason
284
+ ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
285
+ """,
286
+ (
287
+ str(request.id),
288
+ request.request_type.value,
289
+ data_subject_id,
290
+ contact_email,
291
+ request.status.value,
292
+ request.created_at,
293
+ request.deadline,
294
+ reason,
295
+ ),
296
+ )
297
+ self.conn.commit()
298
+
299
+ logger.info(
300
+ f"Created {request.request_type.value} request {request.id} "
301
+ f"for subject {data_subject_id}"
302
+ )
303
+
304
+ return request
305
+
306
+ def verify_identity(
307
+ self,
308
+ request: DataSubjectRequest,
309
+ verification_method: str = "email",
310
+ ) -> bool:
311
+ """Verify subject identity.
312
+
313
+ Args:
314
+ request: Request to verify
315
+ verification_method: How identity was verified
316
+
317
+ Returns:
318
+ True if identity verified
319
+ """
320
+ # In a real implementation, would:
321
+ # 1. Send verification email with token
322
+ # 2. Require subject to click link
323
+ # 3. Update status when verified
324
+
325
+ # For now, just mark as verified
326
+ request.verified_at = datetime.now()
327
+ request.status = RequestStatus.VERIFIED
328
+ request.verification_method = verification_method
329
+
330
+ # Update database
331
+ with self.conn.cursor() as cursor:
332
+ cursor.execute(
333
+ """
334
+ UPDATE confiture_data_subject_requests
335
+ SET status = %s, verified_at = %s, verification_method = %s
336
+ WHERE id = %s
337
+ """,
338
+ (
339
+ request.status.value,
340
+ request.verified_at,
341
+ verification_method,
342
+ str(request.id),
343
+ ),
344
+ )
345
+ self.conn.commit()
346
+
347
+ logger.info(f"Verified identity for request {request.id} via {verification_method}")
348
+
349
+ return True
350
+
351
+ def fulfill_access_request(
352
+ self,
353
+ request: DataSubjectRequest,
354
+ ) -> dict[str, Any]:
355
+ """Fulfill right to access request.
356
+
357
+ Args:
358
+ request: Access request to fulfill
359
+
360
+ Returns:
361
+ Dictionary of subject's data
362
+ """
363
+ if request.status != RequestStatus.VERIFIED:
364
+ raise ValueError("Request must be verified before fulfillment")
365
+
366
+ request.status = RequestStatus.PROCESSING
367
+
368
+ # In a real implementation, would:
369
+ # 1. Query all tables for subject's data
370
+ # 2. Collect metadata from lineage
371
+ # 3. Collect consent records
372
+ # 4. Export in portable format
373
+
374
+ data = {
375
+ "subject_id": request.data_subject_id,
376
+ "request_id": str(request.id),
377
+ "created_at": request.created_at.isoformat(),
378
+ "requested_data": {},
379
+ }
380
+
381
+ # Example: collect from users table
382
+ with self.conn.cursor() as cursor:
383
+ # This is a placeholder query
384
+ cursor.execute(
385
+ """
386
+ SELECT column_name, data_type
387
+ FROM information_schema.columns
388
+ WHERE table_name = 'users'
389
+ LIMIT 10
390
+ """
391
+ )
392
+ # In real implementation, would fetch actual subject data
393
+
394
+ # Export to portable format
395
+ export_path = self._export_to_file(request, data)
396
+ request.data_location = export_path
397
+
398
+ request.status = RequestStatus.FULFILLED
399
+ request.fulfilled_at = datetime.now()
400
+
401
+ # Update database
402
+ with self.conn.cursor() as cursor:
403
+ cursor.execute(
404
+ """
405
+ UPDATE confiture_data_subject_requests
406
+ SET status = %s, fulfilled_at = %s
407
+ WHERE id = %s
408
+ """,
409
+ (request.status.value, request.fulfilled_at, str(request.id)),
410
+ )
411
+ self.conn.commit()
412
+
413
+ logger.info(f"Fulfilled access request {request.id}")
414
+
415
+ return data
416
+
417
+ def fulfill_erasure_request(
418
+ self,
419
+ request: DataSubjectRequest,
420
+ ) -> int:
421
+ """Fulfill right to erasure (right to be forgotten).
422
+
423
+ Args:
424
+ request: Erasure request to fulfill
425
+
426
+ Returns:
427
+ Number of records deleted
428
+
429
+ Note:
430
+ Deletes all records for subject except where legally required
431
+ to keep (e.g., for tax or regulatory purposes).
432
+ """
433
+ if request.status != RequestStatus.VERIFIED:
434
+ raise ValueError("Request must be verified before fulfillment")
435
+
436
+ request.status = RequestStatus.PROCESSING
437
+
438
+ # In a real implementation, would:
439
+ # 1. Identify all tables with subject data
440
+ # 2. Delete records (soft or hard delete)
441
+ # 3. Document deletion in lineage
442
+ # 4. Update anonymization status
443
+
444
+ deleted_count = 0
445
+
446
+ # Example deletion (placeholder)
447
+ # In real implementation, would have complex deletion logic
448
+
449
+ request.record_count = deleted_count
450
+ request.status = RequestStatus.FULFILLED
451
+ request.fulfilled_at = datetime.now()
452
+ request.processing_notes = f"Deleted {deleted_count} records"
453
+
454
+ # Update database
455
+ with self.conn.cursor() as cursor:
456
+ cursor.execute(
457
+ """
458
+ UPDATE confiture_data_subject_requests
459
+ SET status = %s, fulfilled_at = %s, record_count = %s,
460
+ processing_notes = %s
461
+ WHERE id = %s
462
+ """,
463
+ (
464
+ request.status.value,
465
+ request.fulfilled_at,
466
+ deleted_count,
467
+ request.processing_notes,
468
+ str(request.id),
469
+ ),
470
+ )
471
+ self.conn.commit()
472
+
473
+ logger.warning(
474
+ f"Fulfilled erasure request {request.id}: "
475
+ f"deleted {deleted_count} records for subject {request.data_subject_id}"
476
+ )
477
+
478
+ return deleted_count
479
+
480
+ def fulfill_portability_request(
481
+ self,
482
+ request: DataSubjectRequest,
483
+ format: str = "json",
484
+ ) -> Path:
485
+ """Fulfill right to data portability request.
486
+
487
+ Args:
488
+ request: Portability request
489
+ format: Export format (json, csv)
490
+
491
+ Returns:
492
+ Path to exported file
493
+ """
494
+ if request.status != RequestStatus.VERIFIED:
495
+ raise ValueError("Request must be verified before fulfillment")
496
+
497
+ # Get subject's data
498
+ data = self.fulfill_access_request(request)
499
+
500
+ # Export in requested format
501
+ if format == "json":
502
+ return self._export_json(request, data)
503
+ elif format == "csv":
504
+ return self._export_csv(request, data)
505
+ else:
506
+ raise ValueError(f"Unsupported format: {format}")
507
+
508
+ def _export_to_file(self, request: DataSubjectRequest, data: dict[str, Any]) -> Path:
509
+ """Export data to file.
510
+
511
+ Args:
512
+ request: Request for context
513
+ data: Data to export
514
+
515
+ Returns:
516
+ Path to exported file
517
+ """
518
+ filename = f"dsr_{request.id}_{datetime.now().timestamp()}.json"
519
+ filepath = self.storage_path / filename
520
+
521
+ with open(filepath, "w") as f:
522
+ json.dump(data, f, indent=2, default=str)
523
+
524
+ logger.info(f"Exported data to {filepath}")
525
+ return filepath
526
+
527
+ def _export_json(self, request: DataSubjectRequest, data: dict[str, Any]) -> Path:
528
+ """Export data as JSON."""
529
+ return self._export_to_file(request, data)
530
+
531
+ def _export_csv(self, request: DataSubjectRequest, data: dict[str, Any]) -> Path:
532
+ """Export data as CSV."""
533
+ # In a real implementation, would convert to CSV format
534
+ return self._export_to_file(request, data)
535
+
536
+ def get_request(self, request_id: UUID) -> DataSubjectRequest | None:
537
+ """Retrieve a request by ID.
538
+
539
+ Args:
540
+ request_id: Request ID
541
+
542
+ Returns:
543
+ DataSubjectRequest or None
544
+ """
545
+ with self.conn.cursor() as cursor:
546
+ cursor.execute(
547
+ """
548
+ SELECT id, request_type, data_subject_id, contact_email,
549
+ status, created_at, verified_at, deadline, fulfilled_at,
550
+ reason, verification_method, rejection_reason, record_count,
551
+ processing_notes
552
+ FROM confiture_data_subject_requests
553
+ WHERE id = %s
554
+ """,
555
+ (str(request_id),),
556
+ )
557
+ row = cursor.fetchone()
558
+
559
+ if not row:
560
+ return None
561
+
562
+ return DataSubjectRequest(
563
+ id=row[0],
564
+ request_type=RequestType(row[1]),
565
+ data_subject_id=row[2],
566
+ contact_email=row[3],
567
+ status=RequestStatus(row[4]),
568
+ created_at=row[5],
569
+ verified_at=row[6],
570
+ deadline=row[7],
571
+ fulfilled_at=row[8],
572
+ reason=row[9],
573
+ verification_method=row[10],
574
+ rejection_reason=row[11],
575
+ record_count=row[12],
576
+ processing_notes=row[13],
577
+ )
578
+
579
+ def get_pending_requests(self) -> list[DataSubjectRequest]:
580
+ """Get all pending requests.
581
+
582
+ Returns:
583
+ List of pending requests
584
+ """
585
+ with self.conn.cursor() as cursor:
586
+ cursor.execute(
587
+ """
588
+ SELECT id, request_type, data_subject_id, contact_email,
589
+ status, created_at, verified_at, deadline, fulfilled_at,
590
+ reason, verification_method, rejection_reason, record_count,
591
+ processing_notes
592
+ FROM confiture_data_subject_requests
593
+ WHERE status IN (%s, %s, %s)
594
+ ORDER BY deadline ASC
595
+ """,
596
+ (
597
+ RequestStatus.RECEIVED.value,
598
+ RequestStatus.VERIFYING.value,
599
+ RequestStatus.PROCESSING.value,
600
+ ),
601
+ )
602
+ rows = cursor.fetchall()
603
+
604
+ requests = []
605
+ for row in rows:
606
+ requests.append(
607
+ DataSubjectRequest(
608
+ id=row[0],
609
+ request_type=RequestType(row[1]),
610
+ data_subject_id=row[2],
611
+ contact_email=row[3],
612
+ status=RequestStatus(row[4]),
613
+ created_at=row[5],
614
+ verified_at=row[6],
615
+ deadline=row[7],
616
+ fulfilled_at=row[8],
617
+ reason=row[9],
618
+ verification_method=row[10],
619
+ rejection_reason=row[11],
620
+ record_count=row[12],
621
+ processing_notes=row[13],
622
+ )
623
+ )
624
+
625
+ return requests
626
+
627
+ def get_overdue_requests(self) -> list[DataSubjectRequest]:
628
+ """Get requests that exceeded deadline.
629
+
630
+ Returns:
631
+ List of overdue requests
632
+ """
633
+ with self.conn.cursor() as cursor:
634
+ cursor.execute(
635
+ """
636
+ SELECT id, request_type, data_subject_id, contact_email,
637
+ status, created_at, verified_at, deadline, fulfilled_at,
638
+ reason, verification_method, rejection_reason, record_count,
639
+ processing_notes
640
+ FROM confiture_data_subject_requests
641
+ WHERE deadline < NOW() AND status != %s
642
+ ORDER BY deadline ASC
643
+ """,
644
+ (RequestStatus.FULFILLED.value,),
645
+ )
646
+ rows = cursor.fetchall()
647
+
648
+ requests = []
649
+ for row in rows:
650
+ requests.append(
651
+ DataSubjectRequest(
652
+ id=row[0],
653
+ request_type=RequestType(row[1]),
654
+ data_subject_id=row[2],
655
+ contact_email=row[3],
656
+ status=RequestStatus(row[4]),
657
+ created_at=row[5],
658
+ verified_at=row[6],
659
+ deadline=row[7],
660
+ fulfilled_at=row[8],
661
+ reason=row[9],
662
+ verification_method=row[10],
663
+ rejection_reason=row[11],
664
+ record_count=row[12],
665
+ processing_notes=row[13],
666
+ )
667
+ )
668
+
669
+ return requests