# flask_restful_dbbase/resources/model_resource.py
""""
This module implements a starting point for model resources.
"""
import logging
from flask_restful import request
from .dbbase_resource import DBBaseResource
from ..validations import validate_process
logger = logging.getLogger(__name__)
[docs]class ModelResource(DBBaseResource):
"""
ModelResource Class
This model class implements the base class.
**Class variables**:
model_class: a dbbase.Model
url_prefix: the portion of the path leading up to the resource
For example: /api/v2
url_name: the url_name defaults to a lower case version of the
the model_class name if left as None, but can have an
explicit name if necessary.
serial_fields: if left as None, it uses the serial list from
the model class. However, it can be a list of field names
or
before_commit: A dict with method keys for placing a function
to run just before committing an item to the database. It
can also divert the method to end the HTTP method early and
return something entirely different than the item being applied.
after_commit: A dict with method keys for placing a function
to run just afteer committing an item to the database. It
can also divert the method to end the HTTP method early and
return something entirely different than the item being applied.
"""
process_get_input = None
process_post_input = None
process_put_input = None
process_patch_input = None
process_delete_input = None
[docs] def get(self, **kwargs):
"""
This function is the HTTP GET method for a resource handling
a single item.
"""
# may be used later
# url = request.path
FUNC_NAME = "get"
# the correct key test - raises error if improper url
kdict = self._check_key(kwargs)
# for use only with self.process_get_input
data = request.args.to_dict(flat=False)
query = self.model_class.query
if self.process_get_input is not None:
output = self.process_get_input(query, data, kwargs)
validate_process(output, true_keys=["query", "data"])
if output["status"]:
query = output["query"]
data = output["data"]
else:
message = output["message"]
status_code = output["status_code"]
return message, status_code
try:
for key_name, key in kdict.items():
query = query.filter(
getattr(self.model_class, key_name) == key
)
item = query.first()
except Exception as err:
msg = err.args[0]
logger.error(msg)
return {"message": msg}, 500
sfields, sfield_relations = self._get_serializations(FUNC_NAME)
if item:
result = item.to_dict(
serial_fields=sfields,
serial_field_relations=sfield_relations,
)
logger.debug(result)
return result, 200
msg = f"{self.model_name} with {kdict} not found"
logger.debug(msg)
return {"message": msg}, 404
[docs] def post(self):
"""
This function is the HTTP POST method for a resource handling
a single item.
"""
FUNC_NAME = "post"
# may be used later
# url = request.path
status_code = 201
if request.is_json:
try:
data = request.json
except Exception as err:
msg = err
return_msg = f"A JSON format problem:{msg}: {request.data}"
logger.error(return_msg)
return {"message": return_msg}, 400
else:
logger.info("JSON format is required")
return {"message": "JSON format is required"}, 415
if self.process_post_input is not None:
output = self.process_post_input(data)
validate_process(output, true_keys=["data"])
if output["status"]:
data = output["data"]
else:
message = output["message"]
status_code = output["status_code"]
return message, status_code
obj_params = self.get_obj_params()
try:
status, data = self.screen_data(
self.model_class.deserialize(data), obj_params
)
except Exception as err:
msg = f"malformed data: {err.args[0]}"
logger(msg)
return {"message": msg}, 400
if status is False:
logger.info(data)
return {"message": data}, 400
key_names = self.get_key_names(formatted=False)
item = None
status, kdict = self._all_keys_found(key_names, data)
if status:
# verify it does not already exist
query = self.model_class.query
for key_name, value in kdict.items():
query = query.filter(
getattr(self.model_class, key_name) == value
)
try:
item = query.first()
except Exception as err:
msg = err.args[0]
logger.error(msg)
return {"message": msg}, 400
if item:
msg = f"{kdict} for {self.model_name} already exists."
logger.info(msg)
return (
{"message": msg},
409,
)
non_rel_columns = dict(
[
[key, value]
for key, value in obj_params.items()
if "relationship" not in value
]
)
non_rel_data = dict(
[
[key, value]
for key, value in data.items()
if key in non_rel_columns
]
)
item = self.model_class(**non_rel_data)
rel_columns = dict(
[
[key, value]
for key, value in obj_params.items()
if "relationship" in value
]
)
# relationship data by column
rel_data = dict(
[[key, value] for key, value in data.items() if key in rel_columns]
)
for key, value in rel_data.items():
# key such as invoice_items
rel_info = rel_columns[key]["relationship"]
sub_obj_params = rel_info["fields"]
entity = rel_info["entity"]
sub_class = self.model_class._decl_class_registry[entity]
if rel_info["type"] == "list":
if not isinstance(value, list):
return (
{"message": f"{key} data must be in a list form"},
400,
)
for subitem in value:
# subitem such as invoice item
# screen against column
# no missing data check, parent id auto filled
# NOTE: needs further work
sub_status, sub_data = self.screen_data(
self.model_class.deserialize(subitem),
sub_obj_params,
skip_missing_data=True,
)
if sub_status is False:
# NOTE: look at this further
logger.info(sub_data)
return {"message": sub_data}, 400
getattr(item, key).append(sub_class(**sub_data))
adjust_before = self.before_commit.get(FUNC_NAME)
if adjust_before is not None:
status, result, status_code = self._item_adjust(
adjust_before, item, status_code
)
if status:
item = result
else:
return result, status_code
try:
item.save()
except Exception as err:
msg = err.args[0]
logger.error(msg)
return {"message": msg}, 400
adjust_after = self.after_commit.get(FUNC_NAME)
if adjust_after:
status, result, status_code = self._item_adjust(
adjust_after, item, status_code
)
if status:
item = result
else:
return result, status_code
ser_fields, rel_ser_fields = self._get_serializations(FUNC_NAME)
return (
item.to_dict(
serial_fields=ser_fields,
serial_field_relations=rel_ser_fields,
),
status_code,
)
[docs] def put(self, **kwargs):
"""
This function is the HTTP PUT method for a resource handling a
single item.
"""
url = request.path
FUNC_NAME = "put"
status_code = 200
try:
kdict = self._check_key(kwargs)
except Exception as err:
msg = err.args[0]
logger.info(msg)
return {"message": msg}, 400
if request.is_json:
try:
data = request.json
except Exception as err:
msg = err
return_msg = f"A JSON format problem:{msg}"
logger.info(return_msg)
return {"message": return_msg}, 400
else:
return {"message": "JSON format is required"}, 415
query = self.model_class.query
if self.process_put_input is not None:
output = self.process_put_input(query, data, kwargs)
validate_process(output, true_keys=["query", "data"])
if output["status"]:
query = output["query"]
data = output["data"]
else:
message = output["message"]
status_code = output["status_code"]
return message, status_code
status, kdict = self._all_keys_found(list(kdict.keys()), data)
if status:
for key_name, value in kdict.items():
query = query.filter(
getattr(self.model_class, key_name) == value
)
try:
item = query.first()
except Exception as err:
msg = err.args[0]
logger.error(msg)
return {"message": msg}, 400
data = self.model_class.deserialize(data)
# use the key(s) from the url
data.update(kdict)
status, data = self.screen_data(
self.model_class.deserialize(data), self.get_obj_params()
)
if status is False:
logger.info(f"{str(data)}: 400")
return {"message": data}, 400
if item is None:
item = self.model_class(**data)
else:
for key, value in data.items():
setattr(item, key, value)
adjust_before = self.before_commit.get(FUNC_NAME)
if adjust_before:
status, result, status_code = self._item_adjust(
adjust_before, item, status_code
)
if status:
item = result
else:
return result, status_code
try:
item.save()
except Exception as err:
self.model_class.db.session.rollback()
msg = err.args[0]
logger.info(msg)
logger.error(f"{url} method {FUNC_NAME}: {msg}")
return {"message": msg}, 400
adjust_after = self.after_commit.get(FUNC_NAME)
if adjust_after:
status, result, status_code = self._item_adjust(
adjust_after, item, status_code
)
if status:
item = result
else:
return result, status_code
ser_fields, rel_ser_fields = self._get_serializations(FUNC_NAME)
return (
item.to_dict(
serial_fields=ser_fields,
serial_field_relations=rel_ser_fields,
),
status_code,
)
[docs] def patch(self, **kwargs):
"""
This function is the HTTP PATCH method for a resource handling
a single item.
"""
url = request.path
FUNC_NAME = "patch"
status_code = 200
try:
kdict = self._check_key(kwargs)
except Exception as err:
msg = err.args[0]
return {"message": msg}, 400
if request.is_json:
try:
data = request.json
except Exception as err:
msg = err
return_msg = f"A JSON format problem:{msg}: {request.data}"
return {"message": return_msg}, 400
else:
return {"message": "JSON format is required"}, 415
query = self.model_class.query
if self.process_patch_input is not None:
output = self.process_patch_input(query, data, kwargs)
validate_process(output, true_keys=["query", "data"])
if output["status"]:
query = output["query"]
data = output["data"]
else:
message = output["message"]
status_code = output["status_code"]
return message, status_code
for key_name, value in kdict.items():
query = query.filter(getattr(self.model_class, key_name) == value)
try:
item = query.first()
except Exception as err:
msg = err.args[0]
logger.error(msg)
return {"message": msg}, 500
data = self.model_class.deserialize(data)
data.update(kdict)
status, data = self.screen_data(
data, self.get_obj_params(), skip_missing_data=True
)
if status is False:
return {"message": data}, 400
if item is None:
item = self.model_class(**data)
else:
for key, value in data.items():
setattr(item, key, value)
adjust_before = self.before_commit.get(FUNC_NAME)
if adjust_before is not None:
status, result, status_code = self._item_adjust(
adjust_before, item, status_code
)
if status:
item = result
else:
return result, status_code
try:
item.save()
except Exception as err:
msg = err.args[0]
self.model_class.db.session.rollback()
logger.error(f"{url} method {FUNC_NAME}: {msg}")
return (
{
"message": "An error occurred updating the "
f"{self.model_name}: {msg}."
},
500,
)
adjust_after = self.after_commit.get(FUNC_NAME)
if adjust_after:
status, result, status_code = self._item_adjust(
adjust_after, item, status_code
)
if status:
item = result
else:
return result, status_code
ser_fields, rel_ser_fields = self._get_serializations(FUNC_NAME)
return (
item.to_dict(
serial_fields=ser_fields,
serial_field_relations=rel_ser_fields,
),
status_code,
)
[docs] def delete(self, **kwargs):
"""
This function is the HTTP DELETE method for a resource
handling a single item.
"""
url = request.path
FUNC_NAME = "delete"
status_code = 200
try:
kdict = self._check_key(kwargs)
except Exception as err:
msg = err.args[0]
return {"message": msg}, 400
query = self.model_class.query
if self.process_delete_input is not None:
output = self.process_delete_input(query, kwargs)
validate_process(output, true_keys=["query"])
if output["status"]:
query = output["query"]
else:
message = output["message"]
status_code = output["status_code"]
return {"message": message}, status_code
for key_name, value in kdict.items():
query = query.filter(getattr(self.model_class, key_name) == value)
try:
item = query.first()
except Exception as err:
msg = err.args[0]
logger.error(msg)
return {"message": msg}, 500
if item is None:
msg = f"{self.model_name} with {kdict} not found"
logger.debug(msg)
return {"message": msg}, 404
adjust_before = self.before_commit.get(FUNC_NAME)
if adjust_before is not None:
status, result, status_code = self._item_adjust(
adjust_before, item, status_code
)
if status:
item = result
else:
return result, status_code
try:
item.delete()
except Exception as err:
self.model_class.db.session.rollback()
msg = err.args[0]
logger.error(f"{url} method {FUNC_NAME}: {msg}")
return (
{
"message": "An error occurred deleting the "
f"{self.model_name}: {msg}."
},
500,
)
msg = f"{self.model_name} with {kdict} is deleted"
return {"message": msg}, status_code