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,374 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Refit framework plugin.
|
|
3
|
+
|
|
4
|
+
Refit is a declarative REST client library for .NET — interfaces annotated with
|
|
5
|
+
HTTP verb attributes are auto-implemented as typed HTTP clients at runtime.
|
|
6
|
+
These represent *outbound* call surface (what the app calls), analogous to
|
|
7
|
+
Spring's @FeignClient.
|
|
8
|
+
|
|
9
|
+
Supports:
|
|
10
|
+
- HTTP verbs: [Get], [Post], [Put], [Delete], [Patch], [Head], [Options]
|
|
11
|
+
- Path parameters: method parameters whose names match {template} segments
|
|
12
|
+
- Query parameters: [Query] / [AliasAs] or undecorated primitives outside path
|
|
13
|
+
- Request body: [Body] or undecorated complex type (Refit default inference)
|
|
14
|
+
- Header parameters: [Header("X-Custom")]
|
|
15
|
+
- [Multipart] methods: body treated as multipart/form-data
|
|
16
|
+
- Interface-level and method-level [Headers(...)] static header constants
|
|
17
|
+
- RestService.For<T>() / RefitClient.For<T>() registration detection
|
|
18
|
+
|
|
19
|
+
Routes are emitted with kind="refit_client" and tags=["refit"].
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import re
|
|
25
|
+
from typing import ClassVar
|
|
26
|
+
|
|
27
|
+
from ...core.types import (
|
|
28
|
+
Confidence,
|
|
29
|
+
Framework,
|
|
30
|
+
HttpMethod,
|
|
31
|
+
Language,
|
|
32
|
+
ParameterLocation,
|
|
33
|
+
)
|
|
34
|
+
from ...parsing.base import ParsedClass, ParsedFile, ParsedFunction
|
|
35
|
+
from ...parsing.services import AnalysisContext
|
|
36
|
+
from ..base import (
|
|
37
|
+
BaseFrameworkPlugin,
|
|
38
|
+
ExtractedAuthDependency,
|
|
39
|
+
ExtractedAuthScheme,
|
|
40
|
+
ExtractedBody,
|
|
41
|
+
ExtractedDependency,
|
|
42
|
+
ExtractedMiddleware,
|
|
43
|
+
ExtractedParameter,
|
|
44
|
+
ExtractedResponse,
|
|
45
|
+
ExtractedRoute,
|
|
46
|
+
FrameworkPluginRegistry,
|
|
47
|
+
)
|
|
48
|
+
from .aspnet_plugin import AspNetCorePlugin, _str_to_qname
|
|
49
|
+
|
|
50
|
+
# =============================================================================
|
|
51
|
+
# Constants
|
|
52
|
+
# =============================================================================
|
|
53
|
+
|
|
54
|
+
_REFIT_IMPORTS: frozenset[str] = frozenset(
|
|
55
|
+
{
|
|
56
|
+
"Refit",
|
|
57
|
+
"Refit.Attributes",
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Refit HTTP verb attribute name → HttpMethod
|
|
62
|
+
_REFIT_VERBS: dict[str, HttpMethod] = {
|
|
63
|
+
"Get": HttpMethod.GET,
|
|
64
|
+
"Post": HttpMethod.POST,
|
|
65
|
+
"Put": HttpMethod.PUT,
|
|
66
|
+
"Delete": HttpMethod.DELETE,
|
|
67
|
+
"Patch": HttpMethod.PATCH,
|
|
68
|
+
"Head": HttpMethod.HEAD,
|
|
69
|
+
"Options": HttpMethod.OPTIONS,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Scalar C# type names — undecorated scalars outside the path template become
|
|
73
|
+
# query params (Refit's default). Complex types become the body.
|
|
74
|
+
_SCALAR_TYPES: frozenset[str] = frozenset(
|
|
75
|
+
{
|
|
76
|
+
"bool",
|
|
77
|
+
"Boolean",
|
|
78
|
+
"byte",
|
|
79
|
+
"Byte",
|
|
80
|
+
"sbyte",
|
|
81
|
+
"SByte",
|
|
82
|
+
"char",
|
|
83
|
+
"Char",
|
|
84
|
+
"short",
|
|
85
|
+
"Int16",
|
|
86
|
+
"ushort",
|
|
87
|
+
"UInt16",
|
|
88
|
+
"int",
|
|
89
|
+
"Int32",
|
|
90
|
+
"uint",
|
|
91
|
+
"UInt32",
|
|
92
|
+
"long",
|
|
93
|
+
"Int64",
|
|
94
|
+
"ulong",
|
|
95
|
+
"UInt64",
|
|
96
|
+
"float",
|
|
97
|
+
"Single",
|
|
98
|
+
"double",
|
|
99
|
+
"Double",
|
|
100
|
+
"decimal",
|
|
101
|
+
"Decimal",
|
|
102
|
+
"string",
|
|
103
|
+
"String",
|
|
104
|
+
"Guid",
|
|
105
|
+
"DateTime",
|
|
106
|
+
"DateTimeOffset",
|
|
107
|
+
"DateOnly",
|
|
108
|
+
"TimeOnly",
|
|
109
|
+
"TimeSpan",
|
|
110
|
+
"Uri",
|
|
111
|
+
"int?",
|
|
112
|
+
"long?",
|
|
113
|
+
"bool?",
|
|
114
|
+
"double?",
|
|
115
|
+
"decimal?",
|
|
116
|
+
"Guid?",
|
|
117
|
+
"DateTime?",
|
|
118
|
+
"DateTimeOffset?",
|
|
119
|
+
"CancellationToken", # always skipped
|
|
120
|
+
}
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# =============================================================================
|
|
125
|
+
# RefitPlugin
|
|
126
|
+
# =============================================================================
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class RefitPlugin(BaseFrameworkPlugin):
|
|
130
|
+
"""
|
|
131
|
+
Framework plugin for Refit declarative HTTP client interfaces.
|
|
132
|
+
|
|
133
|
+
Each interface method carrying a Refit HTTP verb attribute is emitted as
|
|
134
|
+
an ExtractedRoute with kind="refit_client".
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
FRAMEWORK: ClassVar[Framework] = Framework.REFIT
|
|
138
|
+
LANGUAGE: ClassVar[Language] = Language.CSHARP
|
|
139
|
+
DETECTION_IMPORTS: ClassVar[frozenset[str]] = _REFIT_IMPORTS
|
|
140
|
+
|
|
141
|
+
# Reuse path helpers from AspNetCorePlugin without inheriting its
|
|
142
|
+
# controller/MVC logic.
|
|
143
|
+
_helper = AspNetCorePlugin()
|
|
144
|
+
|
|
145
|
+
# -------------------------------------------------------------------------
|
|
146
|
+
# Detection
|
|
147
|
+
# -------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
def detect(self, parsed_file: ParsedFile) -> bool:
|
|
150
|
+
for imp in parsed_file.imports:
|
|
151
|
+
if (imp.module or "").startswith("Refit"):
|
|
152
|
+
return True
|
|
153
|
+
# Attribute-only detection (no using statement but attributes present)
|
|
154
|
+
for cls in parsed_file.classes:
|
|
155
|
+
for method in cls.methods:
|
|
156
|
+
if any(dec.name in _REFIT_VERBS for dec in method.decorators):
|
|
157
|
+
return True
|
|
158
|
+
return False
|
|
159
|
+
|
|
160
|
+
# -------------------------------------------------------------------------
|
|
161
|
+
# Route extraction
|
|
162
|
+
# -------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
def extract_routes(
|
|
165
|
+
self,
|
|
166
|
+
parsed_file: ParsedFile,
|
|
167
|
+
context: AnalysisContext | None = None,
|
|
168
|
+
) -> list[ExtractedRoute]:
|
|
169
|
+
routes: list[ExtractedRoute] = []
|
|
170
|
+
|
|
171
|
+
for cls in parsed_file.classes:
|
|
172
|
+
if not self._has_refit_methods(cls):
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
for method in cls.methods:
|
|
176
|
+
route = self._extract_refit_route(method, cls)
|
|
177
|
+
if route:
|
|
178
|
+
routes.append(route)
|
|
179
|
+
|
|
180
|
+
return routes
|
|
181
|
+
|
|
182
|
+
# Refit interfaces define outbound HTTP clients only — no server-side auth,
|
|
183
|
+
# middleware, or DI definitions to extract.
|
|
184
|
+
|
|
185
|
+
def extract_auth_schemes(self, parsed_file: ParsedFile) -> list[ExtractedAuthScheme]:
|
|
186
|
+
return []
|
|
187
|
+
|
|
188
|
+
def extract_auth_dependencies(
|
|
189
|
+
self, parsed_file: ParsedFile, known_scheme_names: set[str] | None = None, **kwargs
|
|
190
|
+
) -> list[ExtractedAuthDependency]:
|
|
191
|
+
return []
|
|
192
|
+
|
|
193
|
+
def extract_dependencies(self, parsed_file: ParsedFile) -> list[ExtractedDependency]:
|
|
194
|
+
return []
|
|
195
|
+
|
|
196
|
+
def extract_middleware(self, parsed_file: ParsedFile) -> list[ExtractedMiddleware]:
|
|
197
|
+
return []
|
|
198
|
+
|
|
199
|
+
# -------------------------------------------------------------------------
|
|
200
|
+
# Internal helpers
|
|
201
|
+
# -------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
def _has_refit_methods(self, cls: ParsedClass) -> bool:
|
|
204
|
+
return any(dec.name in _REFIT_VERBS for method in cls.methods for dec in method.decorators)
|
|
205
|
+
|
|
206
|
+
def _extract_refit_route(
|
|
207
|
+
self, method: ParsedFunction, cls: ParsedClass
|
|
208
|
+
) -> ExtractedRoute | None:
|
|
209
|
+
http_method: HttpMethod | None = None
|
|
210
|
+
path_template: str = ""
|
|
211
|
+
|
|
212
|
+
for dec in method.decorators:
|
|
213
|
+
hm = _REFIT_VERBS.get(dec.name)
|
|
214
|
+
if hm is not None:
|
|
215
|
+
http_method = hm
|
|
216
|
+
raw = self._helper._dec_path(dec) or ""
|
|
217
|
+
path_template = raw
|
|
218
|
+
break
|
|
219
|
+
|
|
220
|
+
if http_method is None:
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
# Normalise path
|
|
224
|
+
if path_template and not path_template.startswith("/"):
|
|
225
|
+
path_template = "/" + path_template
|
|
226
|
+
path_template = self._helper._normalize_path(path_template) if path_template else "/"
|
|
227
|
+
|
|
228
|
+
# Strip route constraints: {id:int} → {id}
|
|
229
|
+
path_template = re.sub(r"\{([^:}]+):[^}]+\}", r"{\1}", path_template)
|
|
230
|
+
|
|
231
|
+
path_tpl_names = {m.group(1) for m in re.finditer(r"\{([^}]+)\}", path_template)}
|
|
232
|
+
|
|
233
|
+
is_multipart = any(dec.name == "Multipart" for dec in method.decorators)
|
|
234
|
+
|
|
235
|
+
path_params: list[ExtractedParameter] = []
|
|
236
|
+
query_params: list[ExtractedParameter] = []
|
|
237
|
+
header_params: list[ExtractedParameter] = []
|
|
238
|
+
body: ExtractedBody | None = None
|
|
239
|
+
|
|
240
|
+
for param in method.parameters:
|
|
241
|
+
# CancellationToken is always a framework param — skip
|
|
242
|
+
base_type = (param.type_annotation or "").split("<")[0].strip().rstrip("?")
|
|
243
|
+
if base_type == "CancellationToken":
|
|
244
|
+
continue
|
|
245
|
+
|
|
246
|
+
# Parameter attributes are in param.metadata (keyed by attribute name).
|
|
247
|
+
# Values: positional string arg, or True for no-arg attributes.
|
|
248
|
+
meta = param.metadata or {}
|
|
249
|
+
|
|
250
|
+
# [AliasAs("name")] renames the parameter in the request
|
|
251
|
+
alias_val = meta.get("AliasAs")
|
|
252
|
+
alias = alias_val if isinstance(alias_val, str) else param.name
|
|
253
|
+
|
|
254
|
+
# [Header("X-Custom")] → explicit header parameter
|
|
255
|
+
if "Header" in meta:
|
|
256
|
+
header_name = meta["Header"] if isinstance(meta["Header"], str) else param.name
|
|
257
|
+
header_params.append(
|
|
258
|
+
ExtractedParameter(
|
|
259
|
+
name=header_name,
|
|
260
|
+
location=ParameterLocation.HEADER,
|
|
261
|
+
type_annotation=param.type_annotation,
|
|
262
|
+
required=True,
|
|
263
|
+
code_location=param.location,
|
|
264
|
+
)
|
|
265
|
+
)
|
|
266
|
+
continue
|
|
267
|
+
|
|
268
|
+
# [Body] → request body
|
|
269
|
+
if "Body" in meta:
|
|
270
|
+
if body is None:
|
|
271
|
+
content_type = "multipart/form-data" if is_multipart else "application/json"
|
|
272
|
+
body = ExtractedBody(
|
|
273
|
+
content_type=content_type,
|
|
274
|
+
model_name=param.type_annotation,
|
|
275
|
+
required=True,
|
|
276
|
+
)
|
|
277
|
+
continue
|
|
278
|
+
|
|
279
|
+
# Path parameter: param name (or alias) matches a {template} segment
|
|
280
|
+
if alias in path_tpl_names or param.name in path_tpl_names:
|
|
281
|
+
matched_name = param.name if param.name in path_tpl_names else alias
|
|
282
|
+
path_params.append(
|
|
283
|
+
ExtractedParameter(
|
|
284
|
+
name=matched_name,
|
|
285
|
+
location=ParameterLocation.PATH,
|
|
286
|
+
type_annotation=param.type_annotation,
|
|
287
|
+
required=True,
|
|
288
|
+
code_location=param.location,
|
|
289
|
+
)
|
|
290
|
+
)
|
|
291
|
+
continue
|
|
292
|
+
|
|
293
|
+
# [Query] or [AliasAs] → explicit query param
|
|
294
|
+
if "Query" in meta or "AliasAs" in meta:
|
|
295
|
+
query_params.append(
|
|
296
|
+
ExtractedParameter(
|
|
297
|
+
name=alias,
|
|
298
|
+
location=ParameterLocation.QUERY,
|
|
299
|
+
type_annotation=param.type_annotation,
|
|
300
|
+
required=param.default_value is None and param.default_value != "null",
|
|
301
|
+
default_value=param.default_value,
|
|
302
|
+
code_location=param.location,
|
|
303
|
+
)
|
|
304
|
+
)
|
|
305
|
+
continue
|
|
306
|
+
|
|
307
|
+
# Undecorated: scalar → query, complex → body
|
|
308
|
+
if self._is_scalar(param.type_annotation):
|
|
309
|
+
query_params.append(
|
|
310
|
+
ExtractedParameter(
|
|
311
|
+
name=param.name,
|
|
312
|
+
location=ParameterLocation.QUERY,
|
|
313
|
+
type_annotation=param.type_annotation,
|
|
314
|
+
required=param.default_value is None and param.default_value != "null",
|
|
315
|
+
default_value=param.default_value,
|
|
316
|
+
code_location=param.location,
|
|
317
|
+
)
|
|
318
|
+
)
|
|
319
|
+
else:
|
|
320
|
+
if body is None:
|
|
321
|
+
content_type = "multipart/form-data" if is_multipart else "application/json"
|
|
322
|
+
body = ExtractedBody(
|
|
323
|
+
content_type=content_type,
|
|
324
|
+
model_name=param.type_annotation,
|
|
325
|
+
required=True,
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
return ExtractedRoute(
|
|
329
|
+
method=http_method,
|
|
330
|
+
path=path_template,
|
|
331
|
+
handler_function=_str_to_qname(f"{cls.name}.{method.name}"),
|
|
332
|
+
handler_location=method.location,
|
|
333
|
+
path_params=path_params,
|
|
334
|
+
query_params=query_params,
|
|
335
|
+
header_params=header_params,
|
|
336
|
+
cookie_params=[],
|
|
337
|
+
body=body,
|
|
338
|
+
response=ExtractedResponse(
|
|
339
|
+
status_code=200,
|
|
340
|
+
model_name=self._extract_return_type(method),
|
|
341
|
+
),
|
|
342
|
+
tags=["refit"],
|
|
343
|
+
dependency_refs=[],
|
|
344
|
+
confidence=Confidence.HIGH,
|
|
345
|
+
kind="refit_client",
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
# -------------------------------------------------------------------------
|
|
349
|
+
# Helpers
|
|
350
|
+
# -------------------------------------------------------------------------
|
|
351
|
+
|
|
352
|
+
def _is_scalar(self, type_annotation: str | None) -> bool:
|
|
353
|
+
if not type_annotation:
|
|
354
|
+
return True
|
|
355
|
+
base = type_annotation.split("<")[0].strip().rstrip("?")
|
|
356
|
+
return base in _SCALAR_TYPES
|
|
357
|
+
|
|
358
|
+
def _extract_return_type(self, method: ParsedFunction) -> str | None:
|
|
359
|
+
"""
|
|
360
|
+
Extract the inner model type from Task<T> or IObservable<T> return.
|
|
361
|
+
e.g. Task<List<User>> → List<User>, Task<User> → User, Task → None
|
|
362
|
+
"""
|
|
363
|
+
ret = method.return_type or ""
|
|
364
|
+
# Unwrap Task<> / IObservable<> / ValueTask<>
|
|
365
|
+
for wrapper in ("Task<", "IObservable<", "ValueTask<", "ApiResponse<"):
|
|
366
|
+
if ret.startswith(wrapper) and ret.endswith(">"):
|
|
367
|
+
inner = ret[len(wrapper) : -1].strip()
|
|
368
|
+
return inner if inner else None
|
|
369
|
+
return None
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
# Self-registration
|
|
373
|
+
_refit_plugin = RefitPlugin()
|
|
374
|
+
FrameworkPluginRegistry.register(_refit_plugin)
|