Skip to content

Commit 286b914

Browse files
committed
Update with improved coverage
1 parent 1d53734 commit 286b914

File tree

26 files changed

+688
-34
lines changed

26 files changed

+688
-34
lines changed

.github/workflows/uv.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ name: uv
33
on:
44
push:
55
branches:
6-
- main
6+
- master
77
pull_request:
88
branches:
9-
- main
9+
- master
1010

1111
jobs:
1212
test:

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
*venv*
44
__pycache__
55
*.pyc
6+
coverage.json

COMPLIANCE.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ The following OpenAPI 3.2.0 structures are fully modeled in `src/openapi_client/
2323
- **Encoding Object**: Supports headers, standard encodings, `prefixEncoding`, and `itemEncoding`.
2424

2525
## Partial / Ongoing Support
26-
While the data models strictly validate against OpenAPI 3.2.0 schemas, the *AST manipulation* logic is still incrementally supporting features:
26+
All previous ongoing issues have been implemented:
2727

28-
1. **Complex Parameters**: Deep object serializations for Query strings (`spaceDelimited`, `pipeDelimited`) are modeled but code-generation for them currently maps to generic `Any` types in the Python client.
29-
2. **Streaming / Webhooks**: Models exist, but FastAPI mock extraction and test extraction for event-driven / server-sent events (`text/event-stream`) is currently stubbed.
30-
3. **Implicit Connections**: Cross-document `$ref` resolution is currently limited to local `#/...` JSON pointers within the `openapi.json` file.
28+
1. **Complex Parameters**: Deep object serializations for Query strings (`spaceDelimited`, `pipeDelimited`) are fully generated in the Python client, automatically building delimited query strings and strongly typed input arguments.
29+
2. **Streaming / Webhooks**: FastAPI mock extraction properly extracts event-driven / server-sent events (`text/event-stream`) for `EventSourceResponse`, and test extraction parses tests checking for SSE streams.
30+
3. **Implicit Connections**: Cross-document `$ref` resolution is fully supported across local JSON file scopes.
3131

3232
## Testing Compliance
3333
Compliance is strictly enforced via 100% test coverage and `mypy` strict mode checking across all model definitions and serializers.

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# CDD Python Client (OpenAPI 3.2.0)
22

33
[![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)
4+
![Test Coverage](https://img.shields.io/badge/Test_Coverage-100.0%25-brightgreen)
5+
![Doc Coverage](https://img.shields.io/badge/Doc_Coverage-100.0%25-brightgreen)
46

57
A highly modular, strictly typed, bidirectional OpenAPI 3.2.0 client generator and extractor for Python.
68

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ dependencies = [
1818
dev = [
1919
"pytest",
2020
"pytest-cov",
21+
"interrogate",
2122
]
2223

2324
[project.scripts]

scripts/doc_coverage.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import ast
2+
import os
3+
4+
5+
def calculate_doc_coverage(directory):
6+
total = 0
7+
with_docs = 0
8+
9+
for root, _, files in os.walk(directory):
10+
for file in files:
11+
if not file.endswith(".py"):
12+
continue
13+
path = os.path.join(root, file)
14+
with open(path, "r", encoding="utf-8") as f:
15+
content = f.read()
16+
try:
17+
tree = ast.parse(content)
18+
except Exception:
19+
continue
20+
21+
# Check module docstring
22+
total += 1
23+
if ast.get_docstring(tree):
24+
with_docs += 1
25+
else:
26+
pass # print(f"Missing docstring: Module {path}")
27+
28+
for node in ast.walk(tree):
29+
if isinstance(
30+
node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)
31+
):
32+
# skip private methods but keep __init__
33+
if node.name.startswith("__") and node.name != "__init__":
34+
continue
35+
# Actually let's just ignore __init__ or count it if the class has a docstring?
36+
# Let's count __init__ as requiring a docstring.
37+
total += 1
38+
if ast.get_docstring(node):
39+
with_docs += 1
40+
else:
41+
pass # print(f"Missing docstring: {node.name} in {path}")
42+
43+
percent = (with_docs / total * 100) if total > 0 else 0
44+
return percent, with_docs, total
45+
46+
47+
if __name__ == "__main__":
48+
p, w, t = calculate_doc_coverage("src/openapi_client")
49+
print(f"{p:.2f}")

scripts/pre-commit

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/usr/bin/env bash
2+
3+
set -e
4+
5+
echo "[pre-commit] Calculating coverage and updating badges..."
6+
uv run python scripts/update_badges.py
7+
8+
if git diff --name-only | grep -q "README.md"; then
9+
echo "[pre-commit] README.md was updated with new coverage badges. Staging changes..."
10+
git add README.md
11+
fi

scripts/update_badges.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import json
2+
import re
3+
import subprocess
4+
import sys
5+
import os
6+
7+
8+
def run_tests():
9+
print("Running tests and coverage...")
10+
subprocess.run(
11+
[
12+
"uv",
13+
"run",
14+
"pytest",
15+
"--cov=src/openapi_client",
16+
"--cov-report=json",
17+
"tests/",
18+
],
19+
check=True,
20+
)
21+
with open("coverage.json", "r") as f:
22+
data = json.load(f)
23+
test_cov = data["totals"]["percent_covered"]
24+
if os.path.exists("coverage.json"):
25+
os.remove("coverage.json")
26+
return test_cov
27+
28+
29+
def run_interrogate():
30+
print("Running interrogate...")
31+
res = subprocess.run(
32+
[
33+
"uv",
34+
"run",
35+
"interrogate",
36+
"--ignore-init-method",
37+
"--ignore-init-module",
38+
"src/openapi_client/",
39+
],
40+
capture_output=True,
41+
text=True,
42+
)
43+
match = re.search(r"actual: ([\d.]+)%", res.stdout)
44+
if match:
45+
return float(match.group(1))
46+
match = re.search(r"actual: ([\d.]+)%", res.stdout + res.stderr)
47+
if match:
48+
return float(match.group(1))
49+
print("Could not find interrogate coverage")
50+
return 0.0
51+
52+
53+
def update_readme(test_cov, doc_cov):
54+
print(f"Test Coverage: {test_cov:.1f}%")
55+
print(f"Doc Coverage: {doc_cov:.1f}%")
56+
57+
with open("README.md", "r") as f:
58+
content = f.read()
59+
60+
def get_color(cov):
61+
if cov >= 95:
62+
return "brightgreen"
63+
if cov >= 80:
64+
return "green"
65+
if cov >= 70:
66+
return "yellow"
67+
if cov >= 60:
68+
return "orange"
69+
return "red"
70+
71+
test_color = get_color(test_cov)
72+
doc_color = get_color(doc_cov)
73+
74+
test_badge = f"![Test Coverage](https://img.shields.io/badge/Test_Coverage-{test_cov:.1f}%25-{test_color})"
75+
doc_badge = f"![Doc Coverage](https://img.shields.io/badge/Doc_Coverage-{doc_cov:.1f}%25-{doc_color})"
76+
77+
has_test_badge = "![Test Coverage]" in content
78+
has_doc_badge = "![Doc Coverage]" in content
79+
80+
if has_test_badge and has_doc_badge:
81+
content = re.sub(
82+
r"!\[Test Coverage\]\(https://img\.shields\.io/badge/Test_Coverage-[^\)]+\)",
83+
test_badge,
84+
content,
85+
)
86+
content = re.sub(
87+
r"!\[Doc Coverage\]\(https://img\.shields\.io/badge/Doc_Coverage-[^\)]+\)",
88+
doc_badge,
89+
content,
90+
)
91+
else:
92+
lines = content.split("\n")
93+
for i, line in enumerate(lines):
94+
if "[![uv]" in line:
95+
lines[i] = f"{line}\n{test_badge}\n{doc_badge}"
96+
break
97+
content = "\n".join(lines)
98+
99+
with open("README.md", "w") as f:
100+
f.write(content)
101+
102+
103+
if __name__ == "__main__":
104+
try:
105+
t_cov = run_tests()
106+
d_cov = run_interrogate()
107+
update_readme(t_cov, d_cov)
108+
except Exception as e:
109+
print(f"Failed to update badges: {e}")
110+
sys.exit(1)

src/openapi_client/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@
55
from openapi_client.routes.emit import ClientGenerator
66
from openapi_client.routes.parse import extract_from_code
77
from openapi_client.models import OpenAPI
8+
9+
__all__ = ["ClientGenerator", "extract_from_code", "OpenAPI"]

src/openapi_client/classes/emit.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
Module for emitting Python classes (Pydantic models) from OpenAPI schemas.
33
"""
44

5-
from typing import Dict, List, Optional, Union
5+
from typing import Dict, List
66
import libcst as cst
77
from openapi_client.models import Schema, Reference, SchemaOrReference
88

0 commit comments

Comments
 (0)