Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/transformers #4

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
9 changes: 8 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,9 @@
APP_NAME=
APP_PORT=
APP_PORT=

DB_DRIVER=
DB_USER=
DB_PASSWORD=
DB_HOST=
DB_PORT=
DB_NAME=
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
File renamed without changes.
5 changes: 5 additions & 0 deletions apps/jobs/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from pkgs.core import create_server
from .container import JobContainer
from .controllers import controllers

app = create_server(JobContainer, controllers)
9 changes: 9 additions & 0 deletions apps/jobs/container.py
Original file line number Diff line number Diff line change
@@ -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)
3 changes: 3 additions & 0 deletions apps/jobs/controllers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import jobs

controllers = [jobs]
44 changes: 44 additions & 0 deletions apps/jobs/controllers/jobs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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 JobTransformer
from pkgs.core.http import Request
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(JobTransformer)
async def get_jobs(request: Request, job_service: JobService = Depends(Provide[JobContainer.job_service])):
jobs = await job_service.get_all_job()
return list(map(lambda x: x.to_dict(), jobs))


@router.get('/jobs/{id}')
@transform(JobTransformer)
@inject
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()


@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)
1 change: 1 addition & 0 deletions apps/jobs/services/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .jobs import JobService
28 changes: 28 additions & 0 deletions apps/jobs/services/jobs.py
Original file line number Diff line number Diff line change
@@ -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(relations=['created_by'])

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 })

2 changes: 2 additions & 0 deletions apps/jobs/validators/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .create_job import CreateJobDTO
from .update_job import UpdateJobDTO
6 changes: 6 additions & 0 deletions apps/jobs/validators/create_job.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from pydantic import BaseModel

class CreateJobDTO(BaseModel):
title: str
description: str
created_by_id: int
7 changes: 7 additions & 0 deletions apps/jobs/validators/update_job.py
Original file line number Diff line number Diff line change
@@ -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]
5 changes: 4 additions & 1 deletion apps/users/container.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from dependency_injector.containers import DeclarativeContainer
from dependency_injector.providers import Singleton
from pkgs.users import UserLibContainer

from .services import UserService

class UserContainer(DeclarativeContainer):
user_service = Singleton(UserService)
user_container = UserLibContainer()
user_service = Singleton(UserService, service=user_container.service)
42 changes: 36 additions & 6 deletions apps/users/controllers/user.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,43 @@
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 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.user import UserModel
from ..validators import CreateUserDTO, UpdateUserDTO

router = create_router('/api/v1')

@router.post('/users')
@inject
def get_users(user: UserModel, user_service: UserService = Depends(Provide[UserContainer.user_service])) -> Dict:
return user_service.greet()
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
@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, request: Request, user_service: UserService = Depends(Provide[UserContainer.user_service])):
user = await user_service.get_user_by_id(id)
return user.to_dict()


@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)
30 changes: 27 additions & 3 deletions apps/users/services/users.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,29 @@
from typing import Dict
from dependency_injector.wiring import inject

from uuid import uuid4
from pkgs.users.services import UserLibService
from ..validators import CreateUserDTO, UpdateUserDTO

@inject
class UserService:
def greet(self) -> Dict:
return {'message': "Hello there!"}
def __init__(self, service: UserLibService) -> None:
self.service = service

async def create_user(self, data: CreateUserDTO):
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):
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))

async def delete_user_by_id(self, id: int):
return await self.service.repo.delete_where({ "id": id })

2 changes: 2 additions & 0 deletions apps/users/validators/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .create_user import CreateUserDTO
from .update_user import UpdateUserDTO
6 changes: 6 additions & 0 deletions apps/users/validators/create_user.py
Original file line number Diff line number Diff line change
@@ -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)
7 changes: 7 additions & 0 deletions apps/users/validators/update_user.py
Original file line number Diff line number Diff line change
@@ -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)]
6 changes: 0 additions & 6 deletions apps/users/validators/user.py

This file was deleted.

4 changes: 3 additions & 1 deletion config/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from .app import app
from .database import db

config = {
"app": app
"app": app,
"db": db
}

from .utils.resolve import resolve
2 changes: 1 addition & 1 deletion config/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
10 changes: 10 additions & 0 deletions config/database.py
Original file line number Diff line number Diff line change
@@ -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'),
}
1 change: 1 addition & 0 deletions config/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .resolve import resolve
2 changes: 1 addition & 1 deletion config/utils/resolve.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
8 changes: 4 additions & 4 deletions main.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from glob import glob
import uvicorn
from dotenv import load_dotenv, find_dotenv
load_dotenv(find_dotenv())
from config import resolve
from dotenv import load_dotenv

load_dotenv()

if __name__ == '__main__':
uvicorn.run('apps.users.app:app', host='localhost', port=resolve('app.port'), reload=True)
uvicorn.run('apps.jobs.app:app', host='localhost', port=resolve('app.port'), reload=True)
19 changes: 19 additions & 0 deletions migrations/1676731094599205_create_users_table.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[up]
CREATE TABLE users (
id SERIAL,
uuid VARCHAR(255),
name VARCHAR(255),
email VARCHAR(255),
password VARCHAR(255),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TRIGGER update_users_trigger
BEFORE UPDATE ON users
FOR EACH ROW
EXECUTE PROCEDURE on_update_timestamp();

[down]
DROP TRIGGER update_users_trigger on users;
DROP TABLE users;
19 changes: 19 additions & 0 deletions migrations/1679077537322008_create_jobs_table.sql
Original file line number Diff line number Diff line change
@@ -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;
Empty file added pkgs/common/__init__.py
Empty file.
2 changes: 2 additions & 0 deletions pkgs/common/transformers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .users import UserTransformer
from .jobs import JobTransformer
16 changes: 16 additions & 0 deletions pkgs/common/transformers/jobs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from pkgs.core.transformer import Transformer
from .users import UserTransformer

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')
}

def include_created_by(self, data, include_options = {}):
return self.item(data.get('created_by'), UserTransformer(), include_options)
Loading