class DateHelperTest(TestCase): """Test the DateHelper.""" def setUp(self): """Test setup.""" self.date_helper = DateHelper() self.date_helper._now = datetime.datetime(1970, 1, 10, 12, 59, 59) def test_this_hour(self): """Test this_hour property.""" expected = datetime.datetime(1970, 1, 10, 12, 0, 0, 0) self.assertEqual(self.date_helper.this_hour, expected) def test_next_hour(self): """Test next_hour property.""" expected = datetime.datetime(1970, 1, 10, 13, 0, 0, 0) self.assertEqual(self.date_helper.next_hour, expected) def test_prev_hour(self): """Test previous_hour property.""" expected = datetime.datetime(1970, 1, 10, 11, 0, 0, 0) self.assertEqual(self.date_helper.previous_hour, expected) def test_today(self): """Test today property.""" expected = datetime.datetime(1970, 1, 10, 0, 0, 0, 0) self.assertEqual(self.date_helper.today, expected) def test_yesterday(self): """Test yesterday property.""" date_helper = DateHelper() date_helper._now = datetime.datetime(1970, 1, 1, 12, 59, 59) expected = datetime.datetime(1969, 12, 31, 0, 0, 0, 0) self.assertEqual(date_helper.yesterday, expected) def test_tomorrow(self): """Test tomorrow property.""" expected = datetime.datetime(1970, 1, 11, 0, 0, 0, 0) self.assertEqual(self.date_helper.tomorrow, expected) def test_this_month_start(self): """Test this_month_start property.""" expected = datetime.datetime(1970, 1, 1, 0, 0, 0, 0) self.assertEqual(self.date_helper.this_month_start, expected) def test_this_month_end(self): """Test this_month_end property.""" expected = datetime.datetime(1970, 1, 31, 0, 0, 0, 0) self.assertEqual(self.date_helper.this_month_end, expected) def test_next_month_start(self): """Test next_month_start property.""" expected = datetime.datetime(1970, 2, 1, 0, 0, 0, 0) self.assertEqual(self.date_helper.next_month_start, expected) def test_next_month_end(self): """Test next_month_end property.""" expected = datetime.datetime(1970, 2, 28, 0, 0, 0, 0) self.assertEqual(self.date_helper.next_month_end, expected) def test_last_month_start(self): """Test last_month_start property.""" expected = datetime.datetime(1969, 12, 1, 0, 0, 0, 0) self.assertEqual(self.date_helper.last_month_start, expected) def test_last_month_end(self): """Test last_month_end property.""" expected = datetime.datetime(1969, 12, 31, 0, 0, 0, 0) self.assertEqual(self.date_helper.last_month_end, expected) def test_next_month(self): """Test the next_month method.""" current_month = datetime.datetime.now().replace(microsecond=0, second=0, minute=0, hour=0, day=1) last_month = current_month - relativedelta(months=1) self.assertEqual(current_month, DateHelper().next_month(last_month)) def test_previous_month(self): """Test the previous_month method.""" current_month = datetime.datetime.now().replace(microsecond=0, second=0, minute=0, hour=0, day=1) last_month = current_month - relativedelta(months=1) self.assertEqual(last_month, DateHelper().previous_month(current_month)) def test_list_days(self): """Test the list_days method.""" first = datetime.datetime.now().replace(microsecond=0, second=0, minute=0, hour=0, day=1) second = first.replace(day=2) third = first.replace(day=3) expected = [first, second, third] self.assertEqual(self.date_helper.list_days(first, third), expected) def test_list_months(self): """Test the list_months method.""" first = datetime.datetime(1970, 1, 1) second = datetime.datetime(1970, 2, 1) third = datetime.datetime(1970, 3, 1) expected = [first, second, third] self.assertEqual(self.date_helper.list_months(first, third), expected) def test_n_days_ago(self): """Test the n_days_ago method.""" delta_day = datetime.timedelta(days=1) today = timezone.now().replace(microsecond=0, second=0, minute=0, hour=0) two_days_ago = (today - delta_day) - delta_day self.assertEqual(self.date_helper.n_days_ago(today, 2), two_days_ago) def test_month_start(self): """Test month start method.""" today = self.date_helper.today expected = datetime.datetime(1970, 1, 1, 0, 0, 0, 0) self.assertEqual(self.date_helper.month_start(today), expected) def test_month_end(self): """Test month end method.""" today = self.date_helper.today expected = datetime.datetime(1970, 1, 31, 0, 0, 0, 0) self.assertEqual(self.date_helper.month_end(today), expected) today_date = today.date() expected = datetime.date(1970, 1, 31) self.assertEqual(self.date_helper.month_end(today_date), expected) def test_midnight(self): """Test midnight property.""" expected = datetime.time(0, 0, 0, 0) self.assertEqual(self.date_helper.midnight, expected)
class QueryHandler: """Handles report queries and responses.""" def __init__(self, parameters): """Establish query handler. Args: parameters (QueryParameters): parameter object for query """ LOG.debug(f"Query Params: {parameters}") self.dh = DateHelper() parameters = self.filter_to_order_by(parameters) self.tenant = parameters.tenant self.access = parameters.access self.parameters = parameters self.default_ordering = self._mapper._report_type_map.get( "default_ordering") self.time_interval = [] self._max_rank = 0 self.time_scope_units = self.parameters.get_filter("time_scope_units") if self.parameters.get_filter("time_scope_value"): self.time_scope_value = int( self.parameters.get_filter("time_scope_value")) # self.time_order = parameters["date"] # self.start_datetime = parameters["start_date"] # self.end_datetime = parameters["end_date"] for param, attr in [("start_date", "start_datetime"), ("end_date", "end_datetime")]: p = self.parameters.get(param) if p: setattr( self, attr, datetime.datetime.combine(parser.parse(p).date(), self.dh.midnight, tzinfo=UTC)) else: setattr(self, attr, None) if self.resolution == "monthly": self.date_to_string = lambda dt: dt.strftime("%Y-%m") self.string_to_date = lambda dt: datetime.datetime.strptime( dt, "%Y-%m").date() self.date_trunc = TruncMonthString self.gen_time_interval = DateHelper().list_months else: self.date_to_string = lambda dt: dt.strftime("%Y-%m-%d") self.string_to_date = lambda dt: datetime.datetime.strptime( dt, "%Y-%m-%d").date() self.date_trunc = TruncDayString self.gen_time_interval = DateHelper().list_days if not (self.start_datetime or self.end_datetime): self._get_timeframe() self._create_time_interval() # FIXME: move this to a standalone utility function. @staticmethod def has_wildcard(in_list): """Check if list has wildcard. Args: in_list (List[String]): List of strings to check for wildcard Return: (Boolean): if wildcard is present in list """ if isinstance(in_list, bool): return False if not in_list: return False return any(WILDCARD == item for item in in_list) @property def order(self): """Extract order_by parameter and apply ordering to the appropriate field. Returns: (String): Ordering value. Default is '-total' Example: `order_by[total]=asc` returns `total` `order_by[total]=desc` returns `-total` """ order_map = {"asc": "", "desc": "-"} order = [] order_by = self.parameters.get("order_by", self.default_ordering) for order_field, order_direction in order_by.items(): if order_direction not in order_map and order_field == "date": # We've overloaded date to hold a specific date, not asc/desc order.append(order_direction) else: order.append(f"{order_map[order_direction]}{order_field}") return order @property def order_field(self): """Order-by field name. The default is 'total' """ order_by = self.parameters.get("order_by", self.default_ordering) return list(order_by.keys()).pop() @property def order_direction(self): """Order-by orientation value. Returns: (str) 'asc' or 'desc'; default is 'desc' """ order_by = self.parameters.get("order_by", self.default_ordering) return list(order_by.values()).pop() @property def max_rank(self): """Return the max rank of a ranked list.""" return self._max_rank @max_rank.setter def max_rank(self, max_rank): """Max rank setter.""" self._max_rank = max_rank @property def resolution(self): """Extract resolution or provide default. Returns: (String): The value of how data will be sliced. """ return self.parameters.get_filter("resolution", default="daily") def check_query_params(self, key, in_key): """Test if query parameters has a given key and key within it. Args: key (String): key to check in query parameters in_key (String): key to check if key is found in query parameters Returns: (Boolean): True if they keys given appear in given query parameters. """ return self.parameters and key in self.parameters and in_key in self.parameters.get( key) # noqa: W504 def get_time_scope_units(self): """Extract time scope units or provide default. Returns: (String): The value of how data will be sliced. """ if self.time_scope_units: return self.time_scope_units time_scope_units = self.parameters.get_filter("time_scope_units", default="day") self.time_scope_units = time_scope_units return self.time_scope_units def get_time_scope_value(self): """Extract time scope value or provide default. Returns: (Integer): time relative value providing query scope """ if self.time_scope_value: return self.time_scope_value time_scope_value = self.parameters.get_filter("time_scope_value", default=-10) self.time_scope_value = int(time_scope_value) return self.time_scope_value def _get_timeframe(self): """Obtain timeframe start and end dates. Returns: (DateTime): start datetime for query filter (DateTime): end datetime for query filter """ time_scope_value = self.get_time_scope_value() time_scope_units = self.get_time_scope_units() start = None end = None if time_scope_units == "month": if time_scope_value == -1: # get current month start = self.dh.this_month_start end = self.dh.today elif time_scope_value == -3: start = self.dh.relative_month_start(-2) end = self.dh.month_end(start) else: # get previous month start = self.dh.last_month_start end = self.dh.last_month_end else: if time_scope_value == -10: # get last 10 days start = self.dh.n_days_ago(self.dh.this_hour, 9) end = self.dh.this_hour elif time_scope_value == -90: start = self.dh.n_days_ago(self.dh.this_hour, 89) end = self.dh.this_hour else: # get last 30 days start = self.dh.n_days_ago(self.dh.this_hour, 29) end = self.dh.this_hour self.start_datetime = start self.end_datetime = end return (self.start_datetime, self.end_datetime, self.time_interval) def _create_time_interval(self): """Create list of date times in interval. Returns: (List[DateTime]): List of all interval slices by resolution """ self.time_interval = sorted( self.gen_time_interval(self.start_datetime, self.end_datetime)) return self.time_interval def _get_date_delta(self): """Return a time delta.""" if self.time_scope_value in [-1, -2, -3]: date_delta = relativedelta.relativedelta( months=abs(self.time_scope_value)) elif self.time_scope_value in (-90, -30, -10): date_delta = datetime.timedelta(days=abs(self.time_scope_value)) else: date_delta = datetime.timedelta(days=10) return date_delta def _get_time_based_filters(self, delta=False): if delta: date_delta = self._get_date_delta() start = self.start_datetime - date_delta end = self.end_datetime - date_delta else: start = self.start_datetime end = self.end_datetime start_filter = QueryFilter(field="usage_start", operation="gte", parameter=start.date()) end_filter = QueryFilter(field="usage_start", operation="lte", parameter=end.date()) return start_filter, end_filter def _get_filter(self, delta=False): """Create dictionary for filter parameters. Args: delta (Boolean): Construct timeframe for delta Returns: (Dict): query filter dictionary """ filters = QueryFilterCollection() # add time constraint filters start_filter, end_filter = self._get_time_based_filters(delta) filters.add(query_filter=start_filter) filters.add(query_filter=end_filter) return filters def _get_gcp_filter(self, delta=False): """Create dictionary for filter parameters for GCP. For the gcp filters when the time scope is -1 or -2 we remove the usage_start & usage_end filters and only use the invoice month. Args: delta (Boolean): Construct timeframe for delta Returns: (Dict): query filter dictionary """ filters = QueryFilterCollection() if delta: date_delta = self._get_date_delta() start = self.start_datetime - date_delta end = self.end_datetime - date_delta else: start = self.start_datetime end = self.end_datetime start_filter = QueryFilter(field="usage_start", operation="gte", parameter=start.date()) end_filter = QueryFilter(field="usage_start", operation="lte", parameter=end.date()) invoice_months = self.dh.gcp_find_invoice_months_in_date_range( start.date(), end.date()) invoice_filter = QueryFilter(field="invoice_month", operation="in", parameter=invoice_months) filters.add(invoice_filter) if self.parameters.get_filter( "time_scope_value") and self.time_scope_value in [-1, -2]: # we don't add the time filters to time scopes -1 or -2 unless they are using delta. if delta: filters.add(query_filter=start_filter) filters.add(query_filter=end_filter) else: filters.add(query_filter=start_filter) filters.add(query_filter=end_filter) return filters def filter_to_order_by(self, parameters): # noqa: C901 """Remove group_by[NAME]=* and replace it with group_by[NAME]=X. The parameters object contains a list of filters and a list of group_bys. For example, if the parameters object contained the following: group_by[X] = Y group_by[Z] = * # removes this line filter[Z] = L filter[X] = Y The returned parameters object would contain lists that look like this: group_by[X] = Y group_by[Z] = L # adds this line filter[Z] = L filter[X] = Y Thereby removing the star when there is a filter provided. Args: parameters (QueryParameters): The parameters object Returns: parameters (QueryParameters): The parameters object """ # find if there is a filter[key]=value that matches this group_by[key]=value for key, value in parameters.parameters.get("group_by", {}).items(): if self.has_wildcard(value): filter_value = parameters.parameters.get("filter", {}).get(key) if filter_value: parameters.parameters["group_by"][key] = filter_value return parameters def set_access_filters(self, access, filt, filters): """ Sets the access filters to ensure RBAC restrictions given the users access, the current filter and the filter collection Args: access (list) the list containing the users relevant access filt (list or dict) contains the filters that need filters (QueryFilterCollection) the filter collection to add the new filters to returns: None """ for _filt in filt if isinstance(filt, list) else [filt]: check_field_type = None try: if hasattr(self, "query_table"): # Reports APIs check_field_type = self.query_table._meta.get_field( _filt.get("field", "")).get_internal_type() elif hasattr(self, "data_sources"): # Tags APIs check_field_type = ( self.data_sources[0].get("db_table")._meta.get_field( _filt.get("field", "")).get_internal_type()) except FieldDoesNotExist: pass _filt[ "operation"] = "contains" if check_field_type == "ArrayField" else "in" q_filter = QueryFilter(parameter=access, **_filt) filters.add(q_filter)
def end_date(self): """Return an end date.""" dh = DateHelper() return dh.month_end(self.start_date)