diff --git a/backend/.vscode/settings.json b/backend/.vscode/settings.json new file mode 100644 index 00000000..fa853d8c --- /dev/null +++ b/backend/.vscode/settings.json @@ -0,0 +1,14 @@ +{ + "python.analysis.packageIndexDepths": [ + { + "name": "sqlalchemy", + "depth": 4, + "includeAllSymbols": true + }, + { + "name": "fastapi", + "depth": 2, + "includeAllSymbols": true + } + ] +} diff --git a/backend/Makefile b/backend/Makefile index 1a93a33a..2c462580 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -1,4 +1,5 @@ -DIRS_PYTHON := app tests ../docs +DIRS_PYTHON_NO_ALEMBIC := app tests ../docs +DIRS_PYTHON := alembic $(DIRS_PYTHON_NO_ALEMBIC) .PHONY: help help: @@ -57,7 +58,7 @@ flake8: .PHONY: lint-mypy lint-mypy: - pipenv run mypy $(DIRS_PYTHON) + pipenv run mypy $(DIRS_PYTHON_NO_ALEMBIC) .PHONY: test test: @@ -79,7 +80,7 @@ docs: .PHONY: serve serve: - pipenv run uvicorn app.main:app --host 0.0.0.0 --port 8080 --reload + pipenv run uvicorn app.main:app --host 0.0.0.0 --port 8080 --reload --workers 8 .PHONY: alembic-check alembic-check: diff --git a/backend/Pipfile b/backend/Pipfile index e2a39618..fc584f0c 100644 --- a/backend/Pipfile +++ b/backend/Pipfile @@ -4,24 +4,28 @@ verify_ssl = true name = "pypi" [packages] +asyncpg = "*" fastapi = "*" +fastapi-users = {extras = ["oauth", "sqlalchemy", "redis"], version = "*"} httpx = "*" install = "*" +passlib = {extras = ["bcrypt"], version = "*"} pip = "*" +psycopg2-binary = "*" pydantic = {extras = ["email"], version = "*"} pydantic-settings = "*" +python-dateutil = "*" python-dotenv = "*" requests-mock = "*" +sqlalchemy = "*" tenacity = "*" uvicorn = "*" -jose = {extras = ["cryptography"], version = "*"} -passlib = {extras = ["bcrypt"], version = "*"} -psycopg2-binary = "*" -python-dateutil = "*" -sqlalchemy = "*" +alembic = "*" [dev-packages] +aiosqlite = "*" black = "*" +faker = "*" flake8 = "*" install = "*" isort = "*" @@ -30,16 +34,13 @@ pip = "*" pytest = "*" pytest-asyncio = "*" pytest-cov = "*" +pytest-faker = "*" pytest-httpx = "*" pytest-subprocess = "*" sphinx = "*" sphinx-rtd-theme = "*" starlette = "*" -types-python-jose = "*" types-passlib = "*" -sqlalchemy-stubs = "*" -faker = "*" -pytest-faker = "*" [requires] python_version = "3.10" diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock index 1007a962..7b5096ca 100644 --- a/backend/Pipfile.lock +++ b/backend/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "e56f9e48c3cd92dc5d05f5b05088faee6fcb6d31f5cea6d87985626f8578e5dc" + "sha256": "5adacf2c122c59ce295e553fd74267e07757196337fea89450663c8e12581423" }, "pipfile-spec": 6, "requires": { @@ -16,6 +16,15 @@ ] }, "default": { + "alembic": { + "hashes": [ + "sha256:03226222f1cf943deee6c85d9464261a6c710cd19b4fe867a3ad1f25afda610f", + "sha256:8e7645c32e4f200675e69f0745415335eb59a3663f5feb487abfa0b30c45888b" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==1.12.0" + }, "annotated-types": { "hashes": [ "sha256:47cdc3490d9ac1506ce92c7aaa76c579dc3509ff11e098fc867e5130ab7be802", @@ -32,6 +41,61 @@ "markers": "python_version >= '3.7'", "version": "==3.7.1" }, + "async-timeout": { + "hashes": [ + "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f", + "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028" + ], + "markers": "python_full_version <= '3.11.2'", + "version": "==4.0.3" + }, + "asyncpg": { + "hashes": [ + "sha256:0740f836985fd2bd73dca42c50c6074d1d61376e134d7ad3ad7566c4f79f8184", + "sha256:0a6d1b954d2b296292ddff4e0060f494bb4270d87fb3655dd23c5c6096d16d83", + "sha256:0c402745185414e4c204a02daca3d22d732b37359db4d2e705172324e2d94e85", + "sha256:1c56092465e718a9fdcc726cc3d9dcf3a692e4834031c9a9f871d92a75d20d48", + "sha256:319f5fa1ab0432bc91fb39b3960b0d591e6b5c7844dafc92c79e3f1bff96abef", + "sha256:3ed77f00c6aacfe9d79e9eff9e21729ce92a4b38e80ea99a58ed382f42ebd55b", + "sha256:41e97248d9076bc8e4849da9e33e051be7ba37cd507cbd51dfe4b2d99c70e3dc", + "sha256:4acd6830a7da0eb4426249d71353e8895b350daae2380cb26d11e0d4a01c5472", + "sha256:4d32b680a9b16d2957a0a3cc6b7fa39068baba8e6b728f2e0a148a67644578f4", + "sha256:4f20cac332c2576c79c2e8e6464791c1f1628416d1115935a34ddd7121bfc6a4", + "sha256:59f9712ce01e146ff71d95d561fb68bd2d588a35a187116ef05028675462d5ed", + "sha256:5e18438a0730d1c0c1715016eacda6e9a505fc5aa931b37c97d928d44941b4bf", + "sha256:5e7337c98fb493079d686a4a6965e8bcb059b8e1b8ec42106322fc6c1c889bb0", + "sha256:63861bb4a540fa033a56db3bb58b0c128c56fad5d24e6d0a8c37cb29b17c1c7d", + "sha256:7252cdc3acb2f52feaa3664280d3bcd78a46bd6c10bfd681acfffefa1120e278", + "sha256:76aacdcd5e2e9999e83c8fbcb748208b60925cc714a578925adcb446d709016c", + "sha256:7b48ceed606cce9e64fd5480a9b0b9a95cea2b798bb95129687abd8599c8b019", + "sha256:86b339984d55e8202e0c4b252e9573e26e5afa05617ed02252544f7b3e6de3e9", + "sha256:8858f713810f4fe67876728680f42e93b7e7d5c7b61cf2118ef9153ec16b9423", + "sha256:8aec08e7310f9ab322925ae5c768532e1d78cfb6440f63c078b8392a38aa636a", + "sha256:8ba7d06a0bea539e0487234511d4adf81dc8762249858ed2a580534e1720db00", + "sha256:90a7bae882a9e65a9e448fdad3e090c2609bb4637d2a9c90bfdcebbfc334bf89", + "sha256:99417210461a41891c4ff301490a8713d1ca99b694fef05dabd7139f9d64bd6c", + "sha256:9e721dccd3838fcff66da98709ed884df1e30a95f6ba19f595a3706b4bc757e3", + "sha256:a0e08fe2c9b3618459caaef35979d45f4e4f8d4f79490c9fa3367251366af207", + "sha256:a93a94ae777c70772073d0512f21c74ac82a8a49be3a1d982e3f259ab5f27307", + "sha256:ad1d6abf6c2f5152f46fff06b0e74f25800ce8ec6c80967f0bc789974de3c652", + "sha256:b24e521f6060ff5d35f761a623b0042c84b9c9b9fb82786aadca95a9cb4a893b", + "sha256:b337ededaabc91c26bf577bfcd19b5508d879c0ad009722be5bb0a9dd30b85a0", + "sha256:c88eef5e096296626e9688f00ab627231f709d0e7e3fb84bb4413dff81d996d7", + "sha256:d009b08602b8b18edef3a731f2ce6d3f57d8dac2a0a4140367e194eabd3de457", + "sha256:d14681110e51a9bc9c065c4e7944e8139076a778e56d6f6a306a26e740ed86d2", + "sha256:d7fa81ada2807bc50fea1dc741b26a4e99258825ba55913b0ddbf199a10d69d8", + "sha256:e907cf620a819fab1737f2dd90c0f185e2a796f139ac7de6aa3212a8af96c050", + "sha256:e9c433f6fcdd61c21a715ee9128a3ca48be8ac16fa07be69262f016bb0f4dbd2", + "sha256:ec46a58d81446d580fb21b376ec6baecab7288ce5a578943e2fc7ab73bf7eb39", + "sha256:f029c5adf08c47b10bcdc857001bbef551ae51c57b3110964844a9d79ca0f267", + "sha256:f33c5685e97821533df3ada9384e7784bd1e7865d2b22f153f2e4bd4a083e102", + "sha256:f4f62f04cdf38441a70f279505ef3b4eadf64479b17e707c950515846a2df197", + "sha256:fc9e9f9ff1aa0eddcc3247a180ac9e9b51a62311e988809ac6152e8fb8097756" + ], + "index": "pypi", + "markers": "python_full_version >= '3.7.0'", + "version": "==0.28.0" + }, "bcrypt": { "hashes": [ "sha256:089098effa1bc35dc055366740a067a2fc76987e8ec75349eb9484061c54f535", @@ -66,6 +130,75 @@ "markers": "python_version >= '3.6'", "version": "==2023.7.22" }, + "cffi": { + "hashes": [ + "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5", + "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef", + "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104", + "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426", + "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405", + "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375", + "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a", + "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e", + "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc", + "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf", + "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185", + "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497", + "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3", + "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35", + "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c", + "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83", + "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21", + "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca", + "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984", + "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac", + "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd", + "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee", + "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a", + "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2", + "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192", + "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7", + "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585", + "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f", + "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e", + "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27", + "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b", + "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e", + "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e", + "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d", + "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c", + "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415", + "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82", + "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02", + "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314", + "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325", + "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c", + "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3", + "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914", + "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045", + "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d", + "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9", + "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5", + "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2", + "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c", + "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3", + "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2", + "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8", + "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d", + "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d", + "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9", + "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162", + "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76", + "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4", + "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e", + "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9", + "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6", + "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b", + "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01", + "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0" + ], + "version": "==1.15.1" + }, "charset-normalizer": { "hashes": [ "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96", @@ -155,6 +288,34 @@ "markers": "python_version >= '3.7'", "version": "==8.1.7" }, + "cryptography": { + "hashes": [ + "sha256:004b6ccc95943f6a9ad3142cfabcc769d7ee38a3f60fb0dddbfb431f818c3a67", + "sha256:047c4603aeb4bbd8db2756e38f5b8bd7e94318c047cfe4efeb5d715e08b49311", + "sha256:0d9409894f495d465fe6fda92cb70e8323e9648af912d5b9141d616df40a87b8", + "sha256:23a25c09dfd0d9f28da2352503b23e086f8e78096b9fd585d1d14eca01613e13", + "sha256:2ed09183922d66c4ec5fdaa59b4d14e105c084dd0febd27452de8f6f74704143", + "sha256:35c00f637cd0b9d5b6c6bd11b6c3359194a8eba9c46d4e875a3660e3b400005f", + "sha256:37480760ae08065437e6573d14be973112c9e6dcaf5f11d00147ee74f37a3829", + "sha256:3b224890962a2d7b57cf5eeb16ccaafba6083f7b811829f00476309bce2fe0fd", + "sha256:5a0f09cefded00e648a127048119f77bc2b2ec61e736660b5789e638f43cc397", + "sha256:5b72205a360f3b6176485a333256b9bcd48700fc755fef51c8e7e67c4b63e3ac", + "sha256:7e53db173370dea832190870e975a1e09c86a879b613948f09eb49324218c14d", + "sha256:7febc3094125fc126a7f6fb1f420d0da639f3f32cb15c8ff0dc3997c4549f51a", + "sha256:80907d3faa55dc5434a16579952ac6da800935cd98d14dbd62f6f042c7f5e839", + "sha256:86defa8d248c3fa029da68ce61fe735432b047e32179883bdb1e79ed9bb8195e", + "sha256:8ac4f9ead4bbd0bc8ab2d318f97d85147167a488be0e08814a37eb2f439d5cf6", + "sha256:93530900d14c37a46ce3d6c9e6fd35dbe5f5601bf6b3a5c325c7bffc030344d9", + "sha256:9eeb77214afae972a00dee47382d2591abe77bdae166bda672fb1e24702a3860", + "sha256:b5f4dfe950ff0479f1f00eda09c18798d4f49b98f4e2006d644b3301682ebdca", + "sha256:c3391bd8e6de35f6f1140e50aaeb3e2b3d6a9012536ca23ab0d9c35ec18c8a91", + "sha256:c880eba5175f4307129784eca96f4e70b88e57aa3f680aeba3bab0e980b0f37d", + "sha256:cecfefa17042941f94ab54f769c8ce0fe14beff2694e9ac684176a2535bf9714", + "sha256:e40211b4923ba5a6dc9769eab704bdb3fbb58d56c5b336d30996c24fcf12aadb", + "sha256:efc8ad4e6fc4f1752ebfb58aefece8b4e3c4cae940b0994d43649bdfce8d0d4f" + ], + "version": "==41.0.4" + }, "dnspython": { "hashes": [ "sha256:57c6fbaaeaaf39c891292012060beb141791735dbb4004798328fc2c467402d8", @@ -168,6 +329,7 @@ "sha256:1ff6e86044200c56ae23595695c54e9614f4a9551e0e393614f764860b3d7900", "sha256:2466ba57cda361fb7309fd3d5a225723c788ca4bbad32a0ebd5373b99730285c" ], + "markers": "python_version >= '3.7'", "version": "==2.0.0.post2" }, "exceptiongroup": { @@ -187,6 +349,96 @@ "markers": "python_version >= '3.7'", "version": "==0.103.1" }, + "fastapi-users": { + "extras": [ + "oauth", + "redis", + "sqlalchemy" + ], + "hashes": [ + "sha256:768e3b17b9b89b5f5d3c303afefd69874da0fc97ebac16a86167d4c8d414d479", + "sha256:9a9b44e0917c483aaba2bf80ea488b4ba6475b0fb24e2abfaf21e0b5c5db59bf" + ], + "markers": "python_version >= '3.8'", + "version": "==12.1.2" + }, + "fastapi-users-db-sqlalchemy": { + "hashes": [ + "sha256:d1050ec31eb75e8c4fa9abafa4addaf0baf5c97afeea2f0f910ea55e2451fcad", + "sha256:f0ef9fe3250453712d25c13170700c80fa205867ce7add7ef391c384ec27cbe1" + ], + "version": "==6.0.1" + }, + "greenlet": { + "hashes": [ + "sha256:03a8f4f3430c3b3ff8d10a2a86028c660355ab637cee9333d63d66b56f09d52a", + "sha256:0bf60faf0bc2468089bdc5edd10555bab6e85152191df713e2ab1fcc86382b5a", + "sha256:1087300cf9700bbf455b1b97e24db18f2f77b55302a68272c56209d5587c12d1", + "sha256:18a7f18b82b52ee85322d7a7874e676f34ab319b9f8cce5de06067384aa8ff43", + "sha256:18e98fb3de7dba1c0a852731c3070cf022d14f0d68b4c87a19cc1016f3bb8b33", + "sha256:1a819eef4b0e0b96bb0d98d797bef17dc1b4a10e8d7446be32d1da33e095dbb8", + "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088", + "sha256:2780572ec463d44c1d3ae850239508dbeb9fed38e294c68d19a24d925d9223ca", + "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343", + "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645", + "sha256:2dd11f291565a81d71dab10b7033395b7a3a5456e637cf997a6f33ebdf06f8db", + "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df", + "sha256:32e5b64b148966d9cccc2c8d35a671409e45f195864560829f395a54226408d3", + "sha256:36abbf031e1c0f79dd5d596bfaf8e921c41df2bdf54ee1eed921ce1f52999a86", + "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2", + "sha256:3a51c9751078733d88e013587b108f1b7a1fb106d402fb390740f002b6f6551a", + "sha256:3c9b12575734155d0c09d6c3e10dbd81665d5c18e1a7c6597df72fd05990c8cf", + "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7", + "sha256:4b58adb399c4d61d912c4c331984d60eb66565175cdf4a34792cd9600f21b394", + "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40", + "sha256:5454276c07d27a740c5892f4907c86327b632127dd9abec42ee62e12427ff7e3", + "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6", + "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74", + "sha256:703f18f3fda276b9a916f0934d2fb6d989bf0b4fb5a64825260eb9bfd52d78f0", + "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3", + "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91", + "sha256:7cafd1208fdbe93b67c7086876f061f660cfddc44f404279c1585bbf3cdc64c5", + "sha256:7efde645ca1cc441d6dc4b48c0f7101e8d86b54c8530141b09fd31cef5149ec9", + "sha256:8512a0c38cfd4e66a858ddd1b17705587900dd760c6003998e9472b77b56d417", + "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8", + "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b", + "sha256:910841381caba4f744a44bf81bfd573c94e10b3045ee00de0cbf436fe50673a6", + "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb", + "sha256:937e9020b514ceedb9c830c55d5c9872abc90f4b5862f89c0887033ae33c6f73", + "sha256:94c817e84245513926588caf1152e3b559ff794d505555211ca041f032abbb6b", + "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df", + "sha256:9d14b83fab60d5e8abe587d51c75b252bcc21683f24699ada8fb275d7712f5a9", + "sha256:9f35ec95538f50292f6d8f2c9c9f8a3c6540bbfec21c9e5b4b751e0a7c20864f", + "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0", + "sha256:acd2162a36d3de67ee896c43effcd5ee3de247eb00354db411feb025aa319857", + "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a", + "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249", + "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30", + "sha256:b9ec052b06a0524f0e35bd8790686a1da006bd911dd1ef7d50b77bfbad74e292", + "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b", + "sha256:bdfea8c661e80d3c1c99ad7c3ff74e6e87184895bbaca6ee8cc61209f8b9b85d", + "sha256:be4ed120b52ae4d974aa40215fcdfde9194d63541c7ded40ee12eb4dda57b76b", + "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c", + "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca", + "sha256:c9c59a2120b55788e800d82dfa99b9e156ff8f2227f07c5e3012a45a399620b7", + "sha256:cd021c754b162c0fb55ad5d6b9d960db667faad0fa2ff25bb6e1301b0b6e6a75", + "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae", + "sha256:d4606a527e30548153be1a9f155f4e283d109ffba663a15856089fb55f933e47", + "sha256:d5508f0b173e6aa47273bdc0a0b5ba055b59662ba7c7ee5119528f466585526b", + "sha256:d75209eed723105f9596807495d58d10b3470fa6732dd6756595e89925ce2470", + "sha256:d967650d3f56af314b72df7089d96cda1083a7fc2da05b375d2bc48c82ab3f3c", + "sha256:db1a39669102a1d8d12b57de2bb7e2ec9066a6f2b3da35ae511ff93b01b5d564", + "sha256:dbfcfc0218093a19c252ca8eb9aee3d29cfdcb586df21049b9d777fd32c14fd9", + "sha256:e0f72c9ddb8cd28532185f54cc1453f2c16fb417a08b53a855c4e6a418edd099", + "sha256:e7c8dc13af7db097bed64a051d2dd49e9f0af495c26995c00a9ee842690d34c0", + "sha256:ea9872c80c132f4663822dd2a08d404073a5a9b5ba6155bea72fb2a79d1093b5", + "sha256:eff4eb9b7eb3e4d0cae3d28c283dc16d9bed6b193c2e1ace3ed86ce48ea8df19", + "sha256:f82d4d717d8ef19188687aa32b8363e96062911e63ba22a0cff7802a8e58e5f1", + "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526" + ], + "markers": "platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32')))))", + "version": "==2.0.2" + }, "h11": { "hashes": [ "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", @@ -197,20 +449,27 @@ }, "httpcore": { "hashes": [ - "sha256:13b5e5cd1dca1a6636a6aaea212b19f4f85cd88c366a2b82304181b769aab3c9", - "sha256:adc5398ee0a476567bf87467063ee63584a8bce86078bf748e48754f60202ced" + "sha256:a6f30213335e34c1ade7be6ec7c47f19f50c56db36abef1a9dfa3815b1cb3888", + "sha256:c2789b767ddddfa2a5782e3199b2b7f6894540b17b16ec26b2c4d8e103510b87" ], - "markers": "python_version >= '3.8'", - "version": "==0.18.0" + "markers": "python_version >= '3.7'", + "version": "==0.17.3" }, "httpx": { "hashes": [ - "sha256:181ea7f8ba3a82578be86ef4171554dd45fec26a02556a744db029a0a27b7100", - "sha256:47ecda285389cb32bb2691cc6e069e3ab0205956f681c5b2ad2325719751d875" + "sha256:06781eb9ac53cde990577af654bd990a4949de37a28bdb4a230d434f3a30b9bd", + "sha256:5853a43053df830c20f8110c5e69fe44d035d850b2dfe795e196f00fdb774bdd" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==0.25.0" + "markers": "python_version >= '3.7'", + "version": "==0.24.1" + }, + "httpx-oauth": { + "hashes": [ + "sha256:245f04bc8340da4184d26ec0d589e35e7e8f5af6157f30b6e012c0896f80f875", + "sha256:ed7359053ac1b1652adbb18b074b6709bcee54e28fb76dd8062c408cfc4589c3" + ], + "version": "==0.13.0" }, "idna": { "hashes": [ @@ -229,14 +488,86 @@ "markers": "python_version >= '2.7'", "version": "==1.3.5" }, - "jose": { - "extras": [ - "cryptography" + "makefun": { + "hashes": [ + "sha256:40b0f118b6ded0d8d78c78f1eb679b8b6b2462e3c1b3e05fb1b2da8cd46b48a5", + "sha256:a63cfc7b47a539c76d97bd4fdb833c7d0461e759fd1225f580cb4be6200294d4" ], + "version": "==1.15.1" + }, + "mako": { "hashes": [ - "sha256:8436c3617cd94e1ba97828fbb1ce27c129f66c78fb855b4bb47e122b5f345fba" + "sha256:c97c79c018b9165ac9922ae4f32da095ffd3c4e6872b45eded42926deea46818", + "sha256:d60a3903dc3bb01a18ad6a89cdbe2e4eadc69c0bc8ef1e3773ba53d44c3f7a34" ], - "version": "==1.0.0" + "markers": "python_version >= '3.7'", + "version": "==1.2.4" + }, + "markupsafe": { + "hashes": [ + "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e", + "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e", + "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431", + "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686", + "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c", + "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559", + "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc", + "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb", + "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939", + "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c", + "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0", + "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4", + "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9", + "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575", + "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba", + "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d", + "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd", + "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3", + "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00", + "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155", + "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac", + "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52", + "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f", + "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8", + "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b", + "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007", + "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24", + "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea", + "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198", + "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0", + "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee", + "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be", + "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2", + "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1", + "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707", + "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6", + "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c", + "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58", + "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823", + "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779", + "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636", + "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c", + "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad", + "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee", + "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc", + "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2", + "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48", + "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7", + "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e", + "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b", + "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa", + "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5", + "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e", + "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb", + "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9", + "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57", + "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc", + "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc", + "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2", + "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11" + ], + "markers": "python_version >= '3.7'", + "version": "==2.1.3" }, "passlib": { "extras": [ @@ -324,16 +655,23 @@ "markers": "python_version >= '3.6'", "version": "==2.9.7" }, + "pycparser": { + "hashes": [ + "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", + "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" + ], + "version": "==2.21" + }, "pydantic": { "extras": [ "email" ], "hashes": [ - "sha256:2b2240c8d54bb8f84b88e061fac1bdfa1761c2859c367f9d3afe0ec2966deddc", - "sha256:b172505886028e4356868d617d2d1a776d7af1625d1313450fd51bdd19d9d61f" + "sha256:94f336138093a5d7f426aac732dcfe7ab4eb4da243c88f891d65deb4a2556ee7", + "sha256:bc3ddf669d234f4220e6e1c4d96b061abe0998185a8d7855c0126782b7abc8c1" ], "markers": "python_version >= '3.7'", - "version": "==2.4.1" + "version": "==2.4.2" }, "pydantic-core": { "hashes": [ @@ -456,6 +794,17 @@ "markers": "python_version >= '3.7'", "version": "==2.0.3" }, + "pyjwt": { + "extras": [ + "crypto" + ], + "hashes": [ + "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de", + "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320" + ], + "markers": "python_version >= '3.7'", + "version": "==2.8.0" + }, "python-dateutil": { "hashes": [ "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", @@ -474,6 +823,21 @@ "markers": "python_version >= '3.8'", "version": "==1.0.0" }, + "python-multipart": { + "hashes": [ + "sha256:e9925a80bb668529f1b67c7fdb0a5dacdd7cbfc6fb0bff3ea443fe22bdd62132", + "sha256:ee698bab5ef148b0a760751c261902cd096e57e10558e11aca17646b74ee1c18" + ], + "markers": "python_version >= '3.7'", + "version": "==0.0.6" + }, + "redis": { + "hashes": [ + "sha256:0dab495cd5753069d3bc650a0dde8a8f9edde16fc5691b689a566eda58100d0f", + "sha256:ed4802971884ae19d640775ba3b03aa2e7bd5e8fb8dfaed2decce4d0fc48391f" + ], + "version": "==5.0.1" + }, "requests": { "hashes": [ "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", @@ -506,6 +870,54 @@ "markers": "python_version >= '3.7'", "version": "==1.3.0" }, + "sqlalchemy": { + "hashes": [ + "sha256:014794b60d2021cc8ae0f91d4d0331fe92691ae5467a00841f7130fe877b678e", + "sha256:0268256a34806e5d1c8f7ee93277d7ea8cc8ae391f487213139018b6805aeaf6", + "sha256:05b971ab1ac2994a14c56b35eaaa91f86ba080e9ad481b20d99d77f381bb6258", + "sha256:141675dae56522126986fa4ca713739d00ed3a6f08f3c2eb92c39c6dfec463ce", + "sha256:1e7dc99b23e33c71d720c4ae37ebb095bebebbd31a24b7d99dfc4753d2803ede", + "sha256:2e617727fe4091cedb3e4409b39368f424934c7faa78171749f704b49b4bb4ce", + "sha256:3cf229704074bce31f7f47d12883afee3b0a02bb233a0ba45ddbfe542939cca4", + "sha256:3eb7c03fe1cd3255811cd4e74db1ab8dca22074d50cd8937edf4ef62d758cdf4", + "sha256:3f7d57a7e140efe69ce2d7b057c3f9a595f98d0bbdfc23fd055efdfbaa46e3a5", + "sha256:419b1276b55925b5ac9b4c7044e999f1787c69761a3c9756dec6e5c225ceca01", + "sha256:44ac5c89b6896f4740e7091f4a0ff2e62881da80c239dd9408f84f75a293dae9", + "sha256:4615623a490e46be85fbaa6335f35cf80e61df0783240afe7d4f544778c315a9", + "sha256:50a69067af86ec7f11a8e50ba85544657b1477aabf64fa447fd3736b5a0a4f67", + "sha256:513fd5b6513d37e985eb5b7ed89da5fd9e72354e3523980ef00d439bc549c9e9", + "sha256:6ff3dc2f60dbf82c9e599c2915db1526d65415be323464f84de8db3e361ba5b9", + "sha256:73c079e21d10ff2be54a4699f55865d4b275fd6c8bd5d90c5b1ef78ae0197301", + "sha256:7614f1eab4336df7dd6bee05bc974f2b02c38d3d0c78060c5faa4cd1ca2af3b8", + "sha256:785e2f2c1cb50d0a44e2cdeea5fd36b5bf2d79c481c10f3a88a8be4cfa2c4615", + "sha256:7ca38746eac23dd7c20bec9278d2058c7ad662b2f1576e4c3dbfcd7c00cc48fa", + "sha256:7f0c4ee579acfe6c994637527c386d1c22eb60bc1c1d36d940d8477e482095d4", + "sha256:87bf91ebf15258c4701d71dcdd9c4ba39521fb6a37379ea68088ce8cd869b446", + "sha256:89e274604abb1a7fd5c14867a412c9d49c08ccf6ce3e1e04fffc068b5b6499d4", + "sha256:8c323813963b2503e54d0944813cd479c10c636e3ee223bcbd7bd478bf53c178", + "sha256:a95aa0672e3065d43c8aa80080cdd5cc40fe92dc873749e6c1cf23914c4b83af", + "sha256:af520a730d523eab77d754f5cf44cc7dd7ad2d54907adeb3233177eeb22f271b", + "sha256:b19ae41ef26c01a987e49e37c77b9ad060c59f94d3b3efdfdbf4f3daaca7b5fe", + "sha256:b4eae01faee9f2b17f08885e3f047153ae0416648f8e8c8bd9bc677c5ce64be9", + "sha256:b69f1f754d92eb1cc6b50938359dead36b96a1dcf11a8670bff65fd9b21a4b09", + "sha256:b977bfce15afa53d9cf6a632482d7968477625f030d86a109f7bdfe8ce3c064a", + "sha256:bf8eebccc66829010f06fbd2b80095d7872991bfe8415098b9fe47deaaa58063", + "sha256:c111cd40910ffcb615b33605fc8f8e22146aeb7933d06569ac90f219818345ef", + "sha256:c2d494b6a2a2d05fb99f01b84cc9af9f5f93bf3e1e5dbdafe4bed0c2823584c1", + "sha256:c9cba4e7369de663611ce7460a34be48e999e0bbb1feb9130070f0685e9a6b66", + "sha256:cca720d05389ab1a5877ff05af96551e58ba65e8dc65582d849ac83ddde3e231", + "sha256:ccb99c3138c9bde118b51a289d90096a3791658da9aea1754667302ed6564f6e", + "sha256:d59cb9e20d79686aa473e0302e4a82882d7118744d30bb1dfb62d3c47141b3ec", + "sha256:e36339a68126ffb708dc6d1948161cea2a9e85d7d7b0c54f6999853d70d44430", + "sha256:ea7da25ee458d8f404b93eb073116156fd7d8c2a776d8311534851f28277b4ce", + "sha256:f9fefd6298433b6e9188252f3bff53b9ff0443c8fde27298b8a2b19f6617eeb9", + "sha256:fb87f763b5d04a82ae84ccff25554ffd903baafba6698e18ebaf32561f2fe4aa", + "sha256:fc6b15465fabccc94bf7e38777d665b6a4f95efd1725049d6184b3a39fd54880" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==2.0.21" + }, "starlette": { "hashes": [ "sha256:6a6b0d042acb8d469a01eba54e9cda6cbd24ac602c4cd016723117d6a7e73b75", @@ -550,6 +962,15 @@ } }, "develop": { + "aiosqlite": { + "hashes": [ + "sha256:95ee77b91c8d2808bd08a59fbebf66270e9090c3d92ffbf260dc0db0b979577d", + "sha256:edba222e03453e094a3ce605db1b970c4b3376264e56f32e2a4959f948d66a96" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==0.19.0" + }, "alabaster": { "hashes": [ "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3", @@ -805,20 +1226,20 @@ }, "httpcore": { "hashes": [ - "sha256:13b5e5cd1dca1a6636a6aaea212b19f4f85cd88c366a2b82304181b769aab3c9", - "sha256:adc5398ee0a476567bf87467063ee63584a8bce86078bf748e48754f60202ced" + "sha256:a6f30213335e34c1ade7be6ec7c47f19f50c56db36abef1a9dfa3815b1cb3888", + "sha256:c2789b767ddddfa2a5782e3199b2b7f6894540b17b16ec26b2c4d8e103510b87" ], - "markers": "python_version >= '3.8'", - "version": "==0.18.0" + "markers": "python_version >= '3.7'", + "version": "==0.17.3" }, "httpx": { "hashes": [ - "sha256:181ea7f8ba3a82578be86ef4171554dd45fec26a02556a744db029a0a27b7100", - "sha256:47ecda285389cb32bb2691cc6e069e3ab0205956f681c5b2ad2325719751d875" + "sha256:06781eb9ac53cde990577af654bd990a4949de37a28bdb4a230d434f3a30b9bd", + "sha256:5853a43053df830c20f8110c5e69fe44d035d850b2dfe795e196f00fdb774bdd" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==0.25.0" + "markers": "python_version >= '3.7'", + "version": "==0.24.1" }, "idna": { "hashes": [ @@ -1217,14 +1638,6 @@ "markers": "python_version >= '3.9'", "version": "==1.1.9" }, - "sqlalchemy-stubs": { - "hashes": [ - "sha256:5eec7aa110adf9b957b631799a72fef396b23ff99fe296df726645d01e312aa5", - "sha256:c665d6dd4482ef642f01027fa06c3d5e91befabb219dc71fc2a09e7d7695f7ae" - ], - "index": "pypi", - "version": "==0.4" - }, "starlette": { "hashes": [ "sha256:6a6b0d042acb8d469a01eba54e9cda6cbd24ac602c4cd016723117d6a7e73b75", @@ -1249,21 +1662,6 @@ "index": "pypi", "version": "==1.7.7.13" }, - "types-pyasn1": { - "hashes": [ - "sha256:8f1965d0b79152f9d1efc89f9aa9a8cdda7cd28b2619df6737c095cbedeff98b", - "sha256:dd5fc818864e63a66cd714be0a7a59a493f4a81b87ee9ac978c41f1eaa9a0cef" - ], - "version": "==0.4.0.6" - }, - "types-python-jose": { - "hashes": [ - "sha256:3c316675c3cee059ccb9aff87358254344915239fa7f19cee2787155a7db14ac", - "sha256:95592273443b45dc5cc88f7c56aa5a97725428753fb738b794e63ccb4904954e" - ], - "index": "pypi", - "version": "==3.3.4.8" - }, "typing-extensions": { "hashes": [ "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0", diff --git a/backend/alembic/env.py b/backend/alembic/env.py index 857bc509..4ae9761b 100644 --- a/backend/alembic/env.py +++ b/backend/alembic/env.py @@ -1,11 +1,12 @@ from __future__ import with_statement import os +from logging.config import fileConfig -from alembic import context from dotenv import load_dotenv from sqlalchemy import engine_from_config, pool -from logging.config import fileConfig + +from alembic import context # Load environment env = os.environ @@ -25,8 +26,8 @@ # target_metadata = mymodel.Base.metadata # target_metadata = None -from app.db.base import Base # noqa import app.models # noqa +from app.db.base import Base # noqa target_metadata = Base.metadata @@ -76,13 +77,13 @@ def run_migrations_online(): configuration = config.get_section(config.config_ini_section) configuration["sqlalchemy.url"] = get_url() connectable = engine_from_config( - configuration, prefix="sqlalchemy.", poolclass=pool.NullPool, + configuration, + prefix="sqlalchemy.", + poolclass=pool.NullPool, ) with connectable.connect() as connection: - context.configure( - connection=connection, target_metadata=target_metadata, compare_type=True - ) + context.configure(connection=connection, target_metadata=target_metadata, compare_type=True) with context.begin_transaction(): context.run_migrations() diff --git a/backend/alembic/script.py.mako b/backend/alembic/script.py.mako index 72303dfc..3e043449 100644 --- a/backend/alembic/script.py.mako +++ b/backend/alembic/script.py.mako @@ -9,7 +9,7 @@ from alembic import op import sqlalchemy as sa ${imports if imports else ""} -import app.models.guid # noqa +import fastapi_users_db_sqlalchemy.generics # noqa # revision identifiers, used by Alembic. revision = ${repr(up_revision)} diff --git a/backend/alembic/versions/315675882512_.py b/backend/alembic/versions/315675882512_.py deleted file mode 100644 index dc826881..00000000 --- a/backend/alembic/versions/315675882512_.py +++ /dev/null @@ -1,43 +0,0 @@ -"""empty message - -Revision ID: 315675882512 -Revises: -Create Date: 2023-09-27 14:55:20.332132+02:00 - -""" -from alembic import op -import sqlalchemy as sa - - -import app.models.guid # noqa - -# revision identifiers, used by Alembic. -revision = '315675882512' -down_revision = None -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('adminmessages', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('uuid', app.models.guid.GUID(length=36), nullable=False), - sa.Column('title', sa.String(length=255), nullable=False), - sa.Column('text', sa.Text(), nullable=True), - sa.Column('active_start', sa.DateTime(), nullable=False), - sa.Column('active_stop', sa.DateTime(), nullable=False), - sa.Column('enabled', sa.Boolean(), nullable=False), - sa.PrimaryKeyConstraint('id') - ) - op.create_index(op.f('ix_adminmessages_id'), 'adminmessages', ['id'], unique=False) - op.create_index(op.f('ix_adminmessages_uuid'), 'adminmessages', ['uuid'], unique=True) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_index(op.f('ix_adminmessages_uuid'), table_name='adminmessages') - op.drop_index(op.f('ix_adminmessages_id'), table_name='adminmessages') - op.drop_table('adminmessages') - # ### end Alembic commands ### diff --git a/backend/alembic/versions/8ccd31a4f116_init_adminmsgs.py b/backend/alembic/versions/8ccd31a4f116_init_adminmsgs.py new file mode 100644 index 00000000..2430feb7 --- /dev/null +++ b/backend/alembic/versions/8ccd31a4f116_init_adminmsgs.py @@ -0,0 +1,40 @@ +"""init adminmsgs + +Revision ID: 8ccd31a4f116 +Revises: c8009ed33089 +Create Date: 2023-09-28 10:41:57.671434+02:00 + +""" +import fastapi_users_db_sqlalchemy.generics # noqa +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "8ccd31a4f116" +down_revision = "c8009ed33089" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "adminmessages", + sa.Column("id", fastapi_users_db_sqlalchemy.generics.GUID(), nullable=False), + sa.Column("title", sa.String(length=255), nullable=False), + sa.Column("text", sa.Text(), nullable=True), + sa.Column("active_start", sa.DateTime(), nullable=False), + sa.Column("active_stop", sa.DateTime(), nullable=False), + sa.Column("enabled", sa.Boolean(), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_adminmessages_id"), "adminmessages", ["id"], unique=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f("ix_adminmessages_id"), table_name="adminmessages") + op.drop_table("adminmessages") + # ### end Alembic commands ### diff --git a/backend/alembic/versions/c8009ed33089_init_users.py b/backend/alembic/versions/c8009ed33089_init_users.py new file mode 100644 index 00000000..94f450b4 --- /dev/null +++ b/backend/alembic/versions/c8009ed33089_init_users.py @@ -0,0 +1,62 @@ +"""init users + +Revision ID: c8009ed33089 +Revises: +Create Date: 2023-09-28 10:41:24.861855+02:00 + +""" +import fastapi_users_db_sqlalchemy.generics # noqa +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "c8009ed33089" +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "user", + sa.Column("id", fastapi_users_db_sqlalchemy.generics.GUID(), nullable=False), + sa.Column("email", sa.String(length=320), nullable=False), + sa.Column("hashed_password", sa.String(length=1024), nullable=False), + sa.Column("is_active", sa.Boolean(), nullable=False), + sa.Column("is_superuser", sa.Boolean(), nullable=False), + sa.Column("is_verified", sa.Boolean(), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_user_email"), "user", ["email"], unique=True) + op.create_table( + "oauth_account", + sa.Column("id", fastapi_users_db_sqlalchemy.generics.GUID(), nullable=False), + sa.Column("user_id", fastapi_users_db_sqlalchemy.generics.GUID(), nullable=False), + sa.Column("oauth_name", sa.String(length=100), nullable=False), + sa.Column("access_token", sa.String(length=1024), nullable=False), + sa.Column("expires_at", sa.Integer(), nullable=True), + sa.Column("refresh_token", sa.String(length=1024), nullable=True), + sa.Column("account_id", sa.String(length=320), nullable=False), + sa.Column("account_email", sa.String(length=320), nullable=False), + sa.ForeignKeyConstraint(["user_id"], ["user.id"], ondelete="cascade"), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index( + op.f("ix_oauth_account_account_id"), "oauth_account", ["account_id"], unique=False + ) + op.create_index( + op.f("ix_oauth_account_oauth_name"), "oauth_account", ["oauth_name"], unique=False + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f("ix_oauth_account_oauth_name"), table_name="oauth_account") + op.drop_index(op.f("ix_oauth_account_account_id"), table_name="oauth_account") + op.drop_table("oauth_account") + op.drop_index(op.f("ix_user_email"), table_name="user") + op.drop_table("user") + # ### end Alembic commands ### diff --git a/backend/app/api/api_v1/api.py b/backend/app/api/api_v1/api.py index 59e4572f..8f447cfb 100644 --- a/backend/app/api/api_v1/api.py +++ b/backend/app/api/api_v1/api.py @@ -1,5 +1,35 @@ -from app.api.api_v1.endpoints import adminmsgs from fastapi import APIRouter +from app.api.api_v1.endpoints import adminmsgs +from app.core.auth import auth_backend_bearer, auth_backend_cookie, fastapi_users +from app.schemas.user import UserCreate, UserRead, UserUpdate + api_router = APIRouter() api_router.include_router(adminmsgs.router, prefix="/adminmsgs", tags=["adminmsgs"]) + +api_router.include_router( + fastapi_users.get_auth_router(auth_backend_bearer), prefix="/auth/bearer", tags=["auth"] +) +api_router.include_router( + fastapi_users.get_auth_router(auth_backend_cookie), prefix="/auth/cookie", tags=["auth"] +) +# api_router.include_router( +# fastapi_users.get_register_router(UserRead, UserCreate), +# prefix="/auth", +# tags=["auth"], +# ) +# api_router.include_router( +# fastapi_users.get_reset_password_router(), +# prefix="/auth", +# tags=["auth"], +# ) +# api_router.include_router( +# fastapi_users.get_verify_router(UserRead), +# prefix="/auth", +# tags=["auth"], +# ) +api_router.include_router( + fastapi_users.get_users_router(UserRead, UserUpdate), + prefix="/users", + tags=["users"], +) diff --git a/backend/app/api/api_v1/endpoints/adminmsgs.py b/backend/app/api/api_v1/endpoints/adminmsgs.py index dfc4f9b7..743ad10c 100644 --- a/backend/app/api/api_v1/endpoints/adminmsgs.py +++ b/backend/app/api/api_v1/endpoints/adminmsgs.py @@ -1,20 +1,21 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.ext.asyncio import AsyncSession + from app import crud, models, schemas from app.api import deps -from fastapi import APIRouter, Depends -from sqlalchemy.orm import Session router = APIRouter() -@router.get("/", response_model=list[schemas.AdminMessage]) -def read_adminmsgs( - db: Session = Depends(deps.get_db), +@router.get("/", response_model=list[schemas.AdminMessageRead]) +async def read_adminmsgs( + db: AsyncSession = Depends(deps.get_db), skip: int = 0, limit: int = 100, -) -> list[schemas.AdminMessage]: +) -> list[schemas.AdminMessageRead]: """Retrieve all admin messages""" users = [ - schemas.AdminMessage.model_validate(db_obj) - for db_obj in crud.adminmessage.get_multi(db, skip=skip, limit=limit) + schemas.AdminMessageRead.model_validate(db_obj) + for db_obj in await crud.adminmessage.get_multi(db, skip=skip, limit=limit) ] return users diff --git a/backend/app/api/deps.py b/backend/app/api/deps.py index 9917a1d0..319a901d 100644 --- a/backend/app/api/deps.py +++ b/backend/app/api/deps.py @@ -1,11 +1,19 @@ -from typing import Iterator +from typing import AsyncGenerator, AsyncIterator -from app.db.session import SessionLocal +from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker +from app.db.session import SessionLocal, engine -def get_db() -> Iterator[SessionLocal]: # type: ignore[valid-type] - try: - db = SessionLocal() - yield db - finally: - db.close() + +async def get_db() -> AsyncIterator[SessionLocal]: # type: ignore[valid-type] + db = SessionLocal() + yield db + await db.close() + + +async_session_maker = async_sessionmaker(engine, expire_on_commit=False) + + +async def get_async_session() -> AsyncGenerator[AsyncSession, None]: + async with async_session_maker() as session: + yield session diff --git a/backend/app/api/internal/api.py b/backend/app/api/internal/api.py index 0b490fd3..33ee4a54 100644 --- a/backend/app/api/internal/api.py +++ b/backend/app/api/internal/api.py @@ -1,8 +1,9 @@ import subprocess +from fastapi import APIRouter, Response + from app.api.internal.endpoints import proxy, remote from app.core.config import settings -from fastapi import APIRouter, Response api_router = APIRouter() @@ -11,6 +12,7 @@ @api_router.get("/version") +@api_router.post("/version") async def version(): """Return REEV software version""" if settings.REEV_VERSION: diff --git a/backend/app/api/internal/endpoints/proxy.py b/backend/app/api/internal/endpoints/proxy.py index c616510a..3d9e8fd5 100644 --- a/backend/app/api/internal/endpoints/proxy.py +++ b/backend/app/api/internal/endpoints/proxy.py @@ -1,11 +1,12 @@ """Reverse proxies to internal services.""" import httpx -from app.core.config import settings from fastapi import APIRouter, BackgroundTasks, Request, Response from fastapi.responses import StreamingResponse from starlette.background import BackgroundTask +from app.core.config import settings + router = APIRouter() diff --git a/backend/app/backend_pre_start.py b/backend/app/backend_pre_start.py index 0cee7ff0..69986bcf 100644 --- a/backend/app/backend_pre_start.py +++ b/backend/app/backend_pre_start.py @@ -1,9 +1,10 @@ import logging -from app.db.session import SessionLocal from sqlalchemy import text from tenacity import after_log, before_log, retry, stop_after_attempt, wait_fixed +from app.db.session import SyncSessionLocal + logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -19,7 +20,7 @@ ) def init() -> None: try: - db = SessionLocal() + db = SyncSessionLocal() # Try to create session to check if DB is awake db.execute(text("SELECT 1")) except Exception as e: diff --git a/backend/app/core/auth.py b/backend/app/core/auth.py new file mode 100644 index 00000000..691574c1 --- /dev/null +++ b/backend/app/core/auth.py @@ -0,0 +1,71 @@ +import uuid + +import redis.asyncio +from fastapi import Depends, Request +from fastapi_users import BaseUserManager, FastAPIUsers, UUIDIDMixin +from fastapi_users.authentication import ( + AuthenticationBackend, + BearerTransport, + CookieTransport, + RedisStrategy, +) +from fastapi_users_db_sqlalchemy import SQLAlchemyUserDatabase +from sqlalchemy.ext.asyncio import AsyncSession + +from app.api.deps import get_async_session, get_db +from app.core.config import settings +from app.models.user import OAuthAccount, User + + +async def get_user_db(session: AsyncSession = Depends(get_async_session)): + yield SQLAlchemyUserDatabase(session, User, OAuthAccount) + + +class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]): + reset_password_token_secret = settings.SECRET_KEY + verification_token_secret = settings.SECRET_KEY + + async def on_after_register(self, user: User, request: Request | None = None): + print(f"User {user.id} has registered.") + + async def on_after_forgot_password( + self, user: User, token: str, request: Request | None = None + ): + print(f"User {user.id} has forgot their password. Reset token: {token}") + + async def on_after_request_verify(self, user: User, token: str, request: Request | None = None): + print(f"Verification requested for user {user.id}. Verification token: {token}") + + +async def get_user_manager(user_db: SQLAlchemyUserDatabase = Depends(get_user_db)): + yield UserManager(user_db) + + +bearer_transport = BearerTransport(tokenUrl=f"{settings.API_V1_STR}/auth/login") + +cookie_transport = CookieTransport(cookie_max_age=settings.SESSION_EXPIRE_MINUTES * 60) + +redis_obj = redis.asyncio.from_url(settings.REDIS_URL, decode_responses=True) + + +def get_redis_strategy() -> RedisStrategy: + return RedisStrategy(redis_obj, lifetime_seconds=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60) + + +auth_backend_bearer = AuthenticationBackend( + name="bearer", + transport=bearer_transport, + get_strategy=get_redis_strategy, +) + +auth_backend_cookie = AuthenticationBackend( + name="cookie", + transport=cookie_transport, + get_strategy=get_redis_strategy, +) + +fastapi_users = FastAPIUsers[User, uuid.UUID]( + get_user_manager, [auth_backend_bearer, auth_backend_cookie] +) + +# current_active_user = fastapi_users.current_user(active=True) diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 7a383573..2c8cc07b 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -48,7 +48,9 @@ def assemble_reev_version(cls, v: str | None, info: ValidationInfo) -> str | Non # == security-related settings == #: Secret key - SECRET_KEY: str = secrets.token_urlsafe(32) + SECRET_KEY: str = secrets.token_urlsafe(32) # TODO: load from config + #: Expiration of cookies. + SESSION_EXPIRE_MINUTES: int = 60 * 24 * 8 #: Expiry of access token (60 minutes * 24 hours * 8 days = 8 days) ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8 #: Server hostname @@ -68,23 +70,30 @@ def assemble_cors_origins(cls, v: str | list[str]) -> list[str] | str: # pragma return v raise ValueError(v) - # == backend-related settings == + # == backend-related settings (defaults for production) == - #: Prefix for the backend of annonars service, default is for dev. - BACKEND_PREFIX_ANNONARS: str = "http://localhost:3001" - #: Prefix for the backend of mehari service, default is for dev. - BACKEND_PREFIX_MEHARI: str = "http://localhost:3002" - #: Prefix for the backend of viguno service, default is for dev. - BACKEND_PREFIX_VIGUNO: str = "http://localhost:3003" - #: Prefix for the backend of nginx service, default is for dev. - BACKEND_PREFIX_NGINX: str = "http://localhost:3004" + #: Prefix for the backend of annonars service. + BACKEND_PREFIX_ANNONARS: str = "http://annonars:8080" + #: Prefix for the backend of mehari service. + BACKEND_PREFIX_MEHARI: str = "http://mehari:8080" + #: Prefix for the backend of viguno service. + BACKEND_PREFIX_VIGUNO: str = "http://viguno:8080" + #: Prefix for the backend of nginx service. + BACKEND_PREFIX_NGINX: str = "http://nginx:80" + + #: URL to REDIS service. + REDIS_URL: str = "redis://redis:5379" # -- User-Related Configuration --------------------------------------------- - # FIRST_SUPERUSER: EmailStr - # FIRST_SUPERUSER_PASSWORD: str - # USERS_OPEN_REGISTRATION: bool = False - # EMAIL_TEST_USER: EmailStr = "test@example.com" # type: ignore + #: Superuser email, created on startup. + FIRST_SUPERUSER_EMAIL: EmailStr | None = None + #: Superuser password, created on startup. + FIRST_SUPERUSER_PASSWORD: str | None = None + #: Whether to allow open registration of new users. + USERS_OPEN_REGISTRATION: bool = False + #: Email of test users, ignored. + EMAIL_TEST_USER: EmailStr = "test@example.com" # type: ignore # -- Database Configuration ---------------------------------------------- @@ -107,12 +116,12 @@ def assemble_cors_origins(cls, v: str | list[str]) -> list[str] | str: # pragma @field_validator("SQLALCHEMY_DATABASE_URI", mode="before") def assemble_db_connection(cls, v: str | None, info: ValidationInfo) -> Any: if os.environ.get("CI") == "true": # pragma: no cover - return "sqlite://" + return "sqlite+aiosqlite://" elif isinstance(v, str): # pragma: no cover return v else: return PostgresDsn.build( - scheme="postgresql", + scheme="postgresql+asyncpg", username=info.data.get("POSTGRES_USER"), password=info.data.get("POSTGRES_PASSWORD"), host=info.data.get("POSTGRES_HOST"), diff --git a/backend/app/crud/base.py b/backend/app/crud/base.py index 61dcc2ca..623addb1 100644 --- a/backend/app/crud/base.py +++ b/backend/app/crud/base.py @@ -1,8 +1,10 @@ from typing import Any, Generic, Type, TypeVar -from app.models.utils.helpers import ModelType, sa_model_to_dict from pydantic import BaseModel -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.future import select + +from app.models.utils.helpers import ModelType, sa_model_to_dict CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel) UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel) @@ -20,22 +22,29 @@ def __init__(self, model: Type[ModelType]): """ self.model = model - def get(self, db: Session, id: Any) -> ModelType | None: - return db.query(self.model).filter(self.model.id == id).first() + async def get(self, session: AsyncSession, id: Any) -> ModelType | None: + query = select(self.model).filter(self.model.id == id) + result = await session.execute(query) + return result.scalars().first() - def get_multi(self, db: Session, *, skip: int = 0, limit: int = 100) -> list[ModelType]: - return db.query(self.model).offset(skip).limit(limit).all() + async def get_multi( + self, session: AsyncSession, *, skip: int = 0, limit: int = 100 + ) -> list[ModelType]: + query = select(self.model).offset(skip).limit(limit) + result = await session.execute(query) + all_scalars: list[ModelType] = result.scalars().all() # type: ignore[assignment] + return all_scalars - def create(self, db: Session, *, obj_in: CreateSchemaType) -> ModelType: + async def create(self, session: AsyncSession, *, obj_in: CreateSchemaType) -> ModelType: obj_in_data = obj_in.model_dump() - db_obj = self.model(**obj_in_data) # type: ignore - db.add(db_obj) - db.commit() - db.refresh(db_obj) + db_obj = self.model(**obj_in_data) + session.add(db_obj) + await session.commit() + await session.refresh(db_obj) return db_obj - def update( - self, db: Session, *, db_obj: ModelType, obj_in: UpdateSchemaType | dict[str, Any] + async def update( + self, session: AsyncSession, *, db_obj: ModelType, obj_in: UpdateSchemaType | dict[str, Any] ) -> ModelType: obj_data = sa_model_to_dict(db_obj) if isinstance(obj_in, dict): @@ -45,14 +54,14 @@ def update( for field in obj_data: if field in update_data: setattr(db_obj, field, update_data[field]) - db.add(db_obj) - db.commit() - db.refresh(db_obj) + session.add(db_obj) + await session.commit() + await session.refresh(db_obj) return db_obj - def remove(self, db: Session, *, id: int) -> ModelType | None: - obj = db.query(self.model).get(id) + async def remove(self, session: AsyncSession, *, id: Any) -> ModelType | None: + obj = await session.get(self.model, id) if obj: - db.delete(obj) - db.commit() + await session.delete(obj) + await session.commit() return obj diff --git a/backend/app/db/base.py b/backend/app/db/base.py index 9acb6047..6ff940e4 100644 --- a/backend/app/db/base.py +++ b/backend/app/db/base.py @@ -1 +1,2 @@ +# Re-export ``Base`` for convenience. from app.db.session import Base # noqa # pragma: no cover diff --git a/backend/app/db/init_db.py b/backend/app/db/init_db.py index 452679c4..35e8285a 100644 --- a/backend/app/db/init_db.py +++ b/backend/app/db/init_db.py @@ -1,25 +1,44 @@ -from app import crud, schemas +import contextlib +import logging + +from fastapi_users.exceptions import UserAlreadyExists + +from app.api.deps import get_async_session +from app.core.auth import get_user_db, get_user_manager from app.core.config import settings -from app.db import base # noqa: F401 -from sqlalchemy.orm import Session - -# make sure all SQL Alchemy models are imported (app.db.base) before initializing DB -# otherwise, SQL Alchemy might fail to initialize relationships properly -# for more details: https://github.com/tiangolo/full-stack-fastapi-postgresql/issues/28 - - -def init_db(db: Session) -> None: - # Tables should be created with Alembic migrations - # But if you don't want to use migrations, create - # the tables un-commenting the next line - # Base.metadata.create_all(bind=engine) - - pass - # user = crud.user.get_by_email(db, email=settings.FIRST_SUPERUSER) - # if not user: - # user_in = schemas.UserCreate( - # email=settings.FIRST_SUPERUSER, - # password=settings.FIRST_SUPERUSER_PASSWORD, - # is_superuser=True, - # ) - # user = crud.user.create(db, obj_in=user_in) # noqa: F841 +from app.db.session import SessionLocal +from app.schemas import UserCreate + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +get_async_session_context = contextlib.asynccontextmanager(get_async_session) +get_user_db_context = contextlib.asynccontextmanager(get_user_db) +get_user_manager_context = contextlib.asynccontextmanager(get_user_manager) + + +async def create_user(email: str, password: str, is_superuser: bool = False): + """Create user with the given email and password and superusers status.""" + try: + async with get_async_session_context() as session: + async with get_user_db_context(session) as user_db: + async with get_user_manager_context(user_db) as user_manager: + user = await user_manager.create( + UserCreate(email=email, password=password, is_superuser=is_superuser) + ) + logger.info(f"User created {email}") + except UserAlreadyExists: + logger.info(f"User {email} already exists") + + +async def create_superuser(): + if settings.FIRST_SUPERUSER_EMAIL and settings.FIRST_SUPERUSER_PASSWORD: + await create_user( + email=settings.FIRST_SUPERUSER_EMAIL, + password=settings.FIRST_SUPERUSER_PASSWORD, + is_superuser=True, + ) + + +async def init_db(): + await create_superuser() diff --git a/backend/app/db/session.py b/backend/app/db/session.py index f2ee1f87..d88c021b 100644 --- a/backend/app/db/session.py +++ b/backend/app/db/session.py @@ -1,8 +1,19 @@ -from app.core.config import settings from sqlalchemy import create_engine -from sqlalchemy.orm import declarative_base, sessionmaker # type: ignore[attr-defined] +from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine +from sqlalchemy.orm import declarative_base, sessionmaker + +from app.core.config import settings + +#: Async engine, to be used throughout the app. +engine = create_async_engine(str(settings.SQLALCHEMY_DATABASE_URI), pool_pre_ping=True) +# Local async session, to be used throughout the app. +SessionLocal = async_sessionmaker( + autocommit=False, autoflush=False, expire_on_commit=False, bind=engine +) -engine = create_engine(str(settings.SQLALCHEMY_DATABASE_URI), pool_pre_ping=True) -SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +#: Sync engine for Alembic migrations. +sync_engine = create_engine(str(settings.SQLALCHEMY_DATABASE_URI), pool_pre_ping=True) +# Sync session, to be used for Alembic migrations. +SyncSessionLocal = sessionmaker(autocommit=False, expire_on_commit=False, bind=sync_engine) Base = declarative_base() diff --git a/backend/app/initial_data.py b/backend/app/initial_data.py index c50646d2..dd55b04b 100644 --- a/backend/app/initial_data.py +++ b/backend/app/initial_data.py @@ -1,22 +1,17 @@ +import asyncio import logging from app.db.init_db import init_db -from app.db.session import SessionLocal logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) -def init() -> None: - db = SessionLocal() - init_db(db) - - -def main() -> None: +async def main() -> None: logger.info("Creating initial data") - init() + await init_db() logger.info("Initial data created") if __name__ == "__main__": - main() + asyncio.run(main()) diff --git a/backend/app/main.py b/backend/app/main.py index 68f551c3..48fed40a 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,16 +1,24 @@ import logging import pathlib -from app.api.api_v1.api import api_router as api_v1_router -from app.api.internal.api import api_router as internal_router -from app.core.config import settings -from fastapi import FastAPI +from fastapi import FastAPI, Request from fastapi.staticfiles import StaticFiles from starlette.middleware.cors import CORSMiddleware -from starlette.requests import Request from starlette.responses import FileResponse -app = FastAPI(title="REEV", openapi_url=f"{settings.API_V1_STR}/openapi.json") +from app.api.api_v1.api import api_router as api_v1_router +from app.api.internal.api import api_router as internal_router +from app.core.config import settings +from app.db.init_db import create_superuser + +logger = logging.getLogger(__name__) + +app = FastAPI( + title="REEV", + openapi_url=f"{settings.API_V1_STR}/openapi.json", + docs_url=f"{settings.API_V1_STR}/docs", + debug=settings.DEBUG, +) # Set all CORS enabled origins if settings.BACKEND_CORS_ORIGINS: @@ -28,6 +36,11 @@ app.include_router(api_v1_router, prefix=settings.API_V1_STR) +@app.on_event("startup") +async def create_superuser_on_startup(): + await create_superuser() + + @app.get("/favicon.ico", include_in_schema=False) async def favicon(): """Serve favicon""" diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index 67bc0542..a60fac75 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -1 +1,2 @@ from app.models.adminmsg import AdminMessage # noqa +from app.models.user import OAuthAccount, User # noqa diff --git a/backend/app/models/adminmsg.py b/backend/app/models/adminmsg.py index 43623d5d..7c433cdb 100644 --- a/backend/app/models/adminmsg.py +++ b/backend/app/models/adminmsg.py @@ -1,11 +1,16 @@ """Models for admin messages.""" -import uuid +import datetime +import uuid as uuid_module from typing import TYPE_CHECKING +from fastapi_users_db_sqlalchemy.generics import GUID # noqa +from sqlalchemy import Boolean, Column, DateTime, String, Text +from sqlalchemy.orm import Mapped, mapped_column + from app.db.session import Base -from app.models.utils.guid import GUID -from sqlalchemy import Boolean, Column, DateTime, Integer, String, Text + +UUID_ID = uuid_module.UUID class AdminMessage(Base): @@ -16,19 +21,24 @@ class AdminMessage(Base): __tablename__ = "adminmessages" - #: Primary key. - id = Column(Integer, primary_key=True, index=True) - #: UUID for external reference. - uuid = Column( - GUID(36), default=lambda: str(uuid.uuid4()), nullable=False, unique=True, index=True - ) - #: Message title. - title = Column(String(255), nullable=False) - #: The message's text. - text = Column(Text, nullable=True) - #: When to start displaying the message. - active_start = Column(DateTime, nullable=False) - #: When to stop displaying the message. - active_stop = Column(DateTime, nullable=False) - #: Whether the message is enabled at all. - enabled = Column(Boolean, default=True, nullable=False) + if TYPE_CHECKING: # pragma: no cover + id: UUID_ID + title: str + text: str + active_start: datetime.datetime + active_stop: datetime.datetime + else: + #: UUID of the message. + id: Mapped[UUID_ID] = mapped_column( + GUID, primary_key=True, index=True, default=uuid_module.uuid4 + ) + #: Message title. + title = Column(String(255), nullable=False) + #: The message's text. + text = Column(Text, nullable=True) + #: When to start displaying the message. + active_start = Column(DateTime, nullable=False) + #: When to stop displaying the message. + active_stop = Column(DateTime, nullable=False) + #: Whether the message is enabled at all. + enabled = Column(Boolean, default=True, nullable=False) diff --git a/backend/app/models/user.py b/backend/app/models/user.py new file mode 100644 index 00000000..f7141116 --- /dev/null +++ b/backend/app/models/user.py @@ -0,0 +1,17 @@ +from typing import List + +from fastapi_users_db_sqlalchemy import ( + SQLAlchemyBaseOAuthAccountTableUUID, + SQLAlchemyBaseUserTableUUID, +) +from sqlalchemy.orm import Mapped, relationship + +from app.db.base import Base + + +class OAuthAccount(SQLAlchemyBaseOAuthAccountTableUUID, Base): + pass + + +class User(SQLAlchemyBaseUserTableUUID, Base): + oauth_accounts: Mapped[List[OAuthAccount]] = relationship("OAuthAccount", lazy="joined") diff --git a/backend/app/models/utils/guid.py b/backend/app/models/utils/guid.py deleted file mode 100644 index 26b27d3c..00000000 --- a/backend/app/models/utils/guid.py +++ /dev/null @@ -1,50 +0,0 @@ -"""Helper to provide UUIDs that also work with SQLite - -Source: https://gist.github.com/gmolveau/7caeeefe637679005a7bb9ae1b5e421e -""" - -import typing -import uuid - -from sqlalchemy.dialects.postgresql import UUID -from sqlalchemy.engine.interfaces import Dialect -from sqlalchemy.sql.type_api import TypeEngine -from sqlalchemy.types import CHAR, TypeDecorator - - -class GUID(TypeDecorator): - """Platform-independent GUID type. - - Uses PostgreSQL's UUID type, otherwise uses - CHAR(32), storing as stringified hex values. - - """ - - impl = CHAR - - def load_dialect_impl(self, dialect: typing.Any) -> TypeEngine: - if dialect.name == "postgresql": - return dialect.type_descriptor(UUID()) - else: - return dialect.type_descriptor(CHAR(32)) - - def process_bind_param(self, value: typing.Any, dialect: typing.Any) -> str | None: - if value is None: - return value - elif dialect.name == "postgresql": - return str(value) - else: - if not isinstance(value, uuid.UUID): - return "%.32x" % uuid.UUID(value).int - else: - # hexstring - return "%.32x" % value.int - - def process_result_value(self, value: typing.Any, dialect: typing.Any) -> uuid.UUID | None: - _ = dialect - if value is None: - return value - else: - if not isinstance(value, uuid.UUID): - value = uuid.UUID(value) - return value diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py index 2d164f06..d5735761 100644 --- a/backend/app/schemas/__init__.py +++ b/backend/app/schemas/__init__.py @@ -1 +1,2 @@ -from app.schemas.adminmsg import AdminMessage, AdminMessageCreate, AdminMessageUpdate # noqa +from app.schemas.adminmsg import AdminMessageCreate, AdminMessageRead, AdminMessageUpdate # noqa +from app.schemas.user import UserCreate, UserRead, UserUpdate # noqa diff --git a/backend/app/schemas/adminmsg.py b/backend/app/schemas/adminmsg.py index ee49623c..8b66ab59 100644 --- a/backend/app/schemas/adminmsg.py +++ b/backend/app/schemas/adminmsg.py @@ -1,7 +1,7 @@ import datetime from uuid import UUID -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict class AdminMessageBase(BaseModel): @@ -21,14 +21,13 @@ class AdminMessageUpdate(AdminMessageBase): class AdminMessageInDbBase(AdminMessageBase): + model_config = ConfigDict(from_attributes=True) + id: int uuid: UUID - class Config: - from_attributes = True - -class AdminMessage(AdminMessageInDbBase): +class AdminMessageRead(AdminMessageInDbBase): pass diff --git a/backend/app/schemas/user.py b/backend/app/schemas/user.py new file mode 100644 index 00000000..de1169e4 --- /dev/null +++ b/backend/app/schemas/user.py @@ -0,0 +1,15 @@ +import uuid + +from fastapi_users import schemas + + +class UserRead(schemas.BaseUser[uuid.UUID]): + pass + + +class UserCreate(schemas.BaseUserCreate): + pass + + +class UserUpdate(schemas.BaseUserUpdate): + pass diff --git a/backend/env.dev b/backend/env.dev index d34ca25c..05de635c 100644 --- a/backend/env.dev +++ b/backend/env.dev @@ -1,7 +1,7 @@ # Application configuration SERVER_NAME=localhost SERVER_HOST=http://localhost:8080 -BACKEND_CORS_ORIGINS=["*"] +BACKEND_CORS_ORIGINS=["http://localhost:8081"] DEBUG=1 # Postgres configuration for dev, defaults as in reev-docker-compose @@ -10,3 +10,13 @@ POSTGRES_PASSWORD=db-password POSTGRES_HOST=localhost POSTGRES_PORT=3020 POSTGRES_DB=reev + +# Prefixes to the backend services running in Docker Compose. +BACKEND_PREFIX_ANNONARS=http://localhost:3001 +BACKEND_PREFIX_MEHARI=http://localhost:3002 +BACKEND_PREFIX_VIGUNO=http://localhost:3003 +BACKEND_PREFIX_NGINX=http://localhost:3004 + +# Superuser to setup on startup +FIRST_SUPERUSER_EMAIL=admin@example.com +FIRST_SUPERUSER_PASSWORD=password diff --git a/backend/login.sh b/backend/login.sh new file mode 100644 index 00000000..e464920b --- /dev/null +++ b/backend/login.sh @@ -0,0 +1,7 @@ +curl \ +-v \ +-H "Content-Type: multipart/form-data" \ +-X POST \ +-F "username=manuel.holtgrewe@bih-charite.de" \ +-F "password=password" \ +http://localhost:8080/api/v1/auth/cookie/login diff --git a/backend/setup.cfg b/backend/setup.cfg index d1bb5a37..3d67d74f 100644 --- a/backend/setup.cfg +++ b/backend/setup.cfg @@ -1,6 +1,18 @@ [tool:pytest] testpaths = tests +filterwarnings = + ignore:.*pkg_resources is deprecated as an API.*:DeprecationWarning + ignore:.*pkg_resources.declare_namespace.*:DeprecationWarning + +[tool:isort] +profile = "black" +multi_line_output = 3 +include_trailing_comma = true +force_grid_wrap = 0 +use_parentheses = true +ensure_newline_before_comments = true +line_length = 100 [coverage:run] omit = diff --git a/backend/tests/api/api_v1/test_adminmsgs.py b/backend/tests/api/api_v1/test_adminmsgs.py index 21833610..b334b585 100644 --- a/backend/tests/api/api_v1/test_adminmsgs.py +++ b/backend/tests/api/api_v1/test_adminmsgs.py @@ -1,12 +1,14 @@ import pytest -from app.core.config import settings from fastapi.testclient import TestClient -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.config import settings @pytest.mark.asyncio -async def test_adminmsgs_list(db: Session, client: TestClient): +async def test_adminmsgs_list(db_session: AsyncSession, client: TestClient): """Test proxying to annonars backend.""" + _ = db_session # via ``get_db()`` dependency injection response = client.get(f"{settings.API_V1_STR}/adminmsgs/") assert response.status_code == 200 assert response.json() == [] diff --git a/backend/tests/api/internal/test_proxy.py b/backend/tests/api/internal/test_proxy.py index 08a60a57..399dd1cb 100644 --- a/backend/tests/api/internal/test_proxy.py +++ b/backend/tests/api/internal/test_proxy.py @@ -1,10 +1,11 @@ import pytest from _pytest.monkeypatch import MonkeyPatch -from app.api.internal.endpoints.remote import default_acmg_rating -from app.core.config import settings from fastapi.testclient import TestClient from pytest_httpx._httpx_mock import HTTPXMock +from app.api.internal.endpoints.remote import default_acmg_rating +from app.core.config import settings + #: Host name to use for the mocked backend. MOCKED_BACKEND_HOST = "mocked-backend" diff --git a/backend/tests/api/internal/test_remote.py b/backend/tests/api/internal/test_remote.py index c92ac328..7210c11a 100644 --- a/backend/tests/api/internal/test_remote.py +++ b/backend/tests/api/internal/test_remote.py @@ -1,8 +1,9 @@ import pytest -from app.api.internal.endpoints.remote import default_acmg_rating from fastapi.testclient import TestClient from pytest_httpx._httpx_mock import HTTPXMock +from app.api.internal.endpoints.remote import default_acmg_rating + #: Host name to use for the mocked backend. MOCKED_BACKEND_HOST = "mocked-backend" diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 10790e93..8b9ea2c6 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -1,50 +1,63 @@ -from typing import Iterator +from typing import AsyncGenerator, Iterator import pytest +import pytest_asyncio from _pytest.monkeypatch import MonkeyPatch -from app import models # noqa +from fastapi.testclient import TestClient +from sqlalchemy import StaticPool +from sqlalchemy.ext.asyncio import ( + AsyncEngine, + AsyncSession, + async_sessionmaker, + create_async_engine, +) + from app.api import deps from app.db import session from app.db.base import Base from app.main import app -from fastapi.testclient import TestClient -from sqlalchemy.engine import Engine, create_engine -from sqlalchemy.orm import sessionmaker -from sqlalchemy.pool import StaticPool @pytest.fixture() -def db_engine() -> Iterator[Engine]: +def db_engine() -> Iterator[AsyncEngine]: # setup engine with in-memory sqlite for testing - engine = create_engine( - "sqlite://", + engine = create_async_engine( + "sqlite+aiosqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool, ) yield engine -@pytest.fixture() -def db(db_engine: Engine, monkeypatch: MonkeyPatch) -> Iterator: +@pytest_asyncio.fixture() +async def db_session( + db_engine: AsyncEngine, monkeypatch: MonkeyPatch +) -> AsyncGenerator[AsyncSession, None]: """Create in-memory sqlite database for testing.""" # create all tables - Base.metadata.create_all(bind=db_engine) - # create a session for testing - TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=db_engine) - try: - monkeypatch.setattr(session, "engine", db_engine) - monkeypatch.setattr(session, "SessionLocal", TestingSessionLocal) - db = TestingSessionLocal() - - def override_get_db(): + async with db_engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + + # create a session for testing + TestingSessionLocal = async_sessionmaker( + autocommit=False, autoflush=False, expire_on_commit=False, bind=db_engine + ) + try: + monkeypatch.setattr(session, "engine", db_engine) + monkeypatch.setattr(session, "SessionLocal", TestingSessionLocal) + db = TestingSessionLocal() + + def override_get_db(): + yield db + + app.dependency_overrides[deps.get_db] = override_get_db yield db + finally: + await db.close() - app.dependency_overrides[deps.get_db] = override_get_db - yield db - finally: - db.close() # drop all tables - Base.metadata.drop_all(bind=db_engine) + async with db_engine.begin() as conn: + await conn.run_sync(Base.metadata.drop_all) @pytest.fixture(scope="module") diff --git a/backend/tests/crud/test_adminmsg.py b/backend/tests/crud/test_adminmsg.py index 6cd3e2a9..ddea162e 100644 --- a/backend/tests/crud/test_adminmsg.py +++ b/backend/tests/crud/test_adminmsg.py @@ -1,9 +1,10 @@ import typing import pytest +from sqlalchemy.ext.asyncio import AsyncSession + from app import crud from app.schemas.adminmsg import AdminMessageCreate, AdminMessageUpdate -from sqlalchemy.orm import Session @pytest.fixture @@ -17,33 +18,47 @@ def adminmessage_create(faker: typing.Any) -> AdminMessageCreate: ) -def test_create_get_adminmessage(db: Session, adminmessage_create: AdminMessageCreate) -> None: - adminmessage_postcreate = crud.adminmessage.create(db=db, obj_in=adminmessage_create) - stored_item = crud.adminmessage.get(db=db, id=adminmessage_postcreate.id) +@pytest.mark.asyncio +async def test_create_get_adminmessage( + db_session: AsyncSession, adminmessage_create: AdminMessageCreate +): + adminmessage_postcreate = await crud.adminmessage.create( + session=db_session, obj_in=adminmessage_create + ) + stored_item = await crud.adminmessage.get(session=db_session, id=adminmessage_postcreate.id) assert stored_item assert adminmessage_postcreate.id == stored_item.id - assert adminmessage_postcreate.uuid == stored_item.uuid assert adminmessage_postcreate.text == stored_item.text assert adminmessage_postcreate.active_start == stored_item.active_start assert adminmessage_postcreate.active_stop == stored_item.active_stop -def test_create_update_adminmessage( - db: Session, faker: typing.Any, adminmessage_create: AdminMessageCreate +@pytest.mark.asyncio +async def test_create_update_adminmessage( + db_session: AsyncSession, faker: typing.Any, adminmessage_create: AdminMessageCreate ) -> None: adminmessage_update = AdminMessageUpdate( title=faker.sentence(), ) - adminmessage_postcreate = crud.adminmessage.create(db=db, obj_in=adminmessage_create) - adminmessage_postupdate = crud.adminmessage.update( - db=db, db_obj=adminmessage_postcreate, obj_in=adminmessage_update + adminmessage_postcreate = await crud.adminmessage.create( + session=db_session, obj_in=adminmessage_create + ) + adminmessage_postupdate = await crud.adminmessage.update( + session=db_session, db_obj=adminmessage_postcreate, obj_in=adminmessage_update ) assert adminmessage_postupdate assert adminmessage_postupdate.title == adminmessage_update.title -def test_delete_adminmessage(db: Session, adminmessage_create: AdminMessageCreate) -> None: - adminmessage_postcreate = crud.adminmessage.create(db=db, obj_in=adminmessage_create) - crud.adminmessage.remove(db=db, id=adminmessage_postcreate.id) - adminmessage_postdelete = crud.adminmessage.get(db=db, id=adminmessage_postcreate.id) +@pytest.mark.asyncio +async def test_delete_adminmessage( + db_session: AsyncSession, adminmessage_create: AdminMessageCreate +): + adminmessage_postcreate = await crud.adminmessage.create( + session=db_session, obj_in=adminmessage_create + ) + await crud.adminmessage.remove(session=db_session, id=adminmessage_postcreate.id) + adminmessage_postdelete = await crud.adminmessage.get( + session=db_session, id=adminmessage_postcreate.id + ) assert adminmessage_postdelete is None diff --git a/backend/tests/test_main.py b/backend/tests/test_main.py index 0a7e62a9..d11b2311 100644 --- a/backend/tests/test_main.py +++ b/backend/tests/test_main.py @@ -1,8 +1,9 @@ import pytest from _pytest.monkeypatch import MonkeyPatch -from app.core.config import settings from fastapi.testclient import TestClient +from app.core.config import settings + @pytest.mark.asyncio async def test_version(monkeypatch: MonkeyPatch, client: TestClient): diff --git a/docs/dev_backend.rst b/docs/dev_backend.rst new file mode 100644 index 00000000..e119548c --- /dev/null +++ b/docs/dev_backend.rst @@ -0,0 +1,7 @@ +.. _backend_development: + +=================== +Backend Development +=================== + +This section describes the best practices to use for backend development. diff --git a/docs/dev_frontend.rst b/docs/dev_frontend.rst new file mode 100644 index 00000000..d9c6ebd7 --- /dev/null +++ b/docs/dev_frontend.rst @@ -0,0 +1,38 @@ +.. _frontend_development: + +==================== +Frontend Development +==================== + +This section describes the best practices to use for frontend development. + +------------ +Import Order +------------ + +Import order should be (and is also enforced by prettier): + +- external packages +- project includes with ``@/`` prefix +- relative includes with ``./`` prefix + +Overall, restrict relative include order for tests to include code to be tested with ``../``. + +-------------- +Test Structure +-------------- + +Consider the following structure, an example is given below. + +- imports +- define fixture data + - put larger fixture data into ``.json`` files within the ``__tests__`` folders (will go to LFS by our ``.gitattributes`` configuration) +- define the tests +- use ``describe.concurrent`` to describe the tests, usually one block per ``.spec.ts`` file +- use ``it`` to define the tests + - use ``async () => { ... }`` only when necessary, e.g., for ``await nextTick()`` +- use the ``setupMountedComponents()`` helper from ``@/components/__tests__/utils`` to mount components, setup store, and setup router with mocks + +.. literalinclude:: ../frontend/src/components/__tests__/UserProfileButton.spec.ts + :language: typescript + diff --git a/docs/dev_quickstart.rst b/docs/dev_quickstart.rst index ae995399..9f621a20 100644 --- a/docs/dev_quickstart.rst +++ b/docs/dev_quickstart.rst @@ -64,6 +64,17 @@ You can use the provided ``Makefile`` files to install the dependencies. $ make deps +----------------- +Setup Environment +----------------- + +You need to create an ``.env`` file for the backend. +The values in ``env-dev`` are suitable for development with the ``reev-docker-compose`` with ``docker-compose.override.yml-dev``. + +.. code-block:: bash + + $ ln -sr backend/env-dev backend/.env + ------------------- Running the Servers ------------------- @@ -79,3 +90,12 @@ In case of weird issues, try to stop them with ``Ctrl-C`` and starting them agai $ make -C backend serve $ make -C frontend serve + +Now you can navigate to the frontend development server at http://localhost:8081. +This server will transparently forward the API requests to the backend server at http://localhost:8081. + +----- +Notes +----- + +- A superuser will be created if you configured its email and password in environment variables ``FIRST_USER_EMAIL`` and ``FIRST_USER_PASSWORD``. diff --git a/docs/index.rst b/docs/index.rst index 170aeb94..3dae2ed5 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -16,6 +16,8 @@ It supports them by providing aggregated information on genomic variants. dev_quickstart dev_makefiles + dev_frontend + dev_backend dev_ci dev_deployment dev_docs diff --git a/frontend/.prettierrc.json b/frontend/.prettierrc.json index 66e23359..5eddadd7 100644 --- a/frontend/.prettierrc.json +++ b/frontend/.prettierrc.json @@ -1,8 +1,12 @@ { "$schema": "https://json.schemastore.org/prettierrc", + "printWidth": 100, "semi": false, - "tabWidth": 2, "singleQuote": true, - "printWidth": 100, - "trailingComma": "none" -} \ No newline at end of file + "tabWidth": 2, + "trailingComma": "none", + "importOrder": ["^@/(.*)$", "^[./]"], + "importOrderSeparation": true, + "importOrderSortSpecifiers": true, + "plugins": ["@trivago/prettier-plugin-sort-imports"] +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f53fb0ef..81c5e1ec 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,7 +12,6 @@ "@reactgular/chunks": "^1.0.1", "igv": "^2.15.11", "pinia": "^2.1.6", - "resize-observer-polyfill": "^1.5.1", "vega": "^5.25.0", "vega-embed": "^6.22.2", "vue": "^3.3.4", @@ -23,6 +22,7 @@ "devDependencies": { "@pinia/testing": "^0.1.3", "@rushstack/eslint-patch": "^1.5.0", + "@trivago/prettier-plugin-sort-imports": "^4.2.0", "@tsconfig/node18": "^18.2.2", "@types/jsdom": "^21.1.3", "@types/node": "^20.7.0", @@ -30,7 +30,7 @@ "@vitejs/plugin-vue-jsx": "^3.0.2", "@vitest/coverage-istanbul": "^0.34.5", "@vue/eslint-config-prettier": "^8.0.0", - "@vue/eslint-config-typescript": "^11.0.3", + "@vue/eslint-config-typescript": "^12.0.0", "@vue/test-utils": "^2.4.1", "@vue/tsconfig": "^0.4.0", "eslint": "^8.50.0", @@ -39,10 +39,11 @@ "npm-check-updates": "^16.14.4", "npm-run-all": "^4.1.5", "prettier": "^3.0.3", + "resize-observer-polyfill": "^1.5.1", "typescript": "^5.2.2", "vite": "^4.4.9", "vite-plugin-vuetify": "^1.0.2", - "vitest": "^0.32.4", + "vitest": "^0.34.5", "vitest-canvas-mock": "^0.3.3", "vitest-fetch-mock": "^0.2.2", "vue-cli-plugin-vuetify": "~2.5.8", @@ -1587,6 +1588,86 @@ "node": ">= 10" } }, + "node_modules/@trivago/prettier-plugin-sort-imports": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.2.0.tgz", + "integrity": "sha512-YBepjbt+ZNBVmN3ev1amQH3lWCmHyt5qTbLCp/syXJRu/Kw2koXh44qayB1gMRxcL/gV8egmjN5xWSrYyfUtyw==", + "dev": true, + "dependencies": { + "@babel/generator": "7.17.7", + "@babel/parser": "^7.20.5", + "@babel/traverse": "7.17.3", + "@babel/types": "7.17.0", + "javascript-natural-sort": "0.7.1", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "@vue/compiler-sfc": "3.x", + "prettier": "2.x - 3.x" + }, + "peerDependenciesMeta": { + "@vue/compiler-sfc": { + "optional": true + } + } + }, + "node_modules/@trivago/prettier-plugin-sort-imports/node_modules/@babel/generator": { + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.7.tgz", + "integrity": "sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.17.0", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@trivago/prettier-plugin-sort-imports/node_modules/@babel/traverse": { + "version": "7.17.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.3.tgz", + "integrity": "sha512-5irClVky7TxRWIRtxlh2WPUUOLhcPN06AGgaQSB8AEwuyEBgJVuJ5imdHm5zxk8w0QS5T+tDfnDxAlhWjpb7cw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.16.7", + "@babel/generator": "^7.17.3", + "@babel/helper-environment-visitor": "^7.16.7", + "@babel/helper-function-name": "^7.16.7", + "@babel/helper-hoist-variables": "^7.16.7", + "@babel/helper-split-export-declaration": "^7.16.7", + "@babel/parser": "^7.17.3", + "@babel/types": "^7.17.0", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@trivago/prettier-plugin-sort-imports/node_modules/@babel/types": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", + "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.16.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@trivago/prettier-plugin-sort-imports/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@tsconfig/node18": { "version": "18.2.2", "resolved": "https://registry.npmjs.org/@tsconfig/node18/-/node18-18.2.2.tgz", @@ -1710,9 +1791,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.7.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.7.0.tgz", - "integrity": "sha512-zI22/pJW2wUZOVyguFaUL1HABdmSVxpXrzIqkjsHmyUjNhPoWM1CKfvVuXfetHhIok4RY573cqS0mZ1SJEnoTg==", + "version": "20.7.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.7.1.tgz", + "integrity": "sha512-LT+OIXpp2kj4E2S/p91BMe+VgGX2+lfO+XTpfXhh+bCk2LkQtHZSub8ewFBMGP5ClysPjTDFa4sMI8Q3n4T0wg==", "dev": true }, "node_modules/@types/semver": { @@ -1728,32 +1809,33 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", - "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.7.3.tgz", + "integrity": "sha512-vntq452UHNltxsaaN+L9WyuMch8bMd9CqJ3zhzTPXXidwbf5mqqKCVXEuvRZUqLJSTLeWE65lQwyXsRGnXkCTA==", "dev": true, "dependencies": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/type-utils": "5.62.0", - "@typescript-eslint/utils": "5.62.0", + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.7.3", + "@typescript-eslint/type-utils": "6.7.3", + "@typescript-eslint/utils": "6.7.3", + "@typescript-eslint/visitor-keys": "6.7.3", "debug": "^4.3.4", "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -1795,25 +1877,26 @@ "dev": true }, "node_modules/@typescript-eslint/parser": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", - "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.7.3.tgz", + "integrity": "sha512-TlutE+iep2o7R8Lf+yoer3zU6/0EAUc8QIBB3GYBc1KGz4c4TRm83xwXUZVPlZ6YCLss4r77jbu6j3sendJoiQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/scope-manager": "6.7.3", + "@typescript-eslint/types": "6.7.3", + "@typescript-eslint/typescript-estree": "6.7.3", + "@typescript-eslint/visitor-keys": "6.7.3", "debug": "^4.3.4" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -1822,16 +1905,16 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", - "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.7.3.tgz", + "integrity": "sha512-wOlo0QnEou9cHO2TdkJmzF7DFGvAKEnB82PuPNHpT8ZKKaZu6Bm63ugOTn9fXNJtvuDPanBc78lGUGGytJoVzQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0" + "@typescript-eslint/types": "6.7.3", + "@typescript-eslint/visitor-keys": "6.7.3" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -1839,25 +1922,25 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", - "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.7.3.tgz", + "integrity": "sha512-Fc68K0aTDrKIBvLnKTZ5Pf3MXK495YErrbHb1R6aTpfK5OdSFj0rVN7ib6Tx6ePrZ2gsjLqr0s98NG7l96KSQw==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "5.62.0", - "@typescript-eslint/utils": "5.62.0", + "@typescript-eslint/typescript-estree": "6.7.3", + "@typescript-eslint/utils": "6.7.3", "debug": "^4.3.4", - "tsutils": "^3.21.0" + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "*" + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -1866,12 +1949,12 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", - "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.7.3.tgz", + "integrity": "sha512-4g+de6roB2NFcfkZb439tigpAMnvEIg3rIjWQ+EM7IBaYt/CdJt6em9BJ4h4UpdgaBWdmx2iWsafHTrqmgIPNw==", "dev": true, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -1879,21 +1962,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", - "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.7.3.tgz", + "integrity": "sha512-YLQ3tJoS4VxLFYHTw21oe1/vIZPRqAO91z6Uv0Ss2BKm/Ag7/RVQBcXTGcXhgJMdA4U+HrKuY5gWlJlvoaKZ5g==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0", + "@typescript-eslint/types": "6.7.3", + "@typescript-eslint/visitor-keys": "6.7.3", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -1939,29 +2022,28 @@ "dev": true }, "node_modules/@typescript-eslint/utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", - "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.7.3.tgz", + "integrity": "sha512-vzLkVder21GpWRrmSR9JxGZ5+ibIUSudXlW52qeKpzUEQhRSmyZiVDDj3crAth7+5tmN1ulvgKaCU2f/bPRCzg==", "dev": true, "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.7.3", + "@typescript-eslint/types": "6.7.3", + "@typescript-eslint/typescript-estree": "6.7.3", + "semver": "^7.5.4" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "eslint": "^7.0.0 || ^8.0.0" } }, "node_modules/@typescript-eslint/utils/node_modules/lru-cache": { @@ -1998,16 +2080,16 @@ "dev": true }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", - "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.7.3.tgz", + "integrity": "sha512-HEVXkU9IB+nk9o63CeICMHxFWbHWr3E1mpilIQBe9+7L/lH97rleFLVtYsfnWB+JVMaiFnEaxvknvmIzX+CqVg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.62.0", - "eslint-visitor-keys": "^3.3.0" + "@typescript-eslint/types": "6.7.3", + "eslint-visitor-keys": "^3.4.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -2067,13 +2149,13 @@ } }, "node_modules/@vitest/expect": { - "version": "0.32.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.32.4.tgz", - "integrity": "sha512-m7EPUqmGIwIeoU763N+ivkFjTzbaBn0n9evsTOcde03ugy2avPs3kZbYmw3DkcH1j5mxhMhdamJkLQ6dM1bk/A==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.34.5.tgz", + "integrity": "sha512-/3RBIV9XEH+nRpRMqDJBufKIOQaYUH2X6bt0rKSCW0MfKhXFLYsR5ivHifeajRSTsln0FwJbitxLKHSQz/Xwkw==", "dev": true, "dependencies": { - "@vitest/spy": "0.32.4", - "@vitest/utils": "0.32.4", + "@vitest/spy": "0.34.5", + "@vitest/utils": "0.34.5", "chai": "^4.3.7" }, "funding": { @@ -2081,12 +2163,12 @@ } }, "node_modules/@vitest/runner": { - "version": "0.32.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.32.4.tgz", - "integrity": "sha512-cHOVCkiRazobgdKLnczmz2oaKK9GJOw6ZyRcaPdssO1ej+wzHVIkWiCiNacb3TTYPdzMddYkCgMjZ4r8C0JFCw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.34.5.tgz", + "integrity": "sha512-RDEE3ViVvl7jFSCbnBRyYuu23XxmvRTSZWW6W4M7eC5dOsK75d5LIf6uhE5Fqf809DQ1+9ICZZNxhIolWHU4og==", "dev": true, "dependencies": { - "@vitest/utils": "0.32.4", + "@vitest/utils": "0.34.5", "p-limit": "^4.0.0", "pathe": "^1.1.1" }, @@ -2122,12 +2204,12 @@ } }, "node_modules/@vitest/snapshot": { - "version": "0.32.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.32.4.tgz", - "integrity": "sha512-IRpyqn9t14uqsFlVI2d7DFMImGMs1Q9218of40bdQQgMePwVdmix33yMNnebXcTzDU5eiV3eUsoxxH5v0x/IQA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.34.5.tgz", + "integrity": "sha512-+ikwSbhu6z2yOdtKmk/aeoDZ9QPm2g/ZO5rXT58RR9Vmu/kB2MamyDSx77dctqdZfP3Diqv4mbc/yw2kPT8rmA==", "dev": true, "dependencies": { - "magic-string": "^0.30.0", + "magic-string": "^0.30.1", "pathe": "^1.1.1", "pretty-format": "^29.5.0" }, @@ -2136,9 +2218,9 @@ } }, "node_modules/@vitest/spy": { - "version": "0.32.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.32.4.tgz", - "integrity": "sha512-oA7rCOqVOOpE6rEoXuCOADX7Lla1LIa4hljI2MSccbpec54q+oifhziZIJXxlE/CvI2E+ElhBHzVu0VEvJGQKQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.34.5.tgz", + "integrity": "sha512-epsicsfhvBjRjCMOC/3k00mP/TBGQy8/P0DxOFiWyLt55gnZ99dqCfCiAsKO17BWVjn4eZRIjKvcqNmSz8gvmg==", "dev": true, "dependencies": { "tinyspy": "^2.1.1" @@ -2148,9 +2230,9 @@ } }, "node_modules/@vitest/utils": { - "version": "0.32.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.32.4.tgz", - "integrity": "sha512-Gwnl8dhd1uJ+HXrYyV0eRqfmk9ek1ASE/LWfTCuWMw+d07ogHqp4hEAV28NiecimK6UY9DpSEPh+pXBA5gtTBg==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.34.5.tgz", + "integrity": "sha512-ur6CmmYQoeHMwmGb0v+qwkwN3yopZuZyf4xt1DBBSGBed8Hf9Gmbm/5dEWqgpLPdRx6Av6jcWXrjcKfkTzg/pw==", "dev": true, "dependencies": { "diff-sequences": "^29.4.3", @@ -2280,14 +2362,14 @@ } }, "node_modules/@vue/eslint-config-typescript": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-11.0.3.tgz", - "integrity": "sha512-dkt6W0PX6H/4Xuxg/BlFj5xHvksjpSlVjtkQCpaYJBIEuKj2hOVU7r+TIe+ysCwRYFz/lGqvklntRkCAibsbPw==", + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-12.0.0.tgz", + "integrity": "sha512-StxLFet2Qe97T8+7L8pGlhYBBr8Eg05LPuTDVopQV6il+SK6qqom59BA/rcFipUef2jD8P2X44Vd8tMFytfvlg==", "dev": true, "dependencies": { - "@typescript-eslint/eslint-plugin": "^5.59.1", - "@typescript-eslint/parser": "^5.59.1", - "vue-eslint-parser": "^9.1.1" + "@typescript-eslint/eslint-plugin": "^6.7.0", + "@typescript-eslint/parser": "^6.7.0", + "vue-eslint-parser": "^9.3.1" }, "engines": { "node": "^14.17.0 || >=16.0.0" @@ -3282,9 +3364,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001540", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001540.tgz", - "integrity": "sha512-9JL38jscuTJBTcuETxm8QLsFr/F6v0CYYTEU6r5+qSM98P2Q0Hmu0eG1dTG5GBUmywU3UlcVOUSIJYY47rdFSw==", + "version": "1.0.30001541", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001541.tgz", + "integrity": "sha512-bLOsqxDgTqUBkzxbNlSBt8annkDpQB9NdzdTbO2ooJ+eC/IQcvDspDc058g84ejCelF7vHUx57KIOjEecOHXaw==", "dev": true, "funding": [ { @@ -3302,18 +3384,18 @@ ] }, "node_modules/chai": { - "version": "4.3.8", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.8.tgz", - "integrity": "sha512-vX4YvVVtxlfSZ2VecZgFUTU5qPCYsobVI2O9FmwEXBhDigYGQA6jRXCycIs1yJnnWbZ6/+a2zNIF5DfVCcJBFQ==", + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", + "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==", "dev": true, "dependencies": { "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^4.1.2", - "get-func-name": "^2.0.0", - "loupe": "^2.3.1", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", "pathval": "^1.1.1", - "type-detect": "^4.0.5" + "type-detect": "^4.0.8" }, "engines": { "node": ">=4" @@ -3334,10 +3416,13 @@ } }, "node_modules/check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", "dev": true, + "dependencies": { + "get-func-name": "^2.0.2" + }, "engines": { "node": "*" } @@ -4228,9 +4313,9 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.4.531", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.531.tgz", - "integrity": "sha512-H6gi5E41Rn3/mhKlPaT1aIMg/71hTAqn0gYEllSuw9igNWtvQwu185jiCZoZD29n7Zukgh7GVZ3zGf0XvkhqjQ==", + "version": "1.4.532", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.532.tgz", + "integrity": "sha512-piIR0QFdIGKmOJTSNg5AwxZRNWQSXlRYycqDB9Srstx4lip8KpcmRxVP6zuFWExWziHYZpJ0acX7TxqX95KBpg==", "dev": true }, "node_modules/emoji-regex": { @@ -4613,16 +4698,19 @@ "dev": true }, "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "dependencies": { "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" + "estraverse": "^5.2.0" }, "engines": { - "node": ">=8.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-visitor-keys": { @@ -4698,31 +4786,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, "node_modules/eslint/node_modules/globals": { "version": "13.22.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.22.0.tgz", @@ -4800,15 +4863,6 @@ "node": ">=0.10" } }, - "node_modules/esquery/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, "node_modules/esrecurse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", @@ -4821,7 +4875,7 @@ "node": ">=4.0" } }, - "node_modules/esrecurse/node_modules/estraverse": { + "node_modules/estraverse": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", @@ -4830,15 +4884,6 @@ "node": ">=4.0" } }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, "node_modules/estree-walker": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", @@ -5117,9 +5162,9 @@ } }, "node_modules/fp-and-or": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/fp-and-or/-/fp-and-or-0.1.3.tgz", - "integrity": "sha512-wJaE62fLaB3jCYvY2ZHjZvmKK2iiLiiehX38rz5QZxtdN8fVPJDeZUiVvJrHStdTc+23LHlyZuSEKgFc0pxi2g==", + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/fp-and-or/-/fp-and-or-0.1.4.tgz", + "integrity": "sha512-+yRYRhpnFPWXSly/6V4Lw9IfOV26uu30kynGJ03PW+MnjOEQe45RZ141QcS0aJehYBYA50GfCDnsRbFJdhssRw==", "dev": true, "engines": { "node": ">=10" @@ -6389,9 +6434,9 @@ } }, "node_modules/jackspeak": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.5.tgz", - "integrity": "sha512-Ratx+B8WeXLAtRJn26hrhY8S1+Jz6pxPMrkrdkgb/NstTNiqMhX0/oFVu5wX+g5n6JlEu2LPsDJmY8nRP4+alw==", + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", "dev": true, "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -6406,6 +6451,12 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/javascript-natural-sort": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", + "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==", + "dev": true + }, "node_modules/jest-canvas-mock": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jest-canvas-mock/-/jest-canvas-mock-2.5.2.tgz", @@ -7274,12 +7325,6 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "node_modules/natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", - "dev": true - }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -9155,7 +9200,8 @@ "node_modules/resize-observer-polyfill": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", - "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "dev": true }, "node_modules/resolve": { "version": "1.22.6", @@ -9264,9 +9310,9 @@ "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" }, "node_modules/rollup": { - "version": "3.29.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.3.tgz", - "integrity": "sha512-T7du6Hum8jOkSWetjRgbwpM6Sy0nECYrYRSmZjayFcOddtKJWU4d17AC3HNUk7HRuqy4p+G7aEZclSHytqUmEg==", + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", "devOptional": true, "bin": { "rollup": "dist/bin/rollup" @@ -10251,9 +10297,9 @@ "dev": true }, "node_modules/tinypool": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.5.0.tgz", - "integrity": "sha512-paHQtnrlS1QZYKF/GnLoOM/DN9fqaGOFbCbxzAhwniySnzl9Ebk8w73/dd34DAhe/obUbPAOldTyYXQZxnPBPQ==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.7.0.tgz", + "integrity": "sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==", "dev": true, "engines": { "node": ">=14.0.0" @@ -10346,31 +10392,22 @@ "node": ">=14" } }, - "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - }, - "node_modules/tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "node_modules/ts-api-utils": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", + "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", "dev": true, - "dependencies": { - "tslib": "^1.8.1" - }, "engines": { - "node": ">= 6" + "node": ">=16.13.0" }, "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + "typescript": ">=4.2.0" } }, - "node_modules/tsutils/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/tuf-js": { "version": "1.1.7", @@ -10507,9 +10544,9 @@ } }, "node_modules/ufo": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.0.tgz", - "integrity": "sha512-bRn3CsoojyNStCZe0BG0Mt4Nr/4KF+rhFlnNXybgqt5pXHNFRlqinSoQaTrGyzE4X8aHplSb+TorH+COin9Yxw==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.1.tgz", + "integrity": "sha512-uY/99gMLIOlJPwATcMVYfqDSxUR9//AUcgZMzwfSTJPDKzA1S8mX4VLqa+fiAtveraQUBCz4FFcwVZBGbwBXIw==", "dev": true }, "node_modules/unbox-primitive": { @@ -11261,9 +11298,9 @@ } }, "node_modules/vite-node": { - "version": "0.32.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.32.4.tgz", - "integrity": "sha512-L2gIw+dCxO0LK14QnUMoqSYpa9XRGnTTTDjW2h19Mr+GR0EFj4vx52W41gFXfMLqpA00eK4ZjOVYo1Xk//LFEw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.34.5.tgz", + "integrity": "sha512-RNZ+DwbCvDoI5CbCSQSyRyzDTfFvFauvMs6Yq4ObJROKlIKuat1KgSX/Ako5rlDMfVCyMcpMRMTkJBxd6z8YRA==", "dev": true, "dependencies": { "cac": "^6.7.14", @@ -11271,7 +11308,7 @@ "mlly": "^1.4.0", "pathe": "^1.1.1", "picocolors": "^1.0.0", - "vite": "^3.0.0 || ^4.0.0" + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" @@ -11302,34 +11339,34 @@ } }, "node_modules/vitest": { - "version": "0.32.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.32.4.tgz", - "integrity": "sha512-3czFm8RnrsWwIzVDu/Ca48Y/M+qh3vOnF16czJm98Q/AN1y3B6PBsyV8Re91Ty5s7txKNjEhpgtGPcfdbh2MZg==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.5.tgz", + "integrity": "sha512-CPI68mmnr2DThSB3frSuE5RLm9wo5wU4fbDrDwWQQB1CWgq9jQVoQwnQSzYAjdoBOPoH2UtXpOgHVge/uScfZg==", "dev": true, "dependencies": { "@types/chai": "^4.3.5", "@types/chai-subset": "^1.3.3", "@types/node": "*", - "@vitest/expect": "0.32.4", - "@vitest/runner": "0.32.4", - "@vitest/snapshot": "0.32.4", - "@vitest/spy": "0.32.4", - "@vitest/utils": "0.32.4", + "@vitest/expect": "0.34.5", + "@vitest/runner": "0.34.5", + "@vitest/snapshot": "0.34.5", + "@vitest/spy": "0.34.5", + "@vitest/utils": "0.34.5", "acorn": "^8.9.0", "acorn-walk": "^8.2.0", "cac": "^6.7.14", "chai": "^4.3.7", "debug": "^4.3.4", "local-pkg": "^0.4.3", - "magic-string": "^0.30.0", + "magic-string": "^0.30.1", "pathe": "^1.1.1", "picocolors": "^1.0.0", "std-env": "^3.3.3", "strip-literal": "^1.0.1", "tinybench": "^2.5.0", - "tinypool": "^0.5.0", - "vite": "^3.0.0 || ^4.0.0", - "vite-node": "0.32.4", + "tinypool": "^0.7.0", + "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0", + "vite-node": "0.34.5", "why-is-node-running": "^2.2.2" }, "bin": { @@ -11502,31 +11539,6 @@ "eslint": ">=6.0.0" } }, - "node_modules/vue-eslint-parser/node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/vue-eslint-parser/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, "node_modules/vue-eslint-parser/node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -11768,6 +11780,30 @@ "node": ">=10.13.0" } }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "peer": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=4.0" + } + }, "node_modules/webpack/node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 526e216d..efa604ba 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,7 +18,6 @@ "@reactgular/chunks": "^1.0.1", "igv": "^2.15.11", "pinia": "^2.1.6", - "resize-observer-polyfill": "^1.5.1", "vega": "^5.25.0", "vega-embed": "^6.22.2", "vue": "^3.3.4", @@ -29,6 +28,7 @@ "devDependencies": { "@pinia/testing": "^0.1.3", "@rushstack/eslint-patch": "^1.5.0", + "@trivago/prettier-plugin-sort-imports": "^4.2.0", "@tsconfig/node18": "^18.2.2", "@types/jsdom": "^21.1.3", "@types/node": "^20.7.0", @@ -36,7 +36,7 @@ "@vitejs/plugin-vue-jsx": "^3.0.2", "@vitest/coverage-istanbul": "^0.34.5", "@vue/eslint-config-prettier": "^8.0.0", - "@vue/eslint-config-typescript": "^11.0.3", + "@vue/eslint-config-typescript": "^12.0.0", "@vue/test-utils": "^2.4.1", "@vue/tsconfig": "^0.4.0", "eslint": "^8.50.0", @@ -45,10 +45,11 @@ "npm-check-updates": "^16.14.4", "npm-run-all": "^4.1.5", "prettier": "^3.0.3", + "resize-observer-polyfill": "^1.5.1", "typescript": "^5.2.2", "vite": "^4.4.9", "vite-plugin-vuetify": "^1.0.2", - "vitest": "^0.32.4", + "vitest": "^0.34.5", "vitest-canvas-mock": "^0.3.3", "vitest-fetch-mock": "^0.2.2", "vue-cli-plugin-vuetify": "~2.5.8", diff --git a/frontend/src/api/__tests__/annonars.spec.ts b/frontend/src/api/__tests__/annonars.spec.ts index 829e234c..3fbe0322 100644 --- a/frontend/src/api/__tests__/annonars.spec.ts +++ b/frontend/src/api/__tests__/annonars.spec.ts @@ -1,11 +1,12 @@ -import { beforeEach, describe, it, expect, vi } from 'vitest' +import { beforeEach, describe, expect, it, vi } from 'vitest' import createFetchMock from 'vitest-fetch-mock' -import { AnnonarsClient } from '../annonars' import * as BRCA1geneInfo from '@/assets/__tests__/BRCA1GeneInfo.json' import * as BRCA1VariantInfo from '@/assets/__tests__/BRCA1VariantInfo.json' import * as EMPSearchInfo from '@/assets/__tests__/EMPSearchInfo.json' +import { AnnonarsClient } from '../annonars' + const fetchMocker = createFetchMock(vi) describe.concurrent('Annonars Client', () => { diff --git a/frontend/src/api/__tests__/common.spec.ts b/frontend/src/api/__tests__/common.spec.ts index 8217a707..4673e9b9 100644 --- a/frontend/src/api/__tests__/common.spec.ts +++ b/frontend/src/api/__tests__/common.spec.ts @@ -1,34 +1,34 @@ -import { describe, it, expect } from 'vitest' +import { describe, expect, it } from 'vitest' import { - API_BASE_PREFIX, - API_BASE_PREFIX_ANNONARS, - API_BASE_PREFIX_MEHARI, - API_BASE_PREFIX_NGINX + API_INTERNALBASE_PREFIX_MEHARI, + API_INTERNAL_BASE_PREFIX, + API_INTERNAL_BASE_PREFIX_ANNONARS, + API_INTERNAL_BASE_PREFIX_NGINX } from '../common' describe.concurrent('API_BASE_PREFIX constants', () => { it('returns the correct API base prefix in production mode', () => { const originalMode = import.meta.env.MODE - expect(API_BASE_PREFIX).toBe('/internal/') + expect(API_INTERNAL_BASE_PREFIX).toBe('/internal/') import.meta.env.MODE = originalMode }) it('returns the correct API base prefix for annonars in production mode', () => { const originalMode = import.meta.env.MODE - expect(API_BASE_PREFIX_ANNONARS).toBe('/internal/proxy/annonars') + expect(API_INTERNAL_BASE_PREFIX_ANNONARS).toBe('/internal/proxy/annonars') import.meta.env.MODE = originalMode }) it('returns the correct API base prefix for mehari in production mode', () => { const originalMode = import.meta.env.MODE - expect(API_BASE_PREFIX_MEHARI).toBe('/internal/proxy/mehari') + expect(API_INTERNALBASE_PREFIX_MEHARI).toBe('/internal/proxy/mehari') import.meta.env.MODE = originalMode }) it('returns the correct API base prefix for nginx in production mode', () => { const originalMode = import.meta.env.MODE - expect(API_BASE_PREFIX_NGINX).toBe('/internal/proxy/nginx') + expect(API_INTERNAL_BASE_PREFIX_NGINX).toBe('/internal/proxy/nginx') import.meta.env.MODE = originalMode }) }) diff --git a/frontend/src/api/__tests__/mehari.spec.ts b/frontend/src/api/__tests__/mehari.spec.ts index 4a036b30..a4392726 100644 --- a/frontend/src/api/__tests__/mehari.spec.ts +++ b/frontend/src/api/__tests__/mehari.spec.ts @@ -1,9 +1,10 @@ -import { beforeEach, describe, it, expect, vi } from 'vitest' +import { beforeEach, describe, expect, it, vi } from 'vitest' import createFetchMock from 'vitest-fetch-mock' -import { MehariClient } from '../mehari' import * as BRCA1TxInfo from '@/assets/__tests__/BRCA1TxInfo.json' +import { MehariClient } from '../mehari' + const fetchMocker = createFetchMock(vi) describe.concurrent('Mehari Client', () => { diff --git a/frontend/src/api/__tests__/misc.spec.ts b/frontend/src/api/__tests__/misc.spec.ts index 2a9f84c6..c9a22306 100644 --- a/frontend/src/api/__tests__/misc.spec.ts +++ b/frontend/src/api/__tests__/misc.spec.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, it, expect, vi } from 'vitest' +import { beforeEach, describe, expect, it, vi } from 'vitest' import createFetchMock from 'vitest-fetch-mock' import { MiscClient } from '../misc' diff --git a/frontend/src/api/annonars.ts b/frontend/src/api/annonars.ts index 951a13d1..aca29546 100644 --- a/frontend/src/api/annonars.ts +++ b/frontend/src/api/annonars.ts @@ -1,7 +1,8 @@ import { chunks } from '@reactgular/chunks' -import { API_BASE_PREFIX_ANNONARS } from '@/api/common' -const API_BASE_URL = `${API_BASE_PREFIX_ANNONARS}/` +import { API_INTERNAL_BASE_PREFIX_ANNONARS } from '@/api/common' + +const API_BASE_URL = `${API_INTERNAL_BASE_PREFIX_ANNONARS}/` export class AnnonarsClient { private apiBaseUrl: string diff --git a/frontend/src/api/auth.ts b/frontend/src/api/auth.ts new file mode 100644 index 00000000..67b04b2b --- /dev/null +++ b/frontend/src/api/auth.ts @@ -0,0 +1,40 @@ +import { API_V1_BASE_PREFIX } from '@/api/common' + +/** Access to the authentication-related part of the API. + */ +export class AuthClient { + private apiBaseUrl: string + private csrfToken: string | null + + constructor(apiBaseUrl?: string, csrfToken?: string) { + this.apiBaseUrl = apiBaseUrl ?? API_V1_BASE_PREFIX + this.csrfToken = csrfToken ?? null + } + + /** + * Perform login and return whether the login was successful. + * + * @param username The username/email to login with. + * @param password The password to login with. + * @returns Whether the login was successful. + */ + async login(username: string, password: string): Promise { + const response = await fetch(`${this.apiBaseUrl}auth/cookie/login`, { + method: 'POST', + mode: 'cors', + credentials: 'include', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' + }, + body: `username=${encodeURIComponent(username)}&` + `password=${encodeURIComponent(password)}` + }) + return response.status === 204 + } + + async logout(): Promise { + const response = await fetch(`${this.apiBaseUrl}auth/cookie/logout`, { + method: 'POST' + }) + return await response.text() + } +} diff --git a/frontend/src/api/common.ts b/frontend/src/api/common.ts index 5c22d497..599befa0 100644 --- a/frontend/src/api/common.ts +++ b/frontend/src/api/common.ts @@ -1,17 +1,20 @@ -export const API_BASE_PREFIX = - import.meta.env.MODE == 'development' ? '//localhost:8080/internal/' : '/internal/' +export const API_INTERNAL_BASE_PREFIX = '/internal/' +// import.meta.env.MODE == 'development' ? '//localhost:8080/internal/' : '/internal/' -export const API_BASE_PREFIX_ANNONARS = - import.meta.env.MODE == 'development' - ? `//localhost:8080/internal/proxy/annonars` - : '/internal/proxy/annonars' +export const API_INTERNAL_BASE_PREFIX_ANNONARS = '/internal/proxy/annonars' +// import.meta.env.MODE == 'development' +// ? `//localhost:8080/internal/proxy/annonars` +// : '/internal/proxy/annonars' -export const API_BASE_PREFIX_MEHARI = - import.meta.env.MODE == 'development' - ? '//localhost:8080/internal/proxy/mehari' - : '/internal/proxy/mehari' +export const API_INTERNALBASE_PREFIX_MEHARI = '/internal/proxy/mehari' +// import.meta.env.MODE == 'development' +// ? '//localhost:8080/internal/proxy/mehari' +// : '/internal/proxy/mehari' -export const API_BASE_PREFIX_NGINX = - import.meta.env.MODE == 'development' - ? '//localhost:8080/internal/proxy/proxy/nginx' - : '/internal/proxy/nginx' +export const API_INTERNAL_BASE_PREFIX_NGINX = '/internal/proxy/nginx' +// import.meta.env.MODE == 'development' +// ? '//localhost:8080/internal/proxy/proxy/nginx' +// : '/internal/proxy/nginx' + +export const API_V1_BASE_PREFIX = '/api/v1/' +// import.meta.env.MODE == 'development' ? '//localhost:8080/api/v1/' : '/api/v1/' diff --git a/frontend/src/api/mehari.ts b/frontend/src/api/mehari.ts index 3c0b80b5..22e20291 100644 --- a/frontend/src/api/mehari.ts +++ b/frontend/src/api/mehari.ts @@ -1,6 +1,6 @@ -import { API_BASE_PREFIX_MEHARI } from '@/api/common' +import { API_INTERNALBASE_PREFIX_MEHARI } from '@/api/common' -const API_BASE_URL = `${API_BASE_PREFIX_MEHARI}/` +const API_BASE_URL = `${API_INTERNALBASE_PREFIX_MEHARI}/` export class MehariClient { private apiBaseUrl: string diff --git a/frontend/src/api/misc.ts b/frontend/src/api/misc.ts index dac0d2fa..ba7c6402 100644 --- a/frontend/src/api/misc.ts +++ b/frontend/src/api/misc.ts @@ -1,6 +1,6 @@ -import { API_BASE_PREFIX } from '@/api/common' +import { API_INTERNAL_BASE_PREFIX } from '@/api/common' -const API_BASE_URL = API_BASE_PREFIX +const API_BASE_URL = API_INTERNAL_BASE_PREFIX export class MiscClient { private apiBaseUrl: string diff --git a/frontend/src/api/users.ts b/frontend/src/api/users.ts new file mode 100644 index 00000000..8c3c4a3d --- /dev/null +++ b/frontend/src/api/users.ts @@ -0,0 +1,43 @@ +import { API_V1_BASE_PREFIX } from '@/api/common' + +export class UnauthenticatedError extends Error { + constructor(message?: string) { + super(message) + this.name = 'UnauthenticatedError' + } +} + +/** Access to the users part of the API. + */ +export class UsersClient { + private apiBaseUrl: string + private csrfToken: string | null + + constructor(apiBaseUrl?: string, csrfToken?: string) { + this.apiBaseUrl = apiBaseUrl ?? API_V1_BASE_PREFIX + this.csrfToken = csrfToken ?? null + } + + /** + * Obtains the currently logged in user's information. + * + * @returns `UserClient` if authenticated + * @throws `UnauthenticatedError` if not authenticated + */ + async fetchCurrentUserProfile(): Promise { + const response = await fetch(`${this.apiBaseUrl}users/me`, { + method: 'GET', + mode: 'cors', + credentials: 'include' + // headers: { + // 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' + // }, + // body: `username=${encodeURIComponent(username)}&` + `password=${encodeURIComponent(password)}` + }) + if (response.status !== 200) { + throw new UnauthenticatedError("There was an error fetching the current user's profile.") + } else { + return await response.json() + } + } +} diff --git a/frontend/src/components/AcmgCriteriaCard.vue b/frontend/src/components/AcmgCriteriaCard.vue index 920e101d..cdeca892 100644 --- a/frontend/src/components/AcmgCriteriaCard.vue +++ b/frontend/src/components/AcmgCriteriaCard.vue @@ -1,14 +1,14 @@ + diff --git a/frontend/src/components/HeaderDetailPage.vue b/frontend/src/components/HeaderDetailPage.vue index e339d149..bc6de4be 100644 --- a/frontend/src/components/HeaderDetailPage.vue +++ b/frontend/src/components/HeaderDetailPage.vue @@ -1,8 +1,9 @@ + + diff --git a/frontend/src/components/VariantDetails/AcmgRating.vue b/frontend/src/components/VariantDetails/AcmgRating.vue index 2879c7f9..afd82830 100644 --- a/frontend/src/components/VariantDetails/AcmgRating.vue +++ b/frontend/src/components/VariantDetails/AcmgRating.vue @@ -1,17 +1,17 @@ + + diff --git a/frontend/src/views/ProfileView.vue b/frontend/src/views/ProfileView.vue new file mode 100644 index 00000000..1e17b3bd --- /dev/null +++ b/frontend/src/views/ProfileView.vue @@ -0,0 +1,88 @@ + + + diff --git a/frontend/src/views/SvDetailView.vue b/frontend/src/views/SvDetailView.vue index 1bd962a5..072c4ca6 100644 --- a/frontend/src/views/SvDetailView.vue +++ b/frontend/src/views/SvDetailView.vue @@ -1,15 +1,14 @@