ebay_client/scripts/generate_clients.py

126 lines
3.8 KiB
Python

from __future__ import annotations
import argparse
import shutil
import subprocess
from dataclasses import dataclass
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
GENERATED_ROOT = ROOT / "ebay_client" / "generated"
CODEGEN_PYTHON = ROOT / ".venv_codegen" / "Scripts" / "python.exe"
@dataclass(frozen=True)
class ApiSpec:
name: str
spec_path: Path
output_path: Path
API_SPECS = {
"media": ApiSpec(
name="media",
spec_path=ROOT / "commerce_media_v1_beta_oas3.yaml",
output_path=GENERATED_ROOT / "media",
),
"notification": ApiSpec(
name="notification",
spec_path=ROOT / "commerce_notification_v1_oas3.yaml",
output_path=GENERATED_ROOT / "notification",
),
"inventory": ApiSpec(
name="inventory",
spec_path=ROOT / "sell_inventory_v1_oas3.yaml",
output_path=GENERATED_ROOT / "inventory",
),
"fulfillment": ApiSpec(
name="fulfillment",
spec_path=ROOT / "sell_fulfillment_v1_oas3.yaml",
output_path=GENERATED_ROOT / "fulfillment",
),
"account": ApiSpec(
name="account",
spec_path=ROOT / "sell_account_v1_oas3.yaml",
output_path=GENERATED_ROOT / "account",
),
"feed": ApiSpec(
name="feed",
spec_path=ROOT / "sell_feed_v1_oas3.yaml",
output_path=GENERATED_ROOT / "feed",
),
}
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Generate isolated low-level clients from the eBay OpenAPI contracts.")
parser.add_argument("--api", choices=sorted(API_SPECS), help="Generate only one API package.")
parser.add_argument("--fail-on-warning", action="store_true", help="Fail if the generator emits warnings.")
return parser.parse_args()
def run_generation(spec: ApiSpec, *, fail_on_warning: bool) -> None:
if not spec.spec_path.exists():
raise FileNotFoundError(f"OpenAPI spec not found: {spec.spec_path}")
if not CODEGEN_PYTHON.exists():
raise FileNotFoundError(
"Code generation interpreter not found. Create .venv_codegen with a supported Python version first."
)
if spec.output_path.exists():
shutil.rmtree(spec.output_path)
spec.output_path.mkdir(parents=True, exist_ok=True)
command = [
str(CODEGEN_PYTHON),
"-m",
"datamodel_code_generator",
"--input",
str(spec.spec_path),
"--input-file-type",
"openapi",
"--output",
str(spec.output_path / "models.py"),
"--output-model-type",
"pydantic_v2.BaseModel",
"--target-python-version",
"3.11",
"--reuse-model",
"--use-schema-description",
"--field-constraints",
"--use-double-quotes",
]
if fail_on_warning:
command.append("--disable-warnings")
subprocess.run(command, check=True, cwd=str(ROOT))
normalize_generated_module(spec.output_path / "models.py")
(spec.output_path / "__init__.py").write_text(
'"""Generated Pydantic models from the OpenAPI contract."""\n\nfrom .models import *\n',
encoding="utf-8",
)
def normalize_generated_module(file_path: Path) -> None:
raw_bytes = file_path.read_bytes()
try:
content = raw_bytes.decode("utf-8")
except UnicodeDecodeError:
content = raw_bytes.decode("cp1252")
content = content.replace("\u00a0", " ")
file_path.write_text(content, encoding="utf-8")
def main() -> int:
args = parse_args()
specs = [API_SPECS[args.api]] if args.api else [API_SPECS[name] for name in sorted(API_SPECS)]
for spec in specs:
print(f"Generating {spec.name} from {spec.spec_path.name} -> {spec.output_path}")
run_generation(spec, fail_on_warning=args.fail_on_warning)
return 0
if __name__ == "__main__":
raise SystemExit(main())