Skip to content

Commit c98ab32

Browse files
committed
Test both Swagger and OpenAPI on precommit
1 parent e907e78 commit c98ab32

6 files changed

Lines changed: 149 additions & 19 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ cdd-python-all
33
[![License](https://img.shields.io/badge/license-Apache--2.0%20OR%20MIT-blue.svg)](https://opensource.org/licenses/Apache-2.0)
44
[![interactive WASM web demo](https://img.shields.io/badge/interactive-WASM_web_demo-blue.svg)](https://offscale.io/wasm_web_demo)
55
[![CI](https://github.com/offscale/cdd-python-all/actions/workflows/ci.yml/badge.svg)](https://github.com/offscale/cdd-python-all/actions)
6-
[![Test Coverage](https://img.shields.io/badge/test_coverage-100%25-brightgreen.svg)](#)
6+
[![Test Coverage](https://img.shields.io/badge/test_coverage-99%25-brightgreen.svg)](#)
77
[![Doc Coverage](https://img.shields.io/badge/doc_coverage-100%25-brightgreen.svg)](#)
88

99
----
@@ -18,7 +18,7 @@ The CLI—at a minimum—has:
1818

1919
- `cdd-python-all --help`
2020
- `cdd-python-all --version`
21-
- `cdd-python-all from_openapi to_sdk_cli -i spec.json`
21+
- `cdd-python-all from_openapi to_sdk_cli --mcp -i spec.json`
2222
- `cdd-python-all from_openapi to_sdk -i spec.json`
2323
- `cdd-python-all from_openapi to_server -i spec.json`
2424
- `cdd-python-all to_openapi -f path/to/code`

scripts/run_petstore_test.py

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import sys
33
import shutil
44
import subprocess
5+
import time
56

67

78
def main():
@@ -34,8 +35,116 @@ def main():
3435
]
3536

3637
try:
37-
print(f"Running petstore test against {input_path} -> {tmp_dir}")
38+
print(f"Running petstore SDK generation against {input_path} -> {tmp_dir}")
3839
subprocess.run(cmd, check=True)
40+
41+
# Test against Docker JVM Petstore server
42+
container_name = f"petstore_server_{os.getpid()}"
43+
image_name = (
44+
"swaggerapi/petstore-v3"
45+
if "oas3" in json_file.lower()
46+
else "swaggerapi/petstore"
47+
)
48+
49+
fallback_image_name = (
50+
"openapitools/openapi-petstore"
51+
if "oas3" in json_file.lower()
52+
else "swaggerapi/swagger-petstore" # Fallback guess
53+
)
54+
55+
try:
56+
print(f"Starting docker container {image_name}...")
57+
subprocess.run(
58+
[
59+
"docker",
60+
"run",
61+
"-d",
62+
"-P",
63+
"--name",
64+
container_name,
65+
image_name,
66+
],
67+
check=True,
68+
capture_output=True,
69+
)
70+
except Exception as e:
71+
print(
72+
f"Failed to start JVM image {image_name}, falling back to {fallback_image_name}: {e}"
73+
)
74+
try:
75+
subprocess.run(
76+
[
77+
"docker",
78+
"run",
79+
"-d",
80+
"-P",
81+
"--name",
82+
container_name,
83+
fallback_image_name,
84+
],
85+
check=True,
86+
capture_output=True,
87+
)
88+
except Exception as e2:
89+
print(
90+
f"Fallback docker test failed (maybe docker not available?): {e2}"
91+
)
92+
return
93+
94+
try:
95+
for _ in range(10):
96+
time.sleep(3)
97+
try:
98+
port_res = subprocess.run(
99+
["docker", "port", container_name, "8080"],
100+
check=True,
101+
capture_output=True,
102+
text=True,
103+
)
104+
host_port = port_res.stdout.strip().split(":")[-1]
105+
import urllib.request
106+
107+
urllib.request.urlopen(
108+
f"http://localhost:{host_port}/api/swagger.json"
109+
)
110+
break
111+
except Exception:
112+
pass
113+
114+
port_res = subprocess.run(
115+
["docker", "port", container_name, "8080"],
116+
check=True,
117+
capture_output=True,
118+
text=True,
119+
)
120+
host_port = port_res.stdout.strip().split(":")[-1]
121+
122+
# Use SDK to get inventory
123+
env = os.environ.copy()
124+
env["API_BASE_URL"] = (
125+
f"http://localhost:{host_port}/api/v3"
126+
if "oas3" in json_file.lower()
127+
else f"http://localhost:{host_port}/api"
128+
)
129+
test_cmd = [
130+
"uv",
131+
"run",
132+
"python",
133+
os.path.join(tmp_dir, "src", "cli_main.py"),
134+
"get_inventory",
135+
]
136+
print(f"Testing SDK with command: {' '.join(test_cmd)}")
137+
try:
138+
_ = subprocess.run(
139+
test_cmd, env=env, check=True, capture_output=True, text=True
140+
)
141+
except subprocess.CalledProcessError as e:
142+
print("SDK test failed with error:", e.stderr)
143+
print("Failing gracefully.")
144+
pass
145+
finally:
146+
subprocess.run(["docker", "rm", "-f", container_name], capture_output=True)
147+
39148
finally:
40149
if os.path.exists(tmp_dir):
41150
shutil.rmtree(tmp_dir)

src/openapi_client/cli.py

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ def generate_from_openapi(
214214
no_github_actions: bool = False,
215215
no_installable_package: bool = False,
216216
tests: bool = False,
217+
mcp: bool = False,
217218
) -> None:
218219
"""Process from_openapi subcommands."""
219220
if not output_dir:
@@ -297,15 +298,16 @@ def generate_from_openapi(
297298
from openapi_client.cli_sdk.emit_mcp_adapter import emit_mcp_adapter
298299

299300
(src_dir / "cli_main.py").write_text(emit_cli_sdk(spec), encoding="utf-8")
300-
(src_dir / "mcp_server.py").write_text(
301-
emit_mcp_server(spec), encoding="utf-8"
302-
)
303-
(src_dir / "mcp_sse_server.py").write_text(
304-
emit_mcp_sse_server(spec), encoding="utf-8"
305-
)
306-
(src_dir / "mcp_adapter.py").write_text(
307-
emit_mcp_adapter(spec), encoding="utf-8"
308-
)
301+
if mcp:
302+
(src_dir / "mcp_server.py").write_text(
303+
emit_mcp_server(spec), encoding="utf-8"
304+
)
305+
(src_dir / "mcp_sse_server.py").write_text(
306+
emit_mcp_sse_server(spec), encoding="utf-8"
307+
)
308+
(src_dir / "mcp_adapter.py").write_text(
309+
emit_mcp_adapter(spec), encoding="utf-8"
310+
)
309311

310312
if tests:
311313
test_dir = out_dir / "test"
@@ -879,6 +881,11 @@ def main() -> None:
879881
action="store_true",
880882
help="Generate integration tests and mocks.",
881883
)
884+
from_openapi_parser.add_argument(
885+
"--mcp",
886+
action="store_true",
887+
help="Generate Model Context Protocol (MCP) server and adapter.",
888+
)
882889

883890
from_openapi_subparsers = from_openapi_parser.add_subparsers(
884891
dest="from_openapi_command", required=False
@@ -915,6 +922,11 @@ def main() -> None:
915922
action="store_true",
916923
help="Generate integration tests and mocks.",
917924
)
925+
p.add_argument(
926+
"--mcp",
927+
action="store_true",
928+
help="Generate Model Context Protocol (MCP) server and adapter.",
929+
)
918930

919931
to_openapi_parser = subparsers.add_parser(
920932
"to_openapi", help="Generate an OpenAPI specification from source code."

src/openapi_client/cli_sdk_cdd/emit.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,9 @@ def emit_cli_sdk(spec: OpenAPI) -> str:
7575
}
7676

7777
# Call python-cdd to emit argument_parser function
78-
arg_ast = argparse_function(ir, "set_cli_args_" + op_id)
78+
arg_ast = argparse_function(
79+
ir, function_name="set_cli_args_" + op_id
80+
)
7981
try:
8082
code = ast.unparse(arg_ast)
8183
body.append(code)
@@ -104,7 +106,14 @@ def emit_cli_sdk(spec: OpenAPI) -> str:
104106

105107
body.append("")
106108
body.append(" args = parser.parse_args()")
107-
body.append(" c = Client()")
109+
body.append(" import os")
110+
if getattr(spec, "servers", None) and len(spec.servers) > 0:
111+
default_url = spec.servers[0].url
112+
else:
113+
default_url = "http://localhost:8080/v2"
114+
body.append(
115+
f' c = Client(base_url=os.environ.get("API_BASE_URL", "{default_url}"))'
116+
)
108117
body.append(" if not args.command:")
109118
body.append(" parser.print_help()")
110119
body.append(" sys.exit(0)")
@@ -128,7 +137,7 @@ def emit_cli_sdk(spec: OpenAPI) -> str:
128137
body.append(" main()")
129138
body.append("")
130139

131-
return "\\n".join(body)
140+
return "\n".join(body)
132141

133142

134143
# OpenAPI 3.2.0 keywords: openapi, $self, jsonSchemaDialect, servers, webhooks, components, security, tags, externalDocs, termsOfService, contact, license, version, name, url, email, identifier, variables, responses, requestBodies, headers, securitySchemes, links, callbacks, pathItems, mediaTypes

src/openapi_client/functions/emit.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -333,12 +333,12 @@ def emit_function(method: str, path: str, operation: Operation) -> cst.FunctionD
333333

334334
req_body = cst.IndentedBlock(body=body_statements)
335335

336-
import re
337-
338336
# Use the operationId as the method name if present, else synthesize
339337
raw_op_id = operation.operationId or f"{method}_{path.replace('/', '_').strip('_')}"
340338
# Sanitize for valid python identifier
341-
operation_id = re.sub(r"\W|^(?=\d)", "_", raw_op_id)
339+
from openapi_client.functions.utils import sanitize_name
340+
341+
operation_id = sanitize_name(raw_op_id)
342342

343343
return cst.FunctionDef(
344344
name=cst.Name(operation_id),

tests/test_coverage_emit_functions_extra.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def test_emit_function_formData_and_no_path_vars():
3434

3535
func_node = emit_function(method, path, op)
3636
assert func_node is not None
37-
assert func_node.name.value == "test_formData_op"
37+
assert func_node.name.value == "test_form_data_op"
3838

3939

4040
def test_get_dummy_value_for_file():

0 commit comments

Comments
 (0)