Skip to content

Commit a478f48

Browse files
committed
feat: add schema patcher
Allow us to patch the openapi schema during development or due to a slow release of mfd. Once the changes are part of the released spec, we can remove the override.
1 parent 5f140a8 commit a478f48

6 files changed

Lines changed: 547 additions & 294 deletions

File tree

.github/workflows/publish.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,16 @@ jobs:
7070
echo "Latest MFD tag: $MFD_TAG"
7171
gh api /repos/cloudbeds/mfd/tarball/$MFD_TAG | tar --strip-components=1 --wildcards -zxf - '*/public_accessa/api'
7272
73+
- name: Setup Python
74+
uses: actions/setup-python@v5
75+
with:
76+
python-version-file: .python-version
77+
78+
- name: Patch OpenAPI Spec
79+
run: |
80+
pip install pyyaml
81+
python scripts/patch_openapi_spec.py
82+
7383
- name: Get next version
7484
run: |
7585
if [ -n "${{ inputs.version }}" ]; then

CLAUDE.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Overview
6+
7+
Auto-generated Python SDK for the Cloudbeds PMS v1.3 API, produced by OpenAPI Generator 7.11.0. Nearly all code under `cloudbeds_pms_v1_3/` is generated — do not manually edit generated files.
8+
9+
## Commands
10+
11+
- **Install dependencies**: `uv sync` (dev) or `uv sync --locked --no-dev` (prod)
12+
- **Run all tests**: `pytest`
13+
- **Run a single test**: `pytest cloudbeds_pms_v1_3/test/test_<name>.py`
14+
- **Build**: `uv build`
15+
16+
## Code Generation
17+
18+
The SDK is regenerated from an OpenAPI spec using the config in `openapitools.json`:
19+
- Generator: `python` (OpenAPI Generator CLI 7.11.0)
20+
- Input spec: sourced from Cloudbeds MFD repository (fetched during CI)
21+
- Custom Mustache templates in `templates/` (`model_enum.mustache`, `model_generic.mustache`)
22+
- Key setting: `enumUnknownDefaultCase: true` — enums include an unknown default case
23+
24+
To regenerate: the CI workflow in `.github/workflows/publish.yaml` handles spec fetching, generation, version bumping, and publishing to PyPI.
25+
26+
## Architecture
27+
28+
```
29+
cloudbeds_pms_v1_3/
30+
├── api/ # 21 API endpoint classes (one per resource domain)
31+
├── models/ # ~318 Pydantic v2 models (from OpenAPI schemas)
32+
├── test/ # Unittest stubs (one per model, auto-generated)
33+
├── docs/ # Markdown docs (one per model/endpoint)
34+
├── api_client.py # Core HTTP client (request building, serialization)
35+
├── configuration.py # Auth config: API key (x-api-key header) and OAuth2
36+
├── exceptions.py # Exception hierarchy
37+
└── rest.py # REST layer using urllib3
38+
```
39+
40+
API classes use `@validate_call` from Pydantic and return typed model instances. Auth is configured via `Configuration` with either API key or OAuth2 bearer token.
41+
42+
## Version
43+
44+
Current version: `1.9.0` (tracked in `VERSION` file and `openapitools.json`). Version bumps happen automatically during the publish workflow.

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ testing = [
5050
[dependency-groups]
5151
dev = [
5252
"pytest",
53-
"coverage"
53+
"coverage",
54+
"pyyaml",
5455
]
5556

5657
[tool.setuptools.packages.find]

scripts/patch_openapi_spec.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
#!/usr/bin/env python3
2+
"""Patch the OpenAPI spec with temporary overrides before code generation.
3+
4+
Reads spec_overrides.yaml and applies each override to the OpenAPI spec
5+
defined in openapitools.json. This allows releasing SDK versions with type
6+
fixes while waiting for the upstream spec to be updated.
7+
8+
Usage:
9+
python scripts/patch_openapi_spec.py
10+
11+
The script is a no-op (exits 0) if spec_overrides.yaml doesn't exist or
12+
has no overrides defined.
13+
"""
14+
15+
import json
16+
import sys
17+
from pathlib import Path
18+
19+
import yaml
20+
21+
ROOT = Path(__file__).resolve().parent.parent
22+
OVERRIDES_FILE = ROOT / "spec_overrides.yaml"
23+
OPENAPITOOLS_FILE = ROOT / "openapitools.json"
24+
25+
26+
def load_overrides():
27+
if not OVERRIDES_FILE.exists():
28+
return []
29+
with open(OVERRIDES_FILE) as f:
30+
data = yaml.safe_load(f)
31+
if not data or not data.get("overrides"):
32+
return []
33+
return data["overrides"]
34+
35+
36+
def get_spec_path():
37+
with open(OPENAPITOOLS_FILE) as f:
38+
config = json.load(f)
39+
return ROOT / config["inputSpec"]
40+
41+
42+
def set_nested(doc, dotted_path, value):
43+
"""Set a value in a nested dict using a dot-separated path, creating intermediate keys as needed."""
44+
keys = dotted_path.split(".")
45+
current = doc
46+
for key in keys[:-1]:
47+
if key not in current:
48+
current[key] = {}
49+
current = current[key]
50+
current[keys[-1]] = value
51+
52+
53+
def apply_overrides(spec, overrides):
54+
applied = 0
55+
for override in overrides:
56+
path = override["path"]
57+
schema = override["schema"]
58+
set_nested(spec, path, schema)
59+
print(f" Patched: {path}")
60+
applied += 1
61+
return applied
62+
63+
64+
def main():
65+
overrides = load_overrides()
66+
if not overrides:
67+
print("No spec overrides found, skipping patch step.")
68+
return
69+
70+
spec_path = get_spec_path()
71+
if not spec_path.exists():
72+
print(f"Error: spec file not found at {spec_path}", file=sys.stderr)
73+
sys.exit(1)
74+
75+
with open(spec_path) as f:
76+
spec = yaml.safe_load(f)
77+
78+
print(f"Applying {len(overrides)} override(s) to {spec_path.name}:")
79+
applied = apply_overrides(spec, overrides)
80+
81+
with open(spec_path, "w") as f:
82+
yaml.dump(spec, f, default_flow_style=False, sort_keys=False, allow_unicode=True)
83+
84+
print(f"Done. {applied} override(s) applied.")
85+
86+
87+
if __name__ == "__main__":
88+
main()

spec_overrides.yaml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Temporary OpenAPI spec overrides.
2+
# Each entry patches a specific path in the spec before code generation.
3+
# Remove entries once the upstream spec includes the fix.
4+
#
5+
# Usage:
6+
# python scripts/patch_openapi_spec.py
7+
#
8+
# Path format: dot-separated keys into the spec YAML.
9+
# The "schema" value replaces whatever exists at that path.
10+
#
11+
# Examples:
12+
#
13+
# Override a property type (e.g. datetime -> datetime|string):
14+
# - path: "components.schemas.GetReservationsResponseDataInner.properties.dateCreated"
15+
# schema:
16+
# anyOf:
17+
# - type: string
18+
# format: date-time
19+
# - type: string
20+
# description: "Creation datetime (format: Y-m-d H:i:s)"
21+
#
22+
# Add a new property to an existing schema:
23+
# - path: "components.schemas.SomeModel.properties.newField"
24+
# schema:
25+
# type: string
26+
# description: "A field not yet in the upstream spec"
27+
28+
overrides: [ ]

0 commit comments

Comments
 (0)