Skip to content

Commit 7e202b7

Browse files
committed
Update docs and remove irrelevant hook and conform to ruff
1 parent 157baeb commit 7e202b7

18 files changed

Lines changed: 812 additions & 388 deletions

File tree

‎PUBLISH_OUTPUT.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
# Publishing the Output (Generated SDK)
22

3-
When you generate a client SDK using `cdd-python from_openapi to_sdk`, the generated directory will contain a `pyproject.toml` and GitHub Actions workflow.
3+
When you generate a client SDK using `cdd-python-all from_openapi to_sdk`, the generated directory will contain a `pyproject.toml` and GitHub Actions workflow.
44

55
To automate SDK updates:
66
1. Schedule a GitHub Action to fetch the latest OpenAPI spec (e.g., via cron).
7-
2. Run `cdd-python from_openapi to_sdk` on the fetched spec.
8-
3. Compare `client.py` using `git status`. If changed, commit and bump the version in `pyproject.toml`.
7+
2. Run `cdd-python-all from_openapi to_sdk` on the fetched spec.
8+
3. Compare `src/client.py` using `git status`. If changed, commit and bump the version in `pyproject.toml`.
99
4. Run `python3 -m build` and upload to PyPI or your internal package registry.

‎README.md‎

Lines changed: 487 additions & 129 deletions
Large diffs are not rendered by default.

‎USAGE.md‎

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,46 @@
11
# Usage
22

3-
The `cdd-python` CLI can be used in two main modes:
3+
The `cdd-python-all` (also installed as `cdd-python`) CLI and `openapi_client` Python SDK provide lossless, bidirectional compilation between OpenAPI 3.2.0 / Swagger 2.0 / Arazzo 1.1.0 specifications and Python source code.
44

5-
1. **from_openapi**: Generate Python SDK or server mock from OpenAPI JSON.
5+
## CLI Modes
6+
7+
1. **`from_openapi`**: Generate a Python Client SDK (`to_sdk`), Client CLI (`to_sdk_cli`), or FastAPI + SQLAlchemy Server (`to_server`) from an OpenAPI JSON file (`-i`) or directory (`-d`).
68
```bash
7-
cdd-python from_openapi to_sdk -i openapi.json -o my_client
9+
cdd-python-all from_openapi to_sdk -i openapi.json -o my_client --tests
10+
cdd-python-all from_openapi to_sdk_cli -i openapi.json -o my_cli --tests --mcp
11+
cdd-python-all from_openapi to_server -i openapi.json -o my_server --tests --arazzo arazzo.json
812
```
913

10-
2. **to_openapi**: Extract OpenAPI JSON from Python code.
14+
2. **`to_openapi`**: Statically extract an OpenAPI 3.2.0 JSON specification from a Python file or project directory.
1115
```bash
12-
cdd-python to_openapi -f my_client/client.py -o openapi.json
16+
cdd-python-all to_openapi -i my_client -o openapi.json
1317
```
1418

15-
3. **from_arazzo**: Generate Python workflow orchestrators, CLI runners, FastAPI workflow routes, or E2E pytest suites from Arazzo 1.1.0 JSON.
19+
3. **`from_arazzo`**: Generate Python workflow orchestrators (`to_sdk`), CLI workflow runners (`to_sdk_cli`), FastAPI workflow routes (`to_server`), or standalone E2E pytest suites (`to_tests`) from an Arazzo 1.1.0 JSON specification.
1620
```bash
17-
cdd-python from_arazzo to_sdk -i arazzo.json -o my_workflows
21+
cdd-python-all from_arazzo to_sdk -i arazzo.json -o my_workflows --tests
22+
cdd-python-all from_arazzo to_sdk_cli -i arazzo.json -o my_workflows_cli --tests
23+
cdd-python-all from_arazzo to_server -i arazzo.json -o my_server --tests
24+
cdd-python-all from_arazzo to_tests -i arazzo.json -o my_workflows
1825
```
1926

20-
4. **to_arazzo**: Extract Arazzo 1.1.0 JSON from Python workflow code (`workflows.py`, `cli_workflows.py`, `routes/workflows.py`, or `test_workflows.py`).
27+
4. **`to_arazzo`**: Extract an Arazzo 1.1.0 JSON specification from Python workflow code (`workflows.py`, `cli_workflows.py`, `routes/workflows.py`, or `test_workflows.py`) or a project directory.
2128
```bash
22-
cdd-python to_arazzo -f my_workflows/src/workflows.py -o arazzo.json
29+
cdd-python-all to_arazzo -i my_workflows/src/workflows.py -o arazzo.json
2330
```
2431

25-
It can also sync directories (`--truth arazzo` or `--truth openapi`), generate docs, or serve a JSON-RPC / MCP interface.
32+
5. **`sync`**: Synchronize a directory containing `client.py`, `models.py`, `mock_server.py`, `test_client.py`, `cli_main.py`, `workflows.py`, `openapi.json`, and `arazzo.json` with a configurable source of truth (`class`, `sqlalchemy`, `function`, `openapi`, `arazzo`, or `workflow`).
33+
```bash
34+
cdd-python-all sync -i my_client --truth openapi
35+
```
36+
37+
6. **`to_docs_json`**: Generate documentation-ready Python code snippets JSON from an OpenAPI or Arazzo specification.
38+
```bash
39+
cdd-python-all to_docs_json --no-imports --no-wrapping -i openapi.json -o docs.json
40+
```
41+
42+
7. **`serve_json_rpc` & `mcp`**: Expose the compiler over a JSON-RPC 2.0 HTTP server or a Model Context Protocol (`stdio`) server.
43+
```bash
44+
cdd-python-all serve_json_rpc --port 8080 --listen 127.0.0.1
45+
cdd-python-all mcp
46+
```

‎scripts/run_generated_server_test.py‎

100755100644
File mode changed.

‎scripts/run_petstore_test.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,9 @@ def main() -> None:
187187
finally:
188188
if container_name:
189189
subprocess.run(
190-
["docker", "rm", "-f", container_name], capture_output=True
190+
["docker", "rm", "-f", container_name],
191+
check=False,
192+
capture_output=True,
191193
)
192194

193195
finally:

‎scripts/update_badges.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ def main() -> None:
4040
print("Running tests and calculating coverage...")
4141
test_result = subprocess.run(
4242
["pytest", "tests/", "--cov=src/openapi_client", "--cov-report=term"],
43+
check=False,
4344
stdout=subprocess.PIPE,
4445
stderr=subprocess.STDOUT,
4546
text=True,
@@ -57,6 +58,7 @@ def main() -> None:
5758
print("Running interrogate for doc coverage...")
5859
doc_result = subprocess.run(
5960
["interrogate", "--fail-under=0", "src/openapi_client"],
61+
check=False,
6062
stdout=subprocess.PIPE,
6163
stderr=subprocess.STDOUT,
6264
text=True,

‎src/openapi_client/cli.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ def generate_docs_json(
107107
no_wrapping: If True, omit `main()` wrapper from generated snippets.
108108
output_file: Optional file path to write JSON output; prints to stdout if None.
109109
"""
110-
if input_path.startswith("http://") or input_path.startswith("https://"):
110+
if input_path.startswith(("http://", "https://")):
111111
req = urllib.request.Request(input_path)
112112
with urllib.request.urlopen(req) as response:
113113
content = response.read().decode("utf-8")
@@ -1069,7 +1069,7 @@ def send_response(
10691069
"content": [{"type": "text", "text": buffer.getvalue()}]
10701070
},
10711071
)
1072-
except Exception as e:
1072+
except Exception as e: # noqa: BLE001
10731073
send_response(
10741074
req_id,
10751075
result={
@@ -1082,7 +1082,7 @@ def send_response(
10821082
send_response(
10831083
req_id, error={"code": -32601, "message": "Method not found"}
10841084
)
1085-
except Exception as e:
1085+
except Exception as e: # noqa: BLE001
10861086
if "req_id" in locals() and req_id is not None:
10871087
send_response(req_id, error={"code": -32603, "message": str(e)})
10881088

@@ -1173,7 +1173,7 @@ def do_POST(self) -> None:
11731173
result = "Success"
11741174
else:
11751175
error = {"code": -32601, "message": "Method not found"}
1176-
except Exception as e:
1176+
except Exception as e: # noqa: BLE001
11771177
error = {
11781178
"code": -32000,
11791179
"message": str(e),

‎src/openapi_client/cli_sdk/parse.py‎

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def visit_Call(self, node: cst.Call) -> None:
3636

3737
found = False
3838
if self.spec.paths:
39-
for _path, path_item in self.spec.paths.items():
39+
for path_item in self.spec.paths.values():
4040
for method in ["get", "post", "put", "delete", "patch"]:
4141
op = getattr(path_item, method, None)
4242
if op and op.operationId == op_id:
@@ -56,27 +56,25 @@ def visit_Call(self, node: cst.Call) -> None:
5656
elif (
5757
isinstance(node.func, cst.Attribute)
5858
and node.func.attr.value == "add_argument"
59+
and self.current_op
60+
and node.args
61+
and isinstance(node.args[0].value, cst.SimpleString)
5962
):
60-
if (
61-
self.current_op
62-
and node.args
63-
and isinstance(node.args[0].value, cst.SimpleString)
64-
):
65-
arg_name = node.args[0].value.value.strip("\"'")
66-
if arg_name.startswith("--"):
67-
p_name = arg_name[2:]
68-
69-
p_desc = ""
70-
for arg in node.args:
71-
if getattr(arg.keyword, "value", None) == "help" and isinstance(
72-
arg.value, cst.SimpleString
73-
):
74-
p_desc = arg.value.value.strip("\"'")
75-
76-
if self.current_op.parameters:
77-
for p in self.current_op.parameters:
78-
if getattr(p, "name", "").replace("-", "_") == p_name:
79-
p.description = p_desc
63+
arg_name = node.args[0].value.value.strip("\"'")
64+
if arg_name.startswith("--"):
65+
p_name = arg_name[2:]
66+
67+
p_desc = ""
68+
for arg in node.args:
69+
if getattr(arg.keyword, "value", None) == "help" and isinstance(
70+
arg.value, cst.SimpleString
71+
):
72+
p_desc = arg.value.value.strip("\"'")
73+
74+
if self.current_op.parameters:
75+
for p in self.current_op.parameters:
76+
if getattr(p, "name", "").replace("-", "_") == p_name:
77+
p.description = p_desc
8078

8179

8280
def extract_cli_from_ast(module: cst.Module, spec: OpenAPI) -> None:

‎src/openapi_client/docstrings/parse.py‎

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,18 @@ def parse_docstring(
1818
Returns:
1919
A tuple of `(summary, description)`, where either element may be None.
2020
"""
21-
if isinstance(node.body, cst.IndentedBlock):
22-
if node.body.body:
23-
first_stmt = node.body.body[0]
24-
if isinstance(first_stmt, cst.SimpleStatementLine) and first_stmt.body:
25-
first_expr = first_stmt.body[0]
26-
if isinstance(first_expr, cst.Expr) and isinstance(
27-
first_expr.value, cst.SimpleString
28-
):
29-
doc_val = first_expr.value.evaluated_value
30-
if doc_val:
31-
doc_str = str(doc_val).strip("'\"").strip()
32-
parts = re.split(r"\n\s*\n", doc_str, maxsplit=1)
33-
summary = parts[0].strip() if parts else None
34-
description = parts[1].strip() if len(parts) > 1 else None
35-
return summary, description
21+
if isinstance(node.body, cst.IndentedBlock) and node.body.body:
22+
first_stmt = node.body.body[0]
23+
if isinstance(first_stmt, cst.SimpleStatementLine) and first_stmt.body:
24+
first_expr = first_stmt.body[0]
25+
if isinstance(first_expr, cst.Expr) and isinstance(
26+
first_expr.value, cst.SimpleString
27+
):
28+
doc_val = first_expr.value.evaluated_value
29+
if doc_val:
30+
doc_str = str(doc_val).strip("'\"").strip()
31+
parts = re.split(r"\n\s*\n", doc_str, maxsplit=1)
32+
summary = parts[0].strip() if parts else None
33+
description = parts[1].strip() if len(parts) > 1 else None
34+
return summary, description
3635
return None, None

‎src/openapi_client/fastapi/parse.py‎

Lines changed: 25 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -26,37 +26,33 @@ def visit_FunctionDef(self, node: cst.FunctionDef) -> None:
2626
node: The LibCST FunctionDef node being visited.
2727
"""
2828
for decorator in node.decorators:
29-
if isinstance(decorator.decorator, cst.Call):
30-
if isinstance(decorator.decorator.func, cst.Attribute):
31-
if isinstance(
32-
decorator.decorator.func.value, cst.Name
33-
) and decorator.decorator.func.value.value in ("app", "router"):
34-
method = decorator.decorator.func.attr.value
35-
if method in ["get", "post", "put", "delete", "patch"]:
36-
if decorator.decorator.args and isinstance(
37-
decorator.decorator.args[0].value, cst.SimpleString
38-
):
39-
path = decorator.decorator.args[0].value.value.strip(
40-
"\"'"
41-
)
29+
if (
30+
isinstance(decorator.decorator, cst.Call)
31+
and isinstance(decorator.decorator.func, cst.Attribute)
32+
and isinstance(decorator.decorator.func.value, cst.Name)
33+
and decorator.decorator.func.value.value in ("app", "router")
34+
):
35+
method = decorator.decorator.func.attr.value
36+
if (
37+
method in ["get", "post", "put", "delete", "patch"]
38+
and decorator.decorator.args
39+
and isinstance(decorator.decorator.args[0].value, cst.SimpleString)
40+
):
41+
path = decorator.decorator.args[0].value.value.strip("\"'")
4242

43-
if not self.spec.paths:
44-
self.spec.paths = {}
45-
if path not in self.spec.paths:
46-
self.spec.paths[path] = PathItem()
43+
if not self.spec.paths:
44+
self.spec.paths = {}
45+
if path not in self.spec.paths:
46+
self.spec.paths[path] = PathItem()
4747

48-
summary, description = parse_docstring(node)
49-
op = Operation(
50-
operationId=node.name.value,
51-
summary=summary,
52-
description=description,
53-
responses={
54-
"200": Response(
55-
description="Successful Response"
56-
)
57-
},
58-
)
59-
setattr(self.spec.paths[path], method, op)
48+
summary, description = parse_docstring(node)
49+
op = Operation(
50+
operationId=node.name.value,
51+
summary=summary,
52+
description=description,
53+
responses={"200": Response(description="Successful Response")},
54+
)
55+
setattr(self.spec.paths[path], method, op)
6056

6157

6258
def extract_fastapi_from_ast(module: cst.Module, spec: OpenAPI) -> None:

0 commit comments

Comments
 (0)