From 86e6af0c459aa3d61b5864a480617c1ee091e964 Mon Sep 17 00:00:00 2001 From: Justin Black Date: Sat, 19 Aug 2023 08:50:10 -0700 Subject: [PATCH 1/9] Adds sample that contains contains schemas --- .../python_3_1_0_json_schema.yaml | 5 + .../3_1_0_json_schema/python/.gitignore | 67 + .../3_1_0_json_schema/python/.gitlab-ci.yml | 15 + .../python/.openapi-generator-ignore | 23 + .../python/.openapi-generator/FILES | 73 + .../python/.openapi-generator/VERSION | 1 + .../3_1_0_json_schema/python/.travis.yml | 11 + .../client/3_1_0_json_schema/python/README.md | 198 +++ .../python/docs/apis/tags/default_api.md | 17 + .../schema/any_type_contains_value.md | 12 + .../components/schema/array_contains_value.md | 12 + .../python/docs/paths/some_path/get.md | 105 ++ .../content/application_json/schema.md | 10 + .../python/docs/servers/server_0.md | 7 + .../3_1_0_json_schema/python/git_push.sh | 58 + .../python/migration_2_0_0.md | 203 +++ .../python/migration_3_0_0.md | 30 + .../migration_other_python_generators.md | 77 + .../3_1_0_json_schema/python/pyproject.toml | 41 + .../python/src/json_schema_api/__init__.py | 26 + .../python/src/json_schema_api/api_client.py | 1398 +++++++++++++++++ .../src/json_schema_api/api_response.py | 28 + .../src/json_schema_api/apis/__init__.py | 3 + .../src/json_schema_api/apis/path_to_api.py | 17 + .../json_schema_api/apis/paths/__init__.py | 3 + .../json_schema_api/apis/paths/some_path.py | 13 + .../src/json_schema_api/apis/tag_to_api.py | 17 + .../src/json_schema_api/apis/tags/__init__.py | 3 + .../json_schema_api/apis/tags/default_api.py | 18 + .../json_schema_api/components/__init__.py | 0 .../components/schema/__init__.py | 5 + .../schema/any_type_contains_value.py | 13 + .../components/schema/array_contains_value.py | 25 + .../components/schemas/__init__.py | 15 + .../configurations/__init__.py | 0 .../configurations/api_configuration.py | 281 ++++ .../configurations/schema_configuration.py | 94 ++ .../python/src/json_schema_api/exceptions.py | 132 ++ .../src/json_schema_api/paths/__init__.py | 3 + .../paths/some_path/__init__.py | 5 + .../paths/some_path/get/__init__.py | 0 .../paths/some_path/get/operation.py | 113 ++ .../paths/some_path/get/responses/__init__.py | 0 .../get/responses/response_200/__init__.py | 29 + .../response_200/content/__init__.py | 0 .../content/application_json/__init__.py | 0 .../content/application_json/schema.py | 13 + .../python/src/json_schema_api/py.typed | 0 .../python/src/json_schema_api/rest.py | 270 ++++ .../src/json_schema_api/schemas/__init__.py | 148 ++ .../src/json_schema_api/schemas/format.py | 115 ++ .../schemas/original_immutabledict.py | 97 ++ .../src/json_schema_api/schemas/schema.py | 703 +++++++++ .../src/json_schema_api/schemas/schemas.py | 375 +++++ .../src/json_schema_api/schemas/validation.py | 986 ++++++++++++ .../src/json_schema_api/security_schemes.py | 227 +++ .../python/src/json_schema_api/server.py | 34 + .../src/json_schema_api/servers/__init__.py | 0 .../src/json_schema_api/servers/server_0.py | 11 + .../shared_imports/__init__.py | 0 .../shared_imports/header_imports.py | 15 + .../shared_imports/operation_imports.py | 18 + .../shared_imports/response_imports.py | 25 + .../shared_imports/schema_imports.py | 28 + .../shared_imports/security_scheme_imports.py | 12 + .../shared_imports/server_imports.py | 13 + .../python/test-requirements.txt | 2 + .../3_1_0_json_schema/python/test/__init__.py | 0 .../python/test/components/__init__.py | 0 .../python/test/components/schema/__init__.py | 0 .../schema/test_any_type_contains_value.py | 23 + .../schema/test_array_contains_value.py | 23 + .../python/test/test_paths/__init__.py | 68 + .../test_paths/test_some_path/__init__.py | 0 .../test_paths/test_some_path/test_get.py | 35 + .../client/3_1_0_json_schema/python/tox.ini | 10 + src/test/resources/3_1/json_schema.yaml | 33 + 77 files changed, 6490 insertions(+) create mode 100644 bin/generate_samples_configs/python_3_1_0_json_schema.yaml create mode 100644 samples/client/3_1_0_json_schema/python/.gitignore create mode 100644 samples/client/3_1_0_json_schema/python/.gitlab-ci.yml create mode 100644 samples/client/3_1_0_json_schema/python/.openapi-generator-ignore create mode 100644 samples/client/3_1_0_json_schema/python/.openapi-generator/FILES create mode 100644 samples/client/3_1_0_json_schema/python/.openapi-generator/VERSION create mode 100644 samples/client/3_1_0_json_schema/python/.travis.yml create mode 100644 samples/client/3_1_0_json_schema/python/README.md create mode 100644 samples/client/3_1_0_json_schema/python/docs/apis/tags/default_api.md create mode 100644 samples/client/3_1_0_json_schema/python/docs/components/schema/any_type_contains_value.md create mode 100644 samples/client/3_1_0_json_schema/python/docs/components/schema/array_contains_value.md create mode 100644 samples/client/3_1_0_json_schema/python/docs/paths/some_path/get.md create mode 100644 samples/client/3_1_0_json_schema/python/docs/paths/some_path/get/responses/response_200/content/application_json/schema.md create mode 100644 samples/client/3_1_0_json_schema/python/docs/servers/server_0.md create mode 100644 samples/client/3_1_0_json_schema/python/git_push.sh create mode 100644 samples/client/3_1_0_json_schema/python/migration_2_0_0.md create mode 100644 samples/client/3_1_0_json_schema/python/migration_3_0_0.md create mode 100644 samples/client/3_1_0_json_schema/python/migration_other_python_generators.md create mode 100644 samples/client/3_1_0_json_schema/python/pyproject.toml create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/__init__.py create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/api_client.py create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/api_response.py create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/apis/__init__.py create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/apis/path_to_api.py create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/apis/paths/__init__.py create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/apis/paths/some_path.py create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/apis/tag_to_api.py create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/apis/tags/__init__.py create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/apis/tags/default_api.py create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/components/__init__.py create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/__init__.py create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/any_type_contains_value.py create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/array_contains_value.py create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schemas/__init__.py create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/configurations/__init__.py create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/configurations/api_configuration.py create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/configurations/schema_configuration.py create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/exceptions.py create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/paths/__init__.py create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/paths/some_path/__init__.py create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/paths/some_path/get/__init__.py create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/paths/some_path/get/operation.py create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/paths/some_path/get/responses/__init__.py create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/paths/some_path/get/responses/response_200/__init__.py create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/paths/some_path/get/responses/response_200/content/__init__.py create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/paths/some_path/get/responses/response_200/content/application_json/__init__.py create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/paths/some_path/get/responses/response_200/content/application_json/schema.py create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/py.typed create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/rest.py create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/schemas/__init__.py create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/schemas/format.py create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/schemas/original_immutabledict.py create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/schemas/schema.py create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/schemas/schemas.py create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/schemas/validation.py create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/security_schemes.py create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/server.py create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/servers/__init__.py create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/servers/server_0.py create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/shared_imports/__init__.py create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/shared_imports/header_imports.py create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/shared_imports/operation_imports.py create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/shared_imports/response_imports.py create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/shared_imports/schema_imports.py create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/shared_imports/security_scheme_imports.py create mode 100644 samples/client/3_1_0_json_schema/python/src/json_schema_api/shared_imports/server_imports.py create mode 100644 samples/client/3_1_0_json_schema/python/test-requirements.txt create mode 100644 samples/client/3_1_0_json_schema/python/test/__init__.py create mode 100644 samples/client/3_1_0_json_schema/python/test/components/__init__.py create mode 100644 samples/client/3_1_0_json_schema/python/test/components/schema/__init__.py create mode 100644 samples/client/3_1_0_json_schema/python/test/components/schema/test_any_type_contains_value.py create mode 100644 samples/client/3_1_0_json_schema/python/test/components/schema/test_array_contains_value.py create mode 100644 samples/client/3_1_0_json_schema/python/test/test_paths/__init__.py create mode 100644 samples/client/3_1_0_json_schema/python/test/test_paths/test_some_path/__init__.py create mode 100644 samples/client/3_1_0_json_schema/python/test/test_paths/test_some_path/test_get.py create mode 100644 samples/client/3_1_0_json_schema/python/tox.ini create mode 100644 src/test/resources/3_1/json_schema.yaml diff --git a/bin/generate_samples_configs/python_3_1_0_json_schema.yaml b/bin/generate_samples_configs/python_3_1_0_json_schema.yaml new file mode 100644 index 00000000000..771332c43bc --- /dev/null +++ b/bin/generate_samples_configs/python_3_1_0_json_schema.yaml @@ -0,0 +1,5 @@ +generatorName: python +outputDir: samples/client/3_1_0_json_schema/python +inputSpec: src/test/resources/3_1/json_schema.yaml +additionalProperties: + packageName: json_schema_api diff --git a/samples/client/3_1_0_json_schema/python/.gitignore b/samples/client/3_1_0_json_schema/python/.gitignore new file mode 100644 index 00000000000..a62e8aba43f --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/.gitignore @@ -0,0 +1,67 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt +dev-requirements.txt.log + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ +venv/ +.venv/ +.python-version +.pytest_cache + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +#Ipython Notebook +.ipynb_checkpoints diff --git a/samples/client/3_1_0_json_schema/python/.gitlab-ci.yml b/samples/client/3_1_0_json_schema/python/.gitlab-ci.yml new file mode 100644 index 00000000000..6a58a19c436 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/.gitlab-ci.yml @@ -0,0 +1,15 @@ +# ref: https://docs.gitlab.com/ee/ci/README.html + +stages: + - test + +.tests: + stage: test + script: + - pip install -r requirements.txt + - pip install -r test-requirements.txt + - pytest --cov=json_schema_api + +test-3.8: + extends: .tests + image: python:3.8-alpine diff --git a/samples/client/3_1_0_json_schema/python/.openapi-generator-ignore b/samples/client/3_1_0_json_schema/python/.openapi-generator-ignore new file mode 100644 index 00000000000..d24a2da8ae5 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/.openapi-generator-ignore @@ -0,0 +1,23 @@ +# OpenAPI Generator Ignore +# Generated by openapi-generator https://github.com/openapi-json-schema-tools/openapi-json-schema-generator + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md diff --git a/samples/client/3_1_0_json_schema/python/.openapi-generator/FILES b/samples/client/3_1_0_json_schema/python/.openapi-generator/FILES new file mode 100644 index 00000000000..a1b5d271b6d --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/.openapi-generator/FILES @@ -0,0 +1,73 @@ +.gitignore +.gitlab-ci.yml +.openapi-generator-ignore +.travis.yml +README.md +docs/apis/tags/default_api.md +docs/components/schema/any_type_contains_value.md +docs/components/schema/array_contains_value.md +docs/paths/some_path/get.md +docs/paths/some_path/get/responses/response_200/content/application_json/schema.md +docs/servers/server_0.md +git_push.sh +migration_2_0_0.md +migration_3_0_0.md +migration_other_python_generators.md +pyproject.toml +src/json_schema_api/__init__.py +src/json_schema_api/api_client.py +src/json_schema_api/api_response.py +src/json_schema_api/apis/__init__.py +src/json_schema_api/apis/path_to_api.py +src/json_schema_api/apis/paths/__init__.py +src/json_schema_api/apis/paths/some_path.py +src/json_schema_api/apis/tag_to_api.py +src/json_schema_api/apis/tags/__init__.py +src/json_schema_api/apis/tags/default_api.py +src/json_schema_api/components/__init__.py +src/json_schema_api/components/schema/__init__.py +src/json_schema_api/components/schema/any_type_contains_value.py +src/json_schema_api/components/schema/array_contains_value.py +src/json_schema_api/components/schemas/__init__.py +src/json_schema_api/configurations/__init__.py +src/json_schema_api/configurations/api_configuration.py +src/json_schema_api/configurations/schema_configuration.py +src/json_schema_api/exceptions.py +src/json_schema_api/paths/__init__.py +src/json_schema_api/paths/some_path/__init__.py +src/json_schema_api/paths/some_path/get/__init__.py +src/json_schema_api/paths/some_path/get/operation.py +src/json_schema_api/paths/some_path/get/responses/__init__.py +src/json_schema_api/paths/some_path/get/responses/response_200/__init__.py +src/json_schema_api/paths/some_path/get/responses/response_200/content/__init__.py +src/json_schema_api/paths/some_path/get/responses/response_200/content/application_json/__init__.py +src/json_schema_api/paths/some_path/get/responses/response_200/content/application_json/schema.py +src/json_schema_api/py.typed +src/json_schema_api/rest.py +src/json_schema_api/schemas/__init__.py +src/json_schema_api/schemas/format.py +src/json_schema_api/schemas/original_immutabledict.py +src/json_schema_api/schemas/schema.py +src/json_schema_api/schemas/schemas.py +src/json_schema_api/schemas/validation.py +src/json_schema_api/security_schemes.py +src/json_schema_api/server.py +src/json_schema_api/servers/__init__.py +src/json_schema_api/servers/server_0.py +src/json_schema_api/shared_imports/__init__.py +src/json_schema_api/shared_imports/header_imports.py +src/json_schema_api/shared_imports/operation_imports.py +src/json_schema_api/shared_imports/response_imports.py +src/json_schema_api/shared_imports/schema_imports.py +src/json_schema_api/shared_imports/security_scheme_imports.py +src/json_schema_api/shared_imports/server_imports.py +test-requirements.txt +test/__init__.py +test/components/__init__.py +test/components/schema/__init__.py +test/components/schema/test_any_type_contains_value.py +test/components/schema/test_array_contains_value.py +test/test_paths/__init__.py +test/test_paths/test_some_path/__init__.py +test/test_paths/test_some_path/test_get.py +tox.ini diff --git a/samples/client/3_1_0_json_schema/python/.openapi-generator/VERSION b/samples/client/3_1_0_json_schema/python/.openapi-generator/VERSION new file mode 100644 index 00000000000..717311e32e3 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/.openapi-generator/VERSION @@ -0,0 +1 @@ +unset \ No newline at end of file diff --git a/samples/client/3_1_0_json_schema/python/.travis.yml b/samples/client/3_1_0_json_schema/python/.travis.yml new file mode 100644 index 00000000000..e9d810b7a9c --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/.travis.yml @@ -0,0 +1,11 @@ +# ref: https://docs.travis-ci.com/user/languages/python +language: python +python: + - "3.8" + - "3.9" +# command to install dependencies +install: + - "pip install -r requirements.txt" + - "pip install -r test-requirements.txt" +# command to run tests +script: pytest --cov=json_schema_api diff --git a/samples/client/3_1_0_json_schema/python/README.md b/samples/client/3_1_0_json_schema/python/README.md new file mode 100644 index 00000000000..997e429b100 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/README.md @@ -0,0 +1,198 @@ +# json-schema-api +No description provided (generated by Openapi JSON Schema Generator https://github.com/openapi-json-schema-tools/openapi-json-schema-generator) + +This Python package is automatically generated by the [OpenAPI JSON Schema Generator](https://github.com/openapi-json-schema-tools/openapi-json-schema-generator) project: + +- API version: 1.0.0 +- Package version: 1.0.0 +- Build package: PythonClientGenerator + +## Requirements + +Python >=3.8 + +## Migration Guides +- [3.0.0 Migration Guide](migration_3_0_0.md) +- [2.0.0 Migration Guide](migration_2_0_0.md) +- [Migration from Other Python Generators](migration_other_python_generators.md) + + +## Installation +### pip install + +If the python package is hosted on a repository, you can install directly using: + +```sh +pip install git+https://github.com/GIT_USER_ID/GIT_REPO_ID.git +``` +(you may need to run `pip` with root permission: `sudo pip install git+https://github.com/GIT_USER_ID/GIT_REPO_ID.git`) + +Then import the package: +```python +import json_schema_api +``` + +### Setuptools + +Install via [Setuptools](http://pypi.python.org/pypi/setuptools). + +```sh +python -m pip install . --user +``` +(or `python -m pip install .` to install the package for all users) + +Then import the package: +```python +import json_schema_api +``` + +## Usage Notes +### Validation, Immutability, and Data Type +This python code validates data to schema classes and return back an immutable instance containing the data +which subclasses all validated schema classes. This ensure that +- valid data cannot be mutated and become invalid to a set of schemas + - the one exception is that files are not immutable, so schema instances storing/sending/receiving files are not immutable + +Here is the mapping from json schema types to python subclassed types: +| Json Schema Type | Python Base Class | +| ---------------- | ----------------- | +| object | schemas.immutabledict | +| array | tuple | +| string | str | +| number | float, int | +| integer | int | +| boolean | bool | +| null | None | +| AnyType (unset) | typing.Union[schemas.immutabledict, tuple, str, float, int, bool, None] | + +### Storage of Json Schema Definition in Python Classes +In openapi v3.0.3 there are ~ 28 json schema keywords. Almost all of them can apply if +type is unset. I have chosen to separate the storage of json schema definition info and output +validated classes for payload instantiation. + +
+ Reason + +This json schema data is stored in each class that is written for a schema, in a component or +other openapi document location. This class is only responsible for storing schema info. +Output classes like those that store dict payloads are written separately and are +returned by the Schema.validate method when that method is passed in dict input. +This prevents payload property access methods from +colliding with json schema definition. +
+ +### Json Schema Type Object +Most component schemas (models) are probably of type object. Which is a map data structure. +Json schema allows string keys in this map, which means schema properties can have key names that are +invalid python variable names. Names like: +- "hi-there" +- "1variable" +- "@now" +- " " +- "from" + +To allow these use cases to work, schemas.immutabledict is used as the base class of type object schemas. +This means that one can use normal dict methods on instances of these classes. + +
+ Other Details + +- optional properties which were not set will not exist in the instance +- None is only allowed in as a value if type: "null" was included or nullable: true was set +- preserving the original key names is required to properly validate a payload to multiple json schemas +
+ +### Json Schema Type + Format, Validated Data Storage +N schemas can be validated on the same payload. +To allow multiple schemas to validate, the data must be stored using one base class whether or not +a json schema format constraint exists in the schema. +See the below accessors for string data: +- type string + format: See schemas.as_date, schemas.as_datetime, schemas.as_decimal, schemas.as_uuid + +In json schema, type: number with no format validates both integers and floats, +so int and float values are stored for type number. + +
+ String + Date Example + +For example the string payload '2023-12-20' is validates to both of these schemas: +1. string only +``` +- type: string +``` +2. string and date format +``` +- type: string + format: date +``` +Because of use cases like this, a datetime.date is allowed as an input to this schema, but the data +is stored as a string. +
+ +## Getting Started + +Please follow the [installation procedure](#installation) and then run the following: + +```python +import json_schema_api +from json_schema_api.configurations import api_configuration +from json_schema_api.apis.tags import default_api +from pprint import pprint +used_configuration = api_configuration.ApiConfiguration( +) +# Enter a context with an instance of the API client +with json_schema_api.ApiClient(used_configuration) as api_client: + # Create an instance of the API class + api_instance = default_api.DefaultApi(api_client) + + # example, this endpoint has no required or optional parameters + try: + api_response = api_instance.get_some_path() + pprint(api_response) + except json_schema_api.ApiException as e: + print("Exception when calling DefaultApi->get_some_path: %s\n" % e) +``` + +## Servers +server_index | Class | Description +------------ | ----- | ------------ +0 | [Server0](docs/servers/server_0.md) | + +## Endpoints + +All URIs are relative to the selected server +- The server is selected by passing in server_info and server_index into api_configuration.ApiConfiguration +- Code samples in endpoints documents show how to do this +- server_index can also be passed in to endpoint calls, see endpoint documentation + +HTTP request | Method | Description +------------ | ------ | ------------- +/somePath **get** | [DefaultApi](docs/apis/tags/default_api.md).[get_some_path](docs/paths/some_path/get.md) | + +## Component Schemas + +Class | Description +----- | ------------ +[AnyTypeContainsValue](docs/components/schema/any_type_contains_value.md) | +[ArrayContainsValue](docs/components/schema/array_contains_value.md) | + +## Notes for Large OpenAPI documents +If the OpenAPI document is large, imports in json_schema_api.apis.tags.tag_to_api and json_schema_api.components.schemas may fail with a +RecursionError indicating the maximum recursion limit has been exceeded. In that case, there are a couple of solutions: + +Solution 1: +Use specific imports for apis and models like: +- tagged api: `from json_schema_api.apis.tags.default_api import DefaultApi` +- api for one path: `from json_schema_api.apis.paths.some_path import SomePath` +- api for one operation (path + verb): `from json_schema_api.paths.some_path.get import ApiForget` +- single model import: `from json_schema_api.components.schema.pet import Pet` + +Solution 2: +Before importing the package, adjust the maximum recursion limit as shown below: +``` +import sys +sys.setrecursionlimit(1500) +import json_schema_api +from json_schema_api.apis.tags.tag_to_api import * +from json_schema_api.components.schemas import * +``` diff --git a/samples/client/3_1_0_json_schema/python/docs/apis/tags/default_api.md b/samples/client/3_1_0_json_schema/python/docs/apis/tags/default_api.md new file mode 100644 index 00000000000..e60d861c4f9 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/docs/apis/tags/default_api.md @@ -0,0 +1,17 @@ + +json_schema_api.apis.tags.default_api +# DefaultApi + +## Description +operations that lack tags are assigned this default tag + +All URIs are relative to the selected server +- The server is selected by passing in server_info and server_index into api_configuration.ApiConfiguration +- Code samples in endpoints documents show how to do this +- server_index can also be passed in to endpoint calls, see endpoint documentation + +Method | Description +------ | ------------- +[**get_some_path**](../../paths/some_path/get.md) | + +[[Back to top]](#top) [[Back to Endpoints]](../../../README.md#Endpoints) [[Back to README]](../../../README.md) diff --git a/samples/client/3_1_0_json_schema/python/docs/components/schema/any_type_contains_value.md b/samples/client/3_1_0_json_schema/python/docs/components/schema/any_type_contains_value.md new file mode 100644 index 00000000000..9df36e2ee52 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/docs/components/schema/any_type_contains_value.md @@ -0,0 +1,12 @@ +# AnyTypeContainsValue +json_schema_api.components.schema.any_type_contains_value +``` +type: schemas.Schema +``` + +## validate method +Input Type | Return Type | Notes +------------ | ------------- | ------------- +dict, schemas.immutabledict, str, datetime.date, datetime.datetime, uuid.UUID, int, float, bool, None, list, tuple, bytes, io.FileIO, io.BufferedReader | schemas.immutabledict, str, float, int, bool, None, tuple, bytes, io.FileIO | + +[[Back to top]](#top) [[Back to Component Schemas]](../../../README.md#Component-Schemas) [[Back to README]](../../../README.md) diff --git a/samples/client/3_1_0_json_schema/python/docs/components/schema/array_contains_value.md b/samples/client/3_1_0_json_schema/python/docs/components/schema/array_contains_value.md new file mode 100644 index 00000000000..1cce4190ed0 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/docs/components/schema/array_contains_value.md @@ -0,0 +1,12 @@ +# ArrayContainsValue +json_schema_api.components.schema.array_contains_value +``` +type: schemas.Schema +``` + +## validate method +Input Type | Return Type | Notes +------------ | ------------- | ------------- + | | + +[[Back to top]](#top) [[Back to Component Schemas]](../../../README.md#Component-Schemas) [[Back to README]](../../../README.md) diff --git a/samples/client/3_1_0_json_schema/python/docs/paths/some_path/get.md b/samples/client/3_1_0_json_schema/python/docs/paths/some_path/get.md new file mode 100644 index 00000000000..086f162630c --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/docs/paths/some_path/get.md @@ -0,0 +1,105 @@ +json_schema_api.paths.some_path.operation +# Operation Method Name + +| Method Name | Api Class | Notes | +| ----------- | --------- | ----- | +| get_some_path | [DefaultApi](../../apis/tags/default_api.md) | This api is only for tag=default | +| get | ApiForGet | This api is only for this endpoint | +| get | SomePath | This api is only for path=/somePath | + +## Table of Contents +- [General Info](#general-info) +- [Arguments](#arguments) +- [Return Types](#return-types) +- [Servers](#servers) +- [Code Sample](#code-sample) + +## General Info +| Field | Value | +| ----- | ----- | +| Path | "/somePath" | +| HTTP Method | get | + +## Arguments + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- +accept_content_types | typing.Tuple[str] | default is ("application/json", ) | Tells the server the content type(s) that are accepted by the client +server_index | typing.Optional[int] | default is None | Allows one to select a different [server](#servers). If not None, must be one of [0] +stream | bool | default is False | if True then the response.content will be streamed and loaded from a file like object. When downloading a file, set this to True to force the code to deserialize the content to a FileSchema file +timeout | typing.Optional[typing.Union[int, typing.Tuple]] | default is None | the timeout used by the rest client +skip_deserialization | bool | default is False | when True, headers and body will be unset and an instance of api_response.ApiResponseWithoutDeserialization will be returned + +## Return Types + +HTTP Status Code | Class | Description +------------- | ------------- | ------------- +n/a | api_response.ApiResponseWithoutDeserialization | When skip_deserialization is True this response is returned +200 | [ResponseFor200.ApiResponse](#responsefor200-apiresponse) | OK + +## ResponseFor200 + +### Description +OK + +### ResponseFor200 ApiResponse +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- +response | urllib3.HTTPResponse | Raw response | +[body](#responsefor200-body) | schemas.immutabledict, str, float, int, bool, None, tuple, bytes, io.FileIO | | +headers | Unset | headers were not defined | + +### ResponseFor200 Body +Content-Type | Schema +------------ | ------- +"application/json" | [content.application_json.Schema](#responsefor200-content-applicationjson-schema) + +### Body Details +#### ResponseFor200 content ApplicationJson Schema +json_schema_api.paths.some_path.get.responses.response_200.content.application_json.schema +``` +type: schemas.Schema +``` + +##### validate method +Input Type | Return Type | Notes +------------ | ------------- | ------------- +dict, schemas.immutabledict, str, datetime.date, datetime.datetime, uuid.UUID, int, float, bool, None, list, tuple, bytes, io.FileIO, io.BufferedReader | schemas.immutabledict, str, float, int, bool, None, tuple, bytes, io.FileIO | + +## Servers + +Set the available servers by defining your used servers in ApiConfiguration.server_info +Then select your server by setting a server index in ApiConfiguration.server_index_info or by +passing server_index in to the endpoint method. +- these servers are the general api servers +- defaults to server_index=0, server.url = http://api.example.xyz/v1 + +server_index | Class | Description +------------ | ----- | ------------ +0 | [Server0](../../servers/server_0.md) | + +## Code Sample + +```python +import json_schema_api +from json_schema_api.configurations import api_configuration +from json_schema_api.apis.tags import default_api +from pprint import pprint +used_configuration = api_configuration.ApiConfiguration( +) +# Enter a context with an instance of the API client +with json_schema_api.ApiClient(used_configuration) as api_client: + # Create an instance of the API class + api_instance = default_api.DefaultApi(api_client) + + # example, this endpoint has no required or optional parameters + try: + api_response = api_instance.get_some_path() + pprint(api_response) + except json_schema_api.ApiException as e: + print("Exception when calling DefaultApi->get_some_path: %s\n" % e) +``` + +[[Back to top]](#top) +[[Back to DefaultApi API]](../../apis/tags/default_api.md) +[[Back to Endpoints]](../../../README.md#Endpoints) [[Back to README]](../../../README.md) \ No newline at end of file diff --git a/samples/client/3_1_0_json_schema/python/docs/paths/some_path/get/responses/response_200/content/application_json/schema.md b/samples/client/3_1_0_json_schema/python/docs/paths/some_path/get/responses/response_200/content/application_json/schema.md new file mode 100644 index 00000000000..c69534cbdd3 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/docs/paths/some_path/get/responses/response_200/content/application_json/schema.md @@ -0,0 +1,10 @@ +# Schema +json_schema_api.paths.some_path.get.responses.response_200.content.application_json.schema +``` +type: schemas.Schema +``` + +## validate method +Input Type | Return Type | Notes +------------ | ------------- | ------------- +dict, schemas.immutabledict, str, datetime.date, datetime.datetime, uuid.UUID, int, float, bool, None, list, tuple, bytes, io.FileIO, io.BufferedReader | schemas.immutabledict, str, float, int, bool, None, tuple, bytes, io.FileIO | diff --git a/samples/client/3_1_0_json_schema/python/docs/servers/server_0.md b/samples/client/3_1_0_json_schema/python/docs/servers/server_0.md new file mode 100644 index 00000000000..d3076afc978 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/docs/servers/server_0.md @@ -0,0 +1,7 @@ +json_schema_api.servers.server_0 +# Server Server0 + +## Url +http://api.example.xyz/v1 + +[[Back to top]](#top) [[Back to Servers]](../../README.md#Servers) [[Back to README]](../../README.md) diff --git a/samples/client/3_1_0_json_schema/python/git_push.sh b/samples/client/3_1_0_json_schema/python/git_push.sh new file mode 100644 index 00000000000..ced3be2b0c7 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/git_push.sh @@ -0,0 +1,58 @@ +#!/bin/sh +# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/ +# +# Usage example: /bin/sh ./git_push.sh wing328 openapi-pestore-perl "minor update" "gitlab.com" + +git_user_id=$1 +git_repo_id=$2 +release_note=$3 +git_host=$4 + +if [ "$git_host" = "" ]; then + git_host="github.com" + echo "[INFO] No command line input provided. Set \$git_host to $git_host" +fi + +if [ "$git_user_id" = "" ]; then + git_user_id="GIT_USER_ID" + echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id" +fi + +if [ "$git_repo_id" = "" ]; then + git_repo_id="GIT_REPO_ID" + echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id" +fi + +if [ "$release_note" = "" ]; then + release_note="Minor update" + echo "[INFO] No command line input provided. Set \$release_note to $release_note" +fi + +# Initialize the local directory as a Git repository +git init + +# Adds the files in the local repository and stages them for commit. +git add . + +# Commits the tracked changes and prepares them to be pushed to a remote repository. +git commit -m "$release_note" + +# Sets the new remote +git_remote=`git remote` +if [ "$git_remote" = "" ]; then # git remote not defined + + if [ "$GIT_TOKEN" = "" ]; then + echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment." + git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git + else + git remote add origin https://${git_user_id}:${GIT_TOKEN}@${git_host}/${git_user_id}/${git_repo_id}.git + fi + +fi + +git pull origin master + +# Pushes (Forces) the changes in the local repository up to the remote repository +echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git" +git push origin master 2>&1 | grep -v 'To https' + diff --git a/samples/client/3_1_0_json_schema/python/migration_2_0_0.md b/samples/client/3_1_0_json_schema/python/migration_2_0_0.md new file mode 100644 index 00000000000..ed19050a503 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/migration_2_0_0.md @@ -0,0 +1,203 @@ +# Migration v1.X.X to v2.0.0 + +- [Compatibility note for opeanpi-generator](#compatibility-note-for-opeanpi-generator) +- [Component Generation](#component-generation) +- [Packaging Changes](#packaging-changes) +- [Path Generation](#path-generation) +- [Configuration Info Refactored](#configuration-info-refactored) +- [Servers and Security Generation](#servers-and-security-generation) +- [Java Classes for Openapi Data Refactored](#java-classes-for-openapi-data-refactored) +- [Api Access by Tags and Paths Updated](#api-access-by-tags-and-paths-updated) +- [Some Method/Property/Input/Class Names Updated](#some-methodpropertyinputclass-names-updated) +- [Documentation Updated](#documentation-updated) + +## Compatibility note for opeanpi-generator +The v1.0.4 release is nearly identical to the openapi-generator v6.3.0 release + +Below is a summary of big changes when updating you code from v1.X.X to 2.0.0 + +## Component Generation +Update: +Openapi document components in "#/components/x" are now generated in a components/x packages +Ths applies to: +- headers +- parameters +- request_bodies +- responses +- schemas +- security_schemes + +The generator now writes a class for each of those generated components. + +### Reason +A lot of openapi data is $ref references throughout an openapi document. +With this update, those $ref source locations are generated with file system paths that closely mirror the openapi +document json schema path to that info. $ref definition is then imported in generated python code. +This minimizes the amount of code that is generated, imposes well defined encapsulation and allows templates to be +re-used for many types of data regardless of where the data is in the source openapi document. + +### Action +- Update where you are importing models from, models are now component schemas + - File path change: model/pet.py -> components/schema/pet.py + +## Packaging changes +Code has been updated to use .toml packaging. Code is now distributed in a src directory + +### Reason +These updates follow latest python packaging best practices + +### Action +- if you are suppressing generation of any files, you will need to edit those file paths + - File Path Change: package_name -> src/package_name + +## Path Generation +If paths contain inline descriptions of parameters, request bodies, responses, security, or servers, +then those are now generated in separate files. Those files are imported into the endpoint code. +File locations closely mirror the openapi data json schema path. + +### Reason +Generating those files in paths that closely mirror the json schema paths will allow +the generator to use $ref to any location in the openapi document in the future, not just components. +These could include: +- relative $refs +- a request body $ref referring to the request body in another endpoint + +This also allowed the generation code to work the same way regardless of where the header/requestBody etc +is in the source spec. + +Note: +Operations are now at +paths/somePath/get/operation.py +and not at +paths/somePath/get/__init__.py +to minimize the amount of memory needed when importing the python package. +The configurations.api_configuration.py file imports all servers defined at: +- openapi document root +- pathItem +- pathItem operation +And if servers were defined in pathItem operations, then every operation that contains a server would be imported +in api_configuration.py. Which could be many many files. + +So instead +- operation endpoint definition was moved to paths/somePath/get/operation.py +- paths/somePath/get/__init__.py stays empty and does not need lots of memory when api_configuration.py imports servers +- server information was kept in paths/somePath/get/servers/server_x.py + +### Action +- if you are importing any endpoint form its file, update your import + - File path update: paths/somePath/get.py -> paths/somePath/get/operation.py + +## Configuration Info Refactored +Configuration information was separated into two classes +- configurations/api_configuration.py ApiConfiguration for: + - server info + - security (auth) info + - logging info +- configurations/schema_configuration.py SchemaConfiguration for: + - disabled openapi/json-schema keywords + +### Reason +Schema validation only relies on SchemaConfiguration data and does not need to know about any ApiConfiguration info +General api configuration info was poorly structured in the legacy configuration class which had 13 inputs. +The refactored ApiConfiguration now has 4 inputs which define servers and security info. +Having these separate classes prevents circular imports when the schemas.py file imports its SchemaConfiguration class. + +### Action +- When you instantiate ApiClient, update your code to pass in an instance of SchemaConfiguration + ApiConfiguration + + +## Servers and Security Generation +Servers are now generated as separate files with one class per file wherever they were defined in the openapi document +- servers/server_0.py +- paths/somePath/servers/server_0.py +- paths/somePath/get/servers/server_0.py +Security requirements objects are now generated as separate files with one security requirement object per file +wherever they were defined in the openapi document +- security/security_requirement_object_0.py +- paths/somePath/get/security/security_requirement_object_0.py + +### Reason +Server classes now re-use schema validation code to ensure that inputs to server variables are valid. +Generating these separate files minimizes generated code and maximizes code re-use. + +### Action +- If endpoints need to use specific servers or security not at index 0, pass security_index_info + server_index_info + into ApiConfiguration when instantiating it +- If you use non-default server variable values, then update your code to pass in server_info into ApiConfiguration + +## Java Classes for Openapi Data Refactored +Almost every java class used to store openapi document data at generation time has been re-written or refactored. +The new classes are much shorter and contain only what is needed to generate code and documentation. +This will make it much easier to add new openapi v3.1.0 features like new json schema keywords. +Generator interface methods were also refactored to simplify Java model instantiation and code generation. +Almost all properties are now public static final and are: +- immutable +- publicly accessible to the templates + +### Reason +These updates make the code much more maintainable. +The number of properties that have to be maintained is much smaller +- Component Schema: ~100 properties in CodegenModel.java -> ~50 properties in CodegenSchema.java +- PathItem Operation: ~75 properties CodegenOperation: ~25 properties + +This will reduce bugs like: why can't I access this property in this template +Because instances are mostly immutable it is very clear where the wrong value is being created/assigned. + +### Action +- if you are customizing the python generator, you will need to update your java code + +## Api Access By Tags and Paths Updated +Previously, keys togo from tags and paths to apis were enums. +The code was updated to use a TypedDict and uses strings as the keys. + +### Reason +Making this change allows type hinting to work for the TypedDict with string keys + +### Action +- If you use path_to_api.py or tag_to_api.py, update the key that you use to access the api + +## Some Method/Property/Input/Class Names Updated +- is_true_oapg -> is_true_ +- is_false_oapg -> is_false_ +- is_none_oapg -> is_none_ +- as_date_oapg -> as_date_ +- as_datetime_oapg -> as_datetime_ +- as_decimal_oapg -> as_decimal_ +- as_uuid_oapg -> as_uuid_ +- as_float_oapg -> as_float_ +- as_int_oapg -> as_int_ +- get_item_oapg -> get_item_ +- from_openapi_data_oapg -> from_openapi_data_ +- _verify_typed_dict_inputs_oapg -> _verify_typed_dict_inputs +- _configuration -> configuration_ +- _arg -> arg_ +- _args -> args_ +- MetaOapg -> Schema_ +- JsonSchema -> OpenApiSchema + +### Reason +Classes can have arbitrarily named properties set on them +Endpoints can have arbitrary operationId method names set +For those reasons, I use the prefix and suffix _ to greatly reduce the likelihood of collisions +on protected + public classes/methods. + +### Action +- if you use the above methods/inputs/properties/classes update them to the latest names + +## Documentation Updated +- components now have sections in the readme + - models became component schemas +- one file is now generated for each endpoint + - that file now has a table of contents + - heading indentation levels now are indented correctly in descending order + - endpoint now includes server and security info (if security exists) +- servers section added to readme if servers were defined in the openapi document root +- security section added to readme if security was defined in the openapi document root + +### Reason +Endpoint documentation had indentation and linking bugs before, this fixes them. +When all endpoints docs were written in one file it was difficult to link ot the correct section. +How to set server and security info was unclear before, the new docs and classes should clarify that. + +### Action +- if you link to specific parts of the documentation, update your links \ No newline at end of file diff --git a/samples/client/3_1_0_json_schema/python/migration_3_0_0.md b/samples/client/3_1_0_json_schema/python/migration_3_0_0.md new file mode 100644 index 00000000000..a7ee4715667 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/migration_3_0_0.md @@ -0,0 +1,30 @@ +# Migration v2.0.0 to v3.0.0 + +- DynamicSchema classes are no longer produced, + all schemas are validated, but only one schema class produces output classes for instantiation. + Instances no longer subclass all validated schemas. + - the schema that is used is the first schema that is checked + - so if you depended on .methodName access on a oneOf schema, that will no longer work + - instead use instance['methodName'] access + - so if you depended on isinstance(instance SomeOneOfSchema) instead check that the instance contains + the needed oneOf properties that you are trying to use +- SomeSchema.__new__ no longer validates payloads + - instead use SomeSchema.validate or SomeSchemaDict.__new__ +- SomeSchema.Schema_ inner class has been moved to the class SomeSchema + - so if you need to access openapi schema info, use SomeSchema + - so if you depended on SomeSchema.SOME_ENUM, update it to SomeSchema.enums.SOME_ENUM +- instance.get_item_ methods have been removed to reduce amount of generated code + - optional properties are now generated as @property methods + - so one can use instance.someProp OR instance.get('someProp', schemas.unset) +- instance methods as_date_, as_datetime_, as_decimal_, as_uuid_ have been changed to functions and moved into the schemas module + - now that python primitives are returned from validation for str/int/float/bool/None, custom methods could not be provided for those instances + - so instead update your code to use schemas.as_date/as_datetime/as_decimal/as_uuid +- Output classes are only written for json schema type object (dict) and array (tuple) types + - so if you depended on boolean/null/string/number instances being an instance of a Schema class + update your code to handle the primitive values instead +- NoneClass instances no longer returned from schema validation, None returned instead + - so update your code to handle None values +- BoolClass instances no longer returned from schema validation, True/False returned instead + - so update you code to handle bool values +- Decimal instances are no longer returned for type integer or type number schemas, int or float values are returned + - so update your code to use the int or float values diff --git a/samples/client/3_1_0_json_schema/python/migration_other_python_generators.md b/samples/client/3_1_0_json_schema/python/migration_other_python_generators.md new file mode 100644 index 00000000000..3195e008bcb --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/migration_other_python_generators.md @@ -0,0 +1,77 @@ +# Migration from Other Python Generators + +When switching from other python client generators you will need to make some changes to your code. + +1. This generator uses spec case for all (object) property names and parameter names. + - So if the spec has a property name like camelCase, it will use camelCase rather than camel_case + - So you will need to update how you input and read properties to use spec case + - endpoint calls will need to have their input arguments updated + - schema instance property usage and instantiation will need to be updated +2. Endpoint parameters are stored in dictionaries to prevent collisions (explanation below) + - So you will need to update how you pass data in to endpoints + - update your endpoint calls to pass in parameter data in path_params, query_params, header_params etc dict inputs +3. Endpoint responses now include the original response, the deserialized response body, and (todo)the deserialized headers + - So you will need to update your code to use response.body to access deserialized data +4. All validated class instances are immutable except for ones based on io.File + - This is because if properties were changed after validation, that validation would no longer apply + - So no changing values or property values after a class has been instantiated +5. String + Number types with formats + - String type data is stored as a string and if you need to access types based on its format like date, + date-time, uuid, number etc then you will need to use schemas accessor functions on the instance + - type string + format: See schemas.as_date, schemas.as_datetime, schemas.as_decimal, schemas.as_uuid + - this was done because openapi/json-schema defines constraints. string data may be type string with no format + keyword in one schema, and include a format constraint in another schema + - So if you need to access a string format based type, use as_date/as_datetime/as_decimal/as_uuid +6. Property access on AnyType(type unset) or object(dict) schemas + - Only required keys with valid python names are properties like .someProp and have type hints + - All optional keys may not exist, properties are defined for them and if they are unset schemas.unset will be returned + - One can also access optional values with dict_instance['optionalProp'] and KeyError will be raised if it does not exist +7. The location of the api classes has changed + - Api classes are located in your_package.apis.tags.some_api + - This change was made to eliminate redundant code generation + - Legacy generators generated the same endpoint twice if it had > 1 tag on it + - This generator defines an endpoint in one class, then inherits that class to generate + apis by tags and by paths + - This change reduces code and allows quicker run time if you use the path apis + - path apis are at your_package.apis.paths.some_path + - Those apis will only load their needed models, which is less to load than all of the resources needed in a tag api + - So you will need to update your import paths to the api classes + +### Why are Leading and Trailing Underscores in some class and method names? +Classes can have arbitrarily named properties set on them +Endpoints can have arbitrary operationId method names set +For those reasons, I use the prefix and suffix _ to greatly reduce the likelihood of collisions +on protected + public classes/methods. + +### Object property spec case +This was done because when payloads are ingested, they can be validated against N number of schemas. +If the input signature used a different property name then that has mutated the payload. +So SchemaA and SchemaB must both see the camelCase spec named variable. +Also it is possible to send in two properties, named camelCase and camel_case in the same payload. +That use case should work, so spec case is used. + +### Parameter spec case +Parameters can be included in different locations including: +- query +- path +- header +- cookie + +Any of those parameters could use the same parameter names, so if every parameter +was included as an endpoint parameter in a function signature, they would collide. +For that reason, each of those inputs have been separated out into separate typed dictionaries: +- query_params +- path_params +- header_params +- cookie_params + +So when updating your code, you will need to pass endpoint parameters in using those +dictionaries. + +### Endpoint responses +Endpoint responses have been enriched to now include more information. +Any response reom an endpoint will now include the following properties: +response: urllib3.HTTPResponse +body: typing.Union[schemas.Unset, typing.Union[str, inf, float, None, schemas.immutabledict, tuple, bytes, schemas.FileIO]] +headers: typing.Union[schemas.Unset, HeaderSchemaDict] +Note: response header deserialization has not yet been added \ No newline at end of file diff --git a/samples/client/3_1_0_json_schema/python/pyproject.toml b/samples/client/3_1_0_json_schema/python/pyproject.toml new file mode 100644 index 00000000000..e6fecdc7932 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/pyproject.toml @@ -0,0 +1,41 @@ +# Generated by: https://github.com/openapi-json-schema-tools/openapi-json-schema-generator + +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools.package-data] +"json_schema_api" = ["py.typed"] + +[project] +name = "json-schema-api" +version = "1.0.0" +authors = [ + { name="OpenAPI JSON Schema Generator community" }, +] +description = "Example" +readme = "README.md" +requires-python = ">=3.8" +dependencies = [ + "certifi >= 14.5.14", + "immutabledict ~= 3.0.0", + "python-dateutil ~= 2.7.0", + "setuptools >= 61.0", + "types-python-dateutil", + "types-urllib3", + "typing_extensions ~= 4.5.0", + "urllib3 ~= 2.0.a3", +] +classifiers = [ + "Programming Language :: Python :: 3", + "License :: MIT", + "Operating System :: OS Independent", + "Topic :: Software Development :: Code Generators" +] + + +[[tool.mypy.overrides]] +module = 'json_schema_api' \ No newline at end of file diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/__init__.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/__init__.py new file mode 100644 index 00000000000..f97c9a6e952 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/__init__.py @@ -0,0 +1,26 @@ +# coding: utf-8 + +# flake8: noqa + +""" + Example + No description provided (generated by Openapi JSON Schema Generator https://github.com/openapi-json-schema-tools/openapi-json-schema-generator) # noqa: E501 + The version of the OpenAPI document: 1.0.0 + Generated by: https://github.com/openapi-json-schema-tools/openapi-json-schema-generator +""" + +__version__ = "1.0.0" + +# import ApiClient +from json_schema_api.api_client import ApiClient + +# import Configuration +from json_schema_api.configurations.api_configuration import ApiConfiguration + +# import exceptions +from json_schema_api.exceptions import OpenApiException +from json_schema_api.exceptions import ApiAttributeError +from json_schema_api.exceptions import ApiTypeError +from json_schema_api.exceptions import ApiValueError +from json_schema_api.exceptions import ApiKeyError +from json_schema_api.exceptions import ApiException diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/api_client.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/api_client.py new file mode 100644 index 00000000000..c1759f7b4ed --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/api_client.py @@ -0,0 +1,1398 @@ +# coding: utf-8 +""" + Example + No description provided (generated by Openapi JSON Schema Generator https://github.com/openapi-json-schema-tools/openapi-json-schema-generator) # noqa: E501 + The version of the OpenAPI document: 1.0.0 + Generated by: https://github.com/openapi-json-schema-tools/openapi-json-schema-generator +""" + +from __future__ import annotations +import abc +import datetime +import dataclasses +import decimal +import enum +import email +import json +import os +import io +import atexit +from multiprocessing import pool +import re +import tempfile +import typing +import typing_extensions +from urllib import parse +import urllib3 +from urllib3 import _collections, fields + + +from json_schema_api import exceptions, rest, schemas, security_schemes, api_response +from json_schema_api.configurations import api_configuration, schema_configuration as schema_configuration_ + + +class JSONEncoder(json.JSONEncoder): + compact_separators = (',', ':') + + def default(self, obj: typing.Any): + if isinstance(obj, str): + return str(obj) + elif isinstance(obj, float): + return obj + elif isinstance(obj, bool): + # must be before int check + return obj + elif isinstance(obj, int): + return obj + elif obj is None: + return None + elif isinstance(obj, (dict, schemas.immutabledict)): + return {key: self.default(val) for key, val in obj.items()} + elif isinstance(obj, (list, tuple)): + return [self.default(item) for item in obj] + raise exceptions.ApiValueError('Unable to prepare type {} for serialization'.format(obj.__class__.__name__)) + + +class ParameterInType(enum.Enum): + QUERY = 'query' + HEADER = 'header' + PATH = 'path' + COOKIE = 'cookie' + + +class ParameterStyle(enum.Enum): + MATRIX = 'matrix' + LABEL = 'label' + FORM = 'form' + SIMPLE = 'simple' + SPACE_DELIMITED = 'spaceDelimited' + PIPE_DELIMITED = 'pipeDelimited' + DEEP_OBJECT = 'deepObject' + + +@dataclasses.dataclass +class PrefixSeparatorIterator: + # A class to store prefixes and separators for rfc6570 expansions + prefix: str + separator: str + first: bool = True + item_separator: str = dataclasses.field(init=False) + + def __post_init__(self): + self.item_separator = self.separator if self.separator in {'.', '|', '%20'} else ',' + + def __iter__(self): + return self + + def __next__(self): + if self.first: + self.first = False + return self.prefix + return self.separator + + +class ParameterSerializerBase: + @staticmethod + def __ref6570_item_value(in_data: typing.Any, percent_encode: bool): + """ + Get representation if str/float/int/None/items in list/ values in dict + None is returned if an item is undefined, use cases are value= + - None + - [] + - {} + - [None, None None] + - {'a': None, 'b': None} + """ + if type(in_data) in {str, float, int}: + if percent_encode: + return parse.quote(str(in_data)) + return str(in_data) + elif in_data is None: + # ignored by the expansion process https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.1 + return None + elif isinstance(in_data, list) and not in_data: + # ignored by the expansion process https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.1 + return None + elif isinstance(in_data, dict) and not in_data: + # ignored by the expansion process https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.1 + return None + raise exceptions.ApiValueError('Unable to generate a ref6570 item representation of {}'.format(in_data)) + + @staticmethod + def _to_dict(name: str, value: str): + return {name: value} + + @classmethod + def __ref6570_str_float_int_expansion( + cls, + variable_name: str, + in_data: typing.Any, + explode: bool, + percent_encode: bool, + prefix_separator_iterator: PrefixSeparatorIterator, + var_name_piece: str, + named_parameter_expansion: bool + ) -> str: + item_value = cls.__ref6570_item_value(in_data, percent_encode) + if item_value is None or (item_value == '' and prefix_separator_iterator.separator == ';'): + return next(prefix_separator_iterator) + var_name_piece + value_pair_equals = '=' if named_parameter_expansion else '' + return next(prefix_separator_iterator) + var_name_piece + value_pair_equals + item_value + + @classmethod + def __ref6570_list_expansion( + cls, + variable_name: str, + in_data: typing.Any, + explode: bool, + percent_encode: bool, + prefix_separator_iterator: PrefixSeparatorIterator, + var_name_piece: str, + named_parameter_expansion: bool + ) -> str: + item_values = [cls.__ref6570_item_value(v, percent_encode) for v in in_data] + item_values = [v for v in item_values if v is not None] + if not item_values: + # ignored by the expansion process https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.1 + return "" + value_pair_equals = '=' if named_parameter_expansion else '' + if not explode: + return ( + next(prefix_separator_iterator) + + var_name_piece + + value_pair_equals + + prefix_separator_iterator.item_separator.join(item_values) + ) + # exploded + return next(prefix_separator_iterator) + next(prefix_separator_iterator).join( + [var_name_piece + value_pair_equals + val for val in item_values] + ) + + @classmethod + def __ref6570_dict_expansion( + cls, + variable_name: str, + in_data: typing.Any, + explode: bool, + percent_encode: bool, + prefix_separator_iterator: PrefixSeparatorIterator, + var_name_piece: str, + named_parameter_expansion: bool + ) -> str: + in_data_transformed = {key: cls.__ref6570_item_value(val, percent_encode) for key, val in in_data.items()} + in_data_transformed = {key: val for key, val in in_data_transformed.items() if val is not None} + if not in_data_transformed: + # ignored by the expansion process https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.1 + return "" + value_pair_equals = '=' if named_parameter_expansion else '' + if not explode: + return ( + next(prefix_separator_iterator) + + var_name_piece + value_pair_equals + + prefix_separator_iterator.item_separator.join( + prefix_separator_iterator.item_separator.join( + item_pair + ) for item_pair in in_data_transformed.items() + ) + ) + # exploded + return next(prefix_separator_iterator) + next(prefix_separator_iterator).join( + [key + '=' + val for key, val in in_data_transformed.items()] + ) + + @classmethod + def _ref6570_expansion( + cls, + variable_name: str, + in_data: typing.Any, + explode: bool, + percent_encode: bool, + prefix_separator_iterator: PrefixSeparatorIterator + ) -> str: + """ + Separator is for separate variables like dict with explode true, not for array item separation + """ + named_parameter_expansion = prefix_separator_iterator.separator in {'&', ';'} + var_name_piece = variable_name if named_parameter_expansion else '' + if type(in_data) in {str, float, int}: + return cls.__ref6570_str_float_int_expansion( + variable_name, + in_data, + explode, + percent_encode, + prefix_separator_iterator, + var_name_piece, + named_parameter_expansion + ) + elif in_data is None: + # ignored by the expansion process https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.1 + return "" + elif isinstance(in_data, list): + return cls.__ref6570_list_expansion( + variable_name, + in_data, + explode, + percent_encode, + prefix_separator_iterator, + var_name_piece, + named_parameter_expansion + ) + elif isinstance(in_data, dict): + return cls.__ref6570_dict_expansion( + variable_name, + in_data, + explode, + percent_encode, + prefix_separator_iterator, + var_name_piece, + named_parameter_expansion + ) + # bool, bytes, etc + raise exceptions.ApiValueError('Unable to generate a ref6570 representation of {}'.format(in_data)) + + +class StyleFormSerializer(ParameterSerializerBase): + @classmethod + def _serialize_form( + cls, + in_data: typing.Union[None, int, float, str, bool, dict, list], + name: str, + explode: bool, + percent_encode: bool, + prefix_separator_iterator: typing.Optional[PrefixSeparatorIterator] = None + ) -> str: + if prefix_separator_iterator is None: + prefix_separator_iterator = PrefixSeparatorIterator('', '&') + return cls._ref6570_expansion( + variable_name=name, + in_data=in_data, + explode=explode, + percent_encode=percent_encode, + prefix_separator_iterator=prefix_separator_iterator + ) + + +class StyleSimpleSerializer(ParameterSerializerBase): + + @classmethod + def _serialize_simple( + cls, + in_data: typing.Union[None, int, float, str, bool, dict, list], + name: str, + explode: bool, + percent_encode: bool + ) -> str: + prefix_separator_iterator = PrefixSeparatorIterator('', ',') + return cls._ref6570_expansion( + variable_name=name, + in_data=in_data, + explode=explode, + percent_encode=percent_encode, + prefix_separator_iterator=prefix_separator_iterator + ) + + @classmethod + def _deserialize_simple( + cls, + in_data: str, + name: str, + explode: bool, + percent_encode: bool + ) -> typing.Union[str, typing.List[str], typing.Dict[str, str]]: + raise NotImplementedError( + "Deserialization of style=simple has not yet been added. " + "If you need this how about you submit a PR adding it?" + ) + + +class JSONDetector: + """ + Works for: + application/json + application/json; charset=UTF-8 + application/json-patch+json + application/geo+json + """ + __json_content_type_pattern = re.compile("application/[^+]*[+]?(json);?.*") + + @classmethod + def _content_type_is_json(cls, content_type: str) -> bool: + if cls.__json_content_type_pattern.match(content_type): + return True + return False + + +class Encoding: + content_type: str + headers: typing.Optional[typing.Dict[str, 'HeaderParameter']] = None + style: typing.Optional[ParameterStyle] = None + explode: bool = False + allow_reserved: bool = False + + +class MediaType: + """ + Used to store request and response body schema information + encoding: + A map between a property name and its encoding information. + The key, being the property name, MUST exist in the schema as a property. + The encoding object SHALL only apply to requestBody objects when the media type is + multipart or application/x-www-form-urlencoded. + """ + schema: typing.Optional[typing.Type[schemas.Schema]] = None + encoding: typing.Optional[typing.Dict[str, Encoding]] = None + + +class ParameterBase(JSONDetector): + in_type: ParameterInType + required: bool + style: typing.Optional[ParameterStyle] + explode: typing.Optional[bool] + allow_reserved: typing.Optional[bool] + schema: typing.Optional[typing.Type[schemas.Schema]] + content: typing.Optional[typing.Dict[str, typing.Type[MediaType]]] + + _json_encoder = JSONEncoder() + + def __init_subclass__(cls, **kwargs): + if cls.explode is None: + if cls.style is ParameterStyle.FORM: + cls.explode = True + else: + cls.explode = False + + @classmethod + def _serialize_json( + cls, + in_data: typing.Union[None, int, float, str, bool, dict, list], + eliminate_whitespace: bool = False + ) -> str: + if eliminate_whitespace: + return json.dumps(in_data, separators=cls._json_encoder.compact_separators) + return json.dumps(in_data) + +_SERIALIZE_TYPES = typing.Union[ + int, + float, + str, + datetime.date, + datetime.datetime, + None, + bool, + list, + tuple, + dict, + schemas.immutabledict +] + +_JSON_TYPES = typing.Union[ + int, + float, + str, + None, + bool, + typing.Tuple['_JSON_TYPES', ...], + schemas.immutabledict[str, '_JSON_TYPES'], +] + +@dataclasses.dataclass +class PathParameter(ParameterBase, StyleSimpleSerializer): + name: str + required: bool = False + in_type: ParameterInType = ParameterInType.PATH + style: ParameterStyle = ParameterStyle.SIMPLE + explode: bool = False + allow_reserved: typing.Optional[bool] = None + schema: typing.Optional[typing.Type[schemas.Schema]] = None + content: typing.Optional[typing.Dict[str, typing.Type[MediaType]]] = None + + @classmethod + def __serialize_label( + cls, + in_data: typing.Union[None, int, float, str, bool, dict, list] + ) -> typing.Dict[str, str]: + prefix_separator_iterator = PrefixSeparatorIterator('.', '.') + value = cls._ref6570_expansion( + variable_name=cls.name, + in_data=in_data, + explode=cls.explode, + percent_encode=True, + prefix_separator_iterator=prefix_separator_iterator + ) + return cls._to_dict(cls.name, value) + + @classmethod + def __serialize_matrix( + cls, + in_data: typing.Union[None, int, float, str, bool, dict, list] + ) -> typing.Dict[str, str]: + prefix_separator_iterator = PrefixSeparatorIterator(';', ';') + value = cls._ref6570_expansion( + variable_name=cls.name, + in_data=in_data, + explode=cls.explode, + percent_encode=True, + prefix_separator_iterator=prefix_separator_iterator + ) + return cls._to_dict(cls.name, value) + + @classmethod + def __serialize_simple( + cls, + in_data: typing.Union[None, int, float, str, bool, dict, list], + ) -> typing.Dict[str, str]: + value = cls._serialize_simple( + in_data=in_data, + name=cls.name, + explode=cls.explode, + percent_encode=True + ) + return cls._to_dict(cls.name, value) + + @classmethod + def serialize( + cls, + in_data: _SERIALIZE_TYPES, + skip_validation: bool = False + ) -> typing.Dict[str, str]: + if cls.schema: + cast_in_data = in_data if skip_validation else cls.schema.validate_base(in_data) + cast_in_data = cls._json_encoder.default(cast_in_data) + """ + simple -> path + path: + returns path_params: dict + label -> path + returns path_params + matrix -> path + returns path_params + """ + if cls.style: + if cls.style is ParameterStyle.SIMPLE: + return cls.__serialize_simple(cast_in_data) + elif cls.style is ParameterStyle.LABEL: + return cls.__serialize_label(cast_in_data) + elif cls.style is ParameterStyle.MATRIX: + return cls.__serialize_matrix(cast_in_data) + assert cls.content is not None + for content_type, media_type in cls.content.items(): + assert media_type.schema is not None + cast_in_data = in_data if skip_validation else media_type.schema.validate_base(in_data) + cast_in_data = cls._json_encoder.default(cast_in_data) + if cls._content_type_is_json(content_type): + value = cls._serialize_json(cast_in_data) + return cls._to_dict(cls.name, value) + else: + raise NotImplementedError('Serialization of {} has not yet been implemented'.format(content_type)) + raise ValueError('Invalid value for content, it was empty and must have 1 key value pair') + + +@dataclasses.dataclass +class QueryParameter(ParameterBase, StyleFormSerializer): + name: str + required: bool = False + in_type: ParameterInType = ParameterInType.QUERY + style: ParameterStyle = ParameterStyle.FORM + explode: typing.Optional[bool] = None + allow_reserved: typing.Optional[bool] = None + schema: typing.Optional[typing.Type[schemas.Schema]] = None + content: typing.Optional[typing.Dict[str, typing.Type[MediaType]]] = None + + @classmethod + def __serialize_space_delimited( + cls, + in_data: typing.Union[None, int, float, str, bool, dict, list], + prefix_separator_iterator: typing.Optional[PrefixSeparatorIterator], + explode: bool + ) -> typing.Dict[str, str]: + if prefix_separator_iterator is None: + prefix_separator_iterator = cls.get_prefix_separator_iterator() + value = cls._ref6570_expansion( + variable_name=cls.name, + in_data=in_data, + explode=explode, + percent_encode=True, + prefix_separator_iterator=prefix_separator_iterator + ) + return cls._to_dict(cls.name, value) + + @classmethod + def __serialize_pipe_delimited( + cls, + in_data: typing.Union[None, int, float, str, bool, dict, list], + prefix_separator_iterator: typing.Optional[PrefixSeparatorIterator], + explode: bool + ) -> typing.Dict[str, str]: + if prefix_separator_iterator is None: + prefix_separator_iterator = cls.get_prefix_separator_iterator() + value = cls._ref6570_expansion( + variable_name=cls.name, + in_data=in_data, + explode=explode, + percent_encode=True, + prefix_separator_iterator=prefix_separator_iterator + ) + return cls._to_dict(cls.name, value) + + @classmethod + def __serialize_form( + cls, + in_data: typing.Union[None, int, float, str, bool, dict, list], + prefix_separator_iterator: typing.Optional[PrefixSeparatorIterator], + explode: bool + ) -> typing.Dict[str, str]: + if prefix_separator_iterator is None: + prefix_separator_iterator = cls.get_prefix_separator_iterator() + value = cls._serialize_form( + in_data, + name=cls.name, + explode=explode, + percent_encode=True, + prefix_separator_iterator=prefix_separator_iterator + ) + return cls._to_dict(cls.name, value) + + @classmethod + def get_prefix_separator_iterator(cls) -> PrefixSeparatorIterator: + if cls.style is ParameterStyle.FORM: + return PrefixSeparatorIterator('?', '&') + elif cls.style is ParameterStyle.SPACE_DELIMITED: + return PrefixSeparatorIterator('', '%20') + elif cls.style is ParameterStyle.PIPE_DELIMITED: + return PrefixSeparatorIterator('', '|') + raise ValueError(f'No iterator possible for style={cls.style}') + + @classmethod + def serialize( + cls, + in_data: _SERIALIZE_TYPES, + prefix_separator_iterator: typing.Optional[PrefixSeparatorIterator] = None, + skip_validation: bool = False + ) -> typing.Dict[str, str]: + if cls.schema: + cast_in_data = in_data if skip_validation else cls.schema.validate_base(in_data) + cast_in_data = cls._json_encoder.default(cast_in_data) + """ + form -> query + query: + - GET/HEAD/DELETE: could use fields + - PUT/POST: must use urlencode to send parameters + returns fields: tuple + spaceDelimited -> query + returns fields + pipeDelimited -> query + returns fields + deepObject -> query, https://github.com/OAI/OpenAPI-Specification/issues/1706 + returns fields + """ + if cls.style: + # TODO update query ones to omit setting values when [] {} or None is input + explode = cls.explode if cls.explode is not None else cls.style == ParameterStyle.FORM + if cls.style is ParameterStyle.FORM: + return cls.__serialize_form(cast_in_data, prefix_separator_iterator, explode) + elif cls.style is ParameterStyle.SPACE_DELIMITED: + return cls.__serialize_space_delimited(cast_in_data, prefix_separator_iterator, explode) + elif cls.style is ParameterStyle.PIPE_DELIMITED: + return cls.__serialize_pipe_delimited(cast_in_data, prefix_separator_iterator, explode) + if prefix_separator_iterator is None: + prefix_separator_iterator = cls.get_prefix_separator_iterator() + assert cls.content is not None + for content_type, media_type in cls.content.items(): + assert media_type.schema is not None + cast_in_data = in_data if skip_validation else media_type.schema.validate_base(in_data) + cast_in_data = cls._json_encoder.default(cast_in_data) + if cls._content_type_is_json(content_type): + value = cls._serialize_json(cast_in_data, eliminate_whitespace=True) + return cls._to_dict( + cls.name, + next(prefix_separator_iterator) + cls.name + '=' + parse.quote(value) + ) + else: + raise NotImplementedError('Serialization of {} has not yet been implemented'.format(content_type)) + raise ValueError('Invalid value for content, it was empty and must have 1 key value pair') + + +@dataclasses.dataclass +class CookieParameter(ParameterBase, StyleFormSerializer): + name: str + required: bool = False + style: ParameterStyle = ParameterStyle.FORM + in_type: ParameterInType = ParameterInType.COOKIE + explode: typing.Optional[bool] = None + allow_reserved: typing.Optional[bool] = None + schema: typing.Optional[typing.Type[schemas.Schema]] = None + content: typing.Optional[typing.Dict[str, typing.Type[MediaType]]] = None + + @classmethod + def serialize( + cls, + in_data: _SERIALIZE_TYPES, + skip_validation: bool = False + ) -> typing.Dict[str, str]: + if cls.schema: + cast_in_data = in_data if skip_validation else cls.schema.validate_base(in_data) + cast_in_data = cls._json_encoder.default(cast_in_data) + """ + form -> cookie + returns fields: tuple + """ + if cls.style: + """ + TODO add escaping of comma, space, equals + or turn encoding on + """ + explode = cls.explode if cls.explode is not None else cls.style == ParameterStyle.FORM + value = cls._serialize_form( + cast_in_data, + explode=explode, + name=cls.name, + percent_encode=False, + prefix_separator_iterator=PrefixSeparatorIterator('', '&') + ) + return cls._to_dict(cls.name, value) + assert cls.content is not None + for content_type, media_type in cls.content.items(): + assert media_type.schema is not None + cast_in_data = in_data if skip_validation else media_type.schema.validate_base(in_data) + cast_in_data = cls._json_encoder.default(cast_in_data) + if cls._content_type_is_json(content_type): + value = cls._serialize_json(cast_in_data) + return cls._to_dict(cls.name, value) + else: + raise NotImplementedError('Serialization of {} has not yet been implemented'.format(content_type)) + raise ValueError('Invalid value for content, it was empty and must have 1 key value pair') + + +class __HeaderParameterBase(ParameterBase, StyleSimpleSerializer): + style: ParameterStyle = ParameterStyle.SIMPLE + schema: typing.Optional[typing.Type[schemas.Schema]] = None + content: typing.Optional[typing.Dict[str, typing.Type[MediaType]]] = None + explode: bool = False + + @staticmethod + def __to_headers(in_data: typing.Tuple[typing.Tuple[str, str], ...]) -> _collections.HTTPHeaderDict: + data = tuple(t for t in in_data if t) + headers = _collections.HTTPHeaderDict() + if not data: + return headers + headers.extend(data) + return headers + + @classmethod + def serialize_with_name( + cls, + in_data: _SERIALIZE_TYPES, + name: str, + skip_validation: bool = False + ) -> _collections.HTTPHeaderDict: + if cls.schema: + cast_in_data = in_data if skip_validation else cls.schema.validate_base(in_data) + cast_in_data = cls._json_encoder.default(cast_in_data) + """ + simple -> header + headers: PoolManager needs a mapping, tuple is close + returns headers: dict + """ + if cls.style: + value = cls._serialize_simple(cast_in_data, name, cls.explode, False) + return cls.__to_headers(((name, value),)) + assert cls.content is not None + for content_type, media_type in cls.content.items(): + assert media_type.schema is not None + cast_in_data = in_data if skip_validation else media_type.schema.validate_base(in_data) + cast_in_data = cls._json_encoder.default(cast_in_data) + if cls._content_type_is_json(content_type): + value = cls._serialize_json(cast_in_data) + return cls.__to_headers(((name, value),)) + else: + raise NotImplementedError('Serialization of {} has not yet been implemented'.format(content_type)) + raise ValueError('Invalid value for content, it was empty and must have 1 key value pair') + + @classmethod + def deserialize( + cls, + in_data: str, + name: str + ): + if cls.schema: + """ + simple -> header + headers: PoolManager needs a mapping, tuple is close + returns headers: dict + """ + if cls.style: + extracted_data = cls._deserialize_simple(in_data, name, cls.explode, False) + return cls.schema.validate_base(extracted_data) + assert cls.content is not None + for content_type, media_type in cls.content.items(): + if cls._content_type_is_json(content_type): + cast_in_data: typing.Union[dict, list, None, int, float, str] = json.loads(in_data) + assert media_type.schema is not None + return media_type.schema.validate_base(cast_in_data) + else: + raise NotImplementedError('Deserialization of {} has not yet been implemented'.format(content_type)) + raise ValueError('Invalid value for content, it was empty and must have 1 key value pair') + + +class HeaderParameterWithoutName(__HeaderParameterBase): + required: bool = False + style: ParameterStyle = ParameterStyle.SIMPLE + in_type: ParameterInType = ParameterInType.HEADER + explode: bool = False + allow_reserved: typing.Optional[bool] = None + schema: typing.Optional[typing.Type[schemas.Schema]] = None + content: typing.Optional[typing.Dict[str, typing.Type[MediaType]]] = None + + @classmethod + def serialize( + cls, + in_data: _SERIALIZE_TYPES, + name: str, + skip_validation: bool = False + ) -> _collections.HTTPHeaderDict: + return cls.serialize_with_name( + in_data, + name, + skip_validation=skip_validation + ) + + +class HeaderParameter(__HeaderParameterBase): + name: str + required: bool = False + style: ParameterStyle = ParameterStyle.SIMPLE + in_type: ParameterInType = ParameterInType.HEADER + explode: bool = False + allow_reserved: typing.Optional[bool] = None + schema: typing.Optional[typing.Type[schemas.Schema]] = None + content: typing.Optional[typing.Dict[str, typing.Type[MediaType]]] = None + + @classmethod + def serialize( + cls, + in_data: _SERIALIZE_TYPES, + skip_validation: bool = False + ) -> _collections.HTTPHeaderDict: + return cls.serialize_with_name( + in_data, + cls.name, + skip_validation=skip_validation + ) + +T = typing.TypeVar("T", bound=api_response.ApiResponse) + + +class OpenApiResponse(typing.Generic[T], JSONDetector, abc.ABC): + __filename_content_disposition_pattern = re.compile('filename="(.+?)"') + content: typing.Optional[typing.Dict[str, typing.Type[MediaType]]] = None + headers: typing.Optional[typing.Dict[str, typing.Type[HeaderParameterWithoutName]]] = None + headers_schema: typing.Optional[typing.Type[schemas.Schema]] = None + + @classmethod + @abc.abstractmethod + def get_response(cls, response, headers, body) -> T: ... + + @staticmethod + def __deserialize_json(response: urllib3.HTTPResponse) -> typing.Any: + # python must be >= 3.9 so we can pass in bytes into json.loads + return json.loads(response.data) + + @staticmethod + def __file_name_from_response_url(response_url: typing.Optional[str]) -> typing.Optional[str]: + if response_url is None: + return None + url_path = parse.urlparse(response_url).path + if url_path: + path_basename = os.path.basename(url_path) + if path_basename: + _filename, ext = os.path.splitext(path_basename) + if ext: + return path_basename + return None + + @classmethod + def __file_name_from_content_disposition(cls, content_disposition: typing.Optional[str]) -> typing.Optional[str]: + if content_disposition is None: + return None + match = cls.__filename_content_disposition_pattern.search(content_disposition) + if not match: + return None + return match.group(1) + + @classmethod + def __deserialize_application_octet_stream( + cls, response: urllib3.HTTPResponse + ) -> typing.Union[bytes, io.BufferedReader]: + """ + urllib3 use cases: + 1. when preload_content=True (stream=False) then supports_chunked_reads is False and bytes are returned + 2. when preload_content=False (stream=True) then supports_chunked_reads is True and + a file will be written and returned + """ + if response.supports_chunked_reads(): + file_name = ( + cls.__file_name_from_content_disposition(response.headers.get('content-disposition')) + or cls.__file_name_from_response_url(response.geturl()) + ) + + if file_name is None: + _fd, path = tempfile.mkstemp() + else: + path = os.path.join(tempfile.gettempdir(), file_name) + + with open(path, 'wb') as write_file: + chunk_size = 1024 + while True: + data = response.read(chunk_size) + if not data: + break + write_file.write(data) + # release_conn is needed for streaming connections only + response.release_conn() + new_file = open(path, 'rb') + return new_file + else: + return response.data + + @staticmethod + def __deserialize_multipart_form_data( + response: urllib3.HTTPResponse + ) -> typing.Dict[str, typing.Any]: + msg = email.message_from_bytes(response.data) + return { + part.get_param("name", header="Content-Disposition"): part.get_payload( + decode=True + ).decode(part.get_content_charset()) + if part.get_content_charset() + else part.get_payload() + for part in msg.get_payload() + } + + @classmethod + def deserialize(cls, response: urllib3.HTTPResponse, configuration: schema_configuration_.SchemaConfiguration) -> T: + content_type = response.headers.get('content-type') + deserialized_body = schemas.unset + streamed = response.supports_chunked_reads() + + deserialized_headers: typing.Union[schemas.Unset, typing.Dict[str, typing.Any]] = schemas.unset + if cls.headers is not None and cls.headers_schema is not None: + deserialized_headers = {} + for header_name, header_param in cls.headers.items(): + header_value = response.headers.get(header_name) + if header_value is None: + continue + header_value = header_param.deserialize(header_value, header_name) + deserialized_headers[header_name] = header_value + deserialized_headers = cls.headers_schema.validate_base(deserialized_headers, configuration=configuration) + + if cls.content is not None: + if content_type not in cls.content: + raise exceptions.ApiValueError( + f"Invalid content_type returned. Content_type='{content_type}' was returned " + f"when only {str(set(cls.content))} are defined for status_code={str(response.status)}" + ) + body_schema = cls.content[content_type].schema + if body_schema is None: + # some specs do not define response content media type schemas + return cls.get_response( + response=response, + headers=deserialized_headers, + body=schemas.unset + ) + + if cls._content_type_is_json(content_type): + body_data = cls.__deserialize_json(response) + elif content_type == 'application/octet-stream': + body_data = cls.__deserialize_application_octet_stream(response) + elif content_type.startswith('multipart/form-data'): + body_data = cls.__deserialize_multipart_form_data(response) + content_type = 'multipart/form-data' + else: + raise NotImplementedError('Deserialization of {} has not yet been implemented'.format(content_type)) + body_schema = schemas.get_class(body_schema) + if body_schema is schemas.BinarySchema: + deserialized_body = body_schema.validate_base(body_data) + else: + deserialized_body = body_schema.validate_base( + body_data, configuration=configuration) + elif streamed: + response.release_conn() + + return cls.get_response( + response=response, + headers=deserialized_headers, + body=deserialized_body + ) + + +@dataclasses.dataclass +class ApiClient: + """Generic API client for OpenAPI client library builds. + + OpenAPI generic API client. This client handles the client- + server communication, and is invariant across implementations. Specifics of + the methods and models for each application are generated from the OpenAPI + templates. + + NOTE: This class is auto generated by OpenAPI JSON Schema Generator. + Ref: https://github.com/openapi-json-schema-tools/openapi-json-schema-generator + Do not edit the class manually. + + :param configuration: api_configuration.ApiConfiguration object for this client + :param schema_configuration: schema_configuration_.SchemaConfiguration object for this client + :param default_headers: any default headers to include when making calls to the API. + :param pool_threads: The number of threads to use for async requests + to the API. More threads means more concurrent API requests. + """ + configuration: api_configuration.ApiConfiguration = dataclasses.field( + default_factory=lambda: api_configuration.ApiConfiguration()) + schema_configuration: schema_configuration_.SchemaConfiguration = dataclasses.field( + default_factory=lambda: schema_configuration_.SchemaConfiguration()) + default_headers: _collections.HTTPHeaderDict = dataclasses.field( + default_factory=lambda: _collections.HTTPHeaderDict()) + pool_threads: int = 1 + user_agent: str = 'OpenAPI-JSON-Schema-Generator/1.0.0/python' + rest_client: rest.RESTClientObject = dataclasses.field(init=False) + + def __post_init__(self): + self._pool = None + self.rest_client = rest.RESTClientObject(self.configuration) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + def close(self): + if self._pool: + self._pool.close() + self._pool.join() + self._pool = None + if hasattr(atexit, 'unregister'): + atexit.unregister(self.close) + + @property + def pool(self): + """Create thread pool on first request + avoids instantiating unused threadpool for blocking clients. + """ + if self._pool is None: + atexit.register(self.close) + self._pool = pool.ThreadPool(self.pool_threads) + return self._pool + + def set_default_header(self, header_name: str, header_value: str): + self.default_headers[header_name] = header_value + + def call_api( + self, + resource_path: str, + method: str, + host: str, + query_params_suffix: typing.Optional[str] = None, + headers: typing.Optional[_collections.HTTPHeaderDict] = None, + body: typing.Union[str, bytes, None] = None, + fields: typing.Optional[typing.Tuple[rest.RequestField, ...]] = None, + security_requirement_object: typing.Optional[security_schemes.SecurityRequirementObject] = None, + stream: bool = False, + timeout: typing.Union[int, float, typing.Tuple, None] = None, + ) -> urllib3.HTTPResponse: + """Makes the HTTP request (synchronous) and returns deserialized data. + + :param resource_path: Path to method endpoint. + :param method: Method to call. + :param headers: Header parameters to be + placed in the request header. + :param body: Request body. + :param fields: Request post form parameters, + for `application/x-www-form-urlencoded`, `multipart/form-data` + :param security_requirement_object: The security requirement object, used to apply auth when making the call + :param async_req: execute request asynchronously + :param stream: if True, the urllib3.HTTPResponse object will + be returned without reading/decoding response + data. Also when True, if the openapi spec describes a file download, + the data will be written to a local filesystem file and the schemas.BinarySchema + instance will also inherit from FileSchema and schemas.FileIO + Default is False. + :type stream: bool, optional + :param timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :param host: api endpoint host + :return: + the method will return the response directly. + """ + # header parameters + used_headers = _collections.HTTPHeaderDict(self.default_headers) + user_agent_key = 'User-Agent' + if user_agent_key not in used_headers and self.user_agent: + used_headers[user_agent_key] = self.user_agent + + # auth setting + self.update_params_for_auth( + used_headers, + security_requirement_object, + resource_path, + method, + body, + query_params_suffix + ) + + # must happen after auth setting in case user is overriding those + if headers: + used_headers.update(headers) + + # request url + url = host + resource_path + if query_params_suffix: + url += query_params_suffix + + # perform request and return response + response = self.request( + method, + url, + headers=used_headers, + fields=fields, + body=body, + stream=stream, + timeout=timeout, + ) + return response + + def request( + self, + method: str, + url: str, + headers: typing.Optional[_collections.HTTPHeaderDict] = None, + fields: typing.Optional[typing.Tuple[rest.RequestField, ...]] = None, + body: typing.Union[str, bytes, None] = None, + stream: bool = False, + timeout: typing.Union[int, float, typing.Tuple, None] = None, + ) -> urllib3.HTTPResponse: + """Makes the HTTP request using RESTClient.""" + if method == "get": + return self.rest_client.get(url, + stream=stream, + timeout=timeout, + headers=headers) + elif method == "head": + return self.rest_client.head(url, + stream=stream, + timeout=timeout, + headers=headers) + elif method == "options": + return self.rest_client.options(url, + headers=headers, + fields=fields, + stream=stream, + timeout=timeout, + body=body) + elif method == "post": + return self.rest_client.post(url, + headers=headers, + fields=fields, + stream=stream, + timeout=timeout, + body=body) + elif method == "put": + return self.rest_client.put(url, + headers=headers, + fields=fields, + stream=stream, + timeout=timeout, + body=body) + elif method == "patch": + return self.rest_client.patch(url, + headers=headers, + fields=fields, + stream=stream, + timeout=timeout, + body=body) + elif method == "delete": + return self.rest_client.delete(url, + headers=headers, + stream=stream, + timeout=timeout, + body=body) + else: + raise exceptions.ApiValueError( + "http method must be `GET`, `HEAD`, `OPTIONS`," + " `POST`, `PATCH`, `PUT` or `DELETE`." + ) + + def update_params_for_auth( + self, + headers: _collections.HTTPHeaderDict, + security_requirement_object: typing.Optional[security_schemes.SecurityRequirementObject], + resource_path: str, + method: str, + body: typing.Union[str, bytes, None] = None, + query_params_suffix: typing.Optional[str] = None + ): + """Updates header and query params based on authentication setting. + + :param headers: Header parameters dict to be updated. + :param security_requirement_object: the openapi security requirement object + :param resource_path: A string representation of the HTTP request resource path. + :param method: A string representation of the HTTP request method. + :param body: A object representing the body of the HTTP request. + The object type is the return value of _encoder.default(). + """ + return + +@dataclasses.dataclass +class Api: + """NOTE: This class is auto generated by OpenAPI JSON Schema Generator + Ref: https://github.com/openapi-json-schema-tools/openapi-json-schema-generator + + Do not edit the class manually. + """ + api_client: ApiClient = dataclasses.field(default_factory=lambda: ApiClient()) + + @staticmethod + def _get_used_path( + used_path: str, + path_parameters: typing.Tuple[typing.Type[PathParameter], ...] = (), + path_params: typing.Optional[typing.Mapping[str, schemas.OUTPUT_BASE_TYPES]] = None, + query_parameters: typing.Tuple[typing.Type[QueryParameter], ...] = (), + query_params: typing.Optional[typing.Mapping[str, schemas.OUTPUT_BASE_TYPES]] = None, + skip_validation: bool = False + ) -> typing.Tuple[str, str]: + used_path_params = {} + if path_params is not None: + for path_parameter in path_parameters: + parameter_data = path_params.get(path_parameter.name, schemas.unset) + if isinstance(parameter_data, schemas.Unset): + continue + assert not isinstance(parameter_data, (bytes, schemas.FileIO)) + serialized_data = path_parameter.serialize(parameter_data, skip_validation=skip_validation) + used_path_params.update(serialized_data) + + for k, v in used_path_params.items(): + used_path = used_path.replace('{%s}' % k, v) + + query_params_suffix = "" + if query_params is not None: + prefix_separator_iterator = None + for query_parameter in query_parameters: + parameter_data = query_params.get(query_parameter.name, schemas.unset) + if isinstance(parameter_data, schemas.Unset): + continue + if prefix_separator_iterator is None: + prefix_separator_iterator = query_parameter.get_prefix_separator_iterator() + assert not isinstance(parameter_data, (bytes, schemas.FileIO)) + serialized_data = query_parameter.serialize( + parameter_data, + prefix_separator_iterator=prefix_separator_iterator, + skip_validation=skip_validation + ) + for serialized_value in serialized_data.values(): + query_params_suffix += serialized_value + return used_path, query_params_suffix + + @staticmethod + def _get_headers( + header_parameters: typing.Tuple[typing.Type[HeaderParameter], ...] = (), + header_params: typing.Optional[typing.Mapping[str, schemas.OUTPUT_BASE_TYPES]] = None, + accept_content_types: typing.Tuple[str, ...] = (), + skip_validation: bool = False + ) -> _collections.HTTPHeaderDict: + headers = _collections.HTTPHeaderDict() + if header_params is not None: + for parameter in header_parameters: + parameter_data = header_params.get(parameter.name, schemas.unset) + if isinstance(parameter_data, schemas.Unset): + continue + assert not isinstance(parameter_data, (bytes, schemas.FileIO)) + serialized_data = parameter.serialize(parameter_data, skip_validation=skip_validation) + headers.extend(serialized_data) + if accept_content_types: + for accept_content_type in accept_content_types: + headers.add('Accept', accept_content_type) + return headers + + @staticmethod + def _get_fields_and_body( + request_body: typing.Type[RequestBody], + body: typing.Union[schemas.INPUT_TYPES_ALL, schemas.Unset], + content_type: str, + headers: _collections.HTTPHeaderDict + ): + if request_body.required and body is schemas.unset: + raise exceptions.ApiValueError( + 'The required body parameter has an invalid value of: unset. Set a valid value instead') + + if isinstance(body, schemas.Unset): + return None, None + + serialized_fields = None + serialized_body = None + serialized_data = request_body.serialize(body, content_type) + headers.add('Content-Type', content_type) + if 'fields' in serialized_data: + serialized_fields = serialized_data['fields'] + elif 'body' in serialized_data: + serialized_body = serialized_data['body'] + return serialized_fields, serialized_body + + @staticmethod + def _verify_response_status(response: api_response.ApiResponse): + if not 200 <= response.response.status <= 399: + raise exceptions.ApiException( + status=response.response.status, + reason=response.response.reason, + api_response=response + ) + + +class SerializedRequestBody(typing.TypedDict, total=False): + body: typing.Union[str, bytes] + fields: typing.Tuple[rest.RequestField, ...] + + +class RequestBody(StyleFormSerializer, JSONDetector): + """ + A request body parameter + content: content_type to MediaType schemas.Schema info + """ + __json_encoder = JSONEncoder() + content: typing.Dict[str, typing.Type[MediaType]] + required: bool = False + + @classmethod + def __serialize_json( + cls, + in_data: _JSON_TYPES + ) -> SerializedRequestBody: + in_data = cls.__json_encoder.default(in_data) + json_str = json.dumps(in_data, separators=(",", ":"), ensure_ascii=False).encode( + "utf-8" + ) + return {'body': json_str} + + @staticmethod + def __serialize_text_plain(in_data: typing.Union[int, float, str]) -> SerializedRequestBody: + return {'body': str(in_data)} + + @classmethod + def __multipart_json_item(cls, key: str, value: _JSON_TYPES) -> rest.RequestField: + json_value = cls.__json_encoder.default(value) + request_field = rest.RequestField(name=key, data=json.dumps(json_value)) + request_field.make_multipart(content_type='application/json') + return request_field + + @classmethod + def __multipart_form_item(cls, key: str, value: typing.Union[_JSON_TYPES, bytes, schemas.FileIO]) -> rest.RequestField: + if isinstance(value, str): + request_field = rest.RequestField(name=key, data=str(value)) + request_field.make_multipart(content_type='text/plain') + elif isinstance(value, bytes): + request_field = rest.RequestField(name=key, data=value) + request_field.make_multipart(content_type='application/octet-stream') + elif isinstance(value, schemas.FileIO): + # TODO use content.encoding to limit allowed content types if they are present + urllib3_request_field = rest.RequestField.from_tuples(key, (os.path.basename(str(value.name)), value.read())) + request_field = typing.cast(rest.RequestField, urllib3_request_field) + value.close() + else: + request_field = cls.__multipart_json_item(key=key, value=value) + return request_field + + @classmethod + def __serialize_multipart_form_data( + cls, in_data: schemas.immutabledict[str, typing.Union[_JSON_TYPES, bytes, schemas.FileIO]] + ) -> SerializedRequestBody: + """ + In a multipart/form-data request body, each schema property, or each element of a schema array property, + takes a section in the payload with an internal header as defined by RFC7578. The serialization strategy + for each property of a multipart/form-data request body can be specified in an associated Encoding Object. + + When passing in multipart types, boundaries MAY be used to separate sections of the content being + transferred – thus, the following default Content-Types are defined for multipart: + + If the (object) property is a primitive, or an array of primitive values, the default Content-Type is text/plain + If the property is complex, or an array of complex values, the default Content-Type is application/json + Question: how is the array of primitives encoded? + If the property is a type: string with a contentEncoding, the default Content-Type is application/octet-stream + """ + fields = [] + for key, value in in_data.items(): + if isinstance(value, tuple): + if value: + # values use explode = True, so the code makes a rest.RequestField for each item with name=key + for item in value: + request_field = cls.__multipart_form_item(key=key, value=item) + fields.append(request_field) + else: + # send an empty array as json because exploding will not send it + request_field = cls.__multipart_json_item(key=key, value=value) # type: ignore + fields.append(request_field) + else: + request_field = cls.__multipart_form_item(key=key, value=value) + fields.append(request_field) + + return {'fields': tuple(fields)} + + @staticmethod + def __serialize_application_octet_stream(in_data: typing.Union[schemas.FileIO, bytes]) -> SerializedRequestBody: + if isinstance(in_data, bytes): + return {'body': in_data} + # schemas.FileIO type + used_in_data = in_data.read() + in_data.close() + return {'body': used_in_data} + + @classmethod + def __serialize_application_x_www_form_data( + cls, in_data: schemas.immutabledict[str, _JSON_TYPES] + ) -> SerializedRequestBody: + """ + POST submission of form data in body + """ + cast_in_data = cls.__json_encoder.default(in_data) + value = cls._serialize_form(cast_in_data, name='', explode=True, percent_encode=True) + return {'body': value} + + @classmethod + def serialize( + cls, in_data: schemas.INPUT_TYPES_ALL, content_type: str + ) -> SerializedRequestBody: + """ + If a str is returned then the result will be assigned to data when making the request + If a tuple is returned then the result will be used as fields input in encode_multipart_formdata + Return a tuple of + + The key of the return dict is + - body for application/json + - encode_multipart and fields for multipart/form-data + """ + media_type = cls.content[content_type] + assert media_type.schema is not None + schema = schemas.get_class(media_type.schema) + cast_in_data = schema.validate_base(in_data) + # TODO check for and use encoding if it exists + # and content_type is multipart or application/x-www-form-urlencoded + if cls._content_type_is_json(content_type): + if isinstance(cast_in_data, (schemas.FileIO, bytes)): + raise ValueError(f"Invalid input data type. Data must be int/float/str/bool/None/tuple/immutabledict and it was type {type(cast_in_data)}") + return cls.__serialize_json(cast_in_data) + elif content_type == 'text/plain': + if not isinstance(cast_in_data, (int, float, str)): + raise ValueError(f"Unable to serialize type {type(cast_in_data)} to text/plain") + return cls.__serialize_text_plain(cast_in_data) + elif content_type == 'multipart/form-data': + if not isinstance(cast_in_data, schemas.immutabledict): + raise ValueError(f"Unable to serialize {cast_in_data} to multipart/form-data because it is not a dict of data") + return cls.__serialize_multipart_form_data(cast_in_data) + elif content_type == 'application/x-www-form-urlencoded': + if not isinstance(cast_in_data, schemas.immutabledict): + raise ValueError( + f"Unable to serialize {cast_in_data} to application/x-www-form-urlencoded because it is not a dict of data") + return cls.__serialize_application_x_www_form_data(cast_in_data) + elif content_type == 'application/octet-stream': + if not isinstance(cast_in_data, (schemas.FileIO, bytes)): + raise ValueError(f"Invalid input data type. Data must be bytes or File for content_type={content_type}") + return cls.__serialize_application_octet_stream(cast_in_data) + raise NotImplementedError('Serialization has not yet been implemented for {}'.format(content_type)) diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/api_response.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/api_response.py new file mode 100644 index 00000000000..ec9907ededb --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/api_response.py @@ -0,0 +1,28 @@ +# coding: utf-8 +""" + Example + No description provided (generated by Openapi JSON Schema Generator https://github.com/openapi-json-schema-tools/openapi-json-schema-generator) # noqa: E501 + The version of the OpenAPI document: 1.0.0 + Generated by: https://github.com/openapi-json-schema-tools/openapi-json-schema-generator +""" + +import dataclasses +import typing + +import urllib3 + +from json_schema_api import schemas + + +@dataclasses.dataclass +class ApiResponse: + response: urllib3.HTTPResponse + body: typing.Union[schemas.Unset, schemas.OUTPUT_BASE_TYPES] = schemas.unset + headers: typing.Union[schemas.Unset, typing.Mapping[str, schemas.OUTPUT_BASE_TYPES]] = schemas.unset + + +@dataclasses.dataclass +class ApiResponseWithoutDeserialization(ApiResponse): + response: urllib3.HTTPResponse + body: schemas.Unset = schemas.unset + headers: schemas.Unset = schemas.unset diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/apis/__init__.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/apis/__init__.py new file mode 100644 index 00000000000..7840f7726f6 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/apis/__init__.py @@ -0,0 +1,3 @@ +# do not import all endpoints into this module because that uses a lot of memory and stack frames +# if you need the ability to import all endpoints then import them from +# tags, paths, or path_to_api, or tag_to_api \ No newline at end of file diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/apis/path_to_api.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/apis/path_to_api.py new file mode 100644 index 00000000000..8c080c5304d --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/apis/path_to_api.py @@ -0,0 +1,17 @@ +import typing +import typing_extensions + +from json_schema_api.apis.paths.some_path import SomePath + +PathToApi = typing.TypedDict( + 'PathToApi', + { + "/somePath": typing.Type[SomePath], + } +) + +path_to_api = PathToApi( + { + "/somePath": SomePath, + } +) diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/apis/paths/__init__.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/apis/paths/__init__.py new file mode 100644 index 00000000000..298f4273cd3 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/apis/paths/__init__.py @@ -0,0 +1,3 @@ +# do not import all endpoints into this module because that uses a lot of memory and stack frames +# if you need the ability to import all endpoints from this module, import them with +# from json_schema_api.apis.path_to_api import path_to_api \ No newline at end of file diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/apis/paths/some_path.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/apis/paths/some_path.py new file mode 100644 index 00000000000..25597763231 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/apis/paths/some_path.py @@ -0,0 +1,13 @@ +# coding: utf-8 + +""" + Generated by: https://github.com/openapi-json-schema-tools/openapi-json-schema-generator +""" + +from json_schema_api.paths.some_path.get.operation import ApiForGet + + +class SomePath( + ApiForGet, +): + pass diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/apis/tag_to_api.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/apis/tag_to_api.py new file mode 100644 index 00000000000..d97b93a57b2 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/apis/tag_to_api.py @@ -0,0 +1,17 @@ +import typing +import typing_extensions + +from json_schema_api.apis.tags.default_api import DefaultApi + +TagToApi = typing.TypedDict( + 'TagToApi', + { + "default": typing.Type[DefaultApi], + } +) + +tag_to_api = TagToApi( + { + "default": DefaultApi, + } +) diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/apis/tags/__init__.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/apis/tags/__init__.py new file mode 100644 index 00000000000..f4326db6655 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/apis/tags/__init__.py @@ -0,0 +1,3 @@ +# do not import all endpoints into this module because that uses a lot of memory and stack frames +# if you need the ability to import all endpoints from this module, import them with +# from json_schema_api.apis.tag_to_api import tag_to_api \ No newline at end of file diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/apis/tags/default_api.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/apis/tags/default_api.py new file mode 100644 index 00000000000..a50d9e3a2c2 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/apis/tags/default_api.py @@ -0,0 +1,18 @@ +# coding: utf-8 + +""" + Generated by: https://github.com/openapi-json-schema-tools/openapi-json-schema-generator +""" + +from json_schema_api.paths.some_path.get.operation import GetSomePath + + +class DefaultApi( + GetSomePath, +): + """NOTE: This class is auto generated by OpenAPI JSON Schema Generator + Ref: https://github.com/openapi-json-schema-tools/openapi-json-schema-generator + + Do not edit the class manually. + """ + pass diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/__init__.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/__init__.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/__init__.py new file mode 100644 index 00000000000..217bb49a46f --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/__init__.py @@ -0,0 +1,5 @@ +# we can not import model classes here because that would create a circular +# reference which would not work in python2 +# do not import all models into this module because that uses a lot of memory and stack frames +# if you need the ability to import all models from one package, import them with +# from json_schema_api.components.schemas import ModelA, ModelB diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/any_type_contains_value.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/any_type_contains_value.py new file mode 100644 index 00000000000..742cde5c332 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/any_type_contains_value.py @@ -0,0 +1,13 @@ +# coding: utf-8 + +""" + Example + No description provided (generated by Openapi JSON Schema Generator https://github.com/openapi-json-schema-tools/openapi-json-schema-generator) # noqa: E501 + The version of the OpenAPI document: 1.0.0 + Generated by: https://github.com/openapi-json-schema-tools/openapi-json-schema-generator +""" + +from __future__ import annotations +from json_schema_api.shared_imports.schema_imports import * # pyright: ignore [reportWildcardImportFromLibrary] + +AnyTypeContainsValue: typing_extensions.TypeAlias = schemas.AnyTypeSchema diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/array_contains_value.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/array_contains_value.py new file mode 100644 index 00000000000..fd6c9516052 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/array_contains_value.py @@ -0,0 +1,25 @@ +# coding: utf-8 + +""" + Example + No description provided (generated by Openapi JSON Schema Generator https://github.com/openapi-json-schema-tools/openapi-json-schema-generator) # noqa: E501 + The version of the OpenAPI document: 1.0.0 + Generated by: https://github.com/openapi-json-schema-tools/openapi-json-schema-generator +""" + +from __future__ import annotations +from json_schema_api.shared_imports.schema_imports import * # pyright: ignore [reportWildcardImportFromLibrary] + + + +@dataclasses.dataclass(frozen=True) +class ArrayContainsValue( + schemas.Schema[schemas.immutabledict[str, schemas.OUTPUT_BASE_TYPES], typing.Tuple[schemas.OUTPUT_BASE_TYPES, ...]], +): + """NOTE: This class is auto generated by OpenAPI JSON Schema Generator. + Ref: https://github.com/openapi-json-schema-tools/openapi-json-schema-generator + + Do not edit the class manually. + """ + + diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schemas/__init__.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schemas/__init__.py new file mode 100644 index 00000000000..f04ff1683c2 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schemas/__init__.py @@ -0,0 +1,15 @@ +# coding: utf-8 + +# flake8: noqa + +# import all models into this package +# if you have many models here with many references from one model to another this may +# raise a RecursionError +# to avoid this, import only the models that you directly need like: +# from from json_schema_api.components.schema.pet import Pet +# or import this package, but before doing it, use: +# import sys +# sys.setrecursionlimit(n) + +from json_schema_api.components.schema.any_type_contains_value import AnyTypeContainsValue +from json_schema_api.components.schema.array_contains_value import ArrayContainsValue diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/configurations/__init__.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/configurations/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/configurations/api_configuration.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/configurations/api_configuration.py new file mode 100644 index 00000000000..66121393ba9 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/configurations/api_configuration.py @@ -0,0 +1,281 @@ +# coding: utf-8 + +""" + Example + No description provided (generated by Openapi JSON Schema Generator https://github.com/openapi-json-schema-tools/openapi-json-schema-generator) # noqa: E501 + The version of the OpenAPI document: 1.0.0 + Generated by: https://github.com/openapi-json-schema-tools/openapi-json-schema-generator +""" + +import copy +from http import client as http_client +import logging +import multiprocessing +import sys +import typing +import typing_extensions + +import urllib3 + +from json_schema_api import exceptions +from json_schema_api.servers import server_0 + +# the server to use at each openapi document json path +ServerInfo = typing.TypedDict( + 'ServerInfo', + { + 'servers/0': server_0.Server0, + }, + total=False +) + + +class ServerIndexInfoRequired(typing.TypedDict): + servers: typing.Literal[0] + +ServerIndexInfoOptional = typing.TypedDict( + 'ServerIndexInfoOptional', + { + }, + total=False +) + + +class ServerIndexInfo(ServerIndexInfoRequired, ServerIndexInfoOptional): + """ + the default server_index to use at each openapi document json path + the fallback value is stored in the 'servers' key + """ + + +class ApiConfiguration(object): + """NOTE: This class is auto generated by OpenAPI JSON Schema Generator + + Ref: https://github.com/openapi-json-schema-tools/openapi-json-schema-generator + Do not edit the class manually. + + :param server_info: the servers that can be used to make endpoint calls + :param server_index_info: index to servers configuration + """ + + def __init__( + self, + server_info: typing.Optional[ServerInfo] = None, + server_index_info: typing.Optional[ServerIndexInfo] = None, + ): + """Constructor + """ + # Authentication Settings + self.security_scheme_info = {} + self.security_index_info = {'security': 0} + # Server Info + self.server_info: ServerInfo = server_info or { + 'servers/0': server_0.Server0(), + } + self.server_index_info: ServerIndexInfo = server_index_info or {'servers': 0} + self.logger = {} + """Logging Settings + """ + self.logger["package_logger"] = logging.getLogger("json_schema_api") + self.logger["urllib3_logger"] = logging.getLogger("urllib3") + self.logger_format = '%(asctime)s %(levelname)s %(message)s' + """Log format + """ + self.logger_stream_handler = None + """Log stream handler + """ + self.logger_file_handler = None + """Log file handler + """ + self.logger_file = None + """Debug file location + """ + self.debug = False + """Debug switch + """ + + self.verify_ssl = True + """SSL/TLS verification + Set this to false to skip verifying SSL certificate when calling API + from https server. + """ + self.ssl_ca_cert = None + """Set this to customize the certificate file to verify the peer. + """ + self.cert_file = None + """client certificate file + """ + self.key_file = None + """client key file + """ + self.assert_hostname = None + """Set this to True/False to enable/disable SSL hostname verification. + """ + + self.connection_pool_maxsize = multiprocessing.cpu_count() * 5 + """urllib3 connection pool's maximum number of connections saved + per pool. urllib3 uses 1 connection as default value, but this is + not the best value when you are making a lot of possibly parallel + requests to the same host, which is often the case here. + cpu_count * 5 is used as default value to increase performance. + """ + + self.proxy = None + """Proxy URL + """ + self.proxy_headers = None + """Proxy headers + """ + self.safe_chars_for_path_param = '' + """Safe chars for path_param + """ + self.retries = None + """Adding retries to override urllib3 default value 3 + """ + # Enable client side validation + self.client_side_validation = True + + # Options to pass down to the underlying urllib3 socket + self.socket_options = None + + def __deepcopy__(self, memo): + cls = self.__class__ + result = cls.__new__(cls) + memo[id(self)] = result + for k, v in self.__dict__.items(): + if k not in ('logger', 'logger_file_handler'): + setattr(result, k, copy.deepcopy(v, memo)) + # shallow copy of loggers + result.logger = copy.copy(self.logger) + # use setters to configure loggers + result.logger_file = self.logger_file + result.debug = self.debug + return result + + @property + def logger_file(self): + """The logger file. + + If the logger_file is None, then add stream handler and remove file + handler. Otherwise, add file handler and remove stream handler. + + :param value: The logger_file path. + :type: str + """ + return self.__logger_file + + @logger_file.setter + def logger_file(self, value): + """The logger file. + + If the logger_file is None, then add stream handler and remove file + handler. Otherwise, add file handler and remove stream handler. + + :param value: The logger_file path. + :type: str + """ + self.__logger_file = value + if self.__logger_file: + # If set logging file, + # then add file handler and remove stream handler. + self.logger_file_handler = logging.FileHandler(self.__logger_file) + self.logger_file_handler.setFormatter(self.logger_formatter) + for _, logger in self.logger.items(): + logger.addHandler(self.logger_file_handler) + + @property + def debug(self): + """Debug status + + :param value: The debug status, True or False. + :type: bool + """ + return self.__debug + + @debug.setter + def debug(self, value): + """Debug status + + :param value: The debug status, True or False. + :type: bool + """ + self.__debug = value + if self.__debug: + # if debug status is True, turn on debug logging + for _, logger in self.logger.items(): + logger.setLevel(logging.DEBUG) + # turn on http_client debug + http_client.HTTPConnection.debuglevel = 1 + else: + # if debug status is False, turn off debug logging, + # setting log level to default `logging.WARNING` + for _, logger in self.logger.items(): + logger.setLevel(logging.WARNING) + # turn off http_client debug + http_client.HTTPConnection.debuglevel = 0 + + @property + def logger_format(self): + """The logger format. + + The logger_formatter will be updated when sets logger_format. + + :param value: The format string. + :type: str + """ + return self.__logger_format + + @logger_format.setter + def logger_format(self, value): + """The logger format. + + The logger_formatter will be updated when sets logger_format. + + :param value: The format string. + :type: str + """ + self.__logger_format = value + self.logger_formatter = logging.Formatter(self.__logger_format) + + def to_debug_report(self): + """Gets the essential information for debugging. + + :return: The report for debugging. + """ + return "Python SDK Debug Report:\n"\ + "OS: {env}\n"\ + "Python Version: {pyversion}\n"\ + "Version of the API: 1.0.0\n"\ + "SDK Package Version: 1.0.0".\ + format(env=sys.platform, pyversion=sys.version) + + def get_server_url( + self, + key_prefix: typing.Literal[ + "servers", + ], + index: typing.Optional[int], + ) -> str: + """Gets host URL based on the index + :param index: array index of the host settings + :return: URL based on host settings + """ + if index: + used_index = index + else: + try: + used_index = self.server_index_info[key_prefix] + except KeyError: + # fallback and use the default index + used_index = self.server_index_info.get("servers", 0) + server_info_key = typing.cast( + typing.Literal[ + "servers/0", + ], + f"{key_prefix}/{used_index}" + ) + try: + server = self.server_info[server_info_key] + except KeyError as ex: + raise ex + return server.url diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/configurations/schema_configuration.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/configurations/schema_configuration.py new file mode 100644 index 00000000000..63bda4240a0 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/configurations/schema_configuration.py @@ -0,0 +1,94 @@ +# coding: utf-8 + +""" + Example + No description provided (generated by Openapi JSON Schema Generator https://github.com/openapi-json-schema-tools/openapi-json-schema-generator) # noqa: E501 + The version of the OpenAPI document: 1.0.0 + Generated by: https://github.com/openapi-json-schema-tools/openapi-json-schema-generator +""" + +import typing + +from json_schema_api import exceptions + + +PYTHON_KEYWORD_TO_JSON_SCHEMA_KEYWORD = { + 'additional_properties': 'additionalProperties', + 'all_of': 'allOf', + 'any_of': 'anyOf', + 'discriminator': 'discriminator', + # default omitted because it has no validation impact + 'enum_value_to_name': 'enum', + 'exclusive_maximum': 'exclusiveMaximum', + 'exclusive_minimum': 'exclusiveMinimum', + 'format': 'format', + 'inclusive_maximum': 'maximum', + 'inclusive_minimum': 'minimum', + 'items': 'items', + 'max_items': 'maxItems', + 'max_length': 'maxLength', + 'max_properties': 'maxProperties', + 'min_items': 'minItems', + 'min_length': 'minLength', + 'min_properties': 'minProperties', + 'multiple_of': 'multipleOf', + 'not_': 'not', + 'one_of': 'oneOf', + 'pattern': 'pattern', + 'properties': 'properties', + 'required': 'required', + 'types': 'type', + 'unique_items': 'uniqueItems' +} + +class SchemaConfiguration: + """NOTE: This class is auto generated by OpenAPI JSON Schema Generator + + Ref: https://github.com/openapi-json-schema-tools/openapi-json-schema-generator + Do not edit the class manually. + + :param disabled_json_schema_keywords (set): Set of + JSON schema validation keywords to disable JSON schema structural validation + rules. The following keywords may be specified: multipleOf, maximum, + exclusiveMaximum, minimum, exclusiveMinimum, maxLength, minLength, pattern, + maxItems, minItems. + By default, the validation is performed for data generated locally by the client + and data received from the server, independent of any validation performed by + the server side. If the input data does not satisfy the JSON schema validation + rules specified in the OpenAPI document, an exception is raised. + If disabled_json_schema_keywords is set, structural validation is + disabled. This can be useful to troubleshoot data validation problem, such as + when the OpenAPI document validation rules do not match the actual API data + received by the server. + :param server_index: Index to servers configuration. + """ + + def __init__( + self, + disabled_json_schema_keywords = set(), + ): + """Constructor + """ + self.disabled_json_schema_keywords = disabled_json_schema_keywords + + @property + def disabled_json_schema_python_keywords(self) -> typing.Set[str]: + return self.__disabled_json_schema_python_keywords + + @property + def disabled_json_schema_keywords(self) -> typing.Set[str]: + return self.__disabled_json_schema_keywords + + @disabled_json_schema_keywords.setter + def disabled_json_schema_keywords(self, json_keywords: typing.Set[str]): + disabled_json_schema_keywords = set() + disabled_json_schema_python_keywords = set() + for k in json_keywords: + python_keywords = {key for key, val in PYTHON_KEYWORD_TO_JSON_SCHEMA_KEYWORD.items() if val == k} + if not python_keywords: + raise exceptions.ApiValueError( + "Invalid keyword: '{0}''".format(k)) + disabled_json_schema_keywords.add(k) + disabled_json_schema_python_keywords.update(python_keywords) + self.__disabled_json_schema_keywords = disabled_json_schema_keywords + self.__disabled_json_schema_python_keywords = disabled_json_schema_python_keywords \ No newline at end of file diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/exceptions.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/exceptions.py new file mode 100644 index 00000000000..de07c63ae57 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/exceptions.py @@ -0,0 +1,132 @@ +# coding: utf-8 + +""" + Example + No description provided (generated by Openapi JSON Schema Generator https://github.com/openapi-json-schema-tools/openapi-json-schema-generator) # noqa: E501 + The version of the OpenAPI document: 1.0.0 + Generated by: https://github.com/openapi-json-schema-tools/openapi-json-schema-generator +""" + +import dataclasses +import typing + +from json_schema_api import api_response + + +class OpenApiException(Exception): + """The base exception class for all OpenAPIExceptions""" + +def render_path(path_to_item): + """Returns a string representation of a path""" + result = "" + for pth in path_to_item: + if isinstance(pth, int): + result += "[{0}]".format(pth) + else: + result += "['{0}']".format(pth) + return result + + +class ApiTypeError(OpenApiException, TypeError): + def __init__(self, msg, path_to_item=None, valid_classes=None, + key_type=None): + """ Raises an exception for TypeErrors + + Args: + msg (str): the exception message + + Keyword Args: + path_to_item (list): a list of keys an indices to get to the + current_item + None if unset + valid_classes (tuple): the primitive classes that current item + should be an instance of + None if unset + key_type (bool): False if our value is a value in a dict + True if it is a key in a dict + False if our item is an item in a list + None if unset + """ + self.path_to_item = path_to_item + self.valid_classes = valid_classes + self.key_type = key_type + full_msg = msg + if path_to_item: + full_msg = "{0} at {1}".format(msg, render_path(path_to_item)) + super(ApiTypeError, self).__init__(full_msg) + + +class ApiValueError(OpenApiException, ValueError): + def __init__(self, msg, path_to_item=None): + """ + Args: + msg (str): the exception message + + Keyword Args: + path_to_item (list) the path to the exception in the + received_data dict. None if unset + """ + + self.path_to_item = path_to_item + full_msg = msg + if path_to_item: + full_msg = "{0} at {1}".format(msg, render_path(path_to_item)) + super(ApiValueError, self).__init__(full_msg) + + +class ApiAttributeError(OpenApiException, AttributeError): + def __init__(self, msg, path_to_item=None): + """ + Raised when an attribute reference or assignment fails. + + Args: + msg (str): the exception message + + Keyword Args: + path_to_item (None/list) the path to the exception in the + received_data dict + """ + self.path_to_item = path_to_item + full_msg = msg + if path_to_item: + full_msg = "{0} at {1}".format(msg, render_path(path_to_item)) + super(ApiAttributeError, self).__init__(full_msg) + + +class ApiKeyError(OpenApiException, KeyError): + def __init__(self, msg, path_to_item=None): + """ + Args: + msg (str): the exception message + + Keyword Args: + path_to_item (None/list) the path to the exception in the + received_data dict + """ + self.path_to_item = path_to_item + full_msg = msg + if path_to_item: + full_msg = "{0} at {1}".format(msg, render_path(path_to_item)) + super(ApiKeyError, self).__init__(full_msg) + +T = typing.TypeVar('T', bound=api_response.ApiResponse) + + +@dataclasses.dataclass +class ApiException(OpenApiException, typing.Generic[T]): + status: int + reason: typing.Optional[str] = None + api_response: typing.Optional[T] = None + + def __str__(self): + """Custom error messages for exception""" + error_message = "({0})\n"\ + "Reason: {1}\n".format(self.status, self.reason) + if self.api_response: + if self.api_response.response.headers: + error_message += "HTTP response headers: {0}\n".format( + self.api_response.response.headers) + if self.api_response.response.data: + error_message += "HTTP response body: {0}\n".format(self.api_response.response.data) + + return error_message diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/paths/__init__.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/paths/__init__.py new file mode 100644 index 00000000000..269cc181bd4 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/paths/__init__.py @@ -0,0 +1,3 @@ +# do not import all endpoints into this module because that uses a lot of memory and stack frames +# if you need the ability to import all endpoints from this module, import them with +# from json_schema_api.apis import path_to_api diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/paths/some_path/__init__.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/paths/some_path/__init__.py new file mode 100644 index 00000000000..837999dee22 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/paths/some_path/__init__.py @@ -0,0 +1,5 @@ +# do not import all endpoints into this module because that uses a lot of memory and stack frames +# if you need the ability to import all endpoints from this module, import them with +# from json_schema_api.apis.paths.some_path import SomePath + +path = "/somePath" \ No newline at end of file diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/paths/some_path/get/__init__.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/paths/some_path/get/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/paths/some_path/get/operation.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/paths/some_path/get/operation.py new file mode 100644 index 00000000000..ced9b15ada3 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/paths/some_path/get/operation.py @@ -0,0 +1,113 @@ +# coding: utf-8 + +""" + Generated by: https://github.com/openapi-json-schema-tools/openapi-json-schema-generator +""" + +from json_schema_api import api_client +from json_schema_api.shared_imports.operation_imports import * # pyright: ignore [reportWildcardImportFromLibrary] + +from .. import path +from .responses import response_200 + + +__StatusCodeToResponse = typing.TypedDict( + '__StatusCodeToResponse', + { + '200': typing.Type[response_200.ResponseFor200], + } +) +_status_code_to_response: __StatusCodeToResponse = { + '200': response_200.ResponseFor200, +} +_non_error_status_codes = frozenset({ + '200', +}) + +_all_accept_content_types = ( + "application/json", +) + + +class BaseApi(api_client.Api): + @typing.overload + def _get_some_path( + self, + *, + skip_deserialization: typing.Literal[False] = False, + accept_content_types: typing.Tuple[str, ...] = _all_accept_content_types, + server_index: typing.Optional[int] = None, + stream: bool = False, + timeout: typing.Optional[typing.Union[int, float, typing.Tuple]] = None, + ) -> response_200.ApiResponse: ... + + @typing.overload + def _get_some_path( + self, + *, + skip_deserialization: typing.Literal[True], + accept_content_types: typing.Tuple[str, ...] = _all_accept_content_types, + server_index: typing.Optional[int] = None, + stream: bool = False, + timeout: typing.Optional[typing.Union[int, float, typing.Tuple]] = None, + ) -> api_response.ApiResponseWithoutDeserialization: ... + + def _get_some_path( + self, + *, + skip_deserialization: bool = False, + accept_content_types: typing.Tuple[str, ...] = _all_accept_content_types, + server_index: typing.Optional[int] = None, + stream: bool = False, + timeout: typing.Optional[typing.Union[int, float, typing.Tuple]] = None, + ): + """ + :param skip_deserialization: If true then api_response.response will be set but + api_response.body and api_response.headers will not be deserialized into schema + class instances + """ + used_path = path + headers = self._get_headers(accept_content_types=accept_content_types) + # TODO add cookie handling + host = self.api_client.configuration.get_server_url( + "servers", server_index + ) + + raw_response = self.api_client.call_api( + resource_path=used_path, + method='get', + host=host, + headers=headers, + stream=stream, + timeout=timeout, + ) + + if skip_deserialization: + skip_deser_response = api_response.ApiResponseWithoutDeserialization(response=raw_response) + self._verify_response_status(skip_deser_response) + return skip_deser_response + + status = str(raw_response.status) + if status in _non_error_status_codes: + status_code = typing.cast( + typing.Literal[ + '200', + ], + status + ) + return _status_code_to_response[status_code].deserialize( + raw_response, self.api_client.schema_configuration) + + response = api_response.ApiResponseWithoutDeserialization(response=raw_response) + self._verify_response_status(response) + return response + + +class GetSomePath(BaseApi): + # this class is used by api classes that refer to endpoints with operationId.snakeCase fn names + get_some_path = BaseApi._get_some_path + + +class ApiForGet(BaseApi): + # this class is used by api classes that refer to endpoints by path and http method names + get = BaseApi._get_some_path diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/paths/some_path/get/responses/__init__.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/paths/some_path/get/responses/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/paths/some_path/get/responses/response_200/__init__.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/paths/some_path/get/responses/response_200/__init__.py new file mode 100644 index 00000000000..da45069e001 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/paths/some_path/get/responses/response_200/__init__.py @@ -0,0 +1,29 @@ +# coding: utf-8 + +""" + Generated by: https://github.com/openapi-json-schema-tools/openapi-json-schema-generator +""" + +from json_schema_api.shared_imports.response_imports import * # pyright: ignore [reportWildcardImportFromLibrary] + +from .content.application_json import schema as application_json_schema + + +@dataclasses.dataclass +class ApiResponse(api_response.ApiResponse): + response: urllib3.HTTPResponse + body: schemas.OUTPUT_BASE_TYPES + headers: schemas.Unset = schemas.unset + + +class ResponseFor200(api_client.OpenApiResponse[ApiResponse]): + @classmethod + def get_response(cls, response, headers, body) -> ApiResponse: + return ApiResponse(response=response, body=body, headers=headers) + + + class ApplicationJsonMediaType(api_client.MediaType): + schema: typing_extensions.TypeAlias = application_json_schema.Schema + content = { + 'application/json': ApplicationJsonMediaType, + } diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/paths/some_path/get/responses/response_200/content/__init__.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/paths/some_path/get/responses/response_200/content/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/paths/some_path/get/responses/response_200/content/application_json/__init__.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/paths/some_path/get/responses/response_200/content/application_json/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/paths/some_path/get/responses/response_200/content/application_json/schema.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/paths/some_path/get/responses/response_200/content/application_json/schema.py new file mode 100644 index 00000000000..c5c1b0ea3dd --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/paths/some_path/get/responses/response_200/content/application_json/schema.py @@ -0,0 +1,13 @@ +# coding: utf-8 + +""" + Example + No description provided (generated by Openapi JSON Schema Generator https://github.com/openapi-json-schema-tools/openapi-json-schema-generator) # noqa: E501 + The version of the OpenAPI document: 1.0.0 + Generated by: https://github.com/openapi-json-schema-tools/openapi-json-schema-generator +""" + +from __future__ import annotations +from json_schema_api.shared_imports.schema_imports import * # pyright: ignore [reportWildcardImportFromLibrary] + +Schema: typing_extensions.TypeAlias = schemas.AnyTypeSchema diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/py.typed b/samples/client/3_1_0_json_schema/python/src/json_schema_api/py.typed new file mode 100644 index 00000000000..e69de29bb2d diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/rest.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/rest.py new file mode 100644 index 00000000000..99c8591a46a --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/rest.py @@ -0,0 +1,270 @@ +# coding: utf-8 + +""" + Example + No description provided (generated by Openapi JSON Schema Generator https://github.com/openapi-json-schema-tools/openapi-json-schema-generator) # noqa: E501 + The version of the OpenAPI document: 1.0.0 + Generated by: https://github.com/openapi-json-schema-tools/openapi-json-schema-generator +""" + +import logging +import ssl +from urllib.parse import urlencode +import typing + +import certifi # type: ignore[import] +import urllib3 +from urllib3 import fields +from urllib3 import exceptions as urllib3_exceptions +from urllib3._collections import HTTPHeaderDict + +from json_schema_api import exceptions + + +logger = logging.getLogger(__name__) +_TYPE_FIELD_VALUE = typing.Union[str, bytes] + + +class RequestField(fields.RequestField): + def __init__( + self, + name: str, + data: _TYPE_FIELD_VALUE, + filename: typing.Optional[str] = None, + headers: typing.Optional[typing.Mapping[str, typing.Union[str, None]]] = None, + header_formatter: typing.Optional[typing.Callable[[str, _TYPE_FIELD_VALUE], str]] = None, + ): + super().__init__(name, data, filename, headers, header_formatter) # type: ignore + + def __eq__(self, other): + if not isinstance(other, fields.RequestField): + return False + return self.__dict__ == other.__dict__ + + +class RESTClientObject(object): + + def __init__(self, configuration, pools_size=4, maxsize=None): + # urllib3.PoolManager will pass all kw parameters to connectionpool + # https://github.com/shazow/urllib3/blob/f9409436f83aeb79fbaf090181cd81b784f1b8ce/urllib3/poolmanager.py#L75 # noqa: E501 + # https://github.com/shazow/urllib3/blob/f9409436f83aeb79fbaf090181cd81b784f1b8ce/urllib3/connectionpool.py#L680 # noqa: E501 + # maxsize is the number of requests to host that are allowed in parallel # noqa: E501 + # Custom SSL certificates and client certificates: http://urllib3.readthedocs.io/en/latest/advanced-usage.html # noqa: E501 + + # cert_reqs + if configuration.verify_ssl: + cert_reqs = ssl.CERT_REQUIRED + else: + cert_reqs = ssl.CERT_NONE + + # ca_certs + if configuration.ssl_ca_cert: + ca_certs = configuration.ssl_ca_cert + else: + # if not set certificate file, use Mozilla's root certificates. + ca_certs = certifi.where() + + addition_pool_args = {} + if configuration.assert_hostname is not None: + addition_pool_args['assert_hostname'] = configuration.assert_hostname # noqa: E501 + + if configuration.retries is not None: + addition_pool_args['retries'] = configuration.retries + + if configuration.socket_options is not None: + addition_pool_args['socket_options'] = configuration.socket_options + + if maxsize is None: + if configuration.connection_pool_maxsize is not None: + maxsize = configuration.connection_pool_maxsize + else: + maxsize = 4 + + # https pool manager + if configuration.proxy: + self.pool_manager = urllib3.ProxyManager( + num_pools=pools_size, + maxsize=maxsize, + cert_reqs=cert_reqs, + ca_certs=ca_certs, + cert_file=configuration.cert_file, + key_file=configuration.key_file, + proxy_url=configuration.proxy, + proxy_headers=configuration.proxy_headers, + **addition_pool_args + ) + else: + self.pool_manager = urllib3.PoolManager( + num_pools=pools_size, + maxsize=maxsize, + cert_reqs=cert_reqs, + ca_certs=ca_certs, + cert_file=configuration.cert_file, + key_file=configuration.key_file, + **addition_pool_args + ) + + def request( + self, + method: str, + url: str, + headers: typing.Optional[HTTPHeaderDict] = None, + fields: typing.Optional[typing.Tuple[RequestField, ...]] = None, + body: typing.Optional[typing.Union[str, bytes]] = None, + stream: bool = False, + timeout: typing.Optional[typing.Union[int, float, typing.Tuple]] = None, + ) -> urllib3.HTTPResponse: + """Perform requests. + + :param method: http request method + :param url: http request url + :param headers: http request headers + :param body: request body, for other types + :param fields: request parameters for + `application/x-www-form-urlencoded` + or `multipart/form-data` + :param stream: if True, the urllib3.HTTPResponse object will + be returned without reading/decoding response + data. Default is False. + :param timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + """ + assert method in ['GET', 'HEAD', 'DELETE', 'POST', 'PUT', + 'PATCH', 'OPTIONS'] + + if fields and body: + raise exceptions.ApiValueError( + "body parameter cannot be used with fields parameter." + ) + + headers = headers or HTTPHeaderDict() + + used_timeout: typing.Optional[urllib3.Timeout] = None + if timeout: + if isinstance(timeout, (int, float)): + used_timeout = urllib3.Timeout(total=timeout) + elif (isinstance(timeout, tuple) and + len(timeout) == 2): + used_timeout = urllib3.Timeout(connect=timeout[0], read=timeout[1]) + + try: + # For `POST`, `PUT`, `PATCH`, `OPTIONS`, `DELETE` + if method in {'POST', 'PUT', 'PATCH', 'OPTIONS', 'DELETE'}: + if 'Content-Type' not in headers and body is None: + r = self.pool_manager.request( + method, + url, + preload_content=not stream, + timeout=used_timeout, + headers=headers + ) + elif headers['Content-Type'] == 'application/x-www-form-urlencoded': # noqa: E501 + r = self.pool_manager.request( + method, url, + body=body, + encode_multipart=False, + preload_content=not stream, + timeout=used_timeout, + headers=headers) + elif headers['Content-Type'] == 'multipart/form-data': + # must del headers['Content-Type'], or the correct + # Content-Type which generated by urllib3 will be + # overwritten. + del headers['Content-Type'] + r = self.pool_manager.request( + method, url, + fields=fields, + encode_multipart=True, + preload_content=not stream, + timeout=used_timeout, + headers=headers) + # Pass a `string` parameter directly in the body to support + # other content types than Json when `body` argument is + # provided in serialized form + elif isinstance(body, str) or isinstance(body, bytes): + request_body = body + r = self.pool_manager.request( + method, url, + body=request_body, + preload_content=not stream, + timeout=used_timeout, + headers=headers) + else: + # Cannot generate the request from given parameters + msg = """Cannot prepare a request message for provided + arguments. Please check that your arguments match + declared content type.""" + raise exceptions.ApiException(status=0, reason=msg) + # For `GET`, `HEAD` + else: + r = self.pool_manager.request(method, url, + preload_content=not stream, + timeout=used_timeout, + headers=headers) + except urllib3_exceptions.SSLError as e: + msg = "{0}\n{1}".format(type(e).__name__, str(e)) + raise exceptions.ApiException(status=0, reason=msg) + + if not stream: + # log response body + logger.debug("response body: %s", r.data) + + return r + + def get(self, url, headers=None, stream=False, + timeout=None, fields=None) -> urllib3.HTTPResponse: + return self.request("GET", url, + headers=headers, + stream=stream, + timeout=timeout, + fields=fields) + + def head(self, url, headers=None, stream=False, + timeout=None, fields=None) -> urllib3.HTTPResponse: + return self.request("HEAD", url, + headers=headers, + stream=stream, + timeout=timeout, + fields=fields) + + def options(self, url, headers=None, + body=None, stream=False, timeout=None, fields=None) -> urllib3.HTTPResponse: + return self.request("OPTIONS", url, + headers=headers, + stream=stream, + timeout=timeout, + body=body, fields=fields) + + def delete(self, url, headers=None, body=None, + stream=False, timeout=None, fields=None) -> urllib3.HTTPResponse: + return self.request("DELETE", url, + headers=headers, + stream=stream, + timeout=timeout, + body=body, fields=fields) + + def post(self, url, headers=None, + body=None, stream=False, timeout=None, fields=None) -> urllib3.HTTPResponse: + return self.request("POST", url, + headers=headers, + stream=stream, + timeout=timeout, + body=body, fields=fields) + + def put(self, url, headers=None, + body=None, stream=False, timeout=None, fields=None) -> urllib3.HTTPResponse: + return self.request("PUT", url, + headers=headers, + stream=stream, + timeout=timeout, + body=body, fields=fields) + + def patch(self, url, headers=None, + body=None, stream=False, timeout=None, fields=None) -> urllib3.HTTPResponse: + return self.request("PATCH", url, + headers=headers, + stream=stream, + timeout=timeout, + body=body, fields=fields) diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/schemas/__init__.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/schemas/__init__.py new file mode 100644 index 00000000000..7f86329469c --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/schemas/__init__.py @@ -0,0 +1,148 @@ +# coding: utf-8 + +""" + Example + No description provided (generated by Openapi JSON Schema Generator https://github.com/openapi-json-schema-tools/openapi-json-schema-generator) # noqa: E501 + The version of the OpenAPI document: 1.0.0 + Generated by: https://github.com/openapi-json-schema-tools/openapi-json-schema-generator +""" + +import typing + +import typing_extensions + +from .schema import ( + get_class, + none_type_, + classproperty, + Bool, + FileIO, + Schema, + SingletonMeta, + AnyTypeSchema, + UnsetAnyTypeSchema, + INPUT_TYPES_ALL +) + +from .schemas import ( + ListSchema, + NoneSchema, + NumberSchema, + IntSchema, + Int32Schema, + Int64Schema, + Float32Schema, + Float64Schema, + StrSchema, + UUIDSchema, + DateSchema, + DateTimeSchema, + DecimalSchema, + BytesSchema, + FileSchema, + BinarySchema, + BoolSchema, + NotAnyTypeSchema, + OUTPUT_BASE_TYPES, + DictSchema +) +from .validation import ( + PatternInfo, + ValidationMetadata, + immutabledict +) +from .format import ( + as_date, + as_datetime, + as_decimal, + as_uuid +) + +def typed_dict_to_instance(t_dict: typing_extensions._TypedDictMeta) -> typing.Mapping: # type: ignore + res = {} + for key, val in t_dict.__annotations__.items(): + if isinstance(val, typing._GenericAlias): # type: ignore + # typing.Type[W] -> W + val_cls = typing.get_args(val)[0] + res[key] = val_cls + return res + +X = typing.TypeVar('X', bound=typing.Tuple) + +def tuple_to_instance(tup: typing.Type[X]) -> X: + res = [] + for arg in typing.get_args(tup): + if isinstance(arg, typing._GenericAlias): # type: ignore + # typing.Type[Schema] -> Schema + arg_cls = typing.get_args(arg)[0] + res.append(arg_cls) + return tuple(res) # type: ignore + + +class Unset: + """ + An instance of this class is set as the default value for object type(dict) properties that are optional + When a property has an unset value, that property will not be assigned in the dict + """ + pass + +unset: Unset = Unset() + +def key_unknown_error_msg(key: str) -> str: + return (f"Invalid key. The key {key} is not a known key in this payload. " + "If this key is an additional property, use get_additional_property_" + ) + +def raise_if_key_known( + key: str, + required_keys: typing.FrozenSet[str], + optional_keys: typing.FrozenSet[str] +): + if key in required_keys or key in optional_keys: + raise ValueError(f"The key {key} is a known property, use get_property to access its value") + +__all__ = [ + 'get_class', + 'none_type_', + 'classproperty', + 'Bool', + 'FileIO', + 'Schema', + 'SingletonMeta', + 'AnyTypeSchema', + 'UnsetAnyTypeSchema', + 'INPUT_TYPES_ALL', + 'ListSchema', + 'NoneSchema', + 'NumberSchema', + 'IntSchema', + 'Int32Schema', + 'Int64Schema', + 'Float32Schema', + 'Float64Schema', + 'StrSchema', + 'UUIDSchema', + 'DateSchema', + 'DateTimeSchema', + 'DecimalSchema', + 'BytesSchema', + 'FileSchema', + 'BinarySchema', + 'BoolSchema', + 'NotAnyTypeSchema', + 'OUTPUT_BASE_TYPES', + 'DictSchema', + 'PatternInfo', + 'ValidationMetadata', + 'immutabledict', + 'as_date', + 'as_datetime', + 'as_decimal', + 'as_uuid', + 'typed_dict_to_instance', + 'tuple_to_instance', + 'Unset', + 'unset', + 'key_unknown_error_msg', + 'raise_if_key_known' +] \ No newline at end of file diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/schemas/format.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/schemas/format.py new file mode 100644 index 00000000000..bb614903b82 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/schemas/format.py @@ -0,0 +1,115 @@ +import datetime +import decimal +import functools +import typing +import uuid + +from dateutil import parser, tz + + +class CustomIsoparser(parser.isoparser): + def __init__(self, sep: typing.Optional[str] = None): + """ + :param sep: + A single character that separates date and time portions. If + ``None``, the parser will accept any single character. + For strict ISO-8601 adherence, pass ``'T'``. + """ + if sep is not None: + if (len(sep) != 1 or ord(sep) >= 128 or sep in '0123456789'): + raise ValueError('Separator must be a single, non-numeric ' + + 'ASCII character') + + used_sep = sep.encode('ascii') + else: + used_sep = None + + self._sep = used_sep + + @staticmethod + def __get_ascii_bytes(str_in: str) -> bytes: + # If it's unicode, turn it into bytes, since ISO-8601 only covers ASCII + # ASCII is the same in UTF-8 + try: + return str_in.encode('ascii') + except UnicodeEncodeError as e: + msg = 'ISO-8601 strings should contain only ASCII characters' + raise ValueError(msg) from e + + def __parse_isodate(self, dt_str: str) -> typing.Tuple[typing.Tuple[int, int, int], int]: + dt_str_ascii = self.__get_ascii_bytes(dt_str) + values = self._parse_isodate(dt_str_ascii) # type: ignore + values = typing.cast(typing.Tuple[typing.List[int], int], values) + components = typing.cast( typing.Tuple[int, int, int], tuple(values[0])) + pos = values[1] + return components, pos + + def __parse_isotime(self, dt_str: str) -> typing.Tuple[int, int, int, int, typing.Optional[typing.Union[tz.tzutc, tz.tzoffset]]]: + dt_str_ascii = self.__get_ascii_bytes(dt_str) + values = self._parse_isotime(dt_str_ascii) # type: ignore + components: typing.Tuple[int, int, int, int, typing.Optional[typing.Union[tz.tzutc, tz.tzoffset]]] = tuple(values) # type: ignore + return components + + def parse_isodatetime(self, dt_str: str) -> datetime.datetime: + date_components, pos = self.__parse_isodate(dt_str) + if len(dt_str) <= pos: + # len(components) <= 3 + raise ValueError('Value is not a datetime') + if self._sep is None or dt_str[pos:pos + 1] == self._sep: + hour, minute, second, microsecond, tzinfo = self.__parse_isotime(dt_str[pos + 1:]) + if hour == 24: + hour = 0 + components = (*date_components, hour, minute, second, microsecond, tzinfo) + return datetime.datetime(*components) + datetime.timedelta(days=1) + else: + components = (*date_components, hour, minute, second, microsecond, tzinfo) + else: + raise ValueError('String contains unknown ISO components') + + return datetime.datetime(*components) + + def parse_isodate_str(self, datestr: str) -> datetime.date: + components, pos = self.__parse_isodate(datestr) + + if len(datestr) > pos: + raise ValueError('String contains invalid time components') + + if len(components) > 3: + raise ValueError('String contains invalid time components') + + return datetime.date(*components) + +DEFAULT_ISOPARSER = CustomIsoparser() + +@functools.lru_cache() +def as_date(arg: str) -> datetime.date: + """ + type = "string" + format = "date" + """ + return DEFAULT_ISOPARSER.parse_isodate_str(arg) + +@functools.lru_cache() +def as_datetime(arg: str) -> datetime.datetime: + """ + type = "string" + format = "date-time" + """ + return DEFAULT_ISOPARSER.parse_isodatetime(arg) + +@functools.lru_cache() +def as_decimal(arg: str) -> decimal.Decimal: + """ + Applicable when storing decimals that are sent over the wire as strings + type = "string" + format = "number" + """ + return decimal.Decimal(arg) + +@functools.lru_cache() +def as_uuid(arg: str) -> uuid.UUID: + """ + type = "string" + format = "uuid" + """ + return uuid.UUID(arg) \ No newline at end of file diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/schemas/original_immutabledict.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/schemas/original_immutabledict.py new file mode 100644 index 00000000000..3897e140a4a --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/schemas/original_immutabledict.py @@ -0,0 +1,97 @@ +""" +MIT License + +Copyright (c) 2020 Corentin Garcia + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" +from __future__ import annotations +import typing +import typing_extensions + +_K = typing.TypeVar("_K") +_V = typing.TypeVar("_V", covariant=True) + + +class immutabledict(typing.Mapping[_K, _V]): + """ + An immutable wrapper around dictionaries that implements + the complete :py:class:`collections.Mapping` interface. + It can be used as a drop-in replacement for dictionaries + where immutability is desired. + + Note: custom version of this class made to remove __init__ + """ + + dict_cls: typing.Type[typing.Dict[typing.Any, typing.Any]] = dict + _dict: typing.Dict[_K, _V] + _hash: typing.Optional[int] + + @classmethod + def fromkeys( + cls, seq: typing.Iterable[_K], value: typing.Optional[_V] = None + ) -> "immutabledict[_K, _V]": + return cls(dict.fromkeys(seq, value)) + + def __new__(cls, *args: typing.Any) -> typing_extensions.Self: + inst = super().__new__(cls) + setattr(inst, '_dict', cls.dict_cls(*args)) + setattr(inst, '_hash', None) + return inst + + def __getitem__(self, key: _K) -> _V: + return self._dict[key] + + def __contains__(self, key: object) -> bool: + return key in self._dict + + def __iter__(self) -> typing.Iterator[_K]: + return iter(self._dict) + + def __len__(self) -> int: + return len(self._dict) + + def __repr__(self) -> str: + return "%s(%r)" % (self.__class__.__name__, self._dict) + + def __hash__(self) -> int: + if self._hash is None: + h = 0 + for key, value in self.items(): + h ^= hash((key, value)) + self._hash = h + + return self._hash + + def __or__(self, other: typing.Any) -> immutabledict[_K, _V]: + if not isinstance(other, (dict, self.__class__)): + return NotImplemented + new = dict(self) + new.update(other) + return self.__class__(new) + + def __ror__(self, other: typing.Any) -> typing.Dict[typing.Any, typing.Any]: + if not isinstance(other, (dict, self.__class__)): + return NotImplemented + new = dict(other) + new.update(self) + return new + + def __ior__(self, other: typing.Any) -> immutabledict[_K, _V]: + raise TypeError(f"'{self.__class__.__name__}' object is not mutable") diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/schemas/schema.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/schemas/schema.py new file mode 100644 index 00000000000..4e4473f2c56 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/schemas/schema.py @@ -0,0 +1,703 @@ +from __future__ import annotations +import datetime +import dataclasses +import io +import types +import typing +import uuid + +import functools +import typing_extensions + +from json_schema_api import exceptions +from json_schema_api.configurations import schema_configuration + +from . import validation + +none_type_ = type(None) +T = typing.TypeVar('T', bound=typing.Mapping) +U = typing.TypeVar('U', bound=typing.Sequence) +W = typing.TypeVar('W') + + +class SchemaTyped: + additional_properties: typing.Type[Schema] + all_of: typing.Tuple[typing.Type[Schema], ...] + any_of: typing.Tuple[typing.Type[Schema], ...] + discriminator: typing.Mapping[str, typing.Mapping[str, typing.Type[Schema]]] + default: typing.Union[str, int, float, bool, None] + enum_value_to_name: typing.Mapping[typing.Union[int, float, str, Bool, None], str] + exclusive_maximum: typing.Union[int, float] + exclusive_minimum: typing.Union[int, float] + format: str + inclusive_maximum: typing.Union[int, float] + inclusive_minimum: typing.Union[int, float] + items: typing.Type[Schema] + max_items: int + max_length: int + max_properties: int + min_items: int + min_length: int + min_properties: int + multiple_of: typing.Union[int, float] + not_: typing.Type[Schema] + one_of: typing.Tuple[typing.Type[Schema], ...] + pattern: validation.PatternInfo + properties: typing.Mapping[str, typing.Type[Schema]] + required: typing.FrozenSet[str] + types: typing.FrozenSet[typing.Type] + unique_items: bool + + +class FileIO(io.FileIO): + """ + A class for storing files + Note: this class is not immutable + """ + + def __new__(cls, arg: typing.Union[io.FileIO, io.BufferedReader]): + if isinstance(arg, (io.FileIO, io.BufferedReader)): + if arg.closed: + raise exceptions.ApiValueError('Invalid file state; file is closed and must be open') + arg.close() + inst = super(FileIO, cls).__new__(cls, arg.name) # type: ignore + super(FileIO, inst).__init__(arg.name) + return inst + raise exceptions.ApiValueError('FileIO must be passed arg which contains the open file') + + def __init__(self, arg: typing.Union[io.FileIO, io.BufferedReader]): + """ + Needed for instantiation when passing in arguments of the above type + """ + pass + + +class classproperty(typing.Generic[W]): + def __init__(self, method: typing.Callable[..., W]): + self.__method = method + functools.update_wrapper(self, method) # type: ignore + + def __get__(self, obj, cls=None) -> W: + if cls is None: + cls = type(obj) + return self.__method(cls) + + +class Bool: + _instances: typing.Dict[typing.Tuple[type, bool], Bool] = {} + """ + This class is needed to replace bool during validation processing + json schema requires that 0 != False and 1 != True + python implementation defines 0 == False and 1 == True + To meet the json schema requirements, all bool instances are replaced with Bool singletons + during validation only, and then bool values are returned from validation + """ + + def __new__(cls, arg_: bool, **kwargs): + """ + Method that implements singleton + cls base classes: BoolClass, NoneClass, str, decimal.Decimal + The 3rd key is used in the tuple below for a corner case where an enum contains integer 1 + However 1.0 can also be ingested into that enum schema because 1.0 == 1 and + Decimal('1.0') == Decimal('1') + But if we omitted the 3rd value in the key, then Decimal('1.0') would be stored as Decimal('1') + and json serializing that instance would be '1' rather than the expected '1.0' + Adding the 3rd value, the str of arg_ ensures that 1.0 -> Decimal('1.0') which is serialized as 1.0 + """ + key = (cls, arg_) + if key not in cls._instances: + inst = super().__new__(cls) + cls._instances[key] = inst + return cls._instances[key] + + def __repr__(self): + if bool(self): + return f'' + return f'' + + @classproperty + def TRUE(cls): + return cls(True) # type: ignore + + @classproperty + def FALSE(cls): + return cls(False) # type: ignore + + @functools.lru_cache() + def __bool__(self) -> bool: + for key, instance in self._instances.items(): + if self is instance: + return bool(key[1]) + raise ValueError('Unable to find the boolean value of this instance') + + +def cast_to_allowed_types( + arg: typing.Union[ + dict, + validation.immutabledict, + list, + tuple, + float, + int, + str, + datetime.date, + datetime.datetime, + uuid.UUID, + bool, + None, + bytes, + io.FileIO, + io.BufferedReader, + ], + from_server: bool, + validated_path_to_schemas: typing.Dict[typing.Tuple[typing.Union[str, int], ...], typing.Set[typing.Union[str, int, float, bool, None, validation.immutabledict, tuple]]], + path_to_item: typing.Tuple[typing.Union[str, int], ...], + path_to_type: typing.Dict[typing.Tuple[typing.Union[str, int], ...], type] +) -> typing.Union[ + validation.immutabledict, + tuple, + float, + int, + str, + bytes, + Bool, + None, + FileIO +]: + """ + Casts the input payload arg into the allowed types + The input validated_path_to_schemas is mutated by running this function + + When from_server is False then + - date/datetime is cast to str + - int/float is cast to Decimal + + If a Schema instance is passed in it is converted back to a primitive instance because + One may need to validate that data to the original Schema class AND additional different classes + those additional classes will need to be added to the new manufactured class for that payload + If the code didn't do this and kept the payload as a Schema instance it would fail to validate to other + Schema classes and the code wouldn't be able to mfg a new class that includes all valid schemas + TODO: store the validated schema classes in validation_metadata + + Args: + arg: the payload + from_server: whether this payload came from the server or not + validated_path_to_schemas: a dict that stores the validated classes at any path location in the payload + """ + type_error = exceptions.ApiTypeError(f"Invalid type. Required value type is str and passed type was {type(arg)} at {path_to_item}") + if isinstance(arg, str): + path_to_type[path_to_item] = str + return str(arg) + elif isinstance(arg, (dict, validation.immutabledict)): + path_to_type[path_to_item] = validation.immutabledict + return validation.immutabledict( + { + key: cast_to_allowed_types( + val, + from_server, + validated_path_to_schemas, + path_to_item + (key,), + path_to_type, + ) + for key, val in arg.items() + } + ) + elif isinstance(arg, bool): + """ + this check must come before isinstance(arg, (int, float)) + because isinstance(True, int) is True + """ + path_to_type[path_to_item] = Bool + if arg: + return Bool.TRUE + return Bool.FALSE + elif isinstance(arg, int): + path_to_type[path_to_item] = int + return arg + elif isinstance(arg, float): + path_to_type[path_to_item] = float + return arg + elif isinstance(arg, (tuple, list)): + path_to_type[path_to_item] = tuple + return tuple( + [ + cast_to_allowed_types( + item, + from_server, + validated_path_to_schemas, + path_to_item + (i,), + path_to_type, + ) + for i, item in enumerate(arg) + ] + ) + elif arg is None: + path_to_type[path_to_item] = type(None) + return None + elif isinstance(arg, (datetime.date, datetime.datetime)): + path_to_type[path_to_item] = str + if not from_server: + return arg.isoformat() + raise type_error + elif isinstance(arg, uuid.UUID): + path_to_type[path_to_item] = str + if not from_server: + return str(arg) + raise type_error + elif isinstance(arg, bytes): + path_to_type[path_to_item] = bytes + return bytes(arg) + elif isinstance(arg, (io.FileIO, io.BufferedReader)): + path_to_type[path_to_item] = FileIO + return FileIO(arg) + raise exceptions.ApiTypeError('Invalid type passed in got input={} type={}'.format(arg, type(arg))) + + +class SingletonMeta(type): + """ + A singleton class for schemas + Schemas are frozen classes that are never instantiated with init args + All args come from defaults + """ + _instances: typing.Dict[type, typing.Any] = {} + + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + cls._instances[cls] = super().__call__(*args, **kwargs) + return cls._instances[cls] + + +class Schema(typing.Generic[T, U], validation.SchemaValidator, metaclass=SingletonMeta): + + @classmethod + def __get_path_to_schemas( + cls, + arg, + validation_metadata: validation.ValidationMetadata, + path_to_type: typing.Dict[typing.Tuple[typing.Union[str, int], ...], typing.Type] + ) -> typing.Dict[typing.Tuple[typing.Union[str, int], ...], typing.Type[Schema]]: + """ + Run all validations in the json schema and return a dict of + json schema to tuple of validated schemas + """ + _path_to_schemas: validation.PathToSchemasType = {} + if validation_metadata.validation_ran_earlier(cls): + validation.add_deeper_validated_schemas(validation_metadata, _path_to_schemas) + else: + other_path_to_schemas = cls._validate(arg, validation_metadata=validation_metadata) + validation.update(_path_to_schemas, other_path_to_schemas) + # loop through it make a new class for each entry + # do not modify the returned result because it is cached and we would be modifying the cached value + path_to_schemas: typing.Dict[typing.Tuple[typing.Union[str, int], ...], typing.Type[Schema]] = {} + for path, schema_classes in _path_to_schemas.items(): + schema = typing.cast(typing.Type[Schema], tuple(schema_classes)[-1]) + path_to_schemas[path] = schema + """ + For locations that validation did not check + the code still needs to store type + schema information for instantiation + All of those schemas will be UnsetAnyTypeSchema + """ + missing_paths = path_to_type.keys() - path_to_schemas.keys() + for missing_path in missing_paths: + path_to_schemas[missing_path] = UnsetAnyTypeSchema + + return path_to_schemas + + @staticmethod + def __get_items( + arg: tuple, + path_to_item: typing.Tuple[typing.Union[str, int], ...], + path_to_schemas: typing.Dict[typing.Tuple[typing.Union[str, int], ...], typing.Type[Schema]] + ): + ''' + Schema __get_items + ''' + cast_items = [] + + for i, value in enumerate(arg): + item_path_to_item = path_to_item + (i,) + item_cls = path_to_schemas[item_path_to_item] + new_value = item_cls._get_new_instance_without_conversion( + value, + item_path_to_item, + path_to_schemas + ) + cast_items.append(new_value) + + return tuple(cast_items) + + @staticmethod + def __get_properties( + arg: validation.immutabledict[str, typing.Any], + path_to_item: typing.Tuple[typing.Union[str, int], ...], + path_to_schemas: typing.Dict[typing.Tuple[typing.Union[str, int], ...], typing.Type[Schema]] + ): + """ + Schema __get_properties, this is how properties are set + These values already passed validation + """ + dict_items = {} + + for property_name_js, value in arg.items(): + property_path_to_item = path_to_item + (property_name_js,) + property_cls = path_to_schemas[property_path_to_item] + new_value = property_cls._get_new_instance_without_conversion( + value, + property_path_to_item, + path_to_schemas + ) + dict_items[property_name_js] = new_value + + return validation.immutabledict(dict_items) + + @classmethod + def _get_new_instance_without_conversion( + cls, + arg: typing.Union[int, float, None, Bool, str, validation.immutabledict, tuple, FileIO, bytes], + path_to_item: typing.Tuple[typing.Union[str, int], ...], + path_to_schemas: typing.Dict[typing.Tuple[typing.Union[str, int], ...], typing.Type[Schema]] + ): + # We have a Dynamic class and we are making an instance of it + if isinstance(arg, validation.immutabledict): + used_arg = cls.__get_properties(arg, path_to_item, path_to_schemas) + elif isinstance(arg, tuple): + used_arg = cls.__get_items(arg, path_to_item, path_to_schemas) + elif isinstance(arg, Bool): + return bool(arg) + else: + """ + str, int, float, FileIO, bytes + FileIO = openapi binary type and the user inputs a file + bytes = openapi binary type and the user inputs bytes + """ + return arg + arg_type = type(arg) + type_to_output_cls = cls.__get_type_to_output_cls() + if type_to_output_cls is None: + return used_arg + if arg_type not in type_to_output_cls: + return used_arg + output_cls = type_to_output_cls[arg_type] + if arg_type is tuple: + inst = super(output_cls, output_cls).__new__(output_cls, used_arg) # type: ignore + inst = typing.cast(U, inst) + return inst + assert issubclass(output_cls, validation.immutabledict) + inst = super(output_cls, output_cls).__new__(output_cls, used_arg) # type: ignore + inst = typing.cast(T, inst) + return inst + + @typing.overload + @classmethod + def validate_base( + cls, + arg: None, + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> None: ... + + @typing.overload + @classmethod + def validate_base( + cls, + arg: typing.Literal[True], + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> typing.Literal[True]: ... + + @typing.overload + @classmethod + def validate_base( + cls, + arg: typing.Literal[False], + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> typing.Literal[False]: ... + + @typing.overload + @classmethod + def validate_base( + cls, + arg: bool, + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> bool: ... + + @typing.overload + @classmethod + def validate_base( + cls, + arg: int, + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> int: ... + + @typing.overload + @classmethod + def validate_base( + cls, + arg: float, + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> float: ... + + @typing.overload + @classmethod + def validate_base( + cls, + arg: typing.Union[datetime.date, datetime.datetime, uuid.UUID], + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> str: ... + + @typing.overload + @classmethod + def validate_base( + cls, + arg: typing.Sequence[INPUT_TYPES_ALL], # also covers str, tuple, list, bytes + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> U: ... + + @typing.overload + @classmethod + def validate_base( + cls, + arg: U, + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> U: ... + + @typing.overload + @classmethod + def validate_base( + cls, + arg: typing.Mapping[str, object], # object needed as value type for typeddict inputs + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> T: ... + + @typing.overload + @classmethod + def validate_base( + cls, + arg: typing.Union[ + typing.Mapping[str, INPUT_TYPES_ALL], + T + ], + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> T: ... + + @typing.overload + @classmethod + def validate_base( + cls, + arg: typing.Union[io.FileIO, io.BufferedReader], + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> FileIO: ... + + @classmethod + def validate_base( + cls, + arg, + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None, + ): + """ + Schema validate_base + + Args: + arg (int/float/str/list/tuple/dict/validation.immutabledict/bool/None): the value + configuration: contains the schema_configuration.SchemaConfiguration that enables json schema validation keywords + like minItems, minLength etc + """ + if isinstance(arg, (tuple, validation.immutabledict)): + type_to_output_cls = cls.__get_type_to_output_cls() + if type_to_output_cls is not None: + for output_cls in type_to_output_cls.values(): + if isinstance(arg, output_cls): + # U + T use case, don't run validations twice + return arg + + from_server = False + validated_path_to_schemas: typing.Dict[ + typing.Tuple[typing.Union[str, int], ...], + typing.Set[typing.Union[str, int, float, bool, None, validation.immutabledict, tuple]] + ] = {} + path_to_type: typing.Dict[typing.Tuple[typing.Union[str, int], ...], type] = {} + cast_arg = cast_to_allowed_types( + arg, from_server, validated_path_to_schemas, ('args[0]',), path_to_type) + validation_metadata = validation.ValidationMetadata( + path_to_item=('args[0]',), + configuration=configuration or schema_configuration.SchemaConfiguration(), + validated_path_to_schemas=validation.immutabledict(validated_path_to_schemas) + ) + path_to_schemas = cls.__get_path_to_schemas(cast_arg, validation_metadata, path_to_type) + return cls._get_new_instance_without_conversion( + cast_arg, + validation_metadata.path_to_item, + path_to_schemas, + ) + + @classmethod + def __get_type_to_output_cls(cls) -> typing.Optional[typing.Mapping[type, type]]: + type_to_output_cls = getattr(cls(), 'type_to_output_cls', None) + type_to_output_cls = typing.cast(typing.Optional[typing.Mapping[type, type]], type_to_output_cls) + return type_to_output_cls + + +def get_class( + item_cls: typing.Union[types.FunctionType, staticmethod, typing.Type[Schema]], + local_namespace: typing.Optional[dict] = None +) -> typing.Type[Schema]: + if isinstance(item_cls, typing._GenericAlias): # type: ignore + # petstore_api.schemas.StrSchema[~U] -> petstore_api.schemas.StrSchema + origin_cls = typing.get_origin(item_cls) + if origin_cls is None: + raise ValueError('origin class must not be None') + return origin_cls + elif isinstance(item_cls, types.FunctionType): + # referenced schema + return item_cls() + elif isinstance(item_cls, staticmethod): + # referenced schema + return item_cls.__func__() + elif isinstance(item_cls, type): + return item_cls + elif isinstance(item_cls, typing.ForwardRef): + return item_cls._evaluate(None, local_namespace) + raise ValueError('invalid class value passed in') + + +@dataclasses.dataclass(frozen=True) +class AnyTypeSchema(Schema[T, U]): + # Python representation of a schema defined as true or {} + + @typing.overload + @classmethod + def validate( + cls, + arg: None, + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> None: ... + + @typing.overload + @classmethod + def validate( + cls, + arg: typing.Literal[True], + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> typing.Literal[True]: ... + + @typing.overload + @classmethod + def validate( + cls, + arg: typing.Literal[False], + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> typing.Literal[False]: ... + + @typing.overload + @classmethod + def validate( + cls, + arg: bool, + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> bool: ... + + @typing.overload + @classmethod + def validate( + cls, + arg: int, + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> int: ... + + @typing.overload + @classmethod + def validate( + cls, + arg: float, + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> float: ... + + @typing.overload + @classmethod + def validate( + cls, + arg: typing.Union[datetime.date, datetime.datetime, uuid.UUID], + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> str: ... + + @typing.overload + @classmethod + def validate( + cls, + arg: typing.Sequence[INPUT_TYPES_ALL], # also covers str, tuple, list, bytes + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> U: ... + + @typing.overload + @classmethod + def validate( + cls, + arg: U, + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> U: ... + + @typing.overload + @classmethod + def validate( + cls, + arg: typing.Union[ + typing.Mapping[str, INPUT_TYPES_ALL], + T + ], + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> T: ... + + @typing.overload + @classmethod + def validate( + cls, + arg: typing.Union[io.FileIO, io.BufferedReader], + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> FileIO: ... + + @classmethod + def validate( + cls, + arg, + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None, + ): + return cls.validate_base( + arg, + configuration=configuration + ) + +class UnsetAnyTypeSchema(AnyTypeSchema[T, U]): + # Used when additionalProperties/items was not explicitly defined and a defining schema is needed + pass + +INPUT_TYPES_NOT_STR_BYTES_FILE = typing.Union[ + dict, + validation.immutabledict, + list, + tuple, + float, + int, + datetime.date, + datetime.datetime, + uuid.UUID, + bool, + None, +] + +INPUT_TYPES_ALL = typing.Union[ + dict, + validation.immutabledict, + typing.Mapping[str, object], # for TypedDict + list, + tuple, + float, + int, + str, + datetime.date, + datetime.datetime, + uuid.UUID, + bool, + None, + bytes, + io.FileIO, + io.BufferedReader, + FileIO +] \ No newline at end of file diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/schemas/schemas.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/schemas/schemas.py new file mode 100644 index 00000000000..c9eeefa6e83 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/schemas/schemas.py @@ -0,0 +1,375 @@ +# coding: utf-8 + +""" + Example + No description provided (generated by Openapi JSON Schema Generator https://github.com/openapi-json-schema-tools/openapi-json-schema-generator) # noqa: E501 + The version of the OpenAPI document: 1.0.0 + Generated by: https://github.com/openapi-json-schema-tools/openapi-json-schema-generator +""" + +from __future__ import annotations +import datetime +import dataclasses +import io +import typing +import uuid + +import typing_extensions + +from json_schema_api.configurations import schema_configuration + +from . import schema, validation + + +@dataclasses.dataclass(frozen=True) +class ListSchema(schema.Schema[validation.immutabledict, tuple]): + types: typing.FrozenSet[typing.Type] = frozenset({tuple}) + + @typing.overload + @classmethod + def validate( + cls, + arg: typing.Union[ + typing.List[schema.INPUT_TYPES_ALL], + schema.U + ], + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> schema.U: ... + + @typing.overload + @classmethod + def validate( + cls, + arg: typing.Union[ + typing.Tuple[schema.INPUT_TYPES_ALL, ...], + schema.U + ], + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> schema.U: ... + + @classmethod + def validate( + cls, + arg, + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ): + return super().validate_base(arg, configuration=configuration) + + +@dataclasses.dataclass(frozen=True) +class NoneSchema(schema.Schema): + types: typing.FrozenSet[typing.Type] = frozenset({type(None)}) + + @classmethod + def validate( + cls, + arg: None, + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> None: + return super().validate_base(arg, configuration=configuration) + + +@dataclasses.dataclass(frozen=True) +class NumberSchema(schema.Schema): + """ + This is used for type: number with no format + Both integers AND floats are accepted + """ + types: typing.FrozenSet[typing.Type] = frozenset({float, int}) + + @typing.overload + @classmethod + def validate( + cls, + arg: int, + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> int: ... + + @typing.overload + @classmethod + def validate( + cls, + arg: float, + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> float: ... + + @classmethod + def validate( + cls, + arg, + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ): + return super().validate_base(arg, configuration=configuration) + + +@dataclasses.dataclass(frozen=True) +class IntSchema(NumberSchema): + types: typing.FrozenSet[typing.Type] = frozenset({int, float}) + format: str = 'int' + + +@dataclasses.dataclass(frozen=True) +class Int32Schema(IntSchema): + types: typing.FrozenSet[typing.Type] = frozenset({int, float}) + format: str = 'int32' + + +@dataclasses.dataclass(frozen=True) +class Int64Schema(IntSchema): + types: typing.FrozenSet[typing.Type] = frozenset({int, float}) + format: str = 'int64' + + +@dataclasses.dataclass(frozen=True) +class Float32Schema(NumberSchema): + types: typing.FrozenSet[typing.Type] = frozenset({float}) + format: str = 'float' + + +@dataclasses.dataclass(frozen=True) +class Float64Schema(NumberSchema): + types: typing.FrozenSet[typing.Type] = frozenset({float}) + format: str = 'double' + + +@dataclasses.dataclass(frozen=True) +class StrSchema(schema.Schema[validation.immutabledict, str]): + """ + date + datetime string types must inherit from this class + That is because one can validate a str payload as both: + - type: string (format unset) + - type: string, format: date + """ + types: typing.FrozenSet[typing.Type] = frozenset({str}) + + @classmethod + def validate( + cls, + arg: str, + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> str: + return super().validate_base(arg, configuration=configuration) + + +@dataclasses.dataclass(frozen=True) +class UUIDSchema(schema.Schema): + types: typing.FrozenSet[typing.Type] = frozenset({str}) + format: str = 'uuid' + + @classmethod + def validate( + cls, + arg: typing.Union[str, uuid.UUID], + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> str: + return super().validate_base(arg, configuration=configuration) + + +@dataclasses.dataclass(frozen=True) +class DateSchema(schema.Schema): + types: typing.FrozenSet[typing.Type] = frozenset({str}) + format: str = 'date' + + @classmethod + def validate( + cls, + arg: typing.Union[str, datetime.date], + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> str: + return super().validate_base(arg, configuration=configuration) + + +@dataclasses.dataclass(frozen=True) +class DateTimeSchema(schema.Schema): + types: typing.FrozenSet[typing.Type] = frozenset({str}) + format: str = 'date-time' + + @classmethod + def validate( + cls, + arg: typing.Union[str, datetime.datetime], + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> str: + return super().validate_base(arg, configuration=configuration) + + +@dataclasses.dataclass(frozen=True) +class DecimalSchema(schema.Schema): + types: typing.FrozenSet[typing.Type] = frozenset({str}) + format: str = 'number' + + @classmethod + def validate( + cls, + arg: str, + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> str: + """ + Note: Decimals may not be passed in because cast_to_allowed_types is only invoked once for payloads + which can be simple (str) or complex (dicts or lists with nested values) + Because casting is only done once and recursively casts all values prior to validation then for a potential + client side Decimal input if Decimal was accepted as an input in DecimalSchema then one would not know + if one was using it for a StrSchema (where it should be cast to str) or one is using it for NumberSchema + where it should stay as Decimal. + """ + return super().validate_base(arg, configuration=configuration) + + +@dataclasses.dataclass(frozen=True) +class BytesSchema(schema.Schema[validation.immutabledict, bytes]): + """ + this class will subclass bytes and is immutable + """ + types: typing.FrozenSet[typing.Type] = frozenset({bytes}) + + @classmethod + def validate( + cls, + arg: bytes, + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> bytes: + return cls.validate_base(arg) + + +@dataclasses.dataclass(frozen=True) +class FileSchema(schema.Schema): + """ + This class is NOT immutable + Dynamic classes are built using it for example when AnyType allows in binary data + Al other schema classes ARE immutable + If one wanted to make this immutable one could make this a DictSchema with required properties: + - data = BytesSchema (which would be an immutable bytes based schema) + - file_name = StrSchema + and cast_to_allowed_types would convert bytes and file instances into dicts containing data + file_name + The downside would be that data would be stored in memory which one may not want to do for very large files + + The developer is responsible for closing this file and deleting it + + This class was kept as mutable: + - to allow file reading and writing to disk + - to be able to preserve file name info + """ + types: typing.FrozenSet[typing.Type] = frozenset({schema.FileIO}) + + @classmethod + def validate( + cls, + arg: typing.Union[io.FileIO, io.BufferedReader], + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> schema.FileIO: + return cls.validate_base(arg) + + +@dataclasses.dataclass(frozen=True) +class BinarySchema(schema.Schema[validation.immutabledict, bytes]): + types: typing.FrozenSet[typing.Type] = frozenset({schema.FileIO, bytes}) + format: str = 'binary' + + one_of: typing.Tuple[typing.Type[schema.Schema], ...] = ( + BytesSchema, + FileSchema, + ) + + @classmethod + def validate( + cls, + arg: typing.Union[io.FileIO, io.BufferedReader, bytes], + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> typing.Union[schema.FileIO, bytes]: + return cls.validate_base(arg) + + +@dataclasses.dataclass(frozen=True) +class BoolSchema(schema.Schema): + types: typing.FrozenSet[typing.Type] = frozenset({schema.Bool}) + + @typing.overload + @classmethod + def validate( + cls, + arg: typing.Literal[True], + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> typing.Literal[True]: ... + + @typing.overload + @classmethod + def validate( + cls, + arg: typing.Literal[False], + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> typing.Literal[False]: ... + + @typing.overload + @classmethod + def validate( + cls, + arg: bool, + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> bool: ... + + @classmethod + def validate( + cls, + arg, + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ): + return super().validate_base(arg, configuration=configuration) + + +@dataclasses.dataclass(frozen=True) +class NotAnyTypeSchema(schema.AnyTypeSchema): + """ + Python representation of a schema defined as false or {'not': {}} + Does not allow inputs in of AnyType + Note: validations on this class are never run because the code knows that no inputs will ever validate + """ + not_: typing.Type[schema.Schema] = schema.AnyTypeSchema + + @classmethod + def validate( + cls, + arg, + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None, + ): + return super().validate_base(arg, configuration=configuration) + +OUTPUT_BASE_TYPES = typing.Union[ + validation.immutabledict[str, 'OUTPUT_BASE_TYPES'], + str, + int, + float, + bool, + schema.none_type_, + typing.Tuple['OUTPUT_BASE_TYPES', ...], + bytes, + schema.FileIO +] + + +@dataclasses.dataclass(frozen=True) +class DictSchema(schema.Schema[schema.validation.immutabledict[str, OUTPUT_BASE_TYPES], tuple]): + types: typing.FrozenSet[typing.Type] = frozenset({validation.immutabledict}) + + @typing.overload + @classmethod + def validate( + cls, + arg: schema.validation.immutabledict[str, OUTPUT_BASE_TYPES], + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> schema.validation.immutabledict[str, OUTPUT_BASE_TYPES]: ... + + @typing.overload + @classmethod + def validate( + cls, + arg: typing.Mapping[str, schema.INPUT_TYPES_ALL], + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> schema.validation.immutabledict[str, OUTPUT_BASE_TYPES]: ... + + @classmethod + def validate( + cls, + arg, + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None, + ) -> schema.validation.immutabledict[str, OUTPUT_BASE_TYPES]: + return super().validate_base(arg, configuration=configuration) diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/schemas/validation.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/schemas/validation.py new file mode 100644 index 00000000000..264399e15ef --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/schemas/validation.py @@ -0,0 +1,986 @@ +# coding: utf-8 + +""" + Example + No description provided (generated by Openapi JSON Schema Generator https://github.com/openapi-json-schema-tools/openapi-json-schema-generator) # noqa: E501 + The version of the OpenAPI document: 1.0.0 + Generated by: https://github.com/openapi-json-schema-tools/openapi-json-schema-generator +""" + +from __future__ import annotations +import collections +import dataclasses +import decimal +import re +import sys +import types +import typing +import uuid + +import typing_extensions + +from json_schema_api import exceptions +from json_schema_api.configurations import schema_configuration + +from . import format, original_immutabledict + +_K = typing.TypeVar('_K') +_V = typing.TypeVar('_V', covariant=True) + + +class immutabledict(typing.Generic[_K, _V], original_immutabledict.immutabledict[_K, _V]): + # this class layer needed to not show init signature when making new instances + pass + + +@dataclasses.dataclass +class ValidationMetadata: + """ + A class storing metadata that is needed to validate OpenApi Schema payloads + """ + path_to_item: typing.Tuple[typing.Union[str, int], ...] + configuration: schema_configuration.SchemaConfiguration + validated_path_to_schemas: typing.Mapping[ + typing.Tuple[typing.Union[str, int], ...], + typing.Mapping[type, None] + ] = dataclasses.field(default_factory=dict) + seen_classes: typing.FrozenSet[type] = frozenset() + + def validation_ran_earlier(self, cls: type) -> bool: + validated_schemas: typing.Union[typing.Mapping[type, None], None] = self.validated_path_to_schemas.get(self.path_to_item) + if validated_schemas and cls in validated_schemas: + return True + if cls in self.seen_classes: + return True + return False + +def _raise_validation_error_message(value, constraint_msg, constraint_value, path_to_item, additional_txt=""): + raise exceptions.ApiValueError( + "Invalid value `{value}`, {constraint_msg} `{constraint_value}`{additional_txt} at {path_to_item}".format( + value=value, + constraint_msg=constraint_msg, + constraint_value=constraint_value, + additional_txt=additional_txt, + path_to_item=path_to_item, + ) + ) + + +class SchemaValidator: + __excluded_cls_properties = { + '__module__', + '__dict__', + '__weakref__', + '__doc__', + '__annotations__', + 'default', # excluded because it has no impact on validation + 'type_to_output_cls', # used to pluck the output class for instantiation + } + + @classmethod + def _validate( + cls, + arg, + validation_metadata: ValidationMetadata, + ) -> PathToSchemasType: + """ + SchemaValidator validate + All keyword validation except for type checking was done in calling stack frames + If those validations passed, the validated classes are collected in path_to_schemas + """ + cls_schema = cls() + json_schema_data = { + k: v + for k, v in vars(cls_schema).items() + if k not in cls.__excluded_cls_properties + and k + not in validation_metadata.configuration.disabled_json_schema_python_keywords + } + path_to_schemas: PathToSchemasType = {} + for keyword, val in json_schema_data.items(): + validator = json_schema_keyword_to_validator[keyword] + + other_path_to_schemas = validator( + arg, + val, + cls, + validation_metadata, + ) + if other_path_to_schemas: + update(path_to_schemas, other_path_to_schemas) + + base_class = type(arg) + if validation_metadata.path_to_item not in path_to_schemas: + path_to_schemas[validation_metadata.path_to_item] = dict() + path_to_schemas[validation_metadata.path_to_item][base_class] = None + path_to_schemas[validation_metadata.path_to_item][cls] = None + return path_to_schemas + +PathToSchemasType = typing.Dict[ + typing.Tuple[typing.Union[str, int], ...], + typing.Dict[ + typing.Union[ + typing.Type[SchemaValidator], + typing.Type[str], + typing.Type[int], + typing.Type[float], + typing.Type[bool], + typing.Type[None], + typing.Type[immutabledict], + typing.Type[tuple] + ], + None + ] +] + +def _get_class( + item_cls: typing.Union[types.FunctionType, staticmethod, typing.Type[SchemaValidator]], + local_namespace: typing.Optional[dict] = None +) -> typing.Type[SchemaValidator]: + if isinstance(item_cls, typing._GenericAlias): # type: ignore + # petstore_api.schemas.StrSchema[~U] -> petstore_api.schemas.StrSchema + origin_cls = typing.get_origin(item_cls) + if origin_cls is None: + raise ValueError('origin class must not be None') + return origin_cls + elif isinstance(item_cls, types.FunctionType): + # referenced schema + return item_cls() + elif isinstance(item_cls, staticmethod): + # referenced schema + return item_cls.__func__() + elif isinstance(item_cls, type): + return item_cls + elif isinstance(item_cls, typing.ForwardRef): + return item_cls._evaluate(None, local_namespace) + raise ValueError('invalid class value passed in') + + +def update(d: dict, u: dict): + """ + Adds u to d + Where each dict is collections.defaultdict(dict) + """ + if not u: + return d + for k, v in u.items(): + if k not in d: + d[k] = v + else: + d[k].update(v) + + +def add_deeper_validated_schemas(validation_metadata: ValidationMetadata, path_to_schemas: dict): + # this is called if validation_ran_earlier and current and deeper locations need to be added + current_path_to_item = validation_metadata.path_to_item + other_path_to_schemas = {} + for path_to_item, schemas in validation_metadata.validated_path_to_schemas.items(): + if len(path_to_item) < len(current_path_to_item): + continue + path_begins_with_current_path = path_to_item[:len(current_path_to_item)] == current_path_to_item + if path_begins_with_current_path: + other_path_to_schemas[path_to_item] = schemas + update(path_to_schemas, other_path_to_schemas) + + +def __get_valid_classes_phrase(input_classes): + """Returns a string phrase describing what types are allowed""" + all_classes = list(input_classes) + all_classes = sorted(all_classes, key=lambda cls: cls.__name__) + all_class_names = [cls.__name__ for cls in all_classes] + if len(all_class_names) == 1: + return "is {0}".format(all_class_names[0]) + return "is one of [{0}]".format(", ".join(all_class_names)) + + +def __type_error_message( + var_value=None, var_name=None, valid_classes=None, key_type=None +): + """ + Keyword Args: + var_value (any): the variable which has the type_error + var_name (str): the name of the variable which has the typ error + valid_classes (tuple): the accepted classes for current_item's + value + key_type (bool): False if our value is a value in a dict + True if it is a key in a dict + False if our item is an item in a tuple + """ + key_or_value = "value" + if key_type: + key_or_value = "key" + valid_classes_phrase = __get_valid_classes_phrase(valid_classes) + msg = "Invalid type. Required {0} type {1} and " "passed type was {2}".format( + key_or_value, + valid_classes_phrase, + type(var_value).__name__, + ) + return msg + + +def __get_type_error(var_value, path_to_item, valid_classes, key_type=False): + error_msg = __type_error_message( + var_name=path_to_item[-1], + var_value=var_value, + valid_classes=valid_classes, + key_type=key_type, + ) + return exceptions.ApiTypeError( + error_msg, + path_to_item=path_to_item, + valid_classes=valid_classes, + key_type=key_type, + ) + + +@dataclasses.dataclass(frozen=True) +class PatternInfo: + pattern: str + flags: typing.Optional[re.RegexFlag] = None + + +def validate_types( + arg: typing.Any, + allowed_types: typing.Set[typing.Type], + cls: typing.Type, + validation_metadata: ValidationMetadata, +) -> None: + if type(arg) not in allowed_types: + raise __get_type_error( + arg, + validation_metadata.path_to_item, + allowed_types, + key_type=False, + ) + return None + +def validate_enum( + arg: typing.Any, + enum_value_to_name: typing.Dict[typing.Any, str], + cls: typing.Type, + validation_metadata: ValidationMetadata, +) -> None: + if arg not in enum_value_to_name: + raise exceptions.ApiValueError("Invalid value {} passed in to {}, allowed_values={}".format(arg, cls, enum_value_to_name.keys())) + return None + + +def validate_unique_items( + arg: typing.Any, + unique_items_value: bool, + cls: typing.Type, + validation_metadata: ValidationMetadata, +) -> None: + if not unique_items_value or not isinstance(arg, tuple): + return None + if len(arg) == len(set(arg)): + return None + _raise_validation_error_message( + value=arg, + constraint_msg="duplicate items were found, and the tuple must not contain duplicates because", + constraint_value='unique_items==True', + path_to_item=validation_metadata.path_to_item + ) + + +def validate_min_items( + arg: typing.Any, + min_items: int, + cls: typing.Type, + validation_metadata: ValidationMetadata, +) -> None: + if not isinstance(arg, tuple): + return None + if len(arg) < min_items: + _raise_validation_error_message( + value=arg, + constraint_msg="number of items must be greater than or equal to", + constraint_value=min_items, + path_to_item=validation_metadata.path_to_item + ) + return None + + +def validate_max_items( + arg: typing.Any, + max_items: int, + cls: typing.Type, + validation_metadata: ValidationMetadata, +) -> None: + if not isinstance(arg, tuple): + return None + if len(arg) > max_items: + _raise_validation_error_message( + value=arg, + constraint_msg="number of items must be less than or equal to", + constraint_value=max_items, + path_to_item=validation_metadata.path_to_item + ) + return None + + +def validate_min_properties( + arg: typing.Any, + min_properties: int, + cls: typing.Type, + validation_metadata: ValidationMetadata, +) -> None: + if not isinstance(arg, immutabledict): + return None + if len(arg) < min_properties: + _raise_validation_error_message( + value=arg, + constraint_msg="number of properties must be greater than or equal to", + constraint_value=min_properties, + path_to_item=validation_metadata.path_to_item + ) + return None + + +def validate_max_properties( + arg: typing.Any, + max_properties: int, + cls: typing.Type, + validation_metadata: ValidationMetadata, +) -> None: + if not isinstance(arg, immutabledict): + return None + if len(arg) > max_properties: + _raise_validation_error_message( + value=arg, + constraint_msg="number of properties must be less than or equal to", + constraint_value=max_properties, + path_to_item=validation_metadata.path_to_item + ) + return None + + +def validate_min_length( + arg: typing.Any, + min_length: int, + cls: typing.Type, + validation_metadata: ValidationMetadata, +) -> None: + if not isinstance(arg, str): + return None + if len(arg) < min_length: + _raise_validation_error_message( + value=arg, + constraint_msg="length must be greater than or equal to", + constraint_value=min_length, + path_to_item=validation_metadata.path_to_item + ) + return None + + +def validate_max_length( + arg: typing.Any, + max_length: int, + cls: typing.Type, + validation_metadata: ValidationMetadata, +) -> None: + if not isinstance(arg, str): + return None + if len(arg) > max_length: + _raise_validation_error_message( + value=arg, + constraint_msg="length must be less than or equal to", + constraint_value=max_length, + path_to_item=validation_metadata.path_to_item + ) + return None + + +def validate_inclusive_minimum( + arg: typing.Any, + inclusive_minimum: typing.Union[int, float], + cls: typing.Type, + validation_metadata: ValidationMetadata, +) -> None: + if not isinstance(arg, (int, float)): + return None + if arg < inclusive_minimum: + _raise_validation_error_message( + value=arg, + constraint_msg="must be a value greater than or equal to", + constraint_value=inclusive_minimum, + path_to_item=validation_metadata.path_to_item + ) + return None + + +def validate_exclusive_minimum( + arg: typing.Any, + exclusive_minimum: typing.Union[int, float], + cls: typing.Type, + validation_metadata: ValidationMetadata, +) -> None: + if not isinstance(arg, (int, float)): + return None + if arg <= exclusive_minimum: + _raise_validation_error_message( + value=arg, + constraint_msg="must be a value greater than", + constraint_value=exclusive_minimum, + path_to_item=validation_metadata.path_to_item + ) + return None + + +def validate_inclusive_maximum( + arg: typing.Any, + inclusive_maximum: typing.Union[int, float], + cls: typing.Type, + validation_metadata: ValidationMetadata, +) -> None: + if not isinstance(arg, (int, float)): + return None + if arg > inclusive_maximum: + _raise_validation_error_message( + value=arg, + constraint_msg="must be a value less than or equal to", + constraint_value=inclusive_maximum, + path_to_item=validation_metadata.path_to_item + ) + return None + + +def validate_exclusive_maximum( + arg: typing.Any, + exclusive_maximum: typing.Union[int, float], + cls: typing.Type, + validation_metadata: ValidationMetadata, +) -> None: + if not isinstance(arg, (int, float)): + return None + if arg >= exclusive_maximum: + _raise_validation_error_message( + value=arg, + constraint_msg="must be a value less than", + constraint_value=exclusive_maximum, + path_to_item=validation_metadata.path_to_item + ) + return None + +def validate_multiple_of( + arg: typing.Any, + multiple_of: typing.Union[int, float], + cls: typing.Type, + validation_metadata: ValidationMetadata, +) -> None: + if not isinstance(arg, (int, float)): + return None + if (not (float(arg) / multiple_of).is_integer()): + # Note 'multipleOf' will be as good as the floating point arithmetic. + _raise_validation_error_message( + value=arg, + constraint_msg="value must be a multiple of", + constraint_value=multiple_of, + path_to_item=validation_metadata.path_to_item + ) + return None + + +def validate_pattern( + arg: typing.Any, + pattern_info: PatternInfo, + cls: typing.Type, + validation_metadata: ValidationMetadata, +) -> None: + if not isinstance(arg, str): + return None + flags = pattern_info.flags if pattern_info.flags is not None else 0 + if not re.search(pattern_info.pattern, arg, flags=flags): + if flags != 0: + # Don't print the regex flags if the flags are not + # specified in the OAS document. + _raise_validation_error_message( + value=arg, + constraint_msg="must match regular expression", + constraint_value=pattern_info.pattern, + path_to_item=validation_metadata.path_to_item, + additional_txt=" with flags=`{}`".format(flags) + ) + _raise_validation_error_message( + value=arg, + constraint_msg="must match regular expression", + constraint_value=pattern_info.pattern, + path_to_item=validation_metadata.path_to_item + ) + return None + + +__int32_inclusive_minimum = -2147483648 +__int32_inclusive_maximum = 2147483647 +__int64_inclusive_minimum = -9223372036854775808 +__int64_inclusive_maximum = 9223372036854775807 +__float_inclusive_minimum = -3.4028234663852886e+38 +__float_inclusive_maximum = 3.4028234663852886e+38 +__double_inclusive_minimum = -1.7976931348623157E+308 +__double_inclusive_maximum = 1.7976931348623157E+308 + +def __validate_numeric_format( + arg: typing.Union[int, float], + format_value: str, + validation_metadata: ValidationMetadata +) -> None: + if format_value[:3] == 'int': + # there is a json schema test where 1.0 validates as an integer + if arg != int(arg): + raise exceptions.ApiValueError( + "Invalid non-integer value '{}' for type {} at {}".format( + arg, format, validation_metadata.path_to_item + ) + ) + if format_value == 'int32': + if not __int32_inclusive_minimum <= arg <= __int32_inclusive_maximum: + raise exceptions.ApiValueError( + "Invalid value '{}' for type int32 at {}".format(arg, validation_metadata.path_to_item) + ) + return None + elif format_value == 'int64': + if not __int64_inclusive_minimum <= arg <= __int64_inclusive_maximum: + raise exceptions.ApiValueError( + "Invalid value '{}' for type int64 at {}".format(arg, validation_metadata.path_to_item) + ) + return None + return None + elif format_value in {'float', 'double'}: + if format_value == 'float': + if not __float_inclusive_minimum <= arg <= __float_inclusive_maximum: + raise exceptions.ApiValueError( + "Invalid value '{}' for type float at {}".format(arg, validation_metadata.path_to_item) + ) + return None + # double + if not __double_inclusive_minimum <= arg <= __double_inclusive_maximum: + raise exceptions.ApiValueError( + "Invalid value '{}' for type double at {}".format(arg, validation_metadata.path_to_item) + ) + return None + return None + + +def __validate_string_format( + arg: str, + format_value: str, + validation_metadata: ValidationMetadata +) -> None: + if format_value == 'uuid': + try: + uuid.UUID(arg) + return None + except ValueError: + raise exceptions.ApiValueError( + "Invalid value '{}' for type UUID at {}".format(arg, validation_metadata.path_to_item) + ) + elif format_value == 'number': + try: + decimal.Decimal(arg) + return None + except decimal.InvalidOperation: + raise exceptions.ApiValueError( + "Value cannot be converted to a decimal. " + "Invalid value '{}' for type decimal at {}".format(arg, validation_metadata.path_to_item) + ) + elif format_value == 'date': + try: + format.DEFAULT_ISOPARSER.parse_isodate_str(arg) + return None + except ValueError: + raise exceptions.ApiValueError( + "Value does not conform to the required ISO-8601 date format. " + "Invalid value '{}' for type date at {}".format(arg, validation_metadata.path_to_item) + ) + elif format_value == 'date-time': + try: + format.DEFAULT_ISOPARSER.parse_isodatetime(arg) + return None + except ValueError: + raise exceptions.ApiValueError( + "Value does not conform to the required ISO-8601 datetime format. " + "Invalid value '{}' for type datetime at {}".format(arg, validation_metadata.path_to_item) + ) + return None + + +def validate_format( + arg: typing.Union[str, int, float], + format_value: str, + cls: typing.Type, + validation_metadata: ValidationMetadata, +) -> None: + # formats work for strings + numbers + if isinstance(arg, (int, float)): + return __validate_numeric_format( + arg, + format_value, + validation_metadata + ) + elif isinstance(arg, str): + return __validate_string_format( + arg, + format_value, + validation_metadata + ) + return None + + +def validate_required( + arg: typing.Any, + required: typing.Set[str], + cls: typing.Type, + validation_metadata: ValidationMetadata, +) -> None: + if not isinstance(arg, immutabledict): + return None + missing_req_args = required - arg.keys() + if missing_req_args: + missing_required_arguments = list(missing_req_args) + missing_required_arguments.sort() + raise exceptions.ApiTypeError( + "{} is missing {} required argument{}: {}".format( + cls.__name__, + len(missing_required_arguments), + "s" if len(missing_required_arguments) > 1 else "", + missing_required_arguments + ) + ) + return None + + +def validate_items( + arg: typing.Any, + item_cls: typing.Type[SchemaValidator], + cls: typing.Type, + validation_metadata: ValidationMetadata, +) -> typing.Optional[PathToSchemasType]: + if not isinstance(arg, tuple): + return None + item_cls = _get_class(item_cls) + path_to_schemas: PathToSchemasType = {} + for i, value in enumerate(arg): + item_validation_metadata = ValidationMetadata( + path_to_item=validation_metadata.path_to_item+(i,), + configuration=validation_metadata.configuration, + validated_path_to_schemas=validation_metadata.validated_path_to_schemas + ) + if item_validation_metadata.validation_ran_earlier(item_cls): + add_deeper_validated_schemas(item_validation_metadata, path_to_schemas) + continue + other_path_to_schemas = item_cls._validate( + value, validation_metadata=item_validation_metadata) + update(path_to_schemas, other_path_to_schemas) + return path_to_schemas + + +def validate_properties( + arg: typing.Any, + properties: typing.Mapping[str, typing.Type[SchemaValidator]], + cls: typing.Type, + validation_metadata: ValidationMetadata, +) -> typing.Optional[PathToSchemasType]: + if not isinstance(arg, immutabledict): + return None + path_to_schemas: PathToSchemasType = {} + present_properties = {k: v for k, v, in arg.items() if k in properties} + module_namespace = vars(sys.modules[cls.__module__]) + for property_name, value in present_properties.items(): + path_to_item = validation_metadata.path_to_item + (property_name,) + schema = properties[property_name] + schema = _get_class(schema, module_namespace) + arg_validation_metadata = ValidationMetadata( + path_to_item=path_to_item, + configuration=validation_metadata.configuration, + validated_path_to_schemas=validation_metadata.validated_path_to_schemas + ) + if arg_validation_metadata.validation_ran_earlier(schema): + add_deeper_validated_schemas(arg_validation_metadata, path_to_schemas) + continue + other_path_to_schemas = schema._validate(value, validation_metadata=arg_validation_metadata) + update(path_to_schemas, other_path_to_schemas) + return path_to_schemas + + +def validate_additional_properties( + arg: typing.Any, + additional_properties_cls: typing.Type[SchemaValidator], + cls: typing.Type, + validation_metadata: ValidationMetadata, +) -> typing.Optional[PathToSchemasType]: + if not isinstance(arg, immutabledict): + return None + schema = _get_class(additional_properties_cls) + path_to_schemas: PathToSchemasType = {} + cls_schema = cls() + properties = cls_schema.properties if hasattr(cls_schema, 'properties') else {} + present_additional_properties = {k: v for k, v, in arg.items() if k not in properties} + for property_name, value in present_additional_properties.items(): + path_to_item = validation_metadata.path_to_item + (property_name,) + arg_validation_metadata = ValidationMetadata( + path_to_item=path_to_item, + configuration=validation_metadata.configuration, + validated_path_to_schemas=validation_metadata.validated_path_to_schemas + ) + if arg_validation_metadata.validation_ran_earlier(schema): + add_deeper_validated_schemas(arg_validation_metadata, path_to_schemas) + continue + other_path_to_schemas = schema._validate(value, validation_metadata=arg_validation_metadata) + update(path_to_schemas, other_path_to_schemas) + return path_to_schemas + + +def validate_one_of( + arg: typing.Any, + classes: typing.Tuple[typing.Type[SchemaValidator], ...], + cls: typing.Type, + validation_metadata: ValidationMetadata, +) -> PathToSchemasType: + oneof_classes = [] + path_to_schemas: PathToSchemasType = collections.defaultdict(dict) + for schema in classes: + schema = _get_class(schema) + if schema in path_to_schemas[validation_metadata.path_to_item]: + oneof_classes.append(schema) + continue + if schema is cls: + """ + optimistically assume that cls schema will pass validation + do not invoke _validate on it because that is recursive + """ + oneof_classes.append(schema) + continue + if validation_metadata.validation_ran_earlier(schema): + oneof_classes.append(schema) + add_deeper_validated_schemas(validation_metadata, path_to_schemas) + continue + try: + path_to_schemas = schema._validate(arg, validation_metadata=validation_metadata) + except (exceptions.ApiValueError, exceptions.ApiTypeError) as ex: + # silence exceptions because the code needs to accumulate oneof_classes + continue + oneof_classes.append(schema) + if not oneof_classes: + raise exceptions.ApiValueError( + "Invalid inputs given to generate an instance of {}. None " + "of the oneOf schemas matched the input data.".format(cls) + ) + elif len(oneof_classes) > 1: + raise exceptions.ApiValueError( + "Invalid inputs given to generate an instance of {}. Multiple " + "oneOf schemas {} matched the inputs, but a max of one is allowed.".format(cls, oneof_classes) + ) + # exactly one class matches + return path_to_schemas + + +def validate_any_of( + arg: typing.Any, + classes: typing.Tuple[typing.Type[SchemaValidator], ...], + cls: typing.Type, + validation_metadata: ValidationMetadata, +) -> PathToSchemasType: + anyof_classes = [] + path_to_schemas: PathToSchemasType = collections.defaultdict(dict) + for schema in classes: + schema = _get_class(schema) + if schema is cls: + """ + optimistically assume that cls schema will pass validation + do not invoke _validate on it because that is recursive + """ + anyof_classes.append(schema) + continue + if validation_metadata.validation_ran_earlier(schema): + anyof_classes.append(schema) + add_deeper_validated_schemas(validation_metadata, path_to_schemas) + continue + + try: + other_path_to_schemas = schema._validate(arg, validation_metadata=validation_metadata) + except (exceptions.ApiValueError, exceptions.ApiTypeError) as ex: + # silence exceptions because the code needs to accumulate anyof_classes + continue + anyof_classes.append(schema) + update(path_to_schemas, other_path_to_schemas) + if not anyof_classes: + raise exceptions.ApiValueError( + "Invalid inputs given to generate an instance of {}. None " + "of the anyOf schemas matched the input data.".format(cls) + ) + return path_to_schemas + + +def validate_all_of( + arg: typing.Any, + classes: typing.Tuple[typing.Type[SchemaValidator], ...], + cls: typing.Type, + validation_metadata: ValidationMetadata, +) -> PathToSchemasType: + path_to_schemas: PathToSchemasType = collections.defaultdict(dict) + for schema in classes: + schema = _get_class(schema) + if schema is cls: + """ + optimistically assume that cls schema will pass validation + do not invoke _validate on it because that is recursive + """ + continue + if validation_metadata.validation_ran_earlier(schema): + add_deeper_validated_schemas(validation_metadata, path_to_schemas) + continue + other_path_to_schemas = schema._validate(arg, validation_metadata=validation_metadata) + update(path_to_schemas, other_path_to_schemas) + return path_to_schemas + + +def validate_not( + arg: typing.Any, + not_cls: typing.Type[SchemaValidator], + cls: typing.Type, + validation_metadata: ValidationMetadata, +) -> None: + not_schema = _get_class(not_cls) + other_path_to_schemas = None + not_exception = exceptions.ApiValueError( + "Invalid value '{}' was passed in to {}. Value is invalid because it is disallowed by {}".format( + arg, + cls.__name__, + not_schema.__name__, + ) + ) + if validation_metadata.validation_ran_earlier(not_schema): + raise not_exception + + try: + other_path_to_schemas = not_schema._validate(arg, validation_metadata=validation_metadata) + except (exceptions.ApiValueError, exceptions.ApiTypeError): + pass + if other_path_to_schemas: + raise not_exception + return None + + +def __ensure_discriminator_value_present( + disc_property_name: str, + validation_metadata: ValidationMetadata, + arg +): + if disc_property_name not in arg: + # The input data does not contain the discriminator property + raise exceptions.ApiValueError( + "Cannot deserialize input data due to missing discriminator. " + "The discriminator property '{}' is missing at path: {}".format(disc_property_name, validation_metadata.path_to_item) + ) + + +def __get_discriminated_class(cls, disc_property_name: str, disc_payload_value: str): + """ + Used in schemas with discriminators + """ + cls_schema = cls() + if not hasattr(cls_schema, 'discriminator'): + return None + disc = cls_schema.discriminator + if disc_property_name not in disc: + return None + discriminated_cls = disc[disc_property_name].get(disc_payload_value) + if discriminated_cls is not None: + return discriminated_cls + if not ( + hasattr(cls_schema, 'all_of') or + hasattr(cls_schema, 'one_of') or + hasattr(cls_schema, 'any_of') + ): + return None + # TODO stop traveling if a cycle is hit + if hasattr(cls_schema, 'all_of'): + for allof_cls in cls_schema.all_of: + discriminated_cls = __get_discriminated_class( + allof_cls, disc_property_name=disc_property_name, disc_payload_value=disc_payload_value) + if discriminated_cls is not None: + return discriminated_cls + if hasattr(cls_schema, 'one_of'): + for oneof_cls in cls_schema.one_of: + discriminated_cls = __get_discriminated_class( + oneof_cls, disc_property_name=disc_property_name, disc_payload_value=disc_payload_value) + if discriminated_cls is not None: + return discriminated_cls + if hasattr(cls_schema, 'any_of'): + for anyof_cls in cls_schema.any_of: + discriminated_cls = __get_discriminated_class( + anyof_cls, disc_property_name=disc_property_name, disc_payload_value=disc_payload_value) + if discriminated_cls is not None: + return discriminated_cls + return None + + +def validate_discriminator( + arg: typing.Any, + discriminator: typing.Mapping[str, typing.Mapping[str, typing.Type[SchemaValidator]]], + cls: typing.Type, + validation_metadata: ValidationMetadata, +) -> typing.Optional[PathToSchemasType]: + if not isinstance(arg, immutabledict): + return None + disc_prop_name = list(discriminator.keys())[0] + __ensure_discriminator_value_present(disc_prop_name, validation_metadata, arg) + discriminated_cls = __get_discriminated_class( + cls, disc_property_name=disc_prop_name, disc_payload_value=arg[disc_prop_name] + ) + if discriminated_cls is None: + raise exceptions.ApiValueError( + "Invalid discriminator value was passed in to {}.{} Only the values {} are allowed at {}".format( + cls.__name__, + disc_prop_name, + list(discriminator[disc_prop_name].keys()), + validation_metadata.path_to_item + (disc_prop_name,) + ) + ) + if discriminated_cls is cls: + """ + Optimistically assume that cls will pass validation + If the code invoked _validate on cls it would infinitely recurse + """ + return None + if validation_metadata.validation_ran_earlier(discriminated_cls): + path_to_schemas: PathToSchemasType = {} + add_deeper_validated_schemas(validation_metadata, path_to_schemas) + return path_to_schemas + updated_vm = ValidationMetadata( + path_to_item=validation_metadata.path_to_item, + configuration=validation_metadata.configuration, + seen_classes=validation_metadata.seen_classes | frozenset({cls}), + validated_path_to_schemas=validation_metadata.validated_path_to_schemas + ) + return discriminated_cls._validate(arg, validation_metadata=updated_vm) + + +validator_type = typing.Callable[[typing.Any, typing.Any, type, ValidationMetadata], typing.Optional[PathToSchemasType]] +json_schema_keyword_to_validator: typing.Mapping[str, validator_type] = { + 'types': validate_types, + 'enum_value_to_name': validate_enum, + 'unique_items': validate_unique_items, + 'min_items': validate_min_items, + 'max_items': validate_max_items, + 'min_properties': validate_min_properties, + 'max_properties': validate_max_properties, + 'min_length': validate_min_length, + 'max_length': validate_max_length, + 'inclusive_minimum': validate_inclusive_minimum, + 'exclusive_minimum': validate_exclusive_minimum, + 'inclusive_maximum': validate_inclusive_maximum, + 'exclusive_maximum': validate_exclusive_maximum, + 'multiple_of': validate_multiple_of, + 'pattern': validate_pattern, + 'format': validate_format, + 'required': validate_required, + 'items': validate_items, + 'properties': validate_properties, + 'additional_properties': validate_additional_properties, + 'one_of': validate_one_of, + 'any_of': validate_any_of, + 'all_of': validate_all_of, + 'not_': validate_not, + 'discriminator': validate_discriminator +} \ No newline at end of file diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/security_schemes.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/security_schemes.py new file mode 100644 index 00000000000..f9eb9b9cf36 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/security_schemes.py @@ -0,0 +1,227 @@ +# coding: utf-8 +""" + Example + No description provided (generated by Openapi JSON Schema Generator https://github.com/openapi-json-schema-tools/openapi-json-schema-generator) # noqa: E501 + The version of the OpenAPI document: 1.0.0 + Generated by: https://github.com/openapi-json-schema-tools/openapi-json-schema-generator +""" + +import abc +import base64 +import dataclasses +import enum +import typing +import typing_extensions + +from urllib3 import _collections + + +class SecuritySchemeType(enum.Enum): + API_KEY = 'apiKey' + HTTP = 'http' + MUTUAL_TLS = 'mutualTLS' + OAUTH_2 = 'oauth2' + OPENID_CONNECT = 'openIdConnect' + + +class ApiKeyInLocation(enum.Enum): + QUERY = 'query' + HEADER = 'header' + COOKIE = 'cookie' + + +class __SecuritySchemeBase(metaclass=abc.ABCMeta): + @abc.abstractmethod + def apply_auth( + self, + headers: _collections.HTTPHeaderDict, + resource_path: str, + method: str, + body: typing.Optional[typing.Union[str, bytes]], + query_params_suffix: typing.Optional[str], + scope_names: typing.Tuple[str, ...] = (), + ) -> None: + pass + + +@dataclasses.dataclass +class ApiKeySecurityScheme(__SecuritySchemeBase, abc.ABC): + api_key: str # this must be set by the developer + name: str = '' + in_location: ApiKeyInLocation = ApiKeyInLocation.QUERY + type: SecuritySchemeType = SecuritySchemeType.API_KEY + + def apply_auth( + self, + headers: _collections.HTTPHeaderDict, + resource_path: str, + method: str, + body: typing.Optional[typing.Union[str, bytes]], + query_params_suffix: typing.Optional[str], + scope_names: typing.Tuple[str, ...] = (), + ) -> None: + if self.in_location is ApiKeyInLocation.COOKIE: + headers.add('Cookie', self.api_key) + elif self.in_location is ApiKeyInLocation.HEADER: + headers.add(self.name, self.api_key) + elif self.in_location is ApiKeyInLocation.QUERY: + # todo add query handling + raise NotImplementedError("ApiKeySecurityScheme in query not yet implemented") + return + + +class HTTPSchemeType(enum.Enum): + BASIC = 'basic' + BEARER = 'bearer' + DIGEST = 'digest' + SIGNATURE = 'signature' # https://datatracker.ietf.org/doc/draft-cavage-http-signatures/ + + +@dataclasses.dataclass +class HTTPBasicSecurityScheme(__SecuritySchemeBase): + user_id: str # user name + password: str + scheme: HTTPSchemeType = HTTPSchemeType.BASIC + encoding: str = 'utf-8' + type: SecuritySchemeType = SecuritySchemeType.HTTP + """ + https://www.rfc-editor.org/rfc/rfc7617.html + """ + + def apply_auth( + self, + headers: _collections.HTTPHeaderDict, + resource_path: str, + method: str, + body: typing.Optional[typing.Union[str, bytes]], + query_params_suffix: typing.Optional[str], + scope_names: typing.Tuple[str, ...] = (), + ) -> None: + user_pass = f"{self.user_id}:{self.password}" + b64_user_pass = base64.b64encode(user_pass.encode(encoding=self.encoding)) + headers.add('Authorization', f"Basic {b64_user_pass.decode()}") + + +@dataclasses.dataclass +class HTTPBearerSecurityScheme(__SecuritySchemeBase): + access_token: str + bearer_format: typing.Optional[str] = None + scheme: HTTPSchemeType = HTTPSchemeType.BEARER + type: SecuritySchemeType = SecuritySchemeType.HTTP + + def apply_auth( + self, + headers: _collections.HTTPHeaderDict, + resource_path: str, + method: str, + body: typing.Optional[typing.Union[str, bytes]], + query_params_suffix: typing.Optional[str], + scope_names: typing.Tuple[str, ...] = (), + ) -> None: + headers.add('Authorization', f"Bearer {self.access_token}") + + +@dataclasses.dataclass +class HTTPDigestSecurityScheme(__SecuritySchemeBase): + scheme: HTTPSchemeType = HTTPSchemeType.DIGEST + type: SecuritySchemeType = SecuritySchemeType.HTTP + + def apply_auth( + self, + headers: _collections.HTTPHeaderDict, + resource_path: str, + method: str, + body: typing.Optional[typing.Union[str, bytes]], + query_params_suffix: typing.Optional[str], + scope_names: typing.Tuple[str, ...] = (), + ) -> None: + raise NotImplementedError("HTTPDigestSecurityScheme not yet implemented") + + +@dataclasses.dataclass +class MutualTLSSecurityScheme(__SecuritySchemeBase): + type: SecuritySchemeType = SecuritySchemeType.MUTUAL_TLS + + def apply_auth( + self, + headers: _collections.HTTPHeaderDict, + resource_path: str, + method: str, + body: typing.Optional[typing.Union[str, bytes]], + query_params_suffix: typing.Optional[str], + scope_names: typing.Tuple[str, ...] = (), + ) -> None: + raise NotImplementedError("MutualTLSSecurityScheme not yet implemented") + + +@dataclasses.dataclass +class ImplicitOAuthFlow: + authorization_url: str + scopes: typing.Dict[str, str] + refresh_url: typing.Optional[str] = None + + +@dataclasses.dataclass +class TokenUrlOauthFlow: + token_url: str + scopes: typing.Dict[str, str] + refresh_url: typing.Optional[str] = None + + +@dataclasses.dataclass +class AuthorizationCodeOauthFlow: + authorization_url: str + token_url: str + scopes: typing.Dict[str, str] + refresh_url: typing.Optional[str] = None + + +@dataclasses.dataclass +class OAuthFlows: + implicit: typing.Optional[ImplicitOAuthFlow] = None + password: typing.Optional[TokenUrlOauthFlow] = None + client_credentials: typing.Optional[TokenUrlOauthFlow] = None + authorization_code: typing.Optional[AuthorizationCodeOauthFlow] = None + + +class OAuth2SecurityScheme(__SecuritySchemeBase, abc.ABC): + flows: OAuthFlows + type: SecuritySchemeType = SecuritySchemeType.OAUTH_2 + + def apply_auth( + self, + headers: _collections.HTTPHeaderDict, + resource_path: str, + method: str, + body: typing.Optional[typing.Union[str, bytes]], + query_params_suffix: typing.Optional[str], + scope_names: typing.Tuple[str, ...] = (), + ) -> None: + raise NotImplementedError("OAuth2SecurityScheme not yet implemented") + + +class OpenIdConnectSecurityScheme(__SecuritySchemeBase, abc.ABC): + openid_connect_url: str + type: SecuritySchemeType = SecuritySchemeType.OPENID_CONNECT + + def apply_auth( + self, + headers: _collections.HTTPHeaderDict, + resource_path: str, + method: str, + body: typing.Optional[typing.Union[str, bytes]], + query_params_suffix: typing.Optional[str], + scope_names: typing.Tuple[str, ...] = (), + ) -> None: + raise NotImplementedError("OpenIdConnectSecurityScheme not yet implemented") + +""" +Key is the Security scheme class +Value is the list of scopes +""" +SecurityRequirementObject = typing.TypedDict( + 'SecurityRequirementObject', + { + }, + total=False +) \ No newline at end of file diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/server.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/server.py new file mode 100644 index 00000000000..ca8af8b111f --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/server.py @@ -0,0 +1,34 @@ +# coding: utf-8 +""" + Example + No description provided (generated by Openapi JSON Schema Generator https://github.com/openapi-json-schema-tools/openapi-json-schema-generator) # noqa: E501 + The version of the OpenAPI document: 1.0.0 + Generated by: https://github.com/openapi-json-schema-tools/openapi-json-schema-generator +""" + +from __future__ import annotations +import abc +import dataclasses +import typing + +from json_schema_api.schemas import validation, schema + + +@dataclasses.dataclass +class ServerWithoutVariables(abc.ABC): + url: str + + +@dataclasses.dataclass +class ServerWithVariables(abc.ABC): + _url: str + variables: validation.immutabledict[str, str] + variables_schema: typing.Type[schema.Schema] + url: str = dataclasses.field(init=False) + + def __post_init__(self): + url = self._url + assert isinstance (self.variables, self.variables_schema().type_to_output_cls[validation.immutabledict]) + for (key, value) in self.variables.items(): + url = url.replace("{" + key + "}", value) + self.url = url diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/servers/__init__.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/servers/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/servers/server_0.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/servers/server_0.py new file mode 100644 index 00000000000..5c91a6f1da4 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/servers/server_0.py @@ -0,0 +1,11 @@ +# coding: utf-8 +""" + Generated by: https://github.com/openapi-json-schema-tools/openapi-json-schema-generator +""" + +from json_schema_api.shared_imports.server_imports import * # pyright: ignore [reportWildcardImportFromLibrary] + + +@dataclasses.dataclass +class Server0(server.ServerWithoutVariables): + url: str = "http://api.example.xyz/v1" diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/shared_imports/__init__.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/shared_imports/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/shared_imports/header_imports.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/shared_imports/header_imports.py new file mode 100644 index 00000000000..d06d8732d13 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/shared_imports/header_imports.py @@ -0,0 +1,15 @@ +import decimal +import io +import typing +import typing_extensions + +from json_schema_api import api_client, schemas + +__all__ = [ + 'decimal', + 'io', + 'typing', + 'typing_extensions', + 'api_client', + 'schemas' +] \ No newline at end of file diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/shared_imports/operation_imports.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/shared_imports/operation_imports.py new file mode 100644 index 00000000000..67a6810c6ec --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/shared_imports/operation_imports.py @@ -0,0 +1,18 @@ +import datetime +import decimal +import io +import typing +import typing_extensions +import uuid + +from json_schema_api import schemas, api_response + +__all__ = [ + 'decimal', + 'io', + 'typing', + 'typing_extensions', + 'uuid', + 'schemas', + 'api_response' +] \ No newline at end of file diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/shared_imports/response_imports.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/shared_imports/response_imports.py new file mode 100644 index 00000000000..1c6cbdb7147 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/shared_imports/response_imports.py @@ -0,0 +1,25 @@ +import dataclasses +import datetime +import decimal +import io +import typing +import uuid + +import typing_extensions +import urllib3 + +from json_schema_api import api_client, schemas, api_response + +__all__ = [ + 'dataclasses', + 'datetime', + 'decimal', + 'io', + 'typing', + 'uuid', + 'typing_extensions', + 'urllib3', + 'api_client', + 'schemas', + 'api_response' +] \ No newline at end of file diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/shared_imports/schema_imports.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/shared_imports/schema_imports.py new file mode 100644 index 00000000000..9ce95d517d9 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/shared_imports/schema_imports.py @@ -0,0 +1,28 @@ +import dataclasses +import datetime +import decimal +import io +import numbers +import re +import typing +import typing_extensions +import uuid + +from json_schema_api import schemas +from json_schema_api.configurations import schema_configuration + +U = typing.TypeVar('U') + +__all__ = [ + 'dataclasses', + 'datetime', + 'decimal', + 'io', + 'numbers', + 're', + 'typing', + 'typing_extensions', + 'uuid', + 'schemas', + 'schema_configuration' +] \ No newline at end of file diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/shared_imports/security_scheme_imports.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/shared_imports/security_scheme_imports.py new file mode 100644 index 00000000000..71b3f27ddd3 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/shared_imports/security_scheme_imports.py @@ -0,0 +1,12 @@ +import dataclasses +import typing +import typing_extensions + +from json_schema_api import security_schemes + +__all__ = [ + 'dataclasses', + 'typing', + 'typing_extensions', + 'security_schemes' +] \ No newline at end of file diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/shared_imports/server_imports.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/shared_imports/server_imports.py new file mode 100644 index 00000000000..1c89ddff73b --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/shared_imports/server_imports.py @@ -0,0 +1,13 @@ +import dataclasses +import typing +import typing_extensions + +from json_schema_api import server, schemas + +__all__ = [ + 'dataclasses', + 'typing', + 'typing_extensions', + 'server', + 'schemas' +] \ No newline at end of file diff --git a/samples/client/3_1_0_json_schema/python/test-requirements.txt b/samples/client/3_1_0_json_schema/python/test-requirements.txt new file mode 100644 index 00000000000..3043888202a --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/test-requirements.txt @@ -0,0 +1,2 @@ +pytest ~= 7.2.0 +pytest-cov ~= 4.0.0 diff --git a/samples/client/3_1_0_json_schema/python/test/__init__.py b/samples/client/3_1_0_json_schema/python/test/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/samples/client/3_1_0_json_schema/python/test/components/__init__.py b/samples/client/3_1_0_json_schema/python/test/components/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/samples/client/3_1_0_json_schema/python/test/components/schema/__init__.py b/samples/client/3_1_0_json_schema/python/test/components/schema/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/samples/client/3_1_0_json_schema/python/test/components/schema/test_any_type_contains_value.py b/samples/client/3_1_0_json_schema/python/test/components/schema/test_any_type_contains_value.py new file mode 100644 index 00000000000..6e985c6ebad --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/test/components/schema/test_any_type_contains_value.py @@ -0,0 +1,23 @@ +# coding: utf-8 + +""" + Example + No description provided (generated by Openapi JSON Schema Generator https://github.com/openapi-json-schema-tools/openapi-json-schema-generator) # noqa: E501 + The version of the OpenAPI document: 1.0.0 + Generated by: https://github.com/openapi-json-schema-tools/openapi-json-schema-generator +""" + +import unittest + +import json_schema_api +from json_schema_api.components.schema.any_type_contains_value import AnyTypeContainsValue +from json_schema_api.configurations import schema_configuration + + +class TestAnyTypeContainsValue(unittest.TestCase): + """AnyTypeContainsValue unit test stubs""" + configuration = schema_configuration.SchemaConfiguration() + + +if __name__ == '__main__': + unittest.main() diff --git a/samples/client/3_1_0_json_schema/python/test/components/schema/test_array_contains_value.py b/samples/client/3_1_0_json_schema/python/test/components/schema/test_array_contains_value.py new file mode 100644 index 00000000000..b128ac08a30 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/test/components/schema/test_array_contains_value.py @@ -0,0 +1,23 @@ +# coding: utf-8 + +""" + Example + No description provided (generated by Openapi JSON Schema Generator https://github.com/openapi-json-schema-tools/openapi-json-schema-generator) # noqa: E501 + The version of the OpenAPI document: 1.0.0 + Generated by: https://github.com/openapi-json-schema-tools/openapi-json-schema-generator +""" + +import unittest + +import json_schema_api +from json_schema_api.components.schema.array_contains_value import ArrayContainsValue +from json_schema_api.configurations import schema_configuration + + +class TestArrayContainsValue(unittest.TestCase): + """ArrayContainsValue unit test stubs""" + configuration = schema_configuration.SchemaConfiguration() + + +if __name__ == '__main__': + unittest.main() diff --git a/samples/client/3_1_0_json_schema/python/test/test_paths/__init__.py b/samples/client/3_1_0_json_schema/python/test/test_paths/__init__.py new file mode 100644 index 00000000000..bcc864a1738 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/test/test_paths/__init__.py @@ -0,0 +1,68 @@ +import json +import typing + +import urllib3 +from urllib3._collections import HTTPHeaderDict + + +class ApiTestMixin: + json_content_type = 'application/json' + user_agent = 'OpenAPI-JSON-Schema-Generator/1.0.0/python' + + @classmethod + def assert_pool_manager_request_called_with( + cls, + mock_request, + url: str, + method: str = 'POST', + body: typing.Optional[bytes] = None, + content_type: typing.Optional[str] = None, + accept_content_type: typing.Optional[str] = None, + stream: bool = False, + ): + headers = { + 'User-Agent': cls.user_agent + } + if accept_content_type: + headers['Accept'] = accept_content_type + if content_type: + headers['Content-Type'] = content_type + kwargs = dict( + headers=HTTPHeaderDict(headers), + preload_content=not stream, + timeout=None, + ) + if content_type and method != 'GET': + kwargs['body'] = body + mock_request.assert_called_with( + method, + url, + **kwargs + ) + + @staticmethod + def headers_for_content_type(content_type: str) -> typing.Dict[str, str]: + return {'content-type': content_type} + + @classmethod + def response( + cls, + body: typing.Union[str, bytes], + status: int = 200, + content_type: str = json_content_type, + headers: typing.Optional[typing.Dict[str, str]] = None, + preload_content: bool = True + ) -> urllib3.HTTPResponse: + if headers is None: + headers = {} + headers.update(cls.headers_for_content_type(content_type)) + return urllib3.HTTPResponse( + body, + headers=headers, + status=status, + preload_content=preload_content + ) + + @staticmethod + def json_bytes(in_data: typing.Any) -> bytes: + return json.dumps(in_data, separators=(",", ":"), ensure_ascii=False).encode('utf-8') diff --git a/samples/client/3_1_0_json_schema/python/test/test_paths/test_some_path/__init__.py b/samples/client/3_1_0_json_schema/python/test/test_paths/test_some_path/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/samples/client/3_1_0_json_schema/python/test/test_paths/test_some_path/test_get.py b/samples/client/3_1_0_json_schema/python/test/test_paths/test_some_path/test_get.py new file mode 100644 index 00000000000..76b738bccd3 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/test/test_paths/test_some_path/test_get.py @@ -0,0 +1,35 @@ +# coding: utf-8 + +""" + Generated by: https://github.com/openapi-json-schema-tools/openapi-json-schema-generator +""" + +import unittest +from unittest.mock import patch + +import urllib3 +import typing_extensions + +import json_schema_api +from json_schema_api.paths.some_path.get import operation as get # noqa: E501 +from json_schema_api import schemas, api_client +from json_schema_api.configurations import api_configuration, schema_configuration + +from .. import ApiTestMixin + + +class TestGet(ApiTestMixin, unittest.TestCase): + """ + Get unit test stubs + """ + api_config = api_configuration.ApiConfiguration() + schema_config = schema_configuration.SchemaConfiguration() + used_api_client = api_client.ApiClient(configuration=api_config, schema_configuration=schema_config) + api = get.ApiForGet(api_client=used_api_client) # noqa: E501 + + response_status = 200 + response_body_schema = get.response_200.ResponseFor200.content["application/json"].schema + assert response_body_schema is not None + +if __name__ == '__main__': + unittest.main() diff --git a/samples/client/3_1_0_json_schema/python/tox.ini b/samples/client/3_1_0_json_schema/python/tox.ini new file mode 100644 index 00000000000..283695fea66 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/tox.ini @@ -0,0 +1,10 @@ +[tox] +envlist = py38 +isolated_build = True + +[testenv] +passenv = PYTHON_VERSION +deps=-r{toxinidir}/test-requirements.txt + +commands= + pytest --cov=json_schema_api diff --git a/src/test/resources/3_1/json_schema.yaml b/src/test/resources/3_1/json_schema.yaml new file mode 100644 index 00000000000..98f912e0a1e --- /dev/null +++ b/src/test/resources/3_1/json_schema.yaml @@ -0,0 +1,33 @@ +# OAS document that uses 3.1 features: +# 'null' type +# type array +openapi: 3.1.0 +info: + version: 1.0.0 + title: Example + license: + name: MIT + identifier: MIT +servers: + - url: http://api.example.xyz/v1 +paths: + /somePath: + get: + operationId: getSomePath + responses: + '200': + description: OK + content: + application/json: + schema: {} +components: + schemas: + ArrayContainsValue: + type: array + contains: + enum: + - 1 + AnyTypeContainsValue: + contains: + enum: + - 1 From d45ba342b328f83b613733b9ee51ced0f2530407 Mon Sep 17 00:00:00 2001 From: Justin Black Date: Sat, 19 Aug 2023 09:32:34 -0700 Subject: [PATCH 2/9] Adds writes contains data in codegenschema --- .../python/.openapi-generator/FILES | 3 -- .../components/schema/array_contains_value.md | 2 +- .../components/schema/array_contains_value.py | 14 +---- .../codegen/generators/DefaultGenerator.java | 53 +++++++++++-------- 4 files changed, 32 insertions(+), 40 deletions(-) diff --git a/samples/client/3_1_0_json_schema/python/.openapi-generator/FILES b/samples/client/3_1_0_json_schema/python/.openapi-generator/FILES index a1b5d271b6d..7c8a35d4ef9 100644 --- a/samples/client/3_1_0_json_schema/python/.openapi-generator/FILES +++ b/samples/client/3_1_0_json_schema/python/.openapi-generator/FILES @@ -1,6 +1,5 @@ .gitignore .gitlab-ci.yml -.openapi-generator-ignore .travis.yml README.md docs/apis/tags/default_api.md @@ -65,8 +64,6 @@ test-requirements.txt test/__init__.py test/components/__init__.py test/components/schema/__init__.py -test/components/schema/test_any_type_contains_value.py -test/components/schema/test_array_contains_value.py test/test_paths/__init__.py test/test_paths/test_some_path/__init__.py test/test_paths/test_some_path/test_get.py diff --git a/samples/client/3_1_0_json_schema/python/docs/components/schema/array_contains_value.md b/samples/client/3_1_0_json_schema/python/docs/components/schema/array_contains_value.md index 1cce4190ed0..a000d0d384f 100644 --- a/samples/client/3_1_0_json_schema/python/docs/components/schema/array_contains_value.md +++ b/samples/client/3_1_0_json_schema/python/docs/components/schema/array_contains_value.md @@ -7,6 +7,6 @@ type: schemas.Schema ## validate method Input Type | Return Type | Notes ------------ | ------------- | ------------- - | | +list, tuple | tuple | [[Back to top]](#top) [[Back to Component Schemas]](../../../README.md#Component-Schemas) [[Back to README]](../../../README.md) diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/array_contains_value.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/array_contains_value.py index fd6c9516052..7e3f507fa7b 100644 --- a/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/array_contains_value.py +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/array_contains_value.py @@ -10,16 +10,4 @@ from __future__ import annotations from json_schema_api.shared_imports.schema_imports import * # pyright: ignore [reportWildcardImportFromLibrary] - - -@dataclasses.dataclass(frozen=True) -class ArrayContainsValue( - schemas.Schema[schemas.immutabledict[str, schemas.OUTPUT_BASE_TYPES], typing.Tuple[schemas.OUTPUT_BASE_TYPES, ...]], -): - """NOTE: This class is auto generated by OpenAPI JSON Schema Generator. - Ref: https://github.com/openapi-json-schema-tools/openapi-json-schema-generator - - Do not edit the class manually. - """ - - +ArrayContainsValue: typing_extensions.TypeAlias = schemas.ListSchema diff --git a/src/main/java/org/openapijsonschematools/codegen/generators/DefaultGenerator.java b/src/main/java/org/openapijsonschematools/codegen/generators/DefaultGenerator.java index 63910aa0b06..9d53a8674a3 100644 --- a/src/main/java/org/openapijsonschematools/codegen/generators/DefaultGenerator.java +++ b/src/main/java/org/openapijsonschematools/codegen/generators/DefaultGenerator.java @@ -28,6 +28,7 @@ import io.swagger.v3.oas.models.ExternalDocumentation; import io.swagger.v3.oas.models.PathItem; import io.swagger.v3.oas.models.Paths; +import io.swagger.v3.oas.models.SpecVersion; import io.swagger.v3.oas.models.security.OAuthFlow; import io.swagger.v3.oas.models.security.SecurityRequirement; import org.apache.commons.text.StringEscapeUtils; @@ -445,7 +446,7 @@ public void processOpts() { this.setEnumUnknownDefaultCase(Boolean.parseBoolean(additionalProperties .get(CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE).toString())); } - requiredAddPropUnsetSchema = fromSchema(new Schema(), null, null); + requiredAddPropUnsetSchema = fromSchema(new JsonSchema(), null, null); } @@ -2186,26 +2187,26 @@ protected LinkedHashSet getTypes(Schema schema) { } if (schema.getTypes() != null) { // NoneFrozenDictTupleStrDecimalBoolFileBytes - if (types.contains("null")) { - types.add("null"); - } - if (types.contains("object")) { - types.add("object"); - } - if (types.contains("array")) { - types.add("array"); - } - if (types.contains("string")) { - types.add("string"); - } - if (types.contains("number")) { - types.add("number"); - } - if (types.contains("integer")) { - types.add("integer"); - } - if (types.contains("boolean")) { - types.add("boolean"); + for (Object typeObj: schema.getTypes()) { + String type = typeObj.toString(); + switch (type) { + case "null": types.add("null"); + break; + case "object": types.add("object"); + break; + case "array": types.add("array"); + break; + case "string": types.add("string"); + break; + case "number": types.add("number"); + break; + case "integer": types.add("integer"); + break; + case "boolean": types.add("boolean"); + break; + default: + break; + } } // the above order used so mixins will stay the same } @@ -2266,8 +2267,10 @@ public CodegenSchema fromSchema(Schema p, String sourceJsonPath, String currentJ assert generatorMetadata != null; addImports(property.imports, getImports(property, generatorMetadata.getFeatureSet())); } - // TODO with 3.1.0 schemas continue processing - return property; + if (p.getSpecVersion().compareTo(SpecVersion.V31) < 0) { + // stop processing if version is less than 3.1.0 + return property; + } } if (p.equals(trueSchema)) { @@ -2293,6 +2296,7 @@ public CodegenSchema fromSchema(Schema p, String sourceJsonPath, String currentJ oneOf not items + contains anyOf allOf additionalProperties @@ -2315,6 +2319,9 @@ public CodegenSchema fromSchema(Schema p, String sourceJsonPath, String currentJ p.getItems(), sourceJsonPath, currentJsonPath + "/items"); } property.enumInfo = getEnumInfo(p, currentJsonPath, sourceJsonPath, property.types); + if (p.getContains() != null) { + property.contains = fromSchema(p.getContains(), sourceJsonPath, currentJsonPath + "/contains"); + } List anyOfs = ((Schema) p).getAnyOf(); if (anyOfs != null && !anyOfs.isEmpty()) { property.anyOf = getComposedProperties(anyOfs, "anyOf", sourceJsonPath, currentJsonPath); From 1112da61736ca7ba21901f6716893e04ae1db9af Mon Sep 17 00:00:00 2001 From: Justin Black Date: Sat, 19 Aug 2023 09:37:18 -0700 Subject: [PATCH 3/9] Writes schema class if it has contains info --- .../schema/any_type_contains_value.py | 14 +++++++++- .../components/schema/array_contains_value.py | 26 ++++++++++++++++++- .../schemas/schema_cls/schema_cls.hbs | 4 +-- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/any_type_contains_value.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/any_type_contains_value.py index 742cde5c332..267824ff80c 100644 --- a/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/any_type_contains_value.py +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/any_type_contains_value.py @@ -10,4 +10,16 @@ from __future__ import annotations from json_schema_api.shared_imports.schema_imports import * # pyright: ignore [reportWildcardImportFromLibrary] -AnyTypeContainsValue: typing_extensions.TypeAlias = schemas.AnyTypeSchema + + +@dataclasses.dataclass(frozen=True) +class AnyTypeContainsValue( + schemas.AnyTypeSchema[schemas.immutabledict[str, schemas.OUTPUT_BASE_TYPES], typing.Tuple[schemas.OUTPUT_BASE_TYPES, ...]], +): + """NOTE: This class is auto generated by OpenAPI JSON Schema Generator. + Ref: https://github.com/openapi-json-schema-tools/openapi-json-schema-generator + + Do not edit the class manually. + """ + # any type + diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/array_contains_value.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/array_contains_value.py index 7e3f507fa7b..429e5100e69 100644 --- a/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/array_contains_value.py +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/array_contains_value.py @@ -10,4 +10,28 @@ from __future__ import annotations from json_schema_api.shared_imports.schema_imports import * # pyright: ignore [reportWildcardImportFromLibrary] -ArrayContainsValue: typing_extensions.TypeAlias = schemas.ListSchema + + +@dataclasses.dataclass(frozen=True) +class ArrayContainsValue( + schemas.Schema[schemas.immutabledict, typing.Tuple[schemas.OUTPUT_BASE_TYPES, ...]] +): + """NOTE: This class is auto generated by OpenAPI JSON Schema Generator. + Ref: https://github.com/openapi-json-schema-tools/openapi-json-schema-generator + + Do not edit the class manually. + """ + + @classmethod + def validate( + cls, + arg: typing.Union[ + typing.List[schemas.INPUT_TYPES_ALL], + typing.Tuple[schemas.INPUT_TYPES_ALL, ...], + ], + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> typing.Tuple[schemas.OUTPUT_BASE_TYPES]: + return super().validate_base( + arg, + configuration=configuration, + ) diff --git a/src/main/resources/python/components/schemas/schema_cls/schema_cls.hbs b/src/main/resources/python/components/schemas/schema_cls/schema_cls.hbs index a2243db020e..0777b24c5c2 100644 --- a/src/main/resources/python/components/schemas/schema_cls/schema_cls.hbs +++ b/src/main/resources/python/components/schemas/schema_cls/schema_cls.hbs @@ -5,7 +5,7 @@ {{> components/schemas/schema_cls/_schema_composed_or_anytype }} {{else}} {{#eq types null }} - {{#or enumInfo hasValidation items properties requiredProperties hasDiscriminatorWithNonEmptyMapping additionalProperties format}} + {{#or enumInfo hasValidation items properties requiredProperties hasDiscriminatorWithNonEmptyMapping additionalProperties format contains}} {{> components/schemas/schema_cls/_schema_composed_or_anytype }} {{else}} {{> components/schemas/schema_cls/_schema_var_equals_cls }} @@ -23,7 +23,7 @@ {{/or}} {{else}} {{#eq this "array"}} - {{#or items hasValidation}} + {{#or items hasValidation contains}} {{> components/schemas/schema_cls/_schema_list }} {{else}} {{> components/schemas/schema_cls/_schema_var_equals_cls }} From 05684503a04cb530678c0ee872322fff43fd994c Mon Sep 17 00:00:00 2001 From: Justin Black Date: Sat, 19 Aug 2023 09:53:49 -0700 Subject: [PATCH 4/9] Writes contains info in schema_cls --- .../components/schema/any_type_contains_value.py | 1 + .../json_schema_api/components/schema/array_contains_value.py | 2 ++ .../python/components/schemas/schema_cls/_contains_partial.hbs | 3 +++ .../schemas/schema_cls/_schema_composed_or_anytype.hbs | 3 +++ .../python/components/schemas/schema_cls/_schema_list.hbs | 3 ++- 5 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 src/main/resources/python/components/schemas/schema_cls/_contains_partial.hbs diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/any_type_contains_value.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/any_type_contains_value.py index 267824ff80c..7d7e4d65f31 100644 --- a/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/any_type_contains_value.py +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/any_type_contains_value.py @@ -22,4 +22,5 @@ class AnyTypeContainsValue( Do not edit the class manually. """ # any type + contains: typing.Type[Contains] = dataclasses.field(default_factory=lambda: Contains) # type: ignore diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/array_contains_value.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/array_contains_value.py index 429e5100e69..6acf19fc635 100644 --- a/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/array_contains_value.py +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/array_contains_value.py @@ -21,6 +21,8 @@ class ArrayContainsValue( Do not edit the class manually. """ + types: typing.FrozenSet[typing.Type] = frozenset({tuple}) + contains: typing.Type[Contains] = dataclasses.field(default_factory=lambda: Contains) # type: ignore @classmethod def validate( diff --git a/src/main/resources/python/components/schemas/schema_cls/_contains_partial.hbs b/src/main/resources/python/components/schemas/schema_cls/_contains_partial.hbs new file mode 100644 index 00000000000..29c3235ffc2 --- /dev/null +++ b/src/main/resources/python/components/schemas/schema_cls/_contains_partial.hbs @@ -0,0 +1,3 @@ +{{#with contains}} +contains: typing.Type[{{#if refInfo.refClass}}{{#if refInfo.refModule}}{{refInfo.refModule}}.{{/if}}{{refInfo.refClass}}{{else}}{{jsonPathPiece.camelCase}}{{/if}}] = dataclasses.field(default_factory=lambda: {{#if refInfo.refClass}}{{#if refInfo.refModule}}{{refInfo.refModule}}.{{/if}}{{refInfo.refClass}}{{else}}{{jsonPathPiece.camelCase}}{{/if}}) # type: ignore +{{/with}} diff --git a/src/main/resources/python/components/schemas/schema_cls/_schema_composed_or_anytype.hbs b/src/main/resources/python/components/schemas/schema_cls/_schema_composed_or_anytype.hbs index 83d55bff1d9..79ea187006d 100644 --- a/src/main/resources/python/components/schemas/schema_cls/_schema_composed_or_anytype.hbs +++ b/src/main/resources/python/components/schemas/schema_cls/_schema_composed_or_anytype.hbs @@ -37,6 +37,9 @@ class {{jsonPathPiece.camelCase}}( {{#if items}} {{> components/schemas/schema_cls/_list_partial }} {{/if}} +{{#if contains}} + {{> components/schemas/schema_cls/_contains_partial }} +{{/if}} {{#or additionalProperties requiredProperties hasDiscriminatorWithNonEmptyMapping properties}} {{> components/schemas/schema_cls/_dict_partial }} {{/or}} diff --git a/src/main/resources/python/components/schemas/schema_cls/_schema_list.hbs b/src/main/resources/python/components/schemas/schema_cls/_schema_list.hbs index 36cd1abab5f..f06c2cbf7b8 100644 --- a/src/main/resources/python/components/schemas/schema_cls/_schema_list.hbs +++ b/src/main/resources/python/components/schemas/schema_cls/_schema_list.hbs @@ -15,12 +15,13 @@ class {{jsonPathPiece.camelCase}}( {{/if}} """ {{/if}} -{{#or items hasValidation}} +{{#or items hasValidation contains}} types: typing.FrozenSet[typing.Type] = frozenset({tuple}) {{#if hasValidation}} {{> components/schemas/schema_cls/_validations }} {{/if}} {{> components/schemas/schema_cls/_list_partial }} + {{> components/schemas/schema_cls/_contains_partial }} {{#if arrayOutputJsonPathPiece}} type_to_output_cls: typing.Mapping[ typing.Type, From da1768788959dd2f98f91024697046cab7f609d9 Mon Sep 17 00:00:00 2001 From: Justin Black Date: Sat, 19 Aug 2023 10:14:47 -0700 Subject: [PATCH 5/9] Writes contains schemas for anyType and array type --- .../schema/any_type_contains_value.py | 49 +++++++++++++++++++ .../components/schema/array_contains_value.py | 49 +++++++++++++++++++ .../openapimodels/CodegenSchema.java | 4 ++ 3 files changed, 102 insertions(+) diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/any_type_contains_value.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/any_type_contains_value.py index 7d7e4d65f31..b44942a9707 100644 --- a/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/any_type_contains_value.py +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/any_type_contains_value.py @@ -12,6 +12,55 @@ +class ContainsEnums: + + @schemas.classproperty + def POSITIVE_1(cls) -> typing.Literal[1]: + return Contains.validate(1) + + +@dataclasses.dataclass(frozen=True) +class Contains( + schemas.Schema +): + types: typing.FrozenSet[typing.Type] = frozenset({ + float, + int, + }) + enum_value_to_name: typing.Mapping[typing.Union[int, float, str, schemas.Bool, None], str] = dataclasses.field( + default_factory=lambda: { + 1: "POSITIVE_1", + } + ) + enums = ContainsEnums + + @typing.overload + @classmethod + def validate( + cls, + arg: typing.Literal[1], + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> typing.Literal[1]: ... + @typing.overload + @classmethod + def validate( + cls, + arg: int, + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> typing.Literal[1,]: ... + @classmethod + def validate( + cls, + arg: typing.Union[int, float], + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> typing.Union[int, float]: + validated_arg = super().validate_base( + arg, + configuration=configuration, + ) + return validated_arg + + @dataclasses.dataclass(frozen=True) class AnyTypeContainsValue( schemas.AnyTypeSchema[schemas.immutabledict[str, schemas.OUTPUT_BASE_TYPES], typing.Tuple[schemas.OUTPUT_BASE_TYPES, ...]], diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/array_contains_value.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/array_contains_value.py index 6acf19fc635..b65f4409295 100644 --- a/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/array_contains_value.py +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/array_contains_value.py @@ -12,6 +12,55 @@ +class ContainsEnums: + + @schemas.classproperty + def POSITIVE_1(cls) -> typing.Literal[1]: + return Contains.validate(1) + + +@dataclasses.dataclass(frozen=True) +class Contains( + schemas.Schema +): + types: typing.FrozenSet[typing.Type] = frozenset({ + float, + int, + }) + enum_value_to_name: typing.Mapping[typing.Union[int, float, str, schemas.Bool, None], str] = dataclasses.field( + default_factory=lambda: { + 1: "POSITIVE_1", + } + ) + enums = ContainsEnums + + @typing.overload + @classmethod + def validate( + cls, + arg: typing.Literal[1], + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> typing.Literal[1]: ... + @typing.overload + @classmethod + def validate( + cls, + arg: int, + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> typing.Literal[1,]: ... + @classmethod + def validate( + cls, + arg: typing.Union[int, float], + configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None + ) -> typing.Union[int, float]: + validated_arg = super().validate_base( + arg, + configuration=configuration, + ) + return validated_arg + + @dataclasses.dataclass(frozen=True) class ArrayContainsValue( schemas.Schema[schemas.immutabledict, typing.Tuple[schemas.OUTPUT_BASE_TYPES, ...]] diff --git a/src/main/java/org/openapijsonschematools/codegen/generators/openapimodels/CodegenSchema.java b/src/main/java/org/openapijsonschematools/codegen/generators/openapimodels/CodegenSchema.java index 01e126ab7fd..6701ae86e91 100644 --- a/src/main/java/org/openapijsonschematools/codegen/generators/openapimodels/CodegenSchema.java +++ b/src/main/java/org/openapijsonschematools/codegen/generators/openapimodels/CodegenSchema.java @@ -238,6 +238,7 @@ private void getAllSchemas(ArrayList schemasBeforeImports, ArrayL additionalProperties allOf anyOf + contains items not oneOf @@ -282,6 +283,9 @@ private void getAllSchemas(ArrayList schemasBeforeImports, ArrayL schemasAfterImports.add(extraSchema); } } + if (contains != null) { + contains.getAllSchemas(schemasBeforeImports, schemasAfterImports, level + 1); + } if (enumInfo != null) { // write the class as a separate entity so enum values do not collide with // json schema keywords From 1e2df77dde2872798dbbcb10eb3889d5df2ab40d Mon Sep 17 00:00:00 2001 From: Justin Black Date: Sat, 19 Aug 2023 10:26:49 -0700 Subject: [PATCH 6/9] Adds contains validator --- .../configurations/schema_configuration.hbs | 1 + .../resources/python/schemas/validation.hbs | 38 ++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/main/resources/python/configurations/schema_configuration.hbs b/src/main/resources/python/configurations/schema_configuration.hbs index 578c12a8dab..cb152d0f145 100644 --- a/src/main/resources/python/configurations/schema_configuration.hbs +++ b/src/main/resources/python/configurations/schema_configuration.hbs @@ -11,6 +11,7 @@ PYTHON_KEYWORD_TO_JSON_SCHEMA_KEYWORD = { 'additional_properties': 'additionalProperties', 'all_of': 'allOf', 'any_of': 'anyOf', + 'contains': 'contains', 'discriminator': 'discriminator', # default omitted because it has no validation impact 'enum_value_to_name': 'enum', diff --git a/src/main/resources/python/schemas/validation.hbs b/src/main/resources/python/schemas/validation.hbs index f2a45cdaddd..119d77ebefe 100644 --- a/src/main/resources/python/schemas/validation.hbs +++ b/src/main/resources/python/schemas/validation.hbs @@ -1097,6 +1097,41 @@ def validate_discriminator( return discriminated_cls._validate(arg, validation_metadata=updated_vm) +def validate_contains( + arg: typing.Any, + contains_cls: typing.Type[SchemaValidator], + cls: typing.Type, + validation_metadata: ValidationMetadata, +{{#if nonCompliantUseDiscriminatorIfCompositionFails}} + **kwargs +{{/if}} +) -> typing.Optional[PathToSchemasType]: + if not isinstance(arg, tuple): + return None + contains_cls = _get_class(contains_cls) + path_to_schemas: PathToSchemasType = {} + array_contains_item = False + for i, value in enumerate(arg): + item_validation_metadata = ValidationMetadata( + path_to_item=validation_metadata.path_to_item+(i,), + configuration=validation_metadata.configuration, + validated_path_to_schemas=validation_metadata.validated_path_to_schemas + ) + if item_validation_metadata.validation_ran_earlier(item_cls): + add_deeper_validated_schemas(item_validation_metadata, path_to_schemas) + return path_to_schemas + other_path_to_schemas = item_cls._validate( + value, validation_metadata=item_validation_metadata) + update(path_to_schemas, other_path_to_schemas) + return path_to_schemas + if not array_contains_item: + raise exceptions.ApiValueError( + "Validation failed for contains keyword in class={} at path_to_item={}. No " + "items validated to the contains schema.".format(cls, validation_metadata.path_to_item) + ) + return path_to_schemas + + validator_type = typing.Callable[[typing.Any, typing.Any, type, ValidationMetadata], typing.Optional[PathToSchemasType]] json_schema_keyword_to_validator: typing.Mapping[str, validator_type] = { 'types': validate_types, @@ -1123,5 +1158,6 @@ json_schema_keyword_to_validator: typing.Mapping[str, validator_type] = { 'any_of': validate_any_of, 'all_of': validate_all_of, 'not_': validate_not, - 'discriminator': validate_discriminator + 'discriminator': validate_discriminator, + 'contains': validate_contains } \ No newline at end of file From 08f78a2f0082434e6220cf92793d081cb228cc51 Mon Sep 17 00:00:00 2001 From: Justin Black Date: Sat, 19 Aug 2023 10:27:35 -0700 Subject: [PATCH 7/9] Sample regen --- .../configurations/schema_configuration.py | 1 + .../src/json_schema_api/schemas/validation.py | 35 ++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/configurations/schema_configuration.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/configurations/schema_configuration.py index 63bda4240a0..b456e9997ac 100644 --- a/samples/client/3_1_0_json_schema/python/src/json_schema_api/configurations/schema_configuration.py +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/configurations/schema_configuration.py @@ -16,6 +16,7 @@ 'additional_properties': 'additionalProperties', 'all_of': 'allOf', 'any_of': 'anyOf', + 'contains': 'contains', 'discriminator': 'discriminator', # default omitted because it has no validation impact 'enum_value_to_name': 'enum', diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/schemas/validation.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/schemas/validation.py index 264399e15ef..5f5cadd5e9a 100644 --- a/samples/client/3_1_0_json_schema/python/src/json_schema_api/schemas/validation.py +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/schemas/validation.py @@ -956,6 +956,38 @@ def validate_discriminator( return discriminated_cls._validate(arg, validation_metadata=updated_vm) +def validate_contains( + arg: typing.Any, + contains_cls: typing.Type[SchemaValidator], + cls: typing.Type, + validation_metadata: ValidationMetadata, +) -> typing.Optional[PathToSchemasType]: + if not isinstance(arg, tuple): + return None + contains_cls = _get_class(contains_cls) + path_to_schemas: PathToSchemasType = {} + array_contains_item = False + for i, value in enumerate(arg): + item_validation_metadata = ValidationMetadata( + path_to_item=validation_metadata.path_to_item+(i,), + configuration=validation_metadata.configuration, + validated_path_to_schemas=validation_metadata.validated_path_to_schemas + ) + if item_validation_metadata.validation_ran_earlier(item_cls): + add_deeper_validated_schemas(item_validation_metadata, path_to_schemas) + return path_to_schemas + other_path_to_schemas = item_cls._validate( + value, validation_metadata=item_validation_metadata) + update(path_to_schemas, other_path_to_schemas) + return path_to_schemas + if not array_contains_item: + raise exceptions.ApiValueError( + "Validation failed for contains keyword in class={} at path_to_item={}. No " + "items validated to the contains schema.".format(cls, validation_metadata.path_to_item) + ) + return path_to_schemas + + validator_type = typing.Callable[[typing.Any, typing.Any, type, ValidationMetadata], typing.Optional[PathToSchemasType]] json_schema_keyword_to_validator: typing.Mapping[str, validator_type] = { 'types': validate_types, @@ -982,5 +1014,6 @@ def validate_discriminator( 'any_of': validate_any_of, 'all_of': validate_all_of, 'not_': validate_not, - 'discriminator': validate_discriminator + 'discriminator': validate_discriminator, + 'contains': validate_contains } \ No newline at end of file From f4823dd9f8def4597b90abefc1e8b1daf6518cc1 Mon Sep 17 00:00:00 2001 From: Justin Black Date: Sat, 19 Aug 2023 15:04:05 -0700 Subject: [PATCH 8/9] Adds sample testing 310 contains --- .circleci/parallel.sh | 1 + .../client/3_1_0_json_schema/python/Makefile | 13 +++++++ .../components/schema/array_contains_value.py | 2 +- .../configurations/api_configuration.py | 2 +- .../src/json_schema_api/schemas/validation.py | 13 ++++--- .../schema/test_any_type_contains_value.py | 12 +++++++ .../3_1_0_json_schema/python/test_python.sh | 34 +++++++++++++++++++ .../codegen/common/CodegenConstants.java | 4 +-- .../schemas/schema_cls/validate/validate.hbs | 4 +-- .../configurations/api_configuration.hbs | 2 +- .../resources/python/schemas/validation.hbs | 13 ++++--- 11 files changed, 83 insertions(+), 17 deletions(-) create mode 100644 samples/client/3_1_0_json_schema/python/Makefile create mode 100755 samples/client/3_1_0_json_schema/python/test_python.sh diff --git a/.circleci/parallel.sh b/.circleci/parallel.sh index b8f42d3a3a1..b7c22ff7298 100755 --- a/.circleci/parallel.sh +++ b/.circleci/parallel.sh @@ -23,6 +23,7 @@ elif [ "$JOB_ID" = "testPythonClientSamples" ]; then (cd samples/client/3_0_3_unit_test/python && make test) (cd samples/client/openapi_features/nonCompliantUseDiscriminatorIfCompositionFails/python && make test) (cd samples/client/openapi_features/security/python && make test) + (cd samples/client/3_1_0_json_schema/python && make test) else echo "Running job $JOB_ID" diff --git a/samples/client/3_1_0_json_schema/python/Makefile b/samples/client/3_1_0_json_schema/python/Makefile new file mode 100644 index 00000000000..b73277d308c --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/Makefile @@ -0,0 +1,13 @@ +SETUP_OUT=*.egg-info +VENV=venv + +clean: + rm -rf $(SETUP_OUT) + rm -rf $(VENV) + rm -rf .tox + rm -rf .coverage + find . -name "*.py[oc]" -delete + find . -name "__pycache__" -delete + +test: clean + bash ./test_python.sh \ No newline at end of file diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/array_contains_value.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/array_contains_value.py index b65f4409295..7cf4593938a 100644 --- a/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/array_contains_value.py +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/components/schema/array_contains_value.py @@ -81,7 +81,7 @@ def validate( typing.Tuple[schemas.INPUT_TYPES_ALL, ...], ], configuration: typing.Optional[schema_configuration.SchemaConfiguration] = None - ) -> typing.Tuple[schemas.OUTPUT_BASE_TYPES]: + ) -> typing.Tuple[schemas.OUTPUT_BASE_TYPES, ...]: return super().validate_base( arg, configuration=configuration, diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/configurations/api_configuration.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/configurations/api_configuration.py index 66121393ba9..b15820bc4fb 100644 --- a/samples/client/3_1_0_json_schema/python/src/json_schema_api/configurations/api_configuration.py +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/configurations/api_configuration.py @@ -66,7 +66,7 @@ def __init__( """Constructor """ # Authentication Settings - self.security_scheme_info = {} + self.security_scheme_info: typing.Dict[str, typing.Any] = {} self.security_index_info = {'security': 0} # Server Info self.server_info: ServerInfo = server_info or { diff --git a/samples/client/3_1_0_json_schema/python/src/json_schema_api/schemas/validation.py b/samples/client/3_1_0_json_schema/python/src/json_schema_api/schemas/validation.py index 5f5cadd5e9a..84c34b36dfd 100644 --- a/samples/client/3_1_0_json_schema/python/src/json_schema_api/schemas/validation.py +++ b/samples/client/3_1_0_json_schema/python/src/json_schema_api/schemas/validation.py @@ -973,13 +973,16 @@ def validate_contains( configuration=validation_metadata.configuration, validated_path_to_schemas=validation_metadata.validated_path_to_schemas ) - if item_validation_metadata.validation_ran_earlier(item_cls): + if item_validation_metadata.validation_ran_earlier(contains_cls): add_deeper_validated_schemas(item_validation_metadata, path_to_schemas) return path_to_schemas - other_path_to_schemas = item_cls._validate( - value, validation_metadata=item_validation_metadata) - update(path_to_schemas, other_path_to_schemas) - return path_to_schemas + try: + other_path_to_schemas = contains_cls._validate( + value, validation_metadata=item_validation_metadata) + update(path_to_schemas, other_path_to_schemas) + return path_to_schemas + except exceptions.OpenApiException: + pass if not array_contains_item: raise exceptions.ApiValueError( "Validation failed for contains keyword in class={} at path_to_item={}. No " diff --git a/samples/client/3_1_0_json_schema/python/test/components/schema/test_any_type_contains_value.py b/samples/client/3_1_0_json_schema/python/test/components/schema/test_any_type_contains_value.py index 6e985c6ebad..d20ef05a495 100644 --- a/samples/client/3_1_0_json_schema/python/test/components/schema/test_any_type_contains_value.py +++ b/samples/client/3_1_0_json_schema/python/test/components/schema/test_any_type_contains_value.py @@ -18,6 +18,18 @@ class TestAnyTypeContainsValue(unittest.TestCase): """AnyTypeContainsValue unit test stubs""" configuration = schema_configuration.SchemaConfiguration() + def test_contains_success_single(self): + inst = AnyTypeContainsValue.validate((1,)) + assert inst == (1,) + + def test_contains_success_multiple(self): + inst = AnyTypeContainsValue.validate((2, 1, 1)) + assert inst == (2, 1, 1) + + def test_contains_failure(self): + with self.assertRaises(json_schema_api.ApiValueError): + AnyTypeContainsValue.validate((2,)) + if __name__ == '__main__': unittest.main() diff --git a/samples/client/3_1_0_json_schema/python/test_python.sh b/samples/client/3_1_0_json_schema/python/test_python.sh new file mode 100755 index 00000000000..4d9747bfc32 --- /dev/null +++ b/samples/client/3_1_0_json_schema/python/test_python.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +SETUP_OUT=*.egg-info +VENV=venv +DEACTIVE=false + +export LC_ALL=en_US.UTF-8 +export LANG=en_US.UTF-8 + +# set virtualenv +if [ -z "$VENVV" ]; then + python3 -m venv $VENV + source $VENV/bin/activate + DEACTIVE=true +fi + +# install dependencies +pip install tox +# locally install the package, needed for pycharm problem checking +python -m pip install . + +# run tests +tox || exit 1 +pip install mypy +# run mypy, static type checking +mypy src/json_schema_api + +# static analysis of code +#flake8 --show-source petstore_api/ + +# deactivate virtualenv +#if [ $DEACTIVE == true ]; then +# deactivate +#fi diff --git a/src/main/java/org/openapijsonschematools/codegen/common/CodegenConstants.java b/src/main/java/org/openapijsonschematools/codegen/common/CodegenConstants.java index 1ead49da6aa..080afef56e4 100644 --- a/src/main/java/org/openapijsonschematools/codegen/common/CodegenConstants.java +++ b/src/main/java/org/openapijsonschematools/codegen/common/CodegenConstants.java @@ -407,8 +407,8 @@ public static enum ENUM_PROPERTY_NAMING_TYPE {camelCase, PascalCase, snake_case, "If true (default), keep the old (incorrect) behaviour that 'additionalProperties' is set to false by default."; public static final String UNSUPPORTED_V310_SPEC_MSG = "Generation using 3.1.0 specs is in development and is not officially supported yet. " + - "If you would like to expedite development, please consider woking on the open issues in the 3.1.0 project: https://github.com/orgs/OpenAPITools/projects/4/views/1 " + - "and reach out to our team on Slack at https://join.slack.com/t/openapi-generator/shared_invite/zt-12jxxd7p2-XUeQM~4pzsU9x~eGLQqX2g"; + "If you would like to expedite development, please consider woking on the open issues in the 3.1.0 project: https://github.com/orgs/openapi-json-schema-tools/projects/4 " + + "and reach out to our team on Discord at https://discord.gg/mHB8WEQuYQ"; public static final String ENUM_UNKNOWN_DEFAULT_CASE = "enumUnknownDefaultCase"; public static final String ENUM_UNKNOWN_DEFAULT_CASE_DESC = "If the server adds new enum cases, that are unknown by an old spec/client, the client will fail to parse the network response." + diff --git a/src/main/resources/python/components/schemas/schema_cls/validate/validate.hbs b/src/main/resources/python/components/schemas/schema_cls/validate/validate.hbs index f677697e84f..4d0d14cac8a 100644 --- a/src/main/resources/python/components/schemas/schema_cls/validate/validate.hbs +++ b/src/main/resources/python/components/schemas/schema_cls/validate/validate.hbs @@ -40,7 +40,7 @@ def validate( @classmethod def validate( {{> components/schemas/schema_cls/validate/_validate_args }} -) -> {{#if ../arrayOutputJsonPathPiece}}{{../arrayOutputJsonPathPiece.camelCase}}{{else}}typing.Tuple[schemas.OUTPUT_BASE_TYPES]{{/if}}: ... +) -> {{#if ../arrayOutputJsonPathPiece}}{{../arrayOutputJsonPathPiece.camelCase}}{{else}}typing.Tuple[schemas.OUTPUT_BASE_TYPES, ...]{{/if}}: ... {{else}} {{#eq this "object"}} @typing.overload @@ -139,7 +139,7 @@ def validate( {{#if arrayOutputJsonPathPiece}} ) -> {{arrayOutputJsonPathPiece.camelCase}}: {{else}} -) -> typing.Tuple[schemas.OUTPUT_BASE_TYPES]: +) -> typing.Tuple[schemas.OUTPUT_BASE_TYPES, ...]: {{/if}} {{/eq}} {{/eq}} diff --git a/src/main/resources/python/configurations/api_configuration.hbs b/src/main/resources/python/configurations/api_configuration.hbs index 13b2d61ad38..7f7117093e8 100644 --- a/src/main/resources/python/configurations/api_configuration.hbs +++ b/src/main/resources/python/configurations/api_configuration.hbs @@ -190,7 +190,7 @@ class ApiConfiguration(object): self.security_scheme_info: SecuritySchemeInfo = security_scheme_info or SecuritySchemeInfo() self.security_index_info: SecurityIndexInfo = security_index_info or {'security': 0} {{else}} - self.security_scheme_info = {} + self.security_scheme_info: typing.Dict[str, typing.Any] = {} self.security_index_info = {'security': 0} {{/if}} # Server Info diff --git a/src/main/resources/python/schemas/validation.hbs b/src/main/resources/python/schemas/validation.hbs index 119d77ebefe..287f8e72f9c 100644 --- a/src/main/resources/python/schemas/validation.hbs +++ b/src/main/resources/python/schemas/validation.hbs @@ -1117,13 +1117,16 @@ def validate_contains( configuration=validation_metadata.configuration, validated_path_to_schemas=validation_metadata.validated_path_to_schemas ) - if item_validation_metadata.validation_ran_earlier(item_cls): + if item_validation_metadata.validation_ran_earlier(contains_cls): add_deeper_validated_schemas(item_validation_metadata, path_to_schemas) return path_to_schemas - other_path_to_schemas = item_cls._validate( - value, validation_metadata=item_validation_metadata) - update(path_to_schemas, other_path_to_schemas) - return path_to_schemas + try: + other_path_to_schemas = contains_cls._validate( + value, validation_metadata=item_validation_metadata) + update(path_to_schemas, other_path_to_schemas) + return path_to_schemas + except exceptions.OpenApiException: + pass if not array_contains_item: raise exceptions.ApiValueError( "Validation failed for contains keyword in class={} at path_to_item={}. No " From 4f7f35d83dd33762ef42b1baeca0db84638c7bc3 Mon Sep 17 00:00:00 2001 From: Justin Black Date: Sat, 19 Aug 2023 15:29:22 -0700 Subject: [PATCH 9/9] Docs regen --- docs/generators/java.md | 1 + docs/generators/jaxrs-jersey.md | 1 + docs/generators/jmeter.md | 1 + docs/generators/kotlin.md | 1 + docs/generators/python.md | 1 + .../configurations/api_configuration.py | 2 +- .../configurations/schema_configuration.py | 1 + .../src/unit_test_api/schemas/validation.py | 38 +++++++++++++++++- .../configurations/api_configuration.py | 2 +- .../configurations/schema_configuration.py | 1 + .../src/this_package/schemas/validation.py | 39 ++++++++++++++++++- .../configurations/schema_configuration.py | 1 + .../src/this_package/schemas/validation.py | 38 +++++++++++++++++- .../configurations/schema_configuration.py | 1 + .../src/petstore_api/schemas/validation.py | 38 +++++++++++++++++- .../generators/PythonClientGenerator.java | 1 + .../features/SchemaFeature.java | 5 ++- 17 files changed, 165 insertions(+), 7 deletions(-) diff --git a/docs/generators/java.md b/docs/generators/java.md index 4c7ba094af6..39dca02fcbc 100644 --- a/docs/generators/java.md +++ b/docs/generators/java.md @@ -316,6 +316,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |AdditionalProperties|✗|OAS2,OAS3 |AllOf|✗|OAS2,OAS3 |AnyOf|✗|OAS3 +|Contains|✗|OAS3 |Default|✗|OAS2,OAS3 |Discriminator|✓|OAS2,OAS3 |Enum|✓|OAS2,OAS3 diff --git a/docs/generators/jaxrs-jersey.md b/docs/generators/jaxrs-jersey.md index de8258f6184..b2601907a84 100644 --- a/docs/generators/jaxrs-jersey.md +++ b/docs/generators/jaxrs-jersey.md @@ -299,6 +299,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |AdditionalProperties|✗|OAS2,OAS3 |AllOf|✗|OAS2,OAS3 |AnyOf|✗|OAS3 +|Contains|✗|OAS3 |Default|✗|OAS2,OAS3 |Discriminator|✓|OAS2,OAS3 |Enum|✓|OAS2,OAS3 diff --git a/docs/generators/jmeter.md b/docs/generators/jmeter.md index 866f884d6b2..bb01c6cb064 100644 --- a/docs/generators/jmeter.md +++ b/docs/generators/jmeter.md @@ -158,6 +158,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |AdditionalProperties|✗|OAS2,OAS3 |AllOf|✗|OAS2,OAS3 |AnyOf|✗|OAS3 +|Contains|✗|OAS3 |Default|✗|OAS2,OAS3 |Discriminator|✓|OAS2,OAS3 |Enum|✓|OAS2,OAS3 diff --git a/docs/generators/kotlin.md b/docs/generators/kotlin.md index 83b16ec866c..ffbe8dae385 100644 --- a/docs/generators/kotlin.md +++ b/docs/generators/kotlin.md @@ -268,6 +268,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |AdditionalProperties|✗|OAS2,OAS3 |AllOf|✗|OAS2,OAS3 |AnyOf|✗|OAS3 +|Contains|✗|OAS3 |Default|✗|OAS2,OAS3 |Discriminator|✓|OAS2,OAS3 |Enum|✓|OAS2,OAS3 diff --git a/docs/generators/python.md b/docs/generators/python.md index 45179cd5279..a3f3d8356bc 100644 --- a/docs/generators/python.md +++ b/docs/generators/python.md @@ -227,6 +227,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |AdditionalProperties|✓|OAS2,OAS3 |AllOf|✓|OAS2,OAS3 |AnyOf|✓|OAS3 +|Contains|✓|OAS3 |Default|✓|OAS2,OAS3 |Discriminator|✓|OAS2,OAS3 |Enum|✓|OAS2,OAS3 diff --git a/samples/client/3_0_3_unit_test/python/src/unit_test_api/configurations/api_configuration.py b/samples/client/3_0_3_unit_test/python/src/unit_test_api/configurations/api_configuration.py index 30deb1c7d10..1abc7dcbc6d 100644 --- a/samples/client/3_0_3_unit_test/python/src/unit_test_api/configurations/api_configuration.py +++ b/samples/client/3_0_3_unit_test/python/src/unit_test_api/configurations/api_configuration.py @@ -66,7 +66,7 @@ def __init__( """Constructor """ # Authentication Settings - self.security_scheme_info = {} + self.security_scheme_info: typing.Dict[str, typing.Any] = {} self.security_index_info = {'security': 0} # Server Info self.server_info: ServerInfo = server_info or { diff --git a/samples/client/3_0_3_unit_test/python/src/unit_test_api/configurations/schema_configuration.py b/samples/client/3_0_3_unit_test/python/src/unit_test_api/configurations/schema_configuration.py index e6a0d5e8b95..6c253e8300f 100644 --- a/samples/client/3_0_3_unit_test/python/src/unit_test_api/configurations/schema_configuration.py +++ b/samples/client/3_0_3_unit_test/python/src/unit_test_api/configurations/schema_configuration.py @@ -16,6 +16,7 @@ 'additional_properties': 'additionalProperties', 'all_of': 'allOf', 'any_of': 'anyOf', + 'contains': 'contains', 'discriminator': 'discriminator', # default omitted because it has no validation impact 'enum_value_to_name': 'enum', diff --git a/samples/client/3_0_3_unit_test/python/src/unit_test_api/schemas/validation.py b/samples/client/3_0_3_unit_test/python/src/unit_test_api/schemas/validation.py index 913dedfc0dd..5c240b8b7a4 100644 --- a/samples/client/3_0_3_unit_test/python/src/unit_test_api/schemas/validation.py +++ b/samples/client/3_0_3_unit_test/python/src/unit_test_api/schemas/validation.py @@ -956,6 +956,41 @@ def validate_discriminator( return discriminated_cls._validate(arg, validation_metadata=updated_vm) +def validate_contains( + arg: typing.Any, + contains_cls: typing.Type[SchemaValidator], + cls: typing.Type, + validation_metadata: ValidationMetadata, +) -> typing.Optional[PathToSchemasType]: + if not isinstance(arg, tuple): + return None + contains_cls = _get_class(contains_cls) + path_to_schemas: PathToSchemasType = {} + array_contains_item = False + for i, value in enumerate(arg): + item_validation_metadata = ValidationMetadata( + path_to_item=validation_metadata.path_to_item+(i,), + configuration=validation_metadata.configuration, + validated_path_to_schemas=validation_metadata.validated_path_to_schemas + ) + if item_validation_metadata.validation_ran_earlier(contains_cls): + add_deeper_validated_schemas(item_validation_metadata, path_to_schemas) + return path_to_schemas + try: + other_path_to_schemas = contains_cls._validate( + value, validation_metadata=item_validation_metadata) + update(path_to_schemas, other_path_to_schemas) + return path_to_schemas + except exceptions.OpenApiException: + pass + if not array_contains_item: + raise exceptions.ApiValueError( + "Validation failed for contains keyword in class={} at path_to_item={}. No " + "items validated to the contains schema.".format(cls, validation_metadata.path_to_item) + ) + return path_to_schemas + + validator_type = typing.Callable[[typing.Any, typing.Any, type, ValidationMetadata], typing.Optional[PathToSchemasType]] json_schema_keyword_to_validator: typing.Mapping[str, validator_type] = { 'types': validate_types, @@ -982,5 +1017,6 @@ def validate_discriminator( 'any_of': validate_any_of, 'all_of': validate_all_of, 'not_': validate_not, - 'discriminator': validate_discriminator + 'discriminator': validate_discriminator, + 'contains': validate_contains } \ No newline at end of file diff --git a/samples/client/openapi_features/nonCompliantUseDiscriminatorIfCompositionFails/python/src/this_package/configurations/api_configuration.py b/samples/client/openapi_features/nonCompliantUseDiscriminatorIfCompositionFails/python/src/this_package/configurations/api_configuration.py index c4f1ac7931d..ee66a00c502 100644 --- a/samples/client/openapi_features/nonCompliantUseDiscriminatorIfCompositionFails/python/src/this_package/configurations/api_configuration.py +++ b/samples/client/openapi_features/nonCompliantUseDiscriminatorIfCompositionFails/python/src/this_package/configurations/api_configuration.py @@ -66,7 +66,7 @@ def __init__( """Constructor """ # Authentication Settings - self.security_scheme_info = {} + self.security_scheme_info: typing.Dict[str, typing.Any] = {} self.security_index_info = {'security': 0} # Server Info self.server_info: ServerInfo = server_info or { diff --git a/samples/client/openapi_features/nonCompliantUseDiscriminatorIfCompositionFails/python/src/this_package/configurations/schema_configuration.py b/samples/client/openapi_features/nonCompliantUseDiscriminatorIfCompositionFails/python/src/this_package/configurations/schema_configuration.py index b1db504bacf..6316b7cae74 100644 --- a/samples/client/openapi_features/nonCompliantUseDiscriminatorIfCompositionFails/python/src/this_package/configurations/schema_configuration.py +++ b/samples/client/openapi_features/nonCompliantUseDiscriminatorIfCompositionFails/python/src/this_package/configurations/schema_configuration.py @@ -16,6 +16,7 @@ 'additional_properties': 'additionalProperties', 'all_of': 'allOf', 'any_of': 'anyOf', + 'contains': 'contains', 'discriminator': 'discriminator', # default omitted because it has no validation impact 'enum_value_to_name': 'enum', diff --git a/samples/client/openapi_features/nonCompliantUseDiscriminatorIfCompositionFails/python/src/this_package/schemas/validation.py b/samples/client/openapi_features/nonCompliantUseDiscriminatorIfCompositionFails/python/src/this_package/schemas/validation.py index 8a7be08eb69..3b602f776f8 100644 --- a/samples/client/openapi_features/nonCompliantUseDiscriminatorIfCompositionFails/python/src/this_package/schemas/validation.py +++ b/samples/client/openapi_features/nonCompliantUseDiscriminatorIfCompositionFails/python/src/this_package/schemas/validation.py @@ -1033,6 +1033,42 @@ def validate_discriminator( return discriminated_cls._validate(arg, validation_metadata=updated_vm) +def validate_contains( + arg: typing.Any, + contains_cls: typing.Type[SchemaValidator], + cls: typing.Type, + validation_metadata: ValidationMetadata, + **kwargs +) -> typing.Optional[PathToSchemasType]: + if not isinstance(arg, tuple): + return None + contains_cls = _get_class(contains_cls) + path_to_schemas: PathToSchemasType = {} + array_contains_item = False + for i, value in enumerate(arg): + item_validation_metadata = ValidationMetadata( + path_to_item=validation_metadata.path_to_item+(i,), + configuration=validation_metadata.configuration, + validated_path_to_schemas=validation_metadata.validated_path_to_schemas + ) + if item_validation_metadata.validation_ran_earlier(contains_cls): + add_deeper_validated_schemas(item_validation_metadata, path_to_schemas) + return path_to_schemas + try: + other_path_to_schemas = contains_cls._validate( + value, validation_metadata=item_validation_metadata) + update(path_to_schemas, other_path_to_schemas) + return path_to_schemas + except exceptions.OpenApiException: + pass + if not array_contains_item: + raise exceptions.ApiValueError( + "Validation failed for contains keyword in class={} at path_to_item={}. No " + "items validated to the contains schema.".format(cls, validation_metadata.path_to_item) + ) + return path_to_schemas + + validator_type = typing.Callable[[typing.Any, typing.Any, type, ValidationMetadata], typing.Optional[PathToSchemasType]] json_schema_keyword_to_validator: typing.Mapping[str, validator_type] = { 'types': validate_types, @@ -1059,5 +1095,6 @@ def validate_discriminator( 'any_of': validate_any_of, 'all_of': validate_all_of, 'not_': validate_not, - 'discriminator': validate_discriminator + 'discriminator': validate_discriminator, + 'contains': validate_contains } \ No newline at end of file diff --git a/samples/client/openapi_features/security/python/src/this_package/configurations/schema_configuration.py b/samples/client/openapi_features/security/python/src/this_package/configurations/schema_configuration.py index 2349b5701e0..22ff15cd614 100644 --- a/samples/client/openapi_features/security/python/src/this_package/configurations/schema_configuration.py +++ b/samples/client/openapi_features/security/python/src/this_package/configurations/schema_configuration.py @@ -16,6 +16,7 @@ 'additional_properties': 'additionalProperties', 'all_of': 'allOf', 'any_of': 'anyOf', + 'contains': 'contains', 'discriminator': 'discriminator', # default omitted because it has no validation impact 'enum_value_to_name': 'enum', diff --git a/samples/client/openapi_features/security/python/src/this_package/schemas/validation.py b/samples/client/openapi_features/security/python/src/this_package/schemas/validation.py index 4f057e65811..93fe5cbb27c 100644 --- a/samples/client/openapi_features/security/python/src/this_package/schemas/validation.py +++ b/samples/client/openapi_features/security/python/src/this_package/schemas/validation.py @@ -956,6 +956,41 @@ def validate_discriminator( return discriminated_cls._validate(arg, validation_metadata=updated_vm) +def validate_contains( + arg: typing.Any, + contains_cls: typing.Type[SchemaValidator], + cls: typing.Type, + validation_metadata: ValidationMetadata, +) -> typing.Optional[PathToSchemasType]: + if not isinstance(arg, tuple): + return None + contains_cls = _get_class(contains_cls) + path_to_schemas: PathToSchemasType = {} + array_contains_item = False + for i, value in enumerate(arg): + item_validation_metadata = ValidationMetadata( + path_to_item=validation_metadata.path_to_item+(i,), + configuration=validation_metadata.configuration, + validated_path_to_schemas=validation_metadata.validated_path_to_schemas + ) + if item_validation_metadata.validation_ran_earlier(contains_cls): + add_deeper_validated_schemas(item_validation_metadata, path_to_schemas) + return path_to_schemas + try: + other_path_to_schemas = contains_cls._validate( + value, validation_metadata=item_validation_metadata) + update(path_to_schemas, other_path_to_schemas) + return path_to_schemas + except exceptions.OpenApiException: + pass + if not array_contains_item: + raise exceptions.ApiValueError( + "Validation failed for contains keyword in class={} at path_to_item={}. No " + "items validated to the contains schema.".format(cls, validation_metadata.path_to_item) + ) + return path_to_schemas + + validator_type = typing.Callable[[typing.Any, typing.Any, type, ValidationMetadata], typing.Optional[PathToSchemasType]] json_schema_keyword_to_validator: typing.Mapping[str, validator_type] = { 'types': validate_types, @@ -982,5 +1017,6 @@ def validate_discriminator( 'any_of': validate_any_of, 'all_of': validate_all_of, 'not_': validate_not, - 'discriminator': validate_discriminator + 'discriminator': validate_discriminator, + 'contains': validate_contains } \ No newline at end of file diff --git a/samples/client/petstore/python/src/petstore_api/configurations/schema_configuration.py b/samples/client/petstore/python/src/petstore_api/configurations/schema_configuration.py index 337118ec54a..3ff81d2cf56 100644 --- a/samples/client/petstore/python/src/petstore_api/configurations/schema_configuration.py +++ b/samples/client/petstore/python/src/petstore_api/configurations/schema_configuration.py @@ -16,6 +16,7 @@ 'additional_properties': 'additionalProperties', 'all_of': 'allOf', 'any_of': 'anyOf', + 'contains': 'contains', 'discriminator': 'discriminator', # default omitted because it has no validation impact 'enum_value_to_name': 'enum', diff --git a/samples/client/petstore/python/src/petstore_api/schemas/validation.py b/samples/client/petstore/python/src/petstore_api/schemas/validation.py index ea1ff8ec02a..5cca1c42d35 100644 --- a/samples/client/petstore/python/src/petstore_api/schemas/validation.py +++ b/samples/client/petstore/python/src/petstore_api/schemas/validation.py @@ -956,6 +956,41 @@ def validate_discriminator( return discriminated_cls._validate(arg, validation_metadata=updated_vm) +def validate_contains( + arg: typing.Any, + contains_cls: typing.Type[SchemaValidator], + cls: typing.Type, + validation_metadata: ValidationMetadata, +) -> typing.Optional[PathToSchemasType]: + if not isinstance(arg, tuple): + return None + contains_cls = _get_class(contains_cls) + path_to_schemas: PathToSchemasType = {} + array_contains_item = False + for i, value in enumerate(arg): + item_validation_metadata = ValidationMetadata( + path_to_item=validation_metadata.path_to_item+(i,), + configuration=validation_metadata.configuration, + validated_path_to_schemas=validation_metadata.validated_path_to_schemas + ) + if item_validation_metadata.validation_ran_earlier(contains_cls): + add_deeper_validated_schemas(item_validation_metadata, path_to_schemas) + return path_to_schemas + try: + other_path_to_schemas = contains_cls._validate( + value, validation_metadata=item_validation_metadata) + update(path_to_schemas, other_path_to_schemas) + return path_to_schemas + except exceptions.OpenApiException: + pass + if not array_contains_item: + raise exceptions.ApiValueError( + "Validation failed for contains keyword in class={} at path_to_item={}. No " + "items validated to the contains schema.".format(cls, validation_metadata.path_to_item) + ) + return path_to_schemas + + validator_type = typing.Callable[[typing.Any, typing.Any, type, ValidationMetadata], typing.Optional[PathToSchemasType]] json_schema_keyword_to_validator: typing.Mapping[str, validator_type] = { 'types': validate_types, @@ -982,5 +1017,6 @@ def validate_discriminator( 'any_of': validate_any_of, 'all_of': validate_all_of, 'not_': validate_not, - 'discriminator': validate_discriminator + 'discriminator': validate_discriminator, + 'contains': validate_contains } \ No newline at end of file diff --git a/src/main/java/org/openapijsonschematools/codegen/generators/PythonClientGenerator.java b/src/main/java/org/openapijsonschematools/codegen/generators/PythonClientGenerator.java index d6676782f94..5ef3a3f8a5f 100644 --- a/src/main/java/org/openapijsonschematools/codegen/generators/PythonClientGenerator.java +++ b/src/main/java/org/openapijsonschematools/codegen/generators/PythonClientGenerator.java @@ -139,6 +139,7 @@ public PythonClientGenerator() { SchemaFeature.AdditionalProperties, SchemaFeature.AllOf, SchemaFeature.AnyOf, + SchemaFeature.Contains, SchemaFeature.Default, SchemaFeature.Discriminator, SchemaFeature.Enum, diff --git a/src/main/java/org/openapijsonschematools/codegen/generators/generatormetadata/features/SchemaFeature.java b/src/main/java/org/openapijsonschematools/codegen/generators/generatormetadata/features/SchemaFeature.java index 59857e55c76..3705e95f165 100644 --- a/src/main/java/org/openapijsonschematools/codegen/generators/generatormetadata/features/SchemaFeature.java +++ b/src/main/java/org/openapijsonschematools/codegen/generators/generatormetadata/features/SchemaFeature.java @@ -16,8 +16,8 @@ package org.openapijsonschematools.codegen.generators.generatormetadata.features; -import org.openapijsonschematools.codegen.generators.generatormetadata.features.annotations.OAS3; import org.openapijsonschematools.codegen.generators.generatormetadata.features.annotations.OAS2; +import org.openapijsonschematools.codegen.generators.generatormetadata.features.annotations.OAS3; /** * Defines special circumstances handled by the generator. @@ -40,6 +40,9 @@ public enum SchemaFeature { @OAS3 AnyOf, + @OAS3 + Contains, + @OAS2 @OAS3 Default,