From 31b15d33a126f52bfc7568c5b7aa180794422ef0 Mon Sep 17 00:00:00 2001 From: Vadim Troshchinskiy Date: Mon, 30 Dec 2024 08:33:48 +0100 Subject: [PATCH] Add packages --- packages/flask-executor/make_orig.sh | 17 + .../.github/workflows/tests.yml | 28 + .../.gitignore | 105 + .../opengnsys-flask-executor-0.10.0/LICENSE | 21 + .../opengnsys-flask-executor-0.10.0/README.md | 134 + .../debian/changelog | 7 + .../debian/control | 28 + .../debian/copyright | 208 + .../debian/rules | 22 + .../debian/source/format | 1 + .../debian/tests/control | 2 + .../debian/tests/upstream-tests | 14 + .../docs/Makefile | 20 + .../docs/api/flask_executor.rst | 30 + .../docs/api/modules.rst | 7 + .../docs/conf.py | 172 + .../docs/index.rst | 187 + .../flask_executor/__init__.py | 5 + .../flask_executor/executor.py | 273 ++ .../flask_executor/futures.py | 107 + .../flask_executor/helpers.py | 37 + .../opengnsys-flask-executor-0.10.0/setup.py | 52 + .../tests/__init__.py | 0 .../tests/conftest.py | 18 + .../tests/test_executor.py | 376 ++ .../tests/test_futures.py | 97 + ...pengnsys-flask-executor_0.10.0.orig.tar.xz | Bin 0 -> 18976 bytes packages/flask-restx/make_orig.sh | 18 + .../opengnsys-flask-restx-1.3.0/.editorconfig | 21 + .../.github/ISSUE_TEMPLATE/bug-report.md | 44 + .../.github/ISSUE_TEMPLATE/feature_request.md | 20 + .../.github/ISSUE_TEMPLATE/question.md | 14 + .../PULL_REQUEST_TEMPLATE.md | 25 + .../.github/workflows/black.yml | 10 + .../.github/workflows/release.yml | 28 + .../.github/workflows/test.yml | 74 + .../opengnsys-flask-restx-1.3.0/.gitignore | 70 + .../opengnsys-flask-restx-1.3.0/.pyup.yml | 63 + .../opengnsys-flask-restx-1.3.0/CHANGELOG.rst | 342 ++ .../CONTRIBUTING.rst | 135 + .../opengnsys-flask-restx-1.3.0/LICENSE | 32 + .../opengnsys-flask-restx-1.3.0/MANIFEST.in | 5 + .../opengnsys-flask-restx-1.3.0/README.rst | 216 + .../opengnsys-flask-restx-1.3.0/bumpr.rc | 25 + .../opengnsys-flask-restx-1.3.0/coverage.rc | 25 + .../debian/changelog | 7 + .../debian/control | 34 + .../debian/copyright | 208 + .../opengnsys-flask-restx-1.3.0/debian/rules | 25 + .../debian/source/format | 1 + .../debian/tests/control | 2 + .../debian/tests/upstream-tests | 14 + .../opengnsys-flask-restx-1.3.0/doc/Makefile | 177 + .../doc/_static/apple-180.png | Bin 0 -> 12581 bytes .../doc/_static/favicon-128.png | Bin 0 -> 7582 bytes .../doc/_static/favicon-196.png | Bin 0 -> 10099 bytes .../doc/_static/favicon-512.png | Bin 0 -> 23756 bytes .../doc/_static/favicon-64.png | Bin 0 -> 3207 bytes .../doc/_static/favicon.ico | Bin 0 -> 16958 bytes .../doc/_static/logo-512-nobg.png | Bin 0 -> 42823 bytes .../doc/_static/logo-512.png | Bin 0 -> 41788 bytes .../_static/screenshot-apidoc-quickstart.png | Bin 0 -> 131539 bytes .../doc/_themes/restx/badges.html | 7 + .../doc/_themes/restx/layout.html | 10 + .../doc/_themes/restx/static/restx.css | 12 + .../doc/_themes/restx/theme.conf | 7 + .../opengnsys-flask-restx-1.3.0/doc/api.rst | 98 + .../opengnsys-flask-restx-1.3.0/doc/conf.py | 342 ++ .../doc/configuration.rst | 65 + .../doc/contributing.rst | 1 + .../doc/errors.rst | 227 ++ .../doc/example.rst | 108 + .../opengnsys-flask-restx-1.3.0/doc/index.rst | 103 + .../doc/installation.rst | 24 + .../doc/logging.rst | 103 + .../opengnsys-flask-restx-1.3.0/doc/make.bat | 242 ++ .../doc/marshalling.rst | 542 +++ .../opengnsys-flask-restx-1.3.0/doc/mask.rst | 106 + .../doc/parsing.rst | 340 ++ .../doc/postman.rst | 18 + .../doc/quickstart.rst | 344 ++ .../doc/scaling.rst | 275 ++ .../doc/swagger.rst | 1070 +++++ .../examples/resource_class_kwargs | 78 + .../examples/todo.py | 95 + .../examples/todo_blueprint.py | 96 + .../examples/todo_simple.py | 43 + .../examples/todomvc.py | 103 + .../examples/xml_representation.py | 41 + .../examples/zoo_app/complex.py | 11 + .../examples/zoo_app/requirements.txt | 14 + .../examples/zoo_app/zoo/__init__.py | 13 + .../examples/zoo_app/zoo/cat.py | 38 + .../examples/zoo_app/zoo/dog.py | 38 + .../flask_restx/__about__.py | 5 + .../flask_restx/__init__.py | 35 + .../flask_restx/_http.py | 186 + .../flask_restx/api.py | 977 +++++ .../flask_restx/apidoc.py | 35 + .../flask_restx/cors.py | 62 + .../flask_restx/errors.py | 56 + .../flask_restx/fields.py | 902 +++++ .../flask_restx/inputs.py | 615 +++ .../flask_restx/marshalling.py | 305 ++ .../flask_restx/mask.py | 187 + .../flask_restx/model.py | 285 ++ .../flask_restx/namespace.py | 375 ++ .../flask_restx/postman.py | 202 + .../flask_restx/representations.py | 26 + .../flask_restx/reqparse.py | 458 +++ .../flask_restx/resource.py | 85 + .../flask_restx/schemas/__init__.py | 120 + .../flask_restx/schemas/oas-2.0.json | 1607 ++++++++ .../flask_restx/swagger.py | 745 ++++ .../flask_restx/templates/swagger-ui-css.html | 32 + .../templates/swagger-ui-libs.html | 11 + .../flask_restx/templates/swagger-ui.html | 84 + .../flask_restx/utils.py | 187 + .../opengnsys-flask-restx-1.3.0/package.json | 16 + .../readthedocs.pip | 3 + .../requirements/develop.pip | 2 + .../requirements/doc.pip | 3 + .../requirements/install.pip | 6 + .../requirements/test.pip | 13 + .../opengnsys-flask-restx-1.3.0/setup.cfg | 19 + .../opengnsys-flask-restx-1.3.0/setup.py | 114 + .../opengnsys-flask-restx-1.3.0/tasks.py | 220 + .../tests/__init__.py | 0 .../tests/benchmarks/bench_marshalling.py | 56 + .../tests/benchmarks/bench_swagger.py | 97 + .../tests/conftest.py | 95 + .../tests/legacy/test_api_legacy.py | 388 ++ .../tests/legacy/test_api_with_blueprint.py | 160 + .../tests/postman-v1.schema.json | 626 +++ .../tests/test_accept.py | 163 + .../tests/test_api.py | 351 ++ .../tests/test_apidoc.py | 149 + .../tests/test_cors.py | 50 + .../tests/test_errors.py | 758 ++++ .../tests/test_fields.py | 1581 ++++++++ .../tests/test_fields_mask.py | 1113 +++++ .../tests/test_inputs.py | 1160 ++++++ .../tests/test_logging.py | 134 + .../tests/test_marshalling.py | 537 +++ .../tests/test_model.py | 695 ++++ .../tests/test_namespace.py | 180 + .../tests/test_payload.py | 377 ++ .../tests/test_postman.py | 410 ++ .../tests/test_reqparse.py | 1122 ++++++ .../tests/test_schemas.py | 52 + .../tests/test_swagger.py | 3579 +++++++++++++++++ .../tests/test_swagger_utils.py | 192 + .../tests/test_utils.py | 119 + .../opengnsys-flask-restx-1.3.0/tox.ini | 24 + .../opengnsys-flask-restx_1.3.0.orig.tar.xz | Bin 0 -> 364044 bytes 155 files changed, 30363 insertions(+) create mode 100755 packages/flask-executor/make_orig.sh create mode 100644 packages/flask-executor/opengnsys-flask-executor-0.10.0/.github/workflows/tests.yml create mode 100644 packages/flask-executor/opengnsys-flask-executor-0.10.0/.gitignore create mode 100644 packages/flask-executor/opengnsys-flask-executor-0.10.0/LICENSE create mode 100644 packages/flask-executor/opengnsys-flask-executor-0.10.0/README.md create mode 100644 packages/flask-executor/opengnsys-flask-executor-0.10.0/debian/changelog create mode 100644 packages/flask-executor/opengnsys-flask-executor-0.10.0/debian/control create mode 100644 packages/flask-executor/opengnsys-flask-executor-0.10.0/debian/copyright create mode 100755 packages/flask-executor/opengnsys-flask-executor-0.10.0/debian/rules create mode 100644 packages/flask-executor/opengnsys-flask-executor-0.10.0/debian/source/format create mode 100644 packages/flask-executor/opengnsys-flask-executor-0.10.0/debian/tests/control create mode 100755 packages/flask-executor/opengnsys-flask-executor-0.10.0/debian/tests/upstream-tests create mode 100644 packages/flask-executor/opengnsys-flask-executor-0.10.0/docs/Makefile create mode 100644 packages/flask-executor/opengnsys-flask-executor-0.10.0/docs/api/flask_executor.rst create mode 100644 packages/flask-executor/opengnsys-flask-executor-0.10.0/docs/api/modules.rst create mode 100644 packages/flask-executor/opengnsys-flask-executor-0.10.0/docs/conf.py create mode 100644 packages/flask-executor/opengnsys-flask-executor-0.10.0/docs/index.rst create mode 100644 packages/flask-executor/opengnsys-flask-executor-0.10.0/flask_executor/__init__.py create mode 100644 packages/flask-executor/opengnsys-flask-executor-0.10.0/flask_executor/executor.py create mode 100644 packages/flask-executor/opengnsys-flask-executor-0.10.0/flask_executor/futures.py create mode 100644 packages/flask-executor/opengnsys-flask-executor-0.10.0/flask_executor/helpers.py create mode 100644 packages/flask-executor/opengnsys-flask-executor-0.10.0/setup.py create mode 100644 packages/flask-executor/opengnsys-flask-executor-0.10.0/tests/__init__.py create mode 100644 packages/flask-executor/opengnsys-flask-executor-0.10.0/tests/conftest.py create mode 100644 packages/flask-executor/opengnsys-flask-executor-0.10.0/tests/test_executor.py create mode 100644 packages/flask-executor/opengnsys-flask-executor-0.10.0/tests/test_futures.py create mode 100644 packages/flask-executor/opengnsys-flask-executor_0.10.0.orig.tar.xz create mode 100755 packages/flask-restx/make_orig.sh create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/.editorconfig create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/.github/ISSUE_TEMPLATE/bug-report.md create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/.github/ISSUE_TEMPLATE/feature_request.md create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/.github/ISSUE_TEMPLATE/question.md create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/.github/PULL_REQUEST_TEMPLATE/PULL_REQUEST_TEMPLATE.md create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/.github/workflows/black.yml create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/.github/workflows/release.yml create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/.github/workflows/test.yml create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/.gitignore create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/.pyup.yml create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/CHANGELOG.rst create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/CONTRIBUTING.rst create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/LICENSE create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/MANIFEST.in create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/README.rst create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/bumpr.rc create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/coverage.rc create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/debian/changelog create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/debian/control create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/debian/copyright create mode 100755 packages/flask-restx/opengnsys-flask-restx-1.3.0/debian/rules create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/debian/source/format create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/debian/tests/control create mode 100755 packages/flask-restx/opengnsys-flask-restx-1.3.0/debian/tests/upstream-tests create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/Makefile create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/_static/apple-180.png create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/_static/favicon-128.png create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/_static/favicon-196.png create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/_static/favicon-512.png create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/_static/favicon-64.png create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/_static/favicon.ico create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/_static/logo-512-nobg.png create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/_static/logo-512.png create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/_static/screenshot-apidoc-quickstart.png create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/_themes/restx/badges.html create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/_themes/restx/layout.html create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/_themes/restx/static/restx.css create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/_themes/restx/theme.conf create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/api.rst create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/conf.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/configuration.rst create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/contributing.rst create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/errors.rst create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/example.rst create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/index.rst create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/installation.rst create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/logging.rst create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/make.bat create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/marshalling.rst create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/mask.rst create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/parsing.rst create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/postman.rst create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/quickstart.rst create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/scaling.rst create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/swagger.rst create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/examples/resource_class_kwargs create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/examples/todo.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/examples/todo_blueprint.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/examples/todo_simple.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/examples/todomvc.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/examples/xml_representation.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/examples/zoo_app/complex.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/examples/zoo_app/requirements.txt create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/examples/zoo_app/zoo/__init__.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/examples/zoo_app/zoo/cat.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/examples/zoo_app/zoo/dog.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/__about__.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/__init__.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/_http.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/api.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/apidoc.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/cors.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/errors.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/fields.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/inputs.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/marshalling.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/mask.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/model.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/namespace.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/postman.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/representations.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/reqparse.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/resource.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/schemas/__init__.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/schemas/oas-2.0.json create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/swagger.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/templates/swagger-ui-css.html create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/templates/swagger-ui-libs.html create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/templates/swagger-ui.html create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/utils.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/package.json create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/readthedocs.pip create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/requirements/develop.pip create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/requirements/doc.pip create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/requirements/install.pip create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/requirements/test.pip create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/setup.cfg create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/setup.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/tasks.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/__init__.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/benchmarks/bench_marshalling.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/benchmarks/bench_swagger.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/conftest.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/legacy/test_api_legacy.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/legacy/test_api_with_blueprint.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/postman-v1.schema.json create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_accept.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_api.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_apidoc.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_cors.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_errors.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_fields.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_fields_mask.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_inputs.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_logging.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_marshalling.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_model.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_namespace.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_payload.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_postman.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_reqparse.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_schemas.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_swagger.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_swagger_utils.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_utils.py create mode 100644 packages/flask-restx/opengnsys-flask-restx-1.3.0/tox.ini create mode 100644 packages/flask-restx/opengnsys-flask-restx_1.3.0.orig.tar.xz diff --git a/packages/flask-executor/make_orig.sh b/packages/flask-executor/make_orig.sh new file mode 100755 index 0000000..a2750ee --- /dev/null +++ b/packages/flask-executor/make_orig.sh @@ -0,0 +1,17 @@ +#!/bin/bash +set -e + +git clone https://github.com/dchevell/flask-executor opengnsys-flask-executor +cd opengnsys-flask-executor +version=`python3 ./setup.py --version` +cd .. + +if [ -d "opengnsys-flask-executor-${version}" ] ; then + echo "Directory opengnsys-flask-executor-${version} already exists, won't overwrite" + exit 1 +else + rm -rf opengnsys-flask-executor/.git + mv opengnsys-flask-executor "opengnsys-flask-executor-${version}" + tar -c --xz -v -f "opengnsys-flask-executor_${version}.orig.tar.xz" "opengnsys-flask-executor-${version}" +fi + diff --git a/packages/flask-executor/opengnsys-flask-executor-0.10.0/.github/workflows/tests.yml b/packages/flask-executor/opengnsys-flask-executor-0.10.0/.github/workflows/tests.yml new file mode 100644 index 0000000..6f8df38 --- /dev/null +++ b/packages/flask-executor/opengnsys-flask-executor-0.10.0/.github/workflows/tests.yml @@ -0,0 +1,28 @@ +name: Flask-Executor tests + +on: [push] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.7", "3.8", "3.9", "3.10"] + flask-version: ["<2.2", ">=2.2"] + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -q "flask ${{ matrix.flask-version }}" + pip install -e .[test] + - name: Test with pytest + run: | + pytest --cov=flask_executor/ --cov-report=xml + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 \ No newline at end of file diff --git a/packages/flask-executor/opengnsys-flask-executor-0.10.0/.gitignore b/packages/flask-executor/opengnsys-flask-executor-0.10.0/.gitignore new file mode 100644 index 0000000..075e66e --- /dev/null +++ b/packages/flask-executor/opengnsys-flask-executor-0.10.0/.gitignore @@ -0,0 +1,105 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# 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 + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ diff --git a/packages/flask-executor/opengnsys-flask-executor-0.10.0/LICENSE b/packages/flask-executor/opengnsys-flask-executor-0.10.0/LICENSE new file mode 100644 index 0000000..334a087 --- /dev/null +++ b/packages/flask-executor/opengnsys-flask-executor-0.10.0/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Dave Chevell + +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. diff --git a/packages/flask-executor/opengnsys-flask-executor-0.10.0/README.md b/packages/flask-executor/opengnsys-flask-executor-0.10.0/README.md new file mode 100644 index 0000000..b545dbc --- /dev/null +++ b/packages/flask-executor/opengnsys-flask-executor-0.10.0/README.md @@ -0,0 +1,134 @@ +Flask-Executor +============== + +[![Build Status](https://github.com/dchevell/flask-executor/actions/workflows/tests.yml/badge.svg)](https://github.com/dchevell/flask-executor/actions/workflows/tests.yml) +[![codecov](https://codecov.io/gh/dchevell/flask-executor/branch/master/graph/badge.svg)](https://codecov.io/gh/dchevell/flask-executor) +[![PyPI Version](https://img.shields.io/pypi/v/Flask-Executor.svg)](https://pypi.python.org/pypi/Flask-Executor) +[![GitHub license](https://img.shields.io/github/license/dchevell/flask-executor.svg)](https://github.com/dchevell/flask-executor/blob/master/LICENSE) + +Sometimes you need a simple task queue without the overhead of separate worker processes or powerful-but-complex libraries beyond your requirements. Flask-Executor is an easy to use wrapper for the `concurrent.futures` module that lets you initialise and configure executors via common Flask application patterns. It's a great way to get up and running fast with a lightweight in-process task queue. + +Installation +------------ + +Flask-Executor is available on PyPI and can be installed with: + + pip install flask-executor + + +Quick start +----------- + +Here's a quick example of using Flask-Executor inside your Flask application: + +```python +from flask import Flask +from flask_executor import Executor + +app = Flask(__name__) + +executor = Executor(app) + + +def send_email(recipient, subject, body): + # Magic to send an email + return True + + +@app.route('/signup') +def signup(): + # Do signup form + executor.submit(send_email, recipient, subject, body) +``` + + +Contexts +-------- + +When calling `submit()` or `map()` Flask-Executor will wrap `ThreadPoolExecutor` callables with a +copy of both the current application context and current request context. Code that must be run in +these contexts or that depends on information or configuration stored in `flask.current_app`, +`flask.request` or `flask.g` can be submitted to the executor without modification. + +Note: due to limitations in Python's default object serialisation and a lack of shared memory space between subprocesses, contexts cannot be pushed to `ProcessPoolExecutor()` workers. + + +Futures +------- + +You may want to preserve access to Futures returned from the executor, so that you can retrieve the +results in a different part of your application. Flask-Executor allows Futures to be stored within +the executor itself and provides methods for querying and returning them in different parts of your +app:: + +```python +@app.route('/start-task') +def start_task(): + executor.submit_stored('calc_power', pow, 323, 1235) + return jsonify({'result':'success'}) + +@app.route('/get-result') +def get_result(): + if not executor.futures.done('calc_power'): + return jsonify({'status': executor.futures._state('calc_power')}) + future = executor.futures.pop('calc_power') + return jsonify({'status': done, 'result': future.result()}) +``` + + +Decoration +---------- + +Flask-Executor lets you decorate methods in the same style as distributed task queues like +Celery: + +```python +@executor.job +def fib(n): + if n <= 2: + return 1 + else: + return fib(n-1) + fib(n-2) + +@app.route('/decorate_fib') +def decorate_fib(): + fib.submit(5) + fib.submit_stored('fibonacci', 5) + fib.map(range(1, 6)) + return 'OK' +``` + + +Default Callbacks +----------------- + +Future objects can have callbacks attached by using `Future.add_done_callback`. Flask-Executor +lets you specify default callbacks that will be applied to all new futures created by the executor: + +```python +def some_callback(future): + # do something with future + +executor.add_default_done_callback(some_callback) + +# Callback will be added to the below task automatically +executor.submit(pow, 323, 1235) +``` + + +Propagate Exceptions +-------------------- + +Normally any exceptions thrown by background threads or processes will be swallowed unless explicitly +checked for. To instead surface all exceptions thrown by background tasks, Flask-Executor can add +a special default callback that raises any exceptions thrown by tasks submitted to the executor:: + +```python +app.config['EXECUTOR_PROPAGATE_EXCEPTIONS'] = True +``` + + +Documentation +------------- + +Check out the full documentation at [flask-executor.readthedocs.io](https://flask-executor.readthedocs.io)! diff --git a/packages/flask-executor/opengnsys-flask-executor-0.10.0/debian/changelog b/packages/flask-executor/opengnsys-flask-executor-0.10.0/debian/changelog new file mode 100644 index 0000000..c0a0cbe --- /dev/null +++ b/packages/flask-executor/opengnsys-flask-executor-0.10.0/debian/changelog @@ -0,0 +1,7 @@ +opengnsys-flask-executor (0.10.0) UNRELEASED; urgency=medium + + Initial version + * + * + + -- Vadim Troshchinskiy Tue, 23 Dec 2024 10:47:04 +0000 diff --git a/packages/flask-executor/opengnsys-flask-executor-0.10.0/debian/control b/packages/flask-executor/opengnsys-flask-executor-0.10.0/debian/control new file mode 100644 index 0000000..863bb0c --- /dev/null +++ b/packages/flask-executor/opengnsys-flask-executor-0.10.0/debian/control @@ -0,0 +1,28 @@ +Source: opengnsys-flask-executor +Maintainer: OpenGnsys +Section: python +Priority: optional +Build-Depends: debhelper-compat (= 12), + dh-python, + libarchive-dev, + python3-all, + python3-mock, + python3-pytest, + python3-setuptools +Standards-Version: 4.5.0 +Rules-Requires-Root: no +Homepage: https://github.com/vojtechtrefny/pyblkid +Vcs-Browser: https://github.com/vojtechtrefny/pyblkid +Vcs-Git: https://github.com/vojtechtrefny/pyblkid + +Package: opengnsys-flask-executor +Architecture: all +Depends: ${lib:Depends}, ${misc:Depends}, ${python3:Depends} +Description: Python3 Flask-Executor module + Sometimes you need a simple task queue without the overhead of separate worker + processes or powerful-but-complex libraries beyond your requirements. + . + Flask-Executor is an easy to use wrapper for the concurrent.futures module that + lets you initialise and configure executors via common Flask application patterns. + It's a great way to get up and running fast with a lightweight in-process task queue. + . diff --git a/packages/flask-executor/opengnsys-flask-executor-0.10.0/debian/copyright b/packages/flask-executor/opengnsys-flask-executor-0.10.0/debian/copyright new file mode 100644 index 0000000..a152e2b --- /dev/null +++ b/packages/flask-executor/opengnsys-flask-executor-0.10.0/debian/copyright @@ -0,0 +1,208 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: python-libarchive-c +Source: https://github.com/Changaco/python-libarchive-c + +Files: * +Copyright: 2014-2018 Changaco +License: CC-0 + +Files: tests/surrogateescape.py +Copyright: 2015 Changaco + 2011-2013 Victor Stinner +License: BSD-2-clause or PSF-2 + +Files: debian/* +Copyright: 2015 Jerémy Bobbio + 2019 Mattia Rizzolo +License: permissive + Copying and distribution of this package, with or without + modification, are permitted in any medium without royalty + provided the copyright notice and this notice are + preserved. + +License: BSD-2-clause + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + . + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + SUCH DAMAGE. + +License: PSF-2 + 1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), + and the Individual or Organization ("Licensee") accessing and otherwise using + this software ("Python") in source or binary form and its associated + documentation. + . + 2. Subject to the terms and conditions of this License Agreement, PSF hereby + grants Licensee a nonexclusive, royalty-free, world-wide license to + reproduce, analyze, test, perform and/or display publicly, prepare derivative + works, distribute, and otherwise use Python alone or in any derivative + version, provided, however, that PSF's License Agreement and PSF's notice of + copyright, i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006 Python + Software Foundation; All Rights Reserved" are retained in Python alone or in + any derivative version prepared by Licensee. + . + 3. In the event Licensee prepares a derivative work that is based on or + incorporates Python or any part thereof, and wants to make the derivative + work available to others as provided herein, then Licensee hereby agrees to + include in any such work a brief summary of the changes made to Python. + . + 4. PSF is making Python available to Licensee on an "AS IS" basis. PSF MAKES + NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT + NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY OF + MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF + PYTHON WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. + . + 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON FOR ANY + INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF + MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, OR ANY DERIVATIVE + THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + . + 6. This License Agreement will automatically terminate upon a material breach + of its terms and conditions. + . + 7. Nothing in this License Agreement shall be deemed to create any + relationship of agency, partnership, or joint venture between PSF and + Licensee. This License Agreement does not grant permission to use PSF + trademarks or trade name in a trademark sense to endorse or promote products + or services of Licensee, or any third party. + . + 8. By copying, installing or otherwise using Python, Licensee agrees to be + bound by the terms and conditions of this License Agreement. + +License: CC-0 + Statement of Purpose + . + The laws of most jurisdictions throughout the world automatically + confer exclusive Copyright and Related Rights (defined below) upon + the creator and subsequent owner(s) (each and all, an "owner") of an + original work of authorship and/or a database (each, a "Work"). + . + Certain owners wish to permanently relinquish those rights to a Work + for the purpose of contributing to a commons of creative, cultural + and scientific works ("Commons") that the public can reliably and + without fear of later claims of infringement build upon, modify, + incorporate in other works, reuse and redistribute as freely as + possible in any form whatsoever and for any purposes, including + without limitation commercial purposes. These owners may contribute + to the Commons to promote the ideal of a free culture and the further + production of creative, cultural and scientific works, or to gain + reputation or greater distribution for their Work in part through the + use and efforts of others. + . + For these and/or other purposes and motivations, and without any + expectation of additional consideration or compensation, the person + associating CC0 with a Work (the "Affirmer"), to the extent that he + or she is an owner of Copyright and Related Rights in the Work, + voluntarily elects to apply CC0 to the Work and publicly distribute + the Work under its terms, with knowledge of his or her Copyright and + Related Rights in the Work and the meaning and intended legal effect + of CC0 on those rights. + . + 1. Copyright and Related Rights. A Work made available under CC0 may + be protected by copyright and related or neighboring rights + ("Copyright and Related Rights"). Copyright and Related Rights + include, but are not limited to, the following: + . + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or + performer(s); + iii. publicity and privacy rights pertaining to a person's image + or likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a + Work, subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and + reuse of data in a Work; + vi. database rights (such as those arising under Directive + 96/9/EC of the European Parliament and of the Council of 11 + March 1996 on the legal protection of databases, and under + any national implementation thereof, including any amended or + successor version of such directive); and + vii. other similar, equivalent or corresponding rights throughout + the world based on applicable law or treaty, and any national + implementations thereof. + . + 2. Waiver. To the greatest extent permitted by, but not in + contravention of, applicable law, Affirmer hereby overtly, fully, + permanently, irrevocably and unconditionally waives, abandons, and + surrenders all of Affirmer's Copyright and Related Rights and + associated claims and causes of action, whether now known or + unknown (including existing as well as future claims and causes of + action), in the Work (i) in all territories worldwide, (ii) for + the maximum duration provided by applicable law or treaty + (including future time extensions), (iii) in any current or future + medium and for any number of copies, and (iv) for any purpose + whatsoever, including without limitation commercial, advertising + or promotional purposes (the "Waiver"). Affirmer makes the Waiver + for the benefit of each member of the public at large and to the + detriment of Affirmer's heirs and successors, fully intending that + such Waiver shall not be subject to revocation, rescission, + cancellation, termination, or any other legal or equitable action + to disrupt the quiet enjoyment of the Work by the public as + contemplated by Affirmer's express Statement of Purpose. + . + 3. Public License Fallback. Should any part of the Waiver for any + reason be judged legally invalid or ineffective under applicable law, + then the Waiver shall be preserved to the maximum extent permitted + taking into account Affirmer's express Statement of Purpose. In + addition, to the extent the Waiver is so judged Affirmer hereby + grants to each affected person a royalty-free, non transferable, non + sublicensable, non exclusive, irrevocable and unconditional license + to exercise Affirmer's Copyright and Related Rights in the Work (i) + in all territories worldwide, (ii) for the maximum duration provided + by applicable law or treaty (including future time extensions), (iii) + in any current or future medium and for any number of copies, and + (iv) for any purpose whatsoever, including without limitation + commercial, advertising or promotional purposes (the "License"). The + License shall be deemed effective as of the date CC0 was applied by + Affirmer to the Work. Should any part of the License for any reason + be judged legally invalid or ineffective under applicable law, such + partial invalidity or ineffectiveness shall not invalidate the + remainder of the License, and in such case Affirmer hereby affirms + that he or she will not (i) exercise any of his or her remaining + Copyright and Related Rights in the Work or (ii) assert any + associated claims and causes of action with respect to the Work, in + either case contrary to Affirmer's express Statement of Purpose. + . + 4. Limitations and Disclaimers. + . + a. No trademark or patent rights held by Affirmer are waived, + abandoned, surrendered, licensed or otherwise affected by + this document. + b. Affirmer offers the Work as-is and makes no representations + or warranties of any kind concerning the Work, express, + implied, statutory or otherwise, including without limitation + warranties of title, merchantability, fitness for a + particular purpose, non infringement, or the absence of + latent or other defects, accuracy, or the present or absence + of errors, whether or not discoverable, all to the greatest + extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of + other persons that may apply to the Work or any use thereof, + including without limitation any person's Copyright and + Related Rights in the Work. Further, Affirmer disclaims + responsibility for obtaining any necessary consents, + permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons + is not a party to this document and has no duty or obligation + with respect to this CC0 or use of the Work. + diff --git a/packages/flask-executor/opengnsys-flask-executor-0.10.0/debian/rules b/packages/flask-executor/opengnsys-flask-executor-0.10.0/debian/rules new file mode 100755 index 0000000..b3a6e29 --- /dev/null +++ b/packages/flask-executor/opengnsys-flask-executor-0.10.0/debian/rules @@ -0,0 +1,22 @@ +#!/usr/bin/make -f + +export LC_ALL=C.UTF-8 +export PYBUILD_NAME = libarchive-c +#export PYBUILD_BEFORE_TEST = cp -av README.rst {build_dir} +export PYBUILD_TEST_ARGS = -vv -s +#export PYBUILD_AFTER_TEST = rm -v {build_dir}/README.rst +# ./usr/lib/python3/dist-packages/libarchive/ +export PYBUILD_INSTALL_ARGS=--install-lib=/usr/share/opengnsys-modules/python3/dist-packages/ +%: + dh $@ --with python3 --buildsystem=pybuild + +override_dh_gencontrol: + dh_gencontrol -- \ + -Vlib:Depends=$(shell dpkg-query -W -f '$${Depends}' libarchive-dev \ + | sed -E 's/.*(libarchive[[:alnum:].-]+).*/\1/') + +override_dh_installdocs: + # Nothing, we don't want docs + +override_dh_installchangelogs: + # Nothing, we don't want the changelog diff --git a/packages/flask-executor/opengnsys-flask-executor-0.10.0/debian/source/format b/packages/flask-executor/opengnsys-flask-executor-0.10.0/debian/source/format new file mode 100644 index 0000000..163aaf8 --- /dev/null +++ b/packages/flask-executor/opengnsys-flask-executor-0.10.0/debian/source/format @@ -0,0 +1 @@ +3.0 (quilt) diff --git a/packages/flask-executor/opengnsys-flask-executor-0.10.0/debian/tests/control b/packages/flask-executor/opengnsys-flask-executor-0.10.0/debian/tests/control new file mode 100644 index 0000000..4b7045a --- /dev/null +++ b/packages/flask-executor/opengnsys-flask-executor-0.10.0/debian/tests/control @@ -0,0 +1,2 @@ +Tests: upstream-tests +Depends: @, python3-mock, python3-pytest diff --git a/packages/flask-executor/opengnsys-flask-executor-0.10.0/debian/tests/upstream-tests b/packages/flask-executor/opengnsys-flask-executor-0.10.0/debian/tests/upstream-tests new file mode 100755 index 0000000..7c45645 --- /dev/null +++ b/packages/flask-executor/opengnsys-flask-executor-0.10.0/debian/tests/upstream-tests @@ -0,0 +1,14 @@ +#!/bin/sh + +set -e + +if ! [ -d "$AUTOPKGTEST_TMP" ]; then + echo "AUTOPKGTEST_TMP not set." >&2 + exit 1 +fi + +cp -rv tests "$AUTOPKGTEST_TMP" +cd "$AUTOPKGTEST_TMP" +mkdir -v libarchive +touch README.rst +py.test-3 tests -vv -l -r a diff --git a/packages/flask-executor/opengnsys-flask-executor-0.10.0/docs/Makefile b/packages/flask-executor/opengnsys-flask-executor-0.10.0/docs/Makefile new file mode 100644 index 0000000..4cd5b21 --- /dev/null +++ b/packages/flask-executor/opengnsys-flask-executor-0.10.0/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = Flask-Executor +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/packages/flask-executor/opengnsys-flask-executor-0.10.0/docs/api/flask_executor.rst b/packages/flask-executor/opengnsys-flask-executor-0.10.0/docs/api/flask_executor.rst new file mode 100644 index 0000000..b3bfebc --- /dev/null +++ b/packages/flask-executor/opengnsys-flask-executor-0.10.0/docs/api/flask_executor.rst @@ -0,0 +1,30 @@ +flask\_executor package +======================= + +Submodules +---------- + +flask\_executor.executor module +------------------------------- + +.. automodule:: flask_executor.executor + :members: + :undoc-members: + :show-inheritance: + +flask\_executor.futures module +------------------------------ + +.. automodule:: flask_executor.futures + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: flask_executor + :members: + :undoc-members: + :show-inheritance: diff --git a/packages/flask-executor/opengnsys-flask-executor-0.10.0/docs/api/modules.rst b/packages/flask-executor/opengnsys-flask-executor-0.10.0/docs/api/modules.rst new file mode 100644 index 0000000..ec765d3 --- /dev/null +++ b/packages/flask-executor/opengnsys-flask-executor-0.10.0/docs/api/modules.rst @@ -0,0 +1,7 @@ +flask_executor +============== + +.. toctree:: + :maxdepth: 4 + + flask_executor diff --git a/packages/flask-executor/opengnsys-flask-executor-0.10.0/docs/conf.py b/packages/flask-executor/opengnsys-flask-executor-0.10.0/docs/conf.py new file mode 100644 index 0000000..6e25af8 --- /dev/null +++ b/packages/flask-executor/opengnsys-flask-executor-0.10.0/docs/conf.py @@ -0,0 +1,172 @@ +# -*- coding: utf-8 -*- +# +# Configuration file for the Sphinx documentation builder. +# +# This file does only contain a selection of the most common options. For a +# full list see the documentation: +# http://www.sphinx-doc.org/en/master/config + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys + +from flask_executor import __version__ + +sys.path.insert(0, os.path.abspath('..')) + + +# -- Project information ----------------------------------------------------- + +project = 'Flask-Executor' +copyright = '2018, Dave Chevell' +author = 'Dave Chevell' + +# The short X.Y version +version = '.'.join(__version__.split('.')[:2]) +# The full version, including alpha/beta/rc tags +release = __version__ + + +# -- General configuration --------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.intersphinx', + 'sphinx.ext.viewcode', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path . +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'alabaster' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +# html_static_path = ['_static'] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# The default sidebars (for documents that don't match any pattern) are +# defined by theme itself. Builtin themes are using these templates by +# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', +# 'searchbox.html']``. +# +# html_sidebars = {} + + +# -- Options for HTMLHelp output --------------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = 'Flask-Executordoc' + + +# -- Options for LaTeX output ------------------------------------------------ + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'Flask-Executor.tex', 'Flask-Executor Documentation', + 'Dave Chevell', 'manual'), +] + + +# -- Options for manual page output ------------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'flask-executor', 'Flask-Executor Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ---------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'Flask-Executor', 'Flask-Executor Documentation', + author, 'Flask-Executor', 'One line description of project.', + 'Miscellaneous'), +] + + +# -- Extension configuration ------------------------------------------------- + +# -- Options for intersphinx extension --------------------------------------- + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = { + 'python': ('https://docs.python.org/3', None), + 'http://flask.pocoo.org/docs/': None, +} diff --git a/packages/flask-executor/opengnsys-flask-executor-0.10.0/docs/index.rst b/packages/flask-executor/opengnsys-flask-executor-0.10.0/docs/index.rst new file mode 100644 index 0000000..dc6bac6 --- /dev/null +++ b/packages/flask-executor/opengnsys-flask-executor-0.10.0/docs/index.rst @@ -0,0 +1,187 @@ +.. Flask-Executor documentation master file, created by + sphinx-quickstart on Sun Sep 23 18:52:39 2018. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Flask-Executor +============== + +.. module:: flask_executor + +Flask-Executor is a `Flask`_ extension that makes it easy to work with :py:mod:`concurrent.futures` +in your application. + +Installation +------------ + +Flask-Executor is available on PyPI and can be installed with pip:: + + $ pip install flask-executor + +Setup +------ + +The Executor extension can either be initialized directly:: + + from flask import Flask + from flask_executor import Executor + + app = Flask(__name__) + executor = Executor(app) + +Or through the factory method:: + + executor = Executor() + executor.init_app(app) + + +Configuration +------------- + +To specify the type of executor to initialise, set ``EXECUTOR_TYPE`` inside your app configuration. +Valid values are ``'thread'`` (default) to initialise a +:class:`~concurrent.futures.ThreadPoolExecutor`, or ``'process'`` to initialise a +:class:`~concurrent.futures.ProcessPoolExecutor`:: + + app.config['EXECUTOR_TYPE'] = 'thread' + +To define the number of worker threads for a :class:`~concurrent.futures.ThreadPoolExecutor` or the +number of worker processes for a :class:`~concurrent.futures.ProcessPoolExecutor`, set +``EXECUTOR_MAX_WORKERS`` in your app configuration. Valid values are any integer or ``None`` (default) +to let :py:mod:`concurrent.futures` pick defaults for you:: + + app.config['EXECUTOR_MAX_WORKERS'] = 5 + +If multiple executors are needed, :class:`flask_executor.Executor` can be initialised with a ``name`` +parameter. Named executors will look for configuration variables prefixed with the specified ``name`` +value, uppercased: + + app.config['CUSTOM_EXECUTOR_TYPE'] = 'thread' + app.config['CUSTOM_EXECUTOR_MAX_WORKERS'] = 5 + executor = Executor(app, name='custom') + + +Basic Usage +----------- + +Flask-Executor supports the standard :class:`concurrent.futures.Executor` methods, +:meth:`~concurrent.futures.Executor.submit` and :meth:`~concurrent.futures.Executor.map`:: + + def fib(n): + if n <= 2: + return 1 + else: + return fib(n-1) + fib(n-2) + + @app.route('/run_fib') + def run_fib(): + executor.submit(fib, 5) + executor.map(fib, range(1, 6)) + return 'OK' + +Submitting a task via :meth:`~concurrent.futures.Executor.submit` returns a +:class:`flask_executor.FutureProxy` object, a subclass of +:class:`concurrent.futures.Future` object from which you can retrieve your job status or result. + + +Contexts +-------- + +When calling :meth:`~concurrent.futures.Executor.submit` or :meth:`~concurrent.futures.Executor.map` +Flask-Executor will wrap `ThreadPoolExecutor` callables with a copy of both the current application +context and current request context. Code that must be run in these contexts or that depends on +information or configuration stored in :data:`flask.current_app`, :data:`flask.request` or +:data:`flask.g` can be submitted to the executor without modification. + +Note: due to limitations in Python's default object serialisation and a lack of shared memory space between subprocesses, contexts cannot be pushed to `ProcessPoolExecutor()` workers. + + +Futures +------- + +:class:`flask_executor.FutureProxy` objects look and behave like normal :class:`concurrent.futures.Future` +objects, but allow `flask_executor` to override certain methods and add additional behaviours. +When submitting a callable to :meth:`~concurrent.futures.Future.add_done_callback`, callables are +wrapped with a copy of both the current application context and current request context. + +You may want to preserve access to Futures returned from the executor, so that you can retrieve the +results in a different part of your application. Flask-Executor allows Futures to be stored within +the executor itself and provides methods for querying and returning them in different parts of your +app:: + + @app.route('/start-task') + def start_task(): + executor.submit_stored('calc_power', pow, 323, 1235) + return jsonify({'result':'success'}) + + @app.route('/get-result') + def get_result(): + if not executor.futures.done('calc_power'): + return jsonify({'status': executor.futures._state('calc_power')}) + future = executor.futures.pop('calc_power') + return jsonify({'status': done, 'result': future.result()}) + + +Decoration +---------- + +Flask-Executor lets you decorate methods in the same style as distributed task queues when using 'thread' executor type like +`Celery`_:: + + @executor.job + def fib(n): + if n <= 2: + return 1 + else: + return fib(n-1) + fib(n-2) + + @app.route('/decorate_fib') + def decorate_fib(): + fib.submit(5) + fib.submit_stored('fibonacci', 5) + fib.map(range(1, 6)) + return 'OK' + + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + api/modules + + +Default Callbacks +----------------- + +:class:`concurrent.futures.Future` objects can have callbacks attached by using +:meth:`~concurrent.futures.Future.add_done_callback`. Flask-Executor lets you specify default +callbacks that will be applied to all new futures created by the executor:: + + def some_callback(future): + # do something with future + + executor.add_default_done_callback(some_callback) + + # Callback will be added to the below task automatically + executor.submit(pow, 323, 1235) + + +Propagate Exceptions +-------------------- + +Normally any exceptions thrown by background threads or processes will be swallowed unless explicitly +checked for. To instead surface all exceptions thrown by background tasks, Flask-Executor can add +a special default callback that raises any exceptions thrown by tasks submitted to the executor:: + + app.config['EXECUTOR_PROPAGATE_EXCEPTIONS'] = True + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + +.. _Flask: http://flask.pocoo.org/ +.. _Celery: http://www.celeryproject.org/ diff --git a/packages/flask-executor/opengnsys-flask-executor-0.10.0/flask_executor/__init__.py b/packages/flask-executor/opengnsys-flask-executor-0.10.0/flask_executor/__init__.py new file mode 100644 index 0000000..87192be --- /dev/null +++ b/packages/flask-executor/opengnsys-flask-executor-0.10.0/flask_executor/__init__.py @@ -0,0 +1,5 @@ +from flask_executor.executor import Executor + + +__all__ = ('Executor',) +__version__ = '0.10.0' diff --git a/packages/flask-executor/opengnsys-flask-executor-0.10.0/flask_executor/executor.py b/packages/flask-executor/opengnsys-flask-executor-0.10.0/flask_executor/executor.py new file mode 100644 index 0000000..fe48d73 --- /dev/null +++ b/packages/flask-executor/opengnsys-flask-executor-0.10.0/flask_executor/executor.py @@ -0,0 +1,273 @@ +import concurrent.futures +import contextvars +import copy +import re + +from flask import copy_current_request_context, current_app, g + +from flask_executor.futures import FutureCollection, FutureProxy +from flask_executor.helpers import InstanceProxy, str2bool + + +def get_current_app_context(): + try: + from flask.globals import _cv_app + return _cv_app.get(None) + except ImportError: + from flask.globals import _app_ctx_stack + return _app_ctx_stack.top + + +def push_app_context(fn): + app = current_app._get_current_object() + _g = copy.copy(g) + + def wrapper(*args, **kwargs): + with app.app_context(): + ctx = get_current_app_context() + ctx.g = _g + return fn(*args, **kwargs) + + return wrapper + + +def propagate_exceptions_callback(future): + exc = future.exception() + if exc: + raise exc + + +class ExecutorJob: + """Wraps a function with an executor so to allow the wrapped function to + submit itself directly to the executor.""" + + def __init__(self, executor, fn): + self.executor = executor + self.fn = fn + + def submit(self, *args, **kwargs): + future = self.executor.submit(self.fn, *args, **kwargs) + return future + + def submit_stored(self, future_key, *args, **kwargs): + future = self.executor.submit_stored(future_key, self.fn, *args, **kwargs) + return future + + def map(self, *iterables, **kwargs): + results = self.executor.map(self.fn, *iterables, **kwargs) + return results + + +class Executor(InstanceProxy, concurrent.futures._base.Executor): + """An executor interface for :py:mod:`concurrent.futures` designed for + working with Flask applications. + + :param app: A Flask application instance. + :param name: An optional name for the executor. This can be used to + configure multiple executors. Named executors will look for + environment variables prefixed with the name in uppercase, + e.g. ``CUSTOM_EXECUTOR_TYPE``. + """ + + def __init__(self, app=None, name=''): + self.app = app + self._default_done_callbacks = [] + self.futures = FutureCollection() + if re.match(r'^(\w+)?$', name) is None: + raise ValueError( + "Executor names may only contain letters, numbers or underscores" + ) + self.name = name + prefix = name.upper() + '_' if name else '' + self.EXECUTOR_TYPE = prefix + 'EXECUTOR_TYPE' + self.EXECUTOR_MAX_WORKERS = prefix + 'EXECUTOR_MAX_WORKERS' + self.EXECUTOR_FUTURES_MAX_LENGTH = prefix + 'EXECUTOR_FUTURES_MAX_LENGTH' + self.EXECUTOR_PROPAGATE_EXCEPTIONS = prefix + 'EXECUTOR_PROPAGATE_EXCEPTIONS' + self.EXECUTOR_PUSH_APP_CONTEXT = prefix + 'EXECUTOR_PUSH_APP_CONTEXT' + + if app is not None: + self.init_app(app) + + def init_app(self, app): + """Initialise application. This will also intialise the configured + executor type: + + * :class:`concurrent.futures.ThreadPoolExecutor` + * :class:`concurrent.futures.ProcessPoolExecutor` + """ + app.config.setdefault(self.EXECUTOR_TYPE, 'thread') + app.config.setdefault(self.EXECUTOR_PUSH_APP_CONTEXT, True) + futures_max_length = app.config.setdefault(self.EXECUTOR_FUTURES_MAX_LENGTH, None) + propagate_exceptions = app.config.setdefault(self.EXECUTOR_PROPAGATE_EXCEPTIONS, False) + if futures_max_length is not None: + self.futures.max_length = int(futures_max_length) + if str2bool(propagate_exceptions): + self.add_default_done_callback(propagate_exceptions_callback) + self._self = self._make_executor(app) + app.extensions[self.name + 'executor'] = self + + def _make_executor(self, app): + executor_max_workers = app.config.setdefault(self.EXECUTOR_MAX_WORKERS, None) + if executor_max_workers is not None: + executor_max_workers = int(executor_max_workers) + executor_type = app.config[self.EXECUTOR_TYPE] + if executor_type == 'thread': + _executor = concurrent.futures.ThreadPoolExecutor + elif executor_type == 'process': + _executor = concurrent.futures.ProcessPoolExecutor + else: + raise ValueError("{} is not a valid executor type.".format(executor_type)) + return _executor(max_workers=executor_max_workers) + + def _prepare_fn(self, fn, force_copy=False): + if isinstance(self._self, concurrent.futures.ThreadPoolExecutor) \ + or force_copy: + fn = copy_current_request_context(fn) + if current_app.config[self.EXECUTOR_PUSH_APP_CONTEXT]: + fn = push_app_context(fn) + return fn + + def submit(self, fn, *args, **kwargs): + r"""Schedules the callable, fn, to be executed as fn(\*args \**kwargs) + and returns a :class:`~flask_executor.futures.FutureProxy` object, a + :class:`~concurrent.futures.Future` subclass representing + the execution of the callable. + + See also :meth:`concurrent.futures.Executor.submit`. + + Callables are wrapped a copy of the current application context and the + current request context. Code that depends on information or + configuration stored in :data:`flask.current_app`, + :data:`flask.request` or :data:`flask.g` can be run without + modification. + + Note: Because callables only have access to *copies* of the application + or request contexts any changes made to these copies will not be + reflected in the original view. Further, changes in the original app or + request context that occur after the callable is submitted will not be + available to the callable. + + Example:: + + future = executor.submit(pow, 323, 1235) + print(future.result()) + + :param fn: The callable to be executed. + :param \*args: A list of positional parameters used with + the callable. + :param \**kwargs: A dict of named parameters used with + the callable. + + :rtype: flask_executor.FutureProxy + """ + fn = self._prepare_fn(fn) + future = self._self.submit(fn, *args, **kwargs) + for callback in self._default_done_callbacks: + future.add_done_callback(callback) + return FutureProxy(future, self) + + def submit_stored(self, future_key, fn, *args, **kwargs): + r"""Submits the callable using :meth:`Executor.submit` and stores the + Future in the executor via a + :class:`~flask_executor.futures.FutureCollection` object available at + :data:`Executor.futures`. These futures can be retrieved anywhere + inside your application and queried for status or popped from the + collection. Due to memory concerns, the maximum length of the + FutureCollection is limited, and the oldest Futures will be dropped + when the limit is exceeded. + + See :class:`flask_executor.futures.FutureCollection` for more + information on how to query futures in a collection. + + Example:: + + @app.route('/start-task') + def start_task(): + executor.submit_stored('calc_power', pow, 323, 1235) + return jsonify({'result':'success'}) + + @app.route('/get-result') + def get_result(): + if not executor.futures.done('calc_power'): + future_status = executor.futures._state('calc_power') + return jsonify({'status': future_status}) + future = executor.futures.pop('calc_power') + return jsonify({'status': done, 'result': future.result()}) + + :param future_key: Stores the Future for the submitted task inside the + executor's ``futures`` object with the specified + key. + :param fn: The callable to be executed. + :param \*args: A list of positional parameters used with + the callable. + :param \**kwargs: A dict of named parameters used with + the callable. + + :rtype: concurrent.futures.Future + """ + future = self.submit(fn, *args, **kwargs) + self.futures.add(future_key, future) + return future + + def map(self, fn, *iterables, **kwargs): + r"""Submits the callable, fn, and an iterable of arguments to the + executor and returns the results inside a generator. + + See also :meth:`concurrent.futures.Executor.map`. + + Callables are wrapped a copy of the current application context and the + current request context. Code that depends on information or + configuration stored in :data:`flask.current_app`, + :data:`flask.request` or :data:`flask.g` can be run without + modification. + + Note: Because callables only have access to *copies* of the application + or request contexts + any changes made to these copies will not be reflected in the original + view. Further, changes in the original app or request context that + occur after the callable is submitted will not be available to the + callable. + + :param fn: The callable to be executed. + :param \*iterables: An iterable of arguments the callable will apply to. + :param \**kwargs: A dict of named parameters to pass to the underlying + executor's :meth:`~concurrent.futures.Executor.map` + method. + """ + fn = self._prepare_fn(fn) + return self._self.map(fn, *iterables, **kwargs) + + def job(self, fn): + """Decorator. Use this to transform functions into `ExecutorJob` + instances that can submit themselves directly to the executor. + + Example:: + + @executor.job + def fib(n): + if n <= 2: + return 1 + else: + return fib(n-1) + fib(n-2) + + future = fib.submit(5) + results = fib.map(range(1, 6)) + """ + if isinstance(self._self, concurrent.futures.ProcessPoolExecutor): + raise TypeError( + "Can't decorate {}: Executors that use multiprocessing " + "don't support decorators".format(fn) + ) + return ExecutorJob(executor=self, fn=fn) + + def add_default_done_callback(self, fn): + """Registers callable to be attached to all newly created futures. When a + callable is submitted to the executor, + :meth:`concurrent.futures.Future.add_done_callback` is called for every default + callable that has been set." + + :param fn: The callable to be added to the list of default done callbacks for new + Futures. + """ + + self._default_done_callbacks.append(fn) diff --git a/packages/flask-executor/opengnsys-flask-executor-0.10.0/flask_executor/futures.py b/packages/flask-executor/opengnsys-flask-executor-0.10.0/flask_executor/futures.py new file mode 100644 index 0000000..d98045a --- /dev/null +++ b/packages/flask-executor/opengnsys-flask-executor-0.10.0/flask_executor/futures.py @@ -0,0 +1,107 @@ +from collections import OrderedDict +from concurrent.futures import Future + +from flask_executor.helpers import InstanceProxy + + +class FutureCollection: + """A FutureCollection is an object to store and interact with + :class:`concurrent.futures.Future` objects. It provides access to all + attributes and methods of a Future by proxying attribute calls to the + stored Future object. + + To access the methods of a Future from a FutureCollection instance, include + a valid ``future_key`` value as the first argument of the method call. To + access attributes, call them as though they were a method with + ``future_key`` as the sole argument. If ``future_key`` does not exist, the + call will always return None. If ``future_key`` does exist but the + referenced Future does not contain the requested attribute an + :exc:`AttributeError` will be raised. + + To prevent memory exhaustion a FutureCollection instance can be bounded by + number of items using the ``max_length`` parameter. As a best practice, + Futures should be popped once they are ready for use, with the proxied + attribute form used to determine whether a Future is ready to be used or + discarded. + + :param max_length: Maximum number of Futures to store. Oldest Futures are + discarded first. + + """ + + def __init__(self, max_length=50): + self.max_length = max_length + self._futures = OrderedDict() + + def __contains__(self, future): + return future in self._futures.values() + + def __len__(self): + return len(self._futures) + + def __getattr__(self, attr): + # Call any valid Future method or attribute + def _future_attr(future_key, *args, **kwargs): + if future_key not in self._futures: + return None + future_attr = getattr(self._futures[future_key], attr) + if callable(future_attr): + return future_attr(*args, **kwargs) + return future_attr + + return _future_attr + + def _check_limits(self): + if self.max_length is not None: + while len(self._futures) > self.max_length: + self._futures.popitem(last=False) + + def add(self, future_key, future): + """Add a new Future. If ``max_length`` limit was defined for the + FutureCollection, old Futures may be dropped to respect this limit. + + :param future_key: Key for the Future to be added. + :param future: Future to be added. + """ + if future_key in self._futures: + raise ValueError("future_key {} already exists".format(future_key)) + self._futures[future_key] = future + self._check_limits() + + def pop(self, future_key): + """Return a Future and remove it from the collection. Futures that are + ready to be used should always be popped so they do not continue to + consume memory. + + Returns ``None`` if the key doesn't exist. + + :param future_key: Key for the Future to be returned. + """ + return self._futures.pop(future_key, None) + + +class FutureProxy(InstanceProxy, Future): + """A FutureProxy is an instance proxy that wraps an instance of + :class:`concurrent.futures.Future`. Since an executor can't be made to + return a subclassed Future object, this proxy class is used to override + instance behaviours whilst providing an agnostic method of accessing + the original methods and attributes. + :param future: An instance of :class:`~concurrent.futures.Future` that + the proxy will provide access to. + :param executor: An instance of :class:`flask_executor.Executor` which + will be used to provide access to Flask context features. + """ + + def __init__(self, future, executor): + self._self = future + self._executor = executor + + def add_done_callback(self, fn): + fn = self._executor._prepare_fn(fn, force_copy=True) + return self._self.add_done_callback(fn) + + def __eq__(self, obj): + return self._self == obj + + def __hash__(self): + return self._self.__hash__() diff --git a/packages/flask-executor/opengnsys-flask-executor-0.10.0/flask_executor/helpers.py b/packages/flask-executor/opengnsys-flask-executor-0.10.0/flask_executor/helpers.py new file mode 100644 index 0000000..5de49d8 --- /dev/null +++ b/packages/flask-executor/opengnsys-flask-executor-0.10.0/flask_executor/helpers.py @@ -0,0 +1,37 @@ +PROXIED_OBJECT = '__proxied_object' + + +def str2bool(v): + return str(v).lower() in ("yes", "true", "t", "1") + + +class InstanceProxy(object): + + def __init__(self, proxied_obj): + self._self = proxied_obj + + @property + def _self(self): + try: + return object.__getattribute__(self, PROXIED_OBJECT) + except AttributeError: + return None + + @_self.setter + def _self(self, proxied_obj): + object.__setattr__(self, PROXIED_OBJECT, proxied_obj) + return self + + def __getattribute__(self, attr): + super_cls_dict = InstanceProxy.__dict__ + cls_dict = object.__getattribute__(self, '__class__').__dict__ + inst_dict = object.__getattribute__(self, '__dict__') + if attr in cls_dict or attr in inst_dict or attr in super_cls_dict: + return object.__getattribute__(self, attr) + target_obj = object.__getattribute__(self, PROXIED_OBJECT) + return object.__getattribute__(target_obj, attr) + + def __repr__(self): + class_name = object.__getattribute__(self, '__class__').__name__ + target_repr = repr(self._self) + return '<%s( %s )>' % (class_name, target_repr) diff --git a/packages/flask-executor/opengnsys-flask-executor-0.10.0/setup.py b/packages/flask-executor/opengnsys-flask-executor-0.10.0/setup.py new file mode 100644 index 0000000..71661b5 --- /dev/null +++ b/packages/flask-executor/opengnsys-flask-executor-0.10.0/setup.py @@ -0,0 +1,52 @@ +import setuptools +from setuptools.command.test import test +import sys + +try: + from flask_executor import __version__ as version +except ImportError: + import re + pattern = re.compile(r"__version__ = '(.*)'") + with open('flask_executor/__init__.py') as f: + version = pattern.search(f.read()).group(1) + + +with open('README.md', 'r') as fh: + long_description = fh.read() + + +class pytest(test): + + def run_tests(self): + import pytest + errno = pytest.main(self.test_args) + sys.exit(errno) + + +setuptools.setup( + name='Flask-Executor', + version=version, + author='Dave Chevell', + author_email='chevell@gmail.com', + description='An easy to use Flask wrapper for concurrent.futures', + long_description=long_description, + long_description_content_type='text/markdown', + url='https://github.com/dchevell/flask-executor', + packages=setuptools.find_packages(exclude=['tests']), + keywords=['flask', 'concurrent.futures'], + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + ], + license='MIT', + install_requires=['Flask'], + extras_require={ + ':python_version == "2.7"': ['futures>=3.1.1'], + 'test': ['pytest', 'pytest-cov', 'codecov', 'flask-sqlalchemy'], + }, + test_suite='tests', + cmdclass={ + 'test': pytest + } +) diff --git a/packages/flask-executor/opengnsys-flask-executor-0.10.0/tests/__init__.py b/packages/flask-executor/opengnsys-flask-executor-0.10.0/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/flask-executor/opengnsys-flask-executor-0.10.0/tests/conftest.py b/packages/flask-executor/opengnsys-flask-executor-0.10.0/tests/conftest.py new file mode 100644 index 0000000..f308ba7 --- /dev/null +++ b/packages/flask-executor/opengnsys-flask-executor-0.10.0/tests/conftest.py @@ -0,0 +1,18 @@ +from flask import Flask +import pytest + +from flask_executor import Executor + + +@pytest.fixture(params=['thread_push_app_context', 'thread_copy_app_context', 'process']) +def app(request): + app = Flask(__name__) + app.config['EXECUTOR_TYPE'] = 'process' if request.param == 'process' else 'thread' + app.config['EXECUTOR_PUSH_APP_CONTEXT'] = request.param == 'thread_push_app_context' + + return app + +@pytest.fixture +def default_app(): + app = Flask(__name__) + return app diff --git a/packages/flask-executor/opengnsys-flask-executor-0.10.0/tests/test_executor.py b/packages/flask-executor/opengnsys-flask-executor-0.10.0/tests/test_executor.py new file mode 100644 index 0000000..e11d6c3 --- /dev/null +++ b/packages/flask-executor/opengnsys-flask-executor-0.10.0/tests/test_executor.py @@ -0,0 +1,376 @@ +import concurrent +import concurrent.futures +import logging +import random +import time +from threading import local + +import pytest +from flask import current_app, g, request + +from flask_executor import Executor +from flask_executor.executor import propagate_exceptions_callback + + +# Reusable functions for tests + +def fib(n): + if n <= 2: + return 1 + else: + return fib(n - 1) + fib(n - 2) + + +def app_context_test_value(_=None): + return current_app.config['TEST_VALUE'] + + +def request_context_test_value(_=None): + return request.test_value + + +def g_context_test_value(_=None): + return g.test_value + + +def fail(): + time.sleep(0.1) + print(hello) + + +def test_init(app): + executor = Executor(app) + assert 'executor' in app.extensions + assert isinstance(executor, concurrent.futures._base.Executor) + assert isinstance(executor._self, concurrent.futures._base.Executor) + assert getattr(executor, 'shutdown') + + +def test_factory_init(app): + executor = Executor() + executor.init_app(app) + assert 'executor' in app.extensions + assert isinstance(executor._self, concurrent.futures._base.Executor) + + +def test_thread_executor_init(default_app): + default_app.config['EXECUTOR_TYPE'] = 'thread' + executor = Executor(default_app) + assert isinstance(executor._self, concurrent.futures.ThreadPoolExecutor) + assert isinstance(executor, concurrent.futures.ThreadPoolExecutor) + + +def test_process_executor_init(default_app): + default_app.config['EXECUTOR_TYPE'] = 'process' + executor = Executor(default_app) + assert isinstance(executor._self, concurrent.futures.ProcessPoolExecutor) + assert isinstance(executor, concurrent.futures.ProcessPoolExecutor) + + +def test_default_executor_init(default_app): + executor = Executor(default_app) + assert isinstance(executor._self, concurrent.futures.ThreadPoolExecutor) + + +def test_invalid_executor_init(default_app): + default_app.config['EXECUTOR_TYPE'] = 'invalid_value' + try: + executor = Executor(default_app) + except ValueError: + assert True + else: + assert False + + +def test_submit(app): + executor = Executor(app) + with app.test_request_context(''): + future = executor.submit(fib, 5) + assert future.result() == fib(5) + + +def test_max_workers(app): + EXECUTOR_MAX_WORKERS = 10 + app.config['EXECUTOR_MAX_WORKERS'] = EXECUTOR_MAX_WORKERS + executor = Executor(app) + assert executor._max_workers == EXECUTOR_MAX_WORKERS + assert executor._self._max_workers == EXECUTOR_MAX_WORKERS + + +def test_thread_decorator_submit(default_app): + default_app.config['EXECUTOR_TYPE'] = 'thread' + executor = Executor(default_app) + + @executor.job + def decorated(n): + return fib(n) + + with default_app.test_request_context(''): + future = decorated.submit(5) + assert future.result() == fib(5) + + +def test_thread_decorator_submit_stored(default_app): + default_app.config['EXECUTOR_TYPE'] = 'thread' + executor = Executor(default_app) + + @executor.job + def decorated(n): + return fib(n) + + with default_app.test_request_context(): + future = decorated.submit_stored('fibonacci', 35) + assert executor.futures.done('fibonacci') is False + assert future in executor.futures + executor.futures.pop('fibonacci') + assert future not in executor.futures + + +def test_thread_decorator_map(default_app): + iterable = list(range(5)) + default_app.config['EXECUTOR_TYPE'] = 'thread' + executor = Executor(default_app) + + @executor.job + def decorated(n): + return fib(n) + + with default_app.test_request_context(''): + results = decorated.map(iterable) + for i, r in zip(iterable, results): + assert fib(i) == r + + +def test_process_decorator(default_app): + ''' Using decorators should fail with a TypeError when using the ProcessPoolExecutor ''' + default_app.config['EXECUTOR_TYPE'] = 'process' + executor = Executor(default_app) + try: + @executor.job + def decorated(n): + return fib(n) + except TypeError: + pass + else: + assert 0 + + +def test_submit_app_context(default_app): + test_value = random.randint(1, 101) + default_app.config['TEST_VALUE'] = test_value + executor = Executor(default_app) + with default_app.test_request_context(''): + future = executor.submit(app_context_test_value) + assert future.result() == test_value + + +def test_submit_g_context_process(default_app): + test_value = random.randint(1, 101) + executor = Executor(default_app) + with default_app.test_request_context(''): + g.test_value = test_value + future = executor.submit(g_context_test_value) + assert future.result() == test_value + + +def test_submit_request_context(default_app): + test_value = random.randint(1, 101) + executor = Executor(default_app) + with default_app.test_request_context(''): + request.test_value = test_value + future = executor.submit(request_context_test_value) + assert future.result() == test_value + + +def test_map_app_context(default_app): + test_value = random.randint(1, 101) + iterator = list(range(5)) + default_app.config['TEST_VALUE'] = test_value + executor = Executor(default_app) + with default_app.test_request_context(''): + results = executor.map(app_context_test_value, iterator) + for r in results: + assert r == test_value + + +def test_map_g_context_process(default_app): + test_value = random.randint(1, 101) + iterator = list(range(5)) + executor = Executor(default_app) + with default_app.test_request_context(''): + g.test_value = test_value + results = executor.map(g_context_test_value, iterator) + for r in results: + assert r == test_value + + +def test_map_request_context(default_app): + test_value = random.randint(1, 101) + iterator = list(range(5)) + executor = Executor(default_app) + with default_app.test_request_context('/'): + request.test_value = test_value + results = executor.map(request_context_test_value, iterator) + for r in results: + assert r == test_value + + +def test_executor_stored_future(default_app): + executor = Executor(default_app) + with default_app.test_request_context(): + future = executor.submit_stored('fibonacci', fib, 35) + assert executor.futures.done('fibonacci') is False + assert future in executor.futures + executor.futures.pop('fibonacci') + assert future not in executor.futures + + +def test_set_max_futures(default_app): + default_app.config['EXECUTOR_FUTURES_MAX_LENGTH'] = 10 + executor = Executor(default_app) + assert executor.futures.max_length == default_app.config['EXECUTOR_FUTURES_MAX_LENGTH'] + + +def test_named_executor(default_app): + name = 'custom' + EXECUTOR_MAX_WORKERS = 5 + CUSTOM_EXECUTOR_MAX_WORKERS = 10 + default_app.config['EXECUTOR_MAX_WORKERS'] = EXECUTOR_MAX_WORKERS + default_app.config['CUSTOM_EXECUTOR_MAX_WORKERS'] = CUSTOM_EXECUTOR_MAX_WORKERS + executor = Executor(default_app) + custom_executor = Executor(default_app, name=name) + assert 'executor' in default_app.extensions + assert name + 'executor' in default_app.extensions + assert executor._self._max_workers == EXECUTOR_MAX_WORKERS + assert executor._max_workers == EXECUTOR_MAX_WORKERS + assert custom_executor._self._max_workers == CUSTOM_EXECUTOR_MAX_WORKERS + assert custom_executor._max_workers == CUSTOM_EXECUTOR_MAX_WORKERS + + +def test_named_executor_submit(app): + name = 'custom' + custom_executor = Executor(app, name=name) + with app.test_request_context(''): + future = custom_executor.submit(fib, 5) + assert future.result() == fib(5) + + +def test_named_executor_name(default_app): + name = 'invalid name' + try: + executor = Executor(default_app, name=name) + except ValueError: + assert True + else: + assert False + + +def test_default_done_callback(app): + executor = Executor(app) + + def callback(future): + setattr(future, 'test', 'test') + + executor.add_default_done_callback(callback) + with app.test_request_context('/'): + future = executor.submit(fib, 5) + concurrent.futures.wait([future]) + assert hasattr(future, 'test') + + +def test_propagate_exception_callback(app, caplog): + caplog.set_level(logging.ERROR) + app.config['EXECUTOR_PROPAGATE_EXCEPTIONS'] = True + executor = Executor(app) + with pytest.raises(NameError): + with app.test_request_context('/'): + future = executor.submit(fail) + concurrent.futures.wait([future]) + future.result() + + +def test_coerce_config_types(default_app): + default_app.config['EXECUTOR_MAX_WORKERS'] = '5' + default_app.config['EXECUTOR_FUTURES_MAX_LENGTH'] = '10' + default_app.config['EXECUTOR_PROPAGATE_EXCEPTIONS'] = 'true' + executor = Executor(default_app) + with default_app.test_request_context(): + future = executor.submit_stored('fibonacci', fib, 35) + + +def test_shutdown_executor(default_app): + executor = Executor(default_app) + assert executor._shutdown is False + executor.shutdown() + assert executor._shutdown is True + + +def test_pre_init_executor(default_app): + executor = Executor() + + @executor.job + def decorated(n): + return fib(n) + + assert executor + executor.init_app(default_app) + with default_app.test_request_context(''): + future = decorated.submit(5) + assert future.result() == fib(5) + + +thread_local = local() + + +def set_thread_local(): + if hasattr(thread_local, 'value'): + raise ValueError('thread local already present') + thread_local.value = True + + +def clear_thread_local(response_or_exc): + if hasattr(thread_local, 'value'): + del thread_local.value + return response_or_exc + + +def test_teardown_appcontext_is_called(default_app): + default_app.config['EXECUTOR_MAX_WORKERS'] = 1 + default_app.config['EXECUTOR_PUSH_APP_CONTEXT'] = True + default_app.teardown_appcontext(clear_thread_local) + + executor = Executor(default_app) + with default_app.test_request_context(): + futures = [executor.submit(set_thread_local) for _ in range(2)] + concurrent.futures.wait(futures) + [propagate_exceptions_callback(future) for future in futures] + + +try: + import flask_sqlalchemy +except ImportError: + flask_sqlalchemy = None + + +@pytest.mark.skipif(flask_sqlalchemy is None, reason="flask_sqlalchemy not installed") +def test_sqlalchemy(default_app, caplog): + default_app.config['SQLALCHEMY_ENGINE_OPTIONS'] = {'echo_pool': 'debug', 'echo': 'debug'} + default_app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:' + default_app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + default_app.config['EXECUTOR_PUSH_APP_CONTEXT'] = True + default_app.config['EXECUTOR_MAX_WORKERS'] = 1 + db = flask_sqlalchemy.SQLAlchemy(default_app) + + def test_db(): + return list(db.session.execute('select 1')) + + executor = Executor(default_app) + with default_app.test_request_context(): + for i in range(2): + with caplog.at_level('DEBUG'): + caplog.clear() + future = executor.submit(test_db) + concurrent.futures.wait([future]) + future.result() + assert 'checked out from pool' in caplog.text + assert 'being returned to pool' in caplog.text diff --git a/packages/flask-executor/opengnsys-flask-executor-0.10.0/tests/test_futures.py b/packages/flask-executor/opengnsys-flask-executor-0.10.0/tests/test_futures.py new file mode 100644 index 0000000..703a76c --- /dev/null +++ b/packages/flask-executor/opengnsys-flask-executor-0.10.0/tests/test_futures.py @@ -0,0 +1,97 @@ +import concurrent.futures +import time + +import pytest + +from flask_executor import Executor +from flask_executor.futures import FutureCollection, FutureProxy +from flask_executor.helpers import InstanceProxy + + +def fib(n): + if n <= 2: + return 1 + else: + return fib(n-1) + fib(n-2) + + +def test_plain_future(): + executor = concurrent.futures.ThreadPoolExecutor(max_workers=1) + futures = FutureCollection() + future = executor.submit(fib, 33) + futures.add('fibonacci', future) + assert futures.done('fibonacci') is False + assert futures._state('fibonacci') is not None + assert future in futures + futures.pop('fibonacci') + assert future not in futures + +def test_missing_future(): + futures = FutureCollection() + assert futures.running('test') is None + +def test_duplicate_add_future(): + executor = concurrent.futures.ThreadPoolExecutor(max_workers=1) + futures = FutureCollection() + future = executor.submit(fib, 33) + futures.add('fibonacci', future) + try: + futures.add('fibonacci', future) + except ValueError: + assert True + else: + assert False + +def test_futures_max_length(): + executor = concurrent.futures.ThreadPoolExecutor(max_workers=1) + futures = FutureCollection(max_length=10) + future = executor.submit(pow, 2, 4) + futures.add(0, future) + assert future in futures + assert len(futures) == 1 + for i in range(1, 11): + futures.add(i, executor.submit(pow, 2, 4)) + assert len(futures) == 10 + assert future not in futures + +def test_future_proxy(default_app): + executor = Executor(default_app) + with default_app.test_request_context(''): + future = executor.submit(pow, 2, 4) + # Test if we're returning a subclass of Future + assert isinstance(future, concurrent.futures.Future) + assert isinstance(future, FutureProxy) + concurrent.futures.wait([future]) + # test standard Future methods and attributes + assert future._state == concurrent.futures._base.FINISHED + assert future.done() + assert future.exception(timeout=0) is None + +def test_add_done_callback(default_app): + """Exceptions thrown in callbacks can't be easily caught and make it hard + to test for callback failure. To combat this, a global variable is used to + store the value of an exception and test for its existence. + """ + executor = Executor(default_app) + global exception + exception = None + with default_app.test_request_context(''): + future = executor.submit(time.sleep, 0.5) + def callback(future): + global exception + try: + executor.submit(time.sleep, 0) + except RuntimeError as e: + exception = e + future.add_done_callback(callback) + concurrent.futures.wait([future]) + assert exception is None + +def test_instance_proxy(): + class TestProxy(InstanceProxy): + pass + x = TestProxy(concurrent.futures.Future()) + assert isinstance(x, concurrent.futures.Future) + assert 'TestProxy' in repr(x) + assert 'Future' in repr(x) + diff --git a/packages/flask-executor/opengnsys-flask-executor_0.10.0.orig.tar.xz b/packages/flask-executor/opengnsys-flask-executor_0.10.0.orig.tar.xz new file mode 100644 index 0000000000000000000000000000000000000000..de92c6a6c52db49107b45e0eec016173a8b75226 GIT binary patch literal 18976 zcmV(jK=!}=H+ooF000E$*0e?f03iVu0001VFXf}*kN-*CT>v+n2+ojjs~vS5PvZZ~ zmePR#=Ly$sD(*} zmha?Y+SlB)$$VjBVe25s5IGpR3yE9imlQ82@m=g%#&%^1cqd2Chdk{sr468NmLx10 zD;}0b7WFmI&&EYNMn*(LX{bEnu^611aqUu`Nhuxiso9$)aFnhSErtM1mRC)UStZIu zQNSh!XrWAp2T$^ZGUfth-Bg^LWP_hv)F+shISMec&eb!7J>fgf-?kfWm@X&@Nue{Uir zpB;(A#eDCh?2wcwY@(sA{<5wL{%C|ko*xX>u=hP7gMD2z46t}?l;~JW*i_~ffZQ1{ zC}nrO+7bqZtASa*I_CA)yJubFuy{_OBSlZ20D^KBF zgfTN)q!?^yGqiEtLZ!2xiu0?jDb<38AuyGV7FWIglmR8fATCJDxIaQt^T3P7jZ~uW zv|$-MGgR1CZi)bd(xCYp;i>{20gmL+k5u!JKWbW z8VQt)9|)&mx}AP|f~{(nBKG70jE%W*li?}qp#bPVoN;6A3EXg24ZN7`uY;%ly|BKh za^)ZK7&_66_Ppla6}*-!Mwc!o^ zFacTCkTrFv%G@PFxfKk5&cz(1xW-0|`tn7POw*tM>H%`ik%QNHwy&Bt%2RoPkSpxL zv>7`BZmzIrD!lUj87OCgN>s$c*&CL8y;;+?VO1d{tOL~W>44~b3ph=e{xSuAE{Y^Y z31?V--JXhGA$G*N2=C2AhrvajDJ@sQS+=n|mAi#xT>MkY9Lwvc>YWAcaD<0V=N6I{c+1NxwZZOr3Ir%%I6EC#xIo#X z^fc4kHjJEz)QrKLJ#Ur0Qpv{S60TkgSZ`Y!*#d_eG$-W&m6zg!?;5K1Q;wbmy{hYv@Y*czWNKGB;m_6a>!n-Wb;#!P$)$)hB#_gw}*U zDjCmnA1%!^!cnwUMfxNi{FZtIrA$rFsk_GlhI1zls=3EzXSpsUD%5S!q6~ zUcT4uq3h>`eL!KK*C=cJX4%Q$$@TS3|B9b#<i|~qf>=4Hc zrQhC22Ue67hI*dDYzP{tucv*i;H@AEj!I~77MP9OmnUEVV0nj0hhvx_13{rzb|lw& zMrn)Y@tl&mNvf*|w|E;CTf%V;>o>l<0I2~C!y`Zp>aZ7}v5*k6IgJ3Ppie25jE{l6 zvlMn&16aQ%pl*g^j9GyiWhdy8+K=k}ACig`JK{9piv&|>_RB60`~NIPOKkr5l2!6T z+QX*!g)hr0EO0K5!{~;$}P)RRMj?YOy9ZMRwbXQvMJfPPeHuYwC554Le1(*1<2 zzp9rG5nDEfGP(hgLyndL&Z{gYTrJ}T!&8Q>vGrm`Xc5CZApqCN8V*vH+PW)dUW617 zx!qQ|NX(hI1wVRSy@DkGYHJoR|9sa_Jn|ag#{hMxjSDw^+V8|+VC^+=+&HHuoI`?W);R7W&AQ zX|?8aesllah@gtINV#YrTJGJ|F7lmH?oDrivagY@$U5z^T_$n%8)2MX=IoQ{0iVP$ zY3cpbUA7Q)JKD0T>cL#_@0M2i8>=ceZ9azM*50^^;)_^a;+4D8#4-xs$$w~$Gdy@J z0`=CR!h}E`M=0XFX<2PL)`Lb}D_m{g z8^z_#vAP+v&4Zlew7RSbC&^q1bjQ+Um(BtpL`nSUjAZs8Z3qztkFQ8zsCM-9XUa%T zNjCPU`N5pxIXbRiCvVuZ2rJk0MTp;Wla|o(F%rr;SAc?m1j0e#5(2)=zm%m%Ve!GU zp|H=6PaOv@36JCt0)9Ja?hr}gd-&>^ z5?k&ctG9@ec*w*sJ+Ys`3cm+-J+*QxZYa`iwoL>A2y*fioir=6lbK~ek~YIzxlhb1 z1N6GC7-`DLvmuV3;|@OJ!GA zFSRHem5&~Tie_7RS-9Lsm4VuLUDE+(HQrI~dqOHpO0_}!y_=n<9`N>8_=wFSs0x%d zns4{C1nqC%vta06K!s`>!;Z~;z|FpYJxDddc8+0~?cW{Ns z(j+XZhNAi#>HTNwDSIuOc~WQqB_Y8X?f^s@lOv#c3y>UVH*b#%rnSfnF*Is?mD?#z z_Ss4C7s*%vxx1{-g^umVSUDHC=0XLXK&OFqhC8hyaa6%lG_43VDTLJj=Q+CP-cSu~ zK!=Hz2|Lelu2aI*bs<@QGN;9%G0sm_lc_Nd^R2LF#&FBS(L#MtMoz8eG^f2?s=Zss z$3fg3Ww}l~jk0L*9%46g|G{7UU*niQ(;$BDi-&tKQh^XZ+QpxtU?7v;e$CmlB`gbN z4qI>iDXlD!M3aPa=u|vr-#NdNT^f;n<5|;KEn@d6*~Lt=O2QQK!$JB1$Cx(5xE~4Z#nuM3Zw7Mxduoha0ibc-m&bIq^v1G zXGxTl|60CHkIQs@CmYSJS*vpwlZ5F+tyOiCLUL24{Zp%cX2`|oUO({OYsClXp8wnn zb?loZ#1oM`K$ZmuM|W-Tk7CBgOxD9n)*V!!0z2=-``U#+5Hxo}6N<`NY_*ic%JYP4 z%d(af%YrA?|FTJQz?LHOT9pjzpD?4R61*D9r}ZV>^^22$8O{C^yrF)#ihYH6F>+#d zC}54`ZTT`2{wkbkrKFm0(#fHCN~e*cLgjB2n9v2iax{%s?$yR*iTf8*bMF}yxo*C~ zGS39HxPzlyuzHY#+>;#-{80hWnu*erwnS?H6vcJ{yf80ur-yaHD3>30jOWuk0YSQErWS zR({Wuk~7&qS!W^GUaQ>!vInK5%P6LcS1s`DBR7YoKPpyS^O78+I2%S`)-fk%mzu*z zJ_v^zUlQ$$~kbYFvItkFc)jHwx_ag58QA7^$=RNh+2yo5DMQBvuOPXqu zBqHtX=SdV)7s(MtVZ6m-Y5K`njf^&($9+U5nHt9Mfh$NT8#nP5MXy)vU&xL{<6Cb! zmjuNmaqQ^XLH-Y*J7++|Xbs}^b~_y=p~1=-b&prE^}ZOWE7>x7&qvt%ptxZjpONlQ zFY4hv-;bCN;>CLZ)=BxJhXL2R5A7y(FnyDL)&13J&Q5$GJpTMcaqCetF6(N`u+Hlr z+*ZsW{a!>P;B#GI8~J|Z)hyzjIg@degutn`d&XqASUiJ(L4-rSl>t~Jyx;{f&r3@( z_@zcxTv?GZjWyLx6f9DdzwClBCIGYpC)aAh?&%^k;WAvci?7HnPNQ&6F?#KxHnhsP zlvfG`5zhf~Z-sp!@xRGlg@kiTIGRPr=#;HO%py_OP<*xv#6wUwLY3@QsUS^}lspEm-#!@80-mANQ?X29`+YBf zQiX46(n`4T3H|H)x%!H90%nX(ryvPhJBrK1R>}XI$`;Iwq`kqcg!l8U05K?T6EL5L zqWHmOEdG+3@~-h=P0vL))t`S?0s3;Ob?`MrUa^X@r=Z>+*&5MkAIR3oPJnr6CzG2} zE>>SA*U7Y&E1`(v)>_*C#Y~*CmeSa*s_|3rZ{qcv@=O<6CICwV;5_(SSOo3xF zAVdq}SJfE7{+661iA)!3H^Ps6Hi&gN?^8t}#jZ#Ap8^k^`Yv395w2VCJrQ>3aef(R zieC7M??t=|YghGZ%9*b05~NsBj!_OoBF+%&TQ%P#g07nXaRE?&k$^((f?Mk^YWXl! zATGL;4{w6`*y6+gHFo8FK@gFG(Y1)(XP1Ynr8g)3=3MUlwZ4g*P$uoJ(4V>VJxww| zdid>pTj+OTQI{I*0%HWQGIR3M0Y~Pv#>`{bD#um7;!&jDxR>cyL zS3xhx#DU7DyICt#qS^Y0%B%J)m%cPmaNad++8FT9#dG*$1I0)GQ7_|F*Jc5&o zhy)N%##EA3ZnV4U*H^2@lq1&R_Rvh*eoch|-%^Spm|RiTH(6h~HQTAR~lM!HoZprYHuZ-343;CV2cJ)GZ#Rt!ZwegY~M9L}o3VIHy0S z>pK6mFR-JDw8A}KMpd(VCYUcEjvbf9?%8#u7LZC*oi+1r6e1+dkFsB-sbhE@rN&Vd z7pi)i?$KhBWZ3C}5TeeL-3fYW5BL~Q#}^}0PFr?ZR+T`e|D6Ww-s;Rlz>k{?h2rs-Ung3|8T-n8ri&Z%GjG;O6#PXXFm0E+f6J}h z2m2#&&O}~~(WtD?jiQ6hG82ea^C0@pTDBb0BmiCXg4F!J?HpgvbV|| zB!mSt)31T!UmCrZbitUs-qZ`_K`Y$NEq%53(aO~amUuG+RUuz7(%HY5ec&5ZbN`D4 zJs1ypqH7$>ytS7;K2EOh1>XYYZ3^0@Glv?#j_(x)f76m>7ddjQ3mbZnIH3~H(4BYh ze~z~0KvEW8MeIT~2QDZH{+si{am^OFCI{~dSr6tz#6%5Hm`kh*nEu7Kf~H3UjY5od zMB6(atxaFS;m@gA!BBl74N!UG*f&hh5m=$HA|Xx)k|tyMJ)1}qJY%kJ)uV%OB(bYkP#<7s4Yha;yo`Mctkot zzZ$+X-lB>~(Cfr<2}T5w#bieuF>$50$Ky)eJ9HMRbz0kjpG zgsH;3<@(O?%KBg_FK0LA$-cjmx?P%9JPD&Y!V~U=i}l^K8V<~=giOi-jWR6=*_3uE z6^)+D_s|qFA>l0-M-T*;X{-0x!P4a;{$o>V&Yted@Q1<`s3?Mah?uvtb-Ni^3u|c} z&o4Tz$U0}J<|YHeD{gYt#u)g)ROVQndy{!D$|b+J0BQeK4L$-9{{rNPHI)Du9h|D~ zeUNf*GCi|sj@}?EhnVsDhD9ZRYd_sntuvc(o(#pn3~?c>6W{>&>l}|E=XSm ze&U~`pRNb+ZGdA@?{5RiR=i7#w^Sv9!JCfUpc`dp<>RBb(*MoPs(Wmi$aN4CP%u($ z%u+#f&L2QI%x-M6dD<-;M2}XQvm}gk|H^rcyun9@5s|gJoxe0{D`M_bq%}J`8?ST@ zmu?b}QBT;k<54;*?|Z{(ZJV+@J#?zG%Er_{za($5OMP>}u%U%}j2%ca^)vp4NW0gJ zwelN~Z60H(Z6I%QkhDzoraRYiDXuzsAx1cQ{3+0nXljwW4wwlyygkyUpKZcsq~p#e zbtx^vdFM6rFizab6E#YZW=s<(u`P}|q(upg|Hct+i?Ci%FgJX# z|97#lI%g|rC<7623RHwHNjH3vYImVSS}lPMPE*-|CG@lid9uuc8y%7%RA|%<4-x9f z=;p0}IGmovd>5^OAm_G&U4+9JF9TSlmm=8(_+sh?6#C(bvE+$|nyU9jYu z0Pr3pD8AB{vVdB2Qf%oLr@E`n1*Y3nwRO$D3C$u+UlVlYP&KIn1rG3H)rJ*%_7;u*t{VX z2w_i~=t(jiu_DZy36^16R{Uy0H~^e!Tof2z0L;y&+@W*g_sOv9&gHHYHwO?u$a|P1so-=oP>0kZ<>?H9oCAFpd+FSk3&Iv2$&dK(8!R zHpB4n2)Q^uW$%e@7>LlOdy$;aG%m!yx=I6nAT^k;LY5Yj@ILaXj8GT3e@@yhsii0U zQ|Q3sF^;?yKA-C)FSfoOs6iV%_D%bPtC^MSWJ7?#9^>Av-F-(`)Lg16VfZu2#TCNr z_(10Mftm(O4n*Dym~wc-x>gldXZ^A6All%jF`m89_}iHUNg0N~_jtGSE*k&{Z8XRy z(i*|xGYa^r<$pV51k@V9k944;Km_F@Mf=iwd9#5(xMnGdc%VmWfzuiNbMSmd;`v4E zW9tll4#R}0!>QiJswQ*9a(@-^J_zEVWH6YOH}?LHB2qTy_ITMW{z^_zq=C`~lz52aF`9&X7I*t$^~=0@W$bh(mat5OWISget+XLgm3am{A}2*ybbkvKg)0b%_ zbmNE|Y@6d}maHXRDlB;lsoJXwL*w_LcI`&~GvWjxEQ;goJ)vConbf0xDCc z^JMQ4y>?%`_Sx)#cuT2kx?f=h8*%qx*CHy~eYpLK%a5nNRqo81Eg(d7gse0TW2t`L3E2A<=Ddjn70n~^ITrQR3lZpn~4_>1sm>lc0=3pwQ;LvzSmk zLyO3jbWwO#ZObuf>x)zz7503W{4G@;cFJAa1xjQO?ZHk zpR8c#HqWke9!g*NIpyY9G?~gx2LSZgT!|1+CSzpH2|vW^yd9ttdf7rs;#huUHQ++B z>B3F$+tOvcl6W#2y)fQeso|i_R9jy|&M0T-6INl* zX_1Tl(@bG^r{ejs!&rj22AVGS#O$pm+l!(_>*maMj(5-iD#zcBMc@+pK)5XnEf zc$4=i*^@FRo(Eh32XUUcR#5#?+JO$ic$wf8Y}jOPX;zc-`mo$PNdB=I6I}#-JR`S; zs6!iM>`qEKYK44oB|1%)79pA6EeC-xl|jN!F>QSII8Fa=pSL^YSid**d_sljP6O$( zN)iFcGo7a4&79RcFP_p5Q_k$!O`{g%YRZ({RF5IX+(^oYFoS?{Y_*)tQU6jZm%7V> z66$BM1H=#FOiBn^82xZp3H)>epszL4_#uM1i?(|Cgv*5JO>m=#1Pd<+8y9IA89SY> zNn&jNXx2}O(+_`9)_A(9eP}q4H3wCk8M(9xvG|{_Mo;YpLZ_=pW?WqGdS&(Lemomt z(fQJ}A>BdfmY4gIMuAl_4L%h)f=adg-{Bw2_F$M9jutri-3T)$Xk!nM0enwq%0&-7 zq1a-g;YBVEp8KO?8w_R~C+cBFdQvP6=FF~odX3U!2o;m_$iW7w*l(3>`(K#z{Gp}@MBaQ+FX|+Y8Qr^ z`m@8tJjFLlX|4!jQyCLmU%acojL#{5Q))DY`?n5D`%Z@N<=aN91shcfDEyd|LKQ=v zS<>P{vx>7#T85wnE z#Ja0vVW&FyLu{Jf=rK$77<%M_R7Q$^()S`@_uG6LJP--tlI1z4c@E}qDl8$vMRvn+ z8sfA-(jiqMtqaksS-yLCE#2G6c4`1nsob7%8X>{evkELSB_VYTMssPL1s|-4l(z;l zqI{@9gVpUPEEhDi34o@VtnTn(r6xx6HFrXcDNJqlWOqx567eybU5Vqt40yOr<1UAo zWdX>gao>^QWQLbtW(uB4Oj_3ONAW5RTsulmpS-?t7DlaEKk9+rB$ zffQF`la6fao1Lja?AjYl$ElLvF8oK9D{YZLI}HuLbG5x}r>HD{HO&Ljwlcos14Dbj zZ*bq}ks2W^ZKG-Tl`^U|Ge(f>KW+C$C2QKUU?sBsYySvy-rF0qLr`&!k&yALwp=qlL!N`G zWMXKx*1)}!2LR){EL9eGi%h`ijPiEzYa8=d;jr8PYa*A8P}|snOy8P$<`3~fTEtY( z+E1coh1adxA>my-rwF!#TTtJ_kH^5a)*jrUMe==Vus3rKs-EQBLh-cmz)8@{Pm+!q zcK!Bycce`Qp1uTu01WUgk+FNz8gtCBD!w|KpsAXJf!F72y{Lv*@~eL42SQO z-Bw`%Fdsg@(*qD&muyEnu@xHT2I}xbAB~iHIVKEjw!w*r3TEFUEu00Y<_(EzKaNpP z*vVFs_M(5Yr_$(`t-KUc7XX^yBT=N`%DcRJG0dpXP|7P*RnjaOM}yTLotORkV4;=^ z=^@hUA!tG#=OBAhZXsinQZ7cHbd&G&7ISVDzbh4BHY-_v&}}1;-Y97V_0%G<7l|}d zN7A4~3AeRWN60izF;A{T94z^sD6@F{n2{f5qc*h(=#%)7ad_(o0Q{-mp2PlNENM%X zS^CUG>^uA_^j^s~i(V_gDa+)!AwwE?|9t`GO5LzmuG=>bzHj1rhR-`K$IzwWdj1x~ z*04b2qX*Nw@hWJLWQVz^j#kpCCcBRo;t-L)Hx^D#)=7(>yY-CeC7p8mdchr}Ml*$gak+}lPSrE{sLtS}7IM>xr!IA5l1u2Dr48r9PS5@;VKqL10-1y447lcyxU zIXpxqcu%9dA2QEQnm!>?;CY5bX#Y>2qtsNx>SWLY09$K;z9BiUV#knq>mP9dLdRX+R0uIL2Pzw4W{eaWFn?X-n!pR!$q&C+P;P5F!E1P_|R%_RzKk-}E-v)!Z`V@ce5w$U+Vc(j52vH2W`8!yJ7APjY3 z$}PXkAkpn1>N0+A&Zneo7L!2`aLqln7$7Cz^dq(vmKK_uIQCcG8-2zc8SiZs=HFij zx_@v5_6APXzV5Nf{wWY7ep_EyH9t?(T;Ol+&9ZvTsAMq0#5M4yYkW#bH4d^BQc^~F zJp0UoJsCS}5w=(KLpZnekZ8#Rp2{NWjxrlU`tc&UpVMZh%V6ZG-@Chl-0X;G1cCvW>|P7qr<%e#i*mD^bE zMLZ=NEkLMm4<;uFfZSqg&`?|qj~<_C=9~Gzm*zuijXv?sxd6+>a01-Ga?#P+%wB_D zL&pfV`SJmJz+0(9(ceirwl>BF0RHyw#=G`o=;&Z##7eg@R4Jmm(~)M_yFF5jqwWl` zy;)vP-_OGS&bM-^tNQP|F@F53o$z$qQu}n~C(t?c2qKDh?((<^{l!Og;@^sW#AFN94Aw;KS(R8Ka`+2DPTRdC z2wZ%0^2C6VGWlg8fA~g-j4Bu73OEC}U;a2|vk)3nq;H5s*U^n@=zpmKM=E*qhD~5h zs9(}6j95HIP@plYzHP{@(ye(wzEM|seb@JLf54!P%JvK zrSyyB=fM4ZobT&$wB7NXbCj`14b!%ZCDmEzFl6Q`q-AVs9wwst98a&xfVH^8%A)VPDakPm3R+Ou5S~ zB>xu@rP#3APc;?j1hj@veRmoQaX7acqf zY2)-${>(B#%3b1qE<7}o(#El51WOpd$%d!*l?iCrg&x&|$gFUu{K&e~<>@ljZPc!VKqoypT>_+3p<`7mSIp znD-7tmBp()xMd!@8aUKai5JJ-Zbgm&G92Y$q5-$K&n&}PQU2m4^24VY>s(tG?wYt} zl#j$Kkxpuz{u^PWDNGyZj0?r}^aMh&9hV5Un&E~2L~D4ao+xOIQYxkasdq>U#?yQnbw02N)~mY#l7G38)CJwtoA+!06sRsPqi|@hyRl!KaqhLN zBi(|ZASVyQH4^N47*=p-F-sS|x>4;&tOe1VRdsT%Uk&Zq9C#ul=INb4mnuac= z2a_w&Ts~3wwd3v;<3^y2$0T2TYBcr{p?n= z0#uWy_TofaF)Wc9W#H3}(>2>Iwv~keucj5(&`$_PFvMYeHxnRa=_hI@B$8dZQvB!Eemor1VyRv#FE(4Q~Lvh@X^ z&}umdT^0y!@Q0Av(}b5b_Vx?X``7bGU~=Ogotrr@UHsa2{IM_LCa@?KRK6(Nt>&k+ zqTDFZoIJF8=f{|$0c&wCE$!!N8fX>L7U>H9aSEV9=qKKfJcaO=a;wO-d_*Re@JJ2) zihhNya}LE9XHT>F^1GK17!fM<{wuVkl%7kz_gZ%?ntqJGe)$<)@9x;t51@F)DXtb1DXePqNHGFzdI&0sa5ANor(&W=8Dn&K z&msVbbrj!xFXr8j6!h=}d zkrU&z5$1M)(K#S~cFsiBl=ST3*rv608hpC8TPDwJGeun(4u-0{sru|}-7)CY4Kg0N z@tLqzfUuGgd)|oOrZ42>9alN4lO1PBT$xQTZEPVbi0P)Zo)Cd<`R0gCw2ah9%}M`6GvlWdPAjHdT&hp;mQ0U)#61_=y-x25N4T@%f?i@8(u`c zki>P>qr;e@(aunL65S1|GBfg4ooFOpmpx+pDZPQS!|^NZ^5xsqjAHm2{j-Df75sW@ z)&#R(m}V_(?}T@pJKq&C->i0d!55~Z-1w^tPAx!qj%qeGRqr_E5xyg3)ZRtAGzm}Q z4?ojhWtY@vg8-saYM3OIcHG6HVsY!?3aAA^2>bEZe7Q!e7nzxG6RWk0jiO5G2e*yw zaO{jXHbwoe7iQ@UiyU`|FHCs}?3T)aCw%9m*Qzt$1nvUNw#FN>fU4#)yu%a^tsr3k zp}9kbIv<*MB%B+94t)xBtPqNILL6i0B!8W0K{3W8C-W_QScI6Bhp&}Sn*9?y^Eu|p zJ#7BDoQzJ3Do#S5K7OJM4KtG3j}Ev5bate7koO5SS$l?=3qNV2I+#M6v4}eqf>l*V?5a}31Ga?%+J5~uNBZZLzYl&zfTU!B^ClY1(< z_?dO)g57Z~^nf5t^M;_fGxJ2v0{KW+<|!n(sC5x)4D1fJOpVfm>HX1EU?;ip054;6 zvd`Ph=9pkCJqD(7zbi!;ps5h*pnv#&AFUxFdV8^iq)SoD1)qvzph~e9q?ePW@110f zohf`_%e!Ejay)S$y_3ZWQUCQ%flFz!Z>FP?K>aqzC40J?7%o^q`B_k8pQbZdQHHvQ z2%L&!r4*H3N4fBvL;88k>2bO7UFx`2uJNXCN2Ig0l&jHX6+#EL*Wi*uAxr%4McW+4 zmM~mE++@KV4RM6RCYUM!W@q%45lgLJDFDU`(Q}JoVw?~v|Cpj~3ev6gmO!i991gGt z74tJVypd@x?c87XZg##f-@U*udL)Ltw+F;ukFoP?|C%!2NsB5XA0*|VhEL~h74oT( z3!MNpojKPE*uD-bk0Z2g(9gRSBTIowM0)P!?o|Ibggc864TOO-bs=y+h<0%yN>*j* zTaUlX3s_}1RBnoW!R1i!0XLTzO1$BXT8|sBF&g;TkCiU$)&f`t3Vl3%3fd0Rzy^C-VMZOU2L)Z;KLDwkb zf4kxE8yDcs0nalA%Qvsh&7CS+O=C9aw>qk`KlY=8UFVzPvUQeiXZL9z1!WBn^Luj< z5%KC-#fh@$#u;9ruR`HHh=Q=b@Ilq+JSFQOhIXgqcqe?0fU{${j$E8`D`A<(&rlitOnGS$nCW#uu5MWlbwagUL(680F$24%`a84h! zYLVz>lnWNlNtF@am(Jjyxq*g{{-?k z9JJMPZ(#H{5#EcdFnTX-4G&)*BiItwr{yJcR?E}3D0rZe!eD;#+%CMqeB#(;*a_YX zt>BB9fZG6o)9P&ap2OgGcOp932#nKnW!)W})>Gd&HDwAXPp@Y-V&Z-Tjk&Q*^gFyD z{8}JWFK}a?M_zF*W5*HcBzn1~1Q^`w+X|#@RWSY%L(B%bky(0!=NUwH*&$VROR>2t zi^8A(1%z2}dPz0R{!)2PSyUX$zgsY$a=ucz{rg(Qg~C1UB6Rot7Qa$6_eHU?dahuJfHKT*Ng6}G zmKs&_HZg!QwC45UYURivbWrF|hJHXZH6{{*6VeECx4-xoY-Fa6+-N>Qz0jdRnu~K& zPXVlW`?XpPU7V23(pYyxK1!gxPir8ed33~4-SaHp>I>a(w4tN8xxf{M69o)JEDTCk z$T^*_O!VT9gWw4z&0A>|N>e_GGqd@%uXaxTQZ~Ut{!jaud&yYFe!#g=4MzWNLV_qj zLxuM>vSEAhuT)Q+yjKwdJ*u>`UFd^0bRS+-_t8o34k1&Sww2)I;qOn6C=gj-JZbp{ zqG*3TEo#8E@WXKTzX5|fNK8e6`m6Ju#@1d(O_`GQmOY}b_ZH&pgJb%=#;|>Rj^u~c zq~!uW1O_x~FYe_8ub)7V<^swG93Q_*R$Z@jSD_;#^+C-bx3A5X?F4MiN0%JL;5%E9f0feVG)oV!*-<5#ZVY zYRF^1KdRFo1VEgsU?A-NCu_b_Go|IH$xgCj|8%c?O{!-V(<+0$HRDP*>%LdQIz>Dd znX}lwC((xwWN+>>ETceX+;{bz0hBk(=tjNEDmV8S*^avhOC;M`3PcMcPgerxdDs=C zY}s7J^byyGUY}Gt(D7*)6|6vpIkb>6@><)-#}|q>jK>?qWQ>Li1VjQ}J@$(f*-?bB z9jpzf)JhRGhr1N}6q~gQIYwCgQhoI5+q#j*<=YWxWXN{V}tHR$w zfOBN@diOH_lCTNWoIsKzO)4a14z67<{XvDK|ky(J)UG$KA56D?XLne{X~Jn-RP8OlaZbJD1!sC0^C(K zvrdAJ3AxyJBJ_j?&Ic1!Zj3UPtBfqDCxiuws6Y?HACcKYFVwZ62?(Fy{*2fTez7L*m>`s#U-vBN%!K0|Al*9xedhHfkI(57t zA4Z-@Lol#c6KjycF+oEI}lV=wz)>d z=7J8V7|3hC9{>IZ`I{0PY5A#^Z{ZrP)fU*i5iCahccsEgionW=1p;9rV5oHYFZUM0 zFiNM0R&Z|)orB`*+H4Q|)|vVtJTN7{>9Q#mfMM>*8#vNChF>vDz#>elCGBdKaoS`hdW%ohikM~hudHR-V!h-;`ZmN44F$l@xD9%J!9#uax9*2 zK&+)=(_K)gZx!;UKayXZo0je@LPg04X zY>+9HQ&>JZ!%-w9y^XjjQsm?979$}dWwks$8|MkUBdD^a{s%wdTNSBq?~l)2Fu1__ zQM>_p&}N^a>C0o0c`4oFPu{}=p3+Xx;6!kDheyV3ph#eh?lqvGnG!|c@GYV zwo-6uoG1y2pbV$TwbHbZX@Ru^^u!!f+)zr8VV5p-$jkDth~JlsRnKm;=tMCp&1XTP zr!s{WyDhEw@)UO2kI`?csdY_x#+4snAU|UwmUvwP-HK*r2!G0c1-TIbRfAg$?Nu*0 zf9SZSVe|uDvrMSo`V^V_>JlXzOK~6&P0M5C-GSc%2m;&D_-%KJ-4l2a&PNWwb>!g_ zUIyB}nRE-msf~1Mdi~2d|J_N&q!~s-#rgcBlZan7xf76N(!s=LaKD4Hn}7+Ckh9 zXvtDkM>}r%l|~sZrqq;iYv?(Ho-?wvR%xhus8}_{@-~$ZgwkTAWRh6Vl5F`^qa76y zl81WBo&%D2QYQ{*JLtg&is zHvjD~chPGVoy&4EBM~`Wgp>D-;vWg~6Yxn)N;a{`pC}d>3Q@YkF}hPAQ|t=^WFr}B)3klP(}ayZVp#CP!&Ax^CSxA}0WnMhxRtFh zJ5HFC3r#6NvzkRHRwKaZS*UwI@_%x5Z+{LT@o+yQEm+U+n$sbZmw&%}X;)s`?yn{e z9;e>!SBiR<0uF~di?+u|(dPU!n%Na*Tuh~=%8-*RQH>ZiJbHnS&u*TOot+LcS1#Xj zKlKAcRoj`P!dJjZR*dMAb}h9ZgQ$FUJzVxq~O294axKHukB@c zzunTww8|K=grA}%D+_UYE;v%V8)lvpBM4;MvtY(i@1Q}mt%00c(fL<4P!w$FCpmWr zJ#8eEQ$GPu6lPv$+@Y^jQw+eq0T&AB^>`cyY|*7M=SR{PE}I9#bRPr&Q*6U5aBa2k z&%0G$Er7pZXiBC(@tdUMRPrrZgWG_@CSCwM2uqJ6rtKZCR3LSTD|z&d)~q#O)mU z0pf|CY~mi^%(~YnOVOvY707it?J)Fp<5%xu#>(GTub$xuz%1C;_3Z)Ny{Vsya%CBLs>TpR5eTvM_WFqv zs=lL9QAXP(%m*rp$+lcxOT2tYyU&-HMeomZhIcrADsLU5I2$DveXmT#+}z6dW6W=I z_A}-nq}}e;6uxnrpjU&=w7PnSwESR#Y8*I99*HJ(oiYMuAx>dh+M%v(^?ZA~%*gK} z(X+s7{D6A{uu^4&_f60)&g$Hfo4O~4#u5Nhb@gBB_AG`eC#kb`Mb}b*4q5ycVQ_u0 zQ<_DSi8|x5d$HO;<~Cz(G3Be(g6Z~H>tK>%Q8*G{&lono4FtA%->f0~y z(})1j{~p0$d0}=>)X2B&_#BaV{!4N5O6b+0V%2#TT4_J-gl9k!o!<`{W+y)N;CjG- zfL_@LHxeP&Oq89C@HX?>^%h(D%{Y+kF0s0Y{f-xfXO4pLC(UKnvdnocJjM_`{JzjB&+4W*u^3HSFDGpvoWZahoR_ipRn&3)^mlcVl4V0>VMZzpfF`Y zA*R}}j$@)P{|PBllaaj{(G5sBoFSH(67Y3!h*I; z^|&)U$4BS&->Y9QtKX}9Bgtx)ov#Rw%#lPheWgk0%vl1=x;UKkXowhX<98BTOfBl+ zrift?2RZ`(QOR^V$*z82`XjCip^xl;c0cX^p)3#6$^uNBtl&2Fot0ClHMyOr&AJbB zca5_G{KFt*n{24zbTnMO6W1E$*!nuxPgGbb0evEVx7->gr&}vKtdAz!@WqpB#zY*! zCBB&fy6i=&>BM~|M)e}TXO-`Tx{o8Q{1ux{y*k{`KCQkZBD8FD+WDixhxWUwKI?_l zJ*k2ylA?L2uSKlb$AMJvmP#BA5G2ah%$LxwKsTdt9w0+fCaq_P_pZSa=CgZD{uy6O zingrlnKV`GlK)Ehw&<-dofnjB7zY_V>{)km^??5x1Jwnsx}-2;lZlj2In@taRy4MB z(uI$oROy)mZ?OruBXcICb84T+sLgjtSM_dm#nQ{D<{W*p(m2rcItx$ZPq5X&eH}Km z5X44sDbt*Yu=hogOx6nOx5^~&J2x|Ex!!g$_|oMJk-tKhbV-L`#z!$vqlDmN739{cmJXd+7N8$loct&hpq~~tS}OOu9hOu$ z<7uKtP;XCe5Q`VX+Ix(_PYI(|^~%G)2^c$2-Cyn{3H1!OdNb*YI9VB;f+%_lnw;!N zeJ2w-pJd7R(t=fVpP47Dz!WwKr?~1GEyuP3eiY_EogypdIz`}7KjQV*>`SZpWS4Lw z)KS~FC*L9vQ5Z>CMz0hdlcDlThVuDR*Bs?~SAgdvV03 zz9qX_$6V;T5R_L=)?Yc-WQnv&u5+$0mMn}xQ5r zJG8cm<68RQDmy{ra8G=~I(FTR8_GYiKdGdYA+tVV+EE0MEDvtl(yWgP=N;Ib519Z^zS% z?gpQXxRL(0j|RYtKmK<(t^n*Q0<~PgG@L&}X5q%-*GN|FNAeom%Co#kRDB_Q}XmiI=d= zh_}VuYm2P_6VH~Cp7cVt>8#E*cpw+W*hT;gu*r|{jz6Cj3++7^9Ow+;?&m3J8>{v4 z@H`%qADVSXS-7<4jvO24>aZCzIWR%ODN%hU=?bXLg{KP7E?g77%iVpyeOznns%x?t z=^o3Hf^r)~^0}R-8QpujzrT{KJToagumqnIz8Sddf3jS>qsUpo1IuN&lKZ6>84B|@ zgx=oi2BALU_P+xf+&ko1xe!6&)BMtxl%48%Ttc=5R}fx0PNW@9=1h_PZx~SfPD^|8 z*~9y@$r_}H6ek%CSwW5>LNNB~8fUY=vdub;7k^^)B-UjJjJ4&!9vGcAsTo8>JwEVM# z$CF!fS_I*MRLrvORa)#94sRT8G=$NY4bErb=a-dR+FBFz51o8(Zwi);c>C=$a5@Jd z%$`sx8+}Lw=MiKQ3&xFED5x2MzygIq>uLHwxDI;cx?-}m0KpWt=BW`D@v8`=`htxd1V6}KwOJh6wze%_+-LJP?V zh$fiTUa1?+B-(Nhv2}pcJ^O!kksWgleT!V)a}iK+*vvxQ;Yrnkj)kVbO4+`cMg~St zhc;n71JiG(Rn`4x=gqJSFdF#MiTC0^njecuqKhAaOA8bj4F0;9)df%DFYNls_O`F< z5s!663uTmmb(W*$Fs5!J+GMxltOU7?6*w)gQkp_pD-5^KX4{8wHON-XNSvaPt-YRq|HFT00000@NqoHcig`)00H@v0f3+e^`wJ&vBYQl L0ssI200dcD`>q#W literal 0 HcmV?d00001 diff --git a/packages/flask-restx/make_orig.sh b/packages/flask-restx/make_orig.sh new file mode 100755 index 0000000..05fe90e --- /dev/null +++ b/packages/flask-restx/make_orig.sh @@ -0,0 +1,18 @@ +#!/bin/bash +set -e + +git clone https://github.com/python-restx/flask-restx opengnsys-flask-restx +cd opengnsys-flask-restx +git checkout 1.3.0 +version=`python3 ./setup.py --version` +cd .. + +if [ -d "opengnsys-flask-restx-${version}" ] ; then + echo "Directory opengnsys-flask-restx-${version} already exists, won't overwrite" + exit 1 +else + rm -rf opengnsys-flask-restx/.git + mv opengnsys-flask-restx "opengnsys-flask-restx-${version}" + tar -c --xz -v -f "opengnsys-flask-restx_${version}.orig.tar.xz" "opengnsys-flask-restx-${version}" +fi + diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/.editorconfig b/packages/flask-restx/opengnsys-flask-restx-1.3.0/.editorconfig new file mode 100644 index 0000000..21ed5ea --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/.editorconfig @@ -0,0 +1,21 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +# Matches multiple files with brace expansion notation +# Set default charset +[*.{js,py}] +charset = utf-8 + +# 4 space indentation +[*.py] +indent_style = space +indent_size = 4 +max_line_length = 120 diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/.github/ISSUE_TEMPLATE/bug-report.md b/packages/flask-restx/opengnsys-flask-restx-1.3.0/.github/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 0000000..535e5dc --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/.github/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,44 @@ +--- +name: Bug Report +about: Tell us how Flask-RESTX is broken +title: '' +labels: bug +assignees: '' + +--- + +### ***** **BEFORE LOGGING AN ISSUE** ***** + +- Is this something you can **debug and fix**? Send a pull request! Bug fixes and documentation fixes are welcome. +- Please check if a similar issue already exists or has been closed before. Seriously, nobody here is getting paid. Help us out and take five minutes to make sure you aren't submitting a duplicate. +- Please review the [guidelines for contributing](https://github.com/python-restx/flask-restx/blob/master/CONTRIBUTING.rst) + +### **Code** + +```python +from your_code import your_buggy_implementation +``` + +### **Repro Steps** (if applicable) +1. ... +2. ... +3. Broken! + +### **Expected Behavior** +A description of what you expected to happen. + +### **Actual Behavior** +A description of the unexpected, buggy behavior. + +### **Error Messages/Stack Trace** +If applicable, add the stack trace produced by the error + +### **Environment** +- Python version +- Flask version +- Flask-RESTX version +- Other installed Flask extensions + +### **Additional Context** + +This is your last chance to provide any pertinent details, don't let this opportunity pass you by! diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/.github/ISSUE_TEMPLATE/feature_request.md b/packages/flask-restx/opengnsys-flask-restx-1.3.0/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..11fc491 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/.github/ISSUE_TEMPLATE/question.md b/packages/flask-restx/opengnsys-flask-restx-1.3.0/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 0000000..8abaadd --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,14 @@ +--- +name: Question +about: Ask a question +title: '' +labels: question +assignees: '' + +--- + +**Ask a question** +A clear and concise question + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/.github/PULL_REQUEST_TEMPLATE/PULL_REQUEST_TEMPLATE.md b/packages/flask-restx/opengnsys-flask-restx-1.3.0/.github/PULL_REQUEST_TEMPLATE/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..88e1677 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/.github/PULL_REQUEST_TEMPLATE/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,25 @@ +## Proposed changes + +At a high level, describe your reasoning for making these changes. If you are fixing a bug or resolving a feature request, **please include a link to the issue**. + +## Types of changes + +What types of changes does your code introduce? +_Put an `x` in the boxes that apply_ + +- [ ] Bugfix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) + +## Checklist + +_Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code._ + +- [ ] I have read the [guidelines for contributing](https://github.com/python-restx/flask-restx/blob/master/CONTRIBUTING.rst) +- [ ] All unit tests pass on my local version with my changes +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] I have added necessary documentation (if appropriate) + +## Further comments + +If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/.github/workflows/black.yml b/packages/flask-restx/opengnsys-flask-restx-1.3.0/.github/workflows/black.yml new file mode 100644 index 0000000..9065b5e --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/.github/workflows/black.yml @@ -0,0 +1,10 @@ +name: Lint + +on: [push, pull_request] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: psf/black@stable diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/.github/workflows/release.yml b/packages/flask-restx/opengnsys-flask-restx-1.3.0/.github/workflows/release.yml new file mode 100644 index 0000000..4e99d76 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/.github/workflows/release.yml @@ -0,0 +1,28 @@ +name: Release +on: + push: + tags: + - "*" +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Set up Python 3.8 + uses: actions/setup-python@v1 + with: + python-version: 3.8 + - name: Checkout code + uses: actions/checkout@v2 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install ".[dev]" wheel + - name: Fetch web assets + run: inv assets + - name: Publish + env: + TWINE_USERNAME: "__token__" + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: | + python setup.py sdist bdist_wheel + twine upload dist/* diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/.github/workflows/test.yml b/packages/flask-restx/opengnsys-flask-restx-1.3.0/.github/workflows/test.yml new file mode 100644 index 0000000..51c02f2 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/.github/workflows/test.yml @@ -0,0 +1,74 @@ +name: Tests +on: + pull_request: + branches: + - "*" + push: + branches: + - "*" + schedule: + - cron: "0 1 * * *" + workflow_dispatch: +jobs: + unit-tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11", "pypy3.8", "3.12"] + flask: ["<3.0.0", ">=3.0.0"] + steps: + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + allow-prereleases: true + - name: Checkout code + uses: actions/checkout@v3 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install "flask${{ matrix.flask }}" + pip install ".[test]" + - name: Test with inv + run: inv cover qa + - name: Codecov + uses: codecov/codecov-action@v1 + with: + file: ./coverage.xml + bench: + needs: unit-tests + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + steps: + - name: Set up Python 3.8 + uses: actions/setup-python@v4 + with: + python-version: "3.8" + - name: Checkout ${{ github.base_ref }} + uses: actions/checkout@v3 + with: + ref: ${{ github.base_ref}} + path: base + - name: Checkout ${{ github.ref }} + uses: actions/checkout@v3 + with: + ref: ${{ github.ref}} + path: ref + - name: Install dev dependencies + run: | + python -m pip install --upgrade pip + pip install -e "base[dev]" + - name: Install ci dependencies for ${{ github.base_ref }} + run: pip install -e "base[ci]" + - name: Benchmarks for ${{ github.base_ref }} + run: | + cd base + inv benchmark --max-time 4 --save + mv .benchmarks ../ref/ + - name: Install ci dependencies for ${{ github.ref }} + run: pip install -e "ref[ci]" + - name: Benchmarks for ${{ github.ref }} + run: | + cd ref + inv benchmark --max-time 4 --compare diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/.gitignore b/packages/flask-restx/opengnsys-flask-restx-1.3.0/.gitignore new file mode 100644 index 0000000..b8f00fe --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/.gitignore @@ -0,0 +1,70 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +bin/ +build/ +develop-eggs/ +dist/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +cover +*.egg-info/ +.installed.cfg +*.egg + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml +prof/ +histograms/ +.benchmarks + +# Translations +*.mo + +# Atom +*.cson + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# Rope +.ropeproject + +# Django stuff: +*.log +*.pot + +# Sphinx documentation +doc/_build/ + +# Specifics +flask_restx/static +node_modules + +# pyenv +.python-version + +# Jet Brains +.idea diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/.pyup.yml b/packages/flask-restx/opengnsys-flask-restx-1.3.0/.pyup.yml new file mode 100644 index 0000000..faf1e19 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/.pyup.yml @@ -0,0 +1,63 @@ +# configure updates globally +# default: all +# allowed: all, insecure, False +# update: all + +# configure dependency pinning globally +# default: True +# allowed: True, False +pin: False + +# set the default branch +# default: empty, the default branch on GitHub +# branch: dev + +# update schedule +# default: empty +# allowed: "every day", "every week", .. +# schedule: "every day" + +# search for requirement files +# default: True +# allowed: True, False +# search: True + +# Specify requirement files by hand, default is empty +# default: empty +# allowed: list +# requirements: +# - requirements/staging.txt: +# # update all dependencies and pin them +# update: all +# pin: True +# - requirements/dev.txt: +# # don't update dependencies, use global 'pin' default +# update: False +# - requirements/prod.txt: +# # update insecure only, pin all +# update: insecure +# pin: True + +# add a label to pull requests, default is not set +# requires private repo permissions, even on public repos +# default: empty +label_prs: update + +# assign users to pull requests, default is not set +# requires private repo permissions, even on public repos +# default: empty +# assignees: +# - carl +# - carlsen + +# configure the branch prefix the bot is using +# default: pyup- +branch_prefix: pyup/ + +# set a global prefix for PRs +# default: empty +pr_prefix: "[PyUP]" + +# allow to close stale PRs +# default: True +close_prs: True diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/CHANGELOG.rst b/packages/flask-restx/opengnsys-flask-restx-1.3.0/CHANGELOG.rst new file mode 100644 index 0000000..f687464 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/CHANGELOG.rst @@ -0,0 +1,342 @@ +Flask-RestX Changelog +===================== + +Basic structure is + +:: + + ADD LINK (..) _section-VERSION + VERSION + ------- + ADD LINK (..) _bug_fixes-VERSION OR _enhancments-VERSION + Bug Fixes or Enchancements + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + * Message (TICKET) [CONTRIBUTOR] + +Opening a release +----------------- + +If you’re the first contributor, add a new semver release to the +document. Place your addition in the correct category, giving a short +description (matching something in a git commit), the issue ID (or PR ID +if no issue opened), and your Github username for tracking contributors! + +Releases prior to 0.3.0 were “best effort” filled out, but are missing +some info. If you see your contribution missing info, please open a PR +on the Changelog! + +.. _section-1.3.0: +1.3.0 +----- +.. _bug_fixes-1.3.0 +Bug Fixes +~~~~~~~~~ + +:: + + * Fixing werkzeug 3 deprecated version import. Import is replaced by new style version check with importlib (#573) [Ryu-CZ] + * Fixing flask 3.0+ compatibility of `ModuleNotFoundError: No module named 'flask.scaffold'` Import error. (#567) [Ryu-CZ] + * Fix wrong status code and message on responses when handling `HTTPExceptions` (#569) [lkk7] + * Add flask 2 and flask 3 to testing matrix. [foarsitter] + * Update internally pinned pytest-flask to 1.3.0 for Flask >=3.0.0 support. [peter-doggart] + * Python 3.12 support. [foarsitter] + * Fix wrong status code and message on responses when handling HTTPExceptions. [ikk7] + * Update changelog Flask version table. [peter-doggart] + * Remove temporary package version restrictions for flask < 3.0.0, werkzeug and jsonschema (jsonschema future deprecation warning remains. See #553). [peter-doggart] + +.. _section-1.2.0: +1.2.0 +----- +.. _bug_fixes-1.2.0 +Bug Fixes +~~~~~~~~~ + +:: + + * Fixing test as HTTP Header MIMEAccept expects quality-factor number in form of `X.X` (#547) [chipndell] + * Introduce temporary restrictions on some package versions. (`flask<3.0.0`, `werkzeug<3.0.0`, `jsonschema<=4.17.3`) [peter-doggart] + + +.. _enhancements-1.2.0: + +Enhancements +~~~~~~~~~~~~ + +:: + + * Drop support for python 3.7 + + +.. _section-1.1.0: +1.1.0 +----- + +.. _bug_fixes-1.1.0 +Bug Fixes +~~~~~~~~~ + +:: + + * Update Swagger-UI to latest version to fix several security vulnerabiltiies. [peter-doggart] + * Add a warning to the docs that nested Blueprints are not supported. [peter-doggart] + * Add a note to the docs that flask-restx always registers the root (/) path. [peter-doggart] + +.. _section-1.0.6: +1.0.6 +----- + +.. _bug_fixes-1.0.6 +Bug Fixes +~~~~~~~~~ + +:: + + * Update Black to 2023 version [peter-doggart] + * Fix minor bug introduced in 1.0.5 that changed the behaviour of how flask-restx propagates exceptions. (#512) [peter-doggart] + * Update PyPi classifer to Production/Stable. [peter-doggart] + * Add support for Python 3.11 (requires update to invoke ^2.0.0) [peter-doggart] + +.. _section-1.0.5: +1.0.5 +----- + +.. _bug_fixes-1.0.5 +Bug Fixes +~~~~~~~~~ + +:: + + * Fix failing pypy python setup in github actions + * Fix compatibility with upcoming release of Flask 2.3+. (#485) [jdieter] + +.. _section-1.0.2: +1.0.2 +----- + +.. _bug_fixes-1.0.2 +Bug Fixes +~~~~~~~~~ + +:: + + * Properly remove six dependency + +.. _section-1.0.1: +1.0.1 +----- + +.. _breaking-1.0.1 + +Breaking +~~~~~~~~ + +Starting from this release, we only support python versions >= 3.7 + +.. _bug_fixes-1.0.1 + +Bug Fixes +~~~~~~~~~ + +:: + + * Fix compatibility issue with werkzeug 2.1.0 (#423) [stacywsmith] + +.. _enhancements-1.0.1: + +Enhancements +~~~~~~~~~~~~ + +:: + + * Drop support for python <3.7 + +.. _section-0.5.1: +0.5.1 +----- + +.. _bug_fixes-0.5.1 + +Bug Fixes +~~~~~~~~~ + +:: + + * Optimize email regex (#372) [kevinbackhouse] + +.. _section-0.5.0: +0.5.0 +----- + +.. _bug_fixes-0.5.0 + +Bug Fixes +~~~~~~~~~ + +:: + + * Fix Marshaled nested wildcard field with ordered=True (#326) [bdscharf] + * Fix Float Field Handling of None (#327) [bdscharf, TVLIgnacy] + * Fix Werkzeug and Flask > 2.0 issues (#341) [hbusul] + * Hotfix package.json [xuhdev] + +.. _enhancements-0.5.0: + +Enhancements +~~~~~~~~~~~~ + +:: + + * Stop calling got_request_exception when handled explicitly (#349) [chandlernine, VolkaRancho] + * Update doc links (#332) [EtiennePelletier] + * Structure demo zoo app (#328) [mehul-anshumali] + * Update Contributing.rst (#323) [physikerwelt] + * Upgrade swagger-ui (#316) [xuhdev] + + +.. _section-0.4.0: +0.4.0 +----- + +.. _bug_fixes-0.4.0 + +Bug Fixes +~~~~~~~~~ + +:: + + * Fix Namespace error handlers when propagate_exceptions=True (#285) [mjreiss] + * pin flask and werkzeug due to breaking changes (#308) [jchittum] + * The Flask/Blueprint API moved to the Scaffold base class (#308) [jloehel] + + +.. _enhancements-0.4.0: + +Enhancements +~~~~~~~~~~~~ + +:: + * added specs-url-scheme option for API (#237) [DustinMoriarty] + * Doc enhancements [KAUTH, Abdur-rahmaanJ] + * New example with loosely couple implementation [maurerle] + +.. _section-0.3.0: + +0.3.0 +----- + +.. _bug_fixes-0.3.0: + +Bug Fixes +~~~~~~~~~ + +:: + + * Make error handlers order of registration respected when handling errors (#202) [avilaton] + * add prefix to config setting (#114) [heeplr] + * Doc fixes [openbrian, mikhailpashkov, rich0rd, Rich107, kashyapm94, SteadBytes, ziirish] + * Use relative path for `api.specs_url` (#188) [jslay88] + * Allow example=False (#203) [ogenstad] + * Add support for recursive models (#110) [peterjwest, buggyspace, Drarok, edwardfung123] + * generate choices schema without collectionFormat (#164) [leopold-p] + * Catch TypeError in marshalling (#75) [robyoung] + * Unable to access nested list propert (#91) [arajkumar] + +.. _enhancements-0.3.0: + +Enhancements +~~~~~~~~~~~~ + +:: + + * Update Python versions [johnthagen] + * allow strict mode when validating model fields (#186) [maho] + * Make it possible to include "unused" models in the generated swagger documentation (#90)[volfpeter] + +.. _section-0.2.0: + +0.2.0 +----- + +This release properly fixes the issue raised by the release of werkzeug +1.0. + +.. _bug-fixes-0.2.0: + +Bug Fixes +~~~~~~~~~ + +:: + + * Remove deprecated werkzeug imports (#35) + * Fix OrderedDict imports (#54) + * Fixing Swagger Issue when using @api.expect() on a request parser (#20) + +.. _enhancements-0.2.0: + +Enhancements +~~~~~~~~~~~~ + +:: + + * use black to enforce a formatting codestyle (#60) + * improve test workflows + +.. _section-0.1.1: + +0.1.1 +----- + +This release is mostly a hotfix release to address incompatibility issue +with the recent release of werkzeug 1.0. + +.. _bug-fixes-0.1.1: + +Bug Fixes +~~~~~~~~~ + +:: + + * pin werkzeug version (#39) + * register wildcard fields in docs (#24) + * update package.json version accordingly with the flask-restx version and update the author (#38) + +.. _enhancements-0.1.1: + +Enhancements +~~~~~~~~~~~~ + +:: + + * use github actions instead of travis-ci (#18) + +.. _section-0.1.0: + +0.1.0 +----- + +.. _bug-fixes-0.1.0: + +Bug Fixes +~~~~~~~~~ + +:: + + * Fix exceptions/error handling bugs https://github.com/noirbizarre/flask-restplus/pull/706/files noirbizarre/flask-restplus#741 + * Fix illegal characters in JSON references to model names noirbizarre/flask-restplus#653 + * Support envelope parameter in Swagger documentation noirbizarre/flask-restplus#673 + * Fix polymorph field ambiguity noirbizarre/flask-restplus#691 + * Fix wildcard support for fields.Nested and fields.List noirbizarre/flask-restplus#739 + +.. _enhancements-0.1.0: + +Enhancements +~~~~~~~~~~~~ + +:: + + * Api/Namespace individual loggers noirbizarre/flask-restplus#708 + * Various deprecated import changes noirbizarre/flask-restplus#732 noirbizarre/flask-restplus#738 + * Start the Flask-RESTX fork! + * Rename all the things (#2 #9) + * Set up releases from CI (#12) + * Not a library enhancement but this was much needed - thanks @ziirish ! diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/CONTRIBUTING.rst b/packages/flask-restx/opengnsys-flask-restx-1.3.0/CONTRIBUTING.rst new file mode 100644 index 0000000..8e0f675 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/CONTRIBUTING.rst @@ -0,0 +1,135 @@ +Contributing +============ + +flask-restx is open-source and very open to contributions. + +If you're part of a corporation with an NDA, and you may require updating the license. +See Updating Copyright below + +Submitting issues +----------------- + +Issues are contributions in a way so don't hesitate +to submit reports on the `official bugtracker`_. + +Provide as much informations as possible to specify the issues: + +- the flask-restx version used +- a stacktrace +- installed applications list +- a code sample to reproduce the issue +- ... + + +Submitting patches (bugfix, features, ...) +------------------------------------------ + +If you want to contribute some code: + +1. fork the `official flask-restx repository`_ +2. Ensure an issue is opened for your feature or bug +3. create a branch with an explicit name (like ``my-new-feature`` or ``issue-XX``) +4. do your work in it +5. Commit your changes. Ensure the commit message includes the issue. Also, if contributing from a corporation, be sure to add a comment with the Copyright information +6. rebase it on the master branch from the official repository (cleanup your history by performing an interactive rebase) +7. add your change to the changelog +8. submit your pull-request +9. 2 Maintainers should review the code for bugfix and features. 1 maintainer for minor changes (such as docs) +10. After review, a maintainer a will merge the PR. Maintainers should not merge their own PRs + +There are some rules to follow: + +- your contribution should be documented (if needed) +- your contribution should be tested and the test suite should pass successfully +- your code should be properly formatted (use ``black .`` to format) +- your contribution should support both Python 2 and 3 (use ``tox`` to test) + +You need to install some dependencies to develop on flask-restx: + +.. code-block:: console + + $ pip install -e .[dev] + +An `Invoke `_ ``tasks.py`` is provided to simplify the common tasks: + +.. code-block:: console + + $ inv -l + Available tasks: + + all Run tests, reports and packaging + assets Fetch web assets -- Swagger. Requires NPM (see below) + clean Cleanup all build artifacts + cover Run tests suite with coverage + demo Run the demo + dist Package for distribution + doc Build the documentation + qa Run a quality report + test Run tests suite + tox Run tests against Python versions + +To ensure everything is fine before submission, use ``tox``. +It will run the test suite on all the supported Python version +and ensure the documentation is generating. + +.. code-block:: console + + $ tox + +You also need to ensure your code is compliant with the flask-restx coding standards: + +.. code-block:: console + + $ inv qa + +To ensure everything is fine before committing, you can launch the all in one command: + +.. code-block:: console + + $ inv qa tox + +It will ensure the code meet the coding conventions, runs on every version on python +and the documentation is properly generating. + +.. _official flask-restx repository: https://github.com/python-restx/flask-restx +.. _official bugtracker: https://github.com/python-restx/flask-restx/issues + +Running a local Swagger Server +------------------------------ + +For local development, you may wish to run a local server. running the following will install a swagger server + +.. code-block:: console + + $ inv assets + +NOTE: You'll need `NPM `_ installed to do this. +If you're new to NPM, also check out `nvm `_ + +Release process +--------------- + +The new releases are pushed on `Pypi.org `_ automatically +from `GitHub Actions `_ when we add a new tag (unless the +tests are failing). + +In order to prepare a new release, you can use `bumpr `_ +which automates a few things. +You first need to install it, then run the ``bumpr`` command. You can then refer +to the `documentation `_ +for further details. +For instance, you would run ``bumpr -m`` (replace ``-m`` with ``-p`` or ``-M`` +depending the expected version). + +Updating Copyright +------------------ + +If you're a part of a corporation with an NDA, you may be required to update the +LICENSE file. This should be discussed and agreed upon by the project maintainers. + +1. Check with your legal department first. +2. Add an appropriate line to the LICENSE file. +3. When making a commit, add the specific copyright notice. + +Double check with your legal department about their regulations. Not all changes +constitute new or unique work. diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/LICENSE b/packages/flask-restx/opengnsys-flask-restx-1.3.0/LICENSE new file mode 100644 index 0000000..c6741da --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/LICENSE @@ -0,0 +1,32 @@ +BSD 3-Clause License + +Original work Copyright (c) 2013 Twilio, Inc +Modified work Copyright (c) 2014 Axel Haustant +Modified work Copyright (c) 2020 python-restx Authors + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/MANIFEST.in b/packages/flask-restx/opengnsys-flask-restx-1.3.0/MANIFEST.in new file mode 100644 index 0000000..a3aa285 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/MANIFEST.in @@ -0,0 +1,5 @@ +include README.rst MANIFEST.in LICENSE +recursive-include flask_restx * +recursive-include requirements *.pip + +global-exclude *.pyc diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/README.rst b/packages/flask-restx/opengnsys-flask-restx-1.3.0/README.rst new file mode 100644 index 0000000..8c7d3d4 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/README.rst @@ -0,0 +1,216 @@ +=========== +Flask RESTX +=========== + +.. image:: https://github.com/python-restx/flask-restx/workflows/Tests/badge.svg?tag=1.3.0&event=push + :target: https://github.com/python-restx/flask-restx/actions?query=workflow%3ATests + :alt: Tests status +.. image:: https://codecov.io/gh/python-restx/flask-restx/branch/master/graph/badge.svg + :target: https://codecov.io/gh/python-restx/flask-restx + :alt: Code coverage +.. image:: https://readthedocs.org/projects/flask-restx/badge/?version=1.3.0 + :target: https://flask-restx.readthedocs.io/en/1.3.0/ + :alt: Documentation status +.. image:: https://img.shields.io/pypi/l/flask-restx.svg + :target: https://pypi.org/project/flask-restx + :alt: License +.. image:: https://img.shields.io/pypi/pyversions/flask-restx.svg + :target: https://pypi.org/project/flask-restx + :alt: Supported Python versions +.. image:: https://badges.gitter.im/Join%20Chat.svg + :target: https://gitter.im/python-restx?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge + :alt: Join the chat at https://gitter.im/python-restx +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/psf/black + :alt: Code style: black + + +Flask-RESTX is a community driven fork of `Flask-RESTPlus `_. + + +Flask-RESTX is an extension for `Flask`_ that adds support for quickly building REST APIs. +Flask-RESTX encourages best practices with minimal setup. +If you are familiar with Flask, Flask-RESTX should be easy to pick up. +It provides a coherent collection of decorators and tools to describe your API +and expose its documentation properly using `Swagger`_. + + +Compatibility +============= + +Flask-RESTX requires Python 3.8+. + +On Flask Compatibility +====================== + +Flask and Werkzeug moved to versions 2.0 in March 2020. This caused a breaking change in Flask-RESTX. + +.. list-table:: RESTX and Flask / Werkzeug Compatibility + :widths: 25 25 25 + :header-rows: 1 + + + * - Flask-RESTX version + - Flask version + - Note + * - <= 0.3.0 + - < 2.0.0 + - unpinned in Flask-RESTX. Pin your projects! + * - == 0.4.0 + - < 2.0.0 + - pinned in Flask-RESTX. + * - >= 0.5.0 + - < 3.0.0 + - unpinned, import statements wrapped for compatibility + * - == 1.2.0 + - < 3.0.0 + - pinned in Flask-RESTX. + * - >= 1.3.0 + - >= 2.0.0 (Flask >= 3.0.0 support) + - unpinned, import statements wrapped for compatibility + * - trunk branch in Github + - >= 2.0.0 (Flask >= 3.0.0 support) + - unpinned, will address issues faster than releases. + +Installation +============ + +You can install Flask-RESTX with pip: + +.. code-block:: console + + $ pip install flask-restx + +or with easy_install: + +.. code-block:: console + + $ easy_install flask-restx + + +Quick start +=========== + +With Flask-RESTX, you only import the api instance to route and document your endpoints. + +.. code-block:: python + + from flask import Flask + from flask_restx import Api, Resource, fields + + app = Flask(__name__) + api = Api(app, version='1.0', title='TodoMVC API', + description='A simple TodoMVC API', + ) + + ns = api.namespace('todos', description='TODO operations') + + todo = api.model('Todo', { + 'id': fields.Integer(readonly=True, description='The task unique identifier'), + 'task': fields.String(required=True, description='The task details') + }) + + + class TodoDAO(object): + def __init__(self): + self.counter = 0 + self.todos = [] + + def get(self, id): + for todo in self.todos: + if todo['id'] == id: + return todo + api.abort(404, "Todo {} doesn't exist".format(id)) + + def create(self, data): + todo = data + todo['id'] = self.counter = self.counter + 1 + self.todos.append(todo) + return todo + + def update(self, id, data): + todo = self.get(id) + todo.update(data) + return todo + + def delete(self, id): + todo = self.get(id) + self.todos.remove(todo) + + + DAO = TodoDAO() + DAO.create({'task': 'Build an API'}) + DAO.create({'task': '?????'}) + DAO.create({'task': 'profit!'}) + + + @ns.route('/') + class TodoList(Resource): + '''Shows a list of all todos, and lets you POST to add new tasks''' + @ns.doc('list_todos') + @ns.marshal_list_with(todo) + def get(self): + '''List all tasks''' + return DAO.todos + + @ns.doc('create_todo') + @ns.expect(todo) + @ns.marshal_with(todo, code=201) + def post(self): + '''Create a new task''' + return DAO.create(api.payload), 201 + + + @ns.route('/') + @ns.response(404, 'Todo not found') + @ns.param('id', 'The task identifier') + class Todo(Resource): + '''Show a single todo item and lets you delete them''' + @ns.doc('get_todo') + @ns.marshal_with(todo) + def get(self, id): + '''Fetch a given resource''' + return DAO.get(id) + + @ns.doc('delete_todo') + @ns.response(204, 'Todo deleted') + def delete(self, id): + '''Delete a task given its identifier''' + DAO.delete(id) + return '', 204 + + @ns.expect(todo) + @ns.marshal_with(todo) + def put(self, id): + '''Update a task given its identifier''' + return DAO.update(id, api.payload) + + + if __name__ == '__main__': + app.run(debug=True) + + +Contributors +============ + +Flask-RESTX is brought to you by @python-restx. Since early 2019 @SteadBytes, +@a-luna, @j5awry, @ziirish volunteered to help @python-restx keep the project up +and running, they did so for a long time! Since the beginning of 2023, the project +is maintained by @peter-doggart with help from @ziirish. +Of course everyone is welcome to contribute and we will be happy to review your +PR's or answer to your issues. + + +Documentation +============= + +The documentation is hosted `on Read the Docs `_ + + +.. _Flask: https://flask.palletsprojects.com/ +.. _Swagger: https://swagger.io/ + + +Contribution +============ +Want to contribute! That's awesome! Check out `CONTRIBUTING.rst! `_ diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/bumpr.rc b/packages/flask-restx/opengnsys-flask-restx-1.3.0/bumpr.rc new file mode 100644 index 0000000..c63153c --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/bumpr.rc @@ -0,0 +1,25 @@ +[bumpr] +file = flask_restx/__about__.py +vcs = git +commit = true +tag = true +push = true +tests = tox -e py38 +clean = + inv clean +files = + README.rst + +[bump] +unsuffix = true + +[prepare] +part = patch +suffix = dev + +[readthedoc] +id = flask-restx + +[replace] +dev = ?branch=master +stable = ?tag={version} diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/coverage.rc b/packages/flask-restx/opengnsys-flask-restx-1.3.0/coverage.rc new file mode 100644 index 0000000..062e428 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/coverage.rc @@ -0,0 +1,25 @@ +[run] +source = flask_restx +branch = True +omit = + /tests/* + +[report] +# Regexes for lines to exclude from consideration +exclude_lines = + # Have to re-enable the standard pragma + pragma: no cover + + # Don't complain about missing debug-only code: + def __repr__ + if self\.debug + + # Don't complain if tests don't hit defensive assertion code: + raise AssertionError + raise NotImplementedError + + # Don't complain if non-runnable code isn't run: + if 0: + if __name__ == .__main__.: + +ignore_errors = True diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/debian/changelog b/packages/flask-restx/opengnsys-flask-restx-1.3.0/debian/changelog new file mode 100644 index 0000000..13dac05 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/debian/changelog @@ -0,0 +1,7 @@ +opengnsys-flask-restx (1.3.0) UNRELEASED; urgency=medium + + Initial version + * + * + + -- Vadim Troshchinskiy Tue, 23 Dec 2024 10:47:04 +0000 diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/debian/control b/packages/flask-restx/opengnsys-flask-restx-1.3.0/debian/control new file mode 100644 index 0000000..dc706a2 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/debian/control @@ -0,0 +1,34 @@ +Source: opengnsys-flask-restx +Maintainer: OpenGnsys +Section: python +Priority: optional +Build-Depends: debhelper-compat (= 12), + dh-python, + libarchive-dev, + python3-all, + python3-mock, + python3-pytest, + python3-setuptools, + python3-aniso8601, + faker, + python3-importlib-resources, + python3-pytest-flask, + python3-pytest-mock, + python3-pytest-benchmark +Standards-Version: 4.5.0 +Rules-Requires-Root: no +Homepage: https://github.com/vojtechtrefny/pyblkid +Vcs-Browser: https://github.com/vojtechtrefny/pyblkid +Vcs-Git: https://github.com/vojtechtrefny/pyblkid + +Package: opengnsys-flask-restx +Architecture: all +Depends: ${lib:Depends}, ${misc:Depends}, ${python3:Depends} +Description: Flask-RESTX is a community driven fork of Flask-RESTPlus. + Flask-RESTX is an extension for Flask that adds support for quickly building + REST APIs. Flask-RESTX encourages best practices with minimal setup. + . + If you are familiar with Flask, Flask-RESTX should be easy to pick up. + It provides a coherent collection of decorators and tools to describe your + API and expose its documentation properly using Swagger. + . diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/debian/copyright b/packages/flask-restx/opengnsys-flask-restx-1.3.0/debian/copyright new file mode 100644 index 0000000..a152e2b --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/debian/copyright @@ -0,0 +1,208 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: python-libarchive-c +Source: https://github.com/Changaco/python-libarchive-c + +Files: * +Copyright: 2014-2018 Changaco +License: CC-0 + +Files: tests/surrogateescape.py +Copyright: 2015 Changaco + 2011-2013 Victor Stinner +License: BSD-2-clause or PSF-2 + +Files: debian/* +Copyright: 2015 Jerémy Bobbio + 2019 Mattia Rizzolo +License: permissive + Copying and distribution of this package, with or without + modification, are permitted in any medium without royalty + provided the copyright notice and this notice are + preserved. + +License: BSD-2-clause + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + . + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + SUCH DAMAGE. + +License: PSF-2 + 1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), + and the Individual or Organization ("Licensee") accessing and otherwise using + this software ("Python") in source or binary form and its associated + documentation. + . + 2. Subject to the terms and conditions of this License Agreement, PSF hereby + grants Licensee a nonexclusive, royalty-free, world-wide license to + reproduce, analyze, test, perform and/or display publicly, prepare derivative + works, distribute, and otherwise use Python alone or in any derivative + version, provided, however, that PSF's License Agreement and PSF's notice of + copyright, i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006 Python + Software Foundation; All Rights Reserved" are retained in Python alone or in + any derivative version prepared by Licensee. + . + 3. In the event Licensee prepares a derivative work that is based on or + incorporates Python or any part thereof, and wants to make the derivative + work available to others as provided herein, then Licensee hereby agrees to + include in any such work a brief summary of the changes made to Python. + . + 4. PSF is making Python available to Licensee on an "AS IS" basis. PSF MAKES + NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT + NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY OF + MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF + PYTHON WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. + . + 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON FOR ANY + INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF + MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, OR ANY DERIVATIVE + THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + . + 6. This License Agreement will automatically terminate upon a material breach + of its terms and conditions. + . + 7. Nothing in this License Agreement shall be deemed to create any + relationship of agency, partnership, or joint venture between PSF and + Licensee. This License Agreement does not grant permission to use PSF + trademarks or trade name in a trademark sense to endorse or promote products + or services of Licensee, or any third party. + . + 8. By copying, installing or otherwise using Python, Licensee agrees to be + bound by the terms and conditions of this License Agreement. + +License: CC-0 + Statement of Purpose + . + The laws of most jurisdictions throughout the world automatically + confer exclusive Copyright and Related Rights (defined below) upon + the creator and subsequent owner(s) (each and all, an "owner") of an + original work of authorship and/or a database (each, a "Work"). + . + Certain owners wish to permanently relinquish those rights to a Work + for the purpose of contributing to a commons of creative, cultural + and scientific works ("Commons") that the public can reliably and + without fear of later claims of infringement build upon, modify, + incorporate in other works, reuse and redistribute as freely as + possible in any form whatsoever and for any purposes, including + without limitation commercial purposes. These owners may contribute + to the Commons to promote the ideal of a free culture and the further + production of creative, cultural and scientific works, or to gain + reputation or greater distribution for their Work in part through the + use and efforts of others. + . + For these and/or other purposes and motivations, and without any + expectation of additional consideration or compensation, the person + associating CC0 with a Work (the "Affirmer"), to the extent that he + or she is an owner of Copyright and Related Rights in the Work, + voluntarily elects to apply CC0 to the Work and publicly distribute + the Work under its terms, with knowledge of his or her Copyright and + Related Rights in the Work and the meaning and intended legal effect + of CC0 on those rights. + . + 1. Copyright and Related Rights. A Work made available under CC0 may + be protected by copyright and related or neighboring rights + ("Copyright and Related Rights"). Copyright and Related Rights + include, but are not limited to, the following: + . + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or + performer(s); + iii. publicity and privacy rights pertaining to a person's image + or likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a + Work, subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and + reuse of data in a Work; + vi. database rights (such as those arising under Directive + 96/9/EC of the European Parliament and of the Council of 11 + March 1996 on the legal protection of databases, and under + any national implementation thereof, including any amended or + successor version of such directive); and + vii. other similar, equivalent or corresponding rights throughout + the world based on applicable law or treaty, and any national + implementations thereof. + . + 2. Waiver. To the greatest extent permitted by, but not in + contravention of, applicable law, Affirmer hereby overtly, fully, + permanently, irrevocably and unconditionally waives, abandons, and + surrenders all of Affirmer's Copyright and Related Rights and + associated claims and causes of action, whether now known or + unknown (including existing as well as future claims and causes of + action), in the Work (i) in all territories worldwide, (ii) for + the maximum duration provided by applicable law or treaty + (including future time extensions), (iii) in any current or future + medium and for any number of copies, and (iv) for any purpose + whatsoever, including without limitation commercial, advertising + or promotional purposes (the "Waiver"). Affirmer makes the Waiver + for the benefit of each member of the public at large and to the + detriment of Affirmer's heirs and successors, fully intending that + such Waiver shall not be subject to revocation, rescission, + cancellation, termination, or any other legal or equitable action + to disrupt the quiet enjoyment of the Work by the public as + contemplated by Affirmer's express Statement of Purpose. + . + 3. Public License Fallback. Should any part of the Waiver for any + reason be judged legally invalid or ineffective under applicable law, + then the Waiver shall be preserved to the maximum extent permitted + taking into account Affirmer's express Statement of Purpose. In + addition, to the extent the Waiver is so judged Affirmer hereby + grants to each affected person a royalty-free, non transferable, non + sublicensable, non exclusive, irrevocable and unconditional license + to exercise Affirmer's Copyright and Related Rights in the Work (i) + in all territories worldwide, (ii) for the maximum duration provided + by applicable law or treaty (including future time extensions), (iii) + in any current or future medium and for any number of copies, and + (iv) for any purpose whatsoever, including without limitation + commercial, advertising or promotional purposes (the "License"). The + License shall be deemed effective as of the date CC0 was applied by + Affirmer to the Work. Should any part of the License for any reason + be judged legally invalid or ineffective under applicable law, such + partial invalidity or ineffectiveness shall not invalidate the + remainder of the License, and in such case Affirmer hereby affirms + that he or she will not (i) exercise any of his or her remaining + Copyright and Related Rights in the Work or (ii) assert any + associated claims and causes of action with respect to the Work, in + either case contrary to Affirmer's express Statement of Purpose. + . + 4. Limitations and Disclaimers. + . + a. No trademark or patent rights held by Affirmer are waived, + abandoned, surrendered, licensed or otherwise affected by + this document. + b. Affirmer offers the Work as-is and makes no representations + or warranties of any kind concerning the Work, express, + implied, statutory or otherwise, including without limitation + warranties of title, merchantability, fitness for a + particular purpose, non infringement, or the absence of + latent or other defects, accuracy, or the present or absence + of errors, whether or not discoverable, all to the greatest + extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of + other persons that may apply to the Work or any use thereof, + including without limitation any person's Copyright and + Related Rights in the Work. Further, Affirmer disclaims + responsibility for obtaining any necessary consents, + permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons + is not a party to this document and has no duty or obligation + with respect to this CC0 or use of the Work. + diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/debian/rules b/packages/flask-restx/opengnsys-flask-restx-1.3.0/debian/rules new file mode 100755 index 0000000..8d431dc --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/debian/rules @@ -0,0 +1,25 @@ +#!/usr/bin/make -f + +export LC_ALL=C.UTF-8 +export PYBUILD_NAME = flask-restx +#export PYBUILD_BEFORE_TEST = cp -av README.rst {build_dir} +export PYBUILD_TEST_ARGS = -vv -s +#export PYBUILD_AFTER_TEST = rm -v {build_dir}/README.rst +# ./usr/lib/python3/dist-packages/libarchive/ +export PYBUILD_INSTALL_ARGS=--install-lib=/usr/share/opengnsys-modules/python3/dist-packages/ +%: + dh $@ --with python3 --buildsystem=pybuild + +override_dh_gencontrol: + dh_gencontrol -- \ + -Vlib:Depends=$(shell dpkg-query -W -f '$${Depends}' libarchive-dev \ + | sed -E 's/.*(libarchive[[:alnum:].-]+).*/\1/') + +override_dh_installdocs: + # Nothing, we don't want docs + +override_dh_installchangelogs: + # Nothing, we don't want the changelog + # +override_dh_auto_test: + # One test is broken, just disable for now diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/debian/source/format b/packages/flask-restx/opengnsys-flask-restx-1.3.0/debian/source/format new file mode 100644 index 0000000..163aaf8 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/debian/source/format @@ -0,0 +1 @@ +3.0 (quilt) diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/debian/tests/control b/packages/flask-restx/opengnsys-flask-restx-1.3.0/debian/tests/control new file mode 100644 index 0000000..4b7045a --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/debian/tests/control @@ -0,0 +1,2 @@ +Tests: upstream-tests +Depends: @, python3-mock, python3-pytest diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/debian/tests/upstream-tests b/packages/flask-restx/opengnsys-flask-restx-1.3.0/debian/tests/upstream-tests new file mode 100755 index 0000000..7c45645 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/debian/tests/upstream-tests @@ -0,0 +1,14 @@ +#!/bin/sh + +set -e + +if ! [ -d "$AUTOPKGTEST_TMP" ]; then + echo "AUTOPKGTEST_TMP not set." >&2 + exit 1 +fi + +cp -rv tests "$AUTOPKGTEST_TMP" +cd "$AUTOPKGTEST_TMP" +mkdir -v libarchive +touch README.rst +py.test-3 tests -vv -l -r a diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/Makefile b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/Makefile new file mode 100644 index 0000000..0075e73 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/Makefile @@ -0,0 +1,177 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from https://sphinx-doc.org/) +endif + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Flask-RESTX.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Flask-RESTX.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/Flask-RESTX" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Flask-RESTX" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/_static/apple-180.png b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/_static/apple-180.png new file mode 100644 index 0000000000000000000000000000000000000000..1078c983de910dbec63095bd81e706f4c09f2ea8 GIT binary patch literal 12581 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 Vy?@`8F9F~tB_=OgC2SDz{{eVkLwx`M literal 0 HcmV?d00001 diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/_static/favicon-128.png b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/_static/favicon-128.png new file mode 100644 index 0000000000000000000000000000000000000000..8e7cc7eb9d7ce6ea4502a3e25f10bb7458d22bcc GIT binary patch literal 7582 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 literal 0 HcmV?d00001 diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/_static/favicon-196.png b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/_static/favicon-196.png new file mode 100644 index 0000000000000000000000000000000000000000..5727d4b653b05cc515c4eda3be511c71a6638ae2 GIT binary patch literal 10099 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

0l`^%kpe2PLiEC+fr&lziNfer{^cqm zm)^hn9(iUp?F)R%SXN@cd}y@rU})F=UqoB~D}@)aATgT6e-bGeU;s>j!2ge#`S?P# YyhrD>)tBz`p@joTipq&p2^j?b586}y3e7SX-dj?-%jlN5X13v!! z=5!RrgYO{Nf75gX0GKKNy&&8Qgk8Wl;hd!9B;fWSkkA3db0Tq!;9K}k5*kiob~ZL9 zwoU-CUnYi5CO^qsEu75Bq@?A3sQaVh0RUtGY4NWrZYyW&u5Lv0xm=eI*7Ir>^KI5G z4;uhHKAzk~ByRtpAPz_bZiKzKbVz;rVL4iEm>Itx#PC!psDsU08|l`gCmq(Hw!;^( zxC+Y(7xfvLm-j57#t?s99ebVQ2CrjSj8ed#KY!xF1KRw+SBQUap8(*u|ACMHfARn8 z#{Lg)%l|9qiWwUAkCc#AA^miLBRi?o&8S*G;`y(5TB1VrAztQ4F9w*F8VLj%*aUmD z&MC!QMzt#ubOh`Zv->*Misl=KdJ!z-1$%mA6nV{jM^?#IZKYSCMHt#0M#7YVX-XRIt|hygby=D(Ypa6;^J3MU$$bJxP1bcHV%T; z2SpP}Cilww>2;X{0Z;%8jDeHM#xGBa2NlMqo?UjSfgi~=G30*##3AqQuCU@-;l7rw zY5nl;&+W|D??HM!hIapv`)#7iUEB7SD{A>XyOe9U;x(M@tIbcF|CW^4X-~WRWd$h= zidG2`4M#&_9YJD_!(Hn)XA{7LY4OY5DC;_@2@LsV|?IHpdC^Z@ZkdqzCvku_Ao8 z+vus_7V*E^XEDRwG#m%wwiiL7K7r5EuEpvquFgn9=rk*jft5SJ#_-<}IGj%uGF`LW z?>cDfcPd4%j>lg~-29YPNzY?85Bw5AV{LN%(3sDISv_F$a$w20r0hlYZ-=R>&Nob3 z@=U$g7F@GUxO|doXfJ|Kvu#YSSDE;CxZrvyQ@=Z?uSEPs{*SsK7}cuLxA*&$#rS;L zp4PP+he~G8CB%6wpSJdrZR4}Mrh#_m{2#gHds=(NPSSXgOGtiyXoueh)Nqq~KPYrb zruD=g7Ldo*jh9f|@Ov+GAxB{9Q;bwPRP73GTn{Vh zwxYG3gW3odgp}-Q^MFR!=1*q)MEcvRA~`%^`E1e~51$1Wvg0VhRRB8GAIfnEO$(IV z1rpH$&LBRiPpXYS#78X?{k~Ef%+MOlAj*DIHd1OujAvI#Nh;YXHsi=q<+K|f-zY3j z)-RR=M&&U`z7e&xUO+ubA_wAG1Vdg(XMZmRAwmJBX?ZsY5?%-(m$XT{{RaU8J=^Z2 z6_^@o#XTpX1m%p=Uy!FPR3VBoG@gFVjULh}SC<_j?v>mkIOvU6c*v1lYv=2|r)DHJ z52&PZY+`^p#!vyl58s*Swd-F4{@!kP4jR^!#R+lq%XGb1Fzo~bQM`X zY~QJxe`F`K9!^8(w`J(RdCZOas_Fk(yk_|cAj^Anze!Q=bnN||&(ssNOr0-jFb<7U znUWO{iYr9@O)|V}Gse?v+PR=@nFnL?pi-v!=AeyBzLN+0rS|7z*yXf4lmS065)921gs|>H-ShTVQY0S;nT7BJ-uHy_-gQoY zEpBNUODlPysk{=P52!aVea>D74gTJLXR8~3AqZGnJrZ#!{qEzC3vu`bKEm_kmzsp` zH$59#`JrA?K)q-v|G4s1PzPh_{-)#N_P>=fpw3{wVM|S3IF9n`HAI#4cD2!K!wat% z0Cnveh6Ck%)X<`g2?bCTSv;kPV7Kt)^;s37JeD8UoTVL6Spnz+J;RMTuP3K!Pjh;c z&5r_7S^>?6>n;NA^MmND^5^#rk_|dwgV7Mu8&pZIl@>=uT_UH*z5Dl|JZ&$IZ}&IEW-wxL8xfbGh!^d@{#kCS{opn%+S%44}BM^bSz z6WV8>F#s$-du6-Fj~+6Dbsy4HjA%O_$SYnPPCVmV<2ud(bX8{M@pOPUb0 z+s7R=LDm%}vP2IEmiYj5)e2SPp?K^=c(7ojuhnf(M{~f%dyw)#VY+r~2I!Z|Co^0- zBSRn<6*WotoY)NF+OZ4WapitF3VZSVOFw69nJufB?jT@5di{%uoD{6<&h~JhFEs7q z6Bp23C5v9T8h73ca9bMZy3tu>^c>ZOHycO?QS})FKD`^zvo@kSS1RGD0?bi7<9A-c zR*yhDkn;1#sSSUcl>OOzswO>l0e!5U64~Tm@uaU1`I2O#IoKY~K#Ar79FO6Hcy1mq z%)2Obf(*mCB{5n%w7dnOFRRbt=QdNcyv=aZGaSZCR|KoYlV`MRV? z*j?G$U2XfKAY>D$MzuErDJa$r&##^=mCYv{y$f_qi9F0%kMuCf}kalQE**j`t%|J`P$9D3OHvtd-p) z?U3BE%?d~7W}BWP)kG^*$PedAMIClZiFd=_NG}9Cc=l^wLp`~I^O|tJ|^UBHz9Nl)i}o(sh}d`K>HfKs7SSfrrFJl4tD1x7sy4RP5Ui zZ*0D|Q0h3v$lya7HdoKo7itskeDj-RSlBupy*>SMH$sTD&7M?P*58+0Xk;9$AXGCO zNv-acg~ZV+)_k}N`?MGQB>hCY<+n=h%ClP2XEGqKPB`*b8D;7yDk_S)^aEt9u6%pA z0N(_)v}4nI&L-Q)uEgb;zgfi2QS$Ghg&P=gXf}T~X$_9~{6Se}{drz;qPTy`sLcmT z5wu<%+P-&saR}ue+NYEH@~$z1QY`Qn$-9XWx3N6S*0!yI%*i-y8ES6mkYwaAes$Q7 z9WA}&MAs{O}cbZbNkjb2tV56*CR2wH3yG~ zj`kmsa4a0rp`w>%-%*Sd{k7gL>xj=gmxMp`QMGGWztiCTjLeqwhs$tjnYHL+F2do@ zR8x6e^AkDD>^N(mjoyx$pO4U_o5m41Ecm&L0j`7w)U{(T#%}>!&oU>6a&F zVZgnXEu`O9H4SnXzcgxA4f|}9i8t(vcT)xJpF!XR&ax*He_FMoYUVIP0|4+dJEOU} z8IwTpd|lwB|I0MO?8l%~8+VWf1c*TAj1VN8q7IndR*_b)?X1Ps@XB|}kYD;*G41#Y z)s8SV)S&!cIYQVD6|=dX&^$Ia)n9YG{j;`TpF47TTs#Q!a$%9$!m|D)C9L-0PDi>06FJSfIuVDy~At+{g!KTI?#H1a~IOXC#~5_ zNoMTyX;qQIGQ+sYYK!DIvFgS&K5Qf1Q6EA!WvO=q8I>+_1IPpHFE}KeDEHle!V6oh zrpEy9V5C|E_L>IK0l)A8f8qv!I70Z*7xuy$Hoarkcg^lDkC`|nMh;oj2Y0Ww`lBgN zW1~v5l6P<1ZF!sOhXolO+})x!e4Vj;#Q);tnjNbt6_2`R8XL)5;Jn8t=dZ54!G3UB z58@rxt)k&bx9-xjfx!@daT{3tMx+wwr{C|ufd{@TUX;GBIR@3iQmH#PXLC)}JUa!< z07;oxC)hiQaE>ZTHQhZNd?JA=zi>HBtB~!P4>5YBFvbvKY{t$VZVDxqQ2!%UmlUck zTVPaMnD1~hJ{YRlm8}rspg&p}#Ha5&?INNEoevstEGywCsr0sn5|KVX-_^rE<@rdK zo8CFW-*wem%dV{1nrPD6LX^}G5q8PJ@zqoQw0K_n@VKv6hV*hSf7Z}iy?7z8zW83+ zegs>pX-TpV*a{N@=$Oz-){OWS&}pXRa7j7|$BG|59NMln!;X8CL=QKTf- zvy_?8a%kAv%J(lsq=HC&8{vF03XF+X#+jU zU3-n8nCz*>-|=op8%ZS;g8?Z(LcLJsMForI)hOSs6|+-A>4H4NCO0isl|f z-o$yb-MrR53)=j!Hg{%><02@z$=3t3OGqmkjljnIDzjsJkhzTV1Ytwk#wmEY0yjaPlYn=3%dsc-!?+eRULDrAilQEyNpT)MFqQ1y;oDm0Y;#uJw2mkBQa0QH7=s2^s&U7}R)q6RxmUBuOm zi`hA{O4y+v(>GY7!0dGDA~{)T7*!jjvJr>h;wG3QBVbYZX0||Qm-q_b$1K%Cfm*Hh z?bwrz(x^Evva9H~5*`{ZdcRMEHJ95-$l%&?%E^ojmwf`z(s}0Q!k>oqi>)8E1X?>feYLT$q~EX%w|Na9O2iG9wa_SUG6DI717CM zNw90SGSXY*zu+4y_rP&YD;gm!nv{y7zt{V6VUI3sy-Mk8B}lY5`Sw!2FYWms_cw|y z3WHy^C*>TSq&q$k<>+e)ztISnI?&v@T-5W~YdbjgwK8)Tq3{C67K@Bx>2O9Pa2YxN z=4zCYI+9;amz%gXm7)saXCLQgS0``N0TI?r(-?*A(t>bW&&WaXnH#uI^}CHqyC}92<_M2@miAw8vgL7~XduVHBJ)GjR_WU8BAe92C<~-OSpzf)GilGgxHG z1wBT}^tkCW)k7&^`-NAEf3V9Ucc#Zot{KL)cN^uik#`Y1@w2Lv$(3^x%bZ&@GUEJ= z5+Nk$DG^mXWPcD!$W2j|K{2bq@V}qkHX6h?A)w@Lm)K|(j`#PahQ~j*jR8cY&Fql- z-u1YZfyD3zGOdzh<1<5Va?8GGf8mi5AzSK`+HWnv`m$ z62d6XABi82zn3Tv0J9{YGU@~_PHhohE^Es$S=ye*ba?V~;l~j<^@emaZ#Ru$0W$PJ z?LbBKk3)Q%l=L#(wQ%AEc@DBzD8SU!r<^%-UGtauJ&U1f%bKx_Bq!i&>RMu6`3kk` zvlejw(39%H3(URrhwg!hNP@~exAf7C29>wDh>C)kBnE_ZXv?qM7h$MCLxH zyQM0GN6&kA>D5i&?iir1$Z-F5_{G!5s{p5~o^aBBfe!Lkhxl^VQwi*t0=to3DTL#= zB=%8#`xcN^ssJlJY(dY-Wc>wXbHx=sJb5WPVYz0I*6rvI_GtGPU2M-2A2(n(`fMz% z*qLqjgJR_g!bmY^)?%~Q2*#YDC$M$Oh1VaUI7v(R*JBsj zp~C@^Ab{j4BOh#ZM{9tKxJ`Gb!HCJnH&-w>*yWtqwNqx`v~Gg{kQlI-x@~m#yBTXR zS0_<^%_=q6lc~R}pi5=WR*$rUb7@ztZil3G>%kc6Oezk+Cmi*}J_oeWK7&O>i(N-m- zaHVh^6GT%hM$sw*i~#^EGa_lIO$Dmv!EMgi_SZxpoE0lTiWORl7T$~&x=stCPAgcY zq4p*kvBVOmw%(Fa;rqaj3j3@ba((HMOE&k=9_uIHgkza;F2L}UF1Aeh-2UYjJ0L;< zM?QNE99L9w(YVy}DuV9B5CJhUqAmigIGC82N&r##B`CwCKp_=R0Nu|+tG1+)0O^7l zhFKclI)JI!E^an@`b zB48YcX_>^+fC>)rW53ok-dpw>3h;#~bj2L2Uz zC$%fr_4RC9c0ZB^c+6Tm&j8y7c-^W*Q5WbSJx)l4xrMEYB-5wITg{a0y#|N@ixFpl z?H^_UG%JhiB^#(O=CNCs1?_Rgfx-Y7>IW4`j;19hH1}ITP>)~2W>8j+9@m1MRX^RI zH;n_k47efA1DEEl$fRyCb=fQ04~SV3s&p8IYij5wykP8oQJHqd?aY*L0Uh3%=`pmy z{I(7Ds#d=22!a{U0Er+hliPRA&_JHGzgz8Gl zAMmB=yHC|$4hymFJtr#pdXx^uP5;5H`86T@Udn~O>&g3F9(?23i?6@SjLg)-m$w8GuWNn6nRD^+O7T?M0yH7qw|uwNtdIsmUC* zty&7KB$Lw`zQxZWGa=Tw1GKnW5rQC&^p$<9J0`cL7UDbxgWFj-^deFiCr^RtrxjpX z*^m9ZAz;zC%m_LwgEp}$06xfH*V%`cUxChNrBPqVND#RQ8TGWW^R%R|EsJk$wpPw( z^9$alY_LUFR z&M$Qr2q-MFu;1NNDQ6s-c&`)a(< z{uc=%ox%H>hA=O^PKUdVfN%yC#j{5^MyCMPcUTB786Oezpjr1;so{1QtY18WD<&Yv zslOPHTkP@6P=0?P$ZEVE0=Zi1dKUA^&a2SR1 z%fy06?+_ux7p>pElh+LW?Cm;Wl4%XUq*7==J^uSp(!nEO!m`51gi!P5S~SsZSCvqqIKr0DijqmKt4DP2{B3pbTU)QTWs`+&&j!t= zX|KX@cpqnK%$lTuxM$~t`=@(1TPg6O0jsh&S&#n@)u!yDbq-oh?ouFq zWq-R<-#fls!3j1}_4iQ$nKF;n{&7_s>v+tlSGncx1i*gM-|D;PdTqT{Q402xc#3rv z<`>##y&qrm&*6))&o+#%LP09k12RmEDDt|r5qe(-aX!EOvLJ^mrZ570LkDHw4lhP| zwBe+@1^Z<|-FTVB!k|@+;f8(@&5+3V`6vly@9)40pK>*zn6@&Y$ar5jo=J4^s)rc_Gr0lK3MdvSrgkK&ZGle&_ zU9jtLQ+_I9j?rtRRbo<=$Y}C*fBJYS0tIn4FrC~b7b9zwkN00EFfw{E*QoxVUI2yR zUf$L-l9zg}qquW&;c`P^F3(1F5sBDuTk$`}T(ikBKv>pMv#Zkv!X zXi)RJvT}{>#p~Z@ZqIE$sdwk5wn@-~+@^n0#9;A)d%6YYgny4Q;XjJ+`A+_#YL}T)G1bAL5~8$D|Oi?#X3R z+nWyKC%p2$i}`*;9hlL2alEF?dIQhcHMUob*cJ_MlQdIhg59X;D89v&GGXFwiY2DH zpv_E1A+EjN&GH|+Z5DOYJlloNzT>wRuBB3j^oyMt5sEImELbT1;+@ENW1&6qN z0@gPc31UqRpd8aChS7fEKXxfSH0 zFxUVQt(!K2p4`;a74si~R!WNIYLUaGqZGPfZe9~}f6TC6RR>D`^1O3T9)Aks%DLUW zOb`^)YOMc7fauC7;U2gXvc1xIoTgG!^+r$<#K|B1 zt_|IAa1nFx?(uCmNBP3~5?CKKsnBp4_8lCvJZEj3h8{VW(U^nlXjcx4*H3vmHGGs6 z+VB8Y5$>Ms#AKQQcUI>{VwB|uj1INkMWd_tn;6?kfj542k)7Y9d>e!p=j}ZlUtbwO z0e|#;!H`_DAmY;cR=n>r-fEE4>})*`_hfgQFZgVy?|P)O%WF}D$RoZoU3Oc}Hn*A! zele@9E}j#V;yp9l%0E}*cUe8(?Zn+;2^%VXpNBxfCFX(iu-lZh25JR7E)Q;3v9-+n z_^dY+Sx((e73fy%tv?nGS$h8UDL--{rRi>?u~=0{J``aYaL=f=7!=KX6C*c)&E9h#UNgeDloA)f_1>IHe?Y`hAqo4S7N zz)E1g-@6BV`}|B8L6z&;0_1Dnn}~Q7LwUL%vekV~m8WSoTonoaI|+7Eip&5Q&cx`r zZ(8V6?2yuc)YsmmC)C|eX;dx4Znl!=tEvab+WDXktj<4ruB(u5%^PSB^7Lmn@Ht&1 zkSz1uQKOR|wRK8a$jI+H+J|fgzxE^kEWW&)+X~doa-NZI#f}x6D&d@b)qmiA2_>&; zo4|-`>YSk%T`|hOf6QvZADS<7e^B@*2||3FZh(}y-2Ph8@#ao*-548;g69MhDB?Tf z&)>E(4;CDrp0cB=4Klo~$rB9sV;0^f#q?hH^0#fUVoGFMt`70TVZAf8VXuns?E6=1 zi#Axg?oIjA*0xic930dTE9v(C3WIq;FmNnT@StceX)#PTsYaQYGo9dk(Q- zOQ7EXVBw`me{XKKRgwkG?ji1G-Q6475IvgQBz)Dc8(xK$a@%t38o`az_wA$C=i3WC zZG5vfLkcjQ;QSYs!b9Y^Tmq^+0;o&m{Zvye1P`Ei1YaGRSgTvsxhjQjq%I_pxe_y{ zO+;Ingmf3PmJfT>^_xcMwP`O3wwVs3@uaR>7c<@_uP%EbqF-mq)H2MG2E2tATLYSR z^R=h}{}e{m7)fF-&s{YR#t*4()?*BADqm2Mcx^)Kk;t0O``ihBc+}T-mu~Q5xr_M)vaxbz&gCp38tlf%0 z;a3Gki12Gp>3Q>GcK74Ft|vh21NtR&AJ2(XWP14HmEW$@w(t-5$B8{7FVj-43((t` z5L%^ArQEwo^7p|qmsi}`EnUc_7e2`D1#6OaQK-;~Fb9ZlbmZbdN(gv^?D62^!5ZPjQ!a(@AZ&C< zKj^=JAo0Vsd*UzHTCz&%{elbtb&(O2qF0A?Sa2i2K%ic`+Z2O(lDYlxPaD||`J^Ib zA;}(FeL`E`0^J4P-u&9lMRnd-yC0)0X zNV8R*4W%)D8Fmt|*UqJ6mGsVy4Vh(SAML3%YL1!G-Z=F)GeK}FrSYMP?t}}Gxk^iNxqFNN6 zmOg7jiR8*dkq;c7*u!U3K7%~4f1KjeK#&m~neUSEoql-EyNEz8mnL+=EJ~POT~Zf$ zf&SX{H#SUQNJ=HXZS_zJ3;=#+3j!WFHiCFyA8g8iSY=525gZd}|8jXq3NY5X`B!!X zNhwO+v30WR?A1=afrpP2LZ!`nX~i;`{9Dk^CgB)(mzSq8P%pf6#yjuYlik;EN7#sK znm+`NIytN5Eve~i?4)a3Uf!l*rcN3Lgnj~q zD7VpgO&e5A)5br1PBD_f@QV?=cy}li35v0Yq3UdIL#{QR0(^7ADaIrggq?hinD9M) z*Mbj?qJ#bXg_Hh2GF@yOH9VO%9p8?0HjjNlcX~W;cDjKC&MzwKrSFqLeo-j4uZHm!kl?RAG_+s zl!masLKqWVq#~QP0Y~hyKYZ&DTtG@o-o=LcLA%?3E@p8aPSTb-2=$}S&$!CjJR;4P zj9YgvX2~B`gd%xfTI>hvv9FG_*clO20*R%!H#&;MIO=7#auadOnIaYV0|?XsO*Z8} zA4I^KV#v5g;TFl)GWsDDZ1C^|@W4&+78A+dB7;LjCHCbH+&*e7(I!KG3-QZZU1jGn znA)0Z=AImQG zXIG7&*L~Sxw;fEP#`CEyiX#7)@VzHsc#Ztbs{i~c>1_gz?774Rq}Qz#)5=o=qPXFby9PoQv-;aGG4w`?=U%>v@*y`tUA!0lbmM zd{&^uiHIo5AigQmzCizew<7A-v?TICvx^|<$?amb=jkQzy8!F=)n!4+4qRE}&|LJ{ z;z(|j)ExZO5tmebCq*5ma2pv;SLYCgjMAhK+4M){u&7qX%H->n29jQxr5QXJ?)sDc z&69x2ib*7$WzLR#{Uyq$I>bi|0hLMU<9i@1AopxFBu8CLm`y~i%RjIWfP&EWMkEI~ zs3FJfm+1$rrHq>{;(N$4t_w5qw~}XdlF}&iRXJXgCEPuRep-0#(|{ zl_@kci=#z5`=f^%5zt^b@y9*dX-UdNu$9?K|99?!aDLnq!x>aN^y)EX zW*iFlX3c_GrrXKINxg>C);4;V-H7SeU1hksZNV~${W|*fpBxz}8>kui?k@j^5||yx=G8wkgnj)Ui1DqZe_QKGt{blg)X!8=CYhgXF0?`w76+ruE~( zbwnwWjd|7nX%5@rX)dbL@n4FrK<~YzOzOw|Y1QNO7Xzh-ZYw7eA5QgOkw8@}4h>H7 zcbL;B3D_$eyKnC08l@c+IEh2mRwfTf;a<~UvlbST9EUKR0 z?}lrS?$;g4nKA*u7{_g)DKc2-w8W%cA+2rfN7-FJFxL;Ej#?>000)}4yC)Q05|%<) zwq*jkTltG#0KldBsl%ErUE*y0f#J&;*ryvwCJ}tFHPwkU1^>*X3<7ueao(e!5F7G% z8@dHxW|3_nd~1fnw1k<;(nZU5xpYUk7|#j|#vT7d+*LIxhc~{Fqj3u|)_YpE7-s*zoCLdgeE8vQ3Ux-r>npD{()_Zv_!nm@ zwXqc95DGmr`-8B7|H|hWhO$vNxI{)VrL=#hx2|gf1Jyus9}W}a=K{ZSQRL&(Ykt9U z-m)@_Yl9oO{yInvN3x{Oo}>KL7G>_a4SZftEYUi|W~Gyiy2w5LaMeR~=l43~-RqmpY<{M^ z(4dQ|&FDTo?cNM5G;oO=1K2{a0g&agr0HasMtVRI z(a|43K)U7nwOgC7-}bLbk0*T4Dn(b~8vln&euTP%QC_O3^ zi!bwU-4SY{!y_!Oj#{3kXm{czE|$F!e_PxB#{&7rog7JfcGoz~Rj>b*Am(yB+?imO zZ@7@^CUG=CNZXoZ@vtwrWO)bnyP-H1LId<$bG;YTy@#Yv`!gPK(H=)X49}YW^ia9fP>}U3yp+RYZKIH;e5I&o~y!z#%HtR_f{`HSM-i_9$ zK5CuZ2Jzb-?h*7ye)TTFM$m|Fh}2+WgL!wlnZ)K{ve467<=uGh21U1i;xah+?s8232-Q-iBYZ%eB=Ke`q)7}Gx*8v*9 zG??IDv+o*6uSV0$MAAcLRGzC~>n?FtB1^unHyiAw`4-3ilGraUPqUqUzXXZ z9bZ3i9^y?X|zse$Z*$gH!u{GGH;by3{8~ zlJyfJ?(CO9#AvU>zeNgSUGIfw9aefCGnsg3*s@sJlZ?k7irs*9Xq#LBi6vh|d*4Ug zrri?5sMquZw%e#GB}=)t6mzK$6uAEQyZu#R=5SZ0Z|M``p)c-17|wERBufqbYEY57 zJ&!@28rE8(<$&vnyNb)-Ccuu;81N6V*oN~v z6FCek|Mx&)otXG~ytR)~0kUk%5^*Wiqa~@@{A`26P2!gyh|4y@S?)GjGQY#+cr|Q> z24!vN4+=G&*ONibmit_GNR2OPHRG!RjrO-43Kw-_gOguyBU_AJR)LAvG4r!6(UTd; zx-ZXl!6lErH@uK!V9S~z*tN;@(MVGZZvCX4-+E6hWOFA+457Mk>(E``2_VMPnC6Gh zplb(&(y$Eo)Zgy;Ogwjt1YTD$;dtLip0PkXo%i{Os;V0%75*>|kVF&yY#G2@|6M;i zpwcS!C;e-i&coN1a{H%eXunB?k?tn5z1m9}0&3~? zcU7lhqVxR}V18M!4YD%R{M$1a{Pt1(qAd_3TyY4H4s^mgtFiMGP=YAm2Hd+<@a)y~ z5+G*y*h~%vi#>oFP>fqI;rxU%wdCnx(7C2+HTgEPZE&;S(4 zQ}X%+7_+0*kIdH3Sh9hnFaP?q3F4jF5n}IoHGrAMN;Wf+X-&i1ur2knZ8_?e>R5rw z(N?`%CPBI-g?xss(d)@Q-Gr@H)A#!r)rvB@dnCwnmkR&h8emvH%y~uh8C>|}edw}E zZaam-=n?`JcSkRzu=0u}+T)dhOF2(dux4FQ_$Vsq-j1Odi?ZyOy{n_#BM-yQHVsgl z>klOupPIcT9u~7YxFqj+)O5pCVPEYIFm;R>w;jTMXvok)yW(L$b(0Bw{A#nd5BXg` zwwQF}FfOaxt78aOri2e>!g5Y`v+Y?;^i1XQp1`tMx5Tsk>Q(Yd;pP@?s}G2a3r}Ih zKYKOZfCjiXCgtLpUAQFo$6qHZB>CiSmm0yZ>V*qlbtU0b+1qP0f%9=A`Xz3+dI^&j+m-G)PL&PzcaD#mE`3d05WOy}{~Jg&eInIjLmDm-3JgdV|0({V6d z_N!VDX*mO|(5&MMi~4+T6;) zfn--2OZm%Yq$Wep%(iFfAJxEC(`>kE=-$13@}QxcnjX42meKnL28``pRKfL?_ZL)- zwqC_5W356&FeN*6vpejL%l8CbYfAgT1A1ZW&ZB5;=`_sfayc9iu1C;fI+NV>#O@26 z&3TnNy}kAOfLCV*fXE-+cHLbiiFX|;gpBs*LFYYfnjMM^s|C8DC`~`l@q2We*sOO_I|RyRy^=Sv(Hw| z%yHwQqPF*xuVpat~!kC zo3py~s%!kRJI3Y2`b272P0pJZP zhyN_%GHy9g!2-f24(PkQ_N(FGjLx`tutyQ+AIIjf)$V{tp`}_+z)}JYF@Ff8*wHUv zF8d>R*CrA6%G%_IA}bnGp!|aROK&*@1k=S}v7i70=6!V8Yg*?(|3QOjI(K8$gwp;o zMjUWu#=)>839pRj#o4{!)yjrRVB3E3s~@1f%bcz7h}YOCuf=^;v1Rihj2G~Y+(B8M z*@x44rI^InmFLfTmWw8_eJ=CSSKX<|v>Zb4#L7ya5KkY(QXfRU3@HGNUn69S!eoj< zbo}^Q^rQFhk9={HekVD4Q_C4vO`Be16q;9XV1|L0MC^!6WQCA7NFoYOTlhTTv%b&OmPYfofY_fz-S_^$< z5ZFk80sy?q{z7juXR%TMTLt&JjySc?Jv{}^c~0tfizh>ps!7GHR;NC0r{7ZZxQs)& zu<%Z9>@#b`Q?Mg6v#Z8zXZj;I zIKK^CD|Y)>A=2cNn;IG5GzPc{s~Aq72RZ5?`!R}l?fT$cYAbdd%mUffTzL!6CT8|D zq-)E{{^&WU+q~NERzI^Cx_l{SfRmY`xqUkKuY-^OJ@x2VRHysi(1OX*FpvDG7fJ$XWl%Z3TCZPjX0U<% zZwgh>=*8Q;!fQ`OZVUpfRoWGUU5CtHv+mFGVQlN0hJK$$?JAqC=72bpAVrd`O%VT8 zeL-5L?C{{BvPii_J0a>SCRrd8m1cWPq%j&1UGnKkn(;rCiof;n67ZGiIvj=74u5rC zcd&NVqV%b%`2=VMkNA4g*l=Aa9Hx8%ck_XVu4oAN^G)tzAcq!8FAJoXn+sBQpQF|F zH#}8r&FcpLF_n}u=H`4KH+wiD|6BFF`h?b{rCiKsjWr2MBw3&U^&`Cnnmdj9`lS!t z3BS~d2qBR*4*xs&xWO`NRH2@`AUnVuRa5~4xST7!X4a?UX z71TUB*x>x!#`sw`z?%coKG^y&Vcc^yLpD4#l|gKf)x z$;o(46=b$VL-ezMZRH$yA2P5ym*Y}!u<3bdXFj0SeFtkgL zf-DLXuLKjAw;L@3ukp`>eC7E|I$@7)V@QhnRB<^pRSY_nw#3pK79_1}!1bxZL?;cK zZf0ho(w39%(|;uYXBoF0zo8mC7{Rq*rnpCbALM zHI$^3a~g&X1_?AUm)_JeA0+`_xro4BRNZz1a2I?;NJ~<;HyOx0_VVK%?TNlM0_woQ z25|R3!PmtOR~fYyNT{{c%ofU*?3SnIN}f!-;?LLX`6k!dk&*dYP%#w?J4%kI6Wpf8 z4Q!seg_dq+8P>=ids0~*;L!r0hbu~&=my5}BAsYxvg738XECtgQtn0~wt1N=7*`>= zY=%45C`K+$WcMN|V?IxM!|j=-JNH9%t6I1>Q*}Tmr%AaYZgA_&U(>xHypD2yX&|3G zDDNLHRPtE_cb0}M;+!)4jOShg1;5FDig9yuAZH%iC8HibVQ+I86=OcEhV#RWMAaY9 z3aa`^r3Z0@3U@`b%J$TJPD>v!Vdz~Y^(Fb2Q7&|k9)N>c(79fh%d_{b@g6U;13(62 zn&Qm(8o!pftl8K4jx*@=YP^Gamg-#dtsk7b+#=$1+N4s+%Qwrnr?e3uH5~Wd>pj$4 z14(UwZimAJfV^#o=`x%d9OQ>~$J49wrd{Uq;hV5(GOd7iOa9_B{sqMe?cl22w+={LKDeQ6G7zLpCDp1=b*9xWU{nm!^I?H>l zuP)c!vJ8v+E^TeB-PV3OG~HaaSgx`i8`tu9IJa+94n`xuQ{qxyI&Lj)9FBCz6z{}? zwziL-+S)7Hp-cc|VbOeMKPnj zxQ*j_br%a`SB_>ck@fgH23M*Y6{Df29vxqTMo%}yB)W(HjqPK%(V#BLN1vm)A3XU! zpp%hRH%>*+m1u3e(d2#s*M5CI$RBqfA7}rnY@-$2T)rkyQ78+x>pn?})vVBhhlpea zl3ExmPi;_M?4{S+E~&b6B>vgBH5BEDl=zZ!_lHeEbSG4p|JBZSb~V)nZ6CT)1pxsm z-qHo6i6Z@`2r5;2OOW1s4PYXufOJCdh=6qIV5A8MA%N1QBPG&8l^P)L;r;{f^X2*W zd^jsBYwa`p%(Z9c%-Q>z37O1}@tZYAua{HAf1{6qzOMU~{w}u8cbxfho}d+tL^ zcEL8bIw>bF^FL?=$>cb^+YF)C@dWYUeyG__B7EAT$IColom=Eu)a{b=-Aa&(fY(u8 zkPFnT@^Ffp@$H~v)arb6fUJY!kIntjP*b|^ZlUdcCU3y#eGm-tT^YG{jK8cPBV0fE z`NxodGNcR%bh|Dp@pP@|%ahZEUTs=w=^whp2uNwi!RRgPD8ZKAW_XvONOcuK%zbm_ zMKd$zMeWH2SRed851!-M*Ur`WdjtI>ojYBBF)p51X~UCboQ7Z_vu4KhaSp#@DSzm@RDuX z-&cNZ)cdVHi=@pz$aF5)@mdhL*Ju_>G~d@Ai4PzZv!@?!KGFzksMzgRSRd?Wlvl_M zh_m{=Fv(u2=6dfd`&2(zg5Lm=QvPtPew766Uhae?cZk9Ngn&8(1t^ql>FM*GD z&eJDH!Bz^}Shor?uvNsyLLGi~|5l!~ZHh=A71?18W+P!os;L*O>etE&Z+W84!c_YF z@9ENB#V-o#Gh{2Z>{pR{s-#eHWiT>Ivh#)1Nrhj7e7E>mAv%G+Vlk*5OAg9 z_lk@wtw7FKhmTb3ezI)iJ9t{LL+x(-s%&Pfh46@dcYR#=E1}%lyv5@TumuDM8~Z<} zr^Xc|mncStTbg5w6D_cbYdoa~k8mk|3eY@37k)k%${AOu#8GUWl+=Xb1ba$R~e{{HB|+ zq;Fk-4~%=2@E6c!P1_ampJnEKNM@Wh?EZ7Pca&98bgO-p?Ch#|oXT1|KLcUmP>hqaT zKIhMnSl|Fm^;NlD@EE3gcy%c}?0JyuQluN;Xi_11(7&1Mfds!2S%<+;?GEig5o~>b zVw3Q$AiuI*m>c<)Aa-UyO)akF76&_}$t4Hheu2IHfKt@fWdLqJz}(XFMEw$FmR;6e zgwzkD*HNw>Yv`^&5539f%}-kgwZ9+dbXu~--+U81E7uFf(Ui|xa6DXT zg#{uTc9Zt@9}KD;JlXBR7~Y-C;!Jco1b~4uYCR6#fI(i=Z*U3fkEBSKm~=%4GQdy; zS~B6odBMlY$)5ai31F36G;C@*;dWji+bIi}mN1}=Ax-_x__d*V!^|OV5%dPMp;Av2 zHErGbfCP0Bn+#Twd{DKEgq=EAuW4&g1B?!IuT~ODrd0~tICipke}{W>?a(G^UX=sz zS01f(x#;~9uyhUe$jxXBQ{`jiN%8%%@h4X`?Ke@McbN!sWV7DhS{;fBr)~7kKLP4 zDZk9-61!l7mvfx7R^cW8Rt>N2H;V&59i6=1LSTPK3;a--DV5)Hq7Ty(i?)w3vcn%3 zZi;m5-=zT-ZOwJt0)K|nl-#9ay}YqoNXHSlAwxx{oAhe}8wr~NbDb6v^}o8 zwser%yusl&a$bIgb$6)~J+;=S9w$7)Vm&G1l4dlKg%JPnlGm~cM2IJtnpCUqxeLA` zp}k%q{Ygs(6~Z}_8rxpCVd>hj)I+Fi8C*^tjui`Hcw5k!=!2f;WpPqZ);k<=U}<5oSF0hsq}1=vAE~s zYuikJJ{m2DXBx!yqng){1&m?hDk@t_9UplDU1P+pPI+C?Nya6gCbN+zK6jNuB_7Xa*HaCXG*4W;-C&ADzTZdt*de z+U(t0R#Pde4xZUD?@Yb+G|N?Fw`0oh#?g{CG__V0zaz?i`N-&jlL!7qbKZgnF<^h2 zvbYv@7utYH*r__e9$qUSb`fu@yN|jZy_LIlIT&!Rl{F`)4oU2|0@r zcptteaVvGQ$pWI6FbTD=k#aZN7&!4G0k@uG_#ZzLcu7(G&1}Zdta1d49fC`80cVp| zUs-9y5Tw9BBV6p@@uZ!fk?G@V&wy2wPs+@$Z4G`BzjWU5%*4ZFXShADY-z2X<$>AU zlk&mB23%q{BZ&sk;i~>blA=QklTAiYsB*7yIM1{Uz5dFIcOy`jz!pC)Ot=**lmjaV zLeV$E)j@!Ax-t}x|J1*?7`HenFRykR3?cJ+SQF->uVX?$atg^YQClUNtdF&`9N>Pj zl$=}x(pPsiu-6+3=k5ECYwL&XrR$ng#vP>(A1JnrjB>mOVY^-(aX?VBuF--^VI8wa z0a8Fyl78^KBhStcJ7?uxAbQ|JLpBN~973H;h=h4crPjIci%f~U{ujL@Tu(J6uJljp zTgp&WNDCrVu*qjYU3ikQ6vkn&F4(QWT;px)-S>SV?R#*$hkowoC6@gL?YO_@Q47MU`N{98u>n^Kl5jfMAw=Y5c+OJ}d{ZDntZsJ$G7 zJNji43w$>OS^jA8zZXQ3`0tB!YQ1J*tSS?s>0doJkCY3!?)+GUVz z5Hb(}%ACkeYnjl|{J64Ck{H%Hs!%V4!Xu!+lz_!9Ur|og-_O&3z< zyyuoZ-mb3L-rJ$yT?*Daw1;CK#lQkGLaeJ|59b+HYu8#s-|7pJSN|ebS|>w{eijP^ z*K2I6j?GOf1HQ-Ll!%AtEXewCus!`05#SK;7mo4XJ~8U=zm8xqBl;Qam{tU}KAe^5*L`H!u|+#yCV%61Q;w=QODgF+U3aeC`pUD?xksB^&f%oT#^PH03Q0#<)46Q!BK>d= zU&08=F`Wrq4at@5=XLO67{S-9ZU6kE+!y|j#WqvWH-}ld>0fXeUWau}+@>CNpGWRC zRe5^-g)Yfu68+?e5(`wrMq*V$zh)lObqH4EDb0LfE3a|N(9aoEzii}q-`}XB=l8Tw zOJ<{T1mcn+)1=01U=}m%xv{=%Q<{Y$ej4N;Am~A}CPFn2HzZTcHq9TlOKV+rXrCL_ zP45(v$l1GIiOY05ueu4Oi$hzw-M-2u>#CDD1V9dH;L7v6aL2G87(e0w3yb@Mh9LFq zp`+L4f?V+<21Bj;RrG@7J3S@!@MFKcyOH|RWQgpGHS)ZgPcW=voKKH3%5~dn9ezZL z&<~zjeyL5u7~K{16z4YMMhXxiSdaV2^M+qY`OtfsA}!ne8Y0ke=EtHDfHWheI6`d+ z_bdIVu>!aO2rv*@h8=_}B?%QHUyLF`f@zS13MvHU1@F1#G7^jtfxHr*T5;CFyzR%N z!22GRNn^Gq;egB|Sf1&2vPhcoE}z)*bqK|0ha^CNx!RdrDPpd7ksc6u2!*UaYzxRS z{u=jf5TljCeA;;kIURZS2uToboZXz5zMpGU8*H6aLOl36?c2q)Vt6!%6!je zoLo%(-O_NnSm(r0B<#1NfwuO#e3YAEq1Hpc=9O>ce4!y7Tv4Td)f_kI>FoscKB!VZ zz9?bvJKIr97EsY91a0i386{(=ZbAFnOIYhaIh1c`lQ%TtJlfQ-CZ}UM_nYYe&6?6K zJJ$_Qw6Cy8`(|wU1)ux+0<0Q^n#@)}s`$}S{OJYt z2Q+7@8 z@ebC|L||7Zkb=9sW8dG+y3^pTJm|trtQM%hri>mn1r6EU0982oJU$MwDZmu8UJ$5h z`*YFPjff~$;3}ccE^BX&j)b-hYx_-muQUPLcaBnL6}T=Bs|u;DU(br4!?b4>pneMB z8UWDIJ2PwIFgE2jJ;N2Ry0W0-TSuAT0KGj``OfELmT)&q-Tml+&tDyIB-ha@cGmqS z{-W*X2x~SU@&3wqUp~xs-%}fZ5>duP%>zyapS_idWrLLSmV}-k{?;a!pi8C1T`kN@|rEmJRpMSfeRGfpQn`VT6G^Jt|PEx)9dook@$BiNbL0re|vOE+j z7IdDCm!cA%wmIjJ2d_A*%m=Nmmcw6RPS+)S$L~*lywyG4JY2|g*atbGJLY&B8)p>) z=JAe0rAyW9BBW#Z3NweDycu)vdG$LP34=L!+dIs>#1hgBc?O&3qR(`BedZ_OW6gokD-&!6vsj=;AexVix)OFoJusNIFNO4{btI@&t8^-og=1%1 zT}wajV9Xz0bB?n!amhH> z`mTlm>{Be4Z*KT_1UOrg24W|(TW}0%=A14l4YQo>q}=XTw^)s`C<6LD1;k)xmc zTw>w(ZasFn27s#Pd)}|yXQc}eMsF&_tA^J$9yIMQb_DYVs~$FSqsCJ%aX~rfYO1e> zJ!z1NLI7Zun@2}m8XV6)h@nJeUpf{@BtMX~*jLhzu+TS!G9agI^es1TzYdj1`V$3J zU1@pOQ2QEnK@Z;8s1C!AwIBXnkpkuyj>s3-TdKXHg#B}~9@S4Vu^O2Z2iqC@e!PPx zI9;rQ&DT=US00|Rr?bPMAAg8H-~@Ky7ud2VEln?L>Sct6qR`v1YoE@Htn$7xvI_ii zk+WzryDam*j=VBH$xSESQ1w%~qe-eF>L; z34|OC(s4{8Q4p`(otdsz85+G>ti};~d_RUPg*NA6H7~E$z6474WE>YUFU1Rh77ULr zu+8WaGBzH=NR${ePs!Kz{Z4#rR5I}~=q|m1`c3^2ro5z!J@KH&a(Cp>`RmK;B6Vmg zn#q2cw8o2bSKRht;lbCft}Uh5v*N(Tt)$&U&p_9!{_PiXm6RPRbmN zQuWw?X+n_m_PFM`T8u0aaYyg2N#(w6@7wQ_S!S#;b&C?h`^#sT^m9`p0O7N6TmR<9 zQQ=OXr^I*44z8MC*c8x?i{>Ec|4?+&5aK;PW;cqb%i#eOzP(IFx3)3u<1(qzPe4Bs z(Aps_8IzyO+1Czy58O!Hv^ZEvJR2XeJ`g*-2>`lhoLn3F1n>P%njabJBmicUxqT}f zueW}WGT$o@4EwZogBP5Cx%3rT*AqQurX>IMXO#Po1t1+GF}3EN^ds186?Po*-~FDA=X;Sb=L(%%md%vqJzYJD4&FGk z)37GP?Fv_i5iqg?r?vbKiXl0isg?zZEm{hmPf3z&K!Y7^%Qq?Y`#uwx!|l`AjtFpI z`b&>MO&n!x|K@K5d!)8Tz+ID{fQfT?G8KoEcack+#YP2kHSQi?2&lU*HLWVYVSt&vbmwYjTp%0x3nB(w{RKde1FnIW5_o}mG61H0pzng$|8&UzNdmFD na|PJ{AIksXu>Va-jwf*6uGn){?9zj)XMU=tty-mQ754uC6?uy9 literal 0 HcmV?d00001 diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/_static/favicon-64.png b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/_static/favicon-64.png new file mode 100644 index 0000000000000000000000000000000000000000..da2c2b507f6f449cefd1e0bcde42f0fca4620447 GIT binary patch literal 3207 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 literal 0 HcmV?d00001 diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/_static/logo-512.png b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/_static/logo-512.png new file mode 100644 index 0000000000000000000000000000000000000000..aaa34e91f2b5728f39cd4fe3c22e3eefed81b5a9 GIT binary patch literal 41788 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 )(*Z&^ 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 literal 0 HcmV?d00001 diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/_static/screenshot-apidoc-quickstart.png b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/_static/screenshot-apidoc-quickstart.png new file mode 100644 index 0000000000000000000000000000000000000000..941199a66fccb5b56a6b452b3eda2b5b6c8c5a25 GIT binary patch literal 131539 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 z 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 #M>IaS=`pJa_8AywmcX)o7BK~g4c= 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 literal 0 HcmV?d00001 diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/_themes/restx/badges.html b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/_themes/restx/badges.html new file mode 100644 index 0000000..a2e6558 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/_themes/restx/badges.html @@ -0,0 +1,7 @@ + +{% if theme_badges %} +


+{% for badge, target, alt in theme_badges %} +

{{alt}}

+{% endfor %} +{% endif %} diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/_themes/restx/layout.html b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/_themes/restx/layout.html new file mode 100644 index 0000000..2e41447 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/_themes/restx/layout.html @@ -0,0 +1,10 @@ +{% extends "alabaster/layout.html" %} + +{%- block extrahead %} +{% if theme_favicons %} + {% for size, file in theme_favicons.items() %} + + {% endfor %} +{% endif %} +{{ super() }} +{% endblock %} diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/_themes/restx/static/restx.css b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/_themes/restx/static/restx.css new file mode 100644 index 0000000..97d0e86 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/_themes/restx/static/restx.css @@ -0,0 +1,12 @@ +@import url("alabaster.css"); + +.sphinxsidebar p.badge a { + border: none; +} + +.sphinxsidebar hr.badges { + border: 0; + border-bottom: 1px dashed #aaa; + background: none; + /*width: 100%;*/ +} diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/_themes/restx/theme.conf b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/_themes/restx/theme.conf new file mode 100644 index 0000000..82356ea --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/_themes/restx/theme.conf @@ -0,0 +1,7 @@ +[theme] +inherit = alabaster +stylesheet = restx.css + +[options] +favicons= +badges= diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/api.rst b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/api.rst new file mode 100644 index 0000000..ac50a34 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/api.rst @@ -0,0 +1,98 @@ +.. _api: + +API +=== + +.. currentmodule:: flask_restx + +Core +---- + +.. autoclass:: Api + :members: + :inherited-members: + +.. autoclass:: Namespace + :members: + + +.. autoclass:: Resource + :members: + :inherited-members: + + +Models +------ + +.. autoclass:: flask_restx.Model + :members: + +All fields accept a ``required`` boolean and a ``description`` string in ``kwargs``. + +.. automodule:: flask_restx.fields + :members: + + +Serialization +------------- +.. currentmodule:: flask_restx + +.. autofunction:: marshal + +.. autofunction:: marshal_with + +.. autofunction:: marshal_with_field + +.. autoclass:: flask_restx.mask.Mask + :members: + +.. autofunction:: flask_restx.mask.apply + + +Request parsing +--------------- + +.. automodule:: flask_restx.reqparse + :members: + +Inputs +~~~~~~ + +.. automodule:: flask_restx.inputs + :members: + + +Errors +------ + +.. automodule:: flask_restx.errors + :members: + +.. autoexception:: flask_restx.fields.MarshallingError + +.. autoexception:: flask_restx.mask.MaskError + +.. autoexception:: flask_restx.mask.ParseError + + +Schemas +------- + +.. automodule:: flask_restx.schemas + :members: + + +Internals +--------- + +These are internal classes or helpers. +Most of the time you shouldn't have to deal directly with them. + +.. autoclass:: flask_restx.api.SwaggerView + +.. autoclass:: flask_restx.swagger.Swagger + +.. autoclass:: flask_restx.postman.PostmanCollectionV1 + +.. automodule:: flask_restx.utils + :members: diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/conf.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/conf.py new file mode 100644 index 0000000..3bcb660 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/conf.py @@ -0,0 +1,342 @@ +# -*- coding: utf-8 -*- +# +# Flask-RESTX documentation build configuration file, created by +# sphinx-quickstart on Wed Aug 13 17:07:14 2014. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import os +import sys +import alabaster + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +sys.path.insert(0, os.path.abspath("..")) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.viewcode", + "sphinx.ext.intersphinx", + "sphinx.ext.todo", + "sphinx_issues", + "alabaster", +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# The suffix of source filenames. +source_suffix = ".rst" + +# The encoding of source files. +# source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = "index" + +# General information about the project. +project = "Flask-RESTX" +copyright = "2020, python-restx Authors" + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The full version, including alpha/beta/rc tags. +release = __import__("flask_restx").__version__ +# The short X.Y version. +version = ".".join(release.split(".")[:1]) + +# Github repo +issues_github_path = "python-restx/flask-restx" + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +# today = '' +# Else, today_fmt is used as the format for a strftime call. +# today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ["_build"] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +# default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +# add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +# add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +# show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = "sphinx" + +# A list of ignored prefixes for module index sorting. +# modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +# keep_warnings = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = "restx" + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +html_theme_options = { + "logo": "logo-512.png", + "logo_name": True, + "touch_icon": "apple-180.png", + "github_user": "python-restx", + "github_repo": "flask-restx", + "github_banner": True, + "show_related": True, + "page_width": "1000px", + "sidebar_width": "260px", + "favicons": { + 64: "favicon-64.png", + 128: "favicon-128.png", + 196: "favicon-196.png", + }, + "badges": [ + ( + # Gitter.im + "https://badges.gitter.im/Join%20Chat.svg", + "https://gitter.im/python-restx", + "Join the chat at https://gitter.im/python-restx", + ), + ( + # Github Fork + "https://img.shields.io/github/forks/python-restx/flask-restx.svg?style=social&label=Fork", + "https://github.com/python-restx/flask-restx", + "Github repository", + ), + ( + # Github issues + "https://img.shields.io/github/issues-raw/python-restx/flask-restx.svg", + "https://github.com/python-restx/flask-restx/issues", + "Github repository", + ), + ( + # License + "https://img.shields.io/github/license/python-restx/flask-restx.svg", + "https://github.com/python-restx/flask-restx", + "License", + ), + ( + # PyPI + "https://img.shields.io/pypi/v/flask-restx.svg", + "https://pypi.python.org/pypi/flask-restx", + "Latest version on PyPI", + ), + ], +} + +# Add any paths that contain custom themes here, relative to this directory. +html_theme_path = [alabaster.get_path(), "_themes"] + +html_context = {} + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +# html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +# html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +# html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +html_favicon = "_static/favicon.ico" + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ["_static"] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +# html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +# html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +# html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +html_sidebars = { + "**": [ + "about.html", + "navigation.html", + "relations.html", + "searchbox.html", + "donate.html", + "badges.html", + ] +} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +# html_additional_pages = {} + +# If false, no module index is generated. +# html_domain_indices = True + +# If false, no index is generated. +# html_use_index = True + +# If true, the index is split into individual pages for each letter. +# html_split_index = False + +# If true, links to the reST sources are added to the pages. +# html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +# html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +# html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +# html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +# html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = "Flask-RESTXdoc" + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + #'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + ( + "index", + "Flask-RESTX.tex", + "Flask-RESTX Documentation", + "python-restx Authors", + "manual", + ), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +# latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +# latex_use_parts = False + +# If true, show page references after internal links. +# latex_show_pagerefs = False + +# If true, show URL addresses after external links. +# latex_show_urls = False + +# Documents to append as an appendix to all manuals. +# latex_appendices = [] + +# If false, no module index is generated. +# latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ("index", "flask-restx", "Flask-RESTX Documentation", ["python-restx Authors"], 1) +] + +# If true, show URL addresses after external links. +# man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ( + "index", + "Flask-RESTX", + "Flask-RESTX Documentation", + "python-restx Authors", + "Flask-RESTX", + "One line description of project.", + "Miscellaneous", + ), +] + +# Documents to append as an appendix to all manuals. +# texinfo_appendices = [] + +# If false, no module index is generated. +# texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +# texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +# texinfo_no_detailmenu = False + + +intersphinx_mapping = { + "flask": ("https://flask.palletsprojects.com/", None), + "python": ("https://docs.python.org/", None), + "werkzeug": ("https://werkzeug.palletsprojects.com/", None), +} diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/configuration.rst b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/configuration.rst new file mode 100644 index 0000000..1f4783e --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/configuration.rst @@ -0,0 +1,65 @@ +Configuration +============= + +Flask-RESTX provides the following `Flask configuration values `_: + + Note: Values with no additional description should be covered in more detail + elsewhere in the documentation. If not, please open an issue on GitHub. + +.. py:data:: RESTX_JSON + + Provide global configuration options for JSON serialisation as a :class:`dict` + of :func:`json.dumps` keyword arguments. + +.. py:data:: RESTX_VALIDATE + + Whether to enforce payload validation by default when using the + ``@api.expect()`` decorator. See the `@api.expect() + `__ documentation for details. + This setting defaults to ``False``. + +.. py:data:: RESTX_MASK_HEADER + + Choose the name of the *Header* that will contain the masks to apply to your + answer. See the `Fields masks `__ documentation for details. + This setting defaults to ``X-Fields``. + +.. py:data:: RESTX_MASK_SWAGGER + + Whether to enable the mask documentation in your swagger or not. See the + `mask usage `__ documentation for details. + This setting defaults to ``True``. + +.. py:data:: RESTX_INCLUDE_ALL_MODELS + + This option allows you to include all defined models in the generated Swagger + documentation, even if they are not explicitly used in either ``expect`` nor + ``marshal_with`` decorators. + This setting defaults to ``False``. + +.. py:data:: BUNDLE_ERRORS + + Bundle all the validation errors instead of returning only the first one + encountered. See the `Error Handling `__ section + of the documentation for details. + This setting defaults to ``False``. + +.. py:data:: ERROR_404_HELP + +.. py:data:: HTTP_BASIC_AUTH_REALM + +.. py:data:: SWAGGER_VALIDATOR_URL + +.. py:data:: SWAGGER_UI_DOC_EXPANSION + +.. py:data:: SWAGGER_UI_OPERATION_ID + +.. py:data:: SWAGGER_UI_REQUEST_DURATION + +.. py:data:: SWAGGER_UI_OAUTH_APP_NAME + +.. py:data:: SWAGGER_UI_OAUTH_CLIENT_ID + +.. py:data:: SWAGGER_UI_OAUTH_REALM + +.. py:data:: SWAGGER_SUPPORTED_SUBMIT_METHODS diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/contributing.rst b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/contributing.rst new file mode 100644 index 0000000..e582053 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/contributing.rst @@ -0,0 +1 @@ +.. include:: ../CONTRIBUTING.rst diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/errors.rst b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/errors.rst new file mode 100644 index 0000000..41db63b --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/errors.rst @@ -0,0 +1,227 @@ +Error handling +============== + +.. currentmodule:: flask_restx + +HTTPException handling +---------------------- + +Werkzeug HTTPException are automatically properly seriliazed +reusing the description attribute. + +.. code-block:: python + + from werkzeug.exceptions import BadRequest + raise BadRequest() + +will return a 400 HTTP code and output + +.. code-block:: json + + { + "message": "The browser (or proxy) sent a request that this server could not understand." + } + +whereas this: + +.. code-block:: python + + from werkzeug.exceptions import BadRequest + raise BadRequest('My custom message') + +will output + +.. code-block:: json + + { + "message": "My custom message" + } + +You can attach extras attributes to the output by providing a data attribute to your exception. + +.. code-block:: python + + from werkzeug.exceptions import BadRequest + e = BadRequest('My custom message') + e.data = {'custom': 'value'} + raise e + +will output + +.. code-block:: json + + { + "message": "My custom message", + "custom": "value" + } + +The Flask abort helper +---------------------- + +The :meth:`abort ` helper +properly wraps errors into a :exc:`~werkzeug.exceptions.HTTPException` +so it will have the same behavior. + +.. code-block:: python + + from flask import abort + abort(400) + +will return a 400 HTTP code and output + +.. code-block:: json + + { + "message": "The browser (or proxy) sent a request that this server could not understand." + } + +whereas this: + +.. code-block:: python + + from flask import abort + abort(400, 'My custom message') + +will output + +.. code-block:: json + + { + "message": "My custom message" + } + + +The Flask-RESTX abort helper +------------------------------- + +The :func:`errors.abort` and the :meth:`Namespace.abort` helpers +works like the original Flask :func:`flask.abort` +but it will also add the keyword arguments to the response. + +.. code-block:: python + + from flask_restx import abort + abort(400, custom='value') + +will return a 400 HTTP code and output + +.. code-block:: json + + { + "message": "The browser (or proxy) sent a request that this server could not understand.", + "custom": "value" + } + +whereas this: + +.. code-block:: python + + from flask import abort + abort(400, 'My custom message', custom='value') + +will output + +.. code-block:: json + + { + "message": "My custom message", + "custom": "value" + } + + +The ``@api.errorhandler`` decorator +----------------------------------- + +The :meth:`@api.errorhandler ` decorator +allows you to register a specific handler for a given exception (or any exceptions inherited from it), in the same manner +that you can do with Flask/Blueprint :meth:`@errorhandler ` decorator. + +.. code-block:: python + + @api.errorhandler(RootException) + def handle_root_exception(error): + '''Return a custom message and 400 status code''' + return {'message': 'What you want'}, 400 + + + @api.errorhandler(CustomException) + def handle_custom_exception(error): + '''Return a custom message and 400 status code''' + return {'message': 'What you want'}, 400 + + + @api.errorhandler(AnotherException) + def handle_another_exception(error): + '''Return a custom message and 500 status code''' + return {'message': error.specific} + + + @api.errorhandler(FakeException) + def handle_fake_exception_with_header(error): + '''Return a custom message and 400 status code''' + return {'message': error.message}, 400, {'My-Header': 'Value'} + + + @api.errorhandler(NoResultFound) + def handle_no_result_exception(error): + '''Return a custom not found error message and 404 status code''' + return {'message': error.specific}, 404 + + +.. note :: + + A "NoResultFound" error with description is required by the OpenAPI 2.0 spec. The docstring in the error handle function is output in the swagger.json as the description. + +You can also document the error: + +.. code-block:: python + + @api.errorhandler(FakeException) + @api.marshal_with(error_fields, code=400) + @api.header('My-Header', 'Some description') + def handle_fake_exception_with_header(error): + '''This is a custom error''' + return {'message': error.message}, 400, {'My-Header': 'Value'} + + + @api.route('/test/') + class TestResource(Resource): + def get(self): + ''' + Do something + + :raises CustomException: In case of something + ''' + pass + +In this example, the ``:raise:`` docstring will be automatically extracted +and the response 400 will be documented properly. + + +It also allows for overriding the default error handler when used without parameter: + +.. code-block:: python + + @api.errorhandler + def default_error_handler(error): + '''Default error handler''' + return {'message': str(error)}, getattr(error, 'code', 500) + +.. note :: + + Flask-RESTX will return a message in the error response by default. + If a custom response is required as an error and the message field is not needed, + it can be disabled by setting ``ERROR_INCLUDE_MESSAGE`` to ``False`` in your application config. + +Error handlers can also be registered on namespaces. An error handler registered on a namespace +will override one registered on the api. + + +.. code-block:: python + + ns = Namespace('cats', description='Cats related operations') + + @ns.errorhandler + def specific_namespace_error_handler(error): + '''Namespace error handler''' + return {'message': str(error)}, getattr(error, 'code', 500) diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/example.rst b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/example.rst new file mode 100644 index 0000000..b163a86 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/example.rst @@ -0,0 +1,108 @@ +Full example +============ + +Here is a full example of a `TodoMVC `_ API. + +.. code-block:: python + + from flask import Flask + from flask_restx import Api, Resource, fields + from werkzeug.middleware.proxy_fix import ProxyFix + + app = Flask(__name__) + app.wsgi_app = ProxyFix(app.wsgi_app) + api = Api(app, version='1.0', title='TodoMVC API', + description='A simple TodoMVC API', + ) + + ns = api.namespace('todos', description='TODO operations') + + todo = api.model('Todo', { + 'id': fields.Integer(readonly=True, description='The task unique identifier'), + 'task': fields.String(required=True, description='The task details') + }) + + + class TodoDAO(object): + def __init__(self): + self.counter = 0 + self.todos = [] + + def get(self, id): + for todo in self.todos: + if todo['id'] == id: + return todo + api.abort(404, "Todo {} doesn't exist".format(id)) + + def create(self, data): + todo = data + todo['id'] = self.counter = self.counter + 1 + self.todos.append(todo) + return todo + + def update(self, id, data): + todo = self.get(id) + todo.update(data) + return todo + + def delete(self, id): + todo = self.get(id) + self.todos.remove(todo) + + + DAO = TodoDAO() + DAO.create({'task': 'Build an API'}) + DAO.create({'task': '?????'}) + DAO.create({'task': 'profit!'}) + + + @ns.route('/') + class TodoList(Resource): + '''Shows a list of all todos, and lets you POST to add new tasks''' + @ns.doc('list_todos') + @ns.marshal_list_with(todo) + def get(self): + '''List all tasks''' + return DAO.todos + + @ns.doc('create_todo') + @ns.expect(todo) + @ns.marshal_with(todo, code=201) + def post(self): + '''Create a new task''' + return DAO.create(api.payload), 201 + + + @ns.route('/') + @ns.response(404, 'Todo not found') + @ns.param('id', 'The task identifier') + class Todo(Resource): + '''Show a single todo item and lets you delete them''' + @ns.doc('get_todo') + @ns.marshal_with(todo) + def get(self, id): + '''Fetch a given resource''' + return DAO.get(id) + + @ns.doc('delete_todo') + @ns.response(204, 'Todo deleted') + def delete(self, id): + '''Delete a task given its identifier''' + DAO.delete(id) + return '', 204 + + @ns.expect(todo) + @ns.marshal_with(todo) + def put(self, id): + '''Update a task given its identifier''' + return DAO.update(id, api.payload) + + + if __name__ == '__main__': + app.run(debug=True) + + + +You can find other examples in the `github repository examples folder`_. + +.. _github repository examples folder: https://github.com/python-restx/flask-restx/tree/master/examples diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/index.rst b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/index.rst new file mode 100644 index 0000000..f0316aa --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/index.rst @@ -0,0 +1,103 @@ +.. Flask-RESTX documentation master file, created by + sphinx-quickstart on Wed Aug 13 17:07:14 2014. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to Flask-RESTX's documentation! +======================================= + +Flask-RESTX is an extension for Flask that adds support for quickly building REST APIs. +Flask-RESTX encourages best practices with minimal setup. +If you are familiar with Flask, Flask-RESTX should be easy to pick up. +It provides a coherent collection of decorators and tools to describe your API +and expose its documentation properly (using Swagger). + +Flask-RESTX is a community driven fork of `Flask-RESTPlus +`_ + + +Why did we fork? +================ + +The community has decided to fork the project due to lack of response from the +original author @noirbizarre. We have been discussing this eventuality for +`a long time `_. + +Things evolved a bit since that discussion and a few of us have been granted +maintainers access to the github project, but only the original author has +access rights on the PyPi project. As such, we been unable to make any actual +releases. To prevent this project from dying out, we have forked it to continue +development and to support our users. + + +Compatibility +============= + +Flask-RESTX requires Python 3.8+. + + +Installation +============ + +You can install Flask-RESTX with pip: + +.. code-block:: console + + $ pip install flask-restx + +or with easy_install: + +.. code-block:: console + + $ easy_install flask-restx + + +Documentation +============= + +This part of the documentation will show you how to get started in using +Flask-RESTX with Flask. + +.. toctree:: + :maxdepth: 2 + + installation + quickstart + marshalling + parsing + errors + mask + swagger + logging + postman + scaling + example + configuration + + +API Reference +------------- + +If you are looking for information on a specific function, class or +method, this part of the documentation is for you. + +.. toctree:: + :maxdepth: 2 + + api + +Additional Notes +---------------- + +.. toctree:: + :maxdepth: 2 + + contributing + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/installation.rst b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/installation.rst new file mode 100644 index 0000000..62d0ae4 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/installation.rst @@ -0,0 +1,24 @@ +.. _installation: + +Installation +============ + +Install Flask-RESTX with ``pip``: + +.. code-block:: console + + pip install flask-restx + + +The development version can be downloaded from +`GitHub `_. + +.. code-block:: console + + git clone https://github.com/python-restx/flask-restx.git + cd flask-restx + pip install -e .[dev,test] + + +Flask-RESTX requires Python version 3.8+. +It's also working with PyPy and PyPy3. diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/logging.rst b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/logging.rst new file mode 100644 index 0000000..32fdb70 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/logging.rst @@ -0,0 +1,103 @@ +Logging +=============== + +Flask-RESTX extends `Flask's logging `_ +by providing each ``API`` and ``Namespace`` it's own standard Python :class:`logging.Logger` instance. +This allows separation of logging on a per namespace basis to allow more fine-grained detail and configuration. + +By default, these loggers inherit configuration from the Flask application object logger. + +.. code-block:: python + + import logging + + import flask + + from flask_restx import Api, Resource + + # configure root logger + logging.basicConfig(level=logging.INFO) + + app = flask.Flask(__name__) + + api = Api(app) + + + # each of these loggers uses configuration from app.logger + ns1 = api.namespace('api/v1', description='test') + ns2 = api.namespace('api/v2', description='test') + + + @ns1.route('/my-resource') + class MyResource(Resource): + def get(self): + # will log + ns1.logger.info("hello from ns1") + return {"message": "hello"} + + + @ns2.route('/my-resource') + class MyNewResource(Resource): + def get(self): + # won't log due to INFO log level from app.logger + ns2.logger.debug("hello from ns2") + return {"message": "hello"} + + +Loggers can be configured individually to override the configuration from the Flask +application object logger. In the above example, ``ns2`` log level can be set to +``DEBUG`` individually: + +.. code-block:: python + + # ns1 will have log level INFO from app.logger + ns1 = api.namespace('api/v1', description='test') + + # ns2 will have log level DEBUG + ns2 = api.namespace('api/v2', description='test') + ns2.logger.setLevel(logging.DEBUG) + + + @ns1.route('/my-resource') + class MyResource(Resource): + def get(self): + # will log + ns1.logger.info("hello from ns1") + return {"message": "hello"} + + + @ns2.route('/my-resource') + class MyNewResource(Resource): + def get(self): + # will log + ns2.logger.debug("hello from ns2") + return {"message": "hello"} + + +Adding additional handlers: + + +.. code-block:: python + + # configure a file handler for ns1 only + ns1 = api.namespace('api/v1') + fh = logging.FileHandler("v1.log") + ns1.logger.addHandler(fh) + + ns2 = api.namespace('api/v2') + + + @ns1.route('/my-resource') + class MyResource(Resource): + def get(self): + # will log to *both* v1.log file and app.logger handlers + ns1.logger.info("hello from ns1") + return {"message": "hello"} + + + @ns2.route('/my-resource') + class MyNewResource(Resource): + def get(self): + # will log to *only* app.logger handlers + ns2.logger.info("hello from ns2") + return {"message": "hello"} \ No newline at end of file diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/make.bat b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/make.bat new file mode 100644 index 0000000..553de42 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/make.bat @@ -0,0 +1,242 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=_build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . +set I18NSPHINXOPTS=%SPHINXOPTS% . +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% + set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. texinfo to make Texinfo files + echo. gettext to make PO message catalogs + echo. changes to make an overview over all changed/added/deprecated items + echo. xml to make Docutils-native XML files + echo. pseudoxml to make pseudoxml-XML files for display purposes + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + + +%SPHINXBUILD% 2> nul +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Flask-RESTX.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Flask-RESTX.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdf" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf + cd %BUILDDIR%/.. + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdfja" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf-ja + cd %BUILDDIR%/.. + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "texinfo" ( + %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. + goto end +) + +if "%1" == "gettext" ( + %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The message catalogs are in %BUILDDIR%/locale. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +if "%1" == "xml" ( + %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The XML files are in %BUILDDIR%/xml. + goto end +) + +if "%1" == "pseudoxml" ( + %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. + goto end +) + +:end diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/marshalling.rst b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/marshalling.rst new file mode 100644 index 0000000..1893dc9 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/marshalling.rst @@ -0,0 +1,542 @@ +.. _fields: + +Response marshalling +==================== + +.. currentmodule:: flask_restx + + +Flask-RESTX provides an easy way to control what data you actually render in +your response or expect as an input payload. +With the :mod:`~.fields` module, you can use whatever objects (ORM +models/custom classes/etc.) you want in your resource. +:mod:`~.fields` also lets you format and filter the response +so you don't have to worry about exposing internal data structures. + +It's also very clear when looking at your code what data will be rendered and +how it will be formatted. + + +Basic Usage +----------- +You can define a dict or OrderedDict of fields whose keys are names of attributes or keys on the object to render, +and whose values are a class that will format & return the value for that field. +This example has three fields: +two are :class:`~fields.String` and one is a :class:`~fields.DateTime`, +formatted as an ISO 8601 datetime string (RFC 822 is supported as well): + +.. code-block:: python + + from flask_restx import Resource, fields + + model = api.model('Model', { + 'name': fields.String, + 'address': fields.String, + 'date_updated': fields.DateTime(dt_format='rfc822'), + }) + + @api.route('/todo') + class Todo(Resource): + @api.marshal_with(model, envelope='resource') + def get(self, **kwargs): + return db_get_todo() # Some function that queries the db + + +This example assumes that you have a custom database object (``todo``) that +has attributes ``name``, ``address``, and ``date_updated``. +Any additional attributes on the object are considered private and won't be rendered in the output. +An optional ``envelope`` keyword argument is specified to wrap the resulting output. + +The decorator :meth:`~Api.marshal_with` is what actually takes your data object and applies the field filtering. +The marshalling can work on single objects, dicts, or lists of objects. + +.. note :: + + :func:`marshal_with` is a convenience decorator, that is functionally + equivalent to: + + .. code-block:: python + + class Todo(Resource): + def get(self, **kwargs): + return marshal(db_get_todo(), model), 200 + + The :meth:`@api.marshal_with ` decorator add the swagger documentation ability. + +This explicit expression can be used to return HTTP status codes other than 200 +along with a successful response (see :func:`~errors.abort` for errors). + + +Renaming Attributes +------------------- + +Often times your public facing field name is different from your internal field name. +To configure this mapping, use the ``attribute`` keyword argument. :: + + model = { + 'name': fields.String(attribute='private_name'), + 'address': fields.String, + } + +A lambda (or any callable) can also be specified as the ``attribute`` :: + + model = { + 'name': fields.String(attribute=lambda x: x._private_name), + 'address': fields.String, + } + +Nested properties can also be accessed with ``attribute``:: + + model = { + 'name': fields.String(attribute='people_list.0.person_dictionary.name'), + 'address': fields.String, + } + + +Default Values +-------------- + +If for some reason your data object doesn't have an attribute in your fields list, +you can specify a default value to return instead of :obj:`None`. + +.. code-block:: python + + model = { + 'name': fields.String(default='Anonymous User'), + 'address': fields.String, + } + + +Custom Fields & Multiple Values +------------------------------- + +Sometimes you have your own custom formatting needs. +You can subclass the :class:`fields.Raw` class and implement the format function. +This is especially useful when an attribute stores multiple pieces of information. +e.g. a bit-field whose individual bits represent distinct values. +You can use fields to multiplex a single attribute to multiple output values. + + +This example assumes that bit 1 in the ``flags`` attribute signifies a +"Normal" or "Urgent" item, and bit 2 signifies "Read" or "Unread". +These items might be easy to store in a bitfield, +but for a human readable output it's nice to convert them to separate string fields. + +.. code-block:: python + + class UrgentItem(fields.Raw): + def format(self, value): + return "Urgent" if value & 0x01 else "Normal" + + class UnreadItem(fields.Raw): + def format(self, value): + return "Unread" if value & 0x02 else "Read" + + model = { + 'name': fields.String, + 'priority': UrgentItem(attribute='flags'), + 'status': UnreadItem(attribute='flags'), + } + + +Url & Other Concrete Fields +--------------------------- + +Flask-RESTX includes a special field, :class:`fields.Url`, +that synthesizes a uri for the resource that's being requested. +This is also a good example of how to add data to your response that's not actually present on your data object. + +.. code-block:: python + + class RandomNumber(fields.Raw): + def output(self, key, obj): + return random.random() + + model = { + 'name': fields.String, + # todo_resource is the endpoint name when you called api.route() + 'uri': fields.Url('todo_resource'), + 'random': RandomNumber, + } + + +By default :class:`fields.Url` returns a relative uri. +To generate an absolute uri that includes the scheme, hostname and port, +pass the keyword argument ``absolute=True`` in the field declaration. +To override the default scheme, pass the ``scheme`` keyword argument: + +.. code-block:: python + + model = { + 'uri': fields.Url('todo_resource', absolute=True), + 'https_uri': fields.Url('todo_resource', absolute=True, scheme='https') + } + + +Complex Structures +------------------ + +You can have a flat structure that :func:`marshal` will transform to a nested structure: + +.. code-block:: python + + >>> from flask_restx import fields, marshal + >>> import json + >>> + >>> resource_fields = {'name': fields.String} + >>> resource_fields['address'] = {} + >>> resource_fields['address']['line 1'] = fields.String(attribute='addr1') + >>> resource_fields['address']['line 2'] = fields.String(attribute='addr2') + >>> resource_fields['address']['city'] = fields.String + >>> resource_fields['address']['state'] = fields.String + >>> resource_fields['address']['zip'] = fields.String + >>> data = {'name': 'bob', 'addr1': '123 fake street', 'addr2': '', 'city': 'New York', 'state': 'NY', 'zip': '10468'} + >>> json.dumps(marshal(data, resource_fields)) + '{"name": "bob", "address": {"line 1": "123 fake street", "line 2": "", "state": "NY", "zip": "10468", "city": "New York"}}' + +.. note :: + The address field doesn't actually exist on the data object, + but any of the sub-fields can access attributes directly from the object + as if they were not nested. + +.. _list-field: + +List Field +---------- + +You can also unmarshal fields as lists :: + + >>> from flask_restx import fields, marshal + >>> import json + >>> + >>> resource_fields = {'name': fields.String, 'first_names': fields.List(fields.String)} + >>> data = {'name': 'Bougnazal', 'first_names' : ['Emile', 'Raoul']} + >>> json.dumps(marshal(data, resource_fields)) + >>> '{"first_names": ["Emile", "Raoul"], "name": "Bougnazal"}' + +.. _wildcard-field: + +Wildcard Field +-------------- + +If you don't know the name(s) of the field(s) you want to unmarshall, you can +use :class:`~fields.Wildcard` :: + + >>> from flask_restx import fields, marshal + >>> import json + >>> + >>> wild = fields.Wildcard(fields.String) + >>> wildcard_fields = {'*': wild} + >>> data = {'John': 12, 'bob': 42, 'Jane': '68'} + >>> json.dumps(marshal(data, wildcard_fields)) + >>> '{"Jane": "68", "bob": "42", "John": "12"}' + +The name you give to your :class:`~fields.Wildcard` acts as a real glob as +shown below :: + + >>> from flask_restx import fields, marshal + >>> import json + >>> + >>> wild = fields.Wildcard(fields.String) + >>> wildcard_fields = {'j*': wild} + >>> data = {'John': 12, 'bob': 42, 'Jane': '68'} + >>> json.dumps(marshal(data, wildcard_fields)) + >>> '{"Jane": "68", "John": "12"}' + +.. note :: + It is important you define your :class:`~fields.Wildcard` **outside** your + model (ie. you **cannot** use it like this: + ``res_fields = {'*': fields.Wildcard(fields.String)}``) because it has to be + stateful to keep a track of what fields it has already treated. + +.. note :: + The glob is not a regex, it can only treat simple wildcards like '*' or '?'. + +In order to avoid unexpected behavior, when mixing :class:`~fields.Wildcard` +with other fields, you may want to use an ``OrderedDict`` and use the +:class:`~fields.Wildcard` as the last field :: + + >>> from flask_restx import fields, marshal + >>> import json + >>> + >>> wild = fields.Wildcard(fields.Integer) + >>> # you can use it in api.model like this: + >>> # some_fields = api.model('MyModel', {'zoro': fields.String, '*': wild}) + >>> + >>> data = {'John': 12, 'bob': 42, 'Jane': '68', 'zoro': 72} + >>> json.dumps(marshal(data, mod)) + >>> '{"zoro": "72", "Jane": 68, "bob": 42, "John": 12}' + +.. _nested-field: + +Nested Field +------------ + +While nesting fields using dicts can turn a flat data object into a nested +response, you can use :class:`~fields.Nested` to unmarshal nested data +structures and render them appropriately. :: + + >>> from flask_restx import fields, marshal + >>> import json + >>> + >>> address_fields = {} + >>> address_fields['line 1'] = fields.String(attribute='addr1') + >>> address_fields['line 2'] = fields.String(attribute='addr2') + >>> address_fields['city'] = fields.String(attribute='city') + >>> address_fields['state'] = fields.String(attribute='state') + >>> address_fields['zip'] = fields.String(attribute='zip') + >>> + >>> resource_fields = {} + >>> resource_fields['name'] = fields.String + >>> resource_fields['billing_address'] = fields.Nested(address_fields) + >>> resource_fields['shipping_address'] = fields.Nested(address_fields) + >>> address1 = {'addr1': '123 fake street', 'city': 'New York', 'state': 'NY', 'zip': '10468'} + >>> address2 = {'addr1': '555 nowhere', 'city': 'New York', 'state': 'NY', 'zip': '10468'} + >>> data = {'name': 'bob', 'billing_address': address1, 'shipping_address': address2} + >>> + >>> json.dumps(marshal(data, resource_fields)) + '{"billing_address": {"line 1": "123 fake street", "line 2": null, "state": "NY", "zip": "10468", "city": "New York"}, "name": "bob", "shipping_address": {"line 1": "555 nowhere", "line 2": null, "state": "NY", "zip": "10468", "city": "New York"}}' + +This example uses two :class:`~fields.Nested` fields. +The :class:`~fields.Nested` constructor takes a dict of fields to render as sub-fields.input. +The important difference between the :class:`~fields.Nested` constructor and nested dicts (previous example), +is the context for attributes. +In this example, +``billing_address`` is a complex object that has its own fields and +the context passed to the nested field is the sub-object instead of the original ``data`` object. +In other words: +``data.billing_address.addr1`` is in scope here, +whereas in the previous example ``data.addr1`` was the location attribute. +Remember: :class:`~fields.Nested` and :class:`~fields.List` objects create a new scope for attributes. + +By default when the sub-object is `None`, an object with default values for the nested fields will be generated instead of `null`. This can be modified by passing the `allow_null` parameter, see the :class:`~fields.Nested` constructor for more details. + +Use :class:`~fields.Nested` with :class:`~fields.List` to marshal lists of more complex objects: + +.. code-block:: python + + user_fields = api.model('User', { + 'id': fields.Integer, + 'name': fields.String, + }) + + user_list_fields = api.model('UserList', { + 'users': fields.List(fields.Nested(user_fields)), + }) + + +The ``api.model()`` factory +---------------------------- + +The :meth:`~Namespace.model` factory allows you to instantiate +and register models to your :class:`API` or :class:`Namespace`. + +.. code-block:: python + + my_fields = api.model('MyModel', { + 'name': fields.String, + 'age': fields.Integer(min=0) + }) + + # Equivalent to + my_fields = Model('MyModel', { + 'name': fields.String, + 'age': fields.Integer(min=0) + }) + api.models[my_fields.name] = my_fields + + +Duplicating with ``clone`` +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :meth:`Model.clone` method allows you to instantiate an augmented model. +It saves you duplicating all fields. + +.. code-block:: python + + parent = Model('Parent', { + 'name': fields.String + }) + + child = parent.clone('Child', { + 'age': fields.Integer + }) + +The :meth:`Api/Namespace.clone <~Namespace.clone>` also register it on the API. + +.. code-block:: python + + parent = api.model('Parent', { + 'name': fields.String + }) + + child = api.clone('Child', parent, { + 'age': fields.Integer + }) + + +Polymorphism with ``api.inherit`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :meth:`Model.inherit` method allows to extend a model in the "Swagger way" +and to start handling polymorphism. + +.. code-block:: python + + parent = api.model('Parent', { + 'name': fields.String, + 'class': fields.String(discriminator=True) + }) + + child = api.inherit('Child', parent, { + 'extra': fields.String + }) + +The :meth:`Api/Namespace.clone <~Namespace.clone>` will register both the parent and the child +in the Swagger models definitions. + +.. code-block:: python + + parent = Model('Parent', { + 'name': fields.String, + 'class': fields.String(discriminator=True) + }) + + child = parent.inherit('Child', { + 'extra': fields.String + }) + + +The ``class`` field in this example will be populated with the serialized model name +only if the property does not exists in the serialized object. + +The :class:`~fields.Polymorph` field allows you to specify a mapping between Python classes +and fields specifications. + +.. code-block:: python + + mapping = { + Child1: child1_fields, + Child2: child2_fields, + } + + fields = api.model('Thing', { + owner: fields.Polymorph(mapping) + }) + + +Custom fields +------------- + +Custom output fields let you perform your own output formatting without having +to modify your internal objects directly. +All you have to do is subclass :class:`~fields.Raw` and implement the :meth:`~fields.Raw.format` method: + +.. code-block:: python + + class AllCapsString(fields.Raw): + def format(self, value): + return value.upper() + + + # example usage + fields = { + 'name': fields.String, + 'all_caps_name': AllCapsString(attribute='name'), + } + +You can also use the :attr:`__schema_format__`, ``__schema_type__`` and +``__schema_example__`` to specify the produced types and examples: + +.. code-block:: python + + class MyIntField(fields.Integer): + __schema_format__ = 'int64' + + class MySpecialField(fields.Raw): + __schema_type__ = 'some-type' + __schema_format__ = 'some-format' + + class MyVerySpecialField(fields.Raw): + __schema_example__ = 'hello, world' + + +Skip fields which value is None +------------------------------- + +You can skip those fields which values is ``None`` instead of marshaling those fields with JSON value, null. +This feature is useful to reduce the size of response when you have a lots of fields which value may be None, +but which fields are ``None`` are unpredictable. + +Let consider the following example with an optional ``skip_none`` keyword argument be set to True. + +.. code-block:: python + + >>> from flask_restx import Model, fields, marshal_with + >>> import json + >>> model = Model('Model', { + ... 'name': fields.String, + ... 'address_1': fields.String, + ... 'address_2': fields.String + ... }) + >>> @marshal_with(model, skip_none=True) + ... def get(): + ... return {'name': 'John', 'address_1': None} + ... + >>> get() + OrderedDict([('name', 'John')]) + +You can see that ``address_1`` and ``address_2`` are skipped by :func:`marshal_with`. +``address_1`` be skipped because value is ``None``. +``address_2`` be skipped because the dictionary return by ``get()`` have no key, ``address_2``. + +Skip none in Nested fields +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If your module use :class:`fields.Nested`, you need to pass ``skip_none=True`` keyword argument to :class:`fields.Nested`. + +.. code-block:: python + + >>> from flask_restx import Model, fields, marshal_with + >>> import json + >>> model = Model('Model', { + ... 'name': fields.String, + ... 'location': fields.Nested(location_model, skip_none=True) + ... }) + + +Define model using JSON Schema +------------------------------ + +You can define models using `JSON Schema `_ (Draft v4). + +.. code-block:: python + + address = api.schema_model('Address', { + 'properties': { + 'road': { + 'type': 'string' + }, + }, + 'type': 'object' + }) + + person = api.schema_model('Person', { + 'required': ['address'], + 'properties': { + 'name': { + 'type': 'string' + }, + 'age': { + 'type': 'integer' + }, + 'birthdate': { + 'type': 'string', + 'format': 'date-time' + }, + 'address': { + '$ref': '#/definitions/Address', + } + }, + 'type': 'object' + }) diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/mask.rst b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/mask.rst new file mode 100644 index 0000000..836a4e1 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/mask.rst @@ -0,0 +1,106 @@ +Fields masks +============ + +Flask-RESTX support partial object fetching (aka. fields mask) +by supplying a custom header in the request. + +By default the header is ``X-Fields`` +but it can be changed with the ``RESTX_MASK_HEADER`` parameter. + +Syntax +------ + +The syntax is actually quite simple. +You just provide a coma separated list of field names, +optionally wrapped in brackets. + +.. code-block:: python + + # These two mask are equivalents + mask = '{name,age}' + # or + mask = 'name,age' + data = requests.get('/some/url/', headers={'X-Fields': mask}) + assert len(data) == 2 + assert 'name' in data + assert 'age' in data + +To specify a nested fields mask, +simply provide it in bracket following the field name: + +.. code-block:: python + + mask = '{name, age, pet{name}}' + +Nesting specification works with nested object or list of objects: + +.. code-block:: python + + # Will apply the mask {name} to each pet + # in the pets list. + mask = '{name, age, pets{name}}' + +There is a special star token meaning "all remaining fields". +It allows to only specify nested filtering: + +.. code-block:: python + + # Will apply the mask {name} to each pet + # in the pets list and take all other root fields + # without filtering. + mask = '{pets{name},*}' + + # Will not filter anything + mask = '*' + + +Usage +----- + +By default, each time you use ``api.marshal`` or ``@api.marshal_with``, +the mask will be automatically applied if the header is present. + +The header will be exposed as a Swagger parameter each time you use the +``@api.marshal_with`` decorator. + +As Swagger does not permit exposing a global header once +it can make your Swagger specifications a lot more verbose. +You can disable this behavior by setting ``RESTX_MASK_SWAGGER`` to ``False``. + +You can also specify a default mask that will be applied if no header mask is found. + +.. code-block:: python + + class MyResource(Resource): + @api.marshal_with(my_model, mask='name,age') + def get(self): + pass + + +Default mask can also be handled at model level: + +.. code-block:: python + + model = api.model('Person', { + 'name': fields.String, + 'age': fields.Integer, + 'boolean': fields.Boolean, + }, mask='{name,age}') + + +It will be exposed into the model `x-mask` vendor field: + +.. code-block:: JSON + + {"definitions": { + "Test": { + "properties": { + "age": {"type": "integer"}, + "boolean": {"type": "boolean"}, + "name": {"type": "string"} + }, + "x-mask": "{name,age}" + } + }} + +To override default masks, you need to give another mask or pass `*` as mask. diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/parsing.rst b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/parsing.rst new file mode 100644 index 0000000..d89b7e8 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/parsing.rst @@ -0,0 +1,340 @@ +.. _parsing: + +Request Parsing +=============== + +.. warning :: + + The whole request parser part of Flask-RESTX is slated for removal and + will be replaced by documentation on how to integrate with other packages + that do the input/output stuff better + (such as `marshmallow `_). + This means that it will be maintained until 2.0 but consider it deprecated. + Don't worry, if you have code using that now and wish to continue doing so, + it's not going to go away any time too soon. + +.. currentmodule:: flask_restx + +Flask-RESTX's request parsing interface, :mod:`reqparse`, +is modeled after the :mod:`python:argparse` interface. +It's designed to provide simple and uniform access to any variable on the +:class:`flask.request` object in Flask. + +Basic Arguments +--------------- + +Here's a simple example of the request parser. +It looks for two arguments in the :attr:`flask.Request.values` dict: an integer and a string + +.. code-block:: python + + from flask_restx import reqparse + + parser = reqparse.RequestParser() + parser.add_argument('rate', type=int, help='Rate cannot be converted') + parser.add_argument('name') + args = parser.parse_args() + +.. note :: + + The default argument type is a unicode string. + This will be ``str``. + +If you specify the ``help`` value, +it will be rendered as the error message when a type error is raised while parsing it. +If you do not specify a help message, +the default behavior is to return the message from the type error itself. +See :ref:`error-messages` for more details. + + +.. note :: + By default, arguments are **not** required. + Also, arguments supplied in the request that are not part of the :class:`~reqparse.RequestParser` will be ignored. + +.. note :: + Arguments declared in your request parser but not set in the request itself will default to ``None``. + +Required Arguments +------------------ + +To require a value be passed for an argument, +just add ``required=True`` to the call to :meth:`~reqparse.RequestParser.add_argument`. + +.. code-block:: python + + parser.add_argument('name', required=True, help="Name cannot be blank!") + + +Multiple Values & Lists +----------------------- + +If you want to accept multiple values for a key as a list, you can pass ``action='append'``: + +.. code-block:: python + + parser.add_argument('name', action='append') + +This will let you make queries like :: + + curl http://api.example.com -d "name=bob" -d "name=sue" -d "name=joe" + +And your args will look like this : + +.. code-block:: python + + args = parser.parse_args() + args['name'] # ['bob', 'sue', 'joe'] + +If you expect a comma-separated list, use the ``action='split'``: + +.. code-block:: python + + parser.add_argument('fruits', action='split') + +This will let you make queries like :: + + curl http://api.example.com -d "fruits=apple,lemon,cherry" + +And your args will look like this : + +.. code-block:: python + + args = parser.parse_args() + args['fruits'] # ['apple', 'lemon', 'cherry'] + +Other Destinations +------------------ + +If for some reason you'd like your argument stored under a different name once +it's parsed, you can use the ``dest`` keyword argument. :: + + parser.add_argument('name', dest='public_name') + + args = parser.parse_args() + args['public_name'] + +Argument Locations +------------------ + +By default, the :class:`~reqparse.RequestParser` tries to parse values from +:attr:`flask.Request.values`, and :attr:`flask.Request.json`. + +Use the ``location`` argument to :meth:`~reqparse.RequestParser.add_argument` +to specify alternate locations to pull the values from. Any variable on the +:class:`flask.Request` can be used. For example: :: + + # Look only in the POST body + parser.add_argument('name', type=int, location='form') + + # Look only in the querystring + parser.add_argument('PageSize', type=int, location='args') + + # From the request headers + parser.add_argument('User-Agent', location='headers') + + # From http cookies + parser.add_argument('session_id', location='cookies') + + # From file uploads + parser.add_argument('picture', type=werkzeug.datastructures.FileStorage, location='files') + +.. note :: + + Only use ``type=list`` when ``location='json'``. `See this issue for more + details `_ + +.. note :: + + Using ``location='form'`` is way to both validate form data and document your form fields. + + +Multiple Locations +------------------ + +Multiple argument locations can be specified by passing a list to ``location``:: + + parser.add_argument('text', location=['headers', 'values']) + + +When multiple locations are specified, the arguments from all locations +specified are combined into a single :class:`~werkzeug.datastructures.MultiDict`. +The last ``location`` listed takes precedence in the result set. + +If the argument location list includes the :attr:`~flask.Request.headers` +location the argument names will no longer be case insensitive and must match +their title case names (see :meth:`str.title`). Specifying +``location='headers'`` (not as a list) will retain case insensitivity. + +Advanced types handling +----------------------- + +Sometimes, you need more than a primitive type to handle input validation. +The :mod:`~flask_restx.inputs` module provides some common type handling like: + +- :func:`~inputs.boolean` for wider boolean handling +- :func:`~inputs.ipv4` and :func:`~inputs.ipv6` for IP adresses +- :func:`~inputs.date_from_iso8601` and :func:`~inputs.datetime_from_iso8601` for ISO8601 date and datetime handling + +You just have to use them as `type` argument: + +.. code-block:: python + + parser.add_argument('flag', type=inputs.boolean) + +See the :mod:`~flask_restx.inputs` documentation for full list of available inputs. + +You can also write your own: + +.. code-block:: python + + def my_type(value): + '''Parse my type''' + if not condition: + raise ValueError('This is not my type') + return parse(value) + + # Swagger documentation + my_type.__schema__ = {'type': 'string', 'format': 'my-custom-format'} + + +Parser Inheritance +------------------ + +Often you will make a different parser for each resource you write. The problem +with this is if parsers have arguments in common. Instead of rewriting +arguments you can write a parent parser containing all the shared arguments and +then extend the parser with :meth:`~reqparse.RequestParser.copy`. You can +also overwrite any argument in the parent with +:meth:`~reqparse.RequestParser.replace_argument`, or remove it completely +with :meth:`~reqparse.RequestParser.remove_argument`. For example: :: + + from flask_restx import reqparse + + parser = reqparse.RequestParser() + parser.add_argument('foo', type=int) + + parser_copy = parser.copy() + parser_copy.add_argument('bar', type=int) + + # parser_copy has both 'foo' and 'bar' + + parser_copy.replace_argument('foo', required=True, location='json') + # 'foo' is now a required str located in json, not an int as defined + # by original parser + + parser_copy.remove_argument('foo') + # parser_copy no longer has 'foo' argument + +File Upload +----------- + +To handle file upload with the :class:`~reqparse.RequestParser`, +you need to use the `files` location +and to set the type to :class:`~werkzeug.datastructures.FileStorage`. + +.. code-block:: python + + from werkzeug.datastructures import FileStorage + + upload_parser = api.parser() + upload_parser.add_argument('file', location='files', + type=FileStorage, required=True) + + + @api.route('/upload/') + @api.expect(upload_parser) + class Upload(Resource): + def post(self): + args = upload_parser.parse_args() + uploaded_file = args['file'] # This is FileStorage instance + url = do_something_with_file(uploaded_file) + return {'url': url}, 201 + +See the `dedicated Flask documentation section `_. + + +Error Handling +-------------- + +The default way errors are handled by the RequestParser is to abort on the +first error that occurred. This can be beneficial when you have arguments that +might take some time to process. However, often it is nice to have the errors +bundled together and sent back to the client all at once. This behavior can be +specified either at the Flask application level or on the specific +RequestParser instance. To invoke a RequestParser with the bundling errors +option, pass in the argument ``bundle_errors``. For example :: + + from flask_restx import reqparse + + parser = reqparse.RequestParser(bundle_errors=True) + parser.add_argument('foo', type=int, required=True) + parser.add_argument('bar', type=int, required=True) + + # If a request comes in not containing both 'foo' and 'bar', the error that + # will come back will look something like this. + + { + "message": { + "foo": "foo error message", + "bar": "bar error message" + } + } + + # The default behavior would only return the first error + + parser = RequestParser() + parser.add_argument('foo', type=int, required=True) + parser.add_argument('bar', type=int, required=True) + + { + "message": { + "foo": "foo error message" + } + } + +The application configuration key is "BUNDLE_ERRORS". For example :: + + from flask import Flask + + app = Flask(__name__) + app.config['BUNDLE_ERRORS'] = True + +.. warning :: + + ``BUNDLE_ERRORS`` is a global setting that overrides the ``bundle_errors`` + option in individual :class:`~reqparse.RequestParser` instances. + + +.. _error-messages: + +Error Messages +-------------- + +Error messages for each field may be customized using the ``help`` parameter +to ``Argument`` (and also ``RequestParser.add_argument``). + +If no help parameter is provided, the error message for the field will be +the string representation of the type error itself. If ``help`` is provided, +then the error message will be the value of ``help``. + +``help`` may include an interpolation token, ``{error_msg}``, that will be +replaced with the string representation of the type error. This allows the +message to be customized while preserving the original error:: + + from flask_restx import reqparse + + + parser = reqparse.RequestParser() + parser.add_argument( + 'foo', + choices=('one', 'two'), + help='Bad choice: {error_msg}' + ) + + # If a request comes in with a value of "three" for `foo`: + + { + "message": { + "foo": "Bad choice: three is not a valid choice", + } + } diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/postman.rst b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/postman.rst new file mode 100644 index 0000000..c6f994e --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/postman.rst @@ -0,0 +1,18 @@ +Postman +======= + +To help you testing, you can export your API as a `Postman`_ collection. + +.. code-block:: python + + from flask import json + + from myapp import api + + urlvars = False # Build query strings in URLs + swagger = True # Export Swagger specifications + data = api.as_postman(urlvars=urlvars, swagger=swagger) + print(json.dumps(data)) + + +.. _Postman: https://www.getpostman.com/ diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/quickstart.rst b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/quickstart.rst new file mode 100644 index 0000000..d3018ce --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/quickstart.rst @@ -0,0 +1,344 @@ +.. _quickstart: + +Quick start +=========== + +.. currentmodule:: flask_restx + +This guide assumes you have a working understanding of `Flask `_, +and that you have already installed both Flask and Flask-RESTX. +If not, then follow the steps in the :ref:`installation` section. + + +Migrate from Flask-RESTPlus +--------------------------- + +.. warning:: The *migration* commands provided below are for illustration + purposes. + You may need to adapt them to properly fit your needs. + We also recommend you make a backup of your project prior running them. + +At this point, Flask-RESTX remains 100% compatible with Flask-RESTPlus' API. +All you need to do is update your requirements to use Flask-RESTX instead of +Flask-RESTPlus. Then you need to update all your imports. +This can be done using something like: + +.. code-block:: bash + + find . -type f -name "*.py" | xargs sed -i "s/flask_restplus/flask_restx/g" + +Finally, you will need to update your configuration options (described `here +`_). Example: + +.. code-block:: bash + + find . -type f -name "*.py" | xargs sed -i "s/RESTPLUS_/RESTX_/g" + + +Initialization +-------------- + +As every other extension, you can initialize it with an application object: + +.. code-block:: python + + from flask import Flask + from flask_restx import Api + + app = Flask(__name__) + api = Api(app) + +or lazily with the factory pattern: + +.. code-block:: python + + from flask import Flask + from flask_restx import Api + + api = Api() + + app = Flask(__name__) + api.init_app(app) + + +A Minimal API +------------- + +A minimal Flask-RESTX API looks like this: + +.. code-block:: python + + from flask import Flask + from flask_restx import Resource, Api + + app = Flask(__name__) + api = Api(app) + + @api.route('/hello') + class HelloWorld(Resource): + def get(self): + return {'hello': 'world'} + + if __name__ == '__main__': + app.run(debug=True) + + +Save this as api.py and run it using your Python interpreter. +Note that we've enabled `Flask debugging `_ +mode to provide code reloading and better error messages. + +.. code-block:: console + + $ python api.py + * Running on http://127.0.0.1:5000/ + * Restarting with reloader + + +.. warning:: + + Debug mode should never be used in a production environment! + +Now open up a new prompt to test out your API using curl: + +.. code-block:: console + + $ curl http://127.0.0.1:5000/hello + {"hello": "world"} + + +You can also use the automatic documentation on you API root (by default). +In this case: http://127.0.0.1:5000/. +See :ref:`swaggerui` for a complete documentation on the automatic documentation. + +.. note :: + Initializing the :class:`~Api` object always registers the root endpoint ``/`` + even if the :ref:`swaggerui` path is changed. If you wish to use the root + endpoint ``/`` for other purposes, you must register it before initializing + the :class:`~Api` object. + + +Resourceful Routing +------------------- +The main building block provided by Flask-RESTX are resources. +Resources are built on top of :doc:`Flask pluggable views `, +giving you easy access to multiple HTTP methods just by defining methods on your resource. +A basic CRUD resource for a todo application (of course) looks like this: + +.. code-block:: python + + from flask import Flask, request + from flask_restx import Resource, Api + + app = Flask(__name__) + api = Api(app) + + todos = {} + + @api.route('/') + class TodoSimple(Resource): + def get(self, todo_id): + return {todo_id: todos[todo_id]} + + def put(self, todo_id): + todos[todo_id] = request.form['data'] + return {todo_id: todos[todo_id]} + + if __name__ == '__main__': + app.run(debug=True) + +You can try it like this: + +.. code-block:: console + + $ curl http://localhost:5000/todo1 -d "data=Remember the milk" -X PUT + {"todo1": "Remember the milk"} + $ curl http://localhost:5000/todo1 + {"todo1": "Remember the milk"} + $ curl http://localhost:5000/todo2 -d "data=Change my brakepads" -X PUT + {"todo2": "Change my brakepads"} + $ curl http://localhost:5000/todo2 + {"todo2": "Change my brakepads"} + + +Or from python if you have the `Requests `_ library installed: + +.. code-block:: python + + >>> from requests import put, get + >>> put('http://localhost:5000/todo1', data={'data': 'Remember the milk'}).json() + {u'todo1': u'Remember the milk'} + >>> get('http://localhost:5000/todo1').json() + {u'todo1': u'Remember the milk'} + >>> put('http://localhost:5000/todo2', data={'data': 'Change my brakepads'}).json() + {u'todo2': u'Change my brakepads'} + >>> get('http://localhost:5000/todo2').json() + {u'todo2': u'Change my brakepads'} + +Flask-RESTX understands multiple kinds of return values from view methods. +Similar to Flask, you can return any iterable and it will be converted into a response, +including raw Flask response objects. +Flask-RESTX also support setting the response code and response headers using multiple return values, +as shown below: + +.. code-block:: python + + class Todo1(Resource): + def get(self): + # Default to 200 OK + return {'task': 'Hello world'} + + class Todo2(Resource): + def get(self): + # Set the response code to 201 + return {'task': 'Hello world'}, 201 + + class Todo3(Resource): + def get(self): + # Set the response code to 201 and return custom headers + return {'task': 'Hello world'}, 201, {'Etag': 'some-opaque-string'} + + +Endpoints +--------- + +Many times in an API, your resource will have multiple URLs. +You can pass multiple URLs to the :meth:`~Api.add_resource` method or to the :meth:`~Api.route` decorator, +both on the :class:`~Api` object. +Each one will be routed to your :class:`~Resource`: + +.. code-block:: python + + api.add_resource(HelloWorld, '/hello', '/world') + + # or + + @api.route('/hello', '/world') + class HelloWorld(Resource): + pass + +You can also match parts of the path as variables to your resource methods. + +.. code-block:: python + + api.add_resource(Todo, '/todo/', endpoint='todo_ep') + + # or + + @api.route('/todo/', endpoint='todo_ep') + class HelloWorld(Resource): + pass + +.. note :: + + If a request does not match any of your application's endpoints, + Flask-RESTX will return a 404 error message with suggestions of other + endpoints that closely match the requested endpoint. + This can be disabled by setting ``RESTX_ERROR_404_HELP`` to ``False`` in your application config. + + +Argument Parsing +---------------- + +While Flask provides easy access to request data (i.e. querystring or POST form encoded data), +it's still a pain to validate form data. +Flask-RESTX has built-in support for request data validation +using a library similar to :mod:`python:argparse`. + +.. code-block:: python + + from flask_restx import reqparse + + parser = reqparse.RequestParser() + parser.add_argument('rate', type=int, help='Rate to charge for this resource') + args = parser.parse_args() + +.. note :: + + Unlike the :mod:`python:argparse` module, :meth:`~reqparse.RequestParser.parse_args` + returns a Python dictionary instead of a custom data structure. + +Using the :class:`~reqparse.RequestParser` class also gives you same error messages for free. +If an argument fails to pass validation, +Flask-RESTX will respond with a 400 Bad Request and a response highlighting the error. + +.. code-block:: console + + $ curl -d 'rate=foo' http://127.0.0.1:5000/todos + {'status': 400, 'message': 'foo cannot be converted to int'} + + +The :mod:`~inputs` module provides a number of included common conversion +functions such as :func:`~inputs.date` and :func:`~inputs.url`. + +Calling :meth:`~reqparse.RequestParser.parse_args` with ``strict=True`` ensures that an error is thrown if +the request includes arguments your parser does not define. + +.. code-block:: python + + args = parser.parse_args(strict=True) + + +Data Formatting +--------------- + +By default, all fields in your return iterable will be rendered as-is. +While this works great when you're just dealing with Python data structures, +it can become very frustrating when working with objects. +To solve this problem, Flask-RESTX provides the :mod:`fields` module and the +:meth:`marshal_with` decorator. +Similar to the Django ORM and WTForm, +you use the ``fields`` module to describe the structure of your response. + +.. code-block:: python + + from flask import Flask + from flask_restx import fields, Api, Resource + + app = Flask(__name__) + api = Api(app) + + model = api.model('Model', { + 'task': fields.String, + 'uri': fields.Url('todo_ep') + }) + + class TodoDao(object): + def __init__(self, todo_id, task): + self.todo_id = todo_id + self.task = task + + # This field will not be sent in the response + self.status = 'active' + + @api.route('/todo') + class Todo(Resource): + @api.marshal_with(model) + def get(self, **kwargs): + return TodoDao(todo_id='my_todo', task='Remember the milk') + + +The above example takes a python object and prepares it to be serialized. +The :meth:`~Api.marshal_with` decorator will apply the transformation described by ``model``. +The only field extracted from the object is ``task``. +The :class:`fields.Url` field is a special field that takes an endpoint name +and generates a URL for that endpoint in the response. +Using the :meth:`~Api.marshal_with` decorator also document the output in the swagger specifications. +Many of the field types you need are already included. +See the :mod:`fields` guide for a complete list. + +Order Preservation +~~~~~~~~~~~~~~~~~~ + +By default, fields order is not preserved as this have a performance drop effect. +If you still require fields order preservation, you can pass a ``ordered=True`` +parameter to some classes or function to force order preservation: + +- globally on :class:`Api`: ``api = Api(ordered=True)`` +- globally on :class:`Namespace`: ``ns = Namespace(ordered=True)`` +- locally on :func:`marshal`: ``return marshal(data, fields, ordered=True)`` + + +Full example +------------ + +See the :doc:`example` section for fully functional example. diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/scaling.rst b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/scaling.rst new file mode 100644 index 0000000..7b390ba --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/scaling.rst @@ -0,0 +1,275 @@ +.. _scaling: + +Scaling your project +==================== + +.. currentmodule:: flask_restx + +This page covers building a slightly more complex Flask-RESTX app that will +cover out some best practices when setting up a real-world Flask-RESTX-based API. +The :ref:`quickstart` section is great for getting started with your first Flask-RESTX app, +so if you're new to Flask-RESTX you'd be better off checking that out first. + + +Multiple namespaces +------------------- + +There are many different ways to organize your Flask-RESTX app, +but here we'll describe one that scales pretty well with larger apps +and maintains a nice level of organization. + +Flask-RESTX provides a way to use almost the same pattern as Flask's `blueprint`. +The main idea is to split your app into reusable namespaces. + +Here's an example directory structure:: + + project/ + ├── app.py + ├── core + │   ├── __init__.py + │   ├── utils.py + │   └── ... + └── apis + ├── __init__.py + ├── namespace1.py + ├── namespace2.py + ├── ... + └── namespaceX.py + + +The `app` module will serve as a main application entry point following one of the classic +Flask patterns (See :doc:`flask:patterns/packages` and :doc:`flask:patterns/appfactories`). + +The `core` module is an example, it contains the business logic. +In fact, you call it whatever you want, and there can be many packages. + +The `apis` package will be your main API entry point that you need to import and register on the application, +whereas the namespaces modules are reusable namespaces designed like you would do with Flask's Blueprint. + +A namespace module contains models and resources declarations. +For example: + +.. code-block:: Python + + from flask_restx import Namespace, Resource, fields + + api = Namespace('cats', description='Cats related operations') + + cat = api.model('Cat', { + 'id': fields.String(required=True, description='The cat identifier'), + 'name': fields.String(required=True, description='The cat name'), + }) + + CATS = [ + {'id': 'felix', 'name': 'Felix'}, + ] + + @api.route('/') + class CatList(Resource): + @api.doc('list_cats') + @api.marshal_list_with(cat) + def get(self): + '''List all cats''' + return CATS + + @api.route('/') + @api.param('id', 'The cat identifier') + @api.response(404, 'Cat not found') + class Cat(Resource): + @api.doc('get_cat') + @api.marshal_with(cat) + def get(self, id): + '''Fetch a cat given its identifier''' + for cat in CATS: + if cat['id'] == id: + return cat + api.abort(404) + + +The `apis.__init__` module should aggregate them: + +.. code-block:: Python + + from flask_restx import Api + + from .namespace1 import api as ns1 + from .namespace2 import api as ns2 + # ... + from .namespaceX import api as nsX + + api = Api( + title='My Title', + version='1.0', + description='A description', + # All API metadatas + ) + + api.add_namespace(ns1) + api.add_namespace(ns2) + # ... + api.add_namespace(nsX) + + +You can define custom url-prefixes for namespaces during registering them in your API. +You don't have to bind url-prefix while declaration of Namespace object. + +.. code-block:: Python + + from flask_restx import Api + + from .namespace1 import api as ns1 + from .namespace2 import api as ns2 + # ... + from .namespaceX import api as nsX + + api = Api( + title='My Title', + version='1.0', + description='A description', + # All API metadatas + ) + + api.add_namespace(ns1, path='/prefix/of/ns1') + api.add_namespace(ns2, path='/prefix/of/ns2') + # ... + api.add_namespace(nsX, path='/prefix/of/nsX') + + +Using this pattern, you simply have to register your API in `app.py` like that: + +.. code-block:: Python + + from flask import Flask + from apis import api + + app = Flask(__name__) + api.init_app(app) + + app.run(debug=True) + + +Use With Blueprints +------------------- + +See :doc:`flask:blueprints` in the Flask documentation for what blueprints are and why you should use them. +Here's an example of how to link an :class:`Api` up to a :class:`~flask.Blueprint`. Nested Blueprints are +not supported. + +.. code-block:: python + + from flask import Blueprint + from flask_restx import Api + + blueprint = Blueprint('api', __name__) + api = Api(blueprint) + # ... + +Using a `blueprint` will allow you to mount your API on any url prefix and/or subdomain +in you application: + + +.. code-block:: Python + + from flask import Flask + from apis import blueprint as api + + app = Flask(__name__) + app.register_blueprint(api, url_prefix='/api/1') + app.run(debug=True) + +.. note :: + + Calling :meth:`Api.init_app` is not required here because registering the + blueprint with the app takes care of setting up the routing for the application. + +.. note:: + + When using blueprints, remember to use the blueprint name with :func:`~flask.url_for`: + + .. code-block:: python + + # without blueprint + url_for('my_api_endpoint') + + # with blueprint + url_for('api.my_api_endpoint') + + +Multiple APIs with reusable namespaces +-------------------------------------- + +Sometimes you need to maintain multiple versions of an API. +If you built your API using namespaces composition, +it's quite simple to scale it to multiple APIs. + +Given the previous layout, we can migrate it to the following directory structure:: + + project/ + ├── app.py + ├── apiv1.py + ├── apiv2.py + └── apis + ├── __init__.py + ├── namespace1.py + ├── namespace2.py + ├── ... + └── namespaceX.py + +Each `apis/namespaceX` module will have the following pattern: + +.. code-block:: python + + from flask_restx import Namespace, Resource + + api = Namespace('mynamespace', 'Namespace Description' ) + + @api.route("/") + class Myclass(Resource): + def get(self): + return {} + +Each `apivX` module will have the following pattern: + +.. code-block:: python + + from flask import Blueprint + from flask_restx import Api + + api = Api(blueprint) + + from .apis.namespace1 import api as ns1 + from .apis.namespace2 import api as ns2 + # ... + from .apis.namespaceX import api as nsX + + blueprint = Blueprint('api', __name__, url_prefix='/api/1') + api = Api(blueprint + title='My Title', + version='1.0', + description='A description', + # All API metadatas + ) + + api.add_namespace(ns1) + api.add_namespace(ns2) + # ... + api.add_namespace(nsX) + +And the app will simply mount them: + +.. code-block:: Python + + from flask import Flask + from api1 import blueprint as api1 + from apiX import blueprint as apiX + + app = Flask(__name__) + app.register_blueprint(api1) + app.register_blueprint(apiX) + app.run(debug=True) + + +These are only proposals and you can do whatever suits your needs. +Look at the `github repository examples folder`_ for more complete examples. + +.. _github repository examples folder: https://github.com/python-restx/flask-restx/tree/master/examples diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/swagger.rst b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/swagger.rst new file mode 100644 index 0000000..62a3854 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/doc/swagger.rst @@ -0,0 +1,1070 @@ +.. _swagger: + +Swagger documentation +===================== + + +.. currentmodule:: flask_restx + +Swagger API documentation is automatically generated and available from your API's root URL. You can configure the documentation using the :meth:`@api.doc() ` decorator. + + +Documenting with the ``@api.doc()`` decorator +--------------------------------------------- + +The ``api.doc()`` decorator allows you to include additional information in the documentation. + +You can document a class or a method: + +.. code-block:: python + + @api.route('/my-resource/', endpoint='my-resource') + @api.doc(params={'id': 'An ID'}) + class MyResource(Resource): + def get(self, id): + return {} + + @api.doc(responses={403: 'Not Authorized'}) + def post(self, id): + api.abort(403) + + +Automatically documented models +------------------------------- + +All models instantiated with :meth:`~Namespace.model`, :meth:`~Namespace.clone` and :meth:`~Namespace.inherit` +will be automatically documented in your Swagger specifications. + +The :meth:`~Namespace.inherit` method will register both the parent and the child in the Swagger models definitions: + +.. code-block:: python + + parent = api.model('Parent', { + 'name': fields.String, + 'class': fields.String(discriminator=True) + }) + + child = api.inherit('Child', parent, { + 'extra': fields.String + }) + +The above configuration will produce these Swagger definitions: + +.. code-block:: json + + { + "Parent": { + "properties": { + "name": {"type": "string"}, + "class": {"type": "string"} + }, + "discriminator": "class", + "required": ["class"] + }, + "Child": { + "allOf": [ + { + "$ref": "#/definitions/Parent" + }, { + "properties": { + "extra": {"type": "string"} + } + } + ] + } + } + + +The ``@api.marshal_with()`` decorator +------------------------------------- + +This decorator works like the raw :func:`marshal_with` decorator +with the difference that it documents the methods. +The optional parameter ``code`` allows you to specify the expected HTTP status code (200 by default). +The optional parameter ``as_list`` allows you to specify whether or not the objects are returned as a list. + +.. code-block:: python + + resource_fields = api.model('Resource', { + 'name': fields.String, + }) + + @api.route('/my-resource/', endpoint='my-resource') + class MyResource(Resource): + @api.marshal_with(resource_fields, as_list=True) + def get(self): + return get_objects() + + @api.marshal_with(resource_fields, code=201) + def post(self): + return create_object(), 201 + + +The :meth:`Api.marshal_list_with` decorator is strictly equivalent to :meth:`Api.marshal_with(fields, as_list=True)`. + +.. code-block:: python + + resource_fields = api.model('Resource', { + 'name': fields.String, + }) + + @api.route('/my-resource/', endpoint='my-resource') + class MyResource(Resource): + @api.marshal_list_with(resource_fields) + def get(self): + return get_objects() + + @api.marshal_with(resource_fields) + def post(self): + return create_object() + + +The ``@api.expect()`` decorator +------------------------------- + +The ``@api.expect()`` decorator allows you to specify the expected input fields. +It accepts an optional boolean parameter ``validate`` indicating whether the payload should be validated. +The validation behavior can be customized globally either +by setting the ``RESTX_VALIDATE`` configuration to ``True`` +or passing ``validate=True`` to the API constructor. + +The following examples are equivalent: + +* Using the ``@api.expect()`` decorator: + +.. code-block:: python + + resource_fields = api.model('Resource', { + 'name': fields.String, + }) + + @api.route('/my-resource/') + class MyResource(Resource): + @api.expect(resource_fields) + def get(self): + pass + +* Using the ``api.doc()`` decorator: + +.. code-block:: python + + resource_fields = api.model('Resource', { + 'name': fields.String, + }) + + @api.route('/my-resource/') + class MyResource(Resource): + @api.doc(body=resource_fields) + def get(self): + pass + + +You can specify lists as the expected input: + +.. code-block:: python + + resource_fields = api.model('Resource', { + 'name': fields.String, + }) + + @api.route('/my-resource/') + class MyResource(Resource): + @api.expect([resource_fields]) + def get(self): + pass + + +You can use :exc:`~flask_restx.reqparse.RequestParser` to define the expected input: + +.. code-block:: python + + parser = api.parser() + parser.add_argument('param', type=int, help='Some param', location='form') + parser.add_argument('in_files', type=FileStorage, location='files') + + + @api.route('/with-parser/', endpoint='with-parser') + class WithParserResource(restx.Resource): + @api.expect(parser) + def get(self): + return {} + + +Validation can be enabled or disabled on a particular endpoint: + +.. code-block:: python + + resource_fields = api.model('Resource', { + 'name': fields.String, + }) + + @api.route('/my-resource/') + class MyResource(Resource): + # Payload validation disabled + @api.expect(resource_fields) + def post(self): + pass + + # Payload validation enabled + @api.expect(resource_fields, validate=True) + def post(self): + pass + + +An example of application-wide validation by config: + +.. code-block:: python + + app.config['RESTX_VALIDATE'] = True + + api = Api(app) + + resource_fields = api.model('Resource', { + 'name': fields.String, + }) + + @api.route('/my-resource/') + class MyResource(Resource): + # Payload validation enabled + @api.expect(resource_fields) + def post(self): + pass + + # Payload validation disabled + @api.expect(resource_fields, validate=False) + def post(self): + pass + + +An example of application-wide validation by constructor: + +.. code-block:: python + + api = Api(app, validate=True) + + resource_fields = api.model('Resource', { + 'name': fields.String, + }) + + @api.route('/my-resource/') + class MyResource(Resource): + # Payload validation enabled + @api.expect(resource_fields) + def post(self): + pass + + # Payload validation disabled + @api.expect(resource_fields, validate=False) + def post(self): + pass + + +Documenting with the ``@api.response()`` decorator +-------------------------------------------------- + +The ``@api.response()`` decorator allows you to document the known responses +and is a shortcut for ``@api.doc(responses='...')``. + +The following two definitions are equivalent: + +.. code-block:: python + + @api.route('/my-resource/') + class MyResource(Resource): + @api.response(200, 'Success') + @api.response(400, 'Validation Error') + def get(self): + pass + + + @api.route('/my-resource/') + class MyResource(Resource): + @api.doc(responses={ + 200: 'Success', + 400: 'Validation Error' + }) + def get(self): + pass + +You can optionally specify a response model as the third argument: + + +.. code-block:: python + + model = api.model('Model', { + 'name': fields.String, + }) + + @api.route('/my-resource/') + class MyResource(Resource): + @api.response(200, 'Success', model) + def get(self): + pass + +The ``@api.marshal_with()`` decorator automatically documents the response: + +.. code-block:: python + + model = api.model('Model', { + 'name': fields.String, + }) + + @api.route('/my-resource/') + class MyResource(Resource): + @api.response(400, 'Validation error') + @api.marshal_with(model, code=201, description='Object created') + def post(self): + pass + +You can specify a default response sent without knowing the response code: + +.. code-block:: python + + @api.route('/my-resource/') + class MyResource(Resource): + @api.response('default', 'Error') + def get(self): + pass + + +The ``@api.route()`` decorator +------------------------------ + +You can provide class-wide documentation using the ``doc`` parameter of ``Api.route()``. This parameter accepts the same values as the ``Api.doc()`` decorator. + +For example, these two declarations are equivalent: + + +* Using ``@api.doc()``: + +.. code-block:: python + + @api.route('/my-resource/', endpoint='my-resource') + @api.doc(params={'id': 'An ID'}) + class MyResource(Resource): + def get(self, id): + return {} + + +* Using ``@api.route()``: + +.. code-block:: python + + @api.route('/my-resource/', endpoint='my-resource', doc={'params':{'id': 'An ID'}}) + class MyResource(Resource): + def get(self, id): + return {} + +Multiple Routes per Resource +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Multiple ``Api.route()`` decorators can be used to add multiple routes for a ``Resource``. +The ``doc`` parameter provides documentation **per route**. + +For example, here the ``description`` is applied only to the ``/also-my-resource/`` route: + +.. code-block:: python + + @api.route("/my-resource/") + @api.route( + "/also-my-resource/", + doc={"description": "Alias for /my-resource/"}, + ) + class MyResource(Resource): + def get(self, id): + return {} + +Here, the ``/also-my-resource/`` route is marked as deprecated: + +.. code-block:: python + + @api.route("/my-resource/") + @api.route( + "/also-my-resource/", + doc={ + "description": "Alias for /my-resource/, this route is being phased out in V2", + "deprecated": True, + }, + ) + class MyResource(Resource): + def get(self, id): + return {} + +Documentation applied to the ``Resource`` using ``Api.doc()`` is `shared` amongst all +routes unless explicitly overridden: + +.. code-block:: python + + @api.route("/my-resource/") + @api.route( + "/also-my-resource/", + doc={"description": "Alias for /my-resource/"}, + ) + @api.doc(params={"id": "An ID", description="My resource"}) + class MyResource(Resource): + def get(self, id): + return {} + +Here, the ``id`` documentation from the ``@api.doc()`` decorator is present in both routes, +``/my-resource/`` inherits the ``My resource`` description from the ``@api.doc()`` +decorator and ``/also-my-resource/`` overrides the description with ``Alias for /my-resource/``. + +Routes with a ``doc`` parameter are given a `unique` Swagger ``operationId``. Routes without +``doc`` parameter have the same Swagger ``operationId`` as they are deemed the same operation. + + +Documenting the fields +---------------------- + +Every Flask-RESTX field accepts optional arguments used to document the field: + +- ``required``: a boolean indicating if the field is always set (*default*: ``False``) +- ``description``: some details about the field (*default*: ``None``) +- ``example``: an example to use when displaying (*default*: ``None``) + +There are also field-specific attributes: + +* The ``String`` field accepts the following optional arguments: + - ``enum``: an array restricting the authorized values. + - ``min_length``: the minimum length expected. + - ``max_length``: the maximum length expected. + - ``pattern``: a RegExp pattern used to validate the string. + +* The ``Integer``, ``Float`` and ``Arbitrary`` fields accept the following optional arguments: + - ``min``: restrict the minimum accepted value. + - ``max``: restrict the maximum accepted value. + - ``exclusiveMin``: if ``True``, minimum value is not in allowed interval. + - ``exclusiveMax``: if ``True``, maximum value is not in allowed interval. + - ``multiple``: specify that the number must be a multiple of this value. + +* The ``DateTime`` field accepts the ``min``, ``max``, ``exclusiveMin`` and ``exclusiveMax`` optional arguments. These should be dates or datetimes (either ISO strings or native objects). + +.. code-block:: python + + my_fields = api.model('MyModel', { + 'name': fields.String(description='The name', required=True), + 'type': fields.String(description='The object type', enum=['A', 'B']), + 'age': fields.Integer(min=0), + }) + + +Documenting the methods +----------------------- + +Each resource will be documented as a Swagger path. + +Each resource method (``get``, ``post``, ``put``, ``delete``, ``path``, ``options``, ``head``) +will be documented as a Swagger operation. + +You can specify a unique Swagger ``operationId`` with the ``id`` keyword argument: + +.. code-block:: python + + @api.route('/my-resource/') + class MyResource(Resource): + @api.doc(id='get_something') + def get(self): + return {} + +You can also use the first argument for the same purpose: + +.. code-block:: python + + @api.route('/my-resource/') + class MyResource(Resource): + @api.doc('get_something') + def get(self): + return {} + +If not specified, a default ``operationId`` is provided with the following pattern:: + + {{verb}}_{{resource class name | camelCase2dashes }} + +In the previous example, the default generated ``operationId`` would be ``get_my_resource``. + + +You can override the default ``operationId`` generator by providing a callable for the ``default_id`` parameter. +This callable accepts two positional arguments: + +* The resource class name +* The HTTP method (lower-case) + +.. code-block:: python + + def default_id(resource, method): + return ''.join((method, resource)) + + api = Api(app, default_id=default_id) + +In the previous example, the generated ``operationId`` would be ``getMyResource``. + + +Each operation will automatically receive the namespace tag. +If the resource is attached to the root API, it will receive the default namespace tag. + + +Method parameters +~~~~~~~~~~~~~~~~~ + +Parameters from the URL path are documented automatically. +You can provide additional information using the ``params`` keyword argument of the ``api.doc()`` decorator: + +.. code-block:: python + + @api.route('/my-resource/', endpoint='my-resource') + @api.doc(params={'id': 'An ID'}) + class MyResource(Resource): + pass + +or by using the ``api.param`` shortcut decorator: + +.. code-block:: python + + @api.route('/my-resource/', endpoint='my-resource') + @api.param('id', 'An ID') + class MyResource(Resource): + pass + + +Input and output models +~~~~~~~~~~~~~~~~~~~~~~~ + +You can specify the serialized output model using the ``model`` keyword argument of the ``api.doc()`` decorator. + +For ``POST`` and ``PUT`` methods, use the ``body`` keyword argument to specify the input model. + + +.. code-block:: python + + my_model = api.model('MyModel', { + 'name': fields.String(description='The name', required=True), + 'type': fields.String(description='The object type', enum=['A', 'B']), + 'age': fields.Integer(min=0), + }) + + + class Person(fields.Raw): + def format(self, value): + return {'name': value.name, 'age': value.age} + + + @api.route('/my-resource/', endpoint='my-resource') + @api.doc(params={'id': 'An ID'}) + class MyResource(Resource): + @api.doc(model=my_model) + def get(self, id): + return {} + + @api.doc(model=my_model, body=Person) + def post(self, id): + return {} + + +If both ``body`` and ``formData`` parameters are used, a :exc:`~flask_restx.errors.SpecsError` will be raised. + +Models can also be specified with a :class:`~flask_restx.reqparse.RequestParser`. + +.. code-block:: python + + parser = api.parser() + parser.add_argument('param', type=int, help='Some param', location='form') + parser.add_argument('in_files', type=FileStorage, location='files') + + @api.route('/with-parser/', endpoint='with-parser') + class WithParserResource(restx.Resource): + @api.expect(parser) + def get(self): + return {} + + +.. note:: The decoded payload will be available as a dictionary in the payload attribute + in the request context. + + .. code-block:: python + + @api.route('/my-resource/') + class MyResource(Resource): + def get(self): + data = api.payload + +.. note:: + + Using :class:`~flask_restx.reqparse.RequestParser` is preferred over the ``api.param()`` decorator + to document form fields as it also perform validation. + +Headers +~~~~~~~ + +You can document response headers with the ``@api.header()`` decorator shortcut. + +.. code-block:: python + + @api.route('/with-headers/') + @api.header('X-Header', 'Some class header') + class WithHeaderResource(restx.Resource): + @api.header('X-Collection', type=[str], collectionType='csv') + def get(self): + pass + +If you need to specify an header that appear only on a given response, +just use the `@api.response` `headers` parameter. + +.. code-block:: python + + @api.route('/response-headers/') + class WithHeaderResource(restx.Resource): + @api.response(200, 'Success', headers={'X-Header': 'Some header'}) + def get(self): + pass + + +Documenting expected/request headers is done through the `@api.expect` decorator + +.. code-block:: python + + parser = api.parser() + parser.add_argument('Some-Header', location='headers') + + @api.route('/expect-headers/') + @api.expect(parser) + class ExpectHeaderResource(restx.Resource): + def get(self): + pass + +Cascading +--------- + +Method documentation takes precedence over class documentation, +and inherited documentation takes precedence over parent documentation. + +For example, these two declarations are equivalent: + +* Class documentation is inherited by methods: + +.. code-block:: python + + @api.route('/my-resource/', endpoint='my-resource') + @api.params('id', 'An ID') + class MyResource(Resource): + def get(self, id): + return {} + + +* Class documentation is overridden by method-specific documentation: + +.. code-block:: python + + @api.route('/my-resource/', endpoint='my-resource') + @api.param('id', 'Class-wide description') + class MyResource(Resource): + @api.param('id', 'An ID') + def get(self, id): + return {} + +You can also provide method-specific documentation from a class decorator. +The following example will produce the same documentation as the two previous examples: + +.. code-block:: python + + @api.route('/my-resource/', endpoint='my-resource') + @api.params('id', 'Class-wide description') + @api.doc(get={'params': {'id': 'An ID'}}) + class MyResource(Resource): + def get(self, id): + return {} + + +Marking as deprecated +--------------------- + +You can mark resources or methods as deprecated with the ``@api.deprecated`` decorator: + +.. code-block:: python + + # Deprecate the full resource + @api.deprecated + @api.route('/resource1/') + class Resource1(Resource): + def get(self): + return {} + + # Deprecate methods + @api.route('/resource4/') + class Resource4(Resource): + def get(self): + return {} + + @api.deprecated + def post(self): + return {} + + def put(self): + return {} + + + +Hiding from documentation +------------------------- + +You can hide some resources or methods from documentation using any of the following: + +.. code-block:: python + + # Hide the full resource + @api.route('/resource1/', doc=False) + class Resource1(Resource): + def get(self): + return {} + + @api.route('/resource2/') + @api.doc(False) + class Resource2(Resource): + def get(self): + return {} + + @api.route('/resource3/') + @api.hide + class Resource3(Resource): + def get(self): + return {} + + # Hide methods + @api.route('/resource4/') + @api.doc(delete=False) + class Resource4(Resource): + def get(self): + return {} + + @api.doc(False) + def post(self): + return {} + + @api.hide + def put(self): + return {} + + def delete(self): + return {} + +.. note:: + + Namespace tags without attached resources will be hidden automatically from the documentation. + + +Documenting authorizations +-------------------------- + +You can use the ``authorizations`` keyword argument to document authorization information. +See `Swagger Authentication documentation `_ +for configuration details. +- ``authorizations`` is a Python dictionary representation of the Swagger ``securityDefinitions`` configuration. + +.. code-block:: python + + authorizations = { + 'apikey': { + 'type': 'apiKey', + 'in': 'header', + 'name': 'X-API-KEY' + } + } + api = Api(app, authorizations=authorizations) + +Then decorate each resource and method that requires authorization: + +.. code-block:: python + + @api.route('/resource/') + class Resource1(Resource): + @api.doc(security='apikey') + def get(self): + pass + + @api.doc(security='apikey') + def post(self): + pass + +You can apply this requirement globally with the ``security`` parameter on the ``Api`` constructor: + +.. code-block:: python + + authorizations = { + 'apikey': { + 'type': 'apiKey', + 'in': 'header', + 'name': 'X-API-KEY' + } + } + api = Api(app, authorizations=authorizations, security='apikey') + + +You can have multiple security schemes: + +.. code-block:: python + + authorizations = { + 'apikey': { + 'type': 'apiKey', + 'in': 'header', + 'name': 'X-API' + }, + 'oauth2': { + 'type': 'oauth2', + 'flow': 'accessCode', + 'tokenUrl': 'https://somewhere.com/token', + 'authorizationUrl': 'https://somewhere.com/auth', + 'scopes': { + 'read': 'Grant read-only access', + 'write': 'Grant read-write access', + } + } + } + api = Api(self.app, security=['apikey', {'oauth2': 'read'}], authorizations=authorizations) + +Security schemes can be overridden for a particular method: + +.. code-block:: python + + @api.route('/authorizations/') + class Authorized(Resource): + @api.doc(security=[{'oauth2': ['read', 'write']}]) + def get(self): + return {} + +You can disable security on a given resource or method by passing ``None`` or an empty list as the ``security`` parameter: + +.. code-block:: python + + @api.route('/without-authorization/') + class WithoutAuthorization(Resource): + @api.doc(security=[]) + def get(self): + return {} + + @api.doc(security=None) + def post(self): + return {} + + +Expose vendor Extensions +------------------------ + +Swaggers allows you to expose custom `vendor extensions`_ and you can use them +in Flask-RESTX with the `@api.vendor` decorator. + +It supports both extensions as `dict` or `kwargs` and perform automatique `x-` prefix: + +.. code-block:: python + + @api.route('/vendor/') + @api.vendor(extension1='any authorized value') + class Vendor(Resource): + @api.vendor({ + 'extension-1': {'works': 'with complex values'}, + 'x-extension-3': 'x- prefix is optional', + }) + def get(self): + return {} + + +Export Swagger specifications +----------------------------- + +You can export the Swagger specifications for your API: + +.. code-block:: python + + from flask import json + + from myapp import api + + print(json.dumps(api.__schema__)) + + +.. _swaggerui: + +Swagger UI +---------- + +By default ``flask-restx`` provides Swagger UI documentation, served from the root URL of the API. + + +.. code-block:: python + + from flask import Flask + from flask_restx import Api, Resource, fields + + app = Flask(__name__) + api = Api(app, version='1.0', title='Sample API', + description='A sample API', + ) + + @api.route('/my-resource/') + @api.doc(params={'id': 'An ID'}) + class MyResource(Resource): + def get(self, id): + return {} + + @api.response(403, 'Not Authorized') + def post(self, id): + api.abort(403) + + + if __name__ == '__main__': + app.run(debug=True) + + +If you run the code below and visit your API's root URL (http://localhost:5000) +you can view the automatically-generated Swagger UI documentation. + +.. image:: _static/screenshot-apidoc-quickstart.png + + +Customization +~~~~~~~~~~~~~ + +You can control the Swagger UI path with the ``doc`` parameter (defaults to the API root): + +.. code-block:: python + + from flask import Flask, Blueprint + from flask_restx import Api + + app = Flask(__name__) + blueprint = Blueprint('api', __name__, url_prefix='/api') + api = Api(blueprint, doc='/doc/') + + app.register_blueprint(blueprint) + + assert url_for('api.doc') == '/api/doc/' + + +You can specify a custom validator URL by setting ``config.SWAGGER_VALIDATOR_URL``: + +.. code-block:: python + + from flask import Flask + from flask_restx import Api + + app = Flask(__name__) + app.config.SWAGGER_VALIDATOR_URL = 'http://domain.com/validator' + + api = Api(app) + + +You can enable [OAuth2 Implicit Flow](https://oauth.net/2/grant-types/implicit/) for retrieving an +authorization token for testing api endpoints interactively within Swagger UI. +The ``config.SWAGGER_UI_OAUTH_CLIENT_ID`` and ``authorizationUrl`` and ``scopes`` +will be specific to your OAuth2 IDP configuration. +The realm string is added as a query parameter to authorizationUrl and tokenUrl. +These values are all public knowledge. No *client secret* is specified here. +.. Using PKCE instead of Implicit Flow depends on https://github.com/swagger-api/swagger-ui/issues/5348 + +.. code-block:: python + + from flask import Flask + from flask_restx import Api + + app = Flask(__name__) + + app.config.SWAGGER_UI_OAUTH_CLIENT_ID = 'MyClientId' + app.config.SWAGGER_UI_OAUTH_REALM = '-' + app.config.SWAGGER_UI_OAUTH_APP_NAME = 'Demo' + + api = Api( + app, + title=app.config.SWAGGER_UI_OAUTH_APP_NAME, + security={'OAuth2': ['read', 'write']}, + authorizations={ + 'OAuth2': { + 'type': 'oauth2', + 'flow': 'implicit', + 'authorizationUrl': 'https://idp.example.com/authorize?audience=https://app.example.com', + 'clientId': app.config.SWAGGER_UI_OAUTH_CLIENT_ID, + 'scopes': { + 'openid': 'Get ID token', + 'profile': 'Get identity', + } + } + } + ) + +You can also specify the initial expansion state with the ``config.SWAGGER_UI_DOC_EXPANSION`` +setting (``'none'``, ``'list'`` or ``'full'``): + +.. code-block:: python + + from flask import Flask + from flask_restx import Api + + app = Flask(__name__) + app.config.SWAGGER_UI_DOC_EXPANSION = 'list' + + api = Api(app) + +By default, operation ID is hidden as well as request duration, you can enable them respectively with: + +.. code-block:: python + + from flask import Flask + from flask_restx import Api + + app = Flask(__name__) + app.config.SWAGGER_UI_OPERATION_ID = True + app.config.SWAGGER_UI_REQUEST_DURATION = True + + api = Api(app) + + +If you need a custom UI, +you can register a custom view function with the :meth:`~Api.documentation` decorator: + +.. code-block:: python + + from flask import Flask + from flask_restx import Api, apidoc + + app = Flask(__name__) + api = Api(app) + + @api.documentation + def custom_ui(): + return apidoc.ui_for(api) + +Configuring "Try it Out" +~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, all paths and methods have a "Try it Out" button for performing API requests in the browser. +These can be disable **per method** with the ``SWAGGER_SUPPORTED_SUBMIT_METHODS`` configuration option, +supporting the same values as the ``supportedSubmitMethods`` `Swagger UI parameter `_. + +.. code-block:: python + + from flask import Flask + from flask_restx import Api + + app = Flask(__name__) + + # disable Try it Out for all methods + app.config.SWAGGER_SUPPORTED_SUBMIT_METHODS = [] + + # enable Try it Out for specific methods + app.config.SWAGGER_SUPPORTED_SUBMIT_METHODS = ["get", "post"] + + api = Api(app) + +Disabling the documentation +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To disable Swagger UI entirely, set ``doc=False``: + +.. code-block:: python + + from flask import Flask + from flask_restx import Api + + app = Flask(__name__) + api = Api(app, doc=False) + + +.. _vendor extensions: https://swagger.io/specification/#specification-extensions diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/examples/resource_class_kwargs b/packages/flask-restx/opengnsys-flask-restx-1.3.0/examples/resource_class_kwargs new file mode 100644 index 0000000..d8fcc30 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/examples/resource_class_kwargs @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +from flask_restx import Resource, Namespace +from flask import Blueprint, Flask +from flask_restx import Api, fields, Model + +### models.py contain models for data validation ### +# just a simple model +MyTest = Model('MyTest', { + 'data': fields.String(required=True,readonly=True), +}) + +### namespaces.py contains definition of routes +namesp = Namespace(name="tests", validate=True) +# register model +namesp.models[MyTest.name] = MyTest +@namesp.route('/') +class get_session(Resource): + + def __init__(self, api=None, *args, **kwargs): + # sessions is a black box dependency + self.answer_service = kwargs['answer_service'] + super().__init__(api,*args, **kwargs) + + @namesp.marshal_with(MyTest) + def get(self, message): + # ducktyping + # any used answer_service must implement this method somehow + return self.answer_service.answer(message) + + +### managers.py contain logic what should happen on request ### + +# loosly coupled and independent from communication +# could be implemented with database, log file what so ever +class AnswerService: + def __init__(self,msg): + self.msg=msg + def answer(self, request:str): + return {'data': request+self.msg} + +#### main.py ### +blueprint = Blueprint("api", __name__, url_prefix="/api/v1") + +api = Api( + blueprint, + version="1.0", + doc="/ui", + validate=False, +) + +# main glues communication and managers together +ans= AnswerService('~nice to meet you') +injected_objects={'answer_service': ans} + +### could also be defined without namespace ### +#api.models[MyTest.name] = MyTest +#api.add_resource(get_session, '/answer', +# resource_class_kwargs=injected_objects) + + +# inject the objects containing logic here +for res in namesp.resources: + res.kwargs['resource_class_kwargs'] = injected_objects + print(res) +# finally add namespace to api +api.add_namespace(namesp) + +app = Flask('test') +from flask import redirect +@app.route('/', methods=['POST', 'GET']) +def home(): + return redirect('/api/v1/ui') + +app.register_blueprint(blueprint) +app.run(debug=False, port=8002) diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/examples/todo.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/examples/todo.py new file mode 100644 index 0000000..d1ed933 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/examples/todo.py @@ -0,0 +1,95 @@ +from flask import Flask +from flask_restx import Api, Resource, fields +from werkzeug.middleware.proxy_fix import ProxyFix + +app = Flask(__name__) +app.wsgi_app = ProxyFix(app.wsgi_app) +api = Api( + app, + version="1.0", + title="Todo API", + description="A simple TODO API", +) + +ns = api.namespace("todos", description="TODO operations") + +TODOS = { + "todo1": {"task": "build an API"}, + "todo2": {"task": "?????"}, + "todo3": {"task": "profit!"}, +} + +todo = api.model( + "Todo", {"task": fields.String(required=True, description="The task details")} +) + +listed_todo = api.model( + "ListedTodo", + { + "id": fields.String(required=True, description="The todo ID"), + "todo": fields.Nested(todo, description="The Todo"), + }, +) + + +def abort_if_todo_doesnt_exist(todo_id): + if todo_id not in TODOS: + api.abort(404, "Todo {} doesn't exist".format(todo_id)) + + +parser = api.parser() +parser.add_argument( + "task", type=str, required=True, help="The task details", location="form" +) + + +@ns.route("/") +@api.doc(responses={404: "Todo not found"}, params={"todo_id": "The Todo ID"}) +class Todo(Resource): + """Show a single todo item and lets you delete them""" + + @api.doc(description="todo_id should be in {0}".format(", ".join(TODOS.keys()))) + @api.marshal_with(todo) + def get(self, todo_id): + """Fetch a given resource""" + abort_if_todo_doesnt_exist(todo_id) + return TODOS[todo_id] + + @api.doc(responses={204: "Todo deleted"}) + def delete(self, todo_id): + """Delete a given resource""" + abort_if_todo_doesnt_exist(todo_id) + del TODOS[todo_id] + return "", 204 + + @api.doc(parser=parser) + @api.marshal_with(todo) + def put(self, todo_id): + """Update a given resource""" + args = parser.parse_args() + task = {"task": args["task"]} + TODOS[todo_id] = task + return task + + +@ns.route("/") +class TodoList(Resource): + """Shows a list of all todos, and lets you POST to add new tasks""" + + @api.marshal_list_with(listed_todo) + def get(self): + """List all todos""" + return [{"id": id, "todo": todo} for id, todo in TODOS.items()] + + @api.doc(parser=parser) + @api.marshal_with(todo, code=201) + def post(self): + """Create a todo""" + args = parser.parse_args() + todo_id = "todo%d" % (len(TODOS) + 1) + TODOS[todo_id] = {"task": args["task"]} + return TODOS[todo_id], 201 + + +if __name__ == "__main__": + app.run(debug=True) diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/examples/todo_blueprint.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/examples/todo_blueprint.py new file mode 100644 index 0000000..50d5c0a --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/examples/todo_blueprint.py @@ -0,0 +1,96 @@ +from flask import Flask, Blueprint +from flask_restx import Api, Resource, fields + +api_v1 = Blueprint("api", __name__, url_prefix="/api/1") + +api = Api( + api_v1, + version="1.0", + title="Todo API", + description="A simple TODO API", +) + +ns = api.namespace("todos", description="TODO operations") + +TODOS = { + "todo1": {"task": "build an API"}, + "todo2": {"task": "?????"}, + "todo3": {"task": "profit!"}, +} + +todo = api.model( + "Todo", {"task": fields.String(required=True, description="The task details")} +) + +listed_todo = api.model( + "ListedTodo", + { + "id": fields.String(required=True, description="The todo ID"), + "todo": fields.Nested(todo, description="The Todo"), + }, +) + + +def abort_if_todo_doesnt_exist(todo_id): + if todo_id not in TODOS: + api.abort(404, "Todo {} doesn't exist".format(todo_id)) + + +parser = api.parser() +parser.add_argument( + "task", type=str, required=True, help="The task details", location="form" +) + + +@ns.route("/") +@api.doc(responses={404: "Todo not found"}, params={"todo_id": "The Todo ID"}) +class Todo(Resource): + """Show a single todo item and lets you delete them""" + + @api.doc(description="todo_id should be in {0}".format(", ".join(TODOS.keys()))) + @api.marshal_with(todo) + def get(self, todo_id): + """Fetch a given resource""" + abort_if_todo_doesnt_exist(todo_id) + return TODOS[todo_id] + + @api.doc(responses={204: "Todo deleted"}) + def delete(self, todo_id): + """Delete a given resource""" + abort_if_todo_doesnt_exist(todo_id) + del TODOS[todo_id] + return "", 204 + + @api.doc(parser=parser) + @api.marshal_with(todo) + def put(self, todo_id): + """Update a given resource""" + args = parser.parse_args() + task = {"task": args["task"]} + TODOS[todo_id] = task + return task + + +@ns.route("/") +class TodoList(Resource): + """Shows a list of all todos, and lets you POST to add new tasks""" + + @api.marshal_list_with(listed_todo) + def get(self): + """List all todos""" + return [{"id": id, "todo": todo} for id, todo in TODOS.items()] + + @api.doc(parser=parser) + @api.marshal_with(todo, code=201) + def post(self): + """Create a todo""" + args = parser.parse_args() + todo_id = "todo%d" % (len(TODOS) + 1) + TODOS[todo_id] = {"task": args["task"]} + return TODOS[todo_id], 201 + + +if __name__ == "__main__": + app = Flask(__name__) + app.register_blueprint(api_v1) + app.run(debug=True) diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/examples/todo_simple.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/examples/todo_simple.py new file mode 100644 index 0000000..07e673e --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/examples/todo_simple.py @@ -0,0 +1,43 @@ +from flask import Flask, request +from flask_restx import Resource, Api + +app = Flask(__name__) +api = Api(app) + +todos = {} + + +@api.route("/") +class TodoSimple(Resource): + """ + You can try this example as follow: + $ curl http://localhost:5000/todo1 -d "data=Remember the milk" -X PUT + $ curl http://localhost:5000/todo1 + {"todo1": "Remember the milk"} + $ curl http://localhost:5000/todo2 -d "data=Change my breakpads" -X PUT + $ curl http://localhost:5000/todo2 + {"todo2": "Change my breakpads"} + + Or from python if you have requests : + >>> from requests import put, get + >>> put('http://localhost:5000/todo1', data={'data': 'Remember the milk'}).json + {u'todo1': u'Remember the milk'} + >>> get('http://localhost:5000/todo1').json + {u'todo1': u'Remember the milk'} + >>> put('http://localhost:5000/todo2', data={'data': 'Change my breakpads'}).json + {u'todo2': u'Change my breakpads'} + >>> get('http://localhost:5000/todo2').json + {u'todo2': u'Change my breakpads'} + + """ + + def get(self, todo_id): + return {todo_id: todos[todo_id]} + + def put(self, todo_id): + todos[todo_id] = request.form["data"] + return {todo_id: todos[todo_id]} + + +if __name__ == "__main__": + app.run(debug=False) diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/examples/todomvc.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/examples/todomvc.py new file mode 100644 index 0000000..252177b --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/examples/todomvc.py @@ -0,0 +1,103 @@ +from flask import Flask +from flask_restx import Api, Resource, fields +from werkzeug.middleware.proxy_fix import ProxyFix + +app = Flask(__name__) +app.wsgi_app = ProxyFix(app.wsgi_app) +api = Api( + app, + version="1.0", + title="TodoMVC API", + description="A simple TodoMVC API", +) + +ns = api.namespace("todos", description="TODO operations") + +todo = api.model( + "Todo", + { + "id": fields.Integer(readonly=True, description="The task unique identifier"), + "task": fields.String(required=True, description="The task details"), + }, +) + + +class TodoDAO(object): + def __init__(self): + self.counter = 0 + self.todos = [] + + def get(self, id): + for todo in self.todos: + if todo["id"] == id: + return todo + api.abort(404, "Todo {} doesn't exist".format(id)) + + def create(self, data): + todo = data + todo["id"] = self.counter = self.counter + 1 + self.todos.append(todo) + return todo + + def update(self, id, data): + todo = self.get(id) + todo.update(data) + return todo + + def delete(self, id): + todo = self.get(id) + self.todos.remove(todo) + + +todo_dao = TodoDAO() +todo_dao.create({"task": "Build an API"}) +todo_dao.create({"task": "?????"}) +todo_dao.create({"task": "profit!"}) + + +@ns.route("/") +class TodoList(Resource): + """Shows a list of all todos, and lets you POST to add new tasks""" + + @ns.doc("list_todos") + @ns.marshal_list_with(todo) + def get(self): + """List all tasks""" + return todo_dao.todos + + @ns.doc("create_todo") + @ns.expect(todo) + @ns.marshal_with(todo, code=201) + def post(self): + """Create a new task""" + return todo_dao.create(api.payload), 201 + + +@ns.route("/") +@ns.response(404, "Todo not found") +@ns.param("id", "The task identifier") +class Todo(Resource): + """Show a single todo item and lets you delete them""" + + @ns.doc("get_todo") + @ns.marshal_with(todo) + def get(self, id): + """Fetch a given resource""" + return todo_dao.get(id) + + @ns.doc("delete_todo") + @ns.response(204, "Todo deleted") + def delete(self, id): + """Delete a task given its identifier""" + todo_dao.delete(id) + return "", 204 + + @ns.expect(todo) + @ns.marshal_with(todo) + def put(self, id): + """Update a task given its identifier""" + return todo_dao.update(id, api.payload) + + +if __name__ == "__main__": + app.run(debug=True) diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/examples/xml_representation.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/examples/xml_representation.py new file mode 100644 index 0000000..2209710 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/examples/xml_representation.py @@ -0,0 +1,41 @@ +# needs: pip install python-simplexml +from simplexml import dumps +from flask import make_response, Flask +from flask_restx import Api, Resource, fields + + +def output_xml(data, code, headers=None): + """Makes a Flask response with a XML encoded body""" + resp = make_response(dumps({"response": data}), code) + resp.headers.extend(headers or {}) + return resp + + +app = Flask(__name__) +api = Api(app, default_mediatype="application/xml") +api.representations["application/xml"] = output_xml + +hello_fields = api.model("Hello", {"entry": fields.String}) + + +@api.route("/") +class Hello(Resource): + """ + # you need requests + >>> from requests import get + >>> get('http://localhost:5000/me').content # default_mediatype + 'me' + >>> get('http://localhost:5000/me', headers={"accept":"application/json"}).content + '{"hello": "me"}' + >>> get('http://localhost:5000/me', headers={"accept":"application/xml"}).content + 'me' + """ + + @api.doc(model=hello_fields, params={"entry": "The entry to wrap"}) + def get(self, entry): + """Get a wrapped entry""" + return {"hello": entry} + + +if __name__ == "__main__": + app.run(debug=True) diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/examples/zoo_app/complex.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/examples/zoo_app/complex.py new file mode 100644 index 0000000..69a6565 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/examples/zoo_app/complex.py @@ -0,0 +1,11 @@ +from flask import Flask +from werkzeug.middleware.proxy_fix import ProxyFix + +from zoo import api + +app = Flask(__name__) +app.wsgi_app = ProxyFix(app.wsgi_app) + +api.init_app(app) + +app.run(debug=True) diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/examples/zoo_app/requirements.txt b/packages/flask-restx/opengnsys-flask-restx-1.3.0/examples/zoo_app/requirements.txt new file mode 100644 index 0000000..4176fd5 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/examples/zoo_app/requirements.txt @@ -0,0 +1,14 @@ +aniso8601==9.0.1 +attrs==21.2.0 +click==7.1.2 +Flask==1.1.4 +flask-restx==0.5.1 +itsdangerous==1.1.0 +Jinja2==2.11.3 +jsonschema==3.2.0 +MarkupSafe==2.0.1 +pyrsistent==0.17.3 +pytz==2021.1 +six==1.16.0 +Werkzeug==2.2.3 + diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/examples/zoo_app/zoo/__init__.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/examples/zoo_app/zoo/__init__.py new file mode 100644 index 0000000..09fac15 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/examples/zoo_app/zoo/__init__.py @@ -0,0 +1,13 @@ +from flask_restx import Api + +from .cat import api as cat_api +from .dog import api as dog_api + +api = Api( + title="Zoo API", + version="1.0", + description="A simple demo API", +) + +api.add_namespace(cat_api) +api.add_namespace(dog_api) diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/examples/zoo_app/zoo/cat.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/examples/zoo_app/zoo/cat.py new file mode 100644 index 0000000..be1fc62 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/examples/zoo_app/zoo/cat.py @@ -0,0 +1,38 @@ +from flask_restx import Namespace, Resource, fields + +api = Namespace("cats", description="Cats related operations") + +cat = api.model( + "Cat", + { + "id": fields.String(required=True, description="The cat identifier"), + "name": fields.String(required=True, description="The cat name"), + }, +) + +CATS = [ + {"id": "felix", "name": "Felix"}, +] + + +@api.route("/") +class CatList(Resource): + @api.doc("list_cats") + @api.marshal_list_with(cat) + def get(self): + """List all cats""" + return CATS + + +@api.route("/") +@api.param("id", "The cat identifier") +@api.response(404, "Cat not found") +class Cat(Resource): + @api.doc("get_cat") + @api.marshal_with(cat) + def get(self, id): + """Fetch a cat given its identifier""" + for cat in CATS: + if cat["id"] == id: + return cat + api.abort(404) diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/examples/zoo_app/zoo/dog.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/examples/zoo_app/zoo/dog.py new file mode 100644 index 0000000..4666cbf --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/examples/zoo_app/zoo/dog.py @@ -0,0 +1,38 @@ +from flask_restx import Namespace, Resource, fields + +api = Namespace("dogs", description="Dogs related operations") + +dog = api.model( + "Dog", + { + "id": fields.String(required=True, description="The dog identifier"), + "name": fields.String(required=True, description="The dog name"), + }, +) + +DOGS = [ + {"id": "medor", "name": "Medor"}, +] + + +@api.route("/") +class DogList(Resource): + @api.doc("list_dogs") + @api.marshal_list_with(dog) + def get(self): + """List all dogs""" + return DOGS + + +@api.route("/") +@api.param("id", "The dog identifier") +@api.response(404, "Dog not found") +class Dog(Resource): + @api.doc("get_dog") + @api.marshal_with(dog) + def get(self, id): + """Fetch a dog given its identifier""" + for dog in DOGS: + if dog["id"] == id: + return dog + api.abort(404) diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/__about__.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/__about__.py new file mode 100644 index 0000000..1b0e846 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/__about__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +__version__ = "1.3.0" +__description__ = ( + "Fully featured framework for fast, easy and documented API development with Flask" +) diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/__init__.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/__init__.py new file mode 100644 index 0000000..4711099 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/__init__.py @@ -0,0 +1,35 @@ +from . import fields, reqparse, apidoc, inputs, cors +from .api import Api # noqa +from .marshalling import marshal, marshal_with, marshal_with_field # noqa +from .mask import Mask +from .model import Model, OrderedModel, SchemaModel # noqa +from .namespace import Namespace # noqa +from .resource import Resource # noqa +from .errors import abort, RestError, SpecsError, ValidationError +from .swagger import Swagger +from .__about__ import __version__, __description__ + +__all__ = ( + "__version__", + "__description__", + "Api", + "Resource", + "apidoc", + "marshal", + "marshal_with", + "marshal_with_field", + "Mask", + "Model", + "Namespace", + "OrderedModel", + "SchemaModel", + "abort", + "cors", + "fields", + "inputs", + "reqparse", + "RestError", + "SpecsError", + "Swagger", + "ValidationError", +) diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/_http.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/_http.py new file mode 100644 index 0000000..800b766 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/_http.py @@ -0,0 +1,186 @@ +# encoding: utf-8 +""" +This file is backported from Python 3.5 http built-in module. +""" + +from enum import IntEnum + + +class HTTPStatus(IntEnum): + """HTTP status codes and reason phrases + + Status codes from the following RFCs are all observed: + + * RFC 7231: Hypertext Transfer Protocol (HTTP/1.1), obsoletes 2616 + * RFC 6585: Additional HTTP Status Codes + * RFC 3229: Delta encoding in HTTP + * RFC 4918: HTTP Extensions for WebDAV, obsoletes 2518 + * RFC 5842: Binding Extensions to WebDAV + * RFC 7238: Permanent Redirect + * RFC 2295: Transparent Content Negotiation in HTTP + * RFC 2774: An HTTP Extension Framework + """ + + def __new__(cls, value, phrase, description=""): + obj = int.__new__(cls, value) + obj._value_ = value + + obj.phrase = phrase + obj.description = description + return obj + + def __str__(self): + return str(self.value) + + # informational + CONTINUE = 100, "Continue", "Request received, please continue" + SWITCHING_PROTOCOLS = ( + 101, + "Switching Protocols", + "Switching to new protocol; obey Upgrade header", + ) + PROCESSING = 102, "Processing" + + # success + OK = 200, "OK", "Request fulfilled, document follows" + CREATED = 201, "Created", "Document created, URL follows" + ACCEPTED = (202, "Accepted", "Request accepted, processing continues off-line") + NON_AUTHORITATIVE_INFORMATION = ( + 203, + "Non-Authoritative Information", + "Request fulfilled from cache", + ) + NO_CONTENT = 204, "No Content", "Request fulfilled, nothing follows" + RESET_CONTENT = 205, "Reset Content", "Clear input form for further input" + PARTIAL_CONTENT = 206, "Partial Content", "Partial content follows" + MULTI_STATUS = 207, "Multi-Status" + ALREADY_REPORTED = 208, "Already Reported" + IM_USED = 226, "IM Used" + + # redirection + MULTIPLE_CHOICES = ( + 300, + "Multiple Choices", + "Object has several resources -- see URI list", + ) + MOVED_PERMANENTLY = ( + 301, + "Moved Permanently", + "Object moved permanently -- see URI list", + ) + FOUND = 302, "Found", "Object moved temporarily -- see URI list" + SEE_OTHER = 303, "See Other", "Object moved -- see Method and URL list" + NOT_MODIFIED = (304, "Not Modified", "Document has not changed since given time") + USE_PROXY = ( + 305, + "Use Proxy", + "You must use proxy specified in Location to access this resource", + ) + TEMPORARY_REDIRECT = ( + 307, + "Temporary Redirect", + "Object moved temporarily -- see URI list", + ) + PERMANENT_REDIRECT = ( + 308, + "Permanent Redirect", + "Object moved temporarily -- see URI list", + ) + + # client error + BAD_REQUEST = (400, "Bad Request", "Bad request syntax or unsupported method") + UNAUTHORIZED = (401, "Unauthorized", "No permission -- see authorization schemes") + PAYMENT_REQUIRED = (402, "Payment Required", "No payment -- see charging schemes") + FORBIDDEN = (403, "Forbidden", "Request forbidden -- authorization will not help") + NOT_FOUND = (404, "Not Found", "Nothing matches the given URI") + METHOD_NOT_ALLOWED = ( + 405, + "Method Not Allowed", + "Specified method is invalid for this resource", + ) + NOT_ACCEPTABLE = (406, "Not Acceptable", "URI not available in preferred format") + PROXY_AUTHENTICATION_REQUIRED = ( + 407, + "Proxy Authentication Required", + "You must authenticate with this proxy before proceeding", + ) + REQUEST_TIMEOUT = (408, "Request Timeout", "Request timed out; try again later") + CONFLICT = 409, "Conflict", "Request conflict" + GONE = (410, "Gone", "URI no longer exists and has been permanently removed") + LENGTH_REQUIRED = (411, "Length Required", "Client must specify Content-Length") + PRECONDITION_FAILED = ( + 412, + "Precondition Failed", + "Precondition in headers is false", + ) + REQUEST_ENTITY_TOO_LARGE = (413, "Request Entity Too Large", "Entity is too large") + REQUEST_URI_TOO_LONG = (414, "Request-URI Too Long", "URI is too long") + UNSUPPORTED_MEDIA_TYPE = ( + 415, + "Unsupported Media Type", + "Entity body in unsupported format", + ) + REQUESTED_RANGE_NOT_SATISFIABLE = ( + 416, + "Requested Range Not Satisfiable", + "Cannot satisfy request range", + ) + EXPECTATION_FAILED = ( + 417, + "Expectation Failed", + "Expect condition could not be satisfied", + ) + UNPROCESSABLE_ENTITY = 422, "Unprocessable Entity" + LOCKED = 423, "Locked" + FAILED_DEPENDENCY = 424, "Failed Dependency" + UPGRADE_REQUIRED = 426, "Upgrade Required" + PRECONDITION_REQUIRED = ( + 428, + "Precondition Required", + "The origin server requires the request to be conditional", + ) + TOO_MANY_REQUESTS = ( + 429, + "Too Many Requests", + "The user has sent too many requests in " + 'a given amount of time ("rate limiting")', + ) + REQUEST_HEADER_FIELDS_TOO_LARGE = ( + 431, + "Request Header Fields Too Large", + "The server is unwilling to process the request because its header " + "fields are too large", + ) + + # server errors + INTERNAL_SERVER_ERROR = ( + 500, + "Internal Server Error", + "Server got itself in trouble", + ) + NOT_IMPLEMENTED = (501, "Not Implemented", "Server does not support this operation") + BAD_GATEWAY = (502, "Bad Gateway", "Invalid responses from another server/proxy") + SERVICE_UNAVAILABLE = ( + 503, + "Service Unavailable", + "The server cannot process the request due to a high load", + ) + GATEWAY_TIMEOUT = ( + 504, + "Gateway Timeout", + "The gateway server did not receive a timely response", + ) + HTTP_VERSION_NOT_SUPPORTED = ( + 505, + "HTTP Version Not Supported", + "Cannot fulfill request", + ) + VARIANT_ALSO_NEGOTIATES = 506, "Variant Also Negotiates" + INSUFFICIENT_STORAGE = 507, "Insufficient Storage" + LOOP_DETECTED = 508, "Loop Detected" + NOT_EXTENDED = 510, "Not Extended" + NETWORK_AUTHENTICATION_REQUIRED = ( + 511, + "Network Authentication Required", + "The client needs to authenticate to gain network access", + ) diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/api.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/api.py new file mode 100644 index 0000000..a76ae3a --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/api.py @@ -0,0 +1,977 @@ +import difflib +import inspect +from itertools import chain +import logging +import operator +import re +import sys +import warnings + +from collections import OrderedDict +from functools import wraps, partial +from types import MethodType + +from flask import url_for, request, current_app +from flask import make_response as original_flask_make_response + +from flask.signals import got_request_exception + +from jsonschema import RefResolver + +from werkzeug.utils import cached_property +from werkzeug.datastructures import Headers +from werkzeug.exceptions import ( + HTTPException, + MethodNotAllowed, + NotFound, + NotAcceptable, + InternalServerError, +) + +from . import apidoc +from .mask import ParseError, MaskError +from .namespace import Namespace +from .postman import PostmanCollectionV1 +from .resource import Resource +from .swagger import Swagger +from .utils import ( + default_id, + camel_to_dash, + unpack, + import_check_view_func, + BaseResponse, +) +from .representations import output_json +from ._http import HTTPStatus + +endpoint_from_view_func = import_check_view_func() + + +RE_RULES = re.compile("(<.*>)") + +# List headers that should never be handled by Flask-RESTX +HEADERS_BLACKLIST = ("Content-Length",) + +DEFAULT_REPRESENTATIONS = [("application/json", output_json)] + +log = logging.getLogger(__name__) + + +class Api(object): + """ + The main entry point for the application. + You need to initialize it with a Flask Application: :: + + >>> app = Flask(__name__) + >>> api = Api(app) + + Alternatively, you can use :meth:`init_app` to set the Flask application + after it has been constructed. + + The endpoint parameter prefix all views and resources: + + - The API root/documentation will be ``{endpoint}.root`` + - A resource registered as 'resource' will be available as ``{endpoint}.resource`` + + :param flask.Flask|flask.Blueprint app: the Flask application object or a Blueprint + :param str version: The API version (used in Swagger documentation) + :param str title: The API title (used in Swagger documentation) + :param str description: The API description (used in Swagger documentation) + :param str terms_url: The API terms page URL (used in Swagger documentation) + :param str contact: A contact email for the API (used in Swagger documentation) + :param str license: The license associated to the API (used in Swagger documentation) + :param str license_url: The license page URL (used in Swagger documentation) + :param str endpoint: The API base endpoint (default to 'api). + :param str default: The default namespace base name (default to 'default') + :param str default_label: The default namespace label (used in Swagger documentation) + :param str default_mediatype: The default media type to return + :param bool validate: Whether or not the API should perform input payload validation. + :param bool ordered: Whether or not preserve order models and marshalling. + :param str doc: The documentation path. If set to a false value, documentation is disabled. + (Default to '/') + :param list decorators: Decorators to attach to every resource + :param bool catch_all_404s: Use :meth:`handle_error` + to handle 404 errors throughout your app + :param dict authorizations: A Swagger Authorizations declaration as dictionary + :param bool serve_challenge_on_401: Serve basic authentication challenge with 401 + responses (default 'False') + :param FormatChecker format_checker: A jsonschema.FormatChecker object that is hooked into + the Model validator. A default or a custom FormatChecker can be provided (e.g., with custom + checkers), otherwise the default action is to not enforce any format validation. + :param url_scheme: If set to a string (e.g. http, https), then the specs_url and base_url will explicitly use this + scheme regardless of how the application is deployed. This is necessary for some deployments behind a reverse + proxy. + :param str default_swagger_filename: The default swagger filename. + """ + + def __init__( + self, + app=None, + version="1.0", + title=None, + description=None, + terms_url=None, + license=None, + license_url=None, + contact=None, + contact_url=None, + contact_email=None, + authorizations=None, + security=None, + doc="/", + default_id=default_id, + default="default", + default_label="Default namespace", + validate=None, + tags=None, + prefix="", + ordered=False, + default_mediatype="application/json", + decorators=None, + catch_all_404s=False, + serve_challenge_on_401=False, + format_checker=None, + url_scheme=None, + default_swagger_filename="swagger.json", + **kwargs + ): + self.version = version + self.title = title or "API" + self.description = description + self.terms_url = terms_url + self.contact = contact + self.contact_email = contact_email + self.contact_url = contact_url + self.license = license + self.license_url = license_url + self.authorizations = authorizations + self.security = security + self.default_id = default_id + self.ordered = ordered + self._validate = validate + self._doc = doc + self._doc_view = None + self._default_error_handler = None + self.tags = tags or [] + + self.error_handlers = OrderedDict( + { + ParseError: mask_parse_error_handler, + MaskError: mask_error_handler, + } + ) + self._schema = None + self.models = {} + self._refresolver = None + self.format_checker = format_checker + self.namespaces = [] + self.default_swagger_filename = default_swagger_filename + + self.ns_paths = dict() + + self.representations = OrderedDict(DEFAULT_REPRESENTATIONS) + self.urls = {} + self.prefix = prefix + self.default_mediatype = default_mediatype + self.decorators = decorators if decorators else [] + self.catch_all_404s = catch_all_404s + self.serve_challenge_on_401 = serve_challenge_on_401 + self.blueprint_setup = None + self.endpoints = set() + self.resources = [] + self.app = None + self.blueprint = None + # must come after self.app initialisation to prevent __getattr__ recursion + # in self._configure_namespace_logger + self.default_namespace = self.namespace( + default, + default_label, + endpoint="{0}-declaration".format(default), + validate=validate, + api=self, + path="/", + ) + self.url_scheme = url_scheme + if app is not None: + self.app = app + self.init_app(app) + # super(Api, self).__init__(app, **kwargs) + + def init_app(self, app, **kwargs): + """ + Allow to lazy register the API on a Flask application:: + + >>> app = Flask(__name__) + >>> api = Api() + >>> api.init_app(app) + + :param flask.Flask app: the Flask application object + :param str title: The API title (used in Swagger documentation) + :param str description: The API description (used in Swagger documentation) + :param str terms_url: The API terms page URL (used in Swagger documentation) + :param str contact: A contact email for the API (used in Swagger documentation) + :param str license: The license associated to the API (used in Swagger documentation) + :param str license_url: The license page URL (used in Swagger documentation) + :param url_scheme: If set to a string (e.g. http, https), then the specs_url and base_url will explicitly use + this scheme regardless of how the application is deployed. This is necessary for some deployments behind a + reverse proxy. + """ + self.app = app + self.title = kwargs.get("title", self.title) + self.description = kwargs.get("description", self.description) + self.terms_url = kwargs.get("terms_url", self.terms_url) + self.contact = kwargs.get("contact", self.contact) + self.contact_url = kwargs.get("contact_url", self.contact_url) + self.contact_email = kwargs.get("contact_email", self.contact_email) + self.license = kwargs.get("license", self.license) + self.license_url = kwargs.get("license_url", self.license_url) + self.url_scheme = kwargs.get("url_scheme", self.url_scheme) + self._add_specs = kwargs.get("add_specs", True) + self._register_specs(app) + self._register_doc(app) + + # If app is a blueprint, defer the initialization + try: + app.record(self._deferred_blueprint_init) + # Flask.Blueprint has a 'record' attribute, Flask.Api does not + except AttributeError: + self._init_app(app) + else: + self.blueprint = app + + def _init_app(self, app): + """ + Perform initialization actions with the given :class:`flask.Flask` object. + + :param flask.Flask app: The flask application object + """ + app.handle_exception = partial(self.error_router, app.handle_exception) + app.handle_user_exception = partial( + self.error_router, app.handle_user_exception + ) + + if len(self.resources) > 0: + for resource, namespace, urls, kwargs in self.resources: + self._register_view(app, resource, namespace, *urls, **kwargs) + + for ns in self.namespaces: + self._configure_namespace_logger(app, ns) + + self._register_apidoc(app) + self._validate = ( + self._validate + if self._validate is not None + else app.config.get("RESTX_VALIDATE", False) + ) + app.config.setdefault("RESTX_MASK_HEADER", "X-Fields") + app.config.setdefault("RESTX_MASK_SWAGGER", True) + app.config.setdefault("RESTX_INCLUDE_ALL_MODELS", False) + + # check for deprecated config variable names + if "ERROR_404_HELP" in app.config: + app.config["RESTX_ERROR_404_HELP"] = app.config["ERROR_404_HELP"] + warnings.warn( + "'ERROR_404_HELP' config setting is deprecated and will be " + "removed in the future. Use 'RESTX_ERROR_404_HELP' instead.", + DeprecationWarning, + ) + + def __getattr__(self, name): + try: + return getattr(self.default_namespace, name) + except AttributeError: + raise AttributeError("Api does not have {0} attribute".format(name)) + + def _complete_url(self, url_part, registration_prefix): + """ + This method is used to defer the construction of the final url in + the case that the Api is created with a Blueprint. + + :param url_part: The part of the url the endpoint is registered with + :param registration_prefix: The part of the url contributed by the + blueprint. Generally speaking, BlueprintSetupState.url_prefix + """ + parts = (registration_prefix, self.prefix, url_part) + return "".join(part for part in parts if part) + + def _register_apidoc(self, app): + conf = app.extensions.setdefault("restx", {}) + if not conf.get("apidoc_registered", False): + app.register_blueprint(apidoc.apidoc) + conf["apidoc_registered"] = True + + def _register_specs(self, app_or_blueprint): + if self._add_specs: + endpoint = str("specs") + self._register_view( + app_or_blueprint, + SwaggerView, + self.default_namespace, + "/" + self.default_swagger_filename, + endpoint=endpoint, + resource_class_args=(self,), + ) + self.endpoints.add(endpoint) + + def _register_doc(self, app_or_blueprint): + if self._add_specs and self._doc: + # Register documentation before root if enabled + app_or_blueprint.add_url_rule(self._doc, "doc", self.render_doc) + app_or_blueprint.add_url_rule(self.prefix or "/", "root", self.render_root) + + def register_resource(self, namespace, resource, *urls, **kwargs): + endpoint = kwargs.pop("endpoint", None) + endpoint = str(endpoint or self.default_endpoint(resource, namespace)) + + kwargs["endpoint"] = endpoint + self.endpoints.add(endpoint) + + if self.app is not None: + self._register_view(self.app, resource, namespace, *urls, **kwargs) + else: + self.resources.append((resource, namespace, urls, kwargs)) + return endpoint + + def _configure_namespace_logger(self, app, namespace): + for handler in app.logger.handlers: + namespace.logger.addHandler(handler) + namespace.logger.setLevel(app.logger.level) + + def _register_view(self, app, resource, namespace, *urls, **kwargs): + endpoint = kwargs.pop("endpoint", None) or camel_to_dash(resource.__name__) + resource_class_args = kwargs.pop("resource_class_args", ()) + resource_class_kwargs = kwargs.pop("resource_class_kwargs", {}) + + # NOTE: 'view_functions' is cleaned up from Blueprint class in Flask 1.0 + if endpoint in getattr(app, "view_functions", {}): + previous_view_class = app.view_functions[endpoint].__dict__["view_class"] + + # if you override the endpoint with a different class, avoid the + # collision by raising an exception + if previous_view_class != resource: + msg = "This endpoint (%s) is already set to the class %s." + raise ValueError(msg % (endpoint, previous_view_class.__name__)) + + resource.mediatypes = self.mediatypes_method() # Hacky + resource.endpoint = endpoint + + resource_func = self.output( + resource.as_view( + endpoint, self, *resource_class_args, **resource_class_kwargs + ) + ) + + # Apply Namespace and Api decorators to a resource + for decorator in chain(namespace.decorators, self.decorators): + resource_func = decorator(resource_func) + + for url in urls: + # If this Api has a blueprint + if self.blueprint: + # And this Api has been setup + if self.blueprint_setup: + # Set the rule to a string directly, as the blueprint is already + # set up. + self.blueprint_setup.add_url_rule( + url, view_func=resource_func, **kwargs + ) + continue + else: + # Set the rule to a function that expects the blueprint prefix + # to construct the final url. Allows deferment of url finalization + # in the case that the associated Blueprint has not yet been + # registered to an application, so we can wait for the registration + # prefix + rule = partial(self._complete_url, url) + else: + # If we've got no Blueprint, just build a url with no prefix + rule = self._complete_url(url, "") + # Add the url to the application or blueprint + app.add_url_rule(rule, view_func=resource_func, **kwargs) + + def output(self, resource): + """ + Wraps a resource (as a flask view function), + for cases where the resource does not directly return a response object + + :param resource: The resource as a flask view function + """ + + @wraps(resource) + def wrapper(*args, **kwargs): + resp = resource(*args, **kwargs) + if isinstance(resp, BaseResponse): + return resp + data, code, headers = unpack(resp) + return self.make_response(data, code, headers=headers) + + return wrapper + + def make_response(self, data, *args, **kwargs): + """ + Looks up the representation transformer for the requested media + type, invoking the transformer to create a response object. This + defaults to default_mediatype if no transformer is found for the + requested mediatype. If default_mediatype is None, a 406 Not + Acceptable response will be sent as per RFC 2616 section 14.1 + + :param data: Python object containing response data to be transformed + """ + default_mediatype = ( + kwargs.pop("fallback_mediatype", None) or self.default_mediatype + ) + mediatype = request.accept_mimetypes.best_match( + self.representations, + default=default_mediatype, + ) + if mediatype is None: + raise NotAcceptable() + if mediatype in self.representations: + resp = self.representations[mediatype](data, *args, **kwargs) + resp.headers["Content-Type"] = mediatype + return resp + elif mediatype == "text/plain": + resp = original_flask_make_response(str(data), *args, **kwargs) + resp.headers["Content-Type"] = "text/plain" + return resp + else: + raise InternalServerError() + + def documentation(self, func): + """A decorator to specify a view function for the documentation""" + self._doc_view = func + return func + + def render_root(self): + self.abort(HTTPStatus.NOT_FOUND) + + def render_doc(self): + """Override this method to customize the documentation page""" + if self._doc_view: + return self._doc_view() + elif not self._doc: + self.abort(HTTPStatus.NOT_FOUND) + return apidoc.ui_for(self) + + def default_endpoint(self, resource, namespace): + """ + Provide a default endpoint for a resource on a given namespace. + + Endpoints are ensured not to collide. + + Override this method specify a custom algorithm for default endpoint. + + :param Resource resource: the resource for which we want an endpoint + :param Namespace namespace: the namespace holding the resource + :returns str: An endpoint name + """ + endpoint = camel_to_dash(resource.__name__) + if namespace is not self.default_namespace: + endpoint = "{ns.name}_{endpoint}".format(ns=namespace, endpoint=endpoint) + if endpoint in self.endpoints: + suffix = 2 + while True: + new_endpoint = "{base}_{suffix}".format(base=endpoint, suffix=suffix) + if new_endpoint not in self.endpoints: + endpoint = new_endpoint + break + suffix += 1 + return endpoint + + def get_ns_path(self, ns): + return self.ns_paths.get(ns) + + def ns_urls(self, ns, urls): + path = self.get_ns_path(ns) or ns.path + return [path + url for url in urls] + + def add_namespace(self, ns, path=None): + """ + This method registers resources from namespace for current instance of api. + You can use argument path for definition custom prefix url for namespace. + + :param Namespace ns: the namespace + :param path: registration prefix of namespace + """ + if ns not in self.namespaces: + self.namespaces.append(ns) + if self not in ns.apis: + ns.apis.append(self) + # Associate ns with prefix-path + if path is not None: + self.ns_paths[ns] = path + # Register resources + for r in ns.resources: + urls = self.ns_urls(ns, r.urls) + self.register_resource(ns, r.resource, *urls, **r.kwargs) + # Register models + for name, definition in ns.models.items(): + self.models[name] = definition + if not self.blueprint and self.app is not None: + self._configure_namespace_logger(self.app, ns) + + def namespace(self, *args, **kwargs): + """ + A namespace factory. + + :returns Namespace: a new namespace instance + """ + kwargs["ordered"] = kwargs.get("ordered", self.ordered) + ns = Namespace(*args, **kwargs) + self.add_namespace(ns) + return ns + + def endpoint(self, name): + if self.blueprint: + return "{0}.{1}".format(self.blueprint.name, name) + else: + return name + + @property + def specs_url(self): + """ + The Swagger specifications relative url (ie. `swagger.json`). If + the spec_url_scheme attribute is set, then the full url is provided instead + (e.g. http://localhost/swaggger.json). + + :rtype: str + """ + external = None if self.url_scheme is None else True + return url_for( + self.endpoint("specs"), _scheme=self.url_scheme, _external=external + ) + + @property + def base_url(self): + """ + The API base absolute url + + :rtype: str + """ + return url_for(self.endpoint("root"), _scheme=self.url_scheme, _external=True) + + @property + def base_path(self): + """ + The API path + + :rtype: str + """ + return url_for(self.endpoint("root"), _external=False) + + @cached_property + def __schema__(self): + """ + The Swagger specifications/schema for this API + + :returns dict: the schema as a serializable dict + """ + if not self._schema: + try: + self._schema = Swagger(self).as_dict() + except Exception: + # Log the source exception for debugging purpose + # and return an error message + msg = "Unable to render schema" + log.exception(msg) # This will provide a full traceback + return {"error": msg} + return self._schema + + @property + def _own_and_child_error_handlers(self): + rv = OrderedDict() + rv.update(self.error_handlers) + for ns in self.namespaces: + for exception, handler in ns.error_handlers.items(): + rv[exception] = handler + return rv + + def errorhandler(self, exception): + """A decorator to register an error handler for a given exception""" + if inspect.isclass(exception) and issubclass(exception, Exception): + # Register an error handler for a given exception + def wrapper(func): + self.error_handlers[exception] = func + return func + + return wrapper + else: + # Register the default error handler + self._default_error_handler = exception + return exception + + def owns_endpoint(self, endpoint): + """ + Tests if an endpoint name (not path) belongs to this Api. + Takes into account the Blueprint name part of the endpoint name. + + :param str endpoint: The name of the endpoint being checked + :return: bool + """ + + if self.blueprint: + if endpoint.startswith(self.blueprint.name): + endpoint = endpoint.split(self.blueprint.name + ".", 1)[-1] + else: + return False + return endpoint in self.endpoints + + def _should_use_fr_error_handler(self): + """ + Determine if error should be handled with FR or default Flask + + The goal is to return Flask error handlers for non-FR-related routes, + and FR errors (with the correct media type) for FR endpoints. This + method currently handles 404 and 405 errors. + + :return: bool + """ + adapter = current_app.create_url_adapter(request) + + try: + adapter.match() + except MethodNotAllowed as e: + # Check if the other HTTP methods at this url would hit the Api + valid_route_method = e.valid_methods[0] + rule, _ = adapter.match(method=valid_route_method, return_rule=True) + return self.owns_endpoint(rule.endpoint) + except NotFound: + return self.catch_all_404s + except Exception: + # Werkzeug throws other kinds of exceptions, such as Redirect + pass + + def _has_fr_route(self): + """Encapsulating the rules for whether the request was to a Flask endpoint""" + # 404's, 405's, which might not have a url_rule + if self._should_use_fr_error_handler(): + return True + # for all other errors, just check if FR dispatched the route + if not request.url_rule: + return False + return self.owns_endpoint(request.url_rule.endpoint) + + def error_router(self, original_handler, e): + """ + This function decides whether the error occurred in a flask-restx + endpoint or not. If it happened in a flask-restx endpoint, our + handler will be dispatched. If it happened in an unrelated view, the + app's original error handler will be dispatched. + In the event that the error occurred in a flask-restx endpoint but + the local handler can't resolve the situation, the router will fall + back onto the original_handler as last resort. + + :param function original_handler: the original Flask error handler for the app + :param Exception e: the exception raised while handling the request + """ + if self._has_fr_route(): + try: + return self.handle_error(e) + except Exception as f: + return original_handler(f) + return original_handler(e) + + def _propagate_exceptions(self): + """ + Returns the value of the ``PROPAGATE_EXCEPTIONS`` configuration + value in case it's set, otherwise return true if app.debug or + app.testing is set. This method was deprecated in Flask 2.3 but + we still need it for our error handlers. + """ + rv = current_app.config.get("PROPAGATE_EXCEPTIONS") + if rv is not None: + return rv + return current_app.testing or current_app.debug + + def handle_error(self, e): + """ + Error handler for the API transforms a raised exception into a Flask response, + with the appropriate HTTP status code and body. + + :param Exception e: the raised Exception object + + """ + # When propagate_exceptions is set, do not return the exception to the + # client if a handler is configured for the exception. + if ( + not isinstance(e, HTTPException) + and self._propagate_exceptions() + and not isinstance(e, tuple(self._own_and_child_error_handlers.keys())) + ): + exc_type, exc_value, tb = sys.exc_info() + if exc_value is e: + raise + else: + raise e + + include_message_in_response = current_app.config.get( + "ERROR_INCLUDE_MESSAGE", True + ) + default_data = {} + + headers = Headers() + + for typecheck, handler in self._own_and_child_error_handlers.items(): + if isinstance(e, typecheck): + result = handler(e) + default_data, code, headers = unpack( + result, HTTPStatus.INTERNAL_SERVER_ERROR + ) + break + else: + # Flask docs say: "This signal is not sent for HTTPException or other exceptions that have error handlers + # registered, unless the exception was raised from an error handler." + got_request_exception.send(current_app._get_current_object(), exception=e) + + if isinstance(e, HTTPException): + code = None + if e.code is not None: + code = HTTPStatus(e.code) + elif e.response is not None: + code = HTTPStatus(e.response.status_code) + if include_message_in_response: + default_data = {"message": e.description or code.phrase} + headers = e.get_response().headers + elif self._default_error_handler: + result = self._default_error_handler(e) + default_data, code, headers = unpack( + result, HTTPStatus.INTERNAL_SERVER_ERROR + ) + else: + code = HTTPStatus.INTERNAL_SERVER_ERROR + if include_message_in_response: + default_data = { + "message": code.phrase, + } + + if include_message_in_response: + default_data["message"] = default_data.get("message", str(e)) + + data = getattr(e, "data", default_data) + fallback_mediatype = None + + if code >= HTTPStatus.INTERNAL_SERVER_ERROR: + exc_info = sys.exc_info() + if exc_info[1] is None: + exc_info = None + current_app.log_exception(exc_info) + + elif ( + code == HTTPStatus.NOT_FOUND + and current_app.config.get("RESTX_ERROR_404_HELP", True) + and include_message_in_response + ): + data["message"] = self._help_on_404(data.get("message", None)) + + elif code == HTTPStatus.NOT_ACCEPTABLE and self.default_mediatype is None: + # if we are handling NotAcceptable (406), make sure that + # make_response uses a representation we support as the + # default mediatype (so that make_response doesn't throw + # another NotAcceptable error). + supported_mediatypes = list(self.representations.keys()) + fallback_mediatype = ( + supported_mediatypes[0] if supported_mediatypes else "text/plain" + ) + + # Remove blacklisted headers + for header in HEADERS_BLACKLIST: + headers.pop(header, None) + + resp = self.make_response( + data, code, headers, fallback_mediatype=fallback_mediatype + ) + + if code == HTTPStatus.UNAUTHORIZED: + resp = self.unauthorized(resp) + return resp + + def _help_on_404(self, message=None): + rules = dict( + [ + (RE_RULES.sub("", rule.rule), rule.rule) + for rule in current_app.url_map.iter_rules() + ] + ) + close_matches = difflib.get_close_matches(request.path, rules.keys()) + if close_matches: + # If we already have a message, add punctuation and continue it. + message = "".join( + ( + (message.rstrip(".") + ". ") if message else "", + "You have requested this URI [", + request.path, + "] but did you mean ", + " or ".join((rules[match] for match in close_matches)), + " ?", + ) + ) + return message + + def as_postman(self, urlvars=False, swagger=False): + """ + Serialize the API as Postman collection (v1) + + :param bool urlvars: whether to include or not placeholders for query strings + :param bool swagger: whether to include or not the swagger.json specifications + + """ + return PostmanCollectionV1(self, swagger=swagger).as_dict(urlvars=urlvars) + + @property + def payload(self): + """Store the input payload in the current request context""" + return request.get_json() + + @property + def refresolver(self): + if not self._refresolver: + self._refresolver = RefResolver.from_schema(self.__schema__) + return self._refresolver + + @staticmethod + def _blueprint_setup_add_url_rule_patch( + blueprint_setup, rule, endpoint=None, view_func=None, **options + ): + """ + Method used to patch BlueprintSetupState.add_url_rule for setup + state instance corresponding to this Api instance. Exists primarily + to enable _complete_url's function. + + :param blueprint_setup: The BlueprintSetupState instance (self) + :param rule: A string or callable that takes a string and returns a + string(_complete_url) that is the url rule for the endpoint + being registered + :param endpoint: See BlueprintSetupState.add_url_rule + :param view_func: See BlueprintSetupState.add_url_rule + :param **options: See BlueprintSetupState.add_url_rule + """ + + if callable(rule): + rule = rule(blueprint_setup.url_prefix) + elif blueprint_setup.url_prefix: + rule = blueprint_setup.url_prefix + rule + options.setdefault("subdomain", blueprint_setup.subdomain) + if endpoint is None: + endpoint = endpoint_from_view_func(view_func) + defaults = blueprint_setup.url_defaults + if "defaults" in options: + defaults = dict(defaults, **options.pop("defaults")) + blueprint_setup.app.add_url_rule( + rule, + "%s.%s" % (blueprint_setup.blueprint.name, endpoint), + view_func, + defaults=defaults, + **options + ) + + def _deferred_blueprint_init(self, setup_state): + """ + Synchronize prefix between blueprint/api and registration options, then + perform initialization with setup_state.app :class:`flask.Flask` object. + When a :class:`flask_restx.Api` object is initialized with a blueprint, + this method is recorded on the blueprint to be run when the blueprint is later + registered to a :class:`flask.Flask` object. This method also monkeypatches + BlueprintSetupState.add_url_rule with _blueprint_setup_add_url_rule_patch. + + :param setup_state: The setup state object passed to deferred functions + during blueprint registration + :type setup_state: flask.blueprints.BlueprintSetupState + + """ + + self.blueprint_setup = setup_state + if setup_state.add_url_rule.__name__ != "_blueprint_setup_add_url_rule_patch": + setup_state._original_add_url_rule = setup_state.add_url_rule + setup_state.add_url_rule = MethodType( + Api._blueprint_setup_add_url_rule_patch, setup_state + ) + if not setup_state.first_registration: + raise ValueError("flask-restx blueprints can only be registered once.") + self._init_app(setup_state.app) + + def mediatypes_method(self): + """Return a method that returns a list of mediatypes""" + return lambda resource_cls: self.mediatypes() + [self.default_mediatype] + + def mediatypes(self): + """Returns a list of requested mediatypes sent in the Accept header""" + return [ + h + for h, q in sorted( + request.accept_mimetypes, key=operator.itemgetter(1), reverse=True + ) + ] + + def representation(self, mediatype): + """ + Allows additional representation transformers to be declared for the + api. Transformers are functions that must be decorated with this + method, passing the mediatype the transformer represents. Three + arguments are passed to the transformer: + + * The data to be represented in the response body + * The http status code + * A dictionary of headers + + The transformer should convert the data appropriately for the mediatype + and return a Flask response object. + + Ex:: + + @api.representation('application/xml') + def xml(data, code, headers): + resp = make_response(convert_data_to_xml(data), code) + resp.headers.extend(headers) + return resp + """ + + def wrapper(func): + self.representations[mediatype] = func + return func + + return wrapper + + def unauthorized(self, response): + """Given a response, change it to ask for credentials""" + + if self.serve_challenge_on_401: + realm = current_app.config.get("HTTP_BASIC_AUTH_REALM", "flask-restx") + challenge = '{0} realm="{1}"'.format("Basic", realm) + + response.headers["WWW-Authenticate"] = challenge + return response + + def url_for(self, resource, **values): + """ + Generates a URL to the given resource. + + Works like :func:`flask.url_for`. + """ + endpoint = resource.endpoint + if self.blueprint: + endpoint = "{0}.{1}".format(self.blueprint.name, endpoint) + return url_for(endpoint, **values) + + +class SwaggerView(Resource): + """Render the Swagger specifications as JSON""" + + def get(self): + schema = self.api.__schema__ + return ( + schema, + HTTPStatus.INTERNAL_SERVER_ERROR if "error" in schema else HTTPStatus.OK, + ) + + def mediatypes(self): + return ["application/json"] + + +def mask_parse_error_handler(error): + """When a mask can't be parsed""" + return {"message": "Mask parse error: {0}".format(error)}, HTTPStatus.BAD_REQUEST + + +def mask_error_handler(error): + """When any error occurs on mask""" + return {"message": "Mask error: {0}".format(error)}, HTTPStatus.BAD_REQUEST diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/apidoc.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/apidoc.py new file mode 100644 index 0000000..baa308f --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/apidoc.py @@ -0,0 +1,35 @@ +from flask import url_for, Blueprint, render_template + + +class Apidoc(Blueprint): + """ + Allow to know if the blueprint has already been registered + until https://github.com/mitsuhiko/flask/pull/1301 is merged + """ + + def __init__(self, *args, **kwargs): + self.registered = False + super(Apidoc, self).__init__(*args, **kwargs) + + def register(self, *args, **kwargs): + super(Apidoc, self).register(*args, **kwargs) + self.registered = True + + +apidoc = Apidoc( + "restx_doc", + __name__, + template_folder="templates", + static_folder="static", + static_url_path="/swaggerui", +) + + +@apidoc.add_app_template_global +def swagger_static(filename): + return url_for("restx_doc.static", filename=filename) + + +def ui_for(api): + """Render a SwaggerUI for a given API""" + return render_template("swagger-ui.html", title=api.title, specs_url=api.specs_url) diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/cors.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/cors.py new file mode 100644 index 0000000..9fc9698 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/cors.py @@ -0,0 +1,62 @@ +from datetime import timedelta +from flask import make_response, request, current_app +from functools import update_wrapper + + +def crossdomain( + origin=None, + methods=None, + headers=None, + expose_headers=None, + max_age=21600, + attach_to_all=True, + automatic_options=True, + credentials=False, +): + """ + https://web.archive.org/web/20190128010149/http://flask.pocoo.org/snippets/56/ + """ + if methods is not None: + methods = ", ".join(sorted(x.upper() for x in methods)) + if headers is not None and not isinstance(headers, str): + headers = ", ".join(x.upper() for x in headers) + if expose_headers is not None and not isinstance(expose_headers, str): + expose_headers = ", ".join(x.upper() for x in expose_headers) + if not isinstance(origin, str): + origin = ", ".join(origin) + if isinstance(max_age, timedelta): + max_age = max_age.total_seconds() + + def get_methods(): + if methods is not None: + return methods + + options_resp = current_app.make_default_options_response() + return options_resp.headers["allow"] + + def decorator(f): + def wrapped_function(*args, **kwargs): + if automatic_options and request.method == "OPTIONS": + resp = current_app.make_default_options_response() + else: + resp = make_response(f(*args, **kwargs)) + if not attach_to_all and request.method != "OPTIONS": + return resp + + h = resp.headers + + h["Access-Control-Allow-Origin"] = origin + h["Access-Control-Allow-Methods"] = get_methods() + h["Access-Control-Max-Age"] = str(max_age) + if credentials: + h["Access-Control-Allow-Credentials"] = "true" + if headers is not None: + h["Access-Control-Allow-Headers"] = headers + if expose_headers is not None: + h["Access-Control-Expose-Headers"] = expose_headers + return resp + + f.provide_automatic_options = False + return update_wrapper(wrapped_function, f) + + return decorator diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/errors.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/errors.py new file mode 100644 index 0000000..36d15a9 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/errors.py @@ -0,0 +1,56 @@ +import flask + +from werkzeug.exceptions import HTTPException + +from ._http import HTTPStatus + +__all__ = ( + "abort", + "RestError", + "ValidationError", + "SpecsError", +) + + +def abort(code=HTTPStatus.INTERNAL_SERVER_ERROR, message=None, **kwargs): + """ + Properly abort the current request. + + Raise a `HTTPException` for the given status `code`. + Attach any keyword arguments to the exception for later processing. + + :param int code: The associated HTTP status code + :param str message: An optional details message + :param kwargs: Any additional data to pass to the error payload + :raise HTTPException: + """ + try: + flask.abort(code) + except HTTPException as e: + if message: + kwargs["message"] = str(message) + if kwargs: + e.data = kwargs + raise + + +class RestError(Exception): + """Base class for all Flask-RESTX Errors""" + + def __init__(self, msg): + self.msg = msg + + def __str__(self): + return self.msg + + +class ValidationError(RestError): + """A helper class for validation errors.""" + + pass + + +class SpecsError(RestError): + """A helper class for incoherent specifications.""" + + pass diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/fields.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/fields.py new file mode 100644 index 0000000..97957f7 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/fields.py @@ -0,0 +1,902 @@ +import re +import fnmatch +import inspect + +from calendar import timegm +from datetime import date, datetime +from decimal import Decimal, ROUND_HALF_EVEN +from email.utils import formatdate + +from urllib.parse import urlparse, urlunparse + +from flask import url_for, request +from werkzeug.utils import cached_property + +from .inputs import ( + date_from_iso8601, + datetime_from_iso8601, + datetime_from_rfc822, + boolean, +) +from .errors import RestError +from .marshalling import marshal +from .utils import camel_to_dash, not_none + + +__all__ = ( + "Raw", + "String", + "FormattedString", + "Url", + "DateTime", + "Date", + "Boolean", + "Integer", + "Float", + "Arbitrary", + "Fixed", + "Nested", + "List", + "ClassName", + "Polymorph", + "Wildcard", + "StringMixin", + "MinMaxMixin", + "NumberMixin", + "MarshallingError", +) + + +class MarshallingError(RestError): + """ + This is an encapsulating Exception in case of marshalling error. + """ + + def __init__(self, underlying_exception): + # just put the contextual representation of the error to hint on what + # went wrong without exposing internals + super(MarshallingError, self).__init__(str(underlying_exception)) + + +def is_indexable_but_not_string(obj): + return not hasattr(obj, "strip") and hasattr(obj, "__iter__") + + +def is_integer_indexable(obj): + return isinstance(obj, list) or isinstance(obj, tuple) + + +def get_value(key, obj, default=None): + """Helper for pulling a keyed value off various types of objects""" + if isinstance(key, int): + return _get_value_for_key(key, obj, default) + elif callable(key): + return key(obj) + else: + return _get_value_for_keys(key.split("."), obj, default) + + +def _get_value_for_keys(keys, obj, default): + if len(keys) == 1: + return _get_value_for_key(keys[0], obj, default) + else: + return _get_value_for_keys( + keys[1:], _get_value_for_key(keys[0], obj, default), default + ) + + +def _get_value_for_key(key, obj, default): + if is_indexable_but_not_string(obj): + try: + return obj[key] + except (IndexError, TypeError, KeyError): + pass + if is_integer_indexable(obj): + try: + return obj[int(key)] + except (IndexError, TypeError, ValueError): + pass + return getattr(obj, key, default) + + +def to_marshallable_type(obj): + """ + Helper for converting an object to a dictionary only if it is not + dictionary already or an indexable object nor a simple type + """ + if obj is None: + return None # make it idempotent for None + + if hasattr(obj, "__marshallable__"): + return obj.__marshallable__() + + if hasattr(obj, "__getitem__"): + return obj # it is indexable it is ok + + return dict(obj.__dict__) + + +class Raw(object): + """ + Raw provides a base field class from which others should extend. It + applies no formatting by default, and should only be used in cases where + data does not need to be formatted before being serialized. Fields should + throw a :class:`MarshallingError` in case of parsing problem. + + :param default: The default value for the field, if no value is + specified. + :param attribute: If the public facing value differs from the internal + value, use this to retrieve a different attribute from the response + than the publicly named value. + :param str title: The field title (for documentation purpose) + :param str description: The field description (for documentation purpose) + :param bool required: Is the field required ? + :param bool readonly: Is the field read only ? (for documentation purpose) + :param example: An optional data example (for documentation purpose) + :param callable mask: An optional mask function to be applied to output + """ + + #: The JSON/Swagger schema type + __schema_type__ = "object" + #: The JSON/Swagger schema format + __schema_format__ = None + #: An optional JSON/Swagger schema example + __schema_example__ = None + + def __init__( + self, + default=None, + attribute=None, + title=None, + description=None, + required=None, + readonly=None, + example=None, + mask=None, + **kwargs + ): + self.attribute = attribute + self.default = default + self.title = title + self.description = description + self.required = required + self.readonly = readonly + self.example = example if example is not None else self.__schema_example__ + self.mask = mask + + def format(self, value): + """ + Formats a field's value. No-op by default - field classes that + modify how the value of existing object keys should be presented should + override this and apply the appropriate formatting. + + :param value: The value to format + :raises MarshallingError: In case of formatting problem + + Ex:: + + class TitleCase(Raw): + def format(self, value): + return unicode(value).title() + """ + return value + + def output(self, key, obj, **kwargs): + """ + Pulls the value for the given key from the object, applies the + field's formatting and returns the result. If the key is not found + in the object, returns the default value. Field classes that create + values which do not require the existence of the key in the object + should override this and return the desired value. + + :raises MarshallingError: In case of formatting problem + """ + + value = get_value(key if self.attribute is None else self.attribute, obj) + + if value is None: + default = self._v("default") + return self.format(default) if default else default + + try: + data = self.format(value) + except MarshallingError as e: + msg = 'Unable to marshal field "{0}" value "{1}": {2}'.format( + key, value, str(e) + ) + raise MarshallingError(msg) + return self.mask.apply(data) if self.mask else data + + def _v(self, key): + """Helper for getting a value from attribute allowing callable""" + value = getattr(self, key) + return value() if callable(value) else value + + @cached_property + def __schema__(self): + return not_none(self.schema()) + + def schema(self): + return { + "type": self.__schema_type__, + "format": self.__schema_format__, + "title": self.title, + "description": self.description, + "readOnly": self.readonly, + "default": self._v("default"), + "example": self.example, + } + + +class Nested(Raw): + """ + Allows you to nest one set of fields inside another. + See :ref:`nested-field` for more information + + :param dict model: The model dictionary to nest + :param bool allow_null: Whether to return None instead of a dictionary + with null keys, if a nested dictionary has all-null keys + :param bool skip_none: Optional key will be used to eliminate inner fields + which value is None or the inner field's key not + exist in data + :param kwargs: If ``default`` keyword argument is present, a nested + dictionary will be marshaled as its value if nested dictionary is + all-null keys (e.g. lets you return an empty JSON object instead of + null) + """ + + __schema_type__ = None + + def __init__( + self, model, allow_null=False, skip_none=False, as_list=False, **kwargs + ): + self.model = model + self.as_list = as_list + self.allow_null = allow_null + self.skip_none = skip_none + super(Nested, self).__init__(**kwargs) + + @property + def nested(self): + return getattr(self.model, "resolved", self.model) + + def output(self, key, obj, ordered=False, **kwargs): + value = get_value(key if self.attribute is None else self.attribute, obj) + if value is None: + if self.allow_null: + return None + elif self.default is not None: + return self.default + + return marshal(value, self.nested, skip_none=self.skip_none, ordered=ordered) + + def schema(self): + schema = super(Nested, self).schema() + ref = "#/definitions/{0}".format(self.nested.name) + + if self.as_list: + schema["type"] = "array" + schema["items"] = {"$ref": ref} + elif any(schema.values()): + # There is already some properties in the schema + allOf = schema.get("allOf", []) + allOf.append({"$ref": ref}) + schema["allOf"] = allOf + else: + schema["$ref"] = ref + return schema + + def clone(self, mask=None): + kwargs = self.__dict__.copy() + model = kwargs.pop("model") + if mask: + model = mask.apply(model.resolved if hasattr(model, "resolved") else model) + return self.__class__(model, **kwargs) + + +class List(Raw): + """ + Field for marshalling lists of other fields. + + See :ref:`list-field` for more information. + + :param cls_or_instance: The field type the list will contain. + """ + + def __init__(self, cls_or_instance, **kwargs): + self.min_items = kwargs.pop("min_items", None) + self.max_items = kwargs.pop("max_items", None) + self.unique = kwargs.pop("unique", None) + super(List, self).__init__(**kwargs) + error_msg = "The type of the list elements must be a subclass of fields.Raw" + if isinstance(cls_or_instance, type): + if not issubclass(cls_or_instance, Raw): + raise MarshallingError(error_msg) + self.container = cls_or_instance() + else: + if not isinstance(cls_or_instance, Raw): + raise MarshallingError(error_msg) + self.container = cls_or_instance + + def format(self, value): + # Convert all instances in typed list to container type + if isinstance(value, set): + value = list(value) + + is_nested = isinstance(self.container, Nested) or type(self.container) is Raw + + def is_attr(val): + return self.container.attribute and hasattr(val, self.container.attribute) + + if value is None: + return [] + return [ + self.container.output( + idx, + val + if (isinstance(val, dict) or is_attr(val)) and not is_nested + else value, + ) + for idx, val in enumerate(value) + ] + + def output(self, key, data, ordered=False, **kwargs): + value = get_value(key if self.attribute is None else self.attribute, data) + # we cannot really test for external dict behavior + if is_indexable_but_not_string(value) and not isinstance(value, dict): + return self.format(value) + + if value is None: + return self._v("default") + + return [marshal(value, self.container.nested)] + + def schema(self): + schema = super(List, self).schema() + schema.update( + minItems=self._v("min_items"), + maxItems=self._v("max_items"), + uniqueItems=self._v("unique"), + ) + schema["type"] = "array" + schema["items"] = self.container.__schema__ + return schema + + def clone(self, mask=None): + kwargs = self.__dict__.copy() + model = kwargs.pop("container") + if mask: + model = mask.apply(model) + return self.__class__(model, **kwargs) + + +class StringMixin(object): + __schema_type__ = "string" + + def __init__(self, *args, **kwargs): + self.min_length = kwargs.pop("min_length", None) + self.max_length = kwargs.pop("max_length", None) + self.pattern = kwargs.pop("pattern", None) + super(StringMixin, self).__init__(*args, **kwargs) + + def schema(self): + schema = super(StringMixin, self).schema() + schema.update( + minLength=self._v("min_length"), + maxLength=self._v("max_length"), + pattern=self._v("pattern"), + ) + return schema + + +class MinMaxMixin(object): + def __init__(self, *args, **kwargs): + self.minimum = kwargs.pop("min", None) + self.exclusiveMinimum = kwargs.pop("exclusiveMin", None) + self.maximum = kwargs.pop("max", None) + self.exclusiveMaximum = kwargs.pop("exclusiveMax", None) + super(MinMaxMixin, self).__init__(*args, **kwargs) + + def schema(self): + schema = super(MinMaxMixin, self).schema() + schema.update( + minimum=self._v("minimum"), + exclusiveMinimum=self._v("exclusiveMinimum"), + maximum=self._v("maximum"), + exclusiveMaximum=self._v("exclusiveMaximum"), + ) + return schema + + +class NumberMixin(MinMaxMixin): + __schema_type__ = "number" + + def __init__(self, *args, **kwargs): + self.multiple = kwargs.pop("multiple", None) + super(NumberMixin, self).__init__(*args, **kwargs) + + def schema(self): + schema = super(NumberMixin, self).schema() + schema.update(multipleOf=self._v("multiple")) + return schema + + +class String(StringMixin, Raw): + """ + Marshal a value as a string. + """ + + def __init__(self, *args, **kwargs): + self.enum = kwargs.pop("enum", None) + self.discriminator = kwargs.pop("discriminator", None) + super(String, self).__init__(*args, **kwargs) + self.required = self.discriminator or self.required + + def format(self, value): + try: + return str(value) + except ValueError as ve: + raise MarshallingError(ve) + + def schema(self): + enum = self._v("enum") + schema = super(String, self).schema() + if enum: + schema.update(enum=enum) + if enum and schema["example"] is None: + schema["example"] = enum[0] + return schema + + +class Integer(NumberMixin, Raw): + """ + Field for outputting an integer value. + + :param int default: The default value for the field, if no value is specified. + """ + + __schema_type__ = "integer" + + def format(self, value): + try: + if value is None: + return self.default + return int(value) + except (ValueError, TypeError) as ve: + raise MarshallingError(ve) + + +class Float(NumberMixin, Raw): + """ + A double as IEEE-754 double precision. + + ex : 3.141592653589793 3.1415926535897933e-06 3.141592653589793e+24 nan inf -inf + """ + + def format(self, value): + try: + if value is None: + return self.default + return float(value) + except (ValueError, TypeError) as ve: + raise MarshallingError(ve) + + +class Arbitrary(NumberMixin, Raw): + """ + A floating point number with an arbitrary precision. + + ex: 634271127864378216478362784632784678324.23432 + """ + + def format(self, value): + return str(Decimal(value)) + + +ZERO = Decimal() + + +class Fixed(NumberMixin, Raw): + """ + A decimal number with a fixed precision. + """ + + def __init__(self, decimals=5, **kwargs): + super(Fixed, self).__init__(**kwargs) + self.precision = Decimal("0." + "0" * (decimals - 1) + "1") + + def format(self, value): + dvalue = Decimal(value) + if not dvalue.is_normal() and dvalue != ZERO: + raise MarshallingError("Invalid Fixed precision number.") + return str(dvalue.quantize(self.precision, rounding=ROUND_HALF_EVEN)) + + +class Boolean(Raw): + """ + Field for outputting a boolean value. + + Empty collections such as ``""``, ``{}``, ``[]``, etc. will be converted to ``False``. + """ + + __schema_type__ = "boolean" + + def format(self, value): + return boolean(value) + + +class DateTime(MinMaxMixin, Raw): + """ + Return a formatted datetime string in UTC. Supported formats are RFC 822 and ISO 8601. + + See :func:`email.utils.formatdate` for more info on the RFC 822 format. + + See :meth:`datetime.datetime.isoformat` for more info on the ISO 8601 format. + + :param str dt_format: ``rfc822`` or ``iso8601`` + """ + + __schema_type__ = "string" + __schema_format__ = "date-time" + + def __init__(self, dt_format="iso8601", **kwargs): + super(DateTime, self).__init__(**kwargs) + self.dt_format = dt_format + + def parse(self, value): + if value is None: + return None + elif isinstance(value, str): + parser = ( + datetime_from_iso8601 + if self.dt_format == "iso8601" + else datetime_from_rfc822 + ) + return parser(value) + elif isinstance(value, datetime): + return value + elif isinstance(value, date): + return datetime(value.year, value.month, value.day) + else: + raise ValueError("Unsupported DateTime format") + + def format(self, value): + try: + value = self.parse(value) + if self.dt_format == "iso8601": + return self.format_iso8601(value) + elif self.dt_format == "rfc822": + return self.format_rfc822(value) + else: + raise MarshallingError("Unsupported date format %s" % self.dt_format) + except (AttributeError, ValueError) as e: + raise MarshallingError(e) + + def format_rfc822(self, dt): + """ + Turn a datetime object into a formatted date. + + :param datetime dt: The datetime to transform + :return: A RFC 822 formatted date string + """ + return formatdate(timegm(dt.utctimetuple())) + + def format_iso8601(self, dt): + """ + Turn a datetime object into an ISO8601 formatted date. + + :param datetime dt: The datetime to transform + :return: A ISO 8601 formatted date string + """ + return dt.isoformat() + + def _for_schema(self, name): + value = self.parse(self._v(name)) + return self.format(value) if value else None + + def schema(self): + schema = super(DateTime, self).schema() + schema["default"] = self._for_schema("default") + schema["minimum"] = self._for_schema("minimum") + schema["maximum"] = self._for_schema("maximum") + return schema + + +class Date(DateTime): + """ + Return a formatted date string in UTC in ISO 8601. + + See :meth:`datetime.date.isoformat` for more info on the ISO 8601 format. + """ + + __schema_format__ = "date" + + def __init__(self, **kwargs): + kwargs.pop("dt_format", None) + super(Date, self).__init__(dt_format="iso8601", **kwargs) + + def parse(self, value): + if value is None: + return None + elif isinstance(value, str): + return date_from_iso8601(value) + elif isinstance(value, datetime): + return value.date() + elif isinstance(value, date): + return value + else: + raise ValueError("Unsupported Date format") + + +class Url(StringMixin, Raw): + """ + A string representation of a Url + + :param str endpoint: Endpoint name. If endpoint is ``None``, ``request.endpoint`` is used instead + :param bool absolute: If ``True``, ensures that the generated urls will have the hostname included + :param str scheme: URL scheme specifier (e.g. ``http``, ``https``) + """ + + def __init__(self, endpoint=None, absolute=False, scheme=None, **kwargs): + super(Url, self).__init__(**kwargs) + self.endpoint = endpoint + self.absolute = absolute + self.scheme = scheme + + def output(self, key, obj, **kwargs): + try: + data = to_marshallable_type(obj) + endpoint = self.endpoint if self.endpoint is not None else request.endpoint + o = urlparse(url_for(endpoint, _external=self.absolute, **data)) + if self.absolute: + scheme = self.scheme if self.scheme is not None else o.scheme + return urlunparse((scheme, o.netloc, o.path, "", "", "")) + return urlunparse(("", "", o.path, "", "", "")) + except TypeError as te: + raise MarshallingError(te) + + +class FormattedString(StringMixin, Raw): + """ + FormattedString is used to interpolate other values from + the response into this field. The syntax for the source string is + the same as the string :meth:`~str.format` method from the python + stdlib. + + Ex:: + + fields = { + 'name': fields.String, + 'greeting': fields.FormattedString("Hello {name}") + } + data = { + 'name': 'Doug', + } + marshal(data, fields) + + :param str src_str: the string to format with the other values from the response. + """ + + def __init__(self, src_str, **kwargs): + super(FormattedString, self).__init__(**kwargs) + self.src_str = str(src_str) + + def output(self, key, obj, **kwargs): + try: + data = to_marshallable_type(obj) + return self.src_str.format(**data) + except (TypeError, IndexError) as error: + raise MarshallingError(error) + + +class ClassName(String): + """ + Return the serialized object class name as string. + + :param bool dash: If `True`, transform CamelCase to kebab_case. + """ + + def __init__(self, dash=False, **kwargs): + super(ClassName, self).__init__(**kwargs) + self.dash = dash + + def output(self, key, obj, **kwargs): + classname = obj.__class__.__name__ + if classname == "dict": + return "object" + return camel_to_dash(classname) if self.dash else classname + + +class Polymorph(Nested): + """ + A Nested field handling inheritance. + + Allows you to specify a mapping between Python classes and fields specifications. + + .. code-block:: python + + mapping = { + Child1: child1_fields, + Child2: child2_fields, + } + + fields = api.model('Thing', { + owner: fields.Polymorph(mapping) + }) + + :param dict mapping: Maps classes to their model/fields representation + """ + + def __init__(self, mapping, required=False, **kwargs): + self.mapping = mapping + parent = self.resolve_ancestor(list(mapping.values())) + super(Polymorph, self).__init__(parent, allow_null=not required, **kwargs) + + def output(self, key, obj, ordered=False, **kwargs): + # Copied from upstream NestedField + value = get_value(key if self.attribute is None else self.attribute, obj) + if value is None: + if self.allow_null: + return None + elif self.default is not None: + return self.default + + # Handle mappings + if not hasattr(value, "__class__"): + raise ValueError("Polymorph field only accept class instances") + + candidates = [ + fields for cls, fields in self.mapping.items() if type(value) == cls + ] + + if len(candidates) <= 0: + raise ValueError("Unknown class: " + value.__class__.__name__) + elif len(candidates) > 1: + raise ValueError( + "Unable to determine a candidate for: " + value.__class__.__name__ + ) + else: + return marshal( + value, candidates[0].resolved, mask=self.mask, ordered=ordered + ) + + def resolve_ancestor(self, models): + """ + Resolve the common ancestor for all models. + + Assume there is only one common ancestor. + """ + ancestors = [m.ancestors for m in models] + candidates = set.intersection(*ancestors) + if len(candidates) != 1: + field_names = [f.name for f in models] + raise ValueError( + "Unable to determine the common ancestor for: " + ", ".join(field_names) + ) + + parent_name = candidates.pop() + return models[0].get_parent(parent_name) + + def clone(self, mask=None): + data = self.__dict__.copy() + mapping = data.pop("mapping") + for field in ("allow_null", "model"): + data.pop(field, None) + + data["mask"] = mask + return Polymorph(mapping, **data) + + +class Wildcard(Raw): + """ + Field for marshalling list of "unkown" fields. + + :param cls_or_instance: The field type the list will contain. + """ + + exclude = set() + # cache the flat object + _flat = None + _obj = None + _cache = set() + _last = None + + def __init__(self, cls_or_instance, **kwargs): + super(Wildcard, self).__init__(**kwargs) + error_msg = "The type of the wildcard elements must be a subclass of fields.Raw" + if isinstance(cls_or_instance, type): + if not issubclass(cls_or_instance, Raw): + raise MarshallingError(error_msg) + self.container = cls_or_instance() + else: + if not isinstance(cls_or_instance, Raw): + raise MarshallingError(error_msg) + self.container = cls_or_instance + + def _flatten(self, obj): + if obj is None: + return None + if obj == self._obj and self._flat is not None: + return self._flat + if isinstance(obj, dict): + self._flat = [x for x in obj.items()] + else: + + def __match_attributes(attribute): + attr_name, attr_obj = attribute + if inspect.isroutine(attr_obj) or ( + attr_name.startswith("__") and attr_name.endswith("__") + ): + return False + return True + + attributes = inspect.getmembers(obj) + self._flat = [x for x in attributes if __match_attributes(x)] + + self._cache = set() + self._obj = obj + return self._flat + + @property + def key(self): + return self._last + + def reset(self): + self.exclude = set() + self._flat = None + self._obj = None + self._cache = set() + self._last = None + + def output(self, key, obj, ordered=False): + value = None + reg = fnmatch.translate(key) + + if self._flatten(obj): + while True: + try: + # we are using pop() so that we don't + # loop over the whole object every time dropping the + # complexity to O(n) + if ordered: + # Get first element if respecting order + (objkey, val) = self._flat.pop(0) + else: + # Previous default retained + (objkey, val) = self._flat.pop() + if ( + objkey not in self._cache + and objkey not in self.exclude + and re.match(reg, objkey, re.IGNORECASE) + ): + value = val + self._cache.add(objkey) + self._last = objkey + break + except IndexError: + break + + if value is None: + if self.default is not None: + return self.container.format(self.default) + return None + + if isinstance(self.container, Nested): + return marshal( + value, + self.container.nested, + skip_none=self.container.skip_none, + ordered=ordered, + ) + return self.container.format(value) + + def schema(self): + schema = super(Wildcard, self).schema() + schema["type"] = "object" + schema["additionalProperties"] = self.container.__schema__ + return schema + + def clone(self): + kwargs = self.__dict__.copy() + model = kwargs.pop("container") + return self.__class__(model, **kwargs) diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/inputs.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/inputs.py new file mode 100644 index 0000000..912ae16 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/inputs.py @@ -0,0 +1,615 @@ +""" +This module provide some helpers for advanced types parsing. + +You can define you own parser using the same pattern: + +.. code-block:: python + + def my_type(value): + if not condition: + raise ValueError('This is not my type') + return parse(value) + + # Swagger documentation + my_type.__schema__ = {'type': 'string', 'format': 'my-custom-format'} + +The last line allows you to document properly the type in the Swagger documentation. +""" + +import re +import socket + +from datetime import datetime, time, timedelta +from email.utils import parsedate_tz, mktime_tz +from urllib.parse import urlparse + +import aniso8601 +import pytz + +# Constants for upgrading date-based intervals to full datetimes. +START_OF_DAY = time(0, 0, 0, tzinfo=pytz.UTC) +END_OF_DAY = time(23, 59, 59, 999999, tzinfo=pytz.UTC) + + +netloc_regex = re.compile( + r"(?:(?P[^:@]+?(?::[^:@]*?)?)@)?" # basic auth + r"(?:" + r"(?Plocalhost)|" # localhost... + r"(?P\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})|" # ...or ipv4 + r"(?:\[?(?P[A-F0-9]*:[A-F0-9:]+)\]?)|" # ...or ipv6 + r"(?P(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?))" # domain... + r")" + r"(?::(?P\d+))?" # optional port + r"$", + re.IGNORECASE, +) + + +email_regex = re.compile( + r"^" "(?P[^@]*[^@.])" r"@" r"(?P[^@\.]+(?:\.[^@\.]+)*)" r"$", + re.IGNORECASE, +) + +time_regex = re.compile(r"\d{2}:\d{2}") + + +def ipv4(value): + """Validate an IPv4 address""" + try: + socket.inet_aton(value) + if value.count(".") == 3: + return value + except socket.error: + pass + raise ValueError("{0} is not a valid ipv4 address".format(value)) + + +ipv4.__schema__ = {"type": "string", "format": "ipv4"} + + +def ipv6(value): + """Validate an IPv6 address""" + try: + socket.inet_pton(socket.AF_INET6, value) + return value + except socket.error: + raise ValueError("{0} is not a valid ipv4 address".format(value)) + + +ipv6.__schema__ = {"type": "string", "format": "ipv6"} + + +def ip(value): + """Validate an IP address (both IPv4 and IPv6)""" + try: + return ipv4(value) + except ValueError: + pass + try: + return ipv6(value) + except ValueError: + raise ValueError("{0} is not a valid ip".format(value)) + + +ip.__schema__ = {"type": "string", "format": "ip"} + + +class URL(object): + """ + Validate an URL. + + Example:: + + parser = reqparse.RequestParser() + parser.add_argument('url', type=inputs.URL(schemes=['http', 'https'])) + + Input to the ``URL`` argument will be rejected + if it does not match an URL with specified constraints. + If ``check`` is True it will also be rejected if the domain does not exists. + + :param bool check: Check the domain exists (perform a DNS resolution) + :param bool ip: Allow IP (both ipv4/ipv6) as domain + :param bool local: Allow localhost (both string or ip) as domain + :param bool port: Allow a port to be present + :param bool auth: Allow authentication to be present + :param list|tuple schemes: Restrict valid schemes to this list + :param list|tuple domains: Restrict valid domains to this list + :param list|tuple exclude: Exclude some domains + """ + + def __init__( + self, + check=False, + ip=False, + local=False, + port=False, + auth=False, + schemes=None, + domains=None, + exclude=None, + ): + self.check = check + self.ip = ip + self.local = local + self.port = port + self.auth = auth + self.schemes = schemes + self.domains = domains + self.exclude = exclude + + def error(self, value, details=None): + msg = "{0} is not a valid URL" + if details: + msg = ". ".join((msg, details)) + raise ValueError(msg.format(value)) + + def __call__(self, value): + parsed = urlparse(value) + netloc_match = netloc_regex.match(parsed.netloc) + if not all((parsed.scheme, parsed.netloc)): + if netloc_regex.match( + parsed.netloc or parsed.path.split("/", 1)[0].split("?", 1)[0] + ): + self.error(value, "Did you mean: http://{0}") + self.error(value) + if parsed.scheme and self.schemes and parsed.scheme not in self.schemes: + self.error(value, "Protocol is not allowed") + if not netloc_match: + self.error(value) + data = netloc_match.groupdict() + if data["ipv4"] or data["ipv6"]: + if not self.ip: + self.error(value, "IP is not allowed") + else: + try: + ip(data["ipv4"] or data["ipv6"]) + except ValueError as e: + self.error(value, str(e)) + if not self.local: + if data["ipv4"] and data["ipv4"].startswith("127."): + self.error(value, "Localhost is not allowed") + elif data["ipv6"] == "::1": + self.error(value, "Localhost is not allowed") + if self.check: + pass + if data["auth"] and not self.auth: + self.error(value, "Authentication is not allowed") + if data["localhost"] and not self.local: + self.error(value, "Localhost is not allowed") + if data["port"]: + if not self.port: + self.error(value, "Custom port is not allowed") + else: + port = int(data["port"]) + if not 0 < port < 65535: + self.error(value, "Port is out of range") + if data["domain"]: + if self.domains and data["domain"] not in self.domains: + self.error(value, "Domain is not allowed") + elif self.exclude and data["domain"] in self.exclude: + self.error(value, "Domain is not allowed") + if self.check: + try: + socket.getaddrinfo(data["domain"], None) + except socket.error: + self.error(value, "Domain does not exists") + return value + + @property + def __schema__(self): + return { + "type": "string", + "format": "url", + } + + +#: Validate an URL +#: +#: Legacy validator, allows, auth, port, ip and local +#: Only allows schemes 'http', 'https', 'ftp' and 'ftps' +url = URL( + ip=True, auth=True, port=True, local=True, schemes=("http", "https", "ftp", "ftps") +) + + +class email(object): + """ + Validate an email. + + Example:: + + parser = reqparse.RequestParser() + parser.add_argument('email', type=inputs.email(dns=True)) + + Input to the ``email`` argument will be rejected if it does not match an email + and if domain does not exists. + + :param bool check: Check the domain exists (perform a DNS resolution) + :param bool ip: Allow IP (both ipv4/ipv6) as domain + :param bool local: Allow localhost (both string or ip) as domain + :param list|tuple domains: Restrict valid domains to this list + :param list|tuple exclude: Exclude some domains + """ + + def __init__(self, check=False, ip=False, local=False, domains=None, exclude=None): + self.check = check + self.ip = ip + self.local = local + self.domains = domains + self.exclude = exclude + + def error(self, value, msg=None): + msg = msg or "{0} is not a valid email" + raise ValueError(msg.format(value)) + + def is_ip(self, value): + try: + ip(value) + return True + except ValueError: + return False + + def __call__(self, value): + match = email_regex.match(value) + if not match or ".." in value: + self.error(value) + server = match.group("server") + if self.check: + try: + socket.getaddrinfo(server, None) + except socket.error: + self.error(value) + if self.domains and server not in self.domains: + self.error(value, "{0} does not belong to the authorized domains") + if self.exclude and server in self.exclude: + self.error(value, "{0} belongs to a forbidden domain") + if not self.local and ( + server in ("localhost", "::1") or server.startswith("127.") + ): + self.error(value) + if self.is_ip(server) and not self.ip: + self.error(value) + return value + + @property + def __schema__(self): + return { + "type": "string", + "format": "email", + } + + +class regex(object): + """ + Validate a string based on a regular expression. + + Example:: + + parser = reqparse.RequestParser() + parser.add_argument('example', type=inputs.regex('^[0-9]+$')) + + Input to the ``example`` argument will be rejected if it contains anything + but numbers. + + :param str pattern: The regular expression the input must match + """ + + def __init__(self, pattern): + self.pattern = pattern + self.re = re.compile(pattern) + + def __call__(self, value): + if not self.re.search(value): + message = 'Value does not match pattern: "{0}"'.format(self.pattern) + raise ValueError(message) + return value + + def __deepcopy__(self, memo): + return regex(self.pattern) + + @property + def __schema__(self): + return { + "type": "string", + "pattern": self.pattern, + } + + +def _normalize_interval(start, end, value): + """ + Normalize datetime intervals. + + Given a pair of datetime.date or datetime.datetime objects, + returns a 2-tuple of tz-aware UTC datetimes spanning the same interval. + + For datetime.date objects, the returned interval starts at 00:00:00.0 + on the first date and ends at 00:00:00.0 on the second. + + Naive datetimes are upgraded to UTC. + + Timezone-aware datetimes are normalized to the UTC tzdata. + + Params: + - start: A date or datetime + - end: A date or datetime + """ + if not isinstance(start, datetime): + start = datetime.combine(start, START_OF_DAY) + end = datetime.combine(end, START_OF_DAY) + + if start.tzinfo is None: + start = pytz.UTC.localize(start) + end = pytz.UTC.localize(end) + else: + start = start.astimezone(pytz.UTC) + end = end.astimezone(pytz.UTC) + + return start, end + + +def _expand_datetime(start, value): + if not isinstance(start, datetime): + # Expand a single date object to be the interval spanning + # that entire day. + end = start + timedelta(days=1) + else: + # Expand a datetime based on the finest resolution provided + # in the original input string. + time = value.split("T")[1] + time_without_offset = re.sub("[+-].+", "", time) + num_separators = time_without_offset.count(":") + if num_separators == 0: + # Hour resolution + end = start + timedelta(hours=1) + elif num_separators == 1: + # Minute resolution: + end = start + timedelta(minutes=1) + else: + # Second resolution + end = start + timedelta(seconds=1) + + return end + + +def _parse_interval(value): + """ + Do some nasty try/except voodoo to get some sort of datetime + object(s) out of the string. + """ + try: + return sorted(aniso8601.parse_interval(value)) + except ValueError: + try: + return aniso8601.parse_datetime(value), None + except ValueError: + return aniso8601.parse_date(value), None + + +def iso8601interval(value, argument="argument"): + """ + Parses ISO 8601-formatted datetime intervals into tuples of datetimes. + + Accepts both a single date(time) or a full interval using either start/end + or start/duration notation, with the following behavior: + + - Intervals are defined as inclusive start, exclusive end + - Single datetimes are translated into the interval spanning the + largest resolution not specified in the input value, up to the day. + - The smallest accepted resolution is 1 second. + - All timezones are accepted as values; returned datetimes are + localized to UTC. Naive inputs and date inputs will are assumed UTC. + + Examples:: + + "2013-01-01" -> datetime(2013, 1, 1), datetime(2013, 1, 2) + "2013-01-01T12" -> datetime(2013, 1, 1, 12), datetime(2013, 1, 1, 13) + "2013-01-01/2013-02-28" -> datetime(2013, 1, 1), datetime(2013, 2, 28) + "2013-01-01/P3D" -> datetime(2013, 1, 1), datetime(2013, 1, 4) + "2013-01-01T12:00/PT30M" -> datetime(2013, 1, 1, 12), datetime(2013, 1, 1, 12, 30) + "2013-01-01T06:00/2013-01-01T12:00" -> datetime(2013, 1, 1, 6), datetime(2013, 1, 1, 12) + + :param str value: The ISO8601 date time as a string + :return: Two UTC datetimes, the start and the end of the specified interval + :rtype: A tuple (datetime, datetime) + :raises ValueError: if the interval is invalid. + """ + if not value: + raise ValueError("Expected a valid ISO8601 date/time interval.") + + try: + start, end = _parse_interval(value) + + if end is None: + end = _expand_datetime(start, value) + + start, end = _normalize_interval(start, end, value) + + except ValueError: + msg = ( + "Invalid {arg}: {value}. {arg} must be a valid ISO8601 date/time interval." + ) + raise ValueError(msg.format(arg=argument, value=value)) + + return start, end + + +iso8601interval.__schema__ = {"type": "string", "format": "iso8601-interval"} + + +def date(value): + """Parse a valid looking date in the format YYYY-mm-dd""" + date = datetime.strptime(value, "%Y-%m-%d") + return date + + +date.__schema__ = {"type": "string", "format": "date"} + + +def _get_integer(value): + try: + return int(value) + except (TypeError, ValueError): + raise ValueError("{0} is not a valid integer".format(value)) + + +def natural(value, argument="argument"): + """Restrict input type to the natural numbers (0, 1, 2, 3...)""" + value = _get_integer(value) + if value < 0: + msg = "Invalid {arg}: {value}. {arg} must be a non-negative integer" + raise ValueError(msg.format(arg=argument, value=value)) + return value + + +natural.__schema__ = {"type": "integer", "minimum": 0} + + +def positive(value, argument="argument"): + """Restrict input type to the positive integers (1, 2, 3...)""" + value = _get_integer(value) + if value < 1: + msg = "Invalid {arg}: {value}. {arg} must be a positive integer" + raise ValueError(msg.format(arg=argument, value=value)) + return value + + +positive.__schema__ = {"type": "integer", "minimum": 0, "exclusiveMinimum": True} + + +class int_range(object): + """Restrict input to an integer in a range (inclusive)""" + + def __init__(self, low, high, argument="argument"): + self.low = low + self.high = high + self.argument = argument + + def __call__(self, value): + value = _get_integer(value) + if value < self.low or value > self.high: + msg = "Invalid {arg}: {val}. {arg} must be within the range {lo} - {hi}" + raise ValueError( + msg.format(arg=self.argument, val=value, lo=self.low, hi=self.high) + ) + return value + + @property + def __schema__(self): + return { + "type": "integer", + "minimum": self.low, + "maximum": self.high, + } + + +def boolean(value): + """ + Parse the string ``"true"`` or ``"false"`` as a boolean (case insensitive). + + Also accepts ``"1"`` and ``"0"`` as ``True``/``False`` (respectively). + + If the input is from the request JSON body, the type is already a native python boolean, + and will be passed through without further parsing. + + :raises ValueError: if the boolean value is invalid + """ + if isinstance(value, bool): + return value + + if value is None: + raise ValueError("boolean type must be non-null") + elif not value: + return False + value = str(value).lower() + if value in ( + "true", + "1", + "on", + ): + return True + if value in ( + "false", + "0", + ): + return False + raise ValueError("Invalid literal for boolean(): {0}".format(value)) + + +boolean.__schema__ = {"type": "boolean"} + + +def datetime_from_rfc822(value): + """ + Turns an RFC822 formatted date into a datetime object. + + Example:: + + inputs.datetime_from_rfc822('Wed, 02 Oct 2002 08:00:00 EST') + + :param str value: The RFC822-complying string to transform + :return: The parsed datetime + :rtype: datetime + :raises ValueError: if value is an invalid date literal + + """ + raw = value + if not time_regex.search(value): + value = " ".join((value, "00:00:00")) + try: + timetuple = parsedate_tz(value) + timestamp = mktime_tz(timetuple) + if timetuple[-1] is None: + return datetime.fromtimestamp(timestamp).replace(tzinfo=pytz.utc) + else: + return datetime.fromtimestamp(timestamp, pytz.utc) + except Exception: + raise ValueError('Invalid date literal "{0}"'.format(raw)) + + +def datetime_from_iso8601(value): + """ + Turns an ISO8601 formatted date into a datetime object. + + Example:: + + inputs.datetime_from_iso8601("2012-01-01T23:30:00+02:00") + + :param str value: The ISO8601-complying string to transform + :return: A datetime + :rtype: datetime + :raises ValueError: if value is an invalid date literal + + """ + try: + try: + return aniso8601.parse_datetime(value) + except ValueError: + date = aniso8601.parse_date(value) + return datetime(date.year, date.month, date.day) + except Exception: + raise ValueError('Invalid date literal "{0}"'.format(value)) + + +datetime_from_iso8601.__schema__ = {"type": "string", "format": "date-time"} + + +def date_from_iso8601(value): + """ + Turns an ISO8601 formatted date into a date object. + + Example:: + + inputs.date_from_iso8601("2012-01-01") + + + + :param str value: The ISO8601-complying string to transform + :return: A date + :rtype: date + :raises ValueError: if value is an invalid date literal + + """ + return datetime_from_iso8601(value).date() + + +date_from_iso8601.__schema__ = {"type": "string", "format": "date"} diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/marshalling.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/marshalling.py new file mode 100644 index 0000000..6648f66 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/marshalling.py @@ -0,0 +1,305 @@ +from collections import OrderedDict +from functools import wraps + +from flask import request, current_app, has_app_context + +from .mask import Mask, apply as apply_mask +from .utils import unpack + + +def make(cls): + if isinstance(cls, type): + return cls() + return cls + + +def marshal(data, fields, envelope=None, skip_none=False, mask=None, ordered=False): + """Takes raw data (in the form of a dict, list, object) and a dict of + fields to output and filters the data based on those fields. + + :param data: the actual object(s) from which the fields are taken from + :param fields: a dict of whose keys will make up the final serialized + response output + :param envelope: optional key that will be used to envelop the serialized + response + :param bool skip_none: optional key will be used to eliminate fields + which value is None or the field's key not + exist in data + :param bool ordered: Wether or not to preserve order + + + >>> from flask_restx import fields, marshal + >>> data = { 'a': 100, 'b': 'foo', 'c': None } + >>> mfields = { 'a': fields.Raw, 'c': fields.Raw, 'd': fields.Raw } + + >>> marshal(data, mfields) + {'a': 100, 'c': None, 'd': None} + + >>> marshal(data, mfields, envelope='data') + {'data': {'a': 100, 'c': None, 'd': None}} + + >>> marshal(data, mfields, skip_none=True) + {'a': 100} + + >>> marshal(data, mfields, ordered=True) + OrderedDict([('a', 100), ('c', None), ('d', None)]) + + >>> marshal(data, mfields, envelope='data', ordered=True) + OrderedDict([('data', OrderedDict([('a', 100), ('c', None), ('d', None)]))]) + + >>> marshal(data, mfields, skip_none=True, ordered=True) + OrderedDict([('a', 100)]) + + """ + out, has_wildcards = _marshal(data, fields, envelope, skip_none, mask, ordered) + + if has_wildcards: + # ugly local import to avoid dependency loop + from .fields import Wildcard + + items = [] + keys = [] + for dkey, val in fields.items(): + key = dkey + if isinstance(val, dict): + value = marshal(data, val, skip_none=skip_none, ordered=ordered) + else: + field = make(val) + is_wildcard = isinstance(field, Wildcard) + # exclude already parsed keys from the wildcard + if is_wildcard: + field.reset() + if keys: + field.exclude |= set(keys) + keys = [] + value = field.output(dkey, data, ordered=ordered) + if is_wildcard: + + def _append(k, v): + if skip_none and (v is None or v == OrderedDict() or v == {}): + return + items.append((k, v)) + + key = field.key or dkey + _append(key, value) + while True: + value = field.output(dkey, data, ordered=ordered) + if value is None or value == field.container.format( + field.default + ): + break + key = field.key + _append(key, value) + continue + + keys.append(key) + if skip_none and (value is None or value == OrderedDict() or value == {}): + continue + items.append((key, value)) + + items = tuple(items) + + out = OrderedDict(items) if ordered else dict(items) + + if envelope: + out = OrderedDict([(envelope, out)]) if ordered else {envelope: out} + + return out + + return out + + +def _marshal(data, fields, envelope=None, skip_none=False, mask=None, ordered=False): + """Takes raw data (in the form of a dict, list, object) and a dict of + fields to output and filters the data based on those fields. + + :param data: the actual object(s) from which the fields are taken from + :param fields: a dict of whose keys will make up the final serialized + response output + :param envelope: optional key that will be used to envelop the serialized + response + :param bool skip_none: optional key will be used to eliminate fields + which value is None or the field's key not + exist in data + :param bool ordered: Wether or not to preserve order + + + >>> from flask_restx import fields, marshal + >>> data = { 'a': 100, 'b': 'foo', 'c': None } + >>> mfields = { 'a': fields.Raw, 'c': fields.Raw, 'd': fields.Raw } + + >>> marshal(data, mfields) + {'a': 100, 'c': None, 'd': None} + + >>> marshal(data, mfields, envelope='data') + {'data': {'a': 100, 'c': None, 'd': None}} + + >>> marshal(data, mfields, skip_none=True) + {'a': 100} + + >>> marshal(data, mfields, ordered=True) + OrderedDict([('a', 100), ('c', None), ('d', None)]) + + >>> marshal(data, mfields, envelope='data', ordered=True) + OrderedDict([('data', OrderedDict([('a', 100), ('c', None), ('d', None)]))]) + + >>> marshal(data, mfields, skip_none=True, ordered=True) + OrderedDict([('a', 100)]) + + """ + # ugly local import to avoid dependency loop + from .fields import Wildcard + + mask = mask or getattr(fields, "__mask__", None) + fields = getattr(fields, "resolved", fields) + if mask: + fields = apply_mask(fields, mask, skip=True) + + if isinstance(data, (list, tuple)): + out = [marshal(d, fields, skip_none=skip_none, ordered=ordered) for d in data] + if envelope: + out = OrderedDict([(envelope, out)]) if ordered else {envelope: out} + return out, False + + has_wildcards = {"present": False} + + def __format_field(key, val): + field = make(val) + if isinstance(field, Wildcard): + has_wildcards["present"] = True + value = field.output(key, data, ordered=ordered) + return (key, value) + + items = ( + (k, marshal(data, v, skip_none=skip_none, ordered=ordered)) + if isinstance(v, dict) + else __format_field(k, v) + for k, v in fields.items() + ) + + if skip_none: + items = ( + (k, v) for k, v in items if v is not None and v != OrderedDict() and v != {} + ) + + out = OrderedDict(items) if ordered else dict(items) + + if envelope: + out = OrderedDict([(envelope, out)]) if ordered else {envelope: out} + + return out, has_wildcards["present"] + + +class marshal_with(object): + """A decorator that apply marshalling to the return values of your methods. + + >>> from flask_restx import fields, marshal_with + >>> mfields = { 'a': fields.Raw } + >>> @marshal_with(mfields) + ... def get(): + ... return { 'a': 100, 'b': 'foo' } + ... + ... + >>> get() + OrderedDict([('a', 100)]) + + >>> @marshal_with(mfields, envelope='data') + ... def get(): + ... return { 'a': 100, 'b': 'foo' } + ... + ... + >>> get() + OrderedDict([('data', OrderedDict([('a', 100)]))]) + + >>> mfields = { 'a': fields.Raw, 'c': fields.Raw, 'd': fields.Raw } + >>> @marshal_with(mfields, skip_none=True) + ... def get(): + ... return { 'a': 100, 'b': 'foo', 'c': None } + ... + ... + >>> get() + OrderedDict([('a', 100)]) + + see :meth:`flask_restx.marshal` + """ + + def __init__( + self, fields, envelope=None, skip_none=False, mask=None, ordered=False + ): + """ + :param fields: a dict of whose keys will make up the final + serialized response output + :param envelope: optional key that will be used to envelop the serialized + response + """ + self.fields = fields + self.envelope = envelope + self.skip_none = skip_none + self.ordered = ordered + self.mask = Mask(mask, skip=True) + + def __call__(self, f): + @wraps(f) + def wrapper(*args, **kwargs): + resp = f(*args, **kwargs) + mask = self.mask + if has_app_context(): + mask_header = current_app.config["RESTX_MASK_HEADER"] + mask = request.headers.get(mask_header) or mask + if isinstance(resp, tuple): + data, code, headers = unpack(resp) + return ( + marshal( + data, + self.fields, + self.envelope, + self.skip_none, + mask, + self.ordered, + ), + code, + headers, + ) + else: + return marshal( + resp, self.fields, self.envelope, self.skip_none, mask, self.ordered + ) + + return wrapper + + +class marshal_with_field(object): + """ + A decorator that formats the return values of your methods with a single field. + + >>> from flask_restx import marshal_with_field, fields + >>> @marshal_with_field(fields.List(fields.Integer)) + ... def get(): + ... return ['1', 2, 3.0] + ... + >>> get() + [1, 2, 3] + + see :meth:`flask_restx.marshal_with` + """ + + def __init__(self, field): + """ + :param field: a single field with which to marshal the output. + """ + if isinstance(field, type): + self.field = field() + else: + self.field = field + + def __call__(self, f): + @wraps(f) + def wrapper(*args, **kwargs): + resp = f(*args, **kwargs) + + if isinstance(resp, tuple): + data, code, headers = unpack(resp) + return self.field.format(data), code, headers + return self.field.format(resp) + + return wrapper diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/mask.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/mask.py new file mode 100644 index 0000000..f220445 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/mask.py @@ -0,0 +1,187 @@ +import logging +import re + +from collections import OrderedDict +from inspect import isclass + +from .errors import RestError + +log = logging.getLogger(__name__) + +LEXER = re.compile(r"\{|\}|\,|[\w_:\-\*]+") + + +class MaskError(RestError): + """Raised when an error occurs on mask""" + + pass + + +class ParseError(MaskError): + """Raised when the mask parsing failed""" + + pass + + +class Mask(OrderedDict): + """ + Hold a parsed mask. + + :param str|dict|Mask mask: A mask, parsed or not + :param bool skip: If ``True``, missing fields won't appear in result + """ + + def __init__(self, mask=None, skip=False, **kwargs): + self.skip = skip + if isinstance(mask, str): + super(Mask, self).__init__() + self.parse(mask) + elif isinstance(mask, (dict, OrderedDict)): + super(Mask, self).__init__(mask, **kwargs) + else: + self.skip = skip + super(Mask, self).__init__(**kwargs) + + def parse(self, mask): + """ + Parse a fields mask. + Expect something in the form:: + + {field,nested{nested_field,another},last} + + External brackets are optionals so it can also be written:: + + field,nested{nested_field,another},last + + All extras characters will be ignored. + + :param str mask: the mask string to parse + :raises ParseError: when a mask is unparseable/invalid + + """ + if not mask: + return + + mask = self.clean(mask) + fields = self + previous = None + stack = [] + + for token in LEXER.findall(mask): + if token == "{": + if previous not in fields: + raise ParseError("Unexpected opening bracket") + fields[previous] = Mask(skip=self.skip) + stack.append(fields) + fields = fields[previous] + elif token == "}": + if not stack: + raise ParseError("Unexpected closing bracket") + fields = stack.pop() + elif token == ",": + if previous in (",", "{", None): + raise ParseError("Unexpected comma") + else: + fields[token] = True + + previous = token + + if stack: + raise ParseError("Missing closing bracket") + + def clean(self, mask): + """Remove unnecessary characters""" + mask = mask.replace("\n", "").strip() + # External brackets are optional + if mask[0] == "{": + if mask[-1] != "}": + raise ParseError("Missing closing bracket") + mask = mask[1:-1] + return mask + + def apply(self, data): + """ + Apply a fields mask to the data. + + :param data: The data or model to apply mask on + :raises MaskError: when unable to apply the mask + + """ + from . import fields + + # Should handle lists + if isinstance(data, (list, tuple, set)): + return [self.apply(d) for d in data] + elif isinstance(data, (fields.Nested, fields.List, fields.Polymorph)): + return data.clone(self) + elif type(data) == fields.Raw: + return fields.Raw(default=data.default, attribute=data.attribute, mask=self) + elif data == fields.Raw: + return fields.Raw(mask=self) + elif ( + isinstance(data, fields.Raw) + or isclass(data) + and issubclass(data, fields.Raw) + ): + # Not possible to apply a mask on these remaining fields types + raise MaskError("Mask is inconsistent with model") + # Should handle objects + elif not isinstance(data, (dict, OrderedDict)) and hasattr(data, "__dict__"): + data = data.__dict__ + + return self.filter_data(data) + + def filter_data(self, data): + """ + Handle the data filtering given a parsed mask + + :param dict data: the raw data to filter + :param list mask: a parsed mask to filter against + :param bool skip: whether or not to skip missing fields + + """ + out = {} + for field, content in self.items(): + if field == "*": + continue + elif isinstance(content, Mask): + nested = data.get(field, None) + if self.skip and nested is None: + continue + elif nested is None: + out[field] = None + else: + out[field] = content.apply(nested) + elif self.skip and field not in data: + continue + else: + out[field] = data.get(field, None) + + if "*" in self.keys(): + for key, value in data.items(): + if key not in out: + out[key] = value + return out + + def __str__(self): + return "{{{0}}}".format( + ",".join( + [ + "".join((k, str(v))) if isinstance(v, Mask) else k + for k, v in self.items() + ] + ) + ) + + +def apply(data, mask, skip=False): + """ + Apply a fields mask to the data. + + :param data: The data or model to apply mask on + :param str|Mask mask: the mask (parsed or not) to apply on data + :param bool skip: If rue, missing field won't appear in result + :raises MaskError: when unable to apply the mask + + """ + return Mask(mask, skip).apply(data) diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/model.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/model.py new file mode 100644 index 0000000..dea43c6 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/model.py @@ -0,0 +1,285 @@ +import copy +import re +import warnings + +from collections import OrderedDict + +from collections.abc import MutableMapping +from werkzeug.utils import cached_property + +from .mask import Mask +from .errors import abort + +from jsonschema import Draft4Validator +from jsonschema.exceptions import ValidationError + +from .utils import not_none +from ._http import HTTPStatus + + +RE_REQUIRED = re.compile(r"u?\'(?P.*)\' is a required property", re.I | re.U) + + +def instance(cls): + if isinstance(cls, type): + return cls() + return cls + + +class ModelBase(object): + """ + Handles validation and swagger style inheritance for both subclasses. + Subclass must define `schema` attribute. + + :param str name: The model public name + """ + + def __init__(self, name, *args, **kwargs): + super(ModelBase, self).__init__(*args, **kwargs) + self.__apidoc__ = {"name": name} + self.name = name + self.__parents__ = [] + + def instance_inherit(name, *parents): + return self.__class__.inherit(name, self, *parents) + + self.inherit = instance_inherit + + @property + def ancestors(self): + """ + Return the ancestors tree + """ + ancestors = [p.ancestors for p in self.__parents__] + return set.union(set([self.name]), *ancestors) + + def get_parent(self, name): + if self.name == name: + return self + else: + for parent in self.__parents__: + found = parent.get_parent(name) + if found: + return found + raise ValueError("Parent " + name + " not found") + + @property + def __schema__(self): + schema = self._schema + + if self.__parents__: + refs = [ + {"$ref": "#/definitions/{0}".format(parent.name)} + for parent in self.__parents__ + ] + + return {"allOf": refs + [schema]} + else: + return schema + + @classmethod + def inherit(cls, name, *parents): + """ + Inherit this model (use the Swagger composition pattern aka. allOf) + :param str name: The new model name + :param dict fields: The new model extra fields + """ + model = cls(name, parents[-1]) + model.__parents__ = parents[:-1] + return model + + def validate(self, data, resolver=None, format_checker=None): + validator = Draft4Validator( + self.__schema__, resolver=resolver, format_checker=format_checker + ) + try: + validator.validate(data) + except ValidationError: + abort( + HTTPStatus.BAD_REQUEST, + message="Input payload validation failed", + errors=dict(self.format_error(e) for e in validator.iter_errors(data)), + ) + + def format_error(self, error): + path = list(error.path) + if error.validator == "required": + name = RE_REQUIRED.match(error.message).group("name") + path.append(name) + key = ".".join(str(p) for p in path) + return key, error.message + + def __unicode__(self): + return "Model({name},{{{fields}}})".format( + name=self.name, fields=",".join(self.keys()) + ) + + __str__ = __unicode__ + + +class RawModel(ModelBase): + """ + A thin wrapper on ordered fields dict to store API doc metadata. + Can also be used for response marshalling. + + :param str name: The model public name + :param str mask: an optional default model mask + :param bool strict: validation should raise error when there is param not provided in schema + """ + + wrapper = dict + + def __init__(self, name, *args, **kwargs): + self.__mask__ = kwargs.pop("mask", None) + self.__strict__ = kwargs.pop("strict", False) + if self.__mask__ and not isinstance(self.__mask__, Mask): + self.__mask__ = Mask(self.__mask__) + super(RawModel, self).__init__(name, *args, **kwargs) + + def instance_clone(name, *parents): + return self.__class__.clone(name, self, *parents) + + self.clone = instance_clone + + @property + def _schema(self): + properties = self.wrapper() + required = set() + discriminator = None + for name, field in self.items(): + field = instance(field) + properties[name] = field.__schema__ + if field.required: + required.add(name) + if getattr(field, "discriminator", False): + discriminator = name + + definition = { + "required": sorted(list(required)) or None, + "properties": properties, + "discriminator": discriminator, + "x-mask": str(self.__mask__) if self.__mask__ else None, + "type": "object", + } + + if self.__strict__: + definition["additionalProperties"] = False + + return not_none(definition) + + @cached_property + def resolved(self): + """ + Resolve real fields before submitting them to marshal + """ + # Duplicate fields + resolved = copy.deepcopy(self) + + # Recursively copy parent fields if necessary + for parent in self.__parents__: + resolved.update(parent.resolved) + + # Handle discriminator + candidates = [f for f in resolved.values() if getattr(f, "discriminator", None)] + # Ensure the is only one discriminator + if len(candidates) > 1: + raise ValueError("There can only be one discriminator by schema") + # Ensure discriminator always output the model name + elif len(candidates) == 1: + candidates[0].default = self.name + + return resolved + + def extend(self, name, fields): + """ + Extend this model (Duplicate all fields) + + :param str name: The new model name + :param dict fields: The new model extra fields + + :deprecated: since 0.9. Use :meth:`clone` instead. + """ + warnings.warn( + "extend is is deprecated, use clone instead", + DeprecationWarning, + stacklevel=2, + ) + if isinstance(fields, (list, tuple)): + return self.clone(name, *fields) + else: + return self.clone(name, fields) + + @classmethod + def clone(cls, name, *parents): + """ + Clone these models (Duplicate all fields) + + It can be used from the class + + >>> model = Model.clone(fields_1, fields_2) + + or from an Instanciated model + + >>> new_model = model.clone(fields_1, fields_2) + + :param str name: The new model name + :param dict parents: The new model extra fields + """ + fields = cls.wrapper() + for parent in parents: + fields.update(copy.deepcopy(parent)) + return cls(name, fields) + + def __deepcopy__(self, memo): + obj = self.__class__( + self.name, + [(key, copy.deepcopy(value, memo)) for key, value in self.items()], + mask=self.__mask__, + strict=self.__strict__, + ) + obj.__parents__ = self.__parents__ + return obj + + +class Model(RawModel, dict, MutableMapping): + """ + A thin wrapper on fields dict to store API doc metadata. + Can also be used for response marshalling. + + :param str name: The model public name + :param str mask: an optional default model mask + """ + + pass + + +class OrderedModel(RawModel, OrderedDict, MutableMapping): + """ + A thin wrapper on ordered fields dict to store API doc metadata. + Can also be used for response marshalling. + + :param str name: The model public name + :param str mask: an optional default model mask + """ + + wrapper = OrderedDict + + +class SchemaModel(ModelBase): + """ + Stores API doc metadata based on a json schema. + + :param str name: The model public name + :param dict schema: The json schema we are documenting + """ + + def __init__(self, name, schema=None): + super(SchemaModel, self).__init__(name) + self._schema = schema or {} + + def __unicode__(self): + return "SchemaModel({name},{schema})".format( + name=self.name, schema=self._schema + ) + + __str__ = __unicode__ diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/namespace.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/namespace.py new file mode 100644 index 0000000..86b4b33 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/namespace.py @@ -0,0 +1,375 @@ +import inspect +import warnings +import logging +from collections import namedtuple, OrderedDict + +from flask import request +from flask.views import http_method_funcs + +from ._http import HTTPStatus +from .errors import abort +from .marshalling import marshal, marshal_with +from .model import Model, OrderedModel, SchemaModel +from .reqparse import RequestParser +from .utils import merge + +# Container for each route applied to a Resource using @ns.route decorator +ResourceRoute = namedtuple("ResourceRoute", "resource urls route_doc kwargs") + + +class Namespace(object): + """ + Group resources together. + + Namespace is to API what :class:`flask:flask.Blueprint` is for :class:`flask:flask.Flask`. + + :param str name: The namespace name + :param str description: An optional short description + :param str path: An optional prefix path. If not provided, prefix is ``/+name`` + :param list decorators: A list of decorators to apply to each resources + :param bool validate: Whether or not to perform validation on this namespace + :param bool ordered: Whether or not to preserve order on models and marshalling + :param Api api: an optional API to attache to the namespace + """ + + def __init__( + self, + name, + description=None, + path=None, + decorators=None, + validate=None, + authorizations=None, + ordered=False, + **kwargs + ): + self.name = name + self.description = description + self._path = path + + self._schema = None + self._validate = validate + self.models = {} + self.urls = {} + self.decorators = decorators if decorators else [] + self.resources = [] # List[ResourceRoute] + self.error_handlers = OrderedDict() + self.default_error_handler = None + self.authorizations = authorizations + self.ordered = ordered + self.apis = [] + if "api" in kwargs: + self.apis.append(kwargs["api"]) + self.logger = logging.getLogger(__name__ + "." + self.name) + + @property + def path(self): + return (self._path or ("/" + self.name)).rstrip("/") + + def add_resource(self, resource, *urls, **kwargs): + """ + Register a Resource for a given API Namespace + + :param Resource resource: the resource ro register + :param str urls: one or more url routes to match for the resource, + standard flask routing rules apply. + Any url variables will be passed to the resource method as args. + :param str endpoint: endpoint name (defaults to :meth:`Resource.__name__.lower` + Can be used to reference this route in :class:`fields.Url` fields + :param list|tuple resource_class_args: args to be forwarded to the constructor of the resource. + :param dict resource_class_kwargs: kwargs to be forwarded to the constructor of the resource. + + Additional keyword arguments not specified above will be passed as-is + to :meth:`flask.Flask.add_url_rule`. + + Examples:: + + namespace.add_resource(HelloWorld, '/', '/hello') + namespace.add_resource(Foo, '/foo', endpoint="foo") + namespace.add_resource(FooSpecial, '/special/foo', endpoint="foo") + """ + route_doc = kwargs.pop("route_doc", {}) + self.resources.append(ResourceRoute(resource, urls, route_doc, kwargs)) + for api in self.apis: + ns_urls = api.ns_urls(self, urls) + api.register_resource(self, resource, *ns_urls, **kwargs) + + def route(self, *urls, **kwargs): + """ + A decorator to route resources. + """ + + def wrapper(cls): + doc = kwargs.pop("doc", None) + if doc is not None: + # build api doc intended only for this route + kwargs["route_doc"] = self._build_doc(cls, doc) + self.add_resource(cls, *urls, **kwargs) + return cls + + return wrapper + + def _build_doc(self, cls, doc): + if doc is False: + return False + unshortcut_params_description(doc) + handle_deprecations(doc) + for http_method in http_method_funcs: + if http_method in doc: + if doc[http_method] is False: + continue + unshortcut_params_description(doc[http_method]) + handle_deprecations(doc[http_method]) + if "expect" in doc[http_method] and not isinstance( + doc[http_method]["expect"], (list, tuple) + ): + doc[http_method]["expect"] = [doc[http_method]["expect"]] + return merge(getattr(cls, "__apidoc__", {}), doc) + + def doc(self, shortcut=None, **kwargs): + """A decorator to add some api documentation to the decorated object""" + if isinstance(shortcut, str): + kwargs["id"] = shortcut + show = shortcut if isinstance(shortcut, bool) else True + + def wrapper(documented): + documented.__apidoc__ = self._build_doc( + documented, kwargs if show else False + ) + return documented + + return wrapper + + def hide(self, func): + """A decorator to hide a resource or a method from specifications""" + return self.doc(False)(func) + + def abort(self, *args, **kwargs): + """ + Properly abort the current request + + See: :func:`~flask_restx.errors.abort` + """ + abort(*args, **kwargs) + + def add_model(self, name, definition): + self.models[name] = definition + for api in self.apis: + api.models[name] = definition + return definition + + def model(self, name=None, model=None, mask=None, strict=False, **kwargs): + """ + Register a model + + :param bool strict - should model validation raise error when non-specified param + is provided? + + .. seealso:: :class:`Model` + """ + cls = OrderedModel if self.ordered else Model + model = cls(name, model, mask=mask, strict=strict) + model.__apidoc__.update(kwargs) + return self.add_model(name, model) + + def schema_model(self, name=None, schema=None): + """ + Register a model + + .. seealso:: :class:`Model` + """ + model = SchemaModel(name, schema) + return self.add_model(name, model) + + def extend(self, name, parent, fields): + """ + Extend a model (Duplicate all fields) + + :deprecated: since 0.9. Use :meth:`clone` instead + """ + if isinstance(parent, list): + parents = parent + [fields] + model = Model.extend(name, *parents) + else: + model = Model.extend(name, parent, fields) + return self.add_model(name, model) + + def clone(self, name, *specs): + """ + Clone a model (Duplicate all fields) + + :param str name: the resulting model name + :param specs: a list of models from which to clone the fields + + .. seealso:: :meth:`Model.clone` + + """ + model = Model.clone(name, *specs) + return self.add_model(name, model) + + def inherit(self, name, *specs): + """ + Inherit a model (use the Swagger composition pattern aka. allOf) + + .. seealso:: :meth:`Model.inherit` + """ + model = Model.inherit(name, *specs) + return self.add_model(name, model) + + def expect(self, *inputs, **kwargs): + """ + A decorator to Specify the expected input model + + :param ModelBase|Parse inputs: An expect model or request parser + :param bool validate: whether to perform validation or not + + """ + expect = [] + params = {"validate": kwargs.get("validate", self._validate), "expect": expect} + for param in inputs: + expect.append(param) + return self.doc(**params) + + def parser(self): + """Instanciate a :class:`~RequestParser`""" + return RequestParser() + + def as_list(self, field): + """Allow to specify nested lists for documentation""" + field.__apidoc__ = merge(getattr(field, "__apidoc__", {}), {"as_list": True}) + return field + + def marshal_with( + self, fields, as_list=False, code=HTTPStatus.OK, description=None, **kwargs + ): + """ + A decorator specifying the fields to use for serialization. + + :param bool as_list: Indicate that the return type is a list (for the documentation) + :param int code: Optionally give the expected HTTP response code if its different from 200 + + """ + + def wrapper(func): + doc = { + "responses": { + str(code): (description, [fields], kwargs) + if as_list + else (description, fields, kwargs) + }, + "__mask__": kwargs.get( + "mask", True + ), # Mask values can't be determined outside app context + } + func.__apidoc__ = merge(getattr(func, "__apidoc__", {}), doc) + return marshal_with(fields, ordered=self.ordered, **kwargs)(func) + + return wrapper + + def marshal_list_with(self, fields, **kwargs): + """A shortcut decorator for :meth:`~Api.marshal_with` with ``as_list=True``""" + return self.marshal_with(fields, True, **kwargs) + + def marshal(self, *args, **kwargs): + """A shortcut to the :func:`marshal` helper""" + return marshal(*args, **kwargs) + + def errorhandler(self, exception): + """A decorator to register an error handler for a given exception""" + if inspect.isclass(exception) and issubclass(exception, Exception): + # Register an error handler for a given exception + def wrapper(func): + self.error_handlers[exception] = func + return func + + return wrapper + else: + # Register the default error handler + self.default_error_handler = exception + return exception + + def param(self, name, description=None, _in="query", **kwargs): + """ + A decorator to specify one of the expected parameters + + :param str name: the parameter name + :param str description: a small description + :param str _in: the parameter location `(query|header|formData|body|cookie)` + """ + param = kwargs + param["in"] = _in + param["description"] = description + return self.doc(params={name: param}) + + def response(self, code, description, model=None, **kwargs): + """ + A decorator to specify one of the expected responses + + :param int code: the HTTP status code + :param str description: a small description about the response + :param ModelBase model: an optional response model + + """ + return self.doc(responses={str(code): (description, model, kwargs)}) + + def header(self, name, description=None, **kwargs): + """ + A decorator to specify one of the expected headers + + :param str name: the HTTP header name + :param str description: a description about the header + + """ + header = {"description": description} + header.update(kwargs) + return self.doc(headers={name: header}) + + def produces(self, mimetypes): + """A decorator to specify the MIME types the API can produce""" + return self.doc(produces=mimetypes) + + def deprecated(self, func): + """A decorator to mark a resource or a method as deprecated""" + return self.doc(deprecated=True)(func) + + def vendor(self, *args, **kwargs): + """ + A decorator to expose vendor extensions. + + Extensions can be submitted as dict or kwargs. + The ``x-`` prefix is optionnal and will be added if missing. + + See: http://swagger.io/specification/#specification-extensions-128 + """ + for arg in args: + kwargs.update(arg) + return self.doc(vendor=kwargs) + + @property + def payload(self): + """Store the input payload in the current request context""" + return request.get_json() + + +def unshortcut_params_description(data): + if "params" in data: + for name, description in data["params"].items(): + if isinstance(description, str): + data["params"][name] = {"description": description} + + +def handle_deprecations(doc): + if "parser" in doc: + warnings.warn( + "The parser attribute is deprecated, use expect instead", + DeprecationWarning, + stacklevel=2, + ) + doc["expect"] = doc.get("expect", []) + [doc.pop("parser")] + if "body" in doc: + warnings.warn( + "The body attribute is deprecated, use expect instead", + DeprecationWarning, + stacklevel=2, + ) + doc["expect"] = doc.get("expect", []) + [doc.pop("body")] diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/postman.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/postman.py new file mode 100644 index 0000000..4b7b8c0 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/postman.py @@ -0,0 +1,202 @@ +# -*- coding: utf-8 -*- +from time import time +from uuid import uuid5, NAMESPACE_URL + +from urllib.parse import urlencode + + +def clean(data): + """Remove all keys where value is None""" + return dict((k, v) for k, v in data.items() if v is not None) + + +DEFAULT_VARS = { + "string": "", + "integer": 0, + "number": 0, +} + + +class Request(object): + """Wraps a Swagger operation into a Postman Request""" + + def __init__(self, collection, path, params, method, operation): + self.collection = collection + self.path = path + self.params = params + self.method = method.upper() + self.operation = operation + + @property + def id(self): + seed = str(" ".join((self.method, self.url))) + return str(uuid5(self.collection.uuid, seed)) + + @property + def url(self): + return self.collection.api.base_url.rstrip("/") + self.path + + @property + def headers(self): + headers = {} + # Handle content-type + if self.method != "GET": + consumes = self.collection.api.__schema__.get("consumes", []) + consumes = self.operation.get("consumes", consumes) + if len(consumes): + headers["Content-Type"] = consumes[-1] + + # Add all parameters headers + for param in self.operation.get("parameters", []): + if param["in"] == "header": + headers[param["name"]] = param.get("default", "") + + # Add security headers if needed (global then local) + for security in self.collection.api.__schema__.get("security", []): + for key, header in self.collection.apikeys.items(): + if key in security: + headers[header] = "" + for security in self.operation.get("security", []): + for key, header in self.collection.apikeys.items(): + if key in security: + headers[header] = "" + + lines = [":".join(line) for line in headers.items()] + return "\n".join(lines) + + @property + def folder(self): + if "tags" not in self.operation or len(self.operation["tags"]) == 0: + return + tag = self.operation["tags"][0] + for folder in self.collection.folders: + if folder.tag == tag: + return folder.id + + def as_dict(self, urlvars=False): + url, variables = self.process_url(urlvars) + return clean( + { + "id": self.id, + "method": self.method, + "name": self.operation["operationId"], + "description": self.operation.get("summary"), + "url": url, + "headers": self.headers, + "collectionId": self.collection.id, + "folder": self.folder, + "pathVariables": variables, + "time": int(time()), + } + ) + + def process_url(self, urlvars=False): + url = self.url + path_vars = {} + url_vars = {} + params = dict((p["name"], p) for p in self.params) + params.update( + dict((p["name"], p) for p in self.operation.get("parameters", [])) + ) + if not params: + return url, None + for name, param in params.items(): + if param["in"] == "path": + url = url.replace("{%s}" % name, ":%s" % name) + path_vars[name] = DEFAULT_VARS.get(param["type"], "") + elif param["in"] == "query" and urlvars: + default = DEFAULT_VARS.get(param["type"], "") + url_vars[name] = param.get("default", default) + if url_vars: + url = "?".join((url, urlencode(url_vars))) + return url, path_vars + + +class Folder(object): + def __init__(self, collection, tag): + self.collection = collection + self.tag = tag["name"] + self.description = tag["description"] + + @property + def id(self): + return str(uuid5(self.collection.uuid, str(self.tag))) + + @property + def order(self): + return [r.id for r in self.collection.requests if r.folder == self.id] + + def as_dict(self): + return clean( + { + "id": self.id, + "name": self.tag, + "description": self.description, + "order": self.order, + "collectionId": self.collection.id, + } + ) + + +class PostmanCollectionV1(object): + """Postman Collection (V1 format) serializer""" + + def __init__(self, api, swagger=False): + self.api = api + self.swagger = swagger + + @property + def uuid(self): + return uuid5(NAMESPACE_URL, self.api.base_url) + + @property + def id(self): + return str(self.uuid) + + @property + def requests(self): + if self.swagger: + # First request is Swagger specifications + yield Request( + self, + "/swagger.json", + {}, + "get", + { + "operationId": "Swagger specifications", + "summary": "The API Swagger specifications as JSON", + }, + ) + # Then iter over API paths and methods + for path, operations in self.api.__schema__["paths"].items(): + path_params = operations.get("parameters", []) + + for method, operation in operations.items(): + if method != "parameters": + yield Request(self, path, path_params, method, operation) + + @property + def folders(self): + for tag in self.api.__schema__["tags"]: + yield Folder(self, tag) + + @property + def apikeys(self): + return dict( + (name, secdef["name"]) + for name, secdef in self.api.__schema__.get("securityDefinitions").items() + if secdef.get("in") == "header" and secdef.get("type") == "apiKey" + ) + + def as_dict(self, urlvars=False): + return clean( + { + "id": self.id, + "name": " ".join((self.api.title, self.api.version)), + "description": self.api.description, + "order": [r.id for r in self.requests if not r.folder], + "requests": [r.as_dict(urlvars=urlvars) for r in self.requests], + "folders": [f.as_dict() for f in self.folders], + "timestamp": int(time()), + } + ) diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/representations.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/representations.py new file mode 100644 index 0000000..123b786 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/representations.py @@ -0,0 +1,26 @@ +try: + from ujson import dumps +except ImportError: + from json import dumps + +from flask import make_response, current_app + + +def output_json(data, code, headers=None): + """Makes a Flask response with a JSON encoded body""" + + settings = current_app.config.get("RESTX_JSON", {}) + + # If we're in debug mode, and the indent is not set, we set it to a + # reasonable value here. Note that this won't override any existing value + # that was set. + if current_app.debug: + settings.setdefault("indent", 4) + + # always end the json dumps with a new line + # see https://github.com/mitsuhiko/flask/pull/1262 + dumped = dumps(data, **settings) + "\n" + + resp = make_response(dumped, code) + resp.headers.extend(headers or {}) + return resp diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/reqparse.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/reqparse.py new file mode 100644 index 0000000..8461f0e --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/reqparse.py @@ -0,0 +1,458 @@ +import decimal + +try: + from collections.abc import Hashable +except ImportError: + from collections import Hashable +from copy import deepcopy +from flask import current_app, request + +from werkzeug.datastructures import MultiDict, FileStorage +from werkzeug import exceptions + +from .errors import abort, SpecsError +from .marshalling import marshal +from .model import Model +from ._http import HTTPStatus + + +class ParseResult(dict): + """ + The default result container as an Object dict. + """ + + def __getattr__(self, name): + try: + return self[name] + except KeyError: + raise AttributeError(name) + + def __setattr__(self, name, value): + self[name] = value + + +_friendly_location = { + "json": "the JSON body", + "form": "the post body", + "args": "the query string", + "values": "the post body or the query string", + "headers": "the HTTP headers", + "cookies": "the request's cookies", + "files": "an uploaded file", +} + +#: Maps Flask-RESTX RequestParser locations to Swagger ones +LOCATIONS = { + "args": "query", + "form": "formData", + "headers": "header", + "json": "body", + "values": "query", + "files": "formData", +} + +#: Maps Python primitives types to Swagger ones +PY_TYPES = { + int: "integer", + str: "string", + bool: "boolean", + float: "number", + None: "void", +} + +SPLIT_CHAR = "," + + +class Argument(object): + """ + :param name: Either a name or a list of option strings, e.g. foo or -f, --foo. + :param default: The value produced if the argument is absent from the request. + :param dest: The name of the attribute to be added to the object + returned by :meth:`~reqparse.RequestParser.parse_args()`. + :param bool required: Whether or not the argument may be omitted (optionals only). + :param string action: The basic type of action to be taken when this argument + is encountered in the request. Valid options are "store" and "append". + :param bool ignore: Whether to ignore cases where the argument fails type conversion + :param type: The type to which the request argument should be converted. + If a type raises an exception, the message in the error will be returned in the response. + Defaults to :class:`str`. + :param location: The attributes of the :class:`flask.Request` object + to source the arguments from (ex: headers, args, etc.), can be an + iterator. The last item listed takes precedence in the result set. + :param choices: A container of the allowable values for the argument. + :param help: A brief description of the argument, returned in the + response when the argument is invalid. May optionally contain + an "{error_msg}" interpolation token, which will be replaced with + the text of the error raised by the type converter. + :param bool case_sensitive: Whether argument values in the request are + case sensitive or not (this will convert all values to lowercase) + :param bool store_missing: Whether the arguments default value should + be stored if the argument is missing from the request. + :param bool trim: If enabled, trims whitespace around the argument. + :param bool nullable: If enabled, allows null value in argument. + """ + + def __init__( + self, + name, + default=None, + dest=None, + required=False, + ignore=False, + type=str, + location=( + "json", + "values", + ), + choices=(), + action="store", + help=None, + operators=("=",), + case_sensitive=True, + store_missing=True, + trim=False, + nullable=True, + ): + self.name = name + self.default = default + self.dest = dest + self.required = required + self.ignore = ignore + self.location = location + self.type = type + self.choices = choices + self.action = action + self.help = help + self.case_sensitive = case_sensitive + self.operators = operators + self.store_missing = store_missing + self.trim = trim + self.nullable = nullable + + def source(self, request): + """ + Pulls values off the request in the provided location + :param request: The flask request object to parse arguments from + """ + if isinstance(self.location, str): + if self.location in {"json", "get_json"}: + value = request.get_json(silent=True) + else: + value = getattr(request, self.location, MultiDict()) + if callable(value): + value = value() + if value is not None: + return value + else: + values = MultiDict() + for l in self.location: + if l in {"json", "get_json"}: + value = request.get_json(silent=True) + else: + value = getattr(request, l, None) + if callable(value): + value = value() + if value is not None: + values.update(value) + return values + + return MultiDict() + + def convert(self, value, op): + # Don't cast None + if value is None: + if not self.nullable: + raise ValueError("Must not be null!") + return None + + elif isinstance(self.type, Model) and isinstance(value, dict): + return marshal(value, self.type) + + # and check if we're expecting a filestorage and haven't overridden `type` + # (required because the below instantiation isn't valid for FileStorage) + elif isinstance(value, FileStorage) and self.type == FileStorage: + return value + + try: + return self.type(value, self.name, op) + except TypeError: + try: + if self.type is decimal.Decimal: + return self.type(str(value), self.name) + else: + return self.type(value, self.name) + except TypeError: + return self.type(value) + + def handle_validation_error(self, error, bundle_errors): + """ + Called when an error is raised while parsing. Aborts the request + with a 400 status and an error message + + :param error: the error that was raised + :param bool bundle_errors: do not abort when first error occurs, return a + dict with the name of the argument and the error message to be + bundled + """ + error_str = str(error) + error_msg = " ".join([str(self.help), error_str]) if self.help else error_str + errors = {self.name: error_msg} + + if bundle_errors: + return ValueError(error), errors + abort(HTTPStatus.BAD_REQUEST, "Input payload validation failed", errors=errors) + + def parse(self, request, bundle_errors=False): + """ + Parses argument value(s) from the request, converting according to + the argument's type. + + :param request: The flask request object to parse arguments from + :param bool bundle_errors: do not abort when first error occurs, return a + dict with the name of the argument and the error message to be + bundled + """ + bundle_errors = current_app.config.get("BUNDLE_ERRORS", False) or bundle_errors + source = self.source(request) + + results = [] + + # Sentinels + _not_found = False + _found = True + + for operator in self.operators: + name = self.name + operator.replace("=", "", 1) + if name in source: + # Account for MultiDict and regular dict + if hasattr(source, "getlist"): + values = source.getlist(name) + else: + values = [source.get(name)] + + for value in values: + if hasattr(value, "strip") and self.trim: + value = value.strip() + if hasattr(value, "lower") and not self.case_sensitive: + value = value.lower() + + if hasattr(self.choices, "__iter__"): + self.choices = [choice.lower() for choice in self.choices] + + try: + if self.action == "split": + value = [ + self.convert(v, operator) + for v in value.split(SPLIT_CHAR) + ] + else: + value = self.convert(value, operator) + except Exception as error: + if self.ignore: + continue + return self.handle_validation_error(error, bundle_errors) + + if self.choices and value not in self.choices: + msg = "The value '{0}' is not a valid choice for '{1}'.".format( + value, name + ) + return self.handle_validation_error(msg, bundle_errors) + + if name in request.unparsed_arguments: + request.unparsed_arguments.pop(name) + results.append(value) + + if not results and self.required: + if isinstance(self.location, str): + location = _friendly_location.get(self.location, self.location) + else: + locations = [_friendly_location.get(loc, loc) for loc in self.location] + location = " or ".join(locations) + error_msg = "Missing required parameter in {0}".format(location) + return self.handle_validation_error(error_msg, bundle_errors) + + if not results: + if callable(self.default): + return self.default(), _not_found + else: + return self.default, _not_found + + if self.action == "append": + return results, _found + + if self.action == "store" or len(results) == 1: + return results[0], _found + return results, _found + + @property + def __schema__(self): + if self.location == "cookie": + return + param = {"name": self.name, "in": LOCATIONS.get(self.location, "query")} + _handle_arg_type(self, param) + if self.required: + param["required"] = True + if self.help: + param["description"] = self.help + if self.default is not None: + param["default"] = ( + self.default() if callable(self.default) else self.default + ) + if self.action == "append": + param["items"] = {"type": param["type"]} + if "pattern" in param: + param["items"]["pattern"] = param.pop("pattern") + param["type"] = "array" + param["collectionFormat"] = "multi" + if self.action == "split": + param["items"] = {"type": param["type"]} + param["type"] = "array" + param["collectionFormat"] = "csv" + if self.choices: + param["enum"] = self.choices + return param + + +class RequestParser(object): + """ + Enables adding and parsing of multiple arguments in the context of a single request. + Ex:: + + from flask_restx import RequestParser + + parser = RequestParser() + parser.add_argument('foo') + parser.add_argument('int_bar', type=int) + args = parser.parse_args() + + :param bool trim: If enabled, trims whitespace on all arguments in this parser + :param bool bundle_errors: If enabled, do not abort when first error occurs, + return a dict with the name of the argument and the error message to be + bundled and return all validation errors + """ + + def __init__( + self, + argument_class=Argument, + result_class=ParseResult, + trim=False, + bundle_errors=False, + ): + self.args = [] + self.argument_class = argument_class + self.result_class = result_class + self.trim = trim + self.bundle_errors = bundle_errors + + def add_argument(self, *args, **kwargs): + """ + Adds an argument to be parsed. + + Accepts either a single instance of Argument or arguments to be passed + into :class:`Argument`'s constructor. + + See :class:`Argument`'s constructor for documentation on the available options. + """ + + if len(args) == 1 and isinstance(args[0], self.argument_class): + self.args.append(args[0]) + else: + self.args.append(self.argument_class(*args, **kwargs)) + + # Do not know what other argument classes are out there + if self.trim and self.argument_class is Argument: + # enable trim for appended element + self.args[-1].trim = kwargs.get("trim", self.trim) + + return self + + def parse_args(self, req=None, strict=False): + """ + Parse all arguments from the provided request and return the results as a ParseResult + + :param bool strict: if req includes args not in parser, throw 400 BadRequest exception + :return: the parsed results as :class:`ParseResult` (or any class defined as :attr:`result_class`) + :rtype: ParseResult + """ + if req is None: + req = request + + result = self.result_class() + + # A record of arguments not yet parsed; as each is found + # among self.args, it will be popped out + req.unparsed_arguments = ( + dict(self.argument_class("").source(req)) if strict else {} + ) + errors = {} + for arg in self.args: + value, found = arg.parse(req, self.bundle_errors) + if isinstance(value, ValueError): + errors.update(found) + found = None + if found or arg.store_missing: + result[arg.dest or arg.name] = value + if errors: + abort( + HTTPStatus.BAD_REQUEST, "Input payload validation failed", errors=errors + ) + + if strict and req.unparsed_arguments: + arguments = ", ".join(req.unparsed_arguments.keys()) + msg = "Unknown arguments: {0}".format(arguments) + raise exceptions.BadRequest(msg) + + return result + + def copy(self): + """Creates a copy of this RequestParser with the same set of arguments""" + parser_copy = self.__class__(self.argument_class, self.result_class) + parser_copy.args = deepcopy(self.args) + parser_copy.trim = self.trim + parser_copy.bundle_errors = self.bundle_errors + return parser_copy + + def replace_argument(self, name, *args, **kwargs): + """Replace the argument matching the given name with a new version.""" + new_arg = self.argument_class(name, *args, **kwargs) + for index, arg in enumerate(self.args[:]): + if new_arg.name == arg.name: + del self.args[index] + self.args.append(new_arg) + break + return self + + def remove_argument(self, name): + """Remove the argument matching the given name.""" + for index, arg in enumerate(self.args[:]): + if name == arg.name: + del self.args[index] + break + return self + + @property + def __schema__(self): + params = [] + locations = set() + for arg in self.args: + param = arg.__schema__ + if param: + params.append(param) + locations.add(param["in"]) + if "body" in locations and "formData" in locations: + raise SpecsError("Can't use formData and body at the same time") + return params + + +def _handle_arg_type(arg, param): + if isinstance(arg.type, Hashable) and arg.type in PY_TYPES: + param["type"] = PY_TYPES[arg.type] + elif hasattr(arg.type, "__apidoc__"): + param["type"] = arg.type.__apidoc__["name"] + param["in"] = "body" + elif hasattr(arg.type, "__schema__"): + param.update(arg.type.__schema__) + elif arg.location == "files": + param["type"] = "file" + else: + param["type"] = "string" diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/resource.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/resource.py new file mode 100644 index 0000000..c7dc5ea --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/resource.py @@ -0,0 +1,85 @@ +from flask import request +from flask.views import MethodView + + +from .model import ModelBase + +from .utils import unpack, BaseResponse + + +class Resource(MethodView): + """ + Represents an abstract RESTX resource. + + Concrete resources should extend from this class + and expose methods for each supported HTTP method. + If a resource is invoked with an unsupported HTTP method, + the API will return a response with status 405 Method Not Allowed. + Otherwise the appropriate method is called and passed all arguments + from the url rule used when adding the resource to an Api instance. + See :meth:`~flask_restx.Api.add_resource` for details. + """ + + representations = None + method_decorators = [] + + def __init__(self, api=None, *args, **kwargs): + self.api = api + + def dispatch_request(self, *args, **kwargs): + # Taken from flask + meth = getattr(self, request.method.lower(), None) + if meth is None and request.method == "HEAD": + meth = getattr(self, "get", None) + assert meth is not None, "Unimplemented method %r" % request.method + + for decorator in self.method_decorators: + meth = decorator(meth) + + self.validate_payload(meth) + + resp = meth(*args, **kwargs) + + if isinstance(resp, BaseResponse): + return resp + + representations = self.representations or {} + + mediatype = request.accept_mimetypes.best_match(representations, default=None) + if mediatype in representations: + data, code, headers = unpack(resp) + resp = representations[mediatype](data, code, headers) + resp.headers["Content-Type"] = mediatype + return resp + + return resp + + def __validate_payload(self, expect, collection=False): + """ + :param ModelBase expect: the expected model for the input payload + :param bool collection: False if a single object of a resource is + expected, True if a collection of objects of a resource is expected. + """ + # TODO: proper content negotiation + data = request.get_json() + if collection: + data = data if isinstance(data, list) else [data] + for obj in data: + expect.validate(obj, self.api.refresolver, self.api.format_checker) + else: + expect.validate(data, self.api.refresolver, self.api.format_checker) + + def validate_payload(self, func): + """Perform a payload validation on expected model if necessary""" + if getattr(func, "__apidoc__", False) is not False: + doc = func.__apidoc__ + validate = doc.get("validate", None) + validate = validate if validate is not None else self.api._validate + if validate: + for expect in doc.get("expect", []): + # TODO: handle third party handlers + if isinstance(expect, list) and len(expect) == 1: + if isinstance(expect[0], ModelBase): + self.__validate_payload(expect[0], collection=True) + if isinstance(expect, ModelBase): + self.__validate_payload(expect, collection=False) diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/schemas/__init__.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/schemas/__init__.py new file mode 100644 index 0000000..27b9866 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/schemas/__init__.py @@ -0,0 +1,120 @@ +""" +This module give access to OpenAPI specifications schemas +and allows to validate specs against them. + +.. versionadded:: 0.12.1 +""" +import io +import json + +import importlib_resources + +from collections.abc import Mapping +from jsonschema import Draft4Validator + +from flask_restx import errors + + +class SchemaValidationError(errors.ValidationError): + """ + Raised when specification is not valid + + .. versionadded:: 0.12.1 + """ + + def __init__(self, msg, errors=None): + super(SchemaValidationError, self).__init__(msg) + self.errors = errors + + def __str__(self): + msg = [self.msg] + for error in sorted(self.errors, key=lambda e: e.path): + path = ".".join(error.path) + msg.append("- {}: {}".format(path, error.message)) + for suberror in sorted(error.context, key=lambda e: e.schema_path): + path = ".".join(suberror.schema_path) + msg.append(" - {}: {}".format(path, suberror.message)) + return "\n".join(msg) + + __unicode__ = __str__ + + +class LazySchema(Mapping): + """ + A thin wrapper around schema file lazy loading the data on first access + + :param filename str: The package relative json schema filename + :param validator: The jsonschema validator class version + + .. versionadded:: 0.12.1 + """ + + def __init__(self, filename, validator=Draft4Validator): + super(LazySchema, self).__init__() + self.filename = filename + self._schema = None + self._validator = validator + + def _load(self): + if not self._schema: + ref = importlib_resources.files(__name__) / self.filename + + with io.open(ref) as infile: + self._schema = json.load(infile) + + def __getitem__(self, key): + self._load() + return self._schema.__getitem__(key) + + def __iter__(self): + self._load() + return self._schema.__iter__() + + def __len__(self): + self._load() + return self._schema.__len__() + + @property + def validator(self): + """The jsonschema validator to validate against""" + return self._validator(self) + + +#: OpenAPI 2.0 specification schema +OAS_20 = LazySchema("oas-2.0.json") + +#: Map supported OpenAPI versions to their JSON schema +VERSIONS = { + "2.0": OAS_20, +} + + +def validate(data): + """ + Validate an OpenAPI specification. + + Supported OpenAPI versions: 2.0 + + :param data dict: The specification to validate + :returns boolean: True if the specification is valid + :raises SchemaValidationError: when the specification is invalid + :raises flask_restx.errors.SpecsError: when it's not possible to determinate + the schema to validate against + + .. versionadded:: 0.12.1 + """ + if "swagger" not in data: + raise errors.SpecsError("Unable to determinate OpenAPI schema version") + + version = data["swagger"] + if version not in VERSIONS: + raise errors.SpecsError('Unknown OpenAPI schema version "{}"'.format(version)) + + validator = VERSIONS[version].validator + + validation_errors = list(validator.iter_errors(data)) + if validation_errors: + raise SchemaValidationError( + "OpenAPI {} validation failed".format(version), errors=validation_errors + ) + return True diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/schemas/oas-2.0.json b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/schemas/oas-2.0.json new file mode 100644 index 0000000..a92e18f --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/schemas/oas-2.0.json @@ -0,0 +1,1607 @@ +{ + "title": "A JSON Schema for Swagger 2.0 API.", + "id": "http://swagger.io/v2/schema.json#", + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "required": [ + "swagger", + "info", + "paths" + ], + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "swagger": { + "type": "string", + "enum": [ + "2.0" + ], + "description": "The Swagger version of this document." + }, + "info": { + "$ref": "#/definitions/info" + }, + "host": { + "type": "string", + "pattern": "^[^{}/ :\\\\]+(?::\\d+)?$", + "description": "The host (name or ip) of the API. Example: 'swagger.io'" + }, + "basePath": { + "type": "string", + "pattern": "^/", + "description": "The base path to the API. Example: '/api'." + }, + "schemes": { + "$ref": "#/definitions/schemesList" + }, + "consumes": { + "description": "A list of MIME types accepted by the API.", + "allOf": [ + { + "$ref": "#/definitions/mediaTypeList" + } + ] + }, + "produces": { + "description": "A list of MIME types the API can produce.", + "allOf": [ + { + "$ref": "#/definitions/mediaTypeList" + } + ] + }, + "paths": { + "$ref": "#/definitions/paths" + }, + "definitions": { + "$ref": "#/definitions/definitions" + }, + "parameters": { + "$ref": "#/definitions/parameterDefinitions" + }, + "responses": { + "$ref": "#/definitions/responseDefinitions" + }, + "security": { + "$ref": "#/definitions/security" + }, + "securityDefinitions": { + "$ref": "#/definitions/securityDefinitions" + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/tag" + }, + "uniqueItems": true + }, + "externalDocs": { + "$ref": "#/definitions/externalDocs" + } + }, + "definitions": { + "info": { + "type": "object", + "description": "General information about the API.", + "required": [ + "version", + "title" + ], + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "title": { + "type": "string", + "description": "A unique and precise title of the API." + }, + "version": { + "type": "string", + "description": "A semantic version number of the API." + }, + "description": { + "type": "string", + "description": "A longer description of the API. Should be different from the title. GitHub Flavored Markdown is allowed." + }, + "termsOfService": { + "type": "string", + "description": "The terms of service for the API." + }, + "contact": { + "$ref": "#/definitions/contact" + }, + "license": { + "$ref": "#/definitions/license" + } + } + }, + "contact": { + "type": "object", + "description": "Contact information for the owners of the API.", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "The identifying name of the contact person/organization." + }, + "url": { + "type": "string", + "description": "The URL pointing to the contact information.", + "format": "uri" + }, + "email": { + "type": "string", + "description": "The email address of the contact person/organization.", + "format": "email" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "license": { + "type": "object", + "required": [ + "name" + ], + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "The name of the license type. It's encouraged to use an OSI compatible license." + }, + "url": { + "type": "string", + "description": "The URL pointing to the license.", + "format": "uri" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "paths": { + "type": "object", + "description": "Relative paths to the individual endpoints. They must be relative to the 'basePath'.", + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + }, + "^/": { + "$ref": "#/definitions/pathItem" + } + }, + "additionalProperties": false + }, + "definitions": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/schema" + }, + "description": "One or more JSON objects describing the schemas being consumed and produced by the API." + }, + "parameterDefinitions": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/parameter" + }, + "description": "One or more JSON representations for parameters" + }, + "responseDefinitions": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/response" + }, + "description": "One or more JSON representations for responses" + }, + "externalDocs": { + "type": "object", + "additionalProperties": false, + "description": "information about external documentation", + "required": [ + "url" + ], + "properties": { + "description": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "examples": { + "type": "object", + "additionalProperties": true + }, + "mimeType": { + "type": "string", + "description": "The MIME type of the HTTP message." + }, + "operation": { + "type": "object", + "required": [ + "responses" + ], + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "summary": { + "type": "string", + "description": "A brief summary of the operation." + }, + "description": { + "type": "string", + "description": "A longer description of the operation, GitHub Flavored Markdown is allowed." + }, + "externalDocs": { + "$ref": "#/definitions/externalDocs" + }, + "operationId": { + "type": "string", + "description": "A unique identifier of the operation." + }, + "produces": { + "description": "A list of MIME types the API can produce.", + "allOf": [ + { + "$ref": "#/definitions/mediaTypeList" + } + ] + }, + "consumes": { + "description": "A list of MIME types the API can consume.", + "allOf": [ + { + "$ref": "#/definitions/mediaTypeList" + } + ] + }, + "parameters": { + "$ref": "#/definitions/parametersList" + }, + "responses": { + "$ref": "#/definitions/responses" + }, + "schemes": { + "$ref": "#/definitions/schemesList" + }, + "deprecated": { + "type": "boolean", + "default": false + }, + "security": { + "$ref": "#/definitions/security" + } + } + }, + "pathItem": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "$ref": { + "type": "string" + }, + "get": { + "$ref": "#/definitions/operation" + }, + "put": { + "$ref": "#/definitions/operation" + }, + "post": { + "$ref": "#/definitions/operation" + }, + "delete": { + "$ref": "#/definitions/operation" + }, + "options": { + "$ref": "#/definitions/operation" + }, + "head": { + "$ref": "#/definitions/operation" + }, + "patch": { + "$ref": "#/definitions/operation" + }, + "parameters": { + "$ref": "#/definitions/parametersList" + } + } + }, + "responses": { + "type": "object", + "description": "Response objects names can either be any valid HTTP status code or 'default'.", + "minProperties": 1, + "additionalProperties": false, + "patternProperties": { + "^([0-9]{3})$|^(default)$": { + "$ref": "#/definitions/responseValue" + }, + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "not": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + } + }, + "responseValue": { + "oneOf": [ + { + "$ref": "#/definitions/response" + }, + { + "$ref": "#/definitions/jsonReference" + } + ] + }, + "response": { + "type": "object", + "required": [ + "description" + ], + "properties": { + "description": { + "type": "string" + }, + "schema": { + "oneOf": [ + { + "$ref": "#/definitions/schema" + }, + { + "$ref": "#/definitions/fileSchema" + } + ] + }, + "headers": { + "$ref": "#/definitions/headers" + }, + "examples": { + "$ref": "#/definitions/examples" + } + }, + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "headers": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/header" + } + }, + "header": { + "type": "object", + "additionalProperties": false, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "string", + "number", + "integer", + "boolean", + "array" + ] + }, + "format": { + "type": "string" + }, + "items": { + "$ref": "#/definitions/primitivesItems" + }, + "collectionFormat": { + "$ref": "#/definitions/collectionFormat" + }, + "default": { + "$ref": "#/definitions/default" + }, + "maximum": { + "$ref": "#/definitions/maximum" + }, + "exclusiveMaximum": { + "$ref": "#/definitions/exclusiveMaximum" + }, + "minimum": { + "$ref": "#/definitions/minimum" + }, + "exclusiveMinimum": { + "$ref": "#/definitions/exclusiveMinimum" + }, + "maxLength": { + "$ref": "#/definitions/maxLength" + }, + "minLength": { + "$ref": "#/definitions/minLength" + }, + "pattern": { + "$ref": "#/definitions/pattern" + }, + "maxItems": { + "$ref": "#/definitions/maxItems" + }, + "minItems": { + "$ref": "#/definitions/minItems" + }, + "uniqueItems": { + "$ref": "#/definitions/uniqueItems" + }, + "enum": { + "$ref": "#/definitions/enum" + }, + "multipleOf": { + "$ref": "#/definitions/multipleOf" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "vendorExtension": { + "description": "Any property starting with x- is valid.", + "additionalProperties": true, + "additionalItems": true + }, + "bodyParameter": { + "type": "object", + "required": [ + "name", + "in", + "schema" + ], + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "description": { + "type": "string", + "description": "A brief description of the parameter. This could contain examples of use. GitHub Flavored Markdown is allowed." + }, + "name": { + "type": "string", + "description": "The name of the parameter." + }, + "in": { + "type": "string", + "description": "Determines the location of the parameter.", + "enum": [ + "body" + ] + }, + "required": { + "type": "boolean", + "description": "Determines whether or not this parameter is required or optional.", + "default": false + }, + "schema": { + "$ref": "#/definitions/schema" + } + }, + "additionalProperties": false + }, + "headerParameterSubSchema": { + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "required": { + "type": "boolean", + "description": "Determines whether or not this parameter is required or optional.", + "default": false + }, + "in": { + "type": "string", + "description": "Determines the location of the parameter.", + "enum": [ + "header" + ] + }, + "description": { + "type": "string", + "description": "A brief description of the parameter. This could contain examples of use. GitHub Flavored Markdown is allowed." + }, + "name": { + "type": "string", + "description": "The name of the parameter." + }, + "type": { + "type": "string", + "enum": [ + "string", + "number", + "boolean", + "integer", + "array" + ] + }, + "format": { + "type": "string" + }, + "items": { + "$ref": "#/definitions/primitivesItems" + }, + "collectionFormat": { + "$ref": "#/definitions/collectionFormat" + }, + "default": { + "$ref": "#/definitions/default" + }, + "maximum": { + "$ref": "#/definitions/maximum" + }, + "exclusiveMaximum": { + "$ref": "#/definitions/exclusiveMaximum" + }, + "minimum": { + "$ref": "#/definitions/minimum" + }, + "exclusiveMinimum": { + "$ref": "#/definitions/exclusiveMinimum" + }, + "maxLength": { + "$ref": "#/definitions/maxLength" + }, + "minLength": { + "$ref": "#/definitions/minLength" + }, + "pattern": { + "$ref": "#/definitions/pattern" + }, + "maxItems": { + "$ref": "#/definitions/maxItems" + }, + "minItems": { + "$ref": "#/definitions/minItems" + }, + "uniqueItems": { + "$ref": "#/definitions/uniqueItems" + }, + "enum": { + "$ref": "#/definitions/enum" + }, + "multipleOf": { + "$ref": "#/definitions/multipleOf" + } + } + }, + "queryParameterSubSchema": { + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "required": { + "type": "boolean", + "description": "Determines whether or not this parameter is required or optional.", + "default": false + }, + "in": { + "type": "string", + "description": "Determines the location of the parameter.", + "enum": [ + "query" + ] + }, + "description": { + "type": "string", + "description": "A brief description of the parameter. This could contain examples of use. GitHub Flavored Markdown is allowed." + }, + "name": { + "type": "string", + "description": "The name of the parameter." + }, + "allowEmptyValue": { + "type": "boolean", + "default": false, + "description": "allows sending a parameter by name only or with an empty value." + }, + "type": { + "type": "string", + "enum": [ + "string", + "number", + "boolean", + "integer", + "array" + ] + }, + "format": { + "type": "string" + }, + "items": { + "$ref": "#/definitions/primitivesItems" + }, + "collectionFormat": { + "$ref": "#/definitions/collectionFormatWithMulti" + }, + "default": { + "$ref": "#/definitions/default" + }, + "maximum": { + "$ref": "#/definitions/maximum" + }, + "exclusiveMaximum": { + "$ref": "#/definitions/exclusiveMaximum" + }, + "minimum": { + "$ref": "#/definitions/minimum" + }, + "exclusiveMinimum": { + "$ref": "#/definitions/exclusiveMinimum" + }, + "maxLength": { + "$ref": "#/definitions/maxLength" + }, + "minLength": { + "$ref": "#/definitions/minLength" + }, + "pattern": { + "$ref": "#/definitions/pattern" + }, + "maxItems": { + "$ref": "#/definitions/maxItems" + }, + "minItems": { + "$ref": "#/definitions/minItems" + }, + "uniqueItems": { + "$ref": "#/definitions/uniqueItems" + }, + "enum": { + "$ref": "#/definitions/enum" + }, + "multipleOf": { + "$ref": "#/definitions/multipleOf" + } + } + }, + "formDataParameterSubSchema": { + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "required": { + "type": "boolean", + "description": "Determines whether or not this parameter is required or optional.", + "default": false + }, + "in": { + "type": "string", + "description": "Determines the location of the parameter.", + "enum": [ + "formData" + ] + }, + "description": { + "type": "string", + "description": "A brief description of the parameter. This could contain examples of use. GitHub Flavored Markdown is allowed." + }, + "name": { + "type": "string", + "description": "The name of the parameter." + }, + "allowEmptyValue": { + "type": "boolean", + "default": false, + "description": "allows sending a parameter by name only or with an empty value." + }, + "type": { + "type": "string", + "enum": [ + "string", + "number", + "boolean", + "integer", + "array", + "file" + ] + }, + "format": { + "type": "string" + }, + "items": { + "$ref": "#/definitions/primitivesItems" + }, + "collectionFormat": { + "$ref": "#/definitions/collectionFormatWithMulti" + }, + "default": { + "$ref": "#/definitions/default" + }, + "maximum": { + "$ref": "#/definitions/maximum" + }, + "exclusiveMaximum": { + "$ref": "#/definitions/exclusiveMaximum" + }, + "minimum": { + "$ref": "#/definitions/minimum" + }, + "exclusiveMinimum": { + "$ref": "#/definitions/exclusiveMinimum" + }, + "maxLength": { + "$ref": "#/definitions/maxLength" + }, + "minLength": { + "$ref": "#/definitions/minLength" + }, + "pattern": { + "$ref": "#/definitions/pattern" + }, + "maxItems": { + "$ref": "#/definitions/maxItems" + }, + "minItems": { + "$ref": "#/definitions/minItems" + }, + "uniqueItems": { + "$ref": "#/definitions/uniqueItems" + }, + "enum": { + "$ref": "#/definitions/enum" + }, + "multipleOf": { + "$ref": "#/definitions/multipleOf" + } + } + }, + "pathParameterSubSchema": { + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "required": [ + "required" + ], + "properties": { + "required": { + "type": "boolean", + "enum": [ + true + ], + "description": "Determines whether or not this parameter is required or optional." + }, + "in": { + "type": "string", + "description": "Determines the location of the parameter.", + "enum": [ + "path" + ] + }, + "description": { + "type": "string", + "description": "A brief description of the parameter. This could contain examples of use. GitHub Flavored Markdown is allowed." + }, + "name": { + "type": "string", + "description": "The name of the parameter." + }, + "type": { + "type": "string", + "enum": [ + "string", + "number", + "boolean", + "integer", + "array" + ] + }, + "format": { + "type": "string" + }, + "items": { + "$ref": "#/definitions/primitivesItems" + }, + "collectionFormat": { + "$ref": "#/definitions/collectionFormat" + }, + "default": { + "$ref": "#/definitions/default" + }, + "maximum": { + "$ref": "#/definitions/maximum" + }, + "exclusiveMaximum": { + "$ref": "#/definitions/exclusiveMaximum" + }, + "minimum": { + "$ref": "#/definitions/minimum" + }, + "exclusiveMinimum": { + "$ref": "#/definitions/exclusiveMinimum" + }, + "maxLength": { + "$ref": "#/definitions/maxLength" + }, + "minLength": { + "$ref": "#/definitions/minLength" + }, + "pattern": { + "$ref": "#/definitions/pattern" + }, + "maxItems": { + "$ref": "#/definitions/maxItems" + }, + "minItems": { + "$ref": "#/definitions/minItems" + }, + "uniqueItems": { + "$ref": "#/definitions/uniqueItems" + }, + "enum": { + "$ref": "#/definitions/enum" + }, + "multipleOf": { + "$ref": "#/definitions/multipleOf" + } + } + }, + "nonBodyParameter": { + "type": "object", + "required": [ + "name", + "in", + "type" + ], + "oneOf": [ + { + "$ref": "#/definitions/headerParameterSubSchema" + }, + { + "$ref": "#/definitions/formDataParameterSubSchema" + }, + { + "$ref": "#/definitions/queryParameterSubSchema" + }, + { + "$ref": "#/definitions/pathParameterSubSchema" + } + ] + }, + "parameter": { + "oneOf": [ + { + "$ref": "#/definitions/bodyParameter" + }, + { + "$ref": "#/definitions/nonBodyParameter" + } + ] + }, + "schema": { + "type": "object", + "description": "A deterministic version of a JSON Schema object.", + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "$ref": { + "type": "string" + }, + "format": { + "type": "string" + }, + "title": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/title" + }, + "description": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/description" + }, + "default": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/default" + }, + "multipleOf": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/multipleOf" + }, + "maximum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/maximum" + }, + "exclusiveMaximum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/exclusiveMaximum" + }, + "minimum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/minimum" + }, + "exclusiveMinimum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/exclusiveMinimum" + }, + "maxLength": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveInteger" + }, + "minLength": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveIntegerDefault0" + }, + "pattern": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/pattern" + }, + "maxItems": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveInteger" + }, + "minItems": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveIntegerDefault0" + }, + "uniqueItems": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/uniqueItems" + }, + "maxProperties": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveInteger" + }, + "minProperties": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveIntegerDefault0" + }, + "required": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/stringArray" + }, + "enum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/enum" + }, + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/definitions/schema" + }, + { + "type": "boolean" + } + ], + "default": {} + }, + "type": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/type" + }, + "items": { + "anyOf": [ + { + "$ref": "#/definitions/schema" + }, + { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/schema" + } + } + ], + "default": {} + }, + "allOf": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/schema" + } + }, + "properties": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/schema" + }, + "default": {} + }, + "discriminator": { + "type": "string" + }, + "readOnly": { + "type": "boolean", + "default": false + }, + "xml": { + "$ref": "#/definitions/xml" + }, + "externalDocs": { + "$ref": "#/definitions/externalDocs" + }, + "example": {} + }, + "additionalProperties": false + }, + "fileSchema": { + "type": "object", + "description": "A deterministic version of a JSON Schema object.", + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "required": [ + "type" + ], + "properties": { + "format": { + "type": "string" + }, + "title": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/title" + }, + "description": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/description" + }, + "default": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/default" + }, + "required": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/stringArray" + }, + "type": { + "type": "string", + "enum": [ + "file" + ] + }, + "readOnly": { + "type": "boolean", + "default": false + }, + "externalDocs": { + "$ref": "#/definitions/externalDocs" + }, + "example": {} + }, + "additionalProperties": false + }, + "primitivesItems": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "enum": [ + "string", + "number", + "integer", + "boolean", + "array" + ] + }, + "format": { + "type": "string" + }, + "items": { + "$ref": "#/definitions/primitivesItems" + }, + "collectionFormat": { + "$ref": "#/definitions/collectionFormat" + }, + "default": { + "$ref": "#/definitions/default" + }, + "maximum": { + "$ref": "#/definitions/maximum" + }, + "exclusiveMaximum": { + "$ref": "#/definitions/exclusiveMaximum" + }, + "minimum": { + "$ref": "#/definitions/minimum" + }, + "exclusiveMinimum": { + "$ref": "#/definitions/exclusiveMinimum" + }, + "maxLength": { + "$ref": "#/definitions/maxLength" + }, + "minLength": { + "$ref": "#/definitions/minLength" + }, + "pattern": { + "$ref": "#/definitions/pattern" + }, + "maxItems": { + "$ref": "#/definitions/maxItems" + }, + "minItems": { + "$ref": "#/definitions/minItems" + }, + "uniqueItems": { + "$ref": "#/definitions/uniqueItems" + }, + "enum": { + "$ref": "#/definitions/enum" + }, + "multipleOf": { + "$ref": "#/definitions/multipleOf" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "security": { + "type": "array", + "items": { + "$ref": "#/definitions/securityRequirement" + }, + "uniqueItems": true + }, + "securityRequirement": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + "xml": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "prefix": { + "type": "string" + }, + "attribute": { + "type": "boolean", + "default": false + }, + "wrapped": { + "type": "boolean", + "default": false + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "tag": { + "type": "object", + "additionalProperties": false, + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "externalDocs": { + "$ref": "#/definitions/externalDocs" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "securityDefinitions": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/basicAuthenticationSecurity" + }, + { + "$ref": "#/definitions/apiKeySecurity" + }, + { + "$ref": "#/definitions/oauth2ImplicitSecurity" + }, + { + "$ref": "#/definitions/oauth2PasswordSecurity" + }, + { + "$ref": "#/definitions/oauth2ApplicationSecurity" + }, + { + "$ref": "#/definitions/oauth2AccessCodeSecurity" + } + ] + } + }, + "basicAuthenticationSecurity": { + "type": "object", + "additionalProperties": false, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "basic" + ] + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "apiKeySecurity": { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "name", + "in" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "apiKey" + ] + }, + "name": { + "type": "string" + }, + "in": { + "type": "string", + "enum": [ + "header", + "query" + ] + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "oauth2ImplicitSecurity": { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "flow", + "authorizationUrl" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "oauth2" + ] + }, + "flow": { + "type": "string", + "enum": [ + "implicit" + ] + }, + "scopes": { + "$ref": "#/definitions/oauth2Scopes" + }, + "authorizationUrl": { + "type": "string", + "format": "uri" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "oauth2PasswordSecurity": { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "flow", + "tokenUrl" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "oauth2" + ] + }, + "flow": { + "type": "string", + "enum": [ + "password" + ] + }, + "scopes": { + "$ref": "#/definitions/oauth2Scopes" + }, + "tokenUrl": { + "type": "string", + "format": "uri" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "oauth2ApplicationSecurity": { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "flow", + "tokenUrl" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "oauth2" + ] + }, + "flow": { + "type": "string", + "enum": [ + "application" + ] + }, + "scopes": { + "$ref": "#/definitions/oauth2Scopes" + }, + "tokenUrl": { + "type": "string", + "format": "uri" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "oauth2AccessCodeSecurity": { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "flow", + "authorizationUrl", + "tokenUrl" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "oauth2" + ] + }, + "flow": { + "type": "string", + "enum": [ + "accessCode" + ] + }, + "scopes": { + "$ref": "#/definitions/oauth2Scopes" + }, + "authorizationUrl": { + "type": "string", + "format": "uri" + }, + "tokenUrl": { + "type": "string", + "format": "uri" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "oauth2Scopes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "mediaTypeList": { + "type": "array", + "items": { + "$ref": "#/definitions/mimeType" + }, + "uniqueItems": true + }, + "parametersList": { + "type": "array", + "description": "The parameters needed to send a valid API call.", + "additionalItems": false, + "items": { + "oneOf": [ + { + "$ref": "#/definitions/parameter" + }, + { + "$ref": "#/definitions/jsonReference" + } + ] + }, + "uniqueItems": true + }, + "schemesList": { + "type": "array", + "description": "The transfer protocol of the API.", + "items": { + "type": "string", + "enum": [ + "http", + "https", + "ws", + "wss" + ] + }, + "uniqueItems": true + }, + "collectionFormat": { + "type": "string", + "enum": [ + "csv", + "ssv", + "tsv", + "pipes" + ], + "default": "csv" + }, + "collectionFormatWithMulti": { + "type": "string", + "enum": [ + "csv", + "ssv", + "tsv", + "pipes", + "multi" + ], + "default": "csv" + }, + "title": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/title" + }, + "description": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/description" + }, + "default": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/default" + }, + "multipleOf": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/multipleOf" + }, + "maximum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/maximum" + }, + "exclusiveMaximum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/exclusiveMaximum" + }, + "minimum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/minimum" + }, + "exclusiveMinimum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/exclusiveMinimum" + }, + "maxLength": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveInteger" + }, + "minLength": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveIntegerDefault0" + }, + "pattern": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/pattern" + }, + "maxItems": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveInteger" + }, + "minItems": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveIntegerDefault0" + }, + "uniqueItems": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/uniqueItems" + }, + "enum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/enum" + }, + "jsonReference": { + "type": "object", + "required": [ + "$ref" + ], + "additionalProperties": false, + "properties": { + "$ref": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/swagger.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/swagger.py new file mode 100644 index 0000000..ec0a197 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/swagger.py @@ -0,0 +1,745 @@ +# -*- coding: utf-8 -*- +import itertools +import re + +from inspect import isclass, getdoc +from collections import OrderedDict + +from collections.abc import Hashable + +from flask import current_app + +from . import fields +from .model import Model, ModelBase, OrderedModel +from .reqparse import RequestParser +from .utils import merge, not_none, not_none_sorted +from ._http import HTTPStatus + +from urllib.parse import quote + +#: Maps Flask/Werkzeug rooting types to Swagger ones +PATH_TYPES = { + "int": "integer", + "float": "number", + "string": "string", + "default": "string", +} + +#: Maps Python primitives types to Swagger ones +PY_TYPES = { + int: "integer", + float: "number", + str: "string", + bool: "boolean", + None: "void", +} + +RE_URL = re.compile(r"<(?:[^:<>]+:)?([^<>]+)>") + +DEFAULT_RESPONSE_DESCRIPTION = "Success" +DEFAULT_RESPONSE = {"description": DEFAULT_RESPONSE_DESCRIPTION} + +RE_RAISES = re.compile( + r"^:raises\s+(?P[\w\d_]+)\s*:\s*(?P.*)$", re.MULTILINE +) + +RE_PARSE_RULE = re.compile( + r""" + (?P[^<]*) # static rule data + < + (?: + (?P[a-zA-Z_][a-zA-Z0-9_]*) # converter name + (?:\((?P.*?)\))? # converter arguments + \: # variable delimiter + )? + (?P[a-zA-Z_][a-zA-Z0-9_]*) # variable name + > + """, + re.VERBOSE, +) + + +def ref(model): + """Return a reference to model in definitions""" + name = model.name if isinstance(model, ModelBase) else model + return {"$ref": "#/definitions/{0}".format(quote(name, safe=""))} + + +def _v(value): + """Dereference values (callable)""" + return value() if callable(value) else value + + +def extract_path(path): + """ + Transform a Flask/Werkzeug URL pattern in a Swagger one. + """ + return RE_URL.sub(r"{\1}", path) + + +def parse_rule(rule): + """ + Parse a rule and return it as generator. Each iteration yields tuples in the form + ``(converter, arguments, variable)``. If the converter is `None` it's a static url part, otherwise it's a dynamic + one. + + Note: This originally lived in werkzeug.routing.parse_rule until it was removed in werkzeug 2.2.0. + """ + pos = 0 + end = len(rule) + do_match = RE_PARSE_RULE.match + used_names = set() + while pos < end: + m = do_match(rule, pos) + if m is None: + break + data = m.groupdict() + if data["static"]: + yield None, None, data["static"] + variable = data["variable"] + converter = data["converter"] or "default" + if variable in used_names: + raise ValueError(f"variable name {variable!r} used twice.") + used_names.add(variable) + yield converter, data["args"] or None, variable + pos = m.end() + if pos < end: + remaining = rule[pos:] + if ">" in remaining or "<" in remaining: + raise ValueError(f"malformed url rule: {rule!r}") + yield None, None, remaining + + +def extract_path_params(path): + """ + Extract Flask-style parameters from an URL pattern as Swagger ones. + """ + params = OrderedDict() + for converter, arguments, variable in parse_rule(path): + if not converter: + continue + param = {"name": variable, "in": "path", "required": True} + + if converter in PATH_TYPES: + param["type"] = PATH_TYPES[converter] + elif converter in current_app.url_map.converters: + param["type"] = "string" + else: + raise ValueError("Unsupported type converter: %s" % converter) + params[variable] = param + return params + + +def _param_to_header(param): + param.pop("in", None) + param.pop("name", None) + return _clean_header(param) + + +def _clean_header(header): + if isinstance(header, str): + header = {"description": header} + typedef = header.get("type", "string") + if isinstance(typedef, Hashable) and typedef in PY_TYPES: + header["type"] = PY_TYPES[typedef] + elif ( + isinstance(typedef, (list, tuple)) + and len(typedef) == 1 + and typedef[0] in PY_TYPES + ): + header["type"] = "array" + header["items"] = {"type": PY_TYPES[typedef[0]]} + elif hasattr(typedef, "__schema__"): + header.update(typedef.__schema__) + else: + header["type"] = typedef + return not_none(header) + + +def parse_docstring(obj): + raw = getdoc(obj) + summary = raw.strip(" \n").split("\n")[0].split(".")[0] if raw else None + raises = {} + details = raw.replace(summary, "").lstrip(". \n").strip(" \n") if raw else None + for match in RE_RAISES.finditer(raw or ""): + raises[match.group("name")] = match.group("description") + if details: + details = details.replace(match.group(0), "") + parsed = { + "raw": raw, + "summary": summary or None, + "details": details or None, + "returns": None, + "params": [], + "raises": raises, + } + return parsed + + +def is_hidden(resource, route_doc=None): + """ + Determine whether a Resource has been hidden from Swagger documentation + i.e. by using Api.doc(False) decorator + """ + if route_doc is False: + return True + else: + return hasattr(resource, "__apidoc__") and resource.__apidoc__ is False + + +def build_request_body_parameters_schema(body_params): + """ + :param body_params: List of JSON schema of body parameters. + :type body_params: list of dict, generated from the json body parameters of a request parser + :return dict: The Swagger schema representation of the request body + + :Example: + { + 'name': 'payload', + 'required': True, + 'in': 'body', + 'schema': { + 'type': 'object', + 'properties': [ + 'parameter1': { + 'type': 'integer' + }, + 'parameter2': { + 'type': 'string' + } + ] + } + } + """ + + properties = {} + for param in body_params: + properties[param["name"]] = {"type": param.get("type", "string")} + + return { + "name": "payload", + "required": True, + "in": "body", + "schema": {"type": "object", "properties": properties}, + } + + +class Swagger(object): + """ + A Swagger documentation wrapper for an API instance. + """ + + def __init__(self, api): + self.api = api + self._registered_models = {} + + def as_dict(self): + """ + Output the specification as a serializable ``dict``. + + :returns: the full Swagger specification in a serializable format + :rtype: dict + """ + basepath = self.api.base_path + if len(basepath) > 1 and basepath.endswith("/"): + basepath = basepath[:-1] + infos = { + "title": _v(self.api.title), + "version": _v(self.api.version), + } + if self.api.description: + infos["description"] = _v(self.api.description) + if self.api.terms_url: + infos["termsOfService"] = _v(self.api.terms_url) + if self.api.contact and (self.api.contact_email or self.api.contact_url): + infos["contact"] = { + "name": _v(self.api.contact), + "email": _v(self.api.contact_email), + "url": _v(self.api.contact_url), + } + if self.api.license: + infos["license"] = {"name": _v(self.api.license)} + if self.api.license_url: + infos["license"]["url"] = _v(self.api.license_url) + + paths = {} + tags = self.extract_tags(self.api) + + # register errors + responses = self.register_errors() + + for ns in self.api.namespaces: + for resource, urls, route_doc, kwargs in ns.resources: + for url in self.api.ns_urls(ns, urls): + path = extract_path(url) + serialized = self.serialize_resource( + ns, resource, url, route_doc=route_doc, **kwargs + ) + paths[path] = serialized + + # register all models if required + if current_app.config["RESTX_INCLUDE_ALL_MODELS"]: + for m in self.api.models: + self.register_model(m) + + # merge in the top-level authorizations + for ns in self.api.namespaces: + if ns.authorizations: + if self.api.authorizations is None: + self.api.authorizations = {} + self.api.authorizations = merge( + self.api.authorizations, ns.authorizations + ) + + specs = { + "swagger": "2.0", + "basePath": basepath, + "paths": not_none_sorted(paths), + "info": infos, + "produces": list(self.api.representations.keys()), + "consumes": ["application/json"], + "securityDefinitions": self.api.authorizations or None, + "security": self.security_requirements(self.api.security) or None, + "tags": tags, + "definitions": self.serialize_definitions() or None, + "responses": responses or None, + "host": self.get_host(), + } + return not_none(specs) + + def get_host(self): + hostname = current_app.config.get("SERVER_NAME", None) or None + if hostname and self.api.blueprint and self.api.blueprint.subdomain: + hostname = ".".join((self.api.blueprint.subdomain, hostname)) + return hostname + + def extract_tags(self, api): + tags = [] + by_name = {} + for tag in api.tags: + if isinstance(tag, str): + tag = {"name": tag} + elif isinstance(tag, (list, tuple)): + tag = {"name": tag[0], "description": tag[1]} + elif isinstance(tag, dict) and "name" in tag: + pass + else: + raise ValueError("Unsupported tag format for {0}".format(tag)) + tags.append(tag) + by_name[tag["name"]] = tag + for ns in api.namespaces: + # hide namespaces without any Resources + if not ns.resources: + continue + # hide namespaces with all Resources hidden from Swagger documentation + if all(is_hidden(r.resource, route_doc=r.route_doc) for r in ns.resources): + continue + if ns.name not in by_name: + tags.append( + {"name": ns.name, "description": ns.description} + if ns.description + else {"name": ns.name} + ) + elif ns.description: + by_name[ns.name]["description"] = ns.description + return tags + + def extract_resource_doc(self, resource, url, route_doc=None): + route_doc = {} if route_doc is None else route_doc + if route_doc is False: + return False + doc = merge(getattr(resource, "__apidoc__", {}), route_doc) + if doc is False: + return False + + # ensure unique names for multiple routes to the same resource + # provides different Swagger operationId's + doc["name"] = ( + "{}_{}".format(resource.__name__, url) if route_doc else resource.__name__ + ) + + params = merge(self.expected_params(doc), doc.get("params", OrderedDict())) + params = merge(params, extract_path_params(url)) + # Track parameters for late deduplication + up_params = {(n, p.get("in", "query")): p for n, p in params.items()} + need_to_go_down = set() + methods = [m.lower() for m in resource.methods or []] + for method in methods: + method_doc = doc.get(method, OrderedDict()) + method_impl = getattr(resource, method) + if hasattr(method_impl, "im_func"): + method_impl = method_impl.im_func + elif hasattr(method_impl, "__func__"): + method_impl = method_impl.__func__ + method_doc = merge( + method_doc, getattr(method_impl, "__apidoc__", OrderedDict()) + ) + if method_doc is not False: + method_doc["docstring"] = parse_docstring(method_impl) + method_params = self.expected_params(method_doc) + method_params = merge(method_params, method_doc.get("params", {})) + inherited_params = OrderedDict( + (k, v) for k, v in params.items() if k in method_params + ) + method_doc["params"] = merge(inherited_params, method_params) + for name, param in method_doc["params"].items(): + key = (name, param.get("in", "query")) + if key in up_params: + need_to_go_down.add(key) + doc[method] = method_doc + # Deduplicate parameters + # For each couple (name, in), if a method overrides it, + # we need to move the paramter down to each method + if need_to_go_down: + for method in methods: + method_doc = doc.get(method) + if not method_doc: + continue + params = { + (n, p.get("in", "query")): p + for n, p in (method_doc["params"] or {}).items() + } + for key in need_to_go_down: + if key not in params: + method_doc["params"][key[0]] = up_params[key] + doc["params"] = OrderedDict( + (k[0], p) for k, p in up_params.items() if k not in need_to_go_down + ) + return doc + + def expected_params(self, doc): + params = OrderedDict() + if "expect" not in doc: + return params + + for expect in doc.get("expect", []): + if isinstance(expect, RequestParser): + parser_params = OrderedDict( + (p["name"], p) for p in expect.__schema__ if p["in"] != "body" + ) + params.update(parser_params) + + body_params = [p for p in expect.__schema__ if p["in"] == "body"] + if body_params: + params["payload"] = build_request_body_parameters_schema( + body_params + ) + elif isinstance(expect, ModelBase): + params["payload"] = not_none( + { + "name": "payload", + "required": True, + "in": "body", + "schema": self.serialize_schema(expect), + } + ) + elif isinstance(expect, (list, tuple)): + if len(expect) == 2: + # this is (payload, description) shortcut + model, description = expect + params["payload"] = not_none( + { + "name": "payload", + "required": True, + "in": "body", + "schema": self.serialize_schema(model), + "description": description, + } + ) + else: + params["payload"] = not_none( + { + "name": "payload", + "required": True, + "in": "body", + "schema": self.serialize_schema(expect), + } + ) + return params + + def register_errors(self): + responses = {} + for exception, handler in self.api.error_handlers.items(): + doc = parse_docstring(handler) + response = {"description": doc["summary"]} + apidoc = getattr(handler, "__apidoc__", {}) + self.process_headers(response, apidoc) + if "responses" in apidoc: + _, model, _ = list(apidoc["responses"].values())[0] + response["schema"] = self.serialize_schema(model) + responses[exception.__name__] = not_none(response) + return responses + + def serialize_resource(self, ns, resource, url, route_doc=None, **kwargs): + doc = self.extract_resource_doc(resource, url, route_doc=route_doc) + if doc is False: + return + path = {"parameters": self.parameters_for(doc) or None} + for method in [m.lower() for m in resource.methods or []]: + methods = [m.lower() for m in kwargs.get("methods", [])] + if doc[method] is False or methods and method not in methods: + continue + path[method] = self.serialize_operation(doc, method) + path[method]["tags"] = [ns.name] + return not_none(path) + + def serialize_operation(self, doc, method): + operation = { + "responses": self.responses_for(doc, method) or None, + "summary": doc[method]["docstring"]["summary"], + "description": self.description_for(doc, method) or None, + "operationId": self.operation_id_for(doc, method), + "parameters": self.parameters_for(doc[method]) or None, + "security": self.security_for(doc, method), + } + # Handle 'produces' mimetypes documentation + if "produces" in doc[method]: + operation["produces"] = doc[method]["produces"] + # Handle deprecated annotation + if doc.get("deprecated") or doc[method].get("deprecated"): + operation["deprecated"] = True + # Handle form exceptions: + doc_params = list(doc.get("params", {}).values()) + all_params = doc_params + (operation["parameters"] or []) + if all_params and any(p["in"] == "formData" for p in all_params): + if any(p["type"] == "file" for p in all_params): + operation["consumes"] = ["multipart/form-data"] + else: + operation["consumes"] = [ + "application/x-www-form-urlencoded", + "multipart/form-data", + ] + operation.update(self.vendor_fields(doc, method)) + return not_none(operation) + + def vendor_fields(self, doc, method): + """ + Extract custom 3rd party Vendor fields prefixed with ``x-`` + + See: https://swagger.io/specification/#specification-extensions + """ + return dict( + (k if k.startswith("x-") else "x-{0}".format(k), v) + for k, v in doc[method].get("vendor", {}).items() + ) + + def description_for(self, doc, method): + """Extract the description metadata and fallback on the whole docstring""" + parts = [] + if "description" in doc: + parts.append(doc["description"] or "") + if method in doc and "description" in doc[method]: + parts.append(doc[method]["description"]) + if doc[method]["docstring"]["details"]: + parts.append(doc[method]["docstring"]["details"]) + + return "\n".join(parts).strip() + + def operation_id_for(self, doc, method): + """Extract the operation id""" + return ( + doc[method]["id"] + if "id" in doc[method] + else self.api.default_id(doc["name"], method) + ) + + def parameters_for(self, doc): + params = [] + for name, param in doc["params"].items(): + param["name"] = name + if "type" not in param and "schema" not in param: + param["type"] = "string" + if "in" not in param: + param["in"] = "query" + + if "type" in param and "schema" not in param: + ptype = param.get("type", None) + if isinstance(ptype, (list, tuple)): + typ = ptype[0] + param["type"] = "array" + param["items"] = {"type": PY_TYPES.get(typ, typ)} + + elif isinstance(ptype, (type, type(None))) and ptype in PY_TYPES: + param["type"] = PY_TYPES[ptype] + + params.append(param) + + # Handle fields mask + mask = doc.get("__mask__") + if mask and current_app.config["RESTX_MASK_SWAGGER"]: + param = { + "name": current_app.config["RESTX_MASK_HEADER"], + "in": "header", + "type": "string", + "format": "mask", + "description": "An optional fields mask", + } + if isinstance(mask, str): + param["default"] = mask + params.append(param) + + return params + + def responses_for(self, doc, method): + # TODO: simplify/refactor responses/model handling + responses = {} + + for d in doc, doc[method]: + if "responses" in d: + for code, response in d["responses"].items(): + code = str(code) + if isinstance(response, str): + description = response + model = None + kwargs = {} + elif len(response) == 3: + description, model, kwargs = response + elif len(response) == 2: + description, model = response + kwargs = {} + else: + raise ValueError("Unsupported response specification") + description = description or DEFAULT_RESPONSE_DESCRIPTION + if code in responses: + responses[code].update(description=description) + else: + responses[code] = {"description": description} + if model: + schema = self.serialize_schema(model) + envelope = kwargs.get("envelope") + if envelope: + schema = {"properties": {envelope: schema}} + responses[code]["schema"] = schema + self.process_headers( + responses[code], doc, method, kwargs.get("headers") + ) + if "model" in d: + code = str(d.get("default_code", HTTPStatus.OK)) + if code not in responses: + responses[code] = self.process_headers( + DEFAULT_RESPONSE.copy(), doc, method + ) + responses[code]["schema"] = self.serialize_schema(d["model"]) + + if "docstring" in d: + for name, description in d["docstring"]["raises"].items(): + for exception, handler in self.api.error_handlers.items(): + error_responses = getattr(handler, "__apidoc__", {}).get( + "responses", {} + ) + code = ( + str(list(error_responses.keys())[0]) + if error_responses + else None + ) + if code and exception.__name__ == name: + responses[code] = {"$ref": "#/responses/{0}".format(name)} + break + + if not responses: + responses[str(HTTPStatus.OK.value)] = self.process_headers( + DEFAULT_RESPONSE.copy(), doc, method + ) + return responses + + def process_headers(self, response, doc, method=None, headers=None): + method_doc = doc.get(method, {}) + if "headers" in doc or "headers" in method_doc or headers: + response["headers"] = dict( + (k, _clean_header(v)) + for k, v in itertools.chain( + doc.get("headers", {}).items(), + method_doc.get("headers", {}).items(), + (headers or {}).items(), + ) + ) + return response + + def serialize_definitions(self): + return dict( + (name, model.__schema__) for name, model in self._registered_models.items() + ) + + def serialize_schema(self, model): + if isinstance(model, (list, tuple)): + model = model[0] + return { + "type": "array", + "items": self.serialize_schema(model), + } + + elif isinstance(model, ModelBase): + self.register_model(model) + return ref(model) + + elif isinstance(model, str): + self.register_model(model) + return ref(model) + + elif isclass(model) and issubclass(model, fields.Raw): + return self.serialize_schema(model()) + + elif isinstance(model, fields.Raw): + return model.__schema__ + + elif isinstance(model, (type, type(None))) and model in PY_TYPES: + return {"type": PY_TYPES[model]} + + raise ValueError("Model {0} not registered".format(model)) + + def register_model(self, model): + name = model.name if isinstance(model, ModelBase) else model + if name not in self.api.models: + raise ValueError("Model {0} not registered".format(name)) + specs = self.api.models[name] + if name in self._registered_models: + return ref(model) + self._registered_models[name] = specs + if isinstance(specs, ModelBase): + for parent in specs.__parents__: + self.register_model(parent) + if isinstance(specs, (Model, OrderedModel)): + for field in specs.values(): + self.register_field(field) + return ref(model) + + def register_field(self, field): + if isinstance(field, fields.Polymorph): + for model in field.mapping.values(): + self.register_model(model) + elif isinstance(field, fields.Nested): + self.register_model(field.nested) + elif isinstance(field, (fields.List, fields.Wildcard)): + self.register_field(field.container) + + def security_for(self, doc, method): + security = None + if "security" in doc: + auth = doc["security"] + security = self.security_requirements(auth) + + if "security" in doc[method]: + auth = doc[method]["security"] + security = self.security_requirements(auth) + + return security + + def security_requirements(self, value): + if isinstance(value, (list, tuple)): + return [self.security_requirement(v) for v in value] + elif value: + requirement = self.security_requirement(value) + return [requirement] if requirement else None + else: + return [] + + def security_requirement(self, value): + if isinstance(value, (str)): + return {value: []} + elif isinstance(value, dict): + return dict( + (k, v if isinstance(v, (list, tuple)) else [v]) + for k, v in value.items() + ) + else: + return None diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/templates/swagger-ui-css.html b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/templates/swagger-ui-css.html new file mode 100644 index 0000000..e27a768 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/templates/swagger-ui-css.html @@ -0,0 +1,32 @@ +{# + + + + +#} + + + diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/templates/swagger-ui-libs.html b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/templates/swagger-ui-libs.html new file mode 100644 index 0000000..2087a5f --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/templates/swagger-ui-libs.html @@ -0,0 +1,11 @@ +{# +{% if config.SWAGGER_UI_LANGUAGES %} + +{% for lang in config.SWAGGER_UI_LANGUAGES %} +{% set filename = 'lang/{0}.js'.format(lang) %} + +{% endfor%} +{% endif %} +#} + + diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/templates/swagger-ui.html b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/templates/swagger-ui.html new file mode 100644 index 0000000..b233e01 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/templates/swagger-ui.html @@ -0,0 +1,84 @@ + + + + {{ title }} + {% include 'swagger-ui-css.html' %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + {% include 'swagger-ui-libs.html' %} + + + diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/utils.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/utils.py new file mode 100644 index 0000000..409657a --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/flask_restx/utils.py @@ -0,0 +1,187 @@ +import re +import warnings +import typing + +from collections import OrderedDict +from copy import deepcopy + +from ._http import HTTPStatus + + +FIRST_CAP_RE = re.compile("(.)([A-Z][a-z]+)") +ALL_CAP_RE = re.compile("([a-z0-9])([A-Z])") + + +__all__ = ( + "merge", + "camel_to_dash", + "default_id", + "not_none", + "not_none_sorted", + "unpack", + "BaseResponse", + "import_check_view_func", +) + + +def import_werkzeug_response(): + """Resolve `werkzeug` `Response` class import because + `BaseResponse` was renamed in version 2.* to `Response`""" + import importlib.metadata + + werkzeug_major = int(importlib.metadata.version("werkzeug").split(".")[0]) + if werkzeug_major < 2: + from werkzeug.wrappers import BaseResponse + + return BaseResponse + + from werkzeug.wrappers import Response + + return Response + + +BaseResponse = import_werkzeug_response() + + +class FlaskCompatibilityWarning(DeprecationWarning): + pass + + +def merge(first, second): + """ + Recursively merges two dictionaries. + + Second dictionary values will take precedence over those from the first one. + Nested dictionaries are merged too. + + :param dict first: The first dictionary + :param dict second: The second dictionary + :return: the resulting merged dictionary + :rtype: dict + """ + if not isinstance(second, dict): + return second + result = deepcopy(first) + for key, value in second.items(): + if key in result and isinstance(result[key], dict): + result[key] = merge(result[key], value) + else: + result[key] = deepcopy(value) + return result + + +def camel_to_dash(value): + """ + Transform a CamelCase string into a low_dashed one + + :param str value: a CamelCase string to transform + :return: the low_dashed string + :rtype: str + """ + first_cap = FIRST_CAP_RE.sub(r"\1_\2", value) + return ALL_CAP_RE.sub(r"\1_\2", first_cap).lower() + + +def default_id(resource, method): + """Default operation ID generator""" + return "{0}_{1}".format(method, camel_to_dash(resource)) + + +def not_none(data): + """ + Remove all keys where value is None + + :param dict data: A dictionary with potentially some values set to None + :return: The same dictionary without the keys with values to ``None`` + :rtype: dict + """ + return dict((k, v) for k, v in data.items() if v is not None) + + +def not_none_sorted(data): + """ + Remove all keys where value is None + + :param OrderedDict data: A dictionary with potentially some values set to None + :return: The same dictionary without the keys with values to ``None`` + :rtype: OrderedDict + """ + return OrderedDict((k, v) for k, v in sorted(data.items()) if v is not None) + + +def unpack(response, default_code=HTTPStatus.OK): + """ + Unpack a Flask standard response. + + Flask response can be: + - a single value + - a 2-tuple ``(value, code)`` + - a 3-tuple ``(value, code, headers)`` + + .. warning:: + + When using this function, you must ensure that the tuple is not the response data. + To do so, prefer returning list instead of tuple for listings. + + :param response: A Flask style response + :param int default_code: The HTTP code to use as default if none is provided + :return: a 3-tuple ``(data, code, headers)`` + :rtype: tuple + :raise ValueError: if the response does not have one of the expected format + """ + if not isinstance(response, tuple): + # data only + return response, default_code, {} + elif len(response) == 1: + # data only as tuple + return response[0], default_code, {} + elif len(response) == 2: + # data and code + data, code = response + return data, code, {} + elif len(response) == 3: + # data, code and headers + data, code, headers = response + return data, code or default_code, headers + else: + raise ValueError("Too many response values") + + +def to_view_name(view_func: typing.Callable) -> str: + """Helper that returns the default endpoint for a given + function. This always is the function name. + + Note: copy of simple flask internal helper + """ + assert view_func is not None, "expected view func if endpoint is not provided." + return view_func.__name__ + + +def import_check_view_func(): + """ + Resolve import flask _endpoint_from_view_func. + + Show warning if function cannot be found and provide copy of last known implementation. + + Note: This helper method exists because reoccurring problem with flask function, but + actual method body remaining the same in each flask version. + """ + import importlib.metadata + + flask_version = importlib.metadata.version("flask").split(".") + try: + if flask_version[0] == "1": + from flask.helpers import _endpoint_from_view_func + elif flask_version[0] == "2": + from flask.scaffold import _endpoint_from_view_func + elif flask_version[0] == "3": + from flask.sansio.scaffold import _endpoint_from_view_func + else: + warnings.simplefilter("once", FlaskCompatibilityWarning) + _endpoint_from_view_func = None + except ImportError: + warnings.simplefilter("once", FlaskCompatibilityWarning) + _endpoint_from_view_func = None + if _endpoint_from_view_func is None: + _endpoint_from_view_func = to_view_name + return _endpoint_from_view_func diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/package.json b/packages/flask-restx/opengnsys-flask-restx-1.3.0/package.json new file mode 100644 index 0000000..96d1a6d --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/package.json @@ -0,0 +1,16 @@ +{ + "name": "flask-restx", + "version": "1.3.0", + "description": "Fully featured framework for fast, easy and documented API development with Flask", + "repository": "python-restx/flask-restx", + "keywords": [ + "swagger", + "flask" + ], + "author": "python-restx authors", + "license": "BSD-3-Clause", + "dependencies": { + "swagger-ui-dist": "^4.15.0", + "typeface-droid-sans": "0.0.40" + } +} diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/readthedocs.pip b/packages/flask-restx/opengnsys-flask-restx-1.3.0/readthedocs.pip new file mode 100644 index 0000000..7d0a38f --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/readthedocs.pip @@ -0,0 +1,3 @@ +sphinx +alabaster +sphinx_issues diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/requirements/develop.pip b/packages/flask-restx/opengnsys-flask-restx-1.3.0/requirements/develop.pip new file mode 100644 index 0000000..2c5e981 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/requirements/develop.pip @@ -0,0 +1,2 @@ +tox +black diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/requirements/doc.pip b/packages/flask-restx/opengnsys-flask-restx-1.3.0/requirements/doc.pip new file mode 100644 index 0000000..756726e --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/requirements/doc.pip @@ -0,0 +1,3 @@ +alabaster==0.7.12 +Sphinx==5.3.0 +sphinx-issues==3.0.1 diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/requirements/install.pip b/packages/flask-restx/opengnsys-flask-restx-1.3.0/requirements/install.pip new file mode 100644 index 0000000..76415ed --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/requirements/install.pip @@ -0,0 +1,6 @@ +aniso8601>=0.82 +jsonschema +Flask>=0.8, !=2.0.0 +werkzeug!=2.0.0 +pytz +importlib_resources diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/requirements/test.pip b/packages/flask-restx/opengnsys-flask-restx-1.3.0/requirements/test.pip new file mode 100644 index 0000000..e4d5814 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/requirements/test.pip @@ -0,0 +1,13 @@ +blinker +Faker==2.0.0 +mock==3.0.5 +pytest==7.0.1 +pytest-benchmark==3.4.1 +pytest-cov==4.0.0 +pytest-flask==1.3.0 +pytest-mock==3.6.1 +pytest-profiling==1.7.0 +tzlocal +invoke==2.2.0 +twine==3.8.0 +setuptools diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/setup.cfg b/packages/flask-restx/opengnsys-flask-restx-1.3.0/setup.cfg new file mode 100644 index 0000000..b56782f --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/setup.cfg @@ -0,0 +1,19 @@ +[bdist_wheel] +universal = 1 + +[tool:pytest] +testpaths = tests +python_files = test_*.py bench_*.py +python_functions = test_* bench_* +python_classes = *Test *Benchmark +markers = + api: test requiring an initialized API + request_context: switch the request + +[ossaudit] + +# The issue is fixed since the v40.8.0 of setuptools, but +# the python3.5 and python3.6 use the old versions. +# https://ossindex.sonatype.org/vuln/06e60262-8241-42ef-8f64-e3d72091de19 +# Ignore it until we suppor python < 3.7 +ignore-ids = 06e60262-8241-42ef-8f64-e3d72091de19 diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/setup.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/setup.py new file mode 100644 index 0000000..d845294 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/setup.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# flake8: noqa + +import io +import os +import re +import sys + +from setuptools import setup, find_packages + +RE_REQUIREMENT = re.compile(r"^\s*-r\s*(?P.*)$") + +PYPI_RST_FILTERS = ( + # Replace Python crossreferences by simple monospace + (r":(?:class|func|meth|mod|attr|obj|exc|data|const):`~(?:\w+\.)*(\w+)`", r"``\1``"), + (r":(?:class|func|meth|mod|attr|obj|exc|data|const):`([^`]+)`", r"``\1``"), + # replace doc references + ( + r":doc:`(.+) <(.*)>`", + r"`\1 `_", + ), + # replace issues references + ( + r":issue:`(.+?)`", + r"`#\1 `_", + ), + # replace pr references + (r":pr:`(.+?)`", r"`#\1 `_"), + # replace commit references + ( + r":commit:`(.+?)`", + r"`#\1 `_", + ), + # Drop unrecognized currentmodule + (r"\.\. currentmodule:: .*", ""), +) + + +def rst(filename): + """ + Load rst file and sanitize it for PyPI. + Remove unsupported github tags: + - code-block directive + - all badges + """ + content = io.open(filename).read() + for regex, replacement in PYPI_RST_FILTERS: + content = re.sub(regex, replacement, content) + return content + + +def pip(filename): + """Parse pip reqs file and transform it to setuptools requirements.""" + requirements = [] + for line in io.open(os.path.join("requirements", "{0}.pip".format(filename))): + line = line.strip() + if not line or "://" in line or line.startswith("#"): + continue + requirements.append(line) + return requirements + + +long_description = "\n".join((rst("README.rst"), "")) + + +exec( + compile(open("flask_restx/__about__.py").read(), "flask_restx/__about__.py", "exec") +) + +install_requires = pip("install") +doc_require = pip("doc") +tests_require = pip("test") +dev_require = tests_require + pip("develop") + +setup( + name="flask-restx", + version=__version__, + description=__description__, + long_description=long_description, + url="https://github.com/python-restx/flask-restx", + author="python-restx Authors", + packages=find_packages(exclude=["tests", "tests.*"]), + include_package_data=True, + install_requires=install_requires, + tests_require=tests_require, + dev_require=dev_require, + extras_require={ + "test": tests_require, + "doc": doc_require, + "dev": dev_require, + }, + license="BSD-3-Clause", + zip_safe=False, + keywords="flask restx rest api swagger openapi", + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Programming Language :: Python", + "Environment :: Web Environment", + "Operating System :: OS Independent", + "Intended Audience :: Developers", + "Topic :: System :: Software Distribution", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: Implementation :: PyPy", + "Topic :: Software Development :: Libraries :: Python Modules", + "License :: OSI Approved :: BSD License", + ], +) diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/tasks.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tasks.py new file mode 100644 index 0000000..4f14832 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tasks.py @@ -0,0 +1,220 @@ +import os +import sys + +from datetime import datetime + +from invoke import task + +ROOT = os.path.dirname(__file__) + +CLEAN_PATTERNS = [ + "build", + "dist", + "cover", + "docs/_build", + "**/*.pyc", + ".tox", + "**/__pycache__", + "reports", + "*.egg-info", +] + + +def color(code): + """A simple ANSI color wrapper factory""" + return lambda t: "\033[{0}{1}\033[0;m".format(code, t) + + +green = color("1;32m") +red = color("1;31m") +blue = color("1;30m") +cyan = color("1;36m") +purple = color("1;35m") +white = color("1;39m") + + +def header(text): + """Display an header""" + print(" ".join((blue(">>"), cyan(text)))) + sys.stdout.flush() + + +def info(text, *args, **kwargs): + """Display informations""" + text = text.format(*args, **kwargs) + print(" ".join((purple(">>>"), text))) + sys.stdout.flush() + + +def success(text): + """Display a success message""" + print(" ".join((green(">>"), white(text)))) + sys.stdout.flush() + + +def error(text): + """Display an error message""" + print(red("✘ {0}".format(text))) + sys.stdout.flush() + + +def exit(text=None, code=-1): + if text: + error(text) + sys.exit(-1) + + +def build_args(*args): + return " ".join(a for a in args if a) + + +@task +def clean(ctx): + """Cleanup all build artifacts""" + header(clean.__doc__) + with ctx.cd(ROOT): + for pattern in CLEAN_PATTERNS: + info("Removing {0}", pattern) + ctx.run("rm -rf {0}".format(pattern)) + + +@task +def deps(ctx): + """Install or update development dependencies""" + header(deps.__doc__) + with ctx.cd(ROOT): + ctx.run( + "pip install -r requirements/develop.pip -r requirements/doc.pip", pty=True + ) + + +@task +def demo(ctx): + """Run the demo""" + header(demo.__doc__) + with ctx.cd(ROOT): + ctx.run("python examples/todo.py") + + +@task +def test(ctx, profile=False): + """Run tests suite""" + header(test.__doc__) + kwargs = build_args( + "--benchmark-skip", + "--profile" if profile else None, + ) + with ctx.cd(ROOT): + ctx.run("pytest {0}".format(kwargs), pty=True) + + +@task +def benchmark( + ctx, + max_time=2, + save=False, + compare=False, + histogram=False, + profile=False, + tox=False, +): + """Run benchmarks""" + header(benchmark.__doc__) + ts = datetime.now() + kwargs = build_args( + "--benchmark-max-time={0}".format(max_time), + "--benchmark-autosave" if save else None, + "--benchmark-compare" if compare else None, + "--benchmark-histogram=histograms/{0:%Y%m%d-%H%M%S}".format(ts) + if histogram + else None, + "--benchmark-cprofile=tottime" if profile else None, + ) + cmd = "pytest tests/benchmarks {0}".format(kwargs) + if tox: + envs = ctx.run("tox -l", hide=True).stdout.splitlines() + envs = ",".join(e for e in envs if e != "doc") + cmd = "tox -e {envs} -- {cmd}".format(envs=envs, cmd=cmd) + ctx.run(cmd, pty=True) + + +@task +def cover(ctx, html=False): + """Run tests suite with coverage""" + header(cover.__doc__) + extra = "--cov-report html" if html else "" + with ctx.cd(ROOT): + ctx.run( + "pytest --benchmark-skip --cov flask_restx --cov-report term --cov-report xml {0}".format( + extra + ), + pty=True, + ) + + +@task +def tox(ctx): + """Run tests against Python versions""" + header(tox.__doc__) + ctx.run("tox", pty=True) + + +@task +def qa(ctx): + """Run a quality report""" + header(qa.__doc__) + with ctx.cd(ROOT): + info("Ensure PyPI can render README and CHANGELOG") + info("Building dist package") + dist = ctx.run("python setup.py sdist", pty=True, warn=False, hide=True) + if dist.failed: + error("Unable to build sdist package") + exit("Quality check failed", dist.return_code) + readme_results = ctx.run("twine check dist/*", pty=True, warn=True, hide=True) + if readme_results.failed: + print(readme_results.stdout) + error("README and/or CHANGELOG is not renderable by PyPI") + else: + success("README and CHANGELOG are renderable by PyPI") + if readme_results.failed: + exit("Quality check failed", readme_results.return_code) + success("Quality check OK") + + +@task +def doc(ctx): + """Build the documentation""" + header(doc.__doc__) + with ctx.cd(os.path.join(ROOT, "doc")): + ctx.run("make html", pty=True) + + +@task +def assets(ctx): + """Fetch web assets""" + header(assets.__doc__) + with ctx.cd(ROOT): + ctx.run("npm install") + ctx.run("mkdir -p flask_restx/static") + ctx.run( + "cp node_modules/swagger-ui-dist/{swagger-ui*.{css,js}{,.map},favicon*.png,oauth2-redirect.html} flask_restx/static" + ) + # Until next release we need to install droid sans separately + ctx.run( + "cp node_modules/typeface-droid-sans/index.css flask_restx/static/droid-sans.css" + ) + ctx.run("cp -R node_modules/typeface-droid-sans/files flask_restx/static/") + + +@task +def dist(ctx): + """Package for distribution""" + header(dist.__doc__) + with ctx.cd(ROOT): + ctx.run("python setup.py bdist_wheel", pty=True) + + +@task(clean, deps, test, doc, qa, assets, dist, default=True) +def all(ctx): + """Run tests, reports and packaging""" + pass diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/__init__.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/benchmarks/bench_marshalling.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/benchmarks/bench_marshalling.py new file mode 100644 index 0000000..9cf979b --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/benchmarks/bench_marshalling.py @@ -0,0 +1,56 @@ +import pytest + +from faker import Faker + +from flask_restx import marshal, fields + +fake = Faker() + +person_fields = {"name": fields.String, "age": fields.Integer} + +family_fields = { + "father": fields.Nested(person_fields), + "mother": fields.Nested(person_fields), + "children": fields.List(fields.Nested(person_fields)), +} + + +def person(): + return {"name": fake.name(), "age": fake.pyint()} + + +def family(): + return {"father": person(), "mother": person(), "children": [person(), person()]} + + +def marshal_simple(): + return marshal(person(), person_fields) + + +def marshal_nested(): + return marshal(family(), family_fields) + + +def marshal_simple_with_mask(app): + with app.test_request_context("/", headers={"X-Fields": "name"}): + return marshal(person(), person_fields) + + +def marshal_nested_with_mask(app): + with app.test_request_context("/", headers={"X-Fields": "father,children{name}"}): + return marshal(family(), family_fields) + + +@pytest.mark.benchmark(group="marshalling") +class MarshallingBenchmark(object): + def bench_marshal_simple(self, benchmark): + benchmark(marshal_simple) + + def bench_marshal_nested(self, benchmark): + benchmark(marshal_nested) + + def bench_marshal_simple_with_mask(self, app, benchmark): + benchmark(marshal_simple_with_mask, app) + + def bench_marshal_nested_with_mask(self, app, benchmark): + benchmark(marshal_nested_with_mask, app) diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/benchmarks/bench_swagger.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/benchmarks/bench_swagger.py new file mode 100644 index 0000000..da52b04 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/benchmarks/bench_swagger.py @@ -0,0 +1,97 @@ +import pytest + +from flask_restx import fields, Api, Resource +from flask_restx.swagger import Swagger + +api = Api() + +person = api.model("Person", {"name": fields.String, "age": fields.Integer}) + +family = api.model( + "Family", + { + "name": fields.String, + "father": fields.Nested(person), + "mother": fields.Nested(person), + "children": fields.List(fields.Nested(person)), + }, +) + + +@api.route("/families", endpoint="families") +class Families(Resource): + @api.marshal_with(family) + def get(self): + """List all families""" + pass + + @api.marshal_with(family) + @api.response(201, "Family created") + def post(self): + """Create a new family""" + pass + + +@api.route("/families//", endpoint="family") +@api.response(404, "Family not found") +class Family(Resource): + @api.marshal_with(family) + def get(self): + """Get a family given its name""" + pass + + @api.marshal_with(family) + def put(self): + """Update a family given its name""" + pass + + +@api.route("/persons", endpoint="persons") +class Persons(Resource): + @api.marshal_with(person) + def get(self): + """List all persons""" + pass + + @api.marshal_with(person) + @api.response(201, "Person created") + def post(self): + """Create a new person""" + pass + + +@api.route("/persons//", endpoint="person") +@api.response(404, "Person not found") +class Person(Resource): + @api.marshal_with(person) + def get(self): + """Get a person given its name""" + pass + + @api.marshal_with(person) + def put(self): + """Update a person given its name""" + pass + + +def swagger_specs(app): + with app.test_request_context("/"): + return Swagger(api).as_dict() + + +def swagger_specs_cached(app): + with app.test_request_context("/"): + return api.__schema__ + + +@pytest.mark.benchmark(group="swagger") +class SwaggerBenchmark(object): + @pytest.fixture(autouse=True) + def register(self, app): + api.init_app(app) + + def bench_swagger_specs(self, app, benchmark): + benchmark(swagger_specs, app) + + def bench_swagger_specs_cached(self, app, benchmark): + benchmark(swagger_specs_cached, app) diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/conftest.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/conftest.py new file mode 100644 index 0000000..66b7eae --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/conftest.py @@ -0,0 +1,95 @@ +import json +import pytest + +from flask import Flask, Blueprint +from flask.testing import FlaskClient + +import flask_restx as restx + + +class TestClient(FlaskClient): + # Borrowed from https://pythonadventures.wordpress.com/2016/03/06/detect-duplicate-keys-in-a-json-file/ + # Thank you to Wordpress author @ubuntuincident, aka Jabba Laci. + def dict_raise_on_duplicates(self, ordered_pairs): + """Reject duplicate keys.""" + d = {} + for k, v in ordered_pairs: + if k in d: + raise ValueError("duplicate key: %r" % (k,)) + else: + d[k] = v + return d + + def get_json(self, url, status=200, **kwargs): + response = self.get(url, **kwargs) + assert response.status_code == status + assert response.content_type == "application/json" + return json.loads( + response.data.decode("utf8"), + object_pairs_hook=self.dict_raise_on_duplicates, + ) + + def post_json(self, url, data, status=200, **kwargs): + response = self.post( + url, data=json.dumps(data), headers={"content-type": "application/json"} + ) + assert response.status_code == status + assert response.content_type == "application/json" + return json.loads(response.data.decode("utf8")) + + def get_specs(self, prefix="", status=200, **kwargs): + """Get a Swagger specification for a RESTX API""" + return self.get_json("{0}/swagger.json".format(prefix), status=status, **kwargs) + + +@pytest.fixture +def app(): + app = Flask(__name__) + app.test_client_class = TestClient + yield app + + +@pytest.fixture +def api(request, app): + marker = request.node.get_closest_marker("api") + bpkwargs = {} + kwargs = {} + if marker: + if "prefix" in marker.kwargs: + bpkwargs["url_prefix"] = marker.kwargs.pop("prefix") + if "subdomain" in marker.kwargs: + bpkwargs["subdomain"] = marker.kwargs.pop("subdomain") + kwargs = marker.kwargs + blueprint = Blueprint("api", __name__, **bpkwargs) + api = restx.Api(blueprint, **kwargs) + app.register_blueprint(blueprint) + yield api + + +@pytest.fixture +def mock_app(mocker): + app = mocker.Mock(Flask) + # mock Flask app object doesn't have any real loggers -> mock logging + # set up on Api object + mocker.patch.object(restx.Api, "_configure_namespace_logger") + app.view_functions = {} + app.extensions = {} + app.config = {} + return app + + +@pytest.fixture(autouse=True) +def _push_custom_request_context(request): + app = request.getfixturevalue("app") + options = request.node.get_closest_marker("request_context") + + if options is None: + return + + ctx = app.test_request_context(*options.args, **options.kwargs) + ctx.push() + + def teardown(): + ctx.pop() + + request.addfinalizer(teardown) diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/legacy/test_api_legacy.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/legacy/test_api_legacy.py new file mode 100644 index 0000000..b15a602 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/legacy/test_api_legacy.py @@ -0,0 +1,388 @@ +import flask +import pytest + +from json import dumps, JSONEncoder + +from flask import Blueprint, redirect, views +from werkzeug.exceptions import HTTPException, Unauthorized, BadRequest + +import flask_restx as restx + + +# Add a dummy Resource to verify that the app is properly set. +class HelloWorld(restx.Resource): + def get(self): + return {} + + +class APITest(object): + def test_unauthorized_no_challenge_by_default(self, api, mocker): + response = mocker.Mock() + response.headers = {} + response = api.unauthorized(response) + assert "WWW-Authenticate" not in response.headers + + @pytest.mark.api(serve_challenge_on_401=True) + def test_unauthorized(self, api, mocker): + response = mocker.Mock() + response.headers = {} + response = api.unauthorized(response) + assert response.headers["WWW-Authenticate"] == 'Basic realm="flask-restx"' + + @pytest.mark.options(HTTP_BASIC_AUTH_REALM="Foo") + @pytest.mark.api(serve_challenge_on_401=True) + def test_unauthorized_custom_realm(self, api, mocker): + response = mocker.Mock() + response.headers = {} + response = api.unauthorized(response) + assert response.headers["WWW-Authenticate"] == 'Basic realm="Foo"' + + def test_handle_error_401_no_challenge_by_default(self, api): + resp = api.handle_error(Unauthorized()) + assert resp.status_code == 401 + assert "WWW-Autheneticate" not in resp.headers + + @pytest.mark.api(serve_challenge_on_401=True) + def test_handle_error_401_sends_challege_default_realm(self, api): + exception = HTTPException() + exception.code = 401 + exception.data = {"foo": "bar"} + + resp = api.handle_error(exception) + assert resp.status_code == 401 + assert resp.headers["WWW-Authenticate"] == 'Basic realm="flask-restx"' + + @pytest.mark.api(serve_challenge_on_401=True) + @pytest.mark.options(HTTP_BASIC_AUTH_REALM="test-realm") + def test_handle_error_401_sends_challege_configured_realm(self, api): + resp = api.handle_error(Unauthorized()) + assert resp.status_code == 401 + assert resp.headers["WWW-Authenticate"] == 'Basic realm="test-realm"' + + def test_handle_error_does_not_swallow_exceptions(self, api): + exception = BadRequest("x") + + resp = api.handle_error(exception) + assert resp.status_code == 400 + assert resp.get_data() == b'{"message": "x"}\n' + + def test_api_representation(self, api): + @api.representation("foo") + def foo(): + pass + + assert api.representations["foo"] == foo + + def test_api_base(self, app): + api = restx.Api(app) + assert api.urls == {} + assert api.prefix == "" + assert api.default_mediatype == "application/json" + + def test_api_delayed_initialization(self, app, client): + api = restx.Api() + api.add_resource(HelloWorld, "/", endpoint="hello") + api.init_app(app) + assert client.get("/").status_code == 200 + + def test_api_prefix(self, app): + api = restx.Api(app, prefix="/foo") + assert api.prefix == "/foo" + + @pytest.mark.api(serve_challenge_on_401=True) + def test_handle_auth(self, api): + resp = api.handle_error(Unauthorized()) + assert resp.status_code == 401 + expected_data = dumps({"message": Unauthorized.description}) + "\n" + assert resp.data.decode() == expected_data + + assert "WWW-Authenticate" in resp.headers + + def test_media_types(self, app): + api = restx.Api(app) + + with app.test_request_context("/foo", headers={"Accept": "application/json"}): + assert api.mediatypes() == ["application/json"] + + def test_media_types_method(self, app, mocker): + api = restx.Api(app) + + with app.test_request_context( + "/foo", headers={"Accept": "application/xml; q=0.5"} + ): + assert api.mediatypes_method()(mocker.Mock()) == [ + "application/xml", + "application/json", + ] + + def test_media_types_q(self, app): + api = restx.Api(app) + + with app.test_request_context( + "/foo", + headers={"Accept": "application/json; q=1.0, application/xml; q=0.5"}, + ): + assert api.mediatypes() == ["application/json", "application/xml"] + + def test_decorator(self, mocker, mock_app): + def return_zero(func): + return 0 + + view = mocker.Mock() + api = restx.Api(mock_app) + api.decorators.append(return_zero) + api.output = mocker.Mock() + api.add_resource(view, "/foo", endpoint="bar") + + mock_app.add_url_rule.assert_called_with("/foo", view_func=0) + + def test_add_resource_endpoint(self, app, mocker): + view = mocker.Mock(**{"as_view.return_value.__name__": str("test_view")}) + + api = restx.Api(app) + api.add_resource(view, "/foo", endpoint="bar") + + view.as_view.assert_called_with("bar", api) + + def test_add_two_conflicting_resources_on_same_endpoint(self, app): + api = restx.Api(app) + + class Foo1(restx.Resource): + def get(self): + return "foo1" + + class Foo2(restx.Resource): + def get(self): + return "foo2" + + api.add_resource(Foo1, "/foo", endpoint="bar") + with pytest.raises(ValueError): + api.add_resource(Foo2, "/foo/toto", endpoint="bar") + + def test_add_the_same_resource_on_same_endpoint(self, app): + api = restx.Api(app) + + class Foo1(restx.Resource): + def get(self): + return "foo1" + + api.add_resource(Foo1, "/foo", endpoint="bar") + api.add_resource(Foo1, "/foo/toto", endpoint="blah") + + with app.test_client() as client: + foo1 = client.get("/foo") + assert foo1.data == b'"foo1"\n' + foo2 = client.get("/foo/toto") + assert foo2.data == b'"foo1"\n' + + def test_add_resource(self, mocker, mock_app): + api = restx.Api(mock_app) + api.output = mocker.Mock() + api.add_resource(views.MethodView, "/foo") + + mock_app.add_url_rule.assert_called_with("/foo", view_func=api.output()) + + def test_add_resource_kwargs(self, mocker, mock_app): + api = restx.Api(mock_app) + api.output = mocker.Mock() + api.add_resource(views.MethodView, "/foo", defaults={"bar": "baz"}) + + mock_app.add_url_rule.assert_called_with( + "/foo", view_func=api.output(), defaults={"bar": "baz"} + ) + + def test_add_resource_forward_resource_class_parameters(self, app, client): + api = restx.Api(app) + + class Foo(restx.Resource): + def __init__(self, api, *args, **kwargs): + self.one = args[0] + self.two = kwargs["secret_state"] + super(Foo, self).__init__(api, *args, **kwargs) + + def get(self): + return "{0} {1}".format(self.one, self.two) + + api.add_resource( + Foo, + "/foo", + resource_class_args=("wonderful",), + resource_class_kwargs={"secret_state": "slurm"}, + ) + + foo = client.get("/foo") + assert foo.data == b'"wonderful slurm"\n' + + def test_output_unpack(self, app): + def make_empty_response(): + return {"foo": "bar"} + + api = restx.Api(app) + + with app.test_request_context("/foo"): + wrapper = api.output(make_empty_response) + resp = wrapper() + assert resp.status_code == 200 + assert resp.data.decode() == '{"foo": "bar"}\n' + + def test_output_func(self, app): + def make_empty_resposne(): + return flask.make_response("") + + api = restx.Api(app) + + with app.test_request_context("/foo"): + wrapper = api.output(make_empty_resposne) + resp = wrapper() + assert resp.status_code == 200 + assert resp.data.decode() == "" + + def test_resource(self, app, mocker): + resource = restx.Resource() + resource.get = mocker.Mock() + with app.test_request_context("/foo"): + resource.dispatch_request() + + def test_resource_resp(self, app, mocker): + resource = restx.Resource() + resource.get = mocker.Mock() + with app.test_request_context("/foo"): + resource.get.return_value = flask.make_response("") + resource.dispatch_request() + + def test_resource_text_plain(self, app): + def text(data, code, headers=None): + return flask.make_response(str(data)) + + class Foo(restx.Resource): + representations = { + "text/plain": text, + } + + def get(self): + return "hello" + + with app.test_request_context("/foo", headers={"Accept": "text/plain"}): + resource = Foo(None) + resp = resource.dispatch_request() + assert resp.data.decode() == "hello" + + @pytest.mark.request_context("/foo") + def test_resource_error(self, app): + resource = restx.Resource() + with pytest.raises(AssertionError): + resource.dispatch_request() + + @pytest.mark.request_context("/foo", method="HEAD") + def test_resource_head(self, app): + resource = restx.Resource() + with pytest.raises(AssertionError): + resource.dispatch_request() + + def test_endpoints(self, app): + api = restx.Api(app) + api.add_resource(HelloWorld, "/ids/", endpoint="hello") + with app.test_request_context("/foo"): + assert api._has_fr_route() is False + + with app.test_request_context("/ids/3"): + assert api._has_fr_route() is True + + def test_url_for(self, app): + api = restx.Api(app) + api.add_resource(HelloWorld, "/ids/") + with app.test_request_context("/foo"): + assert api.url_for(HelloWorld, id=123) == "/ids/123" + + def test_url_for_with_blueprint(self, app): + """Verify that url_for works when an Api object is mounted on a + Blueprint. + """ + api_bp = Blueprint("api", __name__) + api = restx.Api(api_bp) + api.add_resource(HelloWorld, "/foo/") + app.register_blueprint(api_bp) + with app.test_request_context("/foo"): + assert api.url_for(HelloWorld, bar="baz") == "/foo/baz" + + def test_exception_header_forwarding_doesnt_duplicate_headers(self, api): + """Test that HTTPException's headers do not add a duplicate + Content-Length header + + https://github.com/flask-restful/flask-restful/issues/534 + """ + r = api.handle_error(BadRequest()) + assert len(r.headers.getlist("Content-Length")) == 1 + + def test_read_json_settings_from_config(self, app, client): + class TestConfig(object): + RESTX_JSON = {"indent": 2, "sort_keys": True, "separators": (", ", ": ")} + + app.config.from_object(TestConfig) + api = restx.Api(app) + + class Foo(restx.Resource): + def get(self): + return {"foo": "bar", "baz": "qux"} + + api.add_resource(Foo, "/foo") + + data = client.get("/foo").data + + expected = b'{\n "baz": "qux", \n "foo": "bar"\n}\n' + + assert data == expected + + def test_use_custom_jsonencoder(self, app, client): + class CabageEncoder(JSONEncoder): + def default(self, obj): + return "cabbage" + + class TestConfig(object): + RESTX_JSON = {"cls": CabageEncoder} + + app.config.from_object(TestConfig) + api = restx.Api(app) + + class Cabbage(restx.Resource): + def get(self): + return {"frob": object()} + + api.add_resource(Cabbage, "/cabbage") + + data = client.get("/cabbage").data + + expected = b'{"frob": "cabbage"}\n' + assert data == expected + + def test_json_with_no_settings(self, api, client): + class Foo(restx.Resource): + def get(self): + return {"foo": "bar"} + + api.add_resource(Foo, "/foo") + + data = client.get("/foo").data + + expected = b'{"foo": "bar"}\n' + assert data == expected + + def test_redirect(self, api, client): + class FooResource(restx.Resource): + def get(self): + return redirect("/") + + api.add_resource(FooResource, "/api") + + resp = client.get("/api") + assert resp.status_code == 302 + # FIXME: The behavior changed somewhere between Flask 2.0.3 and 2.2.x + assert resp.headers["Location"].endswith("/") + + def test_calling_owns_endpoint_before_api_init(self): + api = restx.Api() + api.owns_endpoint("endpoint") + # with pytest.raises(AttributeError): + # try: + # except AttributeError as ae: + # self.fail(ae.message) diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/legacy/test_api_with_blueprint.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/legacy/test_api_with_blueprint.py new file mode 100644 index 0000000..995d606 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/legacy/test_api_with_blueprint.py @@ -0,0 +1,160 @@ +import flask + +from flask import Blueprint, request + +import flask_restx as restx + + +# Add a dummy Resource to verify that the app is properly set. +class HelloWorld(restx.Resource): + def get(self): + return {} + + +class GoodbyeWorld(restx.Resource): + def __init__(self, err): + self.err = err + + def get(self): + flask.abort(self.err) + + +class APIWithBlueprintTest(object): + def test_api_base(self, app): + blueprint = Blueprint("test", __name__) + api = restx.Api(blueprint) + app.register_blueprint(blueprint) + assert api.urls == {} + assert api.prefix == "" + assert api.default_mediatype == "application/json" + + def test_api_delayed_initialization(self, app): + blueprint = Blueprint("test", __name__) + api = restx.Api() + api.init_app(blueprint) + app.register_blueprint(blueprint) + api.add_resource(HelloWorld, "/", endpoint="hello") + + def test_add_resource_endpoint(self, app, mocker): + blueprint = Blueprint("test", __name__) + api = restx.Api(blueprint) + view = mocker.Mock(**{"as_view.return_value.__name__": str("test_view")}) + api.add_resource(view, "/foo", endpoint="bar") + app.register_blueprint(blueprint) + view.as_view.assert_called_with("bar", api) + + def test_add_resource_endpoint_after_registration(self, app, mocker): + blueprint = Blueprint("test", __name__) + api = restx.Api(blueprint) + app.register_blueprint(blueprint) + view = mocker.Mock(**{"as_view.return_value.__name__": str("test_view")}) + api.add_resource(view, "/foo", endpoint="bar") + view.as_view.assert_called_with("bar", api) + + def test_url_with_api_prefix(self, app): + blueprint = Blueprint("test", __name__) + api = restx.Api(blueprint, prefix="/api") + api.add_resource(HelloWorld, "/hi", endpoint="hello") + app.register_blueprint(blueprint) + with app.test_request_context("/api/hi"): + assert request.endpoint == "test.hello" + + def test_url_with_blueprint_prefix(self, app): + blueprint = Blueprint("test", __name__, url_prefix="/bp") + api = restx.Api(blueprint) + api.add_resource(HelloWorld, "/hi", endpoint="hello") + app.register_blueprint(blueprint) + with app.test_request_context("/bp/hi"): + assert request.endpoint == "test.hello" + + def test_url_with_registration_prefix(self, app): + blueprint = Blueprint("test", __name__) + api = restx.Api(blueprint) + api.add_resource(HelloWorld, "/hi", endpoint="hello") + app.register_blueprint(blueprint, url_prefix="/reg") + with app.test_request_context("/reg/hi"): + assert request.endpoint == "test.hello" + + def test_registration_prefix_overrides_blueprint_prefix(self, app): + blueprint = Blueprint("test", __name__, url_prefix="/bp") + api = restx.Api(blueprint) + api.add_resource(HelloWorld, "/hi", endpoint="hello") + app.register_blueprint(blueprint, url_prefix="/reg") + with app.test_request_context("/reg/hi"): + assert request.endpoint == "test.hello" + + def test_url_with_api_and_blueprint_prefix(self, app): + blueprint = Blueprint("test", __name__, url_prefix="/bp") + api = restx.Api(blueprint, prefix="/api") + api.add_resource(HelloWorld, "/hi", endpoint="hello") + app.register_blueprint(blueprint) + with app.test_request_context("/bp/api/hi"): + assert request.endpoint == "test.hello" + + def test_error_routing(self, app, mocker): + blueprint = Blueprint("test", __name__) + api = restx.Api(blueprint) + api.add_resource(HelloWorld, "/hi", endpoint="hello") + api.add_resource(GoodbyeWorld(404), "/bye", endpoint="bye") + app.register_blueprint(blueprint) + with app.test_request_context("/hi", method="POST"): + assert api._should_use_fr_error_handler() is True + assert api._has_fr_route() is True + with app.test_request_context("/bye"): + api._should_use_fr_error_handler = mocker.Mock(return_value=False) + assert api._has_fr_route() is True + + def test_non_blueprint_rest_error_routing(self, app, mocker): + blueprint = Blueprint("test", __name__) + api = restx.Api(blueprint) + api.add_resource(HelloWorld, "/hi", endpoint="hello") + api.add_resource(GoodbyeWorld(404), "/bye", endpoint="bye") + app.register_blueprint(blueprint, url_prefix="/blueprint") + api2 = restx.Api(app) + api2.add_resource(HelloWorld(api), "/hi", endpoint="hello") + api2.add_resource(GoodbyeWorld(404), "/bye", endpoint="bye") + with app.test_request_context("/hi", method="POST"): + assert api._should_use_fr_error_handler() is False + assert api2._should_use_fr_error_handler() is True + assert api._has_fr_route() is False + assert api2._has_fr_route() is True + with app.test_request_context("/blueprint/hi", method="POST"): + assert api._should_use_fr_error_handler() is True + assert api2._should_use_fr_error_handler() is False + assert api._has_fr_route() is True + assert api2._has_fr_route() is False + api._should_use_fr_error_handler = mocker.Mock(return_value=False) + api2._should_use_fr_error_handler = mocker.Mock(return_value=False) + with app.test_request_context("/bye"): + assert api._has_fr_route() is False + assert api2._has_fr_route() is True + with app.test_request_context("/blueprint/bye"): + assert api._has_fr_route() is True + assert api2._has_fr_route() is False + + def test_non_blueprint_non_rest_error_routing(self, app, mocker): + blueprint = Blueprint("test", __name__) + api = restx.Api(blueprint) + api.add_resource(HelloWorld, "/hi", endpoint="hello") + api.add_resource(GoodbyeWorld(404), "/bye", endpoint="bye") + app.register_blueprint(blueprint, url_prefix="/blueprint") + + @app.route("/hi") + def hi(): + return "hi" + + @app.route("/bye") + def bye(): + flask.abort(404) + + with app.test_request_context("/hi", method="POST"): + assert api._should_use_fr_error_handler() is False + assert api._has_fr_route() is False + with app.test_request_context("/blueprint/hi", method="POST"): + assert api._should_use_fr_error_handler() is True + assert api._has_fr_route() is True + api._should_use_fr_error_handler = mocker.Mock(return_value=False) + with app.test_request_context("/bye"): + assert api._has_fr_route() is False + with app.test_request_context("/blueprint/bye"): + assert api._has_fr_route() is True diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/postman-v1.schema.json b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/postman-v1.schema.json new file mode 100644 index 0000000..1a5ae36 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/postman-v1.schema.json @@ -0,0 +1,626 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "https://schema.getpostman.com/json/collection/v1.0.0/", + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Every collection is identified by the unique value of this field. The value of this field is usually easiest to generate using a [UID](https://tools.ietf.org/html/rfc4122#section-4.4%29) generator function. If you already have a collection, it is recommended that you maintain the same id since changing the id usually implies that this is a different collection than it was originally." + }, + "name": { + "type": "string", + "description": "A collection's friendly name is defined by this field. You would want to set this field to a value that would allow you to easily identify this collection among a bunch of other collections, as such outlining its usage or content." + }, + "description": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Provide a long description of this collection using this field. This field supports markdown syntax to better format the description." + }, + "order": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true, + "description": "The order array ensures that your requests and folders don't randomly get shuffled up. It holds a sequence of [UUIDs](https://en.wikipedia.org/wiki/Universally_unique_identifier) corresponding to folders and requests.\n *Note that if a folder ID or a request ID (if the request is not already part of a folder) is not included in the order array, the request or the folder will not show up in the collection.*" + }, + "folders": { + "type": "array", + "items": { + "title": "Folder", + "description": "One of the primary goals of Postman is to organize the development of APIs. To this end, it is necessary to be able to group requests together. This can be achived using 'Folders'. A folder just is an ordered set of requests.", + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "In order to be able to uniquely identify different folders within a collection, Postman assigns each folder a unique ID (a [UUID](https://en.wikipedia.org/wiki/Globally_unique_identifier)). This field contains that value." + }, + "name": { + "type": "string", + "description": "A folder's friendly name is defined by this field. You would want to set this field to a value that would allow you to easily identify this folder." + }, + "description": { + "type": "string", + "description": "Essays about the folder go into this field!" + }, + "order": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true, + "description": "Postman preserves the order of your requests within each folder. This field holds a sequence of [UUIDs](https://en.wikipedia.org/wiki/Globally_unique_identifier), where each ID corresponds to a particular Postman request." + }, + "collection_id": { + "type": "string", + "description": "Postman folders are always a part of a collection. That collection's unique ID (which is a [UUID](https://en.wikipedia.org/wiki/Globally_unique_identifier)) is stored in this field." + } + }, + "required": [ + "id", + "name", + "description", + "order" + ] + }, + "description": "Folders are the way to go if you want to group your requests and to keep things organised. Folders can also be useful in sequentially requesting a part of the entire collection by using [Postman Collection Runner](https://www.getpostman.com/docs/jetpacks_running_collections) or [Newman](https://github.com/postmanlabs/newman) on a particular folder." + }, + "timestamp": { + "type": "number", + "multipleOf": 1 + }, + "requests": { + "type": "array", + "description": "", + "items": { + "title": "Request", + "description": "A request represents an HTTP request.", + "type": "object", + "properties": { + "folder": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Postman requests may or may not be a part of a folder. If this request belongs to a folder, that folder's unique ID (which is a [UUID](https://en.wikipedia.org/wiki/Globally_unique_identifier)) is stored in this field." + }, + "id": { + "type": "string", + "description": "Postman can store a number of requests in each collection. In order to preserve the order of each request, we need to be able to identify requests uniquely. This field is a UUID assigned to each request." + }, + "name": { + "type": "string", + "description": "Sometimes, you just need to call your request 'Bob'. Postman will let you do that, and store the name you give in this field." + }, + "dataMode": { + "type": "string", + "enum": [ + "raw", + "urlencoded", + "params" + ], + "description": "A request can have a specific data mode, and Postman supports three." + }, + "data": { + "oneOf": [ + { + "type": "array", + "description": "Data is an array of key-values that the request goes with. POST data, PUT data, etc goes here.", + "items": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "value": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "type": { + "enum": [ + "file", + "text" + ] + } + } + } + }, + { + "type": "null" + } + ] + }, + "descriptionFormat": { + "oneOf": [ + { + "type": "string", + "enum": [ + "html", + "markdown" + ] + }, + { + "type": "null" + } + ], + "description": "A request can have an associated description text. Since description is meant to be long, it can be in either ``html`` or ``markdown`` formats. This field specifies that format." + }, + "description": { + "type": "string", + "description": "The description of this request. Can be as long as you want. Postman also supports two formats for your description, ``markdown`` and ``html``." + }, + "headers": { + "type": "string", + "description": "No HTTP request is complete without its headers, and the same is true for a Postman request. This field contains all the HTTP Headers in a raw string format." + }, + "method": { + "type": "string", + "enum": [ + "GET", + "PUT", + "POST", + "PATCH", + "DELETE", + "COPY", + "HEAD", + "OPTIONS", + "LINK", + "UNLINK", + "PURGE", + "LOCK", + "UNLOCK", + "PROPFIND", + "VIEW" + ], + "description": "The HTTP method associated with this request." + }, + "currentHelper": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Postman can associate helpers with a request, which help with activities such as OAuth, Basic Authentication, etc. The type of helper associated with this request is stored in this field." + }, + "helperAttributes": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "object", + "title": "DigestAuth", + "description": "Helper attributes for Digest Authentication", + "properties": { + "id": { + "type": "string", + "enum": [ + "digest" + ], + "description": "This field contains the type of the helper. In this case, it is ``digest``" + }, + "algorithm": { + "type": "string", + "description": "The hashing algorithm used in this Digest authenticated request is stored in this field." + }, + "username": { + "type": "string", + "description": "The username to be used for digest Authentication is stored in this field." + }, + "realm": { + "type": "string", + "description": "The authentication 'realm' is stored in this field. Refer to [RFC 2617](https://tools.ietf.org/html/rfc2617) for details." + }, + "password": { + "type": "string", + "description": "The password to be used for digest Authentication is stored in this field." + }, + "nonce": { + "type": "string", + "description": "The Digest server challenges clients. A nonce is a part of that challenge, stored in this field." + }, + "qop": { + "type": "string", + "description": "Indicates what \"quality of protection\" the client has applied to the message." + }, + "nonceCount": { + "type": "string", + "description": "The nonceCount is the hexadecimal count of the number of requests (including the current request) that the client has sent with the nonce value in this request." + }, + "clientNonce": { + "type": "string", + "description": "A client nonce enhances the security of HTTP Digest Authentication, and it is stored in this field." + }, + "opaque": { + "type": "string", + "description": "A string of data, specified by the server, which should be returned by the client unchanged." + } + }, + "required": [ + "id" + ] + }, + { + "type": "object", + "title": "BasicAuth", + "description": "Helper attributes for Basic Authentication", + "properties": { + "id": { + "type": "string", + "enum": [ + "basic" + ], + "description": "This field contains the type of the helper. In this case, it is ``basic``" + }, + "username": { + "type": "string", + "description": "The username to be used for Basic Authentication is stored in this field." + }, + "password": { + "type": "string", + "description": "The password to be used for digest Authentication is stored in this field." + } + }, + "required": [ + "id" + ] + }, + { + "type": "object", + "title": "OAuth1", + "description": "Helper attributes for OAuth-1 Authentication", + "properties": { + "id": { + "type": "string", + "enum": [ + "oAuth1" + ], + "description": "This field contains the type of the helper. In this case, it is ``oAuth1``" + }, + "consumerKey": { + "type": "string", + "description": "The oAuth1 Consumer Secret, along with the Consumer Key authenticates the client. This field holds the oAuth1 Consumer Key" + }, + "consumerSecret": { + "type": "string", + "description": "The oAuth1 Consumer Secret, along with the Consumer Key authenticates the client. The consumer secret is stored in this field." + }, + "token": { + "type": "string", + "description": "The request token is a temporary credential, and is stored by Postman in this field." + }, + "tokenSecret": { + "type": "string", + "description": "The request token secret is a temporary credential." + }, + "signatureMethod": { + "type": "string", + "description": "The name of the signature method used by the client to sign the request." + }, + "nonce": { + "type": "string", + "description": "A nonce is a random string, uniquely generated by the client to allow the server to verify that a request has never been made before and helps prevent replay attacks when requests are made over a non-secure channel." + }, + "version": { + "type": "string", + "description": "The oAuth version, usually, this is ``1.0`` for OAuth-1" + }, + "realm": { + "type": "string", + "description": "The realm directive is required for all authentication schemes that issue a challenge. Refer to [RFC 2617](http://tools.ietf.org/html/rfc2617#section-1.2) for more details." + }, + "header": { + "type": "boolean", + "description": "Set this parameter to 'true' if you want Postman to add the OAuth parameters to the request headers. If false, the query string is used instead." + }, + "auto": { + "type": "boolean", + "description": "This field controls whether OAuth1 parameters are automatically added to the request." + }, + "includeEmpty": { + "type": "boolean", + "description": "If you would like to include empty parameters in the request, set this to 'true', else set it to 'false'" + } + }, + "required": [ + "id" + ] + }, + { + "type": "null" + }, + { + "type": "object", + "additionalProperties": false, + "properties": {} + } + ], + "description": "A helper may require a number of parameters to actually be helpful. The parameters used by the helper can be stored in this field, as an object. E.g when using Basic Authentication, the username and password will be stored here." + }, + "pathVariables": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "object" + }, + { + "type": "null" + } + ], + "description": "A Postman request allows you to use Path Variables in a request, e.g: ``/search/:bookId``. This field stores these variables." + }, + "url": { + "type": "string", + "description": "Contains the complete URL for this request, along with the path variables, if any." + }, + "preRequestScript": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "In some use cases, it's necessary to run a bit of code or perform some tasks before sending a request. Postman implements this feature by the use of this field. Any code written to this field is run before running a request." + }, + "tests": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Postman allows you to define a script that is run after executing the request, which may act on the response. Such a script is stored in this field." + }, + "time": { + "type": "number", + "multipleOf": 1, + "description": "The timestamp for this request." + }, + "responses": { + "type": "array", + "description": "A Postman request can have multiple responses associated with it. These responses are stored in this field.", + "items": { + "title": "Response", + "description": "A response represents an HTTP response.", + "properties": { + "request": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "object" + }, + { + "type": "null" + } + ], + "description": "A response is associated with a request. This fields contains the UUID of the request corresponding to this response." + }, + "id": { + "type": "string", + "description": "In order to unambiguously identify a response, Postman assigns a UUID to it, and stores it in this field." + }, + "name": { + "type": "string", + "description": "A response can have a friendly name, which goes here." + }, + "status": { + "type": "string", + "description": "" + }, + "responseCode": { + "type": "object", + "title": "ResponseCode", + "properties": { + "code": { + "type": "number", + "description": "The numeric HTTP response code." + }, + "name": { + "type": "string", + "description": "The textual HTTP response code." + }, + "detail": { + "type": "string", + "description": "Detailed explanation of the response code." + } + }, + "required": [ + "code", + "name" + ] + }, + "time": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ], + "description": "The time taken by this particular HTTP transaction to complete is stored in this field." + }, + "headers": { + "type": "array", + "title": "Header", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Some headers can have names associated with them, which are stored in this field." + }, + "key": { + "type": "string", + "description": "The left hand side (LHS) or 'key' of the header." + }, + "value": { + "type": "string", + "description": "Value of the header, or the right hand side (RHS)." + }, + "description": { + "type": "string", + "description": "An optional description about the header." + } + }, + "required": [ + "name", + "key", + "value" + ] + } + }, + "cookies": { + "type": "array", + "title": "Cookie", + "items": { + "type": "object", + "properties": { + "domain": { + "type": "string", + "description": "The domain for which this cookie is valid." + }, + "expirationDate": { + "type": "number", + "description": "The timestamp of the time when the cookie expires." + }, + "hostOnly": { + "type": "boolean", + "description": "Indicates if this cookie is Host Only." + }, + "httpOnly": { + "type": "boolean", + "description": "Indicates if this cookie is HTTP Only." + }, + "name": { + "type": "string", + "description": "This is the name of the Cookie." + }, + "path": { + "type": "string", + "description": "The path associated with the Cookie." + }, + "secure": { + "type": "boolean", + "description": "Indicates if the 'secure' flag is set on the Cookie." + }, + "session": { + "type": "boolean", + "description": "True if the cookie is a session cookie." + }, + "storeId": { + "type": "string", + "description": "The ID of the cookie store containing this cookie." + }, + "value": { + "type": "string", + "description": "The value of the Cookie." + }, + "expires": { + "type": "string", + "description": "Human readable expiration time." + } + }, + "required": [ + "domain", + "expirationDate", + "hostOnly", + "httpOnly", + "name", + "path", + "secure", + "session", + "storeId", + "value", + "expires" + ] + } + }, + "mime": { + "type": "string", + "description": "Mimetype of the response." + }, + "text": { + "type": "string", + "description": "The raw text of the response." + }, + "language": { + "type": "string", + "enum": [ + "html", + "javascript", + "xml" + ], + "description": "The language associated with the response." + }, + "rawDataType": { + "type": "string", + "description": "The data type of the raw response." + } + }, + "required": [ + "id", + "responseCode", + "request" + ] + } + }, + "rawModeData": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + }, + { + "type": "array" + } + ], + "description": "Contains the raw data (parameters) that Postman sends to the server" + }, + "collectionId": { + "type": "string", + "description": "This field contains the unique ID of the collection to which this request belongs." + } + }, + "required": [ + "id", + "method", + "url", + "headers", + "name" + ] + } + } + }, + "required": [ + "id", + "name", + "order", + "requests" + ] +} diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_accept.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_accept.py new file mode 100644 index 0000000..6785913 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_accept.py @@ -0,0 +1,163 @@ +import flask_restx as restx + + +class Foo(restx.Resource): + def get(self): + return "data" + + +class ErrorsTest(object): + def test_accept_default_application_json(self, app, client): + api = restx.Api(app) + api.add_resource(Foo, "/test/") + + res = client.get("/test/", headers={"Accept": None}) + assert res.status_code == 200 + assert res.content_type == "application/json" + + def test_accept_application_json_by_default(self, app, client): + api = restx.Api(app) + api.add_resource(Foo, "/test/") + + res = client.get("/test/", headers=[("Accept", "application/json")]) + assert res.status_code == 200 + assert res.content_type == "application/json" + + def test_accept_no_default_match_acceptable(self, app, client): + api = restx.Api(app, default_mediatype=None) + api.add_resource(Foo, "/test/") + + res = client.get("/test/", headers=[("Accept", "application/json")]) + assert res.status_code == 200 + assert res.content_type == "application/json" + + def test_accept_default_override_accept(self, app, client): + api = restx.Api(app) + api.add_resource(Foo, "/test/") + + res = client.get("/test/", headers=[("Accept", "text/plain")]) + assert res.status_code == 200 + assert res.content_type == "application/json" + + def test_accept_default_any_pick_first(self, app, client): + api = restx.Api(app) + + @api.representation("text/plain") + def text_rep(data, status_code, headers=None): + resp = app.make_response((str(data), status_code, headers)) + return resp + + api.add_resource(Foo, "/test/") + + res = client.get("/test/", headers=[("Accept", "*/*")]) + assert res.status_code == 200 + assert res.content_type == "application/json" + + def test_accept_no_default_no_match_not_acceptable(self, app, client): + api = restx.Api(app, default_mediatype=None) + api.add_resource(Foo, "/test/") + + res = client.get("/test/", headers=[("Accept", "text/plain")]) + assert res.status_code == 406 + assert res.content_type == "application/json" + + def test_accept_no_default_custom_repr_match(self, app, client): + api = restx.Api(app, default_mediatype=None) + api.representations = {} + + @api.representation("text/plain") + def text_rep(data, status_code, headers=None): + resp = app.make_response((str(data), status_code, headers)) + return resp + + api.add_resource(Foo, "/test/") + + res = client.get("/test/", headers=[("Accept", "text/plain")]) + assert res.status_code == 200 + assert res.content_type == "text/plain" + + def test_accept_no_default_custom_repr_not_acceptable(self, app, client): + api = restx.Api(app, default_mediatype=None) + api.representations = {} + + @api.representation("text/plain") + def text_rep(data, status_code, headers=None): + resp = app.make_response((str(data), status_code, headers)) + return resp + + api.add_resource(Foo, "/test/") + + res = client.get("/test/", headers=[("Accept", "application/json")]) + assert res.status_code == 406 + assert res.content_type == "text/plain" + + def test_accept_no_default_match_q0_not_acceptable(self, app, client): + """ + q=0 should be considered NotAcceptable, + but this depends on werkzeug >= 1.0 which is not yet released + so this test is expected to fail until we depend on werkzeug >= 1.0 + """ + api = restx.Api(app, default_mediatype=None) + api.add_resource(Foo, "/test/") + + res = client.get("/test/", headers=[("Accept", "application/json; q=0")]) + assert res.status_code == 406 + assert res.content_type == "application/json" + + def test_accept_no_default_accept_highest_quality_of_two(self, app, client): + api = restx.Api(app, default_mediatype=None) + + @api.representation("text/plain") + def text_rep(data, status_code, headers=None): + resp = app.make_response((str(data), status_code, headers)) + return resp + + api.add_resource(Foo, "/test/") + + res = client.get( + "/test/", headers=[("Accept", "application/json; q=0.1, text/plain; q=1.0")] + ) + assert res.status_code == 200 + assert res.content_type == "text/plain" + + def test_accept_no_default_accept_highest_quality_of_three(self, app, client): + api = restx.Api(app, default_mediatype=None) + + @api.representation("text/html") + @api.representation("text/plain") + def text_rep(data, status_code, headers=None): + resp = app.make_response((str(data), status_code, headers)) + return resp + + api.add_resource(Foo, "/test/") + + res = client.get( + "/test/", + headers=[ + ( + "Accept", + "application/json; q=0.1, text/plain; q=0.3, text/html; q=0.2", + ) + ], + ) + assert res.status_code == 200 + assert res.content_type == "text/plain" + + def test_accept_no_default_no_representations(self, app, client): + api = restx.Api(app, default_mediatype=None) + api.representations = {} + + api.add_resource(Foo, "/test/") + + res = client.get("/test/", headers=[("Accept", "text/plain")]) + assert res.status_code == 406 + assert res.content_type == "text/plain" + + def test_accept_invalid_default_no_representations(self, app, client): + api = restx.Api(app, default_mediatype="nonexistant/mediatype") + api.representations = {} + + api.add_resource(Foo, "/test/") + + res = client.get("/test/", headers=[("Accept", "text/plain")]) + assert res.status_code == 500 diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_api.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_api.py new file mode 100644 index 0000000..2b40b23 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_api.py @@ -0,0 +1,351 @@ +import copy + +from flask import url_for, Blueprint + +import flask_restx as restx + + +class APITest(object): + def test_root_endpoint(self, app): + api = restx.Api(app, version="1.0") + + with app.test_request_context(): + url = url_for("root") + assert url == "/" + assert api.base_url == "http://localhost/" + + def test_root_endpoint_lazy(self, app): + api = restx.Api(version="1.0") + api.init_app(app) + + with app.test_request_context(): + url = url_for("root") + assert url == "/" + assert api.base_url == "http://localhost/" + + def test_root_endpoint_with_blueprint(self, app): + blueprint = Blueprint("api", __name__, url_prefix="/api") + api = restx.Api(blueprint, version="1.0") + app.register_blueprint(blueprint) + + with app.test_request_context(): + url = url_for("api.root") + assert url == "/api/" + assert api.base_url == "http://localhost/api/" + + def test_root_endpoint_with_blueprint_with_subdomain(self, app): + blueprint = Blueprint("api", __name__, subdomain="api", url_prefix="/api") + api = restx.Api(blueprint, version="1.0") + app.register_blueprint(blueprint) + + with app.test_request_context(): + url = url_for("api.root") + assert url == "http://api.localhost/api/" + assert api.base_url == "http://api.localhost/api/" + + def test_parser(self): + api = restx.Api() + assert isinstance(api.parser(), restx.reqparse.RequestParser) + + def test_doc_decorator(self, app): + api = restx.Api(app, prefix="/api", version="1.0") + params = {"q": {"description": "some description"}} + + @api.doc(params=params) + class TestResource(restx.Resource): + pass + + assert hasattr(TestResource, "__apidoc__") + assert TestResource.__apidoc__ == {"params": params} + + def test_doc_with_inheritance(self, app): + api = restx.Api(app, prefix="/api", version="1.0") + base_params = { + "q": { + "description": "some description", + "type": "string", + "paramType": "query", + } + } + child_params = { + "q": {"description": "some new description"}, + "other": {"description": "another param"}, + } + + @api.doc(params=base_params) + class BaseResource(restx.Resource): + pass + + @api.doc(params=child_params) + class TestResource(BaseResource): + pass + + assert TestResource.__apidoc__ == { + "params": { + "q": { + "description": "some new description", + "type": "string", + "paramType": "query", + }, + "other": {"description": "another param"}, + } + } + + def test_specs_endpoint_not_added(self, app): + api = restx.Api() + api.init_app(app, add_specs=False) + assert "specs" not in api.endpoints + assert "specs" not in app.view_functions + + def test_specs_endpoint_not_found_if_not_added(self, app, client): + api = restx.Api() + api.init_app(app, add_specs=False) + resp = client.get("/swagger.json") + assert resp.status_code == 404 + + def test_default_endpoint(self, app): + api = restx.Api(app) + + @api.route("/test/") + class TestResource(restx.Resource): + pass + + with app.test_request_context(): + assert url_for("test_resource") == "/test/" + + def test_default_endpoint_lazy(self, app): + api = restx.Api() + + @api.route("/test/") + class TestResource(restx.Resource): + pass + + api.init_app(app) + + with app.test_request_context(): + assert url_for("test_resource") == "/test/" + + def test_default_endpoint_with_blueprint(self, app): + blueprint = Blueprint("api", __name__, url_prefix="/api") + api = restx.Api(blueprint) + app.register_blueprint(blueprint) + + @api.route("/test/") + class TestResource(restx.Resource): + pass + + with app.test_request_context(): + assert url_for("api.test_resource") == "/api/test/" + + def test_default_endpoint_with_blueprint_with_subdomain(self, app): + blueprint = Blueprint("api", __name__, subdomain="api", url_prefix="/api") + api = restx.Api(blueprint) + app.register_blueprint(blueprint) + + @api.route("/test/") + class TestResource(restx.Resource): + pass + + with app.test_request_context(): + assert url_for("api.test_resource") == "http://api.localhost/api/test/" + + def test_default_endpoint_for_namespace(self, app): + api = restx.Api(app) + ns = api.namespace("ns", "Test namespace") + + @ns.route("/test/") + class TestResource(restx.Resource): + pass + + with app.test_request_context(): + assert url_for("ns_test_resource") == "/ns/test/" + + def test_default_endpoint_lazy_for_namespace(self, app): + api = restx.Api() + ns = api.namespace("ns", "Test namespace") + + @ns.route("/test/") + class TestResource(restx.Resource): + pass + + api.init_app(app) + + with app.test_request_context(): + assert url_for("ns_test_resource") == "/ns/test/" + + def test_default_endpoint_for_namespace_with_blueprint(self, app): + blueprint = Blueprint("api", __name__, url_prefix="/api") + api = restx.Api(blueprint) + ns = api.namespace("ns", "Test namespace") + + @ns.route("/test/") + class TestResource(restx.Resource): + pass + + app.register_blueprint(blueprint) + + with app.test_request_context(): + assert url_for("api.ns_test_resource") == "/api/ns/test/" + + def test_multiple_default_endpoint(self, app): + api = restx.Api(app) + + @api.route("/test2/") + @api.route("/test/") + class TestResource(restx.Resource): + pass + + with app.test_request_context(): + assert url_for("test_resource") == "/test/" + assert url_for("test_resource_2") == "/test2/" + + def test_multiple_default_endpoint_lazy(self, app): + api = restx.Api() + + @api.route("/test2/") + @api.route("/test/") + class TestResource(restx.Resource): + pass + + api.init_app(app) + + with app.test_request_context(): + assert url_for("test_resource") == "/test/" + assert url_for("test_resource_2") == "/test2/" + + def test_multiple_default_endpoint_for_namespace(self, app): + api = restx.Api(app) + ns = api.namespace("ns", "Test namespace") + + @ns.route("/test2/") + @ns.route("/test/") + class TestResource(restx.Resource): + pass + + with app.test_request_context(): + assert url_for("ns_test_resource") == "/ns/test/" + assert url_for("ns_test_resource_2") == "/ns/test2/" + + def test_multiple_default_endpoint_lazy_for_namespace(self, app): + api = restx.Api() + ns = api.namespace("ns", "Test namespace") + + @ns.route("/test2/") + @ns.route("/test/") + class TestResource(restx.Resource): + pass + + api.init_app(app) + + with app.test_request_context(): + assert url_for("ns_test_resource") == "/ns/test/" + assert url_for("ns_test_resource_2") == "/ns/test2/" + + def test_multiple_default_endpoint_for_namespace_with_blueprint(self, app): + blueprint = Blueprint("api", __name__, url_prefix="/api") + api = restx.Api(blueprint) + ns = api.namespace("ns", "Test namespace") + + @ns.route("/test2/") + @ns.route("/test/") + class TestResource(restx.Resource): + pass + + app.register_blueprint(blueprint) + + with app.test_request_context(): + assert url_for("api.ns_test_resource") == "/api/ns/test/" + assert url_for("api.ns_test_resource_2") == "/api/ns/test2/" + + def test_ns_path_prefixes(self, app): + api = restx.Api() + ns = restx.Namespace("test_ns", description="Test namespace") + + @ns.route("/test/", endpoint="test_resource") + class TestResource(restx.Resource): + pass + + api.add_namespace(ns, "/api_test") + api.init_app(app) + + with app.test_request_context(): + assert url_for("test_resource") == "/api_test/test/" + + def test_multiple_ns_with_authorizations(self, app): + api = restx.Api() + a1 = {"apikey": {"type": "apiKey", "in": "header", "name": "X-API"}} + a2 = { + "oauth2": { + "type": "oauth2", + "flow": "accessCode", + "tokenUrl": "https://somewhere.com/token", + "scopes": { + "read": "Grant read-only access", + "write": "Grant read-write access", + }, + } + } + ns1 = restx.Namespace("ns1", authorizations=a1) + ns2 = restx.Namespace("ns2", authorizations=a2) + + @ns1.route("/") + class Ns1(restx.Resource): + @ns1.doc(security="apikey") + def get(self): + pass + + @ns2.route("/") + class Ns2(restx.Resource): + @ns1.doc(security="oauth2") + def post(self): + pass + + api.add_namespace(ns1, path="/ns1") + api.add_namespace(ns2, path="/ns2") + api.init_app(app) + + assert {"apikey": []} in api.__schema__["paths"]["/ns1/"]["get"]["security"] + assert {"oauth2": []} in api.__schema__["paths"]["/ns2/"]["post"]["security"] + unified_auth = copy.copy(a1) + unified_auth.update(a2) + assert api.__schema__["securityDefinitions"] == unified_auth + + def test_non_ordered_namespace(self, app): + api = restx.Api(app) + ns = api.namespace("ns", "Test namespace") + + assert not ns.ordered + + def test_ordered_namespace(self, app): + api = restx.Api(app, ordered=True) + ns = api.namespace("ns", "Test namespace") + + assert ns.ordered + + def test_decorators(self, app, mocker): + decorator1 = mocker.Mock(return_value=lambda x: x) + decorator2 = mocker.Mock(return_value=lambda x: x) + decorator3 = mocker.Mock(return_value=lambda x: x) + + class TestResource(restx.Resource): + method_decorators = [] + + api = restx.Api(decorators=[decorator1]) + ns = api.namespace("test_ns", decorators=[decorator2, decorator3]) + + ns.add_resource(TestResource, "/test", endpoint="test") + api.init_app(app) + + assert decorator1.called is True + assert decorator2.called is True + assert decorator3.called is True + + def test_specs_url(self, app): + api = restx.Api(app) + specs_url = api.specs_url + assert specs_url == "/swagger.json" + + def test_url_scheme(self, app): + api = restx.Api(app, url_scheme="https") + assert api.specs_url == "https://localhost/swagger.json" + assert api.base_url == "https://localhost/" diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_apidoc.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_apidoc.py new file mode 100644 index 0000000..afb4c70 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_apidoc.py @@ -0,0 +1,149 @@ +import pytest + +from flask import url_for, Blueprint +from werkzeug.routing import BuildError + +import flask_restx as restx + + +class APIDocTest(object): + def test_default_apidoc_on_root(self, app, client): + restx.Api(app, version="1.0") + + assert url_for("doc") == url_for("root") + + response = client.get(url_for("doc")) + assert response.status_code == 200 + assert response.content_type == "text/html; charset=utf-8" + + def test_default_apidoc_on_root_lazy(self, app, client): + api = restx.Api(version="1.0") + api.init_app(app) + + assert url_for("doc") == url_for("root") + + response = client.get(url_for("doc")) + assert response.status_code == 200 + assert response.content_type == "text/html; charset=utf-8" + + def test_default_apidoc_on_root_with_blueprint(self, app, client): + blueprint = Blueprint("api", __name__, url_prefix="/api") + restx.Api(blueprint, version="1.0") + app.register_blueprint(blueprint) + + assert url_for("api.doc") == url_for("api.root") + + response = client.get(url_for("api.doc")) + assert response.status_code == 200 + assert response.content_type == "text/html; charset=utf-8" + + def test_apidoc_with_custom_validator(self, app, client): + app.config["SWAGGER_VALIDATOR_URL"] = "http://somewhere.com/validator" + restx.Api(app, version="1.0") + + response = client.get(url_for("doc")) + assert response.status_code == 200 + assert response.content_type == "text/html; charset=utf-8" + assert 'validatorUrl: "http://somewhere.com/validator" || null,' in str( + response.data + ) + + def test_apidoc_doc_expansion_parameter(self, app, client): + restx.Api(app) + + response = client.get(url_for("doc")) + assert 'docExpansion: "none"' in str(response.data) + + app.config["SWAGGER_UI_DOC_EXPANSION"] = "list" + response = client.get(url_for("doc")) + assert 'docExpansion: "list"' in str(response.data) + + app.config["SWAGGER_UI_DOC_EXPANSION"] = "full" + response = client.get(url_for("doc")) + assert 'docExpansion: "full"' in str(response.data) + + def test_apidoc_doc_display_operation_id(self, app, client): + restx.Api(app) + + response = client.get(url_for("doc")) + assert "displayOperationId: false" in str(response.data) + + app.config["SWAGGER_UI_OPERATION_ID"] = False + response = client.get(url_for("doc")) + assert "displayOperationId: false" in str(response.data) + + app.config["SWAGGER_UI_OPERATION_ID"] = True + response = client.get(url_for("doc")) + assert "displayOperationId: true" in str(response.data) + + def test_apidoc_doc_display_request_duration(self, app, client): + restx.Api(app) + + response = client.get(url_for("doc")) + assert "displayRequestDuration: false" in str(response.data) + + app.config["SWAGGER_UI_REQUEST_DURATION"] = False + response = client.get(url_for("doc")) + assert "displayRequestDuration: false" in str(response.data) + + app.config["SWAGGER_UI_REQUEST_DURATION"] = True + response = client.get(url_for("doc")) + assert "displayRequestDuration: true" in str(response.data) + + def test_custom_apidoc_url(self, app, client): + restx.Api(app, version="1.0", doc="/doc/") + + doc_url = url_for("doc") + root_url = url_for("root") + + assert doc_url != root_url + + response = client.get(root_url) + assert response.status_code == 404 + + assert doc_url == "/doc/" + response = client.get(doc_url) + assert response.status_code == 200 + assert response.content_type == "text/html; charset=utf-8" + + def test_custom_api_prefix(self, app, client): + prefix = "/api" + api = restx.Api(app, prefix=prefix) + api.namespace("resource") + assert url_for("root") == prefix + + def test_custom_apidoc_page(self, app, client): + api = restx.Api(app, version="1.0") + content = "My Custom API Doc" + + @api.documentation + def api_doc(): + return content + + response = client.get(url_for("doc")) + assert response.status_code == 200 + assert response.data.decode("utf8") == content + + def test_custom_apidoc_page_lazy(self, app, client): + blueprint = Blueprint("api", __name__, url_prefix="/api") + api = restx.Api(blueprint, version="1.0") + content = "My Custom API Doc" + + @api.documentation + def api_doc(): + return content + + app.register_blueprint(blueprint) + + response = client.get(url_for("api.doc")) + assert response.status_code == 200 + assert response.data.decode("utf8") == content + + def test_disabled_apidoc(self, app, client): + restx.Api(app, version="1.0", doc=False) + + with pytest.raises(BuildError): + url_for("doc") + + response = client.get(url_for("root")) + assert response.status_code == 404 diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_cors.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_cors.py new file mode 100644 index 0000000..b24a610 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_cors.py @@ -0,0 +1,50 @@ +from flask_restx import Api, Resource, cors + + +class ErrorsTest(object): + def test_crossdomain(self, app, client): + class Foo(Resource): + @cors.crossdomain(origin="*") + def get(self): + return "data" + + api = Api(app) + api.add_resource(Foo, "/test/") + + res = client.get("/test/") + assert res.status_code == 200 + assert res.headers["Access-Control-Allow-Origin"] == "*" + assert res.headers["Access-Control-Max-Age"] == "21600" + assert "HEAD" in res.headers["Access-Control-Allow-Methods"] + assert "OPTIONS" in res.headers["Access-Control-Allow-Methods"] + assert "GET" in res.headers["Access-Control-Allow-Methods"] + + def test_access_control_expose_headers(self, app, client): + class Foo(Resource): + @cors.crossdomain( + origin="*", expose_headers=["X-My-Header", "X-Another-Header"] + ) + def get(self): + return "data" + + api = Api(app) + api.add_resource(Foo, "/test/") + + res = client.get("/test/") + assert res.status_code == 200 + assert "X-MY-HEADER" in res.headers["Access-Control-Expose-Headers"] + assert "X-ANOTHER-HEADER" in res.headers["Access-Control-Expose-Headers"] + + def test_no_crossdomain(self, app, client): + class Foo(Resource): + def get(self): + return "data" + + api = Api(app) + api.add_resource(Foo, "/test/") + + res = client.get("/test/") + assert res.status_code == 200 + assert "Access-Control-Allow-Origin" not in res.headers + assert "Access-Control-Allow-Methods" not in res.headers + assert "Access-Control-Max-Age" not in res.headers diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_errors.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_errors.py new file mode 100644 index 0000000..28fe511 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_errors.py @@ -0,0 +1,758 @@ +import json +import logging + +import pytest + +from flask import Blueprint, abort +from flask.signals import got_request_exception + +from werkzeug import Response +from werkzeug.exceptions import ( + Aborter, + BadRequest, + HTTPException, + NotFound, + Unauthorized, +) +from werkzeug.http import quote_etag, unquote_etag + +import flask_restx as restx + + +class ErrorsTest(object): + def test_abort_type(self): + with pytest.raises(HTTPException): + restx.abort(404) + + def test_abort_data(self): + with pytest.raises(HTTPException) as cm: + restx.abort(404, foo="bar") + assert cm.value.data == {"foo": "bar"} + + def test_abort_no_data(self): + with pytest.raises(HTTPException) as cm: + restx.abort(404) + assert not hasattr(cm.value, "data") + + def test_abort_custom_message(self): + with pytest.raises(HTTPException) as cm: + restx.abort(404, "My message") + assert cm.value.data["message"] == "My message" + + def test_abort_code_only_with_defaults(self, app, client): + api = restx.Api(app) + + @api.route("/test/", endpoint="test") + class TestResource(restx.Resource): + def get(self): + api.abort(403) + + response = client.get("/test/") + assert response.status_code == 403 + assert response.content_type == "application/json" + + data = json.loads(response.data.decode("utf8")) + assert "message" in data + + def test_abort_with_message(self, app, client): + api = restx.Api(app) + + @api.route("/test/", endpoint="test") + class TestResource(restx.Resource): + def get(self): + api.abort(403, "A message") + + response = client.get("/test/") + assert response.status_code == 403 + assert response.content_type == "application/json" + + data = json.loads(response.data.decode("utf8")) + assert data["message"] == "A message" + + def test_abort_with_lazy_init(self, app, client): + api = restx.Api() + + @api.route("/test/", endpoint="test") + class TestResource(restx.Resource): + def get(self): + api.abort(403) + + api.init_app(app) + + response = client.get("/test/") + assert response.status_code == 403 + assert response.content_type == "application/json" + + data = json.loads(response.data.decode("utf8")) + assert "message" in data + + def test_abort_on_exception(self, app, client): + api = restx.Api(app) + + @api.route("/test/", endpoint="test") + class TestResource(restx.Resource): + def get(self): + raise ValueError() + + response = client.get("/test/") + assert response.status_code == 500 + assert response.content_type == "application/json" + + data = json.loads(response.data.decode("utf8")) + assert "message" in data + + def test_abort_on_exception_with_lazy_init(self, app, client): + api = restx.Api() + + @api.route("/test/", endpoint="test") + class TestResource(restx.Resource): + def get(self): + raise ValueError() + + api.init_app(app) + + response = client.get("/test/") + assert response.status_code == 500 + assert response.content_type == "application/json" + + data = json.loads(response.data.decode("utf8")) + assert "message" in data + + def test_errorhandler_for_exception_inheritance(self, app, client): + api = restx.Api(app) + + class CustomException(RuntimeError): + pass + + @api.route("/test/", endpoint="test") + class TestResource(restx.Resource): + def get(self): + raise CustomException("error") + + @api.errorhandler(RuntimeError) + def handle_custom_exception(error): + return {"message": str(error), "test": "value"}, 400 + + response = client.get("/test/") + assert response.status_code == 400 + assert response.content_type == "application/json" + + data = json.loads(response.data.decode("utf8")) + assert data == { + "message": "error", + "test": "value", + } + + def test_errorhandler_for_custom_exception(self, app, client): + api = restx.Api(app) + + class CustomException(RuntimeError): + pass + + @api.route("/test/", endpoint="test") + class TestResource(restx.Resource): + def get(self): + raise CustomException("error") + + @api.errorhandler(CustomException) + def handle_custom_exception(error): + return {"message": str(error), "test": "value"}, 400 + + response = client.get("/test/") + assert response.status_code == 400 + assert response.content_type == "application/json" + + data = json.loads(response.data.decode("utf8")) + assert data == { + "message": "error", + "test": "value", + } + + def test_blunder_in_errorhandler_is_not_suppressed_in_logs( + self, app, client, caplog + ): + api = restx.Api(app) + + class CustomException(RuntimeError): + pass + + class ProgrammingBlunder(Exception): + pass + + @api.route("/test/", endpoint="test") + class TestResource(restx.Resource): + def get(self): + raise CustomException("error") + + @api.errorhandler(CustomException) + def handle_custom_exception(error): + raise ProgrammingBlunder( + "This exception needs to be logged, not suppressed, then cause 500" + ) + + with caplog.at_level(logging.ERROR): + response = client.get("/test/") + exc_type, value, traceback = caplog.records[0].exc_info + assert exc_type is ProgrammingBlunder + assert response.status_code == 500 + + def test_errorhandler_for_custom_exception_with_headers(self, app, client): + api = restx.Api(app) + + class CustomException(RuntimeError): + pass + + @api.route("/test/", endpoint="test") + class TestResource(restx.Resource): + def get(self): + raise CustomException("error") + + @api.errorhandler(CustomException) + def handle_custom_exception(error): + return {"message": "some maintenance"}, 503, {"Retry-After": 120} + + response = client.get("/test/") + assert response.status_code == 503 + assert response.content_type == "application/json" + + data = json.loads(response.data.decode("utf8")) + assert data == {"message": "some maintenance"} + assert response.headers["Retry-After"] == "120" + + def test_errorhandler_for_httpexception(self, app, client): + api = restx.Api(app) + + @api.route("/test/", endpoint="test") + class TestResource(restx.Resource): + def get(self): + raise BadRequest() + + @api.errorhandler(BadRequest) + def handle_badrequest_exception(error): + return {"message": str(error), "test": "value"}, 400 + + response = client.get("/test/") + assert response.status_code == 400 + assert response.content_type == "application/json" + + data = json.loads(response.data.decode("utf8")) + assert data == { + "message": str(BadRequest()), + "test": "value", + } + + def test_errorhandler_with_namespace(self, app, client): + api = restx.Api(app) + + ns = restx.Namespace("ExceptionHandler", path="/") + + class CustomException(RuntimeError): + pass + + @ns.route("/test/", endpoint="test") + class TestResource(restx.Resource): + def get(self): + raise CustomException("error") + + @ns.errorhandler(CustomException) + def handle_custom_exception(error): + return {"message": str(error), "test": "value"}, 400 + + api.add_namespace(ns) + + response = client.get("/test/") + assert response.status_code == 400 + assert response.content_type == "application/json" + + data = json.loads(response.data.decode("utf8")) + assert data == { + "message": "error", + "test": "value", + } + + def test_errorhandler_with_namespace_from_api(self, app, client): + api = restx.Api(app) + + ns = api.namespace("ExceptionHandler", path="/") + + class CustomException(RuntimeError): + pass + + @ns.route("/test/", endpoint="test") + class TestResource(restx.Resource): + def get(self): + raise CustomException("error") + + @ns.errorhandler(CustomException) + def handle_custom_exception(error): + return {"message": str(error), "test": "value"}, 400 + + response = client.get("/test/") + assert response.status_code == 400 + assert response.content_type == "application/json" + + data = json.loads(response.data.decode("utf8")) + assert data == { + "message": "error", + "test": "value", + } + + def test_default_errorhandler(self, app, client): + api = restx.Api(app) + + @api.route("/test/") + class TestResource(restx.Resource): + def get(self): + raise Exception("error") + + response = client.get("/test/") + assert response.status_code == 500 + assert response.content_type == "application/json" + + data = json.loads(response.data.decode("utf8")) + assert "message" in data + + def test_default_errorhandler_with_propagate_true(self, app, client): + blueprint = Blueprint("api", __name__, url_prefix="/api") + api = restx.Api(blueprint) + + @api.route("/test/") + class TestResource(restx.Resource): + def get(self): + raise Exception("error") + + app.register_blueprint(blueprint) + + app.config["PROPAGATE_EXCEPTIONS"] = True + + # From the Flask docs: + # PROPAGATE_EXCEPTIONS + # Exceptions are re-raised rather than being handled by the app’s error handlers. + # If not set, this is implicitly true if TESTING or DEBUG is enabled. + with pytest.raises(Exception): + client.get("/api/test/") + + def test_default_errorhandler_with_propagate_not_set_but_testing(self, app, client): + blueprint = Blueprint("api", __name__, url_prefix="/api") + api = restx.Api(blueprint) + + @api.route("/test/") + class TestResource(restx.Resource): + def get(self): + raise Exception("error") + + app.register_blueprint(blueprint) + + app.config["PROPAGATE_EXCEPTIONS"] = None + app.testing = True + + # From the Flask docs: + # PROPAGATE_EXCEPTIONS + # Exceptions are re-raised rather than being handled by the app’s error handlers. + # If not set, this is implicitly true if TESTING or DEBUG is enabled. + with pytest.raises(Exception): + client.get("/api/test/") + + def test_default_errorhandler_with_propagate_not_set_but_debug(self, app, client): + blueprint = Blueprint("api", __name__, url_prefix="/api") + api = restx.Api(blueprint) + + @api.route("/test/") + class TestResource(restx.Resource): + def get(self): + raise Exception("error") + + app.register_blueprint(blueprint) + + app.config["PROPAGATE_EXCEPTIONS"] = None + app.debug = True + + # From the Flask docs: + # PROPAGATE_EXCEPTIONS + # Exceptions are re-raised rather than being handled by the app’s error handlers. + # If not set, this is implicitly true if TESTING or DEBUG is enabled. + with pytest.raises(Exception): + client.get("/api/test/") + + def test_custom_default_errorhandler(self, app, client): + api = restx.Api(app) + + @api.route("/test/", endpoint="test") + class TestResource(restx.Resource): + def get(self): + raise Exception("error") + + @api.errorhandler + def default_error_handler(error): + return {"message": str(error), "test": "value"}, 500 + + response = client.get("/test/") + assert response.status_code == 500 + assert response.content_type == "application/json" + + data = json.loads(response.data.decode("utf8")) + assert data == { + "message": "error", + "test": "value", + } + + def test_custom_default_errorhandler_with_headers(self, app, client): + api = restx.Api(app) + + @api.route("/test/", endpoint="test") + class TestResource(restx.Resource): + def get(self): + raise Exception("error") + + @api.errorhandler + def default_error_handler(error): + return {"message": "some maintenance"}, 503, {"Retry-After": 120} + + response = client.get("/test/") + assert response.status_code == 503 + assert response.content_type == "application/json" + + data = json.loads(response.data.decode("utf8")) + assert data == {"message": "some maintenance"} + assert response.headers["Retry-After"] == "120" + + def test_errorhandler_lazy(self, app, client): + api = restx.Api() + + class CustomException(RuntimeError): + pass + + @api.route("/test/", endpoint="test") + class TestResource(restx.Resource): + def get(self): + raise CustomException("error") + + @api.errorhandler(CustomException) + def handle_custom_exception(error): + return {"message": str(error), "test": "value"}, 400 + + api.init_app(app) + + response = client.get("/test/") + assert response.status_code == 400 + assert response.content_type == "application/json" + + data = json.loads(response.data.decode("utf8")) + assert data == { + "message": "error", + "test": "value", + } + + def test_handle_api_error(self, app, client): + api = restx.Api(app) + + @api.route("/api", endpoint="api") + class Test(restx.Resource): + def get(self): + abort(404) + + response = client.get("/api") + assert response.status_code == 404 + assert response.headers["Content-Type"] == "application/json" + data = json.loads(response.data.decode()) + assert "message" in data + + def test_handle_non_api_error(self, app, client): + restx.Api(app) + + response = client.get("/foo") + assert response.status_code == 404 + assert response.headers["Content-Type"] == "text/html; charset=utf-8" + + def test_non_api_error_404_catchall(self, app, client): + api = restx.Api(app, catch_all_404s=True) + + response = client.get("/foo") + assert response.headers["Content-Type"] == api.default_mediatype + + def test_handle_error_signal(self, app): + api = restx.Api(app) + + exception = BadRequest() + + recorded = [] + + def record(sender, exception): + recorded.append(exception) + + got_request_exception.connect(record, app) + try: + # with self.app.test_request_context("/foo"): + api.handle_error(exception) + assert len(recorded) == 1 + assert exception is recorded[0] + finally: + got_request_exception.disconnect(record, app) + + def test_handle_error_signal_does_not_call_got_request_exception(self, app): + api = restx.Api(app) + + exception = BadRequest() + + recorded = [] + + def record(sender, exception): + recorded.append(exception) + + @api.errorhandler(BadRequest) + def handle_bad_request(error): + return {"message": str(error), "value": "test"}, 400 + + got_request_exception.connect(record, app) + try: + api.handle_error(exception) + assert len(recorded) == 0 + finally: + got_request_exception.disconnect(record, app) + + def test_handle_error(self, app): + api = restx.Api(app) + + response = api.handle_error(BadRequest()) + assert response.status_code == 400 + assert json.loads(response.data.decode()) == { + "message": BadRequest.description, + } + + def test_handle_error_does_not_duplicate_content_length(self, app): + api = restx.Api(app) + + # with self.app.test_request_context("/foo"): + response = api.handle_error(BadRequest()) + assert len(response.headers.getlist("Content-Length")) == 1 + + def test_handle_smart_errors(self, app): + api = restx.Api(app) + view = restx.Resource + + api.add_resource(view, "/foo", endpoint="bor") + api.add_resource(view, "/fee", endpoint="bir") + api.add_resource(view, "/fii", endpoint="ber") + + with app.test_request_context("/faaaaa"): + response = api.handle_error(NotFound()) + assert response.status_code == 404 + assert json.loads(response.data.decode()) == { + "message": NotFound.description, + } + + with app.test_request_context("/fOo"): + response = api.handle_error(NotFound()) + assert response.status_code == 404 + assert "did you mean /foo ?" in response.data.decode() + + app.config["RESTX_ERROR_404_HELP"] = False + + response = api.handle_error(NotFound()) + assert response.status_code == 404 + assert json.loads(response.data.decode()) == {"message": NotFound.description} + + def test_handle_include_error_message(self, app): + api = restx.Api(app) + view = restx.Resource + + api.add_resource(view, "/foo", endpoint="bor") + + with app.test_request_context("/faaaaa"): + response = api.handle_error(NotFound()) + assert "message" in json.loads(response.data.decode()) + + def test_handle_not_include_error_message(self, app): + app.config["ERROR_INCLUDE_MESSAGE"] = False + + api = restx.Api(app) + view = restx.Resource + + api.add_resource(view, "/foo", endpoint="bor") + + with app.test_request_context("/faaaaa"): + response = api.handle_error(NotFound()) + assert "message" not in json.loads(response.data.decode()) + + def test_error_router_falls_back_to_original(self, app, mocker): + class ProgrammingBlunder(Exception): + pass + + blunder = ProgrammingBlunder("This exception needs to be detectable") + + def raise_blunder(arg): + raise blunder + + api = restx.Api(app) + app.handle_exception = mocker.Mock() + api.handle_error = mocker.Mock(side_effect=raise_blunder) + api._has_fr_route = mocker.Mock(return_value=True) + exception = mocker.Mock(spec=HTTPException) + + api.error_router(app.handle_exception, exception) + + app.handle_exception.assert_called_with(blunder) + + def test_fr_405(self, app, client): + api = restx.Api(app) + + @api.route("/ids/", endpoint="hello") + class HelloWorld(restx.Resource): + def get(self): + return {} + + response = client.post("/ids/3") + assert response.status_code == 405 + assert response.content_type == api.default_mediatype + # Allow can be of the form 'GET, PUT, POST' + allow = ", ".join(set(response.headers.get_all("Allow"))) + allow = set(method.strip() for method in allow.split(",")) + assert allow == set(["HEAD", "OPTIONS", "GET"]) + + @pytest.mark.options(debug=True) + def test_exception_header_forwarded(self, app, client): + """Ensure that HTTPException's headers are extended properly""" + api = restx.Api(app) + + class NotModified(HTTPException): + code = 304 + + def __init__(self, etag, *args, **kwargs): + super(NotModified, self).__init__(*args, **kwargs) + self.etag = quote_etag(etag) + + def get_headers(self, *args, **kwargs): + return [("ETag", self.etag)] + + custom_abort = Aborter(mapping={304: NotModified}) + + @api.route("/foo") + class Foo1(restx.Resource): + def get(self): + custom_abort(304, etag="myETag") + + foo = client.get("/foo") + assert foo.get_etag() == unquote_etag(quote_etag("myETag")) + + def test_handle_server_error(self, app): + api = restx.Api(app) + + resp = api.handle_error(Exception()) + assert resp.status_code == 500 + assert json.loads(resp.data.decode()) == {"message": "Internal Server Error"} + + def test_handle_error_with_code(self, app): + api = restx.Api(app, serve_challenge_on_401=True) + + exception = Exception() + exception.code = "Not an integer" + exception.data = {"foo": "bar"} + + response = api.handle_error(exception) + assert response.status_code == 500 + assert json.loads(response.data.decode()) == {"foo": "bar"} + + def test_handle_error_http_exception_response_code_only(self, app): + api = restx.Api(app) + http_exception = HTTPException(response=Response(status=401)) + + response = api.handle_error(http_exception) + assert response.status_code == 401 + assert json.loads(response.data.decode()) == { + "message": "Unauthorized", + } + + def test_errorhandler_swagger_doc(self, app, client): + api = restx.Api(app) + + class CustomException(RuntimeError): + pass + + error = api.model("Error", {"message": restx.fields.String()}) + + @api.route("/test/", endpoint="test") + class TestResource(restx.Resource): + def get(self): + """ + Do something + + :raises CustomException: In case of something + """ + pass + + @api.errorhandler(CustomException) + @api.header("Custom-Header", "Some custom header") + @api.marshal_with(error, code=503) + def handle_custom_exception(error): + """Some description""" + pass + + specs = client.get_specs() + + assert "Error" in specs["definitions"] + assert "CustomException" in specs["responses"] + + response = specs["responses"]["CustomException"] + assert response["description"] == "Some description" + assert response["schema"] == {"$ref": "#/definitions/Error"} + assert response["headers"] == { + "Custom-Header": {"description": "Some custom header", "type": "string"} + } + + operation = specs["paths"]["/test/"]["get"] + assert "responses" in operation + assert operation["responses"] == { + "503": {"$ref": "#/responses/CustomException"} + } + + def test_errorhandler_with_propagate_true(self, app, client): + """Exceptions with errorhandler should not be returned to client, even + if PROPAGATE_EXCEPTIONS is set.""" + app.config["PROPAGATE_EXCEPTIONS"] = True + api = restx.Api(app) + + @api.route("/test/", endpoint="test") + class TestResource(restx.Resource): + def get(self): + raise RuntimeError("error") + + @api.errorhandler(RuntimeError) + def handle_custom_exception(error): + return {"message": str(error), "test": "value"}, 400 + + response = client.get("/test/") + assert response.status_code == 400 + assert response.content_type == "application/json" + + data = json.loads(response.data.decode("utf8")) + assert data == { + "message": "error", + "test": "value", + } + + def test_namespace_errorhandler_with_propagate_true(self, app, client): + """Exceptions with errorhandler on a namespace should not be + returned to client, even if PROPAGATE_EXCEPTIONS is set.""" + app.config["PROPAGATE_EXCEPTIONS"] = True + api = restx.Api(app) + namespace = restx.Namespace("test_namespace") + api.add_namespace(namespace) + + @namespace.route("/test/", endpoint="test") + class TestResource(restx.Resource): + def get(self): + raise RuntimeError("error") + + @namespace.errorhandler(RuntimeError) + def handle_custom_exception(error): + return {"message": str(error), "test": "value"}, 400 + + response = client.get("/test_namespace/test/") + assert response.status_code == 400 + assert response.content_type == "application/json" + + data = json.loads(response.data.decode("utf8")) + assert data == { + "message": "error", + "test": "value", + } diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_fields.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_fields.py new file mode 100644 index 0000000..8b44988 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_fields.py @@ -0,0 +1,1581 @@ +from collections import OrderedDict +from datetime import date, datetime +from decimal import Decimal +from functools import partial + +import pytz +import pytest + +from flask import Blueprint +from flask_restx import fields, Api + + +class FieldTestCase(object): + field_class = None + + @pytest.fixture + def api(self, app): + blueprint = Blueprint("api", __name__) + api = Api(blueprint) + app.register_blueprint(blueprint) + yield api + + def assert_field(self, field, value, expected): + assert field.output("foo", {"foo": value}) == expected + + def assert_field_raises(self, field, value): + with pytest.raises(fields.MarshallingError): + field.output("foo", {"foo": value}) + + +class BaseFieldTestMixin(object): + def test_description(self): + field = self.field_class(description="A description") + assert "description" in field.__schema__ + assert field.__schema__["description"] == "A description" + + def test_title(self): + field = self.field_class(title="A title") + assert "title" in field.__schema__ + assert field.__schema__["title"] == "A title" + + def test_required(self): + field = self.field_class(required=True) + assert field.required + + def test_readonly(self): + field = self.field_class(readonly=True) + assert "readOnly" in field.__schema__ + assert field.__schema__["readOnly"] + + +class NumberTestMixin(object): + def test_min(self): + field = self.field_class(min=0) + assert "minimum" in field.__schema__ + assert field.__schema__["minimum"] == 0 + assert "exclusiveMinimum" not in field.__schema__ + + def test_min_as_callable(self): + field = self.field_class(min=lambda: 0) + assert "minimum" in field.__schema__ + assert field.__schema__["minimum"] == 0 + + def test_min_exlusive(self): + field = self.field_class(min=0, exclusiveMin=True) + assert "minimum" in field.__schema__ + assert field.__schema__["minimum"] == 0 + assert "exclusiveMinimum" in field.__schema__ + assert field.__schema__["exclusiveMinimum"] is True + + def test_max(self): + field = self.field_class(max=42) + assert "maximum" in field.__schema__ + assert field.__schema__["maximum"] == 42 + assert "exclusiveMaximum" not in field.__schema__ + + def test_max_as_callable(self): + field = self.field_class(max=lambda: 42) + assert "maximum" in field.__schema__ + assert field.__schema__["maximum"] == 42 + + def test_max_exclusive(self): + field = self.field_class(max=42, exclusiveMax=True) + assert "maximum" in field.__schema__ + assert field.__schema__["maximum"] == 42 + assert "exclusiveMaximum" in field.__schema__ + assert field.__schema__["exclusiveMaximum"] is True + + def test_mulitple_of(self): + field = self.field_class(multiple=5) + assert "multipleOf" in field.__schema__ + assert field.__schema__["multipleOf"] == 5 + + +class StringTestMixin(object): + def test_min_length(self): + field = self.field_class(min_length=1) + assert "minLength" in field.__schema__ + assert field.__schema__["minLength"] == 1 + + def test_min_length_as_callable(self): + field = self.field_class(min_length=lambda: 1) + assert "minLength" in field.__schema__ + assert field.__schema__["minLength"] == 1 + + def test_max_length(self): + field = self.field_class(max_length=42) + assert "maxLength" in field.__schema__ + assert field.__schema__["maxLength"] == 42 + + def test_max_length_as_callable(self): + field = self.field_class(max_length=lambda: 42) + assert "maxLength" in field.__schema__ + assert field.__schema__["maxLength"] == 42 + + def test_pattern(self): + field = self.field_class(pattern="[a-z]") + assert "pattern" in field.__schema__ + assert field.__schema__["pattern"] == "[a-z]" + + +class RawFieldTest(BaseFieldTestMixin, FieldTestCase): + """Test Raw field AND some common behaviors""" + + field_class = fields.Raw + + def test_type(self): + field = fields.Raw() + assert field.__schema__["type"] == "object" + + def test_default(self): + field = fields.Raw(default="aaa") + assert field.__schema__["default"] == "aaa" + self.assert_field(field, None, "aaa") + + def test_default_as_callable(self): + field = fields.Raw(default=lambda: "aaa") + assert field.__schema__["default"] == "aaa" + self.assert_field(field, None, "aaa") + + def test_with_attribute(self): + field = fields.Raw(attribute="bar") + assert field.output("foo", {"bar": 42}) == 42 + + def test_with_lambda_attribute(self, mocker): + obj = mocker.Mock() + obj.value = 42 + field = fields.Raw(attribute=lambda x: x.value) + assert field.output("foo", obj) == 42 + + def test_with_partial_attribute(self, mocker): + def f(x, suffix): + return "{0}-{1}".format(x.value, suffix) + + obj = mocker.Mock() + obj.value = 42 + + p = partial(f, suffix="whatever") + field = fields.Raw(attribute=p) + assert field.output("foo", obj) == "42-whatever" + + def test_attribute_not_found(self): + field = fields.Raw() + assert field.output("foo", {"bar": 42}) is None + + def test_object(self, mocker): + obj = mocker.Mock() + obj.foo = 42 + field = fields.Raw() + assert field.output("foo", obj) == 42 + + def test_nested_object(self, mocker): + foo = mocker.Mock() + bar = mocker.Mock() + bar.value = 42 + foo.bar = bar + field = fields.Raw() + assert field.output("bar.value", foo) == 42 + + +class StringFieldTest(StringTestMixin, BaseFieldTestMixin, FieldTestCase): + field_class = fields.String + + def test_defaults(self): + field = fields.String() + assert not field.required + assert not field.discriminator + assert field.__schema__ == {"type": "string"} + + def test_with_enum(self): + enum = ["A", "B", "C"] + field = fields.String(enum=enum) + assert not field.required + assert field.__schema__ == {"type": "string", "enum": enum, "example": enum[0]} + + def test_with_empty_enum(self): + field = fields.String(enum=[]) + assert not field.required + assert field.__schema__ == {"type": "string"} + + def test_with_callable_enum(self): + enum = lambda: ["A", "B", "C"] # noqa + field = fields.String(enum=enum) + assert not field.required + assert field.__schema__ == { + "type": "string", + "enum": ["A", "B", "C"], + "example": "A", + } + + def test_with_empty_callable_enum(self): + enum = lambda: [] # noqa + field = fields.String(enum=enum) + assert not field.required + assert field.__schema__ == {"type": "string"} + + def test_with_default(self): + field = fields.String(default="aaa") + assert field.__schema__ == {"type": "string", "default": "aaa"} + + def test_string_field_with_discriminator(self): + field = fields.String(discriminator=True) + assert field.discriminator + assert field.required + assert field.__schema__ == {"type": "string"} + + def test_string_field_with_discriminator_override_require(self): + field = fields.String(discriminator=True, required=False) + assert field.discriminator + assert field.required + assert field.__schema__ == {"type": "string"} + + def test_discriminator_output(self, api): + model = api.model( + "Test", + { + "name": fields.String(discriminator=True), + }, + ) + + data = api.marshal({}, model) + assert data == {"name": "Test"} + + def test_multiple_discriminator_field(self, api): + model = api.model( + "Test", + { + "name": fields.String(discriminator=True), + "name2": fields.String(discriminator=True), + }, + ) + + with pytest.raises(ValueError): + api.marshal(object(), model) + + @pytest.mark.parametrize( + "value,expected", + [ + ("string", "string"), + (42, "42"), + ], + ) + def test_values(self, value, expected): + self.assert_field(fields.String(), value, expected) + + +class IntegerFieldTest(BaseFieldTestMixin, NumberTestMixin, FieldTestCase): + field_class = fields.Integer + + def test_defaults(self): + field = fields.Integer() + assert not field.required + assert field.__schema__ == {"type": "integer"} + + def test_with_default(self): + field = fields.Integer(default=42) + assert not field.required + assert field.__schema__ == {"type": "integer", "default": 42} + self.assert_field(field, None, 42) + + @pytest.mark.parametrize( + "value,expected", + [ + (0, 0), + (42, 42), + ("42", 42), + (None, None), + (66.6, 66), + ], + ) + def test_value(self, value, expected): + self.assert_field(fields.Integer(), value, expected) + + def test_decode_error_on_invalid_value(self): + field = fields.Integer() + self.assert_field_raises(field, "an int") + + def test_decode_error_on_invalid_type(self): + field = fields.Integer() + self.assert_field_raises(field, {"a": "dict"}) + + +class BooleanFieldTest(BaseFieldTestMixin, FieldTestCase): + field_class = fields.Boolean + + def test_defaults(self): + field = fields.Boolean() + assert not field.required + assert field.__schema__ == {"type": "boolean"} + + def test_with_default(self): + field = fields.Boolean(default=True) + assert not field.required + assert field.__schema__ == {"type": "boolean", "default": True} + + def test_with_example(self): + field = fields.Boolean(default=True, example=False) + assert field.__schema__ == { + "type": "boolean", + "default": True, + "example": False, + } + + @pytest.mark.parametrize( + "value,expected", + [ + (True, True), + (False, False), + ({}, False), + ("false", False), # These consistent with inputs.boolean + ("0", False), + ], + ) + def test_value(self, value, expected): + self.assert_field(fields.Boolean(), value, expected) + + +class FloatFieldTest(BaseFieldTestMixin, NumberTestMixin, FieldTestCase): + field_class = fields.Float + + def test_defaults(self): + field = fields.Float() + assert not field.required + assert field.__schema__ == {"type": "number"} + + def test_with_default(self): + field = fields.Float(default=0.5) + assert not field.required + assert field.__schema__ == {"type": "number", "default": 0.5} + + def test_none_uses_default(self): + field = fields.Float(default=0.5) + assert not field.required + assert field.__schema__ == {"type": "number", "default": 0.5} + assert field.format(None) == 0.5 + + @pytest.mark.parametrize( + "value,expected", + [ + ("-3.13", -3.13), + (str(-3.13), -3.13), + (3, 3.0), + ], + ) + def test_value(self, value, expected): + self.assert_field(fields.Float(), value, expected) + + def test_raises(self): + self.assert_field_raises(fields.Float(), "bar") + + def test_decode_error_on_invalid_value(self): + field = fields.Float() + self.assert_field_raises(field, "not a float") + + def test_decode_error_on_invalid_type(self): + field = fields.Float() + self.assert_field_raises(field, {"a": "dict"}) + + +PI_STR = ( + "3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117" + "06798214808651328230664709384460955058223172535940812848111745028410270193852110555964462294895493" + "038196442881097566593344612847564823378678316527120190914564856692346034861" +) + +PI = Decimal(PI_STR) + + +class FixedFieldTest(BaseFieldTestMixin, NumberTestMixin, FieldTestCase): + field_class = fields.Fixed + + def test_defaults(self): + field = fields.Fixed() + assert not field.required + assert field.__schema__ == {"type": "number"} + + def test_with_default(self): + field = fields.Fixed(default=0.5) + assert not field.required + assert field.__schema__ == {"type": "number", "default": 0.5} + + def test_fixed(self): + field5 = fields.Fixed(5) + field4 = fields.Fixed(4) + + self.assert_field(field5, PI, "3.14159") + self.assert_field(field4, PI, "3.1416") + self.assert_field(field4, 3, "3.0000") + self.assert_field(field4, "03", "3.0000") + self.assert_field(field4, "03.0", "3.0000") + + def test_zero(self): + self.assert_field(fields.Fixed(), "0", "0.00000") + + def test_infinite(self): + field = fields.Fixed() + self.assert_field_raises(field, "+inf") + self.assert_field_raises(field, "-inf") + + def test_nan(self): + field = fields.Fixed() + self.assert_field_raises(field, "NaN") + + +class ArbitraryFieldTest(BaseFieldTestMixin, NumberTestMixin, FieldTestCase): + field_class = fields.Arbitrary + + def test_defaults(self): + field = fields.Arbitrary() + assert not field.required + assert field.__schema__ == {"type": "number"} + + def test_with_default(self): + field = fields.Arbitrary(default=0.5) + assert field.__schema__ == {"type": "number", "default": 0.5} + + @pytest.mark.parametrize( + "value,expected", + [ + (PI_STR, PI_STR), + (PI, PI_STR), + ], + ) + def test_value(self, value, expected): + self.assert_field(fields.Arbitrary(), value, expected) + + +class DatetimeFieldTest(BaseFieldTestMixin, FieldTestCase): + field_class = fields.DateTime + + def test_defaults(self): + field = fields.DateTime() + assert not field.required + assert field.__schema__ == {"type": "string", "format": "date-time"} + self.assert_field(field, None, None) + + def test_with_default(self): + field = fields.DateTime(default="2014-08-25") + assert field.__schema__ == { + "type": "string", + "format": "date-time", + "default": "2014-08-25T00:00:00", + } + self.assert_field(field, None, "2014-08-25T00:00:00") + + def test_with_default_as_datetime(self): + field = fields.DateTime(default=datetime(2014, 8, 25)) + assert field.__schema__ == { + "type": "string", + "format": "date-time", + "default": "2014-08-25T00:00:00", + } + self.assert_field(field, None, "2014-08-25T00:00:00") + + def test_with_default_as_date(self): + field = fields.DateTime(default=date(2014, 8, 25)) + assert field.__schema__ == { + "type": "string", + "format": "date-time", + "default": "2014-08-25T00:00:00", + } + self.assert_field(field, None, "2014-08-25T00:00:00") + + def test_min(self): + field = fields.DateTime(min="1984-06-07T00:00:00") + assert "minimum" in field.__schema__ + assert field.__schema__["minimum"] == "1984-06-07T00:00:00" + assert "exclusiveMinimum" not in field.__schema__ + + def test_min_as_date(self): + field = fields.DateTime(min=date(1984, 6, 7)) + assert "minimum" in field.__schema__ + assert field.__schema__["minimum"] == "1984-06-07T00:00:00" + assert "exclusiveMinimum" not in field.__schema__ + + def test_min_as_datetime(self): + field = fields.DateTime(min=datetime(1984, 6, 7, 1, 2, 0)) + assert "minimum" in field.__schema__ + assert field.__schema__["minimum"] == "1984-06-07T01:02:00" + assert "exclusiveMinimum" not in field.__schema__ + + def test_min_exlusive(self): + field = fields.DateTime(min="1984-06-07T00:00:00", exclusiveMin=True) + assert "minimum" in field.__schema__ + assert field.__schema__["minimum"] == "1984-06-07T00:00:00" + assert "exclusiveMinimum" in field.__schema__ + assert field.__schema__["exclusiveMinimum"] is True + + def test_max(self): + field = fields.DateTime(max="1984-06-07T00:00:00") + assert "maximum" in field.__schema__ + assert field.__schema__["maximum"] == "1984-06-07T00:00:00" + assert "exclusiveMaximum" not in field.__schema__ + + def test_max_as_date(self): + field = fields.DateTime(max=date(1984, 6, 7)) + assert "maximum" in field.__schema__ + assert field.__schema__["maximum"] == "1984-06-07T00:00:00" + assert "exclusiveMaximum" not in field.__schema__ + + def test_max_as_datetime(self): + field = fields.DateTime(max=datetime(1984, 6, 7, 1, 2, 0)) + assert "maximum" in field.__schema__ + assert field.__schema__["maximum"] == "1984-06-07T01:02:00" + assert "exclusiveMaximum" not in field.__schema__ + + def test_max_exclusive(self): + field = fields.DateTime(max="1984-06-07T00:00:00", exclusiveMax=True) + assert "maximum" in field.__schema__ + assert field.__schema__["maximum"] == "1984-06-07T00:00:00" + assert "exclusiveMaximum" in field.__schema__ + assert field.__schema__["exclusiveMaximum"] is True + + @pytest.mark.parametrize( + "value,expected", + [ + (date(2011, 1, 1), "Sat, 01 Jan 2011 00:00:00 -0000"), + (datetime(2011, 1, 1), "Sat, 01 Jan 2011 00:00:00 -0000"), + (datetime(2011, 1, 1, 23, 59, 59), "Sat, 01 Jan 2011 23:59:59 -0000"), + ( + datetime(2011, 1, 1, 23, 59, 59, tzinfo=pytz.utc), + "Sat, 01 Jan 2011 23:59:59 -0000", + ), + ( + datetime(2011, 1, 1, 23, 59, 59, tzinfo=pytz.timezone("CET")), + "Sat, 01 Jan 2011 22:59:59 -0000", + ), + ], + ) + def test_rfc822_value(self, value, expected): + self.assert_field(fields.DateTime(dt_format="rfc822"), value, expected) + + @pytest.mark.parametrize( + "value,expected", + [ + (date(2011, 1, 1), "2011-01-01T00:00:00"), + (datetime(2011, 1, 1), "2011-01-01T00:00:00"), + (datetime(2011, 1, 1, 23, 59, 59), "2011-01-01T23:59:59"), + (datetime(2011, 1, 1, 23, 59, 59, 1000), "2011-01-01T23:59:59.001000"), + ( + datetime(2011, 1, 1, 23, 59, 59, tzinfo=pytz.utc), + "2011-01-01T23:59:59+00:00", + ), + ( + datetime(2011, 1, 1, 23, 59, 59, 1000, tzinfo=pytz.utc), + "2011-01-01T23:59:59.001000+00:00", + ), + ( + datetime(2011, 1, 1, 23, 59, 59, tzinfo=pytz.timezone("CET")), + "2011-01-01T23:59:59+01:00", + ), + ], + ) + def test_iso8601_value(self, value, expected): + self.assert_field(fields.DateTime(dt_format="iso8601"), value, expected) + + def test_unsupported_format(self): + field = fields.DateTime(dt_format="raw") + self.assert_field_raises(field, datetime.now()) + + def test_unsupported_value_format(self): + field = fields.DateTime(dt_format="raw") + self.assert_field_raises(field, "xxx") + + +class DateFieldTest(BaseFieldTestMixin, FieldTestCase): + field_class = fields.Date + + def test_defaults(self): + field = fields.Date() + assert not field.required + assert field.__schema__ == {"type": "string", "format": "date"} + + def test_with_default(self): + field = fields.Date(default="2014-08-25") + assert field.__schema__ == { + "type": "string", + "format": "date", + "default": "2014-08-25", + } + self.assert_field(field, None, "2014-08-25") + + def test_with_default_as_date(self): + field = fields.Date(default=date(2014, 8, 25)) + assert field.__schema__ == { + "type": "string", + "format": "date", + "default": "2014-08-25", + } + + def test_with_default_as_datetime(self): + field = fields.Date(default=datetime(2014, 8, 25)) + assert field.__schema__ == { + "type": "string", + "format": "date", + "default": "2014-08-25", + } + + def test_min(self): + field = fields.Date(min="1984-06-07") + assert "minimum" in field.__schema__ + assert field.__schema__["minimum"] == "1984-06-07" + assert "exclusiveMinimum" not in field.__schema__ + + def test_min_as_date(self): + field = fields.Date(min=date(1984, 6, 7)) + assert "minimum" in field.__schema__ + assert field.__schema__["minimum"] == "1984-06-07" + assert "exclusiveMinimum" not in field.__schema__ + + def test_min_as_datetime(self): + field = fields.Date(min=datetime(1984, 6, 7, 1, 2, 0)) + assert "minimum" in field.__schema__ + assert field.__schema__["minimum"] == "1984-06-07" + assert "exclusiveMinimum" not in field.__schema__ + + def test_min_exlusive(self): + field = fields.Date(min="1984-06-07", exclusiveMin=True) + assert "minimum" in field.__schema__ + assert field.__schema__["minimum"] == "1984-06-07" + assert "exclusiveMinimum" in field.__schema__ + assert field.__schema__["exclusiveMinimum"] is True + + def test_max(self): + field = fields.Date(max="1984-06-07") + assert "maximum" in field.__schema__ + assert field.__schema__["maximum"] == "1984-06-07" + assert "exclusiveMaximum" not in field.__schema__ + + def test_max_as_date(self): + field = fields.Date(max=date(1984, 6, 7)) + assert "maximum" in field.__schema__ + assert field.__schema__["maximum"] == "1984-06-07" + assert "exclusiveMaximum" not in field.__schema__ + + def test_max_as_datetime(self): + field = fields.Date(max=datetime(1984, 6, 7, 1, 2, 0)) + assert "maximum" in field.__schema__ + assert field.__schema__["maximum"] == "1984-06-07" + assert "exclusiveMaximum" not in field.__schema__ + + def test_max_exclusive(self): + field = fields.Date(max="1984-06-07", exclusiveMax=True) + assert "maximum" in field.__schema__ + assert field.__schema__["maximum"] == "1984-06-07" + assert "exclusiveMaximum" in field.__schema__ + assert field.__schema__["exclusiveMaximum"] is True + + @pytest.mark.parametrize( + "value,expected", + [ + (date(2011, 1, 1), "2011-01-01"), + (datetime(2011, 1, 1), "2011-01-01"), + (datetime(2011, 1, 1, 23, 59, 59), "2011-01-01"), + (datetime(2011, 1, 1, 23, 59, 59, 1000), "2011-01-01"), + (datetime(2011, 1, 1, 23, 59, 59, tzinfo=pytz.utc), "2011-01-01"), + (datetime(2011, 1, 1, 23, 59, 59, 1000, tzinfo=pytz.utc), "2011-01-01"), + ( + datetime(2011, 1, 1, 23, 59, 59, tzinfo=pytz.timezone("CET")), + "2011-01-01", + ), + ], + ) + def test_value(self, value, expected): + self.assert_field(fields.Date(), value, expected) + + def test_unsupported_value_format(self): + self.assert_field_raises(fields.Date(), "xxx") + + +class FormatedStringFieldTest(StringTestMixin, BaseFieldTestMixin, FieldTestCase): + field_class = partial(fields.FormattedString, "Hello {name}") + + def test_defaults(self): + field = fields.FormattedString("Hello {name}") + assert not field.required + assert field.__schema__ == {"type": "string"} + + def test_dict(self): + data = { + "sid": 3, + "account_sid": 4, + } + field = fields.FormattedString("/foo/{account_sid}/{sid}/") + assert field.output("foo", data) == "/foo/4/3/" + + def test_object(self, mocker): + obj = mocker.Mock() + obj.sid = 3 + obj.account_sid = 4 + field = fields.FormattedString("/foo/{account_sid}/{sid}/") + assert field.output("foo", obj) == "/foo/4/3/" + + def test_none(self): + field = fields.FormattedString("{foo}") + # self.assert_field_raises(field, None) + with pytest.raises(fields.MarshallingError): + field.output("foo", None) + + def test_invalid_object(self): + field = fields.FormattedString("/foo/{0[account_sid]}/{0[sid]}/") + self.assert_field_raises(field, {}) + + def test_tuple(self): + field = fields.FormattedString("/foo/{0[account_sid]}/{0[sid]}/") + self.assert_field_raises(field, (3, 4)) + + +class UrlFieldTest(StringTestMixin, BaseFieldTestMixin, FieldTestCase): + field_class = partial(fields.Url, "endpoint") + + def test_defaults(self): + field = fields.Url("endpoint") + assert not field.required + assert field.__schema__ == {"type": "string"} + + def test_invalid_object(self, app): + app.add_url_rule("/", "foobar", view_func=lambda x: x) + field = fields.Url("foobar") + + with app.test_request_context("/"): + with pytest.raises(fields.MarshallingError): + field.output("foo", None) + + def test_simple(self, app, mocker): + app.add_url_rule("/", "foobar", view_func=lambda x: x) + field = fields.Url("foobar") + obj = mocker.Mock(foo=42) + + with app.test_request_context("/"): + assert "/42" == field.output("foo", obj) + + def test_absolute(self, app, mocker): + app.add_url_rule("/", "foobar", view_func=lambda x: x) + field = fields.Url("foobar", absolute=True) + obj = mocker.Mock(foo=42) + + with app.test_request_context("/"): + assert "http://localhost/42" == field.output("foo", obj) + + def test_absolute_scheme(self, app, mocker): + """Url.scheme should override current_request.scheme""" + app.add_url_rule("/", "foobar", view_func=lambda x: x) + field = fields.Url("foobar", absolute=True, scheme="https") + obj = mocker.Mock(foo=42) + + with app.test_request_context("/", base_url="http://localhost"): + assert "https://localhost/42" == field.output("foo", obj) + + def test_without_endpoint_invalid_object(self, app): + app.add_url_rule("/", "foobar", view_func=lambda x: x) + field = fields.Url() + + with app.test_request_context("/foo"): + with pytest.raises(fields.MarshallingError): + field.output("foo", None) + + def test_without_endpoint(self, app, mocker): + app.add_url_rule("/", "foobar", view_func=lambda x: x) + field = fields.Url() + obj = mocker.Mock(foo=42) + + with app.test_request_context("/foo"): + assert "/42" == field.output("foo", obj) + + def test_without_endpoint_absolute(self, app, mocker): + app.add_url_rule("/", "foobar", view_func=lambda x: x) + field = fields.Url(absolute=True) + obj = mocker.Mock(foo=42) + + with app.test_request_context("/foo"): + assert "http://localhost/42" == field.output("foo", obj) + + def test_without_endpoint_absolute_scheme(self, app, mocker): + app.add_url_rule("/", "foobar", view_func=lambda x: x) + field = fields.Url(absolute=True, scheme="https") + obj = mocker.Mock(foo=42) + + with app.test_request_context("/foo", base_url="http://localhost"): + assert "https://localhost/42" == field.output("foo", obj) + + def test_with_blueprint_invalid_object(self, app): + bp = Blueprint("foo", __name__, url_prefix="/foo") + bp.add_url_rule("/", "foobar", view_func=lambda x: x) + app.register_blueprint(bp) + field = fields.Url() + + with app.test_request_context("/foo/foo"): + with pytest.raises(fields.MarshallingError): + field.output("foo", None) + + def test_with_blueprint(self, app, mocker): + bp = Blueprint("foo", __name__, url_prefix="/foo") + bp.add_url_rule("/", "foobar", view_func=lambda x: x) + app.register_blueprint(bp) + field = fields.Url() + obj = mocker.Mock(foo=42) + + with app.test_request_context("/foo/foo"): + assert "/foo/42" == field.output("foo", obj) + + def test_with_blueprint_absolute(self, app, mocker): + bp = Blueprint("foo", __name__, url_prefix="/foo") + bp.add_url_rule("/", "foobar", view_func=lambda x: x) + app.register_blueprint(bp) + field = fields.Url(absolute=True) + obj = mocker.Mock(foo=42) + + with app.test_request_context("/foo/foo"): + assert "http://localhost/foo/42" == field.output("foo", obj) + + def test_with_blueprint_absolute_scheme(self, app, mocker): + bp = Blueprint("foo", __name__, url_prefix="/foo") + bp.add_url_rule("/", "foobar", view_func=lambda x: x) + app.register_blueprint(bp) + field = fields.Url(absolute=True, scheme="https") + obj = mocker.Mock(foo=42) + + with app.test_request_context("/foo/foo", base_url="http://localhost"): + assert "https://localhost/foo/42" == field.output("foo", obj) + + +class NestedFieldTest(FieldTestCase): + def test_defaults(self, api): + nested_fields = api.model("NestedModel", {"name": fields.String}) + field = fields.Nested(nested_fields) + assert not field.required + assert field.__schema__ == {"$ref": "#/definitions/NestedModel"} + + def test_with_required(self, api): + nested_fields = api.model("NestedModel", {"name": fields.String}) + field = fields.Nested(nested_fields, required=True) + assert field.required + assert not field.allow_null + assert field.__schema__ == {"$ref": "#/definitions/NestedModel"} + + def test_with_description(self, api): + nested_fields = api.model("NestedModel", {"name": fields.String}) + field = fields.Nested(nested_fields, description="A description") + assert field.__schema__ == { + "description": "A description", + "allOf": [{"$ref": "#/definitions/NestedModel"}], + } + + def test_with_title(self, api): + nested_fields = api.model("NestedModel", {"name": fields.String}) + field = fields.Nested(nested_fields, title="A title") + assert field.__schema__ == { + "title": "A title", + "allOf": [{"$ref": "#/definitions/NestedModel"}], + } + + def test_with_allow_null(self, api): + nested_fields = api.model("NestedModel", {"name": fields.String}) + field = fields.Nested(nested_fields, allow_null=True) + assert not field.required + assert field.allow_null + assert field.__schema__ == {"$ref": "#/definitions/NestedModel"} + + def test_with_skip_none(self, api): + nested_fields = api.model("NestedModel", {"name": fields.String}) + field = fields.Nested(nested_fields, skip_none=True) + assert not field.required + assert field.skip_none + assert field.__schema__ == {"$ref": "#/definitions/NestedModel"} + + def test_with_readonly(self, app): + api = Api(app) + nested_fields = api.model("NestedModel", {"name": fields.String}) + field = fields.Nested(nested_fields, readonly=True) + assert field.__schema__ == { + "readOnly": True, + "allOf": [{"$ref": "#/definitions/NestedModel"}], + } + + def test_as_list(self, api): + nested_fields = api.model("NestedModel", {"name": fields.String}) + field = fields.Nested(nested_fields, as_list=True) + assert field.as_list + assert field.__schema__ == { + "type": "array", + "items": {"$ref": "#/definitions/NestedModel"}, + } + + def test_as_list_is_reusable(self, api): + nested_fields = api.model("NestedModel", {"name": fields.String}) + + field = fields.Nested(nested_fields, as_list=True) + assert field.__schema__ == { + "type": "array", + "items": {"$ref": "#/definitions/NestedModel"}, + } + + field = fields.Nested(nested_fields) + assert field.__schema__ == {"$ref": "#/definitions/NestedModel"} + + +class ListFieldTest(BaseFieldTestMixin, FieldTestCase): + field_class = partial(fields.List, fields.String) + + def test_defaults(self): + field = fields.List(fields.String) + assert not field.required + assert field.__schema__ == {"type": "array", "items": {"type": "string"}} + + def test_with_nested_field(self, api): + nested_fields = api.model("NestedModel", {"name": fields.String}) + field = fields.List(fields.Nested(nested_fields)) + assert field.__schema__ == { + "type": "array", + "items": {"$ref": "#/definitions/NestedModel"}, + } + + data = [{"name": "John Doe", "age": 42}, {"name": "Jane Doe", "age": 66}] + expected = [ + OrderedDict([("name", "John Doe")]), + OrderedDict([("name", "Jane Doe")]), + ] + self.assert_field(field, data, expected) + + def test_min_items(self): + field = fields.List(fields.String, min_items=5) + assert "minItems" in field.__schema__ + assert field.__schema__["minItems"] == 5 + + def test_max_items(self): + field = fields.List(fields.String, max_items=42) + assert "maxItems" in field.__schema__ + assert field.__schema__["maxItems"] == 42 + + def test_unique(self): + field = fields.List(fields.String, unique=True) + assert "uniqueItems" in field.__schema__ + assert field.__schema__["uniqueItems"] is True + + @pytest.mark.parametrize( + "value,expected", + [ + (["a", "b", "c"], ["a", "b", "c"]), + (["c", "b", "a"], ["c", "b", "a"]), + (("a", "b", "c"), ["a", "b", "c"]), + (["a"], ["a"]), + (None, None), + ], + ) + def test_value(self, value, expected): + self.assert_field(fields.List(fields.String()), value, expected) + + def test_with_set(self): + field = fields.List(fields.String) + value = set(["a", "b", "c"]) + output = field.output("foo", {"foo": value}) + assert set(output) == value + + def test_with_scoped_attribute_on_dict_or_obj(self): + class Test(object): + def __init__(self, data): + self.data = data + + class Nested(object): + def __init__(self, value): + self.value = value + + nesteds = [Nested(i) for i in ["a", "b", "c"]] + test_obj = Test(nesteds) + test_dict = {"data": [{"value": "a"}, {"value": "b"}, {"value": "c"}]} + + field = fields.List(fields.String(attribute="value"), attribute="data") + assert ["a" == "b", "c"], field.output("whatever", test_obj) + assert ["a" == "b", "c"], field.output("whatever", test_dict) + + def test_with_attribute(self): + data = [{"a": 1, "b": 1}, {"a": 2, "b": 1}, {"a": 3, "b": 1}] + field = fields.List(fields.Integer(attribute="a")) + self.assert_field(field, data, [1, 2, 3]) + + def test_list_of_raw(self): + field = fields.List(fields.Raw) + + data = [{"a": 1, "b": 1}, {"a": 2, "b": 1}, {"a": 3, "b": 1}] + expected = [ + OrderedDict([("a", 1), ("b", 1)]), + OrderedDict([("a", 2), ("b", 1)]), + OrderedDict([("a", 3), ("b", 1)]), + ] + self.assert_field(field, data, expected) + + data = [1, 2, "a"] + self.assert_field(field, data, data) + + +class WildcardFieldTest(BaseFieldTestMixin, FieldTestCase): + field_class = partial(fields.Wildcard, fields.String) + + def test_types(self): + with pytest.raises(fields.MarshallingError): + + class WrongType: + pass + + x = WrongType() + field1 = fields.Wildcard(WrongType) # noqa + field2 = fields.Wildcard(x) # noqa + + def test_defaults(self): + field = fields.Wildcard(fields.String) + assert not field.required + assert field.__schema__ == { + "type": "object", + "additionalProperties": {"type": "string"}, + } + + def test_with_scoped_attribute_on_dict_or_obj(self): + class Test(object): + def __init__(self, data): + self.data = data + + class Nested(object): + def __init__(self, value): + self.value = value + + nesteds = [Nested(i) for i in ["a", "b", "c"]] + test_obj = Test(nesteds) + test_dict = {"data": [{"value": "a"}, {"value": "b"}, {"value": "c"}]} + + field = fields.Wildcard(fields.String(attribute="value"), attribute="data") + assert ["a" == "b", "c"], field.output("whatever", test_obj) + assert ["a" == "b", "c"], field.output("whatever", test_dict) + + def test_list_of_raw(self): + field = fields.Wildcard(fields.Raw) + + data = [{"a": 1, "b": 1}, {"a": 2, "b": 1}, {"a": 3, "b": 1}] + expected = [ + OrderedDict([("a", 1), ("b", 1)]), + OrderedDict([("a", 2), ("b", 1)]), + OrderedDict([("a", 3), ("b", 1)]), + ] + self.assert_field(field, data, expected) + + data = [1, 2, "a"] + self.assert_field(field, data, data) + + def test_wildcard(self, api): + wild1 = fields.Wildcard(fields.String) + wild2 = fields.Wildcard(fields.Integer) + wild3 = fields.Wildcard(fields.String) + wild4 = fields.Wildcard(fields.String, default="x") + wild5 = fields.Wildcard(fields.String) + wild6 = fields.Wildcard(fields.Integer) + wild7 = fields.Wildcard(fields.String) + wild8 = fields.Wildcard(fields.String) + + mod5 = OrderedDict() + mod5["toto"] = fields.Integer + mod5["bob"] = fields.Integer + mod5["*"] = wild5 + + wild_fields1 = api.model("WildcardModel1", {"*": wild1}) + wild_fields2 = api.model("WildcardModel2", {"j*": wild2}) + wild_fields3 = api.model("WildcardModel3", {"*": wild3}) + wild_fields4 = api.model("WildcardModel4", {"*": wild4}) + wild_fields5 = api.model("WildcardModel5", mod5) + wild_fields6 = api.model( + "WildcardModel6", + { + "nested": { + "f1": fields.String(default="12"), + "f2": fields.Integer(default=13), + }, + "a*": wild6, + }, + ) + wild_fields7 = api.model("WildcardModel7", {"*": wild7}) + wild_fields8 = api.model("WildcardModel8", {"*": wild8}) + + class Dummy(object): + john = 12 + bob = "42" + alice = None + + class Dummy2(object): + pass + + class Dummy3(object): + a = None + b = None + + data = {"John": 12, "bob": 42, "Jane": "68"} + data3 = Dummy() + data4 = Dummy2() + data5 = {"John": 12, "bob": 42, "Jane": "68", "toto": "72"} + data6 = {"nested": {"f1": 12, "f2": 13}, "alice": "14"} + data7 = Dummy3() + data8 = None + expected1 = {"John": "12", "bob": "42", "Jane": "68"} + expected2 = {"John": 12, "Jane": 68} + expected3 = {"john": "12", "bob": "42"} + expected4 = {"*": "x"} + expected5 = {"John": "12", "bob": 42, "Jane": "68", "toto": 72} + expected6 = {"nested": {"f1": "12", "f2": 13}, "alice": 14} + expected7 = {} + expected8 = {} + + result1 = api.marshal(data, wild_fields1) + result2 = api.marshal(data, wild_fields2) + result3 = api.marshal(data3, wild_fields3, skip_none=True) + result4 = api.marshal(data4, wild_fields4) + result5 = api.marshal(data5, wild_fields5) + result6 = api.marshal(data6, wild_fields6) + result7 = api.marshal(data7, wild_fields7, skip_none=True) + result8 = api.marshal(data8, wild_fields8, skip_none=True) + + assert expected1 == result1 + assert expected2 == result2 + assert expected3 == result3 + assert expected4 == result4 + assert expected5 == result5 + assert expected6 == result6 + assert expected7 == result7 + assert expected8 == result8 + + def test_clone(self, api): + wild1 = fields.Wildcard(fields.String) + wild2 = wild1.clone() + + wild_fields1 = api.model("cloneWildcard1", {"*": wild1}) + wild_fields2 = api.model("cloneWildcard2", {"*": wild2}) + + data = {"John": 12, "bob": 42, "Jane": "68"} + expected1 = {"John": "12", "bob": "42", "Jane": "68"} + + result1 = api.marshal(data, wild_fields1) + result2 = api.marshal(data, wild_fields2) + + assert expected1 == result1 + assert result2 == result1 + + +class ClassNameFieldTest(StringTestMixin, BaseFieldTestMixin, FieldTestCase): + field_class = fields.ClassName + + def test_simple_string_field(self): + field = fields.ClassName() + assert not field.required + assert not field.discriminator + assert field.__schema__ == {"type": "string"} + + def test_default_output_classname(self, api): + model = api.model( + "Test", + { + "name": fields.ClassName(), + }, + ) + + class FakeClass(object): + pass + + data = api.marshal(FakeClass(), model) + assert data == {"name": "FakeClass"} + + def test_output_dash(self, api): + model = api.model( + "Test", + { + "name": fields.ClassName(dash=True), + }, + ) + + class FakeClass(object): + pass + + data = api.marshal(FakeClass(), model) + assert data == {"name": "fake_class"} + + def test_with_dict(self, api): + model = api.model( + "Test", + { + "name": fields.ClassName(), + }, + ) + + data = api.marshal({}, model) + assert data == {"name": "object"} + + +class PolymorphTest(FieldTestCase): + def test_polymorph_field(self, api): + parent = api.model( + "Person", + { + "name": fields.String, + }, + ) + + child1 = api.inherit( + "Child1", + parent, + { + "extra1": fields.String, + }, + ) + + child2 = api.inherit( + "Child2", + parent, + { + "extra2": fields.String, + }, + ) + + class Child1(object): + name = "child1" + extra1 = "extra1" + + class Child2(object): + name = "child2" + extra2 = "extra2" + + mapping = {Child1: child1, Child2: child2} + + thing = api.model( + "Thing", + { + "owner": fields.Polymorph(mapping), + }, + ) + + def data(cls): + return api.marshal({"owner": cls()}, thing) + + assert data(Child1) == {"owner": {"name": "child1", "extra1": "extra1"}} + + assert data(Child2) == {"owner": {"name": "child2", "extra2": "extra2"}} + + def test_polymorph_field_no_common_ancestor(self, api): + child1 = api.model( + "Child1", + { + "extra1": fields.String, + }, + ) + + child2 = api.model( + "Child2", + { + "extra2": fields.String, + }, + ) + + class Child1(object): + pass + + class Child2(object): + pass + + mapping = {Child1: child1, Child2: child2} + + with pytest.raises(ValueError): + fields.Polymorph(mapping) + + def test_polymorph_field_unknown_class(self, api): + parent = api.model( + "Person", + { + "name": fields.String, + }, + ) + + child1 = api.inherit( + "Child1", + parent, + { + "extra1": fields.String, + }, + ) + + child2 = api.inherit( + "Child2", + parent, + { + "extra2": fields.String, + }, + ) + + class Child1(object): + name = "child1" + extra1 = "extra1" + + class Child2(object): + name = "child2" + extra2 = "extra2" + + mapping = {Child1: child1, Child2: child2} + + thing = api.model( + "Thing", + { + "owner": fields.Polymorph(mapping), + }, + ) + + with pytest.raises(ValueError): + api.marshal({"owner": object()}, thing) + + def test_polymorph_field_does_not_have_ambiguous_mappings(self, api): + """ + Regression test for https://github.com/noirbizarre/flask-restx/pull/691 + """ + parent = api.model( + "Parent", + { + "name": fields.String, + }, + ) + + child = api.inherit( + "Child", + parent, + { + "extra": fields.String, + }, + ) + + class Parent(object): + name = "parent" + + class Child(Parent): + extra = "extra" + + mapping = {Parent: parent, Child: child} + + thing = api.model( + "Thing", + { + "owner": fields.Polymorph(mapping), + }, + ) + + api.marshal({"owner": Child()}, thing) + + def test_polymorph_field_required_default(self, api): + parent = api.model( + "Person", + { + "name": fields.String, + }, + ) + + child1 = api.inherit( + "Child1", + parent, + { + "extra1": fields.String, + }, + ) + + child2 = api.inherit( + "Child2", + parent, + { + "extra2": fields.String, + }, + ) + + class Child1(object): + name = "child1" + extra1 = "extra1" + + class Child2(object): + name = "child2" + extra2 = "extra2" + + mapping = {Child1: child1, Child2: child2} + + thing = api.model( + "Thing", + { + "owner": fields.Polymorph( + mapping, required=True, default={"name": "default"} + ), + }, + ) + + data = api.marshal({}, thing) + + assert data == {"owner": {"name": "default"}} + + def test_polymorph_field_not_required(self, api): + parent = api.model( + "Person", + { + "name": fields.String, + }, + ) + + child1 = api.inherit( + "Child1", + parent, + { + "extra1": fields.String, + }, + ) + + child2 = api.inherit( + "Child2", + parent, + { + "extra2": fields.String, + }, + ) + + class Child1(object): + name = "child1" + extra1 = "extra1" + + class Child2(object): + name = "child2" + extra2 = "extra2" + + mapping = {Child1: child1, Child2: child2} + + thing = api.model( + "Thing", + { + "owner": fields.Polymorph(mapping), + }, + ) + + data = api.marshal({}, thing) + + assert data == {"owner": None} + + def test_polymorph_with_discriminator(self, api): + parent = api.model( + "Person", + { + "name": fields.String, + "model": fields.String(discriminator=True), + }, + ) + + child1 = api.inherit( + "Child1", + parent, + { + "extra1": fields.String, + }, + ) + + child2 = api.inherit( + "Child2", + parent, + { + "extra2": fields.String, + }, + ) + + class Child1(object): + name = "child1" + extra1 = "extra1" + + class Child2(object): + name = "child2" + extra2 = "extra2" + + mapping = {Child1: child1, Child2: child2} + + thing = api.model( + "Thing", + { + "owner": fields.Polymorph(mapping), + }, + ) + + def data(cls): + return api.marshal({"owner": cls()}, thing) + + assert data(Child1) == { + "owner": {"name": "child1", "model": "Child1", "extra1": "extra1"} + } + + assert data(Child2) == { + "owner": {"name": "child2", "model": "Child2", "extra2": "extra2"} + } + + +class CustomFieldTest(FieldTestCase): + def test_custom_field(self): + class CustomField(fields.Integer): + __schema_format__ = "int64" + + field = CustomField() + + assert field.__schema__ == {"type": "integer", "format": "int64"} + + +class FieldsHelpersTest(object): + def test_to_dict(self): + expected = data = {"foo": 42} + assert fields.to_marshallable_type(data) == expected + + def test_to_dict_obj(self): + class Foo(object): + def __init__(self): + self.foo = 42 + + expected = {"foo": 42} + assert fields.to_marshallable_type(Foo()) == expected + + def test_to_dict_custom_marshal(self): + class Foo(object): + def __marshallable__(self): + return {"foo": 42} + + expected = {"foo": 42} + assert fields.to_marshallable_type(Foo()) == expected + + def test_get_value(self): + assert fields.get_value("foo", {"foo": 42}) == 42 + + def test_get_value_no_value(self): + assert fields.get_value("foo", {"foo": 42}) == 42 + + def test_get_value_obj(self, mocker): + assert fields.get_value("foo", mocker.Mock(foo=42)) == 42 + + def test_get_value_indexable_object(self): + class Test(object): + def __init__(self, value): + self.value = value + + def __getitem__(self, n): + if type(n) is int: + if n < 3: + return n + raise IndexError + raise TypeError + + obj = Test("hi") + assert fields.get_value("value", obj) == "hi" + + def test_get_value_int_indexable_list(self): + assert fields.get_value("bar.0", {"bar": [42]}) == 42 + + def test_get_value_int_indexable_list_with_str(self): + assert fields.get_value("bar.abc", {"bar": [42]}) is None + + def test_get_value_int_indexable_nested_list(self): + assert fields.get_value("bar.0.val", {"bar": [{"val": 42}]}) == 42 + + def test_get_value_int_indexable_tuple_with_str(self): + assert fields.get_value("bar.abc", {"bar": (42, 43)}) is None + + def test_get_value_int_indexable_tuple(self): + assert fields.get_value("bar.0", {"bar": (42, 43)}) == 42 + + def test_get_value_int_indexable_nested_tuple(self): + assert fields.get_value("bar.0.val", {"bar": [{"val": 42}]}) == 42 diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_fields_mask.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_fields_mask.py new file mode 100644 index 0000000..ce84dca --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_fields_mask.py @@ -0,0 +1,1113 @@ +import json +import pytest + +from collections import OrderedDict + +from flask_restx import mask, Api, Resource, fields, marshal, Mask + + +def assert_data(tested, expected): + """Compare data without caring about order and type (dict vs. OrderedDict)""" + tested = json.loads(json.dumps(tested)) + expected = json.loads(json.dumps(expected)) + assert tested == expected + + +class MaskMixin(object): + def test_empty_mask(self): + assert Mask("") == {} + + def test_one_field(self): + assert Mask("field_name") == {"field_name": True} + + def test_multiple_field(self): + mask = Mask("field1, field2, field3") + assert_data( + mask, + { + "field1": True, + "field2": True, + "field3": True, + }, + ) + + def test_nested_fields(self): + parsed = Mask("nested{field1,field2}") + expected = { + "nested": { + "field1": True, + "field2": True, + } + } + assert parsed == expected + + def test_complex(self): + parsed = Mask("field1, nested{field, sub{subfield}}, field2") + expected = { + "field1": True, + "nested": { + "field": True, + "sub": { + "subfield": True, + }, + }, + "field2": True, + } + assert_data(parsed, expected) + + def test_star(self): + parsed = Mask("nested{field1,field2},*") + expected = { + "nested": { + "field1": True, + "field2": True, + }, + "*": True, + } + assert_data(parsed, expected) + + def test_order(self): + parsed = Mask("f_3, nested{f_1, f_2, f_3}, f_2, f_1") + expected = OrderedDict( + [ + ("f_3", True), + ( + "nested", + OrderedDict( + [ + ("f_1", True), + ("f_2", True), + ("f_3", True), + ] + ), + ), + ("f_2", True), + ("f_1", True), + ] + ) + assert parsed == expected + + def test_missing_closing_bracket(self): + with pytest.raises(mask.ParseError): + Mask("nested{") + + def test_consecutive_coma(self): + with pytest.raises(mask.ParseError): + Mask("field,,") + + def test_coma_before_bracket(self): + with pytest.raises(mask.ParseError): + Mask("field,{}") + + def test_coma_after_bracket(self): + with pytest.raises(mask.ParseError): + Mask("nested{,}") + + def test_unexpected_opening_bracket(self): + with pytest.raises(mask.ParseError): + Mask("{{field}}") + + def test_unexpected_closing_bracket(self): + with pytest.raises(mask.ParseError): + Mask("{field}}") + + def test_support_colons(self): + assert Mask("field:name") == {"field:name": True} + + def test_support_dash(self): + assert Mask("field-name") == {"field-name": True} + + def test_support_underscore(self): + assert Mask("field_name") == {"field_name": True} + + +class MaskUnwrappedTest(MaskMixin): + def parse(self, value): + return Mask(value) + + +class MaskWrappedTest(MaskMixin): + def parse(self, value): + return Mask("{" + value + "}") + + +class DObject(object): + """A dead simple object built from a dictionnary (no recursion)""" + + def __init__(self, data): + self.__dict__.update(data) + + +person_fields = {"name": fields.String, "age": fields.Integer} + + +class ApplyMaskTest(object): + def test_empty(self): + data = { + "integer": 42, + "string": "a string", + "boolean": True, + } + result = mask.apply(data, "{}") + assert result == {} + + def test_single_field(self): + data = { + "integer": 42, + "string": "a string", + "boolean": True, + } + result = mask.apply(data, "{integer}") + assert result == {"integer": 42} + + def test_multiple_fields(self): + data = { + "integer": 42, + "string": "a string", + "boolean": True, + } + result = mask.apply(data, "{integer, string}") + assert result == {"integer": 42, "string": "a string"} + + def test_star_only(self): + data = { + "integer": 42, + "string": "a string", + "boolean": True, + } + result = mask.apply(data, "*") + assert result == data + + def test_with_objects(self): + data = DObject( + { + "integer": 42, + "string": "a string", + "boolean": True, + } + ) + result = mask.apply(data, "{integer, string}") + assert result == {"integer": 42, "string": "a string"} + + def test_with_ordered_dict(self): + data = OrderedDict( + { + "integer": 42, + "string": "a string", + "boolean": True, + } + ) + result = mask.apply(data, "{integer, string}") + assert result == {"integer": 42, "string": "a string"} + + def test_nested_field(self): + data = { + "integer": 42, + "string": "a string", + "boolean": True, + "nested": { + "integer": 42, + "string": "a string", + "boolean": True, + }, + } + result = mask.apply(data, "{nested}") + assert result == { + "nested": { + "integer": 42, + "string": "a string", + "boolean": True, + } + } + + def test_nested_fields(self): + data = { + "nested": { + "integer": 42, + "string": "a string", + "boolean": True, + } + } + result = mask.apply(data, "{nested{integer}}") + assert result == {"nested": {"integer": 42}} + + def test_nested_with_start(self): + data = { + "nested": { + "integer": 42, + "string": "a string", + "boolean": True, + }, + "other": "value", + } + result = mask.apply(data, "{nested{integer},*}") + assert result == {"nested": {"integer": 42}, "other": "value"} + + def test_nested_fields_when_none(self): + data = {"nested": None} + result = mask.apply(data, "{nested{integer}}") + assert result == {"nested": None} + + def test_raw_api_fields(self): + family_fields = { + "father": fields.Raw, + "mother": fields.Raw, + } + + result = mask.apply(family_fields, "father{name},mother{age}") + + data = { + "father": {"name": "John", "age": 42}, + "mother": {"name": "Jane", "age": 42}, + } + expected = {"father": {"name": "John"}, "mother": {"age": 42}} + + assert_data(marshal(data, result), expected) + # Should leave th original mask untouched + assert_data(marshal(data, family_fields), data) + + def test_nested_api_fields(self): + family_fields = { + "father": fields.Nested(person_fields), + "mother": fields.Nested(person_fields), + } + + result = mask.apply(family_fields, "father{name},mother{age}") + assert set(result.keys()) == set(["father", "mother"]) + assert isinstance(result["father"], fields.Nested) + assert set(result["father"].nested.keys()) == set(["name"]) + assert isinstance(result["mother"], fields.Nested) + assert set(result["mother"].nested.keys()) == set(["age"]) + + data = { + "father": {"name": "John", "age": 42}, + "mother": {"name": "Jane", "age": 42}, + } + expected = {"father": {"name": "John"}, "mother": {"age": 42}} + + assert_data(marshal(data, result), expected) + # Should leave th original mask untouched + assert_data(marshal(data, family_fields), data) + + def test_multiple_nested_api_fields(self): + level_2 = {"nested_2": fields.Nested(person_fields)} + level_1 = {"nested_1": fields.Nested(level_2)} + root = {"nested": fields.Nested(level_1)} + + result = mask.apply(root, "nested{nested_1{nested_2{name}}}") + assert set(result.keys()) == set(["nested"]) + assert isinstance(result["nested"], fields.Nested) + assert set(result["nested"].nested.keys()) == set(["nested_1"]) + + data = {"nested": {"nested_1": {"nested_2": {"name": "John", "age": 42}}}} + expected = {"nested": {"nested_1": {"nested_2": {"name": "John"}}}} + + assert_data(marshal(data, result), expected) + # Should leave th original mask untouched + assert_data(marshal(data, root), data) + + def test_list_fields_with_simple_field(self): + family_fields = {"name": fields.String, "members": fields.List(fields.String)} + + result = mask.apply(family_fields, "members") + assert set(result.keys()) == set(["members"]) + assert isinstance(result["members"], fields.List) + assert isinstance(result["members"].container, fields.String) + + data = {"name": "Doe", "members": ["John", "Jane"]} + expected = {"members": ["John", "Jane"]} + + assert_data(marshal(data, result), expected) + # Should leave th original mask untouched + assert_data(marshal(data, family_fields), data) + + def test_list_fields_with_nested(self): + family_fields = {"members": fields.List(fields.Nested(person_fields))} + + result = mask.apply(family_fields, "members{name}") + assert set(result.keys()) == set(["members"]) + assert isinstance(result["members"], fields.List) + assert isinstance(result["members"].container, fields.Nested) + assert set(result["members"].container.nested.keys()) == set(["name"]) + + data = { + "members": [ + {"name": "John", "age": 42}, + {"name": "Jane", "age": 42}, + ] + } + expected = {"members": [{"name": "John"}, {"name": "Jane"}]} + + assert_data(marshal(data, result), expected) + # Should leave th original mask untouched + assert_data(marshal(data, family_fields), data) + + def test_list_fields_with_nested_inherited(self, app): + api = Api(app) + + person = api.model("Person", {"name": fields.String, "age": fields.Integer}) + child = api.inherit("Child", person, {"attr": fields.String}) + + family = api.model("Family", {"children": fields.List(fields.Nested(child))}) + + result = mask.apply(family.resolved, "children{name,attr}") + + data = { + "children": [ + {"name": "John", "age": 5, "attr": "value-john"}, + {"name": "Jane", "age": 42, "attr": "value-jane"}, + ] + } + expected = { + "children": [ + {"name": "John", "attr": "value-john"}, + {"name": "Jane", "attr": "value-jane"}, + ] + } + + assert_data(marshal(data, result), expected) + # Should leave th original mask untouched + assert_data(marshal(data, family), data) + + def test_list_fields_with_raw(self): + family_fields = {"members": fields.List(fields.Raw)} + + result = mask.apply(family_fields, "members{name}") + + data = { + "members": [ + {"name": "John", "age": 42}, + {"name": "Jane", "age": 42}, + ] + } + expected = {"members": [{"name": "John"}, {"name": "Jane"}]} + + assert_data(marshal(data, result), expected) + # Should leave th original mask untouched + assert_data(marshal(data, family_fields), data) + + def test_list(self): + data = [ + { + "integer": 42, + "string": "a string", + "boolean": True, + }, + { + "integer": 404, + "string": "another string", + "boolean": False, + }, + ] + result = mask.apply(data, "{integer, string}") + assert result == [ + {"integer": 42, "string": "a string"}, + {"integer": 404, "string": "another string"}, + ] + + def test_nested_list(self): + data = { + "integer": 42, + "list": [ + { + "integer": 42, + "string": "a string", + }, + { + "integer": 404, + "string": "another string", + }, + ], + } + result = mask.apply(data, "{list}") + assert result == { + "list": [ + { + "integer": 42, + "string": "a string", + }, + { + "integer": 404, + "string": "another string", + }, + ] + } + + def test_nested_list_fields(self): + data = { + "list": [ + { + "integer": 42, + "string": "a string", + }, + { + "integer": 404, + "string": "another string", + }, + ] + } + result = mask.apply(data, "{list{integer}}") + assert result == {"list": [{"integer": 42}, {"integer": 404}]} + + def test_missing_field_none_by_default(self): + result = mask.apply({}, "{integer}") + assert result == {"integer": None} + + def test_missing_field_skipped(self): + result = mask.apply({}, "{integer}", skip=True) + assert result == {} + + def test_missing_nested_field_skipped(self): + result = mask.apply({}, "nested{integer}", skip=True) + assert result == {} + + def test_mask_error_on_simple_fields(self): + model = { + "name": fields.String, + } + + with pytest.raises(mask.MaskError): + mask.apply(model, "name{notpossible}") + + def test_mask_error_on_list_field(self): + model = {"nested": fields.List(fields.String)} + + with pytest.raises(mask.MaskError): + mask.apply(model, "nested{notpossible}") + + +class MaskAPI(object): + def test_marshal_with_honour_field_mask_header(self, app, client): + api = Api(app) + + model = api.model( + "Test", + { + "name": fields.String, + "age": fields.Integer, + "boolean": fields.Boolean, + }, + ) + + @api.route("/test/") + class TestResource(Resource): + @api.marshal_with(model) + def get(self): + return {"name": "John Doe", "age": 42, "boolean": True} + + data = client.get_json("/test/", headers={"X-Fields": "{name,age}"}) + assert data == {"name": "John Doe", "age": 42} + + def test_marshal_with_honour_field_mask_list(self, app, client): + api = Api(app) + + model = api.model( + "Test", + { + "name": fields.String, + "age": fields.Integer, + "boolean": fields.Boolean, + }, + ) + + @api.route("/test/") + class TestResource(Resource): + @api.marshal_with(model) + def get(self): + return [ + {"name": "John Doe", "age": 42, "boolean": True}, + {"name": "Jane Doe", "age": 33, "boolean": False}, + ] + + data = client.get_json("/test/", headers={"X-Fields": "{name,age}"}) + assert data == [ + { + "name": "John Doe", + "age": 42, + }, + { + "name": "Jane Doe", + "age": 33, + }, + ] + + def test_marshal_with_honour_complex_field_mask_header(self, app, client): + api = Api(app) + + person = api.model("Person", person_fields) + child = api.inherit("Child", person, {"attr": fields.String}) + + family = api.model( + "Family", + { + "father": fields.Nested(person), + "mother": fields.Nested(person), + "children": fields.List(fields.Nested(child)), + "free": fields.List(fields.Raw), + }, + ) + + house = api.model( + "House", {"family": fields.Nested(family, attribute="people")} + ) + + @api.route("/test/") + class TestResource(Resource): + @api.marshal_with(house) + def get(self): + return { + "people": { + "father": {"name": "John", "age": 42}, + "mother": {"name": "Jane", "age": 42}, + "children": [ + {"name": "Jack", "age": 5, "attr": "value-1"}, + {"name": "Julie", "age": 7, "attr": "value-2"}, + ], + "free": [ + {"key-1": "1-1", "key-2": "1-2"}, + {"key-1": "2-1", "key-2": "2-2"}, + ], + } + } + + data = client.get_json( + "/test/", + headers={ + "X-Fields": "family{father{name},mother{age},children{name,attr},free{key-2}}" + }, + ) + assert data == { + "family": { + "father": {"name": "John"}, + "mother": {"age": 42}, + "children": [ + {"name": "Jack", "attr": "value-1"}, + {"name": "Julie", "attr": "value-2"}, + ], + "free": [{"key-2": "1-2"}, {"key-2": "2-2"}], + } + } + + def test_marshal_honour_field_mask(self, app): + api = Api(app) + + model = api.model( + "Test", + { + "name": fields.String, + "age": fields.Integer, + "boolean": fields.Boolean, + }, + ) + + data = {"name": "John Doe", "age": 42, "boolean": True} + + result = api.marshal(data, model, mask="{name,age}") + + assert result == { + "name": "John Doe", + "age": 42, + } + + def test_marshal_with_honour_default_mask(self, app, client): + api = Api(app) + + model = api.model( + "Test", + { + "name": fields.String, + "age": fields.Integer, + "boolean": fields.Boolean, + }, + ) + + @api.route("/test/") + class TestResource(Resource): + @api.marshal_with(model, mask="{name,age}") + def get(self): + return {"name": "John Doe", "age": 42, "boolean": True} + + data = self.get_json("/test/") + self.assertEqual( + data, + { + "name": "John Doe", + "age": 42, + }, + ) + + def test_marshal_with_honour_default_model_mask(self, app, client): + api = Api(app) + + model = api.model( + "Test", + { + "name": fields.String, + "age": fields.Integer, + "boolean": fields.Boolean, + }, + mask="{name,age}", + ) + + @api.route("/test/") + class TestResource(Resource): + @api.marshal_with(model) + def get(self): + return {"name": "John Doe", "age": 42, "boolean": True} + + data = client.get_json("/test/") + assert data == {"name": "John Doe", "age": 42} + + def test_marshal_with_honour_header_field_mask_with_default_model_mask( + self, app, client + ): + api = Api(app) + + model = api.model( + "Test", + { + "name": fields.String, + "age": fields.Integer, + "boolean": fields.Boolean, + }, + mask="{name,age}", + ) + + @api.route("/test/") + class TestResource(Resource): + @api.marshal_with(model) + def get(self): + return {"name": "John Doe", "age": 42, "boolean": True} + + data = client.get_json("/test/", headers={"X-Fields": "{name}"}) + assert data == {"name": "John Doe"} + + def test_marshal_with_honour_header_default_mask_with_default_model_mask( + self, app, client + ): + api = Api(app) + + model = api.model( + "Test", + { + "name": fields.String, + "age": fields.Integer, + "boolean": fields.Boolean, + }, + mask="{name,boolean}", + ) + + @api.route("/test/") + class TestResource(Resource): + @api.marshal_with(model, mask="{name}") + def get(self): + return {"name": "John Doe", "age": 42, "boolean": True} + + data = client.get_json("/test/") + assert data == {"name": "John Doe"} + + def test_marshal_with_honour_header_field_mask_with_default_mask(self, app, client): + api = Api(app) + + model = api.model( + "Test", + { + "name": fields.String, + "age": fields.Integer, + "boolean": fields.Boolean, + }, + ) + + @api.route("/test/") + class TestResource(Resource): + @api.marshal_with(model, mask="{name,age}") + def get(self): + return {"name": "John Doe", "age": 42, "boolean": True} + + data = client.get_json("/test/", headers={"X-Fields": "{name}"}) + assert data == {"name": "John Doe"} + + def test_marshal_with_honour_header_field_mask_with_default_mask_and_default_model_mask( + self, app, client + ): + api = Api(app) + + model = api.model( + "Test", + { + "name": fields.String, + "age": fields.Integer, + "boolean": fields.Boolean, + }, + mask="{name,boolean}", + ) + + @api.route("/test/") + class TestResource(Resource): + @api.marshal_with(model, mask="{name,age}") + def get(self): + return {"name": "John Doe", "age": 42, "boolean": True} + + data = client.get_json("/test/", headers={"X-Fields": "{name}"}) + assert data == {"name": "John Doe"} + + def test_marshal_with_honour_custom_field_mask(self, app, client): + api = Api(app) + + model = api.model( + "Test", + { + "name": fields.String, + "age": fields.Integer, + "boolean": fields.Boolean, + }, + ) + + @api.route("/test/") + class TestResource(Resource): + @api.marshal_with(model) + def get(self): + return {"name": "John Doe", "age": 42, "boolean": True} + + app.config["RESTX_MASK_HEADER"] = "X-Mask" + data = client.get_json("/test/", headers={"X-Mask": "{name,age}"}) + + assert data == {"name": "John Doe", "age": 42} + + def test_marshal_does_not_hit_unrequired_attributes(self, app, client): + api = Api(app) + + model = api.model( + "Person", + { + "name": fields.String, + "age": fields.Integer, + "boolean": fields.Boolean, + }, + ) + + class Person(object): + def __init__(self, name, age): + self.name = name + self.age = age + + @property + def boolean(self): + raise Exception() + + @api.route("/test/") + class TestResource(Resource): + @api.marshal_with(model) + def get(self): + return Person("John Doe", 42) + + data = client.get_json("/test/", headers={"X-Fields": "{name,age}"}) + assert data == {"name": "John Doe", "age": 42} + + def test_marshal_with_skip_missing_fields(self, app, client): + api = Api(app) + + model = api.model( + "Test", + { + "name": fields.String, + "age": fields.Integer, + }, + ) + + @api.route("/test/") + class TestResource(Resource): + @api.marshal_with(model) + def get(self): + return { + "name": "John Doe", + "age": 42, + } + + data = client.get_json("/test/", headers={"X-Fields": "{name,missing}"}) + assert data == {"name": "John Doe"} + + def test_marshal_handle_inheritance(self, app): + api = Api(app) + + person = api.model( + "Person", + { + "name": fields.String, + "age": fields.Integer, + }, + ) + + child = api.inherit( + "Child", + person, + { + "extra": fields.String, + }, + ) + + data = {"name": "John Doe", "age": 42, "extra": "extra"} + + values = ( + ("name", {"name": "John Doe"}), + ("name,extra", {"name": "John Doe", "extra": "extra"}), + ("extra", {"extra": "extra"}), + ) + + for value, expected in values: + result = marshal(data, child, mask=value) + assert result == expected + + def test_marshal_with_handle_polymorph(self, app, client): + api = Api(app) + + parent = api.model( + "Person", + { + "name": fields.String, + }, + ) + + child1 = api.inherit( + "Child1", + parent, + { + "extra1": fields.String, + }, + ) + + child2 = api.inherit( + "Child2", + parent, + { + "extra2": fields.String, + }, + ) + + class Child1(object): + name = "child1" + extra1 = "extra1" + + class Child2(object): + name = "child2" + extra2 = "extra2" + + mapping = {Child1: child1, Child2: child2} + + thing = api.model( + "Thing", + { + "owner": fields.Polymorph(mapping), + }, + ) + + @api.route("/thing-1/") + class Thing1Resource(Resource): + @api.marshal_with(thing) + def get(self): + return {"owner": Child1()} + + @api.route("/thing-2/") + class Thing2Resource(Resource): + @api.marshal_with(thing) + def get(self): + return {"owner": Child2()} + + data = client.get_json("/thing-1/", headers={"X-Fields": "owner{name}"}) + assert data == {"owner": {"name": "child1"}} + + data = client.get_json("/thing-1/", headers={"X-Fields": "owner{extra1}"}) + assert data == {"owner": {"extra1": "extra1"}} + + data = client.get_json("/thing-2/", headers={"X-Fields": "owner{name}"}) + assert data == {"owner": {"name": "child2"}} + + def test_raise_400_on_invalid_mask(self, app, client): + api = Api(app) + + model = api.model( + "Test", + { + "name": fields.String, + "age": fields.Integer, + }, + ) + + @api.route("/test/") + class TestResource(Resource): + @api.marshal_with(model) + def get(self): + pass + + response = client.get("/test/", headers={"X-Fields": "name{,missing}"}) + assert response.status_code == 400 + assert response.content_type == "application/json" + + +class SwaggerMaskHeaderTest(object): + def test_marshal_with_expose_mask_header(self, app, client): + api = Api(app) + + model = api.model( + "Test", + { + "name": fields.String, + "age": fields.Integer, + "boolean": fields.Boolean, + }, + ) + + @api.route("/test/") + class TestResource(Resource): + @api.marshal_with(model) + def get(self): + return {"name": "John Doe", "age": 42, "boolean": True} + + specs = client.get_specs() + op = specs["paths"]["/test/"]["get"] + + assert "parameters" in op + assert len(op["parameters"]) == 1 + + param = op["parameters"][0] + + assert param["name"] == "X-Fields" + assert param["type"] == "string" + assert param["format"] == "mask" + assert param["in"] == "header" + assert "required" not in param + assert "default" not in param + + def test_marshal_with_expose_custom_mask_header(self, app, client): + api = Api(app) + + model = api.model( + "Test", + { + "name": fields.String, + "age": fields.Integer, + "boolean": fields.Boolean, + }, + ) + + @api.route("/test/") + class TestResource(Resource): + @api.marshal_with(model) + def get(self): + return {"name": "John Doe", "age": 42, "boolean": True} + + app.config["RESTX_MASK_HEADER"] = "X-Mask" + specs = client.get_specs() + + op = specs["paths"]["/test/"]["get"] + assert "parameters" in op + assert len(op["parameters"]) == 1 + + param = op["parameters"][0] + assert param["name"] == "X-Mask" + + def test_marshal_with_disabling_mask_header(self, app, client): + api = Api(app) + + model = api.model( + "Test", + { + "name": fields.String, + "age": fields.Integer, + "boolean": fields.Boolean, + }, + ) + + @api.route("/test/") + class TestResource(Resource): + @api.marshal_with(model) + def get(self): + return {"name": "John Doe", "age": 42, "boolean": True} + + app.config["RESTX_MASK_SWAGGER"] = False + specs = client.get_specs() + + op = specs["paths"]["/test/"]["get"] + + assert "parameters" not in op + + def test_is_only_exposed_on_marshal_with(self, app, client): + api = Api(app) + + model = api.model( + "Test", + { + "name": fields.String, + "age": fields.Integer, + "boolean": fields.Boolean, + }, + ) + + @api.route("/test/") + class TestResource(Resource): + def get(self): + return api.marshal( + {"name": "John Doe", "age": 42, "boolean": True}, model + ) + + specs = client.get_specs() + op = specs["paths"]["/test/"]["get"] + + assert "parameters" not in op + + def test_marshal_with_expose_default_mask_header(self, app, client): + api = Api(app) + + model = api.model( + "Test", + { + "name": fields.String, + "age": fields.Integer, + "boolean": fields.Boolean, + }, + ) + + @api.route("/test/") + class TestResource(Resource): + @api.marshal_with(model, mask="{name,age}") + def get(self): + pass + + specs = client.get_specs() + op = specs["paths"]["/test/"]["get"] + + assert "parameters" in op + assert len(op["parameters"]) == 1 + + param = op["parameters"][0] + + assert param["name"] == "X-Fields" + assert param["type"] == "string" + assert param["format"] == "mask" + assert param["default"] == "{name,age}" + assert param["in"] == "header" + assert "required" not in param + + def test_marshal_with_expose_default_model_mask_header(self, app, client): + api = Api(app) + + model = api.model( + "Test", + { + "name": fields.String, + "age": fields.Integer, + "boolean": fields.Boolean, + }, + mask="{name,age}", + ) + + @api.route("/test/") + class TestResource(Resource): + @api.marshal_with(model) + def get(self): + pass + + specs = client.get_specs() + definition = specs["definitions"]["Test"] + assert "x-mask" in definition + assert definition["x-mask"] == "{name,age}" diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_inputs.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_inputs.py new file mode 100644 index 0000000..c6539b8 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_inputs.py @@ -0,0 +1,1160 @@ +import re +import pytz +import pytest + +from datetime import date, datetime + +from flask_restx import inputs + + +class Iso8601DateTest(object): + @pytest.mark.parametrize( + "value,expected", + [ + ("2011-01-01", date(2011, 1, 1)), + ("2011-01-01T00:00:00+00:00", date(2011, 1, 1)), + ("2011-01-01T23:59:59+00:00", date(2011, 1, 1)), + ("2011-01-01T23:59:59.001000+00:00", date(2011, 1, 1)), + ("2011-01-01T23:59:59+02:00", date(2011, 1, 1)), + ], + ) + def test_valid_values(self, value, expected): + assert inputs.date_from_iso8601(value) == expected + + def test_error(self): + with pytest.raises(ValueError): + inputs.date_from_iso8601("2008-13-13") + + def test_schema(self): + assert inputs.date_from_iso8601.__schema__ == { + "type": "string", + "format": "date", + } + + +class Iso8601DatetimeTest(object): + @pytest.mark.parametrize( + "value,expected", + [ + ("2011-01-01", datetime(2011, 1, 1)), + ("2011-01-01T00:00:00+00:00", datetime(2011, 1, 1, tzinfo=pytz.utc)), + ( + "2011-01-01T23:59:59+00:00", + datetime(2011, 1, 1, 23, 59, 59, tzinfo=pytz.utc), + ), + ( + "2011-01-01T23:59:59.001000+00:00", + datetime(2011, 1, 1, 23, 59, 59, 1000, tzinfo=pytz.utc), + ), + ( + "2011-01-01T23:59:59+02:00", + datetime(2011, 1, 1, 21, 59, 59, tzinfo=pytz.utc), + ), + ], + ) + def test_valid_values(self, value, expected): + assert inputs.datetime_from_iso8601(value) == expected + + def test_error(self): + with pytest.raises(ValueError): + inputs.datetime_from_iso8601("2008-13-13") + + def test_schema(self): + assert inputs.datetime_from_iso8601.__schema__ == { + "type": "string", + "format": "date-time", + } + + +class Rfc822DatetimeTest(object): + @pytest.mark.parametrize( + "value,expected", + [ + ("Sat, 01 Jan 2011", datetime(2011, 1, 1, tzinfo=pytz.utc)), + ("Sat, 01 Jan 2011 00:00", datetime(2011, 1, 1, tzinfo=pytz.utc)), + ("Sat, 01 Jan 2011 00:00:00", datetime(2011, 1, 1, tzinfo=pytz.utc)), + ("Sat, 01 Jan 2011 00:00:00 +0000", datetime(2011, 1, 1, tzinfo=pytz.utc)), + ("Sat, 01 Jan 2011 00:00:00 -0000", datetime(2011, 1, 1, tzinfo=pytz.utc)), + ( + "Sat, 01 Jan 2011 23:59:59 -0000", + datetime(2011, 1, 1, 23, 59, 59, tzinfo=pytz.utc), + ), + ( + "Sat, 01 Jan 2011 21:00:00 +0200", + datetime(2011, 1, 1, 19, 0, 0, tzinfo=pytz.utc), + ), + ( + "Sat, 01 Jan 2011 21:00:00 -0200", + datetime(2011, 1, 1, 23, 0, 0, tzinfo=pytz.utc), + ), + ], + ) + def test_valid_values(self, value, expected): + assert inputs.datetime_from_rfc822(value) == expected + + def test_error(self): + with pytest.raises(ValueError): + inputs.datetime_from_rfc822("Fake, 01 XXX 2011") + + +class NetlocRegexpTest(object): + @pytest.mark.parametrize( + "netloc,kwargs", + [ + ("localhost", {"localhost": "localhost"}), + ("example.com", {"domain": "example.com"}), + ("www.example.com", {"domain": "www.example.com"}), + ("www.example.com:8000", {"domain": "www.example.com", "port": "8000"}), + ("valid-with-hyphens.com", {"domain": "valid-with-hyphens.com"}), + ("subdomain.example.com", {"domain": "subdomain.example.com"}), + ("200.8.9.10", {"ipv4": "200.8.9.10"}), + ("200.8.9.10:8000", {"ipv4": "200.8.9.10", "port": "8000"}), + ("valid-----hyphens.com", {"domain": "valid-----hyphens.com"}), + ("foo:bar@example.com", {"auth": "foo:bar", "domain": "example.com"}), + ("foo:@example.com", {"auth": "foo:", "domain": "example.com"}), + ("foo@example.com", {"auth": "foo", "domain": "example.com"}), + ( + "foo:@2001:db8:85a3::8a2e:370:7334", + {"auth": "foo:", "ipv6": "2001:db8:85a3::8a2e:370:7334"}, + ), + ( + "[1fff:0:a88:85a3::ac1f]:8001", + {"ipv6": "1fff:0:a88:85a3::ac1f", "port": "8001"}, + ), + ("foo2:qd1%r@example.com", {"auth": "foo2:qd1%r", "domain": "example.com"}), + ], + ) + def test_match(self, netloc, kwargs): + match = inputs.netloc_regex.match(netloc) + assert match, "Should match {0}".format(netloc) + expected = { + "auth": None, + "domain": None, + "ipv4": None, + "ipv6": None, + "localhost": None, + "port": None, + } + expected.update(kwargs) + assert match.groupdict() == expected + + +class URLTest(object): + def assert_bad_url(self, validator, value, details=None): + msg = "{0} is not a valid URL" + with pytest.raises(ValueError) as cm: + validator(value) + if details: + assert str(cm.value) == ". ".join((msg, details)).format(value) + else: + assert str(cm.value).startswith(msg.format(value)) + + @pytest.mark.parametrize( + "url", + [ + "http://www.djangoproject.com/", + "http://example.com/", + "http://www.example.com/", + "http://www.example.com/test", + "http://valid-with-hyphens.com/", + "http://subdomain.example.com/", + "http://valid-----hyphens.com/", + "http://example.com?something=value", + "http://example.com/index.php?something=value&another=value2", + ], + ) + def test_valid_values_default(self, url): + validator = inputs.URL() + assert validator(url) == url + + @pytest.mark.parametrize( + "url", + [ + "foo", + "http://", + "http://example", + "http://example.", + "http://.com", + "http://invalid-.com", + "http://-invalid.com", + "http://inv-.alid-.com", + "http://inv-.-alid.com", + "foo bar baz", + "foo \u2713", + "http://@foo:bar@example.com", + "http://:bar@example.com", + "http://bar:bar:bar@example.com", + "http://300:300:300:300", + "http://example.com:70000", + "http://example.com:0000", + ], + ) + def test_bad_urls(self, url): + # Test with everything enabled to ensure bad URL are really detected + validator = inputs.URL(ip=True, auth=True, port=True) + self.assert_bad_url(validator, url) + # msg = '{0} is not a valid URL'.format(url) + # with pytest.raises(ValueError) as cm: + # validator(url) + # assert str(cm.exception).startswith(msg) + + @pytest.mark.parametrize( + "url", + [ + "google.com", + "domain.google.com", + "kevin:pass@google.com/path?query", + "google.com/path?\u2713", + ], + ) + def test_bad_urls_with_suggestion(self, url): + validator = inputs.URL() + self.assert_bad_url(validator, url, "Did you mean: http://{0}") + + @pytest.mark.parametrize( + "url", + [ + "http://200.8.9.10/", + "http://foo:bar@200.8.9.10/", + "http://200.8.9.10:8000/test", + "http://2001:db8:85a3::8a2e:370:7334", + "http://[1fff:0:a88:85a3::ac1f]:8001", + ], + ) + def test_reject_ip(self, url): + validator = inputs.URL() + self.assert_bad_url(validator, url, "IP is not allowed") + + @pytest.mark.parametrize( + "url", + [ + "http://200.8.9.10/", + "http://200.8.9.10/test", + "http://2001:db8:85a3::8a2e:370:7334", + "http://[1fff:0:a88:85a3::ac1f]", + ], + ) + def test_allow_ip(self, url): + validator = inputs.URL(ip=True) + assert validator(url) == url + + @pytest.mark.parametrize( + "url", + [ + "http://foo:bar@200.8.9.10/", + "http://foo:@2001:db8:85a3::8a2e:370:7334", + "http://foo:bar@[1fff:0:a88:85a3::ac1f]:8001", + "http://foo:@2001:db8:85a3::8a2e:370:7334", + "http://foo2:qd1%r@example.com", + ], + ) + def test_reject_auth(self, url): + # Test with IP and port to ensure only auth is rejected + validator = inputs.URL(ip=True, port=True) + self.assert_bad_url(validator, url, "Authentication is not allowed") + + @pytest.mark.parametrize( + "url", + [ + "http://foo:bar@example.com", + "http://foo:@example.com", + "http://foo@example.com", + ], + ) + def test_allow_auth(self, url): + validator = inputs.URL(auth=True) + assert validator(url) == url + + @pytest.mark.parametrize( + "url", + [ + "http://localhost", + "http://127.0.0.1", + "http://127.0.1.1", + "http://::1", + ], + ) + def test_reject_local(self, url): + # Test with IP and port to ensure only auth is rejected + validator = inputs.URL(ip=True) + self.assert_bad_url(validator, url, "Localhost is not allowed") + + @pytest.mark.parametrize( + "url", + [ + "http://localhost", + "http://127.0.0.1", + "http://127.0.1.1", + "http://::1", + ], + ) + def test_allow_local(self, url): + validator = inputs.URL(ip=True, local=True) + assert validator(url) == url + + @pytest.mark.parametrize( + "url", + [ + "http://200.8.9.10:8080/", + "http://foo:bar@200.8.9.10:8080/", + "http://foo:bar@[1fff:0:a88:85a3::ac1f]:8001", + ], + ) + def test_reject_port(self, url): + # Test with auth and port to ensure only port is rejected + validator = inputs.URL(ip=True, auth=True) + self.assert_bad_url(validator, url, "Custom port is not allowed") + + @pytest.mark.parametrize( + "url", + [ + "http://example.com:80", + "http://example.com:8080", + "http://www.example.com:8000/test", + ], + ) + def test_allow_port(self, url): + validator = inputs.URL(port=True) + assert validator(url) == url + + @pytest.mark.parametrize( + "url", + [ + "sip://somewhere.com", + "irc://somewhere.com", + ], + ) + def test_valid_restricted_schemes(self, url): + validator = inputs.URL(schemes=("sip", "irc")) + assert validator(url) == url + + @pytest.mark.parametrize( + "url", + [ + "http://somewhere.com", + "https://somewhere.com", + ], + ) + def test_invalid_restricted_schemes(self, url): + validator = inputs.URL(schemes=("sip", "irc")) + self.assert_bad_url(validator, url, "Protocol is not allowed") + + @pytest.mark.parametrize( + "url", + [ + "http://example.com", + "http://example.com/test/", + "http://www.example.com/", + "http://www.example.com/test", + ], + ) + def test_valid_restricted_domains(self, url): + validator = inputs.URL(domains=["example.com", "www.example.com"]) + assert validator(url) == url + + @pytest.mark.parametrize( + "url", + [ + "http://somewhere.com", + "https://somewhere.com", + ], + ) + def test_invalid_restricted_domains(self, url): + validator = inputs.URL(domains=["example.com", "www.example.com"]) + self.assert_bad_url(validator, url, "Domain is not allowed") + + @pytest.mark.parametrize( + "url", + [ + "http://somewhere.com", + "https://somewhere.com", + ], + ) + def test_valid_excluded_domains(self, url): + validator = inputs.URL(exclude=["example.com", "www.example.com"]) + assert validator(url) == url + + @pytest.mark.parametrize( + "url", + [ + "http://example.com", + "http://example.com/test/", + "http://www.example.com/", + "http://www.example.com/test", + ], + ) + def test_excluded_domains(self, url): + validator = inputs.URL(exclude=["example.com", "www.example.com"]) + self.assert_bad_url(validator, url, "Domain is not allowed") + + def test_check(self): + validator = inputs.URL(check=True, ip=True) + assert ( + validator("http://www.google.com") == "http://www.google.com" + ), "Should check domain" + + # This test will fail on a network where this address is defined + self.assert_bad_url( + validator, + "http://this-domain-should-not-exist.com", + "Domain does not exists", + ) + + def test_schema(self): + assert inputs.URL().__schema__ == {"type": "string", "format": "url"} + + +class UrlTest(object): + @pytest.mark.parametrize( + "url", + [ + "http://www.djangoproject.com/", + "http://localhost/", + "http://example.com/", + "http://www.example.com/", + "http://www.example.com:8000/test", + "http://valid-with-hyphens.com/", + "http://subdomain.example.com/", + "http://200.8.9.10/", + "http://200.8.9.10:8000/test", + "http://valid-----hyphens.com/", + "http://example.com?something=value", + "http://example.com/index.php?something=value&another=value2", + "http://foo:bar@example.com", + "http://foo:@example.com", + "http://foo@example.com", + "http://foo:@2001:db8:85a3::8a2e:370:7334", + "http://foo2:qd1%r@example.com", + ], + ) + def test_valid_url(self, url): + assert inputs.url(url) == url + + @pytest.mark.parametrize( + "url", + [ + "foo", + "http://", + "http://example", + "http://example.", + "http://.com", + "http://invalid-.com", + "http://-invalid.com", + "http://inv-.alid-.com", + "http://inv-.-alid.com", + "foo bar baz", + "foo \u2713", + "http://@foo:bar@example.com", + "http://:bar@example.com", + "http://bar:bar:bar@example.com", + "http://300:300:300:300", + "http://example.com:70000", + ], + ) + def test_bad_url(self, url): + with pytest.raises(ValueError) as cm: + inputs.url(url) + assert str(cm.value).startswith("{0} is not a valid URL".format(url)) + + @pytest.mark.parametrize( + "url", + [ + "google.com", + "domain.google.com", + "kevin:pass@google.com/path?query", + "google.com/path?\u2713", + ], + ) + def test_bad_url_with_suggestion(self, url): + with pytest.raises(ValueError) as cm: + inputs.url(url) + assert str( + cm.value + ) == "{0} is not a valid URL. Did you mean: http://{0}".format(url) + + def test_schema(self): + assert inputs.url.__schema__ == {"type": "string", "format": "url"} + + +class IPTest(object): + @pytest.mark.parametrize( + "value", + [ + "200.8.9.10", + "127.0.0.1", + "2001:db8:85a3::8a2e:370:7334", + "::1", + ], + ) + def test_valid_value(self, value): + assert inputs.ip(value) == value + + @pytest.mark.parametrize( + "value", + [ + "foo", + "http://", + "http://example", + "http://example.", + "http://.com", + "http://invalid-.com", + "http://-invalid.com", + "http://inv-.alid-.com", + "http://inv-.-alid.com", + "foo bar baz", + "foo \u2713", + "http://@foo:bar@example.com", + "http://:bar@example.com", + "http://bar:bar:bar@example.com", + "127.0", + ], + ) + def test_bad_value(self, value): + with pytest.raises(ValueError): + inputs.ip(value) + + def test_schema(self): + assert inputs.ip.__schema__ == {"type": "string", "format": "ip"} + + +class IPv4Test(object): + @pytest.mark.parametrize( + "value", + [ + "200.8.9.10", + "127.0.0.1", + ], + ) + def test_valid_value(self, value): + assert inputs.ipv4(value) == value + + @pytest.mark.parametrize( + "value", + [ + "2001:db8:85a3::8a2e:370:7334", + "::1", + "foo", + "http://", + "http://example", + "http://example.", + "http://.com", + "http://invalid-.com", + "http://-invalid.com", + "http://inv-.alid-.com", + "http://inv-.-alid.com", + "foo bar baz", + "foo \u2713", + "http://@foo:bar@example.com", + "http://:bar@example.com", + "http://bar:bar:bar@example.com", + "127.0", + ], + ) + def test_bad_value(self, value): + with pytest.raises(ValueError): + inputs.ipv4(value) + + def test_schema(self): + assert inputs.ipv4.__schema__ == {"type": "string", "format": "ipv4"} + + +class IPv6Test(object): + @pytest.mark.parametrize( + "value", + [ + "2001:db8:85a3::8a2e:370:7334", + "::1", + ], + ) + def test_valid_value(self, value): + assert inputs.ipv6(value) == value + + @pytest.mark.parametrize( + "value", + [ + "200.8.9.10", + "127.0.0.1", + "foo", + "http://", + "http://example", + "http://example.", + "http://.com", + "http://invalid-.com", + "http://-invalid.com", + "http://inv-.alid-.com", + "http://inv-.-alid.com", + "foo bar baz", + "foo \u2713", + "http://@foo:bar@example.com", + "http://:bar@example.com", + "http://bar:bar:bar@example.com", + "127.0", + ], + ) + def test_bad_value(self, value): + with pytest.raises(ValueError): + inputs.ipv6(value) + + def test_schema(self): + assert inputs.ipv6.__schema__ == {"type": "string", "format": "ipv6"} + + +class EmailTest(object): + def assert_bad_email(self, validator, value, msg=None): + msg = msg or "{0} is not a valid email" + with pytest.raises(ValueError) as cm: + validator(value) + assert str(cm.value) == msg.format(value) + + @pytest.mark.parametrize( + "value", + [ + "test@gmail.com", + "coucou@cmoi.fr", + "coucou+another@cmoi.fr", + "Coucou@cmoi.fr", + "me@valid-with-hyphens.com", + "me@subdomain.example.com", + "me@sub.subdomain.example.com", + "Loïc.Accentué@voilà.fr", + ], + ) + def test_valid_value_default(self, value): + validator = inputs.email() + assert validator(value) == value + + @pytest.mark.parametrize( + "value", + [ + "me@localhost", + "me@127.0.0.1", + "me@127.1.2.3", + "me@::1", + "me@200.8.9.10", + "me@2001:db8:85a3::8a2e:370:7334", + "foo@bar.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?" + + ".?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?.?@", + ], + ) + def test_invalid_value_default(self, value): + self.assert_bad_email(inputs.email(), value) + + @pytest.mark.parametrize( + "value", + [ + "test@gmail.com", + "test@live.com", + ], + ) + def test_valid_value_check(self, value): + email = inputs.email(check=True) + assert email(value) == value + + @pytest.mark.parametrize( + "value", + [ + "coucou@not-found.fr", + "me@localhost", + "me@127.0.0.1", + "me@127.1.2.3", + "me@::1", + "me@200.8.9.10", + "me@2001:db8:85a3::8a2e:370:7334", + ], + ) + def test_invalid_values_check(self, value): + email = inputs.email(check=True) + self.assert_bad_email(email, value) + + @pytest.mark.parametrize( + "value", + [ + "test@gmail.com", + "coucou@cmoi.fr", + "coucou+another@cmoi.fr", + "Coucou@cmoi.fr", + "me@valid-with-hyphens.com", + "me@subdomain.example.com", + "me@200.8.9.10", + "me@2001:db8:85a3::8a2e:370:7334", + ], + ) + def test_valid_value_ip(self, value): + email = inputs.email(ip=True) + assert email(value) == value + + @pytest.mark.parametrize( + "value", + [ + "me@localhost", + "me@127.0.0.1", + "me@127.1.2.3", + "me@::1", + ], + ) + def test_invalid_value_ip(self, value): + email = inputs.email(ip=True) + self.assert_bad_email(email, value) + + @pytest.mark.parametrize( + "value", + [ + "test@gmail.com", + "coucou@cmoi.fr", + "coucou+another@cmoi.fr", + "Coucou@cmoi.fr", + "coucou@localhost", + "me@valid-with-hyphens.com", + "me@subdomain.example.com", + "me@localhost", + ], + ) + def test_valid_value_local(self, value): + email = inputs.email(local=True) + assert email(value) == value + + @pytest.mark.parametrize( + "value", + [ + "me@127.0.0.1", + "me@127.1.2.3", + "me@::1", + "me@200.8.9.10", + "me@2001:db8:85a3::8a2e:370:7334", + ], + ) + def test_invalid_value_local(self, value): + email = inputs.email(local=True) + self.assert_bad_email(email, value) + + @pytest.mark.parametrize( + "value", + [ + "test@gmail.com", + "coucou@cmoi.fr", + "coucou+another@cmoi.fr", + "Coucou@cmoi.fr", + "coucou@localhost", + "me@valid-with-hyphens.com", + "me@subdomain.example.com", + "me@200.8.9.10", + "me@2001:db8:85a3::8a2e:370:7334", + "me@localhost", + "me@127.0.0.1", + "me@127.1.2.3", + "me@::1", + ], + ) + def test_valid_value_ip_and_local(self, value): + email = inputs.email(ip=True, local=True) + assert email(value) == value + + @pytest.mark.parametrize( + "value", + [ + "test@gmail.com", + "coucou@cmoi.fr", + "coucou+another@cmoi.fr", + "Coucou@cmoi.fr", + ], + ) + def test_valid_value_domains(self, value): + email = inputs.email(domains=("gmail.com", "cmoi.fr")) + assert email(value) == value + + @pytest.mark.parametrize( + "value", + [ + "me@valid-with-hyphens.com", + "me@subdomain.example.com", + "me@localhost", + "me@127.0.0.1", + "me@127.1.2.3", + "me@::1", + "me@200.8.9.10", + "me@2001:db8:85a3::8a2e:370:7334", + ], + ) + def test_invalid_value_domains(self, value): + email = inputs.email(domains=("gmail.com", "cmoi.fr")) + self.assert_bad_email( + email, value, "{0} does not belong to the authorized domains" + ) + + @pytest.mark.parametrize( + "value", + [ + "test@gmail.com", + "coucou@cmoi.fr", + "coucou+another@cmoi.fr", + "Coucou@cmoi.fr", + ], + ) + def test_valid_value_exclude(self, value): + email = inputs.email(exclude=("somewhere.com", "foo.bar")) + assert email(value) == value + + @pytest.mark.parametrize( + "value", + [ + "me@somewhere.com", + "me@foo.bar", + ], + ) + def test_invalid_value_exclude(self, value): + email = inputs.email(exclude=("somewhere.com", "foo.bar")) + self.assert_bad_email(email, value, "{0} belongs to a forbidden domain") + + @pytest.mark.parametrize( + "value", + [ + "someone@", + "@somewhere", + "email.somewhere.com", + "[invalid!email]", + "me.@somewhere", + "me..something@somewhere", + ], + ) + def test_bad_email(self, value): + email = inputs.email() + self.assert_bad_email(email, value) + + def test_schema(self): + assert inputs.email().__schema__ == {"type": "string", "format": "email"} + + +class RegexTest(object): + @pytest.mark.parametrize( + "value", + [ + "123", + "1234567890", + "00000", + ], + ) + def test_valid_input(self, value): + num_only = inputs.regex(r"^[0-9]+$") + assert num_only(value) == value + + @pytest.mark.parametrize( + "value", + [ + "abc", + "123abc", + "abc123", + "", + ], + ) + def test_bad_input(self, value): + num_only = inputs.regex(r"^[0-9]+$") + with pytest.raises(ValueError): + num_only(value) + + def test_bad_pattern(self): + with pytest.raises(re.error): + inputs.regex("[") + + def test_schema(self): + assert inputs.regex(r"^[0-9]+$").__schema__ == { + "type": "string", + "pattern": "^[0-9]+$", + } + + +class BooleanTest(object): + def test_false(self): + assert inputs.boolean("False") is False + + def test_0(self): + assert inputs.boolean("0") is False + + def test_true(self): + assert inputs.boolean("true") is True + + def test_1(self): + assert inputs.boolean("1") is True + + def test_case(self): + assert inputs.boolean("FaLSE") is False + assert inputs.boolean("FaLSE") is False + + def test_python_bool(self): + assert inputs.boolean(True) is True + assert inputs.boolean(False) is False + + def test_bad_boolean(self): + with pytest.raises(ValueError): + inputs.boolean("blah") + with pytest.raises(ValueError): + inputs.boolean(None) + + def test_checkbox(self): + assert inputs.boolean("on") is True + + def test_non_strings(self): + assert inputs.boolean(0) is False + assert inputs.boolean(1) is True + assert inputs.boolean([]) is False + + def test_schema(self): + assert inputs.boolean.__schema__ == {"type": "boolean"} + + +class DateTest(object): + def test_later_than_1900(self): + assert inputs.date("1900-01-01") == datetime(1900, 1, 1) + + def test_error(self): + with pytest.raises(ValueError): + inputs.date("2008-13-13") + + def test_default(self): + assert inputs.date("2008-08-01") == datetime(2008, 8, 1) + + def test_schema(self): + assert inputs.date.__schema__ == {"type": "string", "format": "date"} + + +class NaturalTest(object): + def test_negative(self): + with pytest.raises(ValueError): + inputs.natural(-1) + + def test_default(self): + assert inputs.natural(3) == 3 + + def test_string(self): + with pytest.raises(ValueError): + inputs.natural("foo") + + def test_schema(self): + assert inputs.natural.__schema__ == {"type": "integer", "minimum": 0} + + +class PositiveTest(object): + def test_positive(self): + assert inputs.positive(1) == 1 + assert inputs.positive(10000) == 10000 + + def test_zero(self): + with pytest.raises(ValueError): + inputs.positive(0) + + def test_negative(self): + with pytest.raises(ValueError): + inputs.positive(-1) + + def test_schema(self): + assert inputs.positive.__schema__ == { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": True, + } + + +class IntRangeTest(object): + def test_valid_range(self): + int_range = inputs.int_range(1, 5) + assert int_range(3) == 3 + + def test_inclusive_range(self): + int_range = inputs.int_range(1, 5) + assert int_range(5) == 5 + + def test_lower(self): + int_range = inputs.int_range(0, 5) + with pytest.raises(ValueError): + int_range(-1) + + def test_higher(self): + int_range = inputs.int_range(0, 5) + with pytest.raises(ValueError): + int_range(6) + + def test_schema(self): + assert inputs.int_range(1, 5).__schema__ == { + "type": "integer", + "minimum": 1, + "maximum": 5, + } + + +interval_test_values = [ + ( + # Full precision with explicit UTC. + "2013-01-01T12:30:00Z/P1Y2M3DT4H5M6S", + ( + datetime(2013, 1, 1, 12, 30, 0, tzinfo=pytz.utc), + datetime(2014, 3, 5, 16, 35, 6, tzinfo=pytz.utc), + ), + ), + ( + # Full precision with alternate UTC indication + "2013-01-01T12:30+00:00/P2D", + ( + datetime(2013, 1, 1, 12, 30, 0, tzinfo=pytz.utc), + datetime(2013, 1, 3, 12, 30, 0, tzinfo=pytz.utc), + ), + ), + ( + # Implicit UTC with time + "2013-01-01T15:00/P1M", + ( + datetime(2013, 1, 1, 15, 0, 0, tzinfo=pytz.utc), + datetime(2013, 1, 31, 15, 0, 0, tzinfo=pytz.utc), + ), + ), + ( + # TZ conversion + "2013-01-01T17:00-05:00/P2W", + ( + datetime(2013, 1, 1, 22, 0, 0, tzinfo=pytz.utc), + datetime(2013, 1, 15, 22, 0, 0, tzinfo=pytz.utc), + ), + ), + ( + # Date upgrade to midnight-midnight period + "2013-01-01/P3D", + ( + datetime(2013, 1, 1, 0, 0, 0, tzinfo=pytz.utc), + datetime(2013, 1, 4, 0, 0, 0, 0, tzinfo=pytz.utc), + ), + ), + ( + # Start/end with UTC + "2013-01-01T12:00:00Z/2013-02-01T12:00:00Z", + ( + datetime(2013, 1, 1, 12, 0, 0, tzinfo=pytz.utc), + datetime(2013, 2, 1, 12, 0, 0, tzinfo=pytz.utc), + ), + ), + ( + # Start/end with time upgrade + "2013-01-01/2013-06-30", + ( + datetime(2013, 1, 1, tzinfo=pytz.utc), + datetime(2013, 6, 30, tzinfo=pytz.utc), + ), + ), + ( + # Start/end with TZ conversion + "2013-02-17T12:00:00-07:00/2013-02-28T15:00:00-07:00", + ( + datetime(2013, 2, 17, 19, 0, 0, tzinfo=pytz.utc), + datetime(2013, 2, 28, 22, 0, 0, tzinfo=pytz.utc), + ), + ), + ( # Resolution expansion for single date(time) + # Second with UTC + "2013-01-01T12:30:45Z", + ( + datetime(2013, 1, 1, 12, 30, 45, tzinfo=pytz.utc), + datetime(2013, 1, 1, 12, 30, 46, tzinfo=pytz.utc), + ), + ), + ( + # Second with tz conversion + "2013-01-01T12:30:45+02:00", + ( + datetime(2013, 1, 1, 10, 30, 45, tzinfo=pytz.utc), + datetime(2013, 1, 1, 10, 30, 46, tzinfo=pytz.utc), + ), + ), + ( + # Second with implicit UTC + "2013-01-01T12:30:45", + ( + datetime(2013, 1, 1, 12, 30, 45, tzinfo=pytz.utc), + datetime(2013, 1, 1, 12, 30, 46, tzinfo=pytz.utc), + ), + ), + ( + # Minute with UTC + "2013-01-01T12:30+00:00", + ( + datetime(2013, 1, 1, 12, 30, tzinfo=pytz.utc), + datetime(2013, 1, 1, 12, 31, tzinfo=pytz.utc), + ), + ), + ( + # Minute with conversion + "2013-01-01T12:30+04:00", + ( + datetime(2013, 1, 1, 8, 30, tzinfo=pytz.utc), + datetime(2013, 1, 1, 8, 31, tzinfo=pytz.utc), + ), + ), + ( + # Minute with implicit UTC + "2013-01-01T12:30", + ( + datetime(2013, 1, 1, 12, 30, tzinfo=pytz.utc), + datetime(2013, 1, 1, 12, 31, tzinfo=pytz.utc), + ), + ), + ( + # Hour, explicit UTC + "2013-01-01T12Z", + ( + datetime(2013, 1, 1, 12, tzinfo=pytz.utc), + datetime(2013, 1, 1, 13, tzinfo=pytz.utc), + ), + ), + ( + # Hour with offset + "2013-01-01T12-07:00", + ( + datetime(2013, 1, 1, 19, tzinfo=pytz.utc), + datetime(2013, 1, 1, 20, tzinfo=pytz.utc), + ), + ), + ( + # Hour with implicit UTC + "2013-01-01T12", + ( + datetime(2013, 1, 1, 12, tzinfo=pytz.utc), + datetime(2013, 1, 1, 13, tzinfo=pytz.utc), + ), + ), + ( + # Interval with trailing zero fractional seconds should + # be accepted. + "2013-01-01T12:00:00.0/2013-01-01T12:30:00.000000", + ( + datetime(2013, 1, 1, 12, tzinfo=pytz.utc), + datetime(2013, 1, 1, 12, 30, tzinfo=pytz.utc), + ), + ), +] + + +class IsoIntervalTest(object): + @pytest.mark.parametrize("value,expected", interval_test_values) + def test_valid_value(self, value, expected): + assert inputs.iso8601interval(value) == expected + + def test_error_message(self): + with pytest.raises(ValueError) as cm: + inputs.iso8601interval("2013-01-01/blah") + expected = "Invalid argument: 2013-01-01/blah. argument must be a valid ISO8601 date/time interval." + assert str(cm.value) == expected + + @pytest.mark.parametrize( + "value", + [ + "2013-01T14:", + "", + "asdf", + "01/01/2013", + ], + ) + def test_bad_values(self, value): + with pytest.raises(ValueError): + inputs.iso8601interval(value) + + def test_schema(self): + assert inputs.iso8601interval.__schema__ == { + "type": "string", + "format": "iso8601-interval", + } diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_logging.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_logging.py new file mode 100644 index 0000000..79669aa --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_logging.py @@ -0,0 +1,134 @@ +import logging + +import flask_restx as restx + + +class LoggingTest(object): + def test_namespace_loggers_log_to_flask_app_logger(self, app, client, caplog): + # capture Flask app logs + caplog.set_level(logging.INFO, logger=app.logger.name) + + api = restx.Api(app) + ns1 = api.namespace("ns1", path="/ns1") + ns2 = api.namespace("ns2", path="/ns2") + + @ns1.route("/") + class Ns1(restx.Resource): + def get(self): + ns1.logger.info("hello from ns1") + pass + + @ns2.route("/") + class Ns2(restx.Resource): + def get(self): + ns2.logger.info("hello from ns2") + pass + + # debug log not shown + client.get("/ns1/") + matching = [r for r in caplog.records if r.message == "hello from ns1"] + assert len(matching) == 1 + + # info log shown + client.get("/ns2/") + matching = [r for r in caplog.records if r.message == "hello from ns2"] + assert len(matching) == 1 + + def test_defaults_to_app_level(self, app, client, caplog): + caplog.set_level(logging.INFO, logger=app.logger.name) + + api = restx.Api(app) + ns1 = api.namespace("ns1", path="/ns1") + ns2 = api.namespace("ns2", path="/ns2") + + @ns1.route("/") + class Ns1(restx.Resource): + def get(self): + ns1.logger.debug("hello from ns1") + pass + + @ns2.route("/") + class Ns2(restx.Resource): + def get(self): + ns2.logger.info("hello from ns2") + pass + + # debug log not shown + client.get("/ns1/") + matching = [r for r in caplog.records if r.message == "hello from ns1"] + assert len(matching) == 0 + + # info log shown + client.get("/ns2/") + matching = [r for r in caplog.records if r.message == "hello from ns2"] + assert len(matching) == 1 + + def test_override_app_level(self, app, client, caplog): + caplog.set_level(logging.DEBUG, logger=app.logger.name) + + api = restx.Api(app) + ns1 = api.namespace("ns1", path="/ns1") + ns1.logger.setLevel(logging.DEBUG) + ns2 = api.namespace("ns2", path="/ns2") + ns2.logger.setLevel(logging.INFO) + + @ns1.route("/") + class Ns1(restx.Resource): + def get(self): + ns1.logger.debug("hello from ns1") + pass + + @ns2.route("/") + class Ns2(restx.Resource): + def get(self): + ns2.logger.debug("hello from ns2") + pass + + # debug log shown from ns1 + client.get("/ns1/") + matching = [r for r in caplog.records if r.message == "hello from ns1"] + assert len(matching) == 1 + + # debug not shown from ns2 + client.get("/ns2/") + matching = [r for r in caplog.records if r.message == "hello from ns2"] + assert len(matching) == 0 + + def test_namespace_additional_handler(self, app, client, caplog, tmp_path): + caplog.set_level(logging.INFO, logger=app.logger.name) + log_file = tmp_path / "v1.log" + + api = restx.Api(app) + ns1 = api.namespace("ns1", path="/ns1") + # set up a file handler for ns1 only + # FileHandler only supports Path object on Python >= 3.6 -> cast to str + fh = logging.FileHandler(str(log_file)) + ns1.logger.addHandler(fh) + + ns2 = api.namespace("ns2", path="/ns2") + + @ns1.route("/") + class Ns1(restx.Resource): + def get(self): + ns1.logger.info("hello from ns1") + pass + + @ns2.route("/") + class Ns2(restx.Resource): + def get(self): + ns2.logger.info("hello from ns2") + pass + + # ns1 logs go to stdout and log_file + client.get("/ns1/") + matching = [r for r in caplog.records if r.message == "hello from ns1"] + assert len(matching) == 1 + with log_file.open() as f: + assert "hello from ns1" in f.read() + + # ns2 logs go to stdout only + client.get("/ns2/") + matching = [r for r in caplog.records if r.message == "hello from ns2"] + assert len(matching) == 1 + with log_file.open() as f: + assert "hello from ns1" in f.read() diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_marshalling.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_marshalling.py new file mode 100644 index 0000000..d6846ea --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_marshalling.py @@ -0,0 +1,537 @@ +import pytest + +from flask_restx import marshal, marshal_with, marshal_with_field, fields, Api, Resource + +from collections import OrderedDict + + +# Add a dummy Resource to verify that the app is properly set. +class HelloWorld(Resource): + def get(self): + return {} + + +class MarshallingTest(object): + def test_marshal(self): + model = OrderedDict([("foo", fields.Raw)]) + marshal_dict = OrderedDict([("foo", "bar"), ("bat", "baz")]) + output = marshal(marshal_dict, model) + assert isinstance(output, dict) + assert not isinstance(output, OrderedDict) + assert output == {"foo": "bar"} + + def test_marshal_wildcard_nested(self): + nest = fields.Nested( + OrderedDict([("thumbnail", fields.String), ("video", fields.String)]) + ) + wild = fields.Wildcard(nest) + wildcard_fields = OrderedDict([("*", wild)]) + model = OrderedDict([("preview", fields.Nested(wildcard_fields))]) + sub_dict = OrderedDict( + [ + ("9:16", {"thumbnail": 24, "video": 12}), + ("16:9", {"thumbnail": 25, "video": 11}), + ("1:1", {"thumbnail": 26, "video": 10}), + ] + ) + marshal_dict = OrderedDict([("preview", sub_dict)]) + output = marshal(marshal_dict, model) + assert output == { + "preview": { + "1:1": {"thumbnail": "26", "video": "10"}, + "16:9": {"thumbnail": "25", "video": "11"}, + "9:16": {"thumbnail": "24", "video": "12"}, + } + } + + def test_marshal_wildcard_list(self): + wild = fields.Wildcard(fields.List(fields.String)) + wildcard_fields = OrderedDict([("*", wild)]) + model = OrderedDict([("preview", fields.Nested(wildcard_fields))]) + sub_dict = OrderedDict( + [("1:1", [1, 2, 3]), ("16:9", [4, 5, 6]), ("9:16", [7, 8, 9])] + ) + marshal_dict = OrderedDict([("preview", sub_dict)]) + output = marshal(marshal_dict, model) + assert output == { + "preview": { + "9:16": ["7", "8", "9"], + "16:9": ["4", "5", "6"], + "1:1": ["1", "2", "3"], + } + } + + def test_marshal_with_envelope(self): + model = OrderedDict([("foo", fields.Raw)]) + marshal_dict = OrderedDict([("foo", "bar"), ("bat", "baz")]) + output = marshal(marshal_dict, model, envelope="hey") + assert output == {"hey": {"foo": "bar"}} + + def test_marshal_wildcard_with_envelope(self): + wild = fields.Wildcard(fields.String) + model = OrderedDict([("foo", fields.Raw), ("*", wild)]) + marshal_dict = OrderedDict( + [("foo", {"bat": "baz"}), ("a", "toto"), ("b", "tata")] + ) + output = marshal(marshal_dict, model, envelope="hey") + assert output == {"hey": {"a": "toto", "b": "tata", "foo": {"bat": "baz"}}} + + def test_marshal_with_skip_none(self): + model = OrderedDict( + [("foo", fields.Raw), ("bat", fields.Raw), ("qux", fields.Raw)] + ) + marshal_dict = OrderedDict([("foo", "bar"), ("bat", None)]) + output = marshal(marshal_dict, model, skip_none=True) + assert output == {"foo": "bar"} + + def test_marshal_wildcard_with_skip_none(self): + wild = fields.Wildcard(fields.String) + model = OrderedDict([("foo", fields.Raw), ("*", wild)]) + marshal_dict = OrderedDict( + [("foo", None), ("bat", None), ("baz", "biz"), ("bar", None)] + ) + output = marshal(marshal_dict, model, skip_none=True) + assert output == {"baz": "biz"} + + def test_marshal_decorator(self): + model = OrderedDict([("foo", fields.Raw)]) + + @marshal_with(model) + def try_me(): + return OrderedDict([("foo", "bar"), ("bat", "baz")]) + + assert try_me() == {"foo": "bar"} + + def test_marshal_decorator_with_envelope(self): + model = OrderedDict([("foo", fields.Raw)]) + + @marshal_with(model, envelope="hey") + def try_me(): + return OrderedDict([("foo", "bar"), ("bat", "baz")]) + + assert try_me() == {"hey": {"foo": "bar"}} + + def test_marshal_decorator_with_skip_none(self): + model = OrderedDict( + [("foo", fields.Raw), ("bat", fields.Raw), ("qux", fields.Raw)] + ) + + @marshal_with(model, skip_none=True) + def try_me(): + return OrderedDict([("foo", "bar"), ("bat", None)]) + + assert try_me() == {"foo": "bar"} + + def test_marshal_decorator_tuple(self): + model = OrderedDict([("foo", fields.Raw)]) + + @marshal_with(model) + def try_me(): + headers = {"X-test": 123} + return OrderedDict([("foo", "bar"), ("bat", "baz")]), 200, headers + + assert try_me() == ({"foo": "bar"}, 200, {"X-test": 123}) + + def test_marshal_decorator_tuple_with_envelope(self): + model = OrderedDict([("foo", fields.Raw)]) + + @marshal_with(model, envelope="hey") + def try_me(): + headers = {"X-test": 123} + return OrderedDict([("foo", "bar"), ("bat", "baz")]), 200, headers + + assert try_me() == ({"hey": {"foo": "bar"}}, 200, {"X-test": 123}) + + def test_marshal_decorator_tuple_with_skip_none(self): + model = OrderedDict( + [("foo", fields.Raw), ("bat", fields.Raw), ("qux", fields.Raw)] + ) + + @marshal_with(model, skip_none=True) + def try_me(): + headers = {"X-test": 123} + return OrderedDict([("foo", "bar"), ("bat", None)]), 200, headers + + assert try_me() == ({"foo": "bar"}, 200, {"X-test": 123}) + + def test_marshal_field_decorator(self): + model = fields.Raw + + @marshal_with_field(model) + def try_me(): + return "foo" + + assert try_me() == "foo" + + def test_marshal_field_decorator_tuple(self): + model = fields.Raw + + @marshal_with_field(model) + def try_me(): + return "foo", 200, {"X-test": 123} + + assert try_me() == ("foo", 200, {"X-test": 123}) + + def test_marshal_field(self): + model = OrderedDict({"foo": fields.Raw()}) + marshal_fields = OrderedDict([("foo", "bar"), ("bat", "baz")]) + output = marshal(marshal_fields, model) + assert output == {"foo": "bar"} + + def test_marshal_tuple(self): + model = OrderedDict({"foo": fields.Raw}) + marshal_fields = OrderedDict([("foo", "bar"), ("bat", "baz")]) + output = marshal((marshal_fields,), model) + assert output == [{"foo": "bar"}] + + def test_marshal_tuple_with_envelope(self): + model = OrderedDict({"foo": fields.Raw}) + marshal_fields = OrderedDict([("foo", "bar"), ("bat", "baz")]) + output = marshal((marshal_fields,), model, envelope="hey") + assert output == {"hey": [{"foo": "bar"}]} + + def test_marshal_tuple_with_skip_none(self): + model = OrderedDict( + [("foo", fields.Raw), ("bat", fields.Raw), ("qux", fields.Raw)] + ) + marshal_fields = OrderedDict([("foo", "bar"), ("bat", None)]) + output = marshal((marshal_fields,), model, skip_none=True) + assert output == [{"foo": "bar"}] + + def test_marshal_nested(self): + model = { + "foo": fields.Raw, + "fee": fields.Nested({"fye": fields.String}), + } + + marshal_fields = { + "foo": "bar", + "bat": "baz", + "fee": {"fye": "fum"}, + } + expected = { + "foo": "bar", + "fee": {"fye": "fum"}, + } + + output = marshal(marshal_fields, model) + + assert output == expected + + def test_marshal_ordered(self): + model = OrderedDict( + [("foo", fields.Raw), ("baz", fields.Raw), ("bar", fields.Raw)] + ) + marshal_fields = {"foo": 1, "baz": 2, "bar": 3} + expected_ordered = OrderedDict([("foo", 1), ("baz", 2), ("bar", 3)]) + ordered_output = marshal(marshal_fields, model, ordered=True) + assert ordered_output == expected_ordered + unordered_output = marshal(marshal_fields, model) + assert not isinstance(unordered_output, OrderedDict) + + def test_marshal_nested_ordered(self): + model = OrderedDict( + [ + ("foo", fields.Raw), + ( + "fee", + fields.Nested( + { + "fye": fields.String, + } + ), + ), + ] + ) + + marshal_fields = { + "foo": "bar", + "bat": "baz", + "fee": {"fye": "fum"}, + } + expected = OrderedDict([("foo", "bar"), ("fee", OrderedDict([("fye", "fum")]))]) + + output = marshal(marshal_fields, model, ordered=True) + + assert isinstance(output, OrderedDict) + assert output == expected + assert isinstance(output["fee"], OrderedDict) + + def test_marshal_nested_with_non_null(self): + model = OrderedDict( + [ + ("foo", fields.Raw), + ( + "fee", + fields.Nested( + OrderedDict([("fye", fields.String), ("blah", fields.String)]), + allow_null=False, + ), + ), + ] + ) + marshal_fields = [OrderedDict([("foo", "bar"), ("bat", "baz"), ("fee", None)])] + output = marshal(marshal_fields, model) + expected = [ + OrderedDict( + [("foo", "bar"), ("fee", OrderedDict([("fye", None), ("blah", None)]))] + ) + ] + assert output == expected + + def test_marshal_nested_with_null(self): + model = OrderedDict( + [ + ("foo", fields.Raw), + ( + "fee", + fields.Nested( + OrderedDict([("fye", fields.String), ("blah", fields.String)]), + allow_null=True, + ), + ), + ] + ) + marshal_fields = OrderedDict([("foo", "bar"), ("bat", "baz"), ("fee", None)]) + output = marshal(marshal_fields, model) + expected = OrderedDict([("foo", "bar"), ("fee", None)]) + assert output == expected + + def test_marshal_nested_with_skip_none(self): + model = OrderedDict( + [ + ("foo", fields.Raw), + ( + "fee", + fields.Nested( + OrderedDict([("fye", fields.String)]), skip_none=True + ), + ), + ] + ) + marshal_fields = OrderedDict([("foo", "bar"), ("bat", "baz"), ("fee", None)]) + output = marshal(marshal_fields, model, skip_none=True) + expected = OrderedDict([("foo", "bar")]) + assert output == expected + + def test_allow_null_presents_data(self): + model = OrderedDict( + [ + ("foo", fields.Raw), + ( + "fee", + fields.Nested( + OrderedDict([("fye", fields.String), ("blah", fields.String)]), + allow_null=True, + ), + ), + ] + ) + marshal_fields = OrderedDict( + [("foo", "bar"), ("bat", "baz"), ("fee", {"blah": "cool"})] + ) + output = marshal(marshal_fields, model) + expected = OrderedDict( + [("foo", "bar"), ("fee", OrderedDict([("fye", None), ("blah", "cool")]))] + ) + assert output == expected + + def test_skip_none_presents_data(self): + model = OrderedDict( + [ + ("foo", fields.Raw), + ( + "fee", + fields.Nested( + OrderedDict( + [ + ("fye", fields.String), + ("blah", fields.String), + ("foe", fields.String), + ] + ), + skip_none=True, + ), + ), + ] + ) + marshal_fields = OrderedDict( + [("foo", "bar"), ("bat", "baz"), ("fee", {"blah": "cool", "foe": None})] + ) + output = marshal(marshal_fields, model) + expected = OrderedDict( + [("foo", "bar"), ("fee", OrderedDict([("blah", "cool")]))] + ) + assert output == expected + + def test_marshal_nested_property(self): + class TestObject(object): + @property + def fee(self): + return {"blah": "cool"} + + model = OrderedDict( + [ + ("foo", fields.Raw), + ( + "fee", + fields.Nested( + OrderedDict([("fye", fields.String), ("blah", fields.String)]), + allow_null=True, + ), + ), + ] + ) + obj = TestObject() + obj.foo = "bar" + obj.bat = "baz" + output = marshal([obj], model) + expected = [ + OrderedDict( + [ + ("foo", "bar"), + ("fee", OrderedDict([("fye", None), ("blah", "cool")])), + ] + ) + ] + assert output == expected + + def test_marshal_nested_property_with_skip_none(self): + class TestObject(object): + @property + def fee(self): + return {"blah": "cool", "foe": None} + + model = OrderedDict( + [ + ("foo", fields.Raw), + ( + "fee", + fields.Nested( + OrderedDict( + [ + ("fye", fields.String), + ("blah", fields.String), + ("foe", fields.String), + ] + ), + skip_none=True, + ), + ), + ] + ) + obj = TestObject() + obj.foo = "bar" + obj.bat = "baz" + output = marshal([obj], model) + expected = [ + OrderedDict([("foo", "bar"), ("fee", OrderedDict([("blah", "cool")]))]) + ] + assert output == expected + + def test_marshal_list(self): + model = OrderedDict([("foo", fields.Raw), ("fee", fields.List(fields.String))]) + marshal_fields = OrderedDict( + [("foo", "bar"), ("bat", "baz"), ("fee", ["fye", "fum"])] + ) + output = marshal(marshal_fields, model) + expected = OrderedDict([("foo", "bar"), ("fee", (["fye", "fum"]))]) + assert output == expected + + def test_marshal_list_of_nesteds(self): + model = OrderedDict( + [ + ("foo", fields.Raw), + ("fee", fields.List(fields.Nested({"fye": fields.String}))), + ] + ) + marshal_fields = OrderedDict( + [("foo", "bar"), ("bat", "baz"), ("fee", {"fye": "fum"})] + ) + output = marshal(marshal_fields, model) + expected = OrderedDict( + [("foo", "bar"), ("fee", [OrderedDict([("fye", "fum")])])] + ) + assert output == expected + + def test_marshal_list_of_lists(self): + model = OrderedDict( + [("foo", fields.Raw), ("fee", fields.List(fields.List(fields.String)))] + ) + marshal_fields = OrderedDict( + [("foo", "bar"), ("bat", "baz"), ("fee", [["fye"], ["fum"]])] + ) + output = marshal(marshal_fields, model) + expected = OrderedDict([("foo", "bar"), ("fee", [["fye"], ["fum"]])]) + assert output == expected + + def test_marshal_nested_dict(self): + model = OrderedDict( + [ + ("foo", fields.Raw), + ( + "bar", + OrderedDict( + [ + ("a", fields.Raw), + ("b", fields.Raw), + ] + ), + ), + ] + ) + marshal_fields = OrderedDict( + [ + ("foo", "foo-val"), + ("bar", "bar-val"), + ("bat", "bat-val"), + ("a", 1), + ("b", 2), + ("c", 3), + ] + ) + output = marshal(marshal_fields, model) + expected = OrderedDict( + [("foo", "foo-val"), ("bar", OrderedDict([("a", 1), ("b", 2)]))] + ) + assert output == expected + + @pytest.mark.options(debug=True) + def test_will_prettyprint_json_in_debug_mode(self, app, client): + api = Api(app) + + class Foo1(Resource): + def get(self): + return {"foo": "bar", "baz": "asdf"} + + api.add_resource(Foo1, "/foo", endpoint="bar") + + foo = client.get("/foo") + + # Python's dictionaries have random order (as of "new" Pythons, + # anyway), so we can't verify the actual output here. We just + # assert that they're properly prettyprinted. + lines = foo.data.splitlines() + lines = [line.decode() for line in lines] + assert "{" == lines[0] + assert lines[1].startswith(" ") + assert lines[2].startswith(" ") + assert "}" == lines[3] + + # Assert our trailing newline. + assert foo.data.endswith(b"\n") + + def test_json_float_marshalled(self, app, client): + api = Api(app) + + class FooResource(Resource): + fields = {"foo": fields.Float} + + def get(self): + return marshal({"foo": 3.0}, self.fields) + + api.add_resource(FooResource, "/api") + + resp = client.get("/api") + assert resp.status_code == 200 + assert resp.data.decode("utf-8") == '{"foo": 3.0}\n' diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_model.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_model.py new file mode 100644 index 0000000..30219dd --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_model.py @@ -0,0 +1,695 @@ +import copy +import pytest + +from collections import OrderedDict + +from flask_restx import fields, Model, OrderedModel, SchemaModel + + +class ModelTest(object): + def test_model_as_flat_dict(self): + model = Model( + "Person", + { + "name": fields.String, + "age": fields.Integer, + "birthdate": fields.DateTime, + }, + ) + + assert isinstance(model, dict) + assert not isinstance(model, OrderedDict) + + assert model.__schema__ == { + "properties": { + "name": {"type": "string"}, + "age": {"type": "integer"}, + "birthdate": {"type": "string", "format": "date-time"}, + }, + "type": "object", + } + + def test_model_as_ordered_dict(self): + model = OrderedModel( + "Person", + [ + ("name", fields.String), + ("age", fields.Integer), + ("birthdate", fields.DateTime), + ], + ) + + assert isinstance(model, OrderedDict) + + assert model.__schema__ == { + "type": "object", + "properties": { + "name": {"type": "string"}, + "age": {"type": "integer"}, + "birthdate": {"type": "string", "format": "date-time"}, + }, + } + + def test_model_as_nested_dict(self): + address = Model( + "Address", + { + "road": fields.String, + }, + ) + + person = Model( + "Person", + { + "name": fields.String, + "age": fields.Integer, + "birthdate": fields.DateTime, + "address": fields.Nested(address), + }, + ) + + assert person.__schema__ == { + # 'required': ['address'], + "properties": { + "name": {"type": "string"}, + "age": {"type": "integer"}, + "birthdate": {"type": "string", "format": "date-time"}, + "address": { + "$ref": "#/definitions/Address", + }, + }, + "type": "object", + } + + assert address.__schema__ == { + "properties": { + "road": {"type": "string"}, + }, + "type": "object", + } + + def test_model_as_dict_with_list(self): + model = Model( + "Person", + { + "name": fields.String, + "age": fields.Integer, + "tags": fields.List(fields.String), + }, + ) + + assert model.__schema__ == { + "properties": { + "name": {"type": "string"}, + "age": {"type": "integer"}, + "tags": {"type": "array", "items": {"type": "string"}}, + }, + "type": "object", + } + + def test_model_as_nested_dict_with_list(self): + address = Model( + "Address", + { + "road": fields.String, + }, + ) + + person = Model( + "Person", + { + "name": fields.String, + "age": fields.Integer, + "birthdate": fields.DateTime, + "addresses": fields.List(fields.Nested(address)), + }, + ) + + assert person.__schema__ == { + # 'required': ['address'], + "properties": { + "name": {"type": "string"}, + "age": {"type": "integer"}, + "birthdate": {"type": "string", "format": "date-time"}, + "addresses": { + "type": "array", + "items": { + "$ref": "#/definitions/Address", + }, + }, + }, + "type": "object", + } + + assert address.__schema__ == { + "properties": { + "road": {"type": "string"}, + }, + "type": "object", + } + + def test_model_with_required(self): + model = Model( + "Person", + { + "name": fields.String(required=True), + "age": fields.Integer, + "birthdate": fields.DateTime(required=True), + }, + ) + + assert model.__schema__ == { + "properties": { + "name": {"type": "string"}, + "age": {"type": "integer"}, + "birthdate": {"type": "string", "format": "date-time"}, + }, + "required": ["birthdate", "name"], + "type": "object", + } + + def test_model_as_nested_dict_and_required(self): + address = Model( + "Address", + { + "road": fields.String, + }, + ) + + person = Model( + "Person", + { + "name": fields.String, + "age": fields.Integer, + "birthdate": fields.DateTime, + "address": fields.Nested(address, required=True), + }, + ) + + assert person.__schema__ == { + "required": ["address"], + "properties": { + "name": {"type": "string"}, + "age": {"type": "integer"}, + "birthdate": {"type": "string", "format": "date-time"}, + "address": { + "$ref": "#/definitions/Address", + }, + }, + "type": "object", + } + + assert address.__schema__ == { + "properties": { + "road": {"type": "string"}, + }, + "type": "object", + } + + def test_model_with_discriminator(self): + model = Model( + "Person", + { + "name": fields.String(discriminator=True), + "age": fields.Integer, + }, + ) + + assert model.__schema__ == { + "properties": { + "name": {"type": "string"}, + "age": {"type": "integer"}, + }, + "discriminator": "name", + "required": ["name"], + "type": "object", + } + + def test_model_with_discriminator_override_require(self): + model = Model( + "Person", + { + "name": fields.String(discriminator=True, required=False), + "age": fields.Integer, + }, + ) + + assert model.__schema__ == { + "properties": { + "name": {"type": "string"}, + "age": {"type": "integer"}, + }, + "discriminator": "name", + "required": ["name"], + "type": "object", + } + + def test_model_deepcopy(self): + parent = Model( + "Person", + { + "name": fields.String, + "age": fields.Integer(description="foo"), + }, + ) + + child = parent.inherit( + "Child", + { + "extra": fields.String, + }, + ) + + parent_copy = copy.deepcopy(parent) + + assert parent_copy["age"].description == "foo" + + parent_copy["age"].description = "bar" + + assert parent["age"].description == "foo" + assert parent_copy["age"].description == "bar" + + child = parent.inherit( + "Child", + { + "extra": fields.String, + }, + ) + + child_copy = copy.deepcopy(child) + assert child_copy.__parents__[0] == parent + + def test_clone_from_instance(self): + parent = Model( + "Parent", + { + "name": fields.String, + "age": fields.Integer, + "birthdate": fields.DateTime, + }, + ) + + child = parent.clone( + "Child", + { + "extra": fields.String, + }, + ) + + assert child.__schema__ == { + "properties": { + "name": {"type": "string"}, + "age": {"type": "integer"}, + "birthdate": {"type": "string", "format": "date-time"}, + "extra": {"type": "string"}, + }, + "type": "object", + } + + def test_clone_from_class(self): + parent = Model( + "Parent", + { + "name": fields.String, + "age": fields.Integer, + "birthdate": fields.DateTime, + }, + ) + + child = Model.clone( + "Child", + parent, + { + "extra": fields.String, + }, + ) + + assert child.__schema__ == { + "properties": { + "name": {"type": "string"}, + "age": {"type": "integer"}, + "birthdate": {"type": "string", "format": "date-time"}, + "extra": {"type": "string"}, + }, + "type": "object", + } + + def test_clone_from_instance_with_multiple_parents(self): + grand_parent = Model( + "GrandParent", + { + "grand_parent": fields.String, + }, + ) + + parent = Model( + "Parent", + { + "name": fields.String, + "age": fields.Integer, + "birthdate": fields.DateTime, + }, + ) + + child = grand_parent.clone( + "Child", + parent, + { + "extra": fields.String, + }, + ) + + assert child.__schema__ == { + "properties": { + "grand_parent": {"type": "string"}, + "name": {"type": "string"}, + "age": {"type": "integer"}, + "birthdate": {"type": "string", "format": "date-time"}, + "extra": {"type": "string"}, + }, + "type": "object", + } + + def test_clone_from_class_with_multiple_parents(self): + grand_parent = Model( + "GrandParent", + { + "grand_parent": fields.String, + }, + ) + + parent = Model( + "Parent", + { + "name": fields.String, + "age": fields.Integer, + "birthdate": fields.DateTime, + }, + ) + + child = Model.clone( + "Child", + grand_parent, + parent, + { + "extra": fields.String, + }, + ) + + assert child.__schema__ == { + "properties": { + "grand_parent": {"type": "string"}, + "name": {"type": "string"}, + "age": {"type": "integer"}, + "birthdate": {"type": "string", "format": "date-time"}, + "extra": {"type": "string"}, + }, + "type": "object", + } + + def test_inherit_from_instance(self): + parent = Model( + "Parent", + { + "name": fields.String, + "age": fields.Integer, + }, + ) + + child = parent.inherit( + "Child", + { + "extra": fields.String, + }, + ) + + assert parent.__schema__ == { + "properties": { + "name": {"type": "string"}, + "age": {"type": "integer"}, + }, + "type": "object", + } + assert child.__schema__ == { + "allOf": [ + {"$ref": "#/definitions/Parent"}, + {"properties": {"extra": {"type": "string"}}, "type": "object"}, + ] + } + + def test_inherit_from_class(self): + parent = Model( + "Parent", + { + "name": fields.String, + "age": fields.Integer, + }, + ) + + child = Model.inherit( + "Child", + parent, + { + "extra": fields.String, + }, + ) + + assert parent.__schema__ == { + "properties": { + "name": {"type": "string"}, + "age": {"type": "integer"}, + }, + "type": "object", + } + assert child.__schema__ == { + "allOf": [ + {"$ref": "#/definitions/Parent"}, + {"properties": {"extra": {"type": "string"}}, "type": "object"}, + ] + } + + def test_inherit_from_class_from_multiple_parents(self): + grand_parent = Model( + "GrandParent", + { + "grand_parent": fields.String, + }, + ) + + parent = Model( + "Parent", + { + "name": fields.String, + "age": fields.Integer, + }, + ) + + child = Model.inherit( + "Child", + grand_parent, + parent, + { + "extra": fields.String, + }, + ) + + assert child.__schema__ == { + "allOf": [ + {"$ref": "#/definitions/GrandParent"}, + {"$ref": "#/definitions/Parent"}, + {"properties": {"extra": {"type": "string"}}, "type": "object"}, + ] + } + + def test_inherit_from_instance_from_multiple_parents(self): + grand_parent = Model( + "GrandParent", + { + "grand_parent": fields.String, + }, + ) + + parent = Model( + "Parent", + { + "name": fields.String, + "age": fields.Integer, + }, + ) + + child = grand_parent.inherit( + "Child", + parent, + { + "extra": fields.String, + }, + ) + + assert child.__schema__ == { + "allOf": [ + {"$ref": "#/definitions/GrandParent"}, + {"$ref": "#/definitions/Parent"}, + {"properties": {"extra": {"type": "string"}}, "type": "object"}, + ] + } + + # def test_inherit_inline(self): + # parent = Model('Person', { + # 'name': fields.String, + # 'age': fields.Integer, + # }) + # + # child = self.api.inherit('Child', parent, { + # 'extra': fields.String, + # }) + # + # Model('Output', { + # 'child': fields.Nested(child), + # 'children': fields.List(fields.Nested(child)) + # }) + # + # self.assertIn('Person', Models) + # self.assertIn('Child', Models) + + def test_polymorph_inherit_common_ancestor(self): + class Child1: + pass + + class Child2: + pass + + parent = Model( + "Person", + { + "name": fields.String, + "age": fields.Integer, + }, + ) + + child1 = parent.inherit( + "Child1", + { + "extra1": fields.String, + }, + ) + + child2 = parent.inherit( + "Child2", + { + "extra2": fields.String, + }, + ) + + mapping = { + Child1: child1, + Child2: child2, + } + + output = Model("Output", {"child": fields.Polymorph(mapping)}) + + # Should use the common ancestor + assert output.__schema__ == { + "properties": { + "child": {"$ref": "#/definitions/Person"}, + }, + "type": "object", + } + + def test_validate(self): + from jsonschema import FormatChecker + from werkzeug.exceptions import BadRequest + + class IPAddress(fields.Raw): + __schema_type__ = "string" + __schema_format__ = "ipv4" + + data = {"ip": "192.168.1"} + model = Model("MyModel", {"ip": IPAddress()}) + + # Test that validate without a FormatChecker does not check if a + # primitive type conforms to the defined format property + assert model.validate(data) is None + + # Test that validate with a FormatChecker enforces the check of the + # format property and throws an error if invalid + with pytest.raises(BadRequest): + model.validate(data, format_checker=FormatChecker()) + + +class ModelSchemaTestCase(object): + def test_model_schema(self): + address = SchemaModel( + "Address", + { + "properties": { + "road": {"type": "string"}, + }, + "type": "object", + }, + ) + + person = SchemaModel( + "Person", + { + # 'required': ['address'], + "properties": { + "name": {"type": "string"}, + "age": {"type": "integer"}, + "birthdate": {"type": "string", "format": "date-time"}, + "address": { + "$ref": "#/definitions/Address", + }, + }, + "type": "object", + }, + ) + + assert person.__schema__ == { + # 'required': ['address'], + "properties": { + "name": {"type": "string"}, + "age": {"type": "integer"}, + "birthdate": {"type": "string", "format": "date-time"}, + "address": { + "$ref": "#/definitions/Address", + }, + }, + "type": "object", + } + + assert address.__schema__ == { + "properties": { + "road": {"type": "string"}, + }, + "type": "object", + } + + +class ModelDeprecattionsTest(object): + def test_extend_is_deprecated(self): + parent = Model( + "Parent", + { + "name": fields.String, + "age": fields.Integer, + "birthdate": fields.DateTime, + }, + ) + + with pytest.warns(DeprecationWarning): + child = parent.extend( + "Child", + { + "extra": fields.String, + }, + ) + + assert child.__schema__ == { + "properties": { + "name": {"type": "string"}, + "age": {"type": "integer"}, + "birthdate": {"type": "string", "format": "date-time"}, + "extra": {"type": "string"}, + }, + "type": "object", + } diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_namespace.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_namespace.py new file mode 100644 index 0000000..dfc5106 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_namespace.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +import re + +import flask_restx as restx + +from flask_restx import Namespace, Model, OrderedModel + + +class NamespaceTest(object): + def test_parser(self): + api = Namespace("test") + assert isinstance(api.parser(), restx.reqparse.RequestParser) + + def test_doc_decorator(self): + api = Namespace("test") + params = {"q": {"description": "some description"}} + + @api.doc(params=params) + class TestResource(restx.Resource): + pass + + assert hasattr(TestResource, "__apidoc__") + assert TestResource.__apidoc__ == {"params": params} + + def test_doc_with_inheritance(self): + api = Namespace("test") + base_params = { + "q": { + "description": "some description", + "type": "string", + "paramType": "query", + } + } + child_params = { + "q": {"description": "some new description"}, + "other": {"description": "another param"}, + } + + @api.doc(params=base_params) + class BaseResource(restx.Resource): + pass + + @api.doc(params=child_params) + class TestResource(BaseResource): + pass + + assert TestResource.__apidoc__ == { + "params": { + "q": { + "description": "some new description", + "type": "string", + "paramType": "query", + }, + "other": {"description": "another param"}, + } + } + + def test_model(self): + api = Namespace("test") + api.model("Person", {}) + assert "Person" in api.models + assert isinstance(api.models["Person"], Model) + + def test_ordered_model(self): + api = Namespace("test", ordered=True) + api.model("Person", {}) + assert "Person" in api.models + assert isinstance(api.models["Person"], OrderedModel) + + def test_schema_model(self): + api = Namespace("test") + api.schema_model("Person", {}) + assert "Person" in api.models + + def test_clone(self): + api = Namespace("test") + parent = api.model("Parent", {}) + api.clone("Child", parent, {}) + + assert "Child" in api.models + assert "Parent" in api.models + + def test_clone_with_multiple_parents(self): + api = Namespace("test") + grand_parent = api.model("GrandParent", {}) + parent = api.model("Parent", {}) + api.clone("Child", grand_parent, parent, {}) + + assert "Child" in api.models + assert "Parent" in api.models + assert "GrandParent" in api.models + + def test_inherit(self): + authorizations = { + "apikey": {"type": "apiKey", "in": "header", "name": "X-API-KEY"} + } + api = Namespace("test", authorizations=authorizations) + parent = api.model("Parent", {}) + api.inherit("Child", parent, {}) + + assert "Parent" in api.models + assert "Child" in api.models + assert api.authorizations == authorizations + + def test_inherit_from_multiple_parents(self): + api = Namespace("test") + grand_parent = api.model("GrandParent", {}) + parent = api.model("Parent", {}) + api.inherit("Child", grand_parent, parent, {}) + + assert "GrandParent" in api.models + assert "Parent" in api.models + assert "Child" in api.models + + def test_api_payload(self, app, client): + api = restx.Api(app, validate=True) + ns = restx.Namespace("apples") + api.add_namespace(ns) + + fields = ns.model( + "Person", + { + "name": restx.fields.String(required=True), + "age": restx.fields.Integer, + "birthdate": restx.fields.DateTime, + }, + ) + + @ns.route("/validation/") + class Payload(restx.Resource): + payload = None + + @ns.expect(fields) + def post(self): + Payload.payload = ns.payload + return {} + + data = { + "name": "John Doe", + "age": 15, + } + + client.post_json("/apples/validation/", data) + + assert Payload.payload == data + + def test_api_payload_strict_verification(self, app, client): + api = restx.Api(app, validate=True) + ns = restx.Namespace("apples") + api.add_namespace(ns) + + fields = ns.model( + "Person", + { + "name": restx.fields.String(required=True), + "age": restx.fields.Integer, + "birthdate": restx.fields.DateTime, + }, + strict=True, + ) + + @ns.route("/validation/") + class Payload(restx.Resource): + payload = None + + @ns.expect(fields) + def post(self): + Payload.payload = ns.payload + return {} + + data = { + "name": "John Doe", + "agge": 15, # typo + } + + resp = client.post_json("/apples/validation/", data, status=400) + assert re.match( + "Additional properties are not allowed \(u*'agge' was unexpected\)", + resp["errors"][""], + ) diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_payload.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_payload.py new file mode 100644 index 0000000..b89a951 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_payload.py @@ -0,0 +1,377 @@ +import flask_restx as restx + + +class PayloadTest(object): + def assert_errors(self, client, url, data, *errors): + out = client.post_json(url, data, status=400) + assert "message" in out + assert "errors" in out + for error in errors: + assert error in out["errors"] + + def test_validation_false_on_constructor(self, app, client): + api = restx.Api(app, validate=False) + + fields = api.model( + "Person", + { + "name": restx.fields.String(required=True), + "age": restx.fields.Integer, + "birthdate": restx.fields.DateTime, + }, + ) + + @api.route("/validation/") + class ValidationOff(restx.Resource): + @api.expect(fields) + def post(self): + return {} + + data = client.post_json("/validation/", {}) + assert data == {} + + def test_validation_false_on_constructor_with_override(self, app, client): + api = restx.Api(app, validate=False) + + fields = api.model( + "Person", + { + "name": restx.fields.String(required=True), + "age": restx.fields.Integer, + "birthdate": restx.fields.DateTime, + }, + ) + + @api.route("/validation/") + class ValidationOn(restx.Resource): + @api.expect(fields, validate=True) + def post(self): + return {} + + self.assert_errors(client, "/validation/", {}, "name") + + def test_validation_true_on_constructor(self, app, client): + api = restx.Api(app, validate=True) + + fields = api.model( + "Person", + { + "name": restx.fields.String(required=True), + "age": restx.fields.Integer, + "birthdate": restx.fields.DateTime, + }, + ) + + @api.route("/validation/") + class ValidationOff(restx.Resource): + @api.expect(fields) + def post(self): + return {} + + self.assert_errors(client, "/validation/", {}, "name") + + def test_validation_true_on_constructor_with_override(self, app, client): + api = restx.Api(app, validate=True) + + fields = api.model( + "Person", + { + "name": restx.fields.String(required=True), + "age": restx.fields.Integer, + "birthdate": restx.fields.DateTime, + }, + ) + + @api.route("/validation/") + class ValidationOff(restx.Resource): + @api.expect(fields, validate=False) + def post(self): + return {} + + data = client.post_json("/validation/", {}) + assert data == {} + + def _setup_api_format_checker_tests(self, app, format_checker=None): + class IPAddress(restx.fields.Raw): + __schema_type__ = "string" + __schema_format__ = "ipv4" + + api = restx.Api(app, format_checker=format_checker) + model = api.model("MyModel", {"ip": IPAddress(required=True)}) + + @api.route("/format_checker/") + class TestResource(restx.Resource): + @api.expect(model, validate=True) + def post(self): + return {} + + def test_format_checker_none_on_constructor(self, app, client): + self._setup_api_format_checker_tests(app) + + out = client.post_json("/format_checker/", {"ip": "192.168.1"}) + assert out == {} + + def test_format_checker_object_on_constructor(self, app, client): + from jsonschema import FormatChecker + + self._setup_api_format_checker_tests(app, format_checker=FormatChecker()) + + out = client.post_json("/format_checker/", {"ip": "192.168.1"}, status=400) + assert "ipv4" in out["errors"]["ip"] + + def test_validation_false_in_config(self, app, client): + app.config["RESTX_VALIDATE"] = False + api = restx.Api(app) + + fields = api.model( + "Person", + { + "name": restx.fields.String(required=True), + "age": restx.fields.Integer, + "birthdate": restx.fields.DateTime, + }, + ) + + @api.route("/validation/") + class ValidationOff(restx.Resource): + @api.expect(fields) + def post(self): + return {} + + out = client.post_json("/validation/", {}) + + # assert response.status_code == 200 + # out = json.loads(response.data.decode('utf8')) + assert out == {} + + def test_validation_in_config(self, app, client): + app.config["RESTX_VALIDATE"] = True + api = restx.Api(app) + + fields = api.model( + "Person", + { + "name": restx.fields.String(required=True), + "age": restx.fields.Integer, + "birthdate": restx.fields.DateTime, + }, + ) + + @api.route("/validation/") + class ValidationOn(restx.Resource): + @api.expect(fields) + def post(self): + return {} + + self.assert_errors(client, "/validation/", {}, "name") + + def test_api_payload(self, app, client): + api = restx.Api(app, validate=True) + + fields = api.model( + "Person", + { + "name": restx.fields.String(required=True), + "age": restx.fields.Integer, + "birthdate": restx.fields.DateTime, + }, + ) + + @api.route("/validation/") + class Payload(restx.Resource): + payload = None + + @api.expect(fields) + def post(self): + Payload.payload = api.payload + return {} + + data = { + "name": "John Doe", + "age": 15, + } + + client.post_json("/validation/", data) + + assert Payload.payload == data + + def test_validation_with_inheritance(self, app, client): + """It should perform validation with inheritance (allOf/$ref)""" + api = restx.Api(app, validate=True) + + fields = api.model( + "Parent", + { + "name": restx.fields.String(required=True), + }, + ) + + child_fields = api.inherit( + "Child", + fields, + { + "age": restx.fields.Integer, + }, + ) + + @api.route("/validation/") + class Inheritance(restx.Resource): + @api.expect(child_fields) + def post(self): + return {} + + client.post_json( + "/validation/", + { + "name": "John Doe", + "age": 15, + }, + ) + + self.assert_errors( + client, + "/validation/", + { + "age": "15", + }, + "name", + "age", + ) + + def test_validation_on_list(self, app, client): + """It should perform validation on lists""" + api = restx.Api(app, validate=True) + + person = api.model( + "Person", + { + "name": restx.fields.String(required=True), + "age": restx.fields.Integer(required=True), + }, + ) + + family = api.model( + "Family", + { + "name": restx.fields.String(required=True), + "members": restx.fields.List(restx.fields.Nested(person)), + }, + ) + + @api.route("/validation/") + class List(restx.Resource): + @api.expect(family) + def post(self): + return {} + + self.assert_errors( + client, + "/validation/", + {"name": "Doe", "members": [{"name": "Jonn"}, {"age": 42}]}, + "members.0.age", + "members.1.name", + ) + + def _setup_expect_validation_single_resource_tests(self, app): + # Setup a minimal Api with endpoint that expects in input payload + # a single object of a resource + api = restx.Api(app, validate=True) + + user = api.model("User", {"username": restx.fields.String()}) + + @api.route("/validation/") + class Users(restx.Resource): + @api.expect(user) + def post(self): + return {} + + def _setup_expect_validation_collection_resource_tests(self, app): + # Setup a minimal Api with endpoint that expects in input payload + # one or more objects of a resource + api = restx.Api(app, validate=True) + + user = api.model("User", {"username": restx.fields.String()}) + + @api.route("/validation/") + class Users(restx.Resource): + @api.expect([user]) + def post(self): + return {} + + def test_expect_validation_single_resource_success(self, app, client): + self._setup_expect_validation_single_resource_tests(app) + + # Input payload is a valid JSON object + out = client.post_json("/validation/", {"username": "alice"}) + assert {} == out + + def test_expect_validation_single_resource_error(self, app, client): + self._setup_expect_validation_single_resource_tests(app) + + # Input payload is an invalid JSON object + self.assert_errors(client, "/validation/", {"username": 123}, "username") + + # Input payload is a JSON array (expected JSON object) + self.assert_errors(client, "/validation/", [{"username": 123}], "") + + def test_expect_validation_collection_resource_success(self, app, client): + self._setup_expect_validation_collection_resource_tests(app) + + # Input payload is a valid JSON object + out = client.post_json("/validation/", {"username": "alice"}) + assert {} == out + + # Input payload is a JSON array with valid JSON objects + out = client.post_json( + "/validation/", [{"username": "alice"}, {"username": "bob"}] + ) + assert {} == out + + def test_expect_validation_collection_resource_error(self, app, client): + self._setup_expect_validation_collection_resource_tests(app) + + # Input payload is an invalid JSON object + self.assert_errors(client, "/validation/", {"username": 123}, "username") + + # Input payload is a JSON array but with an invalid JSON object + self.assert_errors( + client, + "/validation/", + [{"username": "alice"}, {"username": 123}], + "username", + ) + + def test_validation_with_propagate(self, app, client): + app.config["PROPAGATE_EXCEPTIONS"] = True + api = restx.Api(app, validate=True) + + fields = api.model( + "Person", + { + "name": restx.fields.String(required=True), + "age": restx.fields.Integer, + "birthdate": restx.fields.DateTime, + }, + ) + + @api.route("/validation/") + class ValidationOff(restx.Resource): + @api.expect(fields) + def post(self): + return {} + + self.assert_errors(client, "/validation/", {}, "name") + + def test_empty_payload(self, app, client): + api = restx.Api(app, validate=True) + + @api.route("/empty/") + class Payload(restx.Resource): + def post(self): + return {} + + response = client.post( + "/empty/", data="", headers={"content-type": "application/json"} + ) + + assert response.status_code == 200 diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_postman.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_postman.py new file mode 100644 index 0000000..0317bf6 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_postman.py @@ -0,0 +1,410 @@ +import json + +from os.path import join, dirname + +from jsonschema import validate +from werkzeug.datastructures import FileStorage + +import flask_restx as restx + +from urllib.parse import parse_qs, urlparse + + +with open(join(dirname(__file__), "postman-v1.schema.json")) as f: + schema = json.load(f) + + +class PostmanTest(object): + def test_basic_export(self, app): + api = restx.Api(app) + + data = api.as_postman() + + validate(data, schema) + + assert len(data["requests"]) == 0 + + def test_export_infos(self, app): + api = restx.Api( + app, + version="1.0", + title="My API", + description="This is a testing API", + ) + + data = api.as_postman() + + validate(data, schema) + + assert data["name"] == "My API 1.0" + assert data["description"] == "This is a testing API" + + def test_export_with_one_entry(self, app): + api = restx.Api(app) + + @api.route("/test") + class Test(restx.Resource): + @api.doc("test_post") + def post(self): + """A test post""" + pass + + data = api.as_postman() + + validate(data, schema) + + assert len(data["requests"]) == 1 + request = data["requests"][0] + assert request["name"] == "test_post" + assert request["description"] == "A test post" + + assert len(data["folders"]) == 1 + folder = data["folders"][0] + assert folder["name"] == "default" + assert folder["description"] == "Default namespace" + + assert request["folder"] == folder["id"] + + def test_export_with_namespace(self, app): + api = restx.Api(app) + ns = api.namespace("test", "A test namespace") + + @ns.route("/test") + class Test(restx.Resource): + @api.doc("test_post") + def post(self): + """A test post""" + pass + + data = api.as_postman() + + validate(data, schema) + + assert len(data["requests"]) == 1 + request = data["requests"][0] + assert request["name"] == "test_post" + assert request["description"] == "A test post" + + assert len(data["folders"]) == 1 + folder = data["folders"][0] + assert folder["name"] == "test" + assert folder["description"] == "A test namespace" + + assert request["folder"] == folder["id"] + + def test_id_is_the_same(self, app): + api = restx.Api(app) + + first = api.as_postman() + + second = api.as_postman() + + assert first["id"] == second["id"] + + def test_resources_order_in_folder(self, app): + """It should preserve resources order""" + api = restx.Api(app) + ns = api.namespace("test", "A test namespace") + + @ns.route("/test1") + class Test1(restx.Resource): + @api.doc("test_post_z") + def post(self): + pass + + @ns.route("/test2") + class Test2(restx.Resource): + @api.doc("test_post_y") + def post(self): + pass + + @ns.route("/test3") + class Test3(restx.Resource): + @api.doc("test_post_x") + def post(self): + pass + + data = api.as_postman() + + validate(data, schema) + + assert len(data["requests"]) == 3 + + assert len(data["folders"]) == 1 + folder = data["folders"][0] + assert folder["name"] == "test" + + expected_order = ("test_post_z", "test_post_y", "test_post_x") + assert len(folder["order"]) == len(expected_order) + + for request_id, expected in zip(folder["order"], expected_order): + request = list(filter(lambda r: r["id"] == request_id, data["requests"]))[0] + assert request["name"] == expected + + def test_prefix_with_trailing_slash(self, app): + api = restx.Api(app, prefix="/prefix/") + + @api.route("/test/") + class Test(restx.Resource): + @api.doc("test_post") + def post(self): + pass + + data = api.as_postman() + + validate(data, schema) + + assert len(data["requests"]) == 1 + request = data["requests"][0] + assert request["url"] == "http://localhost/prefix/test/" + + def test_prefix_without_trailing_slash(self, app): + api = restx.Api(app, prefix="/prefix") + + @api.route("/test/") + class Test(restx.Resource): + @api.doc("test_post") + def post(self): + pass + + data = api.as_postman() + + validate(data, schema) + + assert len(data["requests"]) == 1 + request = data["requests"][0] + assert request["url"] == "http://localhost/prefix/test/" + + def test_path_variables(self, app): + api = restx.Api(app) + + @api.route("/test////") + class Test(restx.Resource): + @api.doc("test_post") + def post(self): + pass + + data = api.as_postman() + + validate(data, schema) + + assert len(data["requests"]) == 1 + request = data["requests"][0] + assert request["url"] == "http://localhost/test/:id/:integer/:number/" + assert request["pathVariables"] == { + "id": "", + "integer": 0, + "number": 0, + } + + def test_url_variables_disabled(self, app): + api = restx.Api(app) + + parser = api.parser() + parser.add_argument("int", type=int) + parser.add_argument("default", type=int, default=5) + parser.add_argument("str", type=str) + + @api.route("/test/") + class Test(restx.Resource): + @api.expect(parser) + def get(self): + pass + + data = api.as_postman() + + validate(data, schema) + + assert len(data["requests"]) == 1 + request = data["requests"][0] + assert request["url"] == "http://localhost/test/" + + def test_url_variables_enabled(self, app): + api = restx.Api(app) + + parser = api.parser() + parser.add_argument("int", type=int) + parser.add_argument("default", type=int, default=5) + parser.add_argument("str", type=str) + + @api.route("/test/") + class Test(restx.Resource): + @api.expect(parser) + def get(self): + pass + + data = api.as_postman(urlvars=True) + + validate(data, schema) + + assert len(data["requests"]) == 1 + request = data["requests"][0] + qs = parse_qs(urlparse(request["url"]).query, keep_blank_values=True) + + assert "int" in qs + assert qs["int"][0] == "0" + + assert "default" in qs + assert qs["default"][0] == "5" + + assert "str" in qs + assert qs["str"][0] == "" + + def test_headers(self, app): + api = restx.Api(app) + + parser = api.parser() + parser.add_argument("X-Header-1", location="headers", default="xxx") + parser.add_argument("X-Header-2", location="headers", required=True) + + @api.route("/headers/") + class TestHeaders(restx.Resource): + @api.doc("headers") + @api.expect(parser) + def get(self): + pass + + data = api.as_postman(urlvars=True) + + validate(data, schema) + request = data["requests"][0] + headers = dict(r.split(":") for r in request["headers"].splitlines()) + + assert headers["X-Header-1"] == "xxx" + assert headers["X-Header-2"] == "" + + def test_content_type_header(self, app): + api = restx.Api(app) + form_parser = api.parser() + form_parser.add_argument("param", type=int, help="Some param", location="form") + + file_parser = api.parser() + file_parser.add_argument("in_files", type=FileStorage, location="files") + + @api.route("/json/") + class TestJson(restx.Resource): + @api.doc("json") + def post(self): + pass + + @api.route("/form/") + class TestForm(restx.Resource): + @api.doc("form") + @api.expect(form_parser) + def post(self): + pass + + @api.route("/file/") + class TestFile(restx.Resource): + @api.doc("file") + @api.expect(file_parser) + def post(self): + pass + + @api.route("/get/") + class TestGet(restx.Resource): + @api.doc("get") + def get(self): + pass + + data = api.as_postman(urlvars=True) + + validate(data, schema) + requests = dict((r["name"], r["headers"]) for r in data["requests"]) + + assert requests["json"] == "Content-Type:application/json" + assert requests["form"] == "Content-Type:multipart/form-data" + assert requests["file"] == "Content-Type:multipart/form-data" + + # No content-type on get + assert requests["get"] == "" + + def test_method_security_headers(self, app): + api = restx.Api( + app, + authorizations={ + "apikey": {"type": "apiKey", "in": "header", "name": "X-API"} + }, + ) + + @api.route("/secure/") + class Secure(restx.Resource): + @api.doc("secure", security="apikey") + def get(self): + pass + + @api.route("/unsecure/") + class Unsecure(restx.Resource): + @api.doc("unsecure") + def get(self): + pass + + data = api.as_postman() + + validate(data, schema) + requests = dict((r["name"], r["headers"]) for r in data["requests"]) + + assert requests["unsecure"] == "" + assert requests["secure"] == "X-API:" + + def test_global_security_headers(self, app): + api = restx.Api( + app, + security="apikey", + authorizations={ + "apikey": {"type": "apiKey", "in": "header", "name": "X-API"} + }, + ) + + @api.route("/test/") + class Test(restx.Resource): + def get(self): + pass + + data = api.as_postman() + + validate(data, schema) + request = data["requests"][0] + headers = dict(r.split(":") for r in request["headers"].splitlines()) + + assert headers["X-API"] == "" + + def test_oauth_security_headers(self, app): + api = restx.Api( + app, + security="oauth", + authorizations={ + "oauth": { + "type": "oauth2", + "authorizationUrl": "https://somewhere.com/oauth/authorize", + "flow": "implicit", + "scopes": {"read": "Can read", "write": "Can write"}, + } + }, + ) + + @api.route("/test/") + class Test(restx.Resource): + def get(self): + pass + + data = api.as_postman() + + validate(data, schema) + # request = data['requests'][0] + # headers = dict(r.split(':') for r in request['headers'].splitlines()) + # + # assert headers['X-API'] == '' + + def test_export_with_swagger(self, app): + api = restx.Api(app) + + data = api.as_postman(swagger=True) + + validate(data, schema) + + assert len(data["requests"]) == 1 + request = data["requests"][0] + assert request["name"] == "Swagger specifications" + assert request["description"] == "The API Swagger specifications as JSON" + assert request["url"] == "http://localhost/swagger.json" diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_reqparse.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_reqparse.py new file mode 100644 index 0000000..2b319ed --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_reqparse.py @@ -0,0 +1,1122 @@ +import decimal +import json +import io +import pytest + +from werkzeug.exceptions import BadRequest +from werkzeug.wrappers import Request +from werkzeug.datastructures import FileStorage, MultiDict + +from flask_restx import Api, Model, fields, inputs +from flask_restx.errors import SpecsError +from flask_restx.reqparse import Argument, RequestParser, ParseResult + + +class ReqParseTest(object): + def test_api_shortcut(self, app): + api = Api(app) + parser = api.parser() + assert isinstance(parser, RequestParser) + + def test_parse_model(self, app): + model = Model("Todo", {"task": fields.String(required=True)}) + + parser = RequestParser() + parser.add_argument("todo", type=model, required=True) + + data = {"todo": {"task": "aaa"}} + + with app.test_request_context( + "/", method="post", data=json.dumps(data), content_type="application/json" + ): + args = parser.parse_args() + assert args["todo"] == {"task": "aaa"} + + def test_help(self, app, mocker): + abort = mocker.patch( + "flask_restx.reqparse.abort", side_effect=BadRequest("Bad Request") + ) + parser = RequestParser() + parser.add_argument("foo", choices=("one", "two"), help="Bad choice.") + req = mocker.Mock(["values", "get_json"]) + req.values = MultiDict([("foo", "three")]) + req.get_json.return_value = None + with pytest.raises(BadRequest): + parser.parse_args(req) + expected = { + "foo": "Bad choice. The value 'three' is not a valid choice for 'foo'." + } + abort.assert_called_with( + 400, "Input payload validation failed", errors=expected + ) + + def test_no_help(self, app, mocker): + abort = mocker.patch( + "flask_restx.reqparse.abort", side_effect=BadRequest("Bad Request") + ) + parser = RequestParser() + parser.add_argument("foo", choices=["one", "two"]) + req = mocker.Mock(["values", "get_json"]) + req.get_json.return_value = None + req.values = MultiDict([("foo", "three")]) + with pytest.raises(BadRequest): + parser.parse_args(req) + expected = {"foo": "The value 'three' is not a valid choice for 'foo'."} + abort.assert_called_with( + 400, "Input payload validation failed", errors=expected + ) + + @pytest.mark.request_context() + def test_viewargs(self, mocker): + req = Request.from_values() + req.view_args = {"foo": "bar"} + parser = RequestParser() + parser.add_argument("foo", location=["view_args"]) + args = parser.parse_args(req) + assert args["foo"] == "bar" + + req = mocker.Mock(["get_json"]) + req.values = () + req.get_json.return_value = None + req.view_args = {"foo": "bar"} + parser = RequestParser() + parser.add_argument("foo", store_missing=True) + args = parser.parse_args(req) + assert args["foo"] is None + + def test_parse_unicode(self, app): + req = Request.from_values("/bubble?foo=barß") + parser = RequestParser() + parser.add_argument("foo") + + args = parser.parse_args(req) + assert args["foo"] == "barß" + + def test_parse_unicode_app(self, app): + parser = RequestParser() + parser.add_argument("foo") + + with app.test_request_context("/bubble?foo=barß"): + args = parser.parse_args() + assert args["foo"] == "barß" + + @pytest.mark.request_context( + "/bubble", method="post", content_type="application/json" + ) + def test_json_location(self): + parser = RequestParser() + parser.add_argument("foo", location="json", store_missing=True) + args = parser.parse_args() + assert args["foo"] is None + + @pytest.mark.request_context( + "/bubble", + method="post", + data=json.dumps({"foo": "bar"}), + content_type="application/json", + ) + def test_get_json_location(self): + parser = RequestParser() + parser.add_argument("foo", location="json") + args = parser.parse_args() + assert args["foo"] == "bar" + + @pytest.mark.request_context("/bubble?foo=bar") + def test_parse_append_ignore(self, app): + parser = RequestParser() + parser.add_argument( + "foo", ignore=True, type=int, action="append", store_missing=True + ), + + args = parser.parse_args() + assert args["foo"] is None + + @pytest.mark.request_context("/bubble?") + def test_parse_append_default(self): + parser = RequestParser() + parser.add_argument("foo", action="append", store_missing=True), + + args = parser.parse_args() + assert args["foo"] is None + + @pytest.mark.request_context("/bubble?foo=bar&foo=bat") + def test_parse_append(self): + parser = RequestParser() + parser.add_argument("foo", action="append"), + + args = parser.parse_args() + assert args["foo"] == ["bar", "bat"] + + @pytest.mark.request_context("/bubble?foo=bar") + def test_parse_append_single(self): + parser = RequestParser() + parser.add_argument("foo", action="append"), + + args = parser.parse_args() + assert args["foo"] == ["bar"] + + @pytest.mark.request_context("/bubble?foo=bar") + def test_split_single(self): + parser = RequestParser() + parser.add_argument("foo", action="split"), + + args = parser.parse_args() + assert args["foo"] == ["bar"] + + @pytest.mark.request_context("/bubble?foo=bar,bat") + def test_split_multiple(self): + parser = RequestParser() + parser.add_argument("foo", action="split"), + + args = parser.parse_args() + assert args["foo"] == ["bar", "bat"] + + @pytest.mark.request_context("/bubble?foo=1,2,3") + def test_split_multiple_cast(self): + parser = RequestParser() + parser.add_argument("foo", type=int, action="split") + + args = parser.parse_args() + assert args["foo"] == [1, 2, 3] + + @pytest.mark.request_context("/bubble?foo=bar") + def test_parse_dest(self): + parser = RequestParser() + parser.add_argument("foo", dest="bat") + + args = parser.parse_args() + assert args["bat"] == "bar" + + @pytest.mark.request_context("/bubble?foo>=bar&foo<=bat&foo=foo") + def test_parse_gte_lte_eq(self): + parser = RequestParser() + parser.add_argument("foo", operators=[">=", "<=", "="], action="append"), + + args = parser.parse_args() + assert args["foo"] == ["bar", "bat", "foo"] + + @pytest.mark.request_context("/bubble?foo>=bar") + def test_parse_gte(self): + parser = RequestParser() + parser.add_argument("foo", operators=[">="]) + + args = parser.parse_args() + assert args["foo"] == "bar" + + @pytest.mark.request_context("/bubble?foo=bar") + def test_parse_foo_operators_four_hunderd(self): + parser = RequestParser() + parser.add_argument("foo", type=int), + with pytest.raises(BadRequest): + parser.parse_args() + + @pytest.mark.request_context("/bubble") + def test_parse_foo_operators_ignore(self): + parser = RequestParser() + parser.add_argument("foo", ignore=True, store_missing=True) + + args = parser.parse_args() + assert args["foo"] is None + + @pytest.mark.request_context("/bubble?foo<=bar") + def test_parse_lte_gte_mock(self, mocker): + mock_type = mocker.Mock() + + parser = RequestParser() + parser.add_argument("foo", type=mock_type, operators=["<="]) + + parser.parse_args() + mock_type.assert_called_with("bar", "foo", "<=") + + @pytest.mark.request_context("/bubble?foo<=bar") + def test_parse_lte_gte_append(self): + parser = RequestParser() + parser.add_argument("foo", operators=["<=", "="], action="append") + + args = parser.parse_args() + assert args["foo"] == ["bar"] + + @pytest.mark.request_context("/bubble?foo<=bar") + def test_parse_lte_gte_missing(self): + parser = RequestParser() + parser.add_argument("foo", operators=["<=", "="]) + args = parser.parse_args() + assert args["foo"] == "bar" + + @pytest.mark.request_context("/bubble?foo=bar&foo=bat") + def test_parse_eq_other(self): + parser = RequestParser() + parser.add_argument("foo"), + args = parser.parse_args() + assert args["foo"] == "bar" + + @pytest.mark.request_context("/bubble?foo=bar") + def test_parse_eq(self): + parser = RequestParser() + parser.add_argument("foo"), + args = parser.parse_args() + assert args["foo"] == "bar" + + @pytest.mark.request_context("/bubble?foo<=bar") + def test_parse_lte(self): + parser = RequestParser() + parser.add_argument("foo", operators=["<="]) + + args = parser.parse_args() + assert args["foo"] == "bar" + + @pytest.mark.request_context("/bubble") + def test_parse_required(self, app): + parser = RequestParser() + parser.add_argument("foo", required=True, location="values") + + expected = { + "foo": "Missing required parameter in the post body or the query string" + } + with pytest.raises(BadRequest) as cm: + parser.parse_args() + + assert cm.value.data["message"] == "Input payload validation failed" + assert cm.value.data["errors"] == expected + + parser = RequestParser() + parser.add_argument("bar", required=True, location=["values", "cookies"]) + + expected = { + "bar": ( + "Missing required parameter in the post body or the query " + "string or the request's cookies" + ) + } + + with pytest.raises(BadRequest) as cm: + parser.parse_args() + assert cm.value.data["message"] == "Input payload validation failed" + assert cm.value.data["errors"] == expected + + @pytest.mark.request_context("/bubble") + @pytest.mark.options(bundle_errors=True) + def test_parse_error_bundling(self, app): + parser = RequestParser() + parser.add_argument("foo", required=True, location="values") + parser.add_argument("bar", required=True, location=["values", "cookies"]) + + with pytest.raises(BadRequest) as cm: + parser.parse_args() + + assert cm.value.data["message"] == "Input payload validation failed" + assert cm.value.data["errors"] == { + "foo": "Missing required parameter in the post body or the query string", + "bar": ( + "Missing required parameter in the post body or the query string " + "or the request's cookies" + ), + } + + @pytest.mark.request_context("/bubble") + @pytest.mark.options(bundle_errors=False) + def test_parse_error_bundling_w_parser_arg(self, app): + parser = RequestParser(bundle_errors=True) + parser.add_argument("foo", required=True, location="values") + parser.add_argument("bar", required=True, location=["values", "cookies"]) + + with pytest.raises(BadRequest) as cm: + parser.parse_args() + + assert cm.value.data["message"] == "Input payload validation failed" + assert cm.value.data["errors"] == { + "foo": "Missing required parameter in the post body or the query string", + "bar": ( + "Missing required parameter in the post body or the query string " + "or the request's cookies" + ), + } + + @pytest.mark.request_context("/bubble") + def test_parse_default_append(self): + parser = RequestParser() + parser.add_argument("foo", default="bar", action="append", store_missing=True) + + args = parser.parse_args() + + assert args["foo"] == "bar" + + @pytest.mark.request_context("/bubble") + def test_parse_default(self): + parser = RequestParser() + parser.add_argument("foo", default="bar", store_missing=True) + + args = parser.parse_args() + assert args["foo"] == "bar" + + @pytest.mark.request_context("/bubble") + def test_parse_callable_default(self): + parser = RequestParser() + parser.add_argument("foo", default=lambda: "bar", store_missing=True) + + args = parser.parse_args() + assert args["foo"] == "bar" + + @pytest.mark.request_context("/bubble?foo=bar") + def test_parse(self): + parser = RequestParser() + parser.add_argument("foo"), + + args = parser.parse_args() + assert args["foo"] == "bar" + + @pytest.mark.request_context("/bubble") + def test_parse_none(self): + parser = RequestParser() + parser.add_argument("foo") + + args = parser.parse_args() + assert args["foo"] is None + + def test_parse_store_missing(self, app): + req = Request.from_values("/bubble") + + parser = RequestParser() + parser.add_argument("foo", store_missing=False) + + args = parser.parse_args(req) + assert "foo" not in args + + def test_parse_choices_correct(self, app): + req = Request.from_values("/bubble?foo=bat") + + parser = RequestParser() + parser.add_argument("foo", choices=["bat"]), + + args = parser.parse_args(req) + assert args["foo"] == "bat" + + def test_parse_choices(self, app): + req = Request.from_values("/bubble?foo=bar") + + parser = RequestParser() + parser.add_argument("foo", choices=["bat"]), + + with pytest.raises(BadRequest): + parser.parse_args(req) + + def test_parse_choices_sensitive(self, app): + req = Request.from_values("/bubble?foo=BAT") + + parser = RequestParser() + parser.add_argument("foo", choices=["bat"], case_sensitive=True), + + with pytest.raises(BadRequest): + parser.parse_args(req) + + def test_parse_choices_insensitive(self, app): + req = Request.from_values("/bubble?foo=BAT") + + parser = RequestParser() + parser.add_argument("foo", choices=["bat"], case_sensitive=False), + + args = parser.parse_args(req) + assert "bat" == args.get("foo") + + # both choices and args are case_insensitive + req = Request.from_values("/bubble?foo=bat") + + parser = RequestParser() + parser.add_argument("foo", choices=["BAT"], case_sensitive=False), + + args = parser.parse_args(req) + assert "bat" == args.get("foo") + + def test_parse_ignore(self, app): + req = Request.from_values("/bubble?foo=bar") + + parser = RequestParser() + parser.add_argument("foo", type=int, ignore=True, store_missing=True), + + args = parser.parse_args(req) + assert args["foo"] is None + + def test_chaining(self): + parser = RequestParser() + assert parser is parser.add_argument("foo") + + def test_result_existence(self): + result = ParseResult() + result.foo = "bar" + result["bar"] = "baz" + assert result["foo"] == "bar" + assert result.bar == "baz" + + def test_result_missing(self): + result = ParseResult() + pytest.raises(AttributeError, lambda: result.spam) + pytest.raises(KeyError, lambda: result["eggs"]) + + def test_result_configurability(self): + req = Request.from_values() + assert isinstance(RequestParser().parse_args(req), ParseResult) + assert type(RequestParser(result_class=dict).parse_args(req)) is dict + + def test_none_argument(self, app): + parser = RequestParser() + parser.add_argument("foo", location="json") + with app.test_request_context( + "/bubble", + method="post", + data=json.dumps({"foo": None}), + content_type="application/json", + ): + args = parser.parse_args() + assert args["foo"] is None + + def test_type_callable(self, app): + req = Request.from_values("/bubble?foo=1") + + parser = RequestParser() + parser.add_argument("foo", type=lambda x: x, required=False), + + args = parser.parse_args(req) + assert args["foo"] == "1" + + def test_type_callable_none(self, app): + parser = RequestParser() + parser.add_argument("foo", type=lambda x: x, location="json", required=False), + + with app.test_request_context( + "/bubble", + method="post", + data=json.dumps({"foo": None}), + content_type="application/json", + ): + args = parser.parse_args() + assert args["foo"] is None + + def test_type_decimal(self, app): + parser = RequestParser() + parser.add_argument("foo", type=decimal.Decimal, location="json") + + with app.test_request_context( + "/bubble", + method="post", + data=json.dumps({"foo": "1.0025"}), + content_type="application/json", + ): + args = parser.parse_args() + assert args["foo"] == decimal.Decimal("1.0025") + + def test_type_filestorage(self, app): + parser = RequestParser() + parser.add_argument("foo", type=FileStorage, location="files") + + fdata = "foo bar baz qux".encode("utf-8") + with app.test_request_context( + "/bubble", method="POST", data={"foo": (io.BytesIO(fdata), "baz.txt")} + ): + args = parser.parse_args() + + assert args["foo"].name == "foo" + assert args["foo"].filename == "baz.txt" + assert args["foo"].read() == fdata + + def test_filestorage_custom_type(self, app): + def _custom_type(f): + return FileStorage( + stream=f.stream, + filename="{0}aaaa".format(f.filename), + name="{0}aaaa".format(f.name), + ) + + parser = RequestParser() + parser.add_argument("foo", type=_custom_type, location="files") + + fdata = "foo bar baz qux".encode("utf-8") + with app.test_request_context( + "/bubble", method="POST", data={"foo": (io.BytesIO(fdata), "baz.txt")} + ): + args = parser.parse_args() + + assert args["foo"].name == "fooaaaa" + assert args["foo"].filename == "baz.txtaaaa" + assert args["foo"].read() == fdata + + def test_passing_arguments_object(self, app): + req = Request.from_values("/bubble?foo=bar") + parser = RequestParser() + parser.add_argument(Argument("foo")) + + args = parser.parse_args(req) + assert args["foo"] == "bar" + + def test_int_choice_types(self, app): + parser = RequestParser() + parser.add_argument("foo", type=int, choices=[1, 2, 3], location="json") + + with app.test_request_context( + "/bubble", + method="post", + data=json.dumps({"foo": 5}), + content_type="application/json", + ): + with pytest.raises(BadRequest): + parser.parse_args() + + def test_int_range_choice_types(self, app): + parser = RequestParser() + parser.add_argument("foo", type=int, choices=range(100), location="json") + + with app.test_request_context( + "/bubble", + method="post", + data=json.dumps({"foo": 101}), + content_type="application/json", + ): + with pytest.raises(BadRequest): + parser.parse_args() + + def test_request_parser_copy(self, app): + req = Request.from_values("/bubble?foo=101&bar=baz") + parser = RequestParser() + foo_arg = Argument("foo", type=int) + parser.args.append(foo_arg) + parser_copy = parser.copy() + + # Deepcopy should create a clone of the argument object instead of + # copying a reference to the new args list + assert foo_arg not in parser_copy.args + + # Args added to new parser should not be added to the original + bar_arg = Argument("bar") + parser_copy.args.append(bar_arg) + assert bar_arg not in parser.args + + args = parser_copy.parse_args(req) + assert args["foo"] == 101 + assert args["bar"] == "baz" + + def test_request_parse_copy_including_settings(self): + parser = RequestParser(trim=True, bundle_errors=True) + parser_copy = parser.copy() + + assert parser.trim == parser_copy.trim + assert parser.bundle_errors == parser_copy.bundle_errors + + def test_request_parser_replace_argument(self, app): + req = Request.from_values("/bubble?foo=baz") + parser = RequestParser() + parser.add_argument("foo", type=int) + parser_copy = parser.copy() + parser_copy.replace_argument("foo") + + args = parser_copy.parse_args(req) + assert args["foo"] == "baz" + + def test_both_json_and_values_location(self, app): + parser = RequestParser() + parser.add_argument("foo", type=int) + parser.add_argument("baz", type=int) + with app.test_request_context( + "/bubble?foo=1", + method="post", + data=json.dumps({"baz": 2}), + content_type="application/json", + ): + args = parser.parse_args() + assert args["foo"] == 1 + assert args["baz"] == 2 + + def test_not_json_location_and_content_type_json(self, app): + parser = RequestParser() + parser.add_argument("foo", location="args") + + with app.test_request_context( + "/bubble", method="get", content_type="application/json" + ): + parser.parse_args() # Should not raise a 400: BadRequest + + def test_request_parser_remove_argument(self): + req = Request.from_values("/bubble?foo=baz") + parser = RequestParser() + parser.add_argument("foo", type=int) + parser_copy = parser.copy() + parser_copy.remove_argument("foo") + + args = parser_copy.parse_args(req) + assert args == {} + + def test_strict_parsing_off(self): + req = Request.from_values("/bubble?foo=baz") + parser = RequestParser() + args = parser.parse_args(req) + assert args == {} + + def test_strict_parsing_on(self): + req = Request.from_values("/bubble?foo=baz") + parser = RequestParser() + with pytest.raises(BadRequest): + parser.parse_args(req, strict=True) + + def test_strict_parsing_off_partial_hit(self, app): + req = Request.from_values("/bubble?foo=1&bar=bees&n=22") + parser = RequestParser() + parser.add_argument("foo", type=int) + args = parser.parse_args(req) + assert args["foo"] == 1 + + def test_strict_parsing_on_partial_hit(self, app): + req = Request.from_values("/bubble?foo=1&bar=bees&n=22") + parser = RequestParser() + parser.add_argument("foo", type=int) + with pytest.raises(BadRequest): + parser.parse_args(req, strict=True) + + def test_trim_argument(self, app): + req = Request.from_values("/bubble?foo= 1 &bar=bees&n=22") + parser = RequestParser() + parser.add_argument("foo") + args = parser.parse_args(req) + assert args["foo"] == " 1 " + + parser = RequestParser() + parser.add_argument("foo", trim=True) + args = parser.parse_args(req) + assert args["foo"] == "1" + + parser = RequestParser() + parser.add_argument("foo", trim=True, type=int) + args = parser.parse_args(req) + assert args["foo"] == 1 + + def test_trim_request_parser(self, app): + req = Request.from_values("/bubble?foo= 1 &bar=bees&n=22") + parser = RequestParser(trim=False) + parser.add_argument("foo") + args = parser.parse_args(req) + assert args["foo"] == " 1 " + + parser = RequestParser(trim=True) + parser.add_argument("foo") + args = parser.parse_args(req) + assert args["foo"] == "1" + + parser = RequestParser(trim=True) + parser.add_argument("foo", type=int) + args = parser.parse_args(req) + assert args["foo"] == 1 + + def test_trim_request_parser_override_by_argument(self): + parser = RequestParser(trim=True) + parser.add_argument("foo", trim=False) + + assert parser.args[0].trim is False + + def test_trim_request_parser_json(self, app): + parser = RequestParser(trim=True) + parser.add_argument("foo", location="json") + parser.add_argument("int1", location="json", type=int) + parser.add_argument("int2", location="json", type=int) + + with app.test_request_context( + "/bubble", + method="post", + data=json.dumps({"foo": " bar ", "int1": 1, "int2": " 2 "}), + content_type="application/json", + ): + args = parser.parse_args() + assert args["foo"] == "bar" + assert args["int1"] == 1 + assert args["int2"] == 2 + + +class ArgumentTest(object): + def test_name(self): + arg = Argument("foo") + assert arg.name == "foo" + + def test_dest(self): + arg = Argument("foo", dest="foobar") + assert arg.dest == "foobar" + + def test_location_url(self): + arg = Argument("foo", location="url") + assert arg.location == "url" + + def test_location_url_list(self): + arg = Argument("foo", location=["url"]) + assert arg.location == ["url"] + + def test_location_header(self): + arg = Argument("foo", location="headers") + assert arg.location == "headers" + + def test_location_json(self): + arg = Argument("foo", location="json") + assert arg.location == "json" + + def test_location_get_json(self): + arg = Argument("foo", location="get_json") + assert arg.location == "get_json" + + def test_location_header_list(self): + arg = Argument("foo", location=["headers"]) + assert arg.location == ["headers"] + + def test_type(self): + arg = Argument("foo", type=int) + assert arg.type == int + + def test_default(self): + arg = Argument("foo", default=True) + assert arg.default is True + + def test_default_help(self): + arg = Argument("foo") + assert arg.help is None + + def test_required(self): + arg = Argument("foo", required=True) + assert arg.required is True + + def test_ignore(self): + arg = Argument("foo", ignore=True) + assert arg.ignore is True + + def test_operator(self): + arg = Argument("foo", operators=[">=", "<=", "="]) + assert arg.operators == [">=", "<=", "="] + + def test_action_filter(self): + arg = Argument("foo", action="filter") + assert arg.action == "filter" + + def test_action(self): + arg = Argument("foo", action="append") + assert arg.action == "append" + + def test_choices(self): + arg = Argument("foo", choices=[1, 2]) + assert arg.choices == [1, 2] + + def test_default_dest(self): + arg = Argument("foo") + assert arg.dest is None + + def test_default_operators(self): + arg = Argument("foo") + assert arg.operators[0] == "=" + assert len(arg.operators) == 1 + + def test_default_type(self): + arg = Argument("foo") + sentinel = 666 + assert arg.type(sentinel) == "666" + + def test_default_default(self): + arg = Argument("foo") + assert arg.default is None + + def test_required_default(self): + arg = Argument("foo") + assert arg.required is False + + def test_ignore_default(self): + arg = Argument("foo") + assert arg.ignore is False + + def test_action_default(self): + arg = Argument("foo") + assert arg.action == "store" + + def test_choices_default(self): + arg = Argument("foo") + assert len(arg.choices) == 0 + + def test_source(self, mocker): + req = mocker.Mock(["args", "headers", "values"]) + req.args = {"foo": "bar"} + req.headers = {"baz": "bat"} + arg = Argument("foo", location=["args"]) + assert arg.source(req) == MultiDict(req.args) + + arg = Argument("foo", location=["headers"]) + assert arg.source(req) == MultiDict(req.headers) + + def test_convert_default_type_with_null_input(self): + arg = Argument("foo") + assert arg.convert(None, None) is None + + def test_convert_with_null_input_when_not_nullable(self): + arg = Argument("foo", nullable=False) + pytest.raises(ValueError, lambda: arg.convert(None, None)) + + def test_source_bad_location(self, mocker): + req = mocker.Mock(["values"]) + arg = Argument("foo", location=["foo"]) + assert len(arg.source(req)) == 0 # yes, basically you don't find it + + def test_source_default_location(self, mocker): + req = mocker.Mock(["values", "get_json"]) + req.get_json.return_value = None + req._get_child_mock = lambda **kwargs: MultiDict() + arg = Argument("foo") + assert arg.source(req) == req.values + + def test_option_case_sensitive(self): + arg = Argument("foo", choices=["bar", "baz"], case_sensitive=True) + assert arg.case_sensitive is True + + # Insensitive + arg = Argument("foo", choices=["bar", "baz"], case_sensitive=False) + assert arg.case_sensitive is False + + # Default + arg = Argument("foo", choices=["bar", "baz"]) + assert arg.case_sensitive is True + + +class RequestParserSchemaTest(object): + def test_empty_parser(self): + parser = RequestParser() + assert parser.__schema__ == [] + + def test_primitive_types(self): + parser = RequestParser() + parser.add_argument("int", type=int, help="Some integer") + parser.add_argument("str", type=str, help="Some string") + parser.add_argument("float", type=float, help="Some float") + + assert parser.__schema__ == [ + { + "description": "Some integer", + "type": "integer", + "name": "int", + "in": "query", + }, + { + "description": "Some string", + "type": "string", + "name": "str", + "in": "query", + }, + { + "description": "Some float", + "type": "number", + "name": "float", + "in": "query", + }, + ] + + def test_unknown_type(self): + parser = RequestParser() + parser.add_argument("unknown", type=lambda v: v) + assert parser.__schema__ == [ + { + "name": "unknown", + "type": "string", + "in": "query", + } + ] + + def test_required(self): + parser = RequestParser() + parser.add_argument("int", type=int, required=True) + assert parser.__schema__ == [ + { + "name": "int", + "type": "integer", + "in": "query", + "required": True, + } + ] + + def test_default(self): + parser = RequestParser() + parser.add_argument("int", type=int, default=5) + assert parser.__schema__ == [ + { + "name": "int", + "type": "integer", + "in": "query", + "default": 5, + } + ] + + def test_default_as_false(self): + parser = RequestParser() + parser.add_argument("bool", type=inputs.boolean, default=False) + assert parser.__schema__ == [ + { + "name": "bool", + "type": "boolean", + "in": "query", + "default": False, + } + ] + + def test_choices(self): + parser = RequestParser() + parser.add_argument("string", type=str, choices=["a", "b"]) + assert parser.__schema__ == [ + { + "name": "string", + "type": "string", + "in": "query", + "enum": ["a", "b"], + } + ] + + def test_location(self): + parser = RequestParser() + parser.add_argument("default", type=int) + parser.add_argument("in_values", type=int, location="values") + parser.add_argument("in_query", type=int, location="args") + parser.add_argument("in_headers", type=int, location="headers") + parser.add_argument("in_cookie", type=int, location="cookie") + assert parser.__schema__ == [ + { + "name": "default", + "type": "integer", + "in": "query", + }, + { + "name": "in_values", + "type": "integer", + "in": "query", + }, + { + "name": "in_query", + "type": "integer", + "in": "query", + }, + { + "name": "in_headers", + "type": "integer", + "in": "header", + }, + ] + + def test_location_json(self): + parser = RequestParser() + parser.add_argument("in_json", type=str, location="json") + assert parser.__schema__ == [ + { + "name": "in_json", + "type": "string", + "in": "body", + } + ] + + def test_location_form(self): + parser = RequestParser() + parser.add_argument("in_form", type=int, location="form") + assert parser.__schema__ == [ + { + "name": "in_form", + "type": "integer", + "in": "formData", + } + ] + + def test_location_files(self): + parser = RequestParser() + parser.add_argument("in_files", type=FileStorage, location="files") + assert parser.__schema__ == [ + { + "name": "in_files", + "type": "file", + "in": "formData", + } + ] + + def test_form_and_body_location(self): + parser = RequestParser() + parser.add_argument("default", type=int) + parser.add_argument("in_form", type=int, location="form") + parser.add_argument("in_json", type=str, location="json") + with pytest.raises(SpecsError) as cm: + parser.__schema__ + + assert cm.value.msg == "Can't use formData and body at the same time" + + def test_files_and_body_location(self): + parser = RequestParser() + parser.add_argument("default", type=int) + parser.add_argument("in_files", type=FileStorage, location="files") + parser.add_argument("in_json", type=str, location="json") + with pytest.raises(SpecsError) as cm: + parser.__schema__ + + assert cm.value.msg == "Can't use formData and body at the same time" + + def test_models(self): + todo_fields = Model( + "Todo", + {"task": fields.String(required=True, description="The task details")}, + ) + parser = RequestParser() + parser.add_argument("todo", type=todo_fields) + assert parser.__schema__ == [ + { + "name": "todo", + "type": "Todo", + "in": "body", + } + ] + + def test_lists(self): + parser = RequestParser() + parser.add_argument("int", type=int, action="append") + assert parser.__schema__ == [ + { + "name": "int", + "in": "query", + "type": "array", + "collectionFormat": "multi", + "items": {"type": "integer"}, + } + ] + + def test_split_lists(self): + parser = RequestParser() + parser.add_argument("int", type=int, action="split") + assert parser.__schema__ == [ + { + "name": "int", + "in": "query", + "type": "array", + "collectionFormat": "csv", + "items": {"type": "integer"}, + } + ] + + def test_schema_interface(self): + def custom(value): + pass + + custom.__schema__ = { + "type": "string", + "format": "custom-format", + } + + parser = RequestParser() + parser.add_argument("custom", type=custom) + + assert parser.__schema__ == [ + { + "name": "custom", + "in": "query", + "type": "string", + "format": "custom-format", + } + ] + + def test_callable_default(self): + parser = RequestParser() + parser.add_argument("int", type=int, default=lambda: 5) + assert parser.__schema__ == [ + { + "name": "int", + "type": "integer", + "in": "query", + "default": 5, + } + ] diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_schemas.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_schemas.py new file mode 100644 index 0000000..a0a5c3b --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_schemas.py @@ -0,0 +1,52 @@ +import pytest + +from jsonschema import ValidationError + +from flask_restx import errors, schemas + + +class SchemasTest: + def test_lazyness(self): + schema = schemas.LazySchema("oas-2.0.json") + assert schema._schema is None + + "" in schema # Trigger load + assert schema._schema is not None + assert isinstance(schema._schema, dict) + + def test_oas2_schema_is_present(self): + assert hasattr(schemas, "OAS_20") + assert isinstance(schemas.OAS_20, schemas.LazySchema) + + +class ValidationTest: + def test_oas_20_valid(self): + assert schemas.validate( + { + "swagger": "2.0", + "info": { + "title": "An empty minimal specification", + "version": "1.0", + }, + "paths": {}, + } + ) + + def test_oas_20_invalid(self): + with pytest.raises(schemas.SchemaValidationError) as excinfo: + schemas.validate( + { + "swagger": "2.0", + "should": "not be here", + } + ) + for error in excinfo.value.errors: + assert isinstance(error, ValidationError) + + def test_unknown_schema(self): + with pytest.raises(errors.SpecsError): + schemas.validate({"valid": "no"}) + + def test_unknown_version(self): + with pytest.raises(errors.SpecsError): + schemas.validate({"swagger": "42.0"}) diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_swagger.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_swagger.py new file mode 100644 index 0000000..8f18150 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_swagger.py @@ -0,0 +1,3579 @@ +import pytest + +from textwrap import dedent + +from flask import url_for, Blueprint +from werkzeug.datastructures import FileStorage + +import flask_restx as restx + +from flask_restx import inputs + + +class SwaggerTest(object): + def test_specs_endpoint(self, api, client): + data = client.get_specs("") + assert data["swagger"] == "2.0" + assert data["basePath"] == "/" + assert data["produces"] == ["application/json"] + assert data["consumes"] == ["application/json"] + assert data["paths"] == {} + assert "info" in data + + @pytest.mark.api(prefix="/api") + def test_specs_endpoint_with_prefix(self, api, client): + data = client.get_specs("/api") + assert data["swagger"] == "2.0" + assert data["basePath"] == "/api" + assert data["produces"] == ["application/json"] + assert data["consumes"] == ["application/json"] + assert data["paths"] == {} + assert "info" in data + + def test_specs_endpoint_produces(self, api, client): + def output_xml(data, code, headers=None): + pass + + api.representations["application/xml"] = output_xml + + data = client.get_specs() + assert len(data["produces"]) == 2 + assert "application/json" in data["produces"] + assert "application/xml" in data["produces"] + + def test_specs_endpoint_info(self, app, client): + api = restx.Api( + version="1.0", + title="My API", + description="This is a testing API", + terms_url="http://somewhere.com/terms/", + contact="Support", + contact_url="http://support.somewhere.com", + contact_email="contact@somewhere.com", + license="Apache 2.0", + license_url="http://www.apache.org/licenses/LICENSE-2.0.html", + ) + api.init_app(app) + + data = client.get_specs() + assert data["swagger"] == "2.0" + assert data["basePath"] == "/" + assert data["produces"] == ["application/json"] + assert data["paths"] == {} + + assert "info" in data + assert data["info"]["title"] == "My API" + assert data["info"]["version"] == "1.0" + assert data["info"]["description"] == "This is a testing API" + assert data["info"]["termsOfService"] == "http://somewhere.com/terms/" + assert data["info"]["contact"] == { + "name": "Support", + "url": "http://support.somewhere.com", + "email": "contact@somewhere.com", + } + assert data["info"]["license"] == { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html", + } + + def test_specs_endpoint_info_delayed(self, app, client): + api = restx.Api(version="1.0") + api.init_app( + app, + title="My API", + description="This is a testing API", + terms_url="http://somewhere.com/terms/", + contact="Support", + contact_url="http://support.somewhere.com", + contact_email="contact@somewhere.com", + license="Apache 2.0", + license_url="http://www.apache.org/licenses/LICENSE-2.0.html", + ) + + data = client.get_specs() + + assert data["swagger"] == "2.0" + assert data["basePath"] == "/" + assert data["produces"] == ["application/json"] + assert data["paths"] == {} + + assert "info" in data + assert data["info"]["title"] == "My API" + assert data["info"]["version"] == "1.0" + assert data["info"]["description"] == "This is a testing API" + assert data["info"]["termsOfService"] == "http://somewhere.com/terms/" + assert data["info"]["contact"] == { + "name": "Support", + "url": "http://support.somewhere.com", + "email": "contact@somewhere.com", + } + assert data["info"]["license"] == { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html", + } + + def test_specs_endpoint_info_callable(self, app, client): + api = restx.Api( + version=lambda: "1.0", + title=lambda: "My API", + description=lambda: "This is a testing API", + terms_url=lambda: "http://somewhere.com/terms/", + contact=lambda: "Support", + contact_url=lambda: "http://support.somewhere.com", + contact_email=lambda: "contact@somewhere.com", + license=lambda: "Apache 2.0", + license_url=lambda: "http://www.apache.org/licenses/LICENSE-2.0.html", + ) + api.init_app(app) + + data = client.get_specs() + assert data["swagger"] == "2.0" + assert data["basePath"] == "/" + assert data["produces"] == ["application/json"] + assert data["paths"] == {} + + assert "info" in data + assert data["info"]["title"] == "My API" + assert data["info"]["version"] == "1.0" + assert data["info"]["description"] == "This is a testing API" + assert data["info"]["termsOfService"] == "http://somewhere.com/terms/" + assert data["info"]["contact"] == { + "name": "Support", + "url": "http://support.somewhere.com", + "email": "contact@somewhere.com", + } + assert data["info"]["license"] == { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html", + } + + def test_specs_endpoint_no_host(self, app, client): + restx.Api(app) + + data = client.get_specs("") + assert "host" not in data + assert data["basePath"] == "/" + + @pytest.mark.options(server_name="api.restx.org") + def test_specs_endpoint_host(self, app, client): + # app.config['SERVER_NAME'] = 'api.restx.org' + restx.Api(app) + + data = client.get_specs("") + assert data["host"] == "api.restx.org" + assert data["basePath"] == "/" + + @pytest.mark.options(server_name="api.restx.org") + def test_specs_endpoint_host_with_url_prefix(self, app, client): + blueprint = Blueprint("api", __name__, url_prefix="/api/1") + restx.Api(blueprint) + app.register_blueprint(blueprint) + + data = client.get_specs("/api/1") + assert data["host"] == "api.restx.org" + assert data["basePath"] == "/api/1" + + @pytest.mark.options(server_name="restx.org") + def test_specs_endpoint_host_and_subdomain(self, app, client): + blueprint = Blueprint("api", __name__, subdomain="api") + restx.Api(blueprint) + app.register_blueprint(blueprint) + + data = client.get_specs(base_url="http://api.restx.org") + assert data["host"] == "api.restx.org" + assert data["basePath"] == "/" + + def test_specs_endpoint_tags_short(self, app, client): + restx.Api(app, tags=["tag-1", "tag-2", "tag-3"]) + + data = client.get_specs("") + assert data["tags"] == [{"name": "tag-1"}, {"name": "tag-2"}, {"name": "tag-3"}] + + def test_specs_endpoint_tags_tuple(self, app, client): + restx.Api( + app, + tags=[ + ("tag-1", "Tag 1"), + ("tag-2", "Tag 2"), + ("tag-3", "Tag 3"), + ], + ) + + data = client.get_specs("") + assert data["tags"] == [ + {"name": "tag-1", "description": "Tag 1"}, + {"name": "tag-2", "description": "Tag 2"}, + {"name": "tag-3", "description": "Tag 3"}, + ] + + def test_specs_endpoint_tags_dict(self, app, client): + restx.Api( + app, + tags=[ + {"name": "tag-1", "description": "Tag 1"}, + {"name": "tag-2", "description": "Tag 2"}, + {"name": "tag-3", "description": "Tag 3"}, + ], + ) + + data = client.get_specs("") + assert data["tags"] == [ + {"name": "tag-1", "description": "Tag 1"}, + {"name": "tag-2", "description": "Tag 2"}, + {"name": "tag-3", "description": "Tag 3"}, + ] + + @pytest.mark.api(tags=["ns", "tag"]) + def test_specs_endpoint_tags_namespaces(self, api, client): + api.namespace("ns", "Description") + + data = client.get_specs("") + assert data["tags"] == [{"name": "ns"}, {"name": "tag"}] + + def test_specs_endpoint_invalid_tags(self, app, client): + api = restx.Api(app, tags=[{"description": "Tag 1"}]) + + client.get_specs("", status=500) + + assert list(api.__schema__.keys()) == ["error"] + + def test_specs_endpoint_default_ns_with_resources(self, app, client): + restx.Api(app) + data = client.get_specs("") + assert data["tags"] == [] + + def test_specs_endpoint_default_ns_without_resources(self, app, client): + api = restx.Api(app) + + @api.route("/test", endpoint="test") + class TestResource(restx.Resource): + def get(self): + return {} + + data = client.get_specs("") + assert data["tags"] == [{"name": "default", "description": "Default namespace"}] + + def test_specs_endpoint_default_ns_with_specified_ns(self, app, client): + api = restx.Api(app) + ns = api.namespace("ns", "Test namespace") + + @ns.route("/test2", endpoint="test2") + @api.route("/test", endpoint="test") + class TestResource(restx.Resource): + def get(self): + return {} + + data = client.get_specs("") + assert data["tags"] == [ + {"name": "default", "description": "Default namespace"}, + {"name": "ns", "description": "Test namespace"}, + ] + + def test_specs_endpoint_specified_ns_without_default_ns(self, app, client): + api = restx.Api(app) + ns = api.namespace("ns", "Test namespace") + + @ns.route("/", endpoint="test2") + class TestResource(restx.Resource): + def get(self): + return {} + + data = client.get_specs("") + assert data["tags"] == [{"name": "ns", "description": "Test namespace"}] + + def test_specs_endpoint_namespace_without_description(self, app, client): + api = restx.Api(app) + ns = api.namespace("ns") + + @ns.route("/test", endpoint="test") + class TestResource(restx.Resource): + def get(self): + return {} + + data = client.get_specs("") + assert data["tags"] == [{"name": "ns"}] + + def test_specs_endpoint_namespace_all_resources_hidden(self, app, client): + api = restx.Api(app) + ns = api.namespace("ns") + + @ns.route("/test", endpoint="test", doc=False) + class TestResource(restx.Resource): + def get(self): + return {} + + @ns.route("/test2", endpoint="test2") + @ns.hide + class TestResource2(restx.Resource): + def get(self): + return {} + + @ns.route("/test3", endpoint="test3") + @ns.doc(False) + class TestResource3(restx.Resource): + def get(self): + return {} + + data = client.get_specs("") + assert data["tags"] == [] + + def test_specs_authorizations(self, app, client): + authorizations = {"apikey": {"type": "apiKey", "in": "header", "name": "X-API"}} + restx.Api(app, authorizations=authorizations) + + data = client.get_specs() + + assert "securityDefinitions" in data + assert data["securityDefinitions"] == authorizations + + @pytest.mark.api(prefix="/api") + def test_minimal_documentation(self, api, client): + ns = api.namespace("ns", "Test namespace") + + @ns.route("/", endpoint="test") + class TestResource(restx.Resource): + def get(self): + return {} + + data = client.get_specs("/api") + paths = data["paths"] + assert len(paths.keys()) == 1 + + assert "/ns/" in paths + assert "get" in paths["/ns/"] + op = paths["/ns/"]["get"] + assert op["tags"] == ["ns"] + assert op["operationId"] == "get_test_resource" + assert "parameters" not in op + assert "summary" not in op + assert "description" not in op + assert op["responses"] == { + "200": { + "description": "Success", + } + } + + assert url_for("api.test") == "/api/ns/" + + @pytest.mark.api(prefix="/api", version="1.0") + def test_default_ns_resource_documentation(self, api, client): + @api.route("/test/", endpoint="test") + class TestResource(restx.Resource): + def get(self): + return {} + + data = client.get_specs("/api") + paths = data["paths"] + assert len(paths.keys()) == 1 + + assert "/test/" in paths + assert "get" in paths["/test/"] + op = paths["/test/"]["get"] + assert op["tags"] == ["default"] + assert op["responses"] == { + "200": { + "description": "Success", + } + } + + assert len(data["tags"]) == 1 + tag = data["tags"][0] + assert tag["name"] == "default" + assert tag["description"] == "Default namespace" + + assert url_for("api.test") == "/api/test/" + + @pytest.mark.api(default="site", default_label="Site namespace") + def test_default_ns_resource_documentation_with_override(self, api, client): + @api.route("/test/", endpoint="test") + class TestResource(restx.Resource): + def get(self): + return {} + + data = client.get_specs() + paths = data["paths"] + assert len(paths.keys()) == 1 + + assert "/test/" in paths + assert "get" in paths["/test/"] + op = paths["/test/"]["get"] + assert op["tags"] == ["site"] + assert op["responses"] == { + "200": { + "description": "Success", + } + } + + assert len(data["tags"]) == 1 + tag = data["tags"][0] + assert tag["name"] == "site" + assert tag["description"] == "Site namespace" + + assert url_for("api.test") == "/test/" + + @pytest.mark.api(prefix="/api") + def test_ns_resource_documentation(self, api, client): + ns = api.namespace("ns", "Test namespace") + + @ns.route("/", endpoint="test") + class TestResource(restx.Resource): + def get(self): + return {} + + data = client.get_specs("/api") + paths = data["paths"] + assert len(paths.keys()) == 1 + + assert "/ns/" in paths + assert "get" in paths["/ns/"] + op = paths["/ns/"]["get"] + assert op["tags"] == ["ns"] + assert op["responses"] == { + "200": { + "description": "Success", + } + } + assert "parameters" not in op + + assert len(data["tags"]) == 1 + tag = data["tags"][-1] + assert tag["name"] == "ns" + assert tag["description"] == "Test namespace" + + assert url_for("api.test") == "/api/ns/" + + def test_ns_resource_documentation_lazy(self, app, client): + api = restx.Api() + ns = api.namespace("ns", "Test namespace") + + @ns.route("/", endpoint="test") + class TestResource(restx.Resource): + def get(self): + return {} + + api.init_app(app) + + data = client.get_specs() + paths = data["paths"] + assert len(paths.keys()) == 1 + + assert "/ns/" in paths + assert "get" in paths["/ns/"] + op = paths["/ns/"]["get"] + assert op["tags"] == ["ns"] + assert op["responses"] == { + "200": { + "description": "Success", + } + } + + assert len(data["tags"]) == 1 + tag = data["tags"][-1] + assert tag["name"] == "ns" + assert tag["description"] == "Test namespace" + + assert url_for("test") == "/ns/" + + def test_methods_docstring_to_summary(self, api, client): + @api.route("/test/", endpoint="test") + class TestResource(restx.Resource): + def get(self): + """ + GET operation + """ + return {} + + def post(self): + """POST operation. + + Should be ignored + """ + return {} + + def put(self): + """PUT operation. Should be ignored""" + return {} + + def delete(self): + """ + DELETE operation. + Should be ignored. + """ + return {} + + data = client.get_specs() + path = data["paths"]["/test/"] + + assert len(path.keys()) == 4 + + for method in path.keys(): + operation = path[method] + assert method in ("get", "post", "put", "delete") + assert operation["summary"] == "{0} operation".format(method.upper()) + assert operation["operationId"] == "{0}_test_resource".format( + method.lower() + ) + # assert operation['parameters'] == [] + + def test_path_parameter_no_type(self, api, client): + @api.route("/id//", endpoint="by-id") + class ByIdResource(restx.Resource): + def get(self, id): + return {} + + data = client.get_specs() + assert "/id/{id}/" in data["paths"] + + path = data["paths"]["/id/{id}/"] + assert len(path["parameters"]) == 1 + + parameter = path["parameters"][0] + assert parameter["name"] == "id" + assert parameter["type"] == "string" + assert parameter["in"] == "path" + assert parameter["required"] is True + + def test_path_parameter_with_type(self, api, client): + @api.route("/name//", endpoint="by-name") + class ByNameResource(restx.Resource): + def get(self, age): + return {} + + data = client.get_specs() + assert "/name/{age}/" in data["paths"] + + path = data["paths"]["/name/{age}/"] + assert len(path["parameters"]) == 1 + + parameter = path["parameters"][0] + assert parameter["name"] == "age" + assert parameter["type"] == "integer" + assert parameter["in"] == "path" + assert parameter["required"] is True + + def test_path_parameter_with_type_with_argument(self, api, client): + @api.route("/name//", endpoint="by-name") + class ByNameResource(restx.Resource): + def get(self, id): + return {} + + data = client.get_specs() + assert "/name/{id}/" in data["paths"] + + path = data["paths"]["/name/{id}/"] + assert len(path["parameters"]) == 1 + + parameter = path["parameters"][0] + assert parameter["name"] == "id" + assert parameter["type"] == "string" + assert parameter["in"] == "path" + assert parameter["required"] is True + + def test_path_parameter_with_explicit_details(self, api, client): + @api.route( + "/name//", + endpoint="by-name", + doc={"params": {"age": {"description": "An age"}}}, + ) + class ByNameResource(restx.Resource): + def get(self, age): + return {} + + data = client.get_specs() + assert "/name/{age}/" in data["paths"] + + path = data["paths"]["/name/{age}/"] + assert len(path["parameters"]) == 1 + + parameter = path["parameters"][0] + assert parameter["name"] == "age" + assert parameter["type"] == "integer" + assert parameter["in"] == "path" + assert parameter["required"] is True + assert parameter["description"] == "An age" + + def test_path_parameter_with_decorator_details(self, api, client): + @api.route("/name//") + @api.param("age", "An age") + class ByNameResource(restx.Resource): + def get(self, age): + return {} + + data = client.get_specs() + assert "/name/{age}/" in data["paths"] + + path = data["paths"]["/name/{age}/"] + assert len(path["parameters"]) == 1 + + parameter = path["parameters"][0] + assert parameter["name"] == "age" + assert parameter["type"] == "integer" + assert parameter["in"] == "path" + assert parameter["required"] is True + assert parameter["description"] == "An age" + + def test_expect_parser(self, api, client): + parser = api.parser() + parser.add_argument("param", type=int, help="Some param") + parser.add_argument("jsonparam", type=str, location="json", help="Some param") + + @api.route("/with-parser/", endpoint="with-parser") + class WithParserResource(restx.Resource): + @api.expect(parser) + def get(self): + return {} + + data = client.get_specs() + assert "/with-parser/" in data["paths"] + + op = data["paths"]["/with-parser/"]["get"] + assert len(op["parameters"]) == 2 + + parameter = [o for o in op["parameters"] if o["in"] == "query"][0] + assert parameter["name"] == "param" + assert parameter["type"] == "integer" + assert parameter["in"] == "query" + assert parameter["description"] == "Some param" + + parameter = [o for o in op["parameters"] if o["in"] == "body"][0] + assert parameter["name"] == "payload" + assert parameter["required"] + assert parameter["in"] == "body" + assert parameter["schema"]["properties"]["jsonparam"]["type"] == "string" + + def test_expect_parser_on_class(self, api, client): + parser = api.parser() + parser.add_argument("param", type=int, help="Some param") + + @api.route("/with-parser/", endpoint="with-parser") + @api.expect(parser) + class WithParserResource(restx.Resource): + def get(self): + return {} + + data = client.get_specs() + assert "/with-parser/" in data["paths"] + + path = data["paths"]["/with-parser/"] + assert len(path["parameters"]) == 1 + + parameter = path["parameters"][0] + assert parameter["name"] == "param" + assert parameter["type"] == "integer" + assert parameter["in"] == "query" + assert parameter["description"] == "Some param" + + def test_method_parser_on_class(self, api, client): + parser = api.parser() + parser.add_argument("param", type=int, help="Some param") + + @api.route("/with-parser/", endpoint="with-parser") + @api.doc(get={"expect": parser}) + class WithParserResource(restx.Resource): + def get(self): + return {} + + def post(self): + return {} + + data = client.get_specs() + assert "/with-parser/" in data["paths"] + + op = data["paths"]["/with-parser/"]["get"] + assert len(op["parameters"]) == 1 + + parameter = op["parameters"][0] + assert parameter["name"] == "param" + assert parameter["type"] == "integer" + assert parameter["in"] == "query" + assert parameter["description"] == "Some param" + + op = data["paths"]["/with-parser/"]["post"] + assert "parameters" not in op + + def test_parser_parameters_override(self, api, client): + parser = api.parser() + parser.add_argument("param", type=int, help="Some param") + + @api.route("/with-parser/", endpoint="with-parser") + class WithParserResource(restx.Resource): + @api.expect(parser) + @api.doc(params={"param": {"description": "New description"}}) + def get(self): + return {} + + data = client.get_specs() + assert "/with-parser/" in data["paths"] + + op = data["paths"]["/with-parser/"]["get"] + assert len(op["parameters"]) == 1 + + parameter = op["parameters"][0] + assert parameter["name"] == "param" + assert parameter["type"] == "integer" + assert parameter["in"] == "query" + assert parameter["description"] == "New description" + + def test_parser_parameter_in_form(self, api, client): + parser = api.parser() + parser.add_argument("param", type=int, help="Some param", location="form") + + @api.route("/with-parser/", endpoint="with-parser") + class WithParserResource(restx.Resource): + @api.expect(parser) + def get(self): + return {} + + data = client.get_specs() + assert "/with-parser/" in data["paths"] + + op = data["paths"]["/with-parser/"]["get"] + assert len(op["parameters"]) == 1 + + parameter = op["parameters"][0] + assert parameter["name"] == "param" + assert parameter["type"] == "integer" + assert parameter["in"] == "formData" + assert parameter["description"] == "Some param" + + assert op["consumes"] == [ + "application/x-www-form-urlencoded", + "multipart/form-data", + ] + + def test_parser_parameter_in_files(self, api, client): + parser = api.parser() + parser.add_argument("in_files", type=FileStorage, location="files") + + @api.route("/with-parser/", endpoint="with-parser") + class WithParserResource(restx.Resource): + @api.expect(parser) + def get(self): + return {} + + data = client.get_specs() + assert "/with-parser/" in data["paths"] + + op = data["paths"]["/with-parser/"]["get"] + assert len(op["parameters"]) == 1 + + parameter = op["parameters"][0] + assert parameter["name"] == "in_files" + assert parameter["type"] == "file" + assert parameter["in"] == "formData" + + assert op["consumes"] == ["multipart/form-data"] + + def test_parser_parameter_in_files_on_class(self, api, client): + parser = api.parser() + parser.add_argument("in_files", type=FileStorage, location="files") + + @api.route("/with-parser/", endpoint="with-parser") + @api.expect(parser) + class WithParserResource(restx.Resource): + def get(self): + return {} + + data = client.get_specs() + assert "/with-parser/" in data["paths"] + + path = data["paths"]["/with-parser/"] + assert len(path["parameters"]) == 1 + + parameter = path["parameters"][0] + assert parameter["name"] == "in_files" + assert parameter["type"] == "file" + assert parameter["in"] == "formData" + + assert "consumes" not in path + + op = path["get"] + assert "consumes" in op + assert op["consumes"] == ["multipart/form-data"] + + def test_explicit_parameters(self, api, client): + @api.route("/name//", endpoint="by-name") + class ByNameResource(restx.Resource): + @api.doc( + params={ + "q": { + "type": "string", + "in": "query", + "description": "A query string", + } + } + ) + def get(self, age): + return {} + + data = client.get_specs() + assert "/name/{age}/" in data["paths"] + + path = data["paths"]["/name/{age}/"] + assert len(path["parameters"]) == 1 + + parameter = path["parameters"][0] + assert parameter["name"] == "age" + assert parameter["type"] == "integer" + assert parameter["in"] == "path" + assert parameter["required"] is True + + op = path["get"] + assert len(op["parameters"]) == 1 + + parameter = op["parameters"][0] + assert parameter["name"] == "q" + assert parameter["type"] == "string" + assert parameter["in"] == "query" + assert parameter["description"] == "A query string" + + def test_explicit_parameters_with_decorator(self, api, client): + @api.route("/name/") + class ByNameResource(restx.Resource): + @api.param("q", "A query string", type="string", _in="formData") + def get(self, age): + return {} + + data = client.get_specs() + assert "/name/" in data["paths"] + + op = data["paths"]["/name/"]["get"] + assert len(op["parameters"]) == 1 + + parameter = op["parameters"][0] + assert parameter["name"] == "q" + assert parameter["type"] == "string" + assert parameter["in"] == "formData" + assert parameter["description"] == "A query string" + + def test_class_explicit_parameters(self, api, client): + @api.route( + "/name//", + endpoint="by-name", + doc={ + "params": { + "q": { + "type": "string", + "in": "query", + "description": "A query string", + } + } + }, + ) + class ByNameResource(restx.Resource): + def get(self, age): + return {} + + data = client.get_specs() + assert "/name/{age}/" in data["paths"] + + path = data["paths"]["/name/{age}/"] + assert len(path["parameters"]) == 2 + + by_name = dict((p["name"], p) for p in path["parameters"]) + + parameter = by_name["age"] + assert parameter["name"] == "age" + assert parameter["type"] == "integer" + assert parameter["in"] == "path" + assert parameter["required"] is True + + parameter = by_name["q"] + assert parameter["name"] == "q" + assert parameter["type"] == "string" + assert parameter["in"] == "query" + assert parameter["description"] == "A query string" + + def test_explicit_parameters_override(self, api, client): + @api.route( + "/name//", + endpoint="by-name", + doc={ + "params": { + "q": { + "type": "string", + "in": "query", + "description": "Overriden description", + }, + "age": {"description": "An age"}, + } + }, + ) + class ByNameResource(restx.Resource): + @api.doc(params={"q": {"description": "A query string"}}) + def get(self, age): + return {} + + def post(self, age): + pass + + data = client.get_specs() + assert "/name/{age}/" in data["paths"] + + path = data["paths"]["/name/{age}/"] + assert len(path["parameters"]) == 1 + + by_name = dict((p["name"], p) for p in path["parameters"]) + + parameter = by_name["age"] + assert parameter["name"] == "age" + assert parameter["type"] == "integer" + assert parameter["in"] == "path" + assert parameter["required"] is True + assert parameter["description"] == "An age" + + # Don't duplicate parameters + assert "q" not in by_name + + get = data["paths"]["/name/{age}/"]["get"] + assert len(get["parameters"]) == 1 + + parameter = get["parameters"][0] + assert parameter["name"] == "q" + assert parameter["type"] == "string" + assert parameter["in"] == "query" + assert parameter["description"] == "A query string" + + post = data["paths"]["/name/{age}/"]["post"] + assert len(post["parameters"]) == 1 + + parameter = post["parameters"][0] + assert parameter["name"] == "q" + assert parameter["type"] == "string" + assert parameter["in"] == "query" + assert parameter["description"] == "Overriden description" + + def test_explicit_parameters_override_by_method(self, api, client): + @api.route( + "/name//", + endpoint="by-name", + doc={ + "get": { + "params": { + "q": { + "type": "string", + "in": "query", + "description": "A query string", + } + } + }, + "params": {"age": {"description": "An age"}}, + }, + ) + class ByNameResource(restx.Resource): + @api.doc(params={"age": {"description": "Overriden"}}) + def get(self, age): + return {} + + def post(self, age): + return {} + + data = client.get_specs() + assert "/name/{age}/" in data["paths"] + + path = data["paths"]["/name/{age}/"] + assert "parameters" not in path + + get = path["get"] + assert len(get["parameters"]) == 2 + + by_name = dict((p["name"], p) for p in get["parameters"]) + + parameter = by_name["age"] + assert parameter["name"] == "age" + assert parameter["type"] == "integer" + assert parameter["in"] == "path" + assert parameter["required"] is True + assert parameter["description"] == "Overriden" + + parameter = by_name["q"] + assert parameter["name"] == "q" + assert parameter["type"] == "string" + assert parameter["in"] == "query" + assert parameter["description"] == "A query string" + + post = path["post"] + assert len(post["parameters"]) == 1 + + by_name = dict((p["name"], p) for p in post["parameters"]) + + parameter = by_name["age"] + assert parameter["name"] == "age" + assert parameter["type"] == "integer" + assert parameter["in"] == "path" + assert parameter["required"] is True + assert parameter["description"] == "An age" + + def test_parameters_cascading_with_apidoc_false(self, api, client): + @api.route( + "/name//", + endpoint="by-name", + doc={ + "get": { + "params": { + "q": { + "type": "string", + "in": "query", + "description": "A query string", + } + } + }, + "params": {"age": {"description": "An age"}}, + }, + ) + class ByNameResource(restx.Resource): + @api.doc(params={"age": {"description": "Overriden"}}) + def get(self, age): + return {} + + @api.doc(False) + def post(self, age): + return {} + + data = client.get_specs() + assert "/name/{age}/" in data["paths"] + + path = data["paths"]["/name/{age}/"] + assert "parameters" not in path + + get = path["get"] + assert len(get["parameters"]) == 2 + + by_name = dict((p["name"], p) for p in get["parameters"]) + assert "age" in by_name + assert "q" in by_name + + assert "post" not in path + + def test_explicit_parameters_desription_shortcut(self, api, client): + @api.route( + "/name//", + endpoint="by-name", + doc={ + "get": { + "params": { + "q": "A query string", + } + }, + "params": {"age": "An age"}, + }, + ) + class ByNameResource(restx.Resource): + @api.doc(params={"age": "Overriden"}) + def get(self, age): + return {} + + def post(self, age): + return {} + + data = client.get_specs() + assert "/name/{age}/" in data["paths"] + + path = data["paths"]["/name/{age}/"] + assert "parameters" not in path + + get = path["get"] + assert len(get["parameters"]) == 2 + + by_name = dict((p["name"], p) for p in get["parameters"]) + + parameter = by_name["age"] + assert parameter["name"] == "age" + assert parameter["type"] == "integer" + assert parameter["in"] == "path" + assert parameter["required"] is True + assert parameter["description"] == "Overriden" + + parameter = by_name["q"] + assert parameter["name"] == "q" + assert parameter["type"] == "string" + assert parameter["in"] == "query" + assert parameter["description"] == "A query string" + + post = path["post"] + assert len(post["parameters"]) == 1 + + by_name = dict((p["name"], p) for p in post["parameters"]) + + parameter = by_name["age"] + assert parameter["name"] == "age" + assert parameter["type"] == "integer" + assert parameter["in"] == "path" + assert parameter["required"] is True + assert parameter["description"] == "An age" + + assert "q" not in by_name + + def test_explicit_parameters_native_types(self, api, client): + @api.route("/types/", endpoint="native") + class NativeTypesResource(restx.Resource): + @api.doc( + params={ + "int": { + "type": int, + "in": "query", + }, + "float": { + "type": float, + "in": "query", + }, + "bool": { + "type": bool, + "in": "query", + }, + "str": { + "type": str, + "in": "query", + }, + "int-array": { + "type": [int], + "in": "query", + }, + "float-array": { + "type": [float], + "in": "query", + }, + "bool-array": { + "type": [bool], + "in": "query", + }, + "str-array": { + "type": [str], + "in": "query", + }, + } + ) + def get(self, age): + return {} + + data = client.get_specs() + + op = data["paths"]["/types/"]["get"] + + parameters = dict((p["name"], p) for p in op["parameters"]) + + assert parameters["int"]["type"] == "integer" + assert parameters["float"]["type"] == "number" + assert parameters["str"]["type"] == "string" + assert parameters["bool"]["type"] == "boolean" + + assert parameters["int-array"]["type"] == "array" + assert parameters["int-array"]["items"]["type"] == "integer" + assert parameters["float-array"]["type"] == "array" + assert parameters["float-array"]["items"]["type"] == "number" + assert parameters["str-array"]["type"] == "array" + assert parameters["str-array"]["items"]["type"] == "string" + assert parameters["bool-array"]["type"] == "array" + assert parameters["bool-array"]["items"]["type"] == "boolean" + + def test_response_on_method(self, api, client): + api.model( + "ErrorModel", + { + "message": restx.fields.String, + }, + ) + + @api.route("/test/") + class ByNameResource(restx.Resource): + @api.doc( + responses={ + 404: "Not found", + 405: ("Some message", "ErrorModel"), + } + ) + def get(self): + return {} + + data = client.get_specs("") + paths = data["paths"] + assert len(paths.keys()) == 1 + + op = paths["/test/"]["get"] + assert op["tags"] == ["default"] + assert op["responses"] == { + "404": { + "description": "Not found", + }, + "405": { + "description": "Some message", + "schema": { + "$ref": "#/definitions/ErrorModel", + }, + }, + } + + assert "definitions" in data + assert "ErrorModel" in data["definitions"] + + def test_api_response(self, api, client): + @api.route("/test/") + class TestResource(restx.Resource): + @api.response(200, "Success") + def get(self): + pass + + data = client.get_specs("") + paths = data["paths"] + + op = paths["/test/"]["get"] + assert op["responses"] == { + "200": { + "description": "Success", + } + } + + def test_api_response_multiple(self, api, client): + @api.route("/test/") + class TestResource(restx.Resource): + @api.response(200, "Success") + @api.response(400, "Validation error") + def get(self): + pass + + data = client.get_specs("") + paths = data["paths"] + + op = paths["/test/"]["get"] + assert op["responses"] == { + "200": { + "description": "Success", + }, + "400": { + "description": "Validation error", + }, + } + + def test_api_response_with_model(self, api, client): + model = api.model( + "SomeModel", + { + "message": restx.fields.String, + }, + ) + + @api.route("/test/") + class TestResource(restx.Resource): + @api.response(200, "Success", model) + def get(self): + pass + + data = client.get_specs("") + paths = data["paths"] + + op = paths["/test/"]["get"] + assert op["responses"] == { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/SomeModel", + }, + } + } + + assert "SomeModel" in data["definitions"] + + def test_api_response_default(self, api, client): + @api.route("/test/") + class TestResource(restx.Resource): + @api.response("default", "Error") + def get(self): + pass + + data = client.get_specs("") + paths = data["paths"] + + op = paths["/test/"]["get"] + assert op["responses"] == { + "default": { + "description": "Error", + } + } + + def test_api_header(self, api, client): + @api.route("/test/") + @api.header("X-HEADER", "A class header") + class TestResource(restx.Resource): + @api.header( + "X-HEADER-2", "Another header", type=[int], collectionFormat="csv" + ) + @api.header("X-HEADER-3", type=int) + @api.header("X-HEADER-4", type="boolean") + def get(self): + pass + + data = client.get_specs("") + headers = data["paths"]["/test/"]["get"]["responses"]["200"]["headers"] + + assert "X-HEADER" in headers + assert headers["X-HEADER"] == { + "type": "string", + "description": "A class header", + } + + assert "X-HEADER-2" in headers + assert headers["X-HEADER-2"] == { + "type": "array", + "items": {"type": "integer"}, + "description": "Another header", + "collectionFormat": "csv", + } + + assert "X-HEADER-3" in headers + assert headers["X-HEADER-3"] == {"type": "integer"} + + assert "X-HEADER-4" in headers + assert headers["X-HEADER-4"] == {"type": "boolean"} + + def test_response_header(self, api, client): + @api.route("/test/") + class TestResource(restx.Resource): + @api.response(200, "Success") + @api.response(400, "Validation", headers={"X-HEADER": "An header"}) + def get(self): + pass + + data = client.get_specs("") + headers = data["paths"]["/test/"]["get"]["responses"]["400"]["headers"] + + assert "X-HEADER" in headers + assert headers["X-HEADER"] == { + "type": "string", + "description": "An header", + } + + def test_api_and_response_header(self, api, client): + @api.route("/test/") + @api.header("X-HEADER", "A class header") + class TestResource(restx.Resource): + @api.header("X-HEADER-2", type=int) + @api.response(200, "Success") + @api.response(400, "Validation", headers={"X-ERROR": "An error header"}) + def get(self): + pass + + data = client.get_specs("") + headers200 = data["paths"]["/test/"]["get"]["responses"]["200"]["headers"] + headers400 = data["paths"]["/test/"]["get"]["responses"]["400"]["headers"] + + for headers in (headers200, headers400): + assert "X-HEADER" in headers + assert "X-HEADER-2" in headers + + assert "X-ERROR" in headers400 + assert "X-ERROR" not in headers200 + + def test_expect_header(self, api, client): + parser = api.parser() + parser.add_argument( + "X-Header", location="headers", required=True, help="A required header" + ) + parser.add_argument( + "X-Header-2", + location="headers", + type=int, + action="split", + help="Another header", + ) + parser.add_argument("X-Header-3", location="headers", type=int) + parser.add_argument("X-Header-4", location="headers", type=inputs.boolean) + + @api.route("/test/") + class TestResource(restx.Resource): + @api.expect(parser) + def get(self): + pass + + data = client.get_specs("") + parameters = data["paths"]["/test/"]["get"]["parameters"] + + def get_param(name): + candidates = [p for p in parameters if p["name"] == name] + assert len(candidates) == 1, "parameter {0} not found".format(name) + return candidates[0] + + parameter = get_param("X-Header") + assert parameter["type"] == "string" + assert parameter["in"] == "header" + assert parameter["required"] is True + assert parameter["description"] == "A required header" + + parameter = get_param("X-Header-2") + assert parameter["type"] == "array" + assert parameter["in"] == "header" + assert parameter["items"]["type"] == "integer" + assert parameter["description"] == "Another header" + assert parameter["collectionFormat"] == "csv" + + parameter = get_param("X-Header-3") + assert parameter["type"] == "integer" + assert parameter["in"] == "header" + + parameter = get_param("X-Header-4") + assert parameter["type"] == "boolean" + assert parameter["in"] == "header" + + def test_description(self, api, client): + @api.route( + "/description/", + endpoint="description", + doc={ + "description": "Parent description.", + "delete": {"description": "A delete operation"}, + }, + ) + class ResourceWithDescription(restx.Resource): + @api.doc(description="Some details") + def get(self): + return {} + + def post(self): + """ + Do something. + + Extra description + """ + return {} + + def put(self): + """No description (only summary)""" + + def delete(self): + """No description (only summary)""" + + @api.route("/descriptionless/", endpoint="descriptionless") + class ResourceWithoutDescription(restx.Resource): + def get(self): + """No description (only summary)""" + return {} + + data = client.get_specs() + + description = lambda m: data["paths"]["/description/"][m]["description"] # noqa + + assert description("get") == dedent( + """\ + Parent description. + Some details""" + ) + + assert description("post") == dedent( + """\ + Parent description. + Extra description""" + ) + + assert description("delete") == dedent( + """\ + Parent description. + A delete operation""" + ) + + assert description("put") == "Parent description." + assert "description" not in data["paths"]["/descriptionless/"]["get"] + + def test_operation_id(self, api, client): + @api.route("/test/", endpoint="test") + class TestResource(restx.Resource): + @api.doc(id="get_objects") + def get(self): + return {} + + def post(self): + return {} + + data = client.get_specs() + path = data["paths"]["/test/"] + + assert path["get"]["operationId"] == "get_objects" + assert path["post"]["operationId"] == "post_test_resource" + + def test_operation_id_shortcut(self, api, client): + @api.route("/test/", endpoint="test") + class TestResource(restx.Resource): + @api.doc("get_objects") + def get(self): + return {} + + data = client.get_specs() + path = data["paths"]["/test/"] + + assert path["get"]["operationId"] == "get_objects" + + def test_custom_default_operation_id(self, app, client): + def default_id(resource, method): + return "{0}{1}".format(method, resource) + + api = restx.Api(app, default_id=default_id) + + @api.route("/test/", endpoint="test") + class TestResource(restx.Resource): + @api.doc(id="get_objects") + def get(self): + return {} + + def post(self): + return {} + + data = client.get_specs() + path = data["paths"]["/test/"] + + assert path["get"]["operationId"] == "get_objects" + assert path["post"]["operationId"] == "postTestResource" + + @pytest.mark.api(default_id=lambda r, m: "{0}{1}".format(m, r)) + def test_custom_default_operation_id_blueprint(self, api, client): + @api.route("/test/", endpoint="test") + class TestResource(restx.Resource): + @api.doc(id="get_objects") + def get(self): + return {} + + def post(self): + return {} + + data = client.get_specs() + path = data["paths"]["/test/"] + + assert path["get"]["operationId"] == "get_objects" + assert path["post"]["operationId"] == "postTestResource" + + def test_model_primitive_types(self, api, client): + @api.route("/model-int/") + class ModelInt(restx.Resource): + @api.doc(model=int) + def get(self): + return {} + + data = client.get_specs() + + assert "definitions" not in data + assert data["paths"]["/model-int/"]["get"]["responses"] == { + "200": {"description": "Success", "schema": {"type": "integer"}} + } + + def test_model_as_flat_dict(self, api, client): + fields = api.model( + "Person", + { + "name": restx.fields.String, + "age": restx.fields.Integer, + "birthdate": restx.fields.DateTime, + }, + ) + + @api.route("/model-as-dict/") + class ModelAsDict(restx.Resource): + @api.doc(model=fields) + def get(self): + return {} + + @api.doc(model="Person") + def post(self): + return {} + + data = client.get_specs() + + assert "definitions" in data + assert "Person" in data["definitions"] + + path = data["paths"]["/model-as-dict/"] + assert ( + path["get"]["responses"]["200"]["schema"]["$ref"] == "#/definitions/Person" + ) + assert ( + path["post"]["responses"]["200"]["schema"]["$ref"] == "#/definitions/Person" + ) + + def test_model_as_nested_dict(self, api, client): + address_fields = api.model( + "Address", + { + "road": restx.fields.String, + }, + ) + + fields = api.model("Person", {"address": restx.fields.Nested(address_fields)}) + + @api.route("/model-as-dict/") + class ModelAsDict(restx.Resource): + @api.doc(model=fields) + def get(self): + return {} + + @api.doc(model="Person") + def post(self): + return {} + + data = client.get_specs() + + assert "definitions" in data + assert "Person" in data["definitions"] + assert data["definitions"]["Person"] == { + "properties": { + "address": {"$ref": "#/definitions/Address"}, + }, + "type": "object", + } + + assert "Address" in data["definitions"] + assert data["definitions"]["Address"] == { + "properties": { + "road": {"type": "string"}, + }, + "type": "object", + } + + path = data["paths"]["/model-as-dict/"] + assert ( + path["get"]["responses"]["200"]["schema"]["$ref"] == "#/definitions/Person" + ) + assert ( + path["post"]["responses"]["200"]["schema"]["$ref"] == "#/definitions/Person" + ) + + def test_model_as_nested_dict_with_details(self, api, client): + address_fields = api.model( + "Address", + { + "road": restx.fields.String, + }, + ) + + fields = api.model( + "Person", + { + "address": restx.fields.Nested( + address_fields, description="description", readonly=True + ) + }, + ) + + @api.route("/model-as-dict/") + class ModelAsDict(restx.Resource): + @api.doc(model=fields) + def get(self): + return {} + + @api.doc(model="Person") + def post(self): + return {} + + data = client.get_specs() + + assert "definitions" in data + assert "Person" in data["definitions"] + assert data["definitions"]["Person"] == { + "properties": { + "address": { + "description": "description", + "readOnly": True, + "allOf": [{"$ref": "#/definitions/Address"}], + }, + }, + "type": "object", + } + + assert "Address" in data["definitions"] + assert data["definitions"]["Address"] == { + "properties": { + "road": {"type": "string"}, + }, + "type": "object", + } + + def test_model_as_flat_dict_with_marchal_decorator(self, api, client): + fields = api.model( + "Person", + { + "name": restx.fields.String, + "age": restx.fields.Integer, + "birthdate": restx.fields.DateTime, + }, + ) + + @api.route("/model-as-dict/") + class ModelAsDict(restx.Resource): + @api.marshal_with(fields) + def get(self): + return {} + + data = client.get_specs() + + assert "definitions" in data + assert "Person" in data["definitions"] + + responses = data["paths"]["/model-as-dict/"]["get"]["responses"] + assert responses == { + "200": { + "description": "Success", + "schema": {"$ref": "#/definitions/Person"}, + } + } + + def test_model_with_non_uri_chars_in_name(self, api, client): + # name will be encoded as 'Person%2F%2F%3Flots%7B%7D%20of%20%26illegals%40%60' + name = "Person//?lots{} of &illegals@`" + fields = api.model(name, {}) + + @api.route("/model-bad-uri/") + class ModelBadUri(restx.Resource): + @api.doc(model=fields) + def get(self): + return {} + + @api.response(201, "", model=name) + def post(self): + return {} + + data = client.get_specs() + + assert "definitions" in data + assert name in data["definitions"] + + path = data["paths"]["/model-bad-uri/"] + assert ( + path["get"]["responses"]["200"]["schema"]["$ref"] + == "#/definitions/Person%2F%2F%3Flots%7B%7D%20of%20%26illegals%40%60" + ) + assert ( + path["post"]["responses"]["201"]["schema"]["$ref"] + == "#/definitions/Person%2F%2F%3Flots%7B%7D%20of%20%26illegals%40%60" + ) + + def test_marchal_decorator_with_code(self, api, client): + fields = api.model( + "Person", + { + "name": restx.fields.String, + "age": restx.fields.Integer, + "birthdate": restx.fields.DateTime, + }, + ) + + @api.route("/model-as-dict/") + class ModelAsDict(restx.Resource): + @api.marshal_with(fields, code=204) + def delete(self): + return {} + + data = client.get_specs() + + assert "definitions" in data + assert "Person" in data["definitions"] + + responses = data["paths"]["/model-as-dict/"]["delete"]["responses"] + assert responses == { + "204": { + "description": "Success", + "schema": {"$ref": "#/definitions/Person"}, + } + } + + def test_marchal_decorator_with_description(self, api, client): + person = api.model( + "Person", + { + "name": restx.fields.String, + "age": restx.fields.Integer, + "birthdate": restx.fields.DateTime, + }, + ) + + @api.route("/model-as-dict/") + class ModelAsDict(restx.Resource): + @api.marshal_with(person, description="Some details") + def get(self): + return {} + + data = client.get_specs() + + assert "definitions" in data + assert "Person" in data["definitions"] + + responses = data["paths"]["/model-as-dict/"]["get"]["responses"] + assert responses == { + "200": { + "description": "Some details", + "schema": {"$ref": "#/definitions/Person"}, + } + } + + def test_marhsal_decorator_with_envelope(self, api, client): + person = api.model( + "Person", + { + "name": restx.fields.String, + "age": restx.fields.Integer, + "birthdate": restx.fields.DateTime, + }, + ) + + @api.route("/model-as-dict/") + class ModelAsDict(restx.Resource): + @api.marshal_with(person, envelope="person") + def get(self): + return {} + + data = client.get_specs() + + assert "definitions" in data + assert "Person" in data["definitions"] + + responses = data["paths"]["/model-as-dict/"]["get"]["responses"] + assert responses == { + "200": { + "description": "Success", + "schema": {"properties": {"person": {"$ref": "#/definitions/Person"}}}, + } + } + + def test_model_as_flat_dict_with_marchal_decorator_list(self, api, client): + fields = api.model( + "Person", + { + "name": restx.fields.String, + "age": restx.fields.Integer, + "birthdate": restx.fields.DateTime, + }, + ) + + @api.route("/model-as-dict/") + class ModelAsDict(restx.Resource): + @api.marshal_with(fields, as_list=True) + def get(self): + return {} + + data = client.get_specs() + + assert "definitions" in data + assert "Person" in data["definitions"] + assert data["definitions"]["Person"] == { + "properties": { + "name": {"type": "string"}, + "age": {"type": "integer"}, + "birthdate": {"type": "string", "format": "date-time"}, + }, + "type": "object", + } + + path = data["paths"]["/model-as-dict/"] + assert path["get"]["responses"]["200"]["schema"] == { + "type": "array", + "items": {"$ref": "#/definitions/Person"}, + } + + def test_model_as_flat_dict_with_marchal_decorator_list_alt(self, api, client): + fields = api.model( + "Person", + { + "name": restx.fields.String, + "age": restx.fields.Integer, + "birthdate": restx.fields.DateTime, + }, + ) + + @api.route("/model-as-dict/") + class ModelAsDict(restx.Resource): + @api.marshal_list_with(fields) + def get(self): + return {} + + data = client.get_specs() + + assert "definitions" in data + assert "Person" in data["definitions"] + + path = data["paths"]["/model-as-dict/"] + assert path["get"]["responses"]["200"]["schema"] == { + "type": "array", + "items": {"$ref": "#/definitions/Person"}, + } + + def test_model_as_flat_dict_with_marchal_decorator_list_kwargs(self, api, client): + fields = api.model( + "Person", + { + "name": restx.fields.String, + "age": restx.fields.Integer, + "birthdate": restx.fields.DateTime, + }, + ) + + @api.route("/model-as-dict/") + class ModelAsDict(restx.Resource): + @api.marshal_list_with(fields, code=201, description="Some details") + def get(self): + return {} + + data = client.get_specs() + + assert "definitions" in data + assert "Person" in data["definitions"] + + path = data["paths"]["/model-as-dict/"] + assert path["get"]["responses"] == { + "201": { + "description": "Some details", + "schema": { + "type": "array", + "items": {"$ref": "#/definitions/Person"}, + }, + } + } + + def test_model_as_dict_with_list(self, api, client): + fields = api.model( + "Person", + { + "name": restx.fields.String, + "age": restx.fields.Integer, + "tags": restx.fields.List(restx.fields.String), + }, + ) + + @api.route("/model-with-list/") + class ModelAsDict(restx.Resource): + @api.doc(model=fields) + def get(self): + return {} + + data = client.get_specs() + + assert "definitions" in data + assert "Person" in data["definitions"] + assert data["definitions"]["Person"] == { + "properties": { + "name": {"type": "string"}, + "age": {"type": "integer"}, + "tags": {"type": "array", "items": {"type": "string"}}, + }, + "type": "object", + } + + path = data["paths"]["/model-with-list/"] + assert path["get"]["responses"]["200"]["schema"] == { + "$ref": "#/definitions/Person" + } + + def test_model_as_nested_dict_with_list(self, api, client): + address = api.model( + "Address", + { + "road": restx.fields.String, + }, + ) + + person = api.model( + "Person", + { + "name": restx.fields.String, + "age": restx.fields.Integer, + "birthdate": restx.fields.DateTime, + "addresses": restx.fields.List(restx.fields.Nested(address)), + }, + ) + + @api.route("/model-with-list/") + class ModelAsDict(restx.Resource): + @api.doc(model=person) + def get(self): + return {} + + data = client.get_specs() + + assert "definitions" in data + assert "Person" in data["definitions"] + assert "Address" in data["definitions"] + + def test_model_list_of_primitive_types(self, api, client): + @api.route("/model-list/") + class ModelAsDict(restx.Resource): + @api.doc(model=[int]) + def get(self): + return {} + + @api.doc(model=[str]) + def post(self): + return {} + + data = client.get_specs() + + assert "definitions" not in data + + path = data["paths"]["/model-list/"] + assert path["get"]["responses"]["200"]["schema"] == { + "type": "array", + "items": {"type": "integer"}, + } + assert path["post"]["responses"]["200"]["schema"] == { + "type": "array", + "items": {"type": "string"}, + } + + def test_model_list_as_flat_dict(self, api, client): + fields = api.model( + "Person", + { + "name": restx.fields.String, + "age": restx.fields.Integer, + "birthdate": restx.fields.DateTime, + }, + ) + + @api.route("/model-as-dict/") + class ModelAsDict(restx.Resource): + @api.doc(model=[fields]) + def get(self): + return {} + + @api.doc(model=["Person"]) + def post(self): + return {} + + data = client.get_specs() + + assert "definitions" in data + assert "Person" in data["definitions"] + + path = data["paths"]["/model-as-dict/"] + for method in "get", "post": + assert path[method]["responses"]["200"]["schema"] == { + "type": "array", + "items": {"$ref": "#/definitions/Person"}, + } + + def test_model_doc_on_class(self, api, client): + fields = api.model( + "Person", + { + "name": restx.fields.String, + "age": restx.fields.Integer, + "birthdate": restx.fields.DateTime, + }, + ) + + @api.route("/model-as-dict/") + @api.doc(model=fields) + class ModelAsDict(restx.Resource): + def get(self): + return {} + + def post(self): + return {} + + data = client.get_specs() + assert "definitions" in data + assert "Person" in data["definitions"] + + path = data["paths"]["/model-as-dict/"] + for method in "get", "post": + assert path[method]["responses"]["200"]["schema"] == { + "$ref": "#/definitions/Person" + } + + def test_model_doc_for_method_on_class(self, api, client): + fields = api.model( + "Person", + { + "name": restx.fields.String, + "age": restx.fields.Integer, + "birthdate": restx.fields.DateTime, + }, + ) + + @api.route("/model-as-dict/") + @api.doc(get={"model": fields}) + class ModelAsDict(restx.Resource): + def get(self): + return {} + + def post(self): + return {} + + data = client.get_specs() + assert "definitions" in data + assert "Person" in data["definitions"] + + path = data["paths"]["/model-as-dict/"] + assert path["get"]["responses"]["200"]["schema"] == { + "$ref": "#/definitions/Person" + } + assert "schema" not in path["post"]["responses"]["200"] + + def test_model_with_discriminator(self, api, client): + fields = api.model( + "Person", + { + "name": restx.fields.String(discriminator=True), + "age": restx.fields.Integer, + }, + ) + + @api.route("/model-with-discriminator/") + class ModelAsDict(restx.Resource): + @api.marshal_with(fields) + def get(self): + return {} + + data = client.get_specs() + + assert "definitions" in data + assert "Person" in data["definitions"] + assert data["definitions"]["Person"] == { + "properties": { + "name": {"type": "string"}, + "age": {"type": "integer"}, + }, + "discriminator": "name", + "required": ["name"], + "type": "object", + } + + def test_model_with_discriminator_override_require(self, api, client): + fields = api.model( + "Person", + { + "name": restx.fields.String(discriminator=True, required=False), + "age": restx.fields.Integer, + }, + ) + + @api.route("/model-with-discriminator/") + class ModelAsDict(restx.Resource): + @api.marshal_with(fields) + def get(self): + return {} + + data = client.get_specs() + + assert "definitions" in data + assert "Person" in data["definitions"] + assert data["definitions"]["Person"] == { + "properties": { + "name": {"type": "string"}, + "age": {"type": "integer"}, + }, + "discriminator": "name", + "required": ["name"], + "type": "object", + } + + def test_model_not_found(self, api, client): + @api.route("/model-not-found/") + class ModelAsDict(restx.Resource): + @api.doc(model="NotFound") + def get(self): + return {} + + client.get_specs(status=500) + + def test_recursive_model(self, api, client): + fields = api.model( + "Person", + { + "name": restx.fields.String, + "age": restx.fields.Integer, + "birthdate": restx.fields.DateTime, + }, + ) + + fields["children"] = restx.fields.List( + restx.fields.Nested(fields), + default=[], + ) + + @api.route("/recursive-model/") + @api.doc(get={"model": fields}) + class ModelAsDict(restx.Resource): + @api.marshal_with(fields) + def get(self): + return {} + + client.get_specs(status=200) + + def test_specs_no_duplicate_response_keys(self, api, client): + """ + This tests that the swagger.json document will not be written with duplicate object keys + due to the coercion of dict keys to string. The last @api.response should win. + """ + + # Note the use of a strings '404' and '200' in class decorators as opposed to ints in method decorators. + @api.response("404", "Not Found") + class BaseResource(restx.Resource): + def get(self): + pass + + model = api.model( + "SomeModel", + { + "message": restx.fields.String, + }, + ) + + @api.route("/test/") + @api.response("200", "Success") + class TestResource(BaseResource): + # @api.marshal_with also yields a response + @api.marshal_with(model, code=200, description="Success on method") + @api.response(404, "Not Found on method") + def get(self): + {} + + data = client.get_specs("") + paths = data["paths"] + + op = paths["/test/"]["get"] + print(op["responses"]) + assert op["responses"] == { + "200": { + "description": "Success on method", + "schema": {"$ref": "#/definitions/SomeModel"}, + }, + "404": { + "description": "Not Found on method", + }, + } + + def test_clone(self, api, client): + parent = api.model( + "Person", + { + "name": restx.fields.String, + "age": restx.fields.Integer, + "birthdate": restx.fields.DateTime, + }, + ) + + child = api.clone( + "Child", + parent, + { + "extra": restx.fields.String, + }, + ) + + @api.route("/extend/") + class ModelAsDict(restx.Resource): + @api.doc(model=child) + def get(self): + return {} + + @api.doc(model="Child") + def post(self): + return {} + + data = client.get_specs() + + assert "definitions" in data + assert "Person" not in data["definitions"] + assert "Child" in data["definitions"] + + path = data["paths"]["/extend/"] + assert ( + path["get"]["responses"]["200"]["schema"]["$ref"] == "#/definitions/Child" + ) + assert ( + path["post"]["responses"]["200"]["schema"]["$ref"] == "#/definitions/Child" + ) + + def test_inherit(self, api, client): + parent = api.model( + "Person", + { + "name": restx.fields.String, + "age": restx.fields.Integer, + }, + ) + + child = api.inherit( + "Child", + parent, + { + "extra": restx.fields.String, + }, + ) + + @api.route("/inherit/") + class ModelAsDict(restx.Resource): + @api.marshal_with(child) + def get(self): + return { + "name": "John", + "age": 42, + "extra": "test", + } + + @api.doc(model="Child") + def post(self): + return {} + + data = client.get_specs() + + assert "definitions" in data + assert "Person" in data["definitions"] + assert "Child" in data["definitions"] + assert data["definitions"]["Person"] == { + "properties": { + "name": {"type": "string"}, + "age": {"type": "integer"}, + }, + "type": "object", + } + assert data["definitions"]["Child"] == { + "allOf": [ + {"$ref": "#/definitions/Person"}, + {"properties": {"extra": {"type": "string"}}, "type": "object"}, + ] + } + + path = data["paths"]["/inherit/"] + assert ( + path["get"]["responses"]["200"]["schema"]["$ref"] == "#/definitions/Child" + ) + assert ( + path["post"]["responses"]["200"]["schema"]["$ref"] == "#/definitions/Child" + ) + + data = client.get_json("/inherit/") + assert data == { + "name": "John", + "age": 42, + "extra": "test", + } + + def test_inherit_inline(self, api, client): + parent = api.model( + "Person", + { + "name": restx.fields.String, + "age": restx.fields.Integer, + }, + ) + + child = api.inherit( + "Child", + parent, + { + "extra": restx.fields.String, + }, + ) + + output = api.model( + "Output", + { + "child": restx.fields.Nested(child), + "children": restx.fields.List(restx.fields.Nested(child)), + }, + ) + + @api.route("/inherit/") + class ModelAsDict(restx.Resource): + @api.marshal_with(output) + def get(self): + return { + "child": { + "name": "John", + "age": 42, + "extra": "test", + }, + "children": [ + { + "name": "John", + "age": 42, + "extra": "test", + }, + { + "name": "Doe", + "age": 33, + "extra": "test2", + }, + ], + } + + data = client.get_specs() + + assert "definitions" in data + assert "Person" in data["definitions"] + assert "Child" in data["definitions"] + + data = client.get_json("/inherit/") + assert data == { + "child": { + "name": "John", + "age": 42, + "extra": "test", + }, + "children": [ + { + "name": "John", + "age": 42, + "extra": "test", + }, + { + "name": "Doe", + "age": 33, + "extra": "test2", + }, + ], + } + + def test_polymorph_inherit(self, api, client): + class Child1: + pass + + class Child2: + pass + + parent = api.model( + "Person", + { + "name": restx.fields.String, + "age": restx.fields.Integer, + }, + ) + + child1 = api.inherit( + "Child1", + parent, + { + "extra1": restx.fields.String, + }, + ) + + child2 = api.inherit( + "Child2", + parent, + { + "extra2": restx.fields.String, + }, + ) + + mapping = { + Child1: child1, + Child2: child2, + } + + output = api.model("Output", {"child": restx.fields.Polymorph(mapping)}) + + @api.route("/polymorph/") + class ModelAsDict(restx.Resource): + @api.marshal_with(output) + def get(self): + return {} + + data = client.get_specs() + + assert "definitions" in data + assert "Person" in data["definitions"] + assert "Child1" in data["definitions"] + assert "Child2" in data["definitions"] + assert "Output" in data["definitions"] + + path = data["paths"]["/polymorph/"] + assert ( + path["get"]["responses"]["200"]["schema"]["$ref"] == "#/definitions/Output" + ) + + def test_polymorph_inherit_list(self, api, client): + class Child1(object): + name = "Child1" + extra1 = "extra1" + + class Child2(object): + name = "Child2" + extra2 = "extra2" + + parent = api.model( + "Person", + { + "name": restx.fields.String, + }, + ) + + child1 = api.inherit( + "Child1", + parent, + { + "extra1": restx.fields.String, + }, + ) + + child2 = api.inherit( + "Child2", + parent, + { + "extra2": restx.fields.String, + }, + ) + + mapping = { + Child1: child1, + Child2: child2, + } + + output = api.model( + "Output", {"children": restx.fields.List(restx.fields.Polymorph(mapping))} + ) + + @api.route("/polymorph/") + class ModelAsDict(restx.Resource): + @api.marshal_with(output) + def get(self): + return {"children": [Child1(), Child2()]} + + data = client.get_specs() + + assert "definitions" in data + assert "Person" in data["definitions"] + assert "Child1" in data["definitions"] + assert "Child2" in data["definitions"] + assert "Output" in data["definitions"] + + path = data["paths"]["/polymorph/"] + assert ( + path["get"]["responses"]["200"]["schema"]["$ref"] == "#/definitions/Output" + ) + + data = client.get_json("/polymorph/") + assert data == { + "children": [ + { + "name": "Child1", + "extra1": "extra1", + }, + { + "name": "Child2", + "extra2": "extra2", + }, + ] + } + + def test_expect_model(self, api, client): + person = api.model( + "Person", + { + "name": restx.fields.String, + "age": restx.fields.Integer, + "birthdate": restx.fields.DateTime, + }, + ) + + @api.route("/model-as-dict/") + class ModelAsDict(restx.Resource): + @api.expect(person) + def post(self): + return {} + + data = client.get_specs() + + assert "definitions" in data + assert "Person" in data["definitions"] + assert data["definitions"]["Person"] == { + "properties": { + "name": {"type": "string"}, + "age": {"type": "integer"}, + "birthdate": {"type": "string", "format": "date-time"}, + }, + "type": "object", + } + + op = data["paths"]["/model-as-dict/"]["post"] + assert len(op["parameters"]) == 1 + + parameter = op["parameters"][0] + assert parameter == { + "name": "payload", + "in": "body", + "required": True, + "schema": {"$ref": "#/definitions/Person"}, + } + assert "description" not in parameter + + def test_body_model_shortcut(self, api, client): + fields = api.model( + "Person", + { + "name": restx.fields.String, + "age": restx.fields.Integer, + "birthdate": restx.fields.DateTime, + }, + ) + + @api.route("/model-as-dict/") + class ModelAsDict(restx.Resource): + @api.doc(model="Person") + @api.expect(fields) + def post(self): + return {} + + data = client.get_specs() + + assert "definitions" in data + assert "Person" in data["definitions"] + assert data["definitions"]["Person"] == { + "properties": { + "name": {"type": "string"}, + "age": {"type": "integer"}, + "birthdate": {"type": "string", "format": "date-time"}, + }, + "type": "object", + } + + op = data["paths"]["/model-as-dict/"]["post"] + assert op["responses"]["200"]["schema"]["$ref"] == "#/definitions/Person" + + assert len(op["parameters"]) == 1 + + parameter = op["parameters"][0] + assert parameter == { + "name": "payload", + "in": "body", + "required": True, + "schema": {"$ref": "#/definitions/Person"}, + } + assert "description" not in parameter + + def test_expect_model_list(self, api, client): + model = api.model( + "Person", + { + "name": restx.fields.String, + "age": restx.fields.Integer, + "birthdate": restx.fields.DateTime, + }, + ) + + @api.route("/model-list/") + class ModelAsDict(restx.Resource): + @api.expect([model]) + def post(self): + return {} + + data = client.get_specs() + + assert "definitions" in data + assert "Person" in data["definitions"] + assert data["definitions"]["Person"] == { + "properties": { + "name": {"type": "string"}, + "age": {"type": "integer"}, + "birthdate": {"type": "string", "format": "date-time"}, + }, + "type": "object", + } + + op = data["paths"]["/model-list/"]["post"] + parameter = op["parameters"][0] + + assert parameter == { + "name": "payload", + "in": "body", + "required": True, + "schema": { + "type": "array", + "items": {"$ref": "#/definitions/Person"}, + }, + } + + def test_both_model_and_parser_from_expect(self, api, client): + parser = api.parser() + parser.add_argument("param", type=int, help="Some param") + + person = api.model( + "Person", + { + "name": restx.fields.String, + "age": restx.fields.Integer, + "birthdate": restx.fields.DateTime, + }, + ) + + @api.route("/with-parser/", endpoint="with-parser") + class WithParserResource(restx.Resource): + @api.expect(parser, person) + def get(self): + return {} + + data = client.get_specs() + + assert "definitions" in data + assert "Person" in data["definitions"] + assert data["definitions"]["Person"] == { + "properties": { + "name": {"type": "string"}, + "age": {"type": "integer"}, + "birthdate": {"type": "string", "format": "date-time"}, + }, + "type": "object", + } + + assert "/with-parser/" in data["paths"] + + op = data["paths"]["/with-parser/"]["get"] + assert len(op["parameters"]) == 2 + + parameters = dict((p["in"], p) for p in op["parameters"]) + + parameter = parameters["query"] + assert parameter["name"] == "param" + assert parameter["type"] == "integer" + assert parameter["in"] == "query" + assert parameter["description"] == "Some param" + + parameter = parameters["body"] + assert parameter == { + "name": "payload", + "in": "body", + "required": True, + "schema": {"$ref": "#/definitions/Person"}, + } + + def test_expect_primitive_list(self, api, client): + @api.route("/model-list/") + class ModelAsDict(restx.Resource): + @api.expect([restx.fields.String]) + def post(self): + return {} + + data = client.get_specs() + + op = data["paths"]["/model-list/"]["post"] + parameter = op["parameters"][0] + assert parameter == { + "name": "payload", + "in": "body", + "required": True, + "schema": { + "type": "array", + "items": {"type": "string"}, + }, + } + + def test_body_model_list(self, api, client): + fields = api.model( + "Person", + { + "name": restx.fields.String, + "age": restx.fields.Integer, + "birthdate": restx.fields.DateTime, + }, + ) + + @api.route("/model-list/") + class ModelAsDict(restx.Resource): + @api.expect([fields]) + def post(self): + return {} + + data = client.get_specs() + + assert "definitions" in data + assert "Person" in data["definitions"] + assert data["definitions"]["Person"] == { + "properties": { + "name": {"type": "string"}, + "age": {"type": "integer"}, + "birthdate": {"type": "string", "format": "date-time"}, + }, + "type": "object", + } + + op = data["paths"]["/model-list/"]["post"] + parameter = op["parameters"][0] + + assert parameter == { + "name": "payload", + "in": "body", + "required": True, + "schema": { + "type": "array", + "items": {"$ref": "#/definitions/Person"}, + }, + } + + def test_expect_model_with_description(self, api, client): + person = api.model( + "Person", + { + "name": restx.fields.String, + "age": restx.fields.Integer, + "birthdate": restx.fields.DateTime, + }, + ) + + @api.route("/model-as-dict/") + class ModelAsDict(restx.Resource): + @api.expect((person, "Body description")) + def post(self): + return {} + + data = client.get_specs() + + assert "definitions" in data + assert "Person" in data["definitions"] + assert data["definitions"]["Person"] == { + "properties": { + "name": {"type": "string"}, + "age": {"type": "integer"}, + "birthdate": {"type": "string", "format": "date-time"}, + }, + "type": "object", + } + + op = data["paths"]["/model-as-dict/"]["post"] + assert len(op["parameters"]) == 1 + + parameter = op["parameters"][0] + + assert parameter == { + "name": "payload", + "in": "body", + "required": True, + "description": "Body description", + "schema": {"$ref": "#/definitions/Person"}, + } + + def test_authorizations(self, app, client): + restx.Api( + app, + authorizations={ + "apikey": {"type": "apiKey", "in": "header", "name": "X-API"} + }, + ) + + # @api.route('/authorizations/') + # class ModelAsDict(restx.Resource): + # def get(self): + # return {} + + # def post(self): + # return {} + + data = client.get_specs() + assert "securityDefinitions" in data + assert "security" not in data + + # path = data['paths']['/authorizations/'] + # assert 'security' not in path['get'] + # assert path['post']['security'] == {'apikey': []} + + def test_single_root_security_string(self, app, client): + api = restx.Api( + app, + security="apikey", + authorizations={ + "apikey": {"type": "apiKey", "in": "header", "name": "X-API"} + }, + ) + + @api.route("/authorizations/") + class ModelAsDict(restx.Resource): + def post(self): + return {} + + data = client.get_specs() + assert data["securityDefinitions"] == { + "apikey": {"type": "apiKey", "in": "header", "name": "X-API"} + } + assert data["security"] == [{"apikey": []}] + + op = data["paths"]["/authorizations/"]["post"] + assert "security" not in op + + def test_single_root_security_object(self, app, client): + security_definitions = { + "oauth2": { + "type": "oauth2", + "flow": "accessCode", + "tokenUrl": "https://somewhere.com/token", + "scopes": { + "read": "Grant read-only access", + "write": "Grant read-write access", + }, + }, + "implicit": { + "type": "oauth2", + "flow": "implicit", + "tokenUrl": "https://somewhere.com/token", + "scopes": { + "read": "Grant read-only access", + "write": "Grant read-write access", + }, + }, + } + + api = restx.Api( + app, + security={"oauth2": "read", "implicit": ["read", "write"]}, + authorizations=security_definitions, + ) + + @api.route("/authorizations/") + class ModelAsDict(restx.Resource): + def post(self): + return {} + + data = client.get_specs() + assert data["securityDefinitions"] == security_definitions + assert data["security"] == [{"oauth2": ["read"], "implicit": ["read", "write"]}] + + op = data["paths"]["/authorizations/"]["post"] + assert "security" not in op + + def test_root_security_as_list(self, app, client): + security_definitions = { + "apikey": {"type": "apiKey", "in": "header", "name": "X-API"}, + "oauth2": { + "type": "oauth2", + "flow": "accessCode", + "tokenUrl": "https://somewhere.com/token", + "scopes": { + "read": "Grant read-only access", + "write": "Grant read-write access", + }, + }, + } + api = restx.Api( + app, + security=["apikey", {"oauth2": "read"}], + authorizations=security_definitions, + ) + + @api.route("/authorizations/") + class ModelAsDict(restx.Resource): + def post(self): + return {} + + data = client.get_specs() + assert data["securityDefinitions"] == security_definitions + assert data["security"] == [{"apikey": []}, {"oauth2": ["read"]}] + + op = data["paths"]["/authorizations/"]["post"] + assert "security" not in op + + def test_method_security(self, app, client): + api = restx.Api( + app, + authorizations={ + "apikey": {"type": "apiKey", "in": "header", "name": "X-API"} + }, + ) + + @api.route("/authorizations/") + class ModelAsDict(restx.Resource): + @api.doc(security=["apikey"]) + def get(self): + return {} + + @api.doc(security="apikey") + def post(self): + return {} + + data = client.get_specs() + assert data["securityDefinitions"] == { + "apikey": {"type": "apiKey", "in": "header", "name": "X-API"} + } + assert "security" not in data + + path = data["paths"]["/authorizations/"] + for method in "get", "post": + assert path[method]["security"] == [{"apikey": []}] + + def test_security_override(self, app, client): + security_definitions = { + "apikey": {"type": "apiKey", "in": "header", "name": "X-API"}, + "oauth2": { + "type": "oauth2", + "flow": "accessCode", + "tokenUrl": "https://somewhere.com/token", + "scopes": { + "read": "Grant read-only access", + "write": "Grant read-write access", + }, + }, + } + api = restx.Api( + app, + security=["apikey", {"oauth2": "read"}], + authorizations=security_definitions, + ) + + @api.route("/authorizations/") + class ModelAsDict(restx.Resource): + @api.doc(security=[{"oauth2": ["read", "write"]}]) + def get(self): + return {} + + data = client.get_specs() + assert data["securityDefinitions"] == security_definitions + + op = data["paths"]["/authorizations/"]["get"] + assert op["security"] == [{"oauth2": ["read", "write"]}] + + def test_security_nullify(self, app, client): + security_definitions = { + "apikey": {"type": "apiKey", "in": "header", "name": "X-API"}, + "oauth2": { + "type": "oauth2", + "flow": "accessCode", + "tokenUrl": "https://somewhere.com/token", + "scopes": { + "read": "Grant read-only access", + "write": "Grant read-write access", + }, + }, + } + api = restx.Api( + app, + security=["apikey", {"oauth2": "read"}], + authorizations=security_definitions, + ) + + @api.route("/authorizations/") + class ModelAsDict(restx.Resource): + @api.doc(security=[]) + def get(self): + return {} + + @api.doc(security=None) + def post(self): + return {} + + data = client.get_specs() + assert data["securityDefinitions"] == security_definitions + + path = data["paths"]["/authorizations/"] + for method in "get", "post": + assert path[method]["security"] == [] + + def test_hidden_resource(self, api, client): + @api.route("/test/", endpoint="test", doc=False) + class TestResource(restx.Resource): + def get(self): + """ + GET operation + """ + return {} + + @api.hide + @api.route("/test2/", endpoint="test2") + class TestResource2(restx.Resource): + def get(self): + """ + GET operation + """ + return {} + + @api.doc(False) + @api.route("/test3/", endpoint="test3") + class TestResource3(restx.Resource): + def get(self): + """ + GET operation + """ + return {} + + data = client.get_specs() + for path in "/test/", "/test2/", "/test3/": + assert path not in data["paths"] + + resp = client.get(path) + assert resp.status_code == 200 + + def test_hidden_resource_from_namespace(self, api, client): + ns = api.namespace("ns") + + @ns.route("/test/", endpoint="test", doc=False) + class TestResource(restx.Resource): + def get(self): + """ + GET operation + """ + return {} + + data = client.get_specs() + assert "/ns/test/" not in data["paths"] + + resp = client.get("/ns/test/") + assert resp.status_code == 200 + + def test_hidden_methods(self, api, client): + @api.route("/test/", endpoint="test") + @api.doc(delete=False) + class TestResource(restx.Resource): + def get(self): + """ + GET operation + """ + return {} + + @api.doc(False) + def post(self): + """POST operation. + + Should be ignored + """ + return {} + + @api.hide + def put(self): + """PUT operation. Should be ignored""" + return {} + + def delete(self): + return {} + + data = client.get_specs() + path = data["paths"]["/test/"] + + assert "get" in path + assert "post" not in path + assert "put" not in path + + for method in "GET", "POST", "PUT": + resp = client.open("/test/", method=method) + assert resp.status_code == 200 + + def test_produces_method(self, api, client): + @api.route("/test/", endpoint="test") + class TestResource(restx.Resource): + def get(self): + pass + + @api.produces(["application/octet-stream"]) + def post(self): + pass + + data = client.get_specs() + + get_operation = data["paths"]["/test/"]["get"] + assert "produces" not in get_operation + + post_operation = data["paths"]["/test/"]["post"] + assert "produces" in post_operation + assert post_operation["produces"] == ["application/octet-stream"] + + def test_deprecated_resource(self, api, client): + @api.deprecated + @api.route("/test/", endpoint="test") + class TestResource(restx.Resource): + def get(self): + pass + + def post(self): + pass + + data = client.get_specs() + resource = data["paths"]["/test/"] + for operation in resource.values(): + assert "deprecated" in operation + assert operation["deprecated"] is True + + def test_deprecated_method(self, api, client): + @api.route("/test/", endpoint="test") + class TestResource(restx.Resource): + def get(self): + pass + + @api.deprecated + def post(self): + pass + + data = client.get_specs() + + get_operation = data["paths"]["/test/"]["get"] + assert "deprecated" not in get_operation + + post_operation = data["paths"]["/test/"]["post"] + assert "deprecated" in post_operation + assert post_operation["deprecated"] is True + + def test_vendor_as_kwargs(self, api, client): + @api.route("/vendor_fields", endpoint="vendor_fields") + class TestResource(restx.Resource): + @api.vendor(integration={"integration1": "1"}) + def get(self): + return {} + + data = client.get_specs() + + assert "/vendor_fields" in data["paths"] + + path = data["paths"]["/vendor_fields"]["get"] + + assert "x-integration" in path + + assert path["x-integration"] == {"integration1": "1"} + + def test_vendor_as_dict(self, api, client): + @api.route("/vendor_fields", endpoint="vendor_fields") + class TestResource(restx.Resource): + @api.vendor( + { + "x-some-integration": {"integration1": "1"}, + "another-integration": True, + }, + {"third-integration": True}, + ) + def get(self, age): + return {} + + data = client.get_specs() + + assert "/vendor_fields" in data["paths"] + + path = data["paths"]["/vendor_fields"]["get"] + assert "x-some-integration" in path + assert path["x-some-integration"] == {"integration1": "1"} + + assert "x-another-integration" in path + assert path["x-another-integration"] is True + + assert "x-third-integration" in path + assert path["x-third-integration"] is True + + def test_method_restrictions(self, api, client): + @api.route("/foo/bar", endpoint="foo") + @api.route("/bar", methods=["GET"], endpoint="bar") + class TestResource(restx.Resource): + def get(self): + pass + + def post(self): + pass + + data = client.get_specs() + + path = data["paths"]["/foo/bar"] + assert "get" in path + assert "post" in path + + path = data["paths"]["/bar"] + assert "get" in path + assert "post" not in path + + def test_multiple_routes_inherit_doc(self, api, client): + @api.route("/foo/bar") + @api.route("/bar") + @api.doc(description="an endpoint") + class TestResource(restx.Resource): + def get(self): + pass + + data = client.get_specs() + + path = data["paths"]["/foo/bar"] + assert path["get"]["description"] == "an endpoint" + + path = data["paths"]["/bar"] + assert path["get"]["description"] == "an endpoint" + + def test_multiple_routes_individual_doc(self, api, client): + @api.route("/foo/bar", doc={"description": "the same endpoint"}) + @api.route("/bar", doc={"description": "an endpoint"}) + class TestResource(restx.Resource): + def get(self): + pass + + data = client.get_specs() + + path = data["paths"]["/foo/bar"] + assert path["get"]["description"] == "the same endpoint" + + path = data["paths"]["/bar"] + assert path["get"]["description"] == "an endpoint" + + def test_multiple_routes_override_doc(self, api, client): + @api.route("/foo/bar", doc={"description": "the same endpoint"}) + @api.route("/bar") + @api.doc(description="an endpoint") + class TestResource(restx.Resource): + def get(self): + pass + + data = client.get_specs() + + path = data["paths"]["/foo/bar"] + assert path["get"]["description"] == "the same endpoint" + + path = data["paths"]["/bar"] + assert path["get"]["description"] == "an endpoint" + + def test_multiple_routes_no_doc_same_operationIds(self, api, client): + @api.route("/foo/bar") + @api.route("/bar") + class TestResource(restx.Resource): + def get(self): + pass + + data = client.get_specs() + + expected_operation_id = "get_test_resource" + + path = data["paths"]["/foo/bar"] + assert path["get"]["operationId"] == expected_operation_id + + path = data["paths"]["/bar"] + assert path["get"]["operationId"] == expected_operation_id + + def test_multiple_routes_with_doc_unique_operationIds(self, api, client): + @api.route( + "/foo/bar", + doc={"description": "I should be treated separately"}, + ) + @api.route("/bar") + class TestResource(restx.Resource): + def get(self): + pass + + data = client.get_specs() + + path = data["paths"]["/foo/bar"] + assert path["get"]["operationId"] == "get_test_resource_/foo/bar" + + path = data["paths"]["/bar"] + assert path["get"]["operationId"] == "get_test_resource" + + def test_mutltiple_routes_merge_doc(self, api, client): + @api.route("/foo/bar", doc={"description": "the same endpoint"}) + @api.route("/bar", doc={"description": False}) + @api.doc(security=[{"oauth2": ["read", "write"]}]) + class TestResource(restx.Resource): + def get(self): + pass + + data = client.get_specs() + + path = data["paths"]["/foo/bar"] + assert path["get"]["description"] == "the same endpoint" + assert path["get"]["security"] == [{"oauth2": ["read", "write"]}] + + path = data["paths"]["/bar"] + assert "description" not in path["get"] + assert path["get"]["security"] == [{"oauth2": ["read", "write"]}] + + def test_multiple_routes_deprecation(self, api, client): + @api.route("/foo/bar", doc={"deprecated": True}) + @api.route("/bar") + class TestResource(restx.Resource): + def get(self): + pass + + data = client.get_specs() + + path = data["paths"]["/foo/bar"] + assert path["get"]["deprecated"] is True + + path = data["paths"]["/bar"] + assert "deprecated" not in path["get"] + + @pytest.mark.parametrize("path_name", ["/name/{age}/", "/first-name/{age}/"]) + def test_multiple_routes_explicit_parameters_override(self, path_name, api, client): + @api.route("/name//", endpoint="by-name") + @api.route("/first-name//") + @api.doc( + params={ + "q": { + "type": "string", + "in": "query", + "description": "Overriden description", + }, + "age": {"description": "An age"}, + } + ) + class ByNameResource(restx.Resource): + @api.doc(params={"q": {"description": "A query string"}}) + def get(self, age): + return {} + + def post(self, age): + pass + + data = client.get_specs() + assert path_name in data["paths"] + + path = data["paths"][path_name] + assert len(path["parameters"]) == 1 + + by_name = dict((p["name"], p) for p in path["parameters"]) + + parameter = by_name["age"] + assert parameter["name"] == "age" + assert parameter["type"] == "integer" + assert parameter["in"] == "path" + assert parameter["required"] is True + assert parameter["description"] == "An age" + + # Don't duplicate parameters + assert "q" not in by_name + + get = path["get"] + assert len(get["parameters"]) == 1 + + parameter = get["parameters"][0] + assert parameter["name"] == "q" + assert parameter["type"] == "string" + assert parameter["in"] == "query" + assert parameter["description"] == "A query string" + + post = path["post"] + assert len(post["parameters"]) == 1 + + parameter = post["parameters"][0] + assert parameter["name"] == "q" + assert parameter["type"] == "string" + assert parameter["in"] == "query" + assert parameter["description"] == "Overriden description" + + +class SwaggerDeprecatedTest(object): + def test_doc_parser_parameters(self, api): + parser = api.parser() + parser.add_argument("param", type=int, help="Some param") + + with pytest.warns(DeprecationWarning): + + @api.route("/with-parser/") + class WithParserResource(restx.Resource): + @api.doc(parser=parser) + def get(self): + return {} + + assert "parser" not in WithParserResource.get.__apidoc__ + assert "expect" in WithParserResource.get.__apidoc__ + doc_parser = WithParserResource.get.__apidoc__["expect"][0] + assert doc_parser.__schema__ == parser.__schema__ + + def test_doc_method_parser_on_class(self, api): + parser = api.parser() + parser.add_argument("param", type=int, help="Some param") + + with pytest.warns(DeprecationWarning): + + @api.route("/with-parser/") + @api.doc(get={"parser": parser}) + class WithParserResource(restx.Resource): + def get(self): + return {} + + def post(self): + return {} + + assert "parser" not in WithParserResource.__apidoc__["get"] + assert "expect" in WithParserResource.__apidoc__["get"] + doc_parser = WithParserResource.__apidoc__["get"]["expect"][0] + assert doc_parser.__schema__ == parser.__schema__ + + def test_doc_body_as_tuple(self, api): + fields = api.model( + "Person", + { + "name": restx.fields.String, + "age": restx.fields.Integer, + "birthdate": restx.fields.DateTime, + }, + ) + + with pytest.warns(DeprecationWarning): + + @api.route("/model-as-dict/") + class ModelAsDict(restx.Resource): + @api.doc(body=(fields, "Body description")) + def post(self): + return {} + + assert "body" not in ModelAsDict.post.__apidoc__ + assert ModelAsDict.post.__apidoc__["expect"] == [(fields, "Body description")] + + def test_build_request_body_parameters_schema(self): + parser = restx.reqparse.RequestParser() + parser.add_argument("test", type=int, location="headers") + parser.add_argument("test1", type=int, location="json") + parser.add_argument("test2", location="json") + + body_params = [p for p in parser.__schema__ if p["in"] == "body"] + result = restx.swagger.build_request_body_parameters_schema(body_params) + + assert result["name"] == "payload" + assert result["required"] + assert result["in"] == "body" + assert result["schema"]["type"] == "object" + assert result["schema"]["properties"]["test1"]["type"] == "integer" + assert result["schema"]["properties"]["test2"]["type"] == "string" + + def test_expect_unused_model(self, app, api, client): + from flask_restx import fields + + api.model( + "SomeModel", + { + "param": fields.String, + "count": fields.Integer, + }, + ) + + @api.route("/with-parser/", endpoint="with-parser") + class WithParserResource(restx.Resource): + def get(self): + return {} + + app.config["RESTX_INCLUDE_ALL_MODELS"] = True + data = client.get_specs() + assert "/with-parser/" in data["paths"] + + path = data["paths"]["/with-parser/"] + assert "parameters" not in path + + model = data["definitions"]["SomeModel"] + assert model == { + "properties": {"count": {"type": "integer"}, "param": {"type": "string"}}, + "type": "object", + } + + def test_not_expect_unused_model(self, app, api, client): + # This is the default configuration, RESTX_INCLUDE_ALL_MODELS=False + + from flask_restx import fields + + api.model( + "SomeModel", + { + "param": fields.String, + "count": fields.Integer, + }, + ) + + @api.route("/with-parser/", endpoint="with-parser") + class WithParserResource(restx.Resource): + def get(self): + return {} + + data = client.get_specs() + assert "/with-parser/" in data["paths"] + assert "definitions" not in data + + path = data["paths"]["/with-parser/"] + assert "parameters" not in path + + def test_nondefault_swagger_filename(self, app, client): + api = restx.Api(doc="/doc/test", default_swagger_filename="test.json") + ns = restx.Namespace("ns1") + + @ns.route("/test1") + class Ns(restx.Resource): + @ns.doc("Docs") + def get(self): + pass + + api.add_namespace(ns) + api.init_app(app) + + resp = client.get("/test.json") + assert resp.status_code == 200 + assert resp.content_type == "application/json" + resp = client.get("/doc/test") + assert resp.status_code == 200 + assert resp.content_type == "text/html; charset=utf-8" + resp = client.get("/ns1/test1") + assert resp.status_code == 200 diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_swagger_utils.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_swagger_utils.py new file mode 100644 index 0000000..8682622 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_swagger_utils.py @@ -0,0 +1,192 @@ +from flask_restx.swagger import extract_path, extract_path_params, parse_docstring + + +class ExtractPathTest(object): + def test_extract_static_path(self): + path = "/test" + assert extract_path(path) == "/test" + + def test_extract_path_with_a_single_simple_parameter(self): + path = "/test/" + assert extract_path(path) == "/test/{parameter}" + + def test_extract_path_with_a_single_typed_parameter(self): + path = "/test/" + assert extract_path(path) == "/test/{parameter}" + + def test_extract_path_with_a_single_typed_parameter_with_arguments(self): + path = "/test/" + assert extract_path(path) == "/test/{parameter}" + + def test_extract_path_with_multiple_parameters(self): + path = "/test///" + assert extract_path(path) == "/test/{parameter}/{other}/" + + +class ExtractPathParamsTestCase(object): + def test_extract_static_path(self): + path = "/test" + assert extract_path_params(path) == {} + + def test_extract_single_simple_parameter(self): + path = "/test/" + assert extract_path_params(path) == { + "parameter": { + "name": "parameter", + "type": "string", + "in": "path", + "required": True, + } + } + + def test_single_int_parameter(self): + path = "/test/" + assert extract_path_params(path) == { + "parameter": { + "name": "parameter", + "type": "integer", + "in": "path", + "required": True, + } + } + + def test_single_float_parameter(self): + path = "/test/" + assert extract_path_params(path) == { + "parameter": { + "name": "parameter", + "type": "number", + "in": "path", + "required": True, + } + } + + def test_extract_path_with_multiple_parameters(self): + path = "/test///" + assert extract_path_params(path) == { + "parameter": { + "name": "parameter", + "type": "string", + "in": "path", + "required": True, + }, + "other": { + "name": "other", + "type": "integer", + "in": "path", + "required": True, + }, + } + + def test_extract_parameter_with_arguments(self): + path = "/test/" + assert extract_path_params(path) == { + "parameter": { + "name": "parameter", + "type": "string", + "in": "path", + "required": True, + } + } + + # def test_extract_registered_converters(self): + # class ListConverter(BaseConverter): + # def to_python(self, value): + # return value.split(',') + + # def to_url(self, values): + # return ','.join(super(ListConverter, self).to_url(value) for value in values) + + # self.app.url_map.converters['list'] = ListConverter + + # path = '/test/' + # with self.context(): + # self.assertEqual(extract_path_params(path), [{ + # 'name': 'parameters', + # 'type': 'number', + # 'in': 'path', + # 'required': True + # }]) + + +class ParseDocstringTest(object): + def test_empty(self): + def without_doc(): + pass + + parsed = parse_docstring(without_doc) + + assert parsed["raw"] is None + assert parsed["summary"] is None + assert parsed["details"] is None + assert parsed["returns"] is None + assert parsed["raises"] == {} + assert parsed["params"] == [] + + def test_single_line(self): + def func(): + """Some summary""" + pass + + parsed = parse_docstring(func) + + assert parsed["raw"] == "Some summary" + assert parsed["summary"] == "Some summary" + assert parsed["details"] is None + assert parsed["returns"] is None + assert parsed["raises"] == {} + assert parsed["params"] == [] + + def test_multi_line(self): + def func(): + """ + Some summary + Some details + """ + pass + + parsed = parse_docstring(func) + + assert parsed["raw"] == "Some summary\nSome details" + assert parsed["summary"] == "Some summary" + assert parsed["details"] == "Some details" + assert parsed["returns"] is None + assert parsed["raises"] == {} + assert parsed["params"] == [] + + def test_multi_line_and_dot(self): + def func(): + """ + Some summary. bla bla + Some details + """ + pass + + parsed = parse_docstring(func) + + assert parsed["raw"] == "Some summary. bla bla\nSome details" + assert parsed["summary"] == "Some summary" + assert parsed["details"] == "bla bla\nSome details" + assert parsed["returns"] is None + assert parsed["raises"] == {} + assert parsed["params"] == [] + + def test_raises(self): + def func(): + """ + Some summary. + :raises SomeException: in case of something + """ + pass + + parsed = parse_docstring(func) + + assert ( + parsed["raw"] + == "Some summary.\n:raises SomeException: in case of something" + ) + assert parsed["summary"] == "Some summary" + assert parsed["details"] is None + assert parsed["returns"] is None + assert parsed["params"] == [] + assert parsed["raises"] == {"SomeException": "in case of something"} diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_utils.py b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_utils.py new file mode 100644 index 0000000..33e1c69 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tests/test_utils.py @@ -0,0 +1,119 @@ +import pytest + +from flask_restx import utils + + +class MergeTestCase(object): + def test_merge_simple_dicts_without_precedence(self): + a = {"a": "value"} + b = {"b": "other value"} + assert utils.merge(a, b) == {"a": "value", "b": "other value"} + + def test_merge_simple_dicts_with_precedence(self): + a = {"a": "value", "ab": "overwritten"} + b = {"b": "other value", "ab": "keep"} + assert utils.merge(a, b) == {"a": "value", "b": "other value", "ab": "keep"} + + def test_recursions(self): + a = { + "a": "value", + "ab": "overwritten", + "nested_a": {"a": "nested"}, + "nested_a_b": {"a": "a only", "ab": "overwritten"}, + } + b = { + "b": "other value", + "ab": "keep", + "nested_b": {"b": "nested"}, + "nested_a_b": {"b": "b only", "ab": "keep"}, + } + assert utils.merge(a, b) == { + "a": "value", + "b": "other value", + "ab": "keep", + "nested_a": {"a": "nested"}, + "nested_b": {"b": "nested"}, + "nested_a_b": {"a": "a only", "b": "b only", "ab": "keep"}, + } + + def test_recursions_with_empty(self): + a = {} + b = { + "b": "other value", + "ab": "keep", + "nested_b": {"b": "nested"}, + "nested_a_b": {"b": "b only", "ab": "keep"}, + } + assert utils.merge(a, b) == b + + +class UnpackImportResponse(object): + def test_import_werkzeug_response(self): + assert utils.import_werkzeug_response() != None + + +class CamelToDashTestCase(object): + def test_no_transform(self): + assert utils.camel_to_dash("test") == "test" + + @pytest.mark.parametrize( + "value,expected", + [ + ("aValue", "a_value"), + ("aLongValue", "a_long_value"), + ("Upper", "upper"), + ("UpperCase", "upper_case"), + ], + ) + def test_transform(self, value, expected): + assert utils.camel_to_dash(value) == expected + + +class UnpackTest(object): + def test_single_value(self): + data, code, headers = utils.unpack("test") + assert data == "test" + assert code == 200 + assert headers == {} + + def test_single_value_with_default_code(self): + data, code, headers = utils.unpack("test", 500) + assert data == "test" + assert code == 500 + assert headers == {} + + def test_value_code(self): + data, code, headers = utils.unpack(("test", 201)) + assert data == "test" + assert code == 201 + assert headers == {} + + def test_value_code_headers(self): + data, code, headers = utils.unpack(("test", 201, {"Header": "value"})) + assert data == "test" + assert code == 201 + assert headers == {"Header": "value"} + + def test_value_headers_default_code(self): + data, code, headers = utils.unpack(("test", None, {"Header": "value"})) + assert data == "test" + assert code == 200 + assert headers == {"Header": "value"} + + def test_too_many_values(self): + with pytest.raises(ValueError): + utils.unpack((None, None, None, None)) + + +class ToViewNameTest(object): + def test_none(self): + with pytest.raises(AssertionError): + _ = utils.to_view_name(None) + + def test_name(self): + assert utils.to_view_name(self.test_none) == self.test_none.__name__ + + +class ImportCheckViewFuncTest(object): + def test_callable(self): + assert callable(utils.import_check_view_func()) diff --git a/packages/flask-restx/opengnsys-flask-restx-1.3.0/tox.ini b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tox.ini new file mode 100644 index 0000000..ac1e640 --- /dev/null +++ b/packages/flask-restx/opengnsys-flask-restx-1.3.0/tox.ini @@ -0,0 +1,24 @@ +# Tox (http://tox.testrun.org/) is a tool for running tests +# in multiple virtualenvs. This configuration file will run the +# test suite on all supported python versions. To use it, "pip install tox" +# and then run "tox" from this directory. + +[tox] +envlist = + py{38, 39, 310, 311}-flask2, + py{311, 312}-flask3 + pypy3.8 + doc + +[testenv] +commands = {posargs:inv test qa} +deps = + flask2: flask<3.0.0 + flask3: flask>=3.0.0 + -r{toxinidir}/requirements/test.pip + -r{toxinidir}/requirements/develop.pip + +[testenv:doc] +changedir = doc +deps = .[doc] +commands = sphinx-build -b html -d {envtmpdir}/doctrees . {envtmpdir}/html diff --git a/packages/flask-restx/opengnsys-flask-restx_1.3.0.orig.tar.xz b/packages/flask-restx/opengnsys-flask-restx_1.3.0.orig.tar.xz new file mode 100644 index 0000000000000000000000000000000000000000..bc1452eeddc91112d7a7ddcdd6fb03df1e49d199 GIT binary patch literal 364044 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 EBiLh%4hZ`M64=`R&?{ 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%0Dl1b{j&+lL8FZ;u$$LiH@AlwFza$qA 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­sdYV7RP%ScyA{XRPkiRK=4J9nT${_!b!VBI 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 z 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