From 2070e8832ffc60ccfe1db192997509b966c74ce6 Mon Sep 17 00:00:00 2001 From: LuisPi <44235126+lpinon@users.noreply.github.com> Date: Tue, 7 Jun 2022 15:57:15 +0200 Subject: [PATCH 01/19] Init Poetry Project --- .idea/.gitignore | 3 +++ .idea/devon4py.iml | 10 ++++++++++ .idea/inspectionProfiles/profiles_settings.xml | 6 ++++++ .idea/misc.xml | 4 ++++ .idea/modules.xml | 8 ++++++++ .idea/vcs.xml | 6 ++++++ poetry.lock | 8 ++++++++ poetry.toml | 2 ++ pyproject.toml | 14 ++++++++++++++ 9 files changed, 61 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/devon4py.iml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 poetry.lock create mode 100644 poetry.toml create mode 100644 pyproject.toml diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/devon4py.iml b/.idea/devon4py.iml new file mode 100644 index 0000000..2c80e12 --- /dev/null +++ b/.idea/devon4py.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..0505fd3 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..24d276b --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..5b9f72e --- /dev/null +++ b/poetry.lock @@ -0,0 +1,8 @@ +package = [] + +[metadata] +lock-version = "1.1" +python-versions = "^3.10" +content-hash = "17ca553b0bb9298a6ed528dd21e544ca433179192dba32a9920168e1c199d74f" + +[metadata.files] diff --git a/poetry.toml b/poetry.toml new file mode 100644 index 0000000..efa46ec --- /dev/null +++ b/poetry.toml @@ -0,0 +1,2 @@ +[virtualenvs] +in-project = true \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..acc0b91 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,14 @@ +[tool.poetry] +name = "devon4py" +version = "0.0.1" +description = "" +authors = ["LuisPi "] + +[tool.poetry.dependencies] +python = "^3.10" + +[tool.poetry.dev-dependencies] + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" From 157bacaa40889becf6da3ebb9ba3db87bf37100a Mon Sep 17 00:00:00 2001 From: LuisPi <44235126+lpinon@users.noreply.github.com> Date: Tue, 7 Jun 2022 17:34:33 +0200 Subject: [PATCH 02/19] Roadmap --- README.md | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 433146d..c7c22d3 100644 --- a/README.md +++ b/README.md @@ -1 +1,23 @@ -# devon4py \ No newline at end of file +# devon4py + +## Roadmap + +- [X] Swagger / OpenAPI integration +- [ ] Configuration file for each environment +- [ ] Enable/Disable Swagger +- [ ] API Port configuration +- [ ] Log Configuration +- [ ] Database Configuration +- [ ] Swagger Configuration +- [ ] JWT Configuration +- [ ] CORS Configuration +- [ ] Global Exception Management +- [ ] DB Entity Generation +- [ ] Code generation using Templates +- [ ] Log to files integration +- [ ] Log to Database integration +- [ ] Log to GrayLog integration +- [ ] Log to Splunk integration +- [ ] Kafka integration + +## Based on FastAPI From ad54f4560b0a4b6ff7639e40ed23e0be90ffa6b8 Mon Sep 17 00:00:00 2001 From: LuisPi <44235126+lpinon@users.noreply.github.com> Date: Tue, 7 Jun 2022 17:34:45 +0200 Subject: [PATCH 03/19] FastAPI + Environment Config --- README.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/README.md b/README.md index c7c22d3..35aff0b 100644 --- a/README.md +++ b/README.md @@ -21,3 +21,33 @@ - [ ] Kafka integration ## Based on FastAPI + +FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints. + +The key features are: + +- **Fast**: Very high performance, **on par with NodeJS and Go** (thanks to _Starlette_ and _Pydantic_). One of the fastest Python frameworks available. + +- **Fast to code**: Increase the speed to develop features by about 200% to 300%. + +- **Fewer bugs**: Reduce about 40% of human (developer) induced errors. + +- **Intuitive**: Great editor support. Completion everywhere. Less time debugging. + +- **Easy**: Designed to be easy to use and learn. Less time reading docs. + +- **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. + +- **Robust**: Get production-ready code. With automatic interactive documentation. + +- **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema. + +FastAPI works on **_Uvicorn_**, an ASGI web server implementation for Python. + +### Environment Configuration + +You can use Pydantic Settings to handle the settings or configurations for your application, with all the power of Pydantic models. The project uses Dependency Injection for managing dependencies across the application and easy mocking for testing. + +**Create an **_.env_** file for each environment configuration**. The use of @lru_cache() lets you avoid reading the dotenv file again and again for each request, while allowing you to override it during testing. + +Refer to [this link](https://fastapi.tiangolo.com/advanced/settings/) for more information on how to manage the configuration with FastAPI. \ No newline at end of file From b2db75a382e1448681999ba9adf5c5b7fed8fef5 Mon Sep 17 00:00:00 2001 From: LuisPi <44235126+lpinon@users.noreply.github.com> Date: Wed, 8 Jun 2022 12:50:51 +0200 Subject: [PATCH 04/19] Added Full FastAPI deps --- README.md | 5 + poetry.lock | 761 ++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 3 files changed, 765 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 35aff0b..6b890ca 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,11 @@ The key features are: FastAPI works on **_Uvicorn_**, an ASGI web server implementation for Python. +Unlike Flask, FastAPI is an ASGI (Asynchronous Server Gateway Interface) framework, brings together Starlette, Pydantic, OpenAPI, and JSON Schema. +Under the hood, FastAPI uses Pydantic for data validation and Starlette for tooling, making it blazing fast compared to Flask, giving comparable performance to high-speed web APIs in Node or Go. +Starlette + Uvicorn offers async request capability, something that Flask lacks. +With Pydantic along with type hints, you get a nice editor experience with autocompletion. You also get data validation, serialization and deserialization (for building an API), and automatic documentation (via JSON Schema and OpenAPI). + ### Environment Configuration You can use Pydantic Settings to handle the settings or configurations for your application, with all the power of Pydantic models. The project uses Dependency Injection for managing dependencies across the application and easy mocking for testing. diff --git a/poetry.lock b/poetry.lock index 5b9f72e..4b1a411 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,8 +1,765 @@ -package = [] +[[package]] +name = "anyio" +version = "3.6.1" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +category = "main" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" + +[package.extras] +doc = ["packaging", "sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] +test = ["coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "contextlib2", "uvloop (<0.15)", "mock (>=4)", "uvloop (>=0.15)"] +trio = ["trio (>=0.16)"] + +[[package]] +name = "asgiref" +version = "3.5.2" +description = "ASGI specs, helper code, and adapters" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] + +[[package]] +name = "certifi" +version = "2022.5.18.1" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "charset-normalizer" +version = "2.0.12" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.5.0" + +[package.extras] +unicode_backport = ["unicodedata2"] + +[[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "dnspython" +version = "2.2.1" +description = "DNS toolkit" +category = "main" +optional = false +python-versions = ">=3.6,<4.0" + +[package.extras] +dnssec = ["cryptography (>=2.6,<37.0)"] +curio = ["curio (>=1.2,<2.0)", "sniffio (>=1.1,<2.0)"] +doh = ["h2 (>=4.1.0)", "httpx (>=0.21.1)", "requests (>=2.23.0,<3.0.0)", "requests-toolbelt (>=0.9.1,<0.10.0)"] +idna = ["idna (>=2.1,<4.0)"] +trio = ["trio (>=0.14,<0.20)"] +wmi = ["wmi (>=1.5.1,<2.0.0)"] + +[[package]] +name = "email-validator" +version = "1.2.1" +description = "A robust email syntax and deliverability validation library." +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[package.dependencies] +dnspython = ">=1.15.0" +idna = ">=2.0.0" + +[[package]] +name = "fastapi" +version = "0.78.0" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +category = "main" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +email_validator = {version = ">=1.1.1,<2.0.0", optional = true, markers = "extra == \"all\""} +itsdangerous = {version = ">=1.1.0,<3.0.0", optional = true, markers = "extra == \"all\""} +jinja2 = {version = ">=2.11.2,<4.0.0", optional = true, markers = "extra == \"all\""} +orjson = {version = ">=3.2.1,<4.0.0", optional = true, markers = "extra == \"all\""} +pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0" +python-multipart = {version = ">=0.0.5,<0.0.6", optional = true, markers = "extra == \"all\""} +pyyaml = {version = ">=5.3.1,<7.0.0", optional = true, markers = "extra == \"all\""} +requests = {version = ">=2.24.0,<3.0.0", optional = true, markers = "extra == \"all\""} +starlette = "0.19.1" +ujson = {version = ">=4.0.1,<4.0.2 || >4.0.2,<4.1.0 || >4.1.0,<4.2.0 || >4.2.0,<4.3.0 || >4.3.0,<5.0.0 || >5.0.0,<5.1.0 || >5.1.0,<6.0.0", optional = true, markers = "extra == \"all\""} +uvicorn = {version = ">=0.12.0,<0.18.0", extras = ["standard"], optional = true, markers = "extra == \"all\""} + +[package.extras] +all = ["requests (>=2.24.0,<3.0.0)", "jinja2 (>=2.11.2,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "itsdangerous (>=1.1.0,<3.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)", "orjson (>=3.2.1,<4.0.0)", "email_validator (>=1.1.1,<2.0.0)", "uvicorn[standard] (>=0.12.0,<0.18.0)"] +dev = ["python-jose[cryptography] (>=3.3.0,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "autoflake (>=1.4.0,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "uvicorn[standard] (>=0.12.0,<0.18.0)", "pre-commit (>=2.17.0,<3.0.0)"] +doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "typer (>=0.4.1,<0.5.0)", "pyyaml (>=5.3.1,<7.0.0)"] +test = ["pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "mypy (==0.910)", "flake8 (>=3.8.3,<4.0.0)", "black (==22.3.0)", "isort (>=5.0.6,<6.0.0)", "requests (>=2.24.0,<3.0.0)", "httpx (>=0.14.0,<0.19.0)", "email_validator (>=1.1.1,<2.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "peewee (>=3.13.3,<4.0.0)", "databases[sqlite] (>=0.3.2,<0.6.0)", "orjson (>=3.2.1,<4.0.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "flask (>=1.1.2,<3.0.0)", "anyio[trio] (>=3.2.1,<4.0.0)", "types-ujson (==4.2.1)", "types-orjson (==3.6.2)", "types-dataclasses (==0.6.5)"] + +[[package]] +name = "h11" +version = "0.13.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "httptools" +version = "0.4.0" +description = "A collection of framework independent HTTP protocol utils." +category = "main" +optional = false +python-versions = ">=3.5.0" + +[package.extras] +test = ["Cython (>=0.29.24,<0.30.0)"] + +[[package]] +name = "idna" +version = "3.3" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "itsdangerous" +version = "2.1.2" +description = "Safely pass data to untrusted environments and back." +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "jinja2" +version = "3.1.2" +description = "A very fast and expressive template engine." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "markupsafe" +version = "2.1.1" +description = "Safely add untrusted strings to HTML/XML markup." +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "orjson" +version = "3.7.2" +description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "pydantic" +version = "1.9.1" +description = "Data validation and settings management using python type hints" +category = "main" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +typing-extensions = ">=3.7.4.3" + +[package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] + +[[package]] +name = "python-dotenv" +version = "0.20.0" +description = "Read key-value pairs from a .env file and set them as environment variables" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "python-multipart" +version = "0.0.5" +description = "A streaming multipart parser for Python" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +six = ">=1.4.0" + +[[package]] +name = "pyyaml" +version = "6.0" +description = "YAML parser and emitter for Python" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "requests" +version = "2.27.1" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} +idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "sniffio" +version = "1.2.0" +description = "Sniff out which async library your code is running under" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "starlette" +version = "0.19.1" +description = "The little ASGI library that shines." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +anyio = ">=3.4.0,<5" + +[package.extras] +full = ["itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests"] + +[[package]] +name = "typing-extensions" +version = "4.2.0" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "ujson" +version = "5.3.0" +description = "Ultra fast JSON encoder and decoder for Python" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "urllib3" +version = "1.26.9" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "uvicorn" +version = "0.17.6" +description = "The lightning-fast ASGI server." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +asgiref = ">=3.4.0" +click = ">=7.0" +colorama = {version = ">=0.4", optional = true, markers = "sys_platform == \"win32\" and extra == \"standard\""} +h11 = ">=0.8" +httptools = {version = ">=0.4.0", optional = true, markers = "extra == \"standard\""} +python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} +PyYAML = {version = ">=5.1", optional = true, markers = "extra == \"standard\""} +uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\" and extra == \"standard\""} +watchgod = {version = ">=0.6", optional = true, markers = "extra == \"standard\""} +websockets = {version = ">=10.0", optional = true, markers = "extra == \"standard\""} + +[package.extras] +standard = ["websockets (>=10.0)", "httptools (>=0.4.0)", "watchgod (>=0.6)", "python-dotenv (>=0.13)", "PyYAML (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "colorama (>=0.4)"] + +[[package]] +name = "uvloop" +version = "0.16.0" +description = "Fast implementation of asyncio event loop on top of libuv" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +dev = ["Cython (>=0.29.24,<0.30.0)", "pytest (>=3.6.0)", "Sphinx (>=4.1.2,<4.2.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "aiohttp", "flake8 (>=3.9.2,<3.10.0)", "psutil", "pycodestyle (>=2.7.0,<2.8.0)", "pyOpenSSL (>=19.0.0,<19.1.0)", "mypy (>=0.800)"] +docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)"] +test = ["aiohttp", "flake8 (>=3.9.2,<3.10.0)", "psutil", "pycodestyle (>=2.7.0,<2.8.0)", "pyOpenSSL (>=19.0.0,<19.1.0)", "mypy (>=0.800)"] + +[[package]] +name = "watchgod" +version = "0.8.2" +description = "Simple, modern file watching and code reload in python." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +anyio = ">=3.0.0,<4" + +[[package]] +name = "websockets" +version = "10.3" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +category = "main" +optional = false +python-versions = ">=3.7" [metadata] lock-version = "1.1" python-versions = "^3.10" -content-hash = "17ca553b0bb9298a6ed528dd21e544ca433179192dba32a9920168e1c199d74f" +content-hash = "80c31489075cc4e57b837d4f2a92a78f99d46b0a0119dfa584c2f202cf788bf8" [metadata.files] +anyio = [ + {file = "anyio-3.6.1-py3-none-any.whl", hash = "sha256:cb29b9c70620506a9a8f87a309591713446953302d7d995344d0d7c6c0c9a7be"}, + {file = "anyio-3.6.1.tar.gz", hash = "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b"}, +] +asgiref = [ + {file = "asgiref-3.5.2-py3-none-any.whl", hash = "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4"}, + {file = "asgiref-3.5.2.tar.gz", hash = "sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424"}, +] +certifi = [ + {file = "certifi-2022.5.18.1-py3-none-any.whl", hash = "sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a"}, + {file = "certifi-2022.5.18.1.tar.gz", hash = "sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7"}, +] +charset-normalizer = [ + {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, + {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, +] +click = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +dnspython = [ + {file = "dnspython-2.2.1-py3-none-any.whl", hash = "sha256:a851e51367fb93e9e1361732c1d60dab63eff98712e503ea7d92e6eccb109b4f"}, + {file = "dnspython-2.2.1.tar.gz", hash = "sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e"}, +] +email-validator = [ + {file = "email_validator-1.2.1-py2.py3-none-any.whl", hash = "sha256:c8589e691cf73eb99eed8d10ce0e9cbb05a0886ba920c8bcb7c82873f4c5789c"}, + {file = "email_validator-1.2.1.tar.gz", hash = "sha256:6757aea012d40516357c0ac2b1a4c31219ab2f899d26831334c5d069e8b6c3d8"}, +] +fastapi = [ + {file = "fastapi-0.78.0-py3-none-any.whl", hash = "sha256:15fcabd5c78c266fa7ae7d8de9b384bfc2375ee0503463a6febbe3bab69d6f65"}, + {file = "fastapi-0.78.0.tar.gz", hash = "sha256:3233d4a789ba018578658e2af1a4bb5e38bdd122ff722b313666a9b2c6786a83"}, +] +h11 = [ + {file = "h11-0.13.0-py3-none-any.whl", hash = "sha256:8ddd78563b633ca55346c8cd41ec0af27d3c79931828beffb46ce70a379e7442"}, + {file = "h11-0.13.0.tar.gz", hash = "sha256:70813c1135087a248a4d38cc0e1a0181ffab2188141a93eaf567940c3957ff06"}, +] +httptools = [ + {file = "httptools-0.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fcddfe70553be717d9745990dfdb194e22ee0f60eb8f48c0794e7bfeda30d2d5"}, + {file = "httptools-0.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1ee0b459257e222b878a6c09ccf233957d3a4dcb883b0847640af98d2d9aac23"}, + {file = "httptools-0.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceafd5e960b39c7e0d160a1936b68eb87c5e79b3979d66e774f0c77d4d8faaed"}, + {file = "httptools-0.4.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fdb9f9ed79bc6f46b021b3319184699ba1a22410a82204e6e89c774530069683"}, + {file = "httptools-0.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:abe829275cdd4174b4c4e65ad718715d449e308d59793bf3a931ee1bf7e7b86c"}, + {file = "httptools-0.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7af6bdbd21a2a25d6784f6d67f44f5df33ef39b6159543b9f9064d365c01f919"}, + {file = "httptools-0.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:5d1fe6b6661022fd6cac541f54a4237496b246e6f1c0a6b41998ee08a1135afe"}, + {file = "httptools-0.4.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:48e48530d9b995a84d1d89ae6b3ec4e59ea7d494b150ac3bbc5e2ac4acce92cd"}, + {file = "httptools-0.4.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a113789e53ac1fa26edf99856a61e4c493868e125ae0dd6354cf518948fbbd5c"}, + {file = "httptools-0.4.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8e2eb957787cbb614a0f006bfc5798ff1d90ac7c4dd24854c84edbdc8c02369e"}, + {file = "httptools-0.4.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:7ee9f226acab9085037582c059d66769862706e8e8cd2340470ceb8b3850873d"}, + {file = "httptools-0.4.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:701e66b59dd21a32a274771238025d58db7e2b6ecebbab64ceff51b8e31527ae"}, + {file = "httptools-0.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6a1a7dfc1f9c78a833e2c4904757a0f47ce25d08634dd2a52af394eefe5f9777"}, + {file = "httptools-0.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:903f739c9fb78dab8970b0f3ea51f21955b24b45afa77b22ff0e172fc11ef111"}, + {file = "httptools-0.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54bbd295f031b866b9799dd39cb45deee81aca036c9bff9f58ca06726f6494f1"}, + {file = "httptools-0.4.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3194f6d6443befa8d4db16c1946b2fc428a3ceb8ab32eb6f09a59f86104dc1a0"}, + {file = "httptools-0.4.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cd1295f52971097f757edfbfce827b6dbbfb0f7a74901ee7d4933dff5ad4c9af"}, + {file = "httptools-0.4.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:20a45bcf22452a10fa8d58b7dbdb474381f6946bf5b8933e3662d572bc61bae4"}, + {file = "httptools-0.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d1f27bb0f75bef722d6e22dc609612bfa2f994541621cd2163f8c943b6463dfe"}, + {file = "httptools-0.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:7f7bfb74718f52d5ed47d608d507bf66d3bc01d4a8b3e6dd7134daaae129357b"}, + {file = "httptools-0.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a522d12e2ddbc2e91842ffb454a1aeb0d47607972c7d8fc88bd0838d97fb8a2a"}, + {file = "httptools-0.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2db44a0b294d317199e9f80123e72c6b005c55b625b57fae36de68670090fa48"}, + {file = "httptools-0.4.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c286985b5e194ca0ebb2908d71464b9be8f17cc66d6d3e330e8d5407248f56ad"}, + {file = "httptools-0.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d3a4e165ca6204f34856b765d515d558dc84f1352033b8721e8d06c3e44930c3"}, + {file = "httptools-0.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:72aa3fbe636b16d22e04b5a9d24711b043495e0ecfe58080addf23a1a37f3409"}, + {file = "httptools-0.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:9967d9758df505975913304c434cb9ab21e2c609ad859eb921f2f615a038c8de"}, + {file = "httptools-0.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f72b5d24d6730035128b238decdc4c0f2104b7056a7ca55cf047c106842ec890"}, + {file = "httptools-0.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:29bf97a5c532da9c7a04de2c7a9c31d1d54f3abd65a464119b680206bbbb1055"}, + {file = "httptools-0.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98993805f1e3cdb53de4eed02b55dcc953cdf017ba7bbb2fd89226c086a6d855"}, + {file = "httptools-0.4.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d9b90bf58f3ba04e60321a23a8723a1ff2a9377502535e70495e5ada8e6e6722"}, + {file = "httptools-0.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1a99346ebcb801b213c591540837340bdf6fd060a8687518d01c607d338b7424"}, + {file = "httptools-0.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:645373c070080e632480a3d251d892cb795be3d3a15f86975d0f1aca56fd230d"}, + {file = "httptools-0.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:34d2903dd2a3dd85d33705b6fde40bf91fc44411661283763fd0746723963c83"}, + {file = "httptools-0.4.0.tar.gz", hash = "sha256:2c9a930c378b3d15d6b695fb95ebcff81a7395b4f9775c4f10a076beb0b2c1ff"}, +] +idna = [ + {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, + {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, +] +itsdangerous = [ + {file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"}, + {file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"}, +] +jinja2 = [ + {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, + {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, +] +markupsafe = [ + {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, + {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, +] +orjson = [ + {file = "orjson-3.7.2-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:4c6bdb0a7dfe53cca965a40371c7b8e72a0441c8bc4949c9015600f1c7fae408"}, + {file = "orjson-3.7.2-cp310-cp310-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:6e6fc60775bb0a050846710c4a110e8ad17f41e443ff9d0d05145d8f3a74b577"}, + {file = "orjson-3.7.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e4b70bb1f746a9c9afb1f861a0496920b5833ff06f9d1b25b6a7d292cb7e8a06"}, + {file = "orjson-3.7.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99bb2127ee174dd6e68255db26dbef0bd6c4330377a17867ecfa314d47bfac82"}, + {file = "orjson-3.7.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:26306d988401cc34ac94dd38873b8c0384276a5ad80cdf50e266e06083284975"}, + {file = "orjson-3.7.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:34a67d810dbcec77d00d764ab730c5bbb0bee1d75a037c8d8e981506e8fba560"}, + {file = "orjson-3.7.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:14bc727f41ce0dd93d1a6a9fc06076e2401e71b00d0bf107bf64d88d2d963b77"}, + {file = "orjson-3.7.2-cp310-none-win_amd64.whl", hash = "sha256:4c686cbb73ccce02929dd799427897f0a0b2dd597d2f5b6b434917ecc3774146"}, + {file = "orjson-3.7.2-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:12eb683ddbdddd6847ca2b3b074f42574afc0fbf1aff33d8fdf3a4329167762a"}, + {file = "orjson-3.7.2-cp37-cp37m-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:993550e6e451a2b71435142d4824a09f8db80d497abae23dc9f3fe62b6ca24c0"}, + {file = "orjson-3.7.2-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:54cfa4d915a98209366dcf500ee5c3f66408cc9e2b4fd777c8508f69a8f519a1"}, + {file = "orjson-3.7.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f735999d49e2fff2c9812f1ea330b368349f77726894e2a06d17371e61d771bb"}, + {file = "orjson-3.7.2-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:b2b660790b0804624c569ddb8ca9d31bac6f94f880fd54b8cdff4198735a9fec"}, + {file = "orjson-3.7.2-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:590bc5f33e54eb2261de65e4026876e57d04437bab8dcade9514557e31d84537"}, + {file = "orjson-3.7.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8ac61c5c98cbcdcf7a3d0a4b62c873bbd9a996a69eaa44f8356a9e10aa29ef49"}, + {file = "orjson-3.7.2-cp37-none-win_amd64.whl", hash = "sha256:662bda15edf4d25d520945660873e730e3a6d9975041ba9c32f0ce93b632ee0d"}, + {file = "orjson-3.7.2-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:19eb800811a53efc7111ff7536079fb2f62da7098df0a42756ba91e7bdd01aff"}, + {file = "orjson-3.7.2-cp38-cp38-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:54a1e4e39c89d37d3dbc74dde36d09eebcde365ec6803431af9c86604bbbaf3a"}, + {file = "orjson-3.7.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fbd3b46ac514cbe29ecebcee3882383022acf84aa4d3338f26d068c6fbdf56a0"}, + {file = "orjson-3.7.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:891640d332c8c7a1478ea6d13b676d239dc86451afa46000c4e8d0990a0d72dd"}, + {file = "orjson-3.7.2-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:9778a7ec4c72d6814f1e116591f351404a4df2e1dc52d282ff678781f45b509b"}, + {file = "orjson-3.7.2-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:b0b2483f8ad1f93ae4aa43bcf6a985e6ec278e931d0118bae605ffd811b614a1"}, + {file = "orjson-3.7.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2d90ca4e74750c7adfb7708deb096f835f7e6c4b892bdf703fe871565bb04ad7"}, + {file = "orjson-3.7.2-cp38-none-win_amd64.whl", hash = "sha256:b0f4e92bdfe86a0da57028e669bc1f50f48d810ef6f661e63dc6593c450314bf"}, + {file = "orjson-3.7.2-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:b705132b2827d33291684067cca6baa451a499b459e46761d30fcf4d6ce21a9a"}, + {file = "orjson-3.7.2-cp39-cp39-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:c589d00b4fb0777f222b35925e4fa030c4777f16d1623669f44bdc191570be66"}, + {file = "orjson-3.7.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7e197e6779b230e74333e06db804ff876b27306470f68692ec70c27310e7366f"}, + {file = "orjson-3.7.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a82089ec9e1f7e9b992ff5ab98b4c3c2f98e7bbfdc6fadbef046c5aaafec2b54"}, + {file = "orjson-3.7.2-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:3ff49c219b30d715c8baae17c7c5839fe3f2c2db10a66c61d6b91bda80bf8789"}, + {file = "orjson-3.7.2-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:299a743576aaa04f5c7994010608f96df5d4a924d584a686c6e263cee732cb00"}, + {file = "orjson-3.7.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d3ae3ed52c875ce1a6c607f852ca177057445289895483b0247f0dc57b481241"}, + {file = "orjson-3.7.2-cp39-none-win_amd64.whl", hash = "sha256:796914f7463277d371402775536fb461948c0d34a67d20a57dc4ec49a48a8613"}, + {file = "orjson-3.7.2.tar.gz", hash = "sha256:1cf9690a0b7c51a988221376741a31087bc1dc2ac327bb2dde919806dfa59444"}, +] +pydantic = [ + {file = "pydantic-1.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8098a724c2784bf03e8070993f6d46aa2eeca031f8d8a048dff277703e6e193"}, + {file = "pydantic-1.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c320c64dd876e45254bdd350f0179da737463eea41c43bacbee9d8c9d1021f11"}, + {file = "pydantic-1.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18f3e912f9ad1bdec27fb06b8198a2ccc32f201e24174cec1b3424dda605a310"}, + {file = "pydantic-1.9.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c11951b404e08b01b151222a1cb1a9f0a860a8153ce8334149ab9199cd198131"}, + {file = "pydantic-1.9.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8bc541a405423ce0e51c19f637050acdbdf8feca34150e0d17f675e72d119580"}, + {file = "pydantic-1.9.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e565a785233c2d03724c4dc55464559639b1ba9ecf091288dd47ad9c629433bd"}, + {file = "pydantic-1.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:a4a88dcd6ff8fd47c18b3a3709a89adb39a6373f4482e04c1b765045c7e282fd"}, + {file = "pydantic-1.9.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:447d5521575f18e18240906beadc58551e97ec98142266e521c34968c76c8761"}, + {file = "pydantic-1.9.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:985ceb5d0a86fcaa61e45781e567a59baa0da292d5ed2e490d612d0de5796918"}, + {file = "pydantic-1.9.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059b6c1795170809103a1538255883e1983e5b831faea6558ef873d4955b4a74"}, + {file = "pydantic-1.9.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:d12f96b5b64bec3f43c8e82b4aab7599d0157f11c798c9f9c528a72b9e0b339a"}, + {file = "pydantic-1.9.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:ae72f8098acb368d877b210ebe02ba12585e77bd0db78ac04a1ee9b9f5dd2166"}, + {file = "pydantic-1.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:79b485767c13788ee314669008d01f9ef3bc05db9ea3298f6a50d3ef596a154b"}, + {file = "pydantic-1.9.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:494f7c8537f0c02b740c229af4cb47c0d39840b829ecdcfc93d91dcbb0779892"}, + {file = "pydantic-1.9.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0f047e11febe5c3198ed346b507e1d010330d56ad615a7e0a89fae604065a0e"}, + {file = "pydantic-1.9.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:969dd06110cb780da01336b281f53e2e7eb3a482831df441fb65dd30403f4608"}, + {file = "pydantic-1.9.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:177071dfc0df6248fd22b43036f936cfe2508077a72af0933d0c1fa269b18537"}, + {file = "pydantic-1.9.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9bcf8b6e011be08fb729d110f3e22e654a50f8a826b0575c7196616780683380"}, + {file = "pydantic-1.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a955260d47f03df08acf45689bd163ed9df82c0e0124beb4251b1290fa7ae728"}, + {file = "pydantic-1.9.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9ce157d979f742a915b75f792dbd6aa63b8eccaf46a1005ba03aa8a986bde34a"}, + {file = "pydantic-1.9.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0bf07cab5b279859c253d26a9194a8906e6f4a210063b84b433cf90a569de0c1"}, + {file = "pydantic-1.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d93d4e95eacd313d2c765ebe40d49ca9dd2ed90e5b37d0d421c597af830c195"}, + {file = "pydantic-1.9.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1542636a39c4892c4f4fa6270696902acb186a9aaeac6f6cf92ce6ae2e88564b"}, + {file = "pydantic-1.9.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a9af62e9b5b9bc67b2a195ebc2c2662fdf498a822d62f902bf27cccb52dbbf49"}, + {file = "pydantic-1.9.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fe4670cb32ea98ffbf5a1262f14c3e102cccd92b1869df3bb09538158ba90fe6"}, + {file = "pydantic-1.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:9f659a5ee95c8baa2436d392267988fd0f43eb774e5eb8739252e5a7e9cf07e0"}, + {file = "pydantic-1.9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b83ba3825bc91dfa989d4eed76865e71aea3a6ca1388b59fc801ee04c4d8d0d6"}, + {file = "pydantic-1.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1dd8fecbad028cd89d04a46688d2fcc14423e8a196d5b0a5c65105664901f810"}, + {file = "pydantic-1.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02eefd7087268b711a3ff4db528e9916ac9aa18616da7bca69c1871d0b7a091f"}, + {file = "pydantic-1.9.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7eb57ba90929bac0b6cc2af2373893d80ac559adda6933e562dcfb375029acee"}, + {file = "pydantic-1.9.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4ce9ae9e91f46c344bec3b03d6ee9612802682c1551aaf627ad24045ce090761"}, + {file = "pydantic-1.9.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:72ccb318bf0c9ab97fc04c10c37683d9eea952ed526707fabf9ac5ae59b701fd"}, + {file = "pydantic-1.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:61b6760b08b7c395975d893e0b814a11cf011ebb24f7d869e7118f5a339a82e1"}, + {file = "pydantic-1.9.1-py3-none-any.whl", hash = "sha256:4988c0f13c42bfa9ddd2fe2f569c9d54646ce84adc5de84228cfe83396f3bd58"}, + {file = "pydantic-1.9.1.tar.gz", hash = "sha256:1ed987c3ff29fff7fd8c3ea3a3ea877ad310aae2ef9889a119e22d3f2db0691a"}, +] +python-dotenv = [ + {file = "python-dotenv-0.20.0.tar.gz", hash = "sha256:b7e3b04a59693c42c36f9ab1cc2acc46fa5df8c78e178fc33a8d4cd05c8d498f"}, + {file = "python_dotenv-0.20.0-py3-none-any.whl", hash = "sha256:d92a187be61fe482e4fd675b6d52200e7be63a12b724abbf931a40ce4fa92938"}, +] +python-multipart = [ + {file = "python-multipart-0.0.5.tar.gz", hash = "sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"}, +] +pyyaml = [ + {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, + {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, + {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, + {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, + {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, + {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, + {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, + {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, + {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, + {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, + {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, + {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, + {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, + {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, +] +requests = [ + {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, + {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, +] +six = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] +sniffio = [ + {file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"}, + {file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"}, +] +starlette = [ + {file = "starlette-0.19.1-py3-none-any.whl", hash = "sha256:5a60c5c2d051f3a8eb546136aa0c9399773a689595e099e0877704d5888279bf"}, + {file = "starlette-0.19.1.tar.gz", hash = "sha256:c6d21096774ecb9639acad41b86b7706e52ba3bf1dc13ea4ed9ad593d47e24c7"}, +] +typing-extensions = [ + {file = "typing_extensions-4.2.0-py3-none-any.whl", hash = "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"}, + {file = "typing_extensions-4.2.0.tar.gz", hash = "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"}, +] +ujson = [ + {file = "ujson-5.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a933b3a238a48162c382e0ac338b97663d044b0485021b6670565a81e7b7ec98"}, + {file = "ujson-5.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:612015c6e5a9bf041b89f1eaa8ab8682469b3a745a00c7c95bbbee8080f6b346"}, + {file = "ujson-5.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a720b6eff73415249a3dd02e2b1b337de31bb9fa8220bd572dffba23066e538c"}, + {file = "ujson-5.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1408ea1704017289c3023928065233b90953aae3e1d7d06d6d6db667e9fe159"}, + {file = "ujson-5.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5192505798a5734a85c763eff11e6f6072d3595c337b52f72922b4e22fe66e2e"}, + {file = "ujson-5.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bad1471ccfa8d100a0bc513c6db587c38de99384f2aa54eec1016a131d63d3d9"}, + {file = "ujson-5.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b926f2f7a266db8f2c46498f0c2c9fcc7e53c8e0fa8bff7f08ad9c044723a2ec"}, + {file = "ujson-5.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ed9809bc36292e0d3632d50aae497b5827c1a2e07158f7d4d5c53e8e8662bf66"}, + {file = "ujson-5.3.0-cp310-cp310-win32.whl", hash = "sha256:522b1d60872bb6368c14ac538adb55ca9d6c39a7a962832819ef1aafb3446ff5"}, + {file = "ujson-5.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:a609bb1cdda9748e6a8363039926dee5ea2bcc073412279615560b967f92a524"}, + {file = "ujson-5.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7455fc3d69315149b95fd011c01496a5e9442c9e7c4d202bed87c5c2e449ed05"}, + {file = "ujson-5.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:865225a85e4ce48754d0036fdc0eb796b4aaf4f1e928f0efb9b4e1c081647a4c"}, + {file = "ujson-5.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d553f31bceda492c2bda37f48873820d28f07608ae14409c5e9d6c3aa6694840"}, + {file = "ujson-5.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a014531468b78c031aa04e5ca8b64385a6edb48a2e66ebf11093213c678fc383"}, + {file = "ujson-5.3.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b3e6431812d8008dce7b2546b1276f649f6c9aa44617762ebd3529a25092816c"}, + {file = "ujson-5.3.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:089965f964d17905c48cdca88b982d525165e549b438ac86f194c6a9d852fd69"}, + {file = "ujson-5.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ca5eced4ae4ba1e2c9539fca6451694d31e0243de2acfcd6965e2b6e159ba29b"}, + {file = "ujson-5.3.0-cp37-cp37m-win32.whl", hash = "sha256:a4fe193050b519ace09f7d053def30b99deadf650c18a8a874ea0f6c9a2992bc"}, + {file = "ujson-5.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e7961c493a982c03cffc9ce4dc2b23bed1375352296f946cc36ddeb5145fa62c"}, + {file = "ujson-5.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:34592a3c9370745b093ebca60aee6d32f8e7abe3d5c12d54c7dba0b2f81cd863"}, + {file = "ujson-5.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:510c3705b29bc3753ec9e6073b99000160320c1cf6e035884295401acb474dfa"}, + {file = "ujson-5.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:034c07399dff35385ecc53caf9b1f12b3e203834de27b723daeb2cbb3e02ee7f"}, + {file = "ujson-5.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a87e1c05f1efc23c67bfa26be79f12c1f59f71a586b396068d5cf7eb78a2635"}, + {file = "ujson-5.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:972c1850cc52e57ccdea70e3c069e2da5c6090e3ee18d167dff2618a8d7dd127"}, + {file = "ujson-5.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d45e86101a5cddd295d5870b02244fc87ecd9b8936f440acbd2bb30b4c1fe23c"}, + {file = "ujson-5.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:decd32e8d7f934dde484e43431f60b069e87bb30a3a7e186cb6bd69caa0418f3"}, + {file = "ujson-5.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8c734982d6560356c173817576a1f3fa074a2d2b993e63bffa69105ae9ec144b"}, + {file = "ujson-5.3.0-cp38-cp38-win32.whl", hash = "sha256:563b7ed1e789f763410c49e6fab51d61982eb94088b25338e65b89ad20b6b107"}, + {file = "ujson-5.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:8a2cbb044bc6e6764b9a089a2079432b8bd576dbff5faa808b562a8f3c97452b"}, + {file = "ujson-5.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6c5d19fbdd29d5080926c863ba89591a2d3dbf592ea35b456cb2996004433d11"}, + {file = "ujson-5.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4dc79db757b0dfa23a111a4573827a6ef57de65dbe8cdb202e45cf9ddf06aad5"}, + {file = "ujson-5.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5700a179abacbdc8609737e595a598b7f107cd68615ded3f922f4c0d4b6009d6"}, + {file = "ujson-5.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:287dea79473ce4941598c45dc34f9f692d48d7863b451541c5ce960ab54465fb"}, + {file = "ujson-5.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:151faa9085c10351a04aea959a2bc25dfa2e21af26d9b614a221d045b7923ea4"}, + {file = "ujson-5.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:285082924747958aa69e1dc2146c01db6b0921a0bb04b595beefe7fcffaffaf9"}, + {file = "ujson-5.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8dd74570fe59c738d4dc12d44eb89538b0b01fae9dda6cfe3ff3f6934877cf35"}, + {file = "ujson-5.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6aba1e39ffdd83ec14832ea25bbb18266fea46bc69b8c0acbd996495826c0e6f"}, + {file = "ujson-5.3.0-cp39-cp39-win32.whl", hash = "sha256:1358621686ddfda55171fc98c171bf5b1a80ce4d444134b70e1e449925fa014f"}, + {file = "ujson-5.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:d1fab398734634f4b412512ed230d45522fc9f3dd9ca169f579474a491f662aa"}, + {file = "ujson-5.3.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d4830c8df958c45c16dfc43c8353403efd7f1a8e39b91a7e0e848d55b7fa8b48"}, + {file = "ujson-5.3.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48bed7c1f95484644a2cc658efff4d1e75b8c806f6ef2b5c815f59e1cbe0d039"}, + {file = "ujson-5.3.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2db7cbe415d7329b9bff029a83851d1077836ec728fe1c32be34c9c3a5017ab2"}, + {file = "ujson-5.3.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:73636001055667bbcc6a73b232da1d272f68a49a1f192efbe99e99ddf8ef1d21"}, + {file = "ujson-5.3.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:47bf966e1041ae8e568d7e8eb421d72d0521c30c28306b76c256832553e316c6"}, + {file = "ujson-5.3.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:66f857d8b8d7ea44e3fd5f2b7e471334f24b735423729771f5a7a7f69ab645ed"}, + {file = "ujson-5.3.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d2cb50aa526032b8812975c3832058763ee50e1dc3a1302431ed9d0922c3a1b"}, + {file = "ujson-5.3.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f615ee181b813c8f50a57d55354d0c0304a0be066962efdbef6f44517b26e3b2"}, + {file = "ujson-5.3.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5696c99a7dd567566c18490e8e346b2657967feb1e3c2004e91dbb253db0894"}, + {file = "ujson-5.3.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:a68d5a8a46712ffe86db8ae1b4311714db534725521c71fd4c9e1cd062dae9a4"}, + {file = "ujson-5.3.0.tar.gz", hash = "sha256:ab938777b3ac0372231ee654a7f6a13787e587b1ca268d8aa7e6fb6846e477d0"}, +] +urllib3 = [ + {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"}, + {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"}, +] +uvicorn = [ + {file = "uvicorn-0.17.6-py3-none-any.whl", hash = "sha256:19e2a0e96c9ac5581c01eb1a79a7d2f72bb479691acd2b8921fce48ed5b961a6"}, + {file = "uvicorn-0.17.6.tar.gz", hash = "sha256:5180f9d059611747d841a4a4c4ab675edf54c8489e97f96d0583ee90ac3bfc23"}, +] +uvloop = [ + {file = "uvloop-0.16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6224f1401025b748ffecb7a6e2652b17768f30b1a6a3f7b44660e5b5b690b12d"}, + {file = "uvloop-0.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:30ba9dcbd0965f5c812b7c2112a1ddf60cf904c1c160f398e7eed3a6b82dcd9c"}, + {file = "uvloop-0.16.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bd53f7f5db562f37cd64a3af5012df8cac2c464c97e732ed556800129505bd64"}, + {file = "uvloop-0.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:772206116b9b57cd625c8a88f2413df2fcfd0b496eb188b82a43bed7af2c2ec9"}, + {file = "uvloop-0.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b572256409f194521a9895aef274cea88731d14732343da3ecdb175228881638"}, + {file = "uvloop-0.16.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:04ff57aa137230d8cc968f03481176041ae789308b4d5079118331ab01112450"}, + {file = "uvloop-0.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a19828c4f15687675ea912cc28bbcb48e9bb907c801873bd1519b96b04fb805"}, + {file = "uvloop-0.16.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e814ac2c6f9daf4c36eb8e85266859f42174a4ff0d71b99405ed559257750382"}, + {file = "uvloop-0.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bd8f42ea1ea8f4e84d265769089964ddda95eb2bb38b5cbe26712b0616c3edee"}, + {file = "uvloop-0.16.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:647e481940379eebd314c00440314c81ea547aa636056f554d491e40503c8464"}, + {file = "uvloop-0.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e0d26fa5875d43ddbb0d9d79a447d2ace4180d9e3239788208527c4784f7cab"}, + {file = "uvloop-0.16.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6ccd57ae8db17d677e9e06192e9c9ec4bd2066b77790f9aa7dede2cc4008ee8f"}, + {file = "uvloop-0.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:089b4834fd299d82d83a25e3335372f12117a7d38525217c2258e9b9f4578897"}, + {file = "uvloop-0.16.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98d117332cc9e5ea8dfdc2b28b0a23f60370d02e1395f88f40d1effd2cb86c4f"}, + {file = "uvloop-0.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e5f2e2ff51aefe6c19ee98af12b4ae61f5be456cd24396953244a30880ad861"}, + {file = "uvloop-0.16.0.tar.gz", hash = "sha256:f74bc20c7b67d1c27c72601c78cf95be99d5c2cdd4514502b4f3eb0933ff1228"}, +] +watchgod = [ + {file = "watchgod-0.8.2-py3-none-any.whl", hash = "sha256:2f3e8137d98f493ff58af54ea00f4d1433a6afe2ed08ab331a657df468c6bfce"}, + {file = "watchgod-0.8.2.tar.gz", hash = "sha256:cb11ff66657befba94d828e3b622d5fb76f22fbda1376f355f3e6e51e97d9450"}, +] +websockets = [ + {file = "websockets-10.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:661f641b44ed315556a2fa630239adfd77bd1b11cb0b9d96ed8ad90b0b1e4978"}, + {file = "websockets-10.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b529fdfa881b69fe563dbd98acce84f3e5a67df13de415e143ef053ff006d500"}, + {file = "websockets-10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f351c7d7d92f67c0609329ab2735eee0426a03022771b00102816a72715bb00b"}, + {file = "websockets-10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:379e03422178436af4f3abe0aa8f401aa77ae2487843738542a75faf44a31f0c"}, + {file = "websockets-10.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e904c0381c014b914136c492c8fa711ca4cced4e9b3d110e5e7d436d0fc289e8"}, + {file = "websockets-10.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e7e6f2d6fd48422071cc8a6f8542016f350b79cc782752de531577d35e9bd677"}, + {file = "websockets-10.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b9c77f0d1436ea4b4dc089ed8335fa141e6a251a92f75f675056dac4ab47a71e"}, + {file = "websockets-10.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e6fa05a680e35d0fcc1470cb070b10e6fe247af54768f488ed93542e71339d6f"}, + {file = "websockets-10.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2f94fa3ae454a63ea3a19f73b95deeebc9f02ba2d5617ca16f0bbdae375cda47"}, + {file = "websockets-10.3-cp310-cp310-win32.whl", hash = "sha256:6ed1d6f791eabfd9808afea1e068f5e59418e55721db8b7f3bfc39dc831c42ae"}, + {file = "websockets-10.3-cp310-cp310-win_amd64.whl", hash = "sha256:347974105bbd4ea068106ec65e8e8ebd86f28c19e529d115d89bd8cc5cda3079"}, + {file = "websockets-10.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fab7c640815812ed5f10fbee7abbf58788d602046b7bb3af9b1ac753a6d5e916"}, + {file = "websockets-10.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:994cdb1942a7a4c2e10098d9162948c9e7b235df755de91ca33f6e0481366fdb"}, + {file = "websockets-10.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:aad5e300ab32036eb3fdc350ad30877210e2f51bceaca83fb7fef4d2b6c72b79"}, + {file = "websockets-10.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e49ea4c1a9543d2bd8a747ff24411509c29e4bdcde05b5b0895e2120cb1a761d"}, + {file = "websockets-10.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6ea6b300a6bdd782e49922d690e11c3669828fe36fc2471408c58b93b5535a98"}, + {file = "websockets-10.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ef5ce841e102278c1c2e98f043db99d6755b1c58bde475516aef3a008ed7f28e"}, + {file = "websockets-10.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d1655a6fc7aecd333b079d00fb3c8132d18988e47f19740c69303bf02e9883c6"}, + {file = "websockets-10.3-cp37-cp37m-win32.whl", hash = "sha256:83e5ca0d5b743cde3d29fda74ccab37bdd0911f25bd4cdf09ff8b51b7b4f2fa1"}, + {file = "websockets-10.3-cp37-cp37m-win_amd64.whl", hash = "sha256:da4377904a3379f0c1b75a965fff23b28315bcd516d27f99a803720dfebd94d4"}, + {file = "websockets-10.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a1e15b230c3613e8ea82c9fc6941b2093e8eb939dd794c02754d33980ba81e36"}, + {file = "websockets-10.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:31564a67c3e4005f27815634343df688b25705cccb22bc1db621c781ddc64c69"}, + {file = "websockets-10.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c8d1d14aa0f600b5be363077b621b1b4d1eb3fbf90af83f9281cda668e6ff7fd"}, + {file = "websockets-10.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8fbd7d77f8aba46d43245e86dd91a8970eac4fb74c473f8e30e9c07581f852b2"}, + {file = "websockets-10.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:210aad7fdd381c52e58777560860c7e6110b6174488ef1d4b681c08b68bf7f8c"}, + {file = "websockets-10.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6075fd24df23133c1b078e08a9b04a3bc40b31a8def4ee0b9f2c8865acce913e"}, + {file = "websockets-10.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7f6d96fdb0975044fdd7953b35d003b03f9e2bcf85f2d2cf86285ece53e9f991"}, + {file = "websockets-10.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c7250848ce69559756ad0086a37b82c986cd33c2d344ab87fea596c5ac6d9442"}, + {file = "websockets-10.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:28dd20b938a57c3124028680dc1600c197294da5db4292c76a0b48efb3ed7f76"}, + {file = "websockets-10.3-cp38-cp38-win32.whl", hash = "sha256:54c000abeaff6d8771a4e2cef40900919908ea7b6b6a30eae72752607c6db559"}, + {file = "websockets-10.3-cp38-cp38-win_amd64.whl", hash = "sha256:7ab36e17af592eec5747c68ef2722a74c1a4a70f3772bc661079baf4ae30e40d"}, + {file = "websockets-10.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a141de3d5a92188234afa61653ed0bbd2dde46ad47b15c3042ffb89548e77094"}, + {file = "websockets-10.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:97bc9d41e69a7521a358f9b8e44871f6cdeb42af31815c17aed36372d4eec667"}, + {file = "websockets-10.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d6353ba89cfc657a3f5beabb3b69be226adbb5c6c7a66398e17809b0ce3c4731"}, + {file = "websockets-10.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec2b0ab7edc8cd4b0eb428b38ed89079bdc20c6bdb5f889d353011038caac2f9"}, + {file = "websockets-10.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:85506b3328a9e083cc0a0fb3ba27e33c8db78341b3eb12eb72e8afd166c36680"}, + {file = "websockets-10.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8af75085b4bc0b5c40c4a3c0e113fa95e84c60f4ed6786cbb675aeb1ee128247"}, + {file = "websockets-10.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:07cdc0a5b2549bcfbadb585ad8471ebdc7bdf91e32e34ae3889001c1c106a6af"}, + {file = "websockets-10.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:5b936bf552e4f6357f5727579072ff1e1324717902127ffe60c92d29b67b7be3"}, + {file = "websockets-10.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e4e08305bfd76ba8edab08dcc6496f40674f44eb9d5e23153efa0a35750337e8"}, + {file = "websockets-10.3-cp39-cp39-win32.whl", hash = "sha256:bb621ec2dbbbe8df78a27dbd9dd7919f9b7d32a73fafcb4d9252fc4637343582"}, + {file = "websockets-10.3-cp39-cp39-win_amd64.whl", hash = "sha256:51695d3b199cd03098ae5b42833006a0f43dc5418d3102972addc593a783bc02"}, + {file = "websockets-10.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:907e8247480f287aa9bbc9391bd6de23c906d48af54c8c421df84655eef66af7"}, + {file = "websockets-10.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b1359aba0ff810d5830d5ab8e2c4a02bebf98a60aa0124fb29aa78cfdb8031f"}, + {file = "websockets-10.3-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:93d5ea0b5da8d66d868b32c614d2b52d14304444e39e13a59566d4acb8d6e2e4"}, + {file = "websockets-10.3-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7934e055fd5cd9dee60f11d16c8d79c4567315824bacb1246d0208a47eca9755"}, + {file = "websockets-10.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:3eda1cb7e9da1b22588cefff09f0951771d6ee9fa8dbe66f5ae04cc5f26b2b55"}, + {file = "websockets-10.3.tar.gz", hash = "sha256:fc06cc8073c8e87072138ba1e431300e2d408f054b27047d047b549455066ff4"}, +] diff --git a/pyproject.toml b/pyproject.toml index acc0b91..95142a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,7 @@ authors = ["LuisPi "] [tool.poetry.dependencies] python = "^3.10" +fastapi = {extras = ["all"], version = "^0.78.0"} [tool.poetry.dev-dependencies] From 82c29b4124e9d8b7b3f5b0a0f425cc14683202a2 Mon Sep 17 00:00:00 2001 From: LuisPi <44235126+lpinon@users.noreply.github.com> Date: Wed, 8 Jun 2022 12:57:10 +0200 Subject: [PATCH 05/19] Update README.md --- README.md | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6b890ca..760fb75 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,37 @@ Under the hood, FastAPI uses Pydantic for data validation and Starlette for tool Starlette + Uvicorn offers async request capability, something that Flask lacks. With Pydantic along with type hints, you get a nice editor experience with autocompletion. You also get data validation, serialization and deserialization (for building an API), and automatic documentation (via JSON Schema and OpenAPI). -### Environment Configuration +# Run the application +In this section you will find an overview on how to execute and configure the project. + +## Dependencies +Dependencies are automatically managed by **Poetry** + +To install dependencies run +```bash +poetry install +``` +in same folder where your `.toml` file is located. +Poetry will take care of: +- Installing the required Python interpreter +- Installing all the libraries and modules +- Creating the virtual environment for you + +Refer to [this link](https://www.jetbrains.com/help/pycharm/poetry.html) to configure Poetry on PyCharm + +## Running on local + +Start the uvicorn live server with the command: + +``` +uvicorn main:app --reload +``` + +- **_main_**: the file main.py (the Python "module"). +- **_app_**: the object created inside of main.py with the line app = FastAPI(). +- _**--reload**_: make the server restart after code changes. Only use for development. + +## Environment Configuration You can use Pydantic Settings to handle the settings or configurations for your application, with all the power of Pydantic models. The project uses Dependency Injection for managing dependencies across the application and easy mocking for testing. From 3fd1d8a4f274983b1091420409496f7a37fa67f8 Mon Sep 17 00:00:00 2001 From: LuisPi <44235126+lpinon@users.noreply.github.com> Date: Wed, 8 Jun 2022 16:14:18 +0200 Subject: [PATCH 06/19] Configuration file for each environment --- README.md | 10 +++++++++- app/PROD.env | 2 ++ app/TEST.env | 2 ++ app/__init__.py | 0 app/conf/Configuration.py | 21 +++++++++++++++++++++ app/conf/__init__.py | 0 app/main.py | 12 ++++++++++++ 7 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 app/PROD.env create mode 100644 app/TEST.env create mode 100644 app/__init__.py create mode 100644 app/conf/Configuration.py create mode 100644 app/conf/__init__.py create mode 100644 app/main.py diff --git a/README.md b/README.md index 760fb75..b740cb4 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ## Roadmap - [X] Swagger / OpenAPI integration -- [ ] Configuration file for each environment +- [X] Configuration file for each environment - [ ] Enable/Disable Swagger - [ ] API Port configuration - [ ] Log Configuration @@ -85,4 +85,12 @@ You can use Pydantic Settings to handle the settings or configurations for your **Create an **_.env_** file for each environment configuration**. The use of @lru_cache() lets you avoid reading the dotenv file again and again for each request, while allowing you to override it during testing. +Even when using a dotenv file, the application will still read environment variables as well as the dotenv file, **environment variables will always take priority over values loaded from a dotenv file**. + +You can also specify the environment when launching the server. Corresponding **_.env_** file will be automatically loaded. + +``` +ENV=PROD uvicorn main:app --reload +``` + Refer to [this link](https://fastapi.tiangolo.com/advanced/settings/) for more information on how to manage the configuration with FastAPI. \ No newline at end of file diff --git a/app/PROD.env b/app/PROD.env new file mode 100644 index 0000000..b65c8b0 --- /dev/null +++ b/app/PROD.env @@ -0,0 +1,2 @@ +APP_NAME="Devon4py API Production" +ENVIRONMENT="PROD" \ No newline at end of file diff --git a/app/TEST.env b/app/TEST.env new file mode 100644 index 0000000..55c940f --- /dev/null +++ b/app/TEST.env @@ -0,0 +1,2 @@ +APP_NAME="Devon4py API Testing" +ENVIRONMENT="TEST" \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/conf/Configuration.py b/app/conf/Configuration.py new file mode 100644 index 0000000..cf0d198 --- /dev/null +++ b/app/conf/Configuration.py @@ -0,0 +1,21 @@ +from pydantic import BaseSettings +import os +from functools import lru_cache + + +class Settings(BaseSettings): + app_name: str = "My Awesome API" + environment: str = "TEST" + + class Config: + env_file = "TEST.env" + + +@lru_cache() +def get_settings(): + # Import + Cache settings (with lru_cache) + env = os.environ.get("ENV") + if env: + return Settings(_env_file="{}.env".format(env)) + else: + return Settings() diff --git a/app/conf/__init__.py b/app/conf/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..6453f20 --- /dev/null +++ b/app/main.py @@ -0,0 +1,12 @@ +from fastapi import Depends, FastAPI +from conf.Configuration import Settings, get_settings + +app = FastAPI() + + +@app.get("/info") +async def info(settings: Settings = Depends(get_settings)): + return { + "app_name": settings.app_name, + "environment": settings.environment + } From d8ac46e3dffd7b633811759f5be8a9120ff80f5b Mon Sep 17 00:00:00 2001 From: LuisPi <44235126+lpinon@users.noreply.github.com> Date: Wed, 8 Jun 2022 17:29:39 +0200 Subject: [PATCH 07/19] Swagger Configuration --- README.md | 2 +- app/PROD.env | 5 ++++- app/TEST.env | 5 ++++- app/conf/Configuration.py | 1 + app/main.py | 5 +++-- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b740cb4..c454237 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ - [X] Swagger / OpenAPI integration - [X] Configuration file for each environment -- [ ] Enable/Disable Swagger +- [X] Enable/Disable Swagger - [ ] API Port configuration - [ ] Log Configuration - [ ] Database Configuration diff --git a/app/PROD.env b/app/PROD.env index b65c8b0..60e6a3f 100644 --- a/app/PROD.env +++ b/app/PROD.env @@ -1,2 +1,5 @@ APP_NAME="Devon4py API Production" -ENVIRONMENT="PROD" \ No newline at end of file +ENVIRONMENT="PROD" + +# Swagger Configuration +SWAGGER_PATH= \ No newline at end of file diff --git a/app/TEST.env b/app/TEST.env index 55c940f..6b49fc0 100644 --- a/app/TEST.env +++ b/app/TEST.env @@ -1,2 +1,5 @@ APP_NAME="Devon4py API Testing" -ENVIRONMENT="TEST" \ No newline at end of file +ENVIRONMENT="TEST" + +# Swagger Configuration +SWAGGER_PATH=/swagger \ No newline at end of file diff --git a/app/conf/Configuration.py b/app/conf/Configuration.py index cf0d198..6ff369a 100644 --- a/app/conf/Configuration.py +++ b/app/conf/Configuration.py @@ -6,6 +6,7 @@ class Settings(BaseSettings): app_name: str = "My Awesome API" environment: str = "TEST" + swagger_path: str = "docs" class Config: env_file = "TEST.env" diff --git a/app/main.py b/app/main.py index 6453f20..eb48bd2 100644 --- a/app/main.py +++ b/app/main.py @@ -1,12 +1,13 @@ from fastapi import Depends, FastAPI from conf.Configuration import Settings, get_settings -app = FastAPI() +app = FastAPI(docs_url=get_settings().swagger_path) @app.get("/info") async def info(settings: Settings = Depends(get_settings)): return { "app_name": settings.app_name, - "environment": settings.environment + "environment": settings.environment, + "swagger": settings.swagger_path if settings.swagger_path else "DISABLED" } From 9c68c8a2809337e2785e54b26c6f3cc6ecffbd15 Mon Sep 17 00:00:00 2001 From: LuisPi <44235126+lpinon@users.noreply.github.com> Date: Wed, 8 Jun 2022 17:48:37 +0200 Subject: [PATCH 08/19] API Port Configuration & Debug support --- README.md | 16 ++++++++++++++-- app/PROD.env | 1 + app/TEST.env | 1 + app/conf/Configuration.py | 9 +++++---- app/main.py | 12 +++++++++--- 5 files changed, 30 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index c454237..3e9d1fc 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ - [X] Swagger / OpenAPI integration - [X] Configuration file for each environment - [X] Enable/Disable Swagger -- [ ] API Port configuration +- [X] API Port configuration - [ ] Log Configuration - [ ] Database Configuration - [ ] Swagger Configuration @@ -79,6 +79,12 @@ uvicorn main:app --reload - **_app_**: the object created inside of main.py with the line app = FastAPI(). - _**--reload**_: make the server restart after code changes. Only use for development. +You can also launch the uvicorn server programmatically running directly the main.py file. + +``` +python main.py +``` + ## Environment Configuration You can use Pydantic Settings to handle the settings or configurations for your application, with all the power of Pydantic models. The project uses Dependency Injection for managing dependencies across the application and easy mocking for testing. @@ -91,6 +97,12 @@ You can also specify the environment when launching the server. Corresponding ** ``` ENV=PROD uvicorn main:app --reload +ENV=PROD python main.py ``` -Refer to [this link](https://fastapi.tiangolo.com/advanced/settings/) for more information on how to manage the configuration with FastAPI. \ No newline at end of file +### Host & Port Configuration +The Port and Hosting configuration can be set directly on the **.env** file if launching the main.py file. + +However, this configuration is related with the uvicorn server itself and can also be set with the _**--port [int]**_ flag. + +Refer to the [uvicorn documentation](https://www.uvicorn.org/settings/) for more info. \ No newline at end of file diff --git a/app/PROD.env b/app/PROD.env index 60e6a3f..5d8a0d1 100644 --- a/app/PROD.env +++ b/app/PROD.env @@ -1,5 +1,6 @@ APP_NAME="Devon4py API Production" ENVIRONMENT="PROD" +PORT=8000 # Swagger Configuration SWAGGER_PATH= \ No newline at end of file diff --git a/app/TEST.env b/app/TEST.env index 6b49fc0..e653b35 100644 --- a/app/TEST.env +++ b/app/TEST.env @@ -1,5 +1,6 @@ APP_NAME="Devon4py API Testing" ENVIRONMENT="TEST" +PORT=9999 # Swagger Configuration SWAGGER_PATH=/swagger \ No newline at end of file diff --git a/app/conf/Configuration.py b/app/conf/Configuration.py index 6ff369a..7fb5ad7 100644 --- a/app/conf/Configuration.py +++ b/app/conf/Configuration.py @@ -3,9 +3,10 @@ from functools import lru_cache -class Settings(BaseSettings): +class GlobalSettings(BaseSettings): app_name: str = "My Awesome API" environment: str = "TEST" + port: int = 80 swagger_path: str = "docs" class Config: @@ -13,10 +14,10 @@ class Config: @lru_cache() -def get_settings(): +def get_global_settings(): # Import + Cache settings (with lru_cache) env = os.environ.get("ENV") if env: - return Settings(_env_file="{}.env".format(env)) + return GlobalSettings(_env_file="{}.env".format(env)) else: - return Settings() + return GlobalSettings() diff --git a/app/main.py b/app/main.py index eb48bd2..b9a9703 100644 --- a/app/main.py +++ b/app/main.py @@ -1,13 +1,19 @@ +import uvicorn from fastapi import Depends, FastAPI -from conf.Configuration import Settings, get_settings +from conf.Configuration import GlobalSettings, get_global_settings -app = FastAPI(docs_url=get_settings().swagger_path) +app_settings = get_global_settings() +app = FastAPI(docs_url=app_settings.swagger_path) @app.get("/info") -async def info(settings: Settings = Depends(get_settings)): +async def info(settings: GlobalSettings = Depends(get_global_settings)): return { "app_name": settings.app_name, "environment": settings.environment, "swagger": settings.swagger_path if settings.swagger_path else "DISABLED" } + + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=app_settings.port) From dae70d1f7a7e1c36111b5826cb5531d7cf24b1e7 Mon Sep 17 00:00:00 2001 From: LuisPi <44235126+lpinon@users.noreply.github.com> Date: Thu, 9 Jun 2022 15:58:11 +0200 Subject: [PATCH 09/19] Logging configuration --- .gitignore | 3 +++ README.md | 18 +++++++++++++-- app/{conf => core}/Configuration.py | 15 +++++++++++++ app/{conf => core}/__init__.py | 0 app/logging.yaml | 35 +++++++++++++++++++++++++++++ app/main.py | 17 ++++++++++---- 6 files changed, 82 insertions(+), 6 deletions(-) rename app/{conf => core}/Configuration.py (61%) rename app/{conf => core}/__init__.py (100%) create mode 100644 app/logging.yaml diff --git a/.gitignore b/.gitignore index b6e4761..0d05165 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,6 @@ dmypy.json # Pyre type checker .pyre/ + +# Log files +*.log \ No newline at end of file diff --git a/README.md b/README.md index 3e9d1fc..bfdbc4a 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ - [X] Configuration file for each environment - [X] Enable/Disable Swagger - [X] API Port configuration -- [ ] Log Configuration +- [X] Log Configuration - [ ] Database Configuration - [ ] Swagger Configuration - [ ] JWT Configuration @@ -105,4 +105,18 @@ The Port and Hosting configuration can be set directly on the **.env** file if l However, this configuration is related with the uvicorn server itself and can also be set with the _**--port [int]**_ flag. -Refer to the [uvicorn documentation](https://www.uvicorn.org/settings/) for more info. \ No newline at end of file +Refer to the [uvicorn documentation](https://www.uvicorn.org/settings/) for more info. + +### Logging Configuration +The application uses the default logging module. + +To use it inside an specific module init it first with the command: + +``` +logger = logging.getLogger(__name__) +``` + +You can use the __name__ variable to take the current file name as the default or specify a custom module name manually. + +Configure the logging properties in the **_logging.yaml_** file. +You can find more information in the [logging](https://docs.python.org/3/library/logging.html#module-logging) documentation. \ No newline at end of file diff --git a/app/conf/Configuration.py b/app/core/Configuration.py similarity index 61% rename from app/conf/Configuration.py rename to app/core/Configuration.py index 7fb5ad7..c4d4952 100644 --- a/app/conf/Configuration.py +++ b/app/core/Configuration.py @@ -1,3 +1,5 @@ +import yaml +from fastapi import FastAPI from pydantic import BaseSettings import os from functools import lru_cache @@ -21,3 +23,16 @@ def get_global_settings(): return GlobalSettings(_env_file="{}.env".format(env)) else: return GlobalSettings() + + +@lru_cache() +def get_log_config(): + with open("logging.yaml") as logconf: + log_config = yaml.safe_load(logconf) + return log_config + + +def get_app(): + app_settings = get_global_settings() + app = FastAPI(docs_url=app_settings.swagger_path) + return app diff --git a/app/conf/__init__.py b/app/core/__init__.py similarity index 100% rename from app/conf/__init__.py rename to app/core/__init__.py diff --git a/app/logging.yaml b/app/logging.yaml new file mode 100644 index 0000000..bfda358 --- /dev/null +++ b/app/logging.yaml @@ -0,0 +1,35 @@ +--- +version: 1 +disable_existing_loggers: False +formatters: + simple: + format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + +handlers: + console: + class: logging.StreamHandler + level: DEBUG + formatter: simple + stream: ext://sys.stdout + + info_file_handler: + class: logging.handlers.RotatingFileHandler + level: INFO + formatter: simple + filename: info.log + maxBytes: 10485760 # 10MB + backupCount: 20 + encoding: utf8 + + error_file_handler: + class: logging.handlers.RotatingFileHandler + level: ERROR + formatter: simple + filename: errors.log + maxBytes: 10485760 # 10MB + backupCount: 20 + encoding: utf8 + +root: + level: DEBUG + handlers: [console, info_file_handler, error_file_handler] \ No newline at end of file diff --git a/app/main.py b/app/main.py index b9a9703..cba6926 100644 --- a/app/main.py +++ b/app/main.py @@ -1,13 +1,19 @@ import uvicorn from fastapi import Depends, FastAPI -from conf.Configuration import GlobalSettings, get_global_settings +from core.Configuration import GlobalSettings, get_global_settings, get_app, get_log_config +import logging -app_settings = get_global_settings() -app = FastAPI(docs_url=app_settings.swagger_path) +# Init APP with Configuration +app = get_app() +# Init Logger for this Class +logger = logging.getLogger(__name__) @app.get("/info") async def info(settings: GlobalSettings = Depends(get_global_settings)): + logger.info("TEST INFO") + logger.error("TEST ERROR") + logger.debug("TEST DEBUG") return { "app_name": settings.app_name, "environment": settings.environment, @@ -16,4 +22,7 @@ async def info(settings: GlobalSettings = Depends(get_global_settings)): if __name__ == "__main__": - uvicorn.run(app, host="0.0.0.0", port=app_settings.port) + global_settings = get_global_settings() + logging_settings = get_log_config() + uvicorn.run(app, host="0.0.0.0", port=global_settings.port, log_config=logging_settings) + From db73eedfb8f610827d472e1f8e58f8973bcdc6bb Mon Sep 17 00:00:00 2001 From: LuisPi <44235126+lpinon@users.noreply.github.com> Date: Fri, 10 Jun 2022 15:08:05 +0200 Subject: [PATCH 10/19] Added DB Configuration + Documentation --- README.md | 38 ++++++++++++++++++++++++++++++++++++-- app/PROD.env | 10 +++++++++- app/TEST.env | 6 +++++- app/core/Configuration.py | 34 +++++++++++++++++++++++++++++----- app/main.py | 6 ++++-- 5 files changed, 83 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index bfdbc4a..3fdf197 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,35 @@ Under the hood, FastAPI uses Pydantic for data validation and Starlette for tool Starlette + Uvicorn offers async request capability, something that Flask lacks. With Pydantic along with type hints, you get a nice editor experience with autocompletion. You also get data validation, serialization and deserialization (for building an API), and automatic documentation (via JSON Schema and OpenAPI). + +### Unlimited "plug-ins" +Or in other way, no need for them, import and use the code you need. + +Any integration is designed to be so simple to use (with dependencies) that you can create a "plug-in" for your application in 2 lines of code using the same structure and syntax used for your path operations. + +### Tested +- 100% test coverage. +- 100% type annotated code base. +- Used in production applications. + +### Based on Starlette + +FastAPI is fully compatible with (and based on) [Starlette](https://www.starlette.io/). So, any additional Starlette code you have, will also work. + +FastAPI is actually a sub-class of Starlette. So, if you already know or use Starlette, most of the functionality will work the same way. + +With FastAPI you get all of Starlette's features (as FastAPI is just Starlette on steroids): + +- Seriously impressive performance. It is one of the fastest Python frameworks available, on par with NodeJS and Go. +- WebSocket support. +- In-process background tasks. +- Startup and shutdown events. +- Test client built on requests. +- CORS, GZip, Static Files, Streaming responses. +- Session and Cookie support. +- 100% test coverage. +- 100% type annotated codebase. + # Run the application In this section you will find an overview on how to execute and configure the project. @@ -95,6 +124,8 @@ Even when using a dotenv file, the application will still read environment varia You can also specify the environment when launching the server. Corresponding **_.env_** file will be automatically loaded. +Settings and environment variables are managed by **Pydantic**, refer to [the documentation](https://pydantic-docs.helpmanual.io/usage/settings/) for more info. + ``` ENV=PROD uvicorn main:app --reload ENV=PROD python main.py @@ -108,7 +139,7 @@ However, this configuration is related with the uvicorn server itself and can al Refer to the [uvicorn documentation](https://www.uvicorn.org/settings/) for more info. ### Logging Configuration -The application uses the default logging module. +The application uses the default **_logging_** module. To use it inside an specific module init it first with the command: @@ -119,4 +150,7 @@ logger = logging.getLogger(__name__) You can use the __name__ variable to take the current file name as the default or specify a custom module name manually. Configure the logging properties in the **_logging.yaml_** file. -You can find more information in the [logging](https://docs.python.org/3/library/logging.html#module-logging) documentation. \ No newline at end of file +You can find more information in the [logging](https://docs.python.org/3/library/logging.html#module-logging) documentation. + +## Database Management + diff --git a/app/PROD.env b/app/PROD.env index 5d8a0d1..58f3abc 100644 --- a/app/PROD.env +++ b/app/PROD.env @@ -3,4 +3,12 @@ ENVIRONMENT="PROD" PORT=8000 # Swagger Configuration -SWAGGER_PATH= \ No newline at end of file +SWAGGER_PATH= + +# Database Configuration +DB_TYPE=PG +DB_USERNAME=postgres +DB_PASSWORD=pass +DB_HOST=localhost +DB_PORT=5432 +DB_DATABASE=database \ No newline at end of file diff --git a/app/TEST.env b/app/TEST.env index e653b35..16138b4 100644 --- a/app/TEST.env +++ b/app/TEST.env @@ -3,4 +3,8 @@ ENVIRONMENT="TEST" PORT=9999 # Swagger Configuration -SWAGGER_PATH=/swagger \ No newline at end of file +SWAGGER_PATH=/swagger + +# Database Configuration +DB_TYPE=LOCAL +DB_DATABASE=my_database.db \ No newline at end of file diff --git a/app/core/Configuration.py b/app/core/Configuration.py index c4d4952..d75238e 100644 --- a/app/core/Configuration.py +++ b/app/core/Configuration.py @@ -1,3 +1,5 @@ +from typing import Optional + import yaml from fastapi import FastAPI from pydantic import BaseSettings @@ -15,14 +17,36 @@ class Config: env_file = "TEST.env" -@lru_cache() -def get_global_settings(): - # Import + Cache settings (with lru_cache) +# Scheme: "postgres+psycopg2://:@:/" +class DatabaseSettings(BaseSettings): + type: str + username: Optional[str] + password: Optional[str] + host: Optional[str] + port: Optional[int] = None + database: Optional[str] + + class Config: + env_prefix = "DB_" + env_file = "TEST.env" + + +def __load_env_file_on_settings(settings): env = os.environ.get("ENV") if env: - return GlobalSettings(_env_file="{}.env".format(env)) + return settings(_env_file="{}.env".format(env)) else: - return GlobalSettings() + return settings() + + +@lru_cache() +def get_global_settings(): + return __load_env_file_on_settings(GlobalSettings) + + +@lru_cache() +def get_db_settings(): + return __load_env_file_on_settings(DatabaseSettings) @lru_cache() diff --git a/app/main.py b/app/main.py index cba6926..f640a71 100644 --- a/app/main.py +++ b/app/main.py @@ -1,6 +1,7 @@ import uvicorn from fastapi import Depends, FastAPI -from core.Configuration import GlobalSettings, get_global_settings, get_app, get_log_config +from core.Configuration import GlobalSettings, get_global_settings, get_app, get_log_config, DatabaseSettings, \ + get_db_settings import logging # Init APP with Configuration @@ -10,7 +11,8 @@ @app.get("/info") -async def info(settings: GlobalSettings = Depends(get_global_settings)): +async def info(settings: GlobalSettings = Depends(get_global_settings), + db: DatabaseSettings = Depends(get_db_settings)): logger.info("TEST INFO") logger.error("TEST ERROR") logger.debug("TEST DEBUG") From 9aeafc68344ac2121e834f5efa92af5924c9557b Mon Sep 17 00:00:00 2001 From: LuisPi <44235126+lpinon@users.noreply.github.com> Date: Tue, 14 Jun 2022 19:11:31 +0200 Subject: [PATCH 11/19] Added Example Entities with Relations + Database Config & Init + DB Table autocreation --- .gitignore | 5 +- README.md | 5 +- app/PROD.env | 3 +- app/TEST.env | 3 +- app/core/Configuration.py | 2 +- app/core/database.py | 25 ++++++ app/main.py | 12 ++- app/models/__init__.py | 5 ++ app/models/base.py | 15 ++++ app/models/group.py | 18 ++++ app/models/links.py | 9 ++ app/models/role.py | 13 +++ app/models/user.py | 28 +++++++ poetry.lock | 172 +++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 15 files changed, 306 insertions(+), 10 deletions(-) create mode 100644 app/core/database.py create mode 100644 app/models/__init__.py create mode 100644 app/models/base.py create mode 100644 app/models/group.py create mode 100644 app/models/links.py create mode 100644 app/models/role.py create mode 100644 app/models/user.py diff --git a/.gitignore b/.gitignore index 0d05165..aee9c67 100644 --- a/.gitignore +++ b/.gitignore @@ -129,4 +129,7 @@ dmypy.json .pyre/ # Log files -*.log \ No newline at end of file +*.log + +# DB Files +*.db \ No newline at end of file diff --git a/README.md b/README.md index 3fdf197..60b9f13 100644 --- a/README.md +++ b/README.md @@ -7,14 +7,15 @@ - [X] Enable/Disable Swagger - [X] API Port configuration - [X] Log Configuration -- [ ] Database Configuration +- [X] Database Configuration +- [ ] ASYNC Database Configuration - [ ] Swagger Configuration - [ ] JWT Configuration - [ ] CORS Configuration - [ ] Global Exception Management - [ ] DB Entity Generation - [ ] Code generation using Templates -- [ ] Log to files integration +- [X] Log to files integration - [ ] Log to Database integration - [ ] Log to GrayLog integration - [ ] Log to Splunk integration diff --git a/app/PROD.env b/app/PROD.env index 58f3abc..d5df665 100644 --- a/app/PROD.env +++ b/app/PROD.env @@ -11,4 +11,5 @@ DB_USERNAME=postgres DB_PASSWORD=pass DB_HOST=localhost DB_PORT=5432 -DB_DATABASE=database \ No newline at end of file +DB_DATABASE=database +DB_ENABLE_LOGS=False \ No newline at end of file diff --git a/app/TEST.env b/app/TEST.env index 16138b4..f25e2f6 100644 --- a/app/TEST.env +++ b/app/TEST.env @@ -7,4 +7,5 @@ SWAGGER_PATH=/swagger # Database Configuration DB_TYPE=LOCAL -DB_DATABASE=my_database.db \ No newline at end of file +DB_DATABASE=my_database +DB_ENABLE_LOGS=True \ No newline at end of file diff --git a/app/core/Configuration.py b/app/core/Configuration.py index d75238e..b005d00 100644 --- a/app/core/Configuration.py +++ b/app/core/Configuration.py @@ -17,7 +17,6 @@ class Config: env_file = "TEST.env" -# Scheme: "postgres+psycopg2://:@:/" class DatabaseSettings(BaseSettings): type: str username: Optional[str] @@ -25,6 +24,7 @@ class DatabaseSettings(BaseSettings): host: Optional[str] port: Optional[int] = None database: Optional[str] + enable_logs = False class Config: env_prefix = "DB_" diff --git a/app/core/database.py b/app/core/database.py new file mode 100644 index 0000000..1b47504 --- /dev/null +++ b/app/core/database.py @@ -0,0 +1,25 @@ +from functools import lru_cache + +from fastapi import Depends +from sqlalchemy.future import Engine + +from .configuration import DatabaseSettings, get_db_settings +from sqlmodel import Field, Session, SQLModel, create_engine + + +def get_db_engine(db: DatabaseSettings = Depends(get_db_settings)) -> Engine: + if db.type == "LOCAL": + engine = create_engine("sqlite:///{}.db".format(db.database), echo=db.enable_logs, echo_pool=db.enable_logs) + elif db.type == "PG": + engine = create_engine("postgresql+psycopg2://{0}:{1}@{2}:{3}/{4}".format( + db.username, db.password, db.host, db.port, db.database), echo=db.enable_logs, echo_pool=db.enable_logs) + else: + engine = create_engine("sqlite:///database.db") + return engine + + +def init_db_entities(db: DatabaseSettings): + import app.models + engine = get_db_engine(db) + SQLModel.metadata.create_all(engine) + diff --git a/app/main.py b/app/main.py index f640a71..36f4301 100644 --- a/app/main.py +++ b/app/main.py @@ -1,6 +1,10 @@ import uvicorn from fastapi import Depends, FastAPI -from core.Configuration import GlobalSettings, get_global_settings, get_app, get_log_config, DatabaseSettings, \ +from sqlalchemy.future import Engine +from sqlmodel import SQLModel + +from core.database import get_db_engine, init_db_entities +from core.configuration import GlobalSettings, get_global_settings, get_app, get_log_config, DatabaseSettings, \ get_db_settings import logging @@ -11,8 +15,7 @@ @app.get("/info") -async def info(settings: GlobalSettings = Depends(get_global_settings), - db: DatabaseSettings = Depends(get_db_settings)): +def info(settings: GlobalSettings = Depends(get_global_settings)): logger.info("TEST INFO") logger.error("TEST ERROR") logger.debug("TEST DEBUG") @@ -26,5 +29,8 @@ async def info(settings: GlobalSettings = Depends(get_global_settings), if __name__ == "__main__": global_settings = get_global_settings() logging_settings = get_log_config() + db_settings = get_db_settings() + init_db_entities(db_settings) + print(global_settings.port, global_settings.environment) uvicorn.run(app, host="0.0.0.0", port=global_settings.port, log_config=logging_settings) diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000..b1e65ed --- /dev/null +++ b/app/models/__init__.py @@ -0,0 +1,5 @@ +# Import DB entities for init +from .group import Group +from .links import LinkGroupUser +from .role import Role +from .user import User diff --git a/app/models/base.py b/app/models/base.py new file mode 100644 index 0000000..47e5247 --- /dev/null +++ b/app/models/base.py @@ -0,0 +1,15 @@ +from typing import Optional +import uuid as uuid_pkg +from sqlmodel import SQLModel, Field +from datetime import datetime + + +class BaseUUIDModel(SQLModel): + id: uuid_pkg.UUID = Field( + default_factory=uuid_pkg.uuid4, + primary_key=True, + index=True, + nullable=False, + ) + updated_at: Optional[datetime] + created_at: Optional[datetime] diff --git a/app/models/group.py b/app/models/group.py new file mode 100644 index 0000000..c4ad9b7 --- /dev/null +++ b/app/models/group.py @@ -0,0 +1,18 @@ +from sqlmodel import Field, Relationship, SQLModel +from typing import List, Optional +from app.models.links import LinkGroupUser +from app.models.base import BaseUUIDModel +from uuid import UUID + +from app.models.user import User + + +class GroupBase(SQLModel): + name: str + description: str + + +class Group(BaseUUIDModel, GroupBase, table=True): + created_by_id: Optional[UUID] = Field(default=None, foreign_key="user.id") + created_by: "User" = Relationship(sa_relationship_kwargs={"lazy":"selectin", "primaryjoin":"Group.created_by_id==User.id"}) + users: List["User"] = Relationship(back_populates="groups", link_model=LinkGroupUser, sa_relationship_kwargs={"lazy": "selectin"}) diff --git a/app/models/links.py b/app/models/links.py new file mode 100644 index 0000000..6d78091 --- /dev/null +++ b/app/models/links.py @@ -0,0 +1,9 @@ +from sqlmodel import Field +from typing import Optional +from app.models.base import BaseUUIDModel +from uuid import UUID + + +class LinkGroupUser(BaseUUIDModel, table=True): + group_id: Optional[UUID] = Field(default=None, nullable=False, foreign_key="group.id", primary_key=True) + user_id: Optional[UUID] = Field(default=None, nullable=False, foreign_key="user.id", primary_key=True) diff --git a/app/models/role.py b/app/models/role.py new file mode 100644 index 0000000..a25f5e0 --- /dev/null +++ b/app/models/role.py @@ -0,0 +1,13 @@ +from sqlmodel import SQLModel, Relationship +from typing import List +from app.models.base import BaseUUIDModel + + +class RoleBase(SQLModel): + name: str + description: str + + +class Role(BaseUUIDModel, RoleBase, table=True): + users: List["User"] = Relationship(back_populates="role", sa_relationship_kwargs={"lazy": "selectin"}) + diff --git a/app/models/user.py b/app/models/user.py new file mode 100644 index 0000000..0d45c27 --- /dev/null +++ b/app/models/user.py @@ -0,0 +1,28 @@ +from datetime import datetime +from sqlmodel import Field, SQLModel, Relationship, Column, DateTime +from app.models.links import LinkGroupUser +from typing import List, Optional +from pydantic import EmailStr +from app.models.base import BaseUUIDModel +from uuid import UUID + + +class UserBase(SQLModel): + first_name: str + last_name: str + email: EmailStr = Field(nullable=True, index=True, sa_column_kwargs={"unique": True}) + is_active: bool = Field(default=True) + is_superuser: bool = Field(default=False) + birthdate: Optional[datetime] = Field(sa_column=Column(DateTime(timezone=True), nullable=True)) # bday with timezne + phone: Optional[str] + state: Optional[str] + country: Optional[str] + address: Optional[str] + + +class User(BaseUUIDModel, UserBase, table=True): + hashed_password: str = Field(nullable=False, index=True) + role_id: Optional[UUID] = Field(default=None, foreign_key="role.id") + role: Optional["Role"] = Relationship(back_populates="users", sa_relationship_kwargs={"lazy": "selectin"}) + groups: List["Group"] = Relationship(back_populates="users", link_model=LinkGroupUser, sa_relationship_kwargs={"lazy": "selectin"}) + diff --git a/poetry.lock b/poetry.lock index 4b1a411..05fac2e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -119,6 +119,17 @@ dev = ["python-jose[cryptography] (>=3.3.0,<4.0.0)", "passlib[bcrypt] (>=1.7.2,< doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "typer (>=0.4.1,<0.5.0)", "pyyaml (>=5.3.1,<7.0.0)"] test = ["pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "mypy (==0.910)", "flake8 (>=3.8.3,<4.0.0)", "black (==22.3.0)", "isort (>=5.0.6,<6.0.0)", "requests (>=2.24.0,<3.0.0)", "httpx (>=0.14.0,<0.19.0)", "email_validator (>=1.1.1,<2.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "peewee (>=3.13.3,<4.0.0)", "databases[sqlite] (>=0.3.2,<0.6.0)", "orjson (>=3.2.1,<4.0.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "flask (>=1.1.2,<3.0.0)", "anyio[trio] (>=3.2.1,<4.0.0)", "types-ujson (==4.2.1)", "types-orjson (==3.6.2)", "types-dataclasses (==0.6.5)"] +[[package]] +name = "greenlet" +version = "1.1.2" +description = "Lightweight in-process concurrent programming" +category = "main" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" + +[package.extras] +docs = ["sphinx"] + [[package]] name = "h11" version = "0.13.0" @@ -263,6 +274,62 @@ category = "main" optional = false python-versions = ">=3.5" +[[package]] +name = "sqlalchemy" +version = "1.4.37" +description = "Database Abstraction Library" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" + +[package.dependencies] +greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} + +[package.extras] +aiomysql = ["greenlet (!=0.4.17)", "aiomysql"] +aiosqlite = ["typing_extensions (!=3.10.0.1)", "greenlet (!=0.4.17)", "aiosqlite"] +asyncio = ["greenlet (!=0.4.17)"] +asyncmy = ["greenlet (!=0.4.17)", "asyncmy (>=0.2.3,!=0.2.4)"] +mariadb_connector = ["mariadb (>=1.0.1)"] +mssql = ["pyodbc"] +mssql_pymssql = ["pymssql"] +mssql_pyodbc = ["pyodbc"] +mypy = ["sqlalchemy2-stubs", "mypy (>=0.910)"] +mysql = ["mysqlclient (>=1.4.0,<2)", "mysqlclient (>=1.4.0)"] +mysql_connector = ["mysql-connector-python"] +oracle = ["cx_oracle (>=7,<8)", "cx_oracle (>=7)"] +postgresql = ["psycopg2 (>=2.7)"] +postgresql_asyncpg = ["greenlet (!=0.4.17)", "asyncpg"] +postgresql_pg8000 = ["pg8000 (>=1.16.6,!=1.29.0)"] +postgresql_psycopg2binary = ["psycopg2-binary"] +postgresql_psycopg2cffi = ["psycopg2cffi"] +pymysql = ["pymysql (<1)", "pymysql"] +sqlcipher = ["sqlcipher3-binary"] + +[[package]] +name = "sqlalchemy2-stubs" +version = "0.0.2a24" +description = "Typing Stubs for SQLAlchemy 1.4" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +typing-extensions = ">=3.7.4" + +[[package]] +name = "sqlmodel" +version = "0.0.6" +description = "SQLModel, SQL databases in Python, designed for simplicity, compatibility, and robustness." +category = "main" +optional = false +python-versions = ">=3.6.1,<4.0.0" + +[package.dependencies] +pydantic = ">=1.8.2,<2.0.0" +SQLAlchemy = ">=1.4.17,<1.5.0" +sqlalchemy2-stubs = "*" + [[package]] name = "starlette" version = "0.19.1" @@ -364,7 +431,7 @@ python-versions = ">=3.7" [metadata] lock-version = "1.1" python-versions = "^3.10" -content-hash = "80c31489075cc4e57b837d4f2a92a78f99d46b0a0119dfa584c2f202cf788bf8" +content-hash = "35d665498da177002af87d8c28489085f4b730a39ce29f6736dbe750778cdad1" [metadata.files] anyio = [ @@ -403,6 +470,63 @@ fastapi = [ {file = "fastapi-0.78.0-py3-none-any.whl", hash = "sha256:15fcabd5c78c266fa7ae7d8de9b384bfc2375ee0503463a6febbe3bab69d6f65"}, {file = "fastapi-0.78.0.tar.gz", hash = "sha256:3233d4a789ba018578658e2af1a4bb5e38bdd122ff722b313666a9b2c6786a83"}, ] +greenlet = [ + {file = "greenlet-1.1.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:58df5c2a0e293bf665a51f8a100d3e9956febfbf1d9aaf8c0677cf70218910c6"}, + {file = "greenlet-1.1.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:aec52725173bd3a7b56fe91bc56eccb26fbdff1386ef123abb63c84c5b43b63a"}, + {file = "greenlet-1.1.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:833e1551925ed51e6b44c800e71e77dacd7e49181fdc9ac9a0bf3714d515785d"}, + {file = "greenlet-1.1.2-cp27-cp27m-win32.whl", hash = "sha256:aa5b467f15e78b82257319aebc78dd2915e4c1436c3c0d1ad6f53e47ba6e2713"}, + {file = "greenlet-1.1.2-cp27-cp27m-win_amd64.whl", hash = "sha256:40b951f601af999a8bf2ce8c71e8aaa4e8c6f78ff8afae7b808aae2dc50d4c40"}, + {file = "greenlet-1.1.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:95e69877983ea39b7303570fa6760f81a3eec23d0e3ab2021b7144b94d06202d"}, + {file = "greenlet-1.1.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:356b3576ad078c89a6107caa9c50cc14e98e3a6c4874a37c3e0273e4baf33de8"}, + {file = "greenlet-1.1.2-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8639cadfda96737427330a094476d4c7a56ac03de7265622fcf4cfe57c8ae18d"}, + {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97e5306482182170ade15c4b0d8386ded995a07d7cc2ca8f27958d34d6736497"}, + {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6a36bb9474218c7a5b27ae476035497a6990e21d04c279884eb10d9b290f1b1"}, + {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abb7a75ed8b968f3061327c433a0fbd17b729947b400747c334a9c29a9af6c58"}, + {file = "greenlet-1.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b336501a05e13b616ef81ce329c0e09ac5ed8c732d9ba7e3e983fcc1a9e86965"}, + {file = "greenlet-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:14d4f3cd4e8b524ae9b8aa567858beed70c392fdec26dbdb0a8a418392e71708"}, + {file = "greenlet-1.1.2-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:17ff94e7a83aa8671a25bf5b59326ec26da379ace2ebc4411d690d80a7fbcf23"}, + {file = "greenlet-1.1.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9f3cba480d3deb69f6ee2c1825060177a22c7826431458c697df88e6aeb3caee"}, + {file = "greenlet-1.1.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:fa877ca7f6b48054f847b61d6fa7bed5cebb663ebc55e018fda12db09dcc664c"}, + {file = "greenlet-1.1.2-cp35-cp35m-win32.whl", hash = "sha256:7cbd7574ce8e138bda9df4efc6bf2ab8572c9aff640d8ecfece1b006b68da963"}, + {file = "greenlet-1.1.2-cp35-cp35m-win_amd64.whl", hash = "sha256:903bbd302a2378f984aef528f76d4c9b1748f318fe1294961c072bdc7f2ffa3e"}, + {file = "greenlet-1.1.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:049fe7579230e44daef03a259faa24511d10ebfa44f69411d99e6a184fe68073"}, + {file = "greenlet-1.1.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:dd0b1e9e891f69e7675ba5c92e28b90eaa045f6ab134ffe70b52e948aa175b3c"}, + {file = "greenlet-1.1.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:7418b6bfc7fe3331541b84bb2141c9baf1ec7132a7ecd9f375912eca810e714e"}, + {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9d29ca8a77117315101425ec7ec2a47a22ccf59f5593378fc4077ac5b754fce"}, + {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21915eb821a6b3d9d8eefdaf57d6c345b970ad722f856cd71739493ce003ad08"}, + {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eff9d20417ff9dcb0d25e2defc2574d10b491bf2e693b4e491914738b7908168"}, + {file = "greenlet-1.1.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b8c008de9d0daba7b6666aa5bbfdc23dcd78cafc33997c9b7741ff6353bafb7f"}, + {file = "greenlet-1.1.2-cp36-cp36m-win32.whl", hash = "sha256:32ca72bbc673adbcfecb935bb3fb1b74e663d10a4b241aaa2f5a75fe1d1f90aa"}, + {file = "greenlet-1.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f0214eb2a23b85528310dad848ad2ac58e735612929c8072f6093f3585fd342d"}, + {file = "greenlet-1.1.2-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:b92e29e58bef6d9cfd340c72b04d74c4b4e9f70c9fa7c78b674d1fec18896dc4"}, + {file = "greenlet-1.1.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fdcec0b8399108577ec290f55551d926d9a1fa6cad45882093a7a07ac5ec147b"}, + {file = "greenlet-1.1.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:93f81b134a165cc17123626ab8da2e30c0455441d4ab5576eed73a64c025b25c"}, + {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e12bdc622676ce47ae9abbf455c189e442afdde8818d9da983085df6312e7a1"}, + {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c790abda465726cfb8bb08bd4ca9a5d0a7bd77c7ac1ca1b839ad823b948ea28"}, + {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f276df9830dba7a333544bd41070e8175762a7ac20350786b322b714b0e654f5"}, + {file = "greenlet-1.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c5d5b35f789a030ebb95bff352f1d27a93d81069f2adb3182d99882e095cefe"}, + {file = "greenlet-1.1.2-cp37-cp37m-win32.whl", hash = "sha256:64e6175c2e53195278d7388c454e0b30997573f3f4bd63697f88d855f7a6a1fc"}, + {file = "greenlet-1.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b11548073a2213d950c3f671aa88e6f83cda6e2fb97a8b6317b1b5b33d850e06"}, + {file = "greenlet-1.1.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:9633b3034d3d901f0a46b7939f8c4d64427dfba6bbc5a36b1a67364cf148a1b0"}, + {file = "greenlet-1.1.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:eb6ea6da4c787111adf40f697b4e58732ee0942b5d3bd8f435277643329ba627"}, + {file = "greenlet-1.1.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:f3acda1924472472ddd60c29e5b9db0cec629fbe3c5c5accb74d6d6d14773478"}, + {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e859fcb4cbe93504ea18008d1df98dee4f7766db66c435e4882ab35cf70cac43"}, + {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00e44c8afdbe5467e4f7b5851be223be68adb4272f44696ee71fe46b7036a711"}, + {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec8c433b3ab0419100bd45b47c9c8551248a5aee30ca5e9d399a0b57ac04651b"}, + {file = "greenlet-1.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2bde6792f313f4e918caabc46532aa64aa27a0db05d75b20edfc5c6f46479de2"}, + {file = "greenlet-1.1.2-cp38-cp38-win32.whl", hash = "sha256:288c6a76705dc54fba69fbcb59904ae4ad768b4c768839b8ca5fdadec6dd8cfd"}, + {file = "greenlet-1.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:8d2f1fb53a421b410751887eb4ff21386d119ef9cde3797bf5e7ed49fb51a3b3"}, + {file = "greenlet-1.1.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:166eac03e48784a6a6e0e5f041cfebb1ab400b394db188c48b3a84737f505b67"}, + {file = "greenlet-1.1.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:572e1787d1460da79590bf44304abbc0a2da944ea64ec549188fa84d89bba7ab"}, + {file = "greenlet-1.1.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:be5f425ff1f5f4b3c1e33ad64ab994eed12fc284a6ea71c5243fd564502ecbe5"}, + {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1692f7d6bc45e3200844be0dba153612103db241691088626a33ff1f24a0d88"}, + {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7227b47e73dedaa513cdebb98469705ef0d66eb5a1250144468e9c3097d6b59b"}, + {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ff61ff178250f9bb3cd89752df0f1dd0e27316a8bd1465351652b1b4a4cdfd3"}, + {file = "greenlet-1.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0051c6f1f27cb756ffc0ffbac7d2cd48cb0362ac1736871399a739b2885134d3"}, + {file = "greenlet-1.1.2-cp39-cp39-win32.whl", hash = "sha256:f70a9e237bb792c7cc7e44c531fd48f5897961701cdaa06cf22fc14965c496cf"}, + {file = "greenlet-1.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:013d61294b6cd8fe3242932c1c5e36e5d1db2c8afb58606c5a67efce62c1f5fd"}, + {file = "greenlet-1.1.2.tar.gz", hash = "sha256:e30f5ea4ae2346e62cedde8794a56858a67b878dd79f7df76a0767e356b1744a"}, +] h11 = [ {file = "h11-0.13.0-py3-none-any.whl", hash = "sha256:8ddd78563b633ca55346c8cd41ec0af27d3c79931828beffb46ce70a379e7442"}, {file = "h11-0.13.0.tar.gz", hash = "sha256:70813c1135087a248a4d38cc0e1a0181ffab2188141a93eaf567940c3957ff06"}, @@ -623,6 +747,52 @@ sniffio = [ {file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"}, {file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"}, ] +sqlalchemy = [ + {file = "SQLAlchemy-1.4.37-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:d9050b0c4a7f5538650c74aaba5c80cd64450e41c206f43ea6d194ae6d060ff9"}, + {file = "SQLAlchemy-1.4.37-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b4c92823889cf9846b972ee6db30c0e3a92c0ddfc76c6060a6cda467aa5fb694"}, + {file = "SQLAlchemy-1.4.37-cp27-cp27m-win32.whl", hash = "sha256:b55932fd0e81b43f4aff397c8ad0b3c038f540af37930423ab8f47a20b117e4c"}, + {file = "SQLAlchemy-1.4.37-cp27-cp27m-win_amd64.whl", hash = "sha256:4a17c1a1152ca4c29d992714aa9df3054da3af1598e02134f2e7314a32ef69d8"}, + {file = "SQLAlchemy-1.4.37-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ffe487570f47536b96eff5ef2b84034a8ba4e19aab5ab7647e677d94a119ea55"}, + {file = "SQLAlchemy-1.4.37-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:78363f400fbda80f866e8e91d37d36fe6313ff847ded08674e272873c1377ea5"}, + {file = "SQLAlchemy-1.4.37-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ee34c85cbda7779d66abac392c306ec78c13f5c73a1f01b8b767916d4895d23"}, + {file = "SQLAlchemy-1.4.37-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8b38e088659b30c2ca0af63e5d139fad1779a7925d75075a08717a21c406c0f6"}, + {file = "SQLAlchemy-1.4.37-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6629c79967a6c92e33fad811599adf9bc5cee6e504a1027bbf9cc1b6fb2d276d"}, + {file = "SQLAlchemy-1.4.37-cp310-cp310-win32.whl", hash = "sha256:2aac2a685feb9882d09f457f4e5586c885d578af4e97a2b759e91e8c457cbce5"}, + {file = "SQLAlchemy-1.4.37-cp310-cp310-win_amd64.whl", hash = "sha256:7a44683cf97744a405103ef8fdd31199e9d7fc41b4a67e9044523b29541662b0"}, + {file = "SQLAlchemy-1.4.37-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:cffc67cdd07f0e109a1fc83e333972ae423ea5ad414585b63275b66b870ea62b"}, + {file = "SQLAlchemy-1.4.37-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17417327b87a0f703c9a20180f75e953315207d048159aff51822052f3e33e69"}, + {file = "SQLAlchemy-1.4.37-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:aaa0e90e527066409c2ea5676282cf4afb4a40bb9dce0f56c8ec2768bff22a6e"}, + {file = "SQLAlchemy-1.4.37-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c1d9fb3931e27d59166bb5c4dcc911400fee51082cfba66ceb19ac954ade068"}, + {file = "SQLAlchemy-1.4.37-cp36-cp36m-win32.whl", hash = "sha256:0e7fd52e48e933771f177c2a1a484b06ea03774fc7741651ebdf19985a34037c"}, + {file = "SQLAlchemy-1.4.37-cp36-cp36m-win_amd64.whl", hash = "sha256:eec39a17bab3f69c44c9df4e0ed87c7306f2d2bf1eca3070af644927ec4199fa"}, + {file = "SQLAlchemy-1.4.37-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:caca6acf3f90893d7712ae2c6616ecfeac3581b4cc677c928a330ce6fbad4319"}, + {file = "SQLAlchemy-1.4.37-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50c8eaf44c3fed5ba6758d375de25f163e46137c39fda3a72b9ee1d1bb327dfc"}, + {file = "SQLAlchemy-1.4.37-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:139c50b9384e6d32a74fc4dcd0e9717f343ed38f95dbacf832c782c68e3862f3"}, + {file = "SQLAlchemy-1.4.37-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4c3b009c9220ae6e33f17b45f43fb46b9a1d281d76118405af13e26376f2e11"}, + {file = "SQLAlchemy-1.4.37-cp37-cp37m-win32.whl", hash = "sha256:9785d6f962d2c925aeb06a7539ac9d16608877da6aeaaf341984b3693ae80a02"}, + {file = "SQLAlchemy-1.4.37-cp37-cp37m-win_amd64.whl", hash = "sha256:3197441772dc3b1c6419f13304402f2418a18d7fe78000aa5a026e7100836739"}, + {file = "SQLAlchemy-1.4.37-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:3862a069a24f354145e01a76c7c720c263d62405fe5bed038c46a7ce900f5dd6"}, + {file = "SQLAlchemy-1.4.37-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e8706919829d455a9fa687c6bbd1b048e36fec3919a59f2d366247c2bfdbd9c"}, + {file = "SQLAlchemy-1.4.37-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:06ec11a5e6a4b6428167d3ce33b5bd455c020c867dabe3e6951fa98836e0741d"}, + {file = "SQLAlchemy-1.4.37-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d58f2d9d1a4b1459e8956a0153a4119da80f54ee5a9ea623cd568e99459a3ef1"}, + {file = "SQLAlchemy-1.4.37-cp38-cp38-win32.whl", hash = "sha256:d6927c9e3965b194acf75c8e0fb270b4d54512db171f65faae15ef418721996e"}, + {file = "SQLAlchemy-1.4.37-cp38-cp38-win_amd64.whl", hash = "sha256:a91d0668cada27352432f15b92ac3d43e34d8f30973fa8b86f5e9fddee928f3b"}, + {file = "SQLAlchemy-1.4.37-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:f9940528bf9c4df9e3c3872d23078b6b2da6431c19565637c09f1b88a427a684"}, + {file = "SQLAlchemy-1.4.37-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29a742c29fea12259f1d2a9ee2eb7fe4694a85d904a4ac66d15e01177b17ad7f"}, + {file = "SQLAlchemy-1.4.37-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7e579d6e281cc937bdb59917017ab98e618502067e04efb1d24ac168925e1d2a"}, + {file = "SQLAlchemy-1.4.37-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a940c551cfbd2e1e646ceea2777944425f5c3edff914bc808fe734d9e66f8d71"}, + {file = "SQLAlchemy-1.4.37-cp39-cp39-win32.whl", hash = "sha256:5e4e517ce72fad35cce364a01aff165f524449e9c959f1837dc71088afa2824c"}, + {file = "SQLAlchemy-1.4.37-cp39-cp39-win_amd64.whl", hash = "sha256:c37885f83b59e248bebe2b35beabfbea398cb40960cdc6d3a76eac863d4e1938"}, + {file = "SQLAlchemy-1.4.37.tar.gz", hash = "sha256:3688f92c62db6c5df268e2264891078f17ecb91e3141b400f2e28d0f75796dea"}, +] +sqlalchemy2-stubs = [ + {file = "sqlalchemy2-stubs-0.0.2a24.tar.gz", hash = "sha256:e15c45302eafe196ed516f979ef017135fd619d2c62d02de9a5c5f2e59a600c4"}, + {file = "sqlalchemy2_stubs-0.0.2a24-py3-none-any.whl", hash = "sha256:f2399251d3d8f00a88659d711a449c855a0d4e977c7a9134e414f1459b9acc11"}, +] +sqlmodel = [ + {file = "sqlmodel-0.0.6-py3-none-any.whl", hash = "sha256:c5fd8719e09da348cd32ce2a5b6a44f289d3029fa8f1c9818229b6f34f1201b4"}, + {file = "sqlmodel-0.0.6.tar.gz", hash = "sha256:3b4f966b9671b24d85529d274e6c4dbc7753b468e35d2d6a40bd75cad1f66813"}, +] starlette = [ {file = "starlette-0.19.1-py3-none-any.whl", hash = "sha256:5a60c5c2d051f3a8eb546136aa0c9399773a689595e099e0877704d5888279bf"}, {file = "starlette-0.19.1.tar.gz", hash = "sha256:c6d21096774ecb9639acad41b86b7706e52ba3bf1dc13ea4ed9ad593d47e24c7"}, diff --git a/pyproject.toml b/pyproject.toml index 95142a0..1211328 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,7 @@ authors = ["LuisPi "] [tool.poetry.dependencies] python = "^3.10" fastapi = {extras = ["all"], version = "^0.78.0"} +sqlmodel = "^0.0.6" [tool.poetry.dev-dependencies] From 2ef0c1dc74a17a94151d15eec693f396c45db975 Mon Sep 17 00:00:00 2001 From: LuisPi <44235126+lpinon@users.noreply.github.com> Date: Mon, 20 Jun 2022 13:55:43 +0200 Subject: [PATCH 12/19] Controller, Service and Repository Layers --- app/PROD.env => PROD.env | 3 +- README.md | 2 +- app/TEST.env => TEST.env | 3 +- app/controllers/__init__.py | 6 ++++ app/controllers/user.py | 19 ++++++++++ app/core/Configuration.py | 19 +++++----- app/core/database.py | 59 +++++++++++++++++++++++++++----- app/models/__init__.py | 3 +- app/models/user.py | 32 +++++++++++++++-- app/repositories/__init__.py | 0 app/repositories/base.py | 31 +++++++++++++++++ app/repositories/user.py | 43 +++++++++++++++++++++++ app/services/__init__.py | 0 app/services/user.py | 14 ++++++++ app/logging.yaml => logging.yaml | 0 app/main.py => main.py | 25 +++++++------- poetry.lock | 31 ++++++++++++----- pyproject.toml | 2 ++ 18 files changed, 248 insertions(+), 44 deletions(-) rename app/PROD.env => PROD.env (86%) rename app/TEST.env => TEST.env (83%) create mode 100644 app/controllers/__init__.py create mode 100644 app/controllers/user.py create mode 100644 app/repositories/__init__.py create mode 100644 app/repositories/base.py create mode 100644 app/repositories/user.py create mode 100644 app/services/__init__.py create mode 100644 app/services/user.py rename app/logging.yaml => logging.yaml (100%) rename app/main.py => main.py (56%) diff --git a/app/PROD.env b/PROD.env similarity index 86% rename from app/PROD.env rename to PROD.env index d5df665..c47a77d 100644 --- a/app/PROD.env +++ b/PROD.env @@ -12,4 +12,5 @@ DB_PASSWORD=pass DB_HOST=localhost DB_PORT=5432 DB_DATABASE=database -DB_ENABLE_LOGS=False \ No newline at end of file +DB_ENABLE_LOGS=False +DB_POOL_SIZE=5 \ No newline at end of file diff --git a/README.md b/README.md index 60b9f13..c45e3ee 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ - [X] Database Configuration - [ ] ASYNC Database Configuration - [ ] Swagger Configuration -- [ ] JWT Configuration +- [ ] JWT / Keycloak Integration - [ ] CORS Configuration - [ ] Global Exception Management - [ ] DB Entity Generation diff --git a/app/TEST.env b/TEST.env similarity index 83% rename from app/TEST.env rename to TEST.env index f25e2f6..46e48ce 100644 --- a/app/TEST.env +++ b/TEST.env @@ -8,4 +8,5 @@ SWAGGER_PATH=/swagger # Database Configuration DB_TYPE=LOCAL DB_DATABASE=my_database -DB_ENABLE_LOGS=True \ No newline at end of file +DB_ENABLE_LOGS=True +DB_POOL_SIZE=5 \ No newline at end of file diff --git a/app/controllers/__init__.py b/app/controllers/__init__.py new file mode 100644 index 0000000..be187b4 --- /dev/null +++ b/app/controllers/__init__.py @@ -0,0 +1,6 @@ +from fastapi import APIRouter + +from app.controllers import user + +api_router = APIRouter() +api_router.include_router(user.router, tags=["users"]) diff --git a/app/controllers/user.py b/app/controllers/user.py new file mode 100644 index 0000000..9f211bb --- /dev/null +++ b/app/controllers/user.py @@ -0,0 +1,19 @@ +import logging + +from fastapi import APIRouter, Depends + +from app.core.configuration import GlobalSettings, get_global_settings +from app.services.user import UserService + +router = APIRouter(prefix="/users") + +logger = logging.getLogger(__name__) + + +@router.get("/info") +async def info(user_service: UserService = Depends(UserService)): + logger.info("TEST INFO") + logger.error("TEST ERROR") + logger.debug("TEST DEBUG") + await user_service.get_user_by_email(email="test@email.es") + return {} diff --git a/app/core/Configuration.py b/app/core/Configuration.py index b005d00..f8c0ac4 100644 --- a/app/core/Configuration.py +++ b/app/core/Configuration.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Optional, Type, Generic import yaml from fastapi import FastAPI @@ -11,7 +11,7 @@ class GlobalSettings(BaseSettings): app_name: str = "My Awesome API" environment: str = "TEST" port: int = 80 - swagger_path: str = "docs" + swagger_path: str = "/docs" class Config: env_file = "TEST.env" @@ -25,13 +25,14 @@ class DatabaseSettings(BaseSettings): port: Optional[int] = None database: Optional[str] enable_logs = False + pool_size = 5 class Config: env_prefix = "DB_" env_file = "TEST.env" -def __load_env_file_on_settings(settings): +def __load_env_file_on_settings(settings: Type[BaseSettings]): env = os.environ.get("ENV") if env: return settings(_env_file="{}.env".format(env)) @@ -40,12 +41,12 @@ def __load_env_file_on_settings(settings): @lru_cache() -def get_global_settings(): +def get_global_settings() -> GlobalSettings: return __load_env_file_on_settings(GlobalSettings) @lru_cache() -def get_db_settings(): +def get_db_settings() -> DatabaseSettings: return __load_env_file_on_settings(DatabaseSettings) @@ -56,7 +57,9 @@ def get_log_config(): return log_config -def get_app(): +def get_api(): app_settings = get_global_settings() - app = FastAPI(docs_url=app_settings.swagger_path) - return app + from app.controllers import api_router + api = FastAPI(docs_url=app_settings.swagger_path) + api.include_router(api_router) + return api diff --git a/app/core/database.py b/app/core/database.py index 1b47504..3185258 100644 --- a/app/core/database.py +++ b/app/core/database.py @@ -1,25 +1,66 @@ from functools import lru_cache +from typing import Generator from fastapi import Depends +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession from sqlalchemy.future import Engine +from sqlalchemy.orm import sessionmaker from .configuration import DatabaseSettings, get_db_settings from sqlmodel import Field, Session, SQLModel, create_engine -def get_db_engine(db: DatabaseSettings = Depends(get_db_settings)) -> Engine: - if db.type == "LOCAL": - engine = create_engine("sqlite:///{}.db".format(db.database), echo=db.enable_logs, echo_pool=db.enable_logs) - elif db.type == "PG": - engine = create_engine("postgresql+psycopg2://{0}:{1}@{2}:{3}/{4}".format( - db.username, db.password, db.host, db.port, db.database), echo=db.enable_logs, echo_pool=db.enable_logs) +def get_db_uri(db_settings: DatabaseSettings = Depends(get_db_settings)) -> str: + if db_settings.type == "LOCAL": + uri = "sqlite:///{}.db".format(db_settings.database) + elif db_settings.type == "PG": + uri = "postgresql+psycopg2://{0}:{1}@{2}:{3}/{4}".format( + db_settings.username, db_settings.password, db_settings.host, db_settings.port, db_settings.database) else: - engine = create_engine("sqlite:///database.db") - return engine + uri = "sqlite:///database.db" + return uri + + +def get_async_db_uri(db_settings: DatabaseSettings = Depends(get_db_settings)) -> str: + if db_settings.type == "LOCAL": + uri = "sqlite+aiosqlite:///{}.db".format(db_settings.database) + elif db_settings.type == "PG": + uri = "postgresql+psycopg2://{0}:{1}@{2}:{3}/{4}".format( + db_settings.username, db_settings.password, db_settings.host, db_settings.port, db_settings.database) + else: + uri = "sqlite+aiosqlite:///database.db" + return uri + + +def get_db_engine(settings: DatabaseSettings = Depends(get_db_settings), db_uri: str = Depends(get_db_uri)) -> Engine: + return create_engine(db_uri, echo=settings.enable_logs, echo_pool=settings.enable_logs, pool_pre_ping=True) + + +def get_async_db_engine(settings: DatabaseSettings = Depends(get_db_settings), db_uri: str = Depends(get_async_db_uri)): + return create_async_engine(db_uri, + echo=settings.enable_logs, echo_pool=settings.enable_logs, + future=True, pool_pre_ping=True) def init_db_entities(db: DatabaseSettings): import app.models - engine = get_db_engine(db) + engine = get_db_engine(db, get_db_uri(db)) SQLModel.metadata.create_all(engine) + +def get_db_session_factory(engine: Engine = Depends(get_async_db_engine)): + """ + Generates a session factory from the configured SQL Engine + """ + return sessionmaker(autocommit=False, class_=AsyncSession, autoflush=False, bind=engine) + +# +# def get_db(session_factory: sessionmaker = Depends(get_session_factory)) -> Generator: +# """ +# Generates a database session from the configured factory // TODO: Problem with async close +# """ +# try: +# db = session_factory() +# yield db +# finally: +# db.close() diff --git a/app/models/__init__.py b/app/models/__init__.py index b1e65ed..e4e1b26 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -1,4 +1,5 @@ -# Import DB entities for init +# Import DB Entities for init +# Tables must extend SQLModel from .group import Group from .links import LinkGroupUser from .role import Role diff --git a/app/models/user.py b/app/models/user.py index 0d45c27..f978914 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -1,11 +1,15 @@ from datetime import datetime +from enum import Enum + from sqlmodel import Field, SQLModel, Relationship, Column, DateTime from app.models.links import LinkGroupUser from typing import List, Optional -from pydantic import EmailStr +from pydantic import EmailStr, BaseModel from app.models.base import BaseUUIDModel from uuid import UUID +# DB ENTITY + class UserBase(SQLModel): first_name: str @@ -24,5 +28,29 @@ class User(BaseUUIDModel, UserBase, table=True): hashed_password: str = Field(nullable=False, index=True) role_id: Optional[UUID] = Field(default=None, foreign_key="role.id") role: Optional["Role"] = Relationship(back_populates="users", sa_relationship_kwargs={"lazy": "selectin"}) - groups: List["Group"] = Relationship(back_populates="users", link_model=LinkGroupUser, sa_relationship_kwargs={"lazy": "selectin"}) + groups: List["Group"] = Relationship(back_populates="users", link_model=LinkGroupUser, + sa_relationship_kwargs={"lazy": "selectin"}) + + +# REQUESTS + +class UserCreateRequest(BaseModel): + first_name: Optional[str] + last_name: Optional[str] + password: Optional[str] + email: EmailStr + is_superuser: bool = False + role_id: Optional[UUID] + + +class UserUpdateRequest(BaseModel): + id: int + email: EmailStr + is_active: bool = True + + +# RESPONSES +class UserStatusEnum(str, Enum): + active = 'active' + inactive = 'inactive' diff --git a/app/repositories/__init__.py b/app/repositories/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/repositories/base.py b/app/repositories/base.py new file mode 100644 index 0000000..206edd2 --- /dev/null +++ b/app/repositories/base.py @@ -0,0 +1,31 @@ +from typing import Generic, TypeVar, Type, Union, Optional +from uuid import UUID + +from fastapi import Depends +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import selectinload, sessionmaker +from sqlmodel import SQLModel + +from app.core.database import get_db_session_factory + +ModelType = TypeVar("ModelType", bound=SQLModel) + + +class BaseRepository(Generic[ModelType]): + + def __init__(self, model: Type[ModelType], session: sessionmaker = Depends(get_db_session_factory)): + """ + Object with default methods to Create, Read, Update and Delete (CRUD). + """ + self.model = model + self.create_session = session + + async def get(self, *, id: Union[UUID, str]) -> Optional[ModelType]: + async with self.create_session() as db_session: + response = await db_session.exec( + select(self.model) + .where(self.model.id == id) + .options(selectinload('*')) + ) + return response.first() diff --git a/app/repositories/user.py b/app/repositories/user.py new file mode 100644 index 0000000..16f8425 --- /dev/null +++ b/app/repositories/user.py @@ -0,0 +1,43 @@ +from datetime import datetime +from typing import Optional +from uuid import UUID + +from fastapi import Depends +from sqlalchemy import select +from sqlalchemy.orm import sessionmaker +from sqlmodel.ext.asyncio.session import AsyncSession + +from app.core.database import get_db_session_factory +from app.models import User +from app.models.user import UserCreateRequest +from app.repositories.base import BaseRepository + + +class UserRepository(BaseRepository[User]): + + async def get_by_email(self, *, email: str) -> Optional[User]: + async with self.create_session() as db_session: + users = await db_session.execute(select(User).where(User.email == email)) + result = users.first() + return result + + async def create_with_role(self, *, req: UserCreateRequest) -> User: + async with self.create_session() as db_session: + db_obj = User( + first_name=req.first_name, + last_name=req.last_name, + email=req.email, + is_superuser=req.is_superuser, + # hashed_password=get_password_hash(obj_in.password), + created_at=datetime.utcnow(), + updated_at=datetime.utcnow(), + role_id=req.role_id + ) + db_session.add(db_obj) + await db_session.commit() + await db_session.refresh(db_obj) + return db_obj + + +def get_user_repository(session_factory: sessionmaker = Depends(get_db_session_factory)): + return UserRepository(User, session_factory) diff --git a/app/services/__init__.py b/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/services/user.py b/app/services/user.py new file mode 100644 index 0000000..41bdfaf --- /dev/null +++ b/app/services/user.py @@ -0,0 +1,14 @@ +from typing import Optional + +from fastapi import Depends + +from app.models import User +from app.repositories.user import get_user_repository, UserRepository + + +class UserService: + def __init__(self, repository: UserRepository = Depends(get_user_repository)): + self.user_repo = repository + + async def get_user_by_email(self, email: str) -> Optional[User]: + return await self.user_repo.get_by_email(email=email) diff --git a/app/logging.yaml b/logging.yaml similarity index 100% rename from app/logging.yaml rename to logging.yaml diff --git a/app/main.py b/main.py similarity index 56% rename from app/main.py rename to main.py index 36f4301..4d8994b 100644 --- a/app/main.py +++ b/main.py @@ -1,36 +1,35 @@ +import logging import uvicorn -from fastapi import Depends, FastAPI -from sqlalchemy.future import Engine -from sqlmodel import SQLModel +from fastapi import Depends -from core.database import get_db_engine, init_db_entities -from core.configuration import GlobalSettings, get_global_settings, get_app, get_log_config, DatabaseSettings, \ - get_db_settings -import logging +from app.core.configuration import get_global_settings, get_api, get_log_config, get_db_settings, GlobalSettings +from app.core.database import init_db_entities # Init APP with Configuration -app = get_app() +from app.services.user import UserService + +api = get_api() # Init Logger for this Class logger = logging.getLogger(__name__) -@app.get("/info") -def info(settings: GlobalSettings = Depends(get_global_settings)): +@api.get("/info") +async def info(settings: GlobalSettings = Depends(get_global_settings), + user_service: UserService = Depends(UserService)): logger.info("TEST INFO") logger.error("TEST ERROR") logger.debug("TEST DEBUG") + await user_service.get_user_by_email(email="test@email.es") return { "app_name": settings.app_name, "environment": settings.environment, "swagger": settings.swagger_path if settings.swagger_path else "DISABLED" } - if __name__ == "__main__": global_settings = get_global_settings() logging_settings = get_log_config() db_settings = get_db_settings() init_db_entities(db_settings) print(global_settings.port, global_settings.environment) - uvicorn.run(app, host="0.0.0.0", port=global_settings.port, log_config=logging_settings) - + uvicorn.run(api, host="0.0.0.0", port=global_settings.port, log_config=logging_settings) diff --git a/poetry.lock b/poetry.lock index 05fac2e..5dcca60 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,3 +1,14 @@ +[[package]] +name = "aiosqlite" +version = "0.17.0" +description = "asyncio bridge to the standard sqlite3 module" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +typing_extensions = ">=3.7.2" + [[package]] name = "anyio" version = "3.6.1" @@ -242,20 +253,20 @@ python-versions = ">=3.6" [[package]] name = "requests" -version = "2.27.1" +version = "2.28.0" description = "Python HTTP for Humans." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +python-versions = ">=3.7, <4" [package.dependencies] certifi = ">=2017.4.17" -charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} -idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} +charset-normalizer = ">=2.0.0,<2.1.0" +idna = ">=2.5,<4" urllib3 = ">=1.21.1,<1.27" [package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] [[package]] @@ -431,9 +442,13 @@ python-versions = ">=3.7" [metadata] lock-version = "1.1" python-versions = "^3.10" -content-hash = "35d665498da177002af87d8c28489085f4b730a39ce29f6736dbe750778cdad1" +content-hash = "c6591fc2fffa67d98dd21c192d9e315e671b3a2bb94a2b003c83a369f79e039c" [metadata.files] +aiosqlite = [ + {file = "aiosqlite-0.17.0-py3-none-any.whl", hash = "sha256:6c49dc6d3405929b1d08eeccc72306d3677503cc5e5e43771efc1e00232e8231"}, + {file = "aiosqlite-0.17.0.tar.gz", hash = "sha256:f0e6acc24bc4864149267ac82fb46dfb3be4455f99fe21df82609cc6e6baee51"}, +] anyio = [ {file = "anyio-3.6.1-py3-none-any.whl", hash = "sha256:cb29b9c70620506a9a8f87a309591713446953302d7d995344d0d7c6c0c9a7be"}, {file = "anyio-3.6.1.tar.gz", hash = "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b"}, @@ -736,8 +751,8 @@ pyyaml = [ {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, ] requests = [ - {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, - {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, + {file = "requests-2.28.0-py3-none-any.whl", hash = "sha256:bc7861137fbce630f17b03d3ad02ad0bf978c844f3536d0edda6499dafce2b6f"}, + {file = "requests-2.28.0.tar.gz", hash = "sha256:d568723a7ebd25875d8d1eaf5dfa068cd2fc8194b2e483d7b1f7c81918dbec6b"}, ] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, diff --git a/pyproject.toml b/pyproject.toml index 1211328..ac4eb27 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,8 @@ authors = ["LuisPi "] python = "^3.10" fastapi = {extras = ["all"], version = "^0.78.0"} sqlmodel = "^0.0.6" +requests = "^2.28.0" +aiosqlite = "^0.17.0" [tool.poetry.dev-dependencies] From cf14ee79db8eac0270ac35fcd16c4cf6156db6ba Mon Sep 17 00:00:00 2001 From: LuisPi <44235126+lpinon@users.noreply.github.com> Date: Wed, 22 Jun 2022 11:39:42 +0200 Subject: [PATCH 13/19] CORS Configuration --- PROD.env | 3 ++- README.md | 4 ++-- TEST.env | 3 ++- app/core/Configuration.py | 29 ++++++++++++++++++++++++++--- 4 files changed, 32 insertions(+), 7 deletions(-) diff --git a/PROD.env b/PROD.env index c47a77d..adf8e27 100644 --- a/PROD.env +++ b/PROD.env @@ -1,6 +1,7 @@ -APP_NAME="Devon4py API Production" +APP_NAME="Devon4Py API Production" ENVIRONMENT="PROD" PORT=8000 +CORS= # Swagger Configuration SWAGGER_PATH= diff --git a/README.md b/README.md index c45e3ee..2336824 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,9 @@ - [X] Log Configuration - [X] Database Configuration - [ ] ASYNC Database Configuration -- [ ] Swagger Configuration +- [X] Swagger Configuration - [ ] JWT / Keycloak Integration -- [ ] CORS Configuration +- [X] CORS Configuration - [ ] Global Exception Management - [ ] DB Entity Generation - [ ] Code generation using Templates diff --git a/TEST.env b/TEST.env index 46e48ce..37977b4 100644 --- a/TEST.env +++ b/TEST.env @@ -1,6 +1,7 @@ -APP_NAME="Devon4py API Testing" +APP_NAME="Devon4Py API Testing" ENVIRONMENT="TEST" PORT=9999 +CORS=https://www.google.es,https://www.capgemini.com # Swagger Configuration SWAGGER_PATH=/swagger diff --git a/app/core/Configuration.py b/app/core/Configuration.py index f8c0ac4..2d20c38 100644 --- a/app/core/Configuration.py +++ b/app/core/Configuration.py @@ -1,10 +1,11 @@ -from typing import Optional, Type, Generic +from typing import Optional, Type, Generic, Union, List import yaml from fastapi import FastAPI -from pydantic import BaseSettings +from pydantic import BaseSettings, AnyHttpUrl, validator import os from functools import lru_cache +from fastapi.middleware.cors import CORSMiddleware class GlobalSettings(BaseSettings): @@ -12,6 +13,18 @@ class GlobalSettings(BaseSettings): environment: str = "TEST" port: int = 80 swagger_path: str = "/docs" + cors: List[str] | List[AnyHttpUrl] = [] + + @validator("cors", pre=True) + def assemble_cors_origins(cls, v: str | List[str]) -> List[str] | str: + if v is None: + print("CORS Not Specified") + return [] + if isinstance(v, str) and not v.startswith("["): + return [i.strip() for i in v.split(",")] + elif isinstance(v, (list, str)): + return v + raise ValueError(v) class Config: env_file = "TEST.env" @@ -60,6 +73,16 @@ def get_log_config(): def get_api(): app_settings = get_global_settings() from app.controllers import api_router - api = FastAPI(docs_url=app_settings.swagger_path) + api = FastAPI(docs_url=app_settings.swagger_path, title=app_settings.app_name) + # Set all CORS enabled origins + if app_settings.cors and len(app_settings.cors) > 0: + api.add_middleware( + CORSMiddleware, + allow_origins=app_settings.cors, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + expose_headers=["*"] + ) api.include_router(api_router) return api From 29954963796ad9c6ff3a4dfceb683aab559b2c77 Mon Sep 17 00:00:00 2001 From: LuisPi <44235126+lpinon@users.noreply.github.com> Date: Thu, 23 Jun 2022 09:55:27 +0200 Subject: [PATCH 14/19] Keycloak Authentication Integration --- PROD.env | 12 +- TEST.env | 12 +- app/controllers/__common__.py | 24 + app/controllers/__init__.py | 5 +- app/controllers/auth.py | 55 + app/controllers/identity.py | 161 +++ app/controllers/user.py | 1 - app/core/Configuration.py | 50 +- app/core/database.py | 5 +- docker-compose.yaml | 35 + poetry.lock | 116 +- pyproject.toml | 1 + realm-export-orig.json | 2051 ++++++++++++++++++++++++++++ realm-export.json | 2372 +++++++++++++++++++++++++++++++++ 14 files changed, 4887 insertions(+), 13 deletions(-) create mode 100644 app/controllers/__common__.py create mode 100644 app/controllers/auth.py create mode 100644 app/controllers/identity.py create mode 100644 docker-compose.yaml create mode 100644 realm-export-orig.json create mode 100644 realm-export.json diff --git a/PROD.env b/PROD.env index adf8e27..a9659bd 100644 --- a/PROD.env +++ b/PROD.env @@ -3,7 +3,7 @@ ENVIRONMENT="PROD" PORT=8000 CORS= -# Swagger Configuration +# Swagger Configuration | Leave empty to disable SWAGGER_PATH= # Database Configuration @@ -14,4 +14,12 @@ DB_HOST=localhost DB_PORT=5432 DB_DATABASE=database DB_ENABLE_LOGS=False -DB_POOL_SIZE=5 \ No newline at end of file +DB_POOL_SIZE=5 + +# Keycloak Configuration | Remove Configuration to Disable +KEYCLOAK_AUTH_SERVER=http://localhost:8085/auth +KEYCLOAK_CLIENT_ID=test-client +KEYCLOAK_CLIENT_SECRET=GzgACcJzhzQ4j8kWhmhazt7WSdxDVUyE +KEYCLOAK_ADMIN_CLIENT_SECRET=BIcczGsZ6I8W5zf0rZg5qSexlloQLPKB +KEYCLOAK_REALM=Test +KEYCLOAK_CALLBACK_URI=http://localhost:9999/auth/callback \ No newline at end of file diff --git a/TEST.env b/TEST.env index 37977b4..5f7eff2 100644 --- a/TEST.env +++ b/TEST.env @@ -3,11 +3,19 @@ ENVIRONMENT="TEST" PORT=9999 CORS=https://www.google.es,https://www.capgemini.com -# Swagger Configuration +# Swagger Configuration | Leave empty to disable SWAGGER_PATH=/swagger # Database Configuration DB_TYPE=LOCAL DB_DATABASE=my_database DB_ENABLE_LOGS=True -DB_POOL_SIZE=5 \ No newline at end of file +DB_POOL_SIZE=5 + +# Keycloak Configuration | Remove Configuration to Disable +KEYCLOAK_AUTH_SERVER=http://localhost:8085/auth +KEYCLOAK_CLIENT_ID=test-client +KEYCLOAK_CLIENT_SECRET=GzgACcJzhzQ4j8kWhmhazt7WSdxDVUyE +KEYCLOAK_ADMIN_CLIENT_SECRET=kPjQYZkpOvd0oSuRSHbFQbQaYn8kQ2qG +KEYCLOAK_REALM=Test +KEYCLOAK_CALLBACK_URI=http://localhost:9999/auth/callback \ No newline at end of file diff --git a/app/controllers/__common__.py b/app/controllers/__common__.py new file mode 100644 index 0000000..158ca9f --- /dev/null +++ b/app/controllers/__common__.py @@ -0,0 +1,24 @@ +from app.core.configuration import get_idp + +idp = get_idp() + + +# Shortcut for checking current user and roles +def get_user(required_roles: list[str] | None = None): + """Returns the current user based on an access token in the HTTP-header. Optionally verifies roles are possessed + by the user + + Args: + required_roles List[str]: List of role names required for this endpoint + extra_fields List[str]: The names of the additional fields you need that are encoded in JWT + + Returns: + OIDCUser: Decoded JWT content + + Raises: + ExpiredSignatureError: If the token is expired (exp > datetime.now()) + JWTError: If decoding fails or the signature is invalid + JWTClaimsError: If any claim is invalid + HTTPException: If any role required is not contained within the roles of the users + """ + return idp.get_current_user() diff --git a/app/controllers/__init__.py b/app/controllers/__init__.py index be187b4..f6007d1 100644 --- a/app/controllers/__init__.py +++ b/app/controllers/__init__.py @@ -1,6 +1,9 @@ from fastapi import APIRouter -from app.controllers import user +from app.controllers import user, auth, identity +# Include all routers here api_router = APIRouter() api_router.include_router(user.router, tags=["users"]) +api_router.include_router(auth.router, tags=["auth"]) +api_router.include_router(identity.router, tags=["idp"]) diff --git a/app/controllers/auth.py b/app/controllers/auth.py new file mode 100644 index 0000000..a337ce5 --- /dev/null +++ b/app/controllers/auth.py @@ -0,0 +1,55 @@ +import logging + +from fastapi import APIRouter, Depends +from fastapi_keycloak import OIDCUser, UsernamePassword +from starlette.responses import RedirectResponse + +from app.controllers.__common__ import get_user, idp + +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/auth") + +################################# +# Basic Authentication Router +################################# + + +@router.get("/") # Unprotected +def root(): + return 'Hello World' + + +@router.get("/user") # Requires logged in +def current_users(user: OIDCUser = Depends(get_user)): + return user + + +@router.get("/current_user/roles") +def get_current_users_roles(user: OIDCUser = Depends(get_user)): + return user.roles + + +@router.get("/admin") # Requires the admin role +def company_admin(user: OIDCUser = Depends(get_user(required_roles=["admin"]))): + return f'Hi admin {user}' + + +@router.get("/login") +def login_redirect(): + return RedirectResponse(idp.login_uri) + + +@router.get("/login-oauth") +def login(user: UsernamePassword = Depends()): + return idp.user_login(username=user.username, password=user.password.get_secret_value()) + + +@router.get("/callback") +def callback(session_state: str, code: str): + return idp.exchange_authorization_code(session_state=session_state, code=code) # This will return an access token + + +@router.get("/logout") +def logout(): + return idp.logout_uri diff --git a/app/controllers/identity.py b/app/controllers/identity.py new file mode 100644 index 0000000..a8d941b --- /dev/null +++ b/app/controllers/identity.py @@ -0,0 +1,161 @@ +from typing import Optional, List + +from fastapi import APIRouter, Depends, Body, Query +from fastapi_keycloak import HTTPMethod, KeycloakUser +from pydantic import SecretStr + +from app.controllers.__common__ import get_user, idp + +router = APIRouter(prefix="/idp", dependencies=[Depends(get_user)]) # Protect all the paths with user authentication + + +################################# +# IDP Admin Router +################################# + + +@router.post("/proxy", tags=["admin-cli"]) +def proxy_admin_request(relative_path: str, method: HTTPMethod, additional_headers: dict = Body(None), + payload: dict = Body(None)): + return idp.proxy( + additional_headers=additional_headers, + relative_path=relative_path, + method=method, + payload=payload + ) + + +@router.get("/identity-providers", tags=["admin-cli"]) +def get_identity_providers(): + return idp.get_identity_providers() + + +@router.get("/idp-configuration", tags=["admin-cli"]) +def get_idp_config(): + return idp.open_id_configuration + + +# User Management + +@router.get("/users", tags=["user-management"]) +def get_users(): + return idp.get_all_users() + + +@router.get("/user", tags=["user-management"]) +def get_user_by_query(query: str = None): + return idp.get_user(query=query) + + +@router.post("/users", tags=["user-management"]) +def create_user(first_name: str, last_name: str, email: str, password: SecretStr, id: str = None): + return idp.create_user(first_name=first_name, last_name=last_name, username=email, email=email, + password=password.get_secret_value(), id=id) + + +@router.get("/user/{user_id}", tags=["user-management"]) +def get_user(user_id: str = None): + return idp.get_user(user_id=user_id) + + +@router.put("/user", tags=["user-management"]) +def update_user(user: KeycloakUser): + return idp.update_user(user=user) + + +@router.delete("/user/{user_id}", tags=["user-management"]) +def delete_user(user_id: str): + return idp.delete_user(user_id=user_id) + + +@router.put("/user/{user_id}/change-password", tags=["user-management"]) +def change_password(user_id: str, new_password: SecretStr): + return idp.change_password(user_id=user_id, new_password=new_password) + + +@router.put("/user/{user_id}/send-email-verification", tags=["user-management"]) +def send_email_verification(user_id: str): + return idp.send_email_verification(user_id=user_id) + + +# Role Management + +@router.get("/roles", tags=["role-management"]) +def get_all_roles(): + return idp.get_all_roles() + + +@router.get("/role/{role_name}", tags=["role-management"]) +def get_role(role_name: str): + return idp.get_roles([role_name]) + + +@router.post("/roles", tags=["role-management"]) +def add_role(role_name: str): + return idp.create_role(role_name=role_name) + + +@router.delete("/roles", tags=["role-management"]) +def delete_roles(role_name: str): + return idp.delete_role(role_name=role_name) + + +# Group Management + +@router.get("/groups", tags=["group-management"]) +def get_all_groups(): + return idp.get_all_groups() + + +@router.get("/group/{group_name}", tags=["group-management"]) +def get_group(group_name: str): + return idp.get_groups([group_name]) + + +@router.get("/group-by-path/{path: path}", tags=["group-management"]) +def get_group_by_path(path: str): + return idp.get_group_by_path(path) + + +@router.post("/groups", tags=["group-management"]) +def add_group(group_name: str, parent_id: Optional[str] = None): + return idp.create_group(group_name=group_name, parent=parent_id) + + +@router.delete("/groups", tags=["group-management"]) +def delete_groups(group_id: str): + return idp.delete_group(group_id=group_id) + + +# User Roles + +@router.post("/users/{user_id}/roles", tags=["user-roles"]) +def add_roles_to_user(user_id: str, roles: Optional[List[str]] = Query(None)): + return idp.add_user_roles(user_id=user_id, roles=roles) + + +@router.get("/users/{user_id}/roles", tags=["user-roles"]) +def get_user_roles(user_id: str): + return idp.get_user_roles(user_id=user_id) + + +@router.delete("/users/{user_id}/roles", tags=["user-roles"]) +def delete_roles_from_user(user_id: str, roles: Optional[List[str]] = Query(None)): + return idp.remove_user_roles(user_id=user_id, roles=roles) + + +# User Groups + +@router.post("/users/{user_id}/groups", tags=["user-groups"]) +def add_group_to_user(user_id: str, group_id: str): + return idp.add_user_group(user_id=user_id, group_id=group_id) + + +@router.get("/users/{user_id}/groups", tags=["user-groups"]) +def get_user_groups(user_id: str): + return idp.get_user_groups(user_id=user_id) + + +@router.delete("/users/{user_id}/groups", tags=["user-groups"]) +def delete_groups_from_user(user_id: str, group_id: str): + return idp.remove_user_group(user_id=user_id, group_id=group_id) diff --git a/app/controllers/user.py b/app/controllers/user.py index 9f211bb..c57b4a4 100644 --- a/app/controllers/user.py +++ b/app/controllers/user.py @@ -2,7 +2,6 @@ from fastapi import APIRouter, Depends -from app.core.configuration import GlobalSettings, get_global_settings from app.services.user import UserService router = APIRouter(prefix="/users") diff --git a/app/core/Configuration.py b/app/core/Configuration.py index 2d20c38..2755449 100644 --- a/app/core/Configuration.py +++ b/app/core/Configuration.py @@ -2,12 +2,15 @@ import yaml from fastapi import FastAPI +from fastapi_keycloak import FastAPIKeycloak from pydantic import BaseSettings, AnyHttpUrl, validator import os from functools import lru_cache from fastapi.middleware.cors import CORSMiddleware +# Configuration Objects Definitions + class GlobalSettings(BaseSettings): app_name: str = "My Awesome API" environment: str = "TEST" @@ -45,6 +48,21 @@ class Config: env_file = "TEST.env" +class KeycloakSettings(BaseSettings): + auth_server: Optional[str] + client_id: Optional[str] + client_secret: Optional[str] + admin_client_secret: Optional[str] + realm: Optional[str] + callback_uri: Optional[str] + + class Config: + env_prefix = "KEYCLOAK_" + env_file = "TEST.env" + + +# Utils to load Configurations + def __load_env_file_on_settings(settings: Type[BaseSettings]): env = os.environ.get("ENV") if env: @@ -63,6 +81,11 @@ def get_db_settings() -> DatabaseSettings: return __load_env_file_on_settings(DatabaseSettings) +@lru_cache() +def get_keycloak_settings() -> KeycloakSettings: + return __load_env_file_on_settings(KeycloakSettings) + + @lru_cache() def get_log_config(): with open("logging.yaml") as logconf: @@ -70,11 +93,32 @@ def get_log_config(): return log_config +def get_idp(): + print("Init Keycloak") + keycloak_settings = get_keycloak_settings() + # Check if configuration is defined to use Keycloak IDP + if keycloak_settings.auth_server is None or keycloak_settings.realm is None: + return None + # Configure Keycloak Authentication + idp = FastAPIKeycloak( + server_url=keycloak_settings.auth_server, + client_id=keycloak_settings.client_id, + client_secret=keycloak_settings.client_secret, + admin_client_secret=keycloak_settings.admin_client_secret, + realm=keycloak_settings.realm, + callback_uri=keycloak_settings.callback_uri + ) + return idp + + def get_api(): app_settings = get_global_settings() - from app.controllers import api_router api = FastAPI(docs_url=app_settings.swagger_path, title=app_settings.app_name) - # Set all CORS enabled origins + idp = get_idp() + if idp is not None: + # Enable authentication layer to swagger endpoints + idp.add_swagger_config(api) + # Set CORS enabled origins if app_settings.cors and len(app_settings.cors) > 0: api.add_middleware( CORSMiddleware, @@ -84,5 +128,7 @@ def get_api(): allow_headers=["*"], expose_headers=["*"] ) + # Include all Routers + from app.controllers import api_router api.include_router(api_router) return api diff --git a/app/core/database.py b/app/core/database.py index 3185258..c2d6472 100644 --- a/app/core/database.py +++ b/app/core/database.py @@ -1,13 +1,10 @@ -from functools import lru_cache -from typing import Generator - from fastapi import Depends from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession from sqlalchemy.future import Engine from sqlalchemy.orm import sessionmaker +from sqlmodel import SQLModel, create_engine from .configuration import DatabaseSettings, get_db_settings -from sqlmodel import Field, Session, SQLModel, create_engine def get_db_uri(db_settings: DatabaseSettings = Depends(get_db_settings)) -> str: diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..73feb7d --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,35 @@ +version: '3' + +services: + postgres: + image: postgres + environment: + POSTGRES_DB: testkeycloakdb + POSTGRES_USER: testkeycloakuser + POSTGRES_PASSWORD: testkeycloakpassword + restart: + always + + keycloak: + image: jboss/keycloak:16.1.0 + volumes: + - ./realm-export.json:/opt/jboss/keycloak/imports/realm-export.json + command: + - "-b 0.0.0.0 -Dkeycloak.profile.feature.upload_scripts=enabled -Dkeycloak.import=/opt/jboss/keycloak/imports/realm-export.json" + environment: + DB_VENDOR: POSTGRES + DB_ADDR: postgres + DB_DATABASE: testkeycloakdb + DB_USER: testkeycloakuser + DB_SCHEMA: public + DB_PASSWORD: testkeycloakpassword + KEYCLOAK_USER: keycloakuser + KEYCLOAK_PASSWORD: keycloakpassword + PROXY_ADDRESS_FORWARDING: "true" + KEYCLOAK_LOGLEVEL: DEBUG + ports: + - '8085:8080' + depends_on: + - postgres + restart: + always \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 5dcca60..25defb3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -91,6 +91,21 @@ idna = ["idna (>=2.1,<4.0)"] trio = ["trio (>=0.14,<0.20)"] wmi = ["wmi (>=1.5.1,<2.0.0)"] +[[package]] +name = "ecdsa" +version = "0.17.0" +description = "ECDSA cryptographic signature library (pure python)" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[package.dependencies] +six = ">=1.9.0" + +[package.extras] +gmpy = ["gmpy"] +gmpy2 = ["gmpy2"] + [[package]] name = "email-validator" version = "1.2.1" @@ -130,6 +145,37 @@ dev = ["python-jose[cryptography] (>=3.3.0,<4.0.0)", "passlib[bcrypt] (>=1.7.2,< doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "typer (>=0.4.1,<0.5.0)", "pyyaml (>=5.3.1,<7.0.0)"] test = ["pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "mypy (==0.910)", "flake8 (>=3.8.3,<4.0.0)", "black (==22.3.0)", "isort (>=5.0.6,<6.0.0)", "requests (>=2.24.0,<3.0.0)", "httpx (>=0.14.0,<0.19.0)", "email_validator (>=1.1.1,<2.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "peewee (>=3.13.3,<4.0.0)", "databases[sqlite] (>=0.3.2,<0.6.0)", "orjson (>=3.2.1,<4.0.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "flask (>=1.1.2,<3.0.0)", "anyio[trio] (>=3.2.1,<4.0.0)", "types-ujson (==4.2.1)", "types-orjson (==3.6.2)", "types-dataclasses (==0.6.5)"] +[[package]] +name = "fastapi-keycloak" +version = "1.0.6" +description = "Keycloak API Client for integrating authentication and authorization with FastAPI" +category = "main" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +anyio = ">=3.4.0" +asgiref = ">=3.4.1" +certifi = ">=2021.10.8" +charset-normalizer = ">=2.0.9" +click = ">=8.0.3" +ecdsa = ">=0.17.0" +fastapi = ">=0.70.1" +h11 = ">=0.12.0" +idna = ">=3.3" +itsdangerous = ">=2.0.1" +pyasn1 = ">=0.4.8" +pydantic = ">=1.5a1" +python-jose = ">=3.3.0" +requests = ">=2.26.0" +rsa = ">=4.8" +six = ">=1.16.0" +sniffio = ">=1.2.0" +starlette = ">=0.16.0" +typing_extensions = ">=4.0.1" +urllib3 = ">=1.26.7" +uvicorn = ">=0.16.0" + [[package]] name = "greenlet" version = "1.1.2" @@ -206,6 +252,14 @@ category = "main" optional = false python-versions = ">=3.7" +[[package]] +name = "pyasn1" +version = "0.4.8" +description = "ASN.1 types and codecs" +category = "main" +optional = false +python-versions = "*" + [[package]] name = "pydantic" version = "1.9.1" @@ -232,6 +286,24 @@ python-versions = ">=3.5" [package.extras] cli = ["click (>=5.0)"] +[[package]] +name = "python-jose" +version = "3.3.0" +description = "JOSE implementation in Python" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +ecdsa = "!=0.15" +pyasn1 = "*" +rsa = "*" + +[package.extras] +cryptography = ["cryptography (>=3.4.0)"] +pycrypto = ["pycrypto (>=2.6.0,<2.7.0)", "pyasn1"] +pycryptodome = ["pycryptodome (>=3.3.1,<4.0.0)", "pyasn1"] + [[package]] name = "python-multipart" version = "0.0.5" @@ -269,6 +341,17 @@ urllib3 = ">=1.21.1,<1.27" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] +[[package]] +name = "rsa" +version = "4.8" +description = "Pure-Python RSA implementation" +category = "main" +optional = false +python-versions = ">=3.6,<4" + +[package.dependencies] +pyasn1 = ">=0.1.3" + [[package]] name = "six" version = "1.16.0" @@ -442,7 +525,7 @@ python-versions = ">=3.7" [metadata] lock-version = "1.1" python-versions = "^3.10" -content-hash = "c6591fc2fffa67d98dd21c192d9e315e671b3a2bb94a2b003c83a369f79e039c" +content-hash = "62c220b2ed851066c12487d5f59b7997b114bd8f5c50cb70d3a3d40e1ca32cd5" [metadata.files] aiosqlite = [ @@ -477,6 +560,10 @@ dnspython = [ {file = "dnspython-2.2.1-py3-none-any.whl", hash = "sha256:a851e51367fb93e9e1361732c1d60dab63eff98712e503ea7d92e6eccb109b4f"}, {file = "dnspython-2.2.1.tar.gz", hash = "sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e"}, ] +ecdsa = [ + {file = "ecdsa-0.17.0-py2.py3-none-any.whl", hash = "sha256:5cf31d5b33743abe0dfc28999036c849a69d548f994b535e527ee3cb7f3ef676"}, + {file = "ecdsa-0.17.0.tar.gz", hash = "sha256:b9f500bb439e4153d0330610f5d26baaf18d17b8ced1bc54410d189385ea68aa"}, +] email-validator = [ {file = "email_validator-1.2.1-py2.py3-none-any.whl", hash = "sha256:c8589e691cf73eb99eed8d10ce0e9cbb05a0886ba920c8bcb7c82873f4c5789c"}, {file = "email_validator-1.2.1.tar.gz", hash = "sha256:6757aea012d40516357c0ac2b1a4c31219ab2f899d26831334c5d069e8b6c3d8"}, @@ -485,6 +572,10 @@ fastapi = [ {file = "fastapi-0.78.0-py3-none-any.whl", hash = "sha256:15fcabd5c78c266fa7ae7d8de9b384bfc2375ee0503463a6febbe3bab69d6f65"}, {file = "fastapi-0.78.0.tar.gz", hash = "sha256:3233d4a789ba018578658e2af1a4bb5e38bdd122ff722b313666a9b2c6786a83"}, ] +fastapi-keycloak = [ + {file = "fastapi_keycloak-1.0.6-py3-none-any.whl", hash = "sha256:73a79d494666373fbbe45284364cc5925777cbfc933f042eb095d8475b483466"}, + {file = "fastapi_keycloak-1.0.6.tar.gz", hash = "sha256:e55e23b501f28675ed99c9871b5daa9218374256841c301258cd454fcd0b9dd2"}, +] greenlet = [ {file = "greenlet-1.1.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:58df5c2a0e293bf665a51f8a100d3e9956febfbf1d9aaf8c0677cf70218910c6"}, {file = "greenlet-1.1.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:aec52725173bd3a7b56fe91bc56eccb26fbdff1386ef123abb63c84c5b43b63a"}, @@ -671,6 +762,21 @@ orjson = [ {file = "orjson-3.7.2-cp39-none-win_amd64.whl", hash = "sha256:796914f7463277d371402775536fb461948c0d34a67d20a57dc4ec49a48a8613"}, {file = "orjson-3.7.2.tar.gz", hash = "sha256:1cf9690a0b7c51a988221376741a31087bc1dc2ac327bb2dde919806dfa59444"}, ] +pyasn1 = [ + {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"}, + {file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"}, + {file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"}, + {file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"}, + {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"}, + {file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"}, + {file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"}, + {file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"}, + {file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"}, + {file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"}, + {file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"}, + {file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"}, + {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"}, +] pydantic = [ {file = "pydantic-1.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8098a724c2784bf03e8070993f6d46aa2eeca031f8d8a048dff277703e6e193"}, {file = "pydantic-1.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c320c64dd876e45254bdd350f0179da737463eea41c43bacbee9d8c9d1021f11"}, @@ -712,6 +818,10 @@ python-dotenv = [ {file = "python-dotenv-0.20.0.tar.gz", hash = "sha256:b7e3b04a59693c42c36f9ab1cc2acc46fa5df8c78e178fc33a8d4cd05c8d498f"}, {file = "python_dotenv-0.20.0-py3-none-any.whl", hash = "sha256:d92a187be61fe482e4fd675b6d52200e7be63a12b724abbf931a40ce4fa92938"}, ] +python-jose = [ + {file = "python-jose-3.3.0.tar.gz", hash = "sha256:55779b5e6ad599c6336191246e95eb2293a9ddebd555f796a65f838f07e5d78a"}, + {file = "python_jose-3.3.0-py2.py3-none-any.whl", hash = "sha256:9b1376b023f8b298536eedd47ae1089bcdb848f1535ab30555cd92002d78923a"}, +] python-multipart = [ {file = "python-multipart-0.0.5.tar.gz", hash = "sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"}, ] @@ -754,6 +864,10 @@ requests = [ {file = "requests-2.28.0-py3-none-any.whl", hash = "sha256:bc7861137fbce630f17b03d3ad02ad0bf978c844f3536d0edda6499dafce2b6f"}, {file = "requests-2.28.0.tar.gz", hash = "sha256:d568723a7ebd25875d8d1eaf5dfa068cd2fc8194b2e483d7b1f7c81918dbec6b"}, ] +rsa = [ + {file = "rsa-4.8-py3-none-any.whl", hash = "sha256:95c5d300c4e879ee69708c428ba566c59478fd653cc3a22243eeb8ed846950bb"}, + {file = "rsa-4.8.tar.gz", hash = "sha256:5c6bd9dc7a543b7fe4304a631f8a8a3b674e2bbfc49c2ae96200cdbe55df6b17"}, +] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, diff --git a/pyproject.toml b/pyproject.toml index ac4eb27..b878b78 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,7 @@ fastapi = {extras = ["all"], version = "^0.78.0"} sqlmodel = "^0.0.6" requests = "^2.28.0" aiosqlite = "^0.17.0" +fastapi_keycloak = "^1.0.6" [tool.poetry.dev-dependencies] diff --git a/realm-export-orig.json b/realm-export-orig.json new file mode 100644 index 0000000..ecd3056 --- /dev/null +++ b/realm-export-orig.json @@ -0,0 +1,2051 @@ +{ + "id": "Test", + "realm": "Test", + "notBefore": 0, + "defaultSignatureAlgorithm": "RS256", + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 300, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "ssoSessionIdleTimeoutRememberMe": 0, + "ssoSessionMaxLifespanRememberMe": 0, + "offlineSessionIdleTimeout": 2592000, + "offlineSessionMaxLifespanEnabled": false, + "offlineSessionMaxLifespan": 5184000, + "clientSessionIdleTimeout": 0, + "clientSessionMaxLifespan": 0, + "clientOfflineSessionIdleTimeout": 0, + "clientOfflineSessionMaxLifespan": 0, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "oauth2DeviceCodeLifespan": 600, + "oauth2DevicePollingInterval": 5, + "enabled": true, + "sslRequired": "none", + "registrationAllowed": true, + "registrationEmailAsUsername": true, + "rememberMe": true, + "verifyEmail": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": true, + "editUsernameAllowed": true, + "bruteForceProtected": false, + "permanentLockout": false, + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, + "defaultRole": { + "id": "4833bad1-0ba1-4115-a2e1-3b96a90fe268", + "name": "default-roles-test", + "description": "${role_default-roles}", + "composite": true, + "clientRole": false, + "containerId": "Test" + }, + "requiredCredentials": [ + "password" + ], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "otpSupportedApplications": [ + "FreeOTP", + "Google Authenticator" + ], + "webAuthnPolicyRpEntityName": "keycloak", + "webAuthnPolicySignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyRpId": "", + "webAuthnPolicyAttestationConveyancePreference": "not specified", + "webAuthnPolicyAuthenticatorAttachment": "not specified", + "webAuthnPolicyRequireResidentKey": "not specified", + "webAuthnPolicyUserVerificationRequirement": "not specified", + "webAuthnPolicyCreateTimeout": 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyAcceptableAaguids": [], + "webAuthnPolicyPasswordlessRpEntityName": "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyPasswordlessRpId": "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", + "webAuthnPolicyPasswordlessCreateTimeout": 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyPasswordlessAcceptableAaguids": [], + "users": [ + { + "id": "33b940e2-0bdb-49a7-9356-e6e230f49619", + "createdTimestamp": 1640089861472, + "username": "service-account-admin-cli", + "enabled": true, + "totp": false, + "emailVerified": false, + "serviceAccountClientId": "admin-cli", + "disableableCredentialTypes": [], + "requiredActions": [], + "notBefore": 0 + }, + { + "id": "83d84b8e-f053-480e-8b13-713c4fac708d", + "createdTimestamp": 1640089810342, + "username": "service-account-test-client", + "enabled": true, + "totp": false, + "emailVerified": false, + "serviceAccountClientId": "test-client", + "disableableCredentialTypes": [], + "requiredActions": [], + "notBefore": 0 + } + ], + "scopeMappings": [ + { + "clientScope": "offline_access", + "roles": [ + "offline_access" + ] + } + ], + "clientScopeMappings": { + "test-client": [ + { + "client": "admin-cli", + "roles": [ + "uma_protection" + ] + } + ], + "account": [ + { + "client": "account-console", + "roles": [ + "manage-account" + ] + } + ] + }, + "clients": [ + { + "id": "930e41a3-40c7-42a1-9587-2b92f31e68c5", + "clientId": "account", + "name": "${client_account}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/Test/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/Test/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "207a4d3c-cc80-4bd2-91d4-815a1af38778", + "clientId": "account-console", + "name": "${client_account-console}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/Test/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/Test/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "70d4fa1a-79b2-489e-b9a0-47a6772819a6", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + } + ], + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "f8f4baad-a231-4a6a-b97c-5d68ac147279", + "clientId": "admin-cli", + "name": "${client_admin-cli}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "**********", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": true, + "authorizationServicesEnabled": true, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "id.token.as.detached.signature": "false", + "saml.assertion.signature": "false", + "saml.force.post.binding": "false", + "saml.multivalued.roles": "false", + "saml.encrypt": "false", + "oauth2.device.authorization.grant.enabled": "true", + "backchannel.logout.revoke.offline.tokens": "false", + "saml.server.signature": "false", + "saml.server.signature.keyinfo.ext": "false", + "use.refresh.tokens": "true", + "exclude.session.state.from.auth.response": "false", + "oidc.ciba.grant.enabled": "false", + "saml.artifact.binding": "false", + "backchannel.logout.session.required": "false", + "client_credentials.use_refresh_token": "false", + "saml_force_name_id_format": "false", + "require.pushed.authorization.requests": "false", + "saml.client.signature": "false", + "tls.client.certificate.bound.access.tokens": "false", + "saml.authnstatement": "false", + "display.on.consent.screen": "false", + "saml.onetimeuse.condition": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "a73b0f3e-1b0c-4b14-893e-22f4985cfd60", + "name": "Client Host", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientHost", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientHost", + "jsonType.label": "String" + } + }, + { + "id": "030a393a-ff89-4d2e-aa30-063e95b7ce9f", + "name": "Client ID", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientId", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientId", + "jsonType.label": "String" + } + }, + { + "id": "8e4e8915-cba7-4be3-86e8-d6991a0cd273", + "name": "Client IP Address", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientAddress", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientAddress", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ], + "authorizationSettings": { + "allowRemoteResourceManagement": true, + "policyEnforcementMode": "ENFORCING", + "resources": [ + { + "name": "Default Resource", + "type": "urn:admin-cli:resources:default", + "ownerManagedAccess": false, + "attributes": {}, + "_id": "98ea544d-9474-4cde-a7d5-f4aa8438596b", + "uris": [ + "/*" + ] + } + ], + "policies": [ + { + "id": "3747a4f9-0b6b-4ad0-aba4-181193729727", + "name": "Default Policy", + "description": "A policy that grants access only for users within this realm", + "type": "js", + "logic": "POSITIVE", + "decisionStrategy": "AFFIRMATIVE", + "config": { + "code": "// by default, grants any permission associated with this policy\n$evaluation.grant();\n" + } + }, + { + "id": "762a6303-aab7-439b-8a41-0973964640ce", + "name": "Default Permission", + "description": "A permission that applies to the default resource type", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "defaultResourceType": "urn:admin-cli:resources:default", + "applyPolicies": "[\"Default Policy\"]" + } + } + ], + "scopes": [], + "decisionStrategy": "UNANIMOUS" + } + }, + { + "id": "1d1a4841-fbfe-4bda-9bc8-fdc73497aa5c", + "clientId": "broker", + "name": "${client_broker}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "fb6c4935-1d0c-4e82-b262-443672d72930", + "clientId": "realm-management", + "name": "${client_realm-management}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "97d658fa-02d4-43d5-9bba-4d0717a8466d", + "clientId": "security-admin-console", + "name": "${client_security-admin-console}", + "rootUrl": "${authAdminUrl}", + "baseUrl": "/admin/Test/console/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/admin/Test/console/*" + ], + "webOrigins": [ + "+" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "fb2e09ee-c7b0-49b2-870d-758173ec6be7", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "9a76b2ec-b33e-40b0-9cad-e00ca7e77e40", + "clientId": "test-client", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "**********", + "redirectUris": [ + "http://localhost:9999/auth/callback" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": true, + "authorizationServicesEnabled": true, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "id.token.as.detached.signature": "false", + "saml.assertion.signature": "false", + "saml.force.post.binding": "false", + "saml.multivalued.roles": "false", + "saml.encrypt": "false", + "oauth2.device.authorization.grant.enabled": "false", + "backchannel.logout.revoke.offline.tokens": "false", + "saml.server.signature": "false", + "saml.server.signature.keyinfo.ext": "false", + "use.refresh.tokens": "true", + "exclude.session.state.from.auth.response": "false", + "oidc.ciba.grant.enabled": "false", + "saml.artifact.binding": "false", + "backchannel.logout.session.required": "true", + "client_credentials.use_refresh_token": "false", + "saml_force_name_id_format": "false", + "require.pushed.authorization.requests": "false", + "saml.client.signature": "false", + "tls.client.certificate.bound.access.tokens": "false", + "saml.authnstatement": "false", + "display.on.consent.screen": "false", + "saml.onetimeuse.condition": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": -1, + "protocolMappers": [ + { + "id": "3716053c-9672-4685-9fe5-0b44307c65c1", + "name": "Client IP Address", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientAddress", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientAddress", + "jsonType.label": "String" + } + }, + { + "id": "4cffb7d8-1aab-4b35-8111-df1ee341c76a", + "name": "Client ID", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientId", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientId", + "jsonType.label": "String" + } + }, + { + "id": "57540600-0bd8-42dd-8eb1-ca4177c2da57", + "name": "Client Host", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientHost", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientHost", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ], + "authorizationSettings": { + "allowRemoteResourceManagement": true, + "policyEnforcementMode": "ENFORCING", + "resources": [ + { + "name": "Default Resource", + "type": "urn:test-client:resources:default", + "ownerManagedAccess": false, + "attributes": {}, + "_id": "c4c07a91-21b2-4259-b923-4b3d6b05d93f", + "uris": [ + "/*" + ] + } + ], + "policies": [ + { + "id": "b1174446-ce63-4d3d-8829-f1b960a76b42", + "name": "Default Policy", + "description": "A policy that grants access only for users within this realm", + "type": "js", + "logic": "POSITIVE", + "decisionStrategy": "AFFIRMATIVE", + "config": { + "code": "// by default, grants any permission associated with this policy\n$evaluation.grant();\n" + } + }, + { + "id": "c595a3a7-c4d3-47b1-896d-50e5396d1eee", + "name": "Default Permission", + "description": "A permission that applies to the default resource type", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "defaultResourceType": "urn:test-client:resources:default", + "applyPolicies": "[\"Default Policy\"]" + } + } + ], + "scopes": [], + "decisionStrategy": "UNANIMOUS" + } + } + ], + "clientScopes": [ + { + "id": "a894dbe0-76e7-4c22-b7b2-bd3f827e0ef5", + "name": "role_list", + "description": "SAML role list", + "protocol": "saml", + "attributes": { + "consent.screen.text": "${samlRoleListScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "762589d9-35be-4ad7-bed4-4b718d6ef6ec", + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + } + ] + }, + { + "id": "5a5ce089-2139-4d60-8d2a-fd198c5db2ec", + "name": "address", + "description": "OpenID Connect built-in scope: address", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${addressScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "927a5908-7652-4586-9b8a-eb5920ef4150", + "name": "address", + "protocol": "openid-connect", + "protocolMapper": "oidc-address-mapper", + "consentRequired": false, + "config": { + "user.attribute.formatted": "formatted", + "user.attribute.country": "country", + "user.attribute.postal_code": "postal_code", + "userinfo.token.claim": "true", + "user.attribute.street": "street", + "id.token.claim": "true", + "user.attribute.region": "region", + "access.token.claim": "true", + "user.attribute.locality": "locality" + } + } + ] + }, + { + "id": "bf4c9750-93e5-434e-8845-adb5d545b462", + "name": "microprofile-jwt", + "description": "Microprofile - JWT built-in scope", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "c3557b80-20cf-41cc-9732-9ebc2bd65e8a", + "name": "upn", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "upn", + "jsonType.label": "String" + } + }, + { + "id": "9b1e384f-9aed-4592-a40e-734030fdcfcb", + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "multivalued": "true", + "user.attribute": "foo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "groups", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "b19ae76e-fce0-4f6b-8d84-378f60d88f8d", + "name": "roles", + "description": "OpenID Connect scope for add user roles to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "true", + "consent.screen.text": "${rolesScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "83b45cee-daa8-4a98-af4b-b9000f36f2fd", + "name": "client roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "resource_access.${client_id}.roles", + "jsonType.label": "String", + "multivalued": "true" + } + }, + { + "id": "8695784f-2e6b-4571-982b-26b8ba72af98", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + }, + { + "id": "f36f78cb-da3f-4377-8b90-7d28078cc890", + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "realm_access.roles", + "jsonType.label": "String", + "multivalued": "true" + } + } + ] + }, + { + "id": "39baba4a-03aa-4309-8cd2-2591181f21ba", + "name": "web-origins", + "description": "OpenID Connect scope for add allowed web origins to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false", + "consent.screen.text": "" + }, + "protocolMappers": [ + { + "id": "a2a22f05-cf5a-4206-9c7d-57fba22073c9", + "name": "allowed web origins", + "protocol": "openid-connect", + "protocolMapper": "oidc-allowed-origins-mapper", + "consentRequired": false, + "config": {} + } + ] + }, + { + "id": "20187807-6f9e-4438-abec-164ca4e39520", + "name": "phone", + "description": "OpenID Connect built-in scope: phone", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${phoneScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "ec0661bc-d266-4af6-aac4-a1753b1291d4", + "name": "phone number", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumber", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number", + "jsonType.label": "String" + } + }, + { + "id": "a9b3a239-bc80-4067-b787-a2c3ca0d2ec4", + "name": "phone number verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumberVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number_verified", + "jsonType.label": "boolean" + } + } + ] + }, + { + "id": "e3d6fefe-3579-47a1-807d-64fcf7a87dcf", + "name": "offline_access", + "description": "OpenID Connect built-in scope: offline_access", + "protocol": "openid-connect", + "attributes": { + "consent.screen.text": "${offlineAccessScopeConsentText}", + "display.on.consent.screen": "true" + } + }, + { + "id": "e832567a-5345-4f8c-8b35-012f65396f67", + "name": "profile", + "description": "OpenID Connect built-in scope: profile", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${profileScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "fd7a31da-915a-40ae-b633-393615ce2762", + "name": "updated at", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "updatedAt", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "updated_at", + "jsonType.label": "String" + } + }, + { + "id": "75ffd8aa-4326-4923-bc3e-20b09bd875b0", + "name": "middle name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "middleName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "middle_name", + "jsonType.label": "String" + } + }, + { + "id": "f2654fbe-5521-49e4-8e50-ca04651db68b", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + }, + { + "id": "3cf479a7-f66d-4274-af23-ed1c7909b6e5", + "name": "profile", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "profile", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "profile", + "jsonType.label": "String" + } + }, + { + "id": "ef85df6a-0b3d-400b-b882-a2118ad44db5", + "name": "picture", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "picture", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "picture", + "jsonType.label": "String" + } + }, + { + "id": "84451abc-bcbc-4451-9dcf-32836641765c", + "name": "birthdate", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "birthdate", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "birthdate", + "jsonType.label": "String" + } + }, + { + "id": "67318bb2-5f53-4f75-a587-8f3319ebe843", + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + }, + { + "id": "820f7a36-03eb-4503-aa11-5742efe7390e", + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + }, + { + "id": "233c42eb-c87d-4826-8bbc-4683c4f13a1a", + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + }, + { + "id": "eb34b957-ea38-41ac-9199-697e227985e7", + "name": "gender", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "gender", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "gender", + "jsonType.label": "String" + } + }, + { + "id": "50ba7956-d15f-4d8c-90aa-da136f09dcb2", + "name": "website", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "website", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "website", + "jsonType.label": "String" + } + }, + { + "id": "3f7b3d46-c9f0-43c2-90e9-e1a4874bdbcf", + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + }, + { + "id": "01ac5e4b-4945-4667-be10-d29dc5e6ad47", + "name": "nickname", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "nickname", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "nickname", + "jsonType.label": "String" + } + }, + { + "id": "cdb0ce02-86a3-4e76-84f7-167ede3e0ecf", + "name": "zoneinfo", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "zoneinfo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "zoneinfo", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "04ef696e-7196-4c73-872d-10af8ebe4276", + "name": "email", + "description": "OpenID Connect built-in scope: email", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${emailScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "d8c56c76-ff18-4b37-b45a-237fcf8b2950", + "name": "email verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "emailVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email_verified", + "jsonType.label": "boolean" + } + }, + { + "id": "d21719d1-850e-488f-a58d-a4e42c76f2a5", + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + } + ] + } + ], + "defaultDefaultClientScopes": [ + "role_list", + "profile", + "email", + "roles", + "web-origins" + ], + "defaultOptionalClientScopes": [ + "offline_access", + "address", + "phone", + "microprofile-jwt" + ], + "browserSecurityHeaders": { + "contentSecurityPolicyReportOnly": "", + "xContentTypeOptions": "nosniff", + "xRobotsTag": "none", + "xFrameOptions": "SAMEORIGIN", + "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection": "1; mode=block", + "strictTransportSecurity": "max-age=31536000; includeSubDomains" + }, + "smtpServer": {}, + "eventsEnabled": false, + "eventsListeners": [ + "jboss-logging" + ], + "enabledEventTypes": [], + "adminEventsEnabled": false, + "adminEventsDetailsEnabled": false, + "identityProviders": [], + "identityProviderMappers": [], + "components": { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ + { + "id": "632544be-5a8c-4e7e-b3c8-4cb5faedcf66", + "name": "Max Clients Limit", + "providerId": "max-clients", + "subType": "anonymous", + "subComponents": {}, + "config": { + "max-clients": [ + "200" + ] + } + }, + { + "id": "3743b061-854b-43fd-8fcc-b687d015e9b5", + "name": "Trusted Hosts", + "providerId": "trusted-hosts", + "subType": "anonymous", + "subComponents": {}, + "config": { + "host-sending-registration-request-must-match": [ + "true" + ], + "client-uris-must-match": [ + "true" + ] + } + }, + { + "id": "7051cfe2-ab43-4faa-b40d-af6446b18167", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "e0ec37dd-5965-48d3-81a6-3cb99629ccce", + "name": "Full Scope Disabled", + "providerId": "scope", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "929899ea-bf1d-42b0-bd2a-9d1e432db44f", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "saml-user-property-mapper", + "saml-role-list-mapper", + "oidc-address-mapper", + "saml-user-attribute-mapper", + "oidc-usermodel-attribute-mapper", + "oidc-sha256-pairwise-sub-mapper", + "oidc-usermodel-property-mapper", + "oidc-full-name-mapper" + ] + } + }, + { + "id": "d4a2ebb9-a3ae-44be-8678-3e00952c4b94", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "891f4a61-7f6e-4523-af0f-f11c55e9113c", + "name": "Consent Required", + "providerId": "consent-required", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "bd63ffe9-c748-4d6a-85ea-4677fa6260c7", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "oidc-usermodel-property-mapper", + "oidc-sha256-pairwise-sub-mapper", + "saml-user-attribute-mapper", + "saml-role-list-mapper", + "oidc-usermodel-attribute-mapper", + "oidc-full-name-mapper", + "oidc-address-mapper", + "saml-user-property-mapper" + ] + } + } + ], + "org.keycloak.userprofile.UserProfileProvider": [ + { + "id": "41f8cc61-7aeb-44b5-ad6b-990382a76fad", + "providerId": "declarative-user-profile", + "subComponents": {}, + "config": {} + } + ], + "org.keycloak.keys.KeyProvider": [ + { + "id": "acd1a5ea-6013-4353-beb1-4b8b00f50970", + "name": "aes-generated", + "providerId": "aes-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + }, + { + "id": "5b2b6b08-9d27-481a-9110-92ddba95a032", + "name": "rsa-generated", + "providerId": "rsa-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + }, + { + "id": "18f53e6d-9820-4064-ab92-4b4d59766399", + "name": "hmac-generated", + "providerId": "hmac-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ], + "algorithm": [ + "HS256" + ] + } + }, + { + "id": "bb03bb29-3654-40bd-89cf-b97eb025fdf6", + "name": "rsa-enc-generated", + "providerId": "rsa-enc-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ], + "algorithm": [ + "RSA-OAEP" + ] + } + } + ] + }, + "internationalizationEnabled": false, + "supportedLocales": [], + "authenticationFlows": [ + { + "id": "37901a4d-267a-4c6f-ada4-a33cd9b96542", + "alias": "Account verification options", + "description": "Method with which to verity the existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-email-verification", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "flowAlias": "Verify Existing Account by Re-authentication", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "23cf1d10-aea2-438c-9f50-39121274914f", + "alias": "Authentication Options", + "description": "Authentication options.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "basic-auth", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "basic-auth-otp", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "579d3750-b4e7-4ba8-83c8-957204648d8e", + "alias": "Browser - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "9f16f811-7743-4660-8741-2fc5cf4e6f39", + "alias": "Direct Grant - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "direct-grant-validate-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "08dcee36-c7d3-4560-be58-56b9a79632c3", + "alias": "First broker login - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "e22b73b9-7cd1-4901-96d2-97c2fc2e1d00", + "alias": "Handle Existing Account", + "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-confirm-link", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "flowAlias": "Account verification options", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "280e848b-94d5-4d4a-bee0-20bf9f6c2470", + "alias": "Reset - Conditional OTP", + "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "reset-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "9e157795-b68b-4ea4-b180-9a6d18c8f165", + "alias": "User creation or linking", + "description": "Flow for the existing/non-existing user alternatives", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "create unique user config", + "authenticator": "idp-create-user-if-unique", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "flowAlias": "Handle Existing Account", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "d2a3ca48-1750-44c8-b038-0a2c6218815a", + "alias": "Verify Existing Account by Re-authentication", + "description": "Reauthentication of existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "flowAlias": "First broker login - Conditional OTP", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "8f316b7f-9140-481a-ba80-e5c912a5a578", + "alias": "browser", + "description": "browser based authentication", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-cookie", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "identity-provider-redirector", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 25, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 30, + "flowAlias": "forms", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "3165db58-cb49-4838-b909-b8cfa45f01f6", + "alias": "clients", + "description": "Base authentication for clients", + "providerId": "client-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "client-secret", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "client-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "client-secret-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "client-x509", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 40, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "8689bbf5-a814-47df-a208-0a3b779b5762", + "alias": "direct grant", + "description": "OpenID Connect Resource Owner Grant", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "direct-grant-validate-username", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "direct-grant-validate-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 30, + "flowAlias": "Direct Grant - Conditional OTP", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "ff4fd0b1-2fa8-4a94-9a2e-2f9c4721b6dd", + "alias": "docker auth", + "description": "Used by Docker clients to authenticate against the IDP", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "docker-http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "d6044ef0-7e5a-4c44-927c-95417a681770", + "alias": "first broker login", + "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "review profile config", + "authenticator": "idp-review-profile", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "flowAlias": "User creation or linking", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "166c6b57-a5ad-4096-8a23-f8bc4172490c", + "alias": "forms", + "description": "Username, password, otp and other auth forms.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "flowAlias": "Browser - Conditional OTP", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "63173378-6af9-4cb8-922a-08ce29c01d33", + "alias": "http challenge", + "description": "An authentication flow based on challenge-response HTTP Authentication Schemes", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "no-cookie-redirect", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "flowAlias": "Authentication Options", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "ecf68f8d-6c0e-435a-88d8-f7f289319697", + "alias": "registration", + "description": "registration flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-page-form", + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 10, + "flowAlias": "registration form", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "a8e5d299-5481-4514-9e24-54a5b07becd7", + "alias": "registration form", + "description": "registration form", + "providerId": "form-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-user-creation", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "registration-profile-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 40, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "registration-password-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 50, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "registration-recaptcha-action", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 60, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "a579a10b-7b03-41b4-af81-8e96882451b0", + "alias": "reset credentials", + "description": "Reset credentials for a user if they forgot their password or something", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "reset-credentials-choose-user", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "reset-credential-email", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "reset-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 40, + "flowAlias": "Reset - Conditional OTP", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "1fc5e527-c907-4fed-81d8-e772154e05eb", + "alias": "saml ecp", + "description": "SAML ECP Profile Authentication Flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + } + ], + "authenticatorConfig": [ + { + "id": "fb44570d-fa37-4a04-8b03-22e977e1cdd5", + "alias": "create unique user config", + "config": { + "require.password.update.after.registration": "false" + } + }, + { + "id": "d44e61f4-1247-485c-bfc3-3a35d25767b4", + "alias": "review profile config", + "config": { + "update.profile.on.first.login": "missing" + } + } + ], + "requiredActions": [ + { + "alias": "CONFIGURE_TOTP", + "name": "Configure OTP", + "providerId": "CONFIGURE_TOTP", + "enabled": true, + "defaultAction": false, + "priority": 10, + "config": {} + }, + { + "alias": "terms_and_conditions", + "name": "Terms and Conditions", + "providerId": "terms_and_conditions", + "enabled": false, + "defaultAction": false, + "priority": 20, + "config": {} + }, + { + "alias": "UPDATE_PASSWORD", + "name": "Update Password", + "providerId": "UPDATE_PASSWORD", + "enabled": true, + "defaultAction": false, + "priority": 30, + "config": {} + }, + { + "alias": "UPDATE_PROFILE", + "name": "Update Profile", + "providerId": "UPDATE_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 40, + "config": {} + }, + { + "alias": "VERIFY_EMAIL", + "name": "Verify Email", + "providerId": "VERIFY_EMAIL", + "enabled": true, + "defaultAction": false, + "priority": 50, + "config": {} + }, + { + "alias": "delete_account", + "name": "Delete Account", + "providerId": "delete_account", + "enabled": false, + "defaultAction": false, + "priority": 60, + "config": {} + }, + { + "alias": "update_user_locale", + "name": "Update User Locale", + "providerId": "update_user_locale", + "enabled": true, + "defaultAction": false, + "priority": 1000, + "config": {} + } + ], + "browserFlow": "browser", + "registrationFlow": "registration", + "directGrantFlow": "direct grant", + "resetCredentialsFlow": "reset credentials", + "clientAuthenticationFlow": "clients", + "dockerAuthenticationFlow": "docker auth", + "attributes": { + "cibaBackchannelTokenDeliveryMode": "poll", + "cibaExpiresIn": "120", + "cibaAuthRequestedUserHint": "login_hint", + "oauth2DeviceCodeLifespan": "600", + "oauth2DevicePollingInterval": "5", + "clientOfflineSessionMaxLifespan": "0", + "clientSessionIdleTimeout": "0", + "parRequestUriLifespan": "60", + "clientSessionMaxLifespan": "0", + "clientOfflineSessionIdleTimeout": "0", + "cibaInterval": "5" + }, + "keycloakVersion": "16.1.0", + "userManagedAccessAllowed": false, + "clientProfiles": { + "profiles": [] + }, + "clientPolicies": { + "policies": [] + } +} \ No newline at end of file diff --git a/realm-export.json b/realm-export.json new file mode 100644 index 0000000..baa0b8a --- /dev/null +++ b/realm-export.json @@ -0,0 +1,2372 @@ +{ + "id": "Test", + "realm": "Test", + "notBefore": 0, + "defaultSignatureAlgorithm": "RS256", + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 300, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "ssoSessionIdleTimeoutRememberMe": 0, + "ssoSessionMaxLifespanRememberMe": 0, + "offlineSessionIdleTimeout": 2592000, + "offlineSessionMaxLifespanEnabled": false, + "offlineSessionMaxLifespan": 5184000, + "clientSessionIdleTimeout": 0, + "clientSessionMaxLifespan": 0, + "clientOfflineSessionIdleTimeout": 0, + "clientOfflineSessionMaxLifespan": 0, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "oauth2DeviceCodeLifespan": 600, + "oauth2DevicePollingInterval": 5, + "enabled": true, + "sslRequired": "none", + "registrationAllowed": true, + "registrationEmailAsUsername": true, + "rememberMe": true, + "verifyEmail": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": true, + "editUsernameAllowed": true, + "bruteForceProtected": false, + "permanentLockout": false, + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, + "roles": { + "realm": [ + { + "id": "4833bad1-0ba1-4115-a2e1-3b96a90fe268", + "name": "default-roles-test", + "description": "${role_default-roles}", + "composite": true, + "composites": { + "realm": [ + "offline_access", + "uma_authorization" + ] + }, + "clientRole": false, + "containerId": "Test", + "attributes": {} + }, + { + "id": "66865943-5c21-41bf-bbbb-e076764c03d0", + "name": "uma_authorization", + "description": "${role_uma_authorization}", + "composite": false, + "clientRole": false, + "containerId": "Test", + "attributes": {} + }, + { + "id": "1634ee28-25a6-4fe5-9a51-f4ca59080644", + "name": "offline_access", + "description": "${role_offline-access}", + "composite": false, + "clientRole": false, + "containerId": "Test", + "attributes": {} + } + ], + "client": { + "realm-management": [ + { + "id": "57e4c2e5-f0cd-4573-9294-e713f0fd112a", + "name": "manage-clients", + "description": "${role_manage-clients}", + "composite": false, + "clientRole": true, + "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "attributes": {} + }, + { + "id": "2e485d36-2220-479d-8595-d0019b617236", + "name": "manage-events", + "description": "${role_manage-events}", + "composite": false, + "clientRole": true, + "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "attributes": {} + }, + { + "id": "854771dd-57ae-49f7-a3c8-0fa2d88a1f59", + "name": "view-realm", + "description": "${role_view-realm}", + "composite": false, + "clientRole": true, + "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "attributes": {} + }, + { + "id": "32d8a0a2-ab97-4488-a60b-fe1420485933", + "name": "manage-users", + "description": "${role_manage-users}", + "composite": false, + "clientRole": true, + "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "attributes": {} + }, + { + "id": "a180eb27-78a1-4bda-8c41-79853ed01c81", + "name": "view-identity-providers", + "description": "${role_view-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "attributes": {} + }, + { + "id": "50332834-cba8-4e00-857a-778280137cd7", + "name": "view-authorization", + "description": "${role_view-authorization}", + "composite": false, + "clientRole": true, + "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "attributes": {} + }, + { + "id": "38de29df-a057-473a-84cd-8ad9360fa234", + "name": "query-realms", + "description": "${role_query-realms}", + "composite": false, + "clientRole": true, + "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "attributes": {} + }, + { + "id": "b9a9d61f-d420-45fa-840b-15eb5aa83812", + "name": "view-events", + "description": "${role_view-events}", + "composite": false, + "clientRole": true, + "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "attributes": {} + }, + { + "id": "4915b64e-3ffa-40d1-a1b2-bfc9005126b2", + "name": "impersonation", + "description": "${role_impersonation}", + "composite": false, + "clientRole": true, + "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "attributes": {} + }, + { + "id": "738f5208-4376-480a-9cd5-5f6ce608b359", + "name": "query-users", + "description": "${role_query-users}", + "composite": false, + "clientRole": true, + "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "attributes": {} + }, + { + "id": "3fce7eda-2d9d-43e2-b1a4-09f3e1f821db", + "name": "manage-identity-providers", + "description": "${role_manage-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "attributes": {} + }, + { + "id": "a7437db4-2259-4cc2-a746-ab5033ccb3b6", + "name": "manage-authorization", + "description": "${role_manage-authorization}", + "composite": false, + "clientRole": true, + "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "attributes": {} + }, + { + "id": "576e2f03-9bb1-4d0c-bcc8-3acbe8b892dc", + "name": "manage-realm", + "description": "${role_manage-realm}", + "composite": false, + "clientRole": true, + "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "attributes": {} + }, + { + "id": "d13c83e6-eacf-4420-8046-c4c4b0bb00b6", + "name": "query-groups", + "description": "${role_query-groups}", + "composite": false, + "clientRole": true, + "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "attributes": {} + }, + { + "id": "0d278fd6-d59f-4010-9bb1-e9e1c741ee87", + "name": "view-users", + "description": "${role_view-users}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-users", + "query-groups" + ] + } + }, + "clientRole": true, + "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "attributes": {} + }, + { + "id": "2a4b1277-f353-46cf-ab1e-e220128ced4e", + "name": "realm-admin", + "description": "${role_realm-admin}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "manage-clients", + "manage-events", + "view-realm", + "manage-users", + "view-identity-providers", + "view-authorization", + "view-events", + "query-realms", + "impersonation", + "query-users", + "manage-identity-providers", + "manage-authorization", + "view-users", + "manage-realm", + "query-groups", + "query-clients", + "view-clients", + "create-client" + ] + } + }, + "clientRole": true, + "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "attributes": {} + }, + { + "id": "aaef50b0-8203-4e23-98b2-6a793a503696", + "name": "query-clients", + "description": "${role_query-clients}", + "composite": false, + "clientRole": true, + "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "attributes": {} + }, + { + "id": "cde44948-34a5-4e65-a46d-43587638d357", + "name": "view-clients", + "description": "${role_view-clients}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-clients" + ] + } + }, + "clientRole": true, + "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "attributes": {} + }, + { + "id": "4207defe-469c-4aea-b6c1-842e0472866d", + "name": "create-client", + "description": "${role_create-client}", + "composite": false, + "clientRole": true, + "containerId": "fb6c4935-1d0c-4e82-b262-443672d72930", + "attributes": {} + } + ], + "security-admin-console": [], + "admin-cli": [], + "test-client": [ + { + "id": "6e67d70f-83e1-486f-8a71-516f35bfc11b", + "name": "uma_protection", + "composite": false, + "clientRole": true, + "containerId": "9a76b2ec-b33e-40b0-9cad-e00ca7e77e40", + "attributes": {} + } + ], + "account-console": [], + "broker": [], + "account": [ + { + "id": "02315b19-ffb2-4c81-a87c-45becbae37f2", + "name": "delete-account", + "description": "${role_delete-account}", + "composite": false, + "clientRole": true, + "containerId": "930e41a3-40c7-42a1-9587-2b92f31e68c5", + "attributes": {} + }, + { + "id": "d8b2b42f-7b38-4541-b07d-0b9f06fe282c", + "name": "manage-account", + "composite": false, + "clientRole": true, + "containerId": "930e41a3-40c7-42a1-9587-2b92f31e68c5", + "attributes": {} + } + ] + } + }, + "groups": [], + "defaultRole": { + "id": "4833bad1-0ba1-4115-a2e1-3b96a90fe268", + "name": "default-roles-test", + "description": "${role_default-roles}", + "composite": true, + "clientRole": false, + "containerId": "Test" + }, + "requiredCredentials": [ + "password" + ], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "otpSupportedApplications": [ + "FreeOTP", + "Google Authenticator" + ], + "webAuthnPolicyRpEntityName": "keycloak", + "webAuthnPolicySignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyRpId": "", + "webAuthnPolicyAttestationConveyancePreference": "not specified", + "webAuthnPolicyAuthenticatorAttachment": "not specified", + "webAuthnPolicyRequireResidentKey": "not specified", + "webAuthnPolicyUserVerificationRequirement": "not specified", + "webAuthnPolicyCreateTimeout": 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyAcceptableAaguids": [], + "webAuthnPolicyPasswordlessRpEntityName": "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyPasswordlessRpId": "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", + "webAuthnPolicyPasswordlessCreateTimeout": 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyPasswordlessAcceptableAaguids": [], + "users": [ + { + "id": "33b940e2-0bdb-49a7-9356-e6e230f49619", + "createdTimestamp": 1640089861472, + "username": "service-account-admin-cli", + "enabled": true, + "totp": false, + "emailVerified": false, + "serviceAccountClientId": "admin-cli", + "disableableCredentialTypes": [], + "requiredActions": [], + "clientRoles": { + "realm-management": [ + "manage-clients", + "manage-events", + "view-realm", + "manage-users", + "view-identity-providers", + "view-authorization", + "query-realms", + "view-events", + "impersonation", + "query-users", + "manage-identity-providers", + "manage-authorization", + "manage-realm", + "query-groups", + "view-users", + "realm-admin", + "query-clients", + "view-clients", + "create-client" + ], + "account": [ + "delete-account", + "manage-account" + ] + }, + "notBefore": 0, + "groups": [] + }, + { + "id": "83d84b8e-f053-480e-8b13-713c4fac708d", + "createdTimestamp": 1640089810342, + "username": "service-account-test-client", + "enabled": true, + "totp": false, + "emailVerified": false, + "serviceAccountClientId": "test-client", + "disableableCredentialTypes": [], + "requiredActions": [], + "notBefore": 0, + "groups": [] + } + ], + "scopeMappings": [ + { + "clientScope": "offline_access", + "roles": [ + "offline_access" + ] + } + ], + "clientScopeMappings": { + "test-client": [ + { + "client": "admin-cli", + "roles": [ + "uma_protection" + ] + } + ], + "account": [ + { + "client": "account-console", + "roles": [ + "manage-account" + ] + } + ] + }, + "clients": [ + { + "id": "930e41a3-40c7-42a1-9587-2b92f31e68c5", + "clientId": "account", + "name": "${client_account}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/Test/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/Test/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "207a4d3c-cc80-4bd2-91d4-815a1af38778", + "clientId": "account-console", + "name": "${client_account-console}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/Test/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/Test/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "70d4fa1a-79b2-489e-b9a0-47a6772819a6", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + } + ], + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "f8f4baad-a231-4a6a-b97c-5d68ac147279", + "clientId": "admin-cli", + "name": "${client_admin-cli}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "**********", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": true, + "authorizationServicesEnabled": true, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "id.token.as.detached.signature": "false", + "saml.assertion.signature": "false", + "saml.force.post.binding": "false", + "saml.multivalued.roles": "false", + "saml.encrypt": "false", + "oauth2.device.authorization.grant.enabled": "true", + "backchannel.logout.revoke.offline.tokens": "false", + "saml.server.signature": "false", + "saml.server.signature.keyinfo.ext": "false", + "use.refresh.tokens": "true", + "exclude.session.state.from.auth.response": "false", + "oidc.ciba.grant.enabled": "false", + "saml.artifact.binding": "false", + "backchannel.logout.session.required": "false", + "client_credentials.use_refresh_token": "false", + "saml_force_name_id_format": "false", + "require.pushed.authorization.requests": "false", + "saml.client.signature": "false", + "tls.client.certificate.bound.access.tokens": "false", + "saml.authnstatement": "false", + "display.on.consent.screen": "false", + "saml.onetimeuse.condition": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "a73b0f3e-1b0c-4b14-893e-22f4985cfd60", + "name": "Client Host", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientHost", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientHost", + "jsonType.label": "String" + } + }, + { + "id": "030a393a-ff89-4d2e-aa30-063e95b7ce9f", + "name": "Client ID", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientId", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientId", + "jsonType.label": "String" + } + }, + { + "id": "8e4e8915-cba7-4be3-86e8-d6991a0cd273", + "name": "Client IP Address", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientAddress", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientAddress", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ], + "authorizationSettings": { + "allowRemoteResourceManagement": true, + "policyEnforcementMode": "ENFORCING", + "resources": [ + { + "name": "Default Resource", + "type": "urn:admin-cli:resources:default", + "ownerManagedAccess": false, + "attributes": {}, + "_id": "98ea544d-9474-4cde-a7d5-f4aa8438596b", + "uris": [ + "/*" + ] + } + ], + "policies": [ + { + "id": "3747a4f9-0b6b-4ad0-aba4-181193729727", + "name": "Default Policy", + "description": "A policy that grants access only for users within this realm", + "type": "js", + "logic": "POSITIVE", + "decisionStrategy": "AFFIRMATIVE", + "config": { + "code": "// by default, grants any permission associated with this policy\n$evaluation.grant();\n" + } + }, + { + "id": "762a6303-aab7-439b-8a41-0973964640ce", + "name": "Default Permission", + "description": "A permission that applies to the default resource type", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "defaultResourceType": "urn:admin-cli:resources:default", + "applyPolicies": "[\"Default Policy\"]" + } + } + ], + "scopes": [], + "decisionStrategy": "UNANIMOUS" + } + }, + { + "id": "1d1a4841-fbfe-4bda-9bc8-fdc73497aa5c", + "clientId": "broker", + "name": "${client_broker}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "fb6c4935-1d0c-4e82-b262-443672d72930", + "clientId": "realm-management", + "name": "${client_realm-management}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "97d658fa-02d4-43d5-9bba-4d0717a8466d", + "clientId": "security-admin-console", + "name": "${client_security-admin-console}", + "rootUrl": "${authAdminUrl}", + "baseUrl": "/admin/Test/console/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/admin/Test/console/*" + ], + "webOrigins": [ + "+" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "fb2e09ee-c7b0-49b2-870d-758173ec6be7", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "9a76b2ec-b33e-40b0-9cad-e00ca7e77e40", + "clientId": "test-client", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "**********", + "redirectUris": [ + "http://localhost:9999/auth/callback" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": true, + "authorizationServicesEnabled": true, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "id.token.as.detached.signature": "false", + "saml.assertion.signature": "false", + "saml.force.post.binding": "false", + "saml.multivalued.roles": "false", + "saml.encrypt": "false", + "oauth2.device.authorization.grant.enabled": "false", + "backchannel.logout.revoke.offline.tokens": "false", + "saml.server.signature": "false", + "saml.server.signature.keyinfo.ext": "false", + "use.refresh.tokens": "true", + "exclude.session.state.from.auth.response": "false", + "oidc.ciba.grant.enabled": "false", + "saml.artifact.binding": "false", + "backchannel.logout.session.required": "true", + "client_credentials.use_refresh_token": "false", + "saml_force_name_id_format": "false", + "require.pushed.authorization.requests": "false", + "saml.client.signature": "false", + "tls.client.certificate.bound.access.tokens": "false", + "saml.authnstatement": "false", + "display.on.consent.screen": "false", + "saml.onetimeuse.condition": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": -1, + "protocolMappers": [ + { + "id": "3716053c-9672-4685-9fe5-0b44307c65c1", + "name": "Client IP Address", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientAddress", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientAddress", + "jsonType.label": "String" + } + }, + { + "id": "4cffb7d8-1aab-4b35-8111-df1ee341c76a", + "name": "Client ID", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientId", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientId", + "jsonType.label": "String" + } + }, + { + "id": "57540600-0bd8-42dd-8eb1-ca4177c2da57", + "name": "Client Host", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientHost", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientHost", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ], + "authorizationSettings": { + "allowRemoteResourceManagement": true, + "policyEnforcementMode": "ENFORCING", + "resources": [ + { + "name": "Default Resource", + "type": "urn:test-client:resources:default", + "ownerManagedAccess": false, + "attributes": {}, + "_id": "c4c07a91-21b2-4259-b923-4b3d6b05d93f", + "uris": [ + "/*" + ] + } + ], + "policies": [ + { + "id": "b1174446-ce63-4d3d-8829-f1b960a76b42", + "name": "Default Policy", + "description": "A policy that grants access only for users within this realm", + "type": "js", + "logic": "POSITIVE", + "decisionStrategy": "AFFIRMATIVE", + "config": { + "code": "// by default, grants any permission associated with this policy\n$evaluation.grant();\n" + } + }, + { + "id": "c595a3a7-c4d3-47b1-896d-50e5396d1eee", + "name": "Default Permission", + "description": "A permission that applies to the default resource type", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "defaultResourceType": "urn:test-client:resources:default", + "applyPolicies": "[\"Default Policy\"]" + } + } + ], + "scopes": [], + "decisionStrategy": "UNANIMOUS" + } + } + ], + "clientScopes": [ + { + "id": "a894dbe0-76e7-4c22-b7b2-bd3f827e0ef5", + "name": "role_list", + "description": "SAML role list", + "protocol": "saml", + "attributes": { + "consent.screen.text": "${samlRoleListScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "762589d9-35be-4ad7-bed4-4b718d6ef6ec", + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + } + ] + }, + { + "id": "5a5ce089-2139-4d60-8d2a-fd198c5db2ec", + "name": "address", + "description": "OpenID Connect built-in scope: address", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${addressScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "927a5908-7652-4586-9b8a-eb5920ef4150", + "name": "address", + "protocol": "openid-connect", + "protocolMapper": "oidc-address-mapper", + "consentRequired": false, + "config": { + "user.attribute.formatted": "formatted", + "user.attribute.country": "country", + "user.attribute.postal_code": "postal_code", + "userinfo.token.claim": "true", + "user.attribute.street": "street", + "id.token.claim": "true", + "user.attribute.region": "region", + "access.token.claim": "true", + "user.attribute.locality": "locality" + } + } + ] + }, + { + "id": "bf4c9750-93e5-434e-8845-adb5d545b462", + "name": "microprofile-jwt", + "description": "Microprofile - JWT built-in scope", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "c3557b80-20cf-41cc-9732-9ebc2bd65e8a", + "name": "upn", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "upn", + "jsonType.label": "String" + } + }, + { + "id": "9b1e384f-9aed-4592-a40e-734030fdcfcb", + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "multivalued": "true", + "userinfo.token.claim": "true", + "user.attribute": "foo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "groups", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "b19ae76e-fce0-4f6b-8d84-378f60d88f8d", + "name": "roles", + "description": "OpenID Connect scope for add user roles to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "true", + "consent.screen.text": "${rolesScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "83b45cee-daa8-4a98-af4b-b9000f36f2fd", + "name": "client roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "resource_access.${client_id}.roles", + "jsonType.label": "String", + "multivalued": "true" + } + }, + { + "id": "8695784f-2e6b-4571-982b-26b8ba72af98", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + }, + { + "id": "f36f78cb-da3f-4377-8b90-7d28078cc890", + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "realm_access.roles", + "jsonType.label": "String", + "multivalued": "true" + } + } + ] + }, + { + "id": "39baba4a-03aa-4309-8cd2-2591181f21ba", + "name": "web-origins", + "description": "OpenID Connect scope for add allowed web origins to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false", + "consent.screen.text": "" + }, + "protocolMappers": [ + { + "id": "a2a22f05-cf5a-4206-9c7d-57fba22073c9", + "name": "allowed web origins", + "protocol": "openid-connect", + "protocolMapper": "oidc-allowed-origins-mapper", + "consentRequired": false, + "config": {} + } + ] + }, + { + "id": "20187807-6f9e-4438-abec-164ca4e39520", + "name": "phone", + "description": "OpenID Connect built-in scope: phone", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${phoneScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "ec0661bc-d266-4af6-aac4-a1753b1291d4", + "name": "phone number", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumber", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number", + "jsonType.label": "String" + } + }, + { + "id": "a9b3a239-bc80-4067-b787-a2c3ca0d2ec4", + "name": "phone number verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumberVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number_verified", + "jsonType.label": "boolean" + } + } + ] + }, + { + "id": "e3d6fefe-3579-47a1-807d-64fcf7a87dcf", + "name": "offline_access", + "description": "OpenID Connect built-in scope: offline_access", + "protocol": "openid-connect", + "attributes": { + "consent.screen.text": "${offlineAccessScopeConsentText}", + "display.on.consent.screen": "true" + } + }, + { + "id": "e832567a-5345-4f8c-8b35-012f65396f67", + "name": "profile", + "description": "OpenID Connect built-in scope: profile", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${profileScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "fd7a31da-915a-40ae-b633-393615ce2762", + "name": "updated at", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "updatedAt", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "updated_at", + "jsonType.label": "String" + } + }, + { + "id": "75ffd8aa-4326-4923-bc3e-20b09bd875b0", + "name": "middle name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "middleName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "middle_name", + "jsonType.label": "String" + } + }, + { + "id": "f2654fbe-5521-49e4-8e50-ca04651db68b", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + }, + { + "id": "3cf479a7-f66d-4274-af23-ed1c7909b6e5", + "name": "profile", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "profile", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "profile", + "jsonType.label": "String" + } + }, + { + "id": "ef85df6a-0b3d-400b-b882-a2118ad44db5", + "name": "picture", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "picture", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "picture", + "jsonType.label": "String" + } + }, + { + "id": "84451abc-bcbc-4451-9dcf-32836641765c", + "name": "birthdate", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "birthdate", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "birthdate", + "jsonType.label": "String" + } + }, + { + "id": "67318bb2-5f53-4f75-a587-8f3319ebe843", + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + }, + { + "id": "820f7a36-03eb-4503-aa11-5742efe7390e", + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + }, + { + "id": "233c42eb-c87d-4826-8bbc-4683c4f13a1a", + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + }, + { + "id": "eb34b957-ea38-41ac-9199-697e227985e7", + "name": "gender", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "gender", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "gender", + "jsonType.label": "String" + } + }, + { + "id": "50ba7956-d15f-4d8c-90aa-da136f09dcb2", + "name": "website", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "website", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "website", + "jsonType.label": "String" + } + }, + { + "id": "3f7b3d46-c9f0-43c2-90e9-e1a4874bdbcf", + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + }, + { + "id": "01ac5e4b-4945-4667-be10-d29dc5e6ad47", + "name": "nickname", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "nickname", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "nickname", + "jsonType.label": "String" + } + }, + { + "id": "cdb0ce02-86a3-4e76-84f7-167ede3e0ecf", + "name": "zoneinfo", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "zoneinfo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "zoneinfo", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "04ef696e-7196-4c73-872d-10af8ebe4276", + "name": "email", + "description": "OpenID Connect built-in scope: email", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${emailScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "d8c56c76-ff18-4b37-b45a-237fcf8b2950", + "name": "email verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "emailVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email_verified", + "jsonType.label": "boolean" + } + }, + { + "id": "d21719d1-850e-488f-a58d-a4e42c76f2a5", + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + } + ] + } + ], + "defaultDefaultClientScopes": [ + "role_list", + "profile", + "email", + "roles", + "web-origins" + ], + "defaultOptionalClientScopes": [ + "offline_access", + "address", + "phone", + "microprofile-jwt" + ], + "browserSecurityHeaders": { + "contentSecurityPolicyReportOnly": "", + "xContentTypeOptions": "nosniff", + "xRobotsTag": "none", + "xFrameOptions": "SAMEORIGIN", + "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection": "1; mode=block", + "strictTransportSecurity": "max-age=31536000; includeSubDomains" + }, + "smtpServer": {}, + "eventsEnabled": false, + "eventsListeners": [ + "jboss-logging" + ], + "enabledEventTypes": [], + "adminEventsEnabled": false, + "adminEventsDetailsEnabled": false, + "identityProviders": [], + "identityProviderMappers": [], + "components": { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ + { + "id": "632544be-5a8c-4e7e-b3c8-4cb5faedcf66", + "name": "Max Clients Limit", + "providerId": "max-clients", + "subType": "anonymous", + "subComponents": {}, + "config": { + "max-clients": [ + "200" + ] + } + }, + { + "id": "3743b061-854b-43fd-8fcc-b687d015e9b5", + "name": "Trusted Hosts", + "providerId": "trusted-hosts", + "subType": "anonymous", + "subComponents": {}, + "config": { + "host-sending-registration-request-must-match": [ + "true" + ], + "client-uris-must-match": [ + "true" + ] + } + }, + { + "id": "7051cfe2-ab43-4faa-b40d-af6446b18167", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "e0ec37dd-5965-48d3-81a6-3cb99629ccce", + "name": "Full Scope Disabled", + "providerId": "scope", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "929899ea-bf1d-42b0-bd2a-9d1e432db44f", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "oidc-address-mapper", + "oidc-usermodel-property-mapper", + "oidc-sha256-pairwise-sub-mapper", + "saml-user-property-mapper", + "saml-role-list-mapper", + "saml-user-attribute-mapper", + "oidc-usermodel-attribute-mapper", + "oidc-full-name-mapper" + ] + } + }, + { + "id": "d4a2ebb9-a3ae-44be-8678-3e00952c4b94", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "891f4a61-7f6e-4523-af0f-f11c55e9113c", + "name": "Consent Required", + "providerId": "consent-required", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "bd63ffe9-c748-4d6a-85ea-4677fa6260c7", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "oidc-sha256-pairwise-sub-mapper", + "saml-user-property-mapper", + "oidc-usermodel-property-mapper", + "oidc-usermodel-attribute-mapper", + "saml-role-list-mapper", + "oidc-full-name-mapper", + "oidc-address-mapper", + "saml-user-attribute-mapper" + ] + } + } + ], + "org.keycloak.userprofile.UserProfileProvider": [ + { + "id": "41f8cc61-7aeb-44b5-ad6b-990382a76fad", + "providerId": "declarative-user-profile", + "subComponents": {}, + "config": {} + } + ], + "org.keycloak.keys.KeyProvider": [ + { + "id": "acd1a5ea-6013-4353-beb1-4b8b00f50970", + "name": "aes-generated", + "providerId": "aes-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + }, + { + "id": "5b2b6b08-9d27-481a-9110-92ddba95a032", + "name": "rsa-generated", + "providerId": "rsa-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + }, + { + "id": "18f53e6d-9820-4064-ab92-4b4d59766399", + "name": "hmac-generated", + "providerId": "hmac-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ], + "algorithm": [ + "HS256" + ] + } + }, + { + "id": "bb03bb29-3654-40bd-89cf-b97eb025fdf6", + "name": "rsa-enc-generated", + "providerId": "rsa-enc-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ], + "algorithm": [ + "RSA-OAEP" + ] + } + } + ] + }, + "internationalizationEnabled": false, + "supportedLocales": [], + "authenticationFlows": [ + { + "id": "cb243051-e476-4963-b734-1c6835d7a07a", + "alias": "Account verification options", + "description": "Method with which to verity the existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-email-verification", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "flowAlias": "Verify Existing Account by Re-authentication", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "874046ce-5531-4aef-8992-411c23142fec", + "alias": "Authentication Options", + "description": "Authentication options.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "basic-auth", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "basic-auth-otp", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "8e64b661-6379-4c46-b971-7ef55d8a889c", + "alias": "Browser - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "d8696d66-e2ba-436d-b08b-372a83bd5d7e", + "alias": "Direct Grant - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "direct-grant-validate-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "e80561eb-be37-4ac1-a486-97eefd3e3a51", + "alias": "First broker login - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "d80c0d0c-75ac-4e73-93ff-8c89e4fec7c4", + "alias": "Handle Existing Account", + "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-confirm-link", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "flowAlias": "Account verification options", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "15f8b07e-04a1-463b-b0a4-00df4d9b2322", + "alias": "Reset - Conditional OTP", + "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "reset-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "3f29218a-09e5-45c9-8a36-107efd3d49b8", + "alias": "User creation or linking", + "description": "Flow for the existing/non-existing user alternatives", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "create unique user config", + "authenticator": "idp-create-user-if-unique", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "flowAlias": "Handle Existing Account", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "500dbf87-b934-44f4-bd86-173bcfd1fece", + "alias": "Verify Existing Account by Re-authentication", + "description": "Reauthentication of existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "flowAlias": "First broker login - Conditional OTP", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "f2aa7ae7-4e96-40f7-861f-de2fc716bfbd", + "alias": "browser", + "description": "browser based authentication", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-cookie", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "identity-provider-redirector", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 25, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 30, + "flowAlias": "forms", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "a2c7d10e-1d5c-4d23-9e71-d554b68714c3", + "alias": "clients", + "description": "Base authentication for clients", + "providerId": "client-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "client-secret", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "client-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "client-secret-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "client-x509", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 40, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "ebad4b44-7382-4af1-b868-f5cd630d3399", + "alias": "direct grant", + "description": "OpenID Connect Resource Owner Grant", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "direct-grant-validate-username", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "direct-grant-validate-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 30, + "flowAlias": "Direct Grant - Conditional OTP", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "80d8de47-ac9e-4087-9c19-2ef16fa17388", + "alias": "docker auth", + "description": "Used by Docker clients to authenticate against the IDP", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "docker-http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "2474d50a-2e27-4e7f-ba5f-873c9188f702", + "alias": "first broker login", + "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "review profile config", + "authenticator": "idp-review-profile", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "flowAlias": "User creation or linking", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "d993e611-32c8-43e5-a4ab-9d0aad579f0e", + "alias": "forms", + "description": "Username, password, otp and other auth forms.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "flowAlias": "Browser - Conditional OTP", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "4b430ba8-a671-4e4b-99ad-8a5c3164a50d", + "alias": "http challenge", + "description": "An authentication flow based on challenge-response HTTP Authentication Schemes", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "no-cookie-redirect", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "flowAlias": "Authentication Options", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "e84f374f-2585-40e9-be1a-7efb88dcbf11", + "alias": "registration", + "description": "registration flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-page-form", + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 10, + "flowAlias": "registration form", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "37007e85-fdba-40e0-902c-0e6dabb6e195", + "alias": "registration form", + "description": "registration form", + "providerId": "form-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-user-creation", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "registration-profile-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 40, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "registration-password-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 50, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "registration-recaptcha-action", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 60, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "f68c7ca3-707c-42d7-9f46-005500fda800", + "alias": "reset credentials", + "description": "Reset credentials for a user if they forgot their password or something", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "reset-credentials-choose-user", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "reset-credential-email", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "reset-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 40, + "flowAlias": "Reset - Conditional OTP", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "f7c9da78-4d97-4808-b34d-5c1ecc1b5265", + "alias": "saml ecp", + "description": "SAML ECP Profile Authentication Flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + } + ], + "authenticatorConfig": [ + { + "id": "69595c87-df34-4e20-88e1-de484bb118a0", + "alias": "create unique user config", + "config": { + "require.password.update.after.registration": "false" + } + }, + { + "id": "b9c8bdc2-7f20-4c98-9e0c-30a2e9000c50", + "alias": "review profile config", + "config": { + "update.profile.on.first.login": "missing" + } + } + ], + "requiredActions": [ + { + "alias": "CONFIGURE_TOTP", + "name": "Configure OTP", + "providerId": "CONFIGURE_TOTP", + "enabled": true, + "defaultAction": false, + "priority": 10, + "config": {} + }, + { + "alias": "terms_and_conditions", + "name": "Terms and Conditions", + "providerId": "terms_and_conditions", + "enabled": false, + "defaultAction": false, + "priority": 20, + "config": {} + }, + { + "alias": "UPDATE_PASSWORD", + "name": "Update Password", + "providerId": "UPDATE_PASSWORD", + "enabled": true, + "defaultAction": false, + "priority": 30, + "config": {} + }, + { + "alias": "UPDATE_PROFILE", + "name": "Update Profile", + "providerId": "UPDATE_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 40, + "config": {} + }, + { + "alias": "VERIFY_EMAIL", + "name": "Verify Email", + "providerId": "VERIFY_EMAIL", + "enabled": true, + "defaultAction": false, + "priority": 50, + "config": {} + }, + { + "alias": "delete_account", + "name": "Delete Account", + "providerId": "delete_account", + "enabled": false, + "defaultAction": false, + "priority": 60, + "config": {} + }, + { + "alias": "update_user_locale", + "name": "Update User Locale", + "providerId": "update_user_locale", + "enabled": true, + "defaultAction": false, + "priority": 1000, + "config": {} + } + ], + "browserFlow": "browser", + "registrationFlow": "registration", + "directGrantFlow": "direct grant", + "resetCredentialsFlow": "reset credentials", + "clientAuthenticationFlow": "clients", + "dockerAuthenticationFlow": "docker auth", + "attributes": { + "cibaBackchannelTokenDeliveryMode": "poll", + "cibaExpiresIn": "120", + "cibaAuthRequestedUserHint": "login_hint", + "oauth2DeviceCodeLifespan": "600", + "clientOfflineSessionMaxLifespan": "0", + "oauth2DevicePollingInterval": "5", + "clientSessionIdleTimeout": "0", + "parRequestUriLifespan": "60", + "clientSessionMaxLifespan": "0", + "clientOfflineSessionIdleTimeout": "0", + "cibaInterval": "5" + }, + "keycloakVersion": "16.1.0", + "userManagedAccessAllowed": false, + "clientProfiles": { + "profiles": [] + }, + "clientPolicies": { + "policies": [] + } +} \ No newline at end of file From e29b8fbe683d44a374827a0d99ec45b0f4b3a46c Mon Sep 17 00:00:00 2001 From: LuisPi <44235126+lpinon@users.noreply.github.com> Date: Mon, 27 Jun 2022 11:30:20 +0200 Subject: [PATCH 15/19] Custom Exceptions and Handlers --- README.md | 4 ++-- app/core/Configuration.py | 4 ++++ app/core/exception_handlers.py | 28 ++++++++++++++++++++++++++++ app/exceptions/__init__.py | 0 app/exceptions/http.py | 27 +++++++++++++++++++++++++++ app/exceptions/runtime.py | 17 +++++++++++++++++ app/services/user.py | 5 ++++- main.py | 2 ++ 8 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 app/core/exception_handlers.py create mode 100644 app/exceptions/__init__.py create mode 100644 app/exceptions/http.py create mode 100644 app/exceptions/runtime.py diff --git a/README.md b/README.md index 2336824..d78a271 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,9 @@ - [X] Database Configuration - [ ] ASYNC Database Configuration - [X] Swagger Configuration -- [ ] JWT / Keycloak Integration +- [X] JWT / Keycloak Integration - [X] CORS Configuration -- [ ] Global Exception Management +- [X] Global Exception Management - [ ] DB Entity Generation - [ ] Code generation using Templates - [X] Log to files integration diff --git a/app/core/Configuration.py b/app/core/Configuration.py index 2755449..3644e5d 100644 --- a/app/core/Configuration.py +++ b/app/core/Configuration.py @@ -10,6 +10,8 @@ # Configuration Objects Definitions +from app.core.exception_handlers import init_exception_handlers + class GlobalSettings(BaseSettings): app_name: str = "My Awesome API" @@ -131,4 +133,6 @@ def get_api(): # Include all Routers from app.controllers import api_router api.include_router(api_router) + # Init exception Handlers + init_exception_handlers(api) return api diff --git a/app/core/exception_handlers.py b/app/core/exception_handlers.py new file mode 100644 index 0000000..12fc581 --- /dev/null +++ b/app/core/exception_handlers.py @@ -0,0 +1,28 @@ +import logging + +from fastapi import FastAPI +from starlette.requests import Request +from starlette.responses import PlainTextResponse + +from app.exceptions.http import DevonHttpException +from app.exceptions.runtime import DevonCustomException + +logger = logging.getLogger(__name__) + + +def init_exception_handlers(api: FastAPI): + # Custom HTTP Exception Handler + @api.exception_handler(DevonHttpException) + async def http_exception_handler(request: Request, exc): + logger.error(str(request.url) + " - Path params: " + str(request.path_params) + + " - Query Params: " + str(request.query_params)) + logger.exception(exc.detail) + return PlainTextResponse(str(exc.detail), status_code=exc.status_code) + + # Custom Runtime Exception Handler + @api.exception_handler(DevonCustomException) + async def runtime_exception_handler(request: Request, exc: DevonCustomException): + logger.error(str(request.url) + " - Path params: " + str(request.path_params) + + " - Query Params: " + str(request.query_params)) + logger.exception(exc.detail) + return PlainTextResponse(str(exc.detail), status_code=500) diff --git a/app/exceptions/__init__.py b/app/exceptions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/exceptions/http.py b/app/exceptions/http.py new file mode 100644 index 0000000..4a46d8d --- /dev/null +++ b/app/exceptions/http.py @@ -0,0 +1,27 @@ +from typing import Any, Optional, Dict + +from fastapi import HTTPException + + +################################## +# Base HTTP Exception +################################## + + +class DevonHttpException(HTTPException): + def __init__(self, status_code: int, detail: str = None, headers: Optional[Dict[str, Any]] = None): + super().__init__(status_code=status_code, detail=detail, headers=headers) + + +################################## +# Custom HTTP Exceptions +################################## + +class NotFoundException(DevonHttpException): + def __init__(self, detail: str = "Not found", headers: Optional[Dict[str, Any]] = None): + super().__init__(status_code=404, detail=detail, headers=headers) + + +class DevonHttpExceptionWithCustomHeader(DevonHttpException): + def __init__(self): + super().__init__(status_code=403, detail="Custom Header Exception", headers={"X-Error": "There goes my error"}) diff --git a/app/exceptions/runtime.py b/app/exceptions/runtime.py new file mode 100644 index 0000000..28d7ee7 --- /dev/null +++ b/app/exceptions/runtime.py @@ -0,0 +1,17 @@ +################################## +# Base Runtime Exception +################################## + +class DevonCustomException(RuntimeError): + def __init__(self, detail: str = None): + super().__init__() + self.detail = detail + + +################################## +# Custom Runtime Exceptions +################################## + +class UnexpectedStatusException(DevonCustomException): + def __init__(self): + super().__init__("Unexpected status found") diff --git a/app/services/user.py b/app/services/user.py index 41bdfaf..db70183 100644 --- a/app/services/user.py +++ b/app/services/user.py @@ -2,6 +2,7 @@ from fastapi import Depends +from app.exceptions.http import NotFoundException from app.models import User from app.repositories.user import get_user_repository, UserRepository @@ -11,4 +12,6 @@ def __init__(self, repository: UserRepository = Depends(get_user_repository)): self.user_repo = repository async def get_user_by_email(self, email: str) -> Optional[User]: - return await self.user_repo.get_by_email(email=email) + user = await self.user_repo.get_by_email(email=email) + if user is None: + raise NotFoundException(detail="User not found") diff --git a/main.py b/main.py index 4d8994b..0030423 100644 --- a/main.py +++ b/main.py @@ -6,6 +6,8 @@ from app.core.database import init_db_entities # Init APP with Configuration +from app.exceptions.http import NotFoundException +from app.exceptions.runtime import UnexpectedStatusException from app.services.user import UserService api = get_api() From 6da28396ad88f5e253af2842a93d4d2b1f5b5538 Mon Sep 17 00:00:00 2001 From: LuisPi <44235126+lpinon@users.noreply.github.com> Date: Wed, 29 Jun 2022 12:06:31 +0200 Subject: [PATCH 16/19] Refactor to Modularize --- TEST.env | 2 +- app/__init__.py | 3 ++ app/{core => business}/__init__.py | 0 app/business/controllers/__init__.py | 7 +++ app/{ => business}/controllers/user.py | 2 +- .../models}/__init__.py | 0 .../services}/__init__.py | 0 app/{ => business}/services/user.py | 6 +-- .../__common__.py => common/__init__.py} | 13 +++--- app/{services => common/base}/__init__.py | 0 .../base.py => common/base/base_entity.py} | 0 .../base/base_repository.py} | 3 +- app/common/controllers/__init__.py | 8 ++++ app/{ => common}/controllers/auth.py | 6 +-- app/{ => common}/controllers/identity.py | 6 +-- app/common/core/__init__.py | 0 .../core/configuration.py} | 45 ++++--------------- app/{ => common}/core/database.py | 1 - app/{ => common}/core/exception_handlers.py | 4 +- app/common/exceptions/__init__.py | 0 app/{ => common}/exceptions/http.py | 0 app/{ => common}/exceptions/runtime.py | 0 app/common/infra/__init__.py | 0 app/common/infra/keycloak.py | 37 +++++++++++++++ app/controllers/__init__.py | 9 ---- app/domain/__init__.py | 0 app/{ => domain}/models/__init__.py | 0 app/{ => domain}/models/group.py | 6 +-- app/{ => domain}/models/links.py | 3 +- app/{ => domain}/models/role.py | 2 +- app/{ => domain}/models/user.py | 4 +- app/domain/repositories/__init__.py | 0 app/{ => domain}/repositories/user.py | 10 ++--- main.py | 33 +++----------- 34 files changed, 100 insertions(+), 110 deletions(-) rename app/{core => business}/__init__.py (100%) create mode 100644 app/business/controllers/__init__.py rename app/{ => business}/controllers/user.py (88%) rename app/{exceptions => business/models}/__init__.py (100%) rename app/{repositories => business/services}/__init__.py (100%) rename app/{ => business}/services/user.py (71%) rename app/{controllers/__common__.py => common/__init__.py} (61%) rename app/{services => common/base}/__init__.py (100%) rename app/{models/base.py => common/base/base_entity.py} (100%) rename app/{repositories/base.py => common/base/base_repository.py} (90%) create mode 100644 app/common/controllers/__init__.py rename app/{ => common}/controllers/auth.py (87%) rename app/{ => common}/controllers/identity.py (97%) create mode 100644 app/common/core/__init__.py rename app/{core/Configuration.py => common/core/configuration.py} (68%) rename app/{ => common}/core/database.py (99%) rename app/{ => common}/core/exception_handlers.py (89%) create mode 100644 app/common/exceptions/__init__.py rename app/{ => common}/exceptions/http.py (100%) rename app/{ => common}/exceptions/runtime.py (100%) create mode 100644 app/common/infra/__init__.py create mode 100644 app/common/infra/keycloak.py delete mode 100644 app/controllers/__init__.py create mode 100644 app/domain/__init__.py rename app/{ => domain}/models/__init__.py (100%) rename app/{ => domain}/models/group.py (79%) rename app/{ => domain}/models/links.py (86%) rename app/{ => domain}/models/role.py (84%) rename app/{ => domain}/models/user.py (93%) create mode 100644 app/domain/repositories/__init__.py rename app/{ => domain}/repositories/user.py (83%) diff --git a/TEST.env b/TEST.env index 5f7eff2..7c005f1 100644 --- a/TEST.env +++ b/TEST.env @@ -15,7 +15,7 @@ DB_POOL_SIZE=5 # Keycloak Configuration | Remove Configuration to Disable KEYCLOAK_AUTH_SERVER=http://localhost:8085/auth KEYCLOAK_CLIENT_ID=test-client -KEYCLOAK_CLIENT_SECRET=GzgACcJzhzQ4j8kWhmhazt7WSdxDVUyE +KEYCLOAK_CLIENT_SECRET=1r8VZ7IcRBkERXYx7r9RcjiT5fpAX2O8 KEYCLOAK_ADMIN_CLIENT_SECRET=kPjQYZkpOvd0oSuRSHbFQbQaYn8kQ2qG KEYCLOAK_REALM=Test KEYCLOAK_CALLBACK_URI=http://localhost:9999/auth/callback \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py index e69de29..0f95244 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -0,0 +1,3 @@ +from app.common.core.configuration import get_api + +api = get_api() diff --git a/app/core/__init__.py b/app/business/__init__.py similarity index 100% rename from app/core/__init__.py rename to app/business/__init__.py diff --git a/app/business/controllers/__init__.py b/app/business/controllers/__init__.py new file mode 100644 index 0000000..3a21eb3 --- /dev/null +++ b/app/business/controllers/__init__.py @@ -0,0 +1,7 @@ +from fastapi import APIRouter + +from app.business.controllers import user + +# Include all routers here +api_router = APIRouter() +api_router.include_router(user.router, tags=["users"]) diff --git a/app/controllers/user.py b/app/business/controllers/user.py similarity index 88% rename from app/controllers/user.py rename to app/business/controllers/user.py index c57b4a4..d386c0b 100644 --- a/app/controllers/user.py +++ b/app/business/controllers/user.py @@ -2,7 +2,7 @@ from fastapi import APIRouter, Depends -from app.services.user import UserService +from app.business.services.user import UserService router = APIRouter(prefix="/users") diff --git a/app/exceptions/__init__.py b/app/business/models/__init__.py similarity index 100% rename from app/exceptions/__init__.py rename to app/business/models/__init__.py diff --git a/app/repositories/__init__.py b/app/business/services/__init__.py similarity index 100% rename from app/repositories/__init__.py rename to app/business/services/__init__.py diff --git a/app/services/user.py b/app/business/services/user.py similarity index 71% rename from app/services/user.py rename to app/business/services/user.py index db70183..f1667c2 100644 --- a/app/services/user.py +++ b/app/business/services/user.py @@ -2,9 +2,9 @@ from fastapi import Depends -from app.exceptions.http import NotFoundException -from app.models import User -from app.repositories.user import get_user_repository, UserRepository +from app.common.exceptions.http import NotFoundException +from app.domain.models import User +from app.domain.repositories.user import get_user_repository, UserRepository class UserService: diff --git a/app/controllers/__common__.py b/app/common/__init__.py similarity index 61% rename from app/controllers/__common__.py rename to app/common/__init__.py index 158ca9f..4e879bd 100644 --- a/app/controllers/__common__.py +++ b/app/common/__init__.py @@ -1,16 +1,17 @@ -from app.core.configuration import get_idp +from fastapi_keycloak import OIDCUser -idp = get_idp() +from app.common.core.configuration import get_idp, get_keycloak_settings + +idp = get_idp(get_keycloak_settings()) # Shortcut for checking current user and roles def get_user(required_roles: list[str] | None = None): - """Returns the current user based on an access token in the HTTP-header. Optionally verifies roles are possessed - by the user + """Returns a function that checks the current user based on an access token in the HTTP-header. Optionally verifies + roles are possessed by the user Args: required_roles List[str]: List of role names required for this endpoint - extra_fields List[str]: The names of the additional fields you need that are encoded in JWT Returns: OIDCUser: Decoded JWT content @@ -21,4 +22,4 @@ def get_user(required_roles: list[str] | None = None): JWTClaimsError: If any claim is invalid HTTPException: If any role required is not contained within the roles of the users """ - return idp.get_current_user() + return idp.get_current_user(required_roles=required_roles) diff --git a/app/services/__init__.py b/app/common/base/__init__.py similarity index 100% rename from app/services/__init__.py rename to app/common/base/__init__.py diff --git a/app/models/base.py b/app/common/base/base_entity.py similarity index 100% rename from app/models/base.py rename to app/common/base/base_entity.py diff --git a/app/repositories/base.py b/app/common/base/base_repository.py similarity index 90% rename from app/repositories/base.py rename to app/common/base/base_repository.py index 206edd2..822ea89 100644 --- a/app/repositories/base.py +++ b/app/common/base/base_repository.py @@ -3,11 +3,10 @@ from fastapi import Depends from sqlalchemy import select -from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload, sessionmaker from sqlmodel import SQLModel -from app.core.database import get_db_session_factory +from app.common.core.database import get_db_session_factory ModelType = TypeVar("ModelType", bound=SQLModel) diff --git a/app/common/controllers/__init__.py b/app/common/controllers/__init__.py new file mode 100644 index 0000000..4f714b5 --- /dev/null +++ b/app/common/controllers/__init__.py @@ -0,0 +1,8 @@ +# Include all routers here +from fastapi import APIRouter + +from app.common.controllers import auth, identity + +auth_router = APIRouter() +auth_router.include_router(auth.router, tags=["auth"]) +auth_router.include_router(identity.router, tags=["idp"]) diff --git a/app/controllers/auth.py b/app/common/controllers/auth.py similarity index 87% rename from app/controllers/auth.py rename to app/common/controllers/auth.py index a337ce5..828d416 100644 --- a/app/controllers/auth.py +++ b/app/common/controllers/auth.py @@ -4,7 +4,7 @@ from fastapi_keycloak import OIDCUser, UsernamePassword from starlette.responses import RedirectResponse -from app.controllers.__common__ import get_user, idp +from app.common import get_user, idp logger = logging.getLogger(__name__) @@ -21,12 +21,12 @@ def root(): @router.get("/user") # Requires logged in -def current_users(user: OIDCUser = Depends(get_user)): +def current_users(user: OIDCUser = Depends(get_user())): return user @router.get("/current_user/roles") -def get_current_users_roles(user: OIDCUser = Depends(get_user)): +def get_current_users_roles(user: OIDCUser = Depends(get_user())): return user.roles diff --git a/app/controllers/identity.py b/app/common/controllers/identity.py similarity index 97% rename from app/controllers/identity.py rename to app/common/controllers/identity.py index a8d941b..4cd4c9c 100644 --- a/app/controllers/identity.py +++ b/app/common/controllers/identity.py @@ -3,11 +3,9 @@ from fastapi import APIRouter, Depends, Body, Query from fastapi_keycloak import HTTPMethod, KeycloakUser from pydantic import SecretStr +from app.common import get_user, idp -from app.controllers.__common__ import get_user, idp - -router = APIRouter(prefix="/idp", dependencies=[Depends(get_user)]) # Protect all the paths with user authentication - +router = APIRouter(prefix="/idp", dependencies=[Depends(get_user())]) # Protect all the paths with user authentication ################################# # IDP Admin Router diff --git a/app/common/core/__init__.py b/app/common/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/core/Configuration.py b/app/common/core/configuration.py similarity index 68% rename from app/core/Configuration.py rename to app/common/core/configuration.py index 3644e5d..ca37f03 100644 --- a/app/core/Configuration.py +++ b/app/common/core/configuration.py @@ -1,4 +1,4 @@ -from typing import Optional, Type, Generic, Union, List +from typing import Optional, Type, List import yaml from fastapi import FastAPI @@ -10,7 +10,8 @@ # Configuration Objects Definitions -from app.core.exception_handlers import init_exception_handlers +from app.common.core.exception_handlers import init_exception_handlers +from app.common.infra.keycloak import get_idp, KeycloakSettings class GlobalSettings(BaseSettings): @@ -50,19 +51,6 @@ class Config: env_file = "TEST.env" -class KeycloakSettings(BaseSettings): - auth_server: Optional[str] - client_id: Optional[str] - client_secret: Optional[str] - admin_client_secret: Optional[str] - realm: Optional[str] - callback_uri: Optional[str] - - class Config: - env_prefix = "KEYCLOAK_" - env_file = "TEST.env" - - # Utils to load Configurations def __load_env_file_on_settings(settings: Type[BaseSettings]): @@ -95,28 +83,10 @@ def get_log_config(): return log_config -def get_idp(): - print("Init Keycloak") - keycloak_settings = get_keycloak_settings() - # Check if configuration is defined to use Keycloak IDP - if keycloak_settings.auth_server is None or keycloak_settings.realm is None: - return None - # Configure Keycloak Authentication - idp = FastAPIKeycloak( - server_url=keycloak_settings.auth_server, - client_id=keycloak_settings.client_id, - client_secret=keycloak_settings.client_secret, - admin_client_secret=keycloak_settings.admin_client_secret, - realm=keycloak_settings.realm, - callback_uri=keycloak_settings.callback_uri - ) - return idp - - def get_api(): app_settings = get_global_settings() api = FastAPI(docs_url=app_settings.swagger_path, title=app_settings.app_name) - idp = get_idp() + idp = get_idp(keycloak_settings=get_keycloak_settings()) if idp is not None: # Enable authentication layer to swagger endpoints idp.add_swagger_config(api) @@ -130,9 +100,10 @@ def get_api(): allow_headers=["*"], expose_headers=["*"] ) - # Include all Routers - from app.controllers import api_router - api.include_router(api_router) + # Include auth router + if idp is not None: + from app.common.controllers import auth_router + api.include_router(auth_router) # Init exception Handlers init_exception_handlers(api) return api diff --git a/app/core/database.py b/app/common/core/database.py similarity index 99% rename from app/core/database.py rename to app/common/core/database.py index c2d6472..dfc4f01 100644 --- a/app/core/database.py +++ b/app/common/core/database.py @@ -40,7 +40,6 @@ def get_async_db_engine(settings: DatabaseSettings = Depends(get_db_settings), d def init_db_entities(db: DatabaseSettings): - import app.models engine = get_db_engine(db, get_db_uri(db)) SQLModel.metadata.create_all(engine) diff --git a/app/core/exception_handlers.py b/app/common/core/exception_handlers.py similarity index 89% rename from app/core/exception_handlers.py rename to app/common/core/exception_handlers.py index 12fc581..0b97e03 100644 --- a/app/core/exception_handlers.py +++ b/app/common/core/exception_handlers.py @@ -4,8 +4,8 @@ from starlette.requests import Request from starlette.responses import PlainTextResponse -from app.exceptions.http import DevonHttpException -from app.exceptions.runtime import DevonCustomException +from app.common.exceptions.http import DevonHttpException +from app.common.exceptions.runtime import DevonCustomException logger = logging.getLogger(__name__) diff --git a/app/common/exceptions/__init__.py b/app/common/exceptions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/exceptions/http.py b/app/common/exceptions/http.py similarity index 100% rename from app/exceptions/http.py rename to app/common/exceptions/http.py diff --git a/app/exceptions/runtime.py b/app/common/exceptions/runtime.py similarity index 100% rename from app/exceptions/runtime.py rename to app/common/exceptions/runtime.py diff --git a/app/common/infra/__init__.py b/app/common/infra/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/common/infra/keycloak.py b/app/common/infra/keycloak.py new file mode 100644 index 0000000..5f691cb --- /dev/null +++ b/app/common/infra/keycloak.py @@ -0,0 +1,37 @@ +from typing import Optional + +from fastapi_keycloak import FastAPIKeycloak +from pydantic import BaseSettings + + +class KeycloakSettings(BaseSettings): + auth_server: Optional[str] + client_id: Optional[str] + client_secret: Optional[str] + admin_client_secret: Optional[str] + realm: Optional[str] + callback_uri: Optional[str] + + class Config: + env_prefix = "KEYCLOAK_" + env_file = "TEST.env" + + +def get_idp(keycloak_settings: KeycloakSettings): + # Check if configuration is defined to use Keycloak IDP + if keycloak_settings.auth_server is None or keycloak_settings.realm is None: + return None + try: + # Configure Keycloak Authentication + idp = FastAPIKeycloak( + server_url=keycloak_settings.auth_server, + client_id=keycloak_settings.client_id, + client_secret=keycloak_settings.client_secret, + admin_client_secret=keycloak_settings.admin_client_secret, + realm=keycloak_settings.realm, + callback_uri=keycloak_settings.callback_uri + ) + return idp + except: + # If Keycloak not available return None to disable + return None diff --git a/app/controllers/__init__.py b/app/controllers/__init__.py deleted file mode 100644 index f6007d1..0000000 --- a/app/controllers/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from fastapi import APIRouter - -from app.controllers import user, auth, identity - -# Include all routers here -api_router = APIRouter() -api_router.include_router(user.router, tags=["users"]) -api_router.include_router(auth.router, tags=["auth"]) -api_router.include_router(identity.router, tags=["idp"]) diff --git a/app/domain/__init__.py b/app/domain/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/models/__init__.py b/app/domain/models/__init__.py similarity index 100% rename from app/models/__init__.py rename to app/domain/models/__init__.py diff --git a/app/models/group.py b/app/domain/models/group.py similarity index 79% rename from app/models/group.py rename to app/domain/models/group.py index c4ad9b7..64d2018 100644 --- a/app/models/group.py +++ b/app/domain/models/group.py @@ -1,10 +1,10 @@ from sqlmodel import Field, Relationship, SQLModel from typing import List, Optional -from app.models.links import LinkGroupUser -from app.models.base import BaseUUIDModel +from app.domain.models.links import LinkGroupUser +from app.common.base.base_entity import BaseUUIDModel from uuid import UUID -from app.models.user import User +from app.domain.models.user import User class GroupBase(SQLModel): diff --git a/app/models/links.py b/app/domain/models/links.py similarity index 86% rename from app/models/links.py rename to app/domain/models/links.py index 6d78091..0680fab 100644 --- a/app/models/links.py +++ b/app/domain/models/links.py @@ -1,8 +1,9 @@ from sqlmodel import Field from typing import Optional -from app.models.base import BaseUUIDModel from uuid import UUID +from app.common.base.base_entity import BaseUUIDModel + class LinkGroupUser(BaseUUIDModel, table=True): group_id: Optional[UUID] = Field(default=None, nullable=False, foreign_key="group.id", primary_key=True) diff --git a/app/models/role.py b/app/domain/models/role.py similarity index 84% rename from app/models/role.py rename to app/domain/models/role.py index a25f5e0..14d84e2 100644 --- a/app/models/role.py +++ b/app/domain/models/role.py @@ -1,6 +1,6 @@ from sqlmodel import SQLModel, Relationship from typing import List -from app.models.base import BaseUUIDModel +from app.common.base.base_entity import BaseUUIDModel class RoleBase(SQLModel): diff --git a/app/models/user.py b/app/domain/models/user.py similarity index 93% rename from app/models/user.py rename to app/domain/models/user.py index f978914..0476dff 100644 --- a/app/models/user.py +++ b/app/domain/models/user.py @@ -2,10 +2,10 @@ from enum import Enum from sqlmodel import Field, SQLModel, Relationship, Column, DateTime -from app.models.links import LinkGroupUser +from app.domain.models.links import LinkGroupUser from typing import List, Optional from pydantic import EmailStr, BaseModel -from app.models.base import BaseUUIDModel +from app.common.base.base_entity import BaseUUIDModel from uuid import UUID # DB ENTITY diff --git a/app/domain/repositories/__init__.py b/app/domain/repositories/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/repositories/user.py b/app/domain/repositories/user.py similarity index 83% rename from app/repositories/user.py rename to app/domain/repositories/user.py index 16f8425..464be28 100644 --- a/app/repositories/user.py +++ b/app/domain/repositories/user.py @@ -1,16 +1,14 @@ from datetime import datetime from typing import Optional -from uuid import UUID from fastapi import Depends from sqlalchemy import select from sqlalchemy.orm import sessionmaker -from sqlmodel.ext.asyncio.session import AsyncSession -from app.core.database import get_db_session_factory -from app.models import User -from app.models.user import UserCreateRequest -from app.repositories.base import BaseRepository +from app.common.base.base_repository import BaseRepository +from app.common.core.database import get_db_session_factory +from app.domain.models import User +from app.domain.models.user import UserCreateRequest class UserRepository(BaseRepository[User]): diff --git a/main.py b/main.py index 0030423..ee758a8 100644 --- a/main.py +++ b/main.py @@ -1,37 +1,14 @@ import logging import uvicorn -from fastapi import Depends - -from app.core.configuration import get_global_settings, get_api, get_log_config, get_db_settings, GlobalSettings -from app.core.database import init_db_entities # Init APP with Configuration -from app.exceptions.http import NotFoundException -from app.exceptions.runtime import UnexpectedStatusException -from app.services.user import UserService +from app import api +from app.common.core.configuration import get_global_settings, get_log_config, get_db_settings +from app.common.core.database import init_db_entities -api = get_api() # Init Logger for this Class logger = logging.getLogger(__name__) - -@api.get("/info") -async def info(settings: GlobalSettings = Depends(get_global_settings), - user_service: UserService = Depends(UserService)): - logger.info("TEST INFO") - logger.error("TEST ERROR") - logger.debug("TEST DEBUG") - await user_service.get_user_by_email(email="test@email.es") - return { - "app_name": settings.app_name, - "environment": settings.environment, - "swagger": settings.swagger_path if settings.swagger_path else "DISABLED" - } - if __name__ == "__main__": - global_settings = get_global_settings() - logging_settings = get_log_config() - db_settings = get_db_settings() - init_db_entities(db_settings) - print(global_settings.port, global_settings.environment) - uvicorn.run(api, host="0.0.0.0", port=global_settings.port, log_config=logging_settings) + init_db_entities(get_db_settings()) + uvicorn.run(api, host="0.0.0.0", port=get_global_settings().port, log_config=get_log_config()) From 11baa552aa807f7dab5968e6c78ed82d3252dea1 Mon Sep 17 00:00:00 2001 From: LuisPi <44235126+lpinon@users.noreply.github.com> Date: Mon, 4 Jul 2022 10:29:00 +0200 Subject: [PATCH 17/19] Todo Management & Refactor --- app/__init__.py | 3 +- app/business/__init__.py | 7 +++ .../__init__.py | 0 .../controllers/__init__.py | 2 +- .../controllers/user.py | 2 +- .../models}/__init__.py | 0 .../employee_management/services/__init__.py | 0 .../services/user.py | 0 app/business/todo_management/__init__.py | 0 .../todo_management/controllers/__init__.py | 7 +++ .../todo_management/controllers/todo.py | 24 ++++++++ .../todo_management/models/__init__.py | 0 app/business/todo_management/models/todo.py | 20 +++++++ .../todo_management/services/__init__.py | 0 app/business/todo_management/services/todo.py | 29 ++++++++++ app/common/__init__.py | 4 +- app/common/base/base_entity.py | 4 +- app/common/base/base_repository.py | 26 ++++----- app/common/core/configuration.py | 7 ++- app/common/core/database.py | 24 +++++--- app/domain/models/__init__.py | 6 +- app/domain/models/employee.py | 11 ++++ app/domain/models/group.py | 18 ------ app/domain/models/links.py | 10 ---- app/domain/models/role.py | 13 ----- app/domain/models/todo.py | 8 +++ app/domain/models/user.py | 56 ------------------- app/domain/repositories/employee.py | 31 ++++++++++ app/domain/repositories/todo.py | 40 +++++++++++++ app/domain/repositories/user.py | 41 -------------- 30 files changed, 223 insertions(+), 170 deletions(-) rename app/business/{models => employee_management}/__init__.py (100%) rename app/business/{ => employee_management}/controllers/__init__.py (69%) rename app/business/{ => employee_management}/controllers/user.py (84%) rename app/business/{services => employee_management/models}/__init__.py (100%) create mode 100644 app/business/employee_management/services/__init__.py rename app/business/{ => employee_management}/services/user.py (100%) create mode 100644 app/business/todo_management/__init__.py create mode 100644 app/business/todo_management/controllers/__init__.py create mode 100644 app/business/todo_management/controllers/todo.py create mode 100644 app/business/todo_management/models/__init__.py create mode 100644 app/business/todo_management/models/todo.py create mode 100644 app/business/todo_management/services/__init__.py create mode 100644 app/business/todo_management/services/todo.py create mode 100644 app/domain/models/employee.py delete mode 100644 app/domain/models/group.py delete mode 100644 app/domain/models/links.py delete mode 100644 app/domain/models/role.py create mode 100644 app/domain/models/todo.py delete mode 100644 app/domain/models/user.py create mode 100644 app/domain/repositories/employee.py create mode 100644 app/domain/repositories/todo.py delete mode 100644 app/domain/repositories/user.py diff --git a/app/__init__.py b/app/__init__.py index 0f95244..964c82b 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,3 +1,4 @@ +from app.business import all_router from app.common.core.configuration import get_api -api = get_api() +api = get_api(routers=[all_router]) diff --git a/app/business/__init__.py b/app/business/__init__.py index e69de29..d1c1741 100644 --- a/app/business/__init__.py +++ b/app/business/__init__.py @@ -0,0 +1,7 @@ +from fastapi import APIRouter + +# Include all routers here +from app.business.todo_management.controllers import todo_management_router + +all_router = APIRouter() +all_router.include_router(todo_management_router, tags=["Todo Management"]) diff --git a/app/business/models/__init__.py b/app/business/employee_management/__init__.py similarity index 100% rename from app/business/models/__init__.py rename to app/business/employee_management/__init__.py diff --git a/app/business/controllers/__init__.py b/app/business/employee_management/controllers/__init__.py similarity index 69% rename from app/business/controllers/__init__.py rename to app/business/employee_management/controllers/__init__.py index 3a21eb3..1f1fc49 100644 --- a/app/business/controllers/__init__.py +++ b/app/business/employee_management/controllers/__init__.py @@ -1,6 +1,6 @@ from fastapi import APIRouter -from app.business.controllers import user +from app.business.employee_management.controllers import user # Include all routers here api_router = APIRouter() diff --git a/app/business/controllers/user.py b/app/business/employee_management/controllers/user.py similarity index 84% rename from app/business/controllers/user.py rename to app/business/employee_management/controllers/user.py index d386c0b..2f8cfe1 100644 --- a/app/business/controllers/user.py +++ b/app/business/employee_management/controllers/user.py @@ -2,7 +2,7 @@ from fastapi import APIRouter, Depends -from app.business.services.user import UserService +from app.business.employee_management.services.user import UserService router = APIRouter(prefix="/users") diff --git a/app/business/services/__init__.py b/app/business/employee_management/models/__init__.py similarity index 100% rename from app/business/services/__init__.py rename to app/business/employee_management/models/__init__.py diff --git a/app/business/employee_management/services/__init__.py b/app/business/employee_management/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/business/services/user.py b/app/business/employee_management/services/user.py similarity index 100% rename from app/business/services/user.py rename to app/business/employee_management/services/user.py diff --git a/app/business/todo_management/__init__.py b/app/business/todo_management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/business/todo_management/controllers/__init__.py b/app/business/todo_management/controllers/__init__.py new file mode 100644 index 0000000..149f483 --- /dev/null +++ b/app/business/todo_management/controllers/__init__.py @@ -0,0 +1,7 @@ +from fastapi import APIRouter + +# Include all routers here +from app.business.todo_management.controllers import todo + +todo_management_router = APIRouter() +todo_management_router.include_router(todo.router, tags=["Todo"]) diff --git a/app/business/todo_management/controllers/todo.py b/app/business/todo_management/controllers/todo.py new file mode 100644 index 0000000..1aa1224 --- /dev/null +++ b/app/business/todo_management/controllers/todo.py @@ -0,0 +1,24 @@ +import logging + +from fastapi import APIRouter, Depends + +from app.business.todo_management.models.todo import PendingTodosResponse, TodoDto, CreateTodoRequest +from app.business.todo_management.services.todo import TodoService + +router = APIRouter(prefix="/todo") + +logger = logging.getLogger(__name__) + + +@router.get("/pending", description="Gets all pending TODOs", response_model=PendingTodosResponse) +def get_pending_todos(todo_service: TodoService = Depends(TodoService)): + logger.info("Retrieving all the pending TODOs") + todos = todo_service.get_pending_todos() + return PendingTodosResponse(todos=todos) + + +@router.post("/create", description="Creates a new TODO", response_model=TodoDto) +def create_todo(create_request: CreateTodoRequest, todo_service=Depends(TodoService)): + logger.info("Creating a new TODO") + todo = todo_service.create_todo(create_request) + return todo diff --git a/app/business/todo_management/models/__init__.py b/app/business/todo_management/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/business/todo_management/models/todo.py b/app/business/todo_management/models/todo.py new file mode 100644 index 0000000..ca48a63 --- /dev/null +++ b/app/business/todo_management/models/todo.py @@ -0,0 +1,20 @@ +from typing import List +from uuid import UUID + +from pydantic import BaseModel + + +class TodoID(BaseModel): + id: UUID + + +class CreateTodoRequest(BaseModel): + description: str + + +class TodoDto(TodoID, CreateTodoRequest): + done: bool + + +class PendingTodosResponse(BaseModel): + todos: List[TodoDto] diff --git a/app/business/todo_management/services/__init__.py b/app/business/todo_management/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/business/todo_management/services/todo.py b/app/business/todo_management/services/todo.py new file mode 100644 index 0000000..9fa250e --- /dev/null +++ b/app/business/todo_management/services/todo.py @@ -0,0 +1,29 @@ +from typing import List + +from fastapi import Depends + +from app.business.todo_management.models.todo import TodoDto, CreateTodoRequest +from app.domain.models.todo import Todo +from app.domain.repositories.todo import TodoRepository, get_todo_repository + + +def parse_to_dto(todo_entity: Todo): + return TodoDto( + id=todo_entity.id, + description=todo_entity.description, + done=todo_entity.done + ) + + +class TodoService: + def __init__(self, repository: TodoRepository = Depends(get_todo_repository)): + self.todo_repo = repository + + def get_pending_todos(self) -> List[TodoDto]: + raw_todos = self.todo_repo.get_pending_todos() + todo_dtos = map(parse_to_dto, raw_todos) + return list(todo_dtos) + + def create_todo(self, create_req: CreateTodoRequest) -> TodoDto: + raw_new_todo = self.todo_repo.create(description=create_req.description) + return parse_to_dto(raw_new_todo) diff --git a/app/common/__init__.py b/app/common/__init__.py index 4e879bd..659fcbc 100644 --- a/app/common/__init__.py +++ b/app/common/__init__.py @@ -1,3 +1,5 @@ +from typing import Callable, Any + from fastapi_keycloak import OIDCUser from app.common.core.configuration import get_idp, get_keycloak_settings @@ -6,7 +8,7 @@ # Shortcut for checking current user and roles -def get_user(required_roles: list[str] | None = None): +def get_user(required_roles: list[str] | None = None) -> Callable[[], OIDCUser]: """Returns a function that checks the current user based on an access token in the HTTP-header. Optionally verifies roles are possessed by the user diff --git a/app/common/base/base_entity.py b/app/common/base/base_entity.py index 47e5247..be92586 100644 --- a/app/common/base/base_entity.py +++ b/app/common/base/base_entity.py @@ -11,5 +11,5 @@ class BaseUUIDModel(SQLModel): index=True, nullable=False, ) - updated_at: Optional[datetime] - created_at: Optional[datetime] + updated_at: Optional[datetime] = Field(default_factory=datetime.now) + created_at: Optional[datetime] = Field(default_factory=datetime.now) diff --git a/app/common/base/base_repository.py b/app/common/base/base_repository.py index 822ea89..d7a156c 100644 --- a/app/common/base/base_repository.py +++ b/app/common/base/base_repository.py @@ -1,30 +1,30 @@ -from typing import Generic, TypeVar, Type, Union, Optional +from typing import Generic, TypeVar, Type, Union, Optional, Callable from uuid import UUID from fastapi import Depends from sqlalchemy import select from sqlalchemy.orm import selectinload, sessionmaker -from sqlmodel import SQLModel +from sqlmodel import SQLModel, Session +from sqlmodel.ext.asyncio.session import AsyncSession -from app.common.core.database import get_db_session_factory +from app.common.core.database import get_session ModelType = TypeVar("ModelType", bound=SQLModel) class BaseRepository(Generic[ModelType]): - def __init__(self, model: Type[ModelType], session: sessionmaker = Depends(get_db_session_factory)): + def __init__(self, model: Type[ModelType], session: Session = Depends(get_session)): """ Object with default methods to Create, Read, Update and Delete (CRUD). """ self.model = model - self.create_session = session + self.session = session - async def get(self, *, id: Union[UUID, str]) -> Optional[ModelType]: - async with self.create_session() as db_session: - response = await db_session.exec( - select(self.model) - .where(self.model.id == id) - .options(selectinload('*')) - ) - return response.first() + def get(self, *, uid: Union[UUID, str]) -> Optional[ModelType]: + response = self.session.exec( + select(self.model) + .where(self.model.id == uid) + .options(selectinload('*')) + ) + return response.one_or_none() diff --git a/app/common/core/configuration.py b/app/common/core/configuration.py index ca37f03..38f2ca6 100644 --- a/app/common/core/configuration.py +++ b/app/common/core/configuration.py @@ -1,7 +1,7 @@ from typing import Optional, Type, List import yaml -from fastapi import FastAPI +from fastapi import FastAPI, APIRouter from fastapi_keycloak import FastAPIKeycloak from pydantic import BaseSettings, AnyHttpUrl, validator import os @@ -83,7 +83,7 @@ def get_log_config(): return log_config -def get_api(): +def get_api(routers: List[APIRouter]): app_settings = get_global_settings() api = FastAPI(docs_url=app_settings.swagger_path, title=app_settings.app_name) idp = get_idp(keycloak_settings=get_keycloak_settings()) @@ -104,6 +104,9 @@ def get_api(): if idp is not None: from app.common.controllers import auth_router api.include_router(auth_router) + # Include selected routers + for r in routers: + api.include_router(r) # Init exception Handlers init_exception_handlers(api) return api diff --git a/app/common/core/database.py b/app/common/core/database.py index dfc4f01..5de6892 100644 --- a/app/common/core/database.py +++ b/app/common/core/database.py @@ -2,7 +2,7 @@ from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession from sqlalchemy.future import Engine from sqlalchemy.orm import sessionmaker -from sqlmodel import SQLModel, create_engine +from sqlmodel import SQLModel, create_engine, Session from .configuration import DatabaseSettings, get_db_settings @@ -39,16 +39,26 @@ def get_async_db_engine(settings: DatabaseSettings = Depends(get_db_settings), d future=True, pool_pre_ping=True) +def get_session(engine: Engine = Depends(get_db_engine)): + sess = None + try: + sess = Session(engine) + yield sess + finally: + if sess: + sess.close() + + def init_db_entities(db: DatabaseSettings): engine = get_db_engine(db, get_db_uri(db)) SQLModel.metadata.create_all(engine) - -def get_db_session_factory(engine: Engine = Depends(get_async_db_engine)): - """ - Generates a session factory from the configured SQL Engine - """ - return sessionmaker(autocommit=False, class_=AsyncSession, autoflush=False, bind=engine) +# +# def get_db_session_factory(engine: Engine = Depends(get_async_db_engine)): +# """ +# Generates a session factory from the configured SQL Engine +# """ +# return sessionmaker(autocommit=False, class_=AsyncSession, autoflush=False, bind=engine) # # def get_db(session_factory: sessionmaker = Depends(get_session_factory)) -> Generator: diff --git a/app/domain/models/__init__.py b/app/domain/models/__init__.py index e4e1b26..e9f145a 100644 --- a/app/domain/models/__init__.py +++ b/app/domain/models/__init__.py @@ -1,6 +1,4 @@ # Import DB Entities for init # Tables must extend SQLModel -from .group import Group -from .links import LinkGroupUser -from .role import Role -from .user import User +from .todo import Todo +from .employee import Employee diff --git a/app/domain/models/employee.py b/app/domain/models/employee.py new file mode 100644 index 0000000..2e3f442 --- /dev/null +++ b/app/domain/models/employee.py @@ -0,0 +1,11 @@ +from typing import Optional +from pydantic import EmailStr +from sqlmodel import Field +from app.common.base.base_entity import BaseUUIDModel + + +# DB ENTITY +class Employee(BaseUUIDModel, table=True): + name: Optional[str] = Field(nullable=True) + surname: Optional[str] = Field(nullable=True) + mail: EmailStr = Field(nullable=False, index=True, sa_column_kwargs={"unique": True}) diff --git a/app/domain/models/group.py b/app/domain/models/group.py deleted file mode 100644 index 64d2018..0000000 --- a/app/domain/models/group.py +++ /dev/null @@ -1,18 +0,0 @@ -from sqlmodel import Field, Relationship, SQLModel -from typing import List, Optional -from app.domain.models.links import LinkGroupUser -from app.common.base.base_entity import BaseUUIDModel -from uuid import UUID - -from app.domain.models.user import User - - -class GroupBase(SQLModel): - name: str - description: str - - -class Group(BaseUUIDModel, GroupBase, table=True): - created_by_id: Optional[UUID] = Field(default=None, foreign_key="user.id") - created_by: "User" = Relationship(sa_relationship_kwargs={"lazy":"selectin", "primaryjoin":"Group.created_by_id==User.id"}) - users: List["User"] = Relationship(back_populates="groups", link_model=LinkGroupUser, sa_relationship_kwargs={"lazy": "selectin"}) diff --git a/app/domain/models/links.py b/app/domain/models/links.py deleted file mode 100644 index 0680fab..0000000 --- a/app/domain/models/links.py +++ /dev/null @@ -1,10 +0,0 @@ -from sqlmodel import Field -from typing import Optional -from uuid import UUID - -from app.common.base.base_entity import BaseUUIDModel - - -class LinkGroupUser(BaseUUIDModel, table=True): - group_id: Optional[UUID] = Field(default=None, nullable=False, foreign_key="group.id", primary_key=True) - user_id: Optional[UUID] = Field(default=None, nullable=False, foreign_key="user.id", primary_key=True) diff --git a/app/domain/models/role.py b/app/domain/models/role.py deleted file mode 100644 index 14d84e2..0000000 --- a/app/domain/models/role.py +++ /dev/null @@ -1,13 +0,0 @@ -from sqlmodel import SQLModel, Relationship -from typing import List -from app.common.base.base_entity import BaseUUIDModel - - -class RoleBase(SQLModel): - name: str - description: str - - -class Role(BaseUUIDModel, RoleBase, table=True): - users: List["User"] = Relationship(back_populates="role", sa_relationship_kwargs={"lazy": "selectin"}) - diff --git a/app/domain/models/todo.py b/app/domain/models/todo.py new file mode 100644 index 0000000..09b6bc9 --- /dev/null +++ b/app/domain/models/todo.py @@ -0,0 +1,8 @@ +from sqlmodel import Field +from app.common.base.base_entity import BaseUUIDModel + + +# DB ENTITY +class Todo(BaseUUIDModel, table=True): + description: str = Field(nullable=False) + done: bool = Field(nullable=False, default=False) diff --git a/app/domain/models/user.py b/app/domain/models/user.py deleted file mode 100644 index 0476dff..0000000 --- a/app/domain/models/user.py +++ /dev/null @@ -1,56 +0,0 @@ -from datetime import datetime -from enum import Enum - -from sqlmodel import Field, SQLModel, Relationship, Column, DateTime -from app.domain.models.links import LinkGroupUser -from typing import List, Optional -from pydantic import EmailStr, BaseModel -from app.common.base.base_entity import BaseUUIDModel -from uuid import UUID - -# DB ENTITY - - -class UserBase(SQLModel): - first_name: str - last_name: str - email: EmailStr = Field(nullable=True, index=True, sa_column_kwargs={"unique": True}) - is_active: bool = Field(default=True) - is_superuser: bool = Field(default=False) - birthdate: Optional[datetime] = Field(sa_column=Column(DateTime(timezone=True), nullable=True)) # bday with timezne - phone: Optional[str] - state: Optional[str] - country: Optional[str] - address: Optional[str] - - -class User(BaseUUIDModel, UserBase, table=True): - hashed_password: str = Field(nullable=False, index=True) - role_id: Optional[UUID] = Field(default=None, foreign_key="role.id") - role: Optional["Role"] = Relationship(back_populates="users", sa_relationship_kwargs={"lazy": "selectin"}) - groups: List["Group"] = Relationship(back_populates="users", link_model=LinkGroupUser, - sa_relationship_kwargs={"lazy": "selectin"}) - - -# REQUESTS - -class UserCreateRequest(BaseModel): - first_name: Optional[str] - last_name: Optional[str] - password: Optional[str] - email: EmailStr - is_superuser: bool = False - role_id: Optional[UUID] - - -class UserUpdateRequest(BaseModel): - id: int - email: EmailStr - is_active: bool = True - - -# RESPONSES - -class UserStatusEnum(str, Enum): - active = 'active' - inactive = 'inactive' diff --git a/app/domain/repositories/employee.py b/app/domain/repositories/employee.py new file mode 100644 index 0000000..1128d44 --- /dev/null +++ b/app/domain/repositories/employee.py @@ -0,0 +1,31 @@ +from typing import Optional + +from fastapi import Depends +from sqlalchemy.orm import sessionmaker +from sqlmodel import select, Session + +from app.common.base.base_repository import BaseRepository +from app.common.core.database import get_session +from app.common.exceptions.http import NotFoundException +from app.domain.models.employee import Employee + + +class EmployeeRepository(BaseRepository[Employee]): + + def get_by_email(self, *, email: str) -> Optional[Employee]: + employees = self.session.exec(select(Employee).where(Employee.mail == email)) + employee = employees.one_or_none() + if not employee: + raise NotFoundException(detail="Employee with email {} not found".format(email)) + return employee + + def create(self, *, email: str, name: Optional[str] = None, surname: Optional[str]) -> Employee: + new_employee = Employee(email=email, name=name, surname=surname) + self.session.add(new_employee) + self.session.commit() + self.session.refresh(new_employee) + return new_employee + + +def get_employee_repository(session: Session = Depends(get_session)): + return EmployeeRepository(Employee, session) diff --git a/app/domain/repositories/todo.py b/app/domain/repositories/todo.py new file mode 100644 index 0000000..7e69eb2 --- /dev/null +++ b/app/domain/repositories/todo.py @@ -0,0 +1,40 @@ +from typing import Optional, List +from uuid import UUID + +from fastapi import Depends +from sqlalchemy.orm import sessionmaker +from sqlmodel import select, Session + +from app.common.base.base_repository import BaseRepository +from app.common.core.database import get_session +from app.common.exceptions.http import NotFoundException +from app.domain.models.todo import Todo + + +class TodoRepository(BaseRepository[Todo]): + + def create(self, *, description: str) -> Todo: + new_todo = Todo(description=description) + self.session.add(new_todo) + self.session.commit() + self.session.refresh(new_todo) + return new_todo + + def get_pending_todos(self) -> List[Todo]: + todos = self.session.exec(select(Todo).where(Todo.done == False)) + return todos.all() + + def todo_done(self, todo_id: UUID) -> Todo: + query = self.session.exec(select(Todo).where(Todo.id == todo_id)) + todo = query.one_or_none() + if not todo: + raise NotFoundException(detail="TODO not found with ID {}".format(todo_id)) + todo.done = True + self.session.add(todo) + self.session.commit() + self.session.refresh(todo) + return todo + + +def get_todo_repository(session: Session = Depends(get_session)): + return TodoRepository(Todo, session) diff --git a/app/domain/repositories/user.py b/app/domain/repositories/user.py deleted file mode 100644 index 464be28..0000000 --- a/app/domain/repositories/user.py +++ /dev/null @@ -1,41 +0,0 @@ -from datetime import datetime -from typing import Optional - -from fastapi import Depends -from sqlalchemy import select -from sqlalchemy.orm import sessionmaker - -from app.common.base.base_repository import BaseRepository -from app.common.core.database import get_db_session_factory -from app.domain.models import User -from app.domain.models.user import UserCreateRequest - - -class UserRepository(BaseRepository[User]): - - async def get_by_email(self, *, email: str) -> Optional[User]: - async with self.create_session() as db_session: - users = await db_session.execute(select(User).where(User.email == email)) - result = users.first() - return result - - async def create_with_role(self, *, req: UserCreateRequest) -> User: - async with self.create_session() as db_session: - db_obj = User( - first_name=req.first_name, - last_name=req.last_name, - email=req.email, - is_superuser=req.is_superuser, - # hashed_password=get_password_hash(obj_in.password), - created_at=datetime.utcnow(), - updated_at=datetime.utcnow(), - role_id=req.role_id - ) - db_session.add(db_obj) - await db_session.commit() - await db_session.refresh(db_obj) - return db_obj - - -def get_user_repository(session_factory: sessionmaker = Depends(get_db_session_factory)): - return UserRepository(User, session_factory) From bf5c1a9c80b4706a34dad845372b579b4d5718c6 Mon Sep 17 00:00:00 2001 From: Esther PlaIbanez Date: Thu, 14 Jul 2022 14:35:30 +0200 Subject: [PATCH 18/19] Azure function template. --- .funcignore | 5 ++++ .gitignore | 26 +++++++++---------- .vscode/extensions.json | 6 +++++ .vscode/launch.json | 12 +++++++++ .vscode/settings.json | 8 ++++++ .vscode/tasks.json | 26 +++++++++++++++++++ __init__.py | 0 .../controllers/__init__.py | 4 +-- .../controllers/{user.py => employee.py} | 6 ++--- .../employee_management/services/employee.py | 17 ++++++++++++ .../employee_management/services/user.py | 17 ------------ .../todo_management/controllers/todo.py | 2 +- app/common/core/configuration.py | 6 ++--- azure/__init__.py | 7 +++++ azure/function.json | 23 ++++++++++++++++ azure/sample.dat | 3 +++ host.json | 21 +++++++++++++++ requirements.txt | 10 +++++++ 18 files changed, 160 insertions(+), 39 deletions(-) create mode 100644 .funcignore create mode 100644 .vscode/extensions.json create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100644 __init__.py rename app/business/employee_management/controllers/{user.py => employee.py} (53%) create mode 100644 app/business/employee_management/services/employee.py delete mode 100644 app/business/employee_management/services/user.py create mode 100644 azure/__init__.py create mode 100644 azure/function.json create mode 100644 azure/sample.dat create mode 100644 host.json create mode 100644 requirements.txt diff --git a/.funcignore b/.funcignore new file mode 100644 index 0000000..0678ea2 --- /dev/null +++ b/.funcignore @@ -0,0 +1,5 @@ +.git* +.vscode +local.settings.json +test +.venv \ No newline at end of file diff --git a/.gitignore b/.gitignore index aee9c67..7685fc4 100644 --- a/.gitignore +++ b/.gitignore @@ -47,7 +47,6 @@ htmlcov/ nosetests.xml coverage.xml *.cover -*.py,cover .hypothesis/ .pytest_cache/ @@ -59,7 +58,6 @@ coverage.xml *.log local_settings.py db.sqlite3 -db.sqlite3-journal # Flask stuff: instance/ @@ -87,16 +85,12 @@ ipython_config.py # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not +# having no cross-platform support, pipenv may install dependencies that don’t work, or not # install all needed dependencies. #Pipfile.lock -# PEP 582; used by e.g. github.com/David-OConnor/pyflow -__pypackages__/ - -# Celery stuff +# celery beat schedule file celerybeat-schedule -celerybeat.pid # SageMath parsed files *.sage.py @@ -128,8 +122,14 @@ dmypy.json # Pyre type checker .pyre/ -# Log files -*.log - -# DB Files -*.db \ No newline at end of file +# Azure Functions artifacts +bin +obj +appsettings.json +local.settings.json + +# Azurite artifacts +__blobstorage__ +__queuestorage__ +__azurite_db*__.json +.python_packages \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..cbbad0f --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "ms-azuretools.vscode-azurefunctions", + "ms-python.python" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..4508b45 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,12 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Attach to Python Functions", + "type": "python", + "request": "attach", + "port": 9091, + "preLaunchTask": "func: host start" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..1562c57 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "azureFunctions.deploySubpath": ".", + "azureFunctions.scmDoBuildDuringDeployment": true, + "azureFunctions.pythonVenv": ".venv", + "azureFunctions.projectLanguage": "Python", + "azureFunctions.projectRuntime": "~4", + "debug.internalConsoleOptions": "neverOpen" +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..8e2de79 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,26 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "func", + "command": "host start", + "problemMatcher": "$func-python-watch", + "isBackground": true, + "dependsOn": "pip install (functions)" + }, + { + "label": "pip install (functions)", + "type": "shell", + "osx": { + "command": "${config:azureFunctions.pythonVenv}/bin/python -m pip install -r requirements.txt" + }, + "windows": { + "command": "${config:azureFunctions.pythonVenv}\\Scripts\\python -m pip install -r requirements.txt" + }, + "linux": { + "command": "${config:azureFunctions.pythonVenv}/bin/python -m pip install -r requirements.txt" + }, + "problemMatcher": [] + } + ] +} \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/business/employee_management/controllers/__init__.py b/app/business/employee_management/controllers/__init__.py index 1f1fc49..03d5379 100644 --- a/app/business/employee_management/controllers/__init__.py +++ b/app/business/employee_management/controllers/__init__.py @@ -1,7 +1,7 @@ from fastapi import APIRouter -from app.business.employee_management.controllers import user +from app.business.employee_management.controllers import employee # Include all routers here api_router = APIRouter() -api_router.include_router(user.router, tags=["users"]) +api_router.include_router(employee.router, tags=["users"]) diff --git a/app/business/employee_management/controllers/user.py b/app/business/employee_management/controllers/employee.py similarity index 53% rename from app/business/employee_management/controllers/user.py rename to app/business/employee_management/controllers/employee.py index 2f8cfe1..8fc101e 100644 --- a/app/business/employee_management/controllers/user.py +++ b/app/business/employee_management/controllers/employee.py @@ -2,7 +2,7 @@ from fastapi import APIRouter, Depends -from app.business.employee_management.services.user import UserService +from app.business.employee_management.services.employee import EmployeeService router = APIRouter(prefix="/users") @@ -10,9 +10,9 @@ @router.get("/info") -async def info(user_service: UserService = Depends(UserService)): +async def info(employee_service: EmployeeService = Depends(EmployeeService)): logger.info("TEST INFO") logger.error("TEST ERROR") logger.debug("TEST DEBUG") - await user_service.get_user_by_email(email="test@email.es") + await employee_service.get_user_by_email(email="test@email.es") return {} diff --git a/app/business/employee_management/services/employee.py b/app/business/employee_management/services/employee.py new file mode 100644 index 0000000..2453c80 --- /dev/null +++ b/app/business/employee_management/services/employee.py @@ -0,0 +1,17 @@ +from typing import Optional + +from fastapi import Depends + +from app.common.exceptions.http import NotFoundException +from app.domain.models import Employee +from app.domain.repositories.employee import get_employee_repository, EmployeeRepository + + +class EmployeeService: + def __init__(self, repository: EmployeeRepository = Depends(get_employee_repository)): + self.user_repo = repository + + async def get_user_by_email(self, email: str) -> Optional[Employee]: + user = await self.user_repo.get_by_email(email=email) + if user is None: + raise NotFoundException(detail="User not found") diff --git a/app/business/employee_management/services/user.py b/app/business/employee_management/services/user.py deleted file mode 100644 index f1667c2..0000000 --- a/app/business/employee_management/services/user.py +++ /dev/null @@ -1,17 +0,0 @@ -from typing import Optional - -from fastapi import Depends - -from app.common.exceptions.http import NotFoundException -from app.domain.models import User -from app.domain.repositories.user import get_user_repository, UserRepository - - -class UserService: - def __init__(self, repository: UserRepository = Depends(get_user_repository)): - self.user_repo = repository - - async def get_user_by_email(self, email: str) -> Optional[User]: - user = await self.user_repo.get_by_email(email=email) - if user is None: - raise NotFoundException(detail="User not found") diff --git a/app/business/todo_management/controllers/todo.py b/app/business/todo_management/controllers/todo.py index 1aa1224..e847d52 100644 --- a/app/business/todo_management/controllers/todo.py +++ b/app/business/todo_management/controllers/todo.py @@ -18,7 +18,7 @@ def get_pending_todos(todo_service: TodoService = Depends(TodoService)): @router.post("/create", description="Creates a new TODO", response_model=TodoDto) -def create_todo(create_request: CreateTodoRequest, todo_service=Depends(TodoService)): +def create_todo(create_request: CreateTodoRequest, todo_service: TodoService = Depends(TodoService)): logger.info("Creating a new TODO") todo = todo_service.create_todo(create_request) return todo diff --git a/app/common/core/configuration.py b/app/common/core/configuration.py index 38f2ca6..53ddeba 100644 --- a/app/common/core/configuration.py +++ b/app/common/core/configuration.py @@ -1,4 +1,4 @@ -from typing import Optional, Type, List +from typing import Optional, Type, List, Union import yaml from fastapi import FastAPI, APIRouter @@ -19,10 +19,10 @@ class GlobalSettings(BaseSettings): environment: str = "TEST" port: int = 80 swagger_path: str = "/docs" - cors: List[str] | List[AnyHttpUrl] = [] + cors: Union[List[str], List[AnyHttpUrl]] = [] @validator("cors", pre=True) - def assemble_cors_origins(cls, v: str | List[str]) -> List[str] | str: + def assemble_cors_origins(cls, v: Union[str, List[str]]) -> Union[List[str], str]: if v is None: print("CORS Not Specified") return [] diff --git a/azure/__init__.py b/azure/__init__.py new file mode 100644 index 0000000..ce6d550 --- /dev/null +++ b/azure/__init__.py @@ -0,0 +1,7 @@ +import logging + +import azure.functions as func +from app import api as app + +def main(req: func.HttpRequest, context: func.Context) -> func.HttpResponse: + return func.AsgiMiddleware(app).handle(req, context) diff --git a/azure/function.json b/azure/function.json new file mode 100644 index 0000000..d256ba1 --- /dev/null +++ b/azure/function.json @@ -0,0 +1,23 @@ +{ + "scriptFile": "__init__.py", + "bindings": [ + { + "authLevel": "anonymous", + "type": "httpTrigger", + "direction": "in", + "name": "req", + "methods": [ + "get", + "post", + "put", + "delete" + ], + "route": "{*route}" + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ] +} diff --git a/azure/sample.dat b/azure/sample.dat new file mode 100644 index 0000000..26aac46 --- /dev/null +++ b/azure/sample.dat @@ -0,0 +1,3 @@ +{ + "name": "Azure" +} \ No newline at end of file diff --git a/host.json b/host.json new file mode 100644 index 0000000..357472c --- /dev/null +++ b/host.json @@ -0,0 +1,21 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + } + } + }, + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[2.*, 3.0.0)" + } + , + "extensions": { + "http": { + "routePrefix": "" + } + } +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1477a8b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,10 @@ +# DO NOT include azure-functions-worker in this file +# The Python Worker is managed by Azure Functions platform +# Manually managing azure-functions-worker may cause unexpected issues + +azure-functions +fastapi +sqlmodel +fastapi_keycloak +pyyaml +pydantic[dotenv] \ No newline at end of file From 9d459c9a3918c98379f625572c45768d26b906a5 Mon Sep 17 00:00:00 2001 From: Esther PlaIbanez Date: Tue, 19 Jul 2022 13:04:50 +0200 Subject: [PATCH 19/19] Implement Azure Function with fastapi. Change | operator by Union. --- __init__.py | 0 app/common/infra/__init__.py | 3 ++- requirements.txt | 4 +++- 3 files changed, 5 insertions(+), 2 deletions(-) delete mode 100644 __init__.py diff --git a/__init__.py b/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/common/infra/__init__.py b/app/common/infra/__init__.py index a2de92a..7c82495 100644 --- a/app/common/infra/__init__.py +++ b/app/common/infra/__init__.py @@ -1,4 +1,5 @@ from enum import Enum +from typing import Union from fastapi import FastAPI from fastapi_keycloak import FastAPIKeycloak @@ -10,7 +11,7 @@ class IDPType(Enum): KEYCLOAK = 0 -def get_idp(keycloak_settings: KeycloakSettings | None): +def get_idp(keycloak_settings: Union[KeycloakSettings, None]): # Check if configuration is defined to use Keycloak IDP if keycloak_settings is None or keycloak_settings.auth_server is None or keycloak_settings.realm is None: return None, None diff --git a/requirements.txt b/requirements.txt index 1477a8b..9126d3d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,6 @@ fastapi sqlmodel fastapi_keycloak pyyaml -pydantic[dotenv] \ No newline at end of file +pydantic[dotenv] +pydantic[email] +aiosqlite \ No newline at end of file