Skip to content

Commit 1435e2f

Browse files
committed
Added super basic api endpoint support and made generated package installable
1 parent b2c091e commit 1435e2f

File tree

7 files changed

+184
-20
lines changed

7 files changed

+184
-20
lines changed

openapi_python_client/__init__.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,24 @@ def _build_project(openapi: OpenAPI):
3939
project_dir.mkdir()
4040
package_dir = project_dir / package_name
4141
package_dir.mkdir()
42+
package_init = package_dir / "__init__.py"
43+
package_description = f"A client library for accessing {openapi.title}"
44+
package_init.write_text(f'""" {package_description} """')
4245

4346
# Create a pyproject.toml file
4447
pyproject_template = env.get_template("pyproject.toml")
4548
pyproject_path = project_dir / "pyproject.toml"
46-
pyproject_path.write_text(pyproject_template.render(project_name=project_name, package_name=package_name))
49+
pyproject_path.write_text(pyproject_template.render(project_name=project_name, package_name=package_name, description=package_description))
50+
51+
readme = project_dir / "README.md"
52+
readme_template = env.get_template("README.md")
53+
readme.write_text(readme_template.render(description=package_description))
4754

4855
# Generate models
4956
models_dir = package_dir / "models"
5057
models_dir.mkdir()
58+
models_init = models_dir / "__init__.py"
59+
models_init.write_text('""" Contains all the data models used in inputs/outputs """')
5160
model_template = env.get_template("model.pyi")
5261
for schema in openapi.schemas.values():
5362
module_path = models_dir / f"{stringcase.snakecase(schema.title)}.py"
@@ -59,3 +68,19 @@ def _build_project(openapi: OpenAPI):
5968
module_path = models_dir / f"{enum.name}.py"
6069
module_path.write_text(enum_template.render(enum=enum))
6170

71+
# Generate Client
72+
client_path = package_dir / "client.py"
73+
client_template = env.get_template("client.pyi")
74+
client_path.write_text(client_template.render())
75+
76+
# Generate endpoints
77+
api_dir = package_dir / "api"
78+
api_dir.mkdir()
79+
api_init = api_dir / "__init__.py"
80+
api_init.write_text('""" Contains all methods for accessing the API """')
81+
endpoint_template = env.get_template("endpoint_module.pyi")
82+
for tag, endpoints in openapi.endpoints_by_tag.items():
83+
module_path = api_dir / f"{tag}.py"
84+
module_path.write_text(endpoint_template.render(endpoints=endpoints))
85+
86+

openapi_python_client/models/openapi.py

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,14 @@
11
from __future__ import annotations
22

3+
from collections import defaultdict
34
from dataclasses import dataclass, field
45
from enum import Enum
56
from typing import Dict, List, Optional, Set
67

78
import stringcase
89

910
from .properties import Property, property_from_dict, ListProperty, RefProperty, EnumProperty
10-
11-
12-
class Method(Enum):
13-
""" HTTP Methods """
14-
15-
GET = "get"
16-
POST = "post"
17-
PATCH = "patch"
11+
from .responses import Response, response_from_dict
1812

1913

2014
class ParameterLocation(Enum):
@@ -47,31 +41,40 @@ class Endpoint:
4741
"""
4842

4943
path: str
50-
method: Method
44+
method: str
5145
description: Optional[str]
5246
name: str
5347
parameters: List[Parameter]
54-
tag: Optional[str] = None
48+
responses: List[Response]
5549

5650
@staticmethod
57-
def get_list_from_dict(d: Dict[str, Dict[str, Dict]], /) -> List[Endpoint]:
51+
def get_by_tags_from_dict(d: Dict[str, Dict[str, Dict]], /) -> Dict[str, List[Endpoint]]:
5852
""" Parse the openapi paths data to get a list of endpoints """
59-
endpoints = []
53+
# TODO: handle requestBody
54+
endpoints_by_tag: Dict[str, List[Endpoint]] = defaultdict(list)
6055
for path, path_data in d.items():
6156
for method, method_data in path_data.items():
6257
parameters: List[Parameter] = []
58+
responses: List[Response] = []
6359
for param_dict in method_data.get("parameters", []):
6460
parameters.append(Parameter.from_dict(param_dict))
61+
tag = method_data.get("tags", ["default"])[0]
62+
for code, response_dict in method_data["responses"].items():
63+
response = response_from_dict(
64+
status_code=int(code),
65+
data=response_dict,
66+
)
67+
responses.append(response)
6568
endpoint = Endpoint(
6669
path=path,
67-
method=Method(method),
70+
method=method,
6871
description=method_data.get("description"),
6972
name=method_data["operationId"],
7073
parameters=parameters,
71-
tag=method_data.get("tags", [None])[0],
74+
responses=responses,
7275
)
73-
endpoints.append(endpoint)
74-
return endpoints
76+
endpoints_by_tag[tag].append(endpoint)
77+
return endpoints_by_tag
7578

7679

7780
@dataclass
@@ -119,7 +122,7 @@ class OpenAPI:
119122
version: str
120123
security_schemes: Dict
121124
schemas: Dict[str, Schema]
122-
endpoints: List[Endpoint]
125+
endpoints_by_tag: Dict[str, List[Endpoint]]
123126
enums: Dict[str, EnumProperty]
124127

125128
@staticmethod
@@ -144,7 +147,7 @@ def from_dict(d: Dict, /) -> OpenAPI:
144147
title=d["info"]["title"],
145148
description=d["info"]["description"],
146149
version=d["info"]["version"],
147-
endpoints=Endpoint.get_list_from_dict(d["paths"]),
150+
endpoints_by_tag=Endpoint.get_by_tags_from_dict(d["paths"]),
148151
schemas=schemas,
149152
security_schemes=d["components"]["securitySchemes"],
150153
enums=enums,
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
from dataclasses import dataclass, field
2+
from typing import Optional, List, Dict, Union, ClassVar, TypedDict, Literal, cast
3+
import stringcase
4+
5+
6+
ContentType = Union[Literal["application/json"], Literal["text/html"]]
7+
8+
9+
@dataclass
10+
class Response:
11+
""" Describes a single response for an endpoint """
12+
13+
status_code: int
14+
content_type: ContentType
15+
16+
17+
@dataclass
18+
class ListRefResponse(Response):
19+
""" Response is a list of some ref schema """
20+
21+
ref: str
22+
23+
24+
@dataclass
25+
class RefResponse(Response):
26+
""" Response is a single ref schema """
27+
28+
ref: str
29+
30+
31+
@dataclass
32+
class StringResponse(Response):
33+
""" Response is a string """
34+
pass
35+
36+
37+
@dataclass
38+
class EmptyResponse(Response):
39+
""" Response has no payload """
40+
pass
41+
42+
43+
_openapi_types_to_python_type_strings = {
44+
"string": "str",
45+
"number": "float",
46+
"integer": "int",
47+
"boolean": "bool",
48+
"object": "Dict",
49+
}
50+
51+
52+
class _ResponseListSchemaDict(TypedDict):
53+
title: str
54+
type: Literal["array"]
55+
items: Dict[Literal["$ref"], str]
56+
57+
58+
_ResponseRefSchemaDict = Dict[Literal["$ref"], str]
59+
_ResponseStringSchemaDict = Dict[Literal["type"], Literal["string"]]
60+
_ResponseSchemaDict = Union[_ResponseListSchemaDict, _ResponseRefSchemaDict, _ResponseStringSchemaDict]
61+
62+
63+
class _ResponseContentDict(TypedDict):
64+
schema: _ResponseSchemaDict
65+
66+
67+
class _ResponseDict(TypedDict):
68+
description: str
69+
content: Dict[ContentType, _ResponseContentDict]
70+
71+
72+
def response_from_dict(
73+
*, status_code: int, data: _ResponseDict
74+
) -> Response:
75+
""" Generate a Response from the OpenAPI dictionary representation of it """
76+
if "content" not in data:
77+
raise ValueError(f"Cannot parse response data: {data}")
78+
79+
content = data["content"]
80+
content_type: ContentType
81+
if "application/json" in content:
82+
content_type = "application/json"
83+
elif "text/html" in content:
84+
content_type = "text/html"
85+
else:
86+
raise ValueError(f"Cannot parse content type of {data}")
87+
88+
schema_data: _ResponseSchemaDict = data["content"][content_type]["schema"]
89+
90+
if "$ref" in schema_data:
91+
return RefResponse(
92+
status_code=status_code,
93+
content_type=content_type,
94+
ref=schema_data["$ref"].split("/")[-1],
95+
)
96+
if "type" not in schema_data:
97+
return EmptyResponse(
98+
status_code=status_code,
99+
content_type=content_type,
100+
)
101+
if schema_data["type"] == "array":
102+
list_data = cast(_ResponseListSchemaDict, schema_data)
103+
return ListRefResponse(
104+
status_code=status_code,
105+
content_type=content_type,
106+
ref=list_data["items"]["$ref"].split("/")[-1],
107+
)
108+
if schema_data["type"] == "string":
109+
return StringResponse(
110+
status_code=status_code,
111+
content_type=content_type,
112+
)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{{ description }}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from dataclasses import dataclass
2+
3+
@dataclass
4+
class Client:
5+
""" A class for keeping track of data related to the API """
6+
base_url: str
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import requests
2+
3+
from ..client import Client
4+
5+
{% for endpoint in endpoints %}
6+
def {{ endpoint.name }}(client: Client):
7+
""" {{ endpoint.description }} """
8+
url = client.base_url + "{{ endpoint.path }}"
9+
10+
{% if endpoint.method == "get" %}
11+
return requests.get(url)
12+
{% endif %}
13+
14+
15+
{% endfor %}
16+
17+

openapi_python_client/templates/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[tool.poetry]
22
name = "{{ project_name }}"
33
version = "0.1.0"
4-
description = "A client API"
4+
description = "{{ description }}"
55

66
authors = []
77

0 commit comments

Comments
 (0)