coding-client 1.21.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 (109) hide show
  1. coding_client/__init__.py +1 -0
  2. coding_client/api/__init__.py +19 -0
  3. coding_client/api/exception_handlers.py +158 -0
  4. coding_client/api/logging_setup.py +44 -0
  5. coding_client/api/metrics.py +40 -0
  6. coding_client/api/openapi_config.py +124 -0
  7. coding_client/api/request_logging.py +60 -0
  8. coding_client/api/validation.py +150 -0
  9. coding_client/auth/__init__.py +78 -0
  10. coding_client/auth/_default_tenant.py +132 -0
  11. coding_client/auth/_header_trust.py +98 -0
  12. coding_client/auth/_onbehalf.py +315 -0
  13. coding_client/auth/_trusted_networks.py +94 -0
  14. coding_client/auth/dependencies.py +61 -0
  15. coding_client/auth/middleware.py +445 -0
  16. coding_client/auth/models.py +83 -0
  17. coding_client/auth/outbound.py +137 -0
  18. coding_client/auth/rbac.py +21 -0
  19. coding_client/auth/rbac_coding.py +5 -0
  20. coding_client/auth/tenant.py +650 -0
  21. coding_client/auth/websocket.py +148 -0
  22. coding_client/clients/__init__.py +24 -0
  23. coding_client/clients/base.py +131 -0
  24. coding_client/clients/coding_ai.py +33 -0
  25. coding_client/clients/coding_analysis.py +36 -0
  26. coding_client/clients/coding_containers.py +45 -0
  27. coding_client/clients/coding_github.py +127 -0
  28. coding_client/clients/coding_realtime.py +127 -0
  29. coding_client/clients/coding_security.py +51 -0
  30. coding_client/clients/coding_testing.py +31 -0
  31. coding_client/clients/coding_workspaces.py +265 -0
  32. coding_client/config/__init__.py +12 -0
  33. coding_client/config/base_settings.py +61 -0
  34. coding_client/config/yaml_config_loader.py +172 -0
  35. coding_client/db/__init__.py +42 -0
  36. coding_client/db/db_types.py +122 -0
  37. coding_client/db/exceptions.py +59 -0
  38. coding_client/db/postgres_connection.py +572 -0
  39. coding_client/db/tenant_query_helper.py +417 -0
  40. coding_client/db_init.py +49 -0
  41. coding_client/exceptions/__init__.py +67 -0
  42. coding_client/exceptions/api_exceptions.py +395 -0
  43. coding_client/exceptions/pydantic_ai_exceptions.py +533 -0
  44. coding_client/interfaces/__init__.py +22 -0
  45. coding_client/interfaces/cache_interface.py +179 -0
  46. coding_client/interfaces/container_interface.py +49 -0
  47. coding_client/interfaces/docker_interface.py +283 -0
  48. coding_client/interfaces/graph_interface.py +28 -0
  49. coding_client/interfaces/security_interface.py +72 -0
  50. coding_client/interfaces/webhook_interface.py +67 -0
  51. coding_client/llm/__init__.py +54 -0
  52. coding_client/llm/base_agent.py +195 -0
  53. coding_client/llm/context.py +121 -0
  54. coding_client/llm/responses.py +102 -0
  55. coding_client/llm/routing.py +82 -0
  56. coding_client/llm/settings.py +102 -0
  57. coding_client/models/__init__.py +62 -0
  58. coding_client/models/agent.py +38 -0
  59. coding_client/models/ai_test_failure.py +745 -0
  60. coding_client/models/analysis.py +330 -0
  61. coding_client/models/architecture.py +685 -0
  62. coding_client/models/bug.py +128 -0
  63. coding_client/models/code_graph.py +317 -0
  64. coding_client/models/container.py +241 -0
  65. coding_client/models/core.py +878 -0
  66. coding_client/models/docker_container.py +104 -0
  67. coding_client/models/github.py +293 -0
  68. coding_client/models/logging.py +29 -0
  69. coding_client/models/package_manager.py +257 -0
  70. coding_client/models/pr_validation.py +440 -0
  71. coding_client/models/pydantic_ai.py +395 -0
  72. coding_client/models/repo_scanner.py +249 -0
  73. coding_client/models/responses.py +431 -0
  74. coding_client/models/security.py +185 -0
  75. coding_client/models/static_analysis.py +508 -0
  76. coding_client/models/system.py +48 -0
  77. coding_client/models/test_executor.py +204 -0
  78. coding_client/models/testing.py +673 -0
  79. coding_client/models/testing_config.py +200 -0
  80. coding_client/models/validation.py +127 -0
  81. coding_client/models_init.py +272 -0
  82. coding_client/telemetry.py +195 -0
  83. coding_client/testing.py +180 -0
  84. coding_client/types/__init__.py +130 -0
  85. coding_client/types/constraint_types.py +276 -0
  86. coding_client/types/core_types.py +112 -0
  87. coding_client/types/state_machine.py +443 -0
  88. coding_client/utils/__init__.py +86 -0
  89. coding_client/utils/archive_utils.py +387 -0
  90. coding_client/utils/ast_debug.py +45 -0
  91. coding_client/utils/async_timeout.py +364 -0
  92. coding_client/utils/constants.py +518 -0
  93. coding_client/utils/content_processing.py +326 -0
  94. coding_client/utils/database.py +916 -0
  95. coding_client/utils/encoding_helper.py +222 -0
  96. coding_client/utils/enum_helpers.py +166 -0
  97. coding_client/utils/file_operations.py +177 -0
  98. coding_client/utils/filesystem_ops.py +528 -0
  99. coding_client/utils/http_client.py +562 -0
  100. coding_client/utils/path_utils.py +471 -0
  101. coding_client/utils/profiling.py +421 -0
  102. coding_client/utils/response_builder.py +240 -0
  103. coding_client/utils/retry_helper.py +712 -0
  104. coding_client/utils/security_validation.py +423 -0
  105. coding_client-1.21.2.dist-info/METADATA +234 -0
  106. coding_client-1.21.2.dist-info/RECORD +109 -0
  107. coding_client-1.21.2.dist-info/WHEEL +5 -0
  108. coding_client-1.21.2.dist-info/licenses/LICENSE +201 -0
  109. coding_client-1.21.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,572 @@
1
+ """
2
+ PostgreSQL connection manager for code_graph_helper.
3
+
4
+ Designed to work with PgBouncer connection pooler.
5
+ Applications connect through PgBouncer, which handles connection pooling to PostgreSQL.
6
+
7
+ Architecture:
8
+ Application -> PgBouncer (connection pooler) -> PostgreSQL
9
+
10
+ PgBouncer handles:
11
+ - Connection pooling
12
+ - Connection reuse
13
+ - Load balancing
14
+
15
+ This module provides:
16
+ - Simple connection creation (no pooling needed)
17
+ - Transaction management
18
+ - Query builders for graph operations
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import logging
24
+ from collections.abc import AsyncIterator
25
+ from contextlib import asynccontextmanager
26
+
27
+ import psycopg
28
+ from psycopg import sql
29
+
30
+ # get_settings is provided by the service layer (e.g., from app.core.config)
31
+ # Services must call set_settings_provider() before using postgres_connection
32
+ _settings_provider = None
33
+
34
+
35
+ def set_settings_provider(provider):
36
+ """Set the settings provider function. Call this from your service startup."""
37
+ global _settings_provider
38
+ _settings_provider = provider
39
+
40
+
41
+ def get_settings():
42
+ """Get settings from the configured provider."""
43
+ if _settings_provider is None:
44
+ raise RuntimeError("Settings provider not configured. Call set_settings_provider() first.")
45
+ return _settings_provider()
46
+
47
+
48
+ logger = logging.getLogger(__name__)
49
+
50
+ # Connection parameters (cached)
51
+ _connection_params: dict | None = None
52
+
53
+
54
+ def _get_connection_params() -> dict:
55
+ """
56
+ Get PostgreSQL connection parameters.
57
+
58
+ Returns:
59
+ Dictionary of connection parameters for psycopg
60
+ """
61
+ global _connection_params
62
+
63
+ if _connection_params is None:
64
+ settings = get_settings()
65
+ db_settings = settings.database
66
+
67
+ _connection_params = db_settings.as_connection_params()
68
+
69
+ logger.info(
70
+ "PostgreSQL connection configured via PgBouncer: %s:%s/%s",
71
+ _connection_params["host"],
72
+ _connection_params["port"],
73
+ _connection_params["dbname"],
74
+ )
75
+
76
+ return _connection_params
77
+
78
+
79
+ async def init_postgres_pool():
80
+ """
81
+ Initialize PostgreSQL connection (for compatibility).
82
+
83
+ Note: When using PgBouncer, we don't create a connection pool.
84
+ PgBouncer handles pooling. This function just validates connectivity.
85
+
86
+ Returns:
87
+ None
88
+ """
89
+ logger.info("Validating PostgreSQL connection through PgBouncer...")
90
+
91
+ try:
92
+ async with get_postgres_connection() as conn:
93
+ cur = await conn.execute("SELECT version()")
94
+ row = await cur.fetchone()
95
+ version = row[0] if row else "unknown"
96
+ logger.info(f"PostgreSQL connection established (via PgBouncer): {version[:80]}...")
97
+
98
+ # Verify we can access the schema
99
+ cur = await conn.execute(
100
+ "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public'"
101
+ )
102
+ row = await cur.fetchone()
103
+ table_count = row[0] if row else 0
104
+ logger.info(f"Found {table_count} tables in database")
105
+
106
+ except Exception as e:
107
+ # SECURITY: Don't log exception details that might contain passwords
108
+ logger.error("Failed to connect to PostgreSQL through PgBouncer: %s", type(e).__name__)
109
+ raise
110
+
111
+
112
+ async def close_postgres_pool():
113
+ """
114
+ Close PostgreSQL connections (for compatibility).
115
+
116
+ Note: When using PgBouncer, individual connections are closed automatically.
117
+ This is a no-op for compatibility with pooled connection APIs.
118
+ """
119
+ logger.info("PostgreSQL connections will be closed by PgBouncer")
120
+ # No-op when using PgBouncer
121
+
122
+
123
+ @asynccontextmanager
124
+ async def get_postgres_connection() -> AsyncIterator[psycopg.AsyncConnection]:
125
+ """
126
+ Context manager for creating a PostgreSQL connection through PgBouncer.
127
+
128
+ Usage:
129
+ async with get_postgres_connection() as conn:
130
+ cur = await conn.execute("SELECT * FROM nodes LIMIT 10")
131
+ rows = cur.fetchall()
132
+
133
+ Note:
134
+ - Connects through PgBouncer, which handles connection pooling
135
+ - Each connection is lightweight and managed by PgBouncer
136
+ - Connection is automatically closed when context exits
137
+
138
+ Yields:
139
+ psycopg AsyncConnection instance
140
+ """
141
+ import asyncio
142
+
143
+ params = _get_connection_params()
144
+
145
+ # Build conninfo string from params dict
146
+ conninfo_parts = []
147
+ param_map = {
148
+ "host": "host",
149
+ "port": "port",
150
+ "user": "user",
151
+ "password": "password",
152
+ "dbname": "dbname",
153
+ }
154
+ for key, conninfo_key in param_map.items():
155
+ if key in params and params[key]:
156
+ conninfo_parts.append(f"{conninfo_key}={params[key]}")
157
+
158
+ # Add server_settings as options
159
+ server_settings = params.get("server_settings", {})
160
+ if server_settings.get("application_name"):
161
+ conninfo_parts.append(f"application_name={server_settings['application_name']}")
162
+
163
+ conninfo = " ".join(conninfo_parts)
164
+
165
+ conn = await asyncio.wait_for(
166
+ psycopg.AsyncConnection.connect(conninfo=conninfo, autocommit=True),
167
+ timeout=params.get("timeout", 30.0),
168
+ )
169
+
170
+ try:
171
+ # Set session parameters
172
+ settings = get_settings()
173
+ db_settings = settings.database
174
+
175
+ if db_settings.statement_timeout > 0:
176
+ await conn.execute(
177
+ sql.SQL("SET statement_timeout = {}").format(
178
+ sql.Literal(db_settings.statement_timeout)
179
+ )
180
+ )
181
+
182
+ yield conn
183
+
184
+ finally:
185
+ await conn.close()
186
+
187
+
188
+ @asynccontextmanager
189
+ async def postgres_transaction() -> AsyncIterator[psycopg.AsyncConnection]:
190
+ """
191
+ Context manager for PostgreSQL transactions.
192
+
193
+ Usage:
194
+ async with postgres_transaction() as conn:
195
+ await conn.execute("INSERT INTO nodes ...")
196
+ await conn.execute("INSERT INTO edges ...")
197
+ # Automatically commits on success, rolls back on exception
198
+
199
+ Yields:
200
+ psycopg AsyncConnection instance with active transaction
201
+ """
202
+ import asyncio
203
+
204
+ params = _get_connection_params()
205
+
206
+ # Build conninfo string from params dict
207
+ conninfo_parts = []
208
+ param_map = {
209
+ "host": "host",
210
+ "port": "port",
211
+ "user": "user",
212
+ "password": "password",
213
+ "dbname": "dbname",
214
+ }
215
+ for key, conninfo_key in param_map.items():
216
+ if key in params and params[key]:
217
+ conninfo_parts.append(f"{conninfo_key}={params[key]}")
218
+
219
+ server_settings = params.get("server_settings", {})
220
+ if server_settings.get("application_name"):
221
+ conninfo_parts.append(f"application_name={server_settings['application_name']}")
222
+
223
+ conninfo = " ".join(conninfo_parts)
224
+
225
+ # autocommit=False enables transaction mode
226
+ conn = await asyncio.wait_for(
227
+ psycopg.AsyncConnection.connect(conninfo=conninfo, autocommit=False),
228
+ timeout=params.get("timeout", 30.0),
229
+ )
230
+
231
+ try:
232
+ yield conn
233
+ await conn.commit()
234
+ except Exception:
235
+ await conn.rollback()
236
+ raise
237
+ finally:
238
+ await conn.close()
239
+
240
+
241
+ class PostgresQueryBuilder:
242
+ """
243
+ Helper class for building secure PostgreSQL graph queries with parameterized statements.
244
+
245
+ SECURITY: All methods use parameterized queries to prevent SQL injection.
246
+
247
+ TENANT ISOLATION: All queries that accept tenant_id will filter results to only
248
+ include data belonging to that tenant. When tenant_id is None, no filtering is applied
249
+ (for admin access or legacy compatibility).
250
+ """
251
+
252
+ @staticmethod
253
+ def find_descendants(
254
+ start_node_id: str,
255
+ max_depth: int = 10,
256
+ edge_types: list[str] | None = None,
257
+ tenant_id: str | None = None,
258
+ ) -> tuple[str, list]:
259
+ """
260
+ Build recursive CTE query to find all descendants of a node.
261
+
262
+ SECURITY: Uses parameterized queries to prevent SQL injection.
263
+
264
+ Args:
265
+ start_node_id: Starting node ID
266
+ max_depth: Maximum traversal depth
267
+ edge_types: Optional list of edge types to filter by
268
+ tenant_id: Optional tenant ID for data isolation
269
+
270
+ Returns:
271
+ Tuple of (SQL query string with placeholders, list of parameter values)
272
+
273
+ Usage:
274
+ query, params = PostgresQueryBuilder.find_descendants("node_123", tenant_id="tenant_abc")
275
+ cur = await conn.execute(query, params)
276
+ results = cur.fetchall()
277
+ """
278
+ params: list[str | int | list[str]] = [start_node_id, start_node_id, max_depth]
279
+ edge_filter = ""
280
+ tenant_filter = ""
281
+
282
+ if edge_types:
283
+ params.append(edge_types)
284
+ edge_filter = "AND e.edge_type = ANY(%s)"
285
+
286
+ if tenant_id:
287
+ params.extend([tenant_id, tenant_id])
288
+ tenant_filter = "AND n.tenant_id = %s AND e.tenant_id = %s"
289
+
290
+ query = f"""
291
+ WITH RECURSIVE descendants AS (
292
+ -- Base case
293
+ SELECT
294
+ e.target_id as node_id,
295
+ n.name,
296
+ n.node_type,
297
+ 1 as depth,
298
+ ARRAY[%s, e.target_id] as path,
299
+ e.edge_type
300
+ FROM edges e
301
+ JOIN nodes n ON e.target_id = n.node_id
302
+ WHERE e.source_id = %s
303
+ {edge_filter}
304
+ {tenant_filter}
305
+
306
+ UNION ALL
307
+
308
+ -- Recursive case
309
+ SELECT
310
+ e.target_id,
311
+ n.name,
312
+ n.node_type,
313
+ d.depth + 1,
314
+ d.path || e.target_id,
315
+ e.edge_type
316
+ FROM descendants d
317
+ JOIN edges e ON d.node_id = e.source_id
318
+ JOIN nodes n ON e.target_id = n.node_id
319
+ WHERE e.target_id != ALL(d.path)
320
+ AND d.depth < %s
321
+ {edge_filter}
322
+ {tenant_filter}
323
+ )
324
+ SELECT DISTINCT
325
+ node_id,
326
+ name,
327
+ node_type,
328
+ depth,
329
+ path,
330
+ edge_type
331
+ FROM descendants
332
+ ORDER BY depth, name
333
+ """
334
+
335
+ return query, params
336
+
337
+ @staticmethod
338
+ def find_ancestors(
339
+ start_node_id: str,
340
+ max_depth: int = 10,
341
+ edge_types: list[str] | None = None,
342
+ tenant_id: str | None = None,
343
+ ) -> tuple[str, list]:
344
+ """
345
+ Build recursive CTE query to find all ancestors of a node.
346
+
347
+ SECURITY: Uses parameterized queries to prevent SQL injection.
348
+
349
+ Args:
350
+ start_node_id: Starting node ID
351
+ max_depth: Maximum traversal depth
352
+ edge_types: Optional list of edge types to filter by
353
+ tenant_id: Optional tenant ID for data isolation
354
+
355
+ Returns:
356
+ Tuple of (SQL query string with placeholders, list of parameter values)
357
+
358
+ Usage:
359
+ query, params = PostgresQueryBuilder.find_ancestors("node_123", tenant_id="tenant_abc")
360
+ cur = await conn.execute(query, params)
361
+ results = cur.fetchall()
362
+ """
363
+ params: list[str | int | list[str]] = [start_node_id, start_node_id, max_depth]
364
+ edge_filter = ""
365
+ tenant_filter = ""
366
+
367
+ if edge_types:
368
+ params.append(edge_types)
369
+ edge_filter = "AND e.edge_type = ANY(%s)"
370
+
371
+ if tenant_id:
372
+ params.extend([tenant_id, tenant_id])
373
+ tenant_filter = "AND n.tenant_id = %s AND e.tenant_id = %s"
374
+
375
+ query = f"""
376
+ WITH RECURSIVE ancestors AS (
377
+ -- Base case
378
+ SELECT
379
+ e.source_id as node_id,
380
+ n.name,
381
+ n.node_type,
382
+ 1 as depth,
383
+ ARRAY[e.source_id, %s] as path,
384
+ e.edge_type
385
+ FROM edges e
386
+ JOIN nodes n ON e.source_id = n.node_id
387
+ WHERE e.target_id = %s
388
+ {edge_filter}
389
+ {tenant_filter}
390
+
391
+ UNION ALL
392
+
393
+ -- Recursive case
394
+ SELECT
395
+ e.source_id,
396
+ n.name,
397
+ n.node_type,
398
+ a.depth + 1,
399
+ e.source_id || a.path,
400
+ e.edge_type
401
+ FROM ancestors a
402
+ JOIN edges e ON a.node_id = e.target_id
403
+ JOIN nodes n ON e.source_id = n.node_id
404
+ WHERE e.source_id != ALL(a.path)
405
+ AND a.depth < %s
406
+ {edge_filter}
407
+ {tenant_filter}
408
+ )
409
+ SELECT DISTINCT
410
+ node_id,
411
+ name,
412
+ node_type,
413
+ depth,
414
+ path,
415
+ edge_type
416
+ FROM ancestors
417
+ ORDER BY depth, name
418
+ """
419
+
420
+ return query, params
421
+
422
+ @staticmethod
423
+ def find_call_graph(
424
+ function_name: str,
425
+ direction: str = "outgoing",
426
+ max_depth: int = 3,
427
+ tenant_id: str | None = None,
428
+ ) -> tuple[str, list]:
429
+ """
430
+ Build query to find function call graph.
431
+
432
+ SECURITY: Uses parameterized queries to prevent SQL injection.
433
+
434
+ Args:
435
+ function_name: Function name to start from
436
+ direction: 'outgoing' for callees, 'incoming' for callers
437
+ max_depth: Maximum call depth
438
+ tenant_id: Optional tenant ID for data isolation
439
+
440
+ Returns:
441
+ Tuple of (SQL query string with placeholders, list of parameter values)
442
+
443
+ Usage:
444
+ query, params = PostgresQueryBuilder.find_call_graph("my_function", tenant_id="tenant_abc")
445
+ cur = await conn.execute(query, params)
446
+ results = cur.fetchall()
447
+ """
448
+ params = [function_name, function_name, max_depth]
449
+ tenant_filter = ""
450
+
451
+ if tenant_id:
452
+ params.append(tenant_id)
453
+ tenant_filter = "AND n.tenant_id = %s"
454
+
455
+ if direction == "outgoing":
456
+ query = f"""
457
+ WITH RECURSIVE call_graph AS (
458
+ -- Base case: direct calls
459
+ SELECT
460
+ ce.callee_id as function_id,
461
+ n.name as function_name,
462
+ n.file_path,
463
+ 1 as depth,
464
+ ARRAY[(SELECT node_id FROM nodes WHERE name = %s {tenant_filter} LIMIT 1), ce.callee_id] as path
465
+ FROM call_edges ce
466
+ JOIN nodes n ON ce.callee_id = n.node_id
467
+ WHERE ce.caller_id = (SELECT node_id FROM nodes WHERE name = %s {tenant_filter} LIMIT 1)
468
+ {tenant_filter}
469
+
470
+ UNION ALL
471
+
472
+ -- Recursive case
473
+ SELECT
474
+ ce.callee_id,
475
+ n.name,
476
+ n.file_path,
477
+ cg.depth + 1,
478
+ cg.path || ce.callee_id
479
+ FROM call_graph cg
480
+ JOIN call_edges ce ON cg.function_id = ce.caller_id
481
+ JOIN nodes n ON ce.callee_id = n.node_id
482
+ WHERE ce.callee_id != ALL(cg.path)
483
+ AND cg.depth < %s
484
+ {tenant_filter}
485
+ )
486
+ SELECT DISTINCT * FROM call_graph ORDER BY depth, function_name
487
+ """
488
+ else: # incoming
489
+ query = f"""
490
+ WITH RECURSIVE call_graph AS (
491
+ -- Base case: direct callers
492
+ SELECT
493
+ ce.caller_id as function_id,
494
+ n.name as function_name,
495
+ n.file_path,
496
+ 1 as depth,
497
+ ARRAY[ce.caller_id, (SELECT node_id FROM nodes WHERE name = %s {tenant_filter} LIMIT 1)] as path
498
+ FROM call_edges ce
499
+ JOIN nodes n ON ce.caller_id = n.node_id
500
+ WHERE ce.callee_id = (SELECT node_id FROM nodes WHERE name = %s {tenant_filter} LIMIT 1)
501
+ {tenant_filter}
502
+
503
+ UNION ALL
504
+
505
+ -- Recursive case
506
+ SELECT
507
+ ce.caller_id,
508
+ n.name,
509
+ n.file_path,
510
+ cg.depth + 1,
511
+ ce.caller_id || cg.path
512
+ FROM call_graph cg
513
+ JOIN call_edges ce ON cg.function_id = ce.callee_id
514
+ JOIN nodes n ON ce.caller_id = n.node_id
515
+ WHERE ce.caller_id != ALL(cg.path)
516
+ AND cg.depth < %s
517
+ {tenant_filter}
518
+ )
519
+ SELECT DISTINCT * FROM call_graph ORDER BY depth, function_name
520
+ """
521
+
522
+ return query, params
523
+
524
+ @staticmethod
525
+ def find_circular_dependencies() -> str:
526
+ """
527
+ Build query to find circular import dependencies.
528
+
529
+ Returns:
530
+ SQL query string
531
+ """
532
+ return """
533
+ WITH RECURSIVE dependency_chain AS (
534
+ SELECT
535
+ ie.importer_id as start_node,
536
+ ie.importer_id,
537
+ ie.imported_module,
538
+ ARRAY[ie.importer_id] as path,
539
+ 1 as depth
540
+ FROM import_edges ie
541
+
542
+ UNION ALL
543
+
544
+ SELECT
545
+ dc.start_node,
546
+ ie.importer_id,
547
+ ie.imported_module,
548
+ dc.path || ie.importer_id,
549
+ dc.depth + 1
550
+ FROM dependency_chain dc
551
+ JOIN import_edges ie ON dc.imported_module = ie.importer_id
552
+ WHERE ie.importer_id != ALL(dc.path)
553
+ AND dc.depth < 50
554
+ )
555
+ SELECT
556
+ start_node,
557
+ path || ARRAY[imported_module] as cycle_path,
558
+ depth as cycle_length
559
+ FROM dependency_chain
560
+ WHERE imported_module = start_node
561
+ ORDER BY cycle_length, start_node
562
+ """
563
+
564
+
565
+ # Export public API
566
+ __all__ = [
567
+ "init_postgres_pool",
568
+ "close_postgres_pool",
569
+ "get_postgres_connection",
570
+ "postgres_transaction",
571
+ "PostgresQueryBuilder",
572
+ ]