Skip to content

Commit

Permalink
jsonapi_rpc get (#36) + cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
thomaxxl committed May 15, 2019
1 parent 80b2afb commit a612c0b
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 43 deletions.
14 changes: 12 additions & 2 deletions safrs/_api.py
Expand Up @@ -118,7 +118,11 @@ class Class_API(SAFRSRestAPI):
def expose_methods(self, url_prefix, tags):
"""
Expose the safrs "documented_api_method" decorated methods
:param url_prefix: api url prefix
:param tags: swagger tags
:return: None
"""

safrs_object = self.safrs_object
api_methods = safrs_object._s_get_jsonapi_rpc_methods()
for api_method in api_methods:
Expand All @@ -145,7 +149,7 @@ def expose_methods(self, url_prefix, tags):
api_class = api_decorator(type(api_method_class_name, (SAFRSRestMethodAPI,), properties), swagger_decorator)
meth_name = safrs_object.__tablename__ + "." + api_method.__name__
safrs.log.info("Exposing method {} on {}, endpoint: {}".format(meth_name, url, endpoint))
self.add_resource(api_class, url, endpoint=endpoint, methods=get_http_methods(api_method))
self.add_resource(api_class, url, endpoint=endpoint, methods=get_http_methods(api_method), jsonapi_rpc=True)

def expose_relationship(self, relationship, url_prefix, tags):
"""
Expand All @@ -158,6 +162,11 @@ class Parent_X_Child_API(SAFRSRestAPI):
SAFRSObject = safrs_object
add the class as an api resource to /SAFRSObject and /SAFRSObject/{id}
:param relationship: relationship
:param url_prefix: api url prefix
:param tags: swagger tags
:return: None
"""

API_CLASSNAME_FMT = "{}_X_{}_API"
Expand Down Expand Up @@ -241,6 +250,7 @@ def add_resource(self, resource, *urls, **kwargs):
definitions = {}
resource_methods = kwargs.get("methods", HTTP_METHODS)
kwargs.pop("safrs_object", None)
is_jsonapi_rpc = kwargs.pop("jsonapi_rpc", False) # check if the exposed method is a jsonapi_rpc method
for method in [m.lower() for m in resource.methods]:
if not method.upper() in resource_methods:
continue
Expand Down Expand Up @@ -302,7 +312,7 @@ def add_resource(self, resource, *urls, **kwargs):
if param not in filtered_parameters:
filtered_parameters.append(param)

if method == "get" and not swagger_url.endswith(SAFRS_INSTANCE_SUFFIX):
if method == "get" and not swagger_url.endswith(SAFRS_INSTANCE_SUFFIX) and not is_jsonapi_rpc:
# limit parameter specifies the number of items to return

for param in default_paging_parameters():
Expand Down
6 changes: 3 additions & 3 deletions safrs/api_methods.py
Expand Up @@ -3,7 +3,7 @@
"""
from sqlalchemy import or_
from .jsonapi import SAFRSFormattedResponse, paginate, jsonapi_format_response
from .swagger_doc import documented_api_method, jsonapi_rpc
from .swagger_doc import jsonapi_rpc
from .errors import GenericError, ValidationError

# from .safrs_types import SAFRSID
Expand All @@ -27,7 +27,7 @@ def get_list(self, id_list):
return result


@documented_api_method
@jsonapi_rpc(http_methods=['POST'])
def lookup_re_mysql(cls, **kwargs):
"""
pageable: True
Expand Down Expand Up @@ -58,7 +58,7 @@ def lookup_re_mysql(cls, **kwargs):
return result.all()


@documented_api_method
@jsonapi_rpc(http_methods=['POST'])
def startswith(cls, **kwargs):
"""
pageable: True
Expand Down
5 changes: 3 additions & 2 deletions safrs/jsonapi.py
Expand Up @@ -92,15 +92,16 @@ def jsonapi_sort(object_query, safrs_object):
if not sort_columns is None:
for sort_column in sort_columns.split(","):
if sort_column.startswith("-"):
# if the sort column starts with - , then we want to do a reverse sort
attr = getattr(safrs_object, sort_column[1:], None)
if attr is None:
raise ValidationError("Invalid Sort Column Name")
raise ValidationError("Invalid sort column {}".format(sort_column))
attr = attr.desc()
object_query = object_query.order_by(attr)
else:
attr = getattr(safrs_object, sort_column, None)
if attr is None:
raise ValidationError("Invalid Sort Column Name")
raise ValidationError("Invalid sort column {}".format(sort_column))
object_query = object_query.order_by(attr)

return object_query
Expand Down
109 changes: 73 additions & 36 deletions safrs/swagger_doc.py
Expand Up @@ -33,7 +33,7 @@ def parse_object_doc(object):
try:
yaml_doc = yaml.load(raw_doc)
except (SyntaxError, yaml.scanner.ScannerError) as exc:
safrs.LOGGER.error("Failed to parse documentation {} ({})".format(raw_doc, exc))
safrs.log.error("Failed to parse documentation {} ({})".format(raw_doc, exc))
yaml_doc = {"description": raw_doc}

except Exception as exc:
Expand All @@ -51,34 +51,44 @@ def documented_api_method(method):
Decorator to expose functions in the REST API:
When a method is decorated with documented_api_method, this means
it becomes available for use through HTTP POST (i.e. public)
:param method: method to be decorated
:return: decorated method
"""

safrs.log.error(methodxx)
USE_API_METHODS = get_config("USE_API_METHODS")
if USE_API_METHODS:
try:
api_doc = parse_object_doc(method)
except yaml.scanner.ScannerError:
safrs.LOGGER.error("Failed to parse documentation for %s", method)
safrs.log.error("Failed to parse documentation for %s", method)
setattr(method, REST_DOC, api_doc)
return method


def jsonapi_rpc(http_methods):
"""
Decorator to expose functions in the REST API:
When a method is decorated with jsonapi_rpc, this means
it becomes available for use through HTTP POST (i.e. public)
:param http_methods:
:return: function
"""

def _documented_api_method(method):
"""
Decorator to expose functions in the REST API:
When a method is decorated with documented_api_method, this means
it becomes available for use through HTTP POST (i.e. public)
:param method:
add metadata to the method:
REST_DOC: swagger documentation
HTTP_METHODS: the http methods (GET/POST/..) used to call this method
"""
USE_API_METHODS = get_config("USE_API_METHODS")
if USE_API_METHODS:
try:
api_doc = parse_object_doc(method)
except yaml.scanner.ScannerError:
safrs.LOGGER.error("Failed to parse documentation for %s", method)
safrs.log.error("Failed to parse documentation for %s", method)
setattr(method, REST_DOC, api_doc)
setattr(method, HTTP_METHODS, http_methods)
return method
Expand All @@ -91,6 +101,7 @@ def is_public(method):
:param method:
:return: True or False, whether the method is to be exposed
"""

return hasattr(method, REST_DOC)


Expand All @@ -105,14 +116,19 @@ def get_doc(method):

def get_http_methods(method):
"""
get_http_methods
:param method:
:return: a list of http methods used to call this method
"""

return getattr(method, HTTP_METHODS, ["POST"])


def SchemaClassFactory(name, properties):
"""
Generate a Schema class, used to describe swagger schemas
:param name: schema class name
:param properties: class attributes
:return: class
"""

def __init__(self, **kwargs):
Expand Down Expand Up @@ -164,8 +180,11 @@ def encode_schema(obj):
# pylint: disable=redefined-builtin
def schema_from_object(name, object):
"""
schema_from_object
:param name:
:param object:
:return: swagger schema object
"""

properties = {}

if isinstance(object, str):
Expand All @@ -191,7 +210,7 @@ def schema_from_object(name, object):
properties[k] = {"example": "", "type": "string"}
else: # isinstance(object, datetime.datetime):
properties = {"example": str(k), "type": "string"}
safrs.LOGGER.warning("Invalid schema object type %s", type(object))
safrs.log.warning("Invalid schema object type %s", type(object))
else:
raise ValidationError("Invalid schema object type {}".format(type(object)))

Expand All @@ -204,7 +223,7 @@ def schema_from_object(name, object):
return SchemaClassFactory(name, properties)


def get_swagger_doc_post_arguments(cls, method_name):
def get_swagger_doc_arguments(cls, method_name, http_method):
"""
create a schema for all methods which can be called through the
REST POST interface
Expand All @@ -223,6 +242,10 @@ def get_swagger_doc_post_arguments(cls, method_name):
returned by get_doc()
We use "meta" to remain compliant with the jsonapi schema
:param cls:
:param method_name:
:return: parameters, fields, description, method
"""

parameters = []
Expand All @@ -236,18 +259,29 @@ def get_swagger_doc_post_arguments(cls, method_name):
description = rest_doc.get("description", "")
if rest_doc:
method_args = rest_doc.get("args", [])
parameters = rest_doc.get("parameters", [])
if method_args:
model_name = "{}_{}".format(cls.__name__, method_name)
method_field = {"method": method_name, "args": method_args}
fields["meta"] = schema_from_object(model_name, method_field)
if http_method == 'get' and isinstance(method_args, list):
# query string arguments
# e.g. [{'name': 'name', 'type': 'string', 'default': 'def value'}]
for arg in method_args:
arg["in"] = "query"
parameters = method_args

elif isinstance(method_args, dict):
"""
Post arguments, these require a schema
"""
model_name = "{}_{}".format(cls.__name__, method_name)
method_field = {"method": method_name, "args": method_args}
fields["meta"] = schema_from_object(model_name, method_field)

parameters = rest_doc.get("parameters", [])
if rest_doc.get(PAGEABLE):
parameters += default_paging_parameters()
if rest_doc.get(FILTERABLE):
pass
else:
safrs.LOGGER.warning('No documentation for method "{}"'.format(method_name))
safrs.log.warning('No documentation for method "{}"'.format(method_name))
# jsonapi_rpc method has no documentation, generate it w/ inspect
f_args = inspect.getargspec(method).args
f_defaults = inspect.getargspec(method).defaults or []
Expand All @@ -259,11 +293,10 @@ def get_swagger_doc_post_arguments(cls, method_name):
arg_field = {"schema": model, "type": "string"}
method_field = {"method": method_name, "args": args}
fields["meta"] = schema_from_object(model_name, method_field)
print(fields["meta"])

return parameters, fields, description, method

safrs.LOGGER.critical("Shouldnt get here ({})".format(method_name))
safrs.log.critical("Shouldnt get here ({})".format(method_name))


def swagger_method_doc(cls, method_name, tags=None):
Expand All @@ -287,30 +320,34 @@ def swagger_doc_gen(func):

model_name = "{}_{}_{}".format("Invoke ", class_name, method_name)
param_model = SchemaClassFactory(model_name, {})
parameters, fields, description, method = get_swagger_doc_arguments(cls, method_name, http_method=func.__name__)

if func.__name__ == "get":
parameters = [
{
"name": "varargs",
"in": "query",
"description": "{} arguments".format(method_name),
"required": False,
"type": "string",
}
]
else:
# typically POST
parameters, fields, description, method = get_swagger_doc_post_arguments(cls, method_name)
"""if inspect.ismethod(method) and method.__self__ is cls:
# Mark classmethods: only these can be called when no {id} is given as parameter
# in the swagger ui
description += ' (classmethod)' """
if not parameters:
parameters = [
{
"name": "varargs",
"in": "query",
"description": "{} arguments".format(method_name),
"required": False,
"type": "string",
}
]
'''
param_model = SchemaClassFactory(model_name, fields)
parameters.append(
{"name": model_name, "in": "query", "description": description, "schema": param_model, "required": True}
)'''


else:
#
# Retrieve the swagger schemas for the documented_api_methods
# Retrieve the swagger schemas for the jsonapi_rpc methods
#
parameters, fields, description, method = get_swagger_doc_arguments(cls, method_name, http_method=func.__name__)
model_name = "{}_{}_{}".format(func.__name__, cls.__name__, method_name)
param_model = SchemaClassFactory(model_name, fields)

parameters.append(
{"name": model_name, "in": "body", "description": description, "schema": param_model, "required": True}
)
Expand Down Expand Up @@ -429,7 +466,7 @@ def swagger_doc_gen(func):
)
else:
# one of 'options', 'head', 'patch'
safrs.LOGGER.debug('no documentation for "%s" ', http_method)
safrs.log.debug('no documentation for "%s" ', http_method)

responses_str = {}
for k, v in responses.items():
Expand Down Expand Up @@ -563,7 +600,7 @@ def swagger_doc_gen(func):

else:
# one of 'options', 'head', 'patch'
safrs.LOGGER.info('no documentation for "%s" ', http_method)
safrs.log.info('no documentation for "%s" ', http_method)

if http_method in ("patch",):
# put_model, responses = child_class.get_swagger_doc(http_method)
Expand Down

0 comments on commit a612c0b

Please sign in to comment.