specforge-api 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.
- specforge/__init__.py +23 -0
- specforge/adapters/fastapi.py +101 -0
- specforge/adapters/flask.py +78 -0
- specforge/assets/index.html +2995 -0
- specforge/cli/main.py +153 -0
- specforge/config.py +42 -0
- specforge/parser/models.py +131 -0
- specforge/parser/openapi3.py +285 -0
- specforge/parser/resolver.py +101 -0
- specforge/testing/exporters/pytest_export.py +89 -0
- specforge/testing/fuzzers/array_fuzzer.py +56 -0
- specforge/testing/fuzzers/boolean_fuzzer.py +38 -0
- specforge/testing/fuzzers/numeric_fuzzer.py +92 -0
- specforge/testing/fuzzers/object_fuzzer.py +99 -0
- specforge/testing/fuzzers/string_fuzzer.py +157 -0
- specforge/testing/invariants.py +170 -0
- specforge/testing/planner.py +155 -0
- specforge/utils/html_injector.py +43 -0
- specforge_api-0.1.0.dist-info/METADATA +260 -0
- specforge_api-0.1.0.dist-info/RECORD +23 -0
- specforge_api-0.1.0.dist-info/WHEEL +4 -0
- specforge_api-0.1.0.dist-info/entry_points.txt +2 -0
- specforge_api-0.1.0.dist-info/licenses/LICENSE +21 -0
specforge/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""SpecForge: The Interactive API Client & Automated Contract Testing Engine for Python."""
|
|
2
|
+
|
|
3
|
+
from specforge.config import SpecForgeConfig
|
|
4
|
+
from specforge.parser.openapi3 import OpenAPIParser
|
|
5
|
+
from specforge.testing.exporters.pytest_export import PytestExporter
|
|
6
|
+
from specforge.testing.planner import TestPlanner
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
|
|
10
|
+
# Conditionally import FastAPI adapter if available
|
|
11
|
+
try:
|
|
12
|
+
from specforge.adapters.fastapi import SpecForge
|
|
13
|
+
except ImportError:
|
|
14
|
+
SpecForge = None # type: ignore
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"SpecForge",
|
|
18
|
+
"SpecForgeConfig",
|
|
19
|
+
"OpenAPIParser",
|
|
20
|
+
"TestPlanner",
|
|
21
|
+
"PytestExporter",
|
|
22
|
+
"__version__",
|
|
23
|
+
]
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""FastAPI adapter for SpecForge interactive documentation and contract testing."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
from fastapi import FastAPI, Request, Response
|
|
7
|
+
from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse
|
|
8
|
+
except ImportError:
|
|
9
|
+
FastAPI = None # type: ignore
|
|
10
|
+
|
|
11
|
+
from specforge.config import SpecForgeConfig
|
|
12
|
+
from specforge.parser.openapi3 import OpenAPIParser
|
|
13
|
+
from specforge.testing.exporters.pytest_export import PytestExporter
|
|
14
|
+
from specforge.testing.planner import TestPlanner
|
|
15
|
+
from specforge.utils.html_injector import render_html
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SpecForge:
|
|
19
|
+
"""Mounts the SpecForge interactive client and boundary test suite onto a FastAPI application."""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
app: Any,
|
|
24
|
+
path: str = "/docs",
|
|
25
|
+
title: str = "SpecForge API Studio",
|
|
26
|
+
spec_url: Optional[str] = None,
|
|
27
|
+
theme: str = "dark",
|
|
28
|
+
enable_tests: bool = True,
|
|
29
|
+
disable_default_docs: bool = False,
|
|
30
|
+
**kwargs: Any,
|
|
31
|
+
):
|
|
32
|
+
if FastAPI is None:
|
|
33
|
+
raise ImportError(
|
|
34
|
+
"FastAPI is not installed. Install it with `pip install specforge[fastapi]`."
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
self.app = app
|
|
38
|
+
self.path = path
|
|
39
|
+
self.spec_url = spec_url or getattr(app, "openapi_url", "/openapi.json")
|
|
40
|
+
|
|
41
|
+
self.config = SpecForgeConfig(
|
|
42
|
+
title=title,
|
|
43
|
+
path=path,
|
|
44
|
+
spec_url=self.spec_url,
|
|
45
|
+
theme=theme, # type: ignore
|
|
46
|
+
enable_tests=enable_tests,
|
|
47
|
+
**kwargs,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
if disable_default_docs and hasattr(app, "docs_url"):
|
|
51
|
+
app.docs_url = None
|
|
52
|
+
|
|
53
|
+
self._register_routes()
|
|
54
|
+
|
|
55
|
+
def _register_routes(self) -> None:
|
|
56
|
+
"""Register the documentation UI and test plan endpoints on the FastAPI app."""
|
|
57
|
+
|
|
58
|
+
@self.app.get(self.path, response_class=HTMLResponse, include_in_schema=False)
|
|
59
|
+
async def specforge_docs_endpoint(request: Request) -> Response:
|
|
60
|
+
html = render_html(self.config)
|
|
61
|
+
return HTMLResponse(content=html)
|
|
62
|
+
|
|
63
|
+
@self.app.get(
|
|
64
|
+
"/specforge/api/plan",
|
|
65
|
+
response_class=JSONResponse,
|
|
66
|
+
include_in_schema=False,
|
|
67
|
+
)
|
|
68
|
+
async def specforge_test_plan_endpoint(request: Request) -> Response:
|
|
69
|
+
# Generate or retrieve active OpenAPI specification
|
|
70
|
+
raw_spec: dict[str, Any] = self.app.openapi()
|
|
71
|
+
parser = OpenAPIParser(raw_spec)
|
|
72
|
+
spec_model = parser.parse()
|
|
73
|
+
|
|
74
|
+
plans = []
|
|
75
|
+
for op in spec_model.operations:
|
|
76
|
+
plan = TestPlanner.plan_operation_tests(op)
|
|
77
|
+
plans.append(plan.to_dict())
|
|
78
|
+
|
|
79
|
+
return JSONResponse(content={"plans": plans})
|
|
80
|
+
|
|
81
|
+
@self.app.get(
|
|
82
|
+
"/specforge/api/export-pytest",
|
|
83
|
+
response_class=PlainTextResponse,
|
|
84
|
+
include_in_schema=False,
|
|
85
|
+
)
|
|
86
|
+
async def specforge_export_pytest_endpoint(request: Request) -> Response:
|
|
87
|
+
raw_spec: dict[str, Any] = self.app.openapi()
|
|
88
|
+
parser = OpenAPIParser(raw_spec)
|
|
89
|
+
spec_model = parser.parse()
|
|
90
|
+
|
|
91
|
+
all_test_cases = []
|
|
92
|
+
for op in spec_model.operations:
|
|
93
|
+
plan = TestPlanner.plan_operation_tests(op)
|
|
94
|
+
all_test_cases.extend(plan.test_cases)
|
|
95
|
+
|
|
96
|
+
base_url = str(request.base_url).rstrip("/")
|
|
97
|
+
code = PytestExporter.export(all_test_cases, base_url=base_url)
|
|
98
|
+
return PlainTextResponse(
|
|
99
|
+
content=code,
|
|
100
|
+
headers={"Content-Disposition": 'attachment; filename="test_api_contracts.py"'},
|
|
101
|
+
)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Flask adapter for SpecForge interactive documentation and contract testing."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
from flask import (
|
|
7
|
+
Blueprint,
|
|
8
|
+
Flask,
|
|
9
|
+
Response,
|
|
10
|
+
jsonify,
|
|
11
|
+
)
|
|
12
|
+
except ImportError:
|
|
13
|
+
Flask = None # type: ignore
|
|
14
|
+
|
|
15
|
+
from specforge.config import SpecForgeConfig
|
|
16
|
+
from specforge.parser.openapi3 import OpenAPIParser
|
|
17
|
+
from specforge.testing.planner import TestPlanner
|
|
18
|
+
from specforge.utils.html_injector import render_html
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SpecForge:
|
|
22
|
+
"""Mounts the SpecForge interactive client and boundary test suite onto a Flask application."""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
app: Any,
|
|
27
|
+
path: str = "/docs",
|
|
28
|
+
spec_url: str = "/api/spec.json",
|
|
29
|
+
title: str = "SpecForge API Studio",
|
|
30
|
+
theme: str = "dark",
|
|
31
|
+
enable_tests: bool = True,
|
|
32
|
+
spec_dict: Optional[dict[str, Any]] = None,
|
|
33
|
+
**kwargs: Any,
|
|
34
|
+
):
|
|
35
|
+
if Flask is None:
|
|
36
|
+
raise ImportError("Flask is not installed. Install it with `pip install specforge[flask]`.")
|
|
37
|
+
|
|
38
|
+
self.app = app
|
|
39
|
+
self.path = path
|
|
40
|
+
self.spec_url = spec_url
|
|
41
|
+
self.spec_dict = spec_dict
|
|
42
|
+
|
|
43
|
+
self.config = SpecForgeConfig(
|
|
44
|
+
title=title,
|
|
45
|
+
path=path,
|
|
46
|
+
spec_url=spec_url,
|
|
47
|
+
theme=theme, # type: ignore
|
|
48
|
+
enable_tests=enable_tests,
|
|
49
|
+
**kwargs,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
self._register_blueprint()
|
|
53
|
+
|
|
54
|
+
def _register_blueprint(self) -> None:
|
|
55
|
+
"""Register the Flask blueprint."""
|
|
56
|
+
bp = Blueprint("specforge", __name__)
|
|
57
|
+
|
|
58
|
+
@bp.route(self.path, methods=["GET"])
|
|
59
|
+
def specforge_docs() -> Any:
|
|
60
|
+
html = render_html(self.config, self.spec_dict)
|
|
61
|
+
return Response(html, mimetype="text/html")
|
|
62
|
+
|
|
63
|
+
@bp.route("/specforge/api/plan", methods=["GET"])
|
|
64
|
+
def specforge_plan() -> Any:
|
|
65
|
+
if not self.spec_dict:
|
|
66
|
+
return jsonify({"error": "SpecForge Flask adapter requires spec_dict for test planning."}), 400
|
|
67
|
+
|
|
68
|
+
parser = OpenAPIParser(self.spec_dict)
|
|
69
|
+
spec_model = parser.parse()
|
|
70
|
+
|
|
71
|
+
plans = []
|
|
72
|
+
for op in spec_model.operations:
|
|
73
|
+
plan = TestPlanner.plan_operation_tests(op)
|
|
74
|
+
plans.append(plan.to_dict())
|
|
75
|
+
|
|
76
|
+
return jsonify({"plans": plans})
|
|
77
|
+
|
|
78
|
+
self.app.register_blueprint(bp)
|