From ab24d11c8c2bacb58c6dae52ac120721483a9c65 Mon Sep 17 00:00:00 2001 From: Aldrian Harjati Date: Tue, 24 Oct 2023 10:37:58 -0400 Subject: [PATCH 1/2] Update README (#42) Add more details to readme's instructions such as printouts and images Add help section for users to reach out to SBL help Add new section to show how to do local development Co-authored-by: Aldrian Harjati --- README.md | 145 ++++++++++++++++++++++++++++++++++-- images/sbl_project_svcs.png | Bin 0 -> 34899 bytes 2 files changed, 140 insertions(+), 5 deletions(-) create mode 100644 images/sbl_project_svcs.png diff --git a/README.md b/README.md index 2420ffd..307dbc6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ # User and Financial Institutions Management API This app communicates with Keycloak to provide some user management functionality, as well as serving as `Institutions API` to retrieve information about institutions. +--- +## Contact Us +If you have an inquiry or suggestion for the user and financial institutions management API or any SBL related code, please reach out to us at + --- ### Dependencies - [Poetry](https://python-poetry.org/) is used as the package management tool. Once installed, just running `poetry install` in the root of the project should install all the dependencies needed by the app. @@ -8,18 +12,37 @@ This app communicates with Keycloak to provide some user management functionalit - [jq](https://jqlang.github.io/jq/download/) is used for parsing API responses in the curl command examples shown below --- -## Pre-requesites +## Pre-requisites [SBL Project Repo](https://github.com/cfpb/sbl-project) contains the `docker-compose.yml` to run the ancillary services. - Not all services need to run, this module `regtech-user-fi-management` is part of the docker compose file, which doesn't need to be ran in docker for local development. - Issuing `docker compose up -d pg keycloak` would start the necessary services (postgres, and keycloak) +```bash +$ cd ~/Projects/sbl-project +$ docker compose up -d pg keycloak +[+] Running 3/3 + ⠿ Network sbl-project_default Created 0.2s + ⠿ Container sbl-project-pg-1 Started 2.6s + ⠿ Container sbl-project-keycloak-1 Started 13.4s +``` +![Docker](images/sbl_project_svcs.png) --- ## Running the app -Once the [Dependencies](#dependencies), and [Pre-requesites](#pre-requesites) have been satisfied, we can run the app by going into the `src` folder, then issue the poetry run command: +Once the [Dependencies](#dependencies), and [Pre-requisites](#pre-requisites) have been satisfied: +- All dependencies installed +- Postgres and keycloak services started + +we can run the app by going into the `src` folder, then issue the poetry run command: ```bash -cd src -poetry run uvicorn main:app --reload --port 8888 +$ poetry run uvicorn main:app --reload --port 8888 +INFO: Will watch for changes in these directories: ['/Projects/regtech-user-fi-management/src'] +INFO: Uvicorn running on http://127.0.0.1:8888 (Press CTRL+C to quit) +INFO: Started reloader process [37993] using StatReload +INFO: Started server process [37997] +INFO: Waiting for application startup. +INFO: Application startup complete. ``` + ### Local development notes - [.env.template](.env.template) is added to allow VS Code to search the correct path for imports when writing tests, just copy the [.env.template](.env.template) file into `.env` file locally - [src/.env.template](./src/.env.template) is added as the template for the app's environment variables, appropriate values are already provided in [.env.local](./src/.env.local) for local development. If `ENV` variable with default of `LOCAL` is changed, copy this template into `src/.env`, and provide appropriate values, and set all listed empty variables in the environment. @@ -41,6 +64,17 @@ curl 'localhost:8880/realms/regtech/protocol/openid-connect/token' \ --data-urlencode 'client_id=regtech-client' | jq -r '.access_token' ``` +For local development, we can retrieve the access token and store it to local variable then use this variable on API calls. +```bash +export RT_ACCESS_TOKEN=$(curl 'localhost:8880/realms/regtech/protocol/openid-connect/token' \ +-X POST \ +-H 'Content-Type: application/x-www-form-urlencoded' \ +--data-urlencode 'username=user1' \ +--data-urlencode 'password=user' \ +--data-urlencode 'grant_type=password' \ +--data-urlencode 'client_id=regtech-client' | jq -r '.access_token') +``` + --- ## Functionalities There are 2 major functionalities provided by this app, one serves as the integration with Keycloak, and the other to integrate with Institutions database to show institutions' information. Below are the routers for these functionalities. As mentioned above, authentication is required to access the endpoints. @@ -110,13 +144,114 @@ There are 2 major functionalities provided by this app, one serves as the integr ``` For both these routers, the needed roles to access each endpoint is decorated with the `@requires` decorator, i.e. `@requires(["query-groups", "manage-users"])`. Refer to [institutions router](./src/routers/institutions.py) for the decorator example; these roles corresponds to Keycloak's roles. +--- +## Example of Local Development Flow +- Install [Poetry](https://python-poetry.org/), [Docker](https://www.docker.com/) and [jq](https://jqlang.github.io/jq/download/) +- Checkout [SBL Project Repo](https://github.com/cfpb/sbl-project) and [SBL User and Financial Institution Management API Repo](https://github.com/cfpb/regtech-user-fi-management) +- Open a terminal, and run keycloak and postqres +```bash +# go to sbl-project repo root directory +cd ~/Projects/sbl-project + +# bring up postgres and keycloak +docker compose up -d pg keycloak +[+] Running 3/3 + ⠿ Network sbl-project_default Created 0.2s + ⠿ Container sbl-project-pg-1 Started 2.6s + ⠿ Container sbl-project-keycloak-1 Started 13.4s +``` + +- Open another terminal, and run keycloak and postqres +```bash +# go to regtech-user-fi-management's src directory +cd ~/Projects/regtech-user-fi-management/src + +# run local api server +poetry run uvicorn main:app --reload --port 8888 + +INFO: Will watch for changes in these directories: ['/Users/harjatia/Projects/regtech-user-fi-management/src'] +INFO: Uvicorn running on http://127.0.0.1:8888 (Press CTRL+C to quit) +INFO: Started reloader process [42182] using StatReload +INFO: Started server process [42186] +INFO: Waiting for application startup. +INFO: Application startup complete. +INFO: 127.0.0.1:51374 - "GET /v1/admin/me HTTP/1.1" 200 OK +``` + +- Using first terminal or new terminal, run and test API Calls +```bash +# Retrieve and store token key +export RT_ACCESS_TOKEN=$(curl 'localhost:8880/realms/regtech/protocol/openid-connect/token' \ + -X POST \ + -H 'Content-Type: application/x-www-form-urlencoded' \ + --data-urlencode 'username=admin1' \ + --data-urlencode 'password=admin' \ + --data-urlencode 'grant_type=password' \ + --data-urlencode 'client_id=regtech-client' | jq -r '.access_token') + +# test token key +curl localhost:8888/v1/admin/me -H "Authorization: Bearer ${RT_ACCESS_TOKEN}" | jq -r '.' + % Total % Received % Xferd Average Speed Time Time Time Current + Dload Upload Total Spent Left Speed +100 801 100 801 0 0 7545 0 --:--:-- --:--:-- --:--:-- 8010 +{ + "claims": { + "exp": 1697482762, + "iat": 1697482462, + "jti": "3380a7bb-765b-4197-a500-19eb424518ce", + "iss": "http://localhost:8880/realms/regtech", + "aud": [ + "realm-management", + "account" + ], + "sub": "631dbab3-4dcf-46bf-b283-55c18a3722e6", + "typ": "Bearer", + "azp": "regtech-client", + "session_state": "c8d4b4c8-3d3f-41b3-aebc-b4cab3b314eb", + "allowed-origins": [ + "*" + ], + "realm_access": { + "roles": [ + "offline_access", + "uma_authorization", + "default-roles-regtech" + ] + }, + "resource_access": { + "realm-management": { + "roles": [ + "manage-users", + "query-groups" + ] + }, + "account": { + "roles": [ + "manage-account" + ] + } + }, + "scope": "email profile", + "sid": "c8d4b4c8-3d3f-41b3-aebc-b4cab3b314eb", + "email_verified": false, + "preferred_username": "admin1", + "given_name": "", + "family_name": "" + }, + "name": "", + "username": "admin1", + "email": "", + "id": "631dbab3-4dcf-46bf-b283-55c18a3722e6", + "institutions": [] +} +``` + --- ## API Documentation This module uses the [FastAPI](https://fastapi.tiangolo.com/) framework, which affords us built-in [Swagger UI](https://swagger.io/tools/swagger-ui/), this can be accessed by going to `http://localhost:8888/docs` - _Note_: The `Try It Out` feature does not work within the Swagger UI due to the use of `AuthenticationMiddleware` --- - ## Open source licensing info 1. [TERMS](TERMS.md) diff --git a/images/sbl_project_svcs.png b/images/sbl_project_svcs.png new file mode 100644 index 0000000000000000000000000000000000000000..fa969d4e1ff0fb158a9a6b607b7f187d1990bbb6 GIT binary patch literal 34899 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 Date: Thu, 26 Oct 2023 14:21:52 -0400 Subject: [PATCH 2/2] Feature/25 add jwt config (#35) closes #25 --------- Co-authored-by: Hans Keeler --- poetry.lock | 297 +++++++++++++++++++++++++++------- pyproject.toml | 16 +- src/.env.local | 5 +- src/config.py | 61 +++++++ src/entities/engine/engine.py | 8 +- src/entities/models/dto.py | 10 +- src/env.py | 9 -- src/main.py | 8 +- src/oauth2/oauth2_admin.py | 23 ++- tests/app/test_config.py | 23 +++ 10 files changed, 364 insertions(+), 96 deletions(-) create mode 100644 src/config.py delete mode 100644 src/env.py create mode 100644 tests/app/test_config.py diff --git a/poetry.lock b/poetry.lock index 06a709c..a6d063b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,9 +1,10 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. [[package]] name = "aiosqlite" version = "0.19.0" description = "asyncio bridge to the standard sqlite3 module" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -19,6 +20,7 @@ docs = ["sphinx (==6.1.3)", "sphinx-mdinclude (==0.5.3)"] name = "alembic" version = "1.12.0" description = "A database migration tool for SQLAlchemy." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -34,15 +36,28 @@ typing-extensions = ">=4" [package.extras] tz = ["python-dateutil"] +[[package]] +name = "annotated-types" +version = "0.5.0" +description = "Reusable constraint types to use with typing.Annotated" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "annotated_types-0.5.0-py3-none-any.whl", hash = "sha256:58da39888f92c276ad970249761ebea80ba544b77acddaa1a4d6cf78287d45fd"}, + {file = "annotated_types-0.5.0.tar.gz", hash = "sha256:47cdc3490d9ac1506ce92c7aaa76c579dc3509ff11e098fc867e5130ab7be802"}, +] + [[package]] name = "anyio" -version = "3.7.0" +version = "3.7.1" description = "High level compatibility layer for multiple asynchronous event loop implementations" +category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "anyio-3.7.0-py3-none-any.whl", hash = "sha256:eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0"}, - {file = "anyio-3.7.0.tar.gz", hash = "sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce"}, + {file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"}, + {file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"}, ] [package.dependencies] @@ -50,7 +65,7 @@ idna = ">=2.8" sniffio = ">=1.1" [package.extras] -doc = ["Sphinx (>=6.1.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme", "sphinxcontrib-jquery"] +doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-jquery"] test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] trio = ["trio (<0.22)"] @@ -58,6 +73,7 @@ trio = ["trio (<0.22)"] name = "asyncpg" version = "0.27.0" description = "An asyncio PostgreSQL driver" +category = "main" optional = false python-versions = ">=3.7.0" files = [ @@ -108,6 +124,7 @@ test = ["flake8 (>=5.0.4,<5.1.0)", "uvloop (>=0.15.3)"] name = "black" version = "23.7.0" description = "The uncompromising code formatter." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -152,6 +169,7 @@ uvloop = ["uvloop (>=0.15.2)"] name = "certifi" version = "2023.5.7" description = "Python package for providing Mozilla's CA Bundle." +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -163,6 +181,7 @@ files = [ name = "charset-normalizer" version = "3.1.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" optional = false python-versions = ">=3.7.0" files = [ @@ -247,6 +266,7 @@ files = [ name = "click" version = "8.1.3" description = "Composable command line interface toolkit" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -261,6 +281,7 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." +category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -272,6 +293,7 @@ files = [ name = "coverage" version = "7.2.7" description = "Code coverage measurement for Python" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -344,6 +366,7 @@ toml = ["tomli"] name = "deprecation" version = "2.1.0" description = "A library to handle automated deprecations" +category = "main" optional = false python-versions = "*" files = [ @@ -358,6 +381,7 @@ packaging = "*" name = "ecdsa" version = "0.18.0" description = "ECDSA cryptographic signature library (pure python)" +category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -374,26 +398,30 @@ gmpy2 = ["gmpy2"] [[package]] name = "fastapi" -version = "0.97.0" +version = "0.103.1" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "fastapi-0.97.0-py3-none-any.whl", hash = "sha256:95d757511c596409930bd20673358d4a4d709004edb85c5d24d6ffc48fabcbf2"}, - {file = "fastapi-0.97.0.tar.gz", hash = "sha256:b53248ee45f64f19bb7600953696e3edf94b0f7de94df1e5433fc5c6136fa986"}, + {file = "fastapi-0.103.1-py3-none-any.whl", hash = "sha256:5e5f17e826dbd9e9b5a5145976c5cd90bcaa61f2bf9a69aca423f2bcebe44d83"}, + {file = "fastapi-0.103.1.tar.gz", hash = "sha256:345844e6a82062f06a096684196aaf96c1198b25c06b72c1311b882aa2d8a35d"}, ] [package.dependencies] -pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0" +anyio = ">=3.7.1,<4.0.0" +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" starlette = ">=0.27.0,<0.28.0" +typing-extensions = ">=4.5.0" [package.extras] -all = ["email-validator (>=1.1.1)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] [[package]] name = "greenlet" version = "2.0.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.*" files = [ @@ -402,7 +430,6 @@ files = [ {file = "greenlet-2.0.2-cp27-cp27m-win32.whl", hash = "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74"}, {file = "greenlet-2.0.2-cp27-cp27m-win_amd64.whl", hash = "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343"}, {file = "greenlet-2.0.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae"}, - {file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d967650d3f56af314b72df7089d96cda1083a7fc2da05b375d2bc48c82ab3f3c"}, {file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df"}, {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088"}, {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb"}, @@ -411,7 +438,6 @@ files = [ {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91"}, {file = "greenlet-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645"}, {file = "greenlet-2.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c"}, - {file = "greenlet-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d4606a527e30548153be1a9f155f4e283d109ffba663a15856089fb55f933e47"}, {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca"}, {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0"}, {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2"}, @@ -441,7 +467,6 @@ files = [ {file = "greenlet-2.0.2-cp37-cp37m-win32.whl", hash = "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7"}, {file = "greenlet-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3"}, {file = "greenlet-2.0.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30"}, - {file = "greenlet-2.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1087300cf9700bbf455b1b97e24db18f2f77b55302a68272c56209d5587c12d1"}, {file = "greenlet-2.0.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b"}, {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526"}, {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b"}, @@ -450,7 +475,6 @@ files = [ {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a"}, {file = "greenlet-2.0.2-cp38-cp38-win32.whl", hash = "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249"}, {file = "greenlet-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40"}, - {file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8512a0c38cfd4e66a858ddd1b17705587900dd760c6003998e9472b77b56d417"}, {file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8"}, {file = "greenlet-2.0.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6"}, {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df"}, @@ -471,6 +495,7 @@ test = ["objgraph", "psutil"] name = "h11" version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -482,6 +507,7 @@ files = [ name = "httpcore" version = "0.17.3" description = "A minimal low-level HTTP client." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -493,16 +519,17 @@ files = [ anyio = ">=3.0,<5.0" certifi = "*" h11 = ">=0.13,<0.15" -sniffio = "==1.*" +sniffio = ">=1.0.0,<2.0.0" [package.extras] http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] [[package]] name = "httpx" version = "0.24.1" description = "The next generation HTTP client." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -518,14 +545,15 @@ sniffio = "*" [package.extras] brotli = ["brotli", "brotlicffi"] -cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] [[package]] name = "idna" version = "3.4" description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -537,6 +565,7 @@ files = [ name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -548,6 +577,7 @@ files = [ name = "mako" version = "1.2.4" description = "A super-fast templating language that borrows the best ideas from the existing templating languages." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -567,6 +597,7 @@ testing = ["pytest"] name = "markupsafe" version = "2.1.3" description = "Safely add untrusted strings to HTML/XML markup." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -636,6 +667,7 @@ files = [ name = "mypy-extensions" version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." +category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -647,6 +679,7 @@ files = [ name = "packaging" version = "23.1" description = "Core utilities for Python packages" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -658,6 +691,7 @@ files = [ name = "pathspec" version = "0.11.1" description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -669,6 +703,7 @@ files = [ name = "platformdirs" version = "3.9.1" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -684,6 +719,7 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest- name = "pluggy" version = "1.2.0" description = "plugin and hook calling mechanisms for python" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -699,6 +735,7 @@ testing = ["pytest", "pytest-benchmark"] name = "psycopg2-binary" version = "2.9.6" description = "psycopg2 - Python-PostgreSQL Database Adapter" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -770,6 +807,7 @@ files = [ name = "pyasn1" version = "0.5.0" description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" +category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ @@ -779,60 +817,164 @@ files = [ [[package]] name = "pydantic" -version = "1.10.9" -description = "Data validation and settings management using python type hints" +version = "2.4.2" +description = "Data validation using Python type hints" +category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "pydantic-1.10.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e692dec4a40bfb40ca530e07805b1208c1de071a18d26af4a2a0d79015b352ca"}, - {file = "pydantic-1.10.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3c52eb595db83e189419bf337b59154bdcca642ee4b2a09e5d7797e41ace783f"}, - {file = "pydantic-1.10.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:939328fd539b8d0edf244327398a667b6b140afd3bf7e347cf9813c736211896"}, - {file = "pydantic-1.10.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b48d3d634bca23b172f47f2335c617d3fcb4b3ba18481c96b7943a4c634f5c8d"}, - {file = "pydantic-1.10.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f0b7628fb8efe60fe66fd4adadd7ad2304014770cdc1f4934db41fe46cc8825f"}, - {file = "pydantic-1.10.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e1aa5c2410769ca28aa9a7841b80d9d9a1c5f223928ca8bec7e7c9a34d26b1d4"}, - {file = "pydantic-1.10.9-cp310-cp310-win_amd64.whl", hash = "sha256:eec39224b2b2e861259d6f3c8b6290d4e0fbdce147adb797484a42278a1a486f"}, - {file = "pydantic-1.10.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d111a21bbbfd85c17248130deac02bbd9b5e20b303338e0dbe0faa78330e37e0"}, - {file = "pydantic-1.10.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2e9aec8627a1a6823fc62fb96480abe3eb10168fd0d859ee3d3b395105ae19a7"}, - {file = "pydantic-1.10.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07293ab08e7b4d3c9d7de4949a0ea571f11e4557d19ea24dd3ae0c524c0c334d"}, - {file = "pydantic-1.10.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ee829b86ce984261d99ff2fd6e88f2230068d96c2a582f29583ed602ef3fc2c"}, - {file = "pydantic-1.10.9-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4b466a23009ff5cdd7076eb56aca537c745ca491293cc38e72bf1e0e00de5b91"}, - {file = "pydantic-1.10.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7847ca62e581e6088d9000f3c497267868ca2fa89432714e21a4fb33a04d52e8"}, - {file = "pydantic-1.10.9-cp311-cp311-win_amd64.whl", hash = "sha256:7845b31959468bc5b78d7b95ec52fe5be32b55d0d09983a877cca6aedc51068f"}, - {file = "pydantic-1.10.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:517a681919bf880ce1dac7e5bc0c3af1e58ba118fd774da2ffcd93c5f96eaece"}, - {file = "pydantic-1.10.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67195274fd27780f15c4c372f4ba9a5c02dad6d50647b917b6a92bf00b3d301a"}, - {file = "pydantic-1.10.9-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2196c06484da2b3fded1ab6dbe182bdabeb09f6318b7fdc412609ee2b564c49a"}, - {file = "pydantic-1.10.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:6257bb45ad78abacda13f15bde5886efd6bf549dd71085e64b8dcf9919c38b60"}, - {file = "pydantic-1.10.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3283b574b01e8dbc982080d8287c968489d25329a463b29a90d4157de4f2baaf"}, - {file = "pydantic-1.10.9-cp37-cp37m-win_amd64.whl", hash = "sha256:5f8bbaf4013b9a50e8100333cc4e3fa2f81214033e05ac5aa44fa24a98670a29"}, - {file = "pydantic-1.10.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b9cd67fb763248cbe38f0593cd8611bfe4b8ad82acb3bdf2b0898c23415a1f82"}, - {file = "pydantic-1.10.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f50e1764ce9353be67267e7fd0da08349397c7db17a562ad036aa7c8f4adfdb6"}, - {file = "pydantic-1.10.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73ef93e5e1d3c8e83f1ff2e7fdd026d9e063c7e089394869a6e2985696693766"}, - {file = "pydantic-1.10.9-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:128d9453d92e6e81e881dd7e2484e08d8b164da5507f62d06ceecf84bf2e21d3"}, - {file = "pydantic-1.10.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ad428e92ab68798d9326bb3e5515bc927444a3d71a93b4a2ca02a8a5d795c572"}, - {file = "pydantic-1.10.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fab81a92f42d6d525dd47ced310b0c3e10c416bbfae5d59523e63ea22f82b31e"}, - {file = "pydantic-1.10.9-cp38-cp38-win_amd64.whl", hash = "sha256:963671eda0b6ba6926d8fc759e3e10335e1dc1b71ff2a43ed2efd6996634dafb"}, - {file = "pydantic-1.10.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:970b1bdc6243ef663ba5c7e36ac9ab1f2bfecb8ad297c9824b542d41a750b298"}, - {file = "pydantic-1.10.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7e1d5290044f620f80cf1c969c542a5468f3656de47b41aa78100c5baa2b8276"}, - {file = "pydantic-1.10.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83fcff3c7df7adff880622a98022626f4f6dbce6639a88a15a3ce0f96466cb60"}, - {file = "pydantic-1.10.9-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0da48717dc9495d3a8f215e0d012599db6b8092db02acac5e0d58a65248ec5bc"}, - {file = "pydantic-1.10.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0a2aabdc73c2a5960e87c3ffebca6ccde88665616d1fd6d3db3178ef427b267a"}, - {file = "pydantic-1.10.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9863b9420d99dfa9c064042304868e8ba08e89081428a1c471858aa2af6f57c4"}, - {file = "pydantic-1.10.9-cp39-cp39-win_amd64.whl", hash = "sha256:e7c9900b43ac14110efa977be3da28931ffc74c27e96ee89fbcaaf0b0fe338e1"}, - {file = "pydantic-1.10.9-py3-none-any.whl", hash = "sha256:6cafde02f6699ce4ff643417d1a9223716ec25e228ddc3b436fe7e2d25a1f305"}, - {file = "pydantic-1.10.9.tar.gz", hash = "sha256:95c70da2cd3b6ddf3b9645ecaa8d98f3d80c606624b6d245558d202cd23ea3be"}, + {file = "pydantic-2.4.2-py3-none-any.whl", hash = "sha256:bc3ddf669d234f4220e6e1c4d96b061abe0998185a8d7855c0126782b7abc8c1"}, + {file = "pydantic-2.4.2.tar.gz", hash = "sha256:94f336138093a5d7f426aac732dcfe7ab4eb4da243c88f891d65deb4a2556ee7"}, ] [package.dependencies] -typing-extensions = ">=4.2.0" +annotated-types = ">=0.4.0" +pydantic-core = "2.10.1" +typing-extensions = ">=4.6.1" [package.extras] -dotenv = ["python-dotenv (>=0.10.4)"] -email = ["email-validator (>=1.0.3)"] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.10.1" +description = "" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic_core-2.10.1-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:d64728ee14e667ba27c66314b7d880b8eeb050e58ffc5fec3b7a109f8cddbd63"}, + {file = "pydantic_core-2.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:48525933fea744a3e7464c19bfede85df4aba79ce90c60b94d8b6e1eddd67096"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef337945bbd76cce390d1b2496ccf9f90b1c1242a3a7bc242ca4a9fc5993427a"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1392e0638af203cee360495fd2cfdd6054711f2db5175b6e9c3c461b76f5175"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0675ba5d22de54d07bccde38997e780044dcfa9a71aac9fd7d4d7a1d2e3e65f7"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:128552af70a64660f21cb0eb4876cbdadf1a1f9d5de820fed6421fa8de07c893"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f6e6aed5818c264412ac0598b581a002a9f050cb2637a84979859e70197aa9e"}, + {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ecaac27da855b8d73f92123e5f03612b04c5632fd0a476e469dfc47cd37d6b2e"}, + {file = "pydantic_core-2.10.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b3c01c2fb081fced3bbb3da78510693dc7121bb893a1f0f5f4b48013201f362e"}, + {file = "pydantic_core-2.10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:92f675fefa977625105708492850bcbc1182bfc3e997f8eecb866d1927c98ae6"}, + {file = "pydantic_core-2.10.1-cp310-none-win32.whl", hash = "sha256:420a692b547736a8d8703c39ea935ab5d8f0d2573f8f123b0a294e49a73f214b"}, + {file = "pydantic_core-2.10.1-cp310-none-win_amd64.whl", hash = "sha256:0880e239827b4b5b3e2ce05e6b766a7414e5f5aedc4523be6b68cfbc7f61c5d0"}, + {file = "pydantic_core-2.10.1-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:073d4a470b195d2b2245d0343569aac7e979d3a0dcce6c7d2af6d8a920ad0bea"}, + {file = "pydantic_core-2.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:600d04a7b342363058b9190d4e929a8e2e715c5682a70cc37d5ded1e0dd370b4"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39215d809470f4c8d1881758575b2abfb80174a9e8daf8f33b1d4379357e417c"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eeb3d3d6b399ffe55f9a04e09e635554012f1980696d6b0aca3e6cf42a17a03b"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7a7902bf75779bc12ccfc508bfb7a4c47063f748ea3de87135d433a4cca7a2f"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3625578b6010c65964d177626fde80cf60d7f2e297d56b925cb5cdeda6e9925a"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:caa48fc31fc7243e50188197b5f0c4228956f97b954f76da157aae7f67269ae8"}, + {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:07ec6d7d929ae9c68f716195ce15e745b3e8fa122fc67698ac6498d802ed0fa4"}, + {file = "pydantic_core-2.10.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e6f31a17acede6a8cd1ae2d123ce04d8cca74056c9d456075f4f6f85de055607"}, + {file = "pydantic_core-2.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d8f1ebca515a03e5654f88411420fea6380fc841d1bea08effb28184e3d4899f"}, + {file = "pydantic_core-2.10.1-cp311-none-win32.whl", hash = "sha256:6db2eb9654a85ada248afa5a6db5ff1cf0f7b16043a6b070adc4a5be68c716d6"}, + {file = "pydantic_core-2.10.1-cp311-none-win_amd64.whl", hash = "sha256:4a5be350f922430997f240d25f8219f93b0c81e15f7b30b868b2fddfc2d05f27"}, + {file = "pydantic_core-2.10.1-cp311-none-win_arm64.whl", hash = "sha256:5fdb39f67c779b183b0c853cd6b45f7db84b84e0571b3ef1c89cdb1dfc367325"}, + {file = "pydantic_core-2.10.1-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:b1f22a9ab44de5f082216270552aa54259db20189e68fc12484873d926426921"}, + {file = "pydantic_core-2.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8572cadbf4cfa95fb4187775b5ade2eaa93511f07947b38f4cd67cf10783b118"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db9a28c063c7c00844ae42a80203eb6d2d6bbb97070cfa00194dff40e6f545ab"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e2a35baa428181cb2270a15864ec6286822d3576f2ed0f4cd7f0c1708472aff"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05560ab976012bf40f25d5225a58bfa649bb897b87192a36c6fef1ab132540d7"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d6495008733c7521a89422d7a68efa0a0122c99a5861f06020ef5b1f51f9ba7c"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14ac492c686defc8e6133e3a2d9eaf5261b3df26b8ae97450c1647286750b901"}, + {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8282bab177a9a3081fd3d0a0175a07a1e2bfb7fcbbd949519ea0980f8a07144d"}, + {file = "pydantic_core-2.10.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:aafdb89fdeb5fe165043896817eccd6434aee124d5ee9b354f92cd574ba5e78f"}, + {file = "pydantic_core-2.10.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f6defd966ca3b187ec6c366604e9296f585021d922e666b99c47e78738b5666c"}, + {file = "pydantic_core-2.10.1-cp312-none-win32.whl", hash = "sha256:7c4d1894fe112b0864c1fa75dffa045720a194b227bed12f4be7f6045b25209f"}, + {file = "pydantic_core-2.10.1-cp312-none-win_amd64.whl", hash = "sha256:5994985da903d0b8a08e4935c46ed8daf5be1cf217489e673910951dc533d430"}, + {file = "pydantic_core-2.10.1-cp312-none-win_arm64.whl", hash = "sha256:0d8a8adef23d86d8eceed3e32e9cca8879c7481c183f84ed1a8edc7df073af94"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:9badf8d45171d92387410b04639d73811b785b5161ecadabf056ea14d62d4ede"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:ebedb45b9feb7258fac0a268a3f6bec0a2ea4d9558f3d6f813f02ff3a6dc6698"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfe1090245c078720d250d19cb05d67e21a9cd7c257698ef139bc41cf6c27b4f"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e357571bb0efd65fd55f18db0a2fb0ed89d0bb1d41d906b138f088933ae618bb"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b3dcd587b69bbf54fc04ca157c2323b8911033e827fffaecf0cafa5a892a0904"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c120c9ce3b163b985a3b966bb701114beb1da4b0468b9b236fc754783d85aa3"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15d6bca84ffc966cc9976b09a18cf9543ed4d4ecbd97e7086f9ce9327ea48891"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5cabb9710f09d5d2e9e2748c3e3e20d991a4c5f96ed8f1132518f54ab2967221"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:82f55187a5bebae7d81d35b1e9aaea5e169d44819789837cdd4720d768c55d15"}, + {file = "pydantic_core-2.10.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:1d40f55222b233e98e3921df7811c27567f0e1a4411b93d4c5c0f4ce131bc42f"}, + {file = "pydantic_core-2.10.1-cp37-none-win32.whl", hash = "sha256:14e09ff0b8fe6e46b93d36a878f6e4a3a98ba5303c76bb8e716f4878a3bee92c"}, + {file = "pydantic_core-2.10.1-cp37-none-win_amd64.whl", hash = "sha256:1396e81b83516b9d5c9e26a924fa69164156c148c717131f54f586485ac3c15e"}, + {file = "pydantic_core-2.10.1-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:6835451b57c1b467b95ffb03a38bb75b52fb4dc2762bb1d9dbed8de31ea7d0fc"}, + {file = "pydantic_core-2.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b00bc4619f60c853556b35f83731bd817f989cba3e97dc792bb8c97941b8053a"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fa467fd300a6f046bdb248d40cd015b21b7576c168a6bb20aa22e595c8ffcdd"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d99277877daf2efe074eae6338453a4ed54a2d93fb4678ddfe1209a0c93a2468"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa7db7558607afeccb33c0e4bf1c9a9a835e26599e76af6fe2fcea45904083a6"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aad7bd686363d1ce4ee930ad39f14e1673248373f4a9d74d2b9554f06199fb58"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:443fed67d33aa85357464f297e3d26e570267d1af6fef1c21ca50921d2976302"}, + {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:042462d8d6ba707fd3ce9649e7bf268633a41018d6a998fb5fbacb7e928a183e"}, + {file = "pydantic_core-2.10.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ecdbde46235f3d560b18be0cb706c8e8ad1b965e5c13bbba7450c86064e96561"}, + {file = "pydantic_core-2.10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ed550ed05540c03f0e69e6d74ad58d026de61b9eaebebbaaf8873e585cbb18de"}, + {file = "pydantic_core-2.10.1-cp38-none-win32.whl", hash = "sha256:8cdbbd92154db2fec4ec973d45c565e767ddc20aa6dbaf50142676484cbff8ee"}, + {file = "pydantic_core-2.10.1-cp38-none-win_amd64.whl", hash = "sha256:9f6f3e2598604956480f6c8aa24a3384dbf6509fe995d97f6ca6103bb8c2534e"}, + {file = "pydantic_core-2.10.1-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:655f8f4c8d6a5963c9a0687793da37b9b681d9ad06f29438a3b2326d4e6b7970"}, + {file = "pydantic_core-2.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e570ffeb2170e116a5b17e83f19911020ac79d19c96f320cbfa1fa96b470185b"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64322bfa13e44c6c30c518729ef08fda6026b96d5c0be724b3c4ae4da939f875"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:485a91abe3a07c3a8d1e082ba29254eea3e2bb13cbbd4351ea4e5a21912cc9b0"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7c2b8eb9fc872e68b46eeaf835e86bccc3a58ba57d0eedc109cbb14177be531"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5cb87bdc2e5f620693148b5f8f842d293cae46c5f15a1b1bf7ceeed324a740c"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25bd966103890ccfa028841a8f30cebcf5875eeac8c4bde4fe221364c92f0c9a"}, + {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f323306d0556351735b54acbf82904fe30a27b6a7147153cbe6e19aaaa2aa429"}, + {file = "pydantic_core-2.10.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0c27f38dc4fbf07b358b2bc90edf35e82d1703e22ff2efa4af4ad5de1b3833e7"}, + {file = "pydantic_core-2.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f1365e032a477c1430cfe0cf2856679529a2331426f8081172c4a74186f1d595"}, + {file = "pydantic_core-2.10.1-cp39-none-win32.whl", hash = "sha256:a1c311fd06ab3b10805abb72109f01a134019739bd3286b8ae1bc2fc4e50c07a"}, + {file = "pydantic_core-2.10.1-cp39-none-win_amd64.whl", hash = "sha256:ae8a8843b11dc0b03b57b52793e391f0122e740de3df1474814c700d2622950a"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:d43002441932f9a9ea5d6f9efaa2e21458221a3a4b417a14027a1d530201ef1b"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fcb83175cc4936a5425dde3356f079ae03c0802bbdf8ff82c035f8a54b333521"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:962ed72424bf1f72334e2f1e61b68f16c0e596f024ca7ac5daf229f7c26e4208"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2cf5bb4dd67f20f3bbc1209ef572a259027c49e5ff694fa56bed62959b41e1f9"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e544246b859f17373bed915182ab841b80849ed9cf23f1f07b73b7c58baee5fb"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c0877239307b7e69d025b73774e88e86ce82f6ba6adf98f41069d5b0b78bd1bf"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:53df009d1e1ba40f696f8995683e067e3967101d4bb4ea6f667931b7d4a01357"}, + {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a1254357f7e4c82e77c348dabf2d55f1d14d19d91ff025004775e70a6ef40ada"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:524ff0ca3baea164d6d93a32c58ac79eca9f6cf713586fdc0adb66a8cdeab96a"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f0ac9fb8608dbc6eaf17956bf623c9119b4db7dbb511650910a82e261e6600f"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:320f14bd4542a04ab23747ff2c8a778bde727158b606e2661349557f0770711e"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:63974d168b6233b4ed6a0046296803cb13c56637a7b8106564ab575926572a55"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:417243bf599ba1f1fef2bb8c543ceb918676954734e2dcb82bf162ae9d7bd514"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:dda81e5ec82485155a19d9624cfcca9be88a405e2857354e5b089c2a982144b2"}, + {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:14cfbb00959259e15d684505263d5a21732b31248a5dd4941f73a3be233865b9"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:631cb7415225954fdcc2a024119101946793e5923f6c4d73a5914d27eb3d3a05"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:bec7dd208a4182e99c5b6c501ce0b1f49de2802448d4056091f8e630b28e9a52"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:149b8a07712f45b332faee1a2258d8ef1fb4a36f88c0c17cb687f205c5dc6e7d"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d966c47f9dd73c2d32a809d2be529112d509321c5310ebf54076812e6ecd884"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7eb037106f5c6b3b0b864ad226b0b7ab58157124161d48e4b30c4a43fef8bc4b"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:154ea7c52e32dce13065dbb20a4a6f0cc012b4f667ac90d648d36b12007fa9f7"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e562617a45b5a9da5be4abe72b971d4f00bf8555eb29bb91ec2ef2be348cd132"}, + {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:f23b55eb5464468f9e0e9a9935ce3ed2a870608d5f534025cd5536bca25b1402"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:e9121b4009339b0f751955baf4543a0bfd6bc3f8188f8056b1a25a2d45099934"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:0523aeb76e03f753b58be33b26540880bac5aa54422e4462404c432230543f33"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e0e2959ef5d5b8dc9ef21e1a305a21a36e254e6a34432d00c72a92fdc5ecda5"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da01bec0a26befab4898ed83b362993c844b9a607a86add78604186297eb047e"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f2e9072d71c1f6cfc79a36d4484c82823c560e6f5599c43c1ca6b5cdbd54f881"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f36a3489d9e28fe4b67be9992a23029c3cec0babc3bd9afb39f49844a8c721c5"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f64f82cc3443149292b32387086d02a6c7fb39b8781563e0ca7b8d7d9cf72bd7"}, + {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b4a6db486ac8e99ae696e09efc8b2b9fea67b63c8f88ba7a1a16c24a057a0776"}, + {file = "pydantic_core-2.10.1.tar.gz", hash = "sha256:0f8682dbdd2f67f8e1edddcbffcc29f60a6182b4901c367fc8c1c40d30bb0a82"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pydantic-settings" +version = "2.0.3" +description = "Settings management using Pydantic" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic_settings-2.0.3-py3-none-any.whl", hash = "sha256:ddd907b066622bd67603b75e2ff791875540dc485b7307c4fffc015719da8625"}, + {file = "pydantic_settings-2.0.3.tar.gz", hash = "sha256:962dc3672495aad6ae96a4390fac7e593591e144625e5112d359f8f67fb75945"}, +] + +[package.dependencies] +pydantic = ">=2.0.1" +python-dotenv = ">=0.21.0" [[package]] name = "pytest" version = "7.4.0" description = "pytest: simple powerful testing with Python" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -853,6 +995,7 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no name = "pytest-asyncio" version = "0.21.1" description = "Pytest support for asyncio" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -871,6 +1014,7 @@ testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy name = "pytest-cov" version = "4.1.0" description = "Pytest plugin for measuring coverage." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -885,10 +1029,29 @@ pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] +[[package]] +name = "pytest-env" +version = "1.0.1" +description = "py.test plugin that allows you to add environment variables." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest_env-1.0.1-py3-none-any.whl", hash = "sha256:e8faf927c6fcdbbc8fe3317506acc116713c9708d01652a0fd945f9ae27b71aa"}, + {file = "pytest_env-1.0.1.tar.gz", hash = "sha256:603fe216e8e03a5d134989cb41317c59aabef013d2250c71b864ab0798fbe6f6"}, +] + +[package.dependencies] +pytest = ">=7.3.1" + +[package.extras] +test = ["coverage (>=7.2.7)", "pytest-mock (>=3.10)"] + [[package]] name = "pytest-mock" version = "3.11.1" description = "Thin-wrapper around the mock package for easier use with pytest" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -906,6 +1069,7 @@ dev = ["pre-commit", "pytest-asyncio", "tox"] name = "python-dotenv" version = "1.0.0" description = "Read key-value pairs from a .env file and set them as environment variables" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -920,6 +1084,7 @@ cli = ["click (>=5.0)"] name = "python-jose" version = "3.3.0" description = "JOSE implementation in Python" +category = "main" optional = false python-versions = "*" files = [ @@ -941,6 +1106,7 @@ pycryptodome = ["pyasn1", "pycryptodome (>=3.3.1,<4.0.0)"] name = "python-keycloak" version = "3.0.0" description = "python-keycloak is a Python package providing access to the Keycloak API." +category = "main" optional = false python-versions = ">=3.7,<4.0" files = [ @@ -961,6 +1127,7 @@ docs = ["Sphinx (>=5.3.0,<6.0.0)", "alabaster (>=0.7.12,<0.8.0)", "commonmark (> name = "requests" version = "2.31.0" description = "Python HTTP for Humans." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -982,6 +1149,7 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] name = "requests-toolbelt" version = "1.0.0" description = "A utility belt for advanced users of python-requests" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -996,6 +1164,7 @@ requests = ">=2.0.1,<3.0.0" name = "rsa" version = "4.9" description = "Pure-Python RSA implementation" +category = "main" optional = false python-versions = ">=3.6,<4" files = [ @@ -1010,6 +1179,7 @@ pyasn1 = ">=0.1.3" name = "ruff" version = "0.0.278" description = "An extremely fast Python linter, written in Rust." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1036,6 +1206,7 @@ files = [ 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.*" files = [ @@ -1047,6 +1218,7 @@ files = [ name = "sniffio" version = "1.3.0" description = "Sniff out which async library your code is running under" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1058,6 +1230,7 @@ files = [ name = "sqlalchemy" version = "2.0.16" description = "Database Abstraction Library" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1136,6 +1309,7 @@ sqlcipher = ["sqlcipher3-binary"] name = "starlette" version = "0.27.0" description = "The little ASGI library that shines." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1153,6 +1327,7 @@ full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyam name = "typing-extensions" version = "4.6.3" description = "Backported and Experimental Type Hints for Python 3.7+" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1164,6 +1339,7 @@ files = [ name = "urllib3" version = "2.0.3" description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1181,6 +1357,7 @@ zstd = ["zstandard (>=0.18.0)"] name = "uvicorn" version = "0.22.0" description = "The lightning-fast ASGI server." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1198,4 +1375,4 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "3a2a7103b8ae9e008168d9fc7794ed36c8f0c5186df01e4bf368b5641dd321dd" +content-hash = "25c65267b04339cfa148f1ebb0b06df80256debc560519de916f3e85a64d1835" diff --git a/pyproject.toml b/pyproject.toml index 911e5f4..5340533 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ packages = [{ include = "regtech-user-fi-management" }] [tool.poetry.dependencies] python = "^3.11" -fastapi = "^0.97.0" +fastapi = "^0.103.1" uvicorn = "^0.22.0" python-dotenv = "^1.0.0" python-keycloak = "^3.0.0" @@ -18,6 +18,7 @@ python-jose = "^3.3.0" requests = "^2.31.0" asyncpg = "^0.27.0" alembic = "^1.12.0" +pydantic-settings = "^2.0.3" [tool.poetry.group.dev.dependencies] ruff = "^0.0.278" @@ -28,6 +29,7 @@ pytest-asyncio = "^0.21.1" aiosqlite = "^0.19.0" pytest-cov = "^4.1.0" pytest-mock = "^3.11.1" +pytest-env = "^1.0.1" [tool.pytest.ini_options] asyncio_mode = "auto" @@ -43,6 +45,18 @@ addopts = [ "-rfE", ] testpaths = ["tests"] +env = [ + "INST_CONN=postgresql+asyncpg://localhost", + "KC_URL=http://localhost", + "KC_REALM=", + "KC_ADMIN_CLIENT_ID=", + "KC_ADMIN_CLIENT_SECRET=", + "KC_REALM_URL=http://localhost", + "AUTH_URL=http://localhost", + "TOKEN_URL=http://localhost", + "CERTS_URL=http://localhost", + "AUTH_CLIENT=", +] [tool.black] line-length = 120 diff --git a/src/.env.local b/src/.env.local index ad9cc84..ecc4a82 100644 --- a/src/.env.local +++ b/src/.env.local @@ -13,4 +13,7 @@ INST_DB_USER=fi INST_DB_PWD=fi INST_DB_HOST=localhost:5432 INST_DB_SCHEMA=public -INST_CONN=postgresql+asyncpg://${INST_DB_USER}:${INST_DB_PWD}@${INST_DB_HOST}/${INST_DB_NAME} \ No newline at end of file +INST_CONN=postgresql+asyncpg://${INST_DB_USER}:${INST_DB_PWD}@${INST_DB_HOST}/${INST_DB_NAME} +JWT_OPTS_VERIFY_AT_HASH="false" +JWT_OPTS_VERIFY_AUD="false" +JWT_OPTS_VERIFY_ISS="false" \ No newline at end of file diff --git a/src/config.py b/src/config.py new file mode 100644 index 0000000..d8a3f4e --- /dev/null +++ b/src/config.py @@ -0,0 +1,61 @@ +import os +from typing import Dict, Any + +from pydantic import TypeAdapter +from pydantic.networks import HttpUrl, PostgresDsn +from pydantic.types import SecretStr +from pydantic_settings import BaseSettings, SettingsConfigDict + +JWT_OPTS_PREFIX = "jwt_opts_" + +env_files_to_load = [".env"] +if os.getenv("ENV", "LOCAL") == "LOCAL": + env_files_to_load.append(".env.local") + + +class Settings(BaseSettings): + inst_conn: PostgresDsn + inst_db_schema: str = "public" + auth_client: str + auth_url: HttpUrl + token_url: HttpUrl + certs_url: HttpUrl + kc_url: HttpUrl + kc_realm: str + kc_admin_client_id: str + kc_admin_client_secret: SecretStr + kc_realm_url: HttpUrl + jwt_opts: Dict[str, bool | int] = {} + + def __init__(self, **data): + super().__init__(**data) + self.set_jwt_opts() + + def set_jwt_opts(self) -> None: + """ + Converts `jwt_opts_` prefixed settings, and env vars into JWT options dictionary. + all options are boolean, with exception of 'leeway' being int + valid options can be found here: + https://github.com/mpdavis/python-jose/blob/4b0701b46a8d00988afcc5168c2b3a1fd60d15d8/jose/jwt.py#L81 + + Because we're using model_extra to load in jwt_opts as a dynamic dictionary, + normal env overrides does not take place on top of dotenv files, + so we're merging settings.model_extra with environment variables. + """ + jwt_opts_adapter = TypeAdapter(int | bool) + self.jwt_opts = { + **self.parse_jwt_vars(jwt_opts_adapter, self.model_extra.items()), + **self.parse_jwt_vars(jwt_opts_adapter, os.environ.items()), + } + + def parse_jwt_vars(self, type_adapter: TypeAdapter, setting_variables: Dict[str, Any]) -> Dict[str, bool | int]: + return { + key.lower().replace(JWT_OPTS_PREFIX, ""): type_adapter.validate_python(value) + for (key, value) in setting_variables + if key.lower().startswith(JWT_OPTS_PREFIX) + } + + model_config = SettingsConfigDict(env_file=env_files_to_load, extra="allow") + + +settings = Settings() diff --git a/src/entities/engine/engine.py b/src/entities/engine/engine.py index d37c1b1..9a43682 100644 --- a/src/entities/engine/engine.py +++ b/src/entities/engine/engine.py @@ -1,14 +1,14 @@ -import os from sqlalchemy.ext.asyncio import ( create_async_engine, async_sessionmaker, async_scoped_session, ) from asyncio import current_task +from config import settings -DB_URL = os.getenv("INST_CONN") -DB_SCHEMA = os.getenv("INST_DB_SCHEMA", "public") -engine = create_async_engine(DB_URL, echo=True).execution_options(schema_translate_map={None: DB_SCHEMA}) +engine = create_async_engine(settings.inst_conn.unicode_string(), echo=True).execution_options( + schema_translate_map={None: settings.inst_db_schema} +) SessionLocal = async_scoped_session(async_sessionmaker(engine, expire_on_commit=False), current_task) diff --git a/src/entities/models/dto.py b/src/entities/models/dto.py index d78fd6c..a09062b 100644 --- a/src/entities/models/dto.py +++ b/src/entities/models/dto.py @@ -1,4 +1,4 @@ -from typing import List, Dict, Any, Set, Optional +from typing import List, Dict, Any, Set from pydantic import BaseModel from starlette.authentication import BaseUser @@ -15,7 +15,7 @@ class FinancialInsitutionDomainDto(FinancialInsitutionDomainBase): lei: str class Config: - orm_mode = True + from_attributes = True class FinancialInstitutionBase(BaseModel): @@ -26,7 +26,7 @@ class FinancialInstitutionDto(FinancialInstitutionBase): lei: str class Config: - orm_mode = True + from_attributes = True class FinancialInstitutionWithDomainsDto(FinancialInstitutionDto): @@ -37,13 +37,13 @@ class DeniedDomainDto(BaseModel): domain: str class Config: - orm_mode = True + from_attributes = True class UserProfile(BaseModel): first_name: str last_name: str - leis: Optional[Set[str]] + leis: Set[str] | None = None def to_keycloak_user(self): return {"firstName": self.first_name, "lastName": self.last_name} diff --git a/src/env.py b/src/env.py deleted file mode 100644 index abfb2f9..0000000 --- a/src/env.py +++ /dev/null @@ -1,9 +0,0 @@ -import os -from dotenv import load_dotenv - -ENV = os.getenv("ENV", "LOCAL") - -if ENV == "LOCAL": - load_dotenv(".env.local") -else: - load_dotenv() diff --git a/src/main.py b/src/main.py index 1111860..7940eb8 100644 --- a/src/main.py +++ b/src/main.py @@ -1,6 +1,4 @@ -import os import logging -import env # noqa: F401 from http import HTTPStatus from fastapi import FastAPI, HTTPException, Request from fastapi.responses import JSONResponse @@ -12,6 +10,8 @@ from oauth2 import BearerTokenAuthBackend +from config import settings + log = logging.getLogger() app = FastAPI() @@ -32,7 +32,9 @@ async def general_exception_handler(request: Request, exception: Exception) -> J ) -oauth2_scheme = OAuth2AuthorizationCodeBearer(authorizationUrl=os.getenv("AUTH_URL"), tokenUrl=os.getenv("TOKEN_URL")) +oauth2_scheme = OAuth2AuthorizationCodeBearer( + authorizationUrl=settings.auth_url.unicode_string(), tokenUrl=settings.token_url.unicode_string() +) app.add_middleware(AuthenticationMiddleware, backend=BearerTokenAuthBackend(oauth2_scheme)) app.add_middleware( diff --git a/src/oauth2/oauth2_admin.py b/src/oauth2/oauth2_admin.py index ba71b10..f608fd6 100644 --- a/src/oauth2/oauth2_admin.py +++ b/src/oauth2/oauth2_admin.py @@ -1,6 +1,5 @@ from http import HTTPStatus import logging -import os from typing import Dict, Any, Set import jose.jwt @@ -9,6 +8,8 @@ from keycloak import KeycloakAdmin, KeycloakOpenIDConnection, exceptions as kce +from config import settings + log = logging.getLogger(__name__) @@ -16,10 +17,10 @@ class OAuth2Admin: def __init__(self) -> None: self._keys = None conn = KeycloakOpenIDConnection( - server_url=os.getenv("KC_URL"), - realm_name=os.getenv("KC_REALM"), - client_id=os.getenv("KC_ADMIN_CLIENT_ID"), - client_secret_key=os.getenv("KC_ADMIN_CLIENT_SECRET"), + server_url=settings.kc_url.unicode_string(), + realm_name=settings.kc_realm, + client_id=settings.kc_admin_client_id, + client_secret_key=settings.kc_admin_client_secret.get_secret_value(), ) self._admin = KeycloakAdmin(connection=conn) @@ -28,20 +29,16 @@ def get_claims(self, token: str) -> Dict[str, str] | None: return jose.jwt.decode( token=token, key=self._get_keys(), - issuer=os.getenv("KC_REALM_URL"), - audience=os.getenv("AUTH_CLIENT"), - options={ - "verify_at_hash": False, - "verify_aud": False, - "verify_iss": False, - }, + issuer=settings.kc_realm_url.unicode_string(), + audience=settings.auth_client, + options=settings.jwt_opts, ) except jose.ExpiredSignatureError: pass def _get_keys(self) -> Dict[str, Any]: if self._keys is None: - response = requests.get(os.getenv("CERTS_URL")) + response = requests.get(settings.certs_url) self._keys = response.json() return self._keys diff --git a/tests/app/test_config.py b/tests/app/test_config.py new file mode 100644 index 0000000..9336bc1 --- /dev/null +++ b/tests/app/test_config.py @@ -0,0 +1,23 @@ +import pytest +from config import Settings + + +def test_jwt_opts_valid_values(): + mock_config = { + "jwt_opts_test1": "true", + "jwt_opts_test2": "true", + "jwt_opts_test3": "12", + } + settings = Settings(**mock_config) + assert settings.jwt_opts == {"test1": True, "test2": True, "test3": 12} + + +def test_jwt_opts_invalid_values(): + mock_config = { + "jwt_opts_test1": "not a bool or int", + "jwt_opts_test2": "true", + "jwt_opts_test3": "12", + } + with pytest.raises(Exception) as e: + Settings(**mock_config) + assert "validation error" in str(e.value)