diff options
Diffstat (limited to 'gae/webapp/src/endpoint/endpoint_base.py')
-rw-r--r-- | gae/webapp/src/endpoint/endpoint_base.py | 330 |
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 |