diff --git a/.gitignore b/.gitignore index 3d9a83e..0bfa500 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ venv pizzatool.sqlite +__pycache__ *.swp diff --git a/pizzatool/api.py b/pizzatool/api.py index c40a1ca..740bf3d 100644 --- a/pizzatool/api.py +++ b/pizzatool/api.py @@ -1,129 +1,9 @@ -from fastapi import Depends, FastAPI, HTTPException, Response, status -from logging import getLogger -from ormantic.exceptions import NoMatch +from fastapi import FastAPI +from pizzatool.api_helpers import CRUDMapper, RelationshipMapper from pizzatool.model import Ingredient, Pizza, PizzaIngredient -logger = getLogger(__name__) app = FastAPI() - - -@app.get('/ingredients') -async def read_ingredients(): - return await Ingredient.objects.all() - - -@app.post('/ingredients', status_code=status.HTTP_201_CREATED) -async def create_ingredient(name: str, response: Response): - ingredient = await Ingredient.objects.create(name=name) - response.headers['Location'] = 'ingredients/%d' % ingredient.id - return ingredient - - -async def get_ingredient(ingredient_id: int): - try: - return await Ingredient.objects.get(id=ingredient_id) - except NoMatch: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) - - -@app.get('/ingredients/{ingredient_id}') -async def read_ingredient(ingredient: Ingredient = Depends(get_ingredient)): - return ingredient - - -@app.put('/ingredients/{ingredient_id}', - status_code=status.HTTP_204_NO_CONTENT) -async def update_ingredient( - name: str, - ingredient: Ingredient = Depends(get_ingredient)): - await ingredient.update(name=name) - - -@app.delete('/ingredients/{ingredient_id}', - status_code=status.HTTP_204_NO_CONTENT) -async def delete_ingredient(ingredient: Ingredient = Depends(get_ingredient)): - await ingredient.delete() - - -@app.get('/pizzas') -async def read_pizzas(): - return await Pizza.objects.all() - - -@app.post('/pizzas', status_code=status.HTTP_201_CREATED) -async def create_pizza(name: str, response: Response): - pizza = await Pizza.objects.create(name=name) - response.headers['Location'] = 'pizzas/%d' % pizza.id - return pizza - - -async def get_pizza(pizza_id: int): - try: - return await Pizza.objects.get(id=pizza_id) - except NoMatch: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) - - -@app.get('/pizzas/{pizza_id}') -async def read_pizza(pizza: Pizza = Depends(get_pizza)): - return pizza - - -@app.put('/pizzas/{pizza_id}', status_code=status.HTTP_204_NO_CONTENT) -async def update_pizza(name: str, pizza: Pizza = Depends(get_pizza)): - await pizza.update(name=name) - - -@app.delete('/pizzas/{pizza_id}', status_code=status.HTTP_204_NO_CONTENT) -async def delete_pizza(pizza: Pizza = Depends(get_pizza)): - await pizza.delete() - - -@app.get('/pizzas/{pizza_id}/ingredients') -async def read_pizza_ingredients(pizza: Pizza = Depends(get_pizza)): - return await PizzaIngredient.objects.select_related('ingredient') \ - .filter(pizza=pizza).all() - - -@app.post('/pizza/{pizza_id}/ingredients', status_code=status.HTTP_201_CREATED) -async def create_pizza_ingredient( - response: Response, - pizza: Pizza = Depends(get_pizza), - ingredient: Ingredient = Depends(get_ingredient)): - pizza_ingredient = await PizzaIngredient.objects.create( - pizza=pizza, - ingredient=ingredient) - response.headers['Location'] = 'ingredients/%d' % pizza_ingredient.id - return pizza_ingredient - - -async def get_pizza_ingredient( - pizza_ingredient_id: int, - pizza: Pizza = Depends(get_pizza)): - try: - return await PizzaIngredient.objects.select_related('ingredient') \ - .get(id=pizza_ingredient_id, pizza=pizza) - except NoMatch: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) - - -@app.get('/pizzas/{pizza_id}/ingredients/{pizza_ingredient_id}') -async def read_pizza_ingredient( - pizza_ingredient: PizzaIngredient = Depends(get_pizza_ingredient)): - return pizza_ingredient - - -@app.put('/pizzas/{pizza_id}/ingredients/{pizza_ingredient_id}', - status_code=status.HTTP_204_NO_CONTENT) -async def update_pizza_ingredient( - pizza_ingredient: PizzaIngredient = Depends(get_pizza_ingredient), - ingredient: Ingredient = Depends(get_ingredient)): - await pizza_ingredient.update(ingredient=ingredient.id) - - -@app.delete('/pizzas/{pizza_id}/ingredients/{pizza_ingredient_id}', - status_code=status.HTTP_204_NO_CONTENT) -async def delete_pizza_ingredient( - pizza_ingredient: PizzaIngredient = Depends(get_pizza_ingredient)): - await pizza_ingredient.delete() +CRUDMapper(app, Ingredient) +CRUDMapper(app, Pizza) +RelationshipMapper(app, Pizza, Ingredient, PizzaIngredient) diff --git a/pizzatool/api_helpers.py b/pizzatool/api_helpers.py new file mode 100644 index 0000000..267b6da --- /dev/null +++ b/pizzatool/api_helpers.py @@ -0,0 +1,103 @@ +from fastapi import Depends, HTTPException, Response, status +from inspect import Parameter, Signature +from ormantic.exceptions import NoMatch +from sqlite3 import IntegrityError + + +def CRUDMapper(app, ORMClass): + name = ORMClass.Mapping.table_name + collection_path = '/%ss' % name + item_path = '/%ss/{id}' % name + + @app.get(collection_path) + async def read_all(): + return await ORMClass.objects.all() + + @app.post(collection_path, status_code=status.HTTP_201_CREATED) + async def create(values: ORMClass, response: Response): + try: + item = await ORMClass.objects.create(**values.__dict__) + response.headers['Location'] = '%ss/%d' % (name, item.id) + return item + except IntegrityError: + raise HTTPException(status_code=status.HTTP_409_CONFLICT) + + async def get_item(id: int): + try: + return await ORMClass.objects.get(id=id) + except NoMatch: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + + @app.get(item_path) + async def read(item=Depends(get_item)): + return item + + @app.put(item_path, status_code=status.HTTP_204_NO_CONTENT) + async def update(values: ORMClass, item=Depends(get_item)): + await item.update(**values.__dict__) + + @app.delete(item_path, status_code=status.HTTP_204_NO_CONTENT) + async def delete(item=Depends(get_item)): + await item.delete() + + +def RelationshipMapper(app, ORMClass1, ORMClass2, ORMRelationship): + scope_name = ORMClass1.Mapping.table_name + item_name = ORMClass2.Mapping.table_name + collection_path = '/%ss/{id}/%ss' % (scope_name, item_name) + item_path = '/%ss/{id}/%ss/{id2}' % (scope_name, item_name) + + async def get_scope(id: int): + try: + return await ORMClass1.objects.get(id=id) + except NoMatch: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + + async def get_item(**kwargs): + try: + return await ORMClass2.objects.get(id=kwargs['%s_id' % item_name]) + except NoMatch: + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY) + get_item.__signature__ = Signature(parameters=[Parameter( + '%s_id' % item_name, Parameter.KEYWORD_ONLY, annotation=int)]) + + async def get_relationship(id2: int, scope=Depends(get_scope)): + try: + return await ORMRelationship.objects.select_related(item_name) \ + .get(id=id2, **{scope_name: scope}) + except NoMatch: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + + @app.get(collection_path) + async def read_all(scope=Depends(get_scope)): + return await ORMRelationship.objects.select_related(item_name) \ + .filter(**{scope_name: scope}).all() + + @app.post(collection_path, status_code=status.HTTP_201_CREATED) + async def create( + response: Response, + scope=Depends(get_scope), + item=Depends(get_item)): + try: + relationship = await ORMRelationship.objects.create( + **{scope_name: scope, item_name: item}) + response.headers['Location'] = \ + '%ss/%d' % (item_name, relationship.id) + return relationship + except IntegrityError: + raise HTTPException(status_code=status.HTTP_409_CONFLICT) + + @app.get(item_path) + async def read(relationship=Depends(get_relationship)): + return relationship + + @app.put(item_path, status_code=status.HTTP_204_NO_CONTENT) + async def update( + item=Depends(get_item), + relationship=Depends(get_relationship)): + await relationship.update(**{item_name: item.id}) + + @app.delete(item_path, status_code=status.HTTP_204_NO_CONTENT) + async def delete(relationship=Depends(get_relationship)): + await relationship.delete() diff --git a/pizzatool/model.py b/pizzatool/model.py index 9fb7c9d..a38c2c6 100644 --- a/pizzatool/model.py +++ b/pizzatool/model.py @@ -12,7 +12,7 @@ class Ingredient(Model): name: Text() class Mapping: - table_name = 'ingredients' + table_name = 'ingredient' metadata = metadata database = database @@ -22,7 +22,7 @@ class Pizza(Model): name: Text() class Mapping: - table_name = 'pizzas' + table_name = 'pizza' metadata = metadata database = database @@ -33,7 +33,7 @@ class PizzaIngredient(Model): ingredient: ForeignKey(Ingredient) class Mapping: - table_name = 'pizzaingredients' + table_name = 'pizzaingredient' metadata = metadata database = database