From 188947ecadf83d7e127b5379b525f64b2c9312aa Mon Sep 17 00:00:00 2001 From: Scott Idem Date: Mon, 28 Nov 2022 19:40:57 -0500 Subject: [PATCH] New CRUD PATCH endpoint --- app/db_sql.py | 11 +- app/models/api_crud_models.py | 68 ++++++++++++ app/routers/api_crud.py | 188 +++++++++++++++++++--------------- 3 files changed, 184 insertions(+), 83 deletions(-) create mode 100644 app/models/api_crud_models.py diff --git a/app/db_sql.py b/app/db_sql.py index ebcc9e6..f14b38b 100644 --- a/app/db_sql.py +++ b/app/db_sql.py @@ -351,15 +351,22 @@ def sql_update( else: log.debug(result_update) log.debug(f'rowcount = {result_update.rowcount}; lastrowid = {result_update.lastrowid}') - if result_update.rowcount >= 1 and result_update.lastrowid == 0: # update with no change - log.info(f'Updated {result_update.rowcount} records (with no changes?)') # With SQL UPDATE this record may have actually changed + if result_update.rowcount >= 1 and result_update.lastrowid == 0: # one record updated + log.info(f'One record was found and updated (changes unknown). Returning True') # With SQL UPDATE this record may have actually changed return True + elif result_update.rowcount > 1 and result_update.lastrowid == 0: # multiple records updated + log.info(f'Multiple records ({result_update.rowcount}) were found and updated. Returning True') + return True + elif result_update.rowcount == 0 and result_update.lastrowid == 0: # no records found to update (ID probably not found) + log.info('No record(s) found to update. The ID was probably not found. Returning None') + return None elif result_update.rowcount == 2 and result_update.lastrowid > 0: # update with change log.warning('Should we be here???') log.info('Update record with changes') record_id = result_update.lastrowid return record_id else: + log.info('Unknown or unexpected SQL UPDATE response? Returning None') log.debug(result_update) log.debug(vars(result_update)) log.debug(dir(result_update)) diff --git a/app/models/api_crud_models.py b/app/models/api_crud_models.py new file mode 100644 index 0000000..070fbc3 --- /dev/null +++ b/app/models/api_crud_models.py @@ -0,0 +1,68 @@ +import datetime, pytz + +from typing import Dict, List, Optional, Set, Union +from pydantic import BaseModel, EmailStr, Field, Json, PrivateAttr, ValidationError, validator + +from app.db_sql import redis_lookup_id_random +from app.lib_general import log, logging + +from app.models.common_field_schema import base_fields, default_num_bytes + + +# ### BEGIN ### API CRUD Models ### Fundraising_Cfg_Base() ### +class Api_Crud_Base(BaseModel): + log.setLevel(logging.INFO) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL + log.debug(locals()) + + super_key: Optional[str] = None # Query(None, min_length=8, max_length=50), + + create_key: Optional[str] = None # Query(None, min_length=6, max_length=50), + read_key: Optional[str] = None # Query(None, min_length=5, max_length=50), + update_key: Optional[str] = None # Query(None, min_length=6, max_length=50), + delete_key: Optional[str] = None # Query(None, min_length=7, max_length=50), + + # id_random: Optional[str] = Field( + # alias = 'obj_id_random', + # ) + # id: Optional[int] = Field( + # alias = 'obj_id', + # ) + + obj_type: Optional[str] + + # account_id_random: Optional[str] + # account_id: Optional[int] + + data_list: Optional[dict] + + # enable: Optional[bool] + + # hide: Optional[bool] + # priority: Optional[bool] + # sort: Optional[int] + # group: Optional[str] # Same or similar as file_purpose? + + # created_on: Optional[datetime.datetime] = None + # updated_on: Optional[datetime.datetime] = None + + _processed_at: datetime.datetime = PrivateAttr(default_factory=datetime.datetime.now) + + # @validator('id', always=True) + # def fundraising_cfg_id_lookup(cls, v, values, **kwargs): + # if isinstance(v, int) and v > 0: return v + # elif id_random := values.get('id_random'): + # return redis_lookup_id_random(record_id_random=id_random, table_name='v_fundraising_cfg') # There is only a view 2022-11-18 + # return None + + # @validator('account_id', always=True) + # def account_id_lookup(cls, v, values, **kwargs): + # if isinstance(v, int) and v > 0: return v + # elif id_random := values.get('account_id_random'): + # return redis_lookup_id_random(record_id_random=id_random, table_name='account') + # return None + + class Config: + underscore_attrs_are_private = True + allow_population_by_field_name = True + fields = base_fields +# ### END ### API CRUD Models ### Api_Crud_Base() ### diff --git a/app/routers/api_crud.py b/app/routers/api_crud.py index 23a3b79..2942935 100644 --- a/app/routers/api_crud.py +++ b/app/routers/api_crud.py @@ -1,4 +1,4 @@ -import datetime +import datetime, time #from datetime import datetime, time, timedelta from fastapi import APIRouter, Body, Depends, Header, HTTPException, Query, Response, status from pydantic import BaseModel, EmailStr, Field @@ -10,6 +10,8 @@ from app.db_sql import sql_insert, sql_update, sql_insert_or_update, sql_select, from app.models.response_models import * +from app.models.api_crud_models import * + from app.models.account_models import * from app.models.account_cfg_models import * from app.models.activity_log_models import * @@ -325,96 +327,120 @@ async def get_obj( return mk_resp(data=False, status_code=404, response=response) -# @router.patch('/{obj_type_l1}/{obj_id}') -# @router.patch('/{obj_type_l1}/{obj_type_l2}/{obj_id}') -# @router.patch('/{obj_type_l1}/{obj_type_l2}/{obj_type_l3}/{obj_id}') -# async def patch_obj( -# obj_type_l1: Optional[str] = Query(..., max_length=50), -# obj_type_l2: str=None, -# obj_type_l3: str=None, -# obj_id: str = Query(..., min_length=11, max_length=22), +@router.patch('/{obj_type_l1}/{obj_id}') +@router.patch('/{obj_type_l1}/{obj_type_l2}/{obj_id}') +@router.patch('/{obj_type_l1}/{obj_type_l2}/{obj_type_l3}/{obj_id}') +async def patch_obj( + crud: Api_Crud_Base, + obj_type_l1: Optional[str] = Query(..., max_length=50), + obj_type_l2: str = None, + obj_type_l3: str = None, + obj_id: str = Query(..., min_length=11, max_length=22), -# for_obj_type: Optional[str] = Query(None, max_length=50), -# for_obj_id: Optional[str] = Query(None, max_length=22), -# x_account_id: str = Header(...), -# qry_str: Optional[str] = Query(None, max_length=50), -# qry_int: Optional[int] = None, -# by_alias: Optional[bool] = True, -# include: Optional[list] = [], -# exclude: Optional[list] = [], -# exclude_unset: Optional[bool] = True, -# exclude_none: Optional[bool] = True, -# response: Response = Response, + # for_obj_type: Optional[str] = Query(None, max_length=50), + # for_obj_id: Optional[str] = Query(None, max_length=22), -# commons: Common_Route_Params = Depends(common_route_params), -# ): -# """ -# Simple select object type with an ID: -# - **obj_type_l1, obj_type_l2, obj_type_l3**: -# - Examples: -# - /account = account -# - /user = user -# - /user/role = user_role -# - /event = event -# - /event/exhibit = event_exhibit -# - /order = order -# - /order/cart = order_cart -# - /order/cart/line = order_cart_line -# - /lu/some_lookup = lu_some_lookup -# """ -# log.setLevel(logging.DEBUG) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL -# log.debug(locals()) + commons: Common_Route_Params = Depends(common_route_params), + ): + """ + Simple patch object type with an ID: + - **obj_type_l1, obj_type_l2, obj_type_l3**: + - Examples: + - /account = account + - /user = user + - /user/role = user_role + - /event = event + - /event/exhibit = event_exhibit + - /order = order + - /order/cart = order_cart + - /order/cart/line = order_cart_line + - /lu/some_lookup = lu_some_lookup + """ + log.setLevel(logging.DEBUG) # DEBUG, INFO, WARNING, ERROR, EXCEPTION, CRITICAL + log.debug(locals()) -# debug_data = {} -# debug_data['obj_type_l1'] = obj_type_l1 -# debug_data['obj_type_l2'] = obj_type_l2 -# debug_data['obj_type_l3'] = obj_type_l3 -# debug_data['obj_id'] = obj_id -# debug_data['for_obj_type'] = for_obj_type -# debug_data['for_obj_id'] = for_obj_id + if crud.super_key == '789123': pass + else: + log.warning('Access key is missing or incorrect') + return mk_resp(data=False, status_code=400, response=commons.response) -# log.debug(debug_data) + # NOTE: WARNING NOTE: WARNING NOTE: WARNING NOTE: WARNING NOTE: WARNING NOTE: WARNING NOTE: WARNING + # time.sleep(1.5) # NOTE: WARNING NOTE: WARNING NOTE: WARNING NOTE: WARNING NOTE: WARNING NOTE: WARNING + # NOTE: WARNING NOTE: WARNING NOTE: WARNING NOTE: WARNING NOTE: WARNING NOTE: WARNING NOTE: WARNING -# if obj_type_l1 and obj_type_l2 and obj_type_l3: -# obj_name = f'{obj_type_l1}_{obj_type_l2}_{obj_type_l3}' -# if obj_name in obj_type_li: -# #table_name = obj_type_li[obj_name] -# #table_name = obj_type_li[obj_name]['table_name'] -# pass -# else: -# return mk_resp(data=False, status_code=400, response=commons.response) -# elif obj_type_l1 and obj_type_l2: -# obj_name = f'{obj_type_l1}_{obj_type_l2}' -# if obj_name in obj_type_li: -# #table_name = obj_type_li[obj_name]['table_name'] -# pass -# else: -# return mk_resp(data=False, status_code=400, response=commons.response) -# elif obj_type_l1: -# obj_name = f'{obj_type_l1}' -# if obj_name in obj_type_li: -# #table_name = obj_type_li[obj_name]['table_name'] -# pass -# else: -# return mk_resp(data=False, status_code=400, response=commons.response) -# else: -# log.warning('We should not be here') -# return mk_resp(data=False, status_code=400, response=commons.response) + debug_data = {} + debug_data['crud'] = crud -# table_name = obj_type_li[obj_name]['table_name'] + debug_data['create_key'] = crud.create_key + debug_data['read_key'] = crud.read_key + debug_data['update_key'] = crud.update_key + debug_data['delete_key'] = crud.delete_key -# # NOTE: Add a check for the object ID... assuming it is a random ID string for now. -# if sql_result := sql_select(table_name=table_name, record_id_random=obj_id): -# log.debug(sql_result) + debug_data['data_list'] = crud.data_list -# base_name = obj_type_li[obj_name]['base_name'] -# resp_data = base_name(**sql_result).dict(by_alias=commons.by_alias, exclude_unset=commons.exclude_unset) + debug_data['obj_type_l1'] = obj_type_l1 + debug_data['obj_type_l2'] = obj_type_l2 + debug_data['obj_type_l3'] = obj_type_l3 + debug_data['obj_id'] = obj_id -# return mk_resp(data=resp_data, response=response) #, details=debug_data) -# else: -# log.debug(sql_result) -# return mk_resp(data=False, status_code=404, response=response) + log.debug(debug_data) + + if obj_type_l1 and obj_type_l2 and obj_type_l3: + obj_name = f'{obj_type_l1}_{obj_type_l2}_{obj_type_l3}' + if obj_name in obj_type_li: + #table_name = obj_type_li[obj_name] + #table_name = obj_type_li[obj_name]['table_name'] + pass + else: + return mk_resp(data=False, status_code=400, response=commons.response) + elif obj_type_l1 and obj_type_l2: + obj_name = f'{obj_type_l1}_{obj_type_l2}' + if obj_name in obj_type_li: + #table_name = obj_type_li[obj_name]['table_name'] + pass + else: + return mk_resp(data=False, status_code=400, response=commons.response) + elif obj_type_l1: + obj_name = f'{obj_type_l1}' + if obj_name in obj_type_li: + #table_name = obj_type_li[obj_name]['table_name'] + pass + else: + return mk_resp(data=False, status_code=400, response=commons.response) + else: + log.warning('We should not be here') + return mk_resp(data=False, status_code=400, response=commons.response) + + table_name = obj_type_li[obj_name]['table_name'] + + # ### SECTION ### Secondary data validation + obj_id_random = obj_id # This is used later for the response data + if obj_id := redis_lookup_id_random(record_id_random=obj_id, table_name=table_name): pass + else: return mk_resp(data=None, status_code=404, response=commons.response, status_message='The object ID was invalid or not found.') + + # obj_id = 999999 + + + # NOTE: Add a check for the object ID... assuming it is a random ID string for now. + if sql_result := sql_update(data=crud.data_list, table_name=table_name, record_id=obj_id, log_lvl=logging.INFO): + log.info('The record was updated.') + log.debug(sql_result) + + base_name = obj_type_li[obj_name]['base_name'] + # resp_data = base_name(**sql_result).dict(by_alias=commons.by_alias, exclude_unset=commons.exclude_unset) + resp_data = { 'base_name': base_name, 'request_data': crud.data_list } + resp_data = 'Record updated!' + + return mk_resp(data=resp_data, response=commons.response) #, details=debug_data) + elif sql_result == None: + log.info('The record was probably not found to be updated.') + log.debug(sql_result) + return mk_resp(data=None, status_code=404, status_message='The record was probably not found to be updated.', response=commons.response) + else: + log.info('Something unexpected happened while trying to runt he SQL UPDATE. The fields or field values passed may not be valid for the table.') + log.debug(sql_result) + return mk_resp(data=False, status_code=400, status_message='Something unexpected happened while trying to runt he SQL UPDATE. The fields or field values passed may not be valid for the table.', response=commons.response)