Skip to content

Commit 3d0e34d

Browse files
committed
Improve coverage, docs, and add new to_docs subcommand
1 parent ec13111 commit 3d0e34d

15 files changed

Lines changed: 248 additions & 55 deletions

File tree

ARCHITECTURE.md

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
The **cdd-python-client** tool acts as a dedicated compiler and transpiler. Its fundamental architecture follows standard compiler design principles, divided into three distinct phases: **Frontend (Parsing)**, **Intermediate Representation (IR)**, and **Backend (Emitting)**.
1111

12-
This decoupled design ensures that any format capable of being parsed into the IR can subsequently be emitted into any supported output format, whether that is a server-side route, a client-side SDK, a database ORM, or an OpenAPI specification.
12+
This decoupled design ensures that any format capable of being parsed into the IR can subsequently be emitted into any supported output format, whether that is a server-side route, a client-side SDK, a database ORM, or an OpenAPI specification. As a `Client-only` focussed project, its emitters are heavily optimized for generating Python API SDKs and Client Wrappers using LibCST and Pydantic.
1313

1414
## 🏗 High-Level Overview
1515

@@ -23,7 +23,7 @@ graph TD
2323
2424
subgraph Frontend [Parsers]
2525
A[OpenAPI .yaml/.json]:::endpoint --> P1(OpenAPI Parser):::frontend
26-
B[`Python` Models / Source]:::endpoint --> P2(`Python` Parser):::frontend
26+
B[Python Models / Source]:::endpoint --> P2(Python Parser):::frontend
2727
C[Server Routes / Frameworks]:::endpoint --> P3(Framework Parser):::frontend
2828
D[Client SDKs / ORMs]:::endpoint --> P4(Ext Parser):::frontend
2929
end
@@ -34,10 +34,9 @@ graph TD
3434
3535
subgraph Backend [Emitters]
3636
E1(OpenAPI Emitter):::backend --> X[OpenAPI .yaml/.json]:::endpoint
37-
E2(`Python` Emitter):::backend --> Y[`Python` Models / Structs]:::endpoint
37+
E2(Python Emitter):::backend --> Y[Python Models / Structs]:::endpoint
3838
E3(Server Emitter):::backend --> Z[Server Routes / Controllers]:::endpoint
3939
E4(Client Emitter):::backend --> W[Client SDKs / API Calls]:::endpoint
40-
E5(Data Emitter):::backend --> V[ORM Models / CLI Parsers]:::endpoint
4140
end
4241
4342
P1 --> IR
@@ -49,7 +48,6 @@ graph TD
4948
IR --> E2
5049
IR --> E3
5150
IR --> E4
52-
IR --> E5
5351
```
5452

5553
## 🧩 Core Components
@@ -58,7 +56,7 @@ graph TD
5856

5957
The Frontend's responsibility is to read an input source and translate it into the universal CDD Intermediate Representation (IR).
6058

61-
* **Static Analysis (AST-Driven)**: For `Python` source code, the tool **does not** use dynamic reflection or execute the code. Instead, it reads the source files, generates an Abstract Syntax Tree (AST), and navigates the tree to extract classes, structs, functions, type signatures, API client definitions, server routes (like FastAPI mocks), and docstrings.
59+
* **Static Analysis (AST-Driven)**: For `Python` source code, the tool **does not** use dynamic reflection or execute the code. Instead, it reads the source files using LibCST, generates an Abstract Syntax Tree (AST), and navigates the tree to extract classes, structs, functions, type signatures, API client definitions, server routes, and docstrings.
6260
* **OpenAPI Parsing**: For OpenAPI and JSON Schema inputs, the parser normalizes the structure, resolving internal `$ref`s and extracting properties, endpoints (client or server perspectives), and metadata into the IR.
6361

6462
### 2. Intermediate Representation (IR)
@@ -75,9 +73,8 @@ By standardizing on a single IR (heavily inspired by OpenAPI / JSON Schema primi
7573
The Backend's responsibility is to take the universal IR and generate valid target output. Emitters can be written to support various environments (e.g., Client vs Server, Web vs CLI).
7674

7775
* **Code Generation**: Emitters iterate over the IR and generate idiomatic `Python` source code.
78-
* A **Server Emitter** creates routing controllers and request-validation logic (for example, generating local test-mock servers).
79-
* A **Client Emitter** creates API wrappers, fetch functions, and response-parsing logic.
80-
* **Database & CLI Generation**: Emitters can also target ORM models or command-line parsers by mapping IR properties to database columns or CLI arguments.
76+
* A **Server Emitter** creates routing controllers and request-validation logic (such as mocks).
77+
* A **Client Emitter** creates API wrappers, fetch functions, and response-parsing logic tailored for Python clients.
8178
* **Specification Generation**: Emitters translating back to OpenAPI serialize the IR into standard OpenAPI 3.x JSON or YAML, rigorously formatting descriptions, type constraints, and endpoint schemas based on what was parsed from the source code.
8279

8380
## 🔄 Extensibility
@@ -91,4 +88,4 @@ Because of the IR-centric design, adding support for a new `Python` framework (e
9188
1. **A Single Source of Truth**: Developers should be able to maintain their definitions in whichever format is most ergonomic for their team (OpenAPI files, Native Code, Client libraries, ORM models) and generate the rest.
9289
2. **Zero-Execution Parsing**: Ensure security and resilience by strictly statically analyzing inputs. The compiler must never need to run the target code to understand its structure.
9390
3. **Lossless Conversion**: Maximize the retention of metadata (e.g., type annotations, docstrings, default values, validators) during the transition `Source -> IR -> Target`.
94-
4. **Symmetric Operations**: An Endpoint in the IR holds all the information necessary to generate both the Server-side controller that fulfills it, and the Client-side SDK method that calls it.
91+
4. **Symmetric Operations**: An Endpoint in the IR holds all the information necessary to generate both the Server-side controller that fulfills it, and the Client-side SDK method that calls it.

README.md

Lines changed: 46 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,37 @@
1-
cdd-python-client
2-
=================
1+
cdd-Python
2+
============
33

44
[![License](https://img.shields.io/badge/license-Apache--2.0%20OR%20MIT-blue.svg)](https://opensource.org/licenses/Apache-2.0)
5-
[![uv](https://github.com/offscale/cdd-python-client/actions/workflows/uv.yml/badge.svg)](https://github.com/offscale/cdd-python-client/actions/workflows/uv.yml)
5+
[![CI/CD](https://github.com/offscale/cdd-python-client/workflows/CI/badge.svg)](https://github.com/offscale/cdd-python-client/actions)
66
![Test Coverage](https://img.shields.io/badge/Test_Coverage-100.0%25-brightgreen)
77
![Doc Coverage](https://img.shields.io/badge/Doc_Coverage-100.0%25-brightgreen)
88

9-
Welcome to **cdd-python-client**, a code-generation and compilation tool bridging the gap between OpenAPI specifications
10-
and native `Python` source code.
9+
OpenAPI ↔ Python. This is one compiler in a suite, all focussed on the same task: Compiler Driven Development (CDD).
1110

12-
This toolset allows you to fluidly convert between your language's native constructs (like classes, structs, functions,
13-
routing, clients, and ORM models) and OpenAPI specifications, ensuring a single source of truth without sacrificing
14-
developer ergonomics.
11+
Each compiler is written in its target language, is whitespace and comment sensitive, and has both an SDK and CLI.
12+
13+
The CLI—at a minimum—has:
14+
- `cdd --help`
15+
- `cdd --version`
16+
- `cdd sync --from-openapi spec.json --to-python out_dir`
17+
- `cdd sync --from-python client.py --to-openapi spec.json`
18+
- `cdd to_docs_json --no-imports --no-wrapping -i spec.json`
19+
20+
The goal of this project is to enable rapid application development without tradeoffs. Tradeoffs of Protocol Buffers / Thrift etc. are an untouchable "generated" directory and package, compile-time and/or runtime overhead. Tradeoffs of Java or JavaScript for everything are: overhead in hardware access, offline mode, ML inefficiency, and more. And neither of these alterantive approaches are truly integrated into your target system, test frameworks, and bigger abstractions you build in your app. Tradeoffs in CDD are code duplication (but CDD handles the synchronisation for you).
1521

1622
## 🚀 Capabilities
1723

18-
The `cdd-python-client` compiler leverages a unified architecture to support various facets of API and code lifecycle
19-
management.
24+
The `cdd-python-client` compiler leverages a unified architecture to support various facets of API and code lifecycle management.
2025

2126
* **Compilation**:
22-
* **OpenAPI ➡️ `Python`**: Generate idiomatic native models, network routes, client SDKs, database schemas, and
23-
boilerplate directly from OpenAPI (`.json` / `.yaml`) specifications.
24-
* **`Python` ➡️ OpenAPI**: Statically parse existing `Python` source code and emit compliant OpenAPI specifications.
25-
* **AST-Driven & Safe**: Employs static analysis (Abstract Syntax Trees) instead of unsafe dynamic execution or
26-
reflection, allowing it to safely parse and emit code even for incomplete or un-compilable project states.
27-
* **Seamless Sync**: Keep your docs, tests, database, clients, and routing in perfect harmony. Update your code, and
28-
generate the docs; or update the docs, and generate the code.
27+
* **OpenAPI → `Python`**: Generate idiomatic native models, network routes, client SDKs, database schemas, and boilerplate directly from OpenAPI (`.json` / `.yaml`) specifications.
28+
* **`Python` → OpenAPI**: Statically parse existing `Python` source code and emit compliant OpenAPI specifications.
29+
* **AST-Driven & Safe**: Employs static analysis (Abstract Syntax Trees) instead of unsafe dynamic execution or reflection, allowing it to safely parse and emit code even for incomplete or un-compilable project states.
30+
* **Seamless Sync**: Keep your docs, tests, database, clients, and routing in perfect harmony. Update your code, and generate the docs; or update the docs, and generate the code.
2931

3032
## 📦 Installation
3133

32-
Requires Python 3.9+. Install directly via `pip` or use `uv` for modern dependency management:
34+
Requires Python 3.9+.
3335

3436
```bash
3537
pip install openapi-python-client
@@ -39,50 +41,49 @@ pip install openapi-python-client
3941

4042
### Command Line Interface
4143

42-
The `cdd` command provides a straightforward way to keep your Python artifacts and OpenAPI specs synchronized.
43-
44+
Generate a Python client from an OpenAPI specification:
4445
```bash
45-
# Generate Python client, mock server, and tests from an OpenAPI spec
46-
cdd sync --from-openapi openapi.json --to-python ./my_client_dir
46+
cdd sync --from-openapi spec.json --to-python ./my_client
47+
```
4748

48-
# Extract an OpenAPI spec directly from an existing Python client
49-
cdd sync --from-python ./my_client_dir/client.py --to-openapi extracted_openapi.json
49+
Extract an OpenAPI specification from an existing Python client module:
50+
```bash
51+
cdd sync --from-python ./my_client/client.py --to-openapi spec.json
52+
```
5053

51-
# Synchronize an entire directory (merging changes between Python files and openapi.json)
52-
cdd sync --dir ./my_project
54+
Generate a docs JSON:
55+
```bash
56+
cdd to_docs_json -i spec.json --no-imports
5357
```
5458

5559
### Programmatic SDK / Library
5660

57-
You can also leverage the underlying parsers and emitters programmatically within your Python scripts:
58-
5961
```python
60-
from pathlib import Path
6162
from openapi_client.openapi.parse import parse_openapi_json
6263
from openapi_client.routes.emit import ClientGenerator
6364

64-
# 1. Parse an existing OpenAPI JSON spec
65-
spec_json = Path("openapi.json").read_text(encoding="utf-8")
66-
spec = parse_openapi_json(spec_json)
67-
68-
# 2. Emit idiomatic Python client code
65+
spec = parse_openapi_json(open('spec.json').read())
6966
generator = ClientGenerator(spec)
70-
client_code = generator.generate_code()
71-
72-
Path("client.py").write_text(client_code, encoding="utf-8")
67+
print(generator.generate_code())
7368
```
7469

70+
## Design choices
71+
72+
The `cdd-python-client` leverages LibCST for parsing and generating Python Abstract Syntax Trees (AST). LibCST is chosen over Python's built-in `ast` module because it preserves whitespace, comments, and structure, which is crucial for a bidirectional compiler that doesn't mess up your code formatting when syncing changes. Pydantic is used for models generation to provide idiomatic and robust validation for the client interfaces.
73+
7574
## 🏗 Supported Conversions for Python
7675

7776
*(The boxes below reflect the features supported by this specific `cdd-python-client` implementation)*
7877

79-
| Concept | Parse (From) | Emit (To) |
80-
|------------------------------------|--------------|-----------|
81-
| OpenAPI (JSON/YAML) | [x] | [x] |
82-
| `Python` Models / Structs / Types | [x] | [x] |
83-
| `Python` Server Routes / Endpoints | [x] | [x] |
84-
| `Python` API Clients / SDKs | [x] | [x] |
85-
| `Python` Docstrings / Comments | [x] | [x] |
78+
| Concept | Parse (From) | Emit (To) |
79+
|---------|--------------|-----------|
80+
| OpenAPI (JSON/YAML) | [] | [] |
81+
| `Python` Models / Structs / Types | [] | [] |
82+
| `Python` Server Routes / Endpoints | [] | [] |
83+
| `Python` API Clients / SDKs | [] | [] |
84+
| `Python` ORM / DB Schemas | [ ] | [ ] |
85+
| `Python` CLI Argument Parsers | [ ] | [ ] |
86+
| `Python` Docstrings / Comments | [] | [] |
8687

8788
---
8889

@@ -99,4 +100,4 @@ at your option.
99100

100101
Unless you explicitly state otherwise, any contribution intentionally submitted
101102
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be
102-
dual licensed as above, without any additional terms or conditions.
103+
dual licensed as above, without any additional terms or conditions.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Module containing initialization logic."""

src/openapi_client/classes/parse.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class ClassExtractor(cst.CSTVisitor):
1212
"""
1313

1414
def __init__(self, spec: OpenAPI):
15+
"""Initialize ClassExtractor with an OpenAPI spec."""
1516
self.spec = spec
1617
if self.spec.components is None:
1718
self.spec.components = Components(schemas={})

src/openapi_client/cli.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import argparse
44
import sys
5+
import json
56
from pathlib import Path
67
import libcst as cst
78

@@ -17,6 +18,55 @@
1718
from openapi_client.openapi.emit import emit_openapi_json
1819

1920

21+
def generate_docs_json(input_path: str, no_imports: bool, no_wrapping: bool) -> None:
22+
"""Parse OpenAPI spec and output JSON documentation."""
23+
spec_path = Path(input_path)
24+
spec = parse_openapi_json(spec_path.read_text(encoding="utf-8"))
25+
26+
operations = []
27+
28+
if spec.paths:
29+
for path, path_item in spec.paths.items():
30+
for method in ["get", "post", "put", "delete", "patch"]:
31+
operation = getattr(path_item, method, None)
32+
if operation:
33+
op_id = (
34+
operation.operationId
35+
or f"{method}_{path.replace('/', '_').strip('_')}"
36+
)
37+
38+
code = {}
39+
if not no_imports:
40+
code["imports"] = "from client import Client"
41+
if not no_wrapping:
42+
code["wrapper_start"] = (
43+
'def main():\n client = Client(base_url="https://api.example.com")'
44+
)
45+
code["wrapper_end"] = 'if __name__ == "__main__":\n main()'
46+
47+
# Basic snippet with arguments
48+
args = []
49+
if operation.parameters:
50+
for param in operation.parameters:
51+
if hasattr(param, "name"):
52+
p_name = param.name.replace("-", "_")
53+
args.append(f"{p_name}={p_name}")
54+
55+
args_str = ", ".join(args)
56+
indent = " " if not no_wrapping else ""
57+
code["snippet"] = f"{indent}response = client.{op_id}({args_str})"
58+
59+
op_data = {"method": method.upper(), "path": path, "code": code}
60+
if operation.operationId:
61+
op_data["operationId"] = operation.operationId
62+
63+
operations.append(op_data)
64+
65+
output = [{"language": "python", "operations": operations}]
66+
67+
print(json.dumps(output, indent=2))
68+
69+
2070
def sync_to_python(openapi_path: str, output_dir: str) -> None:
2171
"""Generate Python client, tests, and mocks from an OpenAPI spec."""
2272
spec_path = Path(openapi_path)
@@ -140,6 +190,23 @@ def main() -> None:
140190
help="Path to directory containing client.py, mock_server.py, test_client.py",
141191
)
142192

193+
docs_parser = subparsers.add_parser(
194+
"to_docs_json", help="Generate JSON documentation"
195+
)
196+
docs_parser.add_argument(
197+
"-i",
198+
"--input",
199+
type=str,
200+
required=True,
201+
help="Path or URL to the OpenAPI specification",
202+
)
203+
docs_parser.add_argument(
204+
"--no-imports", action="store_true", help="Omit the imports field"
205+
)
206+
docs_parser.add_argument(
207+
"--no-wrapping", action="store_true", help="Omit the wrapper fields"
208+
)
209+
143210
args = parser.parse_args()
144211

145212
if args.command == "sync":
@@ -154,6 +221,8 @@ def main() -> None:
154221
"Invalid sync arguments. Use either --dir, --from-openapi & --to-python OR --from-python & --to-openapi."
155222
)
156223
sys.exit(1)
224+
elif args.command == "to_docs_json":
225+
generate_docs_json(args.input, args.no_imports, args.no_wrapping)
157226

158227

159228
if __name__ == "__main__": # pragma: no cover
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Module containing initialization logic."""
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Module containing initialization logic."""

src/openapi_client/functions/parse.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class FunctionExtractor(cst.CSTVisitor):
1212
"""
1313

1414
def __init__(self, spec: OpenAPI):
15+
"""Initialize FunctionExtractor with an OpenAPI spec."""
1516
self.spec = spec
1617
if not self.spec.paths:
1718
self.spec.paths = {}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Module containing initialization logic."""

src/openapi_client/mocks/parse.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class MockServerExtractor(cst.CSTVisitor):
1212
"""
1313

1414
def __init__(self, spec: OpenAPI):
15+
"""Initialize MockServerExtractor with an OpenAPI spec."""
1516
self.spec = spec
1617
if not self.spec.paths:
1718
self.spec.paths = {}
@@ -62,6 +63,7 @@ class ReturnExtractor(cst.CSTVisitor):
6263
"""Visitor to find event stream responses."""
6364

6465
def __init__(self):
66+
"""Initialize ReturnExtractor."""
6567
self.has_event_stream = False
6668

6769
def visit_Return(

0 commit comments

Comments
 (0)