From b016683f8df4008cf842ce43e764de96577a1ab8 Mon Sep 17 00:00:00 2001 From: Ignacio Peluffo Date: Tue, 12 Dec 2017 09:43:27 +0000 Subject: [PATCH] Time format without seconds added (#17) * Time format without seconds added * Readme update --- README.md | 120 ++++++++++++++++++---------- business_rules/operators.py | 7 ++ tests/operators/test_operators.py | 126 +++++++++++++++++++++++++++++- 3 files changed, 212 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 1f33edac..cf0fd64d 100644 --- a/README.md +++ b/README.md @@ -196,52 +196,93 @@ export_rule_data(ProductVariables, ProductActions) that returns ```json -{"variables": [ - { "name": "expiration_days", +{ + "variables": [ + { + "name": "expiration_days", "label": "Days until expiration", "field_type": "numeric", "options": [], - "params": []}, - { "name": "current_month", + "params": [] + }, + { + "name": "current_month", "label": "Current Month", "field_type": "string", "options": [], - "params": []}, - { "name": "goes_well_with", + "params": [] + }, + { + "name": "goes_well_with", "label": "Goes Well With", "field_type": "select", - "options": ["Eggnog", "Cookies", "Beef Jerkey"], - "params": []}, - { "name": "orders_sold_in_last_x_days", + "options": [ + "Eggnog", + "Cookies", + "Beef Jerkey" + ], + "params": [] + }, + { + "name": "orders_sold_in_last_x_days", "label": "Orders Sold In Last X Days", "field_type": "numeric", "options": [], - "params": [{"field_type": "numeric", "name": "days", "label": "Days"}]} + "params": [ + { + "field_type": "numeric", + "name": "days", + "label": "Days" + } + ] + } ], "actions": [ - { "name": "put_on_sale", + { + "name": "put_on_sale", "label": "Put On Sale", - "params": {"sale_percentage": "numeric"}}, - { "name": "order_more", + "params": { + "sale_percentage": "numeric" + } + }, + { + "name": "order_more", "label": "Order More", - "params": {"number_to_order": "numeric"}} + "params": { + "number_to_order": "numeric" + } + } ], "variable_type_operators": { - "numeric": [ {"name": "equal_to", - "label": "Equal To", - "input_type": "numeric"}, - {"name": "less_than", - "label": "Less Than", - "input_type": "numeric"}, - {"name": "greater_than", - "label": "Greater Than", - "input_type": "numeric"}], - "string": [ { "name": "equal_to", - "label": "Equal To", - "input_type": "text"}, - { "name": "non_empty", - "label": "Non Empty", - "input_type": "none"}] + "numeric": [ + { + "name": "equal_to", + "label": "Equal To", + "input_type": "numeric" + }, + { + "name": "less_than", + "label": "Less Than", + "input_type": "numeric" + }, + { + "name": "greater_than", + "label": "Greater Than", + "input_type": "numeric" + } + ], + "string": [ + { + "name": "equal_to", + "label": "Equal To", + "input_type": "text" + }, + { + "name": "non_empty", + "label": "Non Empty", + "input_type": "none" + } + ] } } ``` @@ -270,7 +311,7 @@ for product in Products.objects.all(): ## API -#### Variable Types and Decorators: +### Variable Types and Decorators: The type represents the type of the value that will be returned for the variable and is necessary since there are different available comparison operators for different types, and the front-end that's generating the rules needs to know which operators are available. @@ -282,7 +323,7 @@ variable function. The available types and decorators are: -**numeric** - an integer, float, or python Decimal. +#### `numeric` - an integer, float, or python Decimal. `@numeric_rule_variable` operators: @@ -294,7 +335,7 @@ The available types and decorators are: Note: to compare floating point equality we just check that the difference is less than some small epsilon -**string** - a python bytestring or unicode string. +#### `string` - a python bytestring or unicode string. `@string_rule_variable` operators: @@ -305,21 +346,21 @@ Note: to compare floating point equality we just check that the difference is le * `matches_regex` * `non_empty` -**boolean** - a True or False value. +#### `boolean` - a True or False value. `@boolean_rule_variable` operators: * `is_true` * `is_false` -**select** - a set of values, where the threshold will be a single item. +#### `select` - a set of values, where the threshold will be a single item. `@select_rule_variable` operators: * `contains` * `does_not_contain` -**select_multiple** - a set of values, where the threshold will be a set of items. +#### `select_multiple` - a set of values, where the threshold will be a set of items. `@select_multiple_rule_variable` operators: @@ -329,7 +370,7 @@ Note: to compare floating point equality we just check that the difference is le * `shares_exactly_one_element_with` * `shares_no_elements_with` -**datetime** - a Timestamp value +#### `datetime` - a Timestamp value A rule variable accepts the following types of values: @@ -354,17 +395,19 @@ A variable can return the following types of values: * `after_than_or_equal_to` -**time** - a Time value +#### `time` - a Time value A rule variable accepts the following types of values: * string with format `%H:%M:%S` +* string with format `%H:%M` A variable can return the following types of values: * datetime * time * string with format `%H:%M:%S` +* string with format `%H:%M` `@time_rule_variable` operators: @@ -374,9 +417,6 @@ A variable can return the following types of values: * `after_than` * `after_than_or_equal_to` -### Returning data to your client - - ## Contributing diff --git a/business_rules/operators.py b/business_rules/operators.py index ea198e3b..e8860983 100644 --- a/business_rules/operators.py +++ b/business_rules/operators.py @@ -314,6 +314,7 @@ def before_than_or_equal_to(self, other_datetime): class TimeType(BaseType): name = "time" TIME_FORMAT = '%H:%M:%S' + TIME_FORMAT_NO_SECONDS = '%H:%M' def _assert_valid_value_and_cast(self, value): """ @@ -331,6 +332,12 @@ def _assert_valid_value_and_cast(self, value): try: dt = datetime.strptime(value, self.TIME_FORMAT) return time(dt.hour, dt.minute, dt.second) + except (ValueError, TypeError): + pass + + try: + dt = datetime.strptime(value, self.TIME_FORMAT_NO_SECONDS) + return time(dt.hour, dt.minute, dt.second) except (ValueError, TypeError): raise AssertionError("{0} is not a valid time type.".format(value)) diff --git a/tests/operators/test_operators.py b/tests/operators/test_operators.py index e0a1fc23..fb796563 100644 --- a/tests/operators/test_operators.py +++ b/tests/operators/test_operators.py @@ -1,5 +1,5 @@ import sys -from datetime import datetime, timedelta, date +from datetime import datetime, timedelta, date, time from decimal import Decimal import pytz @@ -12,6 +12,7 @@ SelectMultipleType, BaseType, DateTimeType, + TimeType, ) from tests import TestCase @@ -357,3 +358,126 @@ def test_datetime_before_than_or_equal_to(self): self.assertTrue( self.datetime_type_date.before_than_or_equal_to(self.TEST_DATETIME_UTC_OBJ + timedelta(seconds=1)) ) + + +class TimeOperatorTests(TestCase): + def setUp(self): + super(TimeOperatorTests, self).setUp() + self.TEST_HOUR = 13 + self.TEST_MINUTE = 55 + self.TEST_SECOND = 00 + self.TEST_TIME = '{hour}:{minute}:{second}'.format( + hour=self.TEST_HOUR, minute=self.TEST_MINUTE, second=self.TEST_SECOND + ) + self.TEST_TIME_NO_SECONDS = '{hour}:{minute}'.format(hour=self.TEST_HOUR, minute=self.TEST_MINUTE) + self.TEST_TIME_OBJ = time(self.TEST_HOUR, self.TEST_MINUTE, self.TEST_SECOND) + + self.time_type_time = TimeType(self.TEST_TIME) + self.time_type_time_no_seconds = TimeType(self.TEST_TIME_NO_SECONDS) + self.time_type_time_obj = TimeType(self.TEST_TIME_OBJ) + + def test_instantiate(self): + err_string = "foo is not a valid time type" + with self.assertRaisesRegexp(AssertionError, err_string): + TimeType("foo") + + def test_time_type_validates_and_cast_time(self): + result = TimeType(self.TEST_TIME) + self.assertTrue(isinstance(result.value, time)) + + result = TimeType(self.TEST_TIME_NO_SECONDS) + self.assertTrue(isinstance(result.value, time)) + + result = TimeType(self.TEST_TIME_OBJ) + self.assertTrue(isinstance(result.value, time)) + + def test_time_equal_to(self): + self.assertTrue(self.time_type_time_no_seconds.equal_to(self.TEST_TIME)) + self.assertTrue(self.time_type_time_no_seconds.equal_to(self.TEST_TIME_OBJ)) + + self.assertTrue(self.time_type_time_obj.equal_to(self.TEST_TIME)) + self.assertTrue(self.time_type_time_obj.equal_to(self.TEST_TIME_OBJ)) + + self.assertTrue(self.time_type_time.equal_to(self.TEST_TIME_NO_SECONDS)) + + def test_other_value_not_time(self): + error_string = "2016-10 is not a valid time type" + with self.assertRaisesRegexp(AssertionError, error_string): + TimeType(self.TEST_TIME_NO_SECONDS).equal_to("2016-10") + + def time_after_than_asserts(self, time_type): + # type: (TimeType) -> None + self.assertFalse(time_type.after_than(self.TEST_TIME)) + self.assertFalse(time_type.after_than(self.TEST_TIME_OBJ)) + + test_time = time(self.TEST_TIME_OBJ.hour, self.TEST_TIME_OBJ.minute - 1, 59) + self.assertTrue(time_type.after_than(test_time)) + + test_time = time(self.TEST_TIME_OBJ.hour, self.TEST_TIME_OBJ.minute, self.TEST_TIME_OBJ.second + 1) + self.assertFalse(time_type.after_than(test_time)) + + def test_time_after_than(self): + self.time_after_than_asserts(self.time_type_time_no_seconds) + self.time_after_than_asserts(self.time_type_time_obj) + + self.assertFalse(self.time_type_time.after_than(self.TEST_TIME_NO_SECONDS)) + + test_time = time(self.TEST_TIME_OBJ.hour, self.TEST_TIME_OBJ.minute, self.TEST_TIME_OBJ.second + 1) + self.assertFalse(self.time_type_time.after_than(test_time)) + + def time_after_than_or_equal_to_asserts(self, time_type): + # type: (TimeType) -> None + self.assertTrue(time_type.after_than_or_equal_to(self.TEST_TIME)) + self.assertTrue(time_type.after_than_or_equal_to(self.TEST_TIME_OBJ)) + + test_time = time(self.TEST_TIME_OBJ.hour, self.TEST_TIME_OBJ.minute - 1, 59) + self.assertTrue(time_type.after_than_or_equal_to(test_time)) + + test_time = time(self.TEST_TIME_OBJ.hour, self.TEST_TIME_OBJ.minute, self.TEST_TIME_OBJ.second + 1) + self.assertFalse(time_type.after_than_or_equal_to(test_time)) + + def test_time_after_than_or_equal_to(self): + self.assertTrue(self.time_type_time.after_than_or_equal_to(self.TEST_TIME_NO_SECONDS)) + + self.time_after_than_or_equal_to_asserts(self.time_type_time_no_seconds) + self.time_after_than_or_equal_to_asserts(self.time_type_time_obj) + + def time_before_than_asserts(self, time_type): + # type: (TimeType) -> None + self.assertFalse(time_type.before_than(self.TEST_TIME)) + self.assertFalse(time_type.before_than(self.TEST_TIME_OBJ)) + + test_time = time(self.TEST_TIME_OBJ.hour, self.TEST_TIME_OBJ.minute - 1, 59) + self.assertFalse(time_type.before_than(test_time)) + + test_time = time(self.TEST_TIME_OBJ.hour, self.TEST_TIME_OBJ.minute, self.TEST_TIME_OBJ.second + 1) + self.assertTrue(time_type.before_than(test_time)) + + def test_time_before_than(self): + self.time_before_than_asserts(self.time_type_time_no_seconds) + self.time_before_than_asserts(self.time_type_time_obj) + + self.assertFalse(self.time_type_time.before_than(self.TEST_TIME_NO_SECONDS)) + + test_time = time(self.TEST_TIME_OBJ.hour, self.TEST_TIME_OBJ.minute, self.TEST_TIME_OBJ.second + 1) + self.assertTrue(self.time_type_time.before_than(test_time)) + + def time_before_than_or_equal_to_asserts(self, time_type): + # type: (TimeType) -> None + self.assertTrue(time_type.before_than_or_equal_to(self.TEST_TIME)) + self.assertTrue(time_type.before_than_or_equal_to(self.TEST_TIME_OBJ)) + + test_time = time(self.TEST_TIME_OBJ.hour, self.TEST_TIME_OBJ.minute - 1, 59) + self.assertFalse(time_type.before_than_or_equal_to(test_time)) + + test_time = time(self.TEST_TIME_OBJ.hour, self.TEST_TIME_OBJ.minute, self.TEST_TIME_OBJ.second + 1) + self.assertTrue(time_type.before_than_or_equal_to(test_time)) + + def test_time_before_than_or_equal_to(self): + self.time_before_than_or_equal_to_asserts(self.time_type_time_no_seconds) + self.time_before_than_or_equal_to_asserts(self.time_type_time_obj) + + self.assertTrue(self.time_type_time.before_than_or_equal_to(self.TEST_TIME_NO_SECONDS)) + + test_time = time(self.TEST_TIME_OBJ.hour, self.TEST_TIME_OBJ.minute, self.TEST_TIME_OBJ.second + 1) + self.assertTrue(self.time_type_time.before_than_or_equal_to(test_time))