Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add category date #410

Merged
merged 4 commits into from
Jun 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/test-and-deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ jobs:
matrix:
python-version:
- 2.7
- 3.5
- 3.6
- 3.7
steps:
Expand Down
9 changes: 8 additions & 1 deletion scrunch/categories.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class Category(ReadOnly):
_MUTABLE_ATTRIBUTES = {'name', 'numeric_value', 'missing', 'selected', 'date'}
_IMMUTABLE_ATTRIBUTES = {'id'}
_ENTITY_ATTRIBUTES = _MUTABLE_ATTRIBUTES | _IMMUTABLE_ATTRIBUTES
_NULLABLE_ATTRIBUTES = {"date", "numeric_value", "selected"}

def __init__(self, variable_resource, category):
super(Category, self).__init__(variable_resource)
Expand All @@ -19,7 +20,13 @@ def __getattr__(self, item):
if item == 'selected':
# Default is False; not always present
return self._category.get('selected', False)
return self._category[item] # Has to exist
try:
return self._category[item]
except KeyError:
if item in self._NULLABLE_ATTRIBUTES:
return None
else:
raise # API returned an incomplete category? Error

# Attribute doesn't exist, must raise an AttributeError
raise AttributeError(
Expand Down
30 changes: 16 additions & 14 deletions scrunch/datasets.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
case_expr, download_file, shoji_entity_wrapper,
subvar_alias, validate_categories, shoji_catalog_wrapper,
get_else_case, else_case_not_selected, SELECTED_ID,
NOT_SELECTED_ID, NO_DATA_ID)
NOT_SELECTED_ID, NO_DATA_ID, valid_categorical_date)
from scrunch.order import DatasetVariablesOrder, ProjectDatasetsOrder
from scrunch.subentity import Deck, Filter, Multitable
from scrunch.variables import (combinations_from_map, combine_categories_expr,
Expand Down Expand Up @@ -3055,7 +3055,7 @@ def integrate(self):
self.resource.edit(derived=False)
self.dataset._reload_variables()

def add_category(self, id, name, numeric_value, missing=False, before_id=False):
def add_category(self, id, name, numeric_value, missing=False, date=None, before_id=False):
if self.resource.body['type'] not in CATEGORICAL_TYPES:
raise TypeError(
"Variable of type %s do not have categories"
Expand All @@ -3065,6 +3065,18 @@ def add_category(self, id, name, numeric_value, missing=False, before_id=False):
raise TypeError("Cannot add categories on derived variables. Re-derive with the appropriate expression")

categories = self.resource.body['categories']
category_data = {
'id': id,
'missing': missing,
'name': name,
'numeric_value': numeric_value,
}
if date is not None:
if not isinstance(date, six.string_types):
raise ValueError("Date must be a string")
if not valid_categorical_date(date):
raise ValueError("Date must conform to Y-m-d format")
category_data["date"] = date

if before_id:
# only accept int type
Expand All @@ -3079,21 +3091,11 @@ def add_category(self, id, name, numeric_value, missing=False, before_id=False):
new_categories = []
for category in categories:
if category['id'] == before_id:
new_categories.append({
'id': id,
'missing': missing,
'name': name,
'numeric_value': numeric_value,
})
new_categories.append(category_data)
new_categories.append(category)
categories = new_categories
else:
categories.append({
'id': id,
'missing': missing,
'name': name,
'numeric_value': numeric_value,
})
categories.append(category_data)

resp = self.resource.edit(categories=categories)
self._reload_variables()
Expand Down
23 changes: 23 additions & 0 deletions scrunch/helpers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import requests
import six
from datetime import datetime

if six.PY2: # pragma: no cover
from urlparse import urljoin
Expand Down Expand Up @@ -232,3 +233,25 @@ def shoji_catalog_wrapper(index, **kwargs):
}
payload.update(**kwargs)
return payload


def valid_categorical_date(date_str):
"""
Categories accept a `date` attribute that needs to be a valid ISO8601 date.
In order to keep dependencies reduced (no dateutil) and Python2x support,
we will support a limited set of simple date formats.
"""
valid_date_masks = [
"%Y",
"%Y-%m",
"%Y-%m-%d",
]
for mask in valid_date_masks:
try:
datetime.strptime(date_str, mask)
return True
except ValueError:
# Did not validate for this mask
continue
return False

30 changes: 30 additions & 0 deletions scrunch/tests/test_datasets.py
Original file line number Diff line number Diff line change
Expand Up @@ -1437,6 +1437,36 @@ def test_add_category(self):
var.add_category(2, 'New category', 2, before_id=9)
var.resource._edit.assert_called_with(categories=var.resource.body['categories'])

def test_add_category_date(self):
ds_mock = self._dataset_mock()
ds = BaseDataset(ds_mock)
var = ds['var4_alias']
var.resource.body['type'] = 'categorical'
categories = [
{"id": 1, "name": "Female", "missing": False, "numeric_value": 1},
{"id": 8, "name": "Male", "missing": False, "numeric_value": 8},
{"id": 9, "name": "No Data", "missing": True, "numeric_value": 9}
]
var.resource.body['categories'] = categories[:]

with pytest.raises(ValueError) as err:
var.add_category(2, 'New category', 2, date=object())
assert str(err.value) == "Date must be a string"

with pytest.raises(ValueError) as err:
var.add_category(2, 'New category', 2, date="invalid date")
assert str(err.value) == "Date must conform to Y-m-d format"

var.add_category(2, 'New category', 2, date="2021-12-12")
new_categories = categories[:] + [{
"id": 2,
"name": "New category",
"date": "2021-12-12",
"numeric_value": 2,
"missing": False
}]
var.resource._edit.assert_called_with(categories=new_categories)

def test_integrate_variables(self):
ds_mock = mock.MagicMock()
var_tuple = mock.MagicMock()
Expand Down
3 changes: 1 addition & 2 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
[tox]
envlist = py27,py34,py35,py36,py37,{py27,py36,py37}-pandas
envlist = py27,py34,py36,py37,{py27,py36,py37}-pandas
minversion = 2.4
skip_missing_interpreters = true

[gh-actions]
python =
2.7: py27,py27-pandas
3.4: py34
3.5: py35,py35-pandas
3.6: py36,py36-pandas
3.7: py37,py37-pandas

Expand Down