From 9a8f4cf33961abeb37fcc4eca45c801ee8e7b461 Mon Sep 17 00:00:00 2001 From: Joshua Hegedus Date: Sat, 23 Mar 2024 15:56:31 +0100 Subject: [PATCH 1/7] added base model --- src/fastapi_crud/lib.py | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 src/fastapi_crud/lib.py diff --git a/src/fastapi_crud/lib.py b/src/fastapi_crud/lib.py new file mode 100644 index 0000000..d05b4ac --- /dev/null +++ b/src/fastapi_crud/lib.py @@ -0,0 +1,6 @@ +from pydantic import BaseModel +from typing import Optional +from pyrepositories import IdTypes + +class Model(BaseModel): + pass From f2ff42f4a6fcf42ecb0ee9e4fb3214e659eca7a1 Mon Sep 17 00:00:00 2001 From: Joshua Hegedus Date: Sat, 23 Mar 2024 15:56:40 +0100 Subject: [PATCH 2/7] added entity factory --- src/fastapi_crud/entities.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/fastapi_crud/entities.py diff --git a/src/fastapi_crud/entities.py b/src/fastapi_crud/entities.py new file mode 100644 index 0000000..79e7a69 --- /dev/null +++ b/src/fastapi_crud/entities.py @@ -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 From 52a33bace5284548ffb4a5d3fc0b312ab0f9ed07 Mon Sep 17 00:00:00 2001 From: Joshua Hegedus Date: Sat, 23 Mar 2024 15:56:46 +0100 Subject: [PATCH 3/7] working app --- src/fastapi_crud/__init__.py | 7 +++-- src/fastapi_crud/app.py | 57 +++++++++++++++++++++--------------- 2 files changed, 37 insertions(+), 27 deletions(-) diff --git a/src/fastapi_crud/__init__.py b/src/fastapi_crud/__init__.py index 699ffbe..359f9f4 100644 --- a/src/fastapi_crud/__init__.py +++ b/src/fastapi_crud/__init__.py @@ -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'] diff --git a/src/fastapi_crud/app.py b/src/fastapi_crud/app.py index 0e14112..f53ae89 100644 --- a/src/fastapi_crud/app.py +++ b/src/fastapi_crud/app.py @@ -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: @@ -22,8 +23,12 @@ 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 @@ -31,42 +36,46 @@ def __init__(self, datasource: DataSource, name: str, use_prefix: bool = True, u 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 @@ -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()) From fc5b83eb001761b16aa72f668694a9064b1bcb59 Mon Sep 17 00:00:00 2001 From: Joshua Hegedus Date: Sat, 23 Mar 2024 15:57:48 +0100 Subject: [PATCH 4/7] added publish ci --- .github/workflows/publish.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..0434acc --- /dev/null +++ b/.github/workflows/publish.yml @@ -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 }} From 571c73f1e5df26058322fb6e6854b7b340734972 Mon Sep 17 00:00:00 2001 From: Joshua Hegedus Date: Sat, 23 Mar 2024 15:58:08 +0100 Subject: [PATCH 5/7] moved tests to script --- scripts/lighthouse.py | 84 +++++++++++++++++++++++++++++++++++++++++++ tests/lighthouse.py | 35 ------------------ 2 files changed, 84 insertions(+), 35 deletions(-) create mode 100644 scripts/lighthouse.py delete mode 100644 tests/lighthouse.py diff --git a/scripts/lighthouse.py b/scripts/lighthouse.py new file mode 100644 index 0000000..20634fb --- /dev/null +++ b/scripts/lighthouse.py @@ -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) + + diff --git a/tests/lighthouse.py b/tests/lighthouse.py deleted file mode 100644 index 50cf8e9..0000000 --- a/tests/lighthouse.py +++ /dev/null @@ -1,35 +0,0 @@ -import os -import sys -from pathlib import Path -from fastapi import FastAPI, Query -import uvicorn -from typing import Annotated - - - -path_root = Path(__file__).parents[1] -sys.path.append(os.path.join(path_root, 'src')) - -from fastapi_bootstrap import CRUDApi, DataSource, DataTable, FilterField - -app = FastAPI() - -t = DataTable("event") -t.set_filter_fields({ - "name": (str, "My Name"), - "date": (str,), - "location": (str,), - "description": (str,) -}) -t.add_filter_field(FilterField("ids", Annotated[list[str], Query()])) -ds = DataSource([t]) - -api = CRUDApi(ds, app) - -api.add_router("event") - - -if __name__ == "__main__": - uvicorn.run(app, host="0.0.0.0", port=1111) - - From a0d39ef03aa7b40a33aa6ee3f23ce2d1ee02a215 Mon Sep 17 00:00:00 2001 From: Joshua Hegedus Date: Sat, 23 Mar 2024 15:58:14 +0100 Subject: [PATCH 6/7] excluded build trash --- .gitignore | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.gitignore b/.gitignore index 2c254cb..e0abf9a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,12 @@ venv/ __pycache__/ .idea/ + +data/ + +dist/ +build/ +*.egg-info/ +*.egg/ +*.log +*.pyc From 48b293370b8a88b52433379abefe26452318d155 Mon Sep 17 00:00:00 2001 From: Joshua Hegedus Date: Sat, 23 Mar 2024 16:03:40 +0100 Subject: [PATCH 7/7] version fix and added dependencies --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 701e166..fe61a94 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "fastapi-crud" -version = "3.1.0" +version = "1.0.0" authors = [ { name="kougen", email="info@kou-gen.net" }, ] @@ -21,6 +21,7 @@ classifiers = [ dependencies = [ 'fastapi', 'uvicorn', + 'pyrepositories', ] [project.urls]