Skip to content

Commit 153b5bc

Browse files
committed
Generate complete client for e2e testing
Closes #17
1 parent ae8adde commit 153b5bc

17 files changed

+300
-1
lines changed

poetry.lock

Lines changed: 67 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ taskipy = "^1.1.3"
3535
isort = "^4.3.21"
3636
safety = "^1.8.5"
3737
pytest-cov = "^2.8.1"
38+
fastapi = "^0.52.0"
3839

3940
[tool.taskipy.tasks]
4041
check = "isort --recursive --apply && black . && safety check && mypy openapi_python_client"

tests/test_end_to_end/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
""" Generate a complete client and verify that it is correct """
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
""" A FastAPI app used to create an OpenAPI document for end-to-end testing """
2+
import json
3+
from pathlib import Path
4+
5+
from fastapi import FastAPI
6+
from pydantic import BaseModel
7+
8+
app = FastAPI(
9+
title="My Test API",
10+
description="An API for testing openapi-python-client",
11+
)
12+
13+
14+
class PingResponse(BaseModel):
15+
success: bool
16+
17+
18+
@app.get("/ping", response_model=PingResponse)
19+
async def ping():
20+
""" A quick check to see if the system is running """
21+
return {"success": True}
22+
23+
if __name__ == '__main__':
24+
path = Path(__file__).parent / "openapi.json"
25+
path.write_text(json.dumps(app.openapi()))
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"openapi": "3.0.2", "info": {"title": "My Test API", "description": "An API for testing openapi-python-client", "version": "0.1.0"}, "paths": {"/ping": {"get": {"summary": "Ping", "description": "A quick check to see if the system is running ", "operationId": "ping_ping_get", "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PingResponse"}}}}}}}}, "components": {"schemas": {"PingResponse": {"title": "PingResponse", "required": ["success"], "type": "object", "properties": {"success": {"title": "Success", "type": "boolean"}}}}}}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
__pycache__/
2+
build/
3+
dist/
4+
*.egg-info/
5+
.pytest_cache/
6+
7+
# pyenv
8+
.python-version
9+
10+
# Environments
11+
.env
12+
.venv
13+
14+
# mypy
15+
.mypy_cache/
16+
.dmypy.json
17+
dmypy.json
18+
19+
# JetBrains
20+
.idea/
21+
22+
/coverage.xml
23+
/.coverage
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# my-test-api-client
2+
A client library for accessing My Test API
3+
4+
## Usage
5+
First, create a client:
6+
7+
```python
8+
from my_test_api_client import Client
9+
10+
client = Client(base_url="https://api.example.com")
11+
```
12+
13+
If the endpoints you're going to hit require authentication, use `AuthenticatedClient` instead:
14+
15+
```python
16+
from my_test_api_client import AuthenticatedClient
17+
18+
client = AuthenticatedClient(base_url="https://api.example.com", token="SuperSecretToken")
19+
```
20+
21+
Now call your endpoint and use your models:
22+
23+
```python
24+
from my_test_api_client.models import MyDataModel
25+
from my_test_api_client.api.my_tag import get_my_data_model
26+
27+
my_data: MyDataModel = get_my_data_model(client=client)
28+
```
29+
30+
Things to know:
31+
1. Every path/method combo becomes a Python function with type annotations.
32+
1. All path/query params, and bodies become method arguments.
33+
1. If your endpoint had any tags on it, the first tag will be used as a module name for the function (my_tag above)
34+
1. Any endpoint which did not have a tag will be in `my_test_api_client.api.default`
35+
1. If the API returns a response code that was not declared in the OpenAPI document, a
36+
`my_test_api_client.api.errors.ApiResponseError` wil be raised
37+
with the `response` attribute set to the `httpx.Response` that was received.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
""" A client library for accessing My Test API """
2+
from .client import AuthenticatedClient, Client
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
""" Contains all methods for accessing the API """
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from dataclasses import asdict
2+
from typing import Dict, List, Optional, Union
3+
4+
import httpx
5+
6+
from ..client import AuthenticatedClient, Client
7+
from .errors import ApiResponseError
8+
9+
from ..models.ping_response import PingResponse
10+
11+
12+
def ping_ping_get(
13+
*,
14+
client: Client,
15+
) -> Union[
16+
PingResponse,
17+
]:
18+
""" A quick check to see if the system is running """
19+
url = f"{client.base_url}/ping"
20+
21+
22+
response = httpx.get(
23+
url=url,
24+
headers=client.get_headers(),
25+
)
26+
27+
if response.status_code == 200:
28+
return PingResponse.from_dict(response.json())
29+
else:
30+
raise ApiResponseError(response=response)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from httpx import Response
2+
3+
class ApiResponseError(Exception):
4+
""" An exception raised when an unknown response occurs """
5+
6+
def __init__(self, *, response: Response):
7+
super().__init__()
8+
self.response: Response = response
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from dataclasses import dataclass
2+
from typing import Dict
3+
4+
@dataclass
5+
class Client:
6+
""" A class for keeping track of data related to the API """
7+
8+
base_url: str
9+
10+
def get_headers(self) -> Dict[str, str]:
11+
""" Get headers to be used in all endpoints """
12+
return {}
13+
14+
@dataclass
15+
class AuthenticatedClient(Client):
16+
""" A Client which has been authenticated for use on secured endpoints """
17+
18+
token: str
19+
20+
def get_headers(self) -> Dict[str, str]:
21+
""" Get headers to be used in authenticated endpoints """
22+
return {"Authorization": f"Bearer {self.token}"}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
""" Contains all the data models used in inputs/outputs """
2+
3+
from .ping_response import PingResponse
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass
4+
from datetime import datetime
5+
from typing import Dict, List, Optional, cast
6+
7+
8+
9+
@dataclass
10+
class PingResponse:
11+
""" """
12+
success: bool
13+
14+
def to_dict(self) -> Dict:
15+
return {
16+
"success": self.success,
17+
}
18+
19+
@staticmethod
20+
def from_dict(d: Dict) -> PingResponse:
21+
22+
success = d["success"]
23+
return PingResponse(
24+
success=success,
25+
)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Marker file for PEP 561
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[tool.poetry]
2+
name = "my-test-api-client"
3+
version = "0.1.0"
4+
description = "A client library for accessing My Test API"
5+
6+
authors = []
7+
8+
readme = "README.md"
9+
packages = [
10+
{include = "my_test_api_client"},
11+
]
12+
include = ["CHANGELOG.md", "my_test_api_client/py.typed"]
13+
14+
15+
[tool.poetry.dependencies]
16+
python = "^3.8"
17+
httpx = "^0.12.1"
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import os
2+
import shutil
3+
from filecmp import dircmp, cmpfiles
4+
from pathlib import Path
5+
6+
import pytest
7+
from typer.testing import CliRunner
8+
from openapi_python_client.cli import app
9+
10+
11+
def _compare_directories(first: Path, second: Path, /):
12+
first_printable = first.relative_to(Path.cwd())
13+
second_printable = second.relative_to(Path.cwd())
14+
dc = dircmp(first, second)
15+
missing_files = dc.left_only + dc.right_only
16+
if missing_files:
17+
pytest.fail(f"{first_printable} or {second_printable} was missing: {missing_files}", pytrace=False)
18+
19+
match, mismatch, errors = cmpfiles(first, second, dc.common_files, shallow=False)
20+
if mismatch:
21+
pytest.fail(f"{first_printable} and {second_printable} had differing files: {mismatch}", pytrace=False)
22+
23+
for sub_path in dc.common_dirs:
24+
_compare_directories(first / sub_path, second / sub_path)
25+
26+
27+
def test_end_to_end(capsys):
28+
runner = CliRunner()
29+
openapi_path = Path(__file__).parent / "fastapi" / "openapi.json"
30+
gm_path = Path(__file__).parent / "golden-master"
31+
output_path = Path.cwd() / "my-test-api-client"
32+
33+
runner.invoke(app, ["generate", f"--path={openapi_path}"])
34+
35+
_compare_directories(gm_path, output_path)
36+
shutil.rmtree(output_path)

0 commit comments

Comments
 (0)