Skip to content

Commit 1738c24

Browse files
committed
ci: Add integration tests
1 parent 3b6db1a commit 1738c24

19 files changed

+1223
-1
lines changed

.github/workflows/checks.yml

+54
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,57 @@ jobs:
6161
- uses: codecov/codecov-action@v2
6262
with:
6363
files: ./coverage.xml
64+
65+
integration:
66+
name: Integration Tests
67+
runs-on: ubuntu-latest
68+
env:
69+
PYTHON_VERSION: 3.10
70+
services:
71+
openapi-test-server:
72+
image: ghcr.io/openapi-generators/openapi-test-server:latest
73+
ports:
74+
- "3000:3000"
75+
steps:
76+
- uses: actions/checkout@v2
77+
- name: Set up Python
78+
uses: actions/setup-python@v2
79+
with:
80+
python-version: ${{ env.PYTHON_VERSION }}
81+
- name: Cache dependencies
82+
uses: actions/cache@v2
83+
with:
84+
path: .venv
85+
key: ${{ runner.os }}-${{ env.PYTHON_VERSION }}-dependencies-${{ hashFiles('**/poetry.lock') }}
86+
restore-keys: |
87+
${{ runner.os }}-${{ env.PYTHON_VERSION }}-dependencies
88+
- name: Install dependencies
89+
run: |
90+
pip install poetry
91+
python -m venv .venv
92+
poetry run python -m pip install --upgrade pip
93+
poetry install
94+
- name: Regenerate Integration Client
95+
run: |
96+
poetry shell
97+
cd integration_tests
98+
openapi-python-client update --url http://localhost:3000/openapi.json
99+
- name: Check if there are changes to the client
100+
run: changed_files=$(git status --porcelain | wc -l) && [ changed_files != 0 ] && exit 1
101+
- name: Cache Generated Client Dependencies
102+
uses: actions/cache@v2
103+
with:
104+
path: integration_tests/openapi-test-server-client/.venv
105+
key: ${{ runner.os }}-${{ env.PYTHON_VERSION }}-integration-dependencies-${{ hashFiles('**/poetry.lock') }}
106+
restore-keys: |
107+
${{ runner.os }}-${{ env.PYTHON_VERSION }}-integration-dependencies
108+
- name: Install Integration Dependencies
109+
run: |
110+
cd integration_tests/openapi-test-server-client
111+
python -m venv .venv
112+
poetry run python -m pip install --upgrade pip
113+
poetry install
114+
- name: Run Tests
115+
run: |
116+
cd integration_tests/openapi-test-server-client
117+
poetry run pytest
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
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# open-api-test-server-client
2+
A client library for accessing OpenAPI Test Server
3+
4+
## Usage
5+
First, create a client:
6+
7+
```python
8+
from open_api_test_server_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 open_api_test_server_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 open_api_test_server_client.models import MyDataModel
25+
from open_api_test_server_client.api.my_tag import get_my_data_model
26+
from open_api_test_server_client.types import Response
27+
28+
my_data: MyDataModel = get_my_data_model.sync(client=client)
29+
# or if you need more info (e.g. status_code)
30+
response: Response[MyDataModel] = get_my_data_model.sync_detailed(client=client)
31+
```
32+
33+
Or do the same thing with an async version:
34+
35+
```python
36+
from open_api_test_server_client.models import MyDataModel
37+
from open_api_test_server_client.api.my_tag import get_my_data_model
38+
from open_api_test_server_client.types import Response
39+
40+
my_data: MyDataModel = await get_my_data_model.asyncio(client=client)
41+
response: Response[MyDataModel] = await get_my_data_model.asyncio_detailed(client=client)
42+
```
43+
44+
By default, when you're calling an HTTPS API it will attempt to verify that SSL is working correctly. Using certificate verification is highly recommended most of the time, but sometimes you may need to authenticate to a server (especially an internal server) using a custom certificate bundle.
45+
46+
```python
47+
client = AuthenticatedClient(
48+
base_url="https://internal_api.example.com",
49+
token="SuperSecretToken",
50+
verify_ssl="/path/to/certificate_bundle.pem",
51+
)
52+
```
53+
54+
You can also disable certificate validation altogether, but beware that **this is a security risk**.
55+
56+
```python
57+
client = AuthenticatedClient(
58+
base_url="https://internal_api.example.com",
59+
token="SuperSecretToken",
60+
verify_ssl=False
61+
)
62+
```
63+
64+
Things to know:
65+
1. Every path/method combo becomes a Python module with four functions:
66+
1. `sync`: Blocking request that returns parsed data (if successful) or `None`
67+
1. `sync_detailed`: Blocking request that always returns a `Request`, optionally with `parsed` set if the request was successful.
68+
1. `asyncio`: Like `sync` but the async instead of blocking
69+
1. `asyncio_detailed`: Like `sync_detailed` by async instead of blocking
70+
71+
1. All path/query params, and bodies become method arguments.
72+
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)
73+
1. Any endpoint which did not have a tag will be in `open_api_test_server_client.api.default`
74+
75+
## Building / publishing this Client
76+
This project uses [Poetry](https://python-poetry.org/) to manage dependencies and packaging. Here are the basics:
77+
1. Update the metadata in pyproject.toml (e.g. authors, version)
78+
1. If you're using a private repository, configure it with Poetry
79+
1. `poetry config repositories.<your-repository-name> <url-to-your-repository>`
80+
1. `poetry config http-basic.<your-repository-name> <username> <password>`
81+
1. Publish the client with `poetry publish --build -r <your-repository-name>` or, if for public PyPI, just `poetry publish --build`
82+
83+
If you want to install this client into another project without publishing it (e.g. for development) then:
84+
1. If that project **is using Poetry**, you can simply do `poetry add <path-to-this-client>` from that project
85+
1. If that project is not using Poetry:
86+
1. Build a wheel with `poetry build -f wheel`
87+
1. Install that wheel from the other project `pip install <path-to-wheel>`
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
""" A client library for accessing OpenAPI Test Server """
2+
from .client import AuthenticatedClient, Client
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
""" Contains methods for accessing the API """

integration_tests/open-api-test-server-client/open_api_test_server_client/api/body/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
from typing import Any, Dict, Optional, Union
2+
3+
import httpx
4+
5+
from ...client import Client
6+
from ...models.post_body_multipart_multipart_data import PostBodyMultipartMultipartData
7+
from ...models.post_body_multipart_response_200 import PostBodyMultipartResponse200
8+
from ...models.public_error import PublicError
9+
from ...types import Response
10+
11+
12+
def _get_kwargs(
13+
*,
14+
client: Client,
15+
multipart_data: PostBodyMultipartMultipartData,
16+
) -> Dict[str, Any]:
17+
url = "{}/body/multipart".format(client.base_url)
18+
19+
headers: Dict[str, Any] = client.get_headers()
20+
cookies: Dict[str, Any] = client.get_cookies()
21+
22+
multipart_multipart_data = multipart_data.to_multipart()
23+
24+
return {
25+
"url": url,
26+
"headers": headers,
27+
"cookies": cookies,
28+
"timeout": client.get_timeout(),
29+
"files": multipart_multipart_data,
30+
}
31+
32+
33+
def _parse_response(*, response: httpx.Response) -> Optional[Union[PostBodyMultipartResponse200, PublicError]]:
34+
if response.status_code == 200:
35+
response_200 = PostBodyMultipartResponse200.from_dict(response.json())
36+
37+
return response_200
38+
if response.status_code == 400:
39+
response_400 = PublicError.from_dict(response.json())
40+
41+
return response_400
42+
return None
43+
44+
45+
def _build_response(*, response: httpx.Response) -> Response[Union[PostBodyMultipartResponse200, PublicError]]:
46+
return Response(
47+
status_code=response.status_code,
48+
content=response.content,
49+
headers=response.headers,
50+
parsed=_parse_response(response=response),
51+
)
52+
53+
54+
def sync_detailed(
55+
*,
56+
client: Client,
57+
multipart_data: PostBodyMultipartMultipartData,
58+
) -> Response[Union[PostBodyMultipartResponse200, PublicError]]:
59+
kwargs = _get_kwargs(
60+
client=client,
61+
multipart_data=multipart_data,
62+
)
63+
64+
response = httpx.post(
65+
verify=client.verify_ssl,
66+
**kwargs,
67+
)
68+
69+
return _build_response(response=response)
70+
71+
72+
def sync(
73+
*,
74+
client: Client,
75+
multipart_data: PostBodyMultipartMultipartData,
76+
) -> Optional[Union[PostBodyMultipartResponse200, PublicError]]:
77+
""" """
78+
79+
return sync_detailed(
80+
client=client,
81+
multipart_data=multipart_data,
82+
).parsed
83+
84+
85+
async def asyncio_detailed(
86+
*,
87+
client: Client,
88+
multipart_data: PostBodyMultipartMultipartData,
89+
) -> Response[Union[PostBodyMultipartResponse200, PublicError]]:
90+
kwargs = _get_kwargs(
91+
client=client,
92+
multipart_data=multipart_data,
93+
)
94+
95+
async with httpx.AsyncClient(verify=client.verify_ssl) as _client:
96+
response = await _client.post(**kwargs)
97+
98+
return _build_response(response=response)
99+
100+
101+
async def asyncio(
102+
*,
103+
client: Client,
104+
multipart_data: PostBodyMultipartMultipartData,
105+
) -> Optional[Union[PostBodyMultipartResponse200, PublicError]]:
106+
""" """
107+
108+
return (
109+
await asyncio_detailed(
110+
client=client,
111+
multipart_data=multipart_data,
112+
)
113+
).parsed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import ssl
2+
from typing import Dict, Union
3+
4+
import attr
5+
6+
7+
@attr.s(auto_attribs=True)
8+
class Client:
9+
"""A class for keeping track of data related to the API"""
10+
11+
base_url: str
12+
cookies: Dict[str, str] = attr.ib(factory=dict, kw_only=True)
13+
headers: Dict[str, str] = attr.ib(factory=dict, kw_only=True)
14+
timeout: float = attr.ib(5.0, kw_only=True)
15+
verify_ssl: Union[str, bool, ssl.SSLContext] = attr.ib(True, kw_only=True)
16+
17+
def get_headers(self) -> Dict[str, str]:
18+
"""Get headers to be used in all endpoints"""
19+
return {**self.headers}
20+
21+
def with_headers(self, headers: Dict[str, str]) -> "Client":
22+
"""Get a new client matching this one with additional headers"""
23+
return attr.evolve(self, headers={**self.headers, **headers})
24+
25+
def get_cookies(self) -> Dict[str, str]:
26+
return {**self.cookies}
27+
28+
def with_cookies(self, cookies: Dict[str, str]) -> "Client":
29+
"""Get a new client matching this one with additional cookies"""
30+
return attr.evolve(self, cookies={**self.cookies, **cookies})
31+
32+
def get_timeout(self) -> float:
33+
return self.timeout
34+
35+
def with_timeout(self, timeout: float) -> "Client":
36+
"""Get a new client matching this one with a new timeout (in seconds)"""
37+
return attr.evolve(self, timeout=timeout)
38+
39+
40+
@attr.s(auto_attribs=True)
41+
class AuthenticatedClient(Client):
42+
"""A Client which has been authenticated for use on secured endpoints"""
43+
44+
token: str
45+
46+
def get_headers(self) -> Dict[str, str]:
47+
"""Get headers to be used in authenticated endpoints"""
48+
return {"Authorization": f"Bearer {self.token}", **self.headers}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
""" Contains all the data models used in inputs/outputs """
2+
3+
from .post_body_multipart_multipart_data import PostBodyMultipartMultipartData
4+
from .post_body_multipart_response_200 import PostBodyMultipartResponse200
5+
from .problem import Problem
6+
from .public_error import PublicError

0 commit comments

Comments
 (0)