Skip to content

Commit

Permalink
Time format without seconds added (venmo#17)
Browse files Browse the repository at this point in the history
* Time format without seconds added

* Readme update
  • Loading branch information
ipeluffo authored Dec 12, 2017
1 parent 0c1dd31 commit b016683
Show file tree
Hide file tree
Showing 3 changed files with 212 additions and 41 deletions.
120 changes: 80 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
}
}
```
Expand Down Expand Up @@ -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.

Expand All @@ -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:

Expand All @@ -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:

Expand All @@ -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:

Expand All @@ -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:

Expand All @@ -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:

Expand All @@ -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

Expand Down
7 changes: 7 additions & 0 deletions business_rules/operators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand All @@ -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))

Expand Down
126 changes: 125 additions & 1 deletion tests/operators/test_operators.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -12,6 +12,7 @@
SelectMultipleType,
BaseType,
DateTimeType,
TimeType,
)
from tests import TestCase

Expand Down Expand Up @@ -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))

0 comments on commit b016683

Please sign in to comment.