Skip to content

Commit ddd531e

Browse files
committed
Add update command and shell completion
1 parent 5daeb9d commit ddd531e

File tree

8 files changed

+260
-57
lines changed

8 files changed

+260
-57
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
- Fix mypy issue in generated models `from_dict` with datetime or reference properties
99
- Generated clients now raise an `ApiResponseError` if they receive a response that was not declared
1010
- Stop including optional query parameters when value is set to None
11+
- Added an `update` command to update a previously generated client
12+
- Added click-completion for installable tab completion in most shells
1113

1214
## 0.1.0 - 2020-02-28
1315
- Initial Release

README.md

+6
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,19 @@ using it (Python developers).
2121
## Installation
2222
`pip install openapi-python-client`
2323

24+
Then, if you want tab completion: `openapi-python-client --install-completion`
25+
2426
## Usage
27+
### Create a new client
2528
`openapi-python-client generate --url https://my.api.com/openapi.json`
2629

2730
This will generate a new client library named based on the title in your OpenAPI spec. For example, if the title
2831
of your API is "My API", the expected output will be "my-api-client". If a folder already exists by that name, you'll
2932
get an error.
3033

34+
### Update an existing client
35+
`openapi-python-client update --url https://my.api.com/openapi.json`
36+
3137
## What You Get
3238
1. A `pyproject.toml` file with some basic metadata intended to be used with [Poetry].
3339
1. A `README.md` you'll most definitely need to update with your project's details

openapi_python_client/__init__.py

+37-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
""" Generate modern Python clients from OpenAPI """
2+
from __future__ import annotations
3+
24
import json
5+
import shutil
36
from pathlib import Path
47
from typing import Any, Dict, Optional
58

@@ -9,14 +12,24 @@
912
from .openapi_parser import OpenAPI, import_string_from_reference
1013

1114

12-
def main(*, url: Optional[str], path: Optional[Path]) -> None:
13-
""" Generate the client library """
15+
def _get_project_for_url_or_path(url: Optional[str], path: Optional[Path]) -> _Project:
1416
data_dict = _get_json(url=url, path=path)
1517
openapi = OpenAPI.from_dict(data_dict)
16-
project = _Project(openapi=openapi)
18+
return _Project(openapi=openapi)
19+
20+
21+
def create_new_client(*, url: Optional[str], path: Optional[Path]) -> None:
22+
""" Generate the client library """
23+
project = _get_project_for_url_or_path(url=url, path=path)
1724
project.build()
1825

1926

27+
def update_existing_client(*, url: Optional[str], path: Optional[Path]) -> None:
28+
""" Update an existing client library """
29+
project = _get_project_for_url_or_path(url=url, path=path)
30+
project.update()
31+
32+
2033
def _get_json(*, url: Optional[str], path: Optional[Path]) -> Dict[str, Any]:
2134
json_bytes: bytes
2235
if url is not None and path is not None:
@@ -41,29 +54,44 @@ def __init__(self, *, openapi: OpenAPI) -> None:
4154

4255
self.package_name: str = self.project_name.replace("-", "_")
4356
self.package_dir: Path = self.project_dir / self.package_name
57+
self.package_description = f"A client library for accessing {self.openapi.title}"
4458

4559
def build(self) -> None:
4660
""" Create the project from templates """
61+
4762
print(f"Generating {self.project_name}")
4863
self.project_dir.mkdir()
49-
self.package_dir.mkdir()
64+
self._create_package()
5065
self._build_metadata()
5166
self._build_models()
5267
self._build_api()
5368

54-
def _build_metadata(self) -> None:
69+
def update(self) -> None:
70+
""" Update an existing project """
71+
72+
if not self.package_dir.is_dir():
73+
raise FileNotFoundError()
74+
print(f"Updating {self.project_name}")
75+
shutil.rmtree(self.package_dir)
76+
self._create_package()
77+
self._build_models()
78+
self._build_api()
79+
80+
def _create_package(self) -> None:
81+
self.package_dir.mkdir()
5582
# Package __init__.py
5683
package_init = self.package_dir / "__init__.py"
57-
package_description = f"A client library for accessing {self.openapi.title}"
84+
5885
package_init_template = self.env.get_template("package_init.pyi")
59-
package_init.write_text(package_init_template.render(description=package_description))
86+
package_init.write_text(package_init_template.render(description=self.package_description))
6087

88+
def _build_metadata(self) -> None:
6189
# Create a pyproject.toml file
6290
pyproject_template = self.env.get_template("pyproject.toml")
6391
pyproject_path = self.project_dir / "pyproject.toml"
6492
pyproject_path.write_text(
6593
pyproject_template.render(
66-
project_name=self.project_name, package_name=self.package_name, description=package_description
94+
project_name=self.project_name, package_name=self.package_name, description=self.package_description
6795
)
6896
)
6997

@@ -72,7 +100,7 @@ def _build_metadata(self) -> None:
72100
readme_template = self.env.get_template("README.md")
73101
readme.write_text(
74102
readme_template.render(
75-
project_name=self.project_name, description=package_description, package_name=self.package_name
103+
project_name=self.project_name, description=self.package_description, package_name=self.package_name
76104
)
77105
)
78106

openapi_python_client/cli.py

+20-3
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33

44
import typer
55

6-
from . import main
7-
86
app = typer.Typer()
97

108

@@ -20,10 +18,29 @@ def generate(
2018
path: Optional[pathlib.Path] = typer.Option(None, help="A path to the JSON file"),
2119
) -> None:
2220
""" Generate a new OpenAPI Client library """
21+
from . import create_new_client
22+
23+
if not url and not path:
24+
typer.secho("You must either provide --url or --path", fg=typer.colors.RED)
25+
raise typer.Exit(code=1)
26+
elif url and path:
27+
typer.secho("Provide either --url or --path, not both", fg=typer.colors.RED)
28+
raise typer.Exit(code=1)
29+
create_new_client(url=url, path=path)
30+
31+
32+
@app.command()
33+
def update(
34+
url: Optional[str] = typer.Option(None, help="A URL to read the JSON from"),
35+
path: Optional[pathlib.Path] = typer.Option(None, help="A path to the JSON file"),
36+
) -> None:
37+
""" Update an existing OpenAPI Client library """
38+
from . import update_existing_client
39+
2340
if not url and not path:
2441
typer.secho("You must either provide --url or --path", fg=typer.colors.RED)
2542
raise typer.Exit(code=1)
2643
elif url and path:
2744
typer.secho("Provide either --url or --path, not both", fg=typer.colors.RED)
2845
raise typer.Exit(code=1)
29-
main(url=url, path=path)
46+
update_existing_client(url=url, path=path)

poetry.lock

+31-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ jinja2 = "^2.11.1"
2121
stringcase = "^1.2.0"
2222
typer = "^0.0.8"
2323
colorama = {version = "^0.4.3", markers = "sys_platform == 'win32'"}
24+
click-completion = "^0.5.2"
2425

2526
[tool.poetry.scripts]
2627
openapi-python-client = "openapi_python_client.cli:app"

0 commit comments

Comments
 (0)