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

Create a simple computation endpoint #528

Merged
merged 50 commits into from
Jun 22, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
a97353d
Handle some bad request cases
fpagnoux Jun 16, 2017
4955be0
Improve entities constructor
fpagnoux Jun 20, 2017
70e6750
Refactor
fpagnoux Jun 20, 2017
8155b52
Refactor test
fpagnoux Jun 20, 2017
71dc291
Init situation with persons variable
fpagnoux Jun 20, 2017
e012e37
Test set_inputs
fpagnoux Jun 20, 2017
5a98c0e
Handle basic calculation
fpagnoux Jun 20, 2017
3bef06b
Ignore null values when parsing situation
fpagnoux Jun 20, 2017
c4546db
Minor changes: flake8
fpagnoux Jun 20, 2017
7a373bc
Refactor entities
fpagnoux Jun 20, 2017
aedddeb
Pass core tests
fpagnoux Jun 20, 2017
9b2394a
Do not use deprecated function
fpagnoux Jun 20, 2017
c35e597
Handle more error cases
fpagnoux Jun 20, 2017
9447b10
Handle internal server errors
fpagnoux Jun 20, 2017
e310cea
Handle unexpected variables
fpagnoux Jun 20, 2017
77ccf04
Handle period-less input
fpagnoux Jun 20, 2017
6ff14fd
Handle wrong input variable type
fpagnoux Jun 20, 2017
6fd4990
Handle unexpected id in role
fpagnoux Jun 20, 2017
d827a1b
Handle person declared several times in an entity
fpagnoux Jun 20, 2017
3ef148b
Handle person not declared in entities
fpagnoux Jun 21, 2017
1c95086
Improve Role __repr__
fpagnoux Jun 21, 2017
d65dc82
Attribute entity-less persons to new entities
fpagnoux Jun 21, 2017
7b06324
Test date input
fpagnoux Jun 21, 2017
3821337
Handle invalid periods
fpagnoux Jun 21, 2017
dab6404
Handle period mismatch
fpagnoux Jun 21, 2017
60e9d1f
Change breaking changes to deprecations
fpagnoux Jun 21, 2017
66b9f3e
Move handle_invalid_json definition
fpagnoux Jun 21, 2017
7d8e5f1
Minor refactor
fpagnoux Jun 21, 2017
e3a4a90
Handle set_input errors
fpagnoux Jun 21, 2017
be9a020
Add dpath to dependencies
fpagnoux Jun 21, 2017
3ad1c05
Detect invalid periods for requested variables
fpagnoux Jun 21, 2017
7b98907
Refactor simulation.instantiate_entities
fpagnoux Jun 21, 2017
c725b2c
Ignore unexpected nulls
fpagnoux Jun 21, 2017
e741d2a
Refactor /calculate
fpagnoux Jun 21, 2017
f1be17f
Bump version number
fpagnoux Jun 21, 2017
4f105c9
Upgrade country template
fpagnoux Jun 21, 2017
05ac86b
Document /calculate route
fpagnoux Jun 21, 2017
22262e5
Refactor OpenAPI spec generation
fpagnoux Jun 21, 2017
0f24871
Document variable values type in Schema
fpagnoux Jun 21, 2017
6623600
Improve OpenAPI spec
fpagnoux Jun 21, 2017
080ecb4
Remove spaces before ":"
fpagnoux Jun 21, 2017
54c72bb
Rename build_from_json - > init_from_json
fpagnoux Jun 22, 2017
d9979c9
Minor refactor
fpagnoux Jun 22, 2017
ac7c7e9
Minor refactor
fpagnoux Jun 22, 2017
38a8cf5
Minor refactor
fpagnoux Jun 22, 2017
cdbb771
Handle ETERNITY in input
fpagnoux Jun 22, 2017
d80924c
Handle unique roles
fpagnoux Jun 22, 2017
48f197f
Handle invalid type in role array
fpagnoux Jun 22, 2017
2096537
Minor refactor
fpagnoux Jun 22, 2017
659de24
Minor refactor
fpagnoux Jun 22, 2017
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
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,33 @@
# Changelog

## 14.1.0 - [#528](https://github.com/openfisca/openfisca-core/pull/528)

#### New features

- Introduce `/calculate` route in the preview API
- Allows to run calculations.
- Takes a simulation `JSON` as an input, and returns a copy of the input extended with calculation results.

- Handle `500` errors in the preview API
- In this case, the API returns a JSON with details about the error.

- Allows simulations to be built from a JSON using their constructor
- For instance `Simulation(simulation_json = {"persons": {...}, "households": {...}}, tax_benefit_system = tax_benefit_system)`

- Allows entities to be built from a JSON using their constructor
- For instance `Household(simulation, {"first_household": {...}})`

- Introduce `tax_benefit_system.get_variables(entity = None)`
- Allows to get all variables contained in a tax and benefit system, with filtering by entity

#### Deprecations

- Deprecate `simulation.holder_by_name`, `simulation.get_holder`, `get_or_new_holder`
- These functionalities are now provided by `entity.get_holder(name)`

- Deprecate constructor `Holder(simulation, column)`
- A `Holder` should now be instanciated with `Holder(entity = entity, column = column)`

## 14.0.1 - [#527](https://github.com/openfisca/openfisca-core/pull/527)

* Improve error message and add stack trace when a module import fails
Expand Down
218 changes: 203 additions & 15 deletions openfisca_core/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@
import warnings

import numpy as np
import dpath

from formulas import ADD, DIVIDE
from scenarios import iter_over_entity_members
from simulations import check_type, SituationParsingError
from holders import Holder, PeriodMismatchError
from periods import compare_period_size, period as make_period
from taxbenefitsystems import VariableNotFound


class Entity(object):
Expand All @@ -14,10 +20,108 @@ class Entity(object):
label = None
is_person = False

def __init__(self, simulation):
def __init__(self, simulation, entities_json = None):
self.simulation = simulation
self.count = 0
self.step_size = 0
self._holders = {}
if entities_json is not None:
self.init_from_json(entities_json)
else:
self.entities_json = None
self.count = 0
self.ids = []
self.step_size = 0

def init_from_json(self, entities_json):
check_type(entities_json, dict, [self.plural])
self.entities_json = entities_json
self.count = len(entities_json)
self.step_size = self.count # Related to axes.
self.ids = sorted(entities_json.keys())
for entity_id, entity_object in entities_json.iteritems():
check_type(entity_object, dict, [self.plural, entity_id])
if not self.is_person:
roles_json, variables_json = self.split_variables_and_roles_json(entity_object)
self.init_members(roles_json, entity_id)
else:
variables_json = entity_object
self.init_variable_values(variables_json, entity_id)

# Due to set_input mechanism, we must bufferize all inputs, then actually set them, so that the months are set first and the years last.
self.finalize_variables_init()

def init_variable_values(self, entity_object, entity_id):
entity_index = self.ids.index(entity_id)
for variable_name, variable_values in entity_object.iteritems():
try:
self.check_variable_defined_for_entity(variable_name)
except ValueError as e: # The variable is defined for another entity
raise SituationParsingError([self.plural, entity_id, variable_name], e.message)
except VariableNotFound as e: # The variable doesn't exist
raise SituationParsingError([self.plural, entity_id, variable_name], e.message, code = 404)

if not isinstance(variable_values, dict):
raise SituationParsingError([self.plural, entity_id, variable_name],
'Invalid type: must be of type object. Input variables must be set for specific periods. For instance: {"salary": {"2017-01": 2000, "2017-02": 2500}}')

holder = self.get_holder(variable_name)
for date, value in variable_values.iteritems():
try:
period = make_period(date)
except ValueError as e:
raise SituationParsingError([self.plural, entity_id, variable_name, date], e.message)
if value is not None:
array = holder.buffer.get(period)
if array is None:
array = holder.default_array()

try:
array[entity_index] = value
except (ValueError, TypeError) as e:
raise SituationParsingError([self.plural, entity_id, variable_name, date],
'Invalid type: must be of type {}.'.format(holder.column.json_type))

holder.buffer[period] = array

def finalize_variables_init(self):
for variable_name, holder in self._holders.iteritems():
periods = holder.buffer.keys()
# We need to handle small periods first for set_input to work
sorted_periods = sorted(periods, cmp = compare_period_size)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Catch ValueError in case of ETERNITY on compare_period_size ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, I was not handling ETERNITY !
Should be good now (testes in test_variables)

for period in sorted_periods:
array = holder.buffer[period]
try:
holder.set_input(period, array)
except PeriodMismatchError as e:
# This errors happens when we try to set a variable value for a period that doesn't match its definition period
# It is only raised when we consume the buffer. We thus don't know which exact key caused the error.
# We do a basic research to find the culprit path
culprit_path = next(
dpath.search(self.entities_json, "*/{}/{}".format(e.variable_name, str(e.period)), yielded = True),
None)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

if culprit_path:
path = [self.plural] + culprit_path[0].split('/')
else:
path = [self.plural] # Fallback: if we can't find the culprit, just set the error at the entities level

raise SituationParsingError(path, e.message)

def clone(self, new_simulation):
"""
Returns an entity instance with the same structure, but no variable value set.
"""
new = Entity(new_simulation)
new_dict = new.__dict__

for key, value in self.__dict__.iteritems():
if key == '_holders':
new_dict[key] = {
name: holder.clone()
for name, holder in self._holders.iteritems()
}
else:
new_dict[key] = value

return new

def __getattr__(self, attribute):
projector = get_projector_from_shortcut(self, attribute)
Expand All @@ -38,20 +142,20 @@ def to_json(cls):
# Calculations

def check_variable_defined_for_entity(self, variable_name):
if not (self.simulation.get_variable_entity(variable_name) == self):
variable_entity = self.simulation.get_variable_entity(variable_name)
raise Exception(
variable_entity = self.simulation.tax_benefit_system.get_column(variable_name, check_existence = True).entity
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Existing self.simulation.get_variable_entity(variable_name) seems to do the same thing. Use it instead ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use check_variable_defined_for_entity when we are building the entity.
At this stage, simulation.entities is not yet defined, so self.simulation.get_variable_entit does not work.

if not isinstance(self, variable_entity):
raise ValueError(
"Variable {} is not defined for {} but for {}".format(
variable_name, self.key, variable_entity.key)
variable_name, self.plural, variable_entity.plural)
)

def check_array_compatible_with_entity(self, array):
if not self.count == array.size:
raise Exception("Input {} is not a valid value for the entity {}".format(array, self.key))
raise ValueError("Input {} is not a valid value for the entity {}".format(array, self.key))

def check_role_validity(self, role):
if role is not None and not type(role) == Role:
raise Exception("{} is not a valid role".format(role))
raise ValueError("{} is not a valid role".format(role))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Catch the exception where check_role_validity is called ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check_role_validity is not called in the entity initialization process.
This was just an opportunistic fix.


def check_period_validity(self, variable_name, period):
if period is None:
Expand Down Expand Up @@ -89,6 +193,20 @@ def filled_array(self, value, dtype = None):
warnings.simplefilter("ignore")
return np.full(self.count, value, dtype)

def get_holder(self, variable_name):
self.check_variable_defined_for_entity(variable_name)
holder = self._holders.get(variable_name)
if holder:
return holder
column = self.simulation.tax_benefit_system.get_column(variable_name)
self._holders[variable_name] = holder = Holder(
entity = self,
column = column,
)
if column.formula_class is not None:
holder.formula = column.formula_class(holder = holder) # Instanciates a Formula
return holder


class PersonEntity(Entity):
is_person = True
Expand Down Expand Up @@ -125,14 +243,81 @@ class GroupEntity(Entity):
flattened_roles = None
roles_description = None

def __init__(self, simulation):
Entity.__init__(self, simulation)
self.members_entity_id = None
self._members_role = None
self._members_position = None
self.members_legacy_role = None
def __init__(self, simulation, entities_json = None):
Entity.__init__(self, simulation, entities_json)
if entities_json is None:
self.members_entity_id = None
self._members_role = None
self._members_position = None
self.members_legacy_role = None
self.members = self.simulation.persons

def split_variables_and_roles_json(self, entity_object):
entity_object = entity_object.copy() # Don't mutate function input

roles_definition = {
role.plural: entity_object.pop(role.plural or role.key, [])
for role in self.roles
}

return roles_definition, entity_object

def init_from_json(self, entities_json):
self.members_entity_id = np.empty(
self.simulation.persons.count,
dtype = np.int32
)
self.members_role = np.empty(
self.simulation.persons.count,
dtype = object
)
self.members_legacy_role = np.empty(
self.simulation.persons.count,
dtype = np.int32
)
self._members_position = None

self.persons_to_allocate = set(self.simulation.persons.ids)

Entity.init_from_json(self, entities_json)

for person in self.persons_to_allocate: # We build a single-person entity for each person who hasn't been declared inside any entity
person_index = self.simulation.persons.ids.index(person)
entity_index = self.count
self.count += 1
self.step_size += 1 # Related to axes
self.ids.append(person)
self.members_entity_id[person_index] = entity_index
self.members_role[person_index] = self.flattened_roles[0]
self.members_legacy_role[person_index] = 0

def init_members(self, roles_json, entity_id):
for role_id, role_definition in roles_json.iteritems():
check_type(role_definition, list, [self.plural, entity_id, role_id])
for index, person_id in enumerate(role_definition):
check_type(person_id, basestring, [self.plural, entity_id, role_id, str(index)])
if person_id not in self.simulation.persons.ids:
raise SituationParsingError([self.plural, entity_id, role_id],
"Unexpected value: {0}. {0} has been declared in {1} {2}, but has not been declared in {3}.".format(
person_id, entity_id, role_id, self.simulation.persons.plural)
)
if person_id not in self.persons_to_allocate:
raise SituationParsingError([self.plural, entity_id, role_id],
"{} has been declared more than once in {}".format(
person_id, self.plural)
)
self.persons_to_allocate.discard(person_id)

entity_index = self.ids.index(entity_id)
for person_role, person_legacy_role, person_id in iter_over_entity_members(self, roles_json):
person_index = self.simulation.persons.ids.index(person_id)
self.members_entity_id[person_index] = entity_index
self.members_role[person_index] = person_role
self.members_legacy_role[person_index] = person_legacy_role

# Deprecated attribute used by deprecated projection opertors, such as sum_by_entity
self.roles_count = self.members_legacy_role.max() + 1

@property
def members_role(self):
if self._members_role is None and self.members_legacy_role is not None:
Expand Down Expand Up @@ -281,6 +466,9 @@ def __init__(self, description, entity):
self.max = description.get('max')
self.subroles = None

def __repr__(self):
return "Role({})".format(self.key)


class Projector(object):
reference_entity = None
Expand Down
4 changes: 2 additions & 2 deletions openfisca_core/formulas.py
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,7 @@ def compute(self, period, **parameters):
step['default_input_variables'] = has_only_default_input_variables = all(
np.all(input_holder.get_array(input_variable_period) == input_holder.column.default)
for input_holder, input_variable_period in (
(simulation.get_holder(input_variable_name), input_variable_period1)
(simulation.get_variable_entity(input_variable_name).get_holder(input_variable_name), input_variable_period1)
for input_variable_name, input_variable_period1 in input_variables_infos
)
)
Expand Down Expand Up @@ -554,7 +554,7 @@ def formula_to_json(self, function, get_input_variables_and_parameters = None, w
if with_input_variables_details:
input_variables_json = []
for variable_name in sorted(variables_name):
variable_holder = simulation.get_or_new_holder(variable_name)
variable_holder = simulation.get_variable_entity(variable_name).get_holder(variable_name)
variable_column = variable_holder.column
input_variables_json.append(collections.OrderedDict((
('entity', variable_holder.entity.key),
Expand Down
Loading