{"id":16142862,"url":"https://github.com/moneymeets/spec2sdk","last_synced_at":"2025-04-22T20:33:09.873Z","repository":{"id":257814976,"uuid":"839246852","full_name":"moneymeets/spec2sdk","owner":"moneymeets","description":"Generate Pydantic models and API client code from OpenAPI 3.x specifications","archived":false,"fork":false,"pushed_at":"2025-04-15T09:58:34.000Z","size":207,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-04-15T10:44:22.374Z","etag":null,"topics":["code-generator","openapi","openapi-codegen","pydantic","python"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/moneymeets.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2024-08-07T08:28:43.000Z","updated_at":"2025-04-15T09:58:36.000Z","dependencies_parsed_at":null,"dependency_job_id":"f78bcc3f-c74e-40e0-9595-f4bd21f3d8b0","html_url":"https://github.com/moneymeets/spec2sdk","commit_stats":null,"previous_names":["moneymeets/spec2sdk"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/moneymeets%2Fspec2sdk","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/moneymeets%2Fspec2sdk/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/moneymeets%2Fspec2sdk/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/moneymeets%2Fspec2sdk/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/moneymeets","download_url":"https://codeload.github.com/moneymeets/spec2sdk/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250318696,"owners_count":21410980,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["code-generator","openapi","openapi-codegen","pydantic","python"],"created_at":"2024-10-10T00:07:09.471Z","updated_at":"2025-04-22T20:33:09.867Z","avatar_url":"https://github.com/moneymeets.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Usage\n\n## From command line\n\n- Local specification `spec2sdk --schema-path path/to/api.yml --output-dir path/to/output-dir/`\n- Remote 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# Remote 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) -\u003e 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\n\nfrom spec2sdk.openapi.entities import DataType, StringDataType\nfrom spec2sdk.models.annotations import TypeAnnotation\nfrom spec2sdk.models.converters import converters, convert_common_fields\nfrom spec2sdk.models.entities import SimpleType\nfrom spec2sdk.models.imports import Import\nfrom spec2sdk.main import generate\n\n\nclass EmailType(SimpleType):\n    @property\n    def type_definition(self) -\u003e TypeAnnotation:\n        return TypeAnnotation(\n            type_hint=\"EmailStr\",\n            type_imports=(Import(name=\"EmailStr\", package=\"pydantic\"),),\n            constraints=(),\n        )\n\n\ndef is_email_format(data_type: DataType) -\u003e 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) -\u003e 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) -\u003e 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) -\u003e 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) -\u003e 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) -\u003e 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    ) -\u003e 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","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmoneymeets%2Fspec2sdk","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmoneymeets%2Fspec2sdk","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmoneymeets%2Fspec2sdk/lists"}