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.
Files changed (237) hide show
  1. openfisca_core/__init__.py +0 -0
  2. openfisca_core/commons/__init__.py +18 -0
  3. openfisca_core/commons/dummy.py +25 -0
  4. openfisca_core/commons/formulas.py +121 -0
  5. openfisca_core/commons/misc.py +109 -0
  6. openfisca_core/commons/py.typed +0 -0
  7. openfisca_core/commons/rates.py +114 -0
  8. openfisca_core/commons/tests/__init__.py +0 -0
  9. openfisca_core/commons/tests/test_dummy.py +9 -0
  10. openfisca_core/commons/tests/test_formulas.py +75 -0
  11. openfisca_core/commons/tests/test_rates.py +26 -0
  12. openfisca_core/data_storage/__init__.py +7 -0
  13. openfisca_core/data_storage/in_memory_storage.py +198 -0
  14. openfisca_core/data_storage/on_disk_storage.py +307 -0
  15. openfisca_core/data_storage/types.py +14 -0
  16. openfisca_core/entities/__init__.py +23 -0
  17. openfisca_core/entities/_core_entity.py +217 -0
  18. openfisca_core/entities/_description.py +55 -0
  19. openfisca_core/entities/entity.py +60 -0
  20. openfisca_core/entities/group_entity.py +123 -0
  21. openfisca_core/entities/helpers.py +165 -0
  22. openfisca_core/entities/py.typed +0 -0
  23. openfisca_core/entities/role.py +92 -0
  24. openfisca_core/entities/tests/__init__.py +0 -0
  25. openfisca_core/entities/tests/test_entity.py +10 -0
  26. openfisca_core/entities/tests/test_group_entity.py +70 -0
  27. openfisca_core/entities/tests/test_role.py +11 -0
  28. openfisca_core/entities/types.py +44 -0
  29. openfisca_core/errors/__init__.py +64 -0
  30. openfisca_core/errors/cycle_error.py +2 -0
  31. openfisca_core/errors/empty_argument_error.py +39 -0
  32. openfisca_core/errors/nan_creation_error.py +2 -0
  33. openfisca_core/errors/parameter_not_found_error.py +16 -0
  34. openfisca_core/errors/parameter_parsing_error.py +18 -0
  35. openfisca_core/errors/period_mismatch_error.py +9 -0
  36. openfisca_core/errors/situation_parsing_error.py +26 -0
  37. openfisca_core/errors/spiral_error.py +2 -0
  38. openfisca_core/errors/variable_name_config_error.py +2 -0
  39. openfisca_core/errors/variable_not_found_error.py +29 -0
  40. openfisca_core/experimental/__init__.py +9 -0
  41. openfisca_core/experimental/_errors.py +5 -0
  42. openfisca_core/experimental/_memory_config.py +41 -0
  43. openfisca_core/formula_helpers.py +9 -0
  44. openfisca_core/holders/__init__.py +33 -0
  45. openfisca_core/holders/helpers.py +93 -0
  46. openfisca_core/holders/holder.py +328 -0
  47. openfisca_core/holders/tests/__init__.py +0 -0
  48. openfisca_core/holders/tests/test_helpers.py +134 -0
  49. openfisca_core/holders/types.py +3 -0
  50. openfisca_core/indexed_enums/__init__.py +18 -0
  51. openfisca_core/indexed_enums/_enum_type.py +79 -0
  52. openfisca_core/indexed_enums/_errors.py +35 -0
  53. openfisca_core/indexed_enums/_guards.py +209 -0
  54. openfisca_core/indexed_enums/_utils.py +180 -0
  55. openfisca_core/indexed_enums/config.py +6 -0
  56. openfisca_core/indexed_enums/enum.py +218 -0
  57. openfisca_core/indexed_enums/enum_array.py +316 -0
  58. openfisca_core/indexed_enums/py.typed +0 -0
  59. openfisca_core/indexed_enums/tests/__init__.py +0 -0
  60. openfisca_core/indexed_enums/tests/test_enum.py +135 -0
  61. openfisca_core/indexed_enums/tests/test_enum_array.py +30 -0
  62. openfisca_core/indexed_enums/types.py +49 -0
  63. openfisca_core/memory_config.py +9 -0
  64. openfisca_core/model_api.py +71 -0
  65. openfisca_core/parameters/__init__.py +71 -0
  66. openfisca_core/parameters/at_instant_like.py +17 -0
  67. openfisca_core/parameters/config.py +55 -0
  68. openfisca_core/parameters/helpers.py +106 -0
  69. openfisca_core/parameters/parameter.py +217 -0
  70. openfisca_core/parameters/parameter_at_instant.py +75 -0
  71. openfisca_core/parameters/parameter_node.py +176 -0
  72. openfisca_core/parameters/parameter_node_at_instant.py +57 -0
  73. openfisca_core/parameters/parameter_scale.py +121 -0
  74. openfisca_core/parameters/parameter_scale_bracket.py +7 -0
  75. openfisca_core/parameters/values_history.py +5 -0
  76. openfisca_core/parameters/vectorial_parameter_node_at_instant.py +179 -0
  77. openfisca_core/periods/__init__.py +77 -0
  78. openfisca_core/periods/_errors.py +28 -0
  79. openfisca_core/periods/_parsers.py +122 -0
  80. openfisca_core/periods/config.py +20 -0
  81. openfisca_core/periods/date_unit.py +110 -0
  82. openfisca_core/periods/helpers.py +314 -0
  83. openfisca_core/periods/instant_.py +226 -0
  84. openfisca_core/periods/period_.py +919 -0
  85. openfisca_core/periods/py.typed +0 -0
  86. openfisca_core/periods/tests/__init__.py +0 -0
  87. openfisca_core/periods/tests/helpers/__init__.py +0 -0
  88. openfisca_core/periods/tests/helpers/test_helpers.py +65 -0
  89. openfisca_core/periods/tests/helpers/test_instant.py +73 -0
  90. openfisca_core/periods/tests/helpers/test_period.py +134 -0
  91. openfisca_core/periods/tests/test_instant.py +32 -0
  92. openfisca_core/periods/tests/test_parsers.py +129 -0
  93. openfisca_core/periods/tests/test_period.py +283 -0
  94. openfisca_core/populations/__init__.py +64 -0
  95. openfisca_core/populations/_core_population.py +454 -0
  96. openfisca_core/populations/_errors.py +65 -0
  97. openfisca_core/populations/group_population.py +326 -0
  98. openfisca_core/populations/population.py +142 -0
  99. openfisca_core/populations/tests/__init__.py +0 -0
  100. openfisca_core/populations/tests/test_members_position.py +116 -0
  101. openfisca_core/populations/types.py +110 -0
  102. openfisca_core/projectors/__init__.py +39 -0
  103. openfisca_core/projectors/entity_to_person_projector.py +12 -0
  104. openfisca_core/projectors/first_person_to_entity_projector.py +13 -0
  105. openfisca_core/projectors/helpers.py +139 -0
  106. openfisca_core/projectors/projector.py +38 -0
  107. openfisca_core/projectors/typing.py +27 -0
  108. openfisca_core/projectors/unique_role_to_entity_projector.py +14 -0
  109. openfisca_core/rates.py +9 -0
  110. openfisca_core/reforms/__init__.py +24 -0
  111. openfisca_core/reforms/in_yaml_test_reform.py +28 -0
  112. openfisca_core/reforms/reform.py +86 -0
  113. openfisca_core/scripts/__init__.py +106 -0
  114. openfisca_core/scripts/__pycache__/__init__.cpython-310-pytest-8.3.3.pyc +0 -0
  115. openfisca_core/scripts/__pycache__/__init__.cpython-310-pytest-8.4.2.pyc +0 -0
  116. openfisca_core/scripts/__pycache__/__init__.cpython-310.pyc +0 -0
  117. openfisca_core/scripts/__pycache__/__init__.cpython-311-pytest-8.3.3.pyc +0 -0
  118. openfisca_core/scripts/__pycache__/__init__.cpython-311-pytest-8.4.2.pyc +0 -0
  119. openfisca_core/scripts/__pycache__/__init__.cpython-311.pyc +0 -0
  120. openfisca_core/scripts/__pycache__/__init__.cpython-312-pytest-8.4.2.pyc +0 -0
  121. openfisca_core/scripts/__pycache__/__init__.cpython-312.pyc +0 -0
  122. openfisca_core/scripts/__pycache__/__init__.cpython-313-pytest-8.3.3.pyc +0 -0
  123. openfisca_core/scripts/__pycache__/__init__.cpython-313-pytest-8.4.2.pyc +0 -0
  124. openfisca_core/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
  125. openfisca_core/scripts/__pycache__/find_placeholders.cpython-311.pyc +0 -0
  126. openfisca_core/scripts/__pycache__/find_placeholders.cpython-313.pyc +0 -0
  127. openfisca_core/scripts/__pycache__/measure_numpy_condition_notations.cpython-311.pyc +0 -0
  128. openfisca_core/scripts/__pycache__/measure_numpy_condition_notations.cpython-313.pyc +0 -0
  129. openfisca_core/scripts/__pycache__/measure_performances.cpython-311.pyc +0 -0
  130. openfisca_core/scripts/__pycache__/measure_performances.cpython-313.pyc +0 -0
  131. openfisca_core/scripts/__pycache__/measure_performances_fancy_indexing.cpython-311.pyc +0 -0
  132. openfisca_core/scripts/__pycache__/measure_performances_fancy_indexing.cpython-313.pyc +0 -0
  133. openfisca_core/scripts/__pycache__/openfisca_command.cpython-310.pyc +0 -0
  134. openfisca_core/scripts/__pycache__/openfisca_command.cpython-311.pyc +0 -0
  135. openfisca_core/scripts/__pycache__/openfisca_command.cpython-312.pyc +0 -0
  136. openfisca_core/scripts/__pycache__/openfisca_command.cpython-313.pyc +0 -0
  137. openfisca_core/scripts/__pycache__/remove_fuzzy.cpython-311.pyc +0 -0
  138. openfisca_core/scripts/__pycache__/remove_fuzzy.cpython-313.pyc +0 -0
  139. openfisca_core/scripts/__pycache__/run_test.cpython-310.pyc +0 -0
  140. openfisca_core/scripts/__pycache__/run_test.cpython-311.pyc +0 -0
  141. openfisca_core/scripts/__pycache__/run_test.cpython-312.pyc +0 -0
  142. openfisca_core/scripts/__pycache__/run_test.cpython-313.pyc +0 -0
  143. openfisca_core/scripts/__pycache__/simulation_generator.cpython-311.pyc +0 -0
  144. openfisca_core/scripts/__pycache__/simulation_generator.cpython-313.pyc +0 -0
  145. openfisca_core/scripts/assets/__init__.py +0 -0
  146. openfisca_core/scripts/assets/__pycache__/__init__.cpython-311.pyc +0 -0
  147. openfisca_core/scripts/assets/__pycache__/__init__.cpython-313.pyc +0 -0
  148. openfisca_core/scripts/assets/index.html +140 -0
  149. openfisca_core/scripts/find_placeholders.py +51 -0
  150. openfisca_core/scripts/measure_numpy_condition_notations.py +137 -0
  151. openfisca_core/scripts/measure_performances.py +291 -0
  152. openfisca_core/scripts/measure_performances_fancy_indexing.py +89 -0
  153. openfisca_core/scripts/migrations/__init__.py +0 -0
  154. openfisca_core/scripts/migrations/__pycache__/__init__.cpython-311.pyc +0 -0
  155. openfisca_core/scripts/migrations/__pycache__/__init__.cpython-313.pyc +0 -0
  156. openfisca_core/scripts/migrations/__pycache__/v24_to_25.cpython-311.pyc +0 -0
  157. openfisca_core/scripts/migrations/__pycache__/v24_to_25.cpython-313.pyc +0 -0
  158. openfisca_core/scripts/migrations/v16_2_to_v17/__init__.py +0 -0
  159. openfisca_core/scripts/migrations/v16_2_to_v17/__pycache__/__init__.cpython-311.pyc +0 -0
  160. openfisca_core/scripts/migrations/v16_2_to_v17/__pycache__/__init__.cpython-313.pyc +0 -0
  161. openfisca_core/scripts/migrations/v16_2_to_v17/__pycache__/xml_to_yaml_country_template.cpython-311.pyc +0 -0
  162. openfisca_core/scripts/migrations/v16_2_to_v17/__pycache__/xml_to_yaml_country_template.cpython-313.pyc +0 -0
  163. openfisca_core/scripts/migrations/v16_2_to_v17/__pycache__/xml_to_yaml_extension_template.cpython-311.pyc +0 -0
  164. openfisca_core/scripts/migrations/v16_2_to_v17/__pycache__/xml_to_yaml_extension_template.cpython-313.pyc +0 -0
  165. openfisca_core/scripts/migrations/v16_2_to_v17/legislation.xsd +102 -0
  166. openfisca_core/scripts/migrations/v16_2_to_v17/xml_to_yaml_country_template.py +30 -0
  167. openfisca_core/scripts/migrations/v16_2_to_v17/xml_to_yaml_extension_template.py +26 -0
  168. openfisca_core/scripts/migrations/v24_to_25.py +156 -0
  169. openfisca_core/scripts/openfisca_command.py +192 -0
  170. openfisca_core/scripts/remove_fuzzy.py +213 -0
  171. openfisca_core/scripts/run_test.py +51 -0
  172. openfisca_core/scripts/simulation_generator.py +90 -0
  173. openfisca_core/simulation_builder.py +16 -0
  174. openfisca_core/simulations/__init__.py +45 -0
  175. openfisca_core/simulations/_build_default_simulation.py +158 -0
  176. openfisca_core/simulations/_build_from_variables.py +230 -0
  177. openfisca_core/simulations/_type_guards.py +298 -0
  178. openfisca_core/simulations/helpers.py +111 -0
  179. openfisca_core/simulations/simulation.py +618 -0
  180. openfisca_core/simulations/simulation_builder.py +855 -0
  181. openfisca_core/simulations/typing.py +203 -0
  182. openfisca_core/taxbenefitsystems/__init__.py +26 -0
  183. openfisca_core/taxbenefitsystems/tax_benefit_system.py +584 -0
  184. openfisca_core/taxscales/__init__.py +35 -0
  185. openfisca_core/taxscales/abstract_rate_tax_scale.py +41 -0
  186. openfisca_core/taxscales/abstract_tax_scale.py +53 -0
  187. openfisca_core/taxscales/amount_tax_scale_like.py +55 -0
  188. openfisca_core/taxscales/helpers.py +44 -0
  189. openfisca_core/taxscales/linear_average_rate_tax_scale.py +76 -0
  190. openfisca_core/taxscales/marginal_amount_tax_scale.py +34 -0
  191. openfisca_core/taxscales/marginal_rate_tax_scale.py +286 -0
  192. openfisca_core/taxscales/rate_tax_scale_like.py +214 -0
  193. openfisca_core/taxscales/single_amount_tax_scale.py +32 -0
  194. openfisca_core/taxscales/tax_scale_like.py +65 -0
  195. openfisca_core/tools/__init__.py +94 -0
  196. openfisca_core/tools/parallel_plugin.py +55 -0
  197. openfisca_core/tools/simulation_dumper.py +137 -0
  198. openfisca_core/tools/test_runner.py +802 -0
  199. openfisca_core/tracers/__init__.py +40 -0
  200. openfisca_core/tracers/computation_log.py +196 -0
  201. openfisca_core/tracers/flat_trace.py +87 -0
  202. openfisca_core/tracers/full_tracer.py +172 -0
  203. openfisca_core/tracers/performance_log.py +133 -0
  204. openfisca_core/tracers/simple_tracer.py +64 -0
  205. openfisca_core/tracers/trace_node.py +121 -0
  206. openfisca_core/tracers/tracing_parameter_node_at_instant.py +81 -0
  207. openfisca_core/types.py +548 -0
  208. openfisca_core/variables/__init__.py +26 -0
  209. openfisca_core/variables/config.py +53 -0
  210. openfisca_core/variables/helpers.py +67 -0
  211. openfisca_core/variables/tests/__init__.py +0 -0
  212. openfisca_core/variables/tests/test_definition_period.py +43 -0
  213. openfisca_core/variables/variable.py +474 -0
  214. openfisca_core/warnings/__init__.py +25 -0
  215. openfisca_core/warnings/libyaml_warning.py +2 -0
  216. openfisca_core/warnings/tempfile_warning.py +2 -0
  217. openfisca_core_test-46.0.0.data/data/share/openfisca/openfisca-core/CHANGELOG.md +4209 -0
  218. openfisca_core_test-46.0.0.data/data/share/openfisca/openfisca-core/README.md +152 -0
  219. openfisca_core_test-46.0.0.dist-info/METADATA +194 -0
  220. openfisca_core_test-46.0.0.dist-info/RECORD +237 -0
  221. openfisca_core_test-46.0.0.dist-info/WHEEL +5 -0
  222. openfisca_core_test-46.0.0.dist-info/entry_points.txt +3 -0
  223. openfisca_core_test-46.0.0.dist-info/licenses/LICENSE +661 -0
  224. openfisca_core_test-46.0.0.dist-info/top_level.txt +2 -0
  225. openfisca_web_api/__init__.py +0 -0
  226. openfisca_web_api/app.py +226 -0
  227. openfisca_web_api/errors.py +11 -0
  228. openfisca_web_api/handlers.py +90 -0
  229. openfisca_web_api/loader/__init__.py +22 -0
  230. openfisca_web_api/loader/entities.py +28 -0
  231. openfisca_web_api/loader/parameters.py +122 -0
  232. openfisca_web_api/loader/spec.py +168 -0
  233. openfisca_web_api/loader/tax_benefit_system.py +27 -0
  234. openfisca_web_api/loader/variables.py +121 -0
  235. openfisca_web_api/openAPI.yml +434 -0
  236. openfisca_web_api/scripts/__init__.py +0 -0
  237. 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
+ )