pysdmx 1.8.0__tar.gz → 1.9.0__tar.gz
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.
- {pysdmx-1.8.0 → pysdmx-1.9.0}/PKG-INFO +4 -1
- {pysdmx-1.8.0 → pysdmx-1.9.0}/pyproject.toml +5 -3
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/__extras_check.py +14 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/__init__.py +1 -1
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/fusion/messages/category.py +69 -41
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/fusion/messages/msd.py +1 -1
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/sdmxjson2/messages/category.py +76 -43
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/sdmxjson2/messages/core.py +2 -1
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/sdmxjson2/messages/msd.py +5 -1
- pysdmx-1.9.0/src/pysdmx/io/json/sdmxjson2/reader/doc_validation.py +108 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/sdmxjson2/reader/metadata.py +8 -1
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/sdmxjson2/reader/structure.py +9 -2
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/reader.py +18 -4
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/xml/__data_aux.py +9 -4
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/xml/__parse_xml.py +2 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/xml/__structure_aux_reader.py +70 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/xml/__structure_aux_writer.py +63 -9
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/xml/__tokens.py +3 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/xml/__write_aux.py +35 -30
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/xml/header.py +7 -1
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/model/__base.py +1 -1
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/model/category.py +6 -1
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/model/message.py +22 -6
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/util/_model_utils.py +40 -3
- {pysdmx-1.8.0 → pysdmx-1.9.0}/LICENSE +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/README.rst +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/api/__init__.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/api/dc/__init__.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/api/dc/_api.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/api/dc/query/__init__.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/api/dc/query/_model.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/api/dc/query/_parsing_model.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/api/dc/query/_parsing_util.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/api/dc/query/_py_parser.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/api/dc/query/_sql_parser.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/api/dc/query/util.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/api/fmr/__init__.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/api/fmr/maintenance.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/api/gds/__init__.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/api/qb/__init__.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/api/qb/availability.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/api/qb/data.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/api/qb/gds.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/api/qb/refmeta.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/api/qb/registration.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/api/qb/schema.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/api/qb/service.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/api/qb/structure.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/api/qb/util.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/errors.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/__init__.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/csv/__csv_aux_reader.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/csv/__csv_aux_writer.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/csv/__init__.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/csv/sdmx10/__init__.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/csv/sdmx10/reader/__init__.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/csv/sdmx10/writer/__init__.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/csv/sdmx20/__init__.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/csv/sdmx20/reader/__init__.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/csv/sdmx20/writer/__init__.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/csv/sdmx21/__init__.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/csv/sdmx21/reader/__init__.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/csv/sdmx21/writer/__init__.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/format.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/input_processor.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/fusion/messages/__init__.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/fusion/messages/code.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/fusion/messages/concept.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/fusion/messages/constraint.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/fusion/messages/core.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/fusion/messages/dataflow.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/fusion/messages/dsd.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/fusion/messages/map.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/fusion/messages/metadataflow.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/fusion/messages/mpa.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/fusion/messages/org.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/fusion/messages/pa.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/fusion/messages/report.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/fusion/messages/schema.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/fusion/messages/vtl.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/fusion/reader/__init__.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/gds/messages/__init__.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/gds/messages/agencies.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/gds/messages/catalog.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/gds/messages/sdmx_api.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/gds/messages/services.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/gds/messages/urn_resolver.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/gds/reader/__init__.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/sdmxjson2/messages/__init__.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/sdmxjson2/messages/agency.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/sdmxjson2/messages/code.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/sdmxjson2/messages/concept.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/sdmxjson2/messages/constraint.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/sdmxjson2/messages/dataflow.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/sdmxjson2/messages/dsd.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/sdmxjson2/messages/map.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/sdmxjson2/messages/metadataflow.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/sdmxjson2/messages/mpa.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/sdmxjson2/messages/pa.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/sdmxjson2/messages/provider.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/sdmxjson2/messages/report.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/sdmxjson2/messages/schema.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/sdmxjson2/messages/structure.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/sdmxjson2/messages/vtl.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/sdmxjson2/reader/__init__.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/sdmxjson2/writer/__init__.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/sdmxjson2/writer/metadata.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/json/sdmxjson2/writer/structure.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/pd.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/serde.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/writer.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/xml/__allowed_lxml_errors.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/xml/__init__.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/xml/__ss_aux_reader.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/xml/__write_data_aux.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/xml/__write_structure_specific_aux.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/xml/config.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/xml/doc_validation.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/xml/sdmx21/__init__.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/xml/sdmx21/reader/__init__.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/xml/sdmx21/reader/error.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/xml/sdmx21/reader/generic.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/xml/sdmx21/reader/structure.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/xml/sdmx21/reader/structure_specific.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/xml/sdmx21/reader/submission.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/xml/sdmx21/writer/__init__.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/xml/sdmx21/writer/error.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/xml/sdmx21/writer/generic.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/xml/sdmx21/writer/structure.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/xml/sdmx21/writer/structure_specific.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/xml/sdmx30/__init__.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/xml/sdmx30/reader/__init__.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/xml/sdmx30/reader/structure.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/xml/sdmx30/reader/structure_specific.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/xml/sdmx30/writer/__init__.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/xml/sdmx30/writer/structure.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/xml/sdmx30/writer/structure_specific.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/xml/sdmx31/__init__.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/xml/sdmx31/reader/__init__.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/xml/sdmx31/reader/structure.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/xml/sdmx31/reader/structure_specific.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/xml/sdmx31/writer/__init__.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/xml/sdmx31/writer/structure.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/xml/sdmx31/writer/structure_specific.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/io/xml/utils.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/model/__init__.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/model/code.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/model/concept.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/model/dataflow.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/model/dataset.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/model/gds.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/model/map.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/model/metadata.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/model/organisation.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/model/submission.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/model/vtl.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/py.typed +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/toolkit/__init__.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/toolkit/pd/__init__.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/toolkit/pd/_data_utils.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/toolkit/vtl/__init__.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/toolkit/vtl/_validations.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/toolkit/vtl/script_generation.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/toolkit/vtl/validation.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/util/__init__.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/util/_date_pattern_map.py +0 -0
- {pysdmx-1.8.0 → pysdmx-1.9.0}/src/pysdmx/util/_net_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pysdmx
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.9.0
|
|
4
4
|
Summary: Your opinionated Python SDMX library
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
License-File: LICENSE
|
|
@@ -16,9 +16,11 @@ Classifier: Typing :: Typed
|
|
|
16
16
|
Provides-Extra: all
|
|
17
17
|
Provides-Extra: data
|
|
18
18
|
Provides-Extra: dc
|
|
19
|
+
Provides-Extra: json
|
|
19
20
|
Provides-Extra: vtl
|
|
20
21
|
Provides-Extra: xml
|
|
21
22
|
Requires-Dist: httpx[http2] (>=0)
|
|
23
|
+
Requires-Dist: jsonschema (>=4.10) ; extra == "json"
|
|
22
24
|
Requires-Dist: lxml (>=5.2) ; extra == "all"
|
|
23
25
|
Requires-Dist: lxml (>=5.2) ; extra == "xml"
|
|
24
26
|
Requires-Dist: msgspec (>=0)
|
|
@@ -28,6 +30,7 @@ Requires-Dist: parsy (>=2.1)
|
|
|
28
30
|
Requires-Dist: python-dateutil (>=2.8.2) ; extra == "all"
|
|
29
31
|
Requires-Dist: python-dateutil (>=2.8.2) ; extra == "dc"
|
|
30
32
|
Requires-Dist: sdmxschemas (>=1.0.0) ; extra == "all"
|
|
33
|
+
Requires-Dist: sdmxschemas (>=1.0.0) ; extra == "json"
|
|
31
34
|
Requires-Dist: sdmxschemas (>=1.0.0) ; extra == "xml"
|
|
32
35
|
Requires-Dist: vtlengine (>=1.1,<2.0) ; extra == "all"
|
|
33
36
|
Requires-Dist: vtlengine (>=1.1,<2.0) ; extra == "vtl"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pysdmx"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.9.0"
|
|
4
4
|
description = "Your opinionated Python SDMX library"
|
|
5
5
|
license = { text = "Apache-2.0" }
|
|
6
6
|
readme = "README.rst"
|
|
@@ -23,7 +23,7 @@ classifiers = [
|
|
|
23
23
|
dependencies = [
|
|
24
24
|
"httpx[http2]>=0.*",
|
|
25
25
|
"msgspec>=0.*",
|
|
26
|
-
"parsy>=2.1"
|
|
26
|
+
"parsy>=2.1"
|
|
27
27
|
]
|
|
28
28
|
|
|
29
29
|
[project.urls]
|
|
@@ -36,6 +36,7 @@ documentation = "https://bis-med-it.github.io/pysdmx"
|
|
|
36
36
|
data = ["pandas>=2.1.4"]
|
|
37
37
|
dc = ["python-dateutil>=2.8.2"]
|
|
38
38
|
vtl = ["vtlengine>=1.1,<2.0"]
|
|
39
|
+
json = ["sdmxschemas>=1.0.0", "jsonschema>=4.10"]
|
|
39
40
|
xml = ["lxml>=5.2", "xmltodict>=0.13", "sdmxschemas>=1.0.0"]
|
|
40
41
|
all = ["lxml>=5.2", "xmltodict>=0.13", "sdmxschemas>=1.0.0", "pandas>=2.1.4", "python-dateutil>=2.8.2", "vtlengine>=1.1,<2.0"]
|
|
41
42
|
|
|
@@ -57,6 +58,7 @@ lxml-stubs = "^0.5.1"
|
|
|
57
58
|
types-xmltodict = "^0.13.0.3"
|
|
58
59
|
types-python-dateutil = "^2.9.0.20240316"
|
|
59
60
|
pandas-stubs = "^2.1.4.231227"
|
|
61
|
+
types-jsonschema = "^4.25.1.20251009"
|
|
60
62
|
|
|
61
63
|
[tool.poetry.group.docs.dependencies]
|
|
62
64
|
sphinx = "^7.2.6"
|
|
@@ -93,7 +95,7 @@ lint.mccabe.max-complexity = 10
|
|
|
93
95
|
lint.pydocstyle.convention = "google"
|
|
94
96
|
|
|
95
97
|
[tool.ruff.lint.per-file-ignores]
|
|
96
|
-
"tests/*" = ["DTZ", "D100", "D103", "D104", "PERF", "S101", "S311"]
|
|
98
|
+
"tests/*" = ["C901", "DTZ", "D100", "D103", "D104", "PERF", "S101", "S311"]
|
|
97
99
|
|
|
98
100
|
[tool.mypy]
|
|
99
101
|
files = "src"
|
|
@@ -55,3 +55,17 @@ def __check_vtl_extra() -> None:
|
|
|
55
55
|
" and prettify",
|
|
56
56
|
)
|
|
57
57
|
) from None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def __check_json_extra() -> None:
|
|
61
|
+
try:
|
|
62
|
+
import jsonschema # noqa: F401
|
|
63
|
+
import sdmxschemas # noqa: F401
|
|
64
|
+
except ImportError:
|
|
65
|
+
raise ImportError(
|
|
66
|
+
ERROR_MESSAGE.format(
|
|
67
|
+
extra_name="json",
|
|
68
|
+
extra_desc="the validation of SDMX-JSON Structure Messages "
|
|
69
|
+
"(hint, use validate=False if you don't need validation)",
|
|
70
|
+
)
|
|
71
|
+
) from None
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Collection of Fusion-JSON schemas for categories and category schemes."""
|
|
2
2
|
|
|
3
3
|
from collections import defaultdict
|
|
4
|
-
from typing import Dict, Optional, Sequence
|
|
4
|
+
from typing import Dict, Optional, Sequence, Tuple, Union
|
|
5
5
|
|
|
6
6
|
from msgspec import Struct
|
|
7
7
|
|
|
@@ -11,6 +11,8 @@ from pysdmx.model import (
|
|
|
11
11
|
Agency,
|
|
12
12
|
Category,
|
|
13
13
|
DataflowRef,
|
|
14
|
+
ItemReference,
|
|
15
|
+
Reference,
|
|
14
16
|
)
|
|
15
17
|
from pysdmx.model import (
|
|
16
18
|
Categorisation as CT,
|
|
@@ -21,7 +23,7 @@ from pysdmx.model import (
|
|
|
21
23
|
from pysdmx.model import (
|
|
22
24
|
Dataflow as DF,
|
|
23
25
|
)
|
|
24
|
-
from pysdmx.util import find_by_urn
|
|
26
|
+
from pysdmx.util import find_by_urn, parse_urn
|
|
25
27
|
|
|
26
28
|
|
|
27
29
|
class FusionCategorisation(Struct, frozen=True, rename={"agency": "agencyId"}):
|
|
@@ -57,14 +59,46 @@ class FusionCategory(Struct, frozen=True):
|
|
|
57
59
|
descriptions: Optional[Sequence[FusionString]] = None
|
|
58
60
|
items: Sequence["FusionCategory"] = ()
|
|
59
61
|
|
|
60
|
-
def
|
|
62
|
+
def __add_flows(
|
|
63
|
+
self, cni: str, cf: Dict[str, list[DF]]
|
|
64
|
+
) -> Sequence[DataflowRef]:
|
|
65
|
+
if cni in cf:
|
|
66
|
+
return [
|
|
67
|
+
DataflowRef(
|
|
68
|
+
(
|
|
69
|
+
df.agency.id
|
|
70
|
+
if isinstance(df.agency, Agency)
|
|
71
|
+
else df.agency
|
|
72
|
+
),
|
|
73
|
+
df.id,
|
|
74
|
+
df.version,
|
|
75
|
+
df.name,
|
|
76
|
+
)
|
|
77
|
+
for df in cf[cni]
|
|
78
|
+
]
|
|
79
|
+
else:
|
|
80
|
+
return ()
|
|
81
|
+
|
|
82
|
+
def to_model(
|
|
83
|
+
self,
|
|
84
|
+
cat_flows: dict[str, list[DF]],
|
|
85
|
+
cat_other: dict[str, list[Union[ItemReference, Reference]]],
|
|
86
|
+
parent_id: Optional[str] = None,
|
|
87
|
+
) -> Category:
|
|
61
88
|
"""Converts a FusionCode to a standard code."""
|
|
62
89
|
description = self.descriptions[0].value if self.descriptions else None
|
|
90
|
+
cni = f"{parent_id}.{self.id}" if parent_id else self.id
|
|
91
|
+
dataflows = self.__add_flows(cni, cat_flows)
|
|
92
|
+
others = cat_other.get(cni, ())
|
|
63
93
|
return Category(
|
|
64
94
|
id=self.id,
|
|
65
95
|
name=self.names[0].value,
|
|
66
96
|
description=description,
|
|
67
|
-
categories=[
|
|
97
|
+
categories=[
|
|
98
|
+
c.to_model(cat_flows, cat_other, cni) for c in self.items
|
|
99
|
+
],
|
|
100
|
+
dataflows=dataflows,
|
|
101
|
+
other_references=others,
|
|
68
102
|
)
|
|
69
103
|
|
|
70
104
|
|
|
@@ -78,16 +112,42 @@ class FusionCategoryScheme(Struct, frozen=True, rename={"agency": "agencyId"}):
|
|
|
78
112
|
version: str = "1.0"
|
|
79
113
|
items: Sequence[FusionCategory] = ()
|
|
80
114
|
|
|
81
|
-
def
|
|
115
|
+
def __group_refs(
|
|
116
|
+
self,
|
|
117
|
+
categorisations: Sequence[FusionCategorisation],
|
|
118
|
+
dataflows: Sequence[FusionDataflow],
|
|
119
|
+
) -> Tuple[
|
|
120
|
+
dict[str, list[DF]], dict[str, list[Union[ItemReference, Reference]]]
|
|
121
|
+
]:
|
|
122
|
+
flows: defaultdict[str, list[DF]] = defaultdict(list)
|
|
123
|
+
other: defaultdict[str, list[Union[ItemReference, Reference]]] = (
|
|
124
|
+
defaultdict(list)
|
|
125
|
+
)
|
|
126
|
+
for c in categorisations:
|
|
127
|
+
ref = parse_urn(c.structureReference)
|
|
128
|
+
src = c.categoryReference[c.categoryReference.find(")") + 2 :]
|
|
129
|
+
if ref.sdmx_type == "Dataflow":
|
|
130
|
+
d = find_by_urn(dataflows, c.structureReference)
|
|
131
|
+
flows[src].append(d.to_model())
|
|
132
|
+
else:
|
|
133
|
+
other[src].append(ref)
|
|
134
|
+
return (flows, other)
|
|
135
|
+
|
|
136
|
+
def to_model(
|
|
137
|
+
self,
|
|
138
|
+
categorisations: Sequence[FusionCategorisation] = (),
|
|
139
|
+
dataflows: Sequence[FusionDataflow] = (),
|
|
140
|
+
) -> CS:
|
|
82
141
|
"""Converts a JsonCodelist to a standard codelist."""
|
|
83
142
|
description = self.descriptions[0].value if self.descriptions else None
|
|
143
|
+
cat_flows, cat_others = self.__group_refs(categorisations, dataflows)
|
|
84
144
|
return CS(
|
|
85
145
|
id=self.id,
|
|
86
146
|
name=self.names[0].value,
|
|
87
147
|
agency=self.agency,
|
|
88
148
|
description=description,
|
|
89
149
|
version=self.version,
|
|
90
|
-
items=[c.to_model() for c in self.items],
|
|
150
|
+
items=[c.to_model(cat_flows, cat_others) for c in self.items],
|
|
91
151
|
)
|
|
92
152
|
|
|
93
153
|
|
|
@@ -98,43 +158,11 @@ class FusionCategorySchemeMessage(Struct, frozen=True):
|
|
|
98
158
|
Categorisation: Sequence[FusionCategorisation] = ()
|
|
99
159
|
Dataflow: Sequence[FusionDataflow] = ()
|
|
100
160
|
|
|
101
|
-
def __group_flows(self) -> defaultdict[str, list[DF]]:
|
|
102
|
-
out: defaultdict[str, list[DF]] = defaultdict(list)
|
|
103
|
-
for c in self.Categorisation:
|
|
104
|
-
d = find_by_urn(self.Dataflow, c.structureReference)
|
|
105
|
-
src = c.categoryReference[c.categoryReference.find(")") + 2 :]
|
|
106
|
-
out[src].append(d.to_model())
|
|
107
|
-
return out
|
|
108
|
-
|
|
109
|
-
def __add_flows(
|
|
110
|
-
self, cat: Category, cni: str, cf: Dict[str, list[DF]]
|
|
111
|
-
) -> None:
|
|
112
|
-
if cat.categories:
|
|
113
|
-
for c in cat.categories:
|
|
114
|
-
self.__add_flows(c, f"{cni}.{c.id}", cf)
|
|
115
|
-
if cni in cf:
|
|
116
|
-
dfrefs = [
|
|
117
|
-
DataflowRef(
|
|
118
|
-
(
|
|
119
|
-
df.agency.id
|
|
120
|
-
if isinstance(df.agency, Agency)
|
|
121
|
-
else df.agency
|
|
122
|
-
),
|
|
123
|
-
df.id,
|
|
124
|
-
df.version,
|
|
125
|
-
df.name,
|
|
126
|
-
)
|
|
127
|
-
for df in cf[cni]
|
|
128
|
-
]
|
|
129
|
-
cat.dataflows = dfrefs
|
|
130
|
-
|
|
131
161
|
def to_model(self) -> CS:
|
|
132
162
|
"""Returns the requested category scheme."""
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
self.__add_flows(c, c.id, cf)
|
|
137
|
-
return cs
|
|
163
|
+
return self.CategoryScheme[0].to_model(
|
|
164
|
+
self.Categorisation, self.Dataflow
|
|
165
|
+
)
|
|
138
166
|
|
|
139
167
|
|
|
140
168
|
class FusionCategorisationMessage(Struct, frozen=True):
|
|
@@ -29,7 +29,7 @@ class FusionMetadataAttribute(Struct, frozen=True):
|
|
|
29
29
|
id: str
|
|
30
30
|
concept: str
|
|
31
31
|
minOccurs: int
|
|
32
|
-
maxOccurs: Union[int, Literal["unbounded"]]
|
|
32
|
+
maxOccurs: Union[int, Literal["unbounded"]] = "unbounded"
|
|
33
33
|
presentational: Optional[bool] = False
|
|
34
34
|
representation: Optional[FusionRepresentation] = None
|
|
35
35
|
metadataAttributes: Sequence["FusionMetadataAttribute"] = ()
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Collection of SDMX-JSON schemas for categories and category schemes."""
|
|
2
2
|
|
|
3
3
|
from collections import defaultdict
|
|
4
|
-
from typing import Dict, Sequence
|
|
4
|
+
from typing import Dict, Optional, Sequence, Tuple, Union
|
|
5
5
|
|
|
6
6
|
from msgspec import Struct
|
|
7
7
|
|
|
@@ -20,8 +20,10 @@ from pysdmx.model import (
|
|
|
20
20
|
CategoryScheme,
|
|
21
21
|
Dataflow,
|
|
22
22
|
DataflowRef,
|
|
23
|
+
ItemReference,
|
|
24
|
+
Reference,
|
|
23
25
|
)
|
|
24
|
-
from pysdmx.util import find_by_urn
|
|
26
|
+
from pysdmx.util import find_by_urn, parse_urn
|
|
25
27
|
|
|
26
28
|
|
|
27
29
|
class JsonCategorisation(
|
|
@@ -84,14 +86,46 @@ class JsonCategory(NameableType, frozen=True, omit_defaults=True):
|
|
|
84
86
|
|
|
85
87
|
categories: Sequence["JsonCategory"] = ()
|
|
86
88
|
|
|
87
|
-
def
|
|
89
|
+
def __add_flows(
|
|
90
|
+
self, cni: str, cf: Dict[str, list[Dataflow]]
|
|
91
|
+
) -> Sequence[DataflowRef]:
|
|
92
|
+
if cni in cf:
|
|
93
|
+
return [
|
|
94
|
+
DataflowRef(
|
|
95
|
+
(
|
|
96
|
+
df.agency.id
|
|
97
|
+
if isinstance(df.agency, Agency)
|
|
98
|
+
else df.agency
|
|
99
|
+
),
|
|
100
|
+
df.id,
|
|
101
|
+
df.version,
|
|
102
|
+
df.name,
|
|
103
|
+
)
|
|
104
|
+
for df in cf[cni]
|
|
105
|
+
]
|
|
106
|
+
else:
|
|
107
|
+
return ()
|
|
108
|
+
|
|
109
|
+
def to_model(
|
|
110
|
+
self,
|
|
111
|
+
cat_flows: dict[str, list[Dataflow]],
|
|
112
|
+
cat_other: dict[str, list[Union[ItemReference, Reference]]],
|
|
113
|
+
parent_id: Optional[str] = None,
|
|
114
|
+
) -> Category:
|
|
88
115
|
"""Converts a FusionCode to a standard code."""
|
|
116
|
+
cni = f"{parent_id}.{self.id}" if parent_id else self.id
|
|
117
|
+
dataflows = self.__add_flows(cni, cat_flows)
|
|
118
|
+
others = cat_other.get(cni, ())
|
|
89
119
|
return Category(
|
|
90
120
|
id=self.id,
|
|
91
121
|
name=self.name,
|
|
92
122
|
description=self.description,
|
|
93
|
-
categories=[
|
|
123
|
+
categories=[
|
|
124
|
+
c.to_model(cat_flows, cat_other, cni) for c in self.categories
|
|
125
|
+
],
|
|
94
126
|
annotations=[a.to_model() for a in self.annotations],
|
|
127
|
+
dataflows=dataflows,
|
|
128
|
+
other_references=others,
|
|
95
129
|
)
|
|
96
130
|
|
|
97
131
|
@classmethod
|
|
@@ -126,15 +160,42 @@ class JsonCategoryScheme(
|
|
|
126
160
|
|
|
127
161
|
categories: Sequence[JsonCategory] = ()
|
|
128
162
|
|
|
129
|
-
def
|
|
130
|
-
|
|
163
|
+
def __group_refs(
|
|
164
|
+
self,
|
|
165
|
+
categorisations: Sequence[JsonCategorisation] = (),
|
|
166
|
+
dataflows: Sequence[JsonDataflow] = (),
|
|
167
|
+
) -> Tuple[
|
|
168
|
+
dict[str, list[Dataflow]],
|
|
169
|
+
dict[str, list[Union[ItemReference, Reference]]],
|
|
170
|
+
]:
|
|
171
|
+
flows: defaultdict[str, list[Dataflow]] = defaultdict(list)
|
|
172
|
+
other: defaultdict[str, list[Union[ItemReference, Reference]]] = (
|
|
173
|
+
defaultdict(list)
|
|
174
|
+
)
|
|
175
|
+
for c in categorisations:
|
|
176
|
+
ref = parse_urn(c.source)
|
|
177
|
+
src = c.target[c.target.find(")") + 2 :]
|
|
178
|
+
if ref.sdmx_type == "Dataflow":
|
|
179
|
+
d = find_by_urn(dataflows, c.source)
|
|
180
|
+
flows[src].append(d.to_model())
|
|
181
|
+
else:
|
|
182
|
+
other[src].append(ref)
|
|
183
|
+
return (flows, other)
|
|
184
|
+
|
|
185
|
+
def to_model(
|
|
186
|
+
self,
|
|
187
|
+
categorisations: Sequence[JsonCategorisation] = (),
|
|
188
|
+
dataflows: Sequence[JsonDataflow] = (),
|
|
189
|
+
) -> CategoryScheme:
|
|
190
|
+
"""Converts a JsonCategoryScheme to a standard one."""
|
|
191
|
+
cat_flows, cat_other = self.__group_refs(categorisations, dataflows)
|
|
131
192
|
return CategoryScheme(
|
|
132
193
|
id=self.id,
|
|
133
194
|
name=self.name,
|
|
134
195
|
agency=self.agency,
|
|
135
196
|
description=self.description,
|
|
136
197
|
version=self.version,
|
|
137
|
-
items=[c.to_model() for c in self.categories],
|
|
198
|
+
items=[c.to_model(cat_flows, cat_other) for c in self.categories],
|
|
138
199
|
is_external_reference=self.isExternalReference,
|
|
139
200
|
is_partial=self.isPartial,
|
|
140
201
|
valid_from=self.validFrom,
|
|
@@ -179,49 +240,21 @@ class JsonCategorySchemes(Struct, frozen=True, omit_defaults=True):
|
|
|
179
240
|
categorisations: Sequence[JsonCategorisation] = ()
|
|
180
241
|
dataflows: Sequence[JsonDataflow] = ()
|
|
181
242
|
|
|
243
|
+
def to_model(self) -> CategoryScheme:
|
|
244
|
+
"""Returns the requested codelist."""
|
|
245
|
+
return self.categorySchemes[0].to_model(
|
|
246
|
+
self.categorisations, self.dataflows
|
|
247
|
+
)
|
|
248
|
+
|
|
182
249
|
|
|
183
250
|
class JsonCategorySchemeMessage(Struct, frozen=True, omit_defaults=True):
|
|
184
251
|
"""SDMX-JSON payload for /categoryscheme queries."""
|
|
185
252
|
|
|
186
253
|
data: JsonCategorySchemes
|
|
187
254
|
|
|
188
|
-
def __group_flows(self) -> defaultdict[str, list[Dataflow]]:
|
|
189
|
-
out: defaultdict[str, list[Dataflow]] = defaultdict(list)
|
|
190
|
-
for c in self.data.categorisations:
|
|
191
|
-
d = find_by_urn(self.data.dataflows, c.source)
|
|
192
|
-
src = c.target[c.target.find(")") + 2 :]
|
|
193
|
-
out[src].append(d.to_model())
|
|
194
|
-
return out
|
|
195
|
-
|
|
196
|
-
def __add_flows(
|
|
197
|
-
self, cat: Category, cni: str, cf: Dict[str, list[Dataflow]]
|
|
198
|
-
) -> None:
|
|
199
|
-
if cat.categories:
|
|
200
|
-
for c in cat.categories:
|
|
201
|
-
self.__add_flows(c, f"{cni}.{c.id}", cf)
|
|
202
|
-
if cni in cf:
|
|
203
|
-
dfrefs = [
|
|
204
|
-
DataflowRef(
|
|
205
|
-
(
|
|
206
|
-
df.agency.id
|
|
207
|
-
if isinstance(df.agency, Agency)
|
|
208
|
-
else df.agency
|
|
209
|
-
),
|
|
210
|
-
df.id,
|
|
211
|
-
df.version,
|
|
212
|
-
df.name,
|
|
213
|
-
)
|
|
214
|
-
for df in cf[cni]
|
|
215
|
-
]
|
|
216
|
-
cat.dataflows = dfrefs
|
|
217
|
-
|
|
218
255
|
def to_model(self) -> CategoryScheme:
|
|
219
|
-
"""Returns the requested
|
|
220
|
-
|
|
221
|
-
cs = self.data.categorySchemes[0].to_model()
|
|
222
|
-
for c in cs:
|
|
223
|
-
self.__add_flows(c, c.id, cf)
|
|
224
|
-
return cs
|
|
256
|
+
"""Returns the requested category scheme."""
|
|
257
|
+
return self.data.to_model()
|
|
225
258
|
|
|
226
259
|
|
|
227
260
|
class JsonCategorisations(Struct, frozen=True, omit_defaults=True):
|
|
@@ -299,6 +299,7 @@ class JsonHeader(msgspec.Struct, frozen=True, omit_defaults=True):
|
|
|
299
299
|
test=self.test,
|
|
300
300
|
prepared=self.prepared,
|
|
301
301
|
sender=self.sender,
|
|
302
|
+
receiver=self.receivers if self.receivers else (),
|
|
302
303
|
)
|
|
303
304
|
|
|
304
305
|
@classmethod
|
|
@@ -313,7 +314,7 @@ class JsonHeader(msgspec.Struct, frozen=True, omit_defaults=True):
|
|
|
313
314
|
header.prepared,
|
|
314
315
|
header.sender,
|
|
315
316
|
header.test,
|
|
316
|
-
receivers=
|
|
317
|
+
receivers=header.receiver if header.receiver else None,
|
|
317
318
|
schema=(
|
|
318
319
|
"https://raw.githubusercontent.com/sdmx-twg/sdmx-json/"
|
|
319
320
|
"develop/structure-message/tools/schemas/2.0.0/"
|
|
@@ -37,6 +37,7 @@ class JsonMetadataAttribute(Struct, frozen=True, omit_defaults=True):
|
|
|
37
37
|
maxOccurs: Union[int, Literal["unbounded"]]
|
|
38
38
|
isPresentational: bool
|
|
39
39
|
localRepresentation: Optional[JsonRepresentation] = None
|
|
40
|
+
metadataAttributes: Sequence["JsonMetadataAttribute"] = ()
|
|
40
41
|
|
|
41
42
|
def to_model(
|
|
42
43
|
self, cs: Sequence[JsonConceptScheme], cls: Sequence[Codelist]
|
|
@@ -72,7 +73,7 @@ class JsonMetadataAttribute(Struct, frozen=True, omit_defaults=True):
|
|
|
72
73
|
local_codes=codes,
|
|
73
74
|
array_def=ab,
|
|
74
75
|
local_enum_ref=local_enum_ref,
|
|
75
|
-
components=(),
|
|
76
|
+
components=[a.to_model(cs, cls) for a in self.metadataAttributes],
|
|
76
77
|
)
|
|
77
78
|
|
|
78
79
|
@classmethod
|
|
@@ -94,6 +95,9 @@ class JsonMetadataAttribute(Struct, frozen=True, omit_defaults=True):
|
|
|
94
95
|
minOccurs=min_occurs,
|
|
95
96
|
maxOccurs=max_occurs,
|
|
96
97
|
isPresentational=cmp.is_presentational,
|
|
98
|
+
metadataAttributes=[
|
|
99
|
+
JsonMetadataAttribute.from_model(c) for c in cmp.components
|
|
100
|
+
],
|
|
97
101
|
)
|
|
98
102
|
|
|
99
103
|
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""SDMX-JSON document validation against JSON schemas."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Callable, Mapping, Match, Optional
|
|
7
|
+
|
|
8
|
+
from jsonschema import Draft202012Validator
|
|
9
|
+
from jsonschema.exceptions import ValidationError
|
|
10
|
+
from sdmxschemas import SDMX_JSON_20_DATA_PATH as SCHEMA_PATH_JSON20_DATA
|
|
11
|
+
from sdmxschemas import (
|
|
12
|
+
SDMX_JSON_20_METADATA_PATH as SCHEMA_PATH_JSON20_METADATA,
|
|
13
|
+
)
|
|
14
|
+
from sdmxschemas import (
|
|
15
|
+
SDMX_JSON_20_STRUCTURE_PATH as SCHEMA_PATH_JSON20_STRUCTURE,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from pysdmx import errors
|
|
19
|
+
|
|
20
|
+
_SCHEMA_FILES: Mapping[str, Path] = {
|
|
21
|
+
"structure": SCHEMA_PATH_JSON20_STRUCTURE,
|
|
22
|
+
"metadata": SCHEMA_PATH_JSON20_METADATA,
|
|
23
|
+
"data": SCHEMA_PATH_JSON20_DATA,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _schema_for(instance: Mapping[str, Any]) -> dict[str, Any]:
|
|
28
|
+
schema_url = instance.get("meta", {}).get("schema")
|
|
29
|
+
p = next(p for p in _SCHEMA_FILES.values() if p.name in schema_url)
|
|
30
|
+
with p.open("r", encoding="utf-8") as f:
|
|
31
|
+
schema = json.load(f)
|
|
32
|
+
return schema
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def validate_sdmx_json(input_str: str) -> None:
|
|
36
|
+
"""Validates an SDMX-JSON message against the appropriate JSON schema.
|
|
37
|
+
|
|
38
|
+
Args: input_str: The SDMX-JSON message to validate.
|
|
39
|
+
Raises:
|
|
40
|
+
invalid: If the SDMX-JSON message does not validate against the schema.
|
|
41
|
+
|
|
42
|
+
"""
|
|
43
|
+
instance = json.loads(input_str)
|
|
44
|
+
schema = _schema_for(instance)
|
|
45
|
+
validator = Draft202012Validator(schema)
|
|
46
|
+
|
|
47
|
+
failures = sorted(
|
|
48
|
+
validator.iter_errors(instance),
|
|
49
|
+
key=lambda e: (list(e.path), e.message),
|
|
50
|
+
)
|
|
51
|
+
if failures:
|
|
52
|
+
|
|
53
|
+
def compact(e: ValidationError) -> str:
|
|
54
|
+
path = "$" if not e.path else "$." + ".".join(map(str, e.path))
|
|
55
|
+
sub = " | ".join(
|
|
56
|
+
getattr(e, "context", [])
|
|
57
|
+
and [c.message for c in e.context]
|
|
58
|
+
or []
|
|
59
|
+
)
|
|
60
|
+
raw = f"{e.message} | {sub}"
|
|
61
|
+
|
|
62
|
+
patterns: list[tuple[str, Callable[[Match[str]], str]]] = [
|
|
63
|
+
(
|
|
64
|
+
r"Additional properties are not allowed.*'([^']+)'",
|
|
65
|
+
lambda m: f"unexpected property '{m.group(1)}'",
|
|
66
|
+
),
|
|
67
|
+
(
|
|
68
|
+
r"is not of type '([^']+)'",
|
|
69
|
+
lambda m: f"invalid type (expected {m.group(1)})",
|
|
70
|
+
),
|
|
71
|
+
(
|
|
72
|
+
r"""['"]?([^'"\n]+)['"]?\s+is not one of\s+\[([^\]]+)\]""",
|
|
73
|
+
lambda m: "invalid value {!r}"
|
|
74
|
+
" (expected one of: {})".format(
|
|
75
|
+
m.group(1),
|
|
76
|
+
", ".join(
|
|
77
|
+
s.strip().strip("'\"")
|
|
78
|
+
for s in m.group(2).split(",")
|
|
79
|
+
),
|
|
80
|
+
),
|
|
81
|
+
),
|
|
82
|
+
(
|
|
83
|
+
r"'([^']+)' is a required property",
|
|
84
|
+
lambda m: f"missing property '{m.group(1)}'",
|
|
85
|
+
),
|
|
86
|
+
(
|
|
87
|
+
r"""does not match ['"]([^'"]+)['"]""",
|
|
88
|
+
lambda m: f"does not match required"
|
|
89
|
+
f" pattern {m.group(1)!r}",
|
|
90
|
+
),
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
msg: Optional[str] = next(
|
|
94
|
+
(
|
|
95
|
+
fmt(re.search(rx, raw)) # type: ignore[arg-type]
|
|
96
|
+
for rx, fmt in patterns
|
|
97
|
+
if re.search(rx, raw)
|
|
98
|
+
),
|
|
99
|
+
None,
|
|
100
|
+
)
|
|
101
|
+
msg = msg or e.message
|
|
102
|
+
return f"{path}: {msg}"
|
|
103
|
+
|
|
104
|
+
summary = "; ".join(compact(e) for e in failures[:3])
|
|
105
|
+
more = (
|
|
106
|
+
f" (+{len(failures) - 3} more errors)" if len(failures) > 3 else ""
|
|
107
|
+
)
|
|
108
|
+
raise errors.Invalid("Validation Error", f"{summary}{more}")
|
|
@@ -3,20 +3,27 @@
|
|
|
3
3
|
import msgspec
|
|
4
4
|
|
|
5
5
|
from pysdmx import errors
|
|
6
|
+
from pysdmx.__extras_check import __check_json_extra
|
|
6
7
|
from pysdmx.io.json.sdmxjson2.messages import JsonMetadataMessage
|
|
8
|
+
from pysdmx.io.json.sdmxjson2.reader.doc_validation import validate_sdmx_json
|
|
7
9
|
from pysdmx.model import decoders
|
|
8
10
|
from pysdmx.model.message import MetadataMessage
|
|
9
11
|
|
|
10
12
|
|
|
11
|
-
def read(input_str: str) -> MetadataMessage:
|
|
13
|
+
def read(input_str: str, validate: bool = True) -> MetadataMessage:
|
|
12
14
|
"""Read an SDMX-JSON 2.0.0 Metadata Message.
|
|
13
15
|
|
|
14
16
|
Args:
|
|
15
17
|
input_str: SDMX-JSON reference metadata message to read.
|
|
18
|
+
validate: If True, the JSON data will be validated against the schemas.
|
|
16
19
|
|
|
17
20
|
Returns:
|
|
18
21
|
A pysdmx MetadataMessage
|
|
19
22
|
"""
|
|
23
|
+
if validate:
|
|
24
|
+
__check_json_extra()
|
|
25
|
+
validate_sdmx_json(input_str)
|
|
26
|
+
|
|
20
27
|
try:
|
|
21
28
|
msg = msgspec.json.Decoder(
|
|
22
29
|
JsonMetadataMessage, dec_hook=decoders
|
|
@@ -3,20 +3,27 @@
|
|
|
3
3
|
import msgspec
|
|
4
4
|
|
|
5
5
|
from pysdmx import errors
|
|
6
|
+
from pysdmx.__extras_check import __check_json_extra
|
|
6
7
|
from pysdmx.io.json.sdmxjson2.messages import JsonStructureMessage
|
|
8
|
+
from pysdmx.io.json.sdmxjson2.reader.doc_validation import validate_sdmx_json
|
|
7
9
|
from pysdmx.model import decoders
|
|
8
10
|
from pysdmx.model.message import StructureMessage
|
|
9
11
|
|
|
10
12
|
|
|
11
|
-
def read(input_str: str) -> StructureMessage:
|
|
12
|
-
"""Read an SDMX-JSON 2.0.0
|
|
13
|
+
def read(input_str: str, validate: bool = True) -> StructureMessage:
|
|
14
|
+
"""Read an SDMX-JSON 2.0.0 Structure Message.
|
|
13
15
|
|
|
14
16
|
Args:
|
|
15
17
|
input_str: SDMX-JSON structure message to read.
|
|
18
|
+
validate: If True, the JSON data will be validated against the schemas.
|
|
16
19
|
|
|
17
20
|
Returns:
|
|
18
21
|
A pysdmx StructureMessage
|
|
19
22
|
"""
|
|
23
|
+
if validate:
|
|
24
|
+
__check_json_extra()
|
|
25
|
+
validate_sdmx_json(input_str)
|
|
26
|
+
|
|
20
27
|
try:
|
|
21
28
|
msg = msgspec.json.Decoder(
|
|
22
29
|
JsonStructureMessage, dec_hook=decoders
|