sdmxlib 0.69.0__tar.gz → 0.70.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.
Files changed (159) hide show
  1. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/PKG-INFO +18 -13
  2. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/README.md +7 -2
  3. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/pyproject.toml +12 -12
  4. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/pyproject.toml.orig +24 -25
  5. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/__init__.py +2 -0
  6. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/__init__.py +68 -0
  7. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_json/_structure_dto.py +20 -0
  8. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_json/_structure_ingest.py +2 -2
  9. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/local/_scan.py +3 -2
  10. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/local/data_store.py +5 -5
  11. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/local/sql.py +9 -4
  12. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/rollup.py +2 -2
  13. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/validation.py +27 -0
  14. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/query/query.py +39 -10
  15. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/query/raw.py +3 -1
  16. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/session.py +63 -6
  17. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/sql.py +1 -1
  18. sdmxlib-0.70.0/src/sdmxlib/storage/_parsed.py +104 -0
  19. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/storage/readers.py +17 -4
  20. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/storage/resolve.py +9 -2
  21. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/storage/writers.py +3 -2
  22. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/_duckdb.py +0 -0
  23. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/_singleflight.py +0 -0
  24. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/_time.py +0 -0
  25. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/api/__init__.py +0 -0
  26. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/api/_freshness.py +0 -0
  27. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/api/client.py +0 -0
  28. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/api/federated.py +0 -0
  29. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/api/policy.py +0 -0
  30. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/api/providers.py +0 -0
  31. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/api/registry.py +0 -0
  32. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/api/session.py +0 -0
  33. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/builders/__init__.py +0 -0
  34. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/builders/contentconstraint.py +0 -0
  35. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/builders/metadata_annotation.py +0 -0
  36. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/data_store.py +0 -0
  37. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/_arrow.py +0 -0
  38. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/_attachment.py +0 -0
  39. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/_coerce.py +0 -0
  40. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/_index.py +0 -0
  41. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/_positions.py +0 -0
  42. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/_refuse.py +0 -0
  43. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/data_format.py +0 -0
  44. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/gaps.py +0 -0
  45. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_csv/__init__.py +0 -0
  46. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_csv/reader.py +0 -0
  47. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_csv/writer.py +0 -0
  48. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_json/__init__.py +0 -0
  49. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_json/_data_common.py +0 -0
  50. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_json/_dto.py +0 -0
  51. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_json/_header.py +0 -0
  52. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_json/_losses.py +0 -0
  53. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_json/_periods.py +0 -0
  54. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_json/_resolve.py +0 -0
  55. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_json/data.py +0 -0
  56. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_json/data_v1.py +0 -0
  57. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_json/data_v2.py +0 -0
  58. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_json/metadata.py +0 -0
  59. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_json/reader.py +0 -0
  60. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_json/structure_values.py +0 -0
  61. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_json/writer.py +0 -0
  62. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_json_1_0/__init__.py +0 -0
  63. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_json_1_0/writer.py +0 -0
  64. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml/__init__.py +0 -0
  65. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml/_allowed_children.py +0 -0
  66. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml/_closed_world.py +0 -0
  67. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml/_common.py +0 -0
  68. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml/_header.py +0 -0
  69. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml/_losses.py +0 -0
  70. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml/_v3_reader.py +0 -0
  71. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml/_v3_writer.py +0 -0
  72. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml21/__init__.py +0 -0
  73. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml21/_providers.py +0 -0
  74. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml21/_synthetic.py +0 -0
  75. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml21/namespaces.py +0 -0
  76. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml21/reader.py +0 -0
  77. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml21/writer.py +0 -0
  78. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml30/__init__.py +0 -0
  79. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml30/metadata.py +0 -0
  80. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml30/namespaces.py +0 -0
  81. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml30/reader.py +0 -0
  82. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml30/writer.py +0 -0
  83. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml31/__init__.py +0 -0
  84. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml31/namespaces.py +0 -0
  85. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml31/reader.py +0 -0
  86. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/formats/sdmx_ml31/writer.py +0 -0
  87. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/local/__init__.py +0 -0
  88. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/local/registry.py +0 -0
  89. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/__init__.py +0 -0
  90. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/_binding.py +0 -0
  91. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/_container.py +0 -0
  92. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/_invariants.py +0 -0
  93. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/_items.py +0 -0
  94. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/_label_match.py +0 -0
  95. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/_rebind.py +0 -0
  96. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/_referenceable.py +0 -0
  97. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/_refshape.py +0 -0
  98. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/_resolvers.py +0 -0
  99. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/_rules.py +0 -0
  100. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/_tree.py +0 -0
  101. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/annotations.py +0 -0
  102. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/base.py +0 -0
  103. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/binding.py +0 -0
  104. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/category.py +0 -0
  105. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/codelist.py +0 -0
  106. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/collections.py +0 -0
  107. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/concept.py +0 -0
  108. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/constraint.py +0 -0
  109. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/convert.py +0 -0
  110. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/dataflow.py +0 -0
  111. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/datastructure.py +0 -0
  112. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/errors.py +0 -0
  113. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/expr.py +0 -0
  114. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/header.py +0 -0
  115. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/hierarchy.py +0 -0
  116. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/in_memory_registry.py +0 -0
  117. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/istring.py +0 -0
  118. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/link.py +0 -0
  119. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/mapping.py +0 -0
  120. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/message.py +0 -0
  121. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/metadata_provision.py +0 -0
  122. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/metadataflow.py +0 -0
  123. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/metadataset.py +0 -0
  124. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/metadatastructure.py +0 -0
  125. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/organisation.py +0 -0
  126. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/provision.py +0 -0
  127. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/ref.py +0 -0
  128. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/registry.py +0 -0
  129. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/representation.py +0 -0
  130. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/rest_api.py +0 -0
  131. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/urn.py +0 -0
  132. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/valuelist.py +0 -0
  133. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/version.py +0 -0
  134. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/model/version_rewrite.py +0 -0
  135. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/polars.py +0 -0
  136. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/py.typed +0 -0
  137. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/query/__init__.py +0 -0
  138. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/query/_closure.py +0 -0
  139. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/query/_common.py +0 -0
  140. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/query/_compile.py +0 -0
  141. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/query/_edges.py +0 -0
  142. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/query/_execute.py +0 -0
  143. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/query/_expr.py +0 -0
  144. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/query/_joins.py +0 -0
  145. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/query/_plan.py +0 -0
  146. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/query/_pushdown.py +0 -0
  147. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/query/_rebase.py +0 -0
  148. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/query/_rest.py +0 -0
  149. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/query/accessors.py +0 -0
  150. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/query/observations.py +0 -0
  151. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/query/schema.py +0 -0
  152. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/rest.py +0 -0
  153. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/storage/__init__.py +0 -0
  154. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/storage/_checked.py +0 -0
  155. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/storage/_kinds.py +0 -0
  156. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/storage/_projections.py +0 -0
  157. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/storage/lazy.py +0 -0
  158. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/storage/schema.py +0 -0
  159. {sdmxlib-0.69.0 → sdmxlib-0.70.0}/src/sdmxlib/versions.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sdmxlib
3
- Version: 0.69.0
3
+ Version: 0.70.0
4
4
  Summary: SDMX structural metadata library for Python
5
5
  Keywords: sdmx,statistics,metadata,datastructure
6
6
  Author: gabrielgellner
@@ -13,18 +13,18 @@ Classifier: Programming Language :: Python :: 3
13
13
  Classifier: Programming Language :: Python :: 3.14
14
14
  Classifier: Topic :: Scientific/Engineering :: Information Analysis
15
15
  Requires-Dist: attrs>=26.1.0
16
- Requires-Dist: duckdb>=1.5.6
16
+ Requires-Dist: duckdb>=2.0.0
17
17
  Requires-Dist: httpx>=0.28.1
18
- Requires-Dist: lxml>=6.0.2
19
- Requires-Dist: msgspec>=0.19
20
- Requires-Dist: polars>=1.39.3
21
- Requires-Dist: pyarrow>=18.0.0
22
- Requires-Dist: typing-extensions>=4.13
18
+ Requires-Dist: lxml>=6.1.3
19
+ Requires-Dist: msgspec>=0.22.0
20
+ Requires-Dist: polars>=2.0.0
21
+ Requires-Dist: pyarrow>=26.0.0
22
+ Requires-Dist: typing-extensions>=4.16.0
23
23
  Requires-Python: >=3.14
24
- Project-URL: Homepage, https://gitlab.com/pinax-suite/sdmxlib
25
- Project-URL: Repository, https://gitlab.com/pinax-suite/sdmxlib
26
- Project-URL: Documentation, https://pinax-suite.gitlab.io/sdmxlib
27
- Project-URL: Changelog, https://gitlab.com/pinax-suite/sdmxlib/-/blob/main/CHANGELOG.md
24
+ Project-URL: Homepage, https://github.com/StatCat-Suite/sdmxlib
25
+ Project-URL: Repository, https://github.com/StatCat-Suite/sdmxlib
26
+ Project-URL: Documentation, https://statcat-suite.github.io/sdmxlib
27
+ Project-URL: Changelog, https://github.com/StatCat-Suite/sdmxlib/blob/main/CHANGELOG.md
28
28
  Description-Content-Type: text/markdown
29
29
 
30
30
  # sdmxlib
@@ -120,7 +120,7 @@ library's actual surface. Point your assistant at them.
120
120
  ## Upgrading
121
121
 
122
122
  sdmxlib is pre-stability and breaks cleanly rather than carrying two spellings
123
- of the same idea. [Upgrading](https://pinax-suite.gitlab.io/sdmxlib/upgrading/)
123
+ of the same idea. [Upgrading](https://statcat-suite.github.io/sdmxlib/upgrading/)
124
124
  lists every breaking change since 0.48, with the old and new spelling of each —
125
125
  `sl.Registry` is now `sl.LocalRegistry`, `get()` takes a URN and a resolution
126
126
  depth, the four query DSLs are one path language, and every model class is
@@ -128,7 +128,7 @@ frozen.
128
128
 
129
129
  ## Documentation
130
130
 
131
- See the [docs](https://pinax-suite.gitlab.io/sdmxlib/) for the full cookbook,
131
+ See the [docs](https://statcat-suite.github.io/sdmxlib/) for the full cookbook,
132
132
  including:
133
133
 
134
134
  - Connecting to registries and choosing a resolution depth
@@ -148,3 +148,8 @@ just test # unit tests
148
148
  just test-integration # end-to-end tests, replayed from recorded cassettes
149
149
  just ci # the full gate: lint, typecheck, unit, docs, integration
150
150
  ```
151
+
152
+ Issues are tracked on [GitHub](https://github.com/StatCat-Suite/sdmxlib/issues).
153
+ The project started on GitLab, so a bare `#492` in code, comments or docs means
154
+ an issue in the [GitLab tracker](https://gitlab.com/pinax-suite/sdmxlib/-/issues),
155
+ which stays readable as an archive. GitHub issues are written `GH-12`.
@@ -91,7 +91,7 @@ library's actual surface. Point your assistant at them.
91
91
  ## Upgrading
92
92
 
93
93
  sdmxlib is pre-stability and breaks cleanly rather than carrying two spellings
94
- of the same idea. [Upgrading](https://pinax-suite.gitlab.io/sdmxlib/upgrading/)
94
+ of the same idea. [Upgrading](https://statcat-suite.github.io/sdmxlib/upgrading/)
95
95
  lists every breaking change since 0.48, with the old and new spelling of each —
96
96
  `sl.Registry` is now `sl.LocalRegistry`, `get()` takes a URN and a resolution
97
97
  depth, the four query DSLs are one path language, and every model class is
@@ -99,7 +99,7 @@ frozen.
99
99
 
100
100
  ## Documentation
101
101
 
102
- See the [docs](https://pinax-suite.gitlab.io/sdmxlib/) for the full cookbook,
102
+ See the [docs](https://statcat-suite.github.io/sdmxlib/) for the full cookbook,
103
103
  including:
104
104
 
105
105
  - Connecting to registries and choosing a resolution depth
@@ -119,3 +119,8 @@ just test # unit tests
119
119
  just test-integration # end-to-end tests, replayed from recorded cassettes
120
120
  just ci # the full gate: lint, typecheck, unit, docs, integration
121
121
  ```
122
+
123
+ Issues are tracked on [GitHub](https://github.com/StatCat-Suite/sdmxlib/issues).
124
+ The project started on GitLab, so a bare `#492` in code, comments or docs means
125
+ an issue in the [GitLab tracker](https://gitlab.com/pinax-suite/sdmxlib/-/issues),
126
+ which stays readable as an archive. GitHub issues are written `GH-12`.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sdmxlib"
3
- version = "0.69.0"
3
+ version = "0.70.0"
4
4
  description = "SDMX structural metadata library for Python"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.14"
@@ -20,13 +20,13 @@ classifiers = [
20
20
  ]
21
21
  dependencies = [
22
22
  "attrs>=26.1.0",
23
- "duckdb>=1.5.6",
23
+ "duckdb>=2.0.0",
24
24
  "httpx>=0.28.1",
25
- "lxml>=6.0.2",
26
- "msgspec>=0.19",
27
- "polars>=1.39.3",
28
- "pyarrow>=18.0.0",
29
- "typing-extensions>=4.13",
25
+ "lxml>=6.1.3",
26
+ "msgspec>=0.22.0",
27
+ "polars>=2.0.0",
28
+ "pyarrow>=26.0.0",
29
+ "typing-extensions>=4.16.0",
30
30
  ]
31
31
 
32
32
  [project.license]
@@ -37,10 +37,10 @@ name = "gabrielgellner"
37
37
  email = "gabrielgellner@gmail.com"
38
38
 
39
39
  [project.urls]
40
- Homepage = "https://gitlab.com/pinax-suite/sdmxlib"
41
- Repository = "https://gitlab.com/pinax-suite/sdmxlib"
42
- Documentation = "https://pinax-suite.gitlab.io/sdmxlib"
43
- Changelog = "https://gitlab.com/pinax-suite/sdmxlib/-/blob/main/CHANGELOG.md"
40
+ Homepage = "https://github.com/StatCat-Suite/sdmxlib"
41
+ Repository = "https://github.com/StatCat-Suite/sdmxlib"
42
+ Documentation = "https://statcat-suite.github.io/sdmxlib"
43
+ Changelog = "https://github.com/StatCat-Suite/sdmxlib/blob/main/CHANGELOG.md"
44
44
 
45
45
  [build-system]
46
46
  requires = ["uv_build>=0.13.0,<0.14.0"]
@@ -105,7 +105,7 @@ dev = [
105
105
  "types-PyYAML>=6.0.12",
106
106
  "mkdocs-material>=9.7.6",
107
107
  "mkdocstrings[python]>=1.0.3",
108
- "polars>=1.39.3",
108
+ "polars>=2.0.0",
109
109
  "pytest>=9.0.2",
110
110
  "jsonschema[format-nongpl]>=4.21.0",
111
111
  "pytest-benchmark>=4.0.0",
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sdmxlib"
3
- version = "0.69.0"
3
+ version = "0.70.0"
4
4
  description = "SDMX structural metadata library for Python"
5
5
  readme = "README.md"
6
6
  license = { text = "Apache-2.0" }
@@ -17,37 +17,36 @@ classifiers = [
17
17
  ]
18
18
  dependencies = [
19
19
  "attrs>=26.1.0",
20
- # >=1.5.3: ducklake extension must support DuckLake catalog format 1.0,
21
- # which is what a bundle built with duckdb 1.5.3 carries. duckdb 1.5.1's
22
- # ducklake tops out at format 0.4 and fails to ATTACH such bundles with "DuckLake catalog version mismatch".
23
- #
24
- # Moved to >=1.5.5 while investigating #400 — two patch releases of fixes,
25
- # taken on their own merits. It is **not** a fix for #400: 1.5.0, 1.5.1,
26
- # 1.5.2, 1.5.3, 1.5.4, 1.5.5 and 1.6.0.dev379 all read the affected bundle
27
- # identically, so that defect is in the file rather than in the reader.
28
- #
29
- # >=1.5.6: test_annotation_subtree_read (#426) asserts the annotation
30
- # subtree read uses the owner index. 1.5.5's planner chooses a SEQ_SCAN
31
- # over `owner_artefact_urn = ?` (502 rows read) and 1.5.6 uses the index,
32
- # so on 1.5.5 the read scans every annotation row.
33
- "duckdb>=1.5.6",
20
+ # >=2.0: an `execute()` result has no `pl()` any more, so frames come from
21
+ # a relation (`sql().pl()`), and a relation call on a handle holding an
22
+ # unfinished result raises rather than clobbering it (`session.completed`).
23
+ # The tests assert 2.0's profile JSON, error text and sort costs.
24
+ "duckdb>=2.0.0",
34
25
  "httpx>=0.28.1",
35
- "lxml>=6.0.2",
36
- "msgspec>=0.19",
37
- "polars>=1.39.3",
38
- "pyarrow>=18.0.0",
26
+ # >=6.1: `resolve_entities` defaults to 'internal' (CVE-2026-41066), so no
27
+ # parse expands an external entity.
28
+ "lxml>=6.1.3",
29
+ # >=0.22: on 3.14 an older msgspec cannot resolve a Struct field's forward
30
+ # reference (`list[SentinelValueJson]` in `_structure_dto`), and importing
31
+ # sdmxlib raises NameError.
32
+ "msgspec>=0.22.0",
33
+ # >=2.0: a cast no longer parses text into a date, so `_typed` parses it
34
+ # (`query._converted`); and `collect()` streams, so an order is sorted for.
35
+ "polars>=2.0.0",
36
+ # >=22 is the first with Python 3.14 wheels; 26 is what the suite runs.
37
+ "pyarrow>=26.0.0",
39
38
  # PEP 728's `closed=True` TypedDict, which `typing` gains only in 3.15: a
40
39
  # TypedDict unpacked into a constructor (`**read_reference_attrs(el)`) must
41
40
  # be closed for a checker to know it holds no other key. httpx already
42
41
  # brings it in, through anyio; declared because we now import it.
43
- "typing-extensions>=4.13",
42
+ "typing-extensions>=4.16.0",
44
43
  ]
45
44
 
46
45
  [project.urls]
47
- Homepage = "https://gitlab.com/pinax-suite/sdmxlib"
48
- Repository = "https://gitlab.com/pinax-suite/sdmxlib"
49
- Documentation = "https://pinax-suite.gitlab.io/sdmxlib"
50
- Changelog = "https://gitlab.com/pinax-suite/sdmxlib/-/blob/main/CHANGELOG.md"
46
+ Homepage = "https://github.com/StatCat-Suite/sdmxlib"
47
+ Repository = "https://github.com/StatCat-Suite/sdmxlib"
48
+ Documentation = "https://statcat-suite.github.io/sdmxlib"
49
+ Changelog = "https://github.com/StatCat-Suite/sdmxlib/blob/main/CHANGELOG.md"
51
50
 
52
51
  [build-system]
53
52
  requires = ["uv_build>=0.13.0,<0.14.0"]
@@ -119,7 +118,7 @@ dev = [
119
118
  "types-PyYAML>=6.0.12",
120
119
  "mkdocs-material>=9.7.6",
121
120
  "mkdocstrings[python]>=1.0.3",
122
- "polars>=1.39.3",
121
+ "polars>=2.0.0",
123
122
  "pytest>=9.0.2",
124
123
  "jsonschema[format-nongpl]>=4.21.0",
125
124
  "pytest-benchmark>=4.0.0",
@@ -54,6 +54,7 @@ from sdmxlib.model.validation import (
54
54
  ReferenceIssue,
55
55
  ReferenceValidationError,
56
56
  SdmxConformanceError,
57
+ SdmxErrorMessage,
57
58
  SdmxParseError,
58
59
  ValidationError,
59
60
  ValidationIssue,
@@ -380,6 +381,7 @@ __all__ = [
380
381
  "RowKind",
381
382
  "SchemaInspector",
382
383
  "SdmxConformanceError",
384
+ "SdmxErrorMessage",
383
385
  "SdmxParseError",
384
386
  "SdmxRef",
385
387
  "SdmxUrn",
@@ -58,6 +58,7 @@ from sdmxlib.model.registry import SupportsAddAll
58
58
  from sdmxlib.model.validation import (
59
59
  LossyReadError,
60
60
  SdmxConformanceError,
61
+ SdmxErrorMessage,
61
62
  SdmxParseError,
62
63
  ValidationError,
63
64
  check_metadata_serialisable,
@@ -163,6 +164,9 @@ def parse(
163
164
  refusals and carries them as ``.refused``, with everything that
164
165
  was read as ``.message``. When ``lossless`` and anything was
165
166
  lost, `LossyReadError`, carrying ``.lost`` and ``.message``.
167
+ When the document is an SDMX error message (``<mes:Error>``,
168
+ ``{"errors": [...]}``), `SdmxErrorMessage`, carrying each
169
+ error's code and text as ``.errors``.
166
170
  """
167
171
  try:
168
172
  # Read straight into an in-memory registry: its references are bound
@@ -201,6 +205,7 @@ def parse(
201
205
  def _dispatch(data: bytes, *, version: str | None) -> StructureMessage | MetadataMessage:
202
206
  """Detect format and message kind, then hand off to the matching reader."""
203
207
  detected = version or _detect_version(data)
208
+ _refuse_error_message(data, detected)
204
209
  kind = _detect_message_kind(data, detected)
205
210
 
206
211
  if kind == "metadata":
@@ -334,6 +339,69 @@ _MESSAGE_NS_VERSION = {_MES_21: "2.1", _MES_30: "3.0", _MES_31: "3.1"}
334
339
  #: Local name of the SDMX-ML root element that carries reported metadata.
335
340
  _METADATA_ROOT = "GenericMetadata"
336
341
 
342
+ #: Local name of the SDMX-ML root element of an error message, and of each error in it.
343
+ _ERROR_ROOT = "Error"
344
+ _ERROR_ENTRY = "ErrorMessage"
345
+
346
+
347
+ class _ErrorProbeEntry(msgspec.Struct):
348
+ """One SDMX-JSON error. ``message`` is not in the schema; FMR 12.4 writes it."""
349
+
350
+ code: int | float | str
351
+ title: str | None = None
352
+ detail: str | None = None
353
+ message: str | None = None
354
+
355
+
356
+ class _ErrorProbe(msgspec.Struct):
357
+ """Just enough of an SDMX-JSON document to tell an error message from the rest."""
358
+
359
+ errors: list[_ErrorProbeEntry] | None = None
360
+ data: msgspec.Raw | None = None
361
+
362
+
363
+ def _refuse_error_message(data: bytes, version: str) -> None:
364
+ """Raise `SdmxErrorMessage` if *data* is an SDMX error message (GH-31).
365
+
366
+ SDMX-ML: the root is ``<mes:Error>``. SDMX-JSON: ``errors`` is non-empty
367
+ and there is no ``data``, since a message may also carry errors beside
368
+ what it answers. A JSON payload this probe cannot decode is left to the
369
+ reader, so the reader's own error is the one raised.
370
+
371
+ Raises:
372
+ SdmxErrorMessage: Carrying each error's code and text.
373
+ """
374
+ if version == "json":
375
+ try:
376
+ probe = msgspec.json.decode(data, type=_ErrorProbe)
377
+ except msgspec.MsgspecError:
378
+ return
379
+ if probe.errors and probe.data is None:
380
+ raise SdmxErrorMessage(tuple((_code(e.code), e.title or e.detail or e.message or "") for e in probe.errors))
381
+ return
382
+ root = _xml_root(data)
383
+ if root is None or root.localname != _ERROR_ROOT or root.namespace not in _MESSAGE_NS_VERSION:
384
+ return
385
+ entries = [
386
+ el for el in etree.fromstring(data) if isinstance(el.tag, str) and etree.QName(el).localname == _ERROR_ENTRY
387
+ ]
388
+ raise SdmxErrorMessage(tuple((el.get("code", ""), _first_text(el)) for el in entries))
389
+
390
+
391
+ def _code(code: float | str) -> str:
392
+ """An SDMX-JSON error code as SDMX-ML writes it: ``404``, not ``404.0``."""
393
+ if isinstance(code, float) and code.is_integer():
394
+ return str(int(code))
395
+ return str(code)
396
+
397
+
398
+ def _first_text(entry: etree._Element) -> str:
399
+ """The first ``<com:Text>`` of an ``<ErrorMessage>``; a message may give several languages."""
400
+ for child in entry:
401
+ if isinstance(child.tag, str) and etree.QName(child).localname == "Text":
402
+ return (child.text or "").strip()
403
+ return ""
404
+
337
405
 
338
406
  def _xml_root(data: bytes) -> etree.QName | None:
339
407
  """The qualified name of the XML root element, or None if *data* is not XML.
@@ -367,6 +367,14 @@ class Format(WireStruct, rename=_camel_rename):
367
367
  end_time: str | None = None
368
368
  #: Values with a special meaning (#422) — a text format's alone.
369
369
  sentinel_values: list[SentinelValueJson] | None = None
370
+ #: Read tolerance: SDMX-JSON 1.0's name for `data_type` (GH-30), moved
371
+ #: there as it is read so nothing downstream sees two spellings.
372
+ text_type: str | None = None
373
+
374
+ def __post_init__(self) -> None:
375
+ """Read 1.0's `text_type` as `data_type`."""
376
+ if self.text_type is not None and self.data_type is None:
377
+ self.data_type, self.text_type = self.text_type, None
370
378
 
371
379
 
372
380
  class SentinelValueJson(WireStruct, rename=_camel_rename):
@@ -404,6 +412,9 @@ class Representation(WireStruct, rename=_camel_rename):
404
412
  #: or ``"unbounded"``.
405
413
  min_occurs: int | None = None
406
414
  max_occurs: int | str | None = None
415
+ #: Read tolerance: SDMX-JSON 1.0's name for `format` (GH-30), moved there
416
+ #: as it is read, before the ``oneOf`` below counts it.
417
+ text_format: Format | None = None
407
418
 
408
419
  def __post_init__(self) -> None:
409
420
  """Every representation type's ``oneOf``: an enumeration or a format (#474).
@@ -411,6 +422,8 @@ class Representation(WireStruct, rename=_camel_rename):
411
422
  Raises:
412
423
  ValueError: If it has neither, or both.
413
424
  """
425
+ if self.text_format is not None and self.format is None:
426
+ self.format, self.text_format = self.text_format, None
414
427
  # Tested inline before `one_of` builds its arguments: this runs for
415
428
  # every component and concept, and the call alone was 2-5% of a read.
416
429
  if (self.enumeration is None) == (self.format is None):
@@ -871,6 +884,11 @@ class AttributeRelationship(WireStruct, rename=_camel_rename):
871
884
  are_dimensions_optional: list[bool] | None = None
872
885
  group: str | None = None
873
886
  dataflow: EmptyObject | None = None
887
+ #: Read tolerance, SDMX-JSON 1.0's two spellings (GH-30): ``none`` is
888
+ #: `dataflow` (2.1's ``<str:None/>``, a dataset-level attribute), and
889
+ #: ``primaryMeasure`` is `observation`, naming the measure 2.1 relates it to.
890
+ none: EmptyObject | None = None
891
+ primary_measure: str | None = None
874
892
 
875
893
  def __post_init__(self) -> None:
876
894
  """The schema's ``oneOf`` (#474).
@@ -884,6 +902,8 @@ class AttributeRelationship(WireStruct, rename=_camel_rename):
884
902
  dimensions=self.dimensions,
885
903
  group=self.group,
886
904
  observation=self.observation,
905
+ none=self.none,
906
+ primaryMeasure=self.primary_measure,
887
907
  )
888
908
 
889
909
 
@@ -540,9 +540,9 @@ def from_attachment(
540
540
  # Required by the schema; one a document omits varies with nothing,
541
541
  # read as dataset-level as the SDMX-ML readers read it (#421).
542
542
  return AttachmentLevel.DATASET, None, frozenset(), None
543
- if ar.observation is not None:
543
+ if ar.observation is not None or ar.primary_measure is not None:
544
544
  return AttachmentLevel.OBSERVATION, None, frozenset(), None
545
- if ar.dataflow is not None:
545
+ if ar.dataflow is not None or ar.none is not None:
546
546
  return AttachmentLevel.DATASET, None, frozenset(), None
547
547
  if ar.dimensions:
548
548
  # `areDimensionsOptional` is parallel to `dimensions` (#422).
@@ -38,6 +38,7 @@ from sdmxlib.model.datastructure import DataStructure
38
38
  from sdmxlib.model.ref import Ref
39
39
  from sdmxlib.model.urn import SdmxUrn
40
40
  from sdmxlib.query.observations import ObsQuery
41
+ from sdmxlib.session import completed
41
42
 
42
43
  #: The catalog every scan's relation lives in — see the module docstring.
43
44
  SCANS = "sdmx_scans"
@@ -99,11 +100,11 @@ def scan_frame_into(
99
100
  if column in frame.columns
100
101
  )
101
102
  name = relation.rpartition(".")[2]
102
- plane.connection.register(name, typed.to_arrow())
103
+ completed(plane.connection).register(name, typed.to_arrow())
103
104
  try:
104
105
  plane.connection.execute(f"CREATE TABLE {relation} AS SELECT * FROM {name}")
105
106
  finally:
106
- plane.connection.unregister(name)
107
+ completed(plane.connection).unregister(name)
107
108
  return plane.obs(binding, dsd=dsd)
108
109
 
109
110
 
@@ -31,7 +31,7 @@ from sdmxlib.model.ref import Ref
31
31
  from sdmxlib.model.representation import Facet
32
32
  from sdmxlib.query.observations import Observations, ObsQuery, Sidecar
33
33
  from sdmxlib.query.query import build
34
- from sdmxlib.session import SessionFactory
34
+ from sdmxlib.session import SessionFactory, completed
35
35
 
36
36
  if TYPE_CHECKING:
37
37
  # `local.registry` imports this module at runtime: a cycle.
@@ -676,10 +676,10 @@ class DuckDBDataStore(SessionFactory):
676
676
  raise ValueError(msg)
677
677
 
678
678
  conn = self.connection
679
- # Register the source as a temporary view the CTAS can scan.
679
+ # Registered for this session, not created as a view: a view is
680
+ # catalog state, and DuckDB refuses one over a Python object.
680
681
  view_name = f"__sdmxlib_write_obs_{id(source):x}"
681
- rel = _as_relation(conn, source)
682
- rel.create_view(view_name, replace=True)
682
+ completed(conn).register(view_name, _as_relation(conn, source))
683
683
  try:
684
684
  target = f"{lake_alias}.{schema}.{table}"
685
685
  conn.execute(f"CREATE SCHEMA IF NOT EXISTS {lake_alias}.{schema}")
@@ -696,7 +696,7 @@ class DuckDBDataStore(SessionFactory):
696
696
  msg = f"Unknown mode: {mode!r} (expected 'overwrite' or 'append')"
697
697
  raise ValueError(msg)
698
698
  finally:
699
- conn.execute(f"DROP VIEW IF EXISTS {view_name}")
699
+ completed(conn).unregister(view_name)
700
700
  # A rewrite can change the target table's schema; drop cached column
701
701
  # lists so the inspector (internal query path + custom SQL) re-reflects.
702
702
  # The target may be cached under a differently-qualified name than the
@@ -52,6 +52,7 @@ import duckdb
52
52
  from attrs import frozen
53
53
 
54
54
  from sdmxlib.model.binding import DataflowBinding
55
+ from sdmxlib.session import cursor_for
55
56
 
56
57
 
57
58
  class SidecarNotUniqueError(ValueError):
@@ -152,7 +153,9 @@ class SchemaInspector:
152
153
  cached = self._columns_cache.get(table)
153
154
  if cached is not None:
154
155
  return cached
155
- rows = self._conn.execute(f"DESCRIBE {table}").fetchall()
156
+ # On this thread's handle, never `_conn` itself: concurrent
157
+ # `execute()` on one handle overwrite each other's rows (#75).
158
+ rows = cursor_for(self._conn).execute(f"DESCRIBE {table}").fetchall()
156
159
  described = tuple((str(r[0]), str(r[1])) for r in rows)
157
160
  self._columns_cache[table] = described
158
161
  return described
@@ -190,9 +193,11 @@ class SchemaInspector:
190
193
  return cached
191
194
  tuple_sql = ", ".join(keys)
192
195
  not_null = " AND ".join(f"{k} IS NOT NULL" for k in keys)
193
- row = self._conn.execute(
194
- f"SELECT count(*), count(DISTINCT ({tuple_sql})) FROM {table} WHERE {not_null}"
195
- ).fetchone()
196
+ row = (
197
+ cursor_for(self._conn)
198
+ .execute(f"SELECT count(*), count(DISTINCT ({tuple_sql})) FROM {table} WHERE {not_null}")
199
+ .fetchone()
200
+ )
196
201
  counts = (int(row[0]), int(row[1])) if row else (0, 0)
197
202
  self._key_counts_cache[cache_key] = counts
198
203
  return counts
@@ -16,7 +16,7 @@ import attrs
16
16
  import duckdb
17
17
  import polars as pl
18
18
 
19
- from sdmxlib.session import cursor_for
19
+ from sdmxlib.session import completed, cursor_for, frame_of
20
20
  from sdmxlib.sql import SqlRelation
21
21
 
22
22
 
@@ -105,7 +105,7 @@ class AncestorMap:
105
105
  def collect(self) -> pl.DataFrame:
106
106
  """Execute and return the mapping as a Polars frame."""
107
107
  sql, params = self.to_sql()
108
- return cursor_for(self._conn).execute(sql, params).pl()
108
+ return frame_of(completed(cursor_for(self._conn)).sql(sql, params=params))
109
109
 
110
110
  def to_dict(self) -> dict[str, str]:
111
111
  """Execute and return ``{path: rollup_path}``.
@@ -222,6 +222,33 @@ class SdmxConformanceError(SdmxParseError):
222
222
  return cls("Structure", detail, refused=refused, message=message)
223
223
 
224
224
 
225
+ class SdmxErrorMessage(SdmxParseError): # noqa: N818 - SDMX's own element name, `ErrorMessage`
226
+ """Raised by `parse` when the document is an SDMX error message (GH-31).
227
+
228
+ A registry answers a query it cannot serve with an error message —
229
+ ``<mes:Error>`` in SDMX-ML, ``{"errors": [...]}`` in SDMX-JSON — rather
230
+ than a structure or metadata message. `parse` used to read one as an empty
231
+ `StructureMessage`, so a saved "100 No Results Found" was indistinguishable
232
+ from a query that matched nothing. The REST client never gets this far: it
233
+ raises on the HTTP status first. This is for bytes read from a file.
234
+
235
+ A `SdmxParseError`, because the payload is still not the message asked
236
+ for; catching the base handles it with every other unusable document.
237
+
238
+ Attributes:
239
+ errors: One ``(code, text)`` pair per error the message reports, in
240
+ document order. The code is text, as SDMX-ML types it; the text
241
+ is the first one given, or empty when there is none.
242
+ """
243
+
244
+ errors: tuple[tuple[str, str], ...]
245
+
246
+ def __init__(self, errors: tuple[tuple[str, str], ...]) -> None:
247
+ self.errors = errors
248
+ reported = "; ".join(f"{code} {text}".strip() for code, text in errors) or "no error given"
249
+ super().__init__(f"the document is an SDMX error message, not a structure or metadata message: {reported}")
250
+
251
+
225
252
  class LossyReadError(SdmxParseError):
226
253
  """Raised by ``parse(..., lossless=True)`` when the message is not exactly the document (#422, #474).
227
254
 
@@ -73,7 +73,7 @@ from sdmxlib.query._expr import (
73
73
  )
74
74
  from sdmxlib.query._plan import Plan, scanned
75
75
  from sdmxlib.query._pushdown import DUCKDB, Program, Push, plan_program
76
- from sdmxlib.session import cursor_for
76
+ from sdmxlib.session import completed, cursor_for, frame_of
77
77
 
78
78
 
79
79
  def _partition_prefix(root: AnyCols, order: Sequence[OrderKey]) -> tuple[str, ...]:
@@ -127,7 +127,7 @@ def _stream_by_partition[R: RowKind, A](
127
127
  # does not pull the next partition until this one is finished — which
128
128
  # is exactly what keeps peak at one partition rather than their sum.
129
129
  cursor = query._cursor().cursor()
130
- yield cursor.execute(compiled.sql, list(compiled.params)).to_arrow_reader(chunk_rows)
130
+ yield completed(cursor).sql(compiled.sql, params=list(compiled.params)).to_arrow_reader(chunk_rows)
131
131
 
132
132
  stream = readers()
133
133
  first = next(stream)
@@ -174,9 +174,9 @@ def _typed(frame: pl.DataFrame, root: AnyCols, columns: Mapping[str, str] | None
174
174
 
175
175
  Raises:
176
176
  ValueError: If a value cannot take its component's type — a code its
177
- codelist does not list, text where the DSD declares a number. Fail
178
- fast; the message names the way to the raw rows and to the report
179
- of every bad value.
177
+ codelist does not list, text where the DSD declares a number or
178
+ a date. Fail fast; the message names the way to the raw rows and
179
+ to the report of every bad value.
180
180
  """
181
181
  dsd = getattr(root, "dsd", None)
182
182
  if not isinstance(dsd, DataStructure):
@@ -193,7 +193,7 @@ def _typed(frame: pl.DataFrame, root: AnyCols, columns: Mapping[str, str] | None
193
193
  if not casts:
194
194
  return frame
195
195
  try:
196
- return frame.with_columns(pl.col(name).cast(dtype) for name, dtype in casts.items())
196
+ return frame.with_columns(_converted(name, frame.schema[name], dtype) for name, dtype in casts.items())
197
197
  except pl.exceptions.PolarsError as exc:
198
198
  msg = (
199
199
  f"an observation column does not fit its component's type in {dsd.id}: {exc}. Results are "
@@ -203,6 +203,35 @@ def _typed(frame: pl.DataFrame, root: AnyCols, columns: Mapping[str, str] | None
203
203
  raise ValueError(msg) from exc
204
204
 
205
205
 
206
+ #: `xs:boolean`'s lexical forms.
207
+ _BOOLEANS = {"true": True, "false": False, "1": True, "0": False}
208
+
209
+
210
+ def _converted(name: str, stored: pl.DataType, dtype: pl.DataType) -> pl.Expr:
211
+ """Column `name`, stored as `stored`, as `dtype` — parsed when it is text.
212
+
213
+ A cast is not a parse: polars 2.0 casts no text to a date, a datetime or a
214
+ time (1.x did dates only), and no version casts text to a boolean. A value
215
+ stored as ``VARCHAR`` — every observation column is — is parsed instead. A
216
+ datetime with an offset lands in UTC, and every datetime is naive UTC, the
217
+ store's convention (`sdmxlib._time`).
218
+ """
219
+ column = pl.col(name)
220
+ if stored != pl.String():
221
+ return column.cast(dtype)
222
+ match dtype:
223
+ case pl.Date():
224
+ return column.str.to_date()
225
+ case pl.Datetime():
226
+ return column.str.to_datetime(time_unit=dtype.time_unit, time_zone="UTC").dt.replace_time_zone(None)
227
+ case pl.Time():
228
+ return column.str.to_time()
229
+ case pl.Boolean():
230
+ return column.replace_strict(_BOOLEANS, return_dtype=pl.Boolean)
231
+ case _:
232
+ return column.cast(dtype)
233
+
234
+
206
235
  def _declared_components(dsd: DataStructure) -> frozenset[str]:
207
236
  """The components whose type the DSD states — see `_typed`."""
208
237
  found = {
@@ -1613,7 +1642,7 @@ class Query[R: RowKind, A = object]:
1613
1642
  their `Facet` declares.
1614
1643
  """
1615
1644
  compiled = self.to_sql()
1616
- frame = _typed(self._cursor().execute(compiled.sql, list(compiled.params)).pl(), self.root)
1645
+ frame = _typed(frame_of(completed(self._cursor()).sql(compiled.sql, params=list(compiled.params))), self.root)
1617
1646
  if not labels:
1618
1647
  return frame
1619
1648
  return _labelled(frame, self.root, labels, label_suffix)
@@ -1699,7 +1728,7 @@ class Query[R: RowKind, A = object]:
1699
1728
  # on one connection would overwrite each other's row buffer (#75), and
1700
1729
  # the reader holds this one until it is exhausted or closed.
1701
1730
  cursor = self._cursor().cursor()
1702
- return cursor.execute(compiled.sql, list(compiled.params)).to_arrow_reader(chunk_rows)
1731
+ return completed(cursor).sql(compiled.sql, params=list(compiled.params)).to_arrow_reader(chunk_rows)
1703
1732
 
1704
1733
  def _partition_values(self, partitions: tuple[str, ...]) -> list[tuple[object, ...]]:
1705
1734
  """Every partition, in key order — one small query before the stream.
@@ -2355,7 +2384,7 @@ class Select[R: RowKind, A = object]:
2355
2384
  # component id -> the name it was projected under, so an aliased column
2356
2385
  # is typed and labelled under the alias.
2357
2386
  aliases = {item.projected_name: name for name, item in self.items if isinstance(item, AnyCol)}
2358
- frame = _as_utc(cursor.execute(compiled.sql, list(compiled.params)).pl(), self.items)
2387
+ frame = _as_utc(frame_of(completed(cursor).sql(compiled.sql, params=list(compiled.params))), self.items)
2359
2388
  frame = _typed(frame, self._query.root, aliases)
2360
2389
  if not labels:
2361
2390
  return frame
@@ -2450,7 +2479,7 @@ class Select[R: RowKind, A = object]:
2450
2479
  # on one connection would overwrite each other's row buffer (#75), and
2451
2480
  # the reader holds this one until it is exhausted or closed.
2452
2481
  cursor = self._query._cursor(self._query._projection_columns(self.items)).cursor()
2453
- return cursor.execute(compiled.sql, list(compiled.params)).to_arrow_reader(chunk_rows)
2482
+ return completed(cursor).sql(compiled.sql, params=list(compiled.params)).to_arrow_reader(chunk_rows)
2454
2483
 
2455
2484
  @override
2456
2485
  def __repr__(self) -> str: