Skip to content

Commit 0f0de4c

Browse files
committed
feat: implement to_docs_json, standardize cli interface, update docs and build systems
1 parent 3d0e34d commit 0f0de4c

16 files changed

Lines changed: 332 additions & 108 deletions

.github/workflows/uv.yml

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: uv
1+
name: CI
22

33
on:
44
push:
@@ -14,9 +14,10 @@ jobs:
1414
strategy:
1515
matrix:
1616
python-version:
17+
- "3.9"
18+
- "3.10"
1719
- "3.11"
1820
- "3.12"
19-
- "3.13"
2021

2122
steps:
2223
- uses: actions/checkout@v4
@@ -32,7 +33,9 @@ jobs:
3233
- name: Install the project
3334
run: |
3435
uv venv --python ${{ matrix.python-version }}
35-
uv pip install -e ".[dev]"
36+
uv pip install -e ".[dev]" pytest pytest-cov interrogate
3637
37-
- name: Run tests
38-
run: uv run pytest
38+
- name: Run tests & update badges
39+
run: |
40+
uv run python scripts/update_badges.py
41+
git diff --exit-code README.md || echo "README.md coverage badges are outdated!"

.pre-commit-config.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
repos:
2+
- repo: https://github.com/pre-commit/pre-commit-hooks
3+
rev: v4.5.0
4+
hooks:
5+
- id: trailing-whitespace
6+
- id: end-of-file-fixer
7+
- id: check-yaml
8+
- id: check-added-large-files
9+
- repo: local
10+
hooks:
11+
- id: run-tests-and-update-badges
12+
name: Run Tests and Update Coverage Badges
13+
entry: uv run python scripts/update_badges.py
14+
language: system
15+
pass_filenames: false
16+
always_run: true

ARCHITECTURE.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
# cdd-python-client Architecture
22

33
<!-- BADGES_START -->
4-
<!-- Replace these placeholders with your repository-specific badges -->
54
[![License](https://img.shields.io/badge/license-Apache--2.0%20OR%20MIT-blue.svg)](https://opensource.org/licenses/Apache-2.0)
65
[![CI/CD](https://github.com/offscale/cdd-python-client/workflows/CI/badge.svg)](https://github.com/offscale/cdd-python-client/actions)
76
[![Coverage](https://codecov.io/gh/offscale/cdd-python-client/branch/master/graph/badge.svg)](https://codecov.io/gh/offscale/cdd-python-client)
87
<!-- BADGES_END -->
98

109
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)**.
1110

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.
11+
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.
1312

1413
## 🏗 High-Level Overview
1514

@@ -37,6 +36,7 @@ graph TD
3736
E2(Python Emitter):::backend --> Y[Python Models / Structs]:::endpoint
3837
E3(Server Emitter):::backend --> Z[Server Routes / Controllers]:::endpoint
3938
E4(Client Emitter):::backend --> W[Client SDKs / API Calls]:::endpoint
39+
E5(Data Emitter):::backend --> V[ORM Models / CLI Parsers]:::endpoint
4040
end
4141
4242
P1 --> IR
@@ -48,6 +48,7 @@ graph TD
4848
IR --> E2
4949
IR --> E3
5050
IR --> E4
51+
IR --> E5
5152
```
5253

5354
## 🧩 Core Components
@@ -56,7 +57,7 @@ graph TD
5657

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

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.
60+
* **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) via `libcst`, and navigates the tree to extract classes, structs, functions, type signatures, API client definitions, server routes, and docstrings.
6061
* **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.
6162

6263
### 2. Intermediate Representation (IR)
@@ -72,9 +73,10 @@ By standardizing on a single IR (heavily inspired by OpenAPI / JSON Schema primi
7273

7374
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).
7475

75-
* **Code Generation**: Emitters iterate over the IR and generate idiomatic `Python` source code.
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.
76+
* **Code Generation**: Emitters iterate over the IR and generate idiomatic `Python` source code.
77+
* A **Server Emitter** creates routing controllers and request-validation logic (like mock servers using FastAPI).
78+
* A **Client Emitter** creates API wrappers, fetch functions, and response-parsing logic using `urllib3` or `requests`.
79+
* **Database & CLI Generation**: Emitters can also target ORM models or command-line parsers by mapping IR properties to database columns or CLI arguments.
7880
* **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.
7981

8082
## 🔄 Extensibility
@@ -88,4 +90,4 @@ Because of the IR-centric design, adding support for a new `Python` framework (e
8890
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.
8991
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.
9092
3. **Lossless Conversion**: Maximize the retention of metadata (e.g., type annotations, docstrings, default values, validators) during the transition `Source -> IR -> Target`.
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.
93+
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.

COMPLIANCE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ The following OpenAPI 3.2.0 structures are fully modeled in `src/openapi_client/
1515
- **Components Object**: Models schemas, responses, parameters, examples, requestBodies, headers, securitySchemes, links, callbacks, pathItems, and `mediaTypes` (new in OAS 3.2.0).
1616

1717
### Schema & Data Types
18-
- **Schema Object**: Includes full JSON Schema validation properties (`type`, `properties`, `items`, `allOf`, `anyOf`, `oneOf`, `discriminator`, etc.).
18+
- **Schema Object**: Includes full JSON Schema validation properties (`type`, `properties`, `items`, `allOf`, `anyOf`, `oneOf`, `discriminator`, etc.).
1919
- **Reference Object**: Full support for `$ref` pointer objects across paths, components, parameters, etc.
2020

2121
### Media Types & Encodings (OAS 3.2.0 Enhancements)

Makefile

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
.PHONY: all help install_base install_deps build_docs build test run
2+
3+
DOCS_DIR := docs
4+
BIN_DIR := dist
5+
6+
# Extract positional arguments for build_docs
7+
ifeq (build_docs,$(firstword $(MAKECMDGOALS)))
8+
DOCS_ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))
9+
ifneq ($(DOCS_ARGS),)
10+
DOCS_DIR := $(word 1,$(DOCS_ARGS))
11+
$(eval $(DOCS_DIR):;@:)
12+
endif
13+
endif
14+
15+
# Extract positional arguments for build
16+
ifeq (build,$(firstword $(MAKECMDGOALS)))
17+
BUILD_ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))
18+
ifneq ($(BUILD_ARGS),)
19+
BIN_DIR := $(word 1,$(BUILD_ARGS))
20+
$(eval $(BIN_DIR):;@:)
21+
endif
22+
endif
23+
24+
# Extract arguments for run
25+
ifeq (run,$(firstword $(MAKECMDGOALS)))
26+
RUN_ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))
27+
$(foreach arg,$(RUN_ARGS),$(eval $(arg):;@:))
28+
endif
29+
30+
# Find all Python files to trigger rebuilds
31+
SRCS := $(shell find src -type f -name '*.py' 2>/dev/null)
32+
33+
all: help
34+
35+
help:
36+
@echo "Usage: make [target] [args...]"
37+
@echo ""
38+
@echo "Targets:"
39+
@echo " install_base Install uv (Python package manager) and OS build tools"
40+
@echo " install_deps Install local dependencies using uv"
41+
@echo " build_docs Build API docs. Positional arg specifies dir (e.g., make build_docs ./custom_docs)"
42+
@echo " build Build the CLI binary. Positional arg specifies dir (e.g., make build ./bin)"
43+
@echo " test Run tests locally"
44+
@echo " run Run the CLI. Passes trailing args to the CLI (note: use 'make run -- --flag' to avoid Make intercepting flags)"
45+
@echo " help Show this help text"
46+
@echo " all Show this help text"
47+
48+
install_base:
49+
@echo "Installing base dependencies..."
50+
@if [ "$$(uname)" = "Darwin" ]; then \
51+
echo "macOS detected. Ensure Xcode command line tools are installed: xcode-select --install"; \
52+
elif [ -f /etc/debian_version ]; then \
53+
echo "Ubuntu/Debian detected. Installing build essentials..."; \
54+
sudo apt-get update && sudo apt-get install -y build-essential curl; \
55+
elif [ -f /etc/redhat-release ]; then \
56+
echo "RPM/RedHat detected. Installing build tools..."; \
57+
sudo dnf groupinstall -y "Development Tools" && sudo dnf install -y curl; \
58+
elif [ "$$(uname)" = "FreeBSD" ]; then \
59+
echo "FreeBSD detected. Installing build tools..."; \
60+
sudo pkg install -y curl gcc; \
61+
fi
62+
@if ! command -v uv >/dev/null 2>&1; then \
63+
echo "Installing uv..."; \
64+
curl -LsSf https://astral.sh/uv/install.sh | sh; \
65+
else \
66+
echo "uv already installed."; \
67+
fi
68+
69+
install_deps:
70+
@echo "Installing local dependencies..."
71+
uv venv --allow-existing
72+
uv pip install -e ".[dev]" pyinstaller pdoc
73+
74+
build_docs: install_deps
75+
@echo "Building API docs in $(DOCS_DIR)..."
76+
uv run pdoc src/openapi_client -o $(DOCS_DIR)
77+
78+
$(BIN_DIR)/cdd: $(SRCS)
79+
@echo "Building CLI binary in $(BIN_DIR)..."
80+
uv run pyinstaller --noconfirm --onefile --distpath $(BIN_DIR) --name cdd src/openapi_client/cli.py
81+
82+
build: install_deps $(BIN_DIR)/cdd
83+
84+
run: build
85+
@echo "Running CLI..."
86+
@./$(BIN_DIR)/cdd $(RUN_ARGS)

PUBLISH_OUTPUT.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,10 @@ jobs:
102102
run: |
103103
git config --global user.name "github-actions[bot]"
104104
git config --global user.email "github-actions[bot]@users.noreply.github.com"
105-
105+
106106
# Automatically bump the patch version in pyproject.toml
107107
bumpver update --patch
108-
108+
109109
git add src/my_api_client/ openapi.json pyproject.toml
110110
git commit -m "chore(openapi): auto-sync client with upstream spec"
111111
git push

README.md

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
cdd-Python
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)
55
[![CI/CD](https://github.com/offscale/cdd-python-client/workflows/CI/badge.svg)](https://github.com/offscale/cdd-python-client/actions)
@@ -11,11 +11,11 @@ OpenAPI ↔ Python. This is one compiler in a suite, all focussed on the same ta
1111
Each compiler is written in its target language, is whitespace and comment sensitive, and has both an SDK and CLI.
1212

1313
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`
14+
- `cdd-python --help`
15+
- `cdd-python --version`
16+
- `cdd-python from_openapi -i spec.json`
17+
- `cdd-python to_openapi -f path/to/code`
18+
- `cdd-python to_docs_json --no-imports --no-wrapping -i spec.json`
1919

2020
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).
2121

@@ -31,29 +31,29 @@ The `cdd-python-client` compiler leverages a unified architecture to support var
3131

3232
## 📦 Installation
3333

34-
Requires Python 3.9+.
34+
Requires Python 3.9+. Install directly from the repository using `uv` or `pip`:
3535

3636
```bash
37-
pip install openapi-python-client
37+
uv pip install git+https://github.com/offscale/cdd-python-client.git
3838
```
3939

4040
## 🛠 Usage
4141

4242
### Command Line Interface
4343

44-
Generate a Python client from an OpenAPI specification:
44+
Generate a Python client, tests, and mocks from an OpenAPI spec:
4545
```bash
46-
cdd sync --from-openapi spec.json --to-python ./my_client
46+
cdd-python from_openapi -i openapi.json -o my_client_dir
4747
```
4848

49-
Extract an OpenAPI specification from an existing Python client module:
49+
Extract an OpenAPI spec back out of your Python source code:
5050
```bash
51-
cdd sync --from-python ./my_client/client.py --to-openapi spec.json
51+
cdd-python to_openapi -f my_client_dir/client.py -o openapi.json
5252
```
5353

54-
Generate a docs JSON:
54+
Generate a docs JSON array for the website:
5555
```bash
56-
cdd to_docs_json -i spec.json --no-imports
56+
cdd-python to_docs_json -i openapi.json --no-imports --no-wrapping
5757
```
5858

5959
### Programmatic SDK / Library
@@ -62,28 +62,31 @@ cdd to_docs_json -i spec.json --no-imports
6262
from openapi_client.openapi.parse import parse_openapi_json
6363
from openapi_client.routes.emit import ClientGenerator
6464

65-
spec = parse_openapi_json(open('spec.json').read())
65+
spec = parse_openapi_json(open("openapi.json").read())
6666
generator = ClientGenerator(spec)
67-
print(generator.generate_code())
67+
client_code = generator.generate_code()
68+
69+
with open("client.py", "w") as f:
70+
f.write(client_code)
6871
```
6972

7073
## Design choices
7174

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.
75+
The project leverages `libcst` to guarantee that code parsing and emission are completely whitespace and comment sensitive. By utilizing a lossless Abstract Syntax Tree (AST), `cdd-python` allows for symmetric conversion back and forth between OpenAPI specifications and rich Python source code without clobbering manual developer interventions like inline comments or non-API-related logic.
7376

7477
## 🏗 Supported Conversions for Python
7578

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

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

8891
---
8992

@@ -100,4 +103,4 @@ at your option.
100103

101104
Unless you explicitly state otherwise, any contribution intentionally submitted
102105
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be
103-
dual licensed as above, without any additional terms or conditions.
106+
dual licensed as above, without any additional terms or conditions.

0 commit comments

Comments
 (0)