# Usage
## From command line
- Local specification `spec2sdk --schema-path path/to/api.yml --output-dir path/to/output-dir/`
- Remove specification `spec2sdk --schema-url https://example.com/path/to/api.yml --output-dir path/to/output-dir/`
## From the code
```python
from pathlib import Path
from spec2sdk.main import generate
# Local specification
generate(schema_url=Path("path/to/api.yml").absolute().as_uri(), output_dir=Path("path/to/output-dir/"))
# Remove specification
generate(schema_url="https://example.com/path/to/api.yml", output_dir=Path("path/to/output-dir/"))
```
# Open API specification requirements
## Operation ID
`operationId` must be specified for each endpoint to generate meaningful method names. It must be unique among all operations described in the API.
### Input
```yaml
paths:
/health:
get:
operationId: healthCheck
responses:
'200':
description: Successful response
```
### Output
```python
class APIClient:
def health_check(self) -> None:
...
```
## Inline schemas
Inline schemas should be annotated with the schema name in the `x-schema-name` field that doesn't overlap with the existing schema names in the specification.
### Input
```yaml
paths:
/me:
get:
operationId: getMe
responses:
'200':
description: Successful response
content:
application/json:
schema:
x-schema-name: User
type: object
properties:
name:
type: string
email:
type: string
```
### Output
```python
class User(Model):
name: str | None = Field(default=None)
email: str | None = Field(default=None)
```
## Enum variable names
Variable names for enums can be specified by the `x-enum-varnames` field.
### Input
```yaml
components:
schemas:
Direction:
x-enum-varnames: [ NORTH, SOUTH, WEST, EAST ]
type: string
enum: [ N, S, W, E ]
```
### Output
```python
from enum import StrEnum
class Direction(StrEnum):
NORTH = "N"
SOUTH = "S"
WEST = "W"
EAST = "E"
```
# Custom types
Register Python converters and renderers to implement custom types.
## Input
```yaml
components:
schemas:
User:
type: object
properties:
name:
type: string
email:
type: string
format: email
```
```python
from pathlib import Path
from typing import Sequence
from spec2sdk.openapi.entities import DataType, StringDataType
from spec2sdk.models.converters import converters, convert_common_fields
from spec2sdk.models.entities import PythonType
from spec2sdk.models.imports import Import
from spec2sdk.main import generate
class EmailType(PythonType):
@property
def type_hint(self) -> str:
return self.name or "EmailStr"
@property
def imports(self) -> Sequence[Import]:
return (
Import(name="EmailStr", package="pydantic"),
)
def render(self) -> str:
return f"type {self.name} = EmailStr" if self.name else ""
def is_email_format(data_type: DataType) -> bool:
return isinstance(data_type, StringDataType) and data_type.format == "email"
@converters.register(predicate=is_email_format)
def convert_email_field(data_type: StringDataType) -> EmailType:
return EmailType(**convert_common_fields(data_type))
if __name__ == "__main__":
generate(schema_url=Path("api.yml").absolute().as_uri(), output_dir=Path("output"))
```
## Output
```python
from pydantic import EmailStr, Field
class User(Model):
name: str | None = Field(default=None)
email: EmailStr | None = Field(default=None)
```
# Using generated client
1. Create HTTP client. It should conform to the `HTTPClientProtocol` which can be found in the generated `http_client.py`. Below is an example of the HTTP client implemented using `httpx` library to handle HTTP requests. Assume that `sdk` is the output directory for the generated code.
```python
from http import HTTPStatus
import httpx
from httpx._types import AuthTypes, TimeoutTypes
from sdk.http_client import HTTPRequest, HTTPResponse
class HTTPClient:
def __init__(self, *, base_url: str, auth: AuthTypes | None = None, timeout: TimeoutTypes | None = None, **kwargs):
self._http_client = httpx.Client(auth=auth, base_url=base_url, timeout=timeout, **kwargs)
def send_request(self, *, request: HTTPRequest) -> HTTPResponse:
response = self._http_client.request(
method=request.method,
url=request.url,
content=request.content,
headers=request.headers,
)
return HTTPResponse(
status_code=HTTPStatus(response.status_code),
content=response.content,
headers=response.headers.multi_items(),
)
```
2. Create API client. It should conform to the `APIClientProtocol` which can be found in the generated `api_client.py`. Below is an example of the API client.
```python
from http import HTTPMethod, HTTPStatus
from types import NoneType
from typing import Any, Mapping, Type
from urllib.parse import urlencode
from pydantic import TypeAdapter
from sdk.api_client import APIClientResponse
from sdk.http_client import HTTPClientProtocol, HTTPRequest
class APIClient:
def __init__(self, http_client: HTTPClientProtocol):
self._http_client = http_client
def serialize[T](self, *, data: T, data_type: Type[T], content_type: str | None) -> bytes:
match content_type:
case "application/json":
return TypeAdapter(data_type).dump_json(data, by_alias=True)
case _:
return data
def deserialize[T](self, *, data: bytes | None, data_type: Type[T], content_type: str | None) -> T:
match content_type:
case "application/json":
return TypeAdapter(data_type).validate_json(data)
case _:
return data
def build_url(self, path: str, query: Mapping[str, Any] | None = None) -> str:
if query is None:
return path
return f"{path}?{urlencode(query, doseq=True)}"
def send_request[I, O](
self,
*,
method: HTTPMethod,
path: str,
query: Mapping[str, Any] | None = None,
content_type: str | None = None,
data: I | None = None,
data_type: Type[I] = NoneType,
accept: str | None = None,
response_type: Type[O] = NoneType,
expected_status_code: HTTPStatus = HTTPStatus.OK,
) -> APIClientResponse[O]:
content = self.serialize(data=data, data_type=data_type, content_type=content_type) if data else None
request = HTTPRequest(
method=method,
url=self.build_url(path, query),
headers=(("Content-Type", content_type),) if content_type else (),
content=content,
)
response = self._http_client.send_request(request=request)
if response.status_code != expected_status_code:
raise Exception(
f"Response has unexpected status code. Expected {expected_status_code}, got {response.status_code}."
)
if accept is not None and not any(
response_content_type := tuple(
value for key, value in response.headers if (key.lower() == "content-type") and (accept in value)
),
):
raise Exception(f"Response has unexpected content type. Expected {accept}, got {response_content_type}.")
return APIClientResponse(
http_response=response,
data=self.deserialize(data=response.content, data_type=response_type, content_type=accept),
)
```
3. Combine clients together to access API.
```python
from sdk.api import API
api = API(
api_client=APIClient(
http_client=HTTPClient(
base_url="https://api.example.com",
auth=BasicAuth(username="user", password="pass"),
),
),
)
```
Raw data
{
"_id": null,
"home_page": "https://github.com/moneymeets/spec2sdk",
"name": "spec2sdk",
"maintainer": null,
"docs_url": null,
"requires_python": "<3.13,>=3.12",
"maintainer_email": null,
"keywords": "openapi, pydantic, code-generator, openapi-codegen",
"author": "moneymeets",
"author_email": "service@moneymeets.com",
"download_url": "https://files.pythonhosted.org/packages/73/81/778110407cf36ab859cd7d50e54303dac2d033d4bd90467f1f42dfd4695e/spec2sdk-1.0.202502171012.tar.gz",
"platform": null,
"description": "# Usage\n\n## From command line\n\n- Local specification `spec2sdk --schema-path path/to/api.yml --output-dir path/to/output-dir/`\n- Remove specification `spec2sdk --schema-url https://example.com/path/to/api.yml --output-dir path/to/output-dir/`\n\n## From the code\n\n```python\nfrom pathlib import Path\nfrom spec2sdk.main import generate\n\n# Local specification\ngenerate(schema_url=Path(\"path/to/api.yml\").absolute().as_uri(), output_dir=Path(\"path/to/output-dir/\"))\n\n# Remove specification\ngenerate(schema_url=\"https://example.com/path/to/api.yml\", output_dir=Path(\"path/to/output-dir/\"))\n```\n\n# Open API specification requirements\n\n## Operation ID\n\n`operationId` must be specified for each endpoint to generate meaningful method names. It must be unique among all operations described in the API.\n\n### Input\n\n```yaml\npaths:\n /health:\n get:\n operationId: healthCheck\n responses:\n '200':\n description: Successful response\n```\n\n### Output\n\n```python\nclass APIClient:\n def health_check(self) -> None:\n ...\n```\n\n## Inline schemas\n\nInline schemas should be annotated with the schema name in the `x-schema-name` field that doesn't overlap with the existing schema names in the specification.\n\n### Input\n\n```yaml\npaths:\n /me:\n get:\n operationId: getMe\n responses:\n '200':\n description: Successful response\n content:\n application/json:\n schema:\n x-schema-name: User\n type: object\n properties:\n name:\n type: string\n email:\n type: string\n```\n\n### Output\n\n```python\nclass User(Model):\n name: str | None = Field(default=None)\n email: str | None = Field(default=None)\n```\n\n## Enum variable names\n\nVariable names for enums can be specified by the `x-enum-varnames` field.\n\n### Input\n\n```yaml\ncomponents: \n schemas:\n Direction:\n x-enum-varnames: [ NORTH, SOUTH, WEST, EAST ]\n type: string\n enum: [ N, S, W, E ]\n```\n\n### Output\n\n```python\nfrom enum import StrEnum\n\nclass Direction(StrEnum):\n NORTH = \"N\"\n SOUTH = \"S\"\n WEST = \"W\"\n EAST = \"E\"\n```\n\n# Custom types\n\nRegister Python converters and renderers to implement custom types.\n\n## Input\n\n```yaml\ncomponents: \n schemas: \n User:\n type: object\n properties:\n name:\n type: string\n email:\n type: string\n format: email\n```\n\n```python\nfrom pathlib import Path\nfrom typing import Sequence\n\nfrom spec2sdk.openapi.entities import DataType, StringDataType\nfrom spec2sdk.models.converters import converters, convert_common_fields\nfrom spec2sdk.models.entities import PythonType\nfrom spec2sdk.models.imports import Import\nfrom spec2sdk.main import generate\n\n\nclass EmailType(PythonType):\n @property\n def type_hint(self) -> str:\n return self.name or \"EmailStr\"\n\n @property\n def imports(self) -> Sequence[Import]:\n return (\n Import(name=\"EmailStr\", package=\"pydantic\"),\n )\n\n def render(self) -> str:\n return f\"type {self.name} = EmailStr\" if self.name else \"\"\n\n\ndef is_email_format(data_type: DataType) -> bool:\n return isinstance(data_type, StringDataType) and data_type.format == \"email\"\n\n\n@converters.register(predicate=is_email_format)\ndef convert_email_field(data_type: StringDataType) -> EmailType:\n return EmailType(**convert_common_fields(data_type))\n\n\nif __name__ == \"__main__\":\n generate(schema_url=Path(\"api.yml\").absolute().as_uri(), output_dir=Path(\"output\"))\n```\n\n## Output\n\n```python\nfrom pydantic import EmailStr, Field\n\nclass User(Model):\n name: str | None = Field(default=None)\n email: EmailStr | None = Field(default=None)\n```\n\n# Using generated client\n\n1. Create HTTP client. It should conform to the `HTTPClientProtocol` which can be found in the generated `http_client.py`. Below is an example of the HTTP client implemented using `httpx` library to handle HTTP requests. Assume that `sdk` is the output directory for the generated code.\n```python\nfrom http import HTTPStatus\n\nimport httpx\nfrom httpx._types import AuthTypes, TimeoutTypes\n\nfrom sdk.http_client import HTTPRequest, HTTPResponse\n\n\nclass HTTPClient:\n def __init__(self, *, base_url: str, auth: AuthTypes | None = None, timeout: TimeoutTypes | None = None, **kwargs):\n self._http_client = httpx.Client(auth=auth, base_url=base_url, timeout=timeout, **kwargs)\n\n def send_request(self, *, request: HTTPRequest) -> HTTPResponse:\n response = self._http_client.request(\n method=request.method,\n url=request.url,\n content=request.content,\n headers=request.headers,\n )\n return HTTPResponse(\n status_code=HTTPStatus(response.status_code),\n content=response.content,\n headers=response.headers.multi_items(),\n )\n```\n2. Create API client. It should conform to the `APIClientProtocol` which can be found in the generated `api_client.py`. Below is an example of the API client.\n```python\nfrom http import HTTPMethod, HTTPStatus\nfrom types import NoneType\nfrom typing import Any, Mapping, Type\nfrom urllib.parse import urlencode\n\nfrom pydantic import TypeAdapter\n\nfrom sdk.api_client import APIClientResponse\nfrom sdk.http_client import HTTPClientProtocol, HTTPRequest\n\n\nclass APIClient:\n def __init__(self, http_client: HTTPClientProtocol):\n self._http_client = http_client\n\n def serialize[T](self, *, data: T, data_type: Type[T], content_type: str | None) -> bytes:\n match content_type:\n case \"application/json\":\n return TypeAdapter(data_type).dump_json(data, by_alias=True)\n case _:\n return data\n\n def deserialize[T](self, *, data: bytes | None, data_type: Type[T], content_type: str | None) -> T:\n match content_type:\n case \"application/json\":\n return TypeAdapter(data_type).validate_json(data)\n case _:\n return data\n\n def build_url(self, path: str, query: Mapping[str, Any] | None = None) -> str:\n if query is None:\n return path\n\n return f\"{path}?{urlencode(query, doseq=True)}\"\n\n def send_request[I, O](\n self,\n *,\n method: HTTPMethod,\n path: str,\n query: Mapping[str, Any] | None = None,\n content_type: str | None = None,\n data: I | None = None,\n data_type: Type[I] = NoneType,\n accept: str | None = None,\n response_type: Type[O] = NoneType,\n expected_status_code: HTTPStatus = HTTPStatus.OK,\n ) -> APIClientResponse[O]:\n content = self.serialize(data=data, data_type=data_type, content_type=content_type) if data else None\n request = HTTPRequest(\n method=method,\n url=self.build_url(path, query),\n headers=((\"Content-Type\", content_type),) if content_type else (),\n content=content,\n )\n response = self._http_client.send_request(request=request)\n\n if response.status_code != expected_status_code:\n raise Exception(\n f\"Response has unexpected status code. Expected {expected_status_code}, got {response.status_code}.\"\n )\n\n if accept is not None and not any(\n response_content_type := tuple(\n value for key, value in response.headers if (key.lower() == \"content-type\") and (accept in value)\n ),\n ):\n raise Exception(f\"Response has unexpected content type. Expected {accept}, got {response_content_type}.\")\n\n return APIClientResponse(\n http_response=response,\n data=self.deserialize(data=response.content, data_type=response_type, content_type=accept),\n )\n```\n3. Combine clients together to access API.\n```python\nfrom sdk.api import API\n\napi = API(\n api_client=APIClient(\n http_client=HTTPClient(\n base_url=\"https://api.example.com\",\n auth=BasicAuth(username=\"user\", password=\"pass\"),\n ),\n ),\n)\n```\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "Generate Pydantic models and API client code from OpenAPI 3.x specifications",
"version": "1.0.202502171012",
"project_urls": {
"Homepage": "https://github.com/moneymeets/spec2sdk",
"Repository": "https://github.com/moneymeets/spec2sdk"
},
"split_keywords": [
"openapi",
" pydantic",
" code-generator",
" openapi-codegen"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "4edac0628f75784e9bc9f2010e40fa028c54622ae020747233168c1c811bdd57",
"md5": "29213d238774cff386a01281062a6c8d",
"sha256": "613f1d05a09e8ed69c6e3b5fc49d574f72e78338610d4f155e0660070c538e9f"
},
"downloads": -1,
"filename": "spec2sdk-1.0.202502171012-py3-none-any.whl",
"has_sig": false,
"md5_digest": "29213d238774cff386a01281062a6c8d",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": "<3.13,>=3.12",
"size": 24111,
"upload_time": "2025-02-17T10:12:23",
"upload_time_iso_8601": "2025-02-17T10:12:23.185110Z",
"url": "https://files.pythonhosted.org/packages/4e/da/c0628f75784e9bc9f2010e40fa028c54622ae020747233168c1c811bdd57/spec2sdk-1.0.202502171012-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "7381778110407cf36ab859cd7d50e54303dac2d033d4bd90467f1f42dfd4695e",
"md5": "638a1fc1ac544b8bc16c04431cc22f0a",
"sha256": "95065942496283081e8727c69d805cadbe1fa14e608695c3347e14129da608e9"
},
"downloads": -1,
"filename": "spec2sdk-1.0.202502171012.tar.gz",
"has_sig": false,
"md5_digest": "638a1fc1ac544b8bc16c04431cc22f0a",
"packagetype": "sdist",
"python_version": "source",
"requires_python": "<3.13,>=3.12",
"size": 18567,
"upload_time": "2025-02-17T10:12:25",
"upload_time_iso_8601": "2025-02-17T10:12:25.848345Z",
"url": "https://files.pythonhosted.org/packages/73/81/778110407cf36ab859cd7d50e54303dac2d033d4bd90467f1f42dfd4695e/spec2sdk-1.0.202502171012.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2025-02-17 10:12:25",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "moneymeets",
"github_project": "spec2sdk",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"lcname": "spec2sdk"
}