From c26c62a6864c9f7a28b96107f16c95f4c9a86415 Mon Sep 17 00:00:00 2001 From: Aayush Kurup Date: Sat, 18 Feb 2023 21:49:36 +0530 Subject: [PATCH 01/12] ADD: database object with tortoise orm --- .../1676731094599205_create_users_table.sql | 19 +++ pkgs/{libs => core/database}/__init__.py | 0 pkgs/core/database/database.py | 13 ++ pkgs/core/database/repository.py | 34 +++++ poetry.lock | 131 +++++++++++++++++- pyproject.toml | 1 + splinter.yaml | 3 + 7 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 migrations/1676731094599205_create_users_table.sql rename pkgs/{libs => core/database}/__init__.py (100%) create mode 100644 pkgs/core/database/database.py create mode 100644 pkgs/core/database/repository.py create mode 100644 splinter.yaml diff --git a/migrations/1676731094599205_create_users_table.sql b/migrations/1676731094599205_create_users_table.sql new file mode 100644 index 0000000..dfbbe35 --- /dev/null +++ b/migrations/1676731094599205_create_users_table.sql @@ -0,0 +1,19 @@ +[up] +CREATE TABLE users ( + id INTEGER, + name VARCHAR(255), + email VARCHAR(255), + password VARCHAR(255), + created_at TIMESTAMP, + updated_at TIMESTAMP +); + +CREATE TRIGGER update_users_trigger +BEFORE UPDATE ON users +FOR EACH ROW +EXECUTE PROCEDURE on_update_timestamp(); + +[down] +DROP FUNCTION on_update_timestamp; +DROP TRIGGER update_users_trigger on users; +DROP TABLE users; diff --git a/pkgs/libs/__init__.py b/pkgs/core/database/__init__.py similarity index 100% rename from pkgs/libs/__init__.py rename to pkgs/core/database/__init__.py diff --git a/pkgs/core/database/database.py b/pkgs/core/database/database.py new file mode 100644 index 0000000..c29c997 --- /dev/null +++ b/pkgs/core/database/database.py @@ -0,0 +1,13 @@ +from tortoise import Tortoise + +class Database: + async def __init__(self, db_url: str) -> None: + self.db_url = f'{"DB_TYPE"}://{"USERNAME"}:{"PASSWORD"}@{"HOST"}:{"PORT"}/{"DB_NAME"}' + await Tortoise.init( + db_url=self.db_url, + modules={"models": ['pkgs.models']}, + ) + + async def __del__(self) -> None: + await Tortoise.close_connections() + diff --git a/pkgs/core/database/repository.py b/pkgs/core/database/repository.py new file mode 100644 index 0000000..b92bba5 --- /dev/null +++ b/pkgs/core/database/repository.py @@ -0,0 +1,34 @@ +from tortoise import Tortoise + +class DBRepository: + def __init__(self, db: Tortoise) -> None: + self._db = db + + + def all(self): + pass + + + def create(self): + pass + + def create_or_update(self): + pass + + def get_where(self): + pass + + def exists(self): + pass + + def first_where(self): + pass + + def bulk_insert(self): + pass + + def update(self): + pass + + def delete_where(self): + pass \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 8f54460..8940847 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,20 @@ # This file is automatically @generated by Poetry and should not be changed by hand. +[[package]] +name = "aiosqlite" +version = "0.17.0" +description = "asyncio bridge to the standard sqlite3 module" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "aiosqlite-0.17.0-py3-none-any.whl", hash = "sha256:6c49dc6d3405929b1d08eeccc72306d3677503cc5e5e43771efc1e00232e8231"}, + {file = "aiosqlite-0.17.0.tar.gz", hash = "sha256:f0e6acc24bc4864149267ac82fb46dfb3be4455f99fe21df82609cc6e6baee51"}, +] + +[package.dependencies] +typing_extensions = ">=3.7.2" + [[package]] name = "anyio" version = "3.6.2" @@ -41,6 +56,57 @@ wrapt = [ {version = ">=1.14,<2", markers = "python_version >= \"3.11\""}, ] +[[package]] +name = "asyncpg" +version = "0.27.0" +description = "An asyncio PostgreSQL driver" +category = "main" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "asyncpg-0.27.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fca608d199ffed4903dce1bcd97ad0fe8260f405c1c225bdf0002709132171c2"}, + {file = "asyncpg-0.27.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:20b596d8d074f6f695c13ffb8646d0b6bb1ab570ba7b0cfd349b921ff03cfc1e"}, + {file = "asyncpg-0.27.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a6206210c869ebd3f4eb9e89bea132aefb56ff3d1b7dd7e26b102b17e27bbb1"}, + {file = "asyncpg-0.27.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7a94c03386bb95456b12c66026b3a87d1b965f0f1e5733c36e7229f8f137747"}, + {file = "asyncpg-0.27.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bfc3980b4ba6f97138b04f0d32e8af21d6c9fa1f8e6e140c07d15690a0a99279"}, + {file = "asyncpg-0.27.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9654085f2b22f66952124de13a8071b54453ff972c25c59b5ce1173a4283ffd9"}, + {file = "asyncpg-0.27.0-cp310-cp310-win32.whl", hash = "sha256:879c29a75969eb2722f94443752f4720d560d1e748474de54ae8dd230bc4956b"}, + {file = "asyncpg-0.27.0-cp310-cp310-win_amd64.whl", hash = "sha256:ab0f21c4818d46a60ca789ebc92327d6d874d3b7ccff3963f7af0a21dc6cff52"}, + {file = "asyncpg-0.27.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:18f77e8e71e826ba2d0c3ba6764930776719ae2b225ca07e014590545928b576"}, + {file = "asyncpg-0.27.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c2232d4625c558f2aa001942cac1d7952aa9f0dbfc212f63bc754277769e1ef2"}, + {file = "asyncpg-0.27.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a3a4ff43702d39e3c97a8786314123d314e0f0e4dabc8367db5b665c93914de"}, + {file = "asyncpg-0.27.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccddb9419ab4e1c48742457d0c0362dbdaeb9b28e6875115abfe319b29ee225d"}, + {file = "asyncpg-0.27.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:768e0e7c2898d40b16d4ef7a0b44e8150db3dd8995b4652aa1fe2902e92c7df8"}, + {file = "asyncpg-0.27.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:609054a1f47292a905582a1cfcca51a6f3f30ab9d822448693e66fdddde27920"}, + {file = "asyncpg-0.27.0-cp311-cp311-win32.whl", hash = "sha256:8113e17cfe236dc2277ec844ba9b3d5312f61bd2fdae6d3ed1c1cdd75f6cf2d8"}, + {file = "asyncpg-0.27.0-cp311-cp311-win_amd64.whl", hash = "sha256:bb71211414dd1eeb8d31ec529fe77cff04bf53efc783a5f6f0a32d84923f45cf"}, + {file = "asyncpg-0.27.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4750f5cf49ed48a6e49c6e5aed390eee367694636c2dcfaf4a273ca832c5c43c"}, + {file = "asyncpg-0.27.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:eca01eb112a39d31cc4abb93a5aef2a81514c23f70956729f42fb83b11b3483f"}, + {file = "asyncpg-0.27.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:5710cb0937f696ce303f5eed6d272e3f057339bb4139378ccecafa9ee923a71c"}, + {file = "asyncpg-0.27.0-cp37-cp37m-win_amd64.whl", hash = "sha256:71cca80a056ebe19ec74b7117b09e650990c3ca535ac1c35234a96f65604192f"}, + {file = "asyncpg-0.27.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4bb366ae34af5b5cabc3ac6a5347dfb6013af38c68af8452f27968d49085ecc0"}, + {file = "asyncpg-0.27.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16ba8ec2e85d586b4a12bcd03e8d29e3d99e832764d6a1d0b8c27dbbe4a2569d"}, + {file = "asyncpg-0.27.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d20dea7b83651d93b1eb2f353511fe7fd554752844523f17ad30115d8b9c8cd6"}, + {file = "asyncpg-0.27.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e56ac8a8237ad4adec97c0cd4728596885f908053ab725e22900b5902e7f8e69"}, + {file = "asyncpg-0.27.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bf21ebf023ec67335258e0f3d3ad7b91bb9507985ba2b2206346de488267cad0"}, + {file = "asyncpg-0.27.0-cp38-cp38-win32.whl", hash = "sha256:69aa1b443a182b13a17ff926ed6627af2d98f62f2fe5890583270cc4073f63bf"}, + {file = "asyncpg-0.27.0-cp38-cp38-win_amd64.whl", hash = "sha256:62932f29cf2433988fcd799770ec64b374a3691e7902ecf85da14d5e0854d1ea"}, + {file = "asyncpg-0.27.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fddcacf695581a8d856654bc4c8cfb73d5c9df26d5f55201722d3e6a699e9629"}, + {file = "asyncpg-0.27.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7d8585707ecc6661d07367d444bbaa846b4e095d84451340da8df55a3757e152"}, + {file = "asyncpg-0.27.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:975a320baf7020339a67315284a4d3bf7460e664e484672bd3e71dbd881bc692"}, + {file = "asyncpg-0.27.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2232ebae9796d4600a7819fc383da78ab51b32a092795f4555575fc934c1c89d"}, + {file = "asyncpg-0.27.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:88b62164738239f62f4af92567b846a8ef7cf8abf53eddd83650603de4d52163"}, + {file = "asyncpg-0.27.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:eb4b2fdf88af4fb1cc569781a8f933d2a73ee82cd720e0cb4edabbaecf2a905b"}, + {file = "asyncpg-0.27.0-cp39-cp39-win32.whl", hash = "sha256:8934577e1ed13f7d2d9cea3cc016cc6f95c19faedea2c2b56a6f94f257cea672"}, + {file = "asyncpg-0.27.0-cp39-cp39-win_amd64.whl", hash = "sha256:1b6499de06fe035cf2fa932ec5617ed3f37d4ebbf663b655922e105a484a6af9"}, + {file = "asyncpg-0.27.0.tar.gz", hash = "sha256:720986d9a4705dd8a40fdf172036f5ae787225036a7eb46e704c45aa8f62c054"}, +] + +[package.extras] +dev = ["Cython (>=0.29.24,<0.30.0)", "Sphinx (>=4.1.2,<4.2.0)", "flake8 (>=5.0.4,<5.1.0)", "pytest (>=6.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)", "uvloop (>=0.15.3)"] +docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] +test = ["flake8 (>=5.0.4,<5.1.0)", "uvloop (>=0.15.3)"] + [[package]] name = "click" version = "8.1.3" @@ -255,6 +321,18 @@ files = [ {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, ] +[[package]] +name = "iso8601" +version = "1.1.0" +description = "Simple module to parse ISO 8601 dates" +category = "main" +optional = false +python-versions = ">=3.6.2,<4.0" +files = [ + {file = "iso8601-1.1.0-py3-none-any.whl", hash = "sha256:8400e90141bf792bce2634df533dc57e3bee19ea120a87bebcd3da89a58ad73f"}, + {file = "iso8601-1.1.0.tar.gz", hash = "sha256:32811e7b81deee2063ea6d2e94f8819a86d1f3811e49d23623a41fa832bef03f"}, +] + [[package]] name = "isort" version = "5.12.0" @@ -523,6 +601,18 @@ tomlkit = ">=0.10.1" spelling = ["pyenchant (>=3.2,<4.0)"] testutils = ["gitpython (>3)"] +[[package]] +name = "pypika-tortoise" +version = "0.1.6" +description = "Forked from pypika and streamline just for tortoise-orm" +category = "main" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "pypika-tortoise-0.1.6.tar.gz", hash = "sha256:d802868f479a708e3263724c7b5719a26ad79399b2a70cea065f4a4cadbebf36"}, + {file = "pypika_tortoise-0.1.6-py3-none-any.whl", hash = "sha256:2d68bbb7e377673743cff42aa1059f3a80228d411fbcae591e4465e173109fd8"}, +] + [[package]] name = "python-dotenv" version = "0.21.1" @@ -538,6 +628,18 @@ files = [ [package.extras] cli = ["click (>=5.0)"] +[[package]] +name = "pytz" +version = "2022.7.1" +description = "World timezone definitions, modern and historical" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2022.7.1-py2.py3-none-any.whl", hash = "sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a"}, + {file = "pytz-2022.7.1.tar.gz", hash = "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0"}, +] + [[package]] name = "six" version = "1.16.0" @@ -622,6 +724,33 @@ files = [ {file = "tomlkit-0.11.6.tar.gz", hash = "sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73"}, ] +[[package]] +name = "tortoise-orm" +version = "0.19.3" +description = "Easy async ORM for python, built with relations in mind" +category = "main" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "tortoise_orm-0.19.3-py3-none-any.whl", hash = "sha256:9e368820c70a0866ef9c521d43aa5503485bd7a20a561edc0933b7b0f7036fbc"}, + {file = "tortoise_orm-0.19.3.tar.gz", hash = "sha256:ca574bca5191f55608f9013314b1f5d1c6ffd4165a1fcc2f60f6c902f529b3b6"}, +] + +[package.dependencies] +aiosqlite = ">=0.16.0,<0.18.0" +asyncpg = {version = "*", optional = true, markers = "extra == \"asyncpg\""} +iso8601 = ">=1.0.2,<2.0.0" +pypika-tortoise = ">=0.1.6,<0.2.0" +pytz = "*" + +[package.extras] +accel = ["ciso8601", "orjson", "uvloop"] +aiomysql = ["aiomysql"] +asyncmy = ["asyncmy (>=0.2.5,<0.3.0)"] +asyncodbc = ["asyncodbc (>=0.1.1,<0.2.0)"] +asyncpg = ["asyncpg"] +psycopg = ["psycopg[binary,pool] (==3.0.12)"] + [[package]] name = "typing-extensions" version = "4.4.0" @@ -730,4 +859,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "fd99568899115c4847077a6e18593fa278b87bae930e1cd0c0ee914660a7eb15" +content-hash = "527fe098aaac4634daec86e69ca7b1f6ccd27bbe998dca77a2a4bc774060e489" diff --git a/pyproject.toml b/pyproject.toml index c7887dc..c826b0c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ uvicorn = "^0.20.0" orjson = "^3.8.5" pydantic = {extras = ["email"], version = "^1.10.4"} python-dotenv = "^0.21.1" +tortoise-orm = {extras = ["asyncpg"], version = "^0.19.3"} [tool.poetry.group.dev.dependencies] diff --git a/splinter.yaml b/splinter.yaml new file mode 100644 index 0000000..2e43bed --- /dev/null +++ b/splinter.yaml @@ -0,0 +1,3 @@ +MIGRATIONS_PATH: ./migrations/ +DRIVER: postgres +DB_URI: postgresql://aayush:1234567890@localhost:5432/aayush \ No newline at end of file From 88a5607f526d818fb9fe41b5ea35548d722f9111 Mon Sep 17 00:00:00 2001 From: Aayush Kurup Date: Sun, 19 Feb 2023 12:52:43 +0530 Subject: [PATCH 02/12] ADD: db config and model --- .env.example | 9 ++++++++- config/database.py | 10 ++++++++++ pkgs/common/models/user.py | 14 ++++++++++++++ pkgs/core/database/database.py | 7 ++++--- 4 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 pkgs/common/models/user.py diff --git a/.env.example b/.env.example index fe611b5..42d8e87 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,9 @@ APP_NAME= -APP_PORT= \ No newline at end of file +APP_PORT= + +DB_DRIVER= +DB_USER= +DB_PASSWORD= +DB_HOST= +DB_PORT= +DB_NAME= \ No newline at end of file diff --git a/config/database.py b/config/database.py index e69de29..e8becd5 100644 --- a/config/database.py +++ b/config/database.py @@ -0,0 +1,10 @@ +import os + +db = { + "driver": os.environ.get('DB_DRIVER'), + "user": os.environ.get('DB_USER'), + "password": os.environ.get('DB_PASSWORD'), + "host": os.environ.get('DB_HOST'), + "port": os.environ.get('DB_PORT'), + "name": os.environ.get('DB_NAME'), +} diff --git a/pkgs/common/models/user.py b/pkgs/common/models/user.py new file mode 100644 index 0000000..4ca4c9a --- /dev/null +++ b/pkgs/common/models/user.py @@ -0,0 +1,14 @@ +from tortoise.fields import IntField, CharField, DateTimeField +from tortoise.models import Model + + +class UserModel(Model): + id = IntField() + name = CharField() + email = CharField() + password = CharField() + created_at = DateTimeField(auto_now=True) + updated_at = DateTimeField(auto_now=True) + + class PydanticMeta: + exclude = ["password"] \ No newline at end of file diff --git a/pkgs/core/database/database.py b/pkgs/core/database/database.py index c29c997..68ac532 100644 --- a/pkgs/core/database/database.py +++ b/pkgs/core/database/database.py @@ -1,11 +1,12 @@ from tortoise import Tortoise +from config.utils import resolve class Database: - async def __init__(self, db_url: str) -> None: - self.db_url = f'{"DB_TYPE"}://{"USERNAME"}:{"PASSWORD"}@{"HOST"}:{"PORT"}/{"DB_NAME"}' + async def __init__(self) -> None: + self.db_url = f"{resolve('db.driver')}://{resolve('db.user')}:{resolve('db.password')}@{resolve('db.host')}:{resolve('db.port')}/{resolve('db.name')}" await Tortoise.init( db_url=self.db_url, - modules={"models": ['pkgs.models']}, + modules={"models": ['pkgs.common.models']}, ) async def __del__(self) -> None: From 9bf8ce0f940a95e2b2c3bae61a8f593c004ea57e Mon Sep 17 00:00:00 2001 From: Aayush Kurup Date: Thu, 2 Mar 2023 12:50:53 +0530 Subject: [PATCH 03/12] ADD: containe for db --- apps/users/container.py | 4 +++- apps/users/controllers/user.py | 2 +- config/__init__.py | 4 +++- config/utils/__init__.py | 1 + config/utils/resolve.py | 2 +- .../1676731094599205_create_users_table.sql | 1 - pkgs/core/__init__.py | 3 ++- pkgs/core/container.py | 9 +++++++++ pkgs/core/database/__init__.py | 1 + pkgs/core/database/database.py | 19 ++++++++++--------- pkgs/core/http/exception.py | 1 - pkgs/core/http/response.py | 5 ++--- splinter.yaml | 2 +- 13 files changed, 34 insertions(+), 20 deletions(-) create mode 100644 config/utils/__init__.py create mode 100644 pkgs/core/container.py diff --git a/apps/users/container.py b/apps/users/container.py index 4bfda23..dac4eb2 100644 --- a/apps/users/container.py +++ b/apps/users/container.py @@ -1,6 +1,8 @@ from dependency_injector.containers import DeclarativeContainer from dependency_injector.providers import Singleton +from pkgs.core import core_container from .services import UserService class UserContainer(DeclarativeContainer): - user_service = Singleton(UserService) \ No newline at end of file + user_service = Singleton(UserService) + db = core_container.db \ No newline at end of file diff --git a/apps/users/controllers/user.py b/apps/users/controllers/user.py index 918dc42..7fd25e6 100644 --- a/apps/users/controllers/user.py +++ b/apps/users/controllers/user.py @@ -9,5 +9,5 @@ @router.post('/users') @inject -def get_users(user: UserModel, user_service: UserService = Depends(Provide[UserContainer.user_service])) -> Dict: +async def get_users(user: UserModel, user_service: UserService = Depends(Provide[UserContainer.user_service]), db = Depends(Provide[UserContainer.db])) -> Dict: return user_service.greet() diff --git a/config/__init__.py b/config/__init__.py index 4d6679f..57f7ec5 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -1,7 +1,9 @@ from .app import app +from .database import db config = { - "app": app + "app": app, + "db": db } from .utils.resolve import resolve \ No newline at end of file diff --git a/config/utils/__init__.py b/config/utils/__init__.py new file mode 100644 index 0000000..5d218f4 --- /dev/null +++ b/config/utils/__init__.py @@ -0,0 +1 @@ +from .resolve import resolve \ No newline at end of file diff --git a/config/utils/resolve.py b/config/utils/resolve.py index eab5134..e796d3f 100644 --- a/config/utils/resolve.py +++ b/config/utils/resolve.py @@ -2,7 +2,7 @@ from .. import config def resolve(key: str): - pattern = r'(\w+.\w+)*(\.\w+)+' + pattern = r'^(\w+(.\w+)+)$' if not re.match(pattern, key): raise ValueError("Invalid key") diff --git a/migrations/1676731094599205_create_users_table.sql b/migrations/1676731094599205_create_users_table.sql index dfbbe35..efd0641 100644 --- a/migrations/1676731094599205_create_users_table.sql +++ b/migrations/1676731094599205_create_users_table.sql @@ -14,6 +14,5 @@ FOR EACH ROW EXECUTE PROCEDURE on_update_timestamp(); [down] -DROP FUNCTION on_update_timestamp; DROP TRIGGER update_users_trigger on users; DROP TABLE users; diff --git a/pkgs/core/__init__.py b/pkgs/core/__init__.py index 11188a9..ef4ec52 100644 --- a/pkgs/core/__init__.py +++ b/pkgs/core/__init__.py @@ -1 +1,2 @@ -from .http import * \ No newline at end of file +from .http import * +from .container import core_container \ No newline at end of file diff --git a/pkgs/core/container.py b/pkgs/core/container.py new file mode 100644 index 0000000..bae1451 --- /dev/null +++ b/pkgs/core/container.py @@ -0,0 +1,9 @@ +from dependency_injector.containers import DeclarativeContainer +from dependency_injector.providers import Resource +from .database import init_db, close_db +class CoreContainer: + db = Resource( + init_db + ) + +core_container = CoreContainer() \ No newline at end of file diff --git a/pkgs/core/database/__init__.py b/pkgs/core/database/__init__.py index e69de29..488b80e 100644 --- a/pkgs/core/database/__init__.py +++ b/pkgs/core/database/__init__.py @@ -0,0 +1 @@ +from .database import * \ No newline at end of file diff --git a/pkgs/core/database/database.py b/pkgs/core/database/database.py index 68ac532..548154f 100644 --- a/pkgs/core/database/database.py +++ b/pkgs/core/database/database.py @@ -1,14 +1,15 @@ from tortoise import Tortoise from config.utils import resolve -class Database: - async def __init__(self) -> None: - self.db_url = f"{resolve('db.driver')}://{resolve('db.user')}:{resolve('db.password')}@{resolve('db.host')}:{resolve('db.port')}/{resolve('db.name')}" - await Tortoise.init( - db_url=self.db_url, - modules={"models": ['pkgs.common.models']}, - ) - async def __del__(self) -> None: - await Tortoise.close_connections() +async def init_db(): + db_url = f"{resolve('db.driver')}://{resolve('db.user')}:{resolve('db.password')}@{resolve('db.host')}:{resolve('db.port')}/{resolve('db.name')}" + await Tortoise.init( + db_url=db_url, + modules={"models": ['pkgs.common.models']}, + ) + return Tortoise.get_connection("default") + +async def close_db(): + await Tortoise.close_connections() diff --git a/pkgs/core/http/exception.py b/pkgs/core/http/exception.py index 7672431..bebac32 100644 --- a/pkgs/core/http/exception.py +++ b/pkgs/core/http/exception.py @@ -21,7 +21,6 @@ async def custom_route_handler(request: Request) -> Request: def parse_validation_error(self, errors: List[Dict]): responses = [] - print(errors) for error in errors: responses.append({ "location": '->'.join(error['loc']), diff --git a/pkgs/core/http/response.py b/pkgs/core/http/response.py index 8178d55..704a873 100644 --- a/pkgs/core/http/response.py +++ b/pkgs/core/http/response.py @@ -1,9 +1,8 @@ from typing import Dict -from fastapi import Response from fastapi.responses import ORJSONResponse import orjson -class HTTPResponse(Response): +class HTTPResponse(ORJSONResponse): media_type = 'application/json' def render(self, content: Dict) -> bytes: @@ -14,4 +13,4 @@ def render(self, content: Dict) -> bytes: response['data'] = content response['code'] = self.status_code response['success'] = True if (self.status_code >= 200 or self.status_code < 300) else False - return ORJSONResponse(response, status_code=self.status_code) \ No newline at end of file + return orjson.dumps(response) \ No newline at end of file diff --git a/splinter.yaml b/splinter.yaml index 2e43bed..fb8b218 100644 --- a/splinter.yaml +++ b/splinter.yaml @@ -1,3 +1,3 @@ MIGRATIONS_PATH: ./migrations/ DRIVER: postgres -DB_URI: postgresql://aayush:1234567890@localhost:5432/aayush \ No newline at end of file +DB_URI: ://@:/ \ No newline at end of file From cd7600fff2bd300c5bd1fdcdf236f502b3b1486f Mon Sep 17 00:00:00 2001 From: Aayush Kurup Date: Thu, 2 Mar 2023 18:48:12 +0530 Subject: [PATCH 04/12] ADD: Container with async db resource --- .../1676731094599205_create_users_table.sql | 6 +++--- pkgs/common/models/user.py | 19 +++++++++---------- pkgs/core/container.py | 3 ++- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/migrations/1676731094599205_create_users_table.sql b/migrations/1676731094599205_create_users_table.sql index efd0641..1e1b04e 100644 --- a/migrations/1676731094599205_create_users_table.sql +++ b/migrations/1676731094599205_create_users_table.sql @@ -1,11 +1,11 @@ [up] CREATE TABLE users ( - id INTEGER, + id SERIAL, name VARCHAR(255), email VARCHAR(255), password VARCHAR(255), - created_at TIMESTAMP, - updated_at TIMESTAMP + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() ); CREATE TRIGGER update_users_trigger diff --git a/pkgs/common/models/user.py b/pkgs/common/models/user.py index 4ca4c9a..7eb9321 100644 --- a/pkgs/common/models/user.py +++ b/pkgs/common/models/user.py @@ -1,14 +1,13 @@ -from tortoise.fields import IntField, CharField, DateTimeField +from tortoise.fields import IntField, CharField, DatetimeField from tortoise.models import Model - class UserModel(Model): - id = IntField() - name = CharField() - email = CharField() - password = CharField() - created_at = DateTimeField(auto_now=True) - updated_at = DateTimeField(auto_now=True) + id = IntField(pk=True) + name = CharField(max_length=255) + email = CharField(max_length=255) + password = CharField(max_length=255) + created_at = DatetimeField(auto_now=True) + updated_at = DatetimeField(auto_now=True) - class PydanticMeta: - exclude = ["password"] \ No newline at end of file + class Meta: + table="users" \ No newline at end of file diff --git a/pkgs/core/container.py b/pkgs/core/container.py index bae1451..4a19038 100644 --- a/pkgs/core/container.py +++ b/pkgs/core/container.py @@ -1,6 +1,7 @@ from dependency_injector.containers import DeclarativeContainer from dependency_injector.providers import Resource -from .database import init_db, close_db +from .database import init_db + class CoreContainer: db = Resource( init_db From f252923fec357b60e19076bc783e38c86667b134 Mon Sep 17 00:00:00 2001 From: Aayush Kurup Date: Thu, 2 Mar 2023 18:49:57 +0530 Subject: [PATCH 05/12] ADD: repository injected into services --- apps/users/container.py | 7 ++++--- apps/users/controllers/user.py | 5 +++-- apps/users/services/users.py | 12 ++++++++++-- pkgs/common/__init__.py | 0 pkgs/common/models/__init__.py | 0 pkgs/core/database/__init__.py | 3 ++- pkgs/core/database/database.py | 5 +---- pkgs/core/database/repository.py | 8 +++++--- pkgs/users/__init__.py | 1 + pkgs/users/container.py | 11 +++++++++++ pkgs/users/repositories/__init__.py | 1 + pkgs/users/repositories/users.py | 7 +++++++ pkgs/users/services/__init__.py | 1 + pkgs/users/services/users.py | 9 +++++++++ 14 files changed, 55 insertions(+), 15 deletions(-) create mode 100644 pkgs/common/__init__.py create mode 100644 pkgs/common/models/__init__.py create mode 100644 pkgs/users/__init__.py create mode 100644 pkgs/users/container.py create mode 100644 pkgs/users/repositories/__init__.py create mode 100644 pkgs/users/repositories/users.py create mode 100644 pkgs/users/services/__init__.py create mode 100644 pkgs/users/services/users.py diff --git a/apps/users/container.py b/apps/users/container.py index dac4eb2..4ecf49c 100644 --- a/apps/users/container.py +++ b/apps/users/container.py @@ -1,8 +1,9 @@ from dependency_injector.containers import DeclarativeContainer from dependency_injector.providers import Singleton -from pkgs.core import core_container +from pkgs.users import UserLibContainer + from .services import UserService class UserContainer(DeclarativeContainer): - user_service = Singleton(UserService) - db = core_container.db \ No newline at end of file + user_container = UserLibContainer() + user_service = Singleton(UserService, service=user_container.service) \ No newline at end of file diff --git a/apps/users/controllers/user.py b/apps/users/controllers/user.py index 7fd25e6..c02b1e6 100644 --- a/apps/users/controllers/user.py +++ b/apps/users/controllers/user.py @@ -9,5 +9,6 @@ @router.post('/users') @inject -async def get_users(user: UserModel, user_service: UserService = Depends(Provide[UserContainer.user_service]), db = Depends(Provide[UserContainer.db])) -> Dict: - return user_service.greet() +async def get_users(user: UserModel, user_service: UserService = Depends(Provide[UserContainer.user_service])): + user = await user_service.get_all() + return user diff --git a/apps/users/services/users.py b/apps/users/services/users.py index 6b0ae5c..fb31697 100644 --- a/apps/users/services/users.py +++ b/apps/users/services/users.py @@ -1,5 +1,13 @@ from typing import Dict +from dependency_injector.wiring import inject +from pkgs.users.services import UserLibService + +@inject class UserService: - def greet(self) -> Dict: - return {'message': "Hello there!"} \ No newline at end of file + def __init__(self, service: UserLibService) -> None: + self.service = service + + async def get_all(self): + users = await self.service.repo.all() + return users \ No newline at end of file diff --git a/pkgs/common/__init__.py b/pkgs/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pkgs/common/models/__init__.py b/pkgs/common/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pkgs/core/database/__init__.py b/pkgs/core/database/__init__.py index 488b80e..0ad6517 100644 --- a/pkgs/core/database/__init__.py +++ b/pkgs/core/database/__init__.py @@ -1 +1,2 @@ -from .database import * \ No newline at end of file +from .database import * +from .repository import * \ No newline at end of file diff --git a/pkgs/core/database/database.py b/pkgs/core/database/database.py index 548154f..f775c30 100644 --- a/pkgs/core/database/database.py +++ b/pkgs/core/database/database.py @@ -6,10 +6,7 @@ async def init_db(): db_url = f"{resolve('db.driver')}://{resolve('db.user')}:{resolve('db.password')}@{resolve('db.host')}:{resolve('db.port')}/{resolve('db.name')}" await Tortoise.init( db_url=db_url, - modules={"models": ['pkgs.common.models']}, + modules={"models": ['pkgs.common.models', 'pkgs.common.models.user']}, ) return Tortoise.get_connection("default") - -async def close_db(): - await Tortoise.close_connections() diff --git a/pkgs/core/database/repository.py b/pkgs/core/database/repository.py index b92bba5..b777825 100644 --- a/pkgs/core/database/repository.py +++ b/pkgs/core/database/repository.py @@ -1,12 +1,14 @@ from tortoise import Tortoise +from tortoise.models import Model class DBRepository: - def __init__(self, db: Tortoise) -> None: + def __init__(self, db: Tortoise, model: Model) -> None: self._db = db + self._model = model - def all(self): - pass + async def all(self): + return await self._model.all(using_db=self._db).values() def create(self): diff --git a/pkgs/users/__init__.py b/pkgs/users/__init__.py new file mode 100644 index 0000000..f8e30d4 --- /dev/null +++ b/pkgs/users/__init__.py @@ -0,0 +1 @@ +from .container import UserLibContainer \ No newline at end of file diff --git a/pkgs/users/container.py b/pkgs/users/container.py new file mode 100644 index 0000000..680411c --- /dev/null +++ b/pkgs/users/container.py @@ -0,0 +1,11 @@ +from dependency_injector.containers import DeclarativeContainer +from dependency_injector.providers import Singleton +from pkgs.core import core_container +from pkgs.common.models.user import UserModel + +from .services import UserLibService +from .repositories import UserRepository + +class UserLibContainer(DeclarativeContainer): + repo = Singleton(UserRepository, db=core_container.db, model=UserModel) + service = Singleton(UserLibService, repo=repo) \ No newline at end of file diff --git a/pkgs/users/repositories/__init__.py b/pkgs/users/repositories/__init__.py new file mode 100644 index 0000000..212ba26 --- /dev/null +++ b/pkgs/users/repositories/__init__.py @@ -0,0 +1 @@ +from .users import UserRepository \ No newline at end of file diff --git a/pkgs/users/repositories/users.py b/pkgs/users/repositories/users.py new file mode 100644 index 0000000..076aa06 --- /dev/null +++ b/pkgs/users/repositories/users.py @@ -0,0 +1,7 @@ +from dependency_injector.wiring import inject + +from pkgs.core.database import DBRepository + +@inject +class UserRepository(DBRepository): + pass \ No newline at end of file diff --git a/pkgs/users/services/__init__.py b/pkgs/users/services/__init__.py new file mode 100644 index 0000000..ac59dcb --- /dev/null +++ b/pkgs/users/services/__init__.py @@ -0,0 +1 @@ +from .users import UserLibService \ No newline at end of file diff --git a/pkgs/users/services/users.py b/pkgs/users/services/users.py new file mode 100644 index 0000000..8817dc6 --- /dev/null +++ b/pkgs/users/services/users.py @@ -0,0 +1,9 @@ +from dependency_injector.wiring import inject + +from ..repositories import UserRepository + + +@inject +class UserLibService: + def __init__(self, repo: UserRepository) -> None: + self.repo = repo From f8cf8067743816519c41c510914bb085681abd04 Mon Sep 17 00:00:00 2001 From: Aayush Kurup Date: Fri, 3 Mar 2023 12:01:14 +0530 Subject: [PATCH 06/12] ADD: repository methods --- pkgs/core/database/repository.py | 35 +++++++++++++++++--------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/pkgs/core/database/repository.py b/pkgs/core/database/repository.py index b777825..7d6273a 100644 --- a/pkgs/core/database/repository.py +++ b/pkgs/core/database/repository.py @@ -11,26 +11,29 @@ async def all(self): return await self._model.all(using_db=self._db).values() - def create(self): - pass + async def create(self, data): + return await self._model.create(using_db=self._db, **data) - def create_or_update(self): - pass + async def create_or_update(self, data): + return await self._model.update_or_create( + data, + using_db=self._db + ) - def get_where(self): - pass + async def get_where(self, filter): + return await self._model.filter(**filter).values() - def exists(self): - pass + async def first_where(self, filter): + return await self._model.filter(**filter).first() - def first_where(self): - pass + async def exists(self, filter): + return await self._model.filter(**filter).count() - def bulk_insert(self): - pass + async def bulk_insert(self, data): + return await self._model.bulk_create(data, using_db=self._db) - def update(self): - pass + async def update_where(self, filter, data): + return await self._model.filter(**filter).update(**data) - def delete_where(self): - pass \ No newline at end of file + async def delete_where(self, filter): + return await self._model.filter(filter).delete() \ No newline at end of file From aff642a6357fd05f37ce99a2efc3a035fe353b05 Mon Sep 17 00:00:00 2001 From: Aayush Kurup Date: Sat, 4 Mar 2023 01:23:10 +0530 Subject: [PATCH 07/12] ADD: basic crud apis as example --- apps/users/controllers/user.py | 35 ++++++++++++++++--- apps/users/services/users.py | 20 ++++++++--- apps/users/validators/__init__.py | 2 ++ apps/users/validators/create_user.py | 6 ++++ apps/users/validators/update_user.py | 7 ++++ apps/users/validators/user.py | 6 ---- .../1676731094599205_create_users_table.sql | 4 +-- pkgs/core/database/repository.py | 15 +++++--- pkgs/core/http/response.py | 4 +++ 9 files changed, 77 insertions(+), 22 deletions(-) create mode 100644 apps/users/validators/__init__.py create mode 100644 apps/users/validators/create_user.py create mode 100644 apps/users/validators/update_user.py delete mode 100644 apps/users/validators/user.py diff --git a/apps/users/controllers/user.py b/apps/users/controllers/user.py index c02b1e6..15d2c10 100644 --- a/apps/users/controllers/user.py +++ b/apps/users/controllers/user.py @@ -1,14 +1,39 @@ -from typing import Dict -from fastapi import Depends, HTTPException +from fastapi import Depends +from fastapi import status from dependency_injector.wiring import inject, Provide from pkgs.core import create_router from ..container import UserContainer from ..services import UserService -from ..validators.user import UserModel +from ..validators import CreateUserDTO, UpdateUserDTO + router = create_router('/api/v1') @router.post('/users') @inject -async def get_users(user: UserModel, user_service: UserService = Depends(Provide[UserContainer.user_service])): - user = await user_service.get_all() +async def get_users(data: CreateUserDTO, user_service: UserService = Depends(Provide[UserContainer.user_service])): + user = await user_service.create_user(data) + return user + +@router.get('/users') +@inject +async def get_users(user_service: UserService = Depends(Provide[UserContainer.user_service])): + user = await user_service.get_all_users() return user + + +@router.get('/users/{id}') +@inject +async def get_users(id: int, user_service: UserService = Depends(Provide[UserContainer.user_service])): + user = await user_service.get_user_by_id(id) + return user + + +@router.patch('/users/{id}', status_code=status.HTTP_204_NO_CONTENT) +@inject +async def get_users(id: int, data: UpdateUserDTO, user_service: UserService = Depends(Provide[UserContainer.user_service])): + await user_service.update_user_by_id(data, { "id": id }) + +@router.delete('/users/{id}', status_code=status.HTTP_204_NO_CONTENT) +@inject +async def get_users(id: int, user_service: UserService = Depends(Provide[UserContainer.user_service])): + await user_service.delete_user_by_id(id) \ No newline at end of file diff --git a/apps/users/services/users.py b/apps/users/services/users.py index fb31697..d3cd810 100644 --- a/apps/users/services/users.py +++ b/apps/users/services/users.py @@ -1,13 +1,25 @@ -from typing import Dict from dependency_injector.wiring import inject from pkgs.users.services import UserLibService +from ..validators import CreateUserDTO, UpdateUserDTO @inject class UserService: def __init__(self, service: UserLibService) -> None: self.service = service - async def get_all(self): - users = await self.service.repo.all() - return users \ No newline at end of file + async def create_user(self, data: CreateUserDTO): + return await self.service.repo.create(data.dict()) + + async def get_all_users(self): + return await self.service.repo.all() + + async def get_user_by_id(self, id: int): + return await self.service.repo.get_where({ "id": id }) + + async def update_user_by_id(self, data: UpdateUserDTO, filter): + return await self.service.repo.update_where(filter, data.dict(exclude_none=True)) + + async def delete_user_by_id(self, id: int): + return await self.service.repo.delete_where({ "id": id }) + diff --git a/apps/users/validators/__init__.py b/apps/users/validators/__init__.py new file mode 100644 index 0000000..6afc363 --- /dev/null +++ b/apps/users/validators/__init__.py @@ -0,0 +1,2 @@ +from .create_user import CreateUserDTO +from .update_user import UpdateUserDTO \ No newline at end of file diff --git a/apps/users/validators/create_user.py b/apps/users/validators/create_user.py new file mode 100644 index 0000000..e83cf92 --- /dev/null +++ b/apps/users/validators/create_user.py @@ -0,0 +1,6 @@ +from pydantic import BaseModel, EmailStr, constr + +class CreateUserDTO(BaseModel): + name: str + email: EmailStr + password: constr(max_length=255, min_length=8) \ No newline at end of file diff --git a/apps/users/validators/update_user.py b/apps/users/validators/update_user.py new file mode 100644 index 0000000..16d3cf8 --- /dev/null +++ b/apps/users/validators/update_user.py @@ -0,0 +1,7 @@ +from typing import Optional +from pydantic import BaseModel, EmailStr, constr + +class UpdateUserDTO(BaseModel): + name: Optional[str] + email: Optional[EmailStr] + password: Optional[constr(max_length=255, min_length=8)] \ No newline at end of file diff --git a/apps/users/validators/user.py b/apps/users/validators/user.py deleted file mode 100644 index 19b17f1..0000000 --- a/apps/users/validators/user.py +++ /dev/null @@ -1,6 +0,0 @@ -from pydantic import BaseModel, EmailStr - -class UserModel(BaseModel): - name: str - email: EmailStr - \ No newline at end of file diff --git a/migrations/1676731094599205_create_users_table.sql b/migrations/1676731094599205_create_users_table.sql index 1e1b04e..675f348 100644 --- a/migrations/1676731094599205_create_users_table.sql +++ b/migrations/1676731094599205_create_users_table.sql @@ -4,8 +4,8 @@ CREATE TABLE users ( name VARCHAR(255), email VARCHAR(255), password VARCHAR(255), - created_at TIMESTAMP DEFAULT NOW(), - updated_at TIMESTAMP DEFAULT NOW() + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE TRIGGER update_users_trigger diff --git a/pkgs/core/database/repository.py b/pkgs/core/database/repository.py index 7d6273a..b880c2e 100644 --- a/pkgs/core/database/repository.py +++ b/pkgs/core/database/repository.py @@ -1,3 +1,4 @@ +from fastapi import HTTPException, status from tortoise import Tortoise from tortoise.models import Model @@ -32,8 +33,12 @@ async def exists(self, filter): async def bulk_insert(self, data): return await self._model.bulk_create(data, using_db=self._db) - async def update_where(self, filter, data): - return await self._model.filter(**filter).update(**data) - - async def delete_where(self, filter): - return await self._model.filter(filter).delete() \ No newline at end of file + async def update_where(self, filter, data, error=True): + rows = await self._model.filter(**filter).update(**data) + if rows <= 0 and error: + raise HTTPException(detail=f"{self._model._meta._model.__qualname__} not found", status_code=status.HTTP_404_NOT_FOUND) + + async def delete_where(self, filter, error = True): + rows = await self._model.filter(**filter).delete() + if rows <= 0 and True: + raise HTTPException(detail=f"{self._model._meta._model.__qualname__} not found", status_code=status.HTTP_404_NOT_FOUND) diff --git a/pkgs/core/http/response.py b/pkgs/core/http/response.py index 704a873..20ed8a8 100644 --- a/pkgs/core/http/response.py +++ b/pkgs/core/http/response.py @@ -1,4 +1,5 @@ from typing import Dict +from fastapi import status from fastapi.responses import ORJSONResponse import orjson @@ -6,6 +7,9 @@ class HTTPResponse(ORJSONResponse): media_type = 'application/json' def render(self, content: Dict) -> bytes: + if self.status_code == status.HTTP_204_NO_CONTENT: + return + response = {} if 'meta' in content: response['meta'] = content.pop('meta') From 62c92aba0d14194bb783d5a879a5421014c14210 Mon Sep 17 00:00:00 2001 From: Aayush Kurup Date: Fri, 17 Mar 2023 23:49:57 +0530 Subject: [PATCH 08/12] ADD: basemodel for extending tortoiseorm models --- config/app.py | 2 +- main.py | 4 ++-- migrations/1676731094599205_create_users_table.sql | 1 + pkgs/common/models/user.py | 5 +++-- pkgs/core/database/__init__.py | 3 ++- pkgs/core/database/basemodel.py | 11 +++++++++++ 6 files changed, 20 insertions(+), 6 deletions(-) create mode 100644 pkgs/core/database/basemodel.py diff --git a/config/app.py b/config/app.py index ebf13a9..f78fea6 100644 --- a/config/app.py +++ b/config/app.py @@ -2,5 +2,5 @@ app = { "name": os.environ.get('APP_NAME') or "fastapi-boilerplate", - "port": os.environ.get('APP_PORT') or 8080 + "port": int(os.environ.get('APP_PORT')) or 8080 } \ No newline at end of file diff --git a/main.py b/main.py index 1413b4b..0bd5227 100644 --- a/main.py +++ b/main.py @@ -1,8 +1,8 @@ import uvicorn from config import resolve -from dotenv import load_dotenv +from dotenv import load_dotenv, find_dotenv -load_dotenv() +load_dotenv(find_dotenv()) if __name__ == '__main__': uvicorn.run('apps.users.app:app', host='localhost', port=resolve('app.port'), reload=True) \ No newline at end of file diff --git a/migrations/1676731094599205_create_users_table.sql b/migrations/1676731094599205_create_users_table.sql index 675f348..705bdc8 100644 --- a/migrations/1676731094599205_create_users_table.sql +++ b/migrations/1676731094599205_create_users_table.sql @@ -1,6 +1,7 @@ [up] CREATE TABLE users ( id SERIAL, + uuid VARCHAR(255), name VARCHAR(255), email VARCHAR(255), password VARCHAR(255), diff --git a/pkgs/common/models/user.py b/pkgs/common/models/user.py index 7eb9321..d01e328 100644 --- a/pkgs/common/models/user.py +++ b/pkgs/common/models/user.py @@ -1,8 +1,9 @@ from tortoise.fields import IntField, CharField, DatetimeField -from tortoise.models import Model +from pkgs.core.database import BaseModel -class UserModel(Model): +class UserModel(BaseModel): id = IntField(pk=True) + uuid = CharField(max_length=255) name = CharField(max_length=255) email = CharField(max_length=255) password = CharField(max_length=255) diff --git a/pkgs/core/database/__init__.py b/pkgs/core/database/__init__.py index 0ad6517..60c4e27 100644 --- a/pkgs/core/database/__init__.py +++ b/pkgs/core/database/__init__.py @@ -1,2 +1,3 @@ from .database import * -from .repository import * \ No newline at end of file +from .repository import * +from .basemodel import * \ No newline at end of file diff --git a/pkgs/core/database/basemodel.py b/pkgs/core/database/basemodel.py new file mode 100644 index 0000000..632bb2a --- /dev/null +++ b/pkgs/core/database/basemodel.py @@ -0,0 +1,11 @@ +from tortoise.models import Model + +class BaseModel(Model): + def to_dict(self): + unwanted_keys = ['_partial', '_saved_in_db', '_custom_generated_pk'] + print(self.__dict__) + result = self.__dict__ + for key in unwanted_keys: + result.pop(key) + + return result From 0e21af112908c65bfad9328c46fcde0a6a47049e Mon Sep 17 00:00:00 2001 From: Aayush Kurup Date: Sat, 18 Mar 2023 20:46:37 +0530 Subject: [PATCH 09/12] ADD: jobs app --- {pkgs/common/models => apps/jobs}/__init__.py | 0 apps/jobs/app.py | 5 +++ apps/jobs/container.py | 9 ++++ apps/jobs/controllers/__init__.py | 3 ++ apps/jobs/controllers/jobs.py | 43 +++++++++++++++++++ apps/jobs/services/__init__.py | 1 + apps/jobs/services/jobs.py | 28 ++++++++++++ apps/jobs/validators/__init__.py | 2 + apps/jobs/validators/create_job.py | 6 +++ apps/jobs/validators/update_job.py | 7 +++ apps/users/services/users.py | 7 ++- main.py | 6 +-- .../1679077537322008_create_jobs_table.sql | 19 ++++++++ pkgs/common/transformers/users.py | 11 +++++ pkgs/core/database/basemodel.py | 1 - pkgs/core/database/database.py | 3 +- pkgs/core/database/repository.py | 14 +++--- pkgs/jobs/__init__.py | 1 + pkgs/jobs/container.py | 11 +++++ pkgs/jobs/models/__init__.py | 1 + pkgs/jobs/models/job.py | 14 ++++++ pkgs/jobs/repositories/__init__.py | 1 + pkgs/jobs/repositories/jobs.py | 7 +++ pkgs/jobs/services/__init__.py | 1 + pkgs/jobs/services/jobs.py | 9 ++++ pkgs/users/container.py | 2 +- pkgs/users/models/__init__.py | 1 + pkgs/{common => users}/models/user.py | 0 28 files changed, 198 insertions(+), 15 deletions(-) rename {pkgs/common/models => apps/jobs}/__init__.py (100%) create mode 100644 apps/jobs/app.py create mode 100644 apps/jobs/container.py create mode 100644 apps/jobs/controllers/__init__.py create mode 100644 apps/jobs/controllers/jobs.py create mode 100644 apps/jobs/services/__init__.py create mode 100644 apps/jobs/services/jobs.py create mode 100644 apps/jobs/validators/__init__.py create mode 100644 apps/jobs/validators/create_job.py create mode 100644 apps/jobs/validators/update_job.py create mode 100644 migrations/1679077537322008_create_jobs_table.sql create mode 100644 pkgs/common/transformers/users.py create mode 100644 pkgs/jobs/__init__.py create mode 100644 pkgs/jobs/container.py create mode 100644 pkgs/jobs/models/__init__.py create mode 100644 pkgs/jobs/models/job.py create mode 100644 pkgs/jobs/repositories/__init__.py create mode 100644 pkgs/jobs/repositories/jobs.py create mode 100644 pkgs/jobs/services/__init__.py create mode 100644 pkgs/jobs/services/jobs.py create mode 100644 pkgs/users/models/__init__.py rename pkgs/{common => users}/models/user.py (100%) diff --git a/pkgs/common/models/__init__.py b/apps/jobs/__init__.py similarity index 100% rename from pkgs/common/models/__init__.py rename to apps/jobs/__init__.py diff --git a/apps/jobs/app.py b/apps/jobs/app.py new file mode 100644 index 0000000..5630667 --- /dev/null +++ b/apps/jobs/app.py @@ -0,0 +1,5 @@ +from pkgs.core import create_server +from .container import JobContainer +from .controllers import controllers + +app = create_server(JobContainer, controllers) diff --git a/apps/jobs/container.py b/apps/jobs/container.py new file mode 100644 index 0000000..b68df8c --- /dev/null +++ b/apps/jobs/container.py @@ -0,0 +1,9 @@ +from dependency_injector.containers import DeclarativeContainer +from dependency_injector.providers import Singleton +from pkgs.jobs import JobLibContainer + +from .services import JobService + +class JobContainer(DeclarativeContainer): + job_container = JobLibContainer() + job_service = Singleton(JobService, service=job_container.service) \ No newline at end of file diff --git a/apps/jobs/controllers/__init__.py b/apps/jobs/controllers/__init__.py new file mode 100644 index 0000000..240d288 --- /dev/null +++ b/apps/jobs/controllers/__init__.py @@ -0,0 +1,3 @@ +from . import jobs + +controllers = [jobs] \ No newline at end of file diff --git a/apps/jobs/controllers/jobs.py b/apps/jobs/controllers/jobs.py new file mode 100644 index 0000000..dbbdd1e --- /dev/null +++ b/apps/jobs/controllers/jobs.py @@ -0,0 +1,43 @@ +from fastapi import Depends +from fastapi import status +from dependency_injector.wiring import inject, Provide +from pkgs.core import create_router +# from pkgs.core.transformer import transform +# from pkgs.common.transformers import UserTransformer +from ..container import JobContainer +from ..services import JobService +from ..validators import CreateJobDTO, UpdateJobDTO + +router = create_router('/api/v1') + +@router.post('/jobs') +@inject +async def create_job(data: CreateJobDTO, job_service: JobService = Depends(Provide[JobContainer.job_service])): + job = await job_service.create_job(data) + return job + +@router.get('/jobs') +@inject +# @transform(UserTransformer) +async def get_jobs(job_service: JobService = Depends(Provide[JobContainer.job_service])): + jobs = await job_service.get_all_job() + return jobs + + +@router.get('/jobs/{id}') +# @transform(UserTransformer) +@inject +async def get_job(id: int, job_service: JobService = Depends(Provide[JobContainer.job_service])): + job = await job_service.get_job_by_id(id) + return job.to_dict() + + +@router.patch('/jobs/{id}', status_code=status.HTTP_204_NO_CONTENT) +@inject +async def update_job(id: int, data: UpdateJobDTO, job_service: JobService = Depends(Provide[JobContainer.job_service])): + await job_service.update_job_by_id(data, { "id": id }) + +@router.delete('/jobs/{id}', status_code=status.HTTP_204_NO_CONTENT) +@inject +async def delete_job(id: int, job_service: JobService = Depends(Provide[JobContainer.job_service])): + await job_service.delete_job(id) \ No newline at end of file diff --git a/apps/jobs/services/__init__.py b/apps/jobs/services/__init__.py new file mode 100644 index 0000000..388eb27 --- /dev/null +++ b/apps/jobs/services/__init__.py @@ -0,0 +1 @@ +from .jobs import JobService \ No newline at end of file diff --git a/apps/jobs/services/jobs.py b/apps/jobs/services/jobs.py new file mode 100644 index 0000000..4bfa8bd --- /dev/null +++ b/apps/jobs/services/jobs.py @@ -0,0 +1,28 @@ +from dependency_injector.wiring import inject + +from uuid import uuid4 +from pkgs.jobs.services import JobLibService +from ..validators import CreateJobDTO, UpdateJobDTO + +@inject +class JobService: + def __init__(self, service: JobLibService) -> None: + self.service = service + + async def create_job(self, data: CreateJobDTO): + payload = data.dict() + payload['uuid'] = uuid4() + return await self.service.repo.create(payload) + + async def get_all_job(self): + return await self.service.repo.all() + + async def get_job_by_id(self, id: int): + return await self.service.repo.first_where({ "id": id }, relations=['created_by']) + + async def update_job_by_id(self, data: UpdateJobDTO, filter): + return await self.service.repo.update_where(filter, data.dict(exclude_none=True)) + + async def delete_job_by_id(self, id: int): + return await self.service.repo.delete_where({ "id": id }) + diff --git a/apps/jobs/validators/__init__.py b/apps/jobs/validators/__init__.py new file mode 100644 index 0000000..ab9bf1e --- /dev/null +++ b/apps/jobs/validators/__init__.py @@ -0,0 +1,2 @@ +from .create_job import CreateJobDTO +from .update_job import UpdateJobDTO \ No newline at end of file diff --git a/apps/jobs/validators/create_job.py b/apps/jobs/validators/create_job.py new file mode 100644 index 0000000..771a4a8 --- /dev/null +++ b/apps/jobs/validators/create_job.py @@ -0,0 +1,6 @@ +from pydantic import BaseModel + +class CreateJobDTO(BaseModel): + title: str + description: str + created_by_id: int \ No newline at end of file diff --git a/apps/jobs/validators/update_job.py b/apps/jobs/validators/update_job.py new file mode 100644 index 0000000..a853d02 --- /dev/null +++ b/apps/jobs/validators/update_job.py @@ -0,0 +1,7 @@ +from typing import Optional +from pydantic import BaseModel + +class UpdateJobDTO(BaseModel): + title: Optional[str] + description: Optional[str] + created_by: Optional[int] \ No newline at end of file diff --git a/apps/users/services/users.py b/apps/users/services/users.py index d3cd810..5ae80be 100644 --- a/apps/users/services/users.py +++ b/apps/users/services/users.py @@ -1,5 +1,6 @@ from dependency_injector.wiring import inject +from uuid import uuid4 from pkgs.users.services import UserLibService from ..validators import CreateUserDTO, UpdateUserDTO @@ -9,13 +10,15 @@ def __init__(self, service: UserLibService) -> None: self.service = service async def create_user(self, data: CreateUserDTO): - return await self.service.repo.create(data.dict()) + payload = data.dict() + payload['uuid'] = uuid4() + return await self.service.repo.create(payload) async def get_all_users(self): return await self.service.repo.all() async def get_user_by_id(self, id: int): - return await self.service.repo.get_where({ "id": id }) + return await self.service.repo.first_where({ "id": id }) async def update_user_by_id(self, data: UpdateUserDTO, filter): return await self.service.repo.update_where(filter, data.dict(exclude_none=True)) diff --git a/main.py b/main.py index 0bd5227..d5e7775 100644 --- a/main.py +++ b/main.py @@ -1,8 +1,8 @@ +from glob import glob import uvicorn -from config import resolve from dotenv import load_dotenv, find_dotenv - load_dotenv(find_dotenv()) +from config import resolve if __name__ == '__main__': - uvicorn.run('apps.users.app:app', host='localhost', port=resolve('app.port'), reload=True) \ No newline at end of file + uvicorn.run('apps.jobs.app:app', host='localhost', port=resolve('app.port'), reload=True) \ No newline at end of file diff --git a/migrations/1679077537322008_create_jobs_table.sql b/migrations/1679077537322008_create_jobs_table.sql new file mode 100644 index 0000000..e0c5283 --- /dev/null +++ b/migrations/1679077537322008_create_jobs_table.sql @@ -0,0 +1,19 @@ +[up] +CREATE TABLE jobs ( + id SERIAL, + uuid VARCHAR(255), + title VARCHAR(255), + description TEXT, + created_by_id INTEGER, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE TRIGGER update_jobs_trigger +BEFORE UPDATE ON jobs +FOR EACH ROW +EXECUTE PROCEDURE on_update_timestamp(); + +[down] +DROP TRIGGER update_jobs_trigger on jobs; +DROP TABLE jobs; diff --git a/pkgs/common/transformers/users.py b/pkgs/common/transformers/users.py new file mode 100644 index 0000000..b73025c --- /dev/null +++ b/pkgs/common/transformers/users.py @@ -0,0 +1,11 @@ +from pkgs.core.transformer import Transformer + +class UserTransformer(Transformer): + def transform(self, obj): + return { + "id": obj.get('uuid'), + "name": obj.get('name'), + "email": obj.get('email'), + "created_at": obj.get('created_at'), + "updated_at": obj.get('updated_at') + } \ No newline at end of file diff --git a/pkgs/core/database/basemodel.py b/pkgs/core/database/basemodel.py index 632bb2a..0419c3a 100644 --- a/pkgs/core/database/basemodel.py +++ b/pkgs/core/database/basemodel.py @@ -3,7 +3,6 @@ class BaseModel(Model): def to_dict(self): unwanted_keys = ['_partial', '_saved_in_db', '_custom_generated_pk'] - print(self.__dict__) result = self.__dict__ for key in unwanted_keys: result.pop(key) diff --git a/pkgs/core/database/database.py b/pkgs/core/database/database.py index f775c30..bdb3d28 100644 --- a/pkgs/core/database/database.py +++ b/pkgs/core/database/database.py @@ -1,3 +1,4 @@ +from glob import glob from tortoise import Tortoise from config.utils import resolve @@ -6,7 +7,7 @@ async def init_db(): db_url = f"{resolve('db.driver')}://{resolve('db.user')}:{resolve('db.password')}@{resolve('db.host')}:{resolve('db.port')}/{resolve('db.name')}" await Tortoise.init( db_url=db_url, - modules={"models": ['pkgs.common.models', 'pkgs.common.models.user']}, + modules={"models": list(filter(lambda x: not x.endswith('__init__') ,map(lambda x: '.'.join(x.split('/')), [i[:-3] for i in glob('pkgs/**/models/*.py')])))} ) return Tortoise.get_connection("default") diff --git a/pkgs/core/database/repository.py b/pkgs/core/database/repository.py index b880c2e..331a4b0 100644 --- a/pkgs/core/database/repository.py +++ b/pkgs/core/database/repository.py @@ -8,8 +8,8 @@ def __init__(self, db: Tortoise, model: Model) -> None: self._model = model - async def all(self): - return await self._model.all(using_db=self._db).values() + async def all(self, relations=[]): + return await self._model.all(using_db=self._db).prefetch_related(*relations).values() async def create(self, data): @@ -21,11 +21,11 @@ async def create_or_update(self, data): using_db=self._db ) - async def get_where(self, filter): - return await self._model.filter(**filter).values() + async def get_where(self, filter, relations=[]): + return await self._model.filter(**filter).prefetch_related(*relations).values() - async def first_where(self, filter): - return await self._model.filter(**filter).first() + async def first_where(self, filter, relations=[]): + return await self._model.filter(**filter).prefetch_related(*relations).first() async def exists(self, filter): return await self._model.filter(**filter).count() @@ -40,5 +40,5 @@ async def update_where(self, filter, data, error=True): async def delete_where(self, filter, error = True): rows = await self._model.filter(**filter).delete() - if rows <= 0 and True: + if rows <= 0 and error: raise HTTPException(detail=f"{self._model._meta._model.__qualname__} not found", status_code=status.HTTP_404_NOT_FOUND) diff --git a/pkgs/jobs/__init__.py b/pkgs/jobs/__init__.py new file mode 100644 index 0000000..bb0e3e3 --- /dev/null +++ b/pkgs/jobs/__init__.py @@ -0,0 +1 @@ +from .container import JobLibContainer \ No newline at end of file diff --git a/pkgs/jobs/container.py b/pkgs/jobs/container.py new file mode 100644 index 0000000..cae2a09 --- /dev/null +++ b/pkgs/jobs/container.py @@ -0,0 +1,11 @@ +from dependency_injector.containers import DeclarativeContainer +from dependency_injector.providers import Singleton +from pkgs.core import core_container + +from .models import JobModel +from .services import JobLibService +from .repositories import JobRepository + +class JobLibContainer(DeclarativeContainer): + repo = Singleton(JobRepository, db=core_container.db, model=JobModel) + service = Singleton(JobLibService, repo=repo) \ No newline at end of file diff --git a/pkgs/jobs/models/__init__.py b/pkgs/jobs/models/__init__.py new file mode 100644 index 0000000..12ec94b --- /dev/null +++ b/pkgs/jobs/models/__init__.py @@ -0,0 +1 @@ +from .job import JobModel \ No newline at end of file diff --git a/pkgs/jobs/models/job.py b/pkgs/jobs/models/job.py new file mode 100644 index 0000000..adcc0fd --- /dev/null +++ b/pkgs/jobs/models/job.py @@ -0,0 +1,14 @@ +from tortoise.fields import IntField, CharField, DatetimeField, TextField, OneToOneField +from pkgs.core.database import BaseModel + +class JobModel(BaseModel): + id = IntField(pk=True) + uuid = CharField(max_length=255) + title = CharField(max_length=255) + description = TextField() + created_by = OneToOneField('models.UserModel') + created_at = DatetimeField(auto_now=True) + updated_at = DatetimeField(auto_now=True) + + class Meta: + table="jobs" \ No newline at end of file diff --git a/pkgs/jobs/repositories/__init__.py b/pkgs/jobs/repositories/__init__.py new file mode 100644 index 0000000..e8206c3 --- /dev/null +++ b/pkgs/jobs/repositories/__init__.py @@ -0,0 +1 @@ +from .jobs import JobRepository \ No newline at end of file diff --git a/pkgs/jobs/repositories/jobs.py b/pkgs/jobs/repositories/jobs.py new file mode 100644 index 0000000..d60c202 --- /dev/null +++ b/pkgs/jobs/repositories/jobs.py @@ -0,0 +1,7 @@ +from dependency_injector.wiring import inject + +from pkgs.core.database import DBRepository + +@inject +class JobRepository(DBRepository): + pass \ No newline at end of file diff --git a/pkgs/jobs/services/__init__.py b/pkgs/jobs/services/__init__.py new file mode 100644 index 0000000..091dff1 --- /dev/null +++ b/pkgs/jobs/services/__init__.py @@ -0,0 +1 @@ +from .jobs import JobLibService \ No newline at end of file diff --git a/pkgs/jobs/services/jobs.py b/pkgs/jobs/services/jobs.py new file mode 100644 index 0000000..a876e26 --- /dev/null +++ b/pkgs/jobs/services/jobs.py @@ -0,0 +1,9 @@ +from dependency_injector.wiring import inject + +from ..repositories import JobRepository + + +@inject +class JobLibService: + def __init__(self, repo: JobRepository) -> None: + self.repo = repo diff --git a/pkgs/users/container.py b/pkgs/users/container.py index 680411c..ee8ca2d 100644 --- a/pkgs/users/container.py +++ b/pkgs/users/container.py @@ -1,7 +1,7 @@ from dependency_injector.containers import DeclarativeContainer from dependency_injector.providers import Singleton from pkgs.core import core_container -from pkgs.common.models.user import UserModel +from .models import UserModel from .services import UserLibService from .repositories import UserRepository diff --git a/pkgs/users/models/__init__.py b/pkgs/users/models/__init__.py new file mode 100644 index 0000000..6ad224f --- /dev/null +++ b/pkgs/users/models/__init__.py @@ -0,0 +1 @@ +from .user import UserModel \ No newline at end of file diff --git a/pkgs/common/models/user.py b/pkgs/users/models/user.py similarity index 100% rename from pkgs/common/models/user.py rename to pkgs/users/models/user.py From 4b6cd2b17553014cc61fc53b53ab895724c1a3c0 Mon Sep 17 00:00:00 2001 From: Aayush Kurup Date: Sat, 18 Mar 2023 20:56:34 +0530 Subject: [PATCH 10/12] ADD: transformer class and decorator --- pkgs/core/transformer/decorator.py | 18 ++++++++++++++++++ pkgs/core/transformer/transformer.py | 19 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 pkgs/core/transformer/decorator.py create mode 100644 pkgs/core/transformer/transformer.py diff --git a/pkgs/core/transformer/decorator.py b/pkgs/core/transformer/decorator.py new file mode 100644 index 0000000..f584973 --- /dev/null +++ b/pkgs/core/transformer/decorator.py @@ -0,0 +1,18 @@ +from functools import wraps + +def transform(transformer_cls): + def decorator(func): + @wraps(func) + async def wrapper(*args, **kwargs): + result = await func(*args, **kwargs) + + transformer = transformer_cls() + + if type(result) == dict: + result = transformer.work(result) + elif type(result) == list: + result = result = transformer.collection(result) + + return result + return wrapper + return decorator \ No newline at end of file diff --git a/pkgs/core/transformer/transformer.py b/pkgs/core/transformer/transformer.py new file mode 100644 index 0000000..4d9b08b --- /dev/null +++ b/pkgs/core/transformer/transformer.py @@ -0,0 +1,19 @@ +from abc import ABC, abstractmethod +class Transformer(ABC): + + @abstractmethod + def transform(): + pass + + def collection(self, data): + result = [] + for datum in data: + result.append(self.transform(datum)) + + return result + + def work(self, data): + result = self.transform(data) + members = [attr for attr in dir(self) if not callable(getattr(self, attr)) and not attr.startswith("_")] + + return result \ No newline at end of file From c07306df249e45d7969a9ce63a43f57e1141f94d Mon Sep 17 00:00:00 2001 From: Aayush Kurup Date: Tue, 21 Mar 2023 14:04:47 +0530 Subject: [PATCH 11/12] ADD: relation mapping user and job --- README.md | 17 +++++++++++++++++ apps/jobs/controllers/jobs.py | 6 +++--- apps/users/controllers/user.py | 10 +++++++--- apps/users/services/users.py | 3 ++- pkgs/common/transformers/__init__.py | 2 ++ pkgs/common/transformers/jobs.py | 12 ++++++++++++ pkgs/core/transformer/__init__.py | 2 ++ pkgs/core/transformer/transformer.py | 3 ++- pkgs/jobs/models/job.py | 4 ++-- pkgs/users/models/user.py | 4 +++- 10 files changed, 52 insertions(+), 11 deletions(-) create mode 100644 README.md create mode 100644 pkgs/common/transformers/__init__.py create mode 100644 pkgs/common/transformers/jobs.py create mode 100644 pkgs/core/transformer/__init__.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..14c86fc --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# FastAPI Boilerplate + +## Current Status + + +![gif](https://media.tenor.com/12DuAMmK3dwAAAAC/sofakingdoge.gif) + + +## Roadmap + +- [x] HTTP package +- [x] Tortoise ORM integration using dependency injectors +- [x] Base DB repository class +- [ ] Relationship mappings +- [ ] Transformers +- [ ] Extra validators +- [ ] Docs using mkdocs \ No newline at end of file diff --git a/apps/jobs/controllers/jobs.py b/apps/jobs/controllers/jobs.py index dbbdd1e..20159f0 100644 --- a/apps/jobs/controllers/jobs.py +++ b/apps/jobs/controllers/jobs.py @@ -2,8 +2,8 @@ from fastapi import status from dependency_injector.wiring import inject, Provide from pkgs.core import create_router -# from pkgs.core.transformer import transform -# from pkgs.common.transformers import UserTransformer +from pkgs.core.transformer import transform +from pkgs.common.transformers import JobTransformer from ..container import JobContainer from ..services import JobService from ..validators import CreateJobDTO, UpdateJobDTO @@ -25,7 +25,7 @@ async def get_jobs(job_service: JobService = Depends(Provide[JobContainer.job_se @router.get('/jobs/{id}') -# @transform(UserTransformer) +@transform(JobTransformer) @inject async def get_job(id: int, job_service: JobService = Depends(Provide[JobContainer.job_service])): job = await job_service.get_job_by_id(id) diff --git a/apps/users/controllers/user.py b/apps/users/controllers/user.py index 15d2c10..96273f7 100644 --- a/apps/users/controllers/user.py +++ b/apps/users/controllers/user.py @@ -1,7 +1,9 @@ from fastapi import Depends from fastapi import status from dependency_injector.wiring import inject, Provide -from pkgs.core import create_router +from pkgs.core import create_router, Request +from pkgs.core.transformer import transform +from pkgs.common.transformers import UserTransformer from ..container import UserContainer from ..services import UserService from ..validators import CreateUserDTO, UpdateUserDTO @@ -16,16 +18,18 @@ async def get_users(data: CreateUserDTO, user_service: UserService = Depends(Pro @router.get('/users') @inject +@transform(UserTransformer) async def get_users(user_service: UserService = Depends(Provide[UserContainer.user_service])): user = await user_service.get_all_users() return user @router.get('/users/{id}') +@transform(UserTransformer) @inject -async def get_users(id: int, user_service: UserService = Depends(Provide[UserContainer.user_service])): +async def get_users(id: int, request: Request, user_service: UserService = Depends(Provide[UserContainer.user_service])): user = await user_service.get_user_by_id(id) - return user + return user.to_dict() @router.patch('/users/{id}', status_code=status.HTTP_204_NO_CONTENT) diff --git a/apps/users/services/users.py b/apps/users/services/users.py index 5ae80be..7875fb5 100644 --- a/apps/users/services/users.py +++ b/apps/users/services/users.py @@ -18,7 +18,8 @@ async def get_all_users(self): return await self.service.repo.all() async def get_user_by_id(self, id: int): - return await self.service.repo.first_where({ "id": id }) + user = await self.service.repo.first_where({ "id": id }, relations=['jobs']) + return user async def update_user_by_id(self, data: UpdateUserDTO, filter): return await self.service.repo.update_where(filter, data.dict(exclude_none=True)) diff --git a/pkgs/common/transformers/__init__.py b/pkgs/common/transformers/__init__.py new file mode 100644 index 0000000..182b103 --- /dev/null +++ b/pkgs/common/transformers/__init__.py @@ -0,0 +1,2 @@ +from .users import UserTransformer +from .jobs import JobTransformer \ No newline at end of file diff --git a/pkgs/common/transformers/jobs.py b/pkgs/common/transformers/jobs.py new file mode 100644 index 0000000..634e01c --- /dev/null +++ b/pkgs/common/transformers/jobs.py @@ -0,0 +1,12 @@ +from pkgs.core.transformer import Transformer + +class JobTransformer(Transformer): + def transform(self, obj): + return { + "id": obj.get('uuid'), + "title": obj.get('title'), + "description": obj.get('description'), + "created_by": obj.get('created_by'), + "created_at": obj.get('created_at'), + "updated_at": obj.get('updated_at') + } \ No newline at end of file diff --git a/pkgs/core/transformer/__init__.py b/pkgs/core/transformer/__init__.py new file mode 100644 index 0000000..29f1b9c --- /dev/null +++ b/pkgs/core/transformer/__init__.py @@ -0,0 +1,2 @@ +from .transformer import Transformer +from .decorator import transform \ No newline at end of file diff --git a/pkgs/core/transformer/transformer.py b/pkgs/core/transformer/transformer.py index 4d9b08b..af83dd5 100644 --- a/pkgs/core/transformer/transformer.py +++ b/pkgs/core/transformer/transformer.py @@ -13,7 +13,8 @@ def collection(self, data): return result def work(self, data): - result = self.transform(data) + result = { k: v for k, v in self.transform(data).items() if v is not None } + members = [attr for attr in dir(self) if not callable(getattr(self, attr)) and not attr.startswith("_")] return result \ No newline at end of file diff --git a/pkgs/jobs/models/job.py b/pkgs/jobs/models/job.py index adcc0fd..55a8bd1 100644 --- a/pkgs/jobs/models/job.py +++ b/pkgs/jobs/models/job.py @@ -1,4 +1,4 @@ -from tortoise.fields import IntField, CharField, DatetimeField, TextField, OneToOneField +from tortoise.fields import IntField, CharField, DatetimeField, TextField, ForeignKeyField from pkgs.core.database import BaseModel class JobModel(BaseModel): @@ -6,7 +6,7 @@ class JobModel(BaseModel): uuid = CharField(max_length=255) title = CharField(max_length=255) description = TextField() - created_by = OneToOneField('models.UserModel') + created_by = ForeignKeyField('models.UserModel', related_name="jobs") created_at = DatetimeField(auto_now=True) updated_at = DatetimeField(auto_now=True) diff --git a/pkgs/users/models/user.py b/pkgs/users/models/user.py index d01e328..338dbc7 100644 --- a/pkgs/users/models/user.py +++ b/pkgs/users/models/user.py @@ -1,5 +1,6 @@ -from tortoise.fields import IntField, CharField, DatetimeField +from tortoise.fields import IntField, CharField, DatetimeField, ReverseRelation from pkgs.core.database import BaseModel +from pkgs.jobs.models import JobModel class UserModel(BaseModel): id = IntField(pk=True) @@ -7,6 +8,7 @@ class UserModel(BaseModel): name = CharField(max_length=255) email = CharField(max_length=255) password = CharField(max_length=255) + jobs = ReverseRelation["JobModel"] created_at = DatetimeField(auto_now=True) updated_at = DatetimeField(auto_now=True) From 74fa6eeb8fc65293f482ac75d10d6647774b58f3 Mon Sep 17 00:00:00 2001 From: Aayush Kurup Date: Sat, 25 Mar 2023 22:42:21 +0530 Subject: [PATCH 12/12] ADD: work, item collection and pareser in transformers --- apps/jobs/controllers/jobs.py | 9 +++--- apps/jobs/services/jobs.py | 2 +- pkgs/common/transformers/jobs.py | 8 +++-- pkgs/core/database/basemodel.py | 14 +++++++-- pkgs/core/database/repository.py | 4 +-- pkgs/core/http/exception.py | 2 ++ pkgs/core/http/request.py | 16 ++++++++++ pkgs/core/transformer/decorator.py | 12 ++++++-- pkgs/core/transformer/parser.py | 45 ++++++++++++++++++++++++++++ pkgs/core/transformer/transformer.py | 24 +++++++++++++-- 10 files changed, 119 insertions(+), 17 deletions(-) create mode 100644 pkgs/core/http/request.py create mode 100644 pkgs/core/transformer/parser.py diff --git a/apps/jobs/controllers/jobs.py b/apps/jobs/controllers/jobs.py index 20159f0..3d2e3bb 100644 --- a/apps/jobs/controllers/jobs.py +++ b/apps/jobs/controllers/jobs.py @@ -4,6 +4,7 @@ from pkgs.core import create_router from pkgs.core.transformer import transform from pkgs.common.transformers import JobTransformer +from pkgs.core.http import Request from ..container import JobContainer from ..services import JobService from ..validators import CreateJobDTO, UpdateJobDTO @@ -18,16 +19,16 @@ async def create_job(data: CreateJobDTO, job_service: JobService = Depends(Provi @router.get('/jobs') @inject -# @transform(UserTransformer) -async def get_jobs(job_service: JobService = Depends(Provide[JobContainer.job_service])): +@transform(JobTransformer) +async def get_jobs(request: Request, job_service: JobService = Depends(Provide[JobContainer.job_service])): jobs = await job_service.get_all_job() - return jobs + return list(map(lambda x: x.to_dict(), jobs)) @router.get('/jobs/{id}') @transform(JobTransformer) @inject -async def get_job(id: int, job_service: JobService = Depends(Provide[JobContainer.job_service])): +async def get_job(request: Request, id: int, job_service: JobService = Depends(Provide[JobContainer.job_service])): job = await job_service.get_job_by_id(id) return job.to_dict() diff --git a/apps/jobs/services/jobs.py b/apps/jobs/services/jobs.py index 4bfa8bd..a78ea98 100644 --- a/apps/jobs/services/jobs.py +++ b/apps/jobs/services/jobs.py @@ -15,7 +15,7 @@ async def create_job(self, data: CreateJobDTO): return await self.service.repo.create(payload) async def get_all_job(self): - return await self.service.repo.all() + return await self.service.repo.all(relations=['created_by']) async def get_job_by_id(self, id: int): return await self.service.repo.first_where({ "id": id }, relations=['created_by']) diff --git a/pkgs/common/transformers/jobs.py b/pkgs/common/transformers/jobs.py index 634e01c..4bbd84a 100644 --- a/pkgs/common/transformers/jobs.py +++ b/pkgs/common/transformers/jobs.py @@ -1,4 +1,5 @@ from pkgs.core.transformer import Transformer +from .users import UserTransformer class JobTransformer(Transformer): def transform(self, obj): @@ -6,7 +7,10 @@ def transform(self, obj): "id": obj.get('uuid'), "title": obj.get('title'), "description": obj.get('description'), - "created_by": obj.get('created_by'), + "created_by": obj.get('_created_by'), "created_at": obj.get('created_at'), "updated_at": obj.get('updated_at') - } \ No newline at end of file + } + + def include_created_by(self, data, include_options = {}): + return self.item(data.get('created_by'), UserTransformer(), include_options) \ No newline at end of file diff --git a/pkgs/core/database/basemodel.py b/pkgs/core/database/basemodel.py index 0419c3a..f355e22 100644 --- a/pkgs/core/database/basemodel.py +++ b/pkgs/core/database/basemodel.py @@ -3,8 +3,16 @@ class BaseModel(Model): def to_dict(self): unwanted_keys = ['_partial', '_saved_in_db', '_custom_generated_pk'] - result = self.__dict__ - for key in unwanted_keys: - result.pop(key) + base_dict = self.__dict__ + result = {} + + for k, v in base_dict.items(): + if k in unwanted_keys: + continue + elif isinstance(v, BaseModel): + result[k[1:]] = v.to_dict() + else: + result[k] = v + return result diff --git a/pkgs/core/database/repository.py b/pkgs/core/database/repository.py index 331a4b0..64abe51 100644 --- a/pkgs/core/database/repository.py +++ b/pkgs/core/database/repository.py @@ -9,8 +9,8 @@ def __init__(self, db: Tortoise, model: Model) -> None: async def all(self, relations=[]): - return await self._model.all(using_db=self._db).prefetch_related(*relations).values() - + query = await self._model.all(using_db=self._db).prefetch_related(*relations) + return query async def create(self, data): return await self._model.create(using_db=self._db, **data) diff --git a/pkgs/core/http/exception.py b/pkgs/core/http/exception.py index bebac32..a673a94 100644 --- a/pkgs/core/http/exception.py +++ b/pkgs/core/http/exception.py @@ -2,6 +2,7 @@ from fastapi import Request, Response from fastapi.responses import ORJSONResponse from fastapi.routing import APIRoute +from .request import Request from fastapi.exceptions import HTTPException, RequestValidationError class ExceptionHandler(APIRoute): @@ -10,6 +11,7 @@ def get_route_handler(self) -> Callable: async def custom_route_handler(request: Request) -> Request: try: + request = Request(request.scope, request.receive) response: Response = await route_handler(request) return response except HTTPException as httpe: diff --git a/pkgs/core/http/request.py b/pkgs/core/http/request.py new file mode 100644 index 0000000..b3155d4 --- /dev/null +++ b/pkgs/core/http/request.py @@ -0,0 +1,16 @@ +from json import loads +from fastapi import Request as BaseRequest + +class Request(BaseRequest): + async def all(self): + body = await self.body() + if body != b'': + body = loads(body) + else: + body = {} + return { + **self.query_params._dict, + **self.path_params, + **body + } + \ No newline at end of file diff --git a/pkgs/core/transformer/decorator.py b/pkgs/core/transformer/decorator.py index f584973..7f9660a 100644 --- a/pkgs/core/transformer/decorator.py +++ b/pkgs/core/transformer/decorator.py @@ -1,17 +1,25 @@ from functools import wraps +from .parser import parse_includes +from .transformer import Transformer -def transform(transformer_cls): +def transform(transformer_cls: Transformer): def decorator(func): @wraps(func) async def wrapper(*args, **kwargs): + request = kwargs.get('request') + all = await request.all() result = await func(*args, **kwargs) transformer = transformer_cls() + transformer.set_includes(parse_includes(all.get('include'))) if type(result) == dict: result = transformer.work(result) elif type(result) == list: - result = result = transformer.collection(result) + output = [] + for datum in result: + output.append(transformer.work(datum)) + result = output return result return wrapper diff --git a/pkgs/core/transformer/parser.py b/pkgs/core/transformer/parser.py new file mode 100644 index 0000000..b4d2fd2 --- /dev/null +++ b/pkgs/core/transformer/parser.py @@ -0,0 +1,45 @@ +def parse_includes(exp: str): + o = {} + p = [] + inArray = False + parsed_obj = [] + + length = len(exp) + i = 0 + while (i < length): + last_char = None if i < 0 else ord(exp[i - 1]) + ch = ord(exp[i]) + if (ch == 91): + o['name'] = ''.join(p) + o['args'] = [] + p = [] + inArray = True + elif (inArray and ch == 44): + o['args'].append(''.join(p)) + p = []; + elif (ch == 93): + o['args'].append(''.join(p)) + parsed_obj.append(o) + o = {} + p = [] + inArray = False + elif (last_char != 93 and ch == 44): + o['name'] = ''.join(p) + parsed_obj.append(o) + o = {} + p = [] + elif (ch != 93 and length - i == 1): + p.append(chr(ch)) + o['name'] = ''.join(p) + parsed_obj.append(o) + elif (ch != 44): + p.append(chr(ch)) + + i+=1 + + final = {} + + for p in parsed_obj: + final[p['name']] = p.get('args') + + return final \ No newline at end of file diff --git a/pkgs/core/transformer/transformer.py b/pkgs/core/transformer/transformer.py index af83dd5..f1b85ed 100644 --- a/pkgs/core/transformer/transformer.py +++ b/pkgs/core/transformer/transformer.py @@ -1,20 +1,38 @@ from abc import ABC, abstractmethod + class Transformer(ABC): + def __init__(self) -> None: + super().__init__() + self._includes = {} @abstractmethod def transform(): pass - def collection(self, data): + def set_includes(self, includes): + self._includes = includes + + def item(self, data, transformer, includes = {}): + transformer.set_includes(includes) + return transformer.work(data) + + def collection(self, data, transformer, includes = {}): + transformer.set_includes(includes) result = [] for datum in data: - result.append(self.transform(datum)) + result.append(transformer.work(datum)) return result def work(self, data): result = { k: v for k, v in self.transform(data).items() if v is not None } - members = [attr for attr in dir(self) if not callable(getattr(self, attr)) and not attr.startswith("_")] + + for include in self._includes: + include_method = f'include_{include}' + nested_includes = self._includes[include] + if(getattr(self, include_method, None)): + result[include] = getattr(self, include_method)(data) + return result \ No newline at end of file