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.
Files changed (58) hide show
  1. sdmxlib/__init__.py +155 -0
  2. sdmxlib/api/__init__.py +7 -0
  3. sdmxlib/api/client.py +246 -0
  4. sdmxlib/api/filters.py +67 -0
  5. sdmxlib/api/providers.py +66 -0
  6. sdmxlib/api/query.py +428 -0
  7. sdmxlib/api/registry.py +666 -0
  8. sdmxlib/api/session.py +124 -0
  9. sdmxlib/formats/__init__.py +166 -0
  10. sdmxlib/formats/sdmx_csv/__init__.py +5 -0
  11. sdmxlib/formats/sdmx_csv/reader.py +147 -0
  12. sdmxlib/formats/sdmx_json/__init__.py +1 -0
  13. sdmxlib/formats/sdmx_json/reader.py +897 -0
  14. sdmxlib/formats/sdmx_json/writer.py +698 -0
  15. sdmxlib/formats/sdmx_ml21/__init__.py +5 -0
  16. sdmxlib/formats/sdmx_ml21/namespaces.py +12 -0
  17. sdmxlib/formats/sdmx_ml21/reader.py +1147 -0
  18. sdmxlib/formats/sdmx_ml21/writer.py +709 -0
  19. sdmxlib/formats/sdmx_ml30/__init__.py +1 -0
  20. sdmxlib/formats/sdmx_ml30/namespaces.py +12 -0
  21. sdmxlib/formats/sdmx_ml30/reader.py +1138 -0
  22. sdmxlib/formats/sdmx_ml30/writer.py +730 -0
  23. sdmxlib/local/__init__.py +6 -0
  24. sdmxlib/local/_registry.py +368 -0
  25. sdmxlib/model/__init__.py +122 -0
  26. sdmxlib/model/annotations.py +103 -0
  27. sdmxlib/model/base.py +61 -0
  28. sdmxlib/model/category.py +88 -0
  29. sdmxlib/model/codelist.py +91 -0
  30. sdmxlib/model/collections.py +382 -0
  31. sdmxlib/model/concept.py +121 -0
  32. sdmxlib/model/constraint.py +92 -0
  33. sdmxlib/model/convert.py +268 -0
  34. sdmxlib/model/dataflow.py +49 -0
  35. sdmxlib/model/dataset.py +337 -0
  36. sdmxlib/model/datastructure.py +390 -0
  37. sdmxlib/model/expr.py +120 -0
  38. sdmxlib/model/hierarchy.py +111 -0
  39. sdmxlib/model/istring.py +81 -0
  40. sdmxlib/model/mapping.py +134 -0
  41. sdmxlib/model/message.py +70 -0
  42. sdmxlib/model/organisation.py +149 -0
  43. sdmxlib/model/provision.py +45 -0
  44. sdmxlib/model/ref.py +195 -0
  45. sdmxlib/model/registry.py +191 -0
  46. sdmxlib/model/representation.py +99 -0
  47. sdmxlib/model/urn.py +130 -0
  48. sdmxlib/model/validation.py +338 -0
  49. sdmxlib/polars.py +392 -0
  50. sdmxlib/py.typed +0 -0
  51. sdmxlib/storage/__init__.py +28 -0
  52. sdmxlib/storage/lazy.py +329 -0
  53. sdmxlib/storage/readers.py +310 -0
  54. sdmxlib/storage/schema.py +289 -0
  55. sdmxlib/storage/writers.py +503 -0
  56. sdmxlib-0.8.0.dist-info/METADATA +102 -0
  57. sdmxlib-0.8.0.dist-info/RECORD +58 -0
  58. 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