Source code for flask_restful_dbbase.doc_utils
# flask_restful_dbbase/utils.py
"""
This module implements utilities.
"""
from dbbase.utils import xlate
[docs]class MetaDoc(object):
"""
This class provides a scaffolding for holding documentation
used when generating meta documents.
The goal of a meta document is to provide a standard way to communicate
the features of the backend API to frontend users.
Resource header:
model_class
url_prefix
base_url
requirements
Resource methods:
single: get/post/put/patch/delete
input
input_modifier
before_commit (if applicable)
after_commit (if applicable)
responses (if applicable)
collection: get
query
input_modifier
Args:
resource_class: (obj) : The resource documented
requirements: (str) : The input requirements documented
methods: (list|dict) : a list or dict of methods that have
special requirements. Leaving None would mean the default
documentation.
"""
all_methods = ("get", "post", "put", "patch", "delete")
[docs] def __init__(
self,
resource_class,
requirements=None,
methods=None,
):
self.resource_class = resource_class
self.model_class = resource_class.model_class._class()
self.url_prefix = resource_class.url_prefix
self.base_url = resource_class.create_url()
if methods:
if isinstance(methods, dict):
self.methods = methods
elif isinstance(methods, list):
# for convenience
self.methods = {}
for method in methods:
self.methods[method.method] = method
else:
raise ValueError("methods must be a dictionary")
else:
self.methods = {}
self.table = None
[docs] def to_dict(self, method=None):
"""
This function returns the settings for the resource.
Args:
method: (str : None) : choices are get/post/put/patch/delete.
Returns:
meta_data (dict) : A dict with the resource characteristics.
If a method is preferred, the focus will be narrowed to that
method.
The intent of this function is to show relevant information for someone
interacting with an API.
"""
model_class = self.resource_class.model_class
db = model_class.db
doc = {}
attr_list = [
"model_class",
"url_prefix",
"base_url",
"table",
]
# header keys to camel_case
for attr in attr_list:
value = getattr(self, attr)
if value:
doc[xlate(attr, camel_case=True)] = value
self.add_methods(doc, method=method)
doc["table"] = db.doc_table(model_class)
return doc
def add_methods(self, doc, method):
doc.setdefault("methods", {})
if method is None:
for tmp_method in self.all_methods:
if hasattr(self.resource_class, tmp_method):
if tmp_method in self.methods:
method_dict = self.methods[tmp_method].to_dict(self)
doc["methods"][tmp_method] = method_dict
else:
doc["methods"][tmp_method] = MethodDoc(
tmp_method
).to_dict(self)
else:
# check for valid method first
if not hasattr(self.resource_class, method):
raise ValueError(
f"Method '{method}' is not found for this resource"
)
doc["methods"][method] = MethodDoc(method).to_dict(self)
[docs]class MethodDoc(object):
"""
This class holds details about a method.
Args:
method: (str) : The method name
input: (str) : Optional input if necessary, otherwise default
input_modifier: (str) : Optional input of process_{method}_inputs
before_commit: (str) : Optional before_commit description
after_commit: (str) : Optional after_commit description
use_default_response: (bool) : A flag to overwrite responses
responses: (list) : A list of possible custom responses
"""
[docs] def __init__(
self,
method,
input=None,
input_modifier=None,
before_commit=None,
after_commit=None,
use_default_response=True,
responses=None,
):
self.method = method
self.input = input
self.input_modifier = input_modifier
self.before_commit = before_commit
self.after_commit = after_commit
self.use_default_response = True
if responses is None:
self.responses = []
else:
self.responses = responses
def _get_inputs(self, resource_class):
"""Dispatch to the right input type."""
if self.method in ["get", "delete"]:
input_dict = self._get_input_keys(resource_class)
else:
input_dict = self._get_input_props(resource_class)
return input_dict
@staticmethod
def _get_input_keys(resource_class):
"Extract keys for methods"
db = resource_class.model_class.db
keys = resource_class.get_key_names()
if len(keys) > 1:
return [
dict([[key, db.doc_column(resource_class.model_class, key)]])
for key in keys
]
else:
key = keys[0]
return {key: db.doc_column(resource_class.model_class, key)}
@staticmethod
def _get_input_props(resource_class):
return resource_class.model_class.filter_columns(
column_props=["!readOnly"],
to_camel_case=True,
)
@staticmethod
def _get_url(doc, method, resource_class):
if method in ["get", "put", "patch", "delete"]:
if resource_class.is_collection():
doc["url"] = resource_class.get_urls()[0]
else:
doc["url"] = resource_class.get_urls()[1]
else:
doc["url"] = resource_class.get_urls()[0]
[docs] def to_dict(self, meta_doc):
"""Convert attributes to a dictionary"""
resource_class = meta_doc.resource_class
doc = {}
self._get_url(doc, self.method, resource_class)
doc["requirements"] = self.get_method_decorators(meta_doc)
if meta_doc.resource_class.is_collection():
doc["queryString"] = self._get_inputs(resource_class)
# hard-coded for the moment
doc["jobParams"] = {
"orderBy": {"type": "string", "list": True},
"maxPageSize": {"type": "integer"},
"offset": {"type": "integer"},
"debug": {"type": "boolean"},
}
else:
doc["input"] = self._get_inputs(resource_class)
if self.input_modifier is not None:
doc["input_modifier"] = self.input_modifier
if self.before_commit is not None:
doc["before_commit"] = self.before_commit
if self.after_commit is not None:
doc["after_commit"] = self.after_commit
if self.responses:
doc["responses"] = self.responses
elif self.use_default_response:
doc["responses"] = [self.get_default_response(meta_doc)]
return doc
[docs] def get_method_decorators(self, meta_doc):
"""
This function returns a string representation of any method
decorators.
Args:
meta_doc: (obj) : The meta document being processed.
Returns:
(list : None) : The string list of method decorator names.
"""
method_decorators = meta_doc.resource_class.method_decorators
if isinstance(method_decorators, list):
return [item.__name__ for item in method_decorators]
if isinstance(method_decorators, dict):
if self.method in method_decorators:
return [
item.__name__ for item in method_decorators[self.method]
]
return None
[docs] def get_default_response(self, meta_doc):
"""
This function returns the standard response for the method.
If something special is done, then the default response would be
supressed.
"""
resource = meta_doc.resource_class
method = self.method
db = resource.model_class.db
outputs = {}
if method != "delete":
serial_fields = resource._get_serial_fields(
method, with_class=True
)
if isinstance(serial_fields, dict):
# foreign class
foreign_class, serial_fields = list(serial_fields.items())[0]
# NOTE: serial field relations is unresolved
doc = db.doc_table(
foreign_class,
serial_fields=serial_fields,
serial_field_relations=(
resource._get_serial_field_relations(method)
),
to_camel_case=True,
)[foreign_class._class()]
else:
if serial_fields is None:
serial_fields = resource.model_class.get_serial_fields()
doc = db.doc_table(
resource.model_class,
serial_fields=serial_fields,
serial_field_relations=(
resource._get_serial_field_relations(method)
),
to_camel_case=True,
)[resource.model_class._class()]
outputs["fields"] = doc["properties"]
# NOTE: here is where the default sort would go for collections
return outputs