blob: d0dddd5629fcd0cdea2079a47b180873255150f4 [file] [log] [blame]
# 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