Skip to content

Commit

Permalink
Merge pull request #22 from phalt/0.7.0
Browse files Browse the repository at this point in the history
0.7.0
  • Loading branch information
phalt authored Oct 24, 2023
2 parents 85e4725 + 4d0aac4 commit 707b8c9
Show file tree
Hide file tree
Showing 60 changed files with 1,083 additions and 635 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11"]
python-version: ["3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v3
#----------------------------------------------
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Change log

## 0.7.0

* Updated all files to use the templates engine.
* Generator files have been reorganised in clientele to support future templates.
* `constants.py` has been renamed to `config.py` to better reflect how it is used. It is not generated from a template like the other files.
* If you are using Python 3.10 or later, the `typing.Unions` types will generate as the short hand `|` instead.
* To regenerate a client (and to prevent accidental overrides) you must now pass `--regen t` or `-r t` to the `generate` command. This is automatically added to the line in `MANIFEST.md` to help.
* Clientele will now automatically run [black](https://black.readthedocs.io/en/stable/) code formatter once a client is generated or regenerated.
* Clientele will now generate absolute paths to refer to adjacent files in the generated client, instead of relative paths. This assumes you are running the `clientele` command in the root directory of your project.
* A lot of documentation and docs strings updates so that code in the generated client is easier to understand.
* Improved the utility for snake-casing enum keys. Tests added for the functions.
* Python 3.12 support.
* Add a "basic" client using the command `generate-basic`. This can be used to keep a consistent file structure for an API that does not use OpenAPI.

## 0.6.3

* Packaged application installs in the correct location. Resolving [#6](https://github.com/phalt/clientele/issues/6)
Expand Down
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ Format and lint the code:
make lint
```

Note that, the auto-generated black formatted code will be changed again because this project uses `ruff` for additional formatting. That's okay.

Make sure you add to `CHANGELOG.md` and `docs/CHANGELOG.md` what changes you have made.

Make sure you add your name to `CONTRIBUTORS.md` as well!
Expand Down
5 changes: 2 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,5 @@ shell: ## Run an ipython shell

generate-test-clients: ## regenerate the test clients in the tests/ directory
poetry install
clientele generate -f example_openapi_specs/best.json -o tests/test_client/
clientele generate -f example_openapi_specs/best.json -o tests/async_test_client/ --asyncio t
black tests/
clientele generate -f example_openapi_specs/best.json -o tests/test_client/ --regen t
clientele generate -f example_openapi_specs/best.json -o tests/async_test_client/ --asyncio t --regen t
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,10 @@ response = await client.simple_request_simple_request_get()
## Other features

* Written entirely in Python.
* Designed to work with [FastAPI](https://fastapi.tiangolo.com/)'s OpenAPI schema generator.
* Designed to work with [FastAPI](https://fastapi.tiangolo.com/)'s and [Ddrf-spectacular](https://github.com/tfranzel/drf-spectacular)'s OpenAPI schema generator.
* The generated client only depends on [httpx](https://www.python-httpx.org/) and [Pydantic 2.4](https://docs.pydantic.dev/latest/).
* HTTP Basic and HTTP Bearer authentication support.
* Support your own configuration - we provide an entry point that will never be overwritten.
* Designed for easy testing with [respx](https://lundberg.github.io/respx/).
* API updated? Just run the same command again and check the git diff.
* Automatically formats the generated client with [black](https://black.readthedocs.io/en/stable/index.html).
49 changes: 38 additions & 11 deletions clientele/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,16 @@ def validate(url, file):


@click.command()
@click.option("-u", "--url", help="URL to openapi schema (json file)", required=False)
@click.option("-f", "--file", help="Path to openapi schema (json file)", required=False)
@click.option("-u", "--url", help="URL to openapi schema (URL)", required=False)
@click.option(
"-f", "--file", help="Path to openapi schema (json or yaml file)", required=False
)
@click.option(
"-o", "--output", help="Directory for the generated client", required=True
)
@click.option("-a", "--asyncio", help="Use Async.IO", required=False)
def generate(url, file, output, asyncio):
@click.option("-a", "--asyncio", help="Generate async client", required=False)
@click.option("-r", "--regen", help="Regenerate client", required=False)
def generate(url, file, output, asyncio, regen):
"""
Generate a new client from an OpenAPI schema
"""
Expand All @@ -81,7 +84,7 @@ def generate(url, file, output, asyncio):

console = Console()

from clientele.generator import Generator
from clientele.generators.standard.generator import StandardGenerator

assert url or file, "Must pass either a URL or a file"

Expand All @@ -106,16 +109,40 @@ def generate(url, file, output, asyncio):
f"[red]Clientele only supports OpenAPI version 3.0.0 and up, and you have {spec['openapi']}"
)
return
Generator(
spec=spec, asyncio=asyncio, output_dir=output, url=url, file=file
).generate()
console.log("\n[green]⚜️ Client generated! ⚜️ \n")
console.log(
"[yellow]REMEMBER: install `httpx` `pydantic`, and `respx` to use your new client"
generator = StandardGenerator(
spec=spec, asyncio=asyncio, regen=regen, output_dir=output, url=url, file=file
)
if generator.prevent_accidental_regens():
generator.generate()
console.log("\n[green]⚜️ Client generated! ⚜️ \n")
console.log(
"[yellow]REMEMBER: install `httpx` `pydantic`, and `respx` to use your new client"
)


@click.command()
@click.option(
"-o", "--output", help="Directory for the generated client", required=True
)
def generate_basic(output):
"""
Generate a "basic" file structure, no code.
"""
from rich.console import Console

from clientele.generators.basic.generator import BasicGenerator

console = Console()

console.log(f"Generating basic client at {output}...")

generator = BasicGenerator(output_dir=output)

generator.generate()


cli_group.add_command(generate)
cli_group.add_command(generate_basic)
cli_group.add_command(version)
cli_group.add_command(validate)

Expand Down
8 changes: 0 additions & 8 deletions clientele/client_template/client.py

This file was deleted.

86 changes: 0 additions & 86 deletions clientele/generator.py

This file was deleted.

13 changes: 13 additions & 0 deletions clientele/generators/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Generators

In the future, this will be the directory for all the possible generators that clientele supports.

Copy the basic template if you want to start your own.

## Standard

The standard generator

## Basic

A basic client with a file structure and not much else.
File renamed without changes.
54 changes: 54 additions & 0 deletions clientele/generators/basic/generator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from os import remove
from os.path import exists

from clientele import settings, utils
from clientele.generators.basic import writer


class BasicGenerator:
"""
Generates a "basic" HTTP client, which is just a file structure
and some useful imports.
This generator can be used as a template for future generators.
It is also a great way to generate a file structure for consistent HTTP API clients
that are not OpenAPI but you want to keep the same file structure.
"""

def __init__(self, output_dir: str) -> None:
self.output_dir = output_dir

self.file_name_writer_tuple = (
("config.py", "config_py.jinja2", writer.write_to_config),
("client.py", "client_py.jinja2", writer.write_to_client),
("http.py", "http_py.jinja2", writer.write_to_http),
("schemas.py", "schemas_py.jinja2", writer.write_to_schemas),
)

def generate(self) -> None:
client_project_directory_path = utils.get_client_project_directory_path(
output_dir=self.output_dir
)
if exists(f"{self.output_dir}/MANIFEST.md"):
remove(f"{self.output_dir}/MANIFEST.md")
manifest_template = writer.templates.get_template("manifest.jinja2")
manifest_content = manifest_template.render(
command=f"-o {self.output_dir}", clientele_version=settings.VERSION
)
writer.write_to_manifest(
content=manifest_content + "\n", output_dir=self.output_dir
)
writer.write_to_init(output_dir=self.output_dir)
for (
client_file,
client_template_file,
write_func,
) in self.file_name_writer_tuple:
if exists(f"{self.output_dir}/{client_file}"):
remove(f"{self.output_dir}/{client_file}")
template = writer.templates.get_template(client_template_file)
content = template.render(
client_project_directory_path=client_project_directory_path,
)
write_func(content, output_dir=self.output_dir)
9 changes: 9 additions & 0 deletions clientele/generators/basic/templates/client_py.jinja2
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""
API Client functions.
"""

from __future__ import annotations

import typing # noqa

from {{client_project_directory_path}} import http, schemas # noqa
3 changes: 3 additions & 0 deletions clientele/generators/basic/templates/config_py.jinja2
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""
API Client configuration.
"""
68 changes: 68 additions & 0 deletions clientele/generators/basic/templates/http_py.jinja2
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""
HTTP layer management.
"""

import typing
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
import httpx # noqa

from {{client_project_directory_path}} import config as c # noqa


class APIException(Exception):
"""Could not match API response to return type of this function"""

reason: str
response: httpx.Response

def __init__(self, response: httpx.Response, reason: str, *args: object) -> None:
self.response = response
self.reason = reason
super().__init__(*args)


def parse_url(url: str) -> str:
"""
Returns the full URL from a string.

Will filter out any optional query parameters if they are None.
"""
api_url = f"{c.api_base_url()}{url}"
url_parts = urlparse(url=api_url)
# Filter out "None" optional query parameters
filtered_query_params = {
k: v for k, v in parse_qs(url_parts.query).items() if v[0] not in ["None", ""]
}
filtered_query_string = urlencode(filtered_query_params, doseq=True)
return urlunparse(
(
url_parts.scheme,
url_parts.netloc,
url_parts.path,
url_parts.params,
filtered_query_string,
url_parts.fragment,
)
)

client = httpx.Client()


def get(url: str, headers: typing.Optional[dict] = None) -> httpx.Response:
"""Issue an HTTP GET request"""
return client.get(parse_url(url), headers=headers)


def post(url: str, data: dict, headers: typing.Optional[dict] = None) -> httpx.Response:
"""Issue an HTTP POST request"""
return client.post(parse_url(url), json=data, headers=headers)


def put(url: str, data: dict, headers: typing.Optional[dict] = None) -> httpx.Response:
"""Issue an HTTP PUT request"""
return client.put(parse_url(url), json=data, headers=headers)


def delete(url: str, headers: typing.Optional[dict] = None) -> httpx.Response:
"""Issue an HTTP DELETE request"""
return client.delete(parse_url(url), headers=headers)
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,11 @@ Install with pipx:
```sh
pipx install clientele
```

CLIENTELE VERSION: {{clientele_version}}

Regnerate using this command:

```sh
clientele generate-basic {{command}}
```
Loading

0 comments on commit 707b8c9

Please sign in to comment.