sdmxlib 0.8.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.
- sdmxlib/__init__.py +155 -0
- sdmxlib/api/__init__.py +7 -0
- sdmxlib/api/client.py +246 -0
- sdmxlib/api/filters.py +67 -0
- sdmxlib/api/providers.py +66 -0
- sdmxlib/api/query.py +428 -0
- sdmxlib/api/registry.py +666 -0
- sdmxlib/api/session.py +124 -0
- sdmxlib/formats/__init__.py +166 -0
- sdmxlib/formats/sdmx_csv/__init__.py +5 -0
- sdmxlib/formats/sdmx_csv/reader.py +147 -0
- sdmxlib/formats/sdmx_json/__init__.py +1 -0
- sdmxlib/formats/sdmx_json/reader.py +897 -0
- sdmxlib/formats/sdmx_json/writer.py +698 -0
- sdmxlib/formats/sdmx_ml21/__init__.py +5 -0
- sdmxlib/formats/sdmx_ml21/namespaces.py +12 -0
- sdmxlib/formats/sdmx_ml21/reader.py +1147 -0
- sdmxlib/formats/sdmx_ml21/writer.py +709 -0
- sdmxlib/formats/sdmx_ml30/__init__.py +1 -0
- sdmxlib/formats/sdmx_ml30/namespaces.py +12 -0
- sdmxlib/formats/sdmx_ml30/reader.py +1138 -0
- sdmxlib/formats/sdmx_ml30/writer.py +730 -0
- sdmxlib/local/__init__.py +6 -0
- sdmxlib/local/_registry.py +368 -0
- sdmxlib/model/__init__.py +122 -0
- sdmxlib/model/annotations.py +103 -0
- sdmxlib/model/base.py +61 -0
- sdmxlib/model/category.py +88 -0
- sdmxlib/model/codelist.py +91 -0
- sdmxlib/model/collections.py +382 -0
- sdmxlib/model/concept.py +121 -0
- sdmxlib/model/constraint.py +92 -0
- sdmxlib/model/convert.py +268 -0
- sdmxlib/model/dataflow.py +49 -0
- sdmxlib/model/dataset.py +337 -0
- sdmxlib/model/datastructure.py +390 -0
- sdmxlib/model/expr.py +120 -0
- sdmxlib/model/hierarchy.py +111 -0
- sdmxlib/model/istring.py +81 -0
- sdmxlib/model/mapping.py +134 -0
- sdmxlib/model/message.py +70 -0
- sdmxlib/model/organisation.py +149 -0
- sdmxlib/model/provision.py +45 -0
- sdmxlib/model/ref.py +195 -0
- sdmxlib/model/registry.py +191 -0
- sdmxlib/model/representation.py +99 -0
- sdmxlib/model/urn.py +130 -0
- sdmxlib/model/validation.py +338 -0
- sdmxlib/polars.py +392 -0
- sdmxlib/py.typed +0 -0
- sdmxlib/storage/__init__.py +28 -0
- sdmxlib/storage/lazy.py +329 -0
- sdmxlib/storage/readers.py +310 -0
- sdmxlib/storage/schema.py +289 -0
- sdmxlib/storage/writers.py +503 -0
- sdmxlib-0.8.0.dist-info/METADATA +102 -0
- sdmxlib-0.8.0.dist-info/RECORD +58 -0
- sdmxlib-0.8.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""DuckDB schema for SDMX structural metadata.
|
|
2
|
+
|
|
3
|
+
Tables are generated programmatically so that language columns
|
|
4
|
+
(``name_en``, ``label_fr``, etc.) are driven by a configurable
|
|
5
|
+
``languages`` list rather than being hard-coded.
|
|
6
|
+
|
|
7
|
+
Example::
|
|
8
|
+
|
|
9
|
+
import duckdb
|
|
10
|
+
from sdmxlib.storage.schema import create_tables, get_languages
|
|
11
|
+
|
|
12
|
+
conn = duckdb.connect("store.duckdb")
|
|
13
|
+
create_tables(conn, languages=["en", "fr"])
|
|
14
|
+
|
|
15
|
+
# Later — discover configured languages
|
|
16
|
+
langs = get_languages(conn) # ["en", "fr"]
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from collections.abc import Sequence
|
|
20
|
+
|
|
21
|
+
import duckdb
|
|
22
|
+
|
|
23
|
+
# ── Table definitions ────────────────────────────────────────────────────
|
|
24
|
+
#
|
|
25
|
+
# Each entry: (table_name, fixed_columns, lang_prefixes)
|
|
26
|
+
# fixed_columns: list of (col_name, col_type) tuples
|
|
27
|
+
# lang_prefixes: list of column-name prefixes that get a _{lang} suffix
|
|
28
|
+
# for each configured language
|
|
29
|
+
|
|
30
|
+
_TABLE_DEFS: list[tuple[str, list[tuple[str, str]], list[str]]] = [
|
|
31
|
+
(
|
|
32
|
+
"artefacts",
|
|
33
|
+
[
|
|
34
|
+
("urn", "VARCHAR PRIMARY KEY"),
|
|
35
|
+
("artefact_type", "VARCHAR NOT NULL"),
|
|
36
|
+
("agency", "VARCHAR NOT NULL"),
|
|
37
|
+
("id", "VARCHAR NOT NULL"),
|
|
38
|
+
("version", "VARCHAR NOT NULL DEFAULT '1.0'"),
|
|
39
|
+
("is_final", "BOOLEAN DEFAULT FALSE"),
|
|
40
|
+
# language columns inserted here
|
|
41
|
+
("_document", "VARCHAR"),
|
|
42
|
+
],
|
|
43
|
+
["name", "description"],
|
|
44
|
+
),
|
|
45
|
+
(
|
|
46
|
+
"codelists",
|
|
47
|
+
[
|
|
48
|
+
("urn", "VARCHAR PRIMARY KEY"),
|
|
49
|
+
("agency", "VARCHAR NOT NULL"),
|
|
50
|
+
("id", "VARCHAR NOT NULL"),
|
|
51
|
+
("version", "VARCHAR NOT NULL DEFAULT '1.0'"),
|
|
52
|
+
# language columns inserted here
|
|
53
|
+
("is_partial", "BOOLEAN DEFAULT FALSE"),
|
|
54
|
+
],
|
|
55
|
+
["name"],
|
|
56
|
+
),
|
|
57
|
+
(
|
|
58
|
+
"codes",
|
|
59
|
+
[
|
|
60
|
+
("codelist_urn", "VARCHAR NOT NULL"),
|
|
61
|
+
("code_id", "VARCHAR NOT NULL"),
|
|
62
|
+
("parent_code_id", "VARCHAR"),
|
|
63
|
+
("position", "INTEGER"),
|
|
64
|
+
# language columns inserted here
|
|
65
|
+
("_annotations", "VARCHAR"),
|
|
66
|
+
],
|
|
67
|
+
["label", "description"],
|
|
68
|
+
),
|
|
69
|
+
(
|
|
70
|
+
"data_structures",
|
|
71
|
+
[
|
|
72
|
+
("urn", "VARCHAR PRIMARY KEY"),
|
|
73
|
+
("agency", "VARCHAR NOT NULL"),
|
|
74
|
+
("id", "VARCHAR NOT NULL"),
|
|
75
|
+
("version", "VARCHAR NOT NULL DEFAULT '1.0'"),
|
|
76
|
+
],
|
|
77
|
+
["name"],
|
|
78
|
+
),
|
|
79
|
+
(
|
|
80
|
+
"dsd_components",
|
|
81
|
+
[
|
|
82
|
+
("dsd_urn", "VARCHAR NOT NULL"),
|
|
83
|
+
("component_id", "VARCHAR NOT NULL"),
|
|
84
|
+
("component_type", "VARCHAR NOT NULL"),
|
|
85
|
+
("position", "INTEGER"),
|
|
86
|
+
("concept_id", "VARCHAR"),
|
|
87
|
+
("concept_urn", "VARCHAR"),
|
|
88
|
+
# language columns inserted here
|
|
89
|
+
("codelist_urn", "VARCHAR"),
|
|
90
|
+
("attachment_level", "VARCHAR"),
|
|
91
|
+
],
|
|
92
|
+
["concept_name"],
|
|
93
|
+
),
|
|
94
|
+
(
|
|
95
|
+
"dataflows",
|
|
96
|
+
[
|
|
97
|
+
("urn", "VARCHAR PRIMARY KEY"),
|
|
98
|
+
("agency", "VARCHAR NOT NULL"),
|
|
99
|
+
("id", "VARCHAR NOT NULL"),
|
|
100
|
+
("version", "VARCHAR NOT NULL DEFAULT '1.0'"),
|
|
101
|
+
# language columns inserted here
|
|
102
|
+
("dsd_urn", "VARCHAR"),
|
|
103
|
+
],
|
|
104
|
+
["name", "description"],
|
|
105
|
+
),
|
|
106
|
+
(
|
|
107
|
+
"concept_schemes",
|
|
108
|
+
[
|
|
109
|
+
("urn", "VARCHAR PRIMARY KEY"),
|
|
110
|
+
("agency", "VARCHAR NOT NULL"),
|
|
111
|
+
("id", "VARCHAR NOT NULL"),
|
|
112
|
+
("version", "VARCHAR NOT NULL DEFAULT '1.0'"),
|
|
113
|
+
],
|
|
114
|
+
["name"],
|
|
115
|
+
),
|
|
116
|
+
(
|
|
117
|
+
"concepts",
|
|
118
|
+
[
|
|
119
|
+
("scheme_urn", "VARCHAR NOT NULL"),
|
|
120
|
+
("concept_id", "VARCHAR NOT NULL"),
|
|
121
|
+
],
|
|
122
|
+
["name", "description"],
|
|
123
|
+
),
|
|
124
|
+
(
|
|
125
|
+
"hierarchy_nodes",
|
|
126
|
+
[
|
|
127
|
+
("hierarchy_urn", "VARCHAR NOT NULL"),
|
|
128
|
+
("node_id", "VARCHAR NOT NULL"),
|
|
129
|
+
("parent_node_id", "VARCHAR"),
|
|
130
|
+
("code_urn", "VARCHAR"),
|
|
131
|
+
# language columns inserted here
|
|
132
|
+
("_annotations", "VARCHAR"),
|
|
133
|
+
("valid_from", "TIMESTAMP"),
|
|
134
|
+
("valid_to", "TIMESTAMP"),
|
|
135
|
+
],
|
|
136
|
+
["name", "description"],
|
|
137
|
+
),
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
# Primary keys that span multiple columns (table_name → comma-separated cols)
|
|
141
|
+
_COMPOSITE_PKS: dict[str, str] = {
|
|
142
|
+
"codes": "codelist_urn, code_id",
|
|
143
|
+
"dsd_components": "dsd_urn, component_id",
|
|
144
|
+
"concepts": "scheme_urn, concept_id",
|
|
145
|
+
"hierarchy_nodes": "hierarchy_urn, node_id",
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
# Indexes to create after tables
|
|
149
|
+
_INDEXES: list[tuple[str, str, str]] = [
|
|
150
|
+
("idx_artefacts_type", "artefacts", "artefact_type"),
|
|
151
|
+
("idx_artefacts_agency_id", "artefacts", "agency, id"),
|
|
152
|
+
("idx_codes_codelist", "codes", "codelist_urn"),
|
|
153
|
+
("idx_hnodes_parent", "hierarchy_nodes", "hierarchy_urn, parent_node_id"),
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# ── DDL generation ───────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _build_create_table(
|
|
161
|
+
table_name: str,
|
|
162
|
+
fixed_columns: list[tuple[str, str]],
|
|
163
|
+
lang_prefixes: list[str],
|
|
164
|
+
languages: Sequence[str],
|
|
165
|
+
) -> str:
|
|
166
|
+
"""Build a CREATE TABLE statement with language columns injected."""
|
|
167
|
+
# Find insertion point: language columns go before the first column
|
|
168
|
+
# whose name starts with '_' or is a trailing fixed column.
|
|
169
|
+
# Strategy: insert lang columns right after the last non-underscore,
|
|
170
|
+
# non-trailing column. Simpler: build all columns in order, injecting
|
|
171
|
+
# lang columns after fixed columns that precede the underscore/trailing ones.
|
|
172
|
+
|
|
173
|
+
cols: list[str] = []
|
|
174
|
+
lang_cols_added = False
|
|
175
|
+
lang_col_strs = [f" {prefix}_{lang} VARCHAR" for prefix in lang_prefixes for lang in languages]
|
|
176
|
+
|
|
177
|
+
for col_name, col_type in fixed_columns:
|
|
178
|
+
# Insert language columns before underscore-prefixed columns
|
|
179
|
+
if not lang_cols_added and col_name.startswith("_"):
|
|
180
|
+
cols.extend(lang_col_strs)
|
|
181
|
+
lang_cols_added = True
|
|
182
|
+
cols.append(f" {col_name} {col_type}")
|
|
183
|
+
|
|
184
|
+
# If no underscore column was found, append lang columns at the end
|
|
185
|
+
if not lang_cols_added:
|
|
186
|
+
cols.extend(lang_col_strs)
|
|
187
|
+
|
|
188
|
+
# Add composite primary key if needed
|
|
189
|
+
if table_name in _COMPOSITE_PKS:
|
|
190
|
+
cols.append(f" PRIMARY KEY ({_COMPOSITE_PKS[table_name]})")
|
|
191
|
+
|
|
192
|
+
if table_name == "artefacts":
|
|
193
|
+
cols.append(" UNIQUE (artefact_type, agency, id, version)")
|
|
194
|
+
|
|
195
|
+
body = ",\n".join(cols)
|
|
196
|
+
return f"""\
|
|
197
|
+
CREATE TABLE IF NOT EXISTS {table_name} (
|
|
198
|
+
{body}
|
|
199
|
+
);"""
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _build_create_index(index_name: str, table_name: str, columns: str) -> str:
|
|
203
|
+
return f"""\
|
|
204
|
+
CREATE INDEX IF NOT EXISTS {index_name}
|
|
205
|
+
ON {table_name}({columns});"""
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# ── Public API ───────────────────────────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def create_tables(
|
|
212
|
+
conn: duckdb.DuckDBPyConnection,
|
|
213
|
+
languages: Sequence[str] = ("en",),
|
|
214
|
+
) -> None:
|
|
215
|
+
"""Create all SDMX structural metadata tables.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
conn: DuckDB connection (file or ``:memory:``).
|
|
219
|
+
languages: Language codes for translatable columns.
|
|
220
|
+
Each configured language generates ``name_{lang}``,
|
|
221
|
+
``label_{lang}``, ``description_{lang}`` etc. columns
|
|
222
|
+
on the relevant tables. Defaults to ``["en"]``.
|
|
223
|
+
|
|
224
|
+
The configured language list is persisted in a ``_meta`` table
|
|
225
|
+
so that writers and readers can discover it later via
|
|
226
|
+
:func:`get_languages`.
|
|
227
|
+
"""
|
|
228
|
+
if not languages:
|
|
229
|
+
msg = "languages must contain at least one language code"
|
|
230
|
+
raise ValueError(msg)
|
|
231
|
+
|
|
232
|
+
# Meta table for configuration
|
|
233
|
+
conn.execute("""
|
|
234
|
+
CREATE TABLE IF NOT EXISTS _meta (
|
|
235
|
+
key VARCHAR PRIMARY KEY,
|
|
236
|
+
value VARCHAR
|
|
237
|
+
);
|
|
238
|
+
""")
|
|
239
|
+
conn.execute(
|
|
240
|
+
"""
|
|
241
|
+
INSERT OR REPLACE INTO _meta (key, value)
|
|
242
|
+
VALUES ($key, $value)
|
|
243
|
+
""",
|
|
244
|
+
{"key": "languages", "value": ",".join(languages)},
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
# Create all structural tables
|
|
248
|
+
for table_name, fixed_columns, lang_prefixes in _TABLE_DEFS:
|
|
249
|
+
ddl = _build_create_table(table_name, fixed_columns, lang_prefixes, languages)
|
|
250
|
+
conn.execute(ddl)
|
|
251
|
+
|
|
252
|
+
# Create indexes
|
|
253
|
+
for index_name, table_name, columns in _INDEXES:
|
|
254
|
+
conn.execute(_build_create_index(index_name, table_name, columns))
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def get_languages(conn: duckdb.DuckDBPyConnection) -> list[str]:
|
|
258
|
+
"""Read the configured language list from the ``_meta`` table.
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
List of language codes (e.g. ``["en", "fr"]``).
|
|
262
|
+
|
|
263
|
+
Raises:
|
|
264
|
+
RuntimeError: If the ``_meta`` table or languages key is missing
|
|
265
|
+
(i.e. :func:`create_tables` was never called).
|
|
266
|
+
"""
|
|
267
|
+
try:
|
|
268
|
+
row = conn.execute("""
|
|
269
|
+
SELECT value FROM _meta WHERE key = 'languages'
|
|
270
|
+
""").fetchone()
|
|
271
|
+
except duckdb.CatalogException:
|
|
272
|
+
msg = "Storage not initialised — call create_tables() first"
|
|
273
|
+
raise RuntimeError(msg) from None
|
|
274
|
+
if row is None:
|
|
275
|
+
msg = "Storage not initialised — call create_tables() first"
|
|
276
|
+
raise RuntimeError(msg)
|
|
277
|
+
return row[0].split(",")
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def tables_exist(conn: duckdb.DuckDBPyConnection) -> bool:
|
|
281
|
+
"""Check whether the storage schema has been created."""
|
|
282
|
+
try:
|
|
283
|
+
conn.execute("""
|
|
284
|
+
SELECT 1 FROM _meta LIMIT 1
|
|
285
|
+
""")
|
|
286
|
+
except duckdb.CatalogException:
|
|
287
|
+
return False
|
|
288
|
+
else:
|
|
289
|
+
return True
|