codegraph-engine 3.0.0__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.
Files changed (130) hide show
  1. codegraph/__init__.py +37 -0
  2. codegraph/agent.py +26 -0
  3. codegraph/agent_brain.py +2212 -0
  4. codegraph/agent_capabilities.py +4058 -0
  5. codegraph/agent_rules.py +531 -0
  6. codegraph/api_drift.py +455 -0
  7. codegraph/architecture.py +371 -0
  8. codegraph/async_queue/__init__.py +50 -0
  9. codegraph/async_queue/detectors/__init__.py +48 -0
  10. codegraph/async_queue/detectors/celery.py +160 -0
  11. codegraph/async_queue/detectors/kafka.py +168 -0
  12. codegraph/async_queue/detectors/rabbitmq.py +116 -0
  13. codegraph/async_queue/detectors/redis.py +130 -0
  14. codegraph/async_queue/detectors/sqs.py +135 -0
  15. codegraph/async_queue/models.py +161 -0
  16. codegraph/async_queue/otel_models.py +74 -0
  17. codegraph/async_queue/otel_parser.py +337 -0
  18. codegraph/async_queue/reconciler.py +427 -0
  19. codegraph/async_queue/scanner.py +291 -0
  20. codegraph/async_queue/traversal.py +447 -0
  21. codegraph/audit.py +106 -0
  22. codegraph/binding_resolver.py +616 -0
  23. codegraph/branch_comparison.py +197 -0
  24. codegraph/cache.py +95 -0
  25. codegraph/change_impact.py +547 -0
  26. codegraph/cli.py +2436 -0
  27. codegraph/cli_output.py +205 -0
  28. codegraph/config.py +45 -0
  29. codegraph/constraints.py +238 -0
  30. codegraph/context.py +2182 -0
  31. codegraph/context_freshness.py +351 -0
  32. codegraph/database/__init__.py +72 -0
  33. codegraph/database/extractor.py +3651 -0
  34. codegraph/database/interrogation.py +1494 -0
  35. codegraph/database/models.py +310 -0
  36. codegraph/database/schema_drift.py +862 -0
  37. codegraph/epistemic.py +200 -0
  38. codegraph/errors.py +282 -0
  39. codegraph/evidence/__init__.py +15 -0
  40. codegraph/evidence/citations.py +418 -0
  41. codegraph/evidence_contract.py +486 -0
  42. codegraph/frameworks.py +960 -0
  43. codegraph/frameworks_nestjs.py +502 -0
  44. codegraph/frameworks_nextjs.py +337 -0
  45. codegraph/frameworks_react.py +457 -0
  46. codegraph/freshness.py +335 -0
  47. codegraph/frontend_html.py +109 -0
  48. codegraph/git.py +362 -0
  49. codegraph/git_state.py +545 -0
  50. codegraph/graph/__init__.py +46 -0
  51. codegraph/graph/models.py +56 -0
  52. codegraph/graph/traversal.py +1993 -0
  53. codegraph/indexing/__init__.py +24 -0
  54. codegraph/indexing/classifier.py +564 -0
  55. codegraph/indexing/indexer.py +2578 -0
  56. codegraph/indexing/models.py +427 -0
  57. codegraph/indexing/parser.py +3404 -0
  58. codegraph/indexing/pool.py +136 -0
  59. codegraph/indexing/scanner.py +211 -0
  60. codegraph/indexing/telemetry.py +326 -0
  61. codegraph/indexing/templates.py +418 -0
  62. codegraph/indexing/test_framework.py +116 -0
  63. codegraph/installer.py +1702 -0
  64. codegraph/interrogation.py +2355 -0
  65. codegraph/llm/__init__.py +3 -0
  66. codegraph/llm/base.py +15 -0
  67. codegraph/llm/context.py +20 -0
  68. codegraph/mcp/__init__.py +3 -0
  69. codegraph/mcp/server.py +1476 -0
  70. codegraph/mcp_diagnostics.py +882 -0
  71. codegraph/memory/__init__.py +3 -0
  72. codegraph/memory/store.py +46 -0
  73. codegraph/models.py +310 -0
  74. codegraph/monorepo.py +925 -0
  75. codegraph/observability.py +262 -0
  76. codegraph/optimizer.py +1014 -0
  77. codegraph/planner.py +417 -0
  78. codegraph/process_lifecycle.py +1153 -0
  79. codegraph/py.typed +1 -0
  80. codegraph/query_expansion.py +199 -0
  81. codegraph/ranking.py +408 -0
  82. codegraph/refactor/__init__.py +36 -0
  83. codegraph/refactor/models.py +114 -0
  84. codegraph/refactor/renamer.py +844 -0
  85. codegraph/refactor/transactions.py +414 -0
  86. codegraph/resolver.py +3968 -0
  87. codegraph/resources/__init__.py +46 -0
  88. codegraph/resources/cache.py +117 -0
  89. codegraph/resources/coalescer.py +83 -0
  90. codegraph/resources/debouncer.py +98 -0
  91. codegraph/resources/governor.py +219 -0
  92. codegraph/resources/monitor.py +102 -0
  93. codegraph/resources/policy.py +123 -0
  94. codegraph/retrieval_policy.py +320 -0
  95. codegraph/roblox.py +2174 -0
  96. codegraph/route_composer.py +363 -0
  97. codegraph/route_topology.py +172 -0
  98. codegraph/runtime/__init__.py +51 -0
  99. codegraph/runtime/collector.py +155 -0
  100. codegraph/runtime/db_watcher.py +191 -0
  101. codegraph/runtime/ingestor.py +692 -0
  102. codegraph/runtime/models.py +130 -0
  103. codegraph/runtime/port_inspector.py +304 -0
  104. codegraph/runtime/reconciliation.py +492 -0
  105. codegraph/runtime/runner.py +353 -0
  106. codegraph/runtime/trace_linker.py +175 -0
  107. codegraph/search/__init__.py +25 -0
  108. codegraph/search/hybrid.py +846 -0
  109. codegraph/search/semantic.py +28 -0
  110. codegraph/security/__init__.py +51 -0
  111. codegraph/security/paths.py +189 -0
  112. codegraph/security/redaction.py +633 -0
  113. codegraph/semantic_conflicts.py +270 -0
  114. codegraph/semantic_decorators.py +372 -0
  115. codegraph/structural_diff.py +542 -0
  116. codegraph/styles.py +144 -0
  117. codegraph/symbol_history.py +277 -0
  118. codegraph/target_resolver.py +348 -0
  119. codegraph/task.py +637 -0
  120. codegraph/tool_selection_eval.py +1158 -0
  121. codegraph/tsconfig.py +236 -0
  122. codegraph/ui/__init__.py +6 -0
  123. codegraph/ui/server.py +755 -0
  124. codegraph/watcher.py +323 -0
  125. codegraph_engine-3.0.0.dist-info/METADATA +859 -0
  126. codegraph_engine-3.0.0.dist-info/RECORD +130 -0
  127. codegraph_engine-3.0.0.dist-info/WHEEL +6 -0
  128. codegraph_engine-3.0.0.dist-info/entry_points.txt +2 -0
  129. codegraph_engine-3.0.0.dist-info/licenses/LICENSE +21 -0
  130. codegraph_engine-3.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,168 @@
1
+ """Apache Kafka producer and consumer detector.
2
+
3
+ Detects:
4
+ - Publishers / Producers: producer.send(topic, ...), producer.produce(topic, ...)
5
+ - Consumers / Workers: @consumer(topic), @subscriber(topic), KafkaConsumer(topic), consumer.subscribe([topic])
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import ast
10
+ from typing import Any
11
+
12
+ from codegraph.async_queue.models import (
13
+ AsyncConsumerRecord,
14
+ AsyncMessagingSystem,
15
+ AsyncProducerRecord,
16
+ )
17
+
18
+ _KAFKA_CONSUMER_DECORATORS = {
19
+ "consumer",
20
+ "kafka_consumer",
21
+ "subscriber",
22
+ "consume",
23
+ "agent",
24
+ }
25
+
26
+
27
+ def detect_kafka(
28
+ tree: ast.AST,
29
+ content: str,
30
+ file_path: str,
31
+ module_name: str,
32
+ ) -> tuple[list[AsyncProducerRecord], list[AsyncConsumerRecord]]:
33
+ producers: list[AsyncProducerRecord] = []
34
+ consumers: list[AsyncConsumerRecord] = []
35
+ lines = content.splitlines()
36
+
37
+ def get_line_snippet(lineno: int) -> str:
38
+ if 1 <= lineno <= len(lines):
39
+ return lines[lineno - 1].strip()
40
+ return ""
41
+
42
+ # 1. Detect consumers via decorators or subscriber calls
43
+ for node in ast.walk(tree):
44
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
45
+ for dec in node.decorator_list:
46
+ dec_func = dec.func if isinstance(dec, ast.Call) else dec
47
+ dec_name = getattr(dec_func, "id", "") or getattr(dec_func, "attr", "")
48
+
49
+ if dec_name.lower() in _KAFKA_CONSUMER_DECORATORS:
50
+ topic = ""
51
+ if isinstance(dec, ast.Call):
52
+ if dec.args and isinstance(dec.args[0], ast.Constant) and isinstance(dec.args[0].value, str):
53
+ topic = dec.args[0].value
54
+ for kw in dec.keywords:
55
+ if kw.arg in ("topic", "topics") and isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
56
+ topic = kw.value.value
57
+
58
+ if topic:
59
+ consumers.append(
60
+ AsyncConsumerRecord(
61
+ system=AsyncMessagingSystem.KAFKA,
62
+ destination=topic,
63
+ queue_id=f"kafka:{topic}",
64
+ handler_canonical_id=f"{module_name}.{node.name}",
65
+ file_path=file_path,
66
+ line=node.lineno,
67
+ handler_snippet=get_line_snippet(node.lineno),
68
+ confidence="HIGH",
69
+ metadata={"decorator": dec_name},
70
+ )
71
+ )
72
+
73
+ # 2. Detect producers & programmatic consumers inside function scopes
74
+ class KafkaCallVisitor(ast.NodeVisitor):
75
+ def __init__(self) -> None:
76
+ self.current_function: str = f"{module_name}.<module>"
77
+
78
+ def visit_FunctionDef(self, fn: ast.FunctionDef) -> None:
79
+ prev = self.current_function
80
+ self.current_function = f"{module_name}.{fn.name}"
81
+ self.generic_visit(fn)
82
+ self.current_function = prev
83
+
84
+ def visit_AsyncFunctionDef(self, fn: ast.AsyncFunctionDef) -> None:
85
+ prev = self.current_function
86
+ self.current_function = f"{module_name}.{fn.name}"
87
+ self.generic_visit(fn)
88
+ self.current_function = prev
89
+
90
+ def visit_Call(self, node: ast.Call) -> None:
91
+ # Check for producer.send(topic, ...) or producer.produce(topic, ...)
92
+ if isinstance(node.func, ast.Attribute) and node.func.attr in ("send", "produce"):
93
+ topic = ""
94
+ meta: dict[str, Any] = {"method": node.func.attr}
95
+ if node.args and isinstance(node.args[0], ast.Constant) and isinstance(node.args[0].value, str):
96
+ topic = node.args[0].value
97
+ for kw in node.keywords:
98
+ if kw.arg == "topic" and isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
99
+ topic = kw.value.value
100
+ elif kw.arg in ("key", "partition", "headers"):
101
+ meta[kw.arg] = True
102
+
103
+ if topic:
104
+ producers.append(
105
+ AsyncProducerRecord(
106
+ system=AsyncMessagingSystem.KAFKA,
107
+ destination=topic,
108
+ queue_id=f"kafka:{topic}",
109
+ caller_canonical_id=self.current_function,
110
+ file_path=file_path,
111
+ line=node.lineno,
112
+ call_snippet=get_line_snippet(node.lineno),
113
+ confidence="HIGH",
114
+ metadata=meta,
115
+ )
116
+ )
117
+
118
+ # Check for consumer.subscribe([topic, ...])
119
+ elif isinstance(node.func, ast.Attribute) and node.func.attr == "subscribe":
120
+ topic_list: list[str] = []
121
+ if node.args and isinstance(node.args[0], (ast.List, ast.Tuple)):
122
+ for elt in node.args[0].elts:
123
+ if isinstance(elt, ast.Constant) and isinstance(elt.value, str):
124
+ topic_list.append(elt.value)
125
+ for kw in node.keywords:
126
+ if kw.arg == "topics" and isinstance(kw.value, (ast.List, ast.Tuple)):
127
+ for elt in kw.value.elts:
128
+ if isinstance(elt, ast.Constant) and isinstance(elt.value, str):
129
+ topic_list.append(elt.value)
130
+
131
+ for t in topic_list:
132
+ consumers.append(
133
+ AsyncConsumerRecord(
134
+ system=AsyncMessagingSystem.KAFKA,
135
+ destination=t,
136
+ queue_id=f"kafka:{t}",
137
+ handler_canonical_id=self.current_function,
138
+ file_path=file_path,
139
+ line=node.lineno,
140
+ handler_snippet=get_line_snippet(node.lineno),
141
+ confidence="HIGH",
142
+ metadata={"method": "subscribe"},
143
+ )
144
+ )
145
+
146
+ # Check for KafkaConsumer(topic, ...)
147
+ elif isinstance(node.func, ast.Name) and node.func.id == "KafkaConsumer":
148
+ if node.args and isinstance(node.args[0], ast.Constant) and isinstance(node.args[0].value, str):
149
+ t = node.args[0].value
150
+ consumers.append(
151
+ AsyncConsumerRecord(
152
+ system=AsyncMessagingSystem.KAFKA,
153
+ destination=t,
154
+ queue_id=f"kafka:{t}",
155
+ handler_canonical_id=self.current_function,
156
+ file_path=file_path,
157
+ line=node.lineno,
158
+ handler_snippet=get_line_snippet(node.lineno),
159
+ confidence="HIGH",
160
+ metadata={"method": "KafkaConsumer"},
161
+ )
162
+ )
163
+
164
+ self.generic_visit(node)
165
+
166
+ KafkaCallVisitor().visit(tree)
167
+
168
+ return producers, consumers
@@ -0,0 +1,116 @@
1
+ """RabbitMQ / AMQP / Pika message producer and consumer detector.
2
+
3
+ Detects:
4
+ - Publishers / Producers: channel.basic_publish(exchange=..., routing_key=...)
5
+ - Consumers / Workers: channel.basic_consume(queue=..., on_message_callback=...)
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import ast
10
+ from typing import Any
11
+
12
+ from codegraph.async_queue.models import (
13
+ AsyncConsumerRecord,
14
+ AsyncMessagingSystem,
15
+ AsyncProducerRecord,
16
+ )
17
+
18
+
19
+ def detect_rabbitmq(
20
+ tree: ast.AST,
21
+ content: str,
22
+ file_path: str,
23
+ module_name: str,
24
+ ) -> tuple[list[AsyncProducerRecord], list[AsyncConsumerRecord]]:
25
+ producers: list[AsyncProducerRecord] = []
26
+ consumers: list[AsyncConsumerRecord] = []
27
+ lines = content.splitlines()
28
+
29
+ def get_line_snippet(lineno: int) -> str:
30
+ if 1 <= lineno <= len(lines):
31
+ return lines[lineno - 1].strip()
32
+ return ""
33
+
34
+ class RabbitMQVisitor(ast.NodeVisitor):
35
+ def __init__(self) -> None:
36
+ self.current_function: str = f"{module_name}.<module>"
37
+
38
+ def visit_FunctionDef(self, fn: ast.FunctionDef) -> None:
39
+ prev = self.current_function
40
+ self.current_function = f"{module_name}.{fn.name}"
41
+ self.generic_visit(fn)
42
+ self.current_function = prev
43
+
44
+ def visit_AsyncFunctionDef(self, fn: ast.AsyncFunctionDef) -> None:
45
+ prev = self.current_function
46
+ self.current_function = f"{module_name}.{fn.name}"
47
+ self.generic_visit(fn)
48
+ self.current_function = prev
49
+
50
+ def visit_Call(self, node: ast.Call) -> None:
51
+ if isinstance(node.func, ast.Attribute):
52
+ attr = node.func.attr
53
+ # Producer: basic_publish
54
+ if attr in ("basic_publish", "publish"):
55
+ routing_key = ""
56
+ exchange = ""
57
+ meta: dict[str, Any] = {"method": attr}
58
+ for kw in node.keywords:
59
+ if kw.arg == "routing_key" and isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
60
+ routing_key = kw.value.value
61
+ elif kw.arg == "exchange" and isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
62
+ exchange = kw.value.value
63
+
64
+ dest = routing_key or exchange
65
+ if dest:
66
+ if exchange:
67
+ meta["exchange"] = exchange
68
+ if routing_key:
69
+ meta["routing_key"] = routing_key
70
+
71
+ producers.append(
72
+ AsyncProducerRecord(
73
+ system=AsyncMessagingSystem.RABBITMQ,
74
+ destination=dest,
75
+ queue_id=f"rabbitmq:{dest}",
76
+ caller_canonical_id=self.current_function,
77
+ file_path=file_path,
78
+ line=node.lineno,
79
+ call_snippet=get_line_snippet(node.lineno),
80
+ confidence="HIGH",
81
+ metadata=meta,
82
+ )
83
+ )
84
+
85
+ # Consumer: basic_consume
86
+ elif attr in ("basic_consume", "consume"):
87
+ queue_name = ""
88
+ callback_fn_name = ""
89
+ for kw in node.keywords:
90
+ if kw.arg == "queue" and isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
91
+ queue_name = kw.value.value
92
+ elif kw.arg == "on_message_callback":
93
+ if isinstance(kw.value, ast.Name):
94
+ callback_fn_name = kw.value.id
95
+
96
+ if queue_name:
97
+ handler_id = f"{module_name}.{callback_fn_name}" if callback_fn_name else self.current_function
98
+ consumers.append(
99
+ AsyncConsumerRecord(
100
+ system=AsyncMessagingSystem.RABBITMQ,
101
+ destination=queue_name,
102
+ queue_id=f"rabbitmq:{queue_name}",
103
+ handler_canonical_id=handler_id,
104
+ file_path=file_path,
105
+ line=node.lineno,
106
+ handler_snippet=get_line_snippet(node.lineno),
107
+ confidence="HIGH",
108
+ metadata={"callback": callback_fn_name} if callback_fn_name else {},
109
+ )
110
+ )
111
+
112
+ self.generic_visit(node)
113
+
114
+ RabbitMQVisitor().visit(tree)
115
+
116
+ return producers, consumers
@@ -0,0 +1,130 @@
1
+ """Redis Pub/Sub and Streams async message detector.
2
+
3
+ Detects:
4
+ - Publishers / Producers: r.publish(channel, message), r.xadd(stream, fields)
5
+ - Consumers / Workers: pubsub.subscribe(channel), r.xread({stream: id})
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import ast
10
+ from typing import Any
11
+
12
+ from codegraph.async_queue.models import (
13
+ AsyncConsumerRecord,
14
+ AsyncMessagingSystem,
15
+ AsyncProducerRecord,
16
+ )
17
+
18
+
19
+ def detect_redis(
20
+ tree: ast.AST,
21
+ content: str,
22
+ file_path: str,
23
+ module_name: str,
24
+ ) -> tuple[list[AsyncProducerRecord], list[AsyncConsumerRecord]]:
25
+ producers: list[AsyncProducerRecord] = []
26
+ consumers: list[AsyncConsumerRecord] = []
27
+ lines = content.splitlines()
28
+
29
+ def get_line_snippet(lineno: int) -> str:
30
+ if 1 <= lineno <= len(lines):
31
+ return lines[lineno - 1].strip()
32
+ return ""
33
+
34
+ class RedisVisitor(ast.NodeVisitor):
35
+ def __init__(self) -> None:
36
+ self.current_function: str = f"{module_name}.<module>"
37
+
38
+ def visit_FunctionDef(self, fn: ast.FunctionDef) -> None:
39
+ prev = self.current_function
40
+ self.current_function = f"{module_name}.{fn.name}"
41
+ self.generic_visit(fn)
42
+ self.current_function = prev
43
+
44
+ def visit_AsyncFunctionDef(self, fn: ast.AsyncFunctionDef) -> None:
45
+ prev = self.current_function
46
+ self.current_function = f"{module_name}.{fn.name}"
47
+ self.generic_visit(fn)
48
+ self.current_function = prev
49
+
50
+ def visit_Call(self, node: ast.Call) -> None:
51
+ if isinstance(node.func, ast.Attribute):
52
+ attr = node.func.attr
53
+ # Producer: r.publish("channel", ...), r.xadd("stream", ...)
54
+ if attr in ("publish", "xadd"):
55
+ channel_or_stream = ""
56
+ if node.args and isinstance(node.args[0], ast.Constant) and isinstance(node.args[0].value, str):
57
+ channel_or_stream = node.args[0].value
58
+ for kw in node.keywords:
59
+ if kw.arg in ("channel", "name") and isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
60
+ channel_or_stream = kw.value.value
61
+
62
+ if channel_or_stream:
63
+ meta: dict[str, Any] = {"method": attr, "type": "stream" if attr == "xadd" else "pubsub"}
64
+ producers.append(
65
+ AsyncProducerRecord(
66
+ system=AsyncMessagingSystem.REDIS,
67
+ destination=channel_or_stream,
68
+ queue_id=f"redis:{channel_or_stream}",
69
+ caller_canonical_id=self.current_function,
70
+ file_path=file_path,
71
+ line=node.lineno,
72
+ call_snippet=get_line_snippet(node.lineno),
73
+ confidence="HIGH",
74
+ metadata=meta,
75
+ )
76
+ )
77
+
78
+ # Consumer: pubsub.subscribe("channel"), r.xread({"stream": ...})
79
+ elif attr == "subscribe":
80
+ channels: list[str] = []
81
+ for arg in node.args:
82
+ if isinstance(arg, ast.Constant) and isinstance(arg.value, str):
83
+ channels.append(arg.value)
84
+ elif isinstance(arg, (ast.List, ast.Tuple)):
85
+ for elt in arg.elts:
86
+ if isinstance(elt, ast.Constant) and isinstance(elt.value, str):
87
+ channels.append(elt.value)
88
+
89
+ for ch in channels:
90
+ consumers.append(
91
+ AsyncConsumerRecord(
92
+ system=AsyncMessagingSystem.REDIS,
93
+ destination=ch,
94
+ queue_id=f"redis:{ch}",
95
+ handler_canonical_id=self.current_function,
96
+ file_path=file_path,
97
+ line=node.lineno,
98
+ handler_snippet=get_line_snippet(node.lineno),
99
+ confidence="HIGH",
100
+ metadata={"type": "pubsub"},
101
+ )
102
+ )
103
+
104
+ elif attr == "xread":
105
+ streams: list[str] = []
106
+ if node.args and isinstance(node.args[0], ast.Dict):
107
+ for k in node.args[0].keys:
108
+ if isinstance(k, ast.Constant) and isinstance(k.value, str):
109
+ streams.append(k.value)
110
+
111
+ for st in streams:
112
+ consumers.append(
113
+ AsyncConsumerRecord(
114
+ system=AsyncMessagingSystem.REDIS,
115
+ destination=st,
116
+ queue_id=f"redis:{st}",
117
+ handler_canonical_id=self.current_function,
118
+ file_path=file_path,
119
+ line=node.lineno,
120
+ handler_snippet=get_line_snippet(node.lineno),
121
+ confidence="HIGH",
122
+ metadata={"type": "stream"},
123
+ )
124
+ )
125
+
126
+ self.generic_visit(node)
127
+
128
+ RedisVisitor().visit(tree)
129
+
130
+ return producers, consumers
@@ -0,0 +1,135 @@
1
+ """AWS SQS message producer and consumer detector.
2
+
3
+ Detects:
4
+ - Publishers / Producers: sqs.send_message(QueueUrl=...), sqs.send_message_batch()
5
+ - Consumers / Workers: sqs.receive_message(QueueUrl=...), @sqs_handler()
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import ast
10
+ from typing import Any
11
+
12
+ from codegraph.async_queue.models import (
13
+ AsyncConsumerRecord,
14
+ AsyncMessagingSystem,
15
+ AsyncProducerRecord,
16
+ )
17
+
18
+
19
+ def _extract_queue_name(url_or_name: str) -> str:
20
+ """Extract queue name from SQS URL or name string."""
21
+ clean = url_or_name.strip().rstrip("/")
22
+ if "/" in clean:
23
+ return clean.split("/")[-1]
24
+ return clean
25
+
26
+
27
+ def detect_sqs(
28
+ tree: ast.AST,
29
+ content: str,
30
+ file_path: str,
31
+ module_name: str,
32
+ ) -> tuple[list[AsyncProducerRecord], list[AsyncConsumerRecord]]:
33
+ producers: list[AsyncProducerRecord] = []
34
+ consumers: list[AsyncConsumerRecord] = []
35
+ lines = content.splitlines()
36
+
37
+ def get_line_snippet(lineno: int) -> str:
38
+ if 1 <= lineno <= len(lines):
39
+ return lines[lineno - 1].strip()
40
+ return ""
41
+
42
+ class SQSVisitor(ast.NodeVisitor):
43
+ def __init__(self) -> None:
44
+ self.current_function: str = f"{module_name}.<module>"
45
+
46
+ def visit_FunctionDef(self, fn: ast.FunctionDef) -> None:
47
+ prev = self.current_function
48
+ self.current_function = f"{module_name}.{fn.name}"
49
+ # Check for decorator like @sqs_handler
50
+ for dec in fn.decorator_list:
51
+ dec_func = dec.func if isinstance(dec, ast.Call) else dec
52
+ dec_name = getattr(dec_func, "id", "") or getattr(dec_func, "attr", "")
53
+ if "sqs" in dec_name.lower():
54
+ q_name = ""
55
+ if isinstance(dec, ast.Call):
56
+ for kw in dec.keywords:
57
+ if kw.arg in ("queue", "queue_name", "queue_url") and isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
58
+ q_name = _extract_queue_name(kw.value.value)
59
+ if q_name:
60
+ consumers.append(
61
+ AsyncConsumerRecord(
62
+ system=AsyncMessagingSystem.AWS_SQS,
63
+ destination=q_name,
64
+ queue_id=f"aws_sqs:{q_name}",
65
+ handler_canonical_id=f"{module_name}.{fn.name}",
66
+ file_path=file_path,
67
+ line=fn.lineno,
68
+ handler_snippet=get_line_snippet(fn.lineno),
69
+ confidence="HIGH",
70
+ metadata={"decorator": dec_name},
71
+ )
72
+ )
73
+ self.generic_visit(fn)
74
+ self.current_function = prev
75
+
76
+ def visit_AsyncFunctionDef(self, fn: ast.AsyncFunctionDef) -> None:
77
+ prev = self.current_function
78
+ self.current_function = f"{module_name}.{fn.name}"
79
+ self.generic_visit(fn)
80
+ self.current_function = prev
81
+
82
+ def visit_Call(self, node: ast.Call) -> None:
83
+ if isinstance(node.func, ast.Attribute):
84
+ attr = node.func.attr
85
+ # Producer: send_message, send_message_batch
86
+ if attr in ("send_message", "send_message_batch"):
87
+ q_name = ""
88
+ meta: dict[str, Any] = {"method": attr}
89
+ for kw in node.keywords:
90
+ if kw.arg == "QueueUrl" and isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
91
+ q_name = _extract_queue_name(kw.value.value)
92
+ elif kw.arg in ("DelaySeconds", "MessageDeduplicationId", "MessageGroupId"):
93
+ meta[kw.arg] = True
94
+
95
+ if q_name:
96
+ producers.append(
97
+ AsyncProducerRecord(
98
+ system=AsyncMessagingSystem.AWS_SQS,
99
+ destination=q_name,
100
+ queue_id=f"aws_sqs:{q_name}",
101
+ caller_canonical_id=self.current_function,
102
+ file_path=file_path,
103
+ line=node.lineno,
104
+ call_snippet=get_line_snippet(node.lineno),
105
+ confidence="HIGH",
106
+ metadata=meta,
107
+ )
108
+ )
109
+
110
+ # Consumer: receive_message
111
+ elif attr == "receive_message":
112
+ q_name = ""
113
+ for kw in node.keywords:
114
+ if kw.arg == "QueueUrl" and isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
115
+ q_name = _extract_queue_name(kw.value.value)
116
+ if q_name:
117
+ consumers.append(
118
+ AsyncConsumerRecord(
119
+ system=AsyncMessagingSystem.AWS_SQS,
120
+ destination=q_name,
121
+ queue_id=f"aws_sqs:{q_name}",
122
+ handler_canonical_id=self.current_function,
123
+ file_path=file_path,
124
+ line=node.lineno,
125
+ handler_snippet=get_line_snippet(node.lineno),
126
+ confidence="HIGH",
127
+ metadata={"method": "receive_message"},
128
+ )
129
+ )
130
+
131
+ self.generic_visit(node)
132
+
133
+ SQSVisitor().visit(tree)
134
+
135
+ return producers, consumers