openapi-client-codegen 0.1.5__tar.gz → 0.1.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. openapi_client_codegen-0.1.7/.github/workflows/ci.yml +44 -0
  2. {openapi_client_codegen-0.1.5 → openapi_client_codegen-0.1.7}/AGENTS.md +11 -11
  3. {openapi_client_codegen-0.1.5 → openapi_client_codegen-0.1.7}/PKG-INFO +2 -1
  4. openapi_client_codegen-0.1.7/README.md +70 -0
  5. {openapi_client_codegen-0.1.5 → openapi_client_codegen-0.1.7}/pyproject.toml +2 -1
  6. openapi_client_codegen-0.1.7/src/openapi_client_codegen/cli.py +80 -0
  7. {openapi_client_codegen-0.1.5 → openapi_client_codegen-0.1.7}/src/openapi_client_codegen/client.py +8 -2
  8. openapi_client_codegen-0.1.7/src/openapi_client_codegen/orchestrator.py +37 -0
  9. openapi_client_codegen-0.1.7/tests/test_cli.py +107 -0
  10. {openapi_client_codegen-0.1.5 → openapi_client_codegen-0.1.7}/tests/test_orchestrate.py +71 -43
  11. {openapi_client_codegen-0.1.5 → openapi_client_codegen-0.1.7}/uv.lock +103 -0
  12. openapi_client_codegen-0.1.5/.github/workflows/ci.yml +0 -27
  13. openapi_client_codegen-0.1.5/README.md +0 -74
  14. openapi_client_codegen-0.1.5/src/openapi_client_codegen/cli.py +0 -91
  15. openapi_client_codegen-0.1.5/src/openapi_client_codegen/naming.py +0 -26
  16. openapi_client_codegen-0.1.5/src/openapi_client_codegen/orchestrator.py +0 -95
  17. openapi_client_codegen-0.1.5/tests/test_naming.py +0 -19
  18. {openapi_client_codegen-0.1.5 → openapi_client_codegen-0.1.7}/.gitattributes +0 -0
  19. {openapi_client_codegen-0.1.5 → openapi_client_codegen-0.1.7}/.gitignore +0 -0
  20. {openapi_client_codegen-0.1.5 → openapi_client_codegen-0.1.7}/.python-version +0 -0
  21. {openapi_client_codegen-0.1.5 → openapi_client_codegen-0.1.7}/CLAUDE.md +0 -0
  22. {openapi_client_codegen-0.1.5 → openapi_client_codegen-0.1.7}/LICENSE +0 -0
  23. {openapi_client_codegen-0.1.5 → openapi_client_codegen-0.1.7}/NOTICE +0 -0
  24. {openapi_client_codegen-0.1.5 → openapi_client_codegen-0.1.7}/publish-config.json +0 -0
  25. {openapi_client_codegen-0.1.5 → openapi_client_codegen-0.1.7}/ruff.base.toml +0 -0
  26. {openapi_client_codegen-0.1.5 → openapi_client_codegen-0.1.7}/ruff.toml +0 -0
  27. {openapi_client_codegen-0.1.5 → openapi_client_codegen-0.1.7}/src/openapi_client_codegen/__init__.py +0 -0
  28. {openapi_client_codegen-0.1.5 → openapi_client_codegen-0.1.7}/src/openapi_client_codegen/_data/configs/csharp.json +0 -0
  29. {openapi_client_codegen-0.1.5 → openapi_client_codegen-0.1.7}/src/openapi_client_codegen/_data/configs/python.json +0 -0
  30. {openapi_client_codegen-0.1.5 → openapi_client_codegen-0.1.7}/src/openapi_client_codegen/_data/openapi-generator-ignore +0 -0
  31. {openapi_client_codegen-0.1.5 → openapi_client_codegen-0.1.7}/src/openapi_client_codegen/_data/templates-patches/csharp/0001-csharp-remove-errant-comma.patch +0 -0
  32. {openapi_client_codegen-0.1.5 → openapi_client_codegen-0.1.7}/src/openapi_client_codegen/_data/templates-patches/csharp/0002-csharp-models-required-only-constructors.patch +0 -0
  33. {openapi_client_codegen-0.1.5 → openapi_client_codegen-0.1.7}/src/openapi_client_codegen/_data/templates-patches/csharp/0003-csharp-httpclient-use-defaultT.patch +0 -0
  34. {openapi_client_codegen-0.1.5 → openapi_client_codegen-0.1.7}/src/openapi_client_codegen/_data/templates-patches/csharp/0004-csharp-support-hybrid-multipart-payloads.patch +0 -0
  35. {openapi_client_codegen-0.1.5 → openapi_client_codegen-0.1.7}/src/openapi_client_codegen/_data/templates-patches/csharp/0006-csharp-streaming-download-response.patch +0 -0
  36. {openapi_client_codegen-0.1.5 → openapi_client_codegen-0.1.7}/src/openapi_client_codegen/_data/templates-patches/csharp/0007-csharp-model-default-initializers.patch +0 -0
  37. {openapi_client_codegen-0.1.5 → openapi_client_codegen-0.1.7}/src/openapi_client_codegen/_data/templates-patches/csharp/0008-csharp-redirect-documentation-file.patch +0 -0
  38. {openapi_client_codegen-0.1.5 → openapi_client_codegen-0.1.7}/src/openapi_client_codegen/downgrade.py +0 -0
  39. {openapi_client_codegen-0.1.5 → openapi_client_codegen-0.1.7}/src/openapi_client_codegen/py.typed +0 -0
  40. {openapi_client_codegen-0.1.5 → openapi_client_codegen-0.1.7}/src/openapi_client_codegen/resources.py +0 -0
  41. {openapi_client_codegen-0.1.5 → openapi_client_codegen-0.1.7}/src/openapi_client_codegen/templates.py +0 -0
  42. {openapi_client_codegen-0.1.5 → openapi_client_codegen-0.1.7}/src/openapi_client_codegen/unity.py +0 -0
  43. {openapi_client_codegen-0.1.5 → openapi_client_codegen-0.1.7}/tests/test_downgrade.py +0 -0
  44. {openapi_client_codegen-0.1.5 → openapi_client_codegen-0.1.7}/tests/test_unity.py +0 -0
@@ -0,0 +1,44 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ concurrency:
10
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ jobs:
14
+ check:
15
+ runs-on: ubuntu-latest
16
+ permissions:
17
+ contents: read
18
+ steps:
19
+ - uses: actions/checkout@v5
20
+
21
+ - uses: astral-sh/setup-uv@v7
22
+ with:
23
+ enable-cache: true
24
+
25
+ - name: Preflight
26
+ run: uvx --from python-devkit==0.1.5 preflight-python
27
+
28
+ publish:
29
+ needs: check
30
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
31
+ runs-on: ubuntu-latest
32
+ permissions:
33
+ contents: write
34
+ id-token: write
35
+ steps:
36
+ - uses: actions/checkout@v5
37
+
38
+ # Workaround: fetch-tags is broken with shallow clones
39
+ - run: git fetch --tags origin
40
+
41
+ - uses: astral-sh/setup-uv@v7
42
+
43
+ - name: Publish
44
+ run: uvx --from release-devkit==0.1.3 publish-packages --config publish-config.json
@@ -2,30 +2,30 @@
2
2
 
3
3
  ## What this is
4
4
 
5
- `openapi-client-codegen` wraps [OpenAPI Generator](https://openapi-generator.tech/) to turn an OpenAPI schema into typed client packages — a Python (`httpx`) client and a Unity-consumable C# (`httpclient`) client. It owns every step that is generic across consumers: the project orchestration loop, downgrading 3.1→3.0, the committed-spec unchanged skip, authoring and patching the C# templates, running the generator, and writing the Unity package metadata. The caller owns what is inherently project-specific: **producing the OpenAPI spec** (injected as a required strategy — any callable from project directory to raw spec JSON; the org's uv convention ships as `dump_openapi_spec`), the project→client mapping, the naming root, the npm scope, license, repository URL, and the generated output root.
5
+ `openapi-client-codegen` wraps [OpenAPI Generator](https://openapi-generator.tech/) to turn an OpenAPI schema into typed client packages — a Python (`httpx`) client and a Unity-consumable C# (`httpclient`) client. It owns every step that is generic across consumers: the project orchestration loop, downgrading 3.1→3.0, the committed-spec unchanged skip, authoring and patching the C# templates, running the generator, and writing the Unity package metadata. The caller owns what is inherently project-specific: **producing the OpenAPI spec** (a command string in the consumer's config, run by the library's `SpecProducer`), the project→client mapping, the naming root, the npm scope, license, repository URL, and the generated output root — all as fields of the `ProjectsConfig` model.
6
6
 
7
- The package is `openapi_client_codegen` (src-layout under `src/openapi_client_codegen/`), renamed from `openapi-clientgen` before the first publish (operator, 2026-09-20 — no artifact carries the old name). Its Python dependencies are `bashrun` (the guardrailed subprocess wrapper) and `typer` (the thin CLI), both from PyPI — git-source pins only in scratch branches testing unreleased changes. At **runtime** it also needs **Java (JDK 11+)** and **`uvx`** on PATH — the generator itself runs as `uvx --from 'openapi-generator-cli[jdk4py]==<pin>' ...`.
7
+ The package is `openapi_client_codegen` (src-layout under `src/openapi_client_codegen/`), renamed from `openapi-clientgen` before the first publish (operator, 2026-09-20 — no artifact carries the old name). Its Python dependencies are `bashrun` (the guardrailed subprocess wrapper), `pydantic` (the config schema), and `typer` (the thin CLI), all from PyPI — git-source pins only in scratch branches testing unreleased changes. At **runtime** it also needs **Java (JDK 11+)** and **`uvx`** on PATH — the generator itself runs as `uvx --from 'openapi-generator-cli[jdk4py]==<pin>' ...`.
8
8
 
9
9
  ## Release flow
10
10
 
11
- Publishing rides `ci.yml`'s `publish` job on every push to `main` (gated on the check job): release-devkit's publish composite action (pinned by SHA; the uvx invocation runs inside the caller's job, keeping OIDC identity local), never a project dependency (openapi-client-codegen sits inside release-devkit's transitive dependency graph; a project-level edge is a resolver cycle) — computes the plan from the tag ledger and path-diff, patches the version ephemerally, and publishes to PyPI under OIDC trusted publishing (pending publisher bound to `ci.yml`, no environment). The committed `pyproject.toml` version is permanently the `0.0.0.dev0` sentinel; the `openapi-client-codegen-v*` tags are the version ledger (first release `0.1.0`, patch-auto thereafter). API-breaking changes ship with a manually bumped version — patch-auto assumes additive changes.
11
+ Publishing rides `ci.yml`'s `publish` job on every push to `main` (gated on the check job): release-devkit's `publish-packages` (an inlined, version-pinned `uvx` step in the publish job, keeping OIDC identity local), never a project dependency (openapi-client-codegen sits inside release-devkit's transitive dependency graph; a project-level edge is a resolver cycle) — computes the plan from the tag ledger and path-diff, patches the version ephemerally, and publishes to PyPI under OIDC trusted publishing (pending publisher bound to `ci.yml`, no environment). The committed `pyproject.toml` version is permanently the `0.0.0.dev0` sentinel; the `openapi-client-codegen-v*` tags are the version ledger (first release `0.1.0`, patch-auto thereafter). API-breaking changes ship with a manually bumped version — patch-auto assumes additive changes.
12
12
 
13
13
  ## Public API
14
14
 
15
- The package init is empty (RUF067 strict mode — the canonical ruff config bans any content in `__init__.py`); import each symbol from its defining module: `client` (`generate_client`), `downgrade` (`downgrade_openapi_3_1_to_3_0`, `JsonDict`), `naming` (`ClientNaming`, `DefaultNamingPolicy`, `NamingPolicy`), `orchestrator` (`generate_projects`, `dump_openapi_spec`, `SpecProducer`), `templates` (`regenerate_templates`), `unity` (`write_unity_package_metadata`).
15
+ The package init is empty (RUF067 strict mode — the canonical ruff config bans any content in `__init__.py`); import each symbol from its defining module: `client` (`generate_client`), `cli` (`app`), `downgrade` (`downgrade_openapi_3_1_to_3_0`, `JsonDict`), `orchestrator` (`ProjectsConfig`, `ClientNaming`, `SpecProducer`), `templates` (`regenerate_templates`), `unity` (`write_unity_package_metadata`).
16
16
 
17
17
  | Symbol | Role |
18
18
  |---|---|
19
19
  | `downgrade_openapi_3_1_to_3_0(schema)` | In-place 3.1.0→3.0.3 fixup (openapi-generator rejects 3.1). Pure function. |
20
- | `generate_projects(projects, root_name, generated_root, dump_spec, npm_scope=None, license_spdx=None, repository_url=None, project=None, client=None, no_cache=False, root=Path())` | The project orchestrator: per mapping entry (`{project path: [generators]}`), produce the spec via the injected `dump_spec` strategy, downgrade it, write `<project>/openapi.json` unless byte-identical to the committed spec (`no_cache` forces through), and `generate_client` per listed generator into `<generated_root>/<generator>/<names.base>`. `project` / `client` restrict the run to one entry; `root` is what project paths resolve against. |
21
- | `SpecProducer(command)` | Callable class wrapping any shell command that prints the spec to stdout, run with the project directory as cwd; the CLI `--spec-command` bridge. The strategy contract itself is untyped vocabulary — any `Callable[[Path], str]` (project directory in, raw OpenAPI JSON out) satisfies `dump_spec`. |
22
- | `dump_openapi_spec(project)` | The org strategy: `uv run --project . python -m src.dump_openapi` with `CODEGEN=1` gating heavy imports (per-call env overlay). Shipped as the exemplar — `dump_spec` is required, so every consumer names its strategy explicitly. |
20
+ | `ProjectsConfig` | The pydantic schema of a consumer's client-generation config (`extra="forbid"`): required `projects`, `root_name`, `generated_root`, `spec_command`; optional `spec_env`, `npm_scope`, `license_spdx`, `repository_url`. The single source of identity — the CLI validates the `--config` file into it and consumes it; nothing overrides it per invocation. |
21
+ | `SpecProducer(command, env=None)` | Callable class wrapping any command that prints the spec to stdout, run with the project directory as cwd and an optional per-call env overlay; the CLI command builds it from the config's `spec_command` / `spec_env`, and programmatic consumers construct it directly. |
22
+ | `app` | The single-command CLI (invoked directly, no subcommand) behind the `openapi-client-codegen` console script and consumer `[project.scripts]` aliases (e.g. placeframe's `generate-clients`). It validates the `--config` JSON against the `ProjectsConfig` pydantic model (`extra="forbid"`: unknown keys rejected — a `ValidationError` propagates) and then runs the whole orchestration itself: per mapping entry (`projects` maps project paths to generator lists), produce the spec via `SpecProducer`, downgrade it, write `<project>/openapi.json` unless byte-identical to the committed spec (`no_cache` forces through), and `generate_client` per listed generator into `<generated_root>/<generator>/<names.base>`. The flags are invocation-scoped only (filters, `--no-cache`, `--root`; project paths resolve against `root`). Commands run shell-less (shlex-split, operators rejected), so env gating is the structured `spec_env`, never a `VAR=…` prefix. |
23
23
  | `regenerate_templates(target_dir, extra_patches_dir=None)` | Author the C# templates into `target_dir/csharp` and apply the shipped (then optional extra) patches. |
24
24
  | `generate_client(spec, generator, output_dir, names, templates_dir=None, extra_references=None, npm_scope=None, license_spdx=None, repository_url=None)` | Run the generator for one `(spec, generator)` and sync the result into `output_dir`. `templates_dir` is required for `csharp`. For csharp, `npm_scope` composes the UPM identity `{scope}.{base-minus-dashes}` (e.g. `org.outernet.placeframe.apiclient`); unset falls back to the legacy `org.nuget.{camel-lower}` identity. `license_spdx` / `repository_url` populate the manifest's `license` / `repository` fields when given. |
25
25
  | `write_unity_package_metadata(package_dir, package_name, extra_references=None, npm_name=None, license_spdx=None, repository_url=None)` | Write `package.json` / `.asmdef` / `csc.rsp` / `Directory.Build.props`. Called by `generate_client` for csharp. |
26
- | `DefaultNamingPolicy(root_name)` / `ClientNaming` / `NamingPolicy` | Package-name derivation. `DefaultNamingPolicy("placeframe")("docker/api")` → base `api-client`, dashed `placeframe-api-client`, underscored `placeframe_api_client`, camel `PlaceframeApiClient`. |
26
+ | `ClientNaming` | The derived per-project package identity: `base` (last path segment + `-client`), `dashed` (`{root_name}-{base}`), `underscored`, `camel`. Constructed inline by the CLI command per project; `DefaultNamingPolicy("placeframe")("docker/api")` semantics live there. |
27
27
 
28
- `generate_projects` orchestrates the whole run — `regenerate_templates` once, then per project: produce (via the injected strategy) → downgrade → committed-spec skip → per-client generate. Consumers hand it their parsed mapping, their spec-production strategy, and their identity parameters. The generator version pin and the shipped configs/patches/ignore-file live in `src/openapi_client_codegen/_data/` (paths in `_data.py`).
28
+ The CLI command orchestrates the whole run — `regenerate_templates` once, then per project: produce (via the config-built `SpecProducer`) → downgrade → committed-spec skip → per-client generate. The generator version pin and the shipped configs/patches/ignore-file live in `src/openapi_client_codegen/_data/` (paths in `_data.py`).
29
29
 
30
30
  ## Constraints
31
31
 
@@ -37,7 +37,7 @@ To author a new patch: author the raw templates (`openapi-generator-cli author t
37
37
 
38
38
  ### Generator env vars are passed per-call, never set at module level
39
39
 
40
- `JAVA_OPTS=-Dlog.level=warn` (quiets the generator) is passed as `bash(..., env={"JAVA_OPTS": "-Dlog.level=warn"})` on the two `openapi-generator-cli` calls that need it, so it reaches only those child processes and never touches this process's `os.environ`. `bashrun`'s `env=` overlays the inherited environment for that one child (see its AGENTS.md), which is the whole point — the alternative, mutating the global `os.environ`, leaks the var into every later subprocess. This covers only the generator this package runs itself. The shipped `dump_openapi_spec` strategy follows the same rule: `CODEGEN=1` (which gates a service's heavy imports so the app imports cleanly for the dump) rides bashrun's per-call env overlay and never touches this process's `os.environ`.
40
+ `JAVA_OPTS=-Dlog.level=warn` (quiets the generator) is passed as `bash(..., env={"JAVA_OPTS": "-Dlog.level=warn"})` on the two `openapi-generator-cli` calls that need it, so it reaches only those child processes and never touches this process's `os.environ`. `bashrun`'s `env=` overlays the inherited environment for that one child (see its AGENTS.md), which is the whole point — the alternative, mutating the global `os.environ`, leaks the var into every later subprocess. This covers the generator this package runs itself; a consumer's spec command needing the same gating (e.g. `CODEGEN=1`) carries it in the config's structured `spec_env` key, which rides the same per-call overlay.
41
41
 
42
42
  ### The Unity reference set is coupled to the patched templates
43
43
 
@@ -51,7 +51,7 @@ The generated `.csproj` stays inside the UPM package because it is the `dotnet p
51
51
 
52
52
  ### What the consumer owns
53
53
 
54
- The spec-production strategy (`dump_spec`, required — the package is consumable by projects that use no Python, no uv, or no dump entry point at all; the org's `dump_openapi_spec` ships beside it, and a project whose spec is a committed file needs only a file read), the project→clients mapping (in whatever config shape it likes — the verb takes the parsed mapping, not a file path), the naming root, the npm scope, license, repository URL, and the generated output root. The orchestrator iterates the mapping itself and fixes `DefaultNamingPolicy`; a consumer needing a different policy calls the per-client API directly — `NamingPolicy` is a `Protocol`, any callable `(project: str) -> ClientNaming` works there.
54
+ The spec-production command (`spec_command` + optional `spec_env`, required — the package is consumable by projects that use no Python, no uv, or no dump entry point at all; a project whose spec is a committed file needs only `cat openapi.json`), the project→clients mapping, the naming root, the npm scope, license, repository URL, and the generated output root — all fields of the `ProjectsConfig` model, all carried in the consumer's config file, none overridable per invocation. Naming derivation is fixed inside the CLI command (`ClientNaming` built inline per project from the root name and the project's last path segment); a consumer needing different names calls the per-client API directly with its own `ClientNaming`.
55
55
 
56
56
  ## See also
57
57
 
@@ -1,9 +1,10 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: openapi-client-codegen
3
- Version: 0.1.5
3
+ Version: 0.1.7
4
4
  Summary: OpenAPI client-generation wrapper: Litestar spec dump, 3.1->3.0 downgrade, patched openapi-generator templates, Unity package metadata
5
5
  License-File: LICENSE
6
6
  License-File: NOTICE
7
7
  Requires-Python: >=3.13
8
8
  Requires-Dist: bashrun>=0.1.0
9
+ Requires-Dist: pydantic>=2.11.7
9
10
  Requires-Dist: typer>=0.17.4
@@ -0,0 +1,70 @@
1
+ # openapi-client-codegen
2
+
3
+ Generate typed API clients from an OpenAPI schema. `openapi-client-codegen` wraps [OpenAPI Generator](https://openapi-generator.tech/): it downgrades OpenAPI 3.1→3.0 (the generator only accepts 3.0), authors and patches the C# templates, runs the generator, and writes Unity package metadata. It produces a Python (`httpx`) client and a Unity-consumable C# (`httpclient`) client.
4
+
5
+ The generic steps live here; the caller supplies the spec-production strategy (any callable from project directory to raw spec JSON — a shell command carried in the CLI config), the project→client mapping, the naming root, the npm scope, license, repository URL, and the output root. See [`AGENTS.md`](./AGENTS.md) for the API surface, the lib-owns/consumer-owns boundary, and the patch mechanism.
6
+
7
+ ## Requirements
8
+
9
+ - Python 3.13+ and [uv](https://docs.astral.sh/uv/)
10
+ - **Java (JDK 11+)** and **`uvx`** on PATH at runtime — the generator runs as `uvx --from 'openapi-generator-cli[jdk4py]==<pin>' ...`
11
+
12
+ ## CLI
13
+
14
+ The CLI is the multi-project orchestrator, invoked directly (single command, no subcommand):
15
+
16
+ ```bash
17
+ uv run openapi-client-codegen --config clients.json
18
+ ```
19
+
20
+ `clients.json` carries everything consumer-owned in one place:
21
+
22
+ ```json
23
+ {
24
+ "projects": {"services/api": ["python", "csharp"]},
25
+ "root_name": "myproject",
26
+ "generated_root": "generated",
27
+ "spec_command": "uv run --project . python -m src.dump_openapi",
28
+ "spec_env": {"CODEGEN": "1"},
29
+ "npm_scope": "org.example.myproject",
30
+ "license_spdx": "Apache-2.0",
31
+ "repository_url": "https://github.com/org/repo.git"
32
+ }
33
+ ```
34
+
35
+ `spec_command` is a command that prints the project's raw OpenAPI JSON to stdout, run with the project directory as cwd — no shell sits in front of it (commands are shlex-split; operators like `|` are rejected), so env gating rides the structured `spec_env` key, overlaid per call. The config is the single source of identity: `root_name`, `generated_root`, and `spec_command` are required there, and nothing overrides them per invocation — a different identity is a different config file. The remaining flags are invocation-scoped (`--project` / `--client` filters, `--no-cache`, `--root`). The orchestrator downgrades each produced spec, skips generation when the committed `<project>/openapi.json` is byte-identical (`--no-cache` forces through), and syncs each client under `<generated-root>/<generator>/<base-name>`.
36
+
37
+ To run it as a first-class command in a consumer repo, point a `[project.scripts]` entry at the same app — e.g. `generate-clients = "openapi_client_codegen.cli:app"`.
38
+
39
+ The same config drives the library API, for in-process consumption from a consumer's own command:
40
+
41
+ ```python
42
+ from pathlib import Path
43
+
44
+ from openapi_client_codegen.orchestrator import ProjectsConfig, generate_projects
45
+
46
+ config = ProjectsConfig.model_validate_json(Path("clients.json").read_text(encoding="utf-8"))
47
+ generate_projects(config)
48
+ ```
49
+
50
+ A non-uv project declares its own `spec_command` instead — e.g. `dotnet run -- dump-openapi`; a project whose spec is a committed file uses `cat openapi.json`.
51
+
52
+ ## Consuming from another repo
53
+
54
+ Install from PyPI:
55
+
56
+ ```toml
57
+ [project]
58
+ dependencies = ["openapi-client-codegen>=0.1.0"]
59
+ ```
60
+
61
+ `bashrun` resolves transitively. At **runtime** the generator also needs Java (JDK 11+) and `uvx` on PATH. To test an unreleased change, pin the repo at a git ref in a scratch branch instead (`openapi-client-codegen = { git = "…", rev = "<sha>" }` under `[tool.uv.sources]`) and drop the pin when the release lands.
62
+
63
+ ## Development
64
+
65
+ ```bash
66
+ uv run ruff check .
67
+ uv run ruff format --check .
68
+ uv run basedpyright
69
+ uv run pytest
70
+ ```
@@ -1,10 +1,11 @@
1
1
  [project]
2
2
  name = "openapi-client-codegen"
3
- version = "0.1.5"
3
+ version = "0.1.7"
4
4
  description = "OpenAPI client-generation wrapper: Litestar spec dump, 3.1->3.0 downgrade, patched openapi-generator templates, Unity package metadata"
5
5
  requires-python = ">=3.13"
6
6
  dependencies = [
7
7
  "bashrun>=0.1.0",
8
+ "pydantic>=2.11.7",
8
9
  "typer>=0.17.4",
9
10
  ]
10
11
 
@@ -0,0 +1,80 @@
1
+ import json
2
+ from pathlib import Path
3
+ from tempfile import TemporaryDirectory
4
+ from typing import Annotated
5
+
6
+ from typer import Option, Typer
7
+
8
+ from .client import generate_client
9
+ from .downgrade import downgrade_openapi_3_1_to_3_0
10
+ from .orchestrator import ClientNaming, ProjectsConfig, SpecProducer
11
+ from .templates import regenerate_templates
12
+
13
+ app = Typer(pretty_exceptions_show_locals=False)
14
+
15
+
16
+ @app.command()
17
+ def generate_projects_command(
18
+ config: Annotated[
19
+ Path, Option(help="JSON config carrying the projects mapping, the client identity, and the spec command")
20
+ ],
21
+ project: Annotated[str | None, Option(help="Generate only this project, as keyed in the config")] = None,
22
+ client: Annotated[str | None, Option(help="Generate only this client generator")] = None,
23
+ no_cache: Annotated[
24
+ bool, Option("--no-cache", help="Regenerate even when the committed spec is unchanged")
25
+ ] = False,
26
+ root: Annotated[Path, Option(help="Repository root the project paths resolve against")] = Path(),
27
+ ) -> None:
28
+ settings = ProjectsConfig.model_validate_json(config.read_text(encoding="utf-8"))
29
+ root = root.resolve()
30
+ dump_spec = SpecProducer(settings.spec_command, env=settings.spec_env)
31
+
32
+ with TemporaryDirectory() as templates_directory_string:
33
+ templates_directory = Path(templates_directory_string)
34
+ regenerate_templates(templates_directory)
35
+
36
+ for project_name, clients in settings.projects.items():
37
+ if project is not None and project != project_name:
38
+ continue
39
+
40
+ project_directory = root / project_name
41
+ print(f"Producing OpenAPI spec for project: {project_directory}")
42
+
43
+ openapi_json = json.loads(dump_spec(project_directory))
44
+ downgrade_openapi_3_1_to_3_0(openapi_json)
45
+ openapi_spec = json.dumps(openapi_json, indent=2)
46
+
47
+ spec_path = project_directory / "openapi.json"
48
+
49
+ if spec_path.exists():
50
+ old_spec = spec_path.read_text(encoding="utf-8")
51
+
52
+ if openapi_spec == old_spec and not no_cache:
53
+ print("OpenAPI spec unchanged, skipping client generation")
54
+ continue
55
+
56
+ spec_path.write_text(openapi_spec, encoding="utf-8")
57
+
58
+ base = f"{project_name.rsplit('/', maxsplit=1)[-1]}-client"
59
+ dashed = f"{settings.root_name}-{base}"
60
+ names = ClientNaming(
61
+ base=base,
62
+ dashed=dashed,
63
+ underscored=dashed.replace("-", "_"),
64
+ camel=f"{settings.root_name.capitalize()}{''.join(part.capitalize() for part in base.split('-'))}",
65
+ )
66
+
67
+ for client_name in clients:
68
+ if client is not None and client_name != client:
69
+ continue
70
+
71
+ generate_client(
72
+ openapi_spec,
73
+ client_name,
74
+ settings.generated_root / client_name / names.base,
75
+ names,
76
+ templates_dir=templates_directory,
77
+ npm_scope=settings.npm_scope,
78
+ license_spdx=settings.license_spdx,
79
+ repository_url=settings.repository_url,
80
+ )
@@ -1,15 +1,21 @@
1
+ from __future__ import annotations
2
+
1
3
  import json
4
+ import sys
2
5
  from os import walk
3
6
  from pathlib import Path
4
7
  from shutil import copytree
5
8
  from tempfile import NamedTemporaryFile, TemporaryDirectory
9
+ from typing import TYPE_CHECKING
6
10
 
7
11
  from bashrun.bash import bash
8
12
 
9
13
  from .resources import CONFIGS_DIR, IGNORE_FILE, OPENAPI_GENERATOR_CLI_VERSION
10
- from .naming import ClientNaming
11
14
  from .unity import write_unity_package_metadata
12
15
 
16
+ if TYPE_CHECKING:
17
+ from .orchestrator import ClientNaming
18
+
13
19
  _HTTP_METHODS = ("get", "put", "post", "delete", "patch", "options", "head", "trace")
14
20
 
15
21
 
@@ -112,4 +118,4 @@ def generate_client(
112
118
  copytree(temporary_directory, output_dir, dirs_exist_ok=True)
113
119
 
114
120
  if generator == "python":
115
- bash(f"uv pip install {output_dir.resolve().as_posix()}")
121
+ bash(f"uv pip install --python {sys.executable} {output_dir.resolve().as_posix()}")
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+
6
+ from bashrun.bash import bash_output
7
+ from pydantic import BaseModel, ConfigDict
8
+
9
+
10
+ class ProjectsConfig(BaseModel):
11
+ model_config = ConfigDict(extra="forbid")
12
+
13
+ projects: dict[str, list[str]]
14
+ root_name: str
15
+ generated_root: Path
16
+ spec_command: str
17
+ spec_env: dict[str, str] | None = None
18
+ npm_scope: str | None = None
19
+ license_spdx: str | None = None
20
+ repository_url: str | None = None
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class ClientNaming:
25
+ base: str
26
+ dashed: str
27
+ underscored: str
28
+ camel: str
29
+
30
+
31
+ class SpecProducer:
32
+ def __init__(self, command: str, env: dict[str, str] | None = None) -> None:
33
+ self.command = command
34
+ self.env = env
35
+
36
+ def __call__(self, project: Path) -> str:
37
+ return bash_output(self.command, cwd=project, env=self.env)
@@ -0,0 +1,107 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ import pytest
5
+ from typer.testing import CliRunner
6
+
7
+ from openapi_client_codegen import cli, orchestrator
8
+ from openapi_client_codegen.cli import app
9
+ from openapi_client_codegen.orchestrator import ClientNaming
10
+
11
+ runner = CliRunner()
12
+
13
+ FULL_SETTINGS: dict[str, object] = {
14
+ "projects": {"docker/api": ["python", "csharp"]},
15
+ "root_name": "placeframe",
16
+ "generated_root": "packages/generated",
17
+ "spec_command": "uv run --project . python -m src.dump_openapi",
18
+ "spec_env": {"CODEGEN": "1"},
19
+ "npm_scope": "org.example.placeframe",
20
+ "license_spdx": "Apache-2.0",
21
+ "repository_url": "https://github.com/org/repo.git",
22
+ }
23
+
24
+
25
+ class Recorder:
26
+ def __init__(self) -> None:
27
+ self.generated: list[tuple[str, Path]] = []
28
+
29
+ def fake_bash_output(self, command: str, *, cwd: Path | None = None, env: dict[str, str] | None = None) -> str:
30
+ return json.dumps({"openapi": "3.1.0", "info": {"title": "demo", "version": "0.0.0"}, "paths": {}})
31
+
32
+ def fake_regenerate_templates(self, target_dir: Path, extra_patches_dir: Path | None = None) -> None:
33
+ pass
34
+
35
+ def fake_generate_client(
36
+ self,
37
+ spec: str,
38
+ generator: str,
39
+ output_dir: Path,
40
+ names: ClientNaming,
41
+ templates_dir: Path | None = None,
42
+ extra_references: list[str] | None = None,
43
+ npm_scope: str | None = None,
44
+ license_spdx: str | None = None,
45
+ repository_url: str | None = None,
46
+ ) -> None:
47
+ self.generated.append((generator, output_dir))
48
+
49
+
50
+ @pytest.fixture
51
+ def recorder(monkeypatch: pytest.MonkeyPatch) -> Recorder:
52
+ recorder = Recorder()
53
+ monkeypatch.setattr(orchestrator, "bash_output", recorder.fake_bash_output)
54
+ monkeypatch.setattr(cli, "regenerate_templates", recorder.fake_regenerate_templates)
55
+ monkeypatch.setattr(cli, "generate_client", recorder.fake_generate_client)
56
+ return recorder
57
+
58
+
59
+ def write_config(directory: Path, settings: dict[str, object]) -> Path:
60
+ config = directory / "clients.json"
61
+ config.write_text(json.dumps(settings), encoding="utf-8")
62
+ return config
63
+
64
+
65
+ def test_filter_flags_restrict_generation(tmp_path: Path, recorder: Recorder) -> None:
66
+ config = write_config(tmp_path, FULL_SETTINGS)
67
+ (tmp_path / "docker" / "api").mkdir(parents=True)
68
+
69
+ result = runner.invoke(
70
+ app,
71
+ ["--config", str(config), "--root", str(tmp_path), "--project", "docker/api", "--client", "csharp"],
72
+ )
73
+
74
+ assert result.exit_code == 0
75
+ assert recorder.generated == [("csharp", Path("packages/generated") / "csharp" / "api-client")]
76
+
77
+
78
+ def test_unknown_config_key_fails_loudly(tmp_path: Path, recorder: Recorder) -> None:
79
+ config = write_config(tmp_path, {"projects": {}, "rot_name": "placeframe"})
80
+
81
+ result = runner.invoke(app, ["--config", str(config)])
82
+
83
+ assert result.exit_code != 0
84
+ assert "rot_name" in str(result.exception)
85
+ assert recorder.generated == []
86
+
87
+
88
+ def test_missing_projects_mapping_fails(tmp_path: Path, recorder: Recorder) -> None:
89
+ config = write_config(tmp_path, {"root_name": "placeframe"})
90
+
91
+ result = runner.invoke(app, ["--config", str(config)])
92
+
93
+ assert result.exit_code != 0
94
+ assert "projects" in str(result.exception)
95
+ assert recorder.generated == []
96
+
97
+
98
+ def test_missing_required_field_fails(tmp_path: Path, recorder: Recorder) -> None:
99
+ settings = dict(FULL_SETTINGS)
100
+ del settings["spec_command"]
101
+ config = write_config(tmp_path, settings)
102
+
103
+ result = runner.invoke(app, ["--config", str(config)])
104
+
105
+ assert result.exit_code != 0
106
+ assert "spec_command" in str(result.exception)
107
+ assert recorder.generated == []