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 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)