summaryrefslogtreecommitdiff
path: root/gae/webapp/src/endpoint/endpoint_base.py
diff options
context:
space:
mode:
Diffstat (limited to 'gae/webapp/src/endpoint/endpoint_base.py')
-rw-r--r--gae/webapp/src/endpoint/endpoint_base.py330
1 files changed, 330 insertions, 0 deletions
diff --git a/gae/webapp/src/endpoint/endpoint_base.py b/gae/webapp/src/endpoint/endpoint_base.py
new file mode 100644
index 0000000..d0dddd5
--- /dev/null
+++ b/gae/webapp/src/endpoint/endpoint_base.py
@@ -0,0 +1,330 @@
+# Copyright 2018 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import datetime
+import inspect
+import logging
+import json
+
+import endpoints
+from protorpc import messages
+from protorpc import remote
+from google.appengine.ext import ndb
+
+from webapp.src import vtslab_status as Status
+from webapp.src.proto import model
+
+MAX_QUERY_SIZE = 1000
+
+COUNT_REQUEST_RESOURCE = endpoints.ResourceContainer(model.CountRequestMessage)
+GET_REQUEST_RESOURCE = endpoints.ResourceContainer(model.GetRequestMessage)
+
+
+class EndpointBase(remote.Service):
+ """A base class for endpoint implementation."""
+
+ def GetCommonAttributes(self, resource, reference):
+ """Gets a list of common attribute names.
+
+ This method finds the attributes assigned in 'resource' instance, and
+ filters out if the attributes are not a member of 'reference' class.
+
+ Args:
+ resource: either a protorpc.messages.Message instance,
+ or a ndb.Model instance.
+ reference: either a protorpc.messages.Message class,
+ or a ndb.Model class.
+
+ Returns:
+ a list of string, attribute names exist on resource and reference.
+
+ Raises:
+ ValueError if resource or reference is not supported class.
+ """
+ # check resource type and absorb list of assigned attributes.
+ resource_attrs = self.GetAttributes(resource, assigned_only=True)
+ reference_attrs = self.GetAttributes(reference)
+ return [x for x in resource_attrs if x in reference_attrs]
+
+ def GetAttributes(self, value, assigned_only=False):
+ """Gets a list of attributes.
+
+ Args:
+ value: a class instance or a class itself.
+ assigned_only: True to get only assigned attributes when value is
+ an instance, False to get all attributes.
+
+ Raises:
+ ValueError if value is not supported class.
+ """
+ attrs = []
+ if inspect.isclass(value):
+ if assigned_only:
+ logging.warning(
+ "Please use a class instance for 'resource' argument.")
+
+ if (issubclass(value, messages.Message)
+ or issubclass(value, ndb.Model)):
+ attrs = [
+ x[0] for x in value.__dict__.items()
+ if not x[0].startswith("_")
+ ]
+ else:
+ raise ValueError("Only protorpc.messages.Message or ndb.Model "
+ "class are supported.")
+ else:
+ if isinstance(value, messages.Message):
+ attrs = [
+ x.name for x in value.all_fields()
+ if not assigned_only or (
+ value.get_assigned_value(x.name) not in [None, []])
+ ]
+ elif isinstance(value, ndb.Model):
+ attrs = [
+ x for x in list(value.to_dict())
+ if not assigned_only or (
+ getattr(value, x, None) not in [None, []])
+ ]
+ else:
+ raise ValueError("Only protorpc.messages.Message or ndb.Model "
+ "class are supported.")
+
+ return attrs
+
+ def Count(self, metaclass, filters=None):
+ """Counts entities from datastore with options.
+
+ Args:
+ metaclass: a metaclass for ndb model.
+ filters: a list of tuples. Each tuple consists of three values:
+ key, method, and value.
+
+ Returns:
+ a number of entities.
+ """
+ query, _ = self.CreateQueryFilter(metaclass=metaclass, filters=filters)
+ return query.count()
+
+ def Fetch(self,
+ metaclass,
+ size,
+ offset=0,
+ filters=None,
+ sort_key="",
+ direction="asc"):
+ """Fetches entities from datastore with options.
+
+ Args:
+ metaclass: a metaclass for ndb model.
+ size: an integer, max number of entities to fetch at once.
+ offset: an integer, number of query results to skip.
+ filters: a list of filter tuple, a form of (key: string,
+ method: integer, value: string).
+ sort_key: a string, key name to sort by.
+ direction: a string, "asc" for ascending order and "desc" for
+ descending order.
+
+ Returns:
+ a list of fetched entities.
+ a boolean, True if there is next page or False if not.
+ """
+ query, empty_repeated_field = self.CreateQueryFilter(
+ metaclass=metaclass, filters=filters)
+ sorted_query = self.SortQuery(
+ query=query,
+ metaclass=metaclass,
+ sort_key=sort_key,
+ direction=direction)
+
+ if size:
+ entities, _, more = sorted_query.fetch_page(
+ page_size=size, offset=offset)
+ else:
+ entities = sorted_query.fetch()
+ more = False
+
+ if empty_repeated_field:
+ entities = [
+ x for x in entities
+ if all([not getattr(x, attr) for attr in empty_repeated_field])
+ ]
+
+ return entities, more
+
+ def CreateQueryFilter(self, metaclass, filters):
+ """Creates a query with the given filters.
+
+ Args:
+ metaclass: a metaclass for ndb model.
+ filters: a list of tuples. Each tuple consists of three values:
+ key, method, and value.
+
+ Returns:
+ a filtered query for the given metaclass.
+ a list of strings that failed to create the query due to its empty
+ value for the repeated property.
+ """
+ empty_repeated_field = []
+ query = metaclass.query()
+ if not filters:
+ return query, empty_repeated_field
+
+ for _filter in filters:
+ property_key = _filter["key"]
+ method = _filter["method"]
+ value = _filter["value"]
+ if type(value) is str or type(value) is unicode:
+ if isinstance(metaclass._properties[property_key],
+ ndb.BooleanProperty):
+ value = value.lower() in ("yes", "true", "1")
+ elif isinstance(metaclass._properties[property_key],
+ ndb.IntegerProperty):
+ value = int(value)
+ if metaclass._properties[property_key]._repeated:
+ if value:
+ value = [value]
+ if method == Status.FILTER_METHOD[Status.FILTER_Has]:
+ query = query.filter(
+ getattr(metaclass, property_key).IN(value))
+ else:
+ logging.warning(
+ "You cannot compare repeated "
+ "properties except 'IN(has)' operation.")
+ else:
+ logging.debug("Empty repeated list cannot be queried.")
+ empty_repeated_field.append(value)
+ elif isinstance(metaclass._properties[property_key],
+ ndb.DateTimeProperty):
+ if method == Status.FILTER_METHOD[Status.FILTER_LessThan]:
+ query = query.filter(
+ getattr(metaclass, property_key) < datetime.datetime.
+ now() - datetime.timedelta(hours=int(value)))
+ elif method == Status.FILTER_METHOD[Status.FILTER_GreaterThan]:
+ query = query.filter(
+ getattr(metaclass, property_key) > datetime.datetime.
+ now() - datetime.timedelta(hours=int(value)))
+ else:
+ logging.debug("DateTimeProperty only allows <=(less than) "
+ "and >=(greater than) operation.")
+ else:
+ if method == Status.FILTER_METHOD[Status.FILTER_EqualTo]:
+ query = query.filter(
+ getattr(metaclass, property_key) == value)
+ elif method == Status.FILTER_METHOD[Status.FILTER_LessThan]:
+ query = query.filter(
+ getattr(metaclass, property_key) < value)
+ elif method == Status.FILTER_METHOD[Status.FILTER_GreaterThan]:
+ query = query.filter(
+ getattr(metaclass, property_key) > value)
+ elif method == Status.FILTER_METHOD[
+ Status.FILTER_LessThanOrEqualTo]:
+ query = query.filter(
+ getattr(metaclass, property_key) <= value)
+ elif method == Status.FILTER_METHOD[
+ Status.FILTER_GreaterThanOrEqualTo]:
+ query = query.filter(
+ getattr(metaclass, property_key) >= value)
+ elif method == Status.FILTER_METHOD[Status.FILTER_NotEqualTo]:
+ query = query.filter(
+ getattr(metaclass, property_key) != value).order(
+ getattr(metaclass, property_key), metaclass.key)
+ elif method == Status.FILTER_METHOD[Status.FILTER_Has]:
+ query = query.filter(
+ getattr(metaclass, property_key).IN(value)).order(
+ getattr(metaclass, property_key), metaclass.key)
+ else:
+ logging.warning(
+ "{} is not supported filter method.".format(method))
+ return query, empty_repeated_field
+
+ def SortQuery(self, query, metaclass, sort_key, direction):
+ """Sorts the given query with sort_key and direction.
+
+ Args:
+ query: a ndb query to sort.
+ metaclass: a metaclass for ndb model.
+ sort_key: a string, key name to sort by.
+ direction: a string, "asc" for ascending order and "desc" for
+ descending order.
+ """
+ if sort_key:
+ if direction == "desc":
+ query = query.order(-getattr(metaclass, sort_key))
+ else:
+ query = query.order(getattr(metaclass, sort_key))
+
+ return query
+
+ def CreateFilterList(self, filter_string, metaclass):
+ """Creates a list of filters.
+
+ Args:
+ filter_string: a string, stringified JSON which contains 'key',
+ 'method', 'value' to build filter information.
+ metaclass: a metaclass for ndb model.
+
+ Returns:
+ a list of tuples where each tuple consists of three values:
+ key, method, and value.
+ """
+ model_properties = self.GetAttributes(metaclass)
+ filters = []
+ if filter_string:
+ filters = json.loads(filter_string)
+ for _filter in filters:
+ if _filter["key"] not in model_properties:
+ filters.remove(_filter)
+ return filters
+
+ def Get(self, request, metaclass, message):
+ """Handles a request through /get endpoints API to retrieves entities.
+
+ Args:
+ request: a request body message received through /get API.
+ metaclass: a metaclass for ndb model. This method will fetch the
+ 'metaclass' type of model from datastore.
+ message: a Protocol RPC message class. Fetched entities will be
+ converted to this message class instances.
+
+ Returns:
+ a list of fetched entities.
+ a boolean, True if there is next page or False if not.
+ """
+ size = request.size if request.size else MAX_QUERY_SIZE
+ offset = request.offset if request.offset else 0
+
+ filters = self.CreateFilterList(
+ filter_string=request.filter, metaclass=metaclass)
+
+ entities, more = self.Fetch(
+ metaclass=metaclass,
+ size=size,
+ filters=filters,
+ offset=offset,
+ sort_key=request.sort,
+ direction=request.direction,
+ )
+
+ return_list = []
+ for entity in entities:
+ entity_dict = {}
+ assigned_attributes = self.GetCommonAttributes(
+ resource=entity, reference=message)
+ for attr in assigned_attributes:
+ entity_dict[attr] = getattr(entity, attr, None)
+ if hasattr(message, "urlsafe_key"):
+ entity_dict["urlsafe_key"] = entity.key.urlsafe()
+ return_list.append(entity_dict)
+
+ return return_list, more