OpenFisca-Core-test 46.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openfisca_core/__init__.py +0 -0
- openfisca_core/commons/__init__.py +18 -0
- openfisca_core/commons/dummy.py +25 -0
- openfisca_core/commons/formulas.py +121 -0
- openfisca_core/commons/misc.py +109 -0
- openfisca_core/commons/py.typed +0 -0
- openfisca_core/commons/rates.py +114 -0
- openfisca_core/commons/tests/__init__.py +0 -0
- openfisca_core/commons/tests/test_dummy.py +9 -0
- openfisca_core/commons/tests/test_formulas.py +75 -0
- openfisca_core/commons/tests/test_rates.py +26 -0
- openfisca_core/data_storage/__init__.py +7 -0
- openfisca_core/data_storage/in_memory_storage.py +198 -0
- openfisca_core/data_storage/on_disk_storage.py +307 -0
- openfisca_core/data_storage/types.py +14 -0
- openfisca_core/entities/__init__.py +23 -0
- openfisca_core/entities/_core_entity.py +217 -0
- openfisca_core/entities/_description.py +55 -0
- openfisca_core/entities/entity.py +60 -0
- openfisca_core/entities/group_entity.py +123 -0
- openfisca_core/entities/helpers.py +165 -0
- openfisca_core/entities/py.typed +0 -0
- openfisca_core/entities/role.py +92 -0
- openfisca_core/entities/tests/__init__.py +0 -0
- openfisca_core/entities/tests/test_entity.py +10 -0
- openfisca_core/entities/tests/test_group_entity.py +70 -0
- openfisca_core/entities/tests/test_role.py +11 -0
- openfisca_core/entities/types.py +44 -0
- openfisca_core/errors/__init__.py +64 -0
- openfisca_core/errors/cycle_error.py +2 -0
- openfisca_core/errors/empty_argument_error.py +39 -0
- openfisca_core/errors/nan_creation_error.py +2 -0
- openfisca_core/errors/parameter_not_found_error.py +16 -0
- openfisca_core/errors/parameter_parsing_error.py +18 -0
- openfisca_core/errors/period_mismatch_error.py +9 -0
- openfisca_core/errors/situation_parsing_error.py +26 -0
- openfisca_core/errors/spiral_error.py +2 -0
- openfisca_core/errors/variable_name_config_error.py +2 -0
- openfisca_core/errors/variable_not_found_error.py +29 -0
- openfisca_core/experimental/__init__.py +9 -0
- openfisca_core/experimental/_errors.py +5 -0
- openfisca_core/experimental/_memory_config.py +41 -0
- openfisca_core/formula_helpers.py +9 -0
- openfisca_core/holders/__init__.py +33 -0
- openfisca_core/holders/helpers.py +93 -0
- openfisca_core/holders/holder.py +328 -0
- openfisca_core/holders/tests/__init__.py +0 -0
- openfisca_core/holders/tests/test_helpers.py +134 -0
- openfisca_core/holders/types.py +3 -0
- openfisca_core/indexed_enums/__init__.py +18 -0
- openfisca_core/indexed_enums/_enum_type.py +79 -0
- openfisca_core/indexed_enums/_errors.py +35 -0
- openfisca_core/indexed_enums/_guards.py +209 -0
- openfisca_core/indexed_enums/_utils.py +180 -0
- openfisca_core/indexed_enums/config.py +6 -0
- openfisca_core/indexed_enums/enum.py +218 -0
- openfisca_core/indexed_enums/enum_array.py +316 -0
- openfisca_core/indexed_enums/py.typed +0 -0
- openfisca_core/indexed_enums/tests/__init__.py +0 -0
- openfisca_core/indexed_enums/tests/test_enum.py +135 -0
- openfisca_core/indexed_enums/tests/test_enum_array.py +30 -0
- openfisca_core/indexed_enums/types.py +49 -0
- openfisca_core/memory_config.py +9 -0
- openfisca_core/model_api.py +71 -0
- openfisca_core/parameters/__init__.py +71 -0
- openfisca_core/parameters/at_instant_like.py +17 -0
- openfisca_core/parameters/config.py +55 -0
- openfisca_core/parameters/helpers.py +106 -0
- openfisca_core/parameters/parameter.py +217 -0
- openfisca_core/parameters/parameter_at_instant.py +75 -0
- openfisca_core/parameters/parameter_node.py +176 -0
- openfisca_core/parameters/parameter_node_at_instant.py +57 -0
- openfisca_core/parameters/parameter_scale.py +121 -0
- openfisca_core/parameters/parameter_scale_bracket.py +7 -0
- openfisca_core/parameters/values_history.py +5 -0
- openfisca_core/parameters/vectorial_parameter_node_at_instant.py +179 -0
- openfisca_core/periods/__init__.py +77 -0
- openfisca_core/periods/_errors.py +28 -0
- openfisca_core/periods/_parsers.py +122 -0
- openfisca_core/periods/config.py +20 -0
- openfisca_core/periods/date_unit.py +110 -0
- openfisca_core/periods/helpers.py +314 -0
- openfisca_core/periods/instant_.py +226 -0
- openfisca_core/periods/period_.py +919 -0
- openfisca_core/periods/py.typed +0 -0
- openfisca_core/periods/tests/__init__.py +0 -0
- openfisca_core/periods/tests/helpers/__init__.py +0 -0
- openfisca_core/periods/tests/helpers/test_helpers.py +65 -0
- openfisca_core/periods/tests/helpers/test_instant.py +73 -0
- openfisca_core/periods/tests/helpers/test_period.py +134 -0
- openfisca_core/periods/tests/test_instant.py +32 -0
- openfisca_core/periods/tests/test_parsers.py +129 -0
- openfisca_core/periods/tests/test_period.py +283 -0
- openfisca_core/populations/__init__.py +64 -0
- openfisca_core/populations/_core_population.py +454 -0
- openfisca_core/populations/_errors.py +65 -0
- openfisca_core/populations/group_population.py +326 -0
- openfisca_core/populations/population.py +142 -0
- openfisca_core/populations/tests/__init__.py +0 -0
- openfisca_core/populations/tests/test_members_position.py +116 -0
- openfisca_core/populations/types.py +110 -0
- openfisca_core/projectors/__init__.py +39 -0
- openfisca_core/projectors/entity_to_person_projector.py +12 -0
- openfisca_core/projectors/first_person_to_entity_projector.py +13 -0
- openfisca_core/projectors/helpers.py +139 -0
- openfisca_core/projectors/projector.py +38 -0
- openfisca_core/projectors/typing.py +27 -0
- openfisca_core/projectors/unique_role_to_entity_projector.py +14 -0
- openfisca_core/rates.py +9 -0
- openfisca_core/reforms/__init__.py +24 -0
- openfisca_core/reforms/in_yaml_test_reform.py +28 -0
- openfisca_core/reforms/reform.py +86 -0
- openfisca_core/scripts/__init__.py +106 -0
- openfisca_core/scripts/__pycache__/__init__.cpython-310-pytest-8.3.3.pyc +0 -0
- openfisca_core/scripts/__pycache__/__init__.cpython-310-pytest-8.4.2.pyc +0 -0
- openfisca_core/scripts/__pycache__/__init__.cpython-310.pyc +0 -0
- openfisca_core/scripts/__pycache__/__init__.cpython-311-pytest-8.3.3.pyc +0 -0
- openfisca_core/scripts/__pycache__/__init__.cpython-311-pytest-8.4.2.pyc +0 -0
- openfisca_core/scripts/__pycache__/__init__.cpython-311.pyc +0 -0
- openfisca_core/scripts/__pycache__/__init__.cpython-312-pytest-8.4.2.pyc +0 -0
- openfisca_core/scripts/__pycache__/__init__.cpython-312.pyc +0 -0
- openfisca_core/scripts/__pycache__/__init__.cpython-313-pytest-8.3.3.pyc +0 -0
- openfisca_core/scripts/__pycache__/__init__.cpython-313-pytest-8.4.2.pyc +0 -0
- openfisca_core/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
- openfisca_core/scripts/__pycache__/find_placeholders.cpython-311.pyc +0 -0
- openfisca_core/scripts/__pycache__/find_placeholders.cpython-313.pyc +0 -0
- openfisca_core/scripts/__pycache__/measure_numpy_condition_notations.cpython-311.pyc +0 -0
- openfisca_core/scripts/__pycache__/measure_numpy_condition_notations.cpython-313.pyc +0 -0
- openfisca_core/scripts/__pycache__/measure_performances.cpython-311.pyc +0 -0
- openfisca_core/scripts/__pycache__/measure_performances.cpython-313.pyc +0 -0
- openfisca_core/scripts/__pycache__/measure_performances_fancy_indexing.cpython-311.pyc +0 -0
- openfisca_core/scripts/__pycache__/measure_performances_fancy_indexing.cpython-313.pyc +0 -0
- openfisca_core/scripts/__pycache__/openfisca_command.cpython-310.pyc +0 -0
- openfisca_core/scripts/__pycache__/openfisca_command.cpython-311.pyc +0 -0
- openfisca_core/scripts/__pycache__/openfisca_command.cpython-312.pyc +0 -0
- openfisca_core/scripts/__pycache__/openfisca_command.cpython-313.pyc +0 -0
- openfisca_core/scripts/__pycache__/remove_fuzzy.cpython-311.pyc +0 -0
- openfisca_core/scripts/__pycache__/remove_fuzzy.cpython-313.pyc +0 -0
- openfisca_core/scripts/__pycache__/run_test.cpython-310.pyc +0 -0
- openfisca_core/scripts/__pycache__/run_test.cpython-311.pyc +0 -0
- openfisca_core/scripts/__pycache__/run_test.cpython-312.pyc +0 -0
- openfisca_core/scripts/__pycache__/run_test.cpython-313.pyc +0 -0
- openfisca_core/scripts/__pycache__/simulation_generator.cpython-311.pyc +0 -0
- openfisca_core/scripts/__pycache__/simulation_generator.cpython-313.pyc +0 -0
- openfisca_core/scripts/assets/__init__.py +0 -0
- openfisca_core/scripts/assets/__pycache__/__init__.cpython-311.pyc +0 -0
- openfisca_core/scripts/assets/__pycache__/__init__.cpython-313.pyc +0 -0
- openfisca_core/scripts/assets/index.html +140 -0
- openfisca_core/scripts/find_placeholders.py +51 -0
- openfisca_core/scripts/measure_numpy_condition_notations.py +137 -0
- openfisca_core/scripts/measure_performances.py +291 -0
- openfisca_core/scripts/measure_performances_fancy_indexing.py +89 -0
- openfisca_core/scripts/migrations/__init__.py +0 -0
- openfisca_core/scripts/migrations/__pycache__/__init__.cpython-311.pyc +0 -0
- openfisca_core/scripts/migrations/__pycache__/__init__.cpython-313.pyc +0 -0
- openfisca_core/scripts/migrations/__pycache__/v24_to_25.cpython-311.pyc +0 -0
- openfisca_core/scripts/migrations/__pycache__/v24_to_25.cpython-313.pyc +0 -0
- openfisca_core/scripts/migrations/v16_2_to_v17/__init__.py +0 -0
- openfisca_core/scripts/migrations/v16_2_to_v17/__pycache__/__init__.cpython-311.pyc +0 -0
- openfisca_core/scripts/migrations/v16_2_to_v17/__pycache__/__init__.cpython-313.pyc +0 -0
- openfisca_core/scripts/migrations/v16_2_to_v17/__pycache__/xml_to_yaml_country_template.cpython-311.pyc +0 -0
- openfisca_core/scripts/migrations/v16_2_to_v17/__pycache__/xml_to_yaml_country_template.cpython-313.pyc +0 -0
- openfisca_core/scripts/migrations/v16_2_to_v17/__pycache__/xml_to_yaml_extension_template.cpython-311.pyc +0 -0
- openfisca_core/scripts/migrations/v16_2_to_v17/__pycache__/xml_to_yaml_extension_template.cpython-313.pyc +0 -0
- openfisca_core/scripts/migrations/v16_2_to_v17/legislation.xsd +102 -0
- openfisca_core/scripts/migrations/v16_2_to_v17/xml_to_yaml_country_template.py +30 -0
- openfisca_core/scripts/migrations/v16_2_to_v17/xml_to_yaml_extension_template.py +26 -0
- openfisca_core/scripts/migrations/v24_to_25.py +156 -0
- openfisca_core/scripts/openfisca_command.py +192 -0
- openfisca_core/scripts/remove_fuzzy.py +213 -0
- openfisca_core/scripts/run_test.py +51 -0
- openfisca_core/scripts/simulation_generator.py +90 -0
- openfisca_core/simulation_builder.py +16 -0
- openfisca_core/simulations/__init__.py +45 -0
- openfisca_core/simulations/_build_default_simulation.py +158 -0
- openfisca_core/simulations/_build_from_variables.py +230 -0
- openfisca_core/simulations/_type_guards.py +298 -0
- openfisca_core/simulations/helpers.py +111 -0
- openfisca_core/simulations/simulation.py +618 -0
- openfisca_core/simulations/simulation_builder.py +855 -0
- openfisca_core/simulations/typing.py +203 -0
- openfisca_core/taxbenefitsystems/__init__.py +26 -0
- openfisca_core/taxbenefitsystems/tax_benefit_system.py +584 -0
- openfisca_core/taxscales/__init__.py +35 -0
- openfisca_core/taxscales/abstract_rate_tax_scale.py +41 -0
- openfisca_core/taxscales/abstract_tax_scale.py +53 -0
- openfisca_core/taxscales/amount_tax_scale_like.py +55 -0
- openfisca_core/taxscales/helpers.py +44 -0
- openfisca_core/taxscales/linear_average_rate_tax_scale.py +76 -0
- openfisca_core/taxscales/marginal_amount_tax_scale.py +34 -0
- openfisca_core/taxscales/marginal_rate_tax_scale.py +286 -0
- openfisca_core/taxscales/rate_tax_scale_like.py +214 -0
- openfisca_core/taxscales/single_amount_tax_scale.py +32 -0
- openfisca_core/taxscales/tax_scale_like.py +65 -0
- openfisca_core/tools/__init__.py +94 -0
- openfisca_core/tools/parallel_plugin.py +55 -0
- openfisca_core/tools/simulation_dumper.py +137 -0
- openfisca_core/tools/test_runner.py +802 -0
- openfisca_core/tracers/__init__.py +40 -0
- openfisca_core/tracers/computation_log.py +196 -0
- openfisca_core/tracers/flat_trace.py +87 -0
- openfisca_core/tracers/full_tracer.py +172 -0
- openfisca_core/tracers/performance_log.py +133 -0
- openfisca_core/tracers/simple_tracer.py +64 -0
- openfisca_core/tracers/trace_node.py +121 -0
- openfisca_core/tracers/tracing_parameter_node_at_instant.py +81 -0
- openfisca_core/types.py +548 -0
- openfisca_core/variables/__init__.py +26 -0
- openfisca_core/variables/config.py +53 -0
- openfisca_core/variables/helpers.py +67 -0
- openfisca_core/variables/tests/__init__.py +0 -0
- openfisca_core/variables/tests/test_definition_period.py +43 -0
- openfisca_core/variables/variable.py +474 -0
- openfisca_core/warnings/__init__.py +25 -0
- openfisca_core/warnings/libyaml_warning.py +2 -0
- openfisca_core/warnings/tempfile_warning.py +2 -0
- openfisca_core_test-46.0.0.data/data/share/openfisca/openfisca-core/CHANGELOG.md +4209 -0
- openfisca_core_test-46.0.0.data/data/share/openfisca/openfisca-core/README.md +152 -0
- openfisca_core_test-46.0.0.dist-info/METADATA +194 -0
- openfisca_core_test-46.0.0.dist-info/RECORD +237 -0
- openfisca_core_test-46.0.0.dist-info/WHEEL +5 -0
- openfisca_core_test-46.0.0.dist-info/entry_points.txt +3 -0
- openfisca_core_test-46.0.0.dist-info/licenses/LICENSE +661 -0
- openfisca_core_test-46.0.0.dist-info/top_level.txt +2 -0
- openfisca_web_api/__init__.py +0 -0
- openfisca_web_api/app.py +226 -0
- openfisca_web_api/errors.py +11 -0
- openfisca_web_api/handlers.py +90 -0
- openfisca_web_api/loader/__init__.py +22 -0
- openfisca_web_api/loader/entities.py +28 -0
- openfisca_web_api/loader/parameters.py +122 -0
- openfisca_web_api/loader/spec.py +168 -0
- openfisca_web_api/loader/tax_benefit_system.py +27 -0
- openfisca_web_api/loader/variables.py +121 -0
- openfisca_web_api/openAPI.yml +434 -0
- openfisca_web_api/scripts/__init__.py +0 -0
- openfisca_web_api/scripts/serve.py +99 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import warnings
|
|
3
|
+
|
|
4
|
+
import yaml
|
|
5
|
+
|
|
6
|
+
from openfisca_core.warnings import LibYAMLWarning
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from yaml import CLoader as Loader
|
|
10
|
+
except ImportError:
|
|
11
|
+
message = [
|
|
12
|
+
"libyaml is not installed in your environment.",
|
|
13
|
+
"This can make OpenFisca slower to start.",
|
|
14
|
+
"Once you have installed libyaml, run 'pip uninstall pyyaml && pip install pyyaml --no-cache-dir'",
|
|
15
|
+
"so that it is used in your Python environment." + os.linesep,
|
|
16
|
+
]
|
|
17
|
+
warnings.warn(" ".join(message), LibYAMLWarning, stacklevel=2)
|
|
18
|
+
from yaml import ( # (see https://github.com/python/mypy/issues/1153#issuecomment-455802270)
|
|
19
|
+
Loader,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# 'unit' and 'reference' are only listed here for backward compatibility.
|
|
23
|
+
# It is now recommended to include them in metadata, until a common consensus emerges.
|
|
24
|
+
ALLOWED_PARAM_TYPES = (float, int, bool, type(None), list, str)
|
|
25
|
+
COMMON_KEYS = {"description", "metadata", "unit", "reference", "documentation"}
|
|
26
|
+
FILE_EXTENSIONS = {".yaml", ".yml"}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def date_constructor(_loader, node):
|
|
30
|
+
return node.value
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
yaml.add_constructor("tag:yaml.org,2002:timestamp", date_constructor, Loader=Loader)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def dict_no_duplicate_constructor(loader, node, deep=False):
|
|
37
|
+
keys = [key.value for key, value in node.value]
|
|
38
|
+
|
|
39
|
+
if len(keys) != len(set(keys)):
|
|
40
|
+
duplicate = next(key for key in keys if keys.count(key) > 1)
|
|
41
|
+
msg = ""
|
|
42
|
+
raise yaml.parser.ParserError(
|
|
43
|
+
msg,
|
|
44
|
+
node.start_mark,
|
|
45
|
+
f"Found duplicate key '{duplicate}'",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
return loader.construct_mapping(node, deep)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
yaml.add_constructor(
|
|
52
|
+
yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
|
|
53
|
+
dict_no_duplicate_constructor,
|
|
54
|
+
Loader=Loader,
|
|
55
|
+
)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import traceback
|
|
3
|
+
|
|
4
|
+
import numpy
|
|
5
|
+
|
|
6
|
+
from openfisca_core import parameters, periods
|
|
7
|
+
from openfisca_core.errors import ParameterParsingError
|
|
8
|
+
from openfisca_core.parameters import config
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def contains_nan(vector):
|
|
12
|
+
if numpy.issubdtype(vector.dtype, numpy.record) or numpy.issubdtype(
|
|
13
|
+
vector.dtype,
|
|
14
|
+
numpy.void,
|
|
15
|
+
):
|
|
16
|
+
return any(contains_nan(vector[name]) for name in vector.dtype.names)
|
|
17
|
+
return numpy.isnan(vector).any()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def load_parameter_file(file_path, name=""):
|
|
21
|
+
"""Load parameters from a YAML file (or a directory containing YAML files).
|
|
22
|
+
|
|
23
|
+
:returns: An instance of :class:`.ParameterNode` or :class:`.ParameterScale` or :class:`.Parameter`.
|
|
24
|
+
"""
|
|
25
|
+
if not os.path.exists(file_path):
|
|
26
|
+
msg = f"{file_path} does not exist"
|
|
27
|
+
raise ValueError(msg)
|
|
28
|
+
if os.path.isdir(file_path):
|
|
29
|
+
return parameters.ParameterNode(name, directory_path=file_path)
|
|
30
|
+
data = _load_yaml_file(file_path)
|
|
31
|
+
return _parse_child(name, data, file_path)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _compose_name(path, child_name=None, item_name=None):
|
|
35
|
+
if not path:
|
|
36
|
+
return child_name
|
|
37
|
+
if child_name is not None:
|
|
38
|
+
return f"{path}.{child_name}"
|
|
39
|
+
if item_name is not None:
|
|
40
|
+
return f"{path}[{item_name}]"
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _load_yaml_file(file_path):
|
|
45
|
+
with open(file_path) as f:
|
|
46
|
+
try:
|
|
47
|
+
return config.yaml.load(f, Loader=config.Loader)
|
|
48
|
+
except (config.yaml.scanner.ScannerError, config.yaml.parser.ParserError):
|
|
49
|
+
stack_trace = traceback.format_exc()
|
|
50
|
+
msg = "Invalid YAML. Check the traceback above for more details."
|
|
51
|
+
raise ParameterParsingError(
|
|
52
|
+
msg,
|
|
53
|
+
file_path,
|
|
54
|
+
stack_trace,
|
|
55
|
+
)
|
|
56
|
+
except Exception:
|
|
57
|
+
stack_trace = traceback.format_exc()
|
|
58
|
+
msg = "Invalid parameter file content. Check the traceback above for more details."
|
|
59
|
+
raise ParameterParsingError(
|
|
60
|
+
msg,
|
|
61
|
+
file_path,
|
|
62
|
+
stack_trace,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _parse_child(child_name, child, child_path):
|
|
67
|
+
if "values" in child:
|
|
68
|
+
return parameters.Parameter(child_name, child, child_path)
|
|
69
|
+
if "brackets" in child:
|
|
70
|
+
return parameters.ParameterScale(child_name, child, child_path)
|
|
71
|
+
if isinstance(child, dict) and all(
|
|
72
|
+
periods.INSTANT_PATTERN.match(str(key)) for key in child
|
|
73
|
+
):
|
|
74
|
+
return parameters.Parameter(child_name, child, child_path)
|
|
75
|
+
return parameters.ParameterNode(child_name, data=child, file_path=child_path)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _set_backward_compatibility_metadata(parameter, data) -> None:
|
|
79
|
+
if data.get("unit") is not None:
|
|
80
|
+
parameter.metadata["unit"] = data["unit"]
|
|
81
|
+
if data.get("reference") is not None:
|
|
82
|
+
parameter.metadata["reference"] = data["reference"]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _validate_parameter(parameter, data, data_type=None, allowed_keys=None) -> None:
|
|
86
|
+
type_map = {
|
|
87
|
+
dict: "object",
|
|
88
|
+
list: "array",
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if data_type is not None and not isinstance(data, data_type):
|
|
92
|
+
msg = f"'{parameter.name}' must be of type {type_map[data_type]}."
|
|
93
|
+
raise ParameterParsingError(
|
|
94
|
+
msg,
|
|
95
|
+
parameter.file_path,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if allowed_keys is not None:
|
|
99
|
+
keys = data.keys()
|
|
100
|
+
for key in keys:
|
|
101
|
+
if key not in allowed_keys:
|
|
102
|
+
msg = f"Unexpected property '{key}' in '{parameter.name}'. Allowed properties are {list(allowed_keys)}."
|
|
103
|
+
raise ParameterParsingError(
|
|
104
|
+
msg,
|
|
105
|
+
parameter.file_path,
|
|
106
|
+
)
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from openfisca_core import commons, periods
|
|
7
|
+
from openfisca_core.errors import ParameterParsingError
|
|
8
|
+
|
|
9
|
+
from . import config, helpers
|
|
10
|
+
from .at_instant_like import AtInstantLike
|
|
11
|
+
from .parameter_at_instant import ParameterAtInstant
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Parameter(AtInstantLike):
|
|
15
|
+
"""A parameter of the legislation.
|
|
16
|
+
|
|
17
|
+
Parameters can change over time.
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
values_list: List of the values, in reverse chronological order.
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
name: Name of the parameter, e.g. "taxes.some_tax.some_param".
|
|
25
|
+
data: Data loaded from a YAML file.
|
|
26
|
+
file_path: File the parameter was loaded from.
|
|
27
|
+
|
|
28
|
+
Instantiate a parameter without metadata:
|
|
29
|
+
|
|
30
|
+
>>> Parameter('rate', data = {
|
|
31
|
+
"2015-01-01": 550,
|
|
32
|
+
"2016-01-01": 600
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
Instantiate a parameter with metadata:
|
|
36
|
+
|
|
37
|
+
>>> Parameter('rate', data = {
|
|
38
|
+
'description': 'Income tax rate applied on salaries',
|
|
39
|
+
'values': {
|
|
40
|
+
"2015-01-01": {'value': 550, 'metadata': {'reference': 'http://taxes.gov/income_tax/2015'}},
|
|
41
|
+
"2016-01-01": {'value': 600, 'metadata': {'reference': 'http://taxes.gov/income_tax/2016'}}
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, name: str, data: dict, file_path: str | None = None) -> None:
|
|
48
|
+
self.name: str = name
|
|
49
|
+
self.file_path: str | None = file_path
|
|
50
|
+
helpers._validate_parameter(self, data, data_type=dict)
|
|
51
|
+
self.description: str | None = None
|
|
52
|
+
self.metadata: dict = {}
|
|
53
|
+
self.documentation: str | None = None
|
|
54
|
+
self.values_history = self # Only for backward compatibility
|
|
55
|
+
|
|
56
|
+
# Normal parameter declaration: the values are declared under the 'values' key: parse the description and metadata.
|
|
57
|
+
if data.get("values"):
|
|
58
|
+
# 'unit' and 'reference' are only listed here for backward compatibility
|
|
59
|
+
helpers._validate_parameter(
|
|
60
|
+
self,
|
|
61
|
+
data,
|
|
62
|
+
allowed_keys=config.COMMON_KEYS.union({"values"}),
|
|
63
|
+
)
|
|
64
|
+
self.description = data.get("description")
|
|
65
|
+
|
|
66
|
+
helpers._set_backward_compatibility_metadata(self, data)
|
|
67
|
+
self.metadata.update(data.get("metadata", {}))
|
|
68
|
+
|
|
69
|
+
helpers._validate_parameter(self, data["values"], data_type=dict)
|
|
70
|
+
values = data["values"]
|
|
71
|
+
|
|
72
|
+
self.documentation = data.get("documentation")
|
|
73
|
+
|
|
74
|
+
else: # Simplified parameter declaration: only values are provided
|
|
75
|
+
values = data
|
|
76
|
+
|
|
77
|
+
instants = sorted(
|
|
78
|
+
values.keys(),
|
|
79
|
+
reverse=True,
|
|
80
|
+
) # sort in reverse chronological order
|
|
81
|
+
|
|
82
|
+
values_list = []
|
|
83
|
+
for instant_str in instants:
|
|
84
|
+
if not periods.INSTANT_PATTERN.match(instant_str):
|
|
85
|
+
msg = f"Invalid property '{instant_str}' in '{self.name}'. Properties must be valid YYYY-MM-DD instants, such as 2017-01-15."
|
|
86
|
+
raise ParameterParsingError(
|
|
87
|
+
msg,
|
|
88
|
+
file_path,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
instant_info = values[instant_str]
|
|
92
|
+
|
|
93
|
+
# Ignore expected values, as they are just metadata
|
|
94
|
+
if (
|
|
95
|
+
instant_info == "expected"
|
|
96
|
+
or isinstance(instant_info, dict)
|
|
97
|
+
and instant_info.get("expected")
|
|
98
|
+
):
|
|
99
|
+
continue
|
|
100
|
+
|
|
101
|
+
value_name = helpers._compose_name(name, item_name=instant_str)
|
|
102
|
+
value_at_instant = ParameterAtInstant(
|
|
103
|
+
value_name,
|
|
104
|
+
instant_str,
|
|
105
|
+
data=instant_info,
|
|
106
|
+
file_path=self.file_path,
|
|
107
|
+
metadata=self.metadata,
|
|
108
|
+
)
|
|
109
|
+
values_list.append(value_at_instant)
|
|
110
|
+
|
|
111
|
+
self.values_list: list[ParameterAtInstant] = values_list
|
|
112
|
+
|
|
113
|
+
def __repr__(self) -> str:
|
|
114
|
+
return os.linesep.join(
|
|
115
|
+
[
|
|
116
|
+
"{}: {}".format(
|
|
117
|
+
value.instant_str,
|
|
118
|
+
value.value if value.value is not None else "null",
|
|
119
|
+
)
|
|
120
|
+
for value in self.values_list
|
|
121
|
+
],
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def __eq__(self, other):
|
|
125
|
+
return (self.name == other.name) and (self.values_list == other.values_list)
|
|
126
|
+
|
|
127
|
+
def clone(self):
|
|
128
|
+
clone = commons.empty_clone(self)
|
|
129
|
+
clone.__dict__ = self.__dict__.copy()
|
|
130
|
+
|
|
131
|
+
clone.metadata = copy.deepcopy(self.metadata)
|
|
132
|
+
clone.values_list = [
|
|
133
|
+
parameter_at_instant.clone() for parameter_at_instant in self.values_list
|
|
134
|
+
]
|
|
135
|
+
return clone
|
|
136
|
+
|
|
137
|
+
def update(self, period=None, start=None, stop=None, value=None) -> None:
|
|
138
|
+
"""Change the value for a given period.
|
|
139
|
+
|
|
140
|
+
:param period: Period where the value is modified. If set, `start` and `stop` should be `None`.
|
|
141
|
+
:param start: Start of the period. Instance of `openfisca_core.periods.Instant`. If set, `period` should be `None`.
|
|
142
|
+
:param stop: Stop of the period. Instance of `openfisca_core.periods.Instant`. If set, `period` should be `None`.
|
|
143
|
+
:param value: New value. If `None`, the parameter is removed from the legislation parameters for the given period.
|
|
144
|
+
"""
|
|
145
|
+
if period is not None:
|
|
146
|
+
if start is not None or stop is not None:
|
|
147
|
+
msg = "Wrong input for 'update' method: use either 'update(period, value = value)' or 'update(start = start, stop = stop, value = value)'. You cannot both use 'period' and 'start' or 'stop'."
|
|
148
|
+
raise TypeError(
|
|
149
|
+
msg,
|
|
150
|
+
)
|
|
151
|
+
if isinstance(period, str):
|
|
152
|
+
period = periods.period(period)
|
|
153
|
+
start = period.start
|
|
154
|
+
stop = period.stop
|
|
155
|
+
if start is None:
|
|
156
|
+
msg = "You must provide either a start or a period"
|
|
157
|
+
raise ValueError(msg)
|
|
158
|
+
start_str = str(start)
|
|
159
|
+
stop_str = str(stop.offset(1, "day")) if stop else None
|
|
160
|
+
|
|
161
|
+
old_values = self.values_list
|
|
162
|
+
new_values = []
|
|
163
|
+
n = len(old_values)
|
|
164
|
+
i = 0
|
|
165
|
+
|
|
166
|
+
# Future intervals : not affected
|
|
167
|
+
if stop_str:
|
|
168
|
+
while (i < n) and (old_values[i].instant_str >= stop_str):
|
|
169
|
+
new_values.append(old_values[i])
|
|
170
|
+
i += 1
|
|
171
|
+
|
|
172
|
+
# Right-overlapped interval
|
|
173
|
+
if stop_str:
|
|
174
|
+
if new_values and (stop_str == new_values[-1].instant_str):
|
|
175
|
+
pass # such interval is empty
|
|
176
|
+
elif i < n:
|
|
177
|
+
overlapped_value = old_values[i].value
|
|
178
|
+
value_name = helpers._compose_name(self.name, item_name=stop_str)
|
|
179
|
+
new_interval = ParameterAtInstant(
|
|
180
|
+
value_name,
|
|
181
|
+
stop_str,
|
|
182
|
+
data={"value": overlapped_value},
|
|
183
|
+
)
|
|
184
|
+
new_values.append(new_interval)
|
|
185
|
+
else:
|
|
186
|
+
value_name = helpers._compose_name(self.name, item_name=stop_str)
|
|
187
|
+
new_interval = ParameterAtInstant(
|
|
188
|
+
value_name,
|
|
189
|
+
stop_str,
|
|
190
|
+
data={"value": None},
|
|
191
|
+
)
|
|
192
|
+
new_values.append(new_interval)
|
|
193
|
+
|
|
194
|
+
# Insert new interval
|
|
195
|
+
value_name = helpers._compose_name(self.name, item_name=start_str)
|
|
196
|
+
new_interval = ParameterAtInstant(value_name, start_str, data={"value": value})
|
|
197
|
+
new_values.append(new_interval)
|
|
198
|
+
|
|
199
|
+
# Remove covered intervals
|
|
200
|
+
while (i < n) and (old_values[i].instant_str >= start_str):
|
|
201
|
+
i += 1
|
|
202
|
+
|
|
203
|
+
# Past intervals : not affected
|
|
204
|
+
while i < n:
|
|
205
|
+
new_values.append(old_values[i])
|
|
206
|
+
i += 1
|
|
207
|
+
|
|
208
|
+
self.values_list = new_values
|
|
209
|
+
|
|
210
|
+
def get_descendants(self):
|
|
211
|
+
return iter(())
|
|
212
|
+
|
|
213
|
+
def _get_at_instant(self, instant):
|
|
214
|
+
for value_at_instant in self.values_list:
|
|
215
|
+
if value_at_instant.instant_str <= instant:
|
|
216
|
+
return value_at_instant.value
|
|
217
|
+
return None
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
|
|
3
|
+
from openfisca_core import commons
|
|
4
|
+
from openfisca_core.errors import ParameterParsingError
|
|
5
|
+
from openfisca_core.parameters import config, helpers
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ParameterAtInstant:
|
|
9
|
+
"""A value of a parameter at a given instant."""
|
|
10
|
+
|
|
11
|
+
# 'unit' and 'reference' are only listed here for backward compatibility
|
|
12
|
+
_allowed_keys = {"value", "metadata", "unit", "reference"}
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self, name, instant_str, data=None, file_path=None, metadata=None
|
|
16
|
+
) -> None:
|
|
17
|
+
""":param str name: name of the parameter, e.g. "taxes.some_tax.some_param"
|
|
18
|
+
:param str instant_str: Date of the value in the format `YYYY-MM-DD`.
|
|
19
|
+
:param dict data: Data, usually loaded from a YAML file.
|
|
20
|
+
"""
|
|
21
|
+
self.name: str = name
|
|
22
|
+
self.instant_str: str = instant_str
|
|
23
|
+
self.file_path: str = file_path
|
|
24
|
+
self.metadata: dict = {}
|
|
25
|
+
|
|
26
|
+
# Accept { 2015-01-01: 4000 }
|
|
27
|
+
if not isinstance(data, dict) and isinstance(data, config.ALLOWED_PARAM_TYPES):
|
|
28
|
+
self.value = data
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
self.validate(data)
|
|
32
|
+
self.value: float = data["value"]
|
|
33
|
+
|
|
34
|
+
if metadata is not None:
|
|
35
|
+
self.metadata.update(metadata) # Inherit metadata from Parameter
|
|
36
|
+
helpers._set_backward_compatibility_metadata(self, data)
|
|
37
|
+
self.metadata.update(data.get("metadata", {}))
|
|
38
|
+
|
|
39
|
+
def validate(self, data) -> None:
|
|
40
|
+
helpers._validate_parameter(
|
|
41
|
+
self,
|
|
42
|
+
data,
|
|
43
|
+
data_type=dict,
|
|
44
|
+
allowed_keys=self._allowed_keys,
|
|
45
|
+
)
|
|
46
|
+
try:
|
|
47
|
+
value = data["value"]
|
|
48
|
+
except KeyError:
|
|
49
|
+
msg = f"Missing 'value' property for {self.name}"
|
|
50
|
+
raise ParameterParsingError(
|
|
51
|
+
msg,
|
|
52
|
+
self.file_path,
|
|
53
|
+
)
|
|
54
|
+
if not isinstance(value, config.ALLOWED_PARAM_TYPES):
|
|
55
|
+
msg = f"Value in {self.name} has type {type(value)}, which is not one of the allowed types ({config.ALLOWED_PARAM_TYPES}): {value}"
|
|
56
|
+
raise ParameterParsingError(
|
|
57
|
+
msg,
|
|
58
|
+
self.file_path,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def __eq__(self, other):
|
|
62
|
+
return (
|
|
63
|
+
(self.name == other.name)
|
|
64
|
+
and (self.instant_str == other.instant_str)
|
|
65
|
+
and (self.value == other.value)
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def __repr__(self) -> str:
|
|
69
|
+
return "ParameterAtInstant({self.instant_str: self.value})"
|
|
70
|
+
|
|
71
|
+
def clone(self):
|
|
72
|
+
clone = commons.empty_clone(self)
|
|
73
|
+
clone.__dict__ = self.__dict__.copy()
|
|
74
|
+
clone.metadata = copy.deepcopy(self.metadata)
|
|
75
|
+
return clone
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import os
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
|
|
7
|
+
from openfisca_core import commons, parameters, tools
|
|
8
|
+
|
|
9
|
+
from . import config, helpers
|
|
10
|
+
from .at_instant_like import AtInstantLike
|
|
11
|
+
from .parameter import Parameter
|
|
12
|
+
from .parameter_node_at_instant import ParameterNodeAtInstant
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ParameterNode(AtInstantLike):
|
|
16
|
+
"""A node in the legislation `parameter tree <https://openfisca.org/doc/coding-the-legislation/legislation_parameters.html>`_."""
|
|
17
|
+
|
|
18
|
+
_allowed_keys: None | Iterable[str] = None # By default, no restriction on the keys
|
|
19
|
+
|
|
20
|
+
def __init__(self, name="", directory_path=None, data=None, file_path=None) -> None:
|
|
21
|
+
"""Instantiate a ParameterNode either from a dict, (using `data`), or from a directory containing YAML files (using `directory_path`).
|
|
22
|
+
|
|
23
|
+
:param str name: Name of the node, eg "taxes.some_tax".
|
|
24
|
+
:param str directory_path: Directory containing YAML files describing the node.
|
|
25
|
+
:param dict data: Object representing the parameter node. It usually has been extracted from a YAML file.
|
|
26
|
+
:param str file_path: YAML file from which the `data` has been extracted from.
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
Instantiate a ParameterNode from a dict:
|
|
30
|
+
|
|
31
|
+
>>> node = ParameterNode('basic_income', data = {
|
|
32
|
+
'amount': {
|
|
33
|
+
'values': {
|
|
34
|
+
"2015-01-01": {'value': 550},
|
|
35
|
+
"2016-01-01": {'value': 600}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
'min_age': {
|
|
39
|
+
'values': {
|
|
40
|
+
"2015-01-01": {'value': 25},
|
|
41
|
+
"2016-01-01": {'value': 18}
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
Instantiate a ParameterNode from a directory containing YAML parameter files:
|
|
47
|
+
|
|
48
|
+
>>> node = ParameterNode(
|
|
49
|
+
... "benefits",
|
|
50
|
+
... directory_path="/path/to/country_package/parameters/benefits",
|
|
51
|
+
... )
|
|
52
|
+
"""
|
|
53
|
+
self.name: str = name
|
|
54
|
+
self.children: dict[
|
|
55
|
+
str,
|
|
56
|
+
ParameterNode | Parameter | parameters.ParameterScale,
|
|
57
|
+
] = {}
|
|
58
|
+
self.description: str = None
|
|
59
|
+
self.documentation: str = None
|
|
60
|
+
self.file_path: str = None
|
|
61
|
+
self.metadata: dict = {}
|
|
62
|
+
|
|
63
|
+
if directory_path:
|
|
64
|
+
self.file_path = directory_path
|
|
65
|
+
for child_name in os.listdir(directory_path):
|
|
66
|
+
child_path = os.path.join(directory_path, child_name)
|
|
67
|
+
if os.path.isfile(child_path):
|
|
68
|
+
child_name, ext = os.path.splitext(child_name)
|
|
69
|
+
|
|
70
|
+
# We ignore non-YAML files
|
|
71
|
+
if ext not in config.FILE_EXTENSIONS:
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
if child_name == "index":
|
|
75
|
+
data = helpers._load_yaml_file(child_path) or {}
|
|
76
|
+
helpers._validate_parameter(
|
|
77
|
+
self,
|
|
78
|
+
data,
|
|
79
|
+
allowed_keys=config.COMMON_KEYS,
|
|
80
|
+
)
|
|
81
|
+
self.description = data.get("description")
|
|
82
|
+
self.documentation = data.get("documentation")
|
|
83
|
+
helpers._set_backward_compatibility_metadata(self, data)
|
|
84
|
+
self.metadata.update(data.get("metadata", {}))
|
|
85
|
+
else:
|
|
86
|
+
child_name_expanded = helpers._compose_name(name, child_name)
|
|
87
|
+
child = helpers.load_parameter_file(
|
|
88
|
+
child_path,
|
|
89
|
+
child_name_expanded,
|
|
90
|
+
)
|
|
91
|
+
self.add_child(child_name, child)
|
|
92
|
+
|
|
93
|
+
elif os.path.isdir(child_path):
|
|
94
|
+
child_name = os.path.basename(child_path)
|
|
95
|
+
child_name_expanded = helpers._compose_name(name, child_name)
|
|
96
|
+
child = ParameterNode(
|
|
97
|
+
child_name_expanded,
|
|
98
|
+
directory_path=child_path,
|
|
99
|
+
)
|
|
100
|
+
self.add_child(child_name, child)
|
|
101
|
+
|
|
102
|
+
else:
|
|
103
|
+
self.file_path = file_path
|
|
104
|
+
helpers._validate_parameter(
|
|
105
|
+
self,
|
|
106
|
+
data,
|
|
107
|
+
data_type=dict,
|
|
108
|
+
allowed_keys=self._allowed_keys,
|
|
109
|
+
)
|
|
110
|
+
self.description = data.get("description")
|
|
111
|
+
self.documentation = data.get("documentation")
|
|
112
|
+
helpers._set_backward_compatibility_metadata(self, data)
|
|
113
|
+
self.metadata.update(data.get("metadata", {}))
|
|
114
|
+
for child_name, child in data.items():
|
|
115
|
+
if child_name in config.COMMON_KEYS:
|
|
116
|
+
continue # do not treat reserved keys as subparameters.
|
|
117
|
+
|
|
118
|
+
child_name = str(child_name)
|
|
119
|
+
child_name_expanded = helpers._compose_name(name, child_name)
|
|
120
|
+
child = helpers._parse_child(child_name_expanded, child, file_path)
|
|
121
|
+
self.add_child(child_name, child)
|
|
122
|
+
|
|
123
|
+
def merge(self, other) -> None:
|
|
124
|
+
"""Merges another ParameterNode into the current node.
|
|
125
|
+
|
|
126
|
+
In case of child name conflict, the other node child will replace the current node child.
|
|
127
|
+
"""
|
|
128
|
+
for child_name, child in other.children.items():
|
|
129
|
+
self.add_child(child_name, child)
|
|
130
|
+
|
|
131
|
+
def add_child(self, name, child) -> None:
|
|
132
|
+
"""Add a new child to the node.
|
|
133
|
+
|
|
134
|
+
:param name: Name of the child that must be used to access that child. Should not contain anything that could interfere with the operator `.` (dot).
|
|
135
|
+
:param child: The new child, an instance of :class:`.ParameterScale` or :class:`.Parameter` or :class:`.ParameterNode`.
|
|
136
|
+
"""
|
|
137
|
+
if name in self.children:
|
|
138
|
+
msg = f"{self.name} has already a child named {name}"
|
|
139
|
+
raise ValueError(msg)
|
|
140
|
+
if not (
|
|
141
|
+
isinstance(child, (ParameterNode, Parameter, parameters.ParameterScale))
|
|
142
|
+
):
|
|
143
|
+
msg = f"child must be of type ParameterNode, Parameter, or Scale. Instead got {type(child)}"
|
|
144
|
+
raise TypeError(
|
|
145
|
+
msg,
|
|
146
|
+
)
|
|
147
|
+
self.children[name] = child
|
|
148
|
+
setattr(self, name, child)
|
|
149
|
+
|
|
150
|
+
def __repr__(self) -> str:
|
|
151
|
+
return os.linesep.join(
|
|
152
|
+
[
|
|
153
|
+
os.linesep.join(["{}:", "{}"]).format(name, tools.indent(repr(value)))
|
|
154
|
+
for name, value in sorted(self.children.items())
|
|
155
|
+
],
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
def get_descendants(self):
|
|
159
|
+
"""Return a generator containing all the parameters and nodes recursively contained in this `ParameterNode`."""
|
|
160
|
+
for child in self.children.values():
|
|
161
|
+
yield child
|
|
162
|
+
yield from child.get_descendants()
|
|
163
|
+
|
|
164
|
+
def clone(self):
|
|
165
|
+
clone = commons.empty_clone(self)
|
|
166
|
+
clone.__dict__ = self.__dict__.copy()
|
|
167
|
+
|
|
168
|
+
clone.metadata = copy.deepcopy(self.metadata)
|
|
169
|
+
clone.children = {key: child.clone() for key, child in self.children.items()}
|
|
170
|
+
for child_key, child in clone.children.items():
|
|
171
|
+
setattr(clone, child_key, child)
|
|
172
|
+
|
|
173
|
+
return clone
|
|
174
|
+
|
|
175
|
+
def _get_at_instant(self, instant):
|
|
176
|
+
return ParameterNodeAtInstant(self.name, self, instant)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
import numpy
|
|
4
|
+
|
|
5
|
+
from openfisca_core import parameters, tools
|
|
6
|
+
from openfisca_core.errors import ParameterNotFoundError
|
|
7
|
+
from openfisca_core.parameters import helpers
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ParameterNodeAtInstant:
|
|
11
|
+
"""Parameter node of the legislation, at a given instant."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, name, node, instant_str) -> None:
|
|
14
|
+
""":param name: Name of the node.
|
|
15
|
+
:param node: Original :any:`ParameterNode` instance.
|
|
16
|
+
:param instant_str: A date in the format `YYYY-MM-DD`.
|
|
17
|
+
"""
|
|
18
|
+
# The "technical" attributes are hidden, so that the node children can be easily browsed with auto-completion without pollution
|
|
19
|
+
self._name = name
|
|
20
|
+
self._instant_str = instant_str
|
|
21
|
+
self._children = {}
|
|
22
|
+
|
|
23
|
+
for child_name, child in node.children.items():
|
|
24
|
+
child_at_instant = child._get_at_instant(instant_str)
|
|
25
|
+
if child_at_instant is not None:
|
|
26
|
+
self.add_child(child_name, child_at_instant)
|
|
27
|
+
|
|
28
|
+
def add_child(self, child_name, child_at_instant) -> None:
|
|
29
|
+
self._children[child_name] = child_at_instant
|
|
30
|
+
setattr(self, child_name, child_at_instant)
|
|
31
|
+
|
|
32
|
+
def __getattr__(self, key):
|
|
33
|
+
param_name = helpers._compose_name(self._name, item_name=key)
|
|
34
|
+
raise ParameterNotFoundError(param_name, self._instant_str)
|
|
35
|
+
|
|
36
|
+
def __getitem__(self, key):
|
|
37
|
+
# If fancy indexing is used, cast to a vectorial node
|
|
38
|
+
if isinstance(key, numpy.ndarray):
|
|
39
|
+
return parameters.VectorialParameterNodeAtInstant.build_from_node_name(
|
|
40
|
+
self, key
|
|
41
|
+
)
|
|
42
|
+
try:
|
|
43
|
+
return self._children[key]
|
|
44
|
+
except KeyError:
|
|
45
|
+
param_name = helpers._compose_name(self._name, item_name=key)
|
|
46
|
+
raise ParameterNotFoundError(param_name, self._instant_str)
|
|
47
|
+
|
|
48
|
+
def __iter__(self):
|
|
49
|
+
return iter(self._children)
|
|
50
|
+
|
|
51
|
+
def __repr__(self) -> str:
|
|
52
|
+
return os.linesep.join(
|
|
53
|
+
[
|
|
54
|
+
os.linesep.join(["{}:", "{}"]).format(name, tools.indent(repr(value)))
|
|
55
|
+
for name, value in self._children.items()
|
|
56
|
+
],
|
|
57
|
+
)
|