Skip to content

Commit

Permalink
Merge pull request #4 from kougen/feature/add-dynamic-models
Browse files Browse the repository at this point in the history
Feature/add dynamic models
  • Loading branch information
joshika39 committed Mar 23, 2024
2 parents 026197e + 48b2933 commit 66660a0
Show file tree
Hide file tree
Showing 9 changed files with 184 additions and 63 deletions.
29 changes: 29 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Upload Python Package

on:
release:
types: [published]

permissions:
contents: read

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install build
- name: Build package
run: python -m build
- name: Publish package
uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
with:
user: __token__
password: ${{ secrets.PYPI_TOKEN }}
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
venv/
__pycache__/
.idea/

data/

dist/
build/
*.egg-info/
*.egg/
*.log
*.pyc
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "fastapi-crud"
version = "3.1.0"
version = "1.0.0"
authors = [
{ name="kougen", email="[email protected]" },
]
Expand All @@ -21,6 +21,7 @@ classifiers = [
dependencies = [
'fastapi',
'uvicorn',
'pyrepositories',
]

[project.urls]
Expand Down
84 changes: 84 additions & 0 deletions scripts/lighthouse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import os
import sys
from pathlib import Path
from fastapi import FastAPI, Query
from pydantic import BaseModel
import uvicorn
from typing import List, Optional
from pyrepositories import JsonTable, DataSource, Entity, IdTypes


path_root = Path(__file__).parents[1]
sys.path.append(os.path.join(path_root, 'src'))

from fastapi_crud import CRUDApi, Model, EntityFactory


class Organizer(Model):
email: str


class Joiner(Model):
name: str
company: str


class Event(Model):
date: str
organizer: Organizer
status: str
max_attendees: int
joiners: Optional[List[Joiner]] = None


class EventEntity(Entity):
@property
def date(self):
return self.fields.get("date")
@date.setter
def date(self, value):
self.fields["date"] = value
@property
def organizer(self):
return self.fields.get("organizer")
@organizer.setter
def organizer(self, value):
self.fields["organizer"] = value
@property
def status(self):
return self.fields.get("status")
@status.setter
def status(self, value):
self.fields["status"] = value
@property
def max_attendees(self):
return self.fields.get("max_attendees")
@max_attendees.setter
def max_attendees(self, value):
self.fields["max_attendees"] = value
@property
def joiners(self):
return self.fields.get("joiners") or []
@joiners.setter
def joiners(self, value):
self.fields["joiners"] = value



app = FastAPI()

ds = DataSource(auto_increment=True, id_type=IdTypes.UUID)
t = JsonTable("event", os.path.join(path_root, "data"))
filters = { "date": (str, ""), "organizer": (str, ""), "status": (str, ""), "event_type": (str, ""), }
t.set_filter_fields(filters)

ds.add_table(t)
api = CRUDApi(ds, app)

api.add_router("event" , Event)


if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=1112)


7 changes: 4 additions & 3 deletions src/fastapi_crud/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .app import CRUDApi
from .datasource import DataSource, DataTable, FilterField
from .app import CRUDApi, CRUDApiRouter
from .lib import Model
from .entities import EntityFactory

__all__ = ['CRUDApi', 'DataSource', 'DataTable']
__all__ = ['CRUDApi', 'CRUDApiRouter', 'Model', 'EntityFactory']
57 changes: 33 additions & 24 deletions src/fastapi_crud/app.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
from typing import List
from enum import Enum
from fastapi import Depends, FastAPI
from pydantic import create_model, BaseModel
from pyrepositories import DataSource, Entity
from pydantic import create_model
from fastapi.routing import APIRouter
from .datasource import DataSource
from .entities import EntityFactory

id_path = '/{id}'
id_path = '/single/{id}'

def construct_path(base_path: str, path: str, is_plural: bool, use_prefix: bool) -> str:
plural = 's' if is_plural else ''
if not use_prefix:
return f'{base_path}{plural}{path}'
else:
return f'{plural}{path}'
return f'{path}'


def get_tags(name: str, use_name_as_tag: bool) -> List[str | Enum] | None:
Expand All @@ -22,51 +23,59 @@ def get_tags(name: str, use_name_as_tag: bool) -> List[str | Enum] | None:
def get_prefix(name: str, use_prefix: bool) -> str:
return f'/{name}' if use_prefix else ''


def format_entities(entities: List[Entity]) -> List[dict]:
return [entity.serialize() for entity in entities]

class CRUDApiRouter:
def __init__(self, datasource: DataSource, name: str, use_prefix: bool = True, use_name_as_tag: bool = True):
def __init__(self, datasource: DataSource, name: str, model_type: type, factory: EntityFactory, use_prefix: bool = True, use_name_as_tag: bool = True):
self.datasource = datasource
self.name = name
self.use_prefix = use_prefix
self.use_name_as_tag = use_name_as_tag
datatype = name.lower()
tags = get_tags(name, use_name_as_tag)
table = self.datasource.get_table(datatype)
base_path = f'/{datatype}'
if not table:
raise ValueError(f'Table {datatype} not found in datasource')

filters = table.get_filter_fields()
base_path = f'/{datatype}'

self.router = APIRouter(
prefix=get_prefix(datatype, use_prefix)
)

if not table:
return

@self.router.get(construct_path(f'{base_path}', '', True, use_prefix), tags=tags)
async def read_items():
return self.datasource.get_all(datatype)
return format_entities(self.datasource.get_all(datatype) or [])

@self.router.get(construct_path(f'{base_path}', '/filter', True, use_prefix), tags=tags)
async def filter_items(params: create_model("Query", **filters) = Depends()):
fields = params.dict()
return format_entities(self.datasource.get_by_filter(datatype, fields) or [])

@self.router.get(construct_path(base_path, id_path, False, use_prefix), tags=tags)
async def read_item(id: int):
async def read_item(id: int | str):
return self.datasource.get_by_id(datatype, id)
filters = table.get_filter_fields()

@self.router.get(construct_path(f'{base_path}', '/filter', True, use_prefix), tags=tags)
async def read_filtered_items(params: create_model("Query", **filters) = Depends()):
params_as_dict = params.dict()
return self.datasource.get_by_filter(datatype, params_as_dict)

@self.router.post(construct_path(base_path, '', False, use_prefix), tags=tags)
async def create_item(item: dict):
return self.datasource.insert(datatype, item)
async def create_item(item: model_type):
return self.datasource.insert(datatype, factory.create_entity(item.model_dump()))

@self.router.put(construct_path(base_path, id_path, False, use_prefix), tags=tags)
async def update_item(id: int, item: dict):
return self.datasource.update(datatype, id, item)
async def update_item(id: int | str, item: model_type):
entity = factory.create_entity(item.model_dump())
return self.datasource.update(datatype, id, entity)

@self.router.delete(construct_path(base_path, id_path, False, use_prefix), tags=tags)
async def delete_item(id: int):
async def delete_item(id: int | str):
return self.datasource.delete(datatype, id)

@self.router.delete(construct_path(base_path, '', True, use_prefix), tags=tags)
async def delete_all_items():
return self.datasource.clear(datatype)

def get_router(self):
return self.router

Expand All @@ -78,8 +87,8 @@ def __init__(self, datasource: DataSource, app: FastAPI):
self.routers = [] # List[CRUDApiRouter]


def add_router(self, datatype: str, use_prefix: bool = True):
router = CRUDApiRouter(self.datasource, datatype, use_prefix)
def add_router(self, datatype: str, model_type: type, factory: EntityFactory = EntityFactory(), use_prefix: bool = True):
router = CRUDApiRouter(self.datasource, datatype, model_type, factory, use_prefix)
self.routers.append(router)
self.app.include_router(router.get_router())

Expand Down
17 changes: 17 additions & 0 deletions src/fastapi_crud/entities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from pydantic import BaseModel
from pyrepositories import Entity


class EntityFactory:
def convert_model(self, model: BaseModel) -> Entity:
fields = model.model_dump()
if 'id' in fields:
entity = Entity(id=fields['id'])
entity.fields = fields
print("Override this method to create an entity")
return Entity()

def create_entity(self, fields: dict) -> Entity:
entity = Entity(fields.get('id'))
entity.fields = fields
return entity
6 changes: 6 additions & 0 deletions src/fastapi_crud/lib.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from pydantic import BaseModel
from typing import Optional
from pyrepositories import IdTypes

class Model(BaseModel):
pass
35 changes: 0 additions & 35 deletions tests/lighthouse.py

This file was deleted.

0 comments on commit 66660a0

Please sign in to comment.