apisec-code-bolt 0.1.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.
- apisec_code_bolt/__init__.py +42 -0
- apisec_code_bolt/__main__.py +11 -0
- apisec_code_bolt/analysis/__init__.py +96 -0
- apisec_code_bolt/analysis/analyzer.py +2309 -0
- apisec_code_bolt/analysis/binding_tracker.py +341 -0
- apisec_code_bolt/analysis/call_graph.py +1197 -0
- apisec_code_bolt/analysis/call_graph_types.py +332 -0
- apisec_code_bolt/analysis/call_resolver.py +988 -0
- apisec_code_bolt/analysis/capability_tagger.py +322 -0
- apisec_code_bolt/analysis/config_scanner.py +197 -0
- apisec_code_bolt/analysis/data_flow.py +1883 -0
- apisec_code_bolt/analysis/dependency_extractor.py +959 -0
- apisec_code_bolt/analysis/flow_analysis.py +1406 -0
- apisec_code_bolt/analysis/hof_catalog.py +61 -0
- apisec_code_bolt/analysis/integration_detector.py +1399 -0
- apisec_code_bolt/analysis/literal_scanner.py +300 -0
- apisec_code_bolt/analysis/path_normalizer.py +55 -0
- apisec_code_bolt/analysis/read_site_detector.py +310 -0
- apisec_code_bolt/analysis/request_patterns.py +162 -0
- apisec_code_bolt/analysis/sensitivity_classifier.py +224 -0
- apisec_code_bolt/analysis/sink_evidence.py +333 -0
- apisec_code_bolt/analysis/url_prefix_resolver.py +338 -0
- apisec_code_bolt/cli/__init__.py +5 -0
- apisec_code_bolt/cli/exit_codes.py +17 -0
- apisec_code_bolt/cli/main.py +1069 -0
- apisec_code_bolt/cloud/__init__.py +1 -0
- apisec_code_bolt/cloud/apisec_client.py +118 -0
- apisec_code_bolt/cloud/client.py +255 -0
- apisec_code_bolt/core/__init__.py +75 -0
- apisec_code_bolt/core/config.py +528 -0
- apisec_code_bolt/core/credentials.py +65 -0
- apisec_code_bolt/core/discovery.py +433 -0
- apisec_code_bolt/core/log_format.py +115 -0
- apisec_code_bolt/core/manifest.py +1009 -0
- apisec_code_bolt/core/repo.py +280 -0
- apisec_code_bolt/core/state.py +59 -0
- apisec_code_bolt/core/telemetry.py +451 -0
- apisec_code_bolt/core/types.py +587 -0
- apisec_code_bolt/fingerprinting/__init__.py +1 -0
- apisec_code_bolt/frameworks/__init__.py +29 -0
- apisec_code_bolt/frameworks/_jwt_common.py +50 -0
- apisec_code_bolt/frameworks/auth_helpers.py +437 -0
- apisec_code_bolt/frameworks/base.py +608 -0
- apisec_code_bolt/frameworks/dotnet/__init__.py +17 -0
- apisec_code_bolt/frameworks/dotnet/_path_helpers.py +43 -0
- apisec_code_bolt/frameworks/dotnet/aspnet_plugin.py +2546 -0
- apisec_code_bolt/frameworks/dotnet/grpc_plugin.py +559 -0
- apisec_code_bolt/frameworks/dotnet/jwt_config_extractor.py +545 -0
- apisec_code_bolt/frameworks/dotnet/legacy_aspnet_plugin.py +732 -0
- apisec_code_bolt/frameworks/dotnet/refit_plugin.py +374 -0
- apisec_code_bolt/frameworks/dotnet/wcf_plugin.py +1239 -0
- apisec_code_bolt/frameworks/java/__init__.py +6 -0
- apisec_code_bolt/frameworks/java/_annotations.py +167 -0
- apisec_code_bolt/frameworks/java/_constraints.py +128 -0
- apisec_code_bolt/frameworks/java/graphql_plugin.py +287 -0
- apisec_code_bolt/frameworks/java/jaxrs_plugin.py +748 -0
- apisec_code_bolt/frameworks/java/jwt_config_extractor.py +361 -0
- apisec_code_bolt/frameworks/java/micronaut_plugin.py +1059 -0
- apisec_code_bolt/frameworks/java/spring_plugin.py +1293 -0
- apisec_code_bolt/frameworks/js/__init__.py +8 -0
- apisec_code_bolt/frameworks/js/express_plugin.py +391 -0
- apisec_code_bolt/frameworks/js/fastify_plugin.py +381 -0
- apisec_code_bolt/frameworks/js/graphql_plugin.py +198 -0
- apisec_code_bolt/frameworks/js/nestjs_plugin.py +423 -0
- apisec_code_bolt/frameworks/python/__init__.py +19 -0
- apisec_code_bolt/frameworks/python/celery_plugin.py +393 -0
- apisec_code_bolt/frameworks/python/click_plugin.py +427 -0
- apisec_code_bolt/frameworks/python/django_plugin.py +867 -0
- apisec_code_bolt/frameworks/python/fastapi/__init__.py +28 -0
- apisec_code_bolt/frameworks/python/fastapi/plugin.py +1390 -0
- apisec_code_bolt/frameworks/python/flask_plugin.py +205 -0
- apisec_code_bolt/frameworks/python/graphql_plugin.py +274 -0
- apisec_code_bolt/frameworks/python/prefect_plugin.py +251 -0
- apisec_code_bolt/frameworks/python/webhook_plugin.py +255 -0
- apisec_code_bolt/parsing/__init__.py +62 -0
- apisec_code_bolt/parsing/base.py +554 -0
- apisec_code_bolt/parsing/csharp/__init__.py +5 -0
- apisec_code_bolt/parsing/csharp/language_services.py +203 -0
- apisec_code_bolt/parsing/csharp/literals.py +72 -0
- apisec_code_bolt/parsing/csharp/parser.py +1158 -0
- apisec_code_bolt/parsing/csharp/type_resolver.py +568 -0
- apisec_code_bolt/parsing/js/__init__.py +5 -0
- apisec_code_bolt/parsing/js/language_services.py +118 -0
- apisec_code_bolt/parsing/js/parser.py +622 -0
- apisec_code_bolt/parsing/jvm/__init__.py +7 -0
- apisec_code_bolt/parsing/jvm/language_services.py +270 -0
- apisec_code_bolt/parsing/jvm/parser.py +774 -0
- apisec_code_bolt/parsing/jvm/type_resolver.py +422 -0
- apisec_code_bolt/parsing/python/__init__.py +150 -0
- apisec_code_bolt/parsing/python/cbv_extractor.py +606 -0
- apisec_code_bolt/parsing/python/constant_resolver.py +500 -0
- apisec_code_bolt/parsing/python/cross_file_resolver.py +1054 -0
- apisec_code_bolt/parsing/python/dynamic_route_detector.py +532 -0
- apisec_code_bolt/parsing/python/expression_utils.py +221 -0
- apisec_code_bolt/parsing/python/extraction_types.py +271 -0
- apisec_code_bolt/parsing/python/language_services.py +487 -0
- apisec_code_bolt/parsing/python/parameter_analyzer.py +789 -0
- apisec_code_bolt/parsing/python/parser.py +719 -0
- apisec_code_bolt/parsing/python/path_resolver.py +576 -0
- apisec_code_bolt/parsing/python/router_registry.py +806 -0
- apisec_code_bolt/parsing/python/type_resolver.py +730 -0
- apisec_code_bolt/parsing/python/visitors.py +1544 -0
- apisec_code_bolt/parsing/services.py +544 -0
- apisec_code_bolt/query/__init__.py +1 -0
- apisec_code_bolt/query/ast_cache.py +182 -0
- apisec_code_bolt/query/executor.py +283 -0
- apisec_code_bolt/query/handlers.py +832 -0
- apisec_code_bolt-0.1.0.dist-info/METADATA +230 -0
- apisec_code_bolt-0.1.0.dist-info/RECORD +111 -0
- apisec_code_bolt-0.1.0.dist-info/WHEEL +4 -0
- apisec_code_bolt-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Flask framework plugin for extracting routes and parameters.
|
|
3
|
+
|
|
4
|
+
Flask uses:
|
|
5
|
+
- @app.route(path) or @bp.route(path) with methods
|
|
6
|
+
- Path params: <int:id>, <uuid:pk> (converted to {id}, {pk})
|
|
7
|
+
- Parameters: from request.args, request.form, request.get_json() in handler body
|
|
8
|
+
(read-site detection handles these; we extract route structure here)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
|
|
15
|
+
from ...core.types import (
|
|
16
|
+
Confidence,
|
|
17
|
+
Framework,
|
|
18
|
+
HttpMethod,
|
|
19
|
+
Language,
|
|
20
|
+
ParameterLocation,
|
|
21
|
+
)
|
|
22
|
+
from ...parsing.base import ParsedDecorator, ParsedFile, ParsedFunction
|
|
23
|
+
from ..base import (
|
|
24
|
+
BaseFrameworkPlugin,
|
|
25
|
+
ExtractedParameter,
|
|
26
|
+
ExtractedResponse,
|
|
27
|
+
ExtractedRoute,
|
|
28
|
+
FrameworkPluginRegistry,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Flask route decorator and HTTP methods
|
|
32
|
+
FLASK_IMPORTS = frozenset(
|
|
33
|
+
{
|
|
34
|
+
"flask",
|
|
35
|
+
"flask.Flask",
|
|
36
|
+
"flask.Blueprint",
|
|
37
|
+
"flask.request",
|
|
38
|
+
}
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Default methods when not specified
|
|
42
|
+
ROUTE_DEFAULT_METHODS = [HttpMethod.GET]
|
|
43
|
+
|
|
44
|
+
# Path param conversion: <int:id> -> {id}, <uuid:pk> -> {pk}
|
|
45
|
+
FLASK_PATH_PARAM_RE = re.compile(r"<(?:(?:\w+):)?(\w+)>")
|
|
46
|
+
|
|
47
|
+
HTTP_METHOD_MAP = {
|
|
48
|
+
"get": HttpMethod.GET,
|
|
49
|
+
"post": HttpMethod.POST,
|
|
50
|
+
"put": HttpMethod.PUT,
|
|
51
|
+
"patch": HttpMethod.PATCH,
|
|
52
|
+
"delete": HttpMethod.DELETE,
|
|
53
|
+
"head": HttpMethod.HEAD,
|
|
54
|
+
"options": HttpMethod.OPTIONS,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _flask_path_to_manifest(path: str) -> str:
|
|
59
|
+
"""Convert Flask path <int:id> to manifest format {id}."""
|
|
60
|
+
return FLASK_PATH_PARAM_RE.sub(r"{\1}", path)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _parse_flask_path_params(path: str) -> list[str]:
|
|
64
|
+
"""Extract path parameter names from Flask route."""
|
|
65
|
+
return FLASK_PATH_PARAM_RE.findall(path)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class FlaskPlugin(BaseFrameworkPlugin):
|
|
69
|
+
"""Plugin for Flask framework route extraction."""
|
|
70
|
+
|
|
71
|
+
FRAMEWORK = Framework.FLASK
|
|
72
|
+
LANGUAGE = Language.PYTHON
|
|
73
|
+
DETECTION_IMPORTS = FLASK_IMPORTS
|
|
74
|
+
|
|
75
|
+
def __init__(self):
|
|
76
|
+
self._blueprint_prefixes: dict[str, str] = {}
|
|
77
|
+
|
|
78
|
+
def detect(self, parsed_file: ParsedFile) -> bool:
|
|
79
|
+
"""Detect Flask usage via imports."""
|
|
80
|
+
for imp in parsed_file.imports:
|
|
81
|
+
mod = (imp.module or "").split(".")[0]
|
|
82
|
+
if mod == "flask":
|
|
83
|
+
return True
|
|
84
|
+
for name in imp.names:
|
|
85
|
+
if name in ("Flask", "Blueprint", "request"):
|
|
86
|
+
return True
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
def extract_routes(self, parsed_file: ParsedFile, context=None) -> list[ExtractedRoute]:
|
|
90
|
+
"""Extract routes from @app.route and @bp.route decorators."""
|
|
91
|
+
routes: list[ExtractedRoute] = []
|
|
92
|
+
|
|
93
|
+
for func in parsed_file.functions:
|
|
94
|
+
routes.extend(self._extract_routes_from_function(func, parsed_file))
|
|
95
|
+
|
|
96
|
+
for cls in parsed_file.classes:
|
|
97
|
+
for method in cls.methods:
|
|
98
|
+
routes.extend(self._extract_routes_from_function(method, parsed_file))
|
|
99
|
+
|
|
100
|
+
return routes
|
|
101
|
+
|
|
102
|
+
def _extract_routes_from_function(
|
|
103
|
+
self,
|
|
104
|
+
func: ParsedFunction,
|
|
105
|
+
parsed_file: ParsedFile,
|
|
106
|
+
) -> list[ExtractedRoute]:
|
|
107
|
+
"""Extract route(s) from a function with @app.route or @bp.route."""
|
|
108
|
+
dec = self._find_route_decorator(func)
|
|
109
|
+
if not dec:
|
|
110
|
+
return []
|
|
111
|
+
|
|
112
|
+
path = self._extract_route_path(dec)
|
|
113
|
+
methods = self._extract_methods(dec)
|
|
114
|
+
manifest_path = _flask_path_to_manifest(path)
|
|
115
|
+
path_param_names = _parse_flask_path_params(path)
|
|
116
|
+
|
|
117
|
+
path_params = [
|
|
118
|
+
ExtractedParameter(name=name, location=ParameterLocation.PATH)
|
|
119
|
+
for name in path_param_names
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
handler_qn = func.qualified_name
|
|
123
|
+
routes = []
|
|
124
|
+
for method in methods:
|
|
125
|
+
routes.append(
|
|
126
|
+
ExtractedRoute(
|
|
127
|
+
method=method,
|
|
128
|
+
path=manifest_path,
|
|
129
|
+
handler_function=handler_qn,
|
|
130
|
+
handler_location=func.location,
|
|
131
|
+
path_params=path_params,
|
|
132
|
+
query_params=[],
|
|
133
|
+
header_params=[],
|
|
134
|
+
cookie_params=[],
|
|
135
|
+
body=None,
|
|
136
|
+
response=ExtractedResponse(),
|
|
137
|
+
router_name=None,
|
|
138
|
+
confidence=Confidence.HIGH,
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
return routes
|
|
142
|
+
|
|
143
|
+
def _find_route_decorator(self, func: ParsedFunction) -> ParsedDecorator | None:
|
|
144
|
+
"""Find @app.route or @bp.route decorator."""
|
|
145
|
+
for dec in func.decorators:
|
|
146
|
+
name = (dec.name or "").lower()
|
|
147
|
+
full = (
|
|
148
|
+
dec.qualified_name.full
|
|
149
|
+
if dec.qualified_name and hasattr(dec.qualified_name, "full")
|
|
150
|
+
else name
|
|
151
|
+
)
|
|
152
|
+
if "route" in name or "route" in full:
|
|
153
|
+
return dec
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
def _extract_route_path(self, dec: ParsedDecorator) -> str:
|
|
157
|
+
"""Extract path from @app.route(path) or @app.route(path, methods=...)."""
|
|
158
|
+
if dec.positional_args:
|
|
159
|
+
first = dec.positional_args[0]
|
|
160
|
+
if isinstance(first, str):
|
|
161
|
+
return first
|
|
162
|
+
if "rule" in dec.arguments:
|
|
163
|
+
return str(dec.arguments["rule"])
|
|
164
|
+
if dec.arguments:
|
|
165
|
+
first_val = next(iter(dec.arguments.values()), None)
|
|
166
|
+
if isinstance(first_val, str) and first_val.startswith("/"):
|
|
167
|
+
return first_val
|
|
168
|
+
return "/"
|
|
169
|
+
|
|
170
|
+
def _extract_methods(self, dec: ParsedDecorator) -> list[HttpMethod]:
|
|
171
|
+
"""Extract HTTP methods from decorator."""
|
|
172
|
+
methods = dec.arguments.get("methods")
|
|
173
|
+
if isinstance(methods, list):
|
|
174
|
+
return [HTTP_METHOD_MAP.get(str(m).lower(), HttpMethod.GET) for m in methods]
|
|
175
|
+
if isinstance(methods, str):
|
|
176
|
+
# Handle tuple/list literals stored as strings by the parser, e.g. "('GET', 'POST')"
|
|
177
|
+
names = re.findall(r"['\"]([A-Za-z]+)['\"]", methods)
|
|
178
|
+
if names:
|
|
179
|
+
return [HTTP_METHOD_MAP.get(n.lower(), HttpMethod.GET) for n in names]
|
|
180
|
+
return [HttpMethod.GET]
|
|
181
|
+
|
|
182
|
+
def extract_dependencies(self, parsed_file: ParsedFile) -> list:
|
|
183
|
+
"""Flask does not use Depends(); return empty."""
|
|
184
|
+
return []
|
|
185
|
+
|
|
186
|
+
def extract_auth_schemes(self, parsed_file: ParsedFile) -> list:
|
|
187
|
+
"""Extract auth schemes if present."""
|
|
188
|
+
return []
|
|
189
|
+
|
|
190
|
+
def extract_auth_dependencies(
|
|
191
|
+
self,
|
|
192
|
+
parsed_file: ParsedFile,
|
|
193
|
+
known_scheme_names: set[str] | None = None,
|
|
194
|
+
**kwargs,
|
|
195
|
+
) -> list:
|
|
196
|
+
"""Extract auth dependencies."""
|
|
197
|
+
return []
|
|
198
|
+
|
|
199
|
+
def extract_middleware(self, parsed_file: ParsedFile) -> list:
|
|
200
|
+
"""Extract Flask middleware (before_request, etc.)."""
|
|
201
|
+
return []
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
_flask_plugin = FlaskPlugin()
|
|
205
|
+
FrameworkPluginRegistry.register(_flask_plugin)
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Python GraphQL framework plugin.
|
|
3
|
+
|
|
4
|
+
Supports:
|
|
5
|
+
- Strawberry: @strawberry.type classes (Query/Mutation/Subscription) with
|
|
6
|
+
@field / @mutation methods
|
|
7
|
+
- Graphene: ObjectType subclasses with resolve_<field> methods
|
|
8
|
+
- Ariadne: @query.field("name") / @mutation.field("name") decorators on functions
|
|
9
|
+
|
|
10
|
+
Path format: /graphql:Query.op_name
|
|
11
|
+
HTTP method: POST for Mutation/Subscription, GET otherwise
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
from typing import TYPE_CHECKING, Any
|
|
18
|
+
|
|
19
|
+
from ...core.types import (
|
|
20
|
+
Framework,
|
|
21
|
+
HttpMethod,
|
|
22
|
+
Language,
|
|
23
|
+
QualifiedName,
|
|
24
|
+
)
|
|
25
|
+
from ...parsing.base import ParsedFile
|
|
26
|
+
from ..base import (
|
|
27
|
+
BaseFrameworkPlugin,
|
|
28
|
+
ExtractedAuthDependency,
|
|
29
|
+
ExtractedAuthScheme,
|
|
30
|
+
ExtractedDependency,
|
|
31
|
+
ExtractedMiddleware,
|
|
32
|
+
ExtractedRoute,
|
|
33
|
+
FrameworkPluginRegistry,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
if TYPE_CHECKING:
|
|
37
|
+
from ...parsing.services import AnalysisContext
|
|
38
|
+
|
|
39
|
+
logger = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
_GRAPHQL_IMPORTS = frozenset(
|
|
42
|
+
{
|
|
43
|
+
"strawberry",
|
|
44
|
+
"graphene",
|
|
45
|
+
"ariadne",
|
|
46
|
+
"graphql",
|
|
47
|
+
"graphql_core",
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _graphql_http_method(operation_type: str) -> HttpMethod:
|
|
53
|
+
"""Return HTTP method for GraphQL operation type."""
|
|
54
|
+
if operation_type.lower() in ("mutation", "subscription"):
|
|
55
|
+
return HttpMethod.POST
|
|
56
|
+
return HttpMethod.GET
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _graphql_path(operation_type: str, field_name: str) -> str:
|
|
60
|
+
"""Return the internal path token for a GraphQL operation.
|
|
61
|
+
|
|
62
|
+
Convention: /graphql:<OperationType>.<fieldName>
|
|
63
|
+
This is NOT a real HTTP URL. Downstream consumers must translate it to a
|
|
64
|
+
GraphQL request (POST /graphql, body {"query": "{ fieldName ... }"}}).
|
|
65
|
+
Query → GET semantics; Mutation/Subscription → POST semantics.
|
|
66
|
+
"""
|
|
67
|
+
return f"/graphql:{operation_type}.{field_name}"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class GraphQLPythonPlugin(BaseFrameworkPlugin):
|
|
71
|
+
"""
|
|
72
|
+
Framework plugin for Python GraphQL frameworks.
|
|
73
|
+
|
|
74
|
+
Detects Strawberry, Graphene, and Ariadne usage and extracts
|
|
75
|
+
GraphQL operations as routes.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
FRAMEWORK = Framework.GRAPHQL
|
|
79
|
+
LANGUAGE = Language.PYTHON
|
|
80
|
+
DETECTION_IMPORTS: frozenset[str] = _GRAPHQL_IMPORTS
|
|
81
|
+
|
|
82
|
+
def detect(self, parsed_file: ParsedFile) -> bool:
|
|
83
|
+
"""Detect GraphQL usage via imports."""
|
|
84
|
+
for imp in parsed_file.imports:
|
|
85
|
+
if imp.module in _GRAPHQL_IMPORTS:
|
|
86
|
+
return True
|
|
87
|
+
if imp.module.startswith("strawberry") or imp.module.startswith("graphene"):
|
|
88
|
+
return True
|
|
89
|
+
if imp.module.startswith("ariadne"):
|
|
90
|
+
return True
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
def extract_routes(
|
|
94
|
+
self,
|
|
95
|
+
parsed_file: ParsedFile,
|
|
96
|
+
context: AnalysisContext | None = None,
|
|
97
|
+
) -> list[ExtractedRoute]:
|
|
98
|
+
"""Extract GraphQL operations as routes."""
|
|
99
|
+
routes: list[ExtractedRoute] = []
|
|
100
|
+
|
|
101
|
+
# Determine primary library
|
|
102
|
+
has_strawberry = any(imp.module.startswith("strawberry") for imp in parsed_file.imports)
|
|
103
|
+
has_graphene = any(imp.module.startswith("graphene") for imp in parsed_file.imports)
|
|
104
|
+
has_ariadne = any(imp.module.startswith("ariadne") for imp in parsed_file.imports)
|
|
105
|
+
|
|
106
|
+
if has_strawberry:
|
|
107
|
+
routes.extend(self._extract_strawberry(parsed_file))
|
|
108
|
+
if has_graphene:
|
|
109
|
+
routes.extend(self._extract_graphene(parsed_file))
|
|
110
|
+
if has_ariadne:
|
|
111
|
+
routes.extend(self._extract_ariadne(parsed_file))
|
|
112
|
+
|
|
113
|
+
return routes
|
|
114
|
+
|
|
115
|
+
def _extract_strawberry(self, parsed_file: ParsedFile) -> list[ExtractedRoute]:
|
|
116
|
+
"""Extract Strawberry GraphQL operations."""
|
|
117
|
+
routes: list[ExtractedRoute] = []
|
|
118
|
+
|
|
119
|
+
for cls in parsed_file.classes:
|
|
120
|
+
# Look for @strawberry.type classes
|
|
121
|
+
operation_type = None
|
|
122
|
+
for dec in cls.decorators:
|
|
123
|
+
if dec.name in ("strawberry.type", "type") and (
|
|
124
|
+
"strawberry" in dec.name
|
|
125
|
+
or any(imp.module.startswith("strawberry") for imp in parsed_file.imports)
|
|
126
|
+
):
|
|
127
|
+
# Determine operation type from class name
|
|
128
|
+
if cls.name == "Query":
|
|
129
|
+
operation_type = "Query"
|
|
130
|
+
elif cls.name == "Mutation":
|
|
131
|
+
operation_type = "Mutation"
|
|
132
|
+
elif cls.name == "Subscription":
|
|
133
|
+
operation_type = "Subscription"
|
|
134
|
+
else:
|
|
135
|
+
operation_type = "Query"
|
|
136
|
+
|
|
137
|
+
if operation_type is None:
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
for method in cls.methods:
|
|
141
|
+
# Look for @strawberry.field or @field or @mutation
|
|
142
|
+
is_field = any(
|
|
143
|
+
dec.name
|
|
144
|
+
in (
|
|
145
|
+
"field",
|
|
146
|
+
"strawberry.field",
|
|
147
|
+
"mutation",
|
|
148
|
+
"strawberry.mutation",
|
|
149
|
+
"subscription",
|
|
150
|
+
"strawberry.subscription",
|
|
151
|
+
)
|
|
152
|
+
for dec in method.decorators
|
|
153
|
+
)
|
|
154
|
+
if not is_field and method.name.startswith("_"):
|
|
155
|
+
continue
|
|
156
|
+
if not is_field:
|
|
157
|
+
continue
|
|
158
|
+
|
|
159
|
+
path = _graphql_path(operation_type, method.name)
|
|
160
|
+
routes.append(
|
|
161
|
+
ExtractedRoute(
|
|
162
|
+
method=_graphql_http_method(operation_type),
|
|
163
|
+
path=path,
|
|
164
|
+
handler_function=QualifiedName(
|
|
165
|
+
module=parsed_file.path.stem,
|
|
166
|
+
name=f"{cls.name}.{method.name}",
|
|
167
|
+
),
|
|
168
|
+
handler_location=method.location,
|
|
169
|
+
kind="http",
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
return routes
|
|
174
|
+
|
|
175
|
+
def _extract_graphene(self, parsed_file: ParsedFile) -> list[ExtractedRoute]:
|
|
176
|
+
"""Extract Graphene GraphQL operations."""
|
|
177
|
+
routes: list[ExtractedRoute] = []
|
|
178
|
+
|
|
179
|
+
for cls in parsed_file.classes:
|
|
180
|
+
# Graphene ObjectType subclasses named Query/Mutation
|
|
181
|
+
base_names = {b.lower() for b in cls.base_classes}
|
|
182
|
+
is_graphene = any("objecttype" in b or "mutation" in b for b in base_names)
|
|
183
|
+
if not is_graphene:
|
|
184
|
+
continue
|
|
185
|
+
|
|
186
|
+
operation_type = "Query"
|
|
187
|
+
if cls.name == "Mutation" or any("mutation" in b for b in base_names):
|
|
188
|
+
operation_type = "Mutation"
|
|
189
|
+
|
|
190
|
+
for method in cls.methods:
|
|
191
|
+
# resolve_<field_name> pattern
|
|
192
|
+
if not method.name.startswith("resolve_"):
|
|
193
|
+
continue
|
|
194
|
+
field_name = method.name[len("resolve_") :]
|
|
195
|
+
path = _graphql_path(operation_type, field_name)
|
|
196
|
+
routes.append(
|
|
197
|
+
ExtractedRoute(
|
|
198
|
+
method=_graphql_http_method(operation_type),
|
|
199
|
+
path=path,
|
|
200
|
+
handler_function=QualifiedName(
|
|
201
|
+
module=parsed_file.path.stem,
|
|
202
|
+
name=f"{cls.name}.{method.name}",
|
|
203
|
+
),
|
|
204
|
+
handler_location=method.location,
|
|
205
|
+
kind="http",
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
return routes
|
|
210
|
+
|
|
211
|
+
def _extract_ariadne(self, parsed_file: ParsedFile) -> list[ExtractedRoute]:
|
|
212
|
+
"""Extract Ariadne GraphQL operations."""
|
|
213
|
+
routes: list[ExtractedRoute] = []
|
|
214
|
+
|
|
215
|
+
for func in parsed_file.functions:
|
|
216
|
+
for dec in func.decorators:
|
|
217
|
+
# @query.field("name") or @mutation.field("name")
|
|
218
|
+
# Python parser may give dec.name = "field" with qualified_name = "query.field"
|
|
219
|
+
dec_full = str(dec.qualified_name) if dec.qualified_name else dec.name
|
|
220
|
+
|
|
221
|
+
if not (dec_full.endswith(".field") or dec.name.endswith(".field")):
|
|
222
|
+
continue
|
|
223
|
+
|
|
224
|
+
operation_prefix = dec_full.split(".")[0].lower()
|
|
225
|
+
if operation_prefix == "query":
|
|
226
|
+
operation_type = "Query"
|
|
227
|
+
elif operation_prefix == "mutation":
|
|
228
|
+
operation_type = "Mutation"
|
|
229
|
+
elif operation_prefix == "subscription":
|
|
230
|
+
operation_type = "Subscription"
|
|
231
|
+
else:
|
|
232
|
+
operation_type = "Query"
|
|
233
|
+
|
|
234
|
+
field_name = dec.positional_args[0] if dec.positional_args else func.name
|
|
235
|
+
|
|
236
|
+
path = _graphql_path(operation_type, str(field_name))
|
|
237
|
+
routes.append(
|
|
238
|
+
ExtractedRoute(
|
|
239
|
+
method=_graphql_http_method(operation_type),
|
|
240
|
+
path=path,
|
|
241
|
+
handler_function=QualifiedName(
|
|
242
|
+
module=parsed_file.path.stem,
|
|
243
|
+
name=func.name,
|
|
244
|
+
),
|
|
245
|
+
handler_location=func.location,
|
|
246
|
+
kind="http",
|
|
247
|
+
)
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
return routes
|
|
251
|
+
|
|
252
|
+
def extract_dependencies(self, parsed_file: ParsedFile) -> list[ExtractedDependency]:
|
|
253
|
+
return []
|
|
254
|
+
|
|
255
|
+
def extract_auth_schemes(self, parsed_file: ParsedFile) -> list[ExtractedAuthScheme]:
|
|
256
|
+
from ..auth_helpers import extract_python_graphql_auth_schemes
|
|
257
|
+
|
|
258
|
+
return extract_python_graphql_auth_schemes(parsed_file)
|
|
259
|
+
|
|
260
|
+
def extract_auth_dependencies(
|
|
261
|
+
self,
|
|
262
|
+
parsed_file: ParsedFile,
|
|
263
|
+
known_scheme_names: set[str] | None = None,
|
|
264
|
+
**kwargs: Any,
|
|
265
|
+
) -> list[ExtractedAuthDependency]:
|
|
266
|
+
from ..auth_helpers import extract_python_graphql_auth_dependencies
|
|
267
|
+
|
|
268
|
+
return extract_python_graphql_auth_dependencies(parsed_file)
|
|
269
|
+
|
|
270
|
+
def extract_middleware(self, parsed_file: ParsedFile) -> list[ExtractedMiddleware]:
|
|
271
|
+
return []
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
FrameworkPluginRegistry.register(GraphQLPythonPlugin())
|