omnibase_infra 0.2.9__py3-none-any.whl → 0.3.1__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.
@@ -0,0 +1,573 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """PostgreSQL Repository Runtime.
4
+
5
+ This module provides a generic runtime for executing repository contracts
6
+ against PostgreSQL databases. The runtime enforces safety constraints,
7
+ deterministic query ordering, and configurable operation limits.
8
+
9
+ Key Features:
10
+ - Contract-driven: All operations defined in ModelDbRepositoryContract
11
+ - Positional parameters: Uses $1, $2, ... (no named param rewriting)
12
+ - Determinism enforcement: ORDER BY injection for multi-row queries
13
+ - Limit enforcement: LIMIT injection with configurable maximum
14
+ - Operation validation: Allowlist-based operation control
15
+ - Timeout enforcement: asyncio.wait_for() for query cancellation
16
+
17
+ Usage Example:
18
+ >>> import asyncpg
19
+ >>> from omnibase_infra.runtime.db import (
20
+ ... ModelDbRepositoryContract,
21
+ ... ModelDbOperation,
22
+ ... ModelDbReturn,
23
+ ... ModelRepositoryRuntimeConfig,
24
+ ... )
25
+ >>> from omnibase_infra.runtime.db.postgres_repository_runtime import (
26
+ ... PostgresRepositoryRuntime,
27
+ ... )
28
+ >>>
29
+ >>> # Create contract
30
+ >>> contract = ModelDbRepositoryContract(
31
+ ... name="users",
32
+ ... database_ref="primary",
33
+ ... ops={
34
+ ... "find_by_id": ModelDbOperation(
35
+ ... mode="select",
36
+ ... sql="SELECT * FROM users WHERE id = $1",
37
+ ... params=["user_id"],
38
+ ... returns=ModelDbReturn(many=False),
39
+ ... ),
40
+ ... },
41
+ ... )
42
+ >>>
43
+ >>> # Create runtime (with pool)
44
+ >>> pool = await asyncpg.create_pool(...)
45
+ >>> runtime = PostgresRepositoryRuntime(pool, contract)
46
+ >>>
47
+ >>> # Execute operation
48
+ >>> user = await runtime.call("find_by_id", 123)
49
+ """
50
+
51
+ from __future__ import annotations
52
+
53
+ import asyncio
54
+ import logging
55
+ import re
56
+ import time
57
+ from typing import TYPE_CHECKING
58
+
59
+ from omnibase_infra.enums import EnumInfraTransportType
60
+ from omnibase_infra.errors.repository import (
61
+ RepositoryContractError,
62
+ RepositoryExecutionError,
63
+ RepositoryTimeoutError,
64
+ RepositoryValidationError,
65
+ )
66
+ from omnibase_infra.models.errors import ModelInfraErrorContext
67
+ from omnibase_infra.runtime.db.models import (
68
+ ModelDbOperation,
69
+ ModelDbRepositoryContract,
70
+ ModelRepositoryRuntimeConfig,
71
+ )
72
+
73
+ if TYPE_CHECKING:
74
+ import asyncpg
75
+
76
+ logger = logging.getLogger(__name__)
77
+
78
+ # =============================================================================
79
+ # SQL Clause Detection Patterns
80
+ # =============================================================================
81
+ #
82
+ # These regex patterns provide simple ORDER BY and LIMIT clause detection
83
+ # for determinism enforcement. They use word boundaries (\b) and case-insensitive
84
+ # matching to identify SQL keywords.
85
+ #
86
+ # KNOWN LIMITATIONS:
87
+ # -----------------
88
+ # These patterns use simple regex matching, NOT a full SQL parser. As such,
89
+ # they can produce false positives in certain edge cases:
90
+ #
91
+ # 1. String Literals: Patterns inside quoted strings will match:
92
+ # - SELECT description FROM items WHERE note = 'sort ORDER BY priority'
93
+ # - SELECT * FROM logs WHERE message LIKE '%LIMIT 10 reached%'
94
+ #
95
+ # 2. Subqueries: Patterns in nested queries will match the outer detection:
96
+ # - SELECT * FROM (SELECT id FROM users ORDER BY created_at LIMIT 5) sub
97
+ # - The outer query appears to have ORDER BY/LIMIT, but doesn't
98
+ #
99
+ # 3. Comments: Patterns inside SQL comments will match:
100
+ # - SELECT * FROM users -- ORDER BY id for debugging
101
+ # - SELECT * FROM users /* LIMIT 100 was here */
102
+ #
103
+ # WHY THIS IS ACCEPTABLE:
104
+ # ----------------------
105
+ # 1. Contract SQL should be simple, predictable queries. Complex queries with
106
+ # subqueries, dynamic string construction, or embedded SQL in literals
107
+ # indicate contract design that should be reconsidered.
108
+ #
109
+ # 2. This detection is defense-in-depth, not primary validation. The contract
110
+ # author has explicit control over the SQL and can always add explicit
111
+ # ORDER BY and LIMIT clauses to avoid injection entirely.
112
+ #
113
+ # 3. False positives (detecting ORDER BY/LIMIT when not present at outer level)
114
+ # are safer than false negatives. A false positive skips injection, leaving
115
+ # the query unchanged. A false negative would inject duplicate clauses.
116
+ #
117
+ # RECOMMENDATION FOR COMPLEX QUERIES:
118
+ # ----------------------------------
119
+ # If your contract requires complex SQL with subqueries or string operations
120
+ # that contain SQL keywords, explicitly include ORDER BY and LIMIT in the
121
+ # outer query. This bypasses regex detection entirely:
122
+ #
123
+ # GOOD: "SELECT * FROM (SELECT id FROM users ORDER BY id) sub ORDER BY id LIMIT 100"
124
+ # AVOID: Relying on injection for queries with embedded SQL-like strings
125
+ #
126
+ # =============================================================================
127
+ _ORDER_BY_PATTERN = re.compile(r"\bORDER\s+BY\b", re.IGNORECASE)
128
+ # Pattern to detect numeric LIMIT for validation (e.g., LIMIT 100)
129
+ _LIMIT_NUMERIC_PATTERN = re.compile(r"\bLIMIT\s+(\d+)\b", re.IGNORECASE)
130
+ # Pattern to detect parameterized LIMIT (e.g., LIMIT $1) - cannot validate at build time
131
+ _LIMIT_PARAM_PATTERN = re.compile(r"\bLIMIT\s+\$\d+\b", re.IGNORECASE)
132
+
133
+
134
+ class PostgresRepositoryRuntime:
135
+ """Runtime for executing repository contracts against PostgreSQL.
136
+
137
+ Executes operations defined in a ModelDbRepositoryContract with
138
+ safety constraints, determinism guarantees, and configurable limits.
139
+
140
+ Thread Safety:
141
+ This class is NOT thread-safe for concurrent modifications.
142
+ The pool itself handles connection-level concurrency.
143
+ Multiple coroutines may call() concurrently on the same runtime.
144
+
145
+ Attributes:
146
+ pool: asyncpg connection pool for database access.
147
+ contract: Repository contract defining available operations.
148
+ config: Runtime configuration for safety and behavior.
149
+
150
+ Example:
151
+ >>> pool = await asyncpg.create_pool(dsn="postgresql://...")
152
+ >>> runtime = PostgresRepositoryRuntime(pool, contract)
153
+ >>> results = await runtime.call("find_all")
154
+ """
155
+
156
+ __slots__ = ("_config", "_contract", "_pool")
157
+
158
+ def __init__(
159
+ self,
160
+ pool: asyncpg.Pool,
161
+ contract: ModelDbRepositoryContract,
162
+ config: ModelRepositoryRuntimeConfig | None = None,
163
+ ) -> None:
164
+ """Initialize the repository runtime.
165
+
166
+ Args:
167
+ pool: asyncpg connection pool for database access.
168
+ contract: Repository contract defining available operations.
169
+ config: Optional runtime configuration. If None, uses defaults.
170
+
171
+ Example:
172
+ >>> runtime = PostgresRepositoryRuntime(
173
+ ... pool=pool,
174
+ ... contract=contract,
175
+ ... config=ModelRepositoryRuntimeConfig(max_row_limit=100),
176
+ ... )
177
+ """
178
+ self._pool = pool
179
+ self._contract = contract
180
+ self._config = config or ModelRepositoryRuntimeConfig()
181
+
182
+ @property
183
+ def contract(self) -> ModelDbRepositoryContract:
184
+ """Get the repository contract."""
185
+ return self._contract
186
+
187
+ @property
188
+ def config(self) -> ModelRepositoryRuntimeConfig:
189
+ """Get the runtime configuration."""
190
+ return self._config
191
+
192
+ async def call(
193
+ self, op_name: str, *args: object
194
+ ) -> list[dict[str, object]] | dict[str, object] | None:
195
+ """Execute a named operation from the contract.
196
+
197
+ Validates the operation exists, checks allowed operations,
198
+ validates argument count, applies determinism and limit
199
+ constraints, and executes with timeout enforcement.
200
+
201
+ Args:
202
+ op_name: Operation name as defined in contract.ops.
203
+ *args: Positional arguments matching contract params order.
204
+
205
+ Returns:
206
+ For many=True: list of dicts (possibly empty)
207
+ For many=False: single dict or None if no row found
208
+
209
+ Raises:
210
+ RepositoryContractError: Operation not found, forbidden mode,
211
+ or determinism constraint violation (no PK for multi-row).
212
+ RepositoryValidationError: Argument count mismatch.
213
+ RepositoryExecutionError: Database execution error.
214
+ RepositoryTimeoutError: Query exceeded timeout.
215
+
216
+ Example:
217
+ >>> # Single row lookup
218
+ >>> user = await runtime.call("find_by_id", 123)
219
+ >>> # Multi-row query
220
+ >>> users = await runtime.call("find_by_status", "active")
221
+ """
222
+ start_time = time.monotonic()
223
+ context = self._create_error_context(op_name)
224
+
225
+ # Lookup operation in contract
226
+ operation = self._get_operation(op_name, context)
227
+
228
+ # Validate operation is allowed
229
+ self._validate_operation_allowed(operation, op_name, context)
230
+
231
+ # Validate argument count
232
+ self._validate_arg_count(operation, args, op_name, context)
233
+
234
+ # Build final SQL with determinism and limit constraints
235
+ sql = self._build_sql(operation, op_name, context)
236
+
237
+ # Execute with timeout
238
+ try:
239
+ result = await self._execute_with_timeout(
240
+ sql, args, operation, op_name, context
241
+ )
242
+ except TimeoutError as e:
243
+ timeout_seconds = self._config.timeout_ms / 1000.0
244
+ raise RepositoryTimeoutError(
245
+ f"Query '{op_name}' exceeded timeout of {timeout_seconds}s",
246
+ op_name=op_name,
247
+ table=self._get_primary_table(),
248
+ timeout_seconds=timeout_seconds,
249
+ sql_fingerprint=self._fingerprint_sql(sql),
250
+ context=context,
251
+ ) from e
252
+
253
+ # Log metrics if enabled
254
+ if self._config.emit_metrics:
255
+ elapsed_ms = (time.monotonic() - start_time) * 1000
256
+ row_count = (
257
+ len(result) if isinstance(result, list) else (1 if result else 0)
258
+ )
259
+ logger.info(
260
+ "Repository operation completed",
261
+ extra={
262
+ "op_name": op_name,
263
+ "duration_ms": round(elapsed_ms, 2),
264
+ "rows_returned": row_count,
265
+ "repository": self._contract.name,
266
+ },
267
+ )
268
+
269
+ return result
270
+
271
+ def _create_error_context(self, op_name: str) -> ModelInfraErrorContext:
272
+ """Create error context for infrastructure errors."""
273
+ return ModelInfraErrorContext.with_correlation(
274
+ transport_type=EnumInfraTransportType.DATABASE,
275
+ operation=f"repository.{op_name}",
276
+ target_name=self._contract.name,
277
+ )
278
+
279
+ def _get_operation(
280
+ self, op_name: str, context: ModelInfraErrorContext
281
+ ) -> ModelDbOperation:
282
+ """Get operation from contract, raising error if not found."""
283
+ operation = self._contract.ops.get(op_name)
284
+ if operation is None:
285
+ available_ops = list(self._contract.ops.keys())
286
+ raise RepositoryContractError(
287
+ f"Unknown operation '{op_name}' not defined in contract '{self._contract.name}'. "
288
+ f"Available operations: {available_ops}",
289
+ op_name=op_name,
290
+ table=self._get_primary_table(),
291
+ context=context,
292
+ )
293
+ return operation
294
+
295
+ def _validate_operation_allowed(
296
+ self,
297
+ operation: ModelDbOperation,
298
+ op_name: str,
299
+ context: ModelInfraErrorContext,
300
+ ) -> None:
301
+ """Validate operation mode is allowed by config.
302
+
303
+ The contract uses 'read' or 'write' modes (validated by omnibase_core
304
+ validators at contract load time to ensure SQL verb matching).
305
+ """
306
+ mode = operation.mode
307
+
308
+ # Check write operations against feature flag
309
+ if mode == "write" and not self._config.allow_write_operations:
310
+ raise RepositoryContractError(
311
+ f"Operation '{op_name}' uses 'write' mode which is disabled. "
312
+ "Set allow_write_operations=True in config to enable.",
313
+ op_name=op_name,
314
+ table=self._get_primary_table(),
315
+ context=context,
316
+ )
317
+
318
+ # Check mode against allowlist
319
+ if mode not in self._config.allowed_modes:
320
+ raise RepositoryContractError(
321
+ f"Operation mode '{mode}' for '{op_name}' is not in allowed_modes. "
322
+ f"Allowed: {set(self._config.allowed_modes)}",
323
+ op_name=op_name,
324
+ table=self._get_primary_table(),
325
+ context=context,
326
+ )
327
+
328
+ def _validate_arg_count(
329
+ self,
330
+ operation: ModelDbOperation,
331
+ args: tuple[object, ...],
332
+ op_name: str,
333
+ context: ModelInfraErrorContext,
334
+ ) -> None:
335
+ """Validate argument count matches contract params.
336
+
337
+ Contract params is a dict[str, ModelDbParam] where keys are param names.
338
+ """
339
+ param_names = list(operation.params.keys())
340
+ expected = len(param_names)
341
+ actual = len(args)
342
+ if actual != expected:
343
+ raise RepositoryValidationError(
344
+ f"Operation '{op_name}' expects {expected} argument(s) ({param_names}), "
345
+ f"but received {actual}",
346
+ op_name=op_name,
347
+ table=self._get_primary_table(),
348
+ context=context,
349
+ expected_args=expected,
350
+ actual_args=actual,
351
+ param_names=param_names,
352
+ )
353
+
354
+ def _build_sql(
355
+ self,
356
+ operation: ModelDbOperation,
357
+ op_name: str,
358
+ context: ModelInfraErrorContext,
359
+ ) -> str:
360
+ """Build final SQL with determinism and limit constraints.
361
+
362
+ Applies ORDER BY injection for multi-row queries without ORDER BY.
363
+ Applies LIMIT injection or validation based on config.
364
+
365
+ Only applies constraints to 'read' mode operations (SELECT).
366
+ """
367
+ sql = operation.sql
368
+ is_read = operation.mode == "read"
369
+ is_multi_row = operation.returns.many
370
+
371
+ # Only apply constraints to read operations
372
+ if not is_read:
373
+ return sql
374
+
375
+ # Apply determinism constraints for multi-row reads
376
+ if is_multi_row:
377
+ sql = self._inject_order_by(sql, op_name, context)
378
+
379
+ # Apply limit constraints for multi-row reads
380
+ if is_multi_row:
381
+ sql = self._inject_limit(sql, op_name, context)
382
+
383
+ return sql
384
+
385
+ def _inject_order_by(
386
+ self,
387
+ sql: str,
388
+ op_name: str,
389
+ context: ModelInfraErrorContext,
390
+ ) -> str:
391
+ """Inject ORDER BY clause for deterministic multi-row results.
392
+
393
+ Rules:
394
+ - If ORDER BY exists: no injection needed
395
+ - If no ORDER BY and PK declared: inject ORDER BY {pk}
396
+ - If no ORDER BY and no PK: HARD ERROR
397
+
398
+ When injecting ORDER BY, the clause is inserted BEFORE any existing
399
+ LIMIT clause to produce valid SQL. For example:
400
+ - Input: "SELECT * FROM users LIMIT $1"
401
+ - Output: "SELECT * FROM users ORDER BY id LIMIT $1"
402
+
403
+ Args:
404
+ sql: The SQL query to potentially modify.
405
+ op_name: Operation name for error context.
406
+ context: Error context for exception raising.
407
+
408
+ Returns:
409
+ SQL with ORDER BY clause (injected or original).
410
+
411
+ Raises:
412
+ RepositoryContractError: No ORDER BY and no primary_key_column.
413
+ """
414
+ has_order_by = bool(_ORDER_BY_PATTERN.search(sql))
415
+ if has_order_by:
416
+ return sql
417
+
418
+ # No ORDER BY - check if we can inject
419
+ pk_column = self._config.primary_key_column
420
+ if pk_column is None:
421
+ raise RepositoryContractError(
422
+ f"Multi-row query '{op_name}' has no ORDER BY clause and "
423
+ "primary_key_column is not configured. Deterministic results "
424
+ "cannot be guaranteed. Either add ORDER BY to the SQL or "
425
+ "set primary_key_column in config.",
426
+ op_name=op_name,
427
+ table=self._get_primary_table(),
428
+ sql_fingerprint=self._fingerprint_sql(sql),
429
+ context=context,
430
+ )
431
+
432
+ # Inject ORDER BY using configured order or just PK
433
+ order_by = self._config.default_order_by or pk_column
434
+
435
+ # Check if LIMIT exists - ORDER BY must be inserted BEFORE LIMIT
436
+ param_match = _LIMIT_PARAM_PATTERN.search(sql)
437
+ numeric_match = _LIMIT_NUMERIC_PATTERN.search(sql)
438
+ limit_match = param_match or numeric_match
439
+
440
+ if limit_match:
441
+ # Insert ORDER BY before LIMIT
442
+ limit_start = limit_match.start()
443
+ return (
444
+ f"{sql[:limit_start].rstrip()} ORDER BY {order_by} {sql[limit_start:]}"
445
+ )
446
+ else:
447
+ # No LIMIT, append at end
448
+ return f"{sql.rstrip().rstrip(';')} ORDER BY {order_by}"
449
+
450
+ def _inject_limit(
451
+ self,
452
+ sql: str,
453
+ op_name: str,
454
+ context: ModelInfraErrorContext,
455
+ ) -> str:
456
+ """Inject or validate LIMIT clause for multi-row results.
457
+
458
+ Rules:
459
+ - If parameterized LIMIT (e.g., $1): OK (no change, can't validate at build time)
460
+ - If numeric LIMIT > max_row_limit: HARD ERROR
461
+ - If numeric LIMIT <= max_row_limit: OK (no change)
462
+ - If no LIMIT: inject LIMIT {max_row_limit}
463
+
464
+ Args:
465
+ sql: The SQL query to potentially modify.
466
+ op_name: Operation name for error context.
467
+ context: Error context for exception raising.
468
+
469
+ Returns:
470
+ SQL with LIMIT clause (injected or original).
471
+
472
+ Raises:
473
+ RepositoryContractError: Numeric LIMIT exceeds max_row_limit.
474
+ """
475
+ max_limit = self._config.max_row_limit
476
+
477
+ # Check for parameterized LIMIT (e.g., LIMIT $1) - can't validate at build time
478
+ if _LIMIT_PARAM_PATTERN.search(sql):
479
+ return sql
480
+
481
+ # Check for numeric LIMIT (e.g., LIMIT 100) - can validate
482
+ limit_match = _LIMIT_NUMERIC_PATTERN.search(sql)
483
+ if limit_match:
484
+ # Existing numeric LIMIT - validate it
485
+ existing_limit = int(limit_match.group(1))
486
+ if existing_limit > max_limit:
487
+ raise RepositoryContractError(
488
+ f"Query '{op_name}' has LIMIT {existing_limit} which exceeds "
489
+ f"max_row_limit of {max_limit}. Reduce the LIMIT or increase "
490
+ "max_row_limit in config.",
491
+ op_name=op_name,
492
+ table=self._get_primary_table(),
493
+ sql_fingerprint=self._fingerprint_sql(sql),
494
+ context=context,
495
+ existing_limit=existing_limit,
496
+ max_row_limit=max_limit,
497
+ )
498
+ return sql
499
+
500
+ # No LIMIT - inject one
501
+ return f"{sql.rstrip().rstrip(';')} LIMIT {max_limit}"
502
+
503
+ async def _execute_with_timeout(
504
+ self,
505
+ sql: str,
506
+ args: tuple[object, ...],
507
+ operation: ModelDbOperation,
508
+ op_name: str,
509
+ context: ModelInfraErrorContext,
510
+ ) -> list[dict[str, object]] | dict[str, object] | None:
511
+ """Execute query with timeout enforcement.
512
+
513
+ Uses asyncio.wait_for() to enforce timeout.
514
+ Uses fetch() for many=True, fetchrow() for many=False.
515
+
516
+ Args:
517
+ sql: Final SQL query to execute.
518
+ args: Positional arguments for the query.
519
+ operation: Operation specification.
520
+ op_name: Operation name for error context.
521
+ context: Error context for exception raising.
522
+
523
+ Returns:
524
+ Query results as appropriate type.
525
+
526
+ Raises:
527
+ asyncio.TimeoutError: Query exceeded timeout (caught by caller).
528
+ RepositoryExecutionError: Database execution error.
529
+ """
530
+ timeout_seconds = self._config.timeout_ms / 1000.0
531
+
532
+ try:
533
+ async with self._pool.acquire() as conn:
534
+ if operation.returns.many:
535
+ # Multi-row: use fetch()
536
+ coro = conn.fetch(sql, *args)
537
+ records = await asyncio.wait_for(coro, timeout=timeout_seconds)
538
+ return [dict(record) for record in records]
539
+ else:
540
+ # Single-row: use fetchrow()
541
+ coro = conn.fetchrow(sql, *args)
542
+ record = await asyncio.wait_for(coro, timeout=timeout_seconds)
543
+ return dict(record) if record is not None else None
544
+ except TimeoutError:
545
+ # Re-raise for caller to handle
546
+ raise
547
+ except Exception as e:
548
+ # Wrap all other exceptions
549
+ raise RepositoryExecutionError(
550
+ f"Failed to execute operation '{op_name}': {e}",
551
+ op_name=op_name,
552
+ table=self._get_primary_table(),
553
+ sql_fingerprint=self._fingerprint_sql(sql),
554
+ context=context,
555
+ ) from e
556
+
557
+ def _get_primary_table(self) -> str | None:
558
+ """Get the primary table from contract for error context."""
559
+ return self._contract.tables[0] if self._contract.tables else None
560
+
561
+ def _fingerprint_sql(self, sql: str) -> str:
562
+ """Create a safe fingerprint of SQL for logging/errors.
563
+
564
+ Truncates long SQL and removes potentially sensitive values.
565
+ """
566
+ # Simple approach: truncate to reasonable length
567
+ max_len = 200
568
+ if len(sql) <= max_len:
569
+ return sql
570
+ return sql[:max_len] + "..."
571
+
572
+
573
+ __all__: list[str] = ["PostgresRepositoryRuntime"]
@@ -446,7 +446,10 @@ INFRA_NODES_PATH = "src/omnibase_infra/nodes/"
446
446
  # - 115 (2026-01-29): OMN-1653 contract registry reducer (+2 unions)
447
447
  # ContractRegistryEvent: 4-type union for event routing
448
448
  # contract_yaml: dict | str for flexible YAML handling
449
- INFRA_MAX_UNIONS = 115
449
+ # - 117 (2026-02-01): OMN-1783 PostgresRepositoryRuntime (+2 unions)
450
+ # call() return type: list[dict] | dict | None
451
+ # _execute_with_timeout() return type: list[dict] | dict | None
452
+ INFRA_MAX_UNIONS = 117
450
453
 
451
454
  # Maximum allowed architecture violations in infrastructure code.
452
455
  # Set to 0 (strict enforcement) to ensure one-model-per-file principle is always followed.
@@ -132,6 +132,24 @@ pattern_exemptions:
132
132
  - CLAUDE.md (Accepted Pattern Exceptions - EventBusKafka Complexity)
133
133
  ticket: OMN-1305
134
134
  # ==========================================================================
135
+ # PostgresRepositoryRuntime Exemptions (OMN-1783)
136
+ # ==========================================================================
137
+ # Contract-driven database runtime with cohesive methods for query execution.
138
+ # All methods serve the same purpose: executing repository contract operations.
139
+ # Public: call(), contract (property), config (property)
140
+ # Private: _create_error_context, _get_operation, _validate_operation_allowed,
141
+ # _validate_arg_count, _build_sql, _inject_order_by, _inject_limit,
142
+ # _execute_with_timeout, _get_primary_table, _fingerprint_sql
143
+ - file_pattern: 'postgres_repository_runtime\.py'
144
+ class_pattern: "Class 'PostgresRepositoryRuntime'"
145
+ violation_pattern: 'has \d+ methods'
146
+ reason: >
147
+ Cohesive runtime class with single responsibility: executing repository contract operations. Methods are logically grouped: validation (3), SQL building (3), execution (1), and helpers (3). Breaking into smaller classes would reduce cohesion and complicate the simple contract-driven execution flow.
148
+
149
+ documentation:
150
+ - OMN-1783 PR description
151
+ ticket: OMN-1783
152
+ # ==========================================================================
135
153
  # Protocol Plugin Architecture Exemptions
136
154
  # ==========================================================================
137
155
  # The 'execute' method name is a standard plugin architecture pattern.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: omnibase_infra
3
- Version: 0.2.9
3
+ Version: 0.3.1
4
4
  Summary: ONEX Infrastructure - Service integration and database infrastructure tools
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -28,7 +28,7 @@ Requires-Dist: jinja2 (>=3.1.6,<4.0.0)
28
28
  Requires-Dist: jsonschema (>=4.20.0,<5.0.0)
29
29
  Requires-Dist: mcp (>=1.25.0,<2.0.0)
30
30
  Requires-Dist: neo4j (>=5.15.0,<6.0.0)
31
- Requires-Dist: omnibase-core (>=0.11.0,<0.12.0)
31
+ Requires-Dist: omnibase-core (>=0.12.0,<0.13.0)
32
32
  Requires-Dist: omnibase-spi (>=0.6.4,<0.7.0)
33
33
  Requires-Dist: opentelemetry-api (>=1.27.0,<2.0.0)
34
34
  Requires-Dist: opentelemetry-exporter-otlp (>=1.27.0,<2.0.0)