archapi 0.3.0__tar.gz
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.
- archapi-0.3.0/LICENSE +21 -0
- archapi-0.3.0/PKG-INFO +79 -0
- archapi-0.3.0/README.md +56 -0
- archapi-0.3.0/archapi/__init__.py +3 -0
- archapi-0.3.0/archapi/core.py +295 -0
- archapi-0.3.0/archapi/frameworks/__init__.py +0 -0
- archapi-0.3.0/archapi/frameworks/base.py +56 -0
- archapi-0.3.0/archapi/frameworks/detector.py +113 -0
- archapi-0.3.0/archapi/frameworks/express_ts/__init__.py +0 -0
- archapi-0.3.0/archapi/frameworks/express_ts/adapter.py +237 -0
- archapi-0.3.0/archapi/frameworks/fastapi_adapter.py +184 -0
- archapi-0.3.0/archapi/frameworks/generic.py +211 -0
- archapi-0.3.0/archapi/frameworks/registry.py +28 -0
- archapi-0.3.0/archapi/generation/__init__.py +0 -0
- archapi-0.3.0/archapi/genome/__init__.py +0 -0
- archapi-0.3.0/archapi/indexing/__init__.py +0 -0
- archapi-0.3.0/archapi/indexing/cache.py +141 -0
- archapi-0.3.0/archapi/mapping/__init__.py +0 -0
- archapi-0.3.0/archapi/planning/__init__.py +0 -0
- archapi-0.3.0/archapi/planning/intent_planner.py +175 -0
- archapi-0.3.0/archapi/planning/task_dag.py +81 -0
- archapi-0.3.0/archapi/scanner/__init__.py +0 -0
- archapi-0.3.0/archapi/security/__init__.py +0 -0
- archapi-0.3.0/archapi/security/context_redactor.py +38 -0
- archapi-0.3.0/archapi/security/policy_gate.py +70 -0
- archapi-0.3.0/archapi/security/secret_scanner.py +90 -0
- archapi-0.3.0/archapi/types.py +103 -0
- archapi-0.3.0/archapi/validation/__init__.py +0 -0
- archapi-0.3.0/archapi/validation/architecture_score.py +77 -0
- archapi-0.3.0/archapi/validation/basic_validators.py +38 -0
- archapi-0.3.0/archapi/validation/command_validator.py +146 -0
- archapi-0.3.0/archapi.egg-info/PKG-INFO +79 -0
- archapi-0.3.0/archapi.egg-info/SOURCES.txt +36 -0
- archapi-0.3.0/archapi.egg-info/dependency_links.txt +1 -0
- archapi-0.3.0/archapi.egg-info/top_level.txt +1 -0
- archapi-0.3.0/pyproject.toml +36 -0
- archapi-0.3.0/setup.cfg +4 -0
- archapi-0.3.0/tests/test_archapi_suite.py +118 -0
archapi-0.3.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rohith Chikkala
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files, to deal in the Software
|
|
7
|
+
without restriction, including without limitation the rights to use, copy,
|
|
8
|
+
modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
|
9
|
+
Software, and to permit persons to whom the Software is furnished to do so,
|
|
10
|
+
subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
archapi-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: archapi
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Architecture-preserving REST API synthesis library
|
|
5
|
+
Author: ArchAPI Research Team
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/rohith5005/archapi
|
|
8
|
+
Project-URL: Repository, https://github.com/rohith5005/archapi
|
|
9
|
+
Project-URL: Issues, https://github.com/rohith5005/archapi/issues
|
|
10
|
+
Keywords: rest-api,code-generation,architecture,express,fastapi
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# ArchAPI
|
|
25
|
+
|
|
26
|
+
ArchAPI is a Python library for architecture-preserving REST API generation.
|
|
27
|
+
|
|
28
|
+
It scans an existing backend project, detects the framework, understands the project structure, plans a REST API, generates framework-specific files, validates the output, and writes files only when explicitly requested.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Current Status
|
|
33
|
+
|
|
34
|
+
Current checkpoint: **Phase 3 complete**
|
|
35
|
+
|
|
36
|
+
Completed:
|
|
37
|
+
|
|
38
|
+
- Functional Python package
|
|
39
|
+
- Express TypeScript adapter
|
|
40
|
+
- FastAPI adapter
|
|
41
|
+
- Generic fallback adapter
|
|
42
|
+
- Framework detection
|
|
43
|
+
- Project scanning
|
|
44
|
+
- API architecture model
|
|
45
|
+
- Confidence scoring
|
|
46
|
+
- Low-confidence blocking
|
|
47
|
+
- Strict config mode
|
|
48
|
+
- REST intent planner
|
|
49
|
+
- Code generation
|
|
50
|
+
- Dry-run generation
|
|
51
|
+
- Safe apply
|
|
52
|
+
- Overwrite protection
|
|
53
|
+
- Cache and changed-file detection
|
|
54
|
+
- Secret scanner
|
|
55
|
+
- Context redaction
|
|
56
|
+
- Policy gate
|
|
57
|
+
- Architecture consistency score
|
|
58
|
+
- Command validation
|
|
59
|
+
- Unified regression test suite
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Supported Frameworks
|
|
64
|
+
|
|
65
|
+
Dedicated generation support currently exists for:
|
|
66
|
+
|
|
67
|
+
- Express TypeScript
|
|
68
|
+
- FastAPI
|
|
69
|
+
|
|
70
|
+
Other frameworks may be detected, but they currently use the generic fallback adapter.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Installation
|
|
75
|
+
|
|
76
|
+
From the project root:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pip install -e .
|
archapi-0.3.0/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# ArchAPI
|
|
2
|
+
|
|
3
|
+
ArchAPI is a Python library for architecture-preserving REST API generation.
|
|
4
|
+
|
|
5
|
+
It scans an existing backend project, detects the framework, understands the project structure, plans a REST API, generates framework-specific files, validates the output, and writes files only when explicitly requested.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Current Status
|
|
10
|
+
|
|
11
|
+
Current checkpoint: **Phase 3 complete**
|
|
12
|
+
|
|
13
|
+
Completed:
|
|
14
|
+
|
|
15
|
+
- Functional Python package
|
|
16
|
+
- Express TypeScript adapter
|
|
17
|
+
- FastAPI adapter
|
|
18
|
+
- Generic fallback adapter
|
|
19
|
+
- Framework detection
|
|
20
|
+
- Project scanning
|
|
21
|
+
- API architecture model
|
|
22
|
+
- Confidence scoring
|
|
23
|
+
- Low-confidence blocking
|
|
24
|
+
- Strict config mode
|
|
25
|
+
- REST intent planner
|
|
26
|
+
- Code generation
|
|
27
|
+
- Dry-run generation
|
|
28
|
+
- Safe apply
|
|
29
|
+
- Overwrite protection
|
|
30
|
+
- Cache and changed-file detection
|
|
31
|
+
- Secret scanner
|
|
32
|
+
- Context redaction
|
|
33
|
+
- Policy gate
|
|
34
|
+
- Architecture consistency score
|
|
35
|
+
- Command validation
|
|
36
|
+
- Unified regression test suite
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Supported Frameworks
|
|
41
|
+
|
|
42
|
+
Dedicated generation support currently exists for:
|
|
43
|
+
|
|
44
|
+
- Express TypeScript
|
|
45
|
+
- FastAPI
|
|
46
|
+
|
|
47
|
+
Other frameworks may be detected, but they currently use the generic fallback adapter.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
From the project root:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install -e .
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Dict, Optional, Union
|
|
5
|
+
|
|
6
|
+
from archapi.frameworks.detector import FrameworkDetector
|
|
7
|
+
from archapi.frameworks.registry import FrameworkRegistry
|
|
8
|
+
from archapi.indexing.cache import CacheManager
|
|
9
|
+
from archapi.security.secret_scanner import SecretScanner
|
|
10
|
+
from archapi.security.context_redactor import ContextRedactor
|
|
11
|
+
from archapi.security.policy_gate import PolicyGate
|
|
12
|
+
from archapi.validation.architecture_score import ArchitectureConsistencyScorer
|
|
13
|
+
from archapi.validation.command_validator import CommandValidator
|
|
14
|
+
from archapi.types import (
|
|
15
|
+
APIGenome,
|
|
16
|
+
APIPlan,
|
|
17
|
+
DetectionResult,
|
|
18
|
+
GenerationResult,
|
|
19
|
+
ScanResult,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ArchAPI:
|
|
24
|
+
def __init__(self, project_path: Union[str, Path], framework: Optional[str] = None, config: Optional[Dict[str, Any]] = None):
|
|
25
|
+
self.project_path = Path(project_path).resolve()
|
|
26
|
+
if not self.project_path.exists():
|
|
27
|
+
raise FileNotFoundError(f"Project path does not exist: {self.project_path}")
|
|
28
|
+
|
|
29
|
+
self._framework_override = framework
|
|
30
|
+
self._config = config or {}
|
|
31
|
+
self._detector = FrameworkDetector()
|
|
32
|
+
self._registry = FrameworkRegistry()
|
|
33
|
+
self._cache = CacheManager(self.project_path)
|
|
34
|
+
self._secret_scanner = SecretScanner(self.project_path)
|
|
35
|
+
self._context_redactor = ContextRedactor()
|
|
36
|
+
self._policy_gate = PolicyGate()
|
|
37
|
+
self._architecture_scorer = ArchitectureConsistencyScorer()
|
|
38
|
+
self._command_validator = CommandValidator(self.project_path)
|
|
39
|
+
|
|
40
|
+
self._detection: Optional[DetectionResult] = None
|
|
41
|
+
self._scan: Optional[ScanResult] = None
|
|
42
|
+
self._maps: Optional[Dict[str, Any]] = None
|
|
43
|
+
self._genome: Optional[APIGenome] = None
|
|
44
|
+
|
|
45
|
+
def detect_framework(self) -> DetectionResult:
|
|
46
|
+
if self._framework_override:
|
|
47
|
+
self._detection = DetectionResult(
|
|
48
|
+
framework=self._framework_override,
|
|
49
|
+
confidence=1.0,
|
|
50
|
+
reasons=["Framework explicitly provided"],
|
|
51
|
+
)
|
|
52
|
+
return self._detection
|
|
53
|
+
|
|
54
|
+
self._detection = self._detector.detect(self.project_path)
|
|
55
|
+
return self._detection
|
|
56
|
+
|
|
57
|
+
def _adapter(self):
|
|
58
|
+
detection = self._detection or self.detect_framework()
|
|
59
|
+
return self._registry.get(detection.framework)
|
|
60
|
+
|
|
61
|
+
def _has_config_hints(self) -> bool:
|
|
62
|
+
hint_keys = {
|
|
63
|
+
"route_dir",
|
|
64
|
+
"controller_dir",
|
|
65
|
+
"service_dir",
|
|
66
|
+
"model_dir",
|
|
67
|
+
"schema_dir",
|
|
68
|
+
"middleware_dir",
|
|
69
|
+
"test_dir",
|
|
70
|
+
}
|
|
71
|
+
return any(key in self._config for key in hint_keys)
|
|
72
|
+
|
|
73
|
+
def scan(self) -> ScanResult:
|
|
74
|
+
detection = self._detection or self.detect_framework()
|
|
75
|
+
|
|
76
|
+
# Strict config mode:
|
|
77
|
+
# If user provides architecture hints, scan ONLY those hinted directories.
|
|
78
|
+
# This prevents accidental scanning of the library repo, sample projects,
|
|
79
|
+
# caches, or unrelated test files.
|
|
80
|
+
if self._has_config_hints():
|
|
81
|
+
self._scan = ScanResult(
|
|
82
|
+
framework=detection.framework,
|
|
83
|
+
project_path=self.project_path,
|
|
84
|
+
)
|
|
85
|
+
self._apply_config_hints_to_scan(self._scan)
|
|
86
|
+
return self._scan
|
|
87
|
+
|
|
88
|
+
adapter = self._adapter()
|
|
89
|
+
self._scan = adapter.scan(self.project_path)
|
|
90
|
+
self._scan.framework = detection.framework
|
|
91
|
+
return self._scan
|
|
92
|
+
|
|
93
|
+
def _apply_config_hints_to_scan(self, scan: ScanResult) -> None:
|
|
94
|
+
"""
|
|
95
|
+
Applies user-provided architecture hints.
|
|
96
|
+
|
|
97
|
+
Supported config keys:
|
|
98
|
+
- route_dir
|
|
99
|
+
- controller_dir
|
|
100
|
+
- service_dir
|
|
101
|
+
- model_dir
|
|
102
|
+
- schema_dir
|
|
103
|
+
- middleware_dir
|
|
104
|
+
- test_dir
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
hint_map = {
|
|
108
|
+
"route_dir": scan.routes,
|
|
109
|
+
"controller_dir": scan.controllers,
|
|
110
|
+
"service_dir": scan.services,
|
|
111
|
+
"model_dir": scan.models,
|
|
112
|
+
"schema_dir": scan.schemas,
|
|
113
|
+
"middleware_dir": scan.middleware,
|
|
114
|
+
"test_dir": scan.tests,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
ignored_parts = {
|
|
118
|
+
".git",
|
|
119
|
+
".venv",
|
|
120
|
+
"node_modules",
|
|
121
|
+
"dist",
|
|
122
|
+
"build",
|
|
123
|
+
"coverage",
|
|
124
|
+
"__pycache__",
|
|
125
|
+
".archapi",
|
|
126
|
+
"archapi.egg-info",
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
for config_key, target_list in hint_map.items():
|
|
130
|
+
raw_dir = self._config.get(config_key)
|
|
131
|
+
if not raw_dir:
|
|
132
|
+
continue
|
|
133
|
+
|
|
134
|
+
hint_path = (self.project_path / raw_dir).resolve()
|
|
135
|
+
|
|
136
|
+
if not hint_path.exists() or not hint_path.is_dir():
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
for file_path in hint_path.rglob("*"):
|
|
140
|
+
if not file_path.is_file():
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
rel_parts = file_path.relative_to(self.project_path).parts
|
|
145
|
+
except ValueError:
|
|
146
|
+
rel_parts = file_path.parts
|
|
147
|
+
|
|
148
|
+
if any(part in ignored_parts for part in rel_parts):
|
|
149
|
+
continue
|
|
150
|
+
|
|
151
|
+
if file_path not in target_list:
|
|
152
|
+
target_list.append(file_path)
|
|
153
|
+
|
|
154
|
+
def config(self) -> Dict[str, Any]:
|
|
155
|
+
return dict(self._config)
|
|
156
|
+
|
|
157
|
+
def build_maps(self) -> Dict[str, Any]:
|
|
158
|
+
scan = self._scan or self.scan()
|
|
159
|
+
adapter = self._adapter()
|
|
160
|
+
self._maps = adapter.build_maps(scan)
|
|
161
|
+
return self._maps
|
|
162
|
+
|
|
163
|
+
def extract_genome(self) -> APIGenome:
|
|
164
|
+
scan = self._scan or self.scan()
|
|
165
|
+
maps = self._maps or self.build_maps()
|
|
166
|
+
adapter = self._adapter()
|
|
167
|
+
self._genome = adapter.extract_genome(maps, scan)
|
|
168
|
+
self._genome.framework = (self._detection or self.detect_framework()).framework
|
|
169
|
+
return self._genome
|
|
170
|
+
|
|
171
|
+
def compute_confidence(self) -> Dict[str, Any]:
|
|
172
|
+
detection = self._detection or self.detect_framework()
|
|
173
|
+
genome = self._genome or self.extract_genome()
|
|
174
|
+
|
|
175
|
+
missing = []
|
|
176
|
+
|
|
177
|
+
if genome.route_style == "unknown":
|
|
178
|
+
missing.append("route style")
|
|
179
|
+
if genome.controller_style == "unknown":
|
|
180
|
+
missing.append("controller style")
|
|
181
|
+
if genome.service_style == "unknown":
|
|
182
|
+
missing.append("service style")
|
|
183
|
+
if genome.schema_style == "unknown":
|
|
184
|
+
missing.append("schema style")
|
|
185
|
+
|
|
186
|
+
# Overall confidence should consider BOTH:
|
|
187
|
+
# 1. framework detection confidence
|
|
188
|
+
# 2. API architecture/genome confidence
|
|
189
|
+
#
|
|
190
|
+
# This prevents generic/unknown projects from being treated as safe
|
|
191
|
+
# just because some folders accidentally look like routes/services.
|
|
192
|
+
overall = round(min(detection.confidence, genome.confidence), 2)
|
|
193
|
+
|
|
194
|
+
mode = "generate"
|
|
195
|
+
|
|
196
|
+
if detection.framework in {"generic", "node-unknown"}:
|
|
197
|
+
mode = "blocked"
|
|
198
|
+
elif overall < 0.30:
|
|
199
|
+
mode = "blocked"
|
|
200
|
+
elif overall < 0.55:
|
|
201
|
+
mode = "plan_only"
|
|
202
|
+
elif overall < 0.75:
|
|
203
|
+
mode = "generate_with_warnings"
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
"overall": overall,
|
|
207
|
+
"detection_confidence": detection.confidence,
|
|
208
|
+
"genome_confidence": genome.confidence,
|
|
209
|
+
"mode": mode,
|
|
210
|
+
"missing": missing,
|
|
211
|
+
"framework": genome.framework,
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
def plan_api(self, request: str) -> APIPlan:
|
|
215
|
+
maps = self._maps or self.build_maps()
|
|
216
|
+
genome = self._genome or self.extract_genome()
|
|
217
|
+
adapter = self._adapter()
|
|
218
|
+
|
|
219
|
+
plan = adapter.plan_api(request, genome, maps)
|
|
220
|
+
confidence = self.compute_confidence()
|
|
221
|
+
|
|
222
|
+
if confidence["mode"] in {"blocked", "plan_only"}:
|
|
223
|
+
plan.generation_allowed = False
|
|
224
|
+
|
|
225
|
+
if confidence["framework"] in {"generic", "node-unknown"}:
|
|
226
|
+
plan.reason = (
|
|
227
|
+
"Framework could not be confidently detected. "
|
|
228
|
+
"Provide framework or config before generation."
|
|
229
|
+
)
|
|
230
|
+
else:
|
|
231
|
+
plan.reason = (
|
|
232
|
+
"Architecture confidence too low for code generation. "
|
|
233
|
+
f"Missing: {', '.join(confidence['missing']) or 'unknown'}"
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
return plan
|
|
237
|
+
|
|
238
|
+
def save_cache(self) -> Dict[str, Path]:
|
|
239
|
+
detection = self._detection or self.detect_framework()
|
|
240
|
+
scan = self._scan or self.scan()
|
|
241
|
+
maps = self._maps or self.build_maps()
|
|
242
|
+
genome = self._genome or self.extract_genome()
|
|
243
|
+
|
|
244
|
+
return self._cache.save_snapshot(
|
|
245
|
+
detection=detection,
|
|
246
|
+
scan=scan,
|
|
247
|
+
maps=maps,
|
|
248
|
+
genome=genome,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
def changed_files(self) -> list:
|
|
252
|
+
return self._cache.changed_files()
|
|
253
|
+
|
|
254
|
+
def scan_secrets(self):
|
|
255
|
+
return self._secret_scanner.scan()
|
|
256
|
+
|
|
257
|
+
def redact_context(self, text: str) -> str:
|
|
258
|
+
return self._context_redactor.redact(text)
|
|
259
|
+
|
|
260
|
+
def validate_policy(self, result: GenerationResult):
|
|
261
|
+
return self._policy_gate.validate_result(result)
|
|
262
|
+
|
|
263
|
+
def score_architecture(self, result: GenerationResult):
|
|
264
|
+
genome = self._genome or self.extract_genome()
|
|
265
|
+
return self._architecture_scorer.score(result.files, genome)
|
|
266
|
+
|
|
267
|
+
def validate_project_commands(self):
|
|
268
|
+
detection = self._detection or self.detect_framework()
|
|
269
|
+
|
|
270
|
+
if detection.framework in {"express-typescript", "nestjs", "node-unknown"}:
|
|
271
|
+
return self._command_validator.validate_node_project()
|
|
272
|
+
|
|
273
|
+
return self._command_validator.validate_node_project()
|
|
274
|
+
|
|
275
|
+
def generate_api(self, request: str, dry_run: bool = True) -> GenerationResult:
|
|
276
|
+
maps = self._maps or self.build_maps()
|
|
277
|
+
genome = self._genome or self.extract_genome()
|
|
278
|
+
plan = self.plan_api(request)
|
|
279
|
+
|
|
280
|
+
adapter = self._adapter()
|
|
281
|
+
files = adapter.generate_code(plan, genome, maps)
|
|
282
|
+
report = adapter.validate_generated_code(files, plan, genome)
|
|
283
|
+
|
|
284
|
+
result = GenerationResult(
|
|
285
|
+
project_path=self.project_path,
|
|
286
|
+
plan=plan,
|
|
287
|
+
files=files,
|
|
288
|
+
validation_report=report,
|
|
289
|
+
warnings=report.warnings,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
if not dry_run and report.success:
|
|
293
|
+
result.apply()
|
|
294
|
+
|
|
295
|
+
return result
|
|
File without changes
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Dict, List
|
|
6
|
+
|
|
7
|
+
from archapi.types import (
|
|
8
|
+
APIPlan,
|
|
9
|
+
APIGenome,
|
|
10
|
+
DetectionResult,
|
|
11
|
+
GeneratedFile,
|
|
12
|
+
ScanResult,
|
|
13
|
+
ValidationReport,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FrameworkAdapter(ABC):
|
|
18
|
+
name: str = "unknown"
|
|
19
|
+
|
|
20
|
+
@abstractmethod
|
|
21
|
+
def detect(self, project_path: Path) -> DetectionResult:
|
|
22
|
+
raise NotImplementedError
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def scan(self, project_path: Path) -> ScanResult:
|
|
26
|
+
raise NotImplementedError
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def build_maps(self, scan_result: ScanResult) -> Dict[str, Any]:
|
|
30
|
+
raise NotImplementedError
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def extract_genome(self, maps: Dict[str, Any], scan_result: ScanResult) -> APIGenome:
|
|
34
|
+
raise NotImplementedError
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def plan_api(self, request: str, genome: APIGenome, maps: Dict[str, Any]) -> APIPlan:
|
|
38
|
+
raise NotImplementedError
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def generate_code(
|
|
42
|
+
self,
|
|
43
|
+
plan: APIPlan,
|
|
44
|
+
genome: APIGenome,
|
|
45
|
+
maps: Dict[str, Any],
|
|
46
|
+
) -> List[GeneratedFile]:
|
|
47
|
+
raise NotImplementedError
|
|
48
|
+
|
|
49
|
+
@abstractmethod
|
|
50
|
+
def validate_generated_code(
|
|
51
|
+
self,
|
|
52
|
+
files: List[GeneratedFile],
|
|
53
|
+
plan: APIPlan,
|
|
54
|
+
genome: APIGenome,
|
|
55
|
+
) -> ValidationReport:
|
|
56
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
from archapi.types import DetectionResult
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FrameworkDetector:
|
|
11
|
+
def detect(self, project_path: Path) -> DetectionResult:
|
|
12
|
+
candidates: List[DetectionResult] = []
|
|
13
|
+
|
|
14
|
+
package_json = project_path / "package.json"
|
|
15
|
+
if package_json.exists():
|
|
16
|
+
try:
|
|
17
|
+
data = json.loads(package_json.read_text(encoding="utf-8"))
|
|
18
|
+
deps = {}
|
|
19
|
+
deps.update(data.get("dependencies", {}))
|
|
20
|
+
deps.update(data.get("devDependencies", {}))
|
|
21
|
+
|
|
22
|
+
if "@nestjs/core" in deps:
|
|
23
|
+
candidates.append(
|
|
24
|
+
DetectionResult("nestjs", 0.95, ["package.json contains @nestjs/core"])
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
if "express" in deps:
|
|
28
|
+
candidates.append(
|
|
29
|
+
DetectionResult("express-typescript", 0.85, ["package.json contains express"])
|
|
30
|
+
)
|
|
31
|
+
except Exception:
|
|
32
|
+
candidates.append(
|
|
33
|
+
DetectionResult("node-unknown", 0.30, ["package.json exists but could not be parsed"])
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
if (project_path / "manage.py").exists():
|
|
37
|
+
candidates.append(
|
|
38
|
+
DetectionResult("django-drf", 0.75, ["manage.py detected"])
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
py_text = ""
|
|
42
|
+
pyproject = project_path / "pyproject.toml"
|
|
43
|
+
requirements = project_path / "requirements.txt"
|
|
44
|
+
|
|
45
|
+
if pyproject.exists():
|
|
46
|
+
py_text += pyproject.read_text(encoding="utf-8", errors="ignore").lower()
|
|
47
|
+
|
|
48
|
+
if requirements.exists():
|
|
49
|
+
py_text += requirements.read_text(encoding="utf-8", errors="ignore").lower()
|
|
50
|
+
|
|
51
|
+
if "fastapi" in py_text:
|
|
52
|
+
candidates.append(
|
|
53
|
+
DetectionResult("fastapi", 0.90, ["fastapi dependency detected"])
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
if "flask" in py_text:
|
|
57
|
+
candidates.append(
|
|
58
|
+
DetectionResult("flask", 0.85, ["flask dependency detected"])
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if (project_path / "pom.xml").exists() or (project_path / "build.gradle").exists():
|
|
62
|
+
candidates.append(
|
|
63
|
+
DetectionResult("spring-boot", 0.70, ["Java build file detected"])
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
if list(project_path.glob("*.csproj")):
|
|
67
|
+
candidates.append(
|
|
68
|
+
DetectionResult("dotnet-core", 0.75, [".csproj file detected"])
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if (project_path / "composer.json").exists():
|
|
72
|
+
composer = (project_path / "composer.json").read_text(
|
|
73
|
+
encoding="utf-8",
|
|
74
|
+
errors="ignore",
|
|
75
|
+
).lower()
|
|
76
|
+
if "laravel" in composer:
|
|
77
|
+
candidates.append(
|
|
78
|
+
DetectionResult("laravel", 0.90, ["composer.json contains laravel"])
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
if (project_path / "Gemfile").exists():
|
|
82
|
+
gemfile = (project_path / "Gemfile").read_text(
|
|
83
|
+
encoding="utf-8",
|
|
84
|
+
errors="ignore",
|
|
85
|
+
).lower()
|
|
86
|
+
if "rails" in gemfile:
|
|
87
|
+
candidates.append(
|
|
88
|
+
DetectionResult("rails", 0.90, ["Gemfile contains rails"])
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
if (project_path / "go.mod").exists():
|
|
92
|
+
go_mod = (project_path / "go.mod").read_text(
|
|
93
|
+
encoding="utf-8",
|
|
94
|
+
errors="ignore",
|
|
95
|
+
).lower()
|
|
96
|
+
|
|
97
|
+
if "gin-gonic/gin" in go_mod or "gofiber/fiber" in go_mod:
|
|
98
|
+
candidates.append(
|
|
99
|
+
DetectionResult("go-api", 0.85, ["go.mod contains Gin or Fiber"])
|
|
100
|
+
)
|
|
101
|
+
else:
|
|
102
|
+
candidates.append(
|
|
103
|
+
DetectionResult("go-api", 0.55, ["go.mod detected"])
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
if not candidates:
|
|
107
|
+
return DetectionResult(
|
|
108
|
+
framework="generic",
|
|
109
|
+
confidence=0.10,
|
|
110
|
+
reasons=["No known framework markers detected"],
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
return sorted(candidates, key=lambda item: item.confidence, reverse=True)[0]
|
|
File without changes
|