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,226 @@
1
+ import logging
2
+ import os
3
+ import traceback
4
+
5
+ from openfisca_core.errors import PeriodMismatchError, SituationParsingError
6
+ from openfisca_web_api import handlers
7
+ from openfisca_web_api.errors import handle_import_error
8
+ from openfisca_web_api.loader import build_data
9
+
10
+ try:
11
+ import werkzeug.exceptions
12
+ from flask import Flask, abort, jsonify, make_response, redirect, request
13
+ from flask_cors import CORS
14
+ from werkzeug.middleware.proxy_fix import ProxyFix
15
+ except ImportError as error:
16
+ handle_import_error(error)
17
+
18
+ log = logging.getLogger("gunicorn.error")
19
+
20
+
21
+ def init_tracker(url, idsite, tracker_token):
22
+ try:
23
+ from openfisca_tracker.piwik import PiwikTracker
24
+
25
+ tracker = PiwikTracker(url, idsite, tracker_token)
26
+
27
+ info = os.linesep.join(
28
+ [
29
+ "You chose to activate the `tracker` module. ",
30
+ "Tracking data will be sent to: " + url,
31
+ "For more information, see <https://github.com/openfisca/openfisca-core#tracker-configuration>.",
32
+ ],
33
+ )
34
+ log.info(info)
35
+ return tracker
36
+
37
+ except ImportError:
38
+ message = os.linesep.join(
39
+ [
40
+ traceback.format_exc(),
41
+ "You chose to activate the `tracker` module, but it is not installed.",
42
+ "For more information, see <https://github.com/openfisca/openfisca-core#tracker-installation>.",
43
+ ],
44
+ )
45
+ log.warning(message)
46
+
47
+
48
+ def create_app(
49
+ tax_benefit_system,
50
+ tracker_url=None,
51
+ tracker_idsite=None,
52
+ tracker_token=None,
53
+ welcome_message=None,
54
+ ):
55
+ if not tracker_url or not tracker_idsite:
56
+ tracker = None
57
+ else:
58
+ tracker = init_tracker(tracker_url, tracker_idsite, tracker_token)
59
+
60
+ app = Flask(__name__)
61
+ # Fix request.remote_addr to get the real client IP address
62
+ app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_host=1)
63
+ CORS(app, origins="*")
64
+
65
+ app.config["JSON_AS_ASCII"] = False # When False, lets jsonify encode to utf-8
66
+ app.url_map.strict_slashes = False # Accept url like /parameters/
67
+ app.url_map.merge_slashes = False # Do not eliminate // in paths
68
+ app.config["JSON_SORT_KEYS"] = False # Don't sort JSON keys in the Web API
69
+
70
+ data = build_data(tax_benefit_system)
71
+
72
+ DEFAULT_WELCOME_MESSAGE = "This is the root of an OpenFisca Web API. To learn how to use it, check the general documentation (https://openfisca.org/doc/) and the OpenAPI specification of this instance ({}spec)."
73
+
74
+ @app.before_request
75
+ def before_request():
76
+ if request.path != "/" and request.path.endswith("/"):
77
+ return redirect(request.path[:-1])
78
+ return None
79
+
80
+ @app.route("/")
81
+ def get_root():
82
+ return (
83
+ jsonify(
84
+ {
85
+ "welcome": welcome_message
86
+ or DEFAULT_WELCOME_MESSAGE.format(request.host_url),
87
+ },
88
+ ),
89
+ 300,
90
+ )
91
+
92
+ @app.route("/parameters")
93
+ def get_parameters():
94
+ parameters = {
95
+ parameter["id"]: {
96
+ "description": parameter["description"],
97
+ "href": f"{request.host_url}parameter/{name}",
98
+ }
99
+ for name, parameter in data["parameters"].items()
100
+ if parameter.get("subparams")
101
+ is None # For now and for backward compat, don't show nodes in overview
102
+ }
103
+
104
+ return jsonify(parameters)
105
+
106
+ @app.route("/parameter/<path:parameter_id>")
107
+ def get_parameter(parameter_id):
108
+ parameter = data["parameters"].get(parameter_id)
109
+ if parameter is None:
110
+ # Try legacy route
111
+ parameter_new_id = parameter_id.replace(".", "/")
112
+ parameter = data["parameters"].get(parameter_new_id)
113
+ if parameter is None:
114
+ raise abort(404)
115
+ return jsonify(parameter)
116
+
117
+ @app.route("/variables")
118
+ def get_variables():
119
+ variables = {
120
+ name: {
121
+ "description": variable["description"],
122
+ "href": f"{request.host_url}variable/{name}",
123
+ }
124
+ for name, variable in data["variables"].items()
125
+ }
126
+ return jsonify(variables)
127
+
128
+ @app.route("/variable/<id>")
129
+ def get_variable(id):
130
+ variable = data["variables"].get(id)
131
+ if variable is None:
132
+ raise abort(404)
133
+ return jsonify(variable)
134
+
135
+ @app.route("/entities")
136
+ def get_entities():
137
+ return jsonify(data["entities"])
138
+
139
+ @app.route("/spec")
140
+ def get_spec():
141
+ scheme = request.environ["wsgi.url_scheme"]
142
+ host = request.host
143
+ url = f"{scheme}://{host}"
144
+
145
+ return jsonify(
146
+ {
147
+ **data["openAPI_spec"],
148
+ "servers": [{"url": url}],
149
+ },
150
+ )
151
+
152
+ def handle_invalid_json(error) -> None:
153
+ json_response = jsonify(
154
+ {
155
+ "error": f"Invalid JSON: {error.args[0]}",
156
+ },
157
+ )
158
+
159
+ abort(make_response(json_response, 400))
160
+
161
+ @app.route("/calculate", methods=["POST"])
162
+ def calculate():
163
+ tax_benefit_system = data["tax_benefit_system"]
164
+ request.on_json_loading_failed = handle_invalid_json
165
+ input_data = request.get_json()
166
+ try:
167
+ result = handlers.calculate(tax_benefit_system, input_data)
168
+ except (SituationParsingError, PeriodMismatchError) as e:
169
+ abort(make_response(jsonify(e.error), e.code or 400))
170
+ except (UnicodeEncodeError, UnicodeDecodeError) as e:
171
+ abort(
172
+ make_response(
173
+ jsonify({"error": "'" + e[1] + "' is not a valid ASCII value."}),
174
+ 400,
175
+ ),
176
+ )
177
+ return jsonify(result)
178
+
179
+ @app.route("/trace", methods=["POST"])
180
+ def trace():
181
+ tax_benefit_system = data["tax_benefit_system"]
182
+ request.on_json_loading_failed = handle_invalid_json
183
+ input_data = request.get_json()
184
+ try:
185
+ result = handlers.trace(tax_benefit_system, input_data)
186
+ except (SituationParsingError, PeriodMismatchError) as e:
187
+ abort(make_response(jsonify(e.error), e.code or 400))
188
+ return jsonify(result)
189
+
190
+ @app.after_request
191
+ def apply_headers(response):
192
+ response.headers.extend(
193
+ {
194
+ "Country-Package": data["country_package_metadata"]["name"],
195
+ "Country-Package-Version": data["country_package_metadata"]["version"],
196
+ },
197
+ )
198
+ return response
199
+
200
+ @app.after_request
201
+ def track_requests(response):
202
+ if tracker:
203
+ if request.headers.get("dnt"):
204
+ source_ip = ""
205
+ elif request.headers.get("X-Forwarded-For"):
206
+ source_ip = request.headers["X-Forwarded-For"].split(", ")[0]
207
+ else:
208
+ source_ip = request.remote_addr
209
+
210
+ api_version = "{}-{}".format(
211
+ data["country_package_metadata"]["name"],
212
+ data["country_package_metadata"]["version"],
213
+ )
214
+
215
+ tracker.track(request.url, source_ip, api_version, request.path)
216
+ return response
217
+
218
+ @app.errorhandler(500)
219
+ def internal_server_error(e: werkzeug.exceptions.InternalServerError):
220
+ message = getattr(e, "original_exception", e.description)
221
+ response = jsonify({"error": "Internal server error: " + str(message)})
222
+ response.status_code = 500
223
+
224
+ return response
225
+
226
+ return app
@@ -0,0 +1,11 @@
1
+ import logging
2
+ from typing import NoReturn
3
+
4
+ log = logging.getLogger("gunicorn.error")
5
+
6
+
7
+ def handle_import_error(error) -> NoReturn:
8
+ msg = f"OpenFisca is missing some dependencies to run the Web API: '{error}'. To install them, run `pip install openfisca_core[web-api]`."
9
+ raise ImportError(
10
+ msg,
11
+ )
@@ -0,0 +1,90 @@
1
+ import dpath
2
+
3
+ from openfisca_core.indexed_enums import Enum
4
+ from openfisca_core.simulations import SimulationBuilder
5
+
6
+
7
+ def calculate(tax_benefit_system, input_data: dict) -> dict:
8
+ """Returns the input_data where the None values are replaced by the calculated values."""
9
+ simulation = SimulationBuilder().build_from_entities(tax_benefit_system, input_data)
10
+ requested_computations = dpath.search(
11
+ input_data,
12
+ "*/*/*/*",
13
+ afilter=lambda t: t is None,
14
+ yielded=True,
15
+ )
16
+ computation_results: dict = {}
17
+ for computation in requested_computations:
18
+ path = computation[
19
+ 0
20
+ ] # format: entity_plural/entity_instance_id/openfisca_variable_name/period
21
+ entity_plural, entity_id, variable_name, period = path.split("/")
22
+ variable = tax_benefit_system.get_variable(variable_name)
23
+ result = simulation.calculate(variable_name, period)
24
+ population = simulation.get_population(entity_plural)
25
+ entity_index = population.get_index(entity_id)
26
+
27
+ if variable.value_type == Enum:
28
+ entity_result = result.decode()[entity_index].name
29
+ elif variable.value_type == float:
30
+ entity_result = float(
31
+ str(result[entity_index]),
32
+ ) # To turn the float32 into a regular float without adding confusing extra decimals. There must be a better way.
33
+ elif variable.value_type == str:
34
+ entity_result = str(result[entity_index])
35
+ else:
36
+ entity_result = result.tolist()[entity_index]
37
+ # Don't use dpath.new, because there is a problem with dpath>=2.0
38
+ # when we have a key that is numeric, like the year.
39
+ # See https://github.com/dpath-maintainers/dpath-python/issues/160
40
+ if computation_results == {}:
41
+ computation_results = {
42
+ entity_plural: {entity_id: {variable_name: {period: entity_result}}},
43
+ }
44
+ elif entity_plural in computation_results:
45
+ if entity_id in computation_results[entity_plural]:
46
+ if variable_name in computation_results[entity_plural][entity_id]:
47
+ computation_results[entity_plural][entity_id][variable_name][
48
+ period
49
+ ] = entity_result
50
+ else:
51
+ computation_results[entity_plural][entity_id][variable_name] = {
52
+ period: entity_result,
53
+ }
54
+ else:
55
+ computation_results[entity_plural][entity_id] = {
56
+ variable_name: {period: entity_result},
57
+ }
58
+ else:
59
+ computation_results[entity_plural] = {
60
+ entity_id: {variable_name: {period: entity_result}},
61
+ }
62
+ dpath.merge(input_data, computation_results)
63
+
64
+ return input_data
65
+
66
+
67
+ def trace(tax_benefit_system, input_data):
68
+ simulation = SimulationBuilder().build_from_entities(tax_benefit_system, input_data)
69
+ simulation.trace = True
70
+
71
+ requested_calculations = []
72
+ requested_computations = dpath.search(
73
+ input_data,
74
+ "*/*/*/*",
75
+ afilter=lambda t: t is None,
76
+ yielded=True,
77
+ )
78
+ for computation in requested_computations:
79
+ path = computation[0]
80
+ _entity_plural, _entity_id, variable_name, period = path.split("/")
81
+ requested_calculations.append(f"{variable_name}<{period!s}>")
82
+ simulation.calculate(variable_name, period)
83
+
84
+ trace = simulation.tracer.get_serialized_flat_trace()
85
+
86
+ return {
87
+ "trace": trace,
88
+ "entitiesDescription": simulation.describe_entities(),
89
+ "requestedCalculations": requested_calculations,
90
+ }
@@ -0,0 +1,22 @@
1
+ from openfisca_web_api.loader.entities import build_entities
2
+ from openfisca_web_api.loader.parameters import build_parameters
3
+ from openfisca_web_api.loader.spec import build_openAPI_specification
4
+ from openfisca_web_api.loader.variables import build_variables
5
+
6
+
7
+ def build_data(tax_benefit_system):
8
+ country_package_metadata = tax_benefit_system.get_package_metadata()
9
+ parameters = build_parameters(tax_benefit_system, country_package_metadata)
10
+ variables = build_variables(tax_benefit_system, country_package_metadata)
11
+ entities = build_entities(tax_benefit_system)
12
+ data = {
13
+ "tax_benefit_system": tax_benefit_system,
14
+ "country_package_metadata": country_package_metadata,
15
+ "openAPI_spec": None,
16
+ "parameters": parameters,
17
+ "variables": variables,
18
+ "entities": entities,
19
+ }
20
+ data["openAPI_spec"] = build_openAPI_specification(data)
21
+
22
+ return data
@@ -0,0 +1,28 @@
1
+ def build_entities(tax_benefit_system):
2
+ return {entity.key: build_entity(entity) for entity in tax_benefit_system.entities}
3
+
4
+
5
+ def build_entity(entity):
6
+ formatted_doc = entity.doc.strip()
7
+
8
+ formatted_entity = {
9
+ "plural": entity.plural,
10
+ "description": entity.label,
11
+ "documentation": formatted_doc,
12
+ }
13
+ if not entity.is_person:
14
+ formatted_entity["roles"] = {
15
+ role.key: build_role(role) for role in entity.roles
16
+ }
17
+ return formatted_entity
18
+
19
+
20
+ def build_role(role):
21
+ formatted_role = {"plural": role.plural, "description": role.doc}
22
+
23
+ if role.max:
24
+ formatted_role["max"] = role.max
25
+ if role.subroles:
26
+ formatted_role["max"] = len(role.subroles)
27
+
28
+ return formatted_role
@@ -0,0 +1,122 @@
1
+ import functools
2
+ import operator
3
+
4
+ from openfisca_core.parameters import Parameter, ParameterNode, Scale
5
+
6
+
7
+ def build_api_values_history(values_history):
8
+ api_values_history = {}
9
+ for value_at_instant in values_history.values_list:
10
+ api_values_history[value_at_instant.instant_str] = value_at_instant.value
11
+
12
+ return api_values_history
13
+
14
+
15
+ def get_value(date, values):
16
+ candidates = sorted(
17
+ [
18
+ (start_date, value)
19
+ for start_date, value in values.items()
20
+ if start_date
21
+ <= date # dates are lexicographically ordered and can be sorted
22
+ ],
23
+ reverse=True,
24
+ )
25
+
26
+ if candidates:
27
+ return candidates[0][1]
28
+ return None
29
+
30
+
31
+ def build_api_scale(scale, value_key_name):
32
+ # preprocess brackets for a scale with 'rates' or 'amounts'
33
+ brackets = [
34
+ {
35
+ "thresholds": build_api_values_history(bracket.threshold),
36
+ "values": build_api_values_history(getattr(bracket, value_key_name)),
37
+ }
38
+ for bracket in scale.brackets
39
+ ]
40
+
41
+ dates = set(
42
+ functools.reduce(
43
+ operator.iadd,
44
+ [
45
+ list(bracket["thresholds"].keys()) + list(bracket["values"].keys())
46
+ for bracket in brackets
47
+ ],
48
+ [],
49
+ ),
50
+ ) # flatten the dates and remove duplicates
51
+
52
+ # We iterate on all dates as we need to build the whole scale for each of them
53
+ api_scale = {}
54
+ for date in dates:
55
+ for bracket in brackets:
56
+ threshold_value = get_value(date, bracket["thresholds"])
57
+ if threshold_value is not None:
58
+ rate_or_amount_value = get_value(date, bracket["values"])
59
+ api_scale[date] = api_scale.get(date) or {}
60
+ api_scale[date][threshold_value] = rate_or_amount_value
61
+
62
+ # Handle stopped parameters: a parameter is stopped if its first bracket is stopped
63
+ latest_date_first_threshold = max(brackets[0]["thresholds"].keys())
64
+ latest_value_first_threshold = brackets[0]["thresholds"][
65
+ latest_date_first_threshold
66
+ ]
67
+
68
+ if latest_value_first_threshold is None:
69
+ api_scale[latest_date_first_threshold] = None
70
+
71
+ return api_scale
72
+
73
+
74
+ def build_source_url(absolute_file_path, country_package_metadata):
75
+ relative_path = absolute_file_path.replace(country_package_metadata["location"], "")
76
+ return "{}/blob/{}{}".format(
77
+ country_package_metadata["repository_url"],
78
+ country_package_metadata["version"],
79
+ relative_path,
80
+ )
81
+
82
+
83
+ def build_api_parameter(parameter, country_package_metadata):
84
+ api_parameter = {
85
+ "description": getattr(parameter, "description", None),
86
+ "id": parameter.name,
87
+ "metadata": parameter.metadata,
88
+ }
89
+ if parameter.file_path:
90
+ api_parameter["source"] = build_source_url(
91
+ parameter.file_path,
92
+ country_package_metadata,
93
+ )
94
+ if isinstance(parameter, Parameter):
95
+ if parameter.documentation:
96
+ api_parameter["documentation"] = parameter.documentation.strip()
97
+ api_parameter["values"] = build_api_values_history(parameter)
98
+ elif isinstance(parameter, Scale):
99
+ if "rate" in parameter.brackets[0].children:
100
+ api_parameter["brackets"] = build_api_scale(parameter, "rate")
101
+ elif "amount" in parameter.brackets[0].children:
102
+ api_parameter["brackets"] = build_api_scale(parameter, "amount")
103
+ elif isinstance(parameter, ParameterNode):
104
+ if parameter.documentation:
105
+ api_parameter["documentation"] = parameter.documentation.strip()
106
+ api_parameter["subparams"] = {
107
+ child_name: {
108
+ "description": child.description,
109
+ }
110
+ for child_name, child in parameter.children.items()
111
+ }
112
+ return api_parameter
113
+
114
+
115
+ def build_parameters(tax_benefit_system, country_package_metadata):
116
+ return {
117
+ parameter.name.replace(".", "/"): build_api_parameter(
118
+ parameter,
119
+ country_package_metadata,
120
+ )
121
+ for parameter in tax_benefit_system.parameters.get_descendants()
122
+ }
@@ -0,0 +1,168 @@
1
+ import os
2
+ from copy import deepcopy
3
+
4
+ import dpath
5
+ import yaml
6
+
7
+ from openfisca_core.indexed_enums import Enum
8
+ from openfisca_web_api import handlers
9
+
10
+ OPEN_API_CONFIG_FILE = os.path.join(
11
+ os.path.dirname(os.path.abspath(__file__)),
12
+ os.path.pardir,
13
+ "openAPI.yml",
14
+ )
15
+
16
+
17
+ def build_openAPI_specification(api_data):
18
+ tax_benefit_system = api_data["tax_benefit_system"]
19
+ with open(OPEN_API_CONFIG_FILE) as file:
20
+ spec = yaml.safe_load(file)
21
+ country_package_name = api_data["country_package_metadata"]["name"].title()
22
+ country_package_version = api_data["country_package_metadata"]["version"]
23
+ dpath.new(
24
+ spec,
25
+ "info/title",
26
+ spec["info"]["title"].replace("{COUNTRY_PACKAGE_NAME}", country_package_name),
27
+ )
28
+ dpath.new(
29
+ spec,
30
+ "info/description",
31
+ spec["info"]["description"].replace(
32
+ "{COUNTRY_PACKAGE_NAME}",
33
+ country_package_name,
34
+ ),
35
+ )
36
+ dpath.new(
37
+ spec,
38
+ "info/version",
39
+ spec["info"]["version"].replace(
40
+ "{COUNTRY_PACKAGE_VERSION}",
41
+ country_package_version,
42
+ ),
43
+ )
44
+
45
+ for entity in tax_benefit_system.entities:
46
+ name = entity.key.title()
47
+ spec["components"]["schemas"][name] = get_entity_json_schema(
48
+ entity,
49
+ tax_benefit_system,
50
+ )
51
+
52
+ situation_schema = get_situation_json_schema(tax_benefit_system)
53
+ dpath.new(spec, "components/schemas/SituationInput", situation_schema)
54
+ dpath.new(spec, "components/schemas/SituationOutput", situation_schema.copy())
55
+ dpath.new(
56
+ spec,
57
+ "components/schemas/Trace/properties/entitiesDescription/properties",
58
+ {
59
+ entity.plural: {"type": "array", "items": {"type": "string"}}
60
+ for entity in tax_benefit_system.entities
61
+ },
62
+ )
63
+
64
+ # Get example from the served tax benefist system
65
+
66
+ if tax_benefit_system.open_api_config.get("parameter_example"):
67
+ parameter_id = tax_benefit_system.open_api_config["parameter_example"]
68
+ parameter_path = parameter_id.replace(".", "/")
69
+ parameter_example = api_data["parameters"][parameter_path]
70
+ else:
71
+ parameter_example = next(iter(api_data["parameters"].values()))
72
+ dpath.new(spec, "components/schemas/Parameter/example", parameter_example)
73
+
74
+ if tax_benefit_system.open_api_config.get("variable_example"):
75
+ variable_example = api_data["variables"][
76
+ tax_benefit_system.open_api_config["variable_example"]
77
+ ]
78
+ else:
79
+ variable_example = next(iter(api_data["variables"].values()))
80
+ dpath.new(spec, "components/schemas/Variable/example", variable_example)
81
+
82
+ if tax_benefit_system.open_api_config.get("simulation_example"):
83
+ simulation_example = tax_benefit_system.open_api_config["simulation_example"]
84
+ dpath.new(
85
+ spec,
86
+ "components/schemas/SituationInput/example",
87
+ simulation_example,
88
+ )
89
+ dpath.new(
90
+ spec,
91
+ "components/schemas/SituationOutput/example",
92
+ handlers.calculate(tax_benefit_system, deepcopy(simulation_example)),
93
+ ) # calculate has side-effects
94
+ dpath.new(
95
+ spec,
96
+ "components/schemas/Trace/example",
97
+ handlers.trace(tax_benefit_system, simulation_example),
98
+ )
99
+ else:
100
+ message = f"No simulation example has been defined for this tax and benefit system. If you are the maintainer of {country_package_name}, you can define an example by following this documentation: https://openfisca.org/doc/openfisca-web-api/config-openapi.html"
101
+ dpath.new(spec, "components/schemas/SituationInput/example", message)
102
+ dpath.new(spec, "components/schemas/SituationOutput/example", message)
103
+ dpath.new(spec, "components/schemas/Trace/example", message)
104
+ return spec
105
+
106
+
107
+ def get_variable_json_schema(variable):
108
+ result = {
109
+ "type": "object",
110
+ "additionalProperties": {"type": variable.json_type},
111
+ }
112
+
113
+ if variable.value_type == Enum:
114
+ result["additionalProperties"]["enum"] = [
115
+ item.name for item in list(variable.possible_values)
116
+ ]
117
+
118
+ return result
119
+
120
+
121
+ def get_entity_json_schema(entity, tax_benefit_system):
122
+ if entity.is_person:
123
+ return {
124
+ "type": "object",
125
+ "properties": {
126
+ variable_name: get_variable_json_schema(variable)
127
+ for variable_name, variable in tax_benefit_system.get_variables(
128
+ entity,
129
+ ).items()
130
+ },
131
+ "additionalProperties": False,
132
+ }
133
+ properties = {}
134
+ properties.update(
135
+ {
136
+ role.plural or role.key: {"type": "array", "items": {"type": "string"}}
137
+ for role in entity.roles
138
+ },
139
+ )
140
+ properties.update(
141
+ {
142
+ variable_name: get_variable_json_schema(variable)
143
+ for variable_name, variable in tax_benefit_system.get_variables(
144
+ entity,
145
+ ).items()
146
+ },
147
+ )
148
+ return {
149
+ "type": "object",
150
+ "properties": properties,
151
+ "additionalProperties": False,
152
+ }
153
+
154
+
155
+ def get_situation_json_schema(tax_benefit_system):
156
+ return {
157
+ "type": "object",
158
+ "additionalProperties": False,
159
+ "properties": {
160
+ entity.plural: {
161
+ "type": "object",
162
+ "additionalProperties": {
163
+ "$ref": f"#/components/schemas/{entity.key.title()}",
164
+ },
165
+ }
166
+ for entity in tax_benefit_system.entities
167
+ },
168
+ }