{"id":21379597,"url":"https://github.com/yugokato/openapi-test-client","last_synced_at":"2025-04-15T17:13:29.328Z","repository":{"id":234744694,"uuid":"763312966","full_name":"yugokato/openapi-test-client","owner":"yugokato","description":"Dynamically generate/update API test clients from any OpenAPI 3.x specifications","archived":false,"fork":false,"pushed_at":"2025-03-24T16:32:06.000Z","size":2037,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-15T17:13:19.003Z","etag":null,"topics":["api-client","client-generator","code-generation","openapi3","python"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":false,"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/yugokato.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}},"created_at":"2024-02-26T03:47:38.000Z","updated_at":"2025-03-24T16:32:11.000Z","dependencies_parsed_at":null,"dependency_job_id":"7d8f2e75-47ca-411a-adfe-d10b374cf7df","html_url":"https://github.com/yugokato/openapi-test-client","commit_stats":null,"previous_names":["yugokato/openapi-test-client"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yugokato%2Fopenapi-test-client","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yugokato%2Fopenapi-test-client/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yugokato%2Fopenapi-test-client/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yugokato%2Fopenapi-test-client/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/yugokato","download_url":"https://codeload.github.com/yugokato/openapi-test-client/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249116198,"owners_count":21215143,"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":["api-client","client-generator","code-generation","openapi3","python"],"created_at":"2024-11-22T10:21:32.500Z","updated_at":"2025-04-15T17:13:29.321Z","avatar_url":"https://github.com/yugokato.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"OpenAPI Test Client\n======================\n[![test](https://github.com/yugokato/openapi-test-client/actions/workflows/test.yml/badge.svg)](https://github.com/yugokato/openapi-test-client/actions/workflows/test.yml)\n[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)\n\n# Overview\n\nOpenAPI Test Client (with an emphasis on \"test\" client) can generate/update API clients in Python from any \nOpenAPI 3.x specifications.  \n\nThe goal of the project is to provide easy-to-use/maintain auto-generated API clients for QA engineers who need to \nthoroughly perform automated API testing against API-first applications in a fast-paced development environment. \n\nDuring each software release cycle, QA will need to perform various testing to ensure that new APIs to be released work \nas expected. The testing tool for QA requires to have a lot of flexibility and intelligence to achieve this efficiently, \nand to fully automate everything which of course includes negative cases and some unusual scenarios.\nThis API test client will make the entire testing process very easy and efficient by automatically handling a lot of \nboilerplate under the hood, while also providing you with full control over how and what you want to test, as well as \nhow you want to extend or customize it for your own use case.\n\n\n# SDK client v.s. Test client\n\nOne important thing to clarify is that this is NOT an SDK client but a \"test\" client although both may share some \ncommon capabilities. An SDK client utilizes stable APIs under the hood for development purposes, while an API test \nclient is designed for testing raw APIs that are currently in development and may still contain bugs. What this means \nis the test client will NOT automatically validate requests and responses for you. It is the QA's main responsibility to \nwork on, and the test client will provide everything needed for testing APIs. \n\n\u003e [!TIP]\n\u003e If you would like to expand the client's use case beyond testing, you can optionally enable \"validation mode\" to make \n\u003e it partially behave like an SDK. When validation mode is enabled, your request payload will be validated in \n\u003e Pydantic strict mode on the client side.  \n\u003e Please note that validation against response is not currently supported.\n\n\n\n\n# Try it out\n\n## 1. Setup\n\n\u003e [!Important]\n\u003e Before moving forward, please make sure to create a new `venv` (requires Python 3.11+) and start with a fresh \n\u003e environment. We will use `pip install` to simplify the setup process, but this project is not intended to be used as \n\u003e a publicly available Python package. Since all dependencies are intentionally pinned strictly to exact versions, \n\u003e dependency conflicts may occur if the installation is done within your existing project. \n\n```\npip install git+https://github.com/yugokato/openapi-test-client.git\n```\n\nOr, if you want to try the demo client with the demo backend app:  \n```\npip install \"openapi-test-client[app] @ git+https://github.com/yugokato/openapi-test-client.git\"\n```\nAlternatively, you can clone the repo and run `pip install -e .` (or `pip install -e .[app]`) if you prefer to reuse \nthis project as your base project. \n\n\n## 2. Generate/Update API client code\n\nOnce the setup is complete, the `openapi-client` CLI command should be available for generating and updating an API \nclient from an OpenAPI spec file. It will take either `generate` or `update` as the first argument. Available command \noptions will change based on which action you choose. (see help of each action with `-h` for more details)\n\n```shell\n$ openapi-client -h\nusage: openapi-client [-h] {generate,update} ...\n\npositional arguments:\n  {generate,update}  Action to take\n    generate         Generate a new API client from an OpenAPI spec URL\n    update           Update an existing API client\n\noptions:\n  -h, --help         show this help message and exit\n\n```\n\n### Generate a new API client\n\nYou can generate a new API client from OpenAPI specs by running the `generate` command:\n```\nopenapi-client generate -u \u003curl\u003e -a \u003capp_name\u003e [--dir \u003cdirectory\u003e]\n```\n`url`: The URL of the OpenAPI spec file (JSON/YAML, must be version 3.x) from which you want to generate the API client  \n`app_name`: The name of the target application. The value will be used for generating the client name  \n`directory`: A directory path to save the generated client modules. The specified directory will become the top-level \nmodule for accessing all your clients. (This is optional if your project was cloned from the original repository)\n\n\u003cimg src=\"images/generate.gif\" width=\"750\"/\u003e\n\nTo generate another client for a different app, simply run the same command with the same `--dir` value, but with new \n`-u` and `-a` values. \n\n\u003e [!NOTE]\n\u003e The following manual step(s) will be needed after generating a client\n\u003e - For each auto-generated API function, rename the function name to whatever value you like. All functions will be generated as \"*\\_unnamed_endpoint\\_{idx}*\" by default\n\u003e - (If needed) Adjust the value of the API base URL in the `cfg/urls.json`, and the `doc` value in the generated API client's `__init__()` code. These values will be automatically added during the client generation based on the `url` value you specify, but your application may expect to split the URL differently\n\n\n### Update the existing API client\n\nYou can update the existing client code by running the `update` command to reflect the latest OpenAPI specs: \n\n```\nopenapi-client update -c \u003cclient_name\u003e\n```\n`client_name`: The name of the client assigned at the client generation step\n\n\u003cimg src=\"images/update.gif\" width=\"750\"/\u003e\n\nThis update functionality primarily focuses on reflecting the latest OpenAPI specs. It will only touch the following areas: \n- Existing API class tag\n- Existing API class functions (signatures and docstrings)\n- Existing API parameter models\n- Currently missing API functions and parameter models (a new function will be added as \"*\\_unnamed_endpoint_{idx}*\")\n- Currently missing API classes (this requires `-A` option)\n\nAnything else, (eg. API class/function names, function body, decorators applied on API functions, etc.) will remain \nintact so that updated client code won't break your existing test code.\n\n\u003e [!TIP]\n\u003e - You might want to run the command with `-d`/`--dry-run` option first to see changes before actually updating code\n\u003e - You can limit the scope of the update (eg. specific tag, specific endpoint, etc.) by using various command options. \n\u003e See more details with `-h`\n\n    \n## 3. Use the API client\n\nYour client should be ready to use by now. For demonstration purposes, the rest of the sections will be explained using \nthe [DemoAppAPIClient](src/openapi_test_client/clients/demo_app) that comes with this repository as an example client. This demo client was actually \ngenerated by the following `openapi-client generate` command against the Quart-based [demo app](src/demo_app) \ndemo application:\n```\nopenapi-client generate -u http://127.0.0.1:5000/openapi.json -a demo_app\n``` \nIt was also manually extended with a custom [post-request hook](src/openapi_test_client/clients/demo_app/api/request_hooks/post_request.py) for automatically managing auth tokens \nbefore login and after logout.\n\n\u003e [!NOTE]\n\u003e If you would like to try this `demo_app` API client by yourself, follow the following steps to start the demo application before moving forward:\n\u003e 1. Clone the repo\n\u003e 2. Set up with `app` server-side extra dependency  (see the \"Setup\" section)\n\u003e 3. Start the application with `QUART_APP=demo_app.main:app quart run` command\n\u003e ```shell\n\u003e $ QUART_APP=demo_app.main:app quart run\n\u003e * Serving Quart app 'demo_app'\n\u003e * Debug mode: False\n\u003e * Please use an ASGI server (e.g. Hypercorn) directly in production\n\u003e * Running on http://127.0.0.1:5000 (CTRL + C to quit)\n\u003e[2024-01-01 00:00:00 -0000] [39691] [INFO] Running on http://127.0.0.1:5000 (CTRL + C to quit)\n\u003e ```\n\n\n### Instantiate the client\n\nOnce you have generated an API client, the client class will be importable as \n`from \u003cyour_module\u003e.clients.\u003cclient_name\u003e import \u003cClientName\u003eAPIClient`. \n\n```pycon\n\u003e\u003e\u003e from openapi_test_client.clients.demo_app import DemoAppAPIClient\n\u003e\u003e\u003e client = DemoAppAPIClient()\n```\nMake sure to replace the \"openapi_test_client\" part with your own module name when importing your own clients.\n\nAlternatively, you can instantiate your client directly from the parent `OpenAPIClient` class.\n\n```pycon\n\u003e\u003e\u003e from openapi_test_client.clients import OpenAPIClient\n\u003e\u003e\u003e client = OpenAPIClient.get_client(\"\u003cclient_name\u003e\")\n```\n\n### Make an API request\n\nTo make an API request with your API client, call an API function as  `client.\u003cApiTag\u003e.\u003capi_function_name\u003e()`. \nThe function will take all parameters documented in the OpenAPI specs as keyword arguments. \n\neg. To call the login API defined under the Auth tag:\n```pycon\n\u003e\u003e\u003e r = client.Auth.login(username='foo', password='bar')\n2024-01-01T00:00:00.863-0800 - request: POST http://127.0.0.1:5000/v1/auth/login\n2024-01-01T00:00:00.877-0800 - response: 201 (Created)\n- request_id: a2b20acf-22d5-4131-ac0d-6796bf19d2af\n- request: POST http://127.0.0.1:5000/v1/auth/login\n- payload: {\"username\": \"foo\", \"password\": \"***\"}\n- status_code: 201 (Created)\n- response: {\n    \"token\": \"IjFlNTAxNmI2LTVlZjctNGQxYi1iMGJhLTYxY2M3ZWIzY2VmYSI.ZadDlA.tKS_La5uDuteXH7OMT_WZVK1o_hAnWZVn_J5rSsJQILHu1juXYg0EYLkgpH1LChzeOhN_YUUXaO37rlt_UV1Ag\"\n}\n- response_time: 0.01344s\n```\n\n\u003e [!NOTE] \n\u003e - A new UUID will be generated at each request and will be set to the `X-Request-ID` header\n\u003e - `DemoAppAPIClient` will automatically set the `token` value to the API session in the client's post-request logic mentioned earlier\n\nThe function call will return a `RestResponse` object where the decoded response is accessible as `response`. If you \nneed to access the raw request and response objects from the `requests` library, you can do so through the `request` \nand `_response` attributes.\n\n```pycon\n\u003e\u003e\u003e r.status_code\n201\n\u003e\u003e\u003e r.response\n{'token': 'IjFlNTAxNmI2LTVlZjctNGQxYi1iMGJhLTYxY2M3ZWIzY2VmYSI.ZadDlA.tKS_La5uDuteXH7OMT_WZVK1o_hAnWZVn_J5rSsJQILHu1juXYg0EYLkgpH1LChzeOhN_YUUXaO37rlt_UV1Ag'}\n\u003e\u003e\u003e pprint(r)\nRestResponse(_response=\u003cResponse [201],\n             request_id='a2b20acf-22d5-4131-ac0d-6796bf19d2af',\n             status_code=201,\n             response={'token': 'IjFlNTAxNmI2LTVlZjctNGQxYi1iMGJhLTYxY2M3ZWIzY2VmYSI.ZadDlA.tKS_La5uDuteXH7OMT_WZVK1o_hAnWZVn_J5rSsJQILHu1juXYg0EYLkgpH1LChzeOhN_YUUXaO37rlt_UV1Ag'},\n             response_time=0.01344,\n             request=\u003cPreparedRequest [POST]\u003e,\n             ok=True,\n             is_stream=False)\n```\nNote that we extend the `requests` library's `PreparedRequest` and `Session` classes to add a few small \ncapabilities. \nAs an example, you can get the request start/end time (UTC) through `request` as a `datatime` object.\n \n```pycon\n\u003e\u003e\u003e r.request.start_time\ndatetime.datetime(2024, 1, 1, 0, 0, 0, 86309, tzinfo=datetime.timezone.utc)\n\u003e\u003e\u003e r.request.end_time\ndatetime.datetime(2024, 1, 1, 0, 0, 0, 87714, tzinfo=datetime.timezone.utc)\n```\n\n## 4. Customize API functions (optional)\n\nThe test client provides a few ways to customize how an API call via an API function will be processed.\n\n### Implement your custom function logic\nBy default, each auto-generated API function will look like a stub function with a placeholder (`...`). For example:\n```python\n@endpoint.is_public\n@endpoint.post(\"/v1/auth/login\")\ndef login(self, *, username: str = Unset, password: str = Unset, **kwargs: Any) -\u003e RestResponse:\n    \"\"\"Login\"\"\"\n    ...\n```\n\nEven without having actual function logic, API requests made through these functions will just work - The function call will automatically make an HTTP request to the associated endpoint with the provided parameters, parse the response, and return it as a `RestResponse` object. This behavior is managed by the `@endpoint.\u003cmethod\u003e(path\u003e)` decorator.  \nIn most cases, you won’t need to modify the generated function body at all.\n\nHowever, if you need to implement custom logic for making an API request for specific API functions, you can replace the placeholder with your own code. Just ensure that the function returns a `RestResponse` object, which is what the underlying Rest API client we use will return.\n\n\u003e [!NOTE]\n\u003e The client will raise a `RuntimeError` if anything other than a `RestResponse` object or `None` is returned.\n\n\u003e [!TIP]\n\u003e If you only need to add extra behavior before or after the request, consider using decorators or request hooks instead\n\n### Decorators\nTo extend the default API call logic, you can apply decorators on an API function. Note that each decorator MUST be decorated with `@endpoint.decorator` for it to work properly.  \n\nFor example:\n- Define a decorator\n```python\nfrom functools import wraps\n\nfrom openapi_test_client.libraries.api import endpoint\n\n@endpoint.decorator\ndef my_decorator(f):\n    @wraps(f)\n    def wrapper(*args, **kwargs):\n        # Do something before request\n        response = f(**args, **kwargs)\n        # Do something after request\n        return response\n    return wrapper\n```\n\n- Apply it\n\n```python\n@my_decorator\n@endpoint.is_public\n@endpoint.post(\"/v1/auth/login\")\ndef login(self, *, username: str = Unset, password: str = Unset, **kwargs: Any) -\u003e RestResponse:\n    \"\"\"Login\"\"\"\n    ...\n```\n\n### Request Hooks\n\nThe test client supports three types of request hooks in API classes:\n- Pre-request Hook: Called before an API request is made\n- Post-request Hook: Called after an API request is completed\n- Request wrapper: Wraps the original API request logic before any decorators are applied, allowing you to apply additional behavior before and after a request\n\nHooks work similarly to decorators but are more useful for applying general control that affects multiple or all endpoints at the central place.\n\nFor examples and usage, see [demo_app client hooks](src/openapi_test_client/clients/demo_app/api/request_hooks)\n\n\n# Deep Dive \n\nThe following are some of the important building blocks of the API test client:\n- API client\n- API class and functions\n- API endpoint function\n- API endpoint model\n- API parameter model\n- Pydantic model (validation mode)\n\n\n## API client\n\nAll API clients will inherit the base client `OpenAPIClient`, which will handle parsing OpenAPI specs and resolving \n`$ref`. The generated API client will have a `@cached_property` definition for each API tag (or set of tags when \nmanaging APIs in multiple tags), where an API class instance will be returned. \n\n```python\n# openapi_test_client/clients/demo_app/demo_app_client.py\n\nfrom functools import cached_property\n\nfrom openapi_test_client.clients.base import OpenAPIClient\n\nfrom .api.auth import AuthAPI\nfrom .api.users import UsersAPI\n\n\nclass DemoAppAPIClient(OpenAPIClient):\n    \"\"\"API client for demo_app\"\"\"\n\n    def __init__(self, env: str = \"dev\") -\u003e None:\n        super().__init__(\"demo_app\", env=env, doc=\"openapi.json\")\n\n    @cached_property\n    def Auth(self) -\u003e AuthAPI:\n        return AuthAPI(self)\n\n    @cached_property\n    def Users(self) -\u003e UsersAPI:\n        return UsersAPI(self)\n```\n\n\n## API class and functions\n\nAll API endpoints for a tag (or set of tags) will be managed inside an \"API class\", where each function will map to a \nspecific endpoint documented in the OpenAPI spec. \n\nFor example, if the OpenAPI spec defines Auth APIs like this: \n```json\n\"paths\": {\n    \"/v1/auth/login\": {\n      \"post\": {\n        \"parameters\": [],\n        \"responses\": {},\n        \"description\": \"\",\n        \"summary\": \"Login\",\n        \"tags\": [\n          \"Auth\"\n        ],\n        \"security\":[],\n        \"requestBody\": {\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"properties\": {\n                  \"username\": {\n                    \"title\": \"Username\",\n                    \"type\": \"string\"\n                  },\n                  \"password\": {\n                    \"title\": \"Password\",\n                    \"type\": \"string\"\n                  }\n                },\n                \"required\": [\n                  \"username\",\n                  \"password\"\n                ],\n                \"title\": \"LoginData\",\n                \"type\": \"object\"\n              }\n            }\n          }\n        },\n        \"operationId\": \"post_login\"\n      }\n    },\n    \"/v1/auth/logout\": {\n      \"get\": {\n        \"parameters\": [],\n        \"responses\": {},\n        \"description\": \"\",\n        \"summary\": \"Logout\",\n        \"tags\": [\n          \"Auth\"\n        ],\n        \"security\":[],\n        \"operationId\": \"get_logout\"\n      }\n    },\n```\n\nThen the API class and functions will be generated like this:\n\n```python\n# openapi_test_client/clients/demo_app/api/auth.py\n\nfrom common_libs.clients.rest_client import RestResponse\n\nfrom openapi_test_client.clients.demo_app.api.base import DemoAppBaseAPI\nfrom openapi_test_client.libraries.api.api_functions import endpoint\nfrom openapi_test_client.libraries.api.types import Unset\n\n\nclass AuthAPI(DemoAppBaseAPI):\n    TAGs = (\"Auth\",)\n\n    @endpoint.is_public\n    @endpoint.post(\"/v1/auth/login\")\n    def login(self, *, username: str = Unset, password: str = Unset, **kwargs: Any) -\u003e RestResponse:\n        \"\"\"Login\"\"\"\n        ...\n\n    @endpoint.is_public\n    @endpoint.get(\"/v1/auth/logout\")\n    def logout(self, **kwargs: Any) -\u003e RestResponse:\n        \"\"\"Logout\"\"\"\n        ...\n\n```\nA function can take API path parameters and query/body parameters as arguments, where path parameters are always \ndefined as positional-only arguments, and other parameters are always defined as keyword-only arguments with a default \nvalue of `Unset`. Any parameters with this sentinel value will be excluded from the actual API call parameters.   \nAdditionally, it will always have `**kwargs` for supporting making a call with any undocumented \nparameters and/or with raw `requests` library options (eg. `timeout`, `headers`, etc) if you need to. \n\n\u003e [!NOTE]\n\u003e Parameters defined as optional in the OpenAPI spec will be annotated with `Optional[]` in the function signature, but \n\u003e this does not mean the parameter is actually optional for the endpoint. It just means you can hit the endpoint \n\u003e without specifying that parameter, whether it is actually required or not (for negative case testing)\n\nSome attributes available from the API class: \n```pycon\n\u003e\u003e\u003e # Get tag data\n\u003e\u003e\u003e client.Auth.TAGs\n('Auth',)\n\u003e\u003e\u003e # Get available endpoints under this API class \n\u003e\u003e\u003e pprint(client.Auth.endpoints)\n[Endpoint(tags=('Auth',),\n          api_class=\u003cclass 'openapi_test_client.clients.demo_app.api.auth.AuthAPI'\u003e,\n          method='post',\n          path='/v1/auth/login',\n          func_name='login',\n          model=\u003cclass 'types.AuthAPILoginEndpointModel'\u003e,\n          url=None,\n          content_type=None,\n          is_public=True,\n          is_documented=True,\n          is_deprecated=False),\n Endpoint(tags=('Auth',),\n          api_class=\u003cclass 'openapi_test_client.clients.demo_app.api.auth.AuthAPI'\u003e,\n          method='get',\n          path='/v1/auth/logout',\n          func_name='logout',\n          model=\u003cclass 'types.AuthAPILogoutEndpointModel'\u003e,\n          url=None,\n          content_type=None,\n          is_public=True,\n          is_documented=True,\n          is_deprecated=False)]\n```\n\nA list of defined API classes are available as `API_CLASSES`.\n```pycon\n\u003e\u003e\u003e from itertools import chain\n\u003e\u003e\u003e from openapi_test_client.clients.demo_app.api import API_CLASSES\n# list all andpoints\n\u003e\u003e\u003e all_endpoints = chain(*[x.endpoints for x in API_CLASSES])\n\u003e\u003e\u003e for endpoint in all_endpoints:\n...     print(endpint)\n... \nPOST /v1/auth/login\nGET /v1/auth/logout\nPOST /v1/users\nGET /v1/users/{user_id}\nGET /v1/users\nPOST /v1/users/images\nDELETE /v1/users/{user_id}\n```\n\n\n## API endpoint function\n\nEach API class function is decorated with a `@endpoint.\u003cmethod\u003e(\u003cpath\u003e)` endpoint decorator. This decorator converts the original function into an instance of the `EndpointHandler` class at runtime. The `EndpointHandler` object acts as a proxy to a per-endpoint `EndpointFunc` object, which is also created at runtime and is responsible for handling the actual API calls via its base class's `__call__()` method. Additionally, the `EndpointFunc` object provides various capabilities and attributes related to the endpoint.\n\neg. The Login API is accessible via `client.Auth.login()` API function, which is actually an instance of \n`AuthAPILoginEndpointFunc` class returned by its associated `EndpointHandler` obj.\n\n```pycon\n\u003e\u003e\u003e client.Auth.login\n\u003copenapi_test_client.libraries.api.api_functions.endpoint.AuthAPILoginEndpointFunc object at 0x1074abf10\u003e\n(mapped to: \u003cfunction AuthAPI.login at 0x10751c360\u003e)\n```\n\nThe endpoint function is also accessible directly from the API class:\n```pycon\n\u003e\u003e\u003e from openapi_test_client.clients.demo_app.api.auth import AuthAPI\n\u003e\u003e\u003e AuthAPI.login\n\u003copenapi_test_client.libraries.api.api_functions.endpoint.AuthAPILoginEndpointFunc object at 0x107650b50\u003e\n(mapped to: \u003cfunction AuthAPI.login at 0x10751c360\u003e)\n```\n\nVarious endpoint data is available from the endpoint function via `endpoint` property:\n```pycon\n\u003e\u003e\u003e print(client.Auth.login.endpoint)\nPOST /v1/auth/login\n\u003e\u003e\u003e pprint(client.Auth.login.endpoint)\nEndpoint(tags=('Auth',),\n         api_class=\u003cclass 'openapi_test_client.clients.demo_app.api.auth.AuthAPI'\u003e,\n         method='post',\n         path='/v1/auth/login',\n         func_name='login',\n         model=\u003cclass 'types.AuthAPILoginEndpointModel'\u003e,\n         url='http://127.0.0.1:5000/v1/auth/login',\n         content_type=None,\n         is_public=True,\n         is_documented=True,\n         is_deprecated=False)\n\u003e\u003e\u003e client.Auth.login.endpoint.method\n'post'\n\u003e\u003e\u003e client.Auth.login.endpoint.path\n'/v1/auth/login'\n\u003e\u003e\u003e client.Auth.login.endpoint.url\n'http://127.0.0.1:5000/v1/auth/login'\n```\n\nNote that the same endpoint data is also available directly from the API class, except for `url` will always be `None`.\n```pycon\n\u003e\u003e\u003e from openapi_test_client.clients.demo_app.api.auth import AuthAPI\n\u003e\u003e\u003e client.Auth.login.endpoint == AuthAPI.login.endpoint\nTrue\n\u003e\u003e\u003e print(AuthAPI.login.endpoint)\nPOST /v1/auth/login\n\u003e\u003e\u003e pprint(AuthAPI.login.endpoint)\nEndpoint(tags=('Auth',),\n         api_class=\u003cclass 'openapi_test_client.clients.demo_app.api.auth.AuthAPI'\u003e,\n         method='post',\n         path='/v1/auth/login',\n         func_name='login',\n         model=\u003cclass 'types.AuthAPILoginEndpointModel'\u003e,\n         url=None,\n         content_type=None,\n         is_public=True,\n         is_documented=True,\n         is_deprecated=False)\n```\n\nAn example of the additional capability the `EndpointFunc` obj provides - Automatic retry:\n```pycon\n# Call the endpiont with the automatic retry (you can specify a retry condition if needed)\n\u003e\u003e\u003e r = client.Auth.login.with_retry(username='foo', password='bar')\n2024-01-01T00:00:00.153-0000 - request: POST http://127.0.0.1:5000/v1/auth/login\n2024-01-01T00:00:00.158-0000 - response: 429 (Too Many Requests)\n- request_id: 1b028ff7-0880-430c-b5a3-12aa057892cf\n- request: POST http://127.0.0.1:5000/v1/auth/login\n- payload: {\"username\": \"foo\", \"password\": \"***\"}\n- status_code: 429 (Too Many Requests)\n{\n    \"error\": \"Too many requests\"\n}\n- response_time: 0.004804s\n2024-01-01T00:00:00.159-0000 - Retry condition matched. Retrying in 5 seconds...\n2024-01-01T00:00:05.166-0000 - request: POST http://127.0.0.1:5000/v1/auth/login\n2024-01-01T00:00:05.171-0000 - response: 201 (Created)\n- request_id: 1a34195e-5cec-4d4e-abe0-61acbfcdae1a\n- request: POST http://127.0.0.1:5000/v1/auth/login\n- payload: {\"username\": \"foo\", \"password\": \"***\"}\n- status_code: 201 (Created)\n- response: {\n    \"token\": \"ImJjMjA5YjkxLTEzYmItNDE3Yi1iN2ExLTdhNmFlNWFjYjFiNSI.ZuSs3Q.KD-zA6KxCUEC14YUAuCcuQNGNoulp2POuKq0yMIfkGIS0E--V473kssohnvGoIHo97FwwSeOV94NnwaZdGtSxA\"\n}\n- response_time: 0.005143s\n\u003e\u003e\u003e \n\u003e\u003e\u003e r.request.retried.request_id\n'1b028ff7-0880-430c-b5a3-12aa057892cf'\n```\n\n\n## API endpoint model\n\nEach endpoint is represented as an `EndpointModel` dataclass model, which holds various context around each \nparameter (eg. type annotation).\n```pycon\n\u003e\u003e\u003e model = client.Auth.login.endpoint.model\n\u003e\u003e\u003e print(model)\n\u003cclass 'types.AuthAPILoginEndpointModel'\u003e\n\u003e\u003e\u003e pprint(model.__dataclass_fields__, sort_dicts=False)\n{'username': Field(name='username',type=\u003cclass 'str'\u003e,default=\u003cobject object at 0x107b410b0\u003e,default_factory=\u003cdataclasses._MISSING_TYPE object at 0x107ea61d0\u003e,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=True,_field_type=_FIELD),\n 'password': Field(name='password',type=\u003cclass 'str'\u003e,default=\u003cobject object at 0x107b410b0\u003e,default_factory=\u003cdataclasses._MISSING_TYPE object at 0x107ea61d0\u003e,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=True,_field_type=_FIELD)}\n```\n\n## API parameter model\n\nIf a parameter type is documented as `object` (or `array` of objects) with `properties` in the OpenAPI specs, it will \nbe represented as a `ParamModel` dataclass model. These models will be automatically generated as a model file (`\u003cyour_module_dir\u003e/clients/\u003cclient_name\u003e/models/\u003capi_tag\u003e.py`) for each API class.\n\neg. \n\n- API function definition (note that the `metadata` parameter is of type `Metadata`)\n```python\n# openapi_test_client/clients/demo_app/api/users.py\n\nfrom typing import Annotated, Literal\n\nfrom common_libs.clients.rest_client import RestResponse\n\nfrom openapi_test_client.clients.demo_app.api.base import DemoAppBaseAPI\nfrom openapi_test_client.libraries.api.api_functions import endpoint\nfrom openapi_test_client.libraries.api.types import Constraint, Format, Optional, Unset\n\nfrom ..models.users import Metadata\n\n\nclass UsersAPI(DemoAppBaseAPI):\n    TAGs = (\"Users\",)\n\n    @endpoint.post(\"/v1/users\")\n    def create_user(\n        self,\n        *,\n        first_name: Annotated[str, Constraint(min_len=1, max_len=255)] = Unset,\n        last_name: Annotated[str, Constraint(min_len=1, max_len=255)] = Unset,\n        email: Annotated[str, Format(\"email\")] = Unset,\n        role: Literal[\"admin\", \"viewer\", \"support\"] = Unset,\n        metadata: Optional[Metadata] = Unset,\n        **kwargs: Any,\n    ) -\u003e RestResponse:\n        \"\"\"Create a new user\"\"\"\n        ...\n```\n\n- Parameter model definition\n```python\n# openapi_test_client/clients/demo_app/models/users.py\n\nfrom dataclasses import dataclass\nfrom typing import Annotated, Literal\n\nfrom openapi_test_client.libraries.api.types import Constraint, Format, Optional, ParamModel, Unset\n\n\n@dataclass\nclass Preferences(ParamModel):\n    theme: Optional[Literal[\"light\", \"dark\", \"system\"]] = Unset\n    language: Optional[str] = Unset\n    font_size: Optional[Annotated[int, Constraint(min=8, max=40, multiple_of=2)]] = Unset\n\n\n@dataclass\nclass SocialLinks(ParamModel):\n    facebook: Optional[Annotated[str, Format(\"uri\"), Constraint(min_len=1)]] = Unset\n    instagram: Optional[Annotated[str, Format(\"uri\"), Constraint(min_len=1)]] = Unset\n    linkedin: Optional[Annotated[str, Format(\"uri\"), Constraint(min_len=1)]] = Unset\n    github: Optional[Annotated[str, Format(\"uri\"), Constraint(min_len=1)]] = Unset\n\n\n@dataclass\nclass Metadata(ParamModel):\n    preferences: Optional[Preferences] = Unset\n    social_links: Optional[SocialLinks] = Unset\n\n```\n\n\u003e [!NOTE]\n\u003e Same like API function parameters:\n\u003e - All model attributes will be defined with a default value of `Unset`. Any parameters with this sentinel value will be excluded from an API call payload\n\u003e - Parameters defined as optional in the OpenAPI spec will be annotated with `Optional[]` in the model definition\n\nA parameter model is a customized `dataclass` that is intended to function exactly the same as a dictionary under the hood. \nUnlike the native dataclass, a parameter model instance will only hold fields that were explicitly given at instantiation. \nThis is very important when you want to differentiate test scenarios for \"not including a parameter in the payload\" v.s. \n\"explicitly giving a `null` value for a parameter\".\n\n```pycon\n\u003e\u003e\u003e from dataclasses import asdict, is_dataclass\n\u003e\u003e\u003e from openapi_test_client.clients.demo_app.models.users import *\n\u003e\u003e\u003e metadata = Metadata(preferences=Preferences(theme='dark'))\n\u003e\u003e\u003e print(metadata)\nMetadata(preferences=Preferences(theme='dark'))\n\u003e\u003e\u003e print(metadata.preferences)\nPreferences(theme='dark')\n\u003e\u003e\u003e print(metadata[\"preferences\"])\nPreferences(theme='dark')\n\u003e\u003e\u003e is_dataclass(metadata) and isinstance(metadata, dict)\nTrue\n\u003e\u003e\u003e metadata.items()\ndict_items([('preferences', Preferences(theme='dark'))])\n\u003e\u003e\u003e asdict(metadata)\n{'preferences': {'theme': 'dark'}}\n```\n\nParameter models function both as a dataclass and a dictionary, and a change made to one will be reflected in the other.\n```pycon\n\u003e\u003e\u003e # Update a model field value\n\u003e\u003e\u003e metadata.preferences.theme = \"light\"\n\u003e\u003e\u003e print(metadata)\nMetadata(preferences=Preferences(theme='light'))\n\u003e\u003e\u003e asdict(metadata)\n{'preferences': {'theme': 'light'}}\n\u003e\u003e\u003e\n\u003e\u003e\u003e # Update as a dictionary\n\u003e\u003e\u003e metadata.preferences[\"theme\"] = \"dark\"\n\u003e\u003e\u003e print(metadata)\nMetadata(preferences=Preferences(theme='dark'))\n\u003e\u003e\u003e asdict(metadata)\n{'preferences': {'theme': 'dark'}}\n```\n\nYou can also dynamically add or delete model fields similar to how you can add or remove a key from a dictionary, if you need to.\n```pycon\n\u003e\u003e\u003e # Add a new field to the model\n\u003e\u003e\u003e metadata.new_attr1 = 123\n\u003e\u003e\u003e print(metadata)\nMetadata(preferences=Preferences(theme='dark'), new_attr1=123)\n\u003e\u003e\u003e asdict(metadata)\n{'preferences': {'theme': 'dark'}, 'new_attr1': 123}\n\u003e\u003e\u003e \n\u003e\u003e\u003e # Add a new field to the dictionary\n\u003e\u003e\u003e metadata[\"new_attr2\"] = 456\n\u003e\u003e\u003e print(metadata)\nMetadata(preferences=Preferences(theme='dark'), new_attr1=123, new_attr2=456)\n\u003e\u003e\u003e asdict(metadata)\n{'preferences': {'theme': 'dark'}, 'new_attr1': 123, 'new_attr2': 456}\n\u003e\u003e\u003e\n\u003e\u003e\u003e # Delete a field from the model\n\u003e\u003e\u003e delattr(metadata, \"new_attr2\")\n\u003e\u003e\u003e print(metadata)\nMetadata(preferences=Preferences(theme='dark'), new_attr1=123)\n\u003e\u003e\u003e asdict(metadata)\n{'preferences': {'theme': 'dark'}, 'new_attr1': 123}\n\u003e\u003e\u003e\n\u003e\u003e\u003e # Delete a field from the dictionary\n\u003e\u003e\u003e del metadata[\"new_attr1\"]\n\u003e\u003e\u003e print(metadata)\nMetadata(preferences=Preferences(theme='dark'))\n\u003e\u003e\u003e asdict(metadata)\n{'preferences': {'theme': 'dark'}}\n```\n\n## Pydantic models (Validation mode)\n\nBy default, our client models will not perform any validation for you, since they are based on `dataclass`. This allows \nthe test client to send whatever invalid values for a parameter to verify the server-side validation logic.    \nTo expand the use case of the client beyond testing, you can optionally enable \"validation mode\". When enabled, all of \nour dataclass models (Endpoint/Param models) will be automatically converted into native Pydantic models that inherit \nthis custom base model class:\n\n```python\nfrom typing import ClassVar\n\nfrom pydantic import BaseModel, ConfigDict\n\n\nclass PydanticModel(BaseModel):\n    \"\"\"Base class for Pydantic endpoint/param models\"\"\"\n\n    model_config: ClassVar = ConfigDict(extra=\"forbid\", validate_assignment=True, strict=True)\n```\n\nWith this dynamic conversion, request parameters will be automatically validated in Pydantic strict mode before an API \nrequest call.  \n\nHere are some comparisons between regular models and pydantic models:\n\n- Regular dataclass model (Validation will be done on the server-side)\n```pycon \n\u003e\u003e\u003e # Model definition\n\u003e\u003e\u003e model = client.Users.create_user.endpoint.model\n\u003e\u003e\u003e print(model)\n\u003cclass 'types.UsersAPICreateUserEndpointModel'\u003e\n\u003e\u003e\u003e pprint(model.__dataclass_fields__, sort_dicts=False)\n{'first_name': Field(name='first_name',type=typing.Annotated[str, Constraint(min=None, max=None, multiple_of=None, min_len=1, max_len=255, nullable=None, pattern=None, exclusive_minimum=None, exclusive_maximum=None)],default=\u003cobject object at 0x107b410b0\u003e,default_factory=\u003cdataclasses._MISSING_TYPE object at 0x107ea61d0\u003e,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=True,_field_type=_FIELD),\n 'last_name': Field(name='last_name',type=typing.Annotated[str, Constraint(min=None, max=None, multiple_of=None, min_len=1, max_len=255, nullable=None, pattern=None, exclusive_minimum=None, exclusive_maximum=None)],default=\u003cobject object at 0x107b410b0\u003e,default_factory=\u003cdataclasses._MISSING_TYPE object at 0x107ea61d0\u003e,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=True,_field_type=_FIELD),\n 'email': Field(name='email',type=typing.Annotated[str, Format(value='email')],default=\u003cobject object at 0x107b410b0\u003e,default_factory=\u003cdataclasses._MISSING_TYPE object at 0x107ea61d0\u003e,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=True,_field_type=_FIELD),\n 'role': Field(name='role',type=typing.Literal['admin', 'viewer', 'support'],default=\u003cobject object at 0x107b410b0\u003e,default_factory=\u003cdataclasses._MISSING_TYPE object at 0x107ea61d0\u003e,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=True,_field_type=_FIELD),\n 'metadata': Field(name='metadata',type=typing.Optional[openapi_test_client.clients.demo_app.models.users.Metadata],default=\u003cobject object at 0x107b410b0\u003e,default_factory=\u003cdataclasses._MISSING_TYPE object at 0x107ea61d0\u003e,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=True,_field_type=_FIELD)}\n\u003e\u003e\u003e\n\u003e\u003e\u003e # Make an API request with the invalid parameter values\n\u003e\u003e\u003e r = client.Users.create_user(first_name=123, email=\"foo\", role=\"something\", metadata=Metadata(social_links=SocialLinks(facebook=\"test\")), extra=123)\n2024-01-01T00:00:00.741-0800 - The request contains one or more parameters UsersAPI.create_user() does not expect:\n- extra\n2024-01-01T00:00:00.742-0800 - request: POST http://127.0.0.1:5000/v1/users\n2024-01-01T00:00:00.752-0800 - response: 400 (Bad Request)\n- request_id: 91b4ac05-1280-473b-ac41-0be30903c448\n- request: POST http://127.0.0.1:5000/v1/users\n- payload: {\"first_name\": 123, \"email\": \"foo\", \"role\": \"something\", \"metadata\": {\"social_links\": {\"facebook\": \"test\"}}, \"extra\": 123}\n- status_code: 400 (Bad Request)\n- response: {\n    \"error\": {\n        \"code\": 400,\n        \"message\": [\n            {\n                \"type\": \"string_type\",\n                \"loc\": [\n                    \"first_name\"\n                ],\n                \"msg\": \"Input should be a valid string\",\n                \"input\": 123,\n                \"url\": \"https://errors.pydantic.dev/2.9/v/string_type\"\n            },\n            {\n                \"type\": \"missing\",\n                \"loc\": [\n                    \"last_name\"\n                ],\n                \"msg\": \"Field required\",\n                \"input\": {\n                    \"first_name\": 123,\n                    \"email\": \"foo\",\n                    \"role\": \"something\",\n                    \"metadata\": {\n                        \"social_links\": {\n                            \"facebook\": \"test\"\n                        }\n                    },\n                    \"extra\": 123\n                },\n                \"url\": \"https://errors.pydantic.dev/2.9/v/missing\"\n            },\n            {\n                \"type\": \"value_error\",\n                \"loc\": [\n                    \"email\"\n                ],\n                \"msg\": \"value is not a valid email address: An email address must have an @-sign.\",\n                \"input\": \"foo\",\n                \"ctx\": {\n                    \"reason\": \"An email address must have an @-sign.\"\n                },\n                \"url\": \"https://errors.pydantic.dev/2.9/v/value_error\"\n            },\n            {\n                \"type\": \"enum\",\n                \"loc\": [\n                    \"role\"\n                ],\n                \"msg\": \"Input should be 'admin', 'viewer' or 'support'\",\n                \"input\": \"something\",\n                \"ctx\": {\n                    \"expected\": \"'admin', 'viewer' or 'support'\"\n                },\n                \"url\": \"https://errors.pydantic.dev/2.9/v/enum\"\n            },\n            {\n                \"type\": \"url_parsing\",\n                \"loc\": [\n                    \"metadata\",\n                    \"social_links\",\n                    \"facebook\"\n                ],\n                \"msg\": \"Input should be a valid URL, relative URL without a base\",\n                \"input\": \"test\",\n                \"ctx\": {\n                    \"error\": \"relative URL without a base\"\n                },\n                \"url\": \"https://errors.pydantic.dev/2.9/v/url_parsing\"\n            }\n        ],\n        \"request_id\": \"91b4ac05-1280-473b-ac41-0be30903c448\"\n    }\n}\n- response_time: 0.010107s\n```\n\n\u003cbr\u003e\n\n- Pydantic model (Validation will be done on the client-side)\n```pycon\n\u003e\u003e\u003e # Model definition\n\u003e\u003e\u003e pydantic_model = client.Users.create_user.endpoint.model.to_pydantic()\n\u003e\u003e\u003e print(pydantic_model)\n\u003cclass 'types.UsersAPICreateUserEndpointModelPydantic'\u003e\n\u003e\u003e\u003e pprint(pydantic_model.model_fields, sort_dicts=False)\n{'first_name': FieldInfo(annotation=str, required=True, metadata=[MinLen(min_length=1), MaxLen(max_length=255)]),\n 'last_name': FieldInfo(annotation=str, required=True, metadata=[MinLen(min_length=1), MaxLen(max_length=255)]),\n 'email': FieldInfo(annotation=EmailStr, required=True, metadata=[Format(value='email')]),\n 'role': FieldInfo(annotation=Literal['admin', 'viewer', 'support'], required=True),\n 'metadata': FieldInfo(annotation=Union[MetadataPydantic, NoneType], required=False, default=None)}\n\u003e\u003e\u003e\n\u003e\u003e\u003e # Make an API request with the same invalid parmeter values, but with validate=True option\n\u003e\u003e\u003e r = client.Users.create_user(first_name=123, email=\"foo\", role=\"something\", metadata=Metadata(social_links=SocialLinks(facebook=\"test\")), extra=123, validate=True)\n2024-01-01T00:00:00.830-0800 - The request contains one or more parameters UsersAPI.create_user() does not expect:\n- extra\nTraceback (most recent call last):\n  File \"\u003cstdin\u003e\", line 1, in \u003cmodule\u003e\n    \u003csnip\u003e\n    raise ValueError(\nValueError: Request parameter validation failed.\n6 validation errors for UsersAPICreateUserEndpointModelPydantic\nfirst_name\n  Input should be a valid string [type=string_type, input_value=123, input_type=int]\n    For further information visit https://errors.pydantic.dev/2.9/v/string_type\nlast_name\n  Field required [type=missing, input_value={'first_name': 123, 'emai... 'test'}}, 'extra': 123}, input_type=dict]\n    For further information visit https://errors.pydantic.dev/2.9/v/missing\nemail\n  value is not a valid email address: An email address must have an @-sign. [type=value_error, input_value='foo', input_type=str]\nrole\n  Input should be 'admin', 'viewer' or 'support' [type=literal_error, input_value='something', input_type=str]\n    For further information visit https://errors.pydantic.dev/2.9/v/literal_error\nmetadata.social_links.facebook\n  Input should be a valid URL, relative URL without a base [type=url_parsing, input_value='test', input_type=str]\n    For further information visit https://errors.pydantic.dev/2.9/v/url_parsing\nextra\n  Extra inputs are not permitted [type=extra_forbidden, input_value=123, input_type=int]\n    For further information visit https://errors.pydantic.dev/2.9/v/extra_forbidden\n```\n\n\u003e [!TIP]\n\u003e The above example enables validation mode \"per request\" by passing `validate=True` to the API function call. \n\u003e Alternatively, you can also set `VALIDATION_MODE='true'` environment variable to globally enable the validation mode.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyugokato%2Fopenapi-test-client","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyugokato%2Fopenapi-test-client","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyugokato%2Fopenapi-test-client/lists"}