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

Make parameters node accessible through the Web API #694

Merged
merged 11 commits into from
Aug 3, 2018
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
48 changes: 47 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,51 @@
# Changelog

## 23.4.0 [#694](https://github.com/openfisca/openfisca-core/pull/694)

* Use `/` rather than `.` in the path to access a parameter:
- For instance `/parameter/benefits.basic_income` becomes `/parameter/benefits/basic_income`
- Using `.` is for now still supported, but is considered deprecated and will be turned to a 301 redirection in the next major version.

* Expose parameters `metadata` and `source` in the Web API and:

For instance, `/parameter/benefits/basic_income` contains:

```JSON
{
"description": "Amount of the basic income",
"id": "benefits.basic_income",
"metadata": {
"reference": "https://law.gov.example/basic-income/amount",
"unit": "currency-EUR"
},
"source": "https://github.com/openfisca/country-template/blob/3.2.2/openfisca_country_template/parameters/benefits/basic_income.yaml",
"values": {
"2015-12-01": 600.0
}
}
```

* Expose parameters nodes in the Web API
Copy link
Collaborator

Choose a reason for hiding this comment

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

Explicit that it exposes 1 level of depth only?

Copy link
Member Author

Choose a reason for hiding this comment

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

OK (see node below)

- For instance, `/parameter/benefits` now exists and contains:
Copy link
Collaborator

@sandcha sandcha Aug 1, 2018

Choose a reason for hiding this comment

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

(Already said above) Shouldn't it be /parameters for a node as it's not a parameter? 🤔


```JSON
{
"description": "Social benefits",
Copy link
Collaborator

Choose a reason for hiding this comment

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

Explicit that a node might have metadata? 🤔

"id": "benefits",
"metadata": {},
"source": "https://github.com/openfisca/country-template/blob/3.2.2/openfisca_country_template/parameters/benefits",
"subparams": {
"basic_income": {
"description": "Amount of the basic income"
},
"housing_allowance": {
"description": "Housing allowance amount (as a fraction of the rent)"
}
}
}
```

Note that this route doesn't _recursively_ explore the node, and only exposes its direct children name and description.

### 23.3.2 [#702](https://github.com/openfisca/openfisca-core/pull/702)

Expand All @@ -13,7 +59,7 @@ Minor Change without any impact for country package developers and users:

* Send reference of the country-package and its version to the tracker so it will appear in the tracking statistics.

### 23.3.0 [#681](https://github.com/openfisca/openfisca-core/pull/681)
## 23.3.0 [#681](https://github.com/openfisca/openfisca-core/pull/681)

* Change the way metadata are declared for Parameter.

Expand Down
10 changes: 7 additions & 3 deletions openfisca_web_api_preview/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,13 @@ def create_app(tax_benefit_system,
def get_parameters():
return jsonify(data['parameters_description'])

@app.route('/parameter/<id>')
def get_parameter(id):
parameter = data['parameters'].get(id)
@app.route('/parameter/<path:parameter_id>')
def get_parameter(parameter_id):
parameter = data['parameters'].get(parameter_id)
if parameter is None:
# Try legacy route
parameter_new_id = parameter_id.replace('.', '/')
parameter = data['parameters'].get(parameter_new_id)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Look for the parameter only when parameter_new_id differs from parameter_id to avoid looking into all parameters in that case (+2000 parameters in french model for example)?

Copy link
Member Author

@fpagnoux fpagnoux Aug 2, 2018

Choose a reason for hiding this comment

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

We are not looking into all parameters. The get method of a dictionary has a O(1) average complexity as it uses hashes.

if parameter is None:
raise abort(404)
return jsonify(parameter)
Expand Down
2 changes: 1 addition & 1 deletion openfisca_web_api_preview/loader/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def extract_description(items):

def build_data(tax_benefit_system):
country_package_metadata = tax_benefit_system.get_package_metadata()
parameters = build_parameters(tax_benefit_system)
parameters = build_parameters(tax_benefit_system, country_package_metadata)
variables = build_variables(tax_benefit_system, country_package_metadata)
openAPI_spec = build_openAPI_specification(tax_benefit_system, country_package_metadata)
return {
Expand Down
74 changes: 46 additions & 28 deletions openfisca_web_api_preview/loader/parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
from openfisca_core.parameters import Parameter, ParameterNode, Scale


def transform_values_history(values_history):
values_history_transformed = {}
def build_api_values_history(values_history):
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍

api_values_history = {}
for value_at_instant in values_history.values_list:
values_history_transformed[value_at_instant.instant_str] = value_at_instant.value
api_values_history[value_at_instant.instant_str] = value_at_instant.value

return values_history_transformed
return api_values_history


def get_value(date, values):
Expand All @@ -25,61 +25,79 @@ def get_value(date, values):
return None


def transform_scale(scale):
def build_api_scale(scale):
# preprocess brackets
brackets = [{
'thresholds': transform_values_history(bracket.threshold),
'rates': transform_values_history(bracket.rate),
'thresholds': build_api_values_history(bracket.threshold),
'rates': build_api_values_history(bracket.rate),
} for bracket in scale.brackets]

dates = set(sum(
[list(bracket['thresholds'].keys()) + list(bracket['rates'].keys()) for bracket in brackets],
[])) # flatten the dates and remove duplicates

# We iterate on all dates as we need to build the whole scale for each of them
brackets_transformed = {}
api_scale = {}
for date in dates:
for bracket in brackets:
threshold_value = get_value(date, bracket['thresholds'])
if threshold_value is not None:
rate_value = get_value(date, bracket['rates'])
brackets_transformed[date] = brackets_transformed.get(date) or {}
brackets_transformed[date][threshold_value] = rate_value
api_scale[date] = api_scale.get(date) or {}
api_scale[date][threshold_value] = rate_value

# Handle stopped parameters: a parameter is stopped if its first bracket is stopped
latest_date_first_threshold = max(brackets[0]['thresholds'].keys())
latest_value_first_threshold = brackets[0]['thresholds'][latest_date_first_threshold]
if latest_value_first_threshold is None:
brackets_transformed[latest_date_first_threshold] = None
api_scale[latest_date_first_threshold] = None

return brackets_transformed
return api_scale


def walk_node(node, parameters, path_fragments):
def build_source_url(absolute_file_path, country_package_metadata):
Copy link
Collaborator

Choose a reason for hiding this comment

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

For taxes/income_tax_rate.yaml parameter, we get "source": "https://github.com/openfisca/openfisca-country-template/blob/3.2.2/openfisca_country_template/parameters/taxes/income_tax_rate.yaml" that leads to 404 error. Fix this?

Copy link
Member Author

Choose a reason for hiding this comment

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

relative_path = absolute_file_path.replace(country_package_metadata['location'], '')
return '{}/blob/{}{}'.format(
country_package_metadata['repository_url'],
country_package_metadata['version'],
relative_path
)


def walk_node(node, parameters, path_fragments, country_package_metadata):
children = node.children

for child_name, child in children.items():
if isinstance(child, ParameterNode):
walk_node(child, parameters, path_fragments + [child_name])
else:
object_transformed = {
'description': getattr(child, "description", None),
'id': '.'.join(path_fragments + [child_name]),
api_parameter = {
'description': getattr(child, "description", None),
'id': '.'.join(path_fragments + [child_name]),
'metadata': child.metadata
}
if child.file_path:
api_parameter['source'] = build_source_url(child.file_path, country_package_metadata)
if isinstance(child, Parameter):
api_parameter['values'] = build_api_values_history(child)
elif isinstance(child, Scale):
api_parameter['brackets'] = build_api_scale(child)
elif isinstance(child, ParameterNode):
api_parameter['subparams'] = {
grandchild_name: {
'description': grandchild.description,
}
for grandchild_name, grandchild in child.children.items()
}
if isinstance(child, Scale):
object_transformed['brackets'] = transform_scale(child)
elif isinstance(child, Parameter):
object_transformed['values'] = transform_values_history(child)
parameters.append(object_transformed)
walk_node(child, parameters, path_fragments + [child_name], country_package_metadata)
parameters.append(api_parameter)


def build_parameters(tax_benefit_system):
def build_parameters(tax_benefit_system, country_package_metadata):
original_parameters = tax_benefit_system.parameters
transformed_parameters = []
api_parameters = []
walk_node(
original_parameters,
parameters = transformed_parameters,
parameters = api_parameters,
path_fragments = [],
country_package_metadata = country_package_metadata,
)

return {parameter['id']: parameter for parameter in transformed_parameters}
return {parameter['id'].replace('.', '/'): parameter for parameter in api_parameters}
14 changes: 14 additions & 0 deletions openfisca_web_api_preview/openAPI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -194,11 +194,22 @@ definitions:
type: "object"
additionalProperties:
$ref: "#/definitions/Brackets"
subparams:
type: "object"
additionalProperties:
type: "object"
properties:
definition:
type: 'string'
metadata:
type: "object"
description:
type: "string"
id:
type: "integer"
format: "string"
source:
type: "string"
example:
id: "cotsoc.gen.smic_h_b"
description: "SMIC horaire brut"
Expand All @@ -222,6 +233,9 @@ definitions:
"2015-01-01": 9.61,
"2016-01-01": 9.67,
"2017-01-01": 9.76}
source: https://github.com/openfisca/openfisca-france/blob/22.2.1/openfisca_france/parameters/cotsoc/gen/smic_h_b.yaml
metadata:
unit: "currency-EUR"

Parameters:
type: "object"
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

setup(
name = 'OpenFisca-Core',
version = '23.3.2',
version = '23.4.0',
author = 'OpenFisca Team',
author_email = 'contact@openfisca.fr',
classifiers = [
Expand Down
11 changes: 11 additions & 0 deletions tests/web_api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-

from __future__ import unicode_literals, print_function, division, absolute_import
import pkg_resources
from openfisca_web_api_preview.app import create_app
from openfisca_core.scripts import build_tax_benefit_system

TEST_COUNTRY_PACKAGE_NAME = 'openfisca_country_template'
distribution = pkg_resources.get_distribution(TEST_COUNTRY_PACKAGE_NAME)
tax_benefit_system = build_tax_benefit_system(TEST_COUNTRY_PACKAGE_NAME, extensions = None, reforms = None)
subject = create_app(tax_benefit_system).test_client()
125 changes: 0 additions & 125 deletions tests/web_api/basic_case/test_parameters.py

This file was deleted.

File renamed without changes.
Loading