diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 7868451..ad3b018 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -5,6 +5,7 @@ on: branches: - 'fix/**' - 'feat/**' + - 'task/**' jobs: Run-Tests: runs-on: ubuntu-latest @@ -24,4 +25,4 @@ jobs: - name: Install dependencies run: poetry install - name: Run tests - run: poetry run pytest + run: poetry run pytest --capture=no diff --git a/README.md b/README.md index 08e4787..d0d8a14 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # fastapi-boilerplate -Kickstart your [FastAPI](https://fastapi.tiangolo.com/) development with ease by forking this boilerplate repository. +Kickstart your [FastAPI](https://fastapi.tiangolo.com/) development with ease. diff --git a/poetry.lock b/poetry.lock index c34a14f..94b79d8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -31,6 +31,60 @@ doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd- 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)"] +[[package]] +name = "asyncpg" +version = "0.29.0" +description = "An asyncio PostgreSQL driver" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "asyncpg-0.29.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72fd0ef9f00aeed37179c62282a3d14262dbbafb74ec0ba16e1b1864d8a12169"}, + {file = "asyncpg-0.29.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52e8f8f9ff6e21f9b39ca9f8e3e33a5fcdceaf5667a8c5c32bee158e313be385"}, + {file = "asyncpg-0.29.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e6823a7012be8b68301342ba33b4740e5a166f6bbda0aee32bc01638491a22"}, + {file = "asyncpg-0.29.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:746e80d83ad5d5464cfbf94315eb6744222ab00aa4e522b704322fb182b83610"}, + {file = "asyncpg-0.29.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ff8e8109cd6a46ff852a5e6bab8b0a047d7ea42fcb7ca5ae6eaae97d8eacf397"}, + {file = "asyncpg-0.29.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:97eb024685b1d7e72b1972863de527c11ff87960837919dac6e34754768098eb"}, + {file = "asyncpg-0.29.0-cp310-cp310-win32.whl", hash = "sha256:5bbb7f2cafd8d1fa3e65431833de2642f4b2124be61a449fa064e1a08d27e449"}, + {file = "asyncpg-0.29.0-cp310-cp310-win_amd64.whl", hash = "sha256:76c3ac6530904838a4b650b2880f8e7af938ee049e769ec2fba7cd66469d7772"}, + {file = "asyncpg-0.29.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4900ee08e85af01adb207519bb4e14b1cae8fd21e0ccf80fac6aa60b6da37b4"}, + {file = "asyncpg-0.29.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a65c1dcd820d5aea7c7d82a3fdcb70e096f8f70d1a8bf93eb458e49bfad036ac"}, + {file = "asyncpg-0.29.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b52e46f165585fd6af4863f268566668407c76b2c72d366bb8b522fa66f1870"}, + {file = "asyncpg-0.29.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc600ee8ef3dd38b8d67421359779f8ccec30b463e7aec7ed481c8346decf99f"}, + {file = "asyncpg-0.29.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:039a261af4f38f949095e1e780bae84a25ffe3e370175193174eb08d3cecab23"}, + {file = "asyncpg-0.29.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6feaf2d8f9138d190e5ec4390c1715c3e87b37715cd69b2c3dfca616134efd2b"}, + {file = "asyncpg-0.29.0-cp311-cp311-win32.whl", hash = "sha256:1e186427c88225ef730555f5fdda6c1812daa884064bfe6bc462fd3a71c4b675"}, + {file = "asyncpg-0.29.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfe73ffae35f518cfd6e4e5f5abb2618ceb5ef02a2365ce64f132601000587d3"}, + {file = "asyncpg-0.29.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6011b0dc29886ab424dc042bf9eeb507670a3b40aece3439944006aafe023178"}, + {file = "asyncpg-0.29.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b544ffc66b039d5ec5a7454667f855f7fec08e0dfaf5a5490dfafbb7abbd2cfb"}, + {file = "asyncpg-0.29.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d84156d5fb530b06c493f9e7635aa18f518fa1d1395ef240d211cb563c4e2364"}, + {file = "asyncpg-0.29.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54858bc25b49d1114178d65a88e48ad50cb2b6f3e475caa0f0c092d5f527c106"}, + {file = "asyncpg-0.29.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bde17a1861cf10d5afce80a36fca736a86769ab3579532c03e45f83ba8a09c59"}, + {file = "asyncpg-0.29.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:37a2ec1b9ff88d8773d3eb6d3784dc7e3fee7756a5317b67f923172a4748a175"}, + {file = "asyncpg-0.29.0-cp312-cp312-win32.whl", hash = "sha256:bb1292d9fad43112a85e98ecdc2e051602bce97c199920586be83254d9dafc02"}, + {file = "asyncpg-0.29.0-cp312-cp312-win_amd64.whl", hash = "sha256:2245be8ec5047a605e0b454c894e54bf2ec787ac04b1cb7e0d3c67aa1e32f0fe"}, + {file = "asyncpg-0.29.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0009a300cae37b8c525e5b449233d59cd9868fd35431abc470a3e364d2b85cb9"}, + {file = "asyncpg-0.29.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cad1324dbb33f3ca0cd2074d5114354ed3be2b94d48ddfd88af75ebda7c43cc"}, + {file = "asyncpg-0.29.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:012d01df61e009015944ac7543d6ee30c2dc1eb2f6b10b62a3f598beb6531548"}, + {file = "asyncpg-0.29.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000c996c53c04770798053e1730d34e30cb645ad95a63265aec82da9093d88e7"}, + {file = "asyncpg-0.29.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e0bfe9c4d3429706cf70d3249089de14d6a01192d617e9093a8e941fea8ee775"}, + {file = "asyncpg-0.29.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:642a36eb41b6313ffa328e8a5c5c2b5bea6ee138546c9c3cf1bffaad8ee36dd9"}, + {file = "asyncpg-0.29.0-cp38-cp38-win32.whl", hash = "sha256:a921372bbd0aa3a5822dd0409da61b4cd50df89ae85150149f8c119f23e8c408"}, + {file = "asyncpg-0.29.0-cp38-cp38-win_amd64.whl", hash = "sha256:103aad2b92d1506700cbf51cd8bb5441e7e72e87a7b3a2ca4e32c840f051a6a3"}, + {file = "asyncpg-0.29.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5340dd515d7e52f4c11ada32171d87c05570479dc01dc66d03ee3e150fb695da"}, + {file = "asyncpg-0.29.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e17b52c6cf83e170d3d865571ba574577ab8e533e7361a2b8ce6157d02c665d3"}, + {file = "asyncpg-0.29.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f100d23f273555f4b19b74a96840aa27b85e99ba4b1f18d4ebff0734e78dc090"}, + {file = "asyncpg-0.29.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48e7c58b516057126b363cec8ca02b804644fd012ef8e6c7e23386b7d5e6ce83"}, + {file = "asyncpg-0.29.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f9ea3f24eb4c49a615573724d88a48bd1b7821c890c2effe04f05382ed9e8810"}, + {file = "asyncpg-0.29.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8d36c7f14a22ec9e928f15f92a48207546ffe68bc412f3be718eedccdf10dc5c"}, + {file = "asyncpg-0.29.0-cp39-cp39-win32.whl", hash = "sha256:797ab8123ebaed304a1fad4d7576d5376c3a006a4100380fb9d517f0b59c1ab2"}, + {file = "asyncpg-0.29.0-cp39-cp39-win_amd64.whl", hash = "sha256:cce08a178858b426ae1aa8409b5cc171def45d4293626e7aa6510696d46decd8"}, + {file = "asyncpg-0.29.0.tar.gz", hash = "sha256:d1c49e1f44fffafd9a55e1a9b101590859d881d639ea2922516f5d9c512d354e"}, +] + +[package.extras] +docs = ["Sphinx (>=5.3.0,<5.4.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] +test = ["flake8 (>=6.1,<7.0)", "uvloop (>=0.15.3)"] + [[package]] name = "black" version = "24.4.0" @@ -86,6 +140,105 @@ files = [ {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, ] +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + [[package]] name = "click" version = "8.1.7" @@ -111,6 +264,28 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "docker" +version = "7.1.0" +description = "A Python library for the Docker Engine API." +optional = false +python-versions = ">=3.8" +files = [ + {file = "docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0"}, + {file = "docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c"}, +] + +[package.dependencies] +pywin32 = {version = ">=304", markers = "sys_platform == \"win32\""} +requests = ">=2.26.0" +urllib3 = ">=1.26.0" + +[package.extras] +dev = ["coverage (==7.2.7)", "pytest (==7.4.2)", "pytest-cov (==4.1.0)", "pytest-timeout (==2.1.0)", "ruff (==0.1.8)"] +docs = ["myst-parser (==0.18.0)", "sphinx (==5.1.1)"] +ssh = ["paramiko (>=2.4.3)"] +websockets = ["websocket-client (>=1.3.0)"] + [[package]] name = "fastapi" version = "0.104.1" @@ -517,6 +692,50 @@ files = [ [package.extras] cli = ["click (>=5.0)"] +[[package]] +name = "pywin32" +version = "306" +description = "Python for Window Extensions" +optional = false +python-versions = "*" +files = [ + {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, + {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, + {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"}, + {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"}, + {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"}, + {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"}, + {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"}, + {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"}, + {file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"}, + {file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"}, + {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"}, + {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"}, + {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"}, + {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, +] + +[[package]] +name = "requests" +version = "2.32.3" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.8" +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + [[package]] name = "sniffio" version = "1.3.1" @@ -545,6 +764,55 @@ anyio = ">=3.4.0,<5" [package.extras] full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"] +[[package]] +name = "testcontainers" +version = "4.7.2" +description = "Python library for throwaway instances of anything that can run in a Docker container" +optional = false +python-versions = "<4.0,>=3.9" +files = [ + {file = "testcontainers-4.7.2-py3-none-any.whl", hash = "sha256:23b13cf8078f615a08c75197f227796d90c46df92d2b282ae7c39b1fc1a9c9ed"}, + {file = "testcontainers-4.7.2.tar.gz", hash = "sha256:9976b1cdcdeb9feeae6a477073e7c8b02cd40ea44f1daa34b5da6d2c918dff0d"}, +] + +[package.dependencies] +docker = "*" +typing-extensions = "*" +urllib3 = "*" +wrapt = "*" + +[package.extras] +arangodb = ["python-arango (>=7.8,<8.0)"] +azurite = ["azure-storage-blob (>=12.19,<13.0)"] +chroma = ["chromadb-client"] +clickhouse = ["clickhouse-driver"] +cosmosdb = ["azure-cosmos"] +generic = ["httpx"] +google = ["google-cloud-datastore (>=2)", "google-cloud-pubsub (>=2)"] +influxdb = ["influxdb", "influxdb-client"] +k3s = ["kubernetes", "pyyaml"] +keycloak = ["python-keycloak"] +localstack = ["boto3"] +mailpit = ["cryptography"] +minio = ["minio"] +mongodb = ["pymongo"] +mssql = ["pymssql", "sqlalchemy"] +mysql = ["pymysql[rsa]", "sqlalchemy"] +nats = ["nats-py"] +neo4j = ["neo4j"] +opensearch = ["opensearch-py"] +oracle = ["oracledb", "sqlalchemy"] +oracle-free = ["oracledb", "sqlalchemy"] +qdrant = ["qdrant-client"] +rabbitmq = ["pika"] +redis = ["redis"] +registry = ["bcrypt"] +selenium = ["selenium"] +sftp = ["cryptography"] +test-module-import = ["httpx"] +trino = ["trino"] +weaviate = ["weaviate-client (>=4.5.4,<5.0.0)"] + [[package]] name = "typing-extensions" version = "4.11.0" @@ -556,6 +824,23 @@ files = [ {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, ] +[[package]] +name = "urllib3" +version = "2.2.2" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, + {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + [[package]] name = "uvicorn" version = "0.29.0" @@ -591,7 +876,86 @@ MarkupSafe = ">=2.1.1" [package.extras] watchdog = ["watchdog (>=2.3)"] +[[package]] +name = "wrapt" +version = "1.16.0" +description = "Module for decorators, wrappers and monkey patching." +optional = false +python-versions = ">=3.6" +files = [ + {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, + {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, + {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, + {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, + {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, + {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, + {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, + {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, + {file = "wrapt-1.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465"}, + {file = "wrapt-1.16.0-cp36-cp36m-win32.whl", hash = "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e"}, + {file = "wrapt-1.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966"}, + {file = "wrapt-1.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win32.whl", hash = "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"}, + {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"}, + {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"}, + {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"}, + {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"}, + {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, + {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, +] + [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "83d2c11647940d87a0ced56ed0566f1942c64d0278dd3c9c832a863cf8dea4d1" +content-hash = "0f24c309c7c5fdaf66c578049ca814891416d95e8626f398ece37d31988cc584" diff --git a/pyproject.toml b/pyproject.toml index d61ae31..8d96fef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ homepage = "https://svaponi.github.io/fastapi-boilerplate" python = "^3.12" fastapi = "^0.104.1" httpx = "^0.27.0" +asyncpg = "^0.29.0" [tool.poetry.group.dev.dependencies] pytest = "^8.1.1" @@ -20,6 +21,7 @@ python-dotenv = "^1.0.1" uvicorn = "^0.29.0" black = "^24.4.0" pytest-httpserver = "^1.0.10" +testcontainers = "^4.7.2" [build-system] requires = ["poetry-core"] diff --git a/src/app/api/__init__.py b/src/app/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/api/users.py b/src/app/api/users.py new file mode 100644 index 0000000..2588514 --- /dev/null +++ b/src/app/api/users.py @@ -0,0 +1,38 @@ +import fastapi + +from app.datalayer.model import UserAccount +from app.datalayer.users import UserService + +router = fastapi.APIRouter() + + +@router.get("") +async def get_users( + data_service: UserService = fastapi.Depends(), + page: int = 0, + size: int = 10, +) -> list[UserAccount]: + # Read users + users = await data_service.read_users() + + # Create users + if not users: + await data_service.create_user("Alice", "alice@example.com") + await data_service.create_user("Bob", "bob@example.com") + + # Read users + users = await data_service.read_users() + + # # Update user + # user_id = users[0].id + # await data_service.update_user(user_id, "John") + # + # # Read users again + # await data_service.read_users() + # + # # Delete user + # await data_service.delete_user(2) + + # Read users again + + return users diff --git a/src/app/api/v1.py b/src/app/api/v1.py new file mode 100644 index 0000000..c36c17f --- /dev/null +++ b/src/app/api/v1.py @@ -0,0 +1,9 @@ +import fastapi + +from app.api import users + + +def setup_api_v1(app: fastapi.FastAPI): + api_router = fastapi.APIRouter() + api_router.include_router(users.router, prefix="/users", tags=["users"]) + app.include_router(api_router, prefix="/api/v1") diff --git a/src/app/app.py b/src/app/app.py new file mode 100644 index 0000000..907ce84 --- /dev/null +++ b/src/app/app.py @@ -0,0 +1,69 @@ +import contextlib +import logging +import typing + +import fastapi +from starlette.responses import RedirectResponse + +from app.api.v1 import setup_api_v1 +from app.core.cors import setup_cors +from app.core.datasource import setup_datasource +from app.core.error_handlers import setup_error_handlers +from app.core.logging import setup_logging +from app.core.migration import run_migrations +from app.core.request_context import setup_request_context + + +# See https://fastapi.tiangolo.com/advanced/events/#lifespan +@contextlib.asynccontextmanager +async def _lifespan(app: "App"): + app.logger.info(f"Starting ...") + await app.datasource.connect() + await run_migrations(app.datasource) + # ...other startup code + app.logger.info("✅ Started") + yield + app.logger.info("Shutting down ...") + await app.datasource.disconnect() + # ...other shutdown code + app.logger.info("🛑 Shutdown") + + +class App(fastapi.FastAPI): + def __init__( + self, + **extra: typing.Any, + ) -> None: + # all logs previous to calling setup_logging will be not formatted + setup_logging() + self.logger = logging.getLogger(f"app") + + super().__init__( + lifespan=_lifespan, + **extra, + ) + + # RequestContext is a nice-to-have util that allows to access request related attributes anywhere in the code + setup_request_context(self) + + # Http error handlers (equivalent to try block that can handle uncaught exceptions) + setup_error_handlers(self) + + # Setup cors + setup_cors(self) + + # Setup API routes + setup_api_v1(self) + + # Setup DB data source + self.datasource = setup_datasource(self) + + if hasattr(self, "docs_url") and self.docs_url: + + @self.get("/", include_in_schema=False) + def root(): + return RedirectResponse(self.docs_url) + + +def create_app(): + return App() diff --git a/src/app/core/app.py b/src/app/core/app.py deleted file mode 100644 index 2e253ff..0000000 --- a/src/app/core/app.py +++ /dev/null @@ -1,46 +0,0 @@ -import contextlib -import logging -import typing - -import fastapi - -from app.core.cors import setup_cors -from app.core.error_handlers import setup_error_handlers -from app.core.logging import setup_logging -from app.core.request_context import setup_request_context - - -@contextlib.asynccontextmanager -async def _lifespan(app: "App"): - app.logger.info(f"Starting 🔄") - # ... - app.logger.info("Started ✅ ") - yield - app.logger.info("Shutting down 🔄") - # ... - app.logger.info("Shutdown 🛑") - - -class App(fastapi.FastAPI): - def __init__( - self, - **extra: typing.Any, - ) -> None: - # IMPORTANT all logs previous to calling setup_logging will be not formatted - setup_logging() - self.logger = logging.getLogger(f"app.core") - - super().__init__(lifespan=_lifespan, **extra) - - # RequestContext is a nice-to-have util that allows to access request related attributes anywhere in the code - setup_request_context(self) - - # Http error handlers (equivalent to try block that can handle uncaught exceptions) - setup_error_handlers(self) - - # Setup cors - setup_cors(self) - - -def create_app(): - return App() diff --git a/src/app/core/datasource.py b/src/app/core/datasource.py new file mode 100644 index 0000000..3261806 --- /dev/null +++ b/src/app/core/datasource.py @@ -0,0 +1,39 @@ +import logging +import os + +import asyncpg +import fastapi + + +# Simple class that takes care of setting up and tearing down the connection pool. +# Useful to decouple for the actual connection pool object. +# It's meant to be used in FastAPI lifespan (see https://fastapi.tiangolo.com/advanced/events/#lifespan). +class DataSource: + def __init__(self, app: fastapi.FastAPI): + self._pool: asyncpg.Pool | None = None + self.logger = logging.getLogger(__name__) + + async def connect(self): + if not self._pool: + postgres_url = os.getenv("POSTGRES_URL") + assert postgres_url, "missing POSTGRES_URL" + self._pool = await asyncpg.create_pool(dsn=postgres_url) + self.logger.info("DataSource created") + + async def disconnect(self): + if self._pool: + await self._pool.close() + self.logger.info("DataSource closed") + + @property + def pool(self) -> asyncpg.Pool: + if not self._pool: + raise RuntimeError("DataSource not initialized, you need to call `await connect()` in the lifespan event, " + "see see https://fastapi.tiangolo.com/advanced/events/#lifespan.") + return self._pool + + +def setup_datasource(app: fastapi.FastAPI) -> DataSource: + return DataSource(app) + + diff --git a/src/app/core/migration.py b/src/app/core/migration.py new file mode 100644 index 0000000..e584d58 --- /dev/null +++ b/src/app/core/migration.py @@ -0,0 +1,20 @@ +import os.path + +from app.core.datasource import DataSource + + +async def run_migrations(datasource: DataSource): + async with datasource.pool.acquire(timeout=2) as connection: + await connection.execute("create table if not exists migration (name text primary key, executed_at timestamp)") + records = await connection.fetch("select * from migration") + print(records) + if len(records) > 0: + print("Migration table already exists") + else: + with open(os.path.join(os.path.dirname(__file__), "../../schema.sql")) as f: + content = f.read() + print(content) + await connection.execute(content) + await connection.execute("insert into migration (name, executed_at) values ('migration.sql', now())") + + diff --git a/src/app/datalayer/__init__.py b/src/app/datalayer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/datalayer/db.py b/src/app/datalayer/db.py new file mode 100644 index 0000000..b0aebc6 --- /dev/null +++ b/src/app/datalayer/db.py @@ -0,0 +1,19 @@ +import contextlib + +import fastapi + + +class DB: + def __init__(self, request: fastapi.Request): + self.pool = request.app.datasource.pool + + @contextlib.asynccontextmanager + async def transaction(self): + async with self.pool.acquire(timeout=2) as connection: + async with connection.transaction(): + yield connection + + @contextlib.asynccontextmanager + async def connection(self): + async with self.pool.acquire(timeout=2) as connection: + yield connection diff --git a/src/app/datalayer/model.py b/src/app/datalayer/model.py new file mode 100644 index 0000000..15eb52e --- /dev/null +++ b/src/app/datalayer/model.py @@ -0,0 +1,262 @@ +from datetime import datetime +from enum import Enum +from typing import Optional +from uuid import UUID + +import pydantic + + +class AdjustedHourTypeEnum(str, Enum): + hours = "hours" + percentage = "percentage" + + +class BenefitTypeEnum(str, Enum): + usd_value = "usd_value" + percentage = "percentage" + + +class TaxTypeEnum(str, Enum): + usd_value = "usd_value" + percentage = "percentage" + + +class UserRoleEnum(str, Enum): + client = "client" + admin = "admin" + super_admin = "super_admin" + + +class AdjustedHour(pydantic.BaseModel): + id: UUID = pydantic.Field(default_factory=UUID) + title: str + value: float + type: Optional[AdjustedHourTypeEnum] = None + company_id: Optional[UUID] = None + + _table_name: str = "adjusted_hours" + + +class Benefit(pydantic.BaseModel): + id: UUID = pydantic.Field(default_factory=UUID) + title: str + value: float + type: BenefitTypeEnum = BenefitTypeEnum.usd_value + company_id: Optional[UUID] = None + + _table_name: str = "benefits" + + +class BenefitProfile(pydantic.BaseModel): + id: UUID = pydantic.Field(default_factory=UUID) + title: str + company_id: Optional[UUID] = None + + _table_name: str = "benefit_profiles" + + +class BenefitProfileBenefit(pydantic.BaseModel): + benefit_profile_id: UUID + benefit_id: UUID + + _table_name: str = "benefit_profile_benefits" + + +class Company(pydantic.BaseModel): + name: str + company_code: str + id: UUID = pydantic.Field(default_factory=UUID) + + _table_name: str = "companies" + + +class JobTitle(pydantic.BaseModel): + id: UUID = pydantic.Field(default_factory=UUID) + title: str + company_id: Optional[UUID] = None + + _table_name: str = "job_titles" + + +class Location(pydantic.BaseModel): + id: UUID = pydantic.Field(default_factory=UUID) + country: str + state: str + company_id: Optional[UUID] = None + + _table_name: str = "locations" + + +class LocationAdjustedHour(pydantic.BaseModel): + location_id: UUID + adjusted_hour_id: UUID + + _table_name: str = "location_adjusted_hours" + + +class LocationBenefit(pydantic.BaseModel): + location_id: UUID + benefit_id: UUID + + _table_name: str = "location_benefits" + + +class LocationTax(pydantic.BaseModel): + location_id: UUID + tax_id: UUID + + _table_name: str = "location_taxes" + + +class PasswordResetToken(pydantic.BaseModel): + id: UUID = pydantic.Field(default_factory=UUID) + token: str + created_at: datetime = pydantic.Field(default_factory=datetime.utcnow) + expires_at: datetime + user_id: Optional[UUID] = None + + _table_name: str = "password_reset_tokens" + + +class Position(pydantic.BaseModel): + id: UUID = pydantic.Field(default_factory=UUID) + title: str + company_id: Optional[UUID] = None + hourly_rate: Optional[float] = None + headcount: Optional[int] = None + is_manual: bool = False + + _table_name: str = "positions" + + +class QuickQuote(pydantic.BaseModel): + id: UUID = pydantic.Field(default_factory=UUID) + title: str + created_at: datetime = pydantic.Field(default_factory=datetime.utcnow) + updated_at: datetime = pydantic.Field(default_factory=datetime.utcnow) + company_id: Optional[UUID] = None + created_by_user_id: Optional[UUID] = None + ratecard_id: Optional[UUID] = None + + _table_name: str = "quick_quotes" + + +class QuickQuoteItem(pydantic.BaseModel): + id: UUID = pydantic.Field(default_factory=UUID) + resource_id: str + dedication_percentage: float + quick_quote_id: Optional[UUID] = None + billable_rate: float = 0.0 + + _table_name: str = "quick_quote_items" + + +class RateCard(pydantic.BaseModel): + id: UUID = pydantic.Field(default_factory=UUID) + net_margin: float = 0.0 + overhead: float = 0.0 + selling_cost: float = 0.0 + company_id: Optional[UUID] = None + title: Optional[str] = None + + _table_name: str = "rate_cards" + + +class Resource(pydantic.BaseModel): + id: UUID = pydantic.Field(default_factory=UUID) + first_name: str + last_name: str + salary: float + total_tax_amount: Optional[float] = None + total_cost: Optional[float] = None + total_benefits: Optional[float] = None + weekly_hours: float = 40.0 + total_adjusted_hours: Optional[float] = None + resource_cost_per_adjusted_hour: Optional[float] = None + position_id: Optional[UUID] = None + location_id: Optional[UUID] = None + company_id: Optional[UUID] = None + team_id: Optional[UUID] = None + manager_id: Optional[UUID] = None + employee_code: Optional[str] = None + total_annual_hours: Optional[float] = None + location_title: Optional[str] = None + total_working_hours: Optional[float] = None + + _table_name: str = "resources" + + +class ResourceSkill(pydantic.BaseModel): + resource_id: UUID + skill_id: UUID + + _table_name: str = "resource_skills" + + +class ResourceTax(pydantic.BaseModel): + id: UUID = pydantic.Field(default_factory=UUID) + total_annual_amount: float + resource_id: Optional[UUID] = None + tax_id: Optional[UUID] = None + + _table_name: str = "resource_taxes" + + +class Skill(pydantic.BaseModel): + id: UUID = pydantic.Field(default_factory=UUID) + title: str + company_id: Optional[UUID] = None + + _table_name: str = "skills" + + +class Tax(pydantic.BaseModel): + id: UUID = pydantic.Field(default_factory=UUID) + title: str + value: float + type: TaxTypeEnum = TaxTypeEnum.usd_value + company_id: Optional[UUID] = None + + _table_name: str = "taxes" + + +class Team(pydantic.BaseModel): + id: UUID = pydantic.Field(default_factory=UUID) + title: str + company_id: Optional[UUID] = None + + _table_name: str = "teams" + + +class TeamBundle(pydantic.BaseModel): + id: UUID = pydantic.Field(default_factory=UUID) + title: str + created_at: datetime = pydantic.Field(default_factory=datetime.utcnow) + updated_at: datetime = pydantic.Field(default_factory=datetime.utcnow) + company_id: Optional[UUID] = None + created_by_user_id: Optional[UUID] = None + + _table_name: str = "team_bundles" + + +class TeamBundleItem(pydantic.BaseModel): + id: UUID = pydantic.Field(default_factory=UUID) + position_id: str + team_bundle_id: Optional[UUID] = None + head_count: Optional[float] = None + + _table_name: str = "team_bundle_items" + + +class UserAccount(pydantic.BaseModel): + id: UUID = pydantic.Field(default_factory=UUID) + full_name: Optional[str] = None + email: str + password: str + phone_number: Optional[str] = None + role: UserRoleEnum = UserRoleEnum.client + created_at: datetime = pydantic.Field(default_factory=datetime.utcnow) + updated_at: datetime = pydantic.Field(default_factory=datetime.utcnow) + company_id: Optional[UUID] = None + + _table_name: str = "user_accounts" diff --git a/src/app/datalayer/users.py b/src/app/datalayer/users.py new file mode 100644 index 0000000..55f65c1 --- /dev/null +++ b/src/app/datalayer/users.py @@ -0,0 +1,52 @@ +import fastapi +import pydantic + +from app.datalayer.db import DB +from app.datalayer.model import UserAccount + +IGNORE_FIELDS = {"_table_name"} + + +class UserService: + + def __init__(self, db: DB = fastapi.Depends()): + super().__init__() + self.db = db + self.record_cls: pydantic.BaseModel = UserAccount + self.field_names = [ + f for f in self.record_cls.__annotations__.keys() if f not in IGNORE_FIELDS + ] + + async def read_users(self) -> list[UserAccount]: + async with self.db.connection() as connection: + columns = ",".join(self.field_names) + records = await connection.fetch(f"SELECT {columns} FROM user_account") + print("Users:", records) + return [self.record_cls(**dict(row)) for row in records] + + async def create_user(self, full_name, email): + async with self.db.transaction() as connection: + await connection.execute( + "INSERT INTO user_account(full_name, email, password) VALUES($1, $2, $3)", + full_name, + email, + "secret", + ) + print(f"User {full_name} added.") + + async def update_user(self, user_id, full_name): + async with self.db.transaction() as connection: + await connection.execute( + "UPDATE user_account SET full_name=$1 WHERE id=$2", + full_name, + user_id, + ) + print(f"User {user_id} updated.") + + async def delete_user(self, user_id): + async with self.db.transaction() as connection: + await connection.execute( + "DELETE FROM user_account WHERE id=$1", + user_id, + ) + print(f"User {user_id} deleted.") diff --git a/src/app/main.py b/src/app/main.py index 2b3602e..971fa6b 100644 --- a/src/app/main.py +++ b/src/app/main.py @@ -1,3 +1,3 @@ -from app.core.app import create_app +from app.app import create_app app = create_app() diff --git a/src/schema.sql b/src/schema.sql new file mode 100644 index 0000000..715b3c8 --- /dev/null +++ b/src/schema.sql @@ -0,0 +1,273 @@ +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; +CREATE TYPE benefit_type_enum AS ENUM ( + 'usd_value', + 'percentage', + 'days', + 'days_percentage' + ); +CREATE TYPE tax_type_enum AS ENUM ( + 'usd_value', + 'percentage' + ); +CREATE TABLE adjusted_hour +( + id uuid DEFAULT uuid_generate_v4() NOT NULL, + title character varying NOT NULL, + value double precision NOT NULL, + type character varying, + company_id uuid +); +CREATE TABLE benefit +( + id uuid DEFAULT uuid_generate_v4() NOT NULL, + title character varying NOT NULL, + value double precision NOT NULL, + type benefit_type_enum DEFAULT 'usd_value'::benefit_type_enum NOT NULL, + company_id uuid +); +CREATE TABLE benefit_profile +( + id uuid DEFAULT uuid_generate_v4() NOT NULL, + title character varying NOT NULL, + company_id uuid +); +CREATE TABLE benefit_profile_benefit +( + benefit_profile_id uuid NOT NULL, + benefit_id uuid NOT NULL +); +CREATE TABLE company +( + name character varying NOT NULL, + company_code character varying NOT NULL, + id uuid DEFAULT uuid_generate_v4() NOT NULL +); +CREATE TABLE job_title +( + id uuid DEFAULT uuid_generate_v4() NOT NULL, + title character varying NOT NULL, + company_id uuid +); +CREATE TABLE location +( + id uuid DEFAULT uuid_generate_v4() NOT NULL, + country character varying NOT NULL, + state character varying NOT NULL, + company_id uuid +); +CREATE TABLE location_adjusted_hour +( + location_id uuid NOT NULL, + adjusted_hour_id uuid NOT NULL +); +CREATE TABLE location_benefit +( + location_id uuid NOT NULL, + benefit_id uuid NOT NULL +); +CREATE TABLE location_tax +( + location_id uuid NOT NULL, + tax_id uuid NOT NULL +); +CREATE TABLE password_reset_token +( + id uuid DEFAULT uuid_generate_v4() NOT NULL, + token character varying NOT NULL, + created_at timestamp without time zone DEFAULT now() NOT NULL, + expires_at timestamp without time zone NOT NULL, + user_id uuid +); +CREATE TABLE position +( + id uuid DEFAULT uuid_generate_v4() NOT NULL, + title character varying NOT NULL, + company_id uuid, + hourly_rate double precision, + headcount integer, + is_manual boolean DEFAULT false NOT NULL +); +CREATE TABLE quick_quote +( + id uuid DEFAULT uuid_generate_v4() NOT NULL, + title character varying NOT NULL, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL, + company_id uuid, + created_by_user_id uuid, + ratecard_id uuid +); +CREATE TABLE quick_quote_item +( + id uuid DEFAULT uuid_generate_v4() NOT NULL, + resource_id character varying NOT NULL, + dedication_percentage double precision NOT NULL, + quick_quote_id uuid, + billable_rate double precision DEFAULT '0'::double precision NOT NULL +); +CREATE TABLE rate_card +( + id uuid DEFAULT uuid_generate_v4() NOT NULL, + net_margin double precision DEFAULT '0'::double precision NOT NULL, + overhead double precision DEFAULT '0'::double precision NOT NULL, + selling_cost double precision DEFAULT '0'::double precision NOT NULL, + company_id uuid, + title character varying +); +CREATE TABLE resource +( + id uuid DEFAULT uuid_generate_v4() NOT NULL, + first_name character varying NOT NULL, + last_name character varying NOT NULL, + salary double precision NOT NULL, + total_tax_amount double precision, + total_cost double precision, + total_benefits double precision, + weekly_hours double precision DEFAULT '40'::double precision NOT NULL, + total_adjusted_hours double precision, + resource_cost_per_adjusted_hour double precision, + position_id uuid, + location_id uuid, + company_id uuid, + team_id uuid, + manager_id uuid, + employee_code character varying, + total_annual_hours double precision, + location_title character varying, + total_working_hours double precision +); +CREATE TABLE resource_skill +( + resource_id uuid NOT NULL, + skill_id uuid NOT NULL +); +CREATE TABLE resource_tax +( + id uuid DEFAULT uuid_generate_v4() NOT NULL, + total_annual_amount double precision NOT NULL, + resource_id uuid, + tax_id uuid +); +CREATE TABLE skill +( + id uuid DEFAULT uuid_generate_v4() NOT NULL, + title character varying NOT NULL, + company_id uuid +); +CREATE TABLE tax +( + id uuid DEFAULT uuid_generate_v4() NOT NULL, + title character varying NOT NULL, + value double precision NOT NULL, + type tax_type_enum DEFAULT 'usd_value'::tax_type_enum NOT NULL, + company_id uuid +); +CREATE TABLE team +( + id uuid DEFAULT uuid_generate_v4() NOT NULL, + title character varying NOT NULL, + company_id uuid +); +CREATE TABLE team_bundle +( + id uuid DEFAULT uuid_generate_v4() NOT NULL, + title character varying NOT NULL, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL, + company_id uuid, + created_by_user_id uuid +); +CREATE TABLE team_bundle_item +( + id uuid DEFAULT uuid_generate_v4() NOT NULL, + position_id character varying NOT NULL, + team_bundle_id uuid, + head_count double precision +); +CREATE TABLE user_account +( + id uuid DEFAULT uuid_generate_v4() NOT NULL, + full_name character varying, + email character varying NOT NULL, + password character varying NOT NULL, + phone_number character varying, + role character varying DEFAULT 'client'::character varying NOT NULL, + created_at timestamp(3) without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL, + company_id uuid +); + +-- Primary Key Constraints +ALTER TABLE ONLY company ADD CONSTRAINT pk_company PRIMARY KEY (id); +ALTER TABLE ONLY resource_skill ADD CONSTRAINT pk_resource_skill PRIMARY KEY (resource_id, skill_id); +ALTER TABLE ONLY skill ADD CONSTRAINT pk_skill PRIMARY KEY (id); +ALTER TABLE ONLY tax ADD CONSTRAINT pk_tax PRIMARY KEY (id); +ALTER TABLE ONLY location_tax ADD CONSTRAINT pk_location_tax PRIMARY KEY (location_id, tax_id); +ALTER TABLE ONLY quick_quote ADD CONSTRAINT pk_quick_quote PRIMARY KEY (id); +ALTER TABLE ONLY team_bundle ADD CONSTRAINT pk_team_bundle PRIMARY KEY (id); +ALTER TABLE ONLY quick_quote_item ADD CONSTRAINT pk_quick_quote_item PRIMARY KEY (id); +ALTER TABLE ONLY adjusted_hour ADD CONSTRAINT pk_adjusted_hour PRIMARY KEY (id); +ALTER TABLE ONLY rate_card ADD CONSTRAINT pk_rate_card PRIMARY KEY (id); +ALTER TABLE ONLY location_benefit ADD CONSTRAINT pk_location_benefit PRIMARY KEY (location_id, benefit_id); +ALTER TABLE ONLY benefit_profile ADD CONSTRAINT pk_benefit_profile PRIMARY KEY (id); +ALTER TABLE ONLY location ADD CONSTRAINT pk_location PRIMARY KEY (id); +ALTER TABLE ONLY user_account ADD CONSTRAINT pk_user_account PRIMARY KEY (id); +ALTER TABLE ONLY position ADD CONSTRAINT pk_position PRIMARY KEY (id); +ALTER TABLE ONLY benefit ADD CONSTRAINT pk_benefit PRIMARY KEY (id); +ALTER TABLE ONLY password_reset_token ADD CONSTRAINT pk_password_reset_token PRIMARY KEY (id); +ALTER TABLE ONLY location_adjusted_hour ADD CONSTRAINT pk_location_adjusted_hour PRIMARY KEY (location_id, adjusted_hour_id); +ALTER TABLE ONLY resource ADD CONSTRAINT pk_resource PRIMARY KEY (id); +ALTER TABLE ONLY job_title ADD CONSTRAINT pk_job_title PRIMARY KEY (id); +ALTER TABLE ONLY team ADD CONSTRAINT pk_team PRIMARY KEY (id); +ALTER TABLE ONLY benefit_profile_benefit ADD CONSTRAINT pk_benefit_profile_benefit PRIMARY KEY (benefit_profile_id, benefit_id); +ALTER TABLE ONLY resource_tax ADD CONSTRAINT pk_resource_tax PRIMARY KEY (id); +ALTER TABLE ONLY team_bundle_item ADD CONSTRAINT pk_team_bundle_item PRIMARY KEY (id); + +-- Unique Constraints +ALTER TABLE ONLY company ADD CONSTRAINT uq_company_code UNIQUE (company_code); +ALTER TABLE ONLY user_account ADD CONSTRAINT uq_user_email UNIQUE (email); + +-- Indexes +CREATE INDEX idx_resource_skill_skill_id ON resource_skill USING btree (skill_id); +CREATE INDEX idx_benefit_profile_benefit_profile_id ON benefit_profile_benefit USING btree (benefit_profile_id); +CREATE INDEX idx_location_benefit_benefit_id ON location_benefit USING btree (benefit_id); +CREATE INDEX idx_location_adjusted_hour_location_id ON location_adjusted_hour USING btree (location_id); +CREATE INDEX idx_resource_skill_resource_id ON resource_skill USING btree (resource_id); +CREATE INDEX idx_location_tax_tax_id ON location_tax USING btree (tax_id); +CREATE INDEX idx_location_adjusted_hour_adjusted_hour_id ON location_adjusted_hour USING btree (adjusted_hour_id); +CREATE INDEX idx_location_benefit_location_id ON location_benefit USING btree (location_id); +CREATE INDEX idx_location_tax_location_id ON location_tax USING btree (location_id); +CREATE INDEX idx_benefit_profile_benefit_benefit_id ON benefit_profile_benefit USING btree (benefit_id); + +-- Foreign Key Constraints +ALTER TABLE ONLY quick_quote_item ADD CONSTRAINT fk_quick_quote_item_quick_quote_id FOREIGN KEY (quick_quote_id) REFERENCES quick_quote (id) ON DELETE CASCADE; +ALTER TABLE ONLY benefit ADD CONSTRAINT fk_benefit_company_id FOREIGN KEY (company_id) REFERENCES company (id); +ALTER TABLE ONLY resource_skill ADD CONSTRAINT fk_resource_skill_skill_id FOREIGN KEY (skill_id) REFERENCES skill (id) ON UPDATE CASCADE ON DELETE CASCADE; +ALTER TABLE ONLY resource_tax ADD CONSTRAINT fk_resource_tax_resource_id FOREIGN KEY (resource_id) REFERENCES resource (id); +ALTER TABLE ONLY benefit_profile_benefit ADD CONSTRAINT fk_benefit_profile_benefit_profile_id FOREIGN KEY (benefit_profile_id) REFERENCES benefit_profile (id) ON UPDATE CASCADE ON DELETE CASCADE; +ALTER TABLE ONLY resource_tax ADD CONSTRAINT fk_resource_tax_tax_id FOREIGN KEY (tax_id) REFERENCES tax (id); +ALTER TABLE ONLY location_tax ADD CONSTRAINT fk_location_tax_location_id FOREIGN KEY (location_id) REFERENCES location (id); +ALTER TABLE ONLY location_adjusted_hour ADD CONSTRAINT fk_location_adjusted_hour_adjusted_hour_id FOREIGN KEY (adjusted_hour_id) REFERENCES adjusted_hour (id); +ALTER TABLE ONLY location_benefit ADD CONSTRAINT fk_location_benefit_benefit_id FOREIGN KEY (benefit_id) REFERENCES benefit (id); +ALTER TABLE ONLY rate_card ADD CONSTRAINT fk_rate_card_company_id FOREIGN KEY (company_id) REFERENCES company (id); +ALTER TABLE ONLY resource ADD CONSTRAINT fk_resource_team_id FOREIGN KEY (team_id) REFERENCES team (id); +ALTER TABLE ONLY benefit_profile ADD CONSTRAINT fk_benefit_profile_company_id FOREIGN KEY (company_id) REFERENCES company (id); +ALTER TABLE ONLY resource ADD CONSTRAINT fk_resource_position_id FOREIGN KEY (position_id) REFERENCES position (id); +ALTER TABLE ONLY resource ADD CONSTRAINT fk_resource_company_id FOREIGN KEY (company_id) REFERENCES company (id); +ALTER TABLE ONLY location_benefit ADD CONSTRAINT fk_location_benefit_location_id FOREIGN KEY (location_id) REFERENCES location (id); +ALTER TABLE ONLY team_bundle_item ADD CONSTRAINT fk_team_bundle_item_team_bundle_id FOREIGN KEY (team_bundle_id) REFERENCES team_bundle (id); +ALTER TABLE ONLY location ADD CONSTRAINT fk_location_company_id FOREIGN KEY (company_id) REFERENCES company (id); +ALTER TABLE ONLY user_account ADD CONSTRAINT fk_user_company_id FOREIGN KEY (company_id) REFERENCES company (id); +ALTER TABLE ONLY password_reset_token ADD CONSTRAINT fk_password_reset_token_user_id FOREIGN KEY (user_id) REFERENCES user_account (id) ON DELETE CASCADE; +ALTER TABLE ONLY team ADD CONSTRAINT fk_team_company_id FOREIGN KEY (company_id) REFERENCES company (id); +ALTER TABLE ONLY team_bundle ADD CONSTRAINT fk_team_bundle_created_by_user_id FOREIGN KEY (created_by_user_id) REFERENCES user_account (id); +ALTER TABLE ONLY quick_quote ADD CONSTRAINT fk_quick_quote_ratecard_id FOREIGN KEY (ratecard_id) REFERENCES rate_card (id) ON DELETE CASCADE; +ALTER TABLE ONLY quick_quote ADD CONSTRAINT fk_quick_quote_company_id FOREIGN KEY (company_id) REFERENCES company (id); +ALTER TABLE ONLY quick_quote ADD CONSTRAINT fk_quick_quote_created_by_user_id FOREIGN KEY (created_by_user_id) REFERENCES user_account (id); +ALTER TABLE ONLY team_bundle ADD CONSTRAINT fk_team_bundle_company_id FOREIGN KEY (company_id) REFERENCES company (id); +ALTER TABLE ONLY job_title ADD CONSTRAINT fk_job_title_company_id FOREIGN KEY (company_id) REFERENCES company (id); +ALTER TABLE ONLY location_adjusted_hour ADD CONSTRAINT fk_location_adjusted_hour_location_id FOREIGN KEY (location_id) REFERENCES location (id); +ALTER TABLE ONLY resource_skill ADD CONSTRAINT fk_resource_skill_resource_id FOREIGN KEY (resource_id) REFERENCES resource (id) ON UPDATE CASCADE ON DELETE CASCADE; +ALTER TABLE ONLY resource ADD CONSTRAINT fk_resource_location_id FOREIGN KEY (location_id) REFERENCES location (id); +ALTER TABLE ONLY location_tax ADD CONSTRAINT fk_location_tax_tax_id FOREIGN KEY (tax_id) REFERENCES tax (id); +ALTER TABLE ONLY benefit_profile_benefit ADD CONSTRAINT fk_benefit_profile_benefit_benefit_id FOREIGN KEY (benefit_id) REFERENCES benefit (id) ON UPDATE CASCADE ON DELETE CASCADE; diff --git a/src/tests/app/api/__init__.py b/src/tests/app/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/tests/app/api/users.py b/src/tests/app/api/users.py new file mode 100644 index 0000000..08e38eb --- /dev/null +++ b/src/tests/app/api/users.py @@ -0,0 +1,4 @@ +def test_v1_user(client): + res = client.get("/api/v1/users") + print(f"{res.request.method} {res.url} >> {res.status_code} {res.text}") + assert res.status_code == 200 diff --git a/src/tests/app/test_integration.py b/src/tests/app/test_integration.py new file mode 100644 index 0000000..abf5bdb --- /dev/null +++ b/src/tests/app/test_integration.py @@ -0,0 +1,4 @@ +def test_root(client): + res = client.get("/api/v1/users") + print(f"{res.request.method} {res.url} >> {res.status_code} {res.text}") + assert res.status_code == 200 diff --git a/src/tests/conftest.py b/src/tests/conftest.py index cef4076..0efef56 100644 --- a/src/tests/conftest.py +++ b/src/tests/conftest.py @@ -1,15 +1,31 @@ import pytest import pytest_httpserver +from httpx import ASGITransport, AsyncClient from starlette.testclient import TestClient +from testcontainers.postgres import PostgresContainer -from app.core.app import create_app +from app.app import create_app +from tests.testutils.mock_environ import mock_environ from tests.testutils.mock_server import MockServer +@pytest.fixture(scope="session") +def postgres_url(): + with PostgresContainer("postgres:15", driver=None) as postgres: + yield postgres.get_connection_url() + + +@pytest.fixture +def app(postgres_url): + with mock_environ(load_dotenv=True, POSTGRES_URL=postgres_url): + app = create_app() + yield app + + @pytest.fixture -def app(): - app = create_app() - yield app +def async_client(app): + transport = ASGITransport(app=app) + return AsyncClient(transport=transport, base_url="http://localhost") @pytest.fixture diff --git a/start-database.sh b/start-database.sh new file mode 100755 index 0000000..7e30610 --- /dev/null +++ b/start-database.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Use this script to start a docker container for a local development database + +POSTGRES_DB=test +POSTGRES_USER=test +POSTGRES_PASSWORD=test +POSTGRES_PORT=5432 + +DB_CONTAINER_NAME=postgres15 + +if [ "$(docker ps -aq -f name="${DB_CONTAINER_NAME}")" ]; then + docker rm -f "${DB_CONTAINER_NAME}" +fi + +docker run -it \ + --name "${DB_CONTAINER_NAME}" \ + -e "POSTGRES_USER=${POSTGRES_USER}" \ + -e "POSTGRES_PASSWORD=${POSTGRES_PASSWORD}" \ + -e "POSTGRES_DB=${POSTGRES_DB}" \ + -p "${POSTGRES_PORT}:5432" \ + docker.io/postgres && echo "Database container '${DB_CONTAINER_NAME}' was successfully created"