From 047bb15b86e956bb1ec0ffd83b2a318d7b463f52 Mon Sep 17 00:00:00 2001 From: Ryan <5668783+ryanzhou7@users.noreply.github.com> Date: Tue, 12 Nov 2024 21:58:40 -0800 Subject: [PATCH 1/2] wip --- fastapp/fastapp/db/base.py | 9 ++ fastapp/fastapp/db/dependencies.py | 20 +++ fastapp/fastapp/db/meta.py | 3 + fastapp/fastapp/db/models/__init__.py | 15 ++ fastapp/fastapp/db/utils.py | 13 ++ fastapp/fastapp/settings.py | 2 +- fastapp/fastapp/web/lifespan.py | 36 +++++ fastapp/poetry.lock | 201 +++++++++++++++++++++++++- fastapp/pyproject.toml | 5 +- fastapp/tests/conftest.py | 71 ++++++++- 10 files changed, 370 insertions(+), 5 deletions(-) create mode 100644 fastapp/fastapp/db/base.py create mode 100644 fastapp/fastapp/db/dependencies.py create mode 100644 fastapp/fastapp/db/meta.py create mode 100644 fastapp/fastapp/db/models/__init__.py create mode 100644 fastapp/fastapp/db/utils.py diff --git a/fastapp/fastapp/db/base.py b/fastapp/fastapp/db/base.py new file mode 100644 index 0000000..306a5e4 --- /dev/null +++ b/fastapp/fastapp/db/base.py @@ -0,0 +1,9 @@ +from sqlalchemy.orm import DeclarativeBase + +from fastapp.db.meta import meta + + +class Base(DeclarativeBase): + """Base for all models.""" + + metadata = meta diff --git a/fastapp/fastapp/db/dependencies.py b/fastapp/fastapp/db/dependencies.py new file mode 100644 index 0000000..eecf86b --- /dev/null +++ b/fastapp/fastapp/db/dependencies.py @@ -0,0 +1,20 @@ +from typing import AsyncGenerator + +from sqlalchemy.ext.asyncio import AsyncSession +from starlette.requests import Request + + +async def get_db_session(request: Request) -> AsyncGenerator[AsyncSession, None]: + """ + Create and get database session. + + :param request: current request. + :yield: database session. + """ + session: AsyncSession = request.app.state.db_session_factory() + + try: + yield session + finally: + await session.commit() + await session.close() diff --git a/fastapp/fastapp/db/meta.py b/fastapp/fastapp/db/meta.py new file mode 100644 index 0000000..4c95254 --- /dev/null +++ b/fastapp/fastapp/db/meta.py @@ -0,0 +1,3 @@ +import sqlalchemy as sa + +meta = sa.MetaData() diff --git a/fastapp/fastapp/db/models/__init__.py b/fastapp/fastapp/db/models/__init__.py new file mode 100644 index 0000000..96ce1c7 --- /dev/null +++ b/fastapp/fastapp/db/models/__init__.py @@ -0,0 +1,15 @@ +"""fastapp models.""" + +import pkgutil +from pathlib import Path + + +def load_all_models() -> None: + """Load all models from this folder.""" + package_dir = Path(__file__).resolve().parent + modules = pkgutil.walk_packages( + path=[str(package_dir)], + prefix="fastapp.db.models.", + ) + for module in modules: + __import__(module.name) diff --git a/fastapp/fastapp/db/utils.py b/fastapp/fastapp/db/utils.py new file mode 100644 index 0000000..470fbf9 --- /dev/null +++ b/fastapp/fastapp/db/utils.py @@ -0,0 +1,13 @@ +from pathlib import Path + +from fastapp.settings import settings + + +async def create_database() -> None: + """Create a database.""" + + +async def drop_database() -> None: + """Drop current database.""" + if settings.db_file.exists(): + Path(settings.db_file).unlink() diff --git a/fastapp/fastapp/settings.py b/fastapp/fastapp/settings.py index 18b27a8..8f3e439 100644 --- a/fastapp/fastapp/settings.py +++ b/fastapp/fastapp/settings.py @@ -49,7 +49,7 @@ def db_url(self) -> URL: :return: database URL. """ - return URL.build(scheme="sqlite", path=f"///{self.db_file}") + return URL.build(scheme="sqlite+aiosqlite", path=f"///{self.db_file}") model_config = SettingsConfigDict( env_file=".env", diff --git a/fastapp/fastapp/web/lifespan.py b/fastapp/fastapp/web/lifespan.py index f65a8da..8963cb3 100644 --- a/fastapp/fastapp/web/lifespan.py +++ b/fastapp/fastapp/web/lifespan.py @@ -2,6 +2,39 @@ from typing import AsyncGenerator from fastapi import FastAPI +from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine + +from fastapp.db.meta import meta +from fastapp.db.models import load_all_models +from fastapp.settings import settings + + +def _setup_db(app: FastAPI) -> None: # pragma: no cover + """ + Creates connection to the database. + + This function creates SQLAlchemy engine instance, + session_factory for creating sessions + and stores them in the application's state property. + + :param app: fastAPI application. + """ + engine = create_async_engine(str(settings.db_url), echo=settings.db_echo) + session_factory = async_sessionmaker( + engine, + expire_on_commit=False, + ) + app.state.db_engine = engine + app.state.db_session_factory = session_factory + + +async def _create_tables() -> None: # pragma: no cover + """Populates tables in the database.""" + load_all_models() + engine = create_async_engine(str(settings.db_url)) + async with engine.begin() as connection: + await connection.run_sync(meta.create_all) + await engine.dispose() @asynccontextmanager @@ -19,6 +52,9 @@ async def lifespan_setup( """ app.middleware_stack = None + _setup_db(app) + await _create_tables() app.middleware_stack = app.build_middleware_stack() yield + await app.state.db_engine.dispose() diff --git a/fastapp/poetry.lock b/fastapp/poetry.lock index 7e9c1ff..e5a9bfd 100644 --- a/fastapp/poetry.lock +++ b/fastapp/poetry.lock @@ -1,5 +1,23 @@ # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +[[package]] +name = "aiosqlite" +version = "0.20.0" +description = "asyncio bridge to the standard sqlite3 module" +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiosqlite-0.20.0-py3-none-any.whl", hash = "sha256:36a1deaca0cac40ebe32aac9977a6e2bbc7f5189f23f4a54d5908986729e5bd6"}, + {file = "aiosqlite-0.20.0.tar.gz", hash = "sha256:6d35c8c256637f4672f843c31021464090805bf925385ac39473fb16eaaca3d7"}, +] + +[package.dependencies] +typing_extensions = ">=4.0" + +[package.extras] +dev = ["attribution (==1.7.0)", "black (==24.2.0)", "coverage[toml] (==7.4.1)", "flake8 (==7.0.0)", "flake8-bugbear (==24.2.6)", "flit (==3.9.0)", "mypy (==1.8.0)", "ufmt (==2.3.0)", "usort (==1.0.8.post1)"] +docs = ["sphinx (==7.2.6)", "sphinx-mdinclude (==0.5.3)"] + [[package]] name = "annotated-types" version = "0.7.0" @@ -322,6 +340,92 @@ docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2. testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] typing = ["typing-extensions (>=4.12.2)"] +[[package]] +name = "greenlet" +version = "3.1.1" +description = "Lightweight in-process concurrent programming" +optional = false +python-versions = ">=3.7" +files = [ + {file = "greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617"}, + {file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7"}, + {file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6"}, + {file = "greenlet-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80"}, + {file = "greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a"}, + {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511"}, + {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395"}, + {file = "greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39"}, + {file = "greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9"}, + {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0"}, + {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942"}, + {file = "greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01"}, + {file = "greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e"}, + {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1"}, + {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c"}, + {file = "greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822"}, + {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01"}, + {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6"}, + {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47da355d8687fd65240c364c90a31569a133b7b60de111c255ef5b606f2ae291"}, + {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:98884ecf2ffb7d7fe6bd517e8eb99d31ff7855a840fa6d0d63cd07c037f6a981"}, + {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1d4aeb8891338e60d1ab6127af1fe45def5259def8094b9c7e34690c8858803"}, + {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db32b5348615a04b82240cc67983cb315309e88d444a288934ee6ceaebcad6cc"}, + {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dcc62f31eae24de7f8dce72134c8651c58000d3b1868e01392baea7c32c247de"}, + {file = "greenlet-3.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1d3755bcb2e02de341c55b4fca7a745a24a9e7212ac953f6b3a48d117d7257aa"}, + {file = "greenlet-3.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b8da394b34370874b4572676f36acabac172602abf054cbc4ac910219f3340af"}, + {file = "greenlet-3.1.1-cp37-cp37m-win32.whl", hash = "sha256:a0dfc6c143b519113354e780a50381508139b07d2177cb6ad6a08278ec655798"}, + {file = "greenlet-3.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:54558ea205654b50c438029505def3834e80f0869a70fb15b871c29b4575ddef"}, + {file = "greenlet-3.1.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:346bed03fe47414091be4ad44786d1bd8bef0c3fcad6ed3dee074a032ab408a9"}, + {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfc59d69fc48664bc693842bd57acfdd490acafda1ab52c7836e3fc75c90a111"}, + {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d21e10da6ec19b457b82636209cbe2331ff4306b54d06fa04b7c138ba18c8a81"}, + {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37b9de5a96111fc15418819ab4c4432e4f3c2ede61e660b1e33971eba26ef9ba"}, + {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef9ea3f137e5711f0dbe5f9263e8c009b7069d8a1acea822bd5e9dae0ae49c8"}, + {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85f3ff71e2e60bd4b4932a043fbbe0f499e263c628390b285cb599154a3b03b1"}, + {file = "greenlet-3.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:95ffcf719966dd7c453f908e208e14cde192e09fde6c7186c8f1896ef778d8cd"}, + {file = "greenlet-3.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:03a088b9de532cbfe2ba2034b2b85e82df37874681e8c470d6fb2f8c04d7e4b7"}, + {file = "greenlet-3.1.1-cp38-cp38-win32.whl", hash = "sha256:8b8b36671f10ba80e159378df9c4f15c14098c4fd73a36b9ad715f057272fbef"}, + {file = "greenlet-3.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:7017b2be767b9d43cc31416aba48aab0d2309ee31b4dbf10a1d38fb7972bdf9d"}, + {file = "greenlet-3.1.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:396979749bd95f018296af156201d6211240e7a23090f50a8d5d18c370084dc3"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca9d0ff5ad43e785350894d97e13633a66e2b50000e8a183a50a88d834752d42"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94ebba31df2aa506d7b14866fed00ac141a867e63143fe5bca82a8e503b36437"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aaad12ac0ff500f62cebed98d8789198ea0e6f233421059fa68a5aa7220145"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63e4844797b975b9af3a3fb8f7866ff08775f5426925e1e0bbcfe7932059a12c"}, + {file = "greenlet-3.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7939aa3ca7d2a1593596e7ac6d59391ff30281ef280d8632fa03d81f7c5f955e"}, + {file = "greenlet-3.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d0028e725ee18175c6e422797c407874da24381ce0690d6b9396c204c7f7276e"}, + {file = "greenlet-3.1.1-cp39-cp39-win32.whl", hash = "sha256:5e06afd14cbaf9e00899fae69b24a32f2196c19de08fcb9f4779dd4f004e5e7c"}, + {file = "greenlet-3.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:3319aa75e0e0639bc15ff54ca327e8dc7a6fe404003496e3c6925cd3142e0e22"}, + {file = "greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467"}, +] + +[package.extras] +docs = ["Sphinx", "furo"] +test = ["objgraph", "psutil"] + [[package]] name = "gunicorn" version = "22.0.0" @@ -1425,6 +1529,101 @@ files = [ {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] +[[package]] +name = "sqlalchemy" +version = "2.0.36" +description = "Database Abstraction Library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "SQLAlchemy-2.0.36-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:59b8f3adb3971929a3e660337f5dacc5942c2cdb760afcabb2614ffbda9f9f72"}, + {file = "SQLAlchemy-2.0.36-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37350015056a553e442ff672c2d20e6f4b6d0b2495691fa239d8aa18bb3bc908"}, + {file = "SQLAlchemy-2.0.36-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8318f4776c85abc3f40ab185e388bee7a6ea99e7fa3a30686580b209eaa35c08"}, + {file = "SQLAlchemy-2.0.36-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c245b1fbade9c35e5bd3b64270ab49ce990369018289ecfde3f9c318411aaa07"}, + {file = "SQLAlchemy-2.0.36-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:69f93723edbca7342624d09f6704e7126b152eaed3cdbb634cb657a54332a3c5"}, + {file = "SQLAlchemy-2.0.36-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f9511d8dd4a6e9271d07d150fb2f81874a3c8c95e11ff9af3a2dfc35fe42ee44"}, + {file = "SQLAlchemy-2.0.36-cp310-cp310-win32.whl", hash = "sha256:c3f3631693003d8e585d4200730616b78fafd5a01ef8b698f6967da5c605b3fa"}, + {file = "SQLAlchemy-2.0.36-cp310-cp310-win_amd64.whl", hash = "sha256:a86bfab2ef46d63300c0f06936bd6e6c0105faa11d509083ba8f2f9d237fb5b5"}, + {file = "SQLAlchemy-2.0.36-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fd3a55deef00f689ce931d4d1b23fa9f04c880a48ee97af488fd215cf24e2a6c"}, + {file = "SQLAlchemy-2.0.36-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4f5e9cd989b45b73bd359f693b935364f7e1f79486e29015813c338450aa5a71"}, + {file = "SQLAlchemy-2.0.36-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0ddd9db6e59c44875211bc4c7953a9f6638b937b0a88ae6d09eb46cced54eff"}, + {file = "SQLAlchemy-2.0.36-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2519f3a5d0517fc159afab1015e54bb81b4406c278749779be57a569d8d1bb0d"}, + {file = "SQLAlchemy-2.0.36-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59b1ee96617135f6e1d6f275bbe988f419c5178016f3d41d3c0abb0c819f75bb"}, + {file = "SQLAlchemy-2.0.36-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:39769a115f730d683b0eb7b694db9789267bcd027326cccc3125e862eb03bfd8"}, + {file = "SQLAlchemy-2.0.36-cp311-cp311-win32.whl", hash = "sha256:66bffbad8d6271bb1cc2f9a4ea4f86f80fe5e2e3e501a5ae2a3dc6a76e604e6f"}, + {file = "SQLAlchemy-2.0.36-cp311-cp311-win_amd64.whl", hash = "sha256:23623166bfefe1487d81b698c423f8678e80df8b54614c2bf4b4cfcd7c711959"}, + {file = "SQLAlchemy-2.0.36-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7b64e6ec3f02c35647be6b4851008b26cff592a95ecb13b6788a54ef80bbdd4"}, + {file = "SQLAlchemy-2.0.36-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46331b00096a6db1fdc052d55b101dbbfc99155a548e20a0e4a8e5e4d1362855"}, + {file = "SQLAlchemy-2.0.36-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdf3386a801ea5aba17c6410dd1dc8d39cf454ca2565541b5ac42a84e1e28f53"}, + {file = "SQLAlchemy-2.0.36-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac9dfa18ff2a67b09b372d5db8743c27966abf0e5344c555d86cc7199f7ad83a"}, + {file = "SQLAlchemy-2.0.36-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:90812a8933df713fdf748b355527e3af257a11e415b613dd794512461eb8a686"}, + {file = "SQLAlchemy-2.0.36-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1bc330d9d29c7f06f003ab10e1eaced295e87940405afe1b110f2eb93a233588"}, + {file = "SQLAlchemy-2.0.36-cp312-cp312-win32.whl", hash = "sha256:79d2e78abc26d871875b419e1fd3c0bca31a1cb0043277d0d850014599626c2e"}, + {file = "SQLAlchemy-2.0.36-cp312-cp312-win_amd64.whl", hash = "sha256:b544ad1935a8541d177cb402948b94e871067656b3a0b9e91dbec136b06a2ff5"}, + {file = "SQLAlchemy-2.0.36-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b5cc79df7f4bc3d11e4b542596c03826063092611e481fcf1c9dfee3c94355ef"}, + {file = "SQLAlchemy-2.0.36-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3c01117dd36800f2ecaa238c65365b7b16497adc1522bf84906e5710ee9ba0e8"}, + {file = "SQLAlchemy-2.0.36-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bc633f4ee4b4c46e7adcb3a9b5ec083bf1d9a97c1d3854b92749d935de40b9b"}, + {file = "SQLAlchemy-2.0.36-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e46ed38affdfc95d2c958de328d037d87801cfcbea6d421000859e9789e61c2"}, + {file = "SQLAlchemy-2.0.36-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b2985c0b06e989c043f1dc09d4fe89e1616aadd35392aea2844f0458a989eacf"}, + {file = "SQLAlchemy-2.0.36-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a121d62ebe7d26fec9155f83f8be5189ef1405f5973ea4874a26fab9f1e262c"}, + {file = "SQLAlchemy-2.0.36-cp313-cp313-win32.whl", hash = "sha256:0572f4bd6f94752167adfd7c1bed84f4b240ee6203a95e05d1e208d488d0d436"}, + {file = "SQLAlchemy-2.0.36-cp313-cp313-win_amd64.whl", hash = "sha256:8c78ac40bde930c60e0f78b3cd184c580f89456dd87fc08f9e3ee3ce8765ce88"}, + {file = "SQLAlchemy-2.0.36-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:be9812b766cad94a25bc63bec11f88c4ad3629a0cec1cd5d4ba48dc23860486b"}, + {file = "SQLAlchemy-2.0.36-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50aae840ebbd6cdd41af1c14590e5741665e5272d2fee999306673a1bb1fdb4d"}, + {file = "SQLAlchemy-2.0.36-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4557e1f11c5f653ebfdd924f3f9d5ebfc718283b0b9beebaa5dd6b77ec290971"}, + {file = "SQLAlchemy-2.0.36-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:07b441f7d03b9a66299ce7ccf3ef2900abc81c0db434f42a5694a37bd73870f2"}, + {file = "SQLAlchemy-2.0.36-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:28120ef39c92c2dd60f2721af9328479516844c6b550b077ca450c7d7dc68575"}, + {file = "SQLAlchemy-2.0.36-cp37-cp37m-win32.whl", hash = "sha256:b81ee3d84803fd42d0b154cb6892ae57ea6b7c55d8359a02379965706c7efe6c"}, + {file = "SQLAlchemy-2.0.36-cp37-cp37m-win_amd64.whl", hash = "sha256:f942a799516184c855e1a32fbc7b29d7e571b52612647866d4ec1c3242578fcb"}, + {file = "SQLAlchemy-2.0.36-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3d6718667da04294d7df1670d70eeddd414f313738d20a6f1d1f379e3139a545"}, + {file = "SQLAlchemy-2.0.36-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:72c28b84b174ce8af8504ca28ae9347d317f9dba3999e5981a3cd441f3712e24"}, + {file = "SQLAlchemy-2.0.36-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b11d0cfdd2b095e7b0686cf5fabeb9c67fae5b06d265d8180715b8cfa86522e3"}, + {file = "SQLAlchemy-2.0.36-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e32092c47011d113dc01ab3e1d3ce9f006a47223b18422c5c0d150af13a00687"}, + {file = "SQLAlchemy-2.0.36-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:6a440293d802d3011028e14e4226da1434b373cbaf4a4bbb63f845761a708346"}, + {file = "SQLAlchemy-2.0.36-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c54a1e53a0c308a8e8a7dffb59097bff7facda27c70c286f005327f21b2bd6b1"}, + {file = "SQLAlchemy-2.0.36-cp38-cp38-win32.whl", hash = "sha256:1e0d612a17581b6616ff03c8e3d5eff7452f34655c901f75d62bd86449d9750e"}, + {file = "SQLAlchemy-2.0.36-cp38-cp38-win_amd64.whl", hash = "sha256:8958b10490125124463095bbdadda5aa22ec799f91958e410438ad6c97a7b793"}, + {file = "SQLAlchemy-2.0.36-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dc022184d3e5cacc9579e41805a681187650e170eb2fd70e28b86192a479dcaa"}, + {file = "SQLAlchemy-2.0.36-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b817d41d692bf286abc181f8af476c4fbef3fd05e798777492618378448ee689"}, + {file = "SQLAlchemy-2.0.36-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4e46a888b54be23d03a89be510f24a7652fe6ff660787b96cd0e57a4ebcb46d"}, + {file = "SQLAlchemy-2.0.36-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4ae3005ed83f5967f961fd091f2f8c5329161f69ce8480aa8168b2d7fe37f06"}, + {file = "SQLAlchemy-2.0.36-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:03e08af7a5f9386a43919eda9de33ffda16b44eb11f3b313e6822243770e9763"}, + {file = "SQLAlchemy-2.0.36-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3dbb986bad3ed5ceaf090200eba750b5245150bd97d3e67343a3cfed06feecf7"}, + {file = "SQLAlchemy-2.0.36-cp39-cp39-win32.whl", hash = "sha256:9fe53b404f24789b5ea9003fc25b9a3988feddebd7e7b369c8fac27ad6f52f28"}, + {file = "SQLAlchemy-2.0.36-cp39-cp39-win_amd64.whl", hash = "sha256:af148a33ff0349f53512a049c6406923e4e02bf2f26c5fb285f143faf4f0e46a"}, + {file = "SQLAlchemy-2.0.36-py3-none-any.whl", hash = "sha256:fddbe92b4760c6f5d48162aef14824add991aeda8ddadb3c31d56eb15ca69f8e"}, + {file = "sqlalchemy-2.0.36.tar.gz", hash = "sha256:7f2767680b6d2398aea7082e45a774b2b0767b5c8d8ffb9c8b683088ea9b29c5"}, +] + +[package.dependencies] +greenlet = {version = "!=0.4.17", optional = true, markers = "python_version < \"3.13\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\") or extra == \"asyncio\""} +typing-extensions = ">=4.6.0" + +[package.extras] +aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"] +aioodbc = ["aioodbc", "greenlet (!=0.4.17)"] +aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] +asyncio = ["greenlet (!=0.4.17)"] +asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] +mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10)"] +mssql = ["pyodbc"] +mssql-pymssql = ["pymssql"] +mssql-pyodbc = ["pyodbc"] +mypy = ["mypy (>=0.910)"] +mysql = ["mysqlclient (>=1.4.0)"] +mysql-connector = ["mysql-connector-python"] +oracle = ["cx_oracle (>=8)"] +oracle-oracledb = ["oracledb (>=1.0.1)"] +postgresql = ["psycopg2 (>=2.7)"] +postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] +postgresql-pg8000 = ["pg8000 (>=1.29.1)"] +postgresql-psycopg = ["psycopg (>=3.0.7)"] +postgresql-psycopg2binary = ["psycopg2-binary"] +postgresql-psycopg2cffi = ["psycopg2cffi"] +postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] +pymysql = ["pymysql"] +sqlcipher = ["sqlcipher3_binary"] + [[package]] name = "starlette" version = "0.37.2" @@ -1938,4 +2137,4 @@ propcache = ">=0.2.0" [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "1b94ef603e8c922b464da8c957513a4abe892389c3687b776a0bf3346b6e0607" +content-hash = "39fb595b0f3024b4b1fbd595bca5cf66bbf31310d47aa91652e3e95fac8bcc91" diff --git a/fastapp/pyproject.toml b/fastapp/pyproject.toml index 19a5138..5bd2882 100644 --- a/fastapp/pyproject.toml +++ b/fastapp/pyproject.toml @@ -19,6 +19,8 @@ pydantic = "^2" pydantic-settings = "^2" yarl = "^1" ujson = "^5.10.0" +SQLAlchemy = {version = "^2.0.31", extras = ["asyncio"]} +aiosqlite = "^0.20.0" httptools = "^0.6.1" pymongo = "^4.8.0" @@ -110,6 +112,7 @@ lint.ignore = [ "D106", # Missing docstring in public nested class ] exclude = [ + "fastapp/db/migrations", ".venv/" ] lint.mccabe = { max-complexity = 10 } @@ -143,7 +146,7 @@ enable_kafka = "None" enable_loguru = "None" traefik_labels = "None" add_dummy = "None" -orm = "none" +orm = "sqlalchemy" self_hosted_swagger = "None" prometheus_enabled = "None" sentry_enabled = "None" diff --git a/fastapp/tests/conftest.py b/fastapp/tests/conftest.py index 386d2ae..31b18b4 100644 --- a/fastapp/tests/conftest.py +++ b/fastapp/tests/conftest.py @@ -3,7 +3,16 @@ import pytest from fastapi import FastAPI from httpx import AsyncClient +from sqlalchemy.ext.asyncio import ( + AsyncEngine, + AsyncSession, + async_sessionmaker, + create_async_engine, +) +from fastapp.db.dependencies import get_db_session +from fastapp.db.utils import create_database, drop_database +from fastapp.settings import settings from fastapp.web.application import get_app @@ -17,15 +26,73 @@ def anyio_backend() -> str: return "asyncio" +@pytest.fixture(scope="session") +async def _engine() -> AsyncGenerator[AsyncEngine, None]: + """ + Create engine and databases. + + :yield: new engine. + """ + from fastapp.db.meta import meta + from fastapp.db.models import load_all_models + + load_all_models() + + await create_database() + + engine = create_async_engine(str(settings.db_url)) + async with engine.begin() as conn: + await conn.run_sync(meta.create_all) + + try: + yield engine + finally: + await engine.dispose() + await drop_database() + + +@pytest.fixture +async def dbsession( + _engine: AsyncEngine, +) -> AsyncGenerator[AsyncSession, None]: + """ + Get session to database. + + Fixture that returns a SQLAlchemy session with a SAVEPOINT, and the rollback to it + after the test completes. + + :param _engine: current engine. + :yields: async session. + """ + connection = await _engine.connect() + trans = await connection.begin() + + session_maker = async_sessionmaker( + connection, + expire_on_commit=False, + ) + session = session_maker() + + try: + yield session + finally: + await session.close() + await trans.rollback() + await connection.close() + + @pytest.fixture -def fastapi_app() -> FastAPI: +def fastapi_app( + dbsession: AsyncSession, +) -> FastAPI: """ Fixture for creating FastAPI app. :return: fastapi app with mocked dependencies. """ application = get_app() - return application # noqa: RET504 + application.dependency_overrides[get_db_session] = lambda: dbsession + return application @pytest.fixture From 118a7c41404fc4c957c048bdee3a0fd7325ca70d Mon Sep 17 00:00:00 2001 From: Ryan <5668783+ryanzhou7@users.noreply.github.com> Date: Tue, 12 Nov 2024 22:08:14 -0800 Subject: [PATCH 2/2] wip --- fastapp/fastapp/db/dao/__init__.py | 1 + fastapp/fastapp/db/dao/dummy_dao.py | 50 +++++++++++++++++++++++ fastapp/fastapp/db/models/dummy_model.py | 13 ++++++ fastapp/fastapp/web/api/dummy/__init__.py | 5 +++ fastapp/fastapp/web/api/dummy/schema.py | 20 +++++++++ fastapp/fastapp/web/api/dummy/views.py | 41 +++++++++++++++++++ fastapp/fastapp/web/api/router.py | 3 +- fastapp/pyproject.toml | 2 +- fastapp/tests/test_dummy.py | 45 ++++++++++++++++++++ 9 files changed, 178 insertions(+), 2 deletions(-) create mode 100644 fastapp/fastapp/db/dao/__init__.py create mode 100644 fastapp/fastapp/db/dao/dummy_dao.py create mode 100644 fastapp/fastapp/db/models/dummy_model.py create mode 100644 fastapp/fastapp/web/api/dummy/__init__.py create mode 100644 fastapp/fastapp/web/api/dummy/schema.py create mode 100644 fastapp/fastapp/web/api/dummy/views.py create mode 100644 fastapp/tests/test_dummy.py diff --git a/fastapp/fastapp/db/dao/__init__.py b/fastapp/fastapp/db/dao/__init__.py new file mode 100644 index 0000000..db62a0a --- /dev/null +++ b/fastapp/fastapp/db/dao/__init__.py @@ -0,0 +1 @@ +"""DAO classes.""" diff --git a/fastapp/fastapp/db/dao/dummy_dao.py b/fastapp/fastapp/db/dao/dummy_dao.py new file mode 100644 index 0000000..3ebd842 --- /dev/null +++ b/fastapp/fastapp/db/dao/dummy_dao.py @@ -0,0 +1,50 @@ +from typing import List, Optional + +from fastapi import Depends +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from fastapp.db.dependencies import get_db_session +from fastapp.db.models.dummy_model import DummyModel + + +class DummyDAO: + """Class for accessing dummy table.""" + + def __init__(self, session: AsyncSession = Depends(get_db_session)) -> None: + self.session = session + + async def create_dummy_model(self, name: str) -> None: + """ + Add single dummy to session. + + :param name: name of a dummy. + """ + self.session.add(DummyModel(name=name)) + + async def get_all_dummies(self, limit: int, offset: int) -> List[DummyModel]: + """ + Get all dummy models with limit/offset pagination. + + :param limit: limit of dummies. + :param offset: offset of dummies. + :return: stream of dummies. + """ + raw_dummies = await self.session.execute( + select(DummyModel).limit(limit).offset(offset), + ) + + return list(raw_dummies.scalars().fetchall()) + + async def filter(self, name: Optional[str] = None) -> List[DummyModel]: + """ + Get specific dummy model. + + :param name: name of dummy instance. + :return: dummy models. + """ + query = select(DummyModel) + if name: + query = query.where(DummyModel.name == name) + rows = await self.session.execute(query) + return list(rows.scalars().fetchall()) diff --git a/fastapp/fastapp/db/models/dummy_model.py b/fastapp/fastapp/db/models/dummy_model.py new file mode 100644 index 0000000..5869eb1 --- /dev/null +++ b/fastapp/fastapp/db/models/dummy_model.py @@ -0,0 +1,13 @@ +from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy.sql.sqltypes import String + +from fastapp.db.base import Base + + +class DummyModel(Base): + """Model for demo purpose.""" + + __tablename__ = "dummy_model" + + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + name: Mapped[str] = mapped_column(String(length=200)) diff --git a/fastapp/fastapp/web/api/dummy/__init__.py b/fastapp/fastapp/web/api/dummy/__init__.py new file mode 100644 index 0000000..925db5b --- /dev/null +++ b/fastapp/fastapp/web/api/dummy/__init__.py @@ -0,0 +1,5 @@ +"""Dummy model API.""" + +from fastapp.web.api.dummy.views import router + +__all__ = ["router"] diff --git a/fastapp/fastapp/web/api/dummy/schema.py b/fastapp/fastapp/web/api/dummy/schema.py new file mode 100644 index 0000000..247c5d1 --- /dev/null +++ b/fastapp/fastapp/web/api/dummy/schema.py @@ -0,0 +1,20 @@ +from pydantic import BaseModel, ConfigDict + + +class DummyModelDTO(BaseModel): + """ + DTO for dummy models. + + It returned when accessing dummy models from the API. + """ + + id: int + name: str + + model_config = ConfigDict(from_attributes=True) + + +class DummyModelInputDTO(BaseModel): + """DTO for creating new dummy model.""" + + name: str diff --git a/fastapp/fastapp/web/api/dummy/views.py b/fastapp/fastapp/web/api/dummy/views.py new file mode 100644 index 0000000..0b8bbfa --- /dev/null +++ b/fastapp/fastapp/web/api/dummy/views.py @@ -0,0 +1,41 @@ +from typing import List + +from fastapi import APIRouter +from fastapi.param_functions import Depends + +from fastapp.db.dao.dummy_dao import DummyDAO +from fastapp.db.models.dummy_model import DummyModel +from fastapp.web.api.dummy.schema import DummyModelDTO, DummyModelInputDTO + +router = APIRouter() + + +@router.get("/", response_model=List[DummyModelDTO]) +async def get_dummy_models( + limit: int = 10, + offset: int = 0, + dummy_dao: DummyDAO = Depends(), +) -> List[DummyModel]: + """ + Retrieve all dummy objects from the database. + + :param limit: limit of dummy objects, defaults to 10. + :param offset: offset of dummy objects, defaults to 0. + :param dummy_dao: DAO for dummy models. + :return: list of dummy objects from database. + """ + return await dummy_dao.get_all_dummies(limit=limit, offset=offset) + + +@router.put("/") +async def create_dummy_model( + new_dummy_object: DummyModelInputDTO, + dummy_dao: DummyDAO = Depends(), +) -> None: + """ + Creates dummy model in the database. + + :param new_dummy_object: new dummy model item. + :param dummy_dao: DAO for dummy models. + """ + await dummy_dao.create_dummy_model(name=new_dummy_object.name) diff --git a/fastapp/fastapp/web/api/router.py b/fastapp/fastapp/web/api/router.py index 883b5b9..49af071 100644 --- a/fastapp/fastapp/web/api/router.py +++ b/fastapp/fastapp/web/api/router.py @@ -1,7 +1,8 @@ from fastapi.routing import APIRouter -from fastapp.web.api import echo, monitoring +from fastapp.web.api import dummy, echo, monitoring api_router = APIRouter() api_router.include_router(monitoring.router) api_router.include_router(echo.router, prefix="/echo", tags=["echo"]) +api_router.include_router(dummy.router, prefix="/dummy", tags=["dummy"]) diff --git a/fastapp/pyproject.toml b/fastapp/pyproject.toml index 5bd2882..e47c7fd 100644 --- a/fastapp/pyproject.toml +++ b/fastapp/pyproject.toml @@ -145,7 +145,7 @@ enable_routers = "True" enable_kafka = "None" enable_loguru = "None" traefik_labels = "None" -add_dummy = "None" +add_dummy = "True" orm = "sqlalchemy" self_hosted_swagger = "None" prometheus_enabled = "None" diff --git a/fastapp/tests/test_dummy.py b/fastapp/tests/test_dummy.py new file mode 100644 index 0000000..c7644ab --- /dev/null +++ b/fastapp/tests/test_dummy.py @@ -0,0 +1,45 @@ +import uuid + +import pytest +from fastapi import FastAPI +from httpx import AsyncClient +from sqlalchemy.ext.asyncio import AsyncSession +from starlette import status + +from fastapp.db.dao.dummy_dao import DummyDAO + + +@pytest.mark.anyio +async def test_creation( + fastapi_app: FastAPI, + client: AsyncClient, + dbsession: AsyncSession, +) -> None: + """Tests dummy instance creation.""" + url = fastapi_app.url_path_for("create_dummy_model") + test_name = uuid.uuid4().hex + response = await client.put(url, json={"name": test_name}) + assert response.status_code == status.HTTP_200_OK + dao = DummyDAO(dbsession) + + instances = await dao.filter(name=test_name) + assert instances[0].name == test_name + + +@pytest.mark.anyio +async def test_getting( + fastapi_app: FastAPI, + client: AsyncClient, + dbsession: AsyncSession, +) -> None: + """Tests dummy instance retrieval.""" + dao = DummyDAO(dbsession) + test_name = uuid.uuid4().hex + await dao.create_dummy_model(name=test_name) + url = fastapi_app.url_path_for("get_dummy_models") + response = await client.get(url) + dummies = response.json() + + assert response.status_code == status.HTTP_200_OK + assert len(dummies) == 1 + assert dummies[0]["name"] == test_name