osscodeiq 0.0.0__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.
- osscodeiq/__init__.py +0 -0
- osscodeiq/analyzer.py +467 -0
- osscodeiq/cache/__init__.py +0 -0
- osscodeiq/cache/hasher.py +23 -0
- osscodeiq/cache/store.py +300 -0
- osscodeiq/classifiers/__init__.py +0 -0
- osscodeiq/classifiers/layer_classifier.py +69 -0
- osscodeiq/cli.py +721 -0
- osscodeiq/config.py +113 -0
- osscodeiq/detectors/__init__.py +0 -0
- osscodeiq/detectors/auth/__init__.py +0 -0
- osscodeiq/detectors/auth/certificate_auth.py +139 -0
- osscodeiq/detectors/auth/ldap_auth.py +89 -0
- osscodeiq/detectors/auth/session_header_auth.py +120 -0
- osscodeiq/detectors/base.py +41 -0
- osscodeiq/detectors/config/__init__.py +0 -0
- osscodeiq/detectors/config/batch_structure.py +128 -0
- osscodeiq/detectors/config/cloudformation.py +183 -0
- osscodeiq/detectors/config/docker_compose.py +179 -0
- osscodeiq/detectors/config/github_actions.py +150 -0
- osscodeiq/detectors/config/gitlab_ci.py +216 -0
- osscodeiq/detectors/config/helm_chart.py +187 -0
- osscodeiq/detectors/config/ini_structure.py +101 -0
- osscodeiq/detectors/config/json_structure.py +72 -0
- osscodeiq/detectors/config/kubernetes.py +305 -0
- osscodeiq/detectors/config/kubernetes_rbac.py +212 -0
- osscodeiq/detectors/config/openapi.py +194 -0
- osscodeiq/detectors/config/package_json.py +99 -0
- osscodeiq/detectors/config/properties_detector.py +108 -0
- osscodeiq/detectors/config/pyproject_toml.py +169 -0
- osscodeiq/detectors/config/sql_structure.py +155 -0
- osscodeiq/detectors/config/toml_structure.py +93 -0
- osscodeiq/detectors/config/tsconfig_json.py +105 -0
- osscodeiq/detectors/config/yaml_structure.py +82 -0
- osscodeiq/detectors/cpp/__init__.py +0 -0
- osscodeiq/detectors/cpp/cpp_structures.py +192 -0
- osscodeiq/detectors/csharp/__init__.py +0 -0
- osscodeiq/detectors/csharp/csharp_efcore.py +184 -0
- osscodeiq/detectors/csharp/csharp_minimal_apis.py +156 -0
- osscodeiq/detectors/csharp/csharp_structures.py +317 -0
- osscodeiq/detectors/docs/__init__.py +0 -0
- osscodeiq/detectors/docs/markdown_structure.py +117 -0
- osscodeiq/detectors/frontend/__init__.py +0 -0
- osscodeiq/detectors/frontend/angular_components.py +177 -0
- osscodeiq/detectors/frontend/frontend_routes.py +259 -0
- osscodeiq/detectors/frontend/react_components.py +148 -0
- osscodeiq/detectors/frontend/svelte_components.py +84 -0
- osscodeiq/detectors/frontend/vue_components.py +150 -0
- osscodeiq/detectors/generic/__init__.py +1 -0
- osscodeiq/detectors/generic/imports_detector.py +413 -0
- osscodeiq/detectors/go/__init__.py +0 -0
- osscodeiq/detectors/go/go_orm.py +202 -0
- osscodeiq/detectors/go/go_structures.py +162 -0
- osscodeiq/detectors/go/go_web.py +157 -0
- osscodeiq/detectors/iac/__init__.py +0 -0
- osscodeiq/detectors/iac/bicep.py +135 -0
- osscodeiq/detectors/iac/dockerfile.py +182 -0
- osscodeiq/detectors/iac/terraform.py +188 -0
- osscodeiq/detectors/java/__init__.py +0 -0
- osscodeiq/detectors/java/azure_functions.py +424 -0
- osscodeiq/detectors/java/azure_messaging.py +350 -0
- osscodeiq/detectors/java/class_hierarchy.py +349 -0
- osscodeiq/detectors/java/config_def.py +82 -0
- osscodeiq/detectors/java/cosmos_db.py +105 -0
- osscodeiq/detectors/java/graphql_resolver.py +188 -0
- osscodeiq/detectors/java/grpc_service.py +142 -0
- osscodeiq/detectors/java/ibm_mq.py +178 -0
- osscodeiq/detectors/java/jaxrs.py +160 -0
- osscodeiq/detectors/java/jdbc.py +196 -0
- osscodeiq/detectors/java/jms.py +116 -0
- osscodeiq/detectors/java/jpa_entity.py +143 -0
- osscodeiq/detectors/java/kafka.py +113 -0
- osscodeiq/detectors/java/kafka_protocol.py +70 -0
- osscodeiq/detectors/java/micronaut.py +248 -0
- osscodeiq/detectors/java/module_deps.py +191 -0
- osscodeiq/detectors/java/public_api.py +206 -0
- osscodeiq/detectors/java/quarkus.py +176 -0
- osscodeiq/detectors/java/rabbitmq.py +150 -0
- osscodeiq/detectors/java/raw_sql.py +136 -0
- osscodeiq/detectors/java/repository.py +131 -0
- osscodeiq/detectors/java/rmi.py +129 -0
- osscodeiq/detectors/java/spring_events.py +117 -0
- osscodeiq/detectors/java/spring_rest.py +168 -0
- osscodeiq/detectors/java/spring_security.py +212 -0
- osscodeiq/detectors/java/tibco_ems.py +193 -0
- osscodeiq/detectors/java/websocket.py +188 -0
- osscodeiq/detectors/kotlin/__init__.py +0 -0
- osscodeiq/detectors/kotlin/kotlin_structures.py +124 -0
- osscodeiq/detectors/kotlin/ktor_routes.py +163 -0
- osscodeiq/detectors/proto/__init__.py +0 -0
- osscodeiq/detectors/proto/proto_structure.py +153 -0
- osscodeiq/detectors/python/__init__.py +0 -0
- osscodeiq/detectors/python/celery_tasks.py +88 -0
- osscodeiq/detectors/python/django_auth.py +132 -0
- osscodeiq/detectors/python/django_models.py +157 -0
- osscodeiq/detectors/python/django_views.py +74 -0
- osscodeiq/detectors/python/fastapi_auth.py +143 -0
- osscodeiq/detectors/python/fastapi_routes.py +68 -0
- osscodeiq/detectors/python/flask_routes.py +67 -0
- osscodeiq/detectors/python/kafka_python.py +175 -0
- osscodeiq/detectors/python/pydantic_models.py +115 -0
- osscodeiq/detectors/python/python_structures.py +234 -0
- osscodeiq/detectors/python/sqlalchemy_models.py +82 -0
- osscodeiq/detectors/registry.py +100 -0
- osscodeiq/detectors/rust/__init__.py +0 -0
- osscodeiq/detectors/rust/actix_web.py +234 -0
- osscodeiq/detectors/rust/rust_structures.py +174 -0
- osscodeiq/detectors/scala/__init__.py +0 -0
- osscodeiq/detectors/scala/scala_structures.py +128 -0
- osscodeiq/detectors/shell/__init__.py +0 -0
- osscodeiq/detectors/shell/bash_detector.py +127 -0
- osscodeiq/detectors/shell/powershell_detector.py +118 -0
- osscodeiq/detectors/typescript/__init__.py +0 -0
- osscodeiq/detectors/typescript/express_routes.py +55 -0
- osscodeiq/detectors/typescript/fastify_routes.py +156 -0
- osscodeiq/detectors/typescript/graphql_resolvers.py +100 -0
- osscodeiq/detectors/typescript/kafka_js.py +164 -0
- osscodeiq/detectors/typescript/mongoose_orm.py +151 -0
- osscodeiq/detectors/typescript/nestjs_controllers.py +99 -0
- osscodeiq/detectors/typescript/nestjs_guards.py +138 -0
- osscodeiq/detectors/typescript/passport_jwt.py +133 -0
- osscodeiq/detectors/typescript/prisma_orm.py +96 -0
- osscodeiq/detectors/typescript/remix_routes.py +160 -0
- osscodeiq/detectors/typescript/sequelize_orm.py +136 -0
- osscodeiq/detectors/typescript/typeorm_entities.py +86 -0
- osscodeiq/detectors/typescript/typescript_structures.py +185 -0
- osscodeiq/detectors/utils.py +49 -0
- osscodeiq/discovery/__init__.py +11 -0
- osscodeiq/discovery/change_detector.py +97 -0
- osscodeiq/discovery/file_discovery.py +342 -0
- osscodeiq/flow/__init__.py +0 -0
- osscodeiq/flow/engine.py +78 -0
- osscodeiq/flow/models.py +72 -0
- osscodeiq/flow/renderer.py +127 -0
- osscodeiq/flow/templates/interactive.html +252 -0
- osscodeiq/flow/vendor/cytoscape-dagre.min.js +8 -0
- osscodeiq/flow/vendor/cytoscape.min.js +32 -0
- osscodeiq/flow/vendor/dagre.min.js +3809 -0
- osscodeiq/flow/views.py +357 -0
- osscodeiq/graph/__init__.py +0 -0
- osscodeiq/graph/backend.py +52 -0
- osscodeiq/graph/backends/__init__.py +23 -0
- osscodeiq/graph/backends/kuzu.py +576 -0
- osscodeiq/graph/backends/networkx.py +135 -0
- osscodeiq/graph/backends/sqlite_backend.py +406 -0
- osscodeiq/graph/builder.py +297 -0
- osscodeiq/graph/query.py +228 -0
- osscodeiq/graph/store.py +183 -0
- osscodeiq/graph/views.py +231 -0
- osscodeiq/models/__init__.py +17 -0
- osscodeiq/models/graph.py +116 -0
- osscodeiq/output/__init__.py +0 -0
- osscodeiq/output/dot.py +171 -0
- osscodeiq/output/mermaid.py +160 -0
- osscodeiq/output/safety.py +58 -0
- osscodeiq/output/serializers.py +42 -0
- osscodeiq/parsing/__init__.py +5 -0
- osscodeiq/parsing/languages/__init__.py +0 -0
- osscodeiq/parsing/languages/base.py +23 -0
- osscodeiq/parsing/languages/java.py +68 -0
- osscodeiq/parsing/languages/python.py +57 -0
- osscodeiq/parsing/languages/typescript.py +95 -0
- osscodeiq/parsing/parser_manager.py +125 -0
- osscodeiq/parsing/structured/__init__.py +0 -0
- osscodeiq/parsing/structured/gradle_parser.py +78 -0
- osscodeiq/parsing/structured/json_parser.py +24 -0
- osscodeiq/parsing/structured/properties_parser.py +56 -0
- osscodeiq/parsing/structured/sql_parser.py +54 -0
- osscodeiq/parsing/structured/xml_parser.py +148 -0
- osscodeiq/parsing/structured/yaml_parser.py +38 -0
- osscodeiq/server/__init__.py +7 -0
- osscodeiq/server/app.py +53 -0
- osscodeiq/server/mcp_server.py +174 -0
- osscodeiq/server/middleware.py +16 -0
- osscodeiq/server/routes.py +184 -0
- osscodeiq/server/service.py +445 -0
- osscodeiq/server/templates/welcome.html +56 -0
- osscodeiq-0.0.0.dist-info/METADATA +30 -0
- osscodeiq-0.0.0.dist-info/RECORD +183 -0
- osscodeiq-0.0.0.dist-info/WHEEL +5 -0
- osscodeiq-0.0.0.dist-info/entry_points.txt +2 -0
- osscodeiq-0.0.0.dist-info/licenses/LICENSE +21 -0
- osscodeiq-0.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Django model detector."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
from osscodeiq.detectors.base import DetectorContext, DetectorResult
|
|
8
|
+
from osscodeiq.detectors.utils import decode_text
|
|
9
|
+
from osscodeiq.models.graph import EdgeKind, GraphEdge, GraphNode, NodeKind, SourceLocation
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DjangoModelDetector:
|
|
13
|
+
"""Detects Django model, manager, and relationship definitions."""
|
|
14
|
+
|
|
15
|
+
name: str = "python.django_models"
|
|
16
|
+
supported_languages: tuple[str, ...] = ("python",)
|
|
17
|
+
|
|
18
|
+
_DJANGO_MODEL_RE = re.compile(
|
|
19
|
+
r"^class\s+(\w+)\s*\(\s*[\w.]*Model\s*\)", re.MULTILINE
|
|
20
|
+
)
|
|
21
|
+
_FK_RE = re.compile(
|
|
22
|
+
r"(\w+)\s*=\s*models\.(?:ForeignKey|OneToOneField)\s*\(\s*[\"']?(\w+)",
|
|
23
|
+
re.MULTILINE,
|
|
24
|
+
)
|
|
25
|
+
_M2M_RE = re.compile(
|
|
26
|
+
r"(\w+)\s*=\s*models\.ManyToManyField\s*\(\s*[\"']?(\w+)", re.MULTILINE
|
|
27
|
+
)
|
|
28
|
+
_FIELD_RE = re.compile(r"(\w+)\s*=\s*models\.(\w+Field)\s*\(", re.MULTILINE)
|
|
29
|
+
_META_TABLE_RE = re.compile(r"db_table\s*=\s*[\"'](\w+)[\"']")
|
|
30
|
+
_META_ORDERING_RE = re.compile(r"ordering\s*=\s*(\[.*?\])")
|
|
31
|
+
_MANAGER_RE = re.compile(
|
|
32
|
+
r"^class\s+(\w+)\s*\(\s*[\w.]*Manager\s*\)", re.MULTILINE
|
|
33
|
+
)
|
|
34
|
+
_MANAGER_ASSIGNMENT_RE = re.compile(r"(\w+)\s*=\s*(\w+)\s*\(\s*\)", re.MULTILINE)
|
|
35
|
+
|
|
36
|
+
def detect(self, ctx: DetectorContext) -> DetectorResult:
|
|
37
|
+
result = DetectorResult()
|
|
38
|
+
text = decode_text(ctx)
|
|
39
|
+
|
|
40
|
+
# Detect managers first so we can link them
|
|
41
|
+
manager_names: dict[str, str] = {}
|
|
42
|
+
for match in self._MANAGER_RE.finditer(text):
|
|
43
|
+
mgr_name = match.group(1)
|
|
44
|
+
line = text[: match.start()].count("\n") + 1
|
|
45
|
+
node_id = f"django:{ctx.file_path}:manager:{mgr_name}"
|
|
46
|
+
manager_names[mgr_name] = node_id
|
|
47
|
+
result.nodes.append(
|
|
48
|
+
GraphNode(
|
|
49
|
+
id=node_id,
|
|
50
|
+
kind=NodeKind.REPOSITORY,
|
|
51
|
+
label=mgr_name,
|
|
52
|
+
fqn=f"{ctx.file_path}::{mgr_name}",
|
|
53
|
+
module=ctx.module_name,
|
|
54
|
+
location=SourceLocation(file_path=ctx.file_path, line_start=line),
|
|
55
|
+
properties={"framework": "django", "type": "manager"},
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Detect models
|
|
60
|
+
for match in self._DJANGO_MODEL_RE.finditer(text):
|
|
61
|
+
class_name = match.group(1)
|
|
62
|
+
line = text[: match.start()].count("\n") + 1
|
|
63
|
+
|
|
64
|
+
# Determine class body boundaries
|
|
65
|
+
class_start = match.start()
|
|
66
|
+
next_class = re.search(r"\nclass\s+\w+", text[match.end() :])
|
|
67
|
+
class_body = (
|
|
68
|
+
text[class_start : match.end() + next_class.start()]
|
|
69
|
+
if next_class
|
|
70
|
+
else text[class_start:]
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Extract fields
|
|
74
|
+
fields: dict[str, str] = {}
|
|
75
|
+
for fm in self._FIELD_RE.finditer(class_body):
|
|
76
|
+
fields[fm.group(1)] = fm.group(2)
|
|
77
|
+
|
|
78
|
+
# Extract Meta properties
|
|
79
|
+
table_name: str | None = None
|
|
80
|
+
ordering: str | None = None
|
|
81
|
+
meta_match = re.search(r"class\s+Meta\s*:", class_body)
|
|
82
|
+
if meta_match:
|
|
83
|
+
meta_start = meta_match.end()
|
|
84
|
+
meta_end = len(class_body)
|
|
85
|
+
for cm in re.finditer(r"\n\s{4}\S", class_body[meta_start:]):
|
|
86
|
+
meta_end = meta_start + cm.start()
|
|
87
|
+
break
|
|
88
|
+
meta_block = class_body[meta_start:meta_end]
|
|
89
|
+
table_match = self._META_TABLE_RE.search(meta_block)
|
|
90
|
+
if table_match:
|
|
91
|
+
table_name = table_match.group(1)
|
|
92
|
+
ordering_match = self._META_ORDERING_RE.search(meta_block)
|
|
93
|
+
if ordering_match:
|
|
94
|
+
ordering = ordering_match.group(1)
|
|
95
|
+
|
|
96
|
+
node_id = f"django:{ctx.file_path}:model:{class_name}"
|
|
97
|
+
props: dict = {
|
|
98
|
+
"fields": fields,
|
|
99
|
+
"framework": "django",
|
|
100
|
+
}
|
|
101
|
+
if table_name:
|
|
102
|
+
props["table_name"] = table_name
|
|
103
|
+
if ordering:
|
|
104
|
+
props["ordering"] = ordering
|
|
105
|
+
|
|
106
|
+
result.nodes.append(
|
|
107
|
+
GraphNode(
|
|
108
|
+
id=node_id,
|
|
109
|
+
kind=NodeKind.ENTITY,
|
|
110
|
+
label=class_name,
|
|
111
|
+
fqn=f"{ctx.file_path}::{class_name}",
|
|
112
|
+
module=ctx.module_name,
|
|
113
|
+
location=SourceLocation(file_path=ctx.file_path, line_start=line),
|
|
114
|
+
properties=props,
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# FK / OneToOne edges
|
|
119
|
+
for fk in self._FK_RE.finditer(class_body):
|
|
120
|
+
target = fk.group(2)
|
|
121
|
+
target_id = f"django:{ctx.file_path}:model:{target}"
|
|
122
|
+
result.edges.append(
|
|
123
|
+
GraphEdge(
|
|
124
|
+
source=node_id,
|
|
125
|
+
target=target_id,
|
|
126
|
+
kind=EdgeKind.DEPENDS_ON,
|
|
127
|
+
label=fk.group(1),
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# M2M edges
|
|
132
|
+
for m2m in self._M2M_RE.finditer(class_body):
|
|
133
|
+
target = m2m.group(2)
|
|
134
|
+
target_id = f"django:{ctx.file_path}:model:{target}"
|
|
135
|
+
result.edges.append(
|
|
136
|
+
GraphEdge(
|
|
137
|
+
source=node_id,
|
|
138
|
+
target=target_id,
|
|
139
|
+
kind=EdgeKind.DEPENDS_ON,
|
|
140
|
+
label=m2m.group(1),
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# Manager assignments (objects = MyManager())
|
|
145
|
+
for ma in self._MANAGER_ASSIGNMENT_RE.finditer(class_body):
|
|
146
|
+
mgr_class = ma.group(2)
|
|
147
|
+
if mgr_class in manager_names:
|
|
148
|
+
result.edges.append(
|
|
149
|
+
GraphEdge(
|
|
150
|
+
source=node_id,
|
|
151
|
+
target=manager_names[mgr_class],
|
|
152
|
+
kind=EdgeKind.QUERIES,
|
|
153
|
+
label=ma.group(1),
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
return result
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Django URL pattern and view detector."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
from osscodeiq.detectors.base import DetectorContext, DetectorResult
|
|
8
|
+
from osscodeiq.detectors.utils import decode_text
|
|
9
|
+
from osscodeiq.models.graph import EdgeKind, GraphEdge, GraphNode, NodeKind, SourceLocation
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DjangoViewDetector:
|
|
13
|
+
"""Detects Django URL patterns, class-based views, and function views."""
|
|
14
|
+
|
|
15
|
+
name: str = "python.django_views"
|
|
16
|
+
supported_languages: tuple[str, ...] = ("python",)
|
|
17
|
+
|
|
18
|
+
# urlpatterns entries: path('api/users/', UserView.as_view(), name='user-list')
|
|
19
|
+
_URL_PATTERN = re.compile(
|
|
20
|
+
r"(?:path|re_path|url)\(\s*['\"]([^'\"]+)['\"]"
|
|
21
|
+
r"\s*,\s*(\w[\w.]*)"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# Class-based views: class UserView(APIView): or class UserViewSet(ModelViewSet):
|
|
25
|
+
_CBV_PATTERN = re.compile(
|
|
26
|
+
r"class\s+(\w+)\(([^)]*(?:View|ViewSet|Mixin)[^)]*)\):"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def detect(self, ctx: DetectorContext) -> DetectorResult:
|
|
30
|
+
result = DetectorResult()
|
|
31
|
+
text = decode_text(ctx)
|
|
32
|
+
|
|
33
|
+
# Detect URL patterns (typically in urls.py)
|
|
34
|
+
if "urlpatterns" in text:
|
|
35
|
+
for match in self._URL_PATTERN.finditer(text):
|
|
36
|
+
path_pattern = match.group(1)
|
|
37
|
+
view_ref = match.group(2)
|
|
38
|
+
line = text[:match.start()].count("\n") + 1
|
|
39
|
+
|
|
40
|
+
node_id = f"endpoint:{ctx.module_name or ''}:ALL:{path_pattern}"
|
|
41
|
+
result.nodes.append(GraphNode(
|
|
42
|
+
id=node_id,
|
|
43
|
+
kind=NodeKind.ENDPOINT,
|
|
44
|
+
label=f"{path_pattern}",
|
|
45
|
+
fqn=view_ref,
|
|
46
|
+
module=ctx.module_name,
|
|
47
|
+
location=SourceLocation(file_path=ctx.file_path, line_start=line),
|
|
48
|
+
properties={
|
|
49
|
+
"protocol": "REST",
|
|
50
|
+
"path_pattern": path_pattern,
|
|
51
|
+
"framework": "django",
|
|
52
|
+
"view_reference": view_ref,
|
|
53
|
+
},
|
|
54
|
+
))
|
|
55
|
+
|
|
56
|
+
# Detect class-based views
|
|
57
|
+
for match in self._CBV_PATTERN.finditer(text):
|
|
58
|
+
class_name = match.group(1)
|
|
59
|
+
bases = match.group(2)
|
|
60
|
+
line = text[:match.start()].count("\n") + 1
|
|
61
|
+
|
|
62
|
+
node_id = f"class:{ctx.file_path}::{class_name}"
|
|
63
|
+
result.nodes.append(GraphNode(
|
|
64
|
+
id=node_id,
|
|
65
|
+
kind=NodeKind.CLASS,
|
|
66
|
+
label=class_name,
|
|
67
|
+
fqn=f"{ctx.file_path}::{class_name}",
|
|
68
|
+
module=ctx.module_name,
|
|
69
|
+
location=SourceLocation(file_path=ctx.file_path, line_start=line),
|
|
70
|
+
annotations=[f"extends:{bases.strip()}"],
|
|
71
|
+
properties={"framework": "django", "stereotype": "view"},
|
|
72
|
+
))
|
|
73
|
+
|
|
74
|
+
return result
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""FastAPI authentication and authorization detector for Python source files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
from osscodeiq.detectors.base import DetectorContext, DetectorResult
|
|
8
|
+
from osscodeiq.detectors.utils import decode_text
|
|
9
|
+
from osscodeiq.models.graph import GraphNode, NodeKind, SourceLocation
|
|
10
|
+
|
|
11
|
+
# Depends(get_current_user) or Depends(get_current_active_user) etc.
|
|
12
|
+
_DEPENDS_AUTH_RE = re.compile(
|
|
13
|
+
r'Depends\(\s*(get_current[\w]*|require_auth[\w]*|auth[\w]*)\s*\)'
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
# Security(oauth2_scheme) or Security(some_auth_scheme, scopes=[...])
|
|
17
|
+
_SECURITY_RE = re.compile(
|
|
18
|
+
r'Security\(\s*(\w+)'
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# HTTPBearer() instantiation
|
|
22
|
+
_HTTP_BEARER_RE = re.compile(
|
|
23
|
+
r'HTTPBearer\s*\('
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# OAuth2PasswordBearer(tokenUrl=...) instantiation
|
|
27
|
+
_OAUTH2_PASSWORD_BEARER_RE = re.compile(
|
|
28
|
+
r'OAuth2PasswordBearer\s*\(\s*tokenUrl\s*=\s*["\']([^"\']*)["\']'
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# HTTPBasic() instantiation
|
|
32
|
+
_HTTP_BASIC_RE = re.compile(
|
|
33
|
+
r'HTTPBasic\s*\('
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _line_number(text: str, pos: int) -> int:
|
|
38
|
+
"""Return 1-based line number for a character offset."""
|
|
39
|
+
return text[:pos].count("\n") + 1
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class FastAPIAuthDetector:
|
|
43
|
+
"""Detects FastAPI auth patterns in Python source files."""
|
|
44
|
+
|
|
45
|
+
name: str = "fastapi_auth"
|
|
46
|
+
supported_languages: tuple[str, ...] = ("python",)
|
|
47
|
+
|
|
48
|
+
def detect(self, ctx: DetectorContext) -> DetectorResult:
|
|
49
|
+
result = DetectorResult()
|
|
50
|
+
text = decode_text(ctx)
|
|
51
|
+
|
|
52
|
+
# Depends(get_current_user) and similar auth dependencies
|
|
53
|
+
for m in _DEPENDS_AUTH_RE.finditer(text):
|
|
54
|
+
line = _line_number(text, m.start())
|
|
55
|
+
dep_name = m.group(1)
|
|
56
|
+
result.nodes.append(GraphNode(
|
|
57
|
+
id=f"auth:{ctx.file_path}:Depends:{line}",
|
|
58
|
+
kind=NodeKind.GUARD,
|
|
59
|
+
label=f"Depends({dep_name})",
|
|
60
|
+
module=ctx.module_name,
|
|
61
|
+
location=SourceLocation(file_path=ctx.file_path, line_start=line),
|
|
62
|
+
annotations=[f"Depends({dep_name})"],
|
|
63
|
+
properties={
|
|
64
|
+
"auth_type": "fastapi",
|
|
65
|
+
"auth_flow": "oauth2",
|
|
66
|
+
"dependency": dep_name,
|
|
67
|
+
"auth_required": True,
|
|
68
|
+
},
|
|
69
|
+
))
|
|
70
|
+
|
|
71
|
+
# Security(scheme) calls
|
|
72
|
+
for m in _SECURITY_RE.finditer(text):
|
|
73
|
+
line = _line_number(text, m.start())
|
|
74
|
+
scheme_name = m.group(1)
|
|
75
|
+
result.nodes.append(GraphNode(
|
|
76
|
+
id=f"auth:{ctx.file_path}:Security:{line}",
|
|
77
|
+
kind=NodeKind.GUARD,
|
|
78
|
+
label=f"Security({scheme_name})",
|
|
79
|
+
module=ctx.module_name,
|
|
80
|
+
location=SourceLocation(file_path=ctx.file_path, line_start=line),
|
|
81
|
+
annotations=[f"Security({scheme_name})"],
|
|
82
|
+
properties={
|
|
83
|
+
"auth_type": "fastapi",
|
|
84
|
+
"auth_flow": "oauth2",
|
|
85
|
+
"scheme": scheme_name,
|
|
86
|
+
"auth_required": True,
|
|
87
|
+
},
|
|
88
|
+
))
|
|
89
|
+
|
|
90
|
+
# HTTPBearer() instantiations
|
|
91
|
+
for m in _HTTP_BEARER_RE.finditer(text):
|
|
92
|
+
line = _line_number(text, m.start())
|
|
93
|
+
result.nodes.append(GraphNode(
|
|
94
|
+
id=f"auth:{ctx.file_path}:HTTPBearer:{line}",
|
|
95
|
+
kind=NodeKind.GUARD,
|
|
96
|
+
label="HTTPBearer()",
|
|
97
|
+
module=ctx.module_name,
|
|
98
|
+
location=SourceLocation(file_path=ctx.file_path, line_start=line),
|
|
99
|
+
annotations=["HTTPBearer"],
|
|
100
|
+
properties={
|
|
101
|
+
"auth_type": "fastapi",
|
|
102
|
+
"auth_flow": "bearer",
|
|
103
|
+
"auth_required": True,
|
|
104
|
+
},
|
|
105
|
+
))
|
|
106
|
+
|
|
107
|
+
# OAuth2PasswordBearer(tokenUrl=...) instantiations
|
|
108
|
+
for m in _OAUTH2_PASSWORD_BEARER_RE.finditer(text):
|
|
109
|
+
line = _line_number(text, m.start())
|
|
110
|
+
token_url = m.group(1)
|
|
111
|
+
result.nodes.append(GraphNode(
|
|
112
|
+
id=f"auth:{ctx.file_path}:OAuth2PasswordBearer:{line}",
|
|
113
|
+
kind=NodeKind.GUARD,
|
|
114
|
+
label=f"OAuth2PasswordBearer({token_url})",
|
|
115
|
+
module=ctx.module_name,
|
|
116
|
+
location=SourceLocation(file_path=ctx.file_path, line_start=line),
|
|
117
|
+
annotations=["OAuth2PasswordBearer"],
|
|
118
|
+
properties={
|
|
119
|
+
"auth_type": "fastapi",
|
|
120
|
+
"auth_flow": "oauth2",
|
|
121
|
+
"token_url": token_url,
|
|
122
|
+
"auth_required": True,
|
|
123
|
+
},
|
|
124
|
+
))
|
|
125
|
+
|
|
126
|
+
# HTTPBasic() instantiations
|
|
127
|
+
for m in _HTTP_BASIC_RE.finditer(text):
|
|
128
|
+
line = _line_number(text, m.start())
|
|
129
|
+
result.nodes.append(GraphNode(
|
|
130
|
+
id=f"auth:{ctx.file_path}:HTTPBasic:{line}",
|
|
131
|
+
kind=NodeKind.GUARD,
|
|
132
|
+
label="HTTPBasic()",
|
|
133
|
+
module=ctx.module_name,
|
|
134
|
+
location=SourceLocation(file_path=ctx.file_path, line_start=line),
|
|
135
|
+
annotations=["HTTPBasic"],
|
|
136
|
+
properties={
|
|
137
|
+
"auth_type": "fastapi",
|
|
138
|
+
"auth_flow": "basic",
|
|
139
|
+
"auth_required": True,
|
|
140
|
+
},
|
|
141
|
+
))
|
|
142
|
+
|
|
143
|
+
return result
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""FastAPI route detector."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
from osscodeiq.detectors.base import DetectorContext, DetectorResult
|
|
8
|
+
from osscodeiq.detectors.utils import decode_text
|
|
9
|
+
from osscodeiq.models.graph import EdgeKind, GraphEdge, GraphNode, NodeKind, SourceLocation
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FastAPIRouteDetector:
|
|
13
|
+
"""Detects FastAPI route decorators (@app.get, @router.post, etc.)."""
|
|
14
|
+
|
|
15
|
+
name: str = "python.fastapi_routes"
|
|
16
|
+
supported_languages: tuple[str, ...] = ("python",)
|
|
17
|
+
|
|
18
|
+
_ROUTE_PATTERN = re.compile(
|
|
19
|
+
r"@(\w+)\.(get|post|put|delete|patch|options|head)\(\s*['\"]([^'\"]+)['\"]"
|
|
20
|
+
r".*?\)\s*\n(?:\s*async\s+)?def\s+(\w+)",
|
|
21
|
+
re.DOTALL,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# APIRouter prefix: router = APIRouter(prefix="/api/v1/users")
|
|
25
|
+
_ROUTER_PREFIX = re.compile(
|
|
26
|
+
r"(\w+)\s*=\s*APIRouter\(.*?prefix\s*=\s*['\"]([^'\"]+)['\"]",
|
|
27
|
+
re.DOTALL,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def detect(self, ctx: DetectorContext) -> DetectorResult:
|
|
31
|
+
result = DetectorResult()
|
|
32
|
+
text = decode_text(ctx)
|
|
33
|
+
|
|
34
|
+
# Extract router prefixes
|
|
35
|
+
prefixes: dict[str, str] = {}
|
|
36
|
+
for match in self._ROUTER_PREFIX.finditer(text):
|
|
37
|
+
prefixes[match.group(1)] = match.group(2)
|
|
38
|
+
|
|
39
|
+
for match in self._ROUTE_PATTERN.finditer(text):
|
|
40
|
+
router_name = match.group(1)
|
|
41
|
+
method = match.group(2).upper()
|
|
42
|
+
path = match.group(3)
|
|
43
|
+
func_name = match.group(4)
|
|
44
|
+
|
|
45
|
+
# Prepend router prefix if known
|
|
46
|
+
prefix = prefixes.get(router_name, "")
|
|
47
|
+
full_path = prefix + path
|
|
48
|
+
|
|
49
|
+
line = text[:match.start()].count("\n") + 1
|
|
50
|
+
|
|
51
|
+
node_id = f"endpoint:{ctx.module_name or ''}:{method}:{full_path}"
|
|
52
|
+
result.nodes.append(GraphNode(
|
|
53
|
+
id=node_id,
|
|
54
|
+
kind=NodeKind.ENDPOINT,
|
|
55
|
+
label=f"{method} {full_path}",
|
|
56
|
+
fqn=f"{ctx.file_path}::{func_name}",
|
|
57
|
+
module=ctx.module_name,
|
|
58
|
+
location=SourceLocation(file_path=ctx.file_path, line_start=line),
|
|
59
|
+
properties={
|
|
60
|
+
"protocol": "REST",
|
|
61
|
+
"http_method": method,
|
|
62
|
+
"path_pattern": full_path,
|
|
63
|
+
"framework": "fastapi",
|
|
64
|
+
"router": router_name,
|
|
65
|
+
},
|
|
66
|
+
))
|
|
67
|
+
|
|
68
|
+
return result
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Flask route detector."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
from osscodeiq.detectors.base import DetectorContext, DetectorResult
|
|
8
|
+
from osscodeiq.detectors.utils import decode_text
|
|
9
|
+
from osscodeiq.models.graph import EdgeKind, GraphEdge, GraphNode, NodeKind, SourceLocation
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FlaskRouteDetector:
|
|
13
|
+
"""Detects Flask route decorators (@app.route, @blueprint.route)."""
|
|
14
|
+
|
|
15
|
+
name: str = "python.flask_routes"
|
|
16
|
+
supported_languages: tuple[str, ...] = ("python",)
|
|
17
|
+
|
|
18
|
+
# Matches @app.route('/path', methods=['GET', 'POST']) and @blueprint.route(...)
|
|
19
|
+
_ROUTE_PATTERN = re.compile(
|
|
20
|
+
r"@(\w+)\.(route)\(\s*['\"]([^'\"]+)['\"]"
|
|
21
|
+
r"(?:.*?methods\s*=\s*\[([^\]]+)\])?"
|
|
22
|
+
r".*?\)\s*\n\s*def\s+(\w+)",
|
|
23
|
+
re.DOTALL,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
def detect(self, ctx: DetectorContext) -> DetectorResult:
|
|
27
|
+
result = DetectorResult()
|
|
28
|
+
text = decode_text(ctx)
|
|
29
|
+
|
|
30
|
+
for match in self._ROUTE_PATTERN.finditer(text):
|
|
31
|
+
blueprint = match.group(1)
|
|
32
|
+
path = match.group(3)
|
|
33
|
+
methods_raw = match.group(4)
|
|
34
|
+
func_name = match.group(5)
|
|
35
|
+
|
|
36
|
+
methods = ["GET"]
|
|
37
|
+
if methods_raw:
|
|
38
|
+
methods = [m.strip().strip("'\"") for m in methods_raw.split(",")]
|
|
39
|
+
|
|
40
|
+
line = text[:match.start()].count("\n") + 1
|
|
41
|
+
|
|
42
|
+
for method in methods:
|
|
43
|
+
node_id = f"endpoint:{ctx.module_name or ''}:{method}:{path}"
|
|
44
|
+
result.nodes.append(GraphNode(
|
|
45
|
+
id=node_id,
|
|
46
|
+
kind=NodeKind.ENDPOINT,
|
|
47
|
+
label=f"{method} {path}",
|
|
48
|
+
fqn=f"{ctx.file_path}::{func_name}",
|
|
49
|
+
module=ctx.module_name,
|
|
50
|
+
location=SourceLocation(file_path=ctx.file_path, line_start=line),
|
|
51
|
+
properties={
|
|
52
|
+
"protocol": "REST",
|
|
53
|
+
"http_method": method,
|
|
54
|
+
"path_pattern": path,
|
|
55
|
+
"framework": "flask",
|
|
56
|
+
"blueprint": blueprint,
|
|
57
|
+
},
|
|
58
|
+
))
|
|
59
|
+
|
|
60
|
+
class_id = f"class:{ctx.file_path}::{blueprint}"
|
|
61
|
+
result.edges.append(GraphEdge(
|
|
62
|
+
source=class_id,
|
|
63
|
+
target=node_id,
|
|
64
|
+
kind=EdgeKind.EXPOSES,
|
|
65
|
+
))
|
|
66
|
+
|
|
67
|
+
return result
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Kafka producer/consumer detector for Python source files.
|
|
2
|
+
|
|
3
|
+
Detects usage of confluent-kafka, aiokafka, and kafka-python libraries.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
|
|
10
|
+
from osscodeiq.detectors.base import DetectorContext, DetectorResult
|
|
11
|
+
from osscodeiq.detectors.utils import decode_text
|
|
12
|
+
from osscodeiq.models.graph import (
|
|
13
|
+
EdgeKind,
|
|
14
|
+
GraphEdge,
|
|
15
|
+
GraphNode,
|
|
16
|
+
NodeKind,
|
|
17
|
+
SourceLocation,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# Producer instantiation patterns
|
|
21
|
+
_PRODUCER_RE = re.compile(
|
|
22
|
+
r"(KafkaProducer|AIOKafkaProducer)\s*\(", re.MULTILINE
|
|
23
|
+
)
|
|
24
|
+
_CONFLUENT_PRODUCER_RE = re.compile(
|
|
25
|
+
r"Producer\s*\(\s*\{", re.MULTILINE
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Consumer instantiation patterns
|
|
29
|
+
_CONSUMER_RE = re.compile(
|
|
30
|
+
r"(KafkaConsumer|AIOKafkaConsumer)\s*\(", re.MULTILINE
|
|
31
|
+
)
|
|
32
|
+
_CONFLUENT_CONSUMER_RE = re.compile(
|
|
33
|
+
r"Consumer\s*\(\s*\{", re.MULTILINE
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Topic send/produce patterns
|
|
37
|
+
_SEND_RE = re.compile(
|
|
38
|
+
r"\.send\s*\(\s*['\"]([^'\"]+)['\"]", re.MULTILINE
|
|
39
|
+
)
|
|
40
|
+
_PRODUCE_RE = re.compile(
|
|
41
|
+
r"\.produce\s*\(\s*['\"]([^'\"]+)['\"]", re.MULTILINE
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Subscribe pattern
|
|
45
|
+
_SUBSCRIBE_RE = re.compile(
|
|
46
|
+
r"\.subscribe\s*\(\s*\[\s*['\"]([^'\"]+)['\"]", re.MULTILINE
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Import patterns
|
|
50
|
+
_IMPORT_RE = re.compile(
|
|
51
|
+
r"(?:from|import)\s+(confluent_kafka|kafka|aiokafka)\b", re.MULTILINE
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class KafkaPythonDetector:
|
|
56
|
+
"""Detects Kafka usage in Python via confluent-kafka, aiokafka, and kafka-python."""
|
|
57
|
+
|
|
58
|
+
name: str = "kafka_python"
|
|
59
|
+
supported_languages: tuple[str, ...] = ("python",)
|
|
60
|
+
|
|
61
|
+
def detect(self, ctx: DetectorContext) -> DetectorResult:
|
|
62
|
+
result = DetectorResult()
|
|
63
|
+
text = decode_text(ctx)
|
|
64
|
+
lines = text.split("\n")
|
|
65
|
+
fp = ctx.file_path
|
|
66
|
+
|
|
67
|
+
# Quick bail-out: check for any Kafka-related keyword
|
|
68
|
+
if not any(kw in text for kw in (
|
|
69
|
+
"KafkaProducer", "KafkaConsumer",
|
|
70
|
+
"AIOKafkaProducer", "AIOKafkaConsumer",
|
|
71
|
+
"confluent_kafka", "from kafka",
|
|
72
|
+
"import kafka", "Producer(", "Consumer(",
|
|
73
|
+
)):
|
|
74
|
+
return result
|
|
75
|
+
|
|
76
|
+
seen_topics: set[str] = set()
|
|
77
|
+
|
|
78
|
+
def _ensure_topic(topic: str, role: str, line: int) -> str:
|
|
79
|
+
topic_id = f"kafka_py:{fp}:topic:{topic}"
|
|
80
|
+
if topic not in seen_topics:
|
|
81
|
+
seen_topics.add(topic)
|
|
82
|
+
result.nodes.append(GraphNode(
|
|
83
|
+
id=topic_id,
|
|
84
|
+
kind=NodeKind.TOPIC,
|
|
85
|
+
label=f"kafka:{topic}",
|
|
86
|
+
module=ctx.module_name,
|
|
87
|
+
location=SourceLocation(file_path=fp, line_start=line),
|
|
88
|
+
properties={"broker": "kafka", "topic": topic, "role": role},
|
|
89
|
+
))
|
|
90
|
+
return topic_id
|
|
91
|
+
|
|
92
|
+
file_node_id = f"kafka_py:{fp}"
|
|
93
|
+
|
|
94
|
+
# Detect producer instantiations
|
|
95
|
+
for i, line in enumerate(lines):
|
|
96
|
+
lineno = i + 1
|
|
97
|
+
if _PRODUCER_RE.search(line) or _CONFLUENT_PRODUCER_RE.search(line):
|
|
98
|
+
result.nodes.append(GraphNode(
|
|
99
|
+
id=f"kafka_py:{fp}:producer:{lineno}",
|
|
100
|
+
kind=NodeKind.TOPIC,
|
|
101
|
+
label="kafka:producer",
|
|
102
|
+
module=ctx.module_name,
|
|
103
|
+
location=SourceLocation(file_path=fp, line_start=lineno),
|
|
104
|
+
properties={"role": "producer"},
|
|
105
|
+
))
|
|
106
|
+
|
|
107
|
+
# Detect consumer instantiations
|
|
108
|
+
for i, line in enumerate(lines):
|
|
109
|
+
lineno = i + 1
|
|
110
|
+
if _CONSUMER_RE.search(line) or _CONFLUENT_CONSUMER_RE.search(line):
|
|
111
|
+
result.nodes.append(GraphNode(
|
|
112
|
+
id=f"kafka_py:{fp}:consumer:{lineno}",
|
|
113
|
+
kind=NodeKind.TOPIC,
|
|
114
|
+
label="kafka:consumer",
|
|
115
|
+
module=ctx.module_name,
|
|
116
|
+
location=SourceLocation(file_path=fp, line_start=lineno),
|
|
117
|
+
properties={"role": "consumer"},
|
|
118
|
+
))
|
|
119
|
+
|
|
120
|
+
# Detect producer.send / producer.produce -> PRODUCES edges
|
|
121
|
+
for i, line in enumerate(lines):
|
|
122
|
+
lineno = i + 1
|
|
123
|
+
m = _SEND_RE.search(line)
|
|
124
|
+
if m and ("send" in line):
|
|
125
|
+
topic = m.group(1)
|
|
126
|
+
topic_id = _ensure_topic(topic, "producer", lineno)
|
|
127
|
+
result.edges.append(GraphEdge(
|
|
128
|
+
source=file_node_id,
|
|
129
|
+
target=topic_id,
|
|
130
|
+
kind=EdgeKind.PRODUCES,
|
|
131
|
+
label=f"produces to {topic}",
|
|
132
|
+
properties={"topic": topic},
|
|
133
|
+
))
|
|
134
|
+
continue
|
|
135
|
+
m = _PRODUCE_RE.search(line)
|
|
136
|
+
if m:
|
|
137
|
+
topic = m.group(1)
|
|
138
|
+
topic_id = _ensure_topic(topic, "producer", lineno)
|
|
139
|
+
result.edges.append(GraphEdge(
|
|
140
|
+
source=file_node_id,
|
|
141
|
+
target=topic_id,
|
|
142
|
+
kind=EdgeKind.PRODUCES,
|
|
143
|
+
label=f"produces to {topic}",
|
|
144
|
+
properties={"topic": topic},
|
|
145
|
+
))
|
|
146
|
+
|
|
147
|
+
# Detect consumer.subscribe -> CONSUMES edges
|
|
148
|
+
for i, line in enumerate(lines):
|
|
149
|
+
lineno = i + 1
|
|
150
|
+
m = _SUBSCRIBE_RE.search(line)
|
|
151
|
+
if m:
|
|
152
|
+
topic = m.group(1)
|
|
153
|
+
topic_id = _ensure_topic(topic, "consumer", lineno)
|
|
154
|
+
result.edges.append(GraphEdge(
|
|
155
|
+
source=file_node_id,
|
|
156
|
+
target=topic_id,
|
|
157
|
+
kind=EdgeKind.CONSUMES,
|
|
158
|
+
label=f"consumes from {topic}",
|
|
159
|
+
properties={"topic": topic},
|
|
160
|
+
))
|
|
161
|
+
|
|
162
|
+
# Detect imports -> IMPORTS edges
|
|
163
|
+
for i, line in enumerate(lines):
|
|
164
|
+
m = _IMPORT_RE.search(line)
|
|
165
|
+
if m:
|
|
166
|
+
lib = m.group(1)
|
|
167
|
+
result.edges.append(GraphEdge(
|
|
168
|
+
source=file_node_id,
|
|
169
|
+
target=f"kafka_py:lib:{lib}",
|
|
170
|
+
kind=EdgeKind.IMPORTS,
|
|
171
|
+
label=f"imports {lib}",
|
|
172
|
+
properties={"library": lib},
|
|
173
|
+
))
|
|
174
|
+
|
|
175
|
+
return result
|