sdmxlib 0.65.0__tar.gz → 0.67.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 (135) hide show
  1. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/PKG-INFO +1 -1
  2. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/pyproject.toml +4 -47
  3. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/pyproject.toml.orig +7 -131
  4. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/api/filters.py +1 -1
  5. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/api/registry.py +1 -1
  6. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/api/session.py +2 -2
  7. sdmxlib-0.67.0/src/sdmxlib/formats/_arrow.py +28 -0
  8. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/formats/sdmx_csv/writer.py +4 -5
  9. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/formats/sdmx_json/_data_common.py +3 -5
  10. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/formats/sdmx_json/_resolve.py +23 -3
  11. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/formats/sdmx_json/_structure_ingest.py +17 -17
  12. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/formats/sdmx_json/writer.py +13 -0
  13. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/formats/sdmx_ml/_common.py +4 -4
  14. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/local/registry.py +14 -29
  15. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/model/annotations.py +1 -1
  16. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/model/base.py +4 -5
  17. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/model/binding.py +4 -4
  18. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/model/category.py +5 -4
  19. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/model/codelist.py +4 -2
  20. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/model/collections.py +20 -34
  21. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/model/concept.py +2 -2
  22. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/model/constraint.py +5 -5
  23. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/model/dataflow.py +2 -2
  24. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/model/dataset.py +14 -7
  25. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/model/datastructure.py +33 -19
  26. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/model/expr.py +2 -2
  27. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/model/hierarchy.py +10 -10
  28. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/model/in_memory_registry.py +5 -6
  29. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/model/mapping.py +18 -12
  30. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/model/message.py +21 -35
  31. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/model/metadata_provision.py +12 -5
  32. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/model/metadataflow.py +2 -2
  33. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/model/metadataset.py +5 -3
  34. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/model/metadatastructure.py +8 -6
  35. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/model/organisation.py +4 -4
  36. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/model/provision.py +6 -5
  37. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/model/ref.py +67 -12
  38. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/model/urn.py +79 -26
  39. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/model/validation.py +37 -28
  40. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/model/version.py +13 -3
  41. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/query/_expr.py +8 -8
  42. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/query/_plan.py +20 -1
  43. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/query/query.py +9 -12
  44. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/storage/lazy.py +3 -3
  45. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/storage/readers.py +156 -195
  46. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/storage/resolve.py +3 -4
  47. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/storage/writers.py +10 -2
  48. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/README.md +0 -0
  49. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/__init__.py +0 -0
  50. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/_duckdb.py +0 -0
  51. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/_singleflight.py +0 -0
  52. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/_time.py +0 -0
  53. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/api/__init__.py +0 -0
  54. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/api/_freshness.py +0 -0
  55. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/api/client.py +0 -0
  56. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/api/federated.py +0 -0
  57. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/api/policy.py +0 -0
  58. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/api/providers.py +0 -0
  59. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/api/query.py +0 -0
  60. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/builders/__init__.py +0 -0
  61. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/builders/contentconstraint.py +0 -0
  62. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/builders/metadata_annotation.py +0 -0
  63. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/data_store.py +0 -0
  64. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/formats/__init__.py +0 -0
  65. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/formats/_coerce.py +0 -0
  66. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/formats/_index.py +0 -0
  67. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/formats/data_format.py +0 -0
  68. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/formats/gaps.py +0 -0
  69. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/formats/sdmx_csv/__init__.py +0 -0
  70. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/formats/sdmx_csv/reader.py +0 -0
  71. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/formats/sdmx_json/__init__.py +0 -0
  72. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/formats/sdmx_json/_dto.py +0 -0
  73. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/formats/sdmx_json/_structure_dto.py +0 -0
  74. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/formats/sdmx_json/data_v1.py +0 -0
  75. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/formats/sdmx_json/data_v2.py +0 -0
  76. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/formats/sdmx_json/metadata.py +0 -0
  77. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/formats/sdmx_json/reader.py +0 -0
  78. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/formats/sdmx_json/structure_values.py +0 -0
  79. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/formats/sdmx_json_1_0/__init__.py +0 -0
  80. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/formats/sdmx_json_1_0/writer.py +0 -0
  81. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/formats/sdmx_ml/__init__.py +0 -0
  82. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/formats/sdmx_ml/_v3_reader.py +0 -0
  83. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/formats/sdmx_ml/_v3_writer.py +0 -0
  84. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/formats/sdmx_ml21/__init__.py +0 -0
  85. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/formats/sdmx_ml21/_synthetic.py +0 -0
  86. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/formats/sdmx_ml21/namespaces.py +0 -0
  87. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/formats/sdmx_ml21/reader.py +0 -0
  88. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/formats/sdmx_ml21/writer.py +0 -0
  89. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/formats/sdmx_ml30/__init__.py +0 -0
  90. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/formats/sdmx_ml30/metadata.py +0 -0
  91. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/formats/sdmx_ml30/namespaces.py +0 -0
  92. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/formats/sdmx_ml30/reader.py +0 -0
  93. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/formats/sdmx_ml30/writer.py +0 -0
  94. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/formats/sdmx_ml31/__init__.py +0 -0
  95. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/formats/sdmx_ml31/namespaces.py +0 -0
  96. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/formats/sdmx_ml31/reader.py +0 -0
  97. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/formats/sdmx_ml31/writer.py +0 -0
  98. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/local/__init__.py +0 -0
  99. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/local/data_store.py +0 -0
  100. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/local/sql.py +0 -0
  101. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/model/__init__.py +0 -0
  102. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/model/_interning.py +0 -0
  103. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/model/_items.py +0 -0
  104. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/model/_label_match.py +0 -0
  105. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/model/_refshape.py +0 -0
  106. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/model/convert.py +0 -0
  107. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/model/errors.py +0 -0
  108. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/model/istring.py +0 -0
  109. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/model/registry.py +0 -0
  110. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/model/representation.py +0 -0
  111. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/model/rest_api.py +0 -0
  112. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/model/rollup.py +0 -0
  113. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/polars.py +0 -0
  114. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/py.typed +0 -0
  115. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/query/__init__.py +0 -0
  116. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/query/_closure.py +0 -0
  117. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/query/_common.py +0 -0
  118. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/query/_compile.py +0 -0
  119. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/query/_edges.py +0 -0
  120. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/query/_execute.py +0 -0
  121. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/query/_joins.py +0 -0
  122. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/query/_pushdown.py +0 -0
  123. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/query/_rebase.py +0 -0
  124. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/query/_rest.py +0 -0
  125. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/query/accessors.py +0 -0
  126. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/query/observations.py +0 -0
  127. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/query/raw.py +0 -0
  128. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/query/schema.py +0 -0
  129. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/rest.py +0 -0
  130. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/session.py +0 -0
  131. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/sql.py +0 -0
  132. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/storage/__init__.py +0 -0
  133. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/storage/_kinds.py +0 -0
  134. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/storage/_projections.py +0 -0
  135. {sdmxlib-0.65.0 → sdmxlib-0.67.0}/src/sdmxlib/storage/schema.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sdmxlib
3
- Version: 0.65.0
3
+ Version: 0.67.0
4
4
  Summary: SDMX structural metadata library for Python
5
5
  Keywords: sdmx,statistics,metadata,datastructure
6
6
  Author: gabrielgellner
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sdmxlib"
3
- version = "0.65.0"
3
+ version = "0.67.0"
4
4
  description = "SDMX structural metadata library for Python"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -72,51 +72,6 @@ exclude_lines = [
72
72
  "@overload",
73
73
  ]
74
74
 
75
- [tool.basedpyright]
76
- pythonVersion = "3.13"
77
- pythonPlatform = "Linux"
78
- venvPath = "."
79
- venv = ".venv"
80
- typeCheckingMode = "standard"
81
- reportUnusedCallResult = false
82
- reportMissingTypeArgument = "error"
83
- reportMissingParameterType = "error"
84
- reportUnnecessaryCast = "error"
85
- reportPropertyTypeMismatch = "error"
86
- reportInconsistentConstructor = "error"
87
- reportCallInDefaultInitializer = "error"
88
- reportUnnecessaryContains = "error"
89
- reportUnnecessaryTypeIgnoreComment = "error"
90
-
91
- [[tool.basedpyright.executionEnvironments]]
92
- root = "tests/integration"
93
- extraPaths = ["."]
94
- reportMissingParameterType = false
95
- reportOptionalMemberAccess = false
96
-
97
- [[tool.basedpyright.executionEnvironments]]
98
- root = "tests/docs"
99
- extraPaths = ["."]
100
- reportMissingParameterType = false
101
- reportOptionalMemberAccess = false
102
-
103
- [[tool.basedpyright.executionEnvironments]]
104
- root = "tests/unit"
105
- extraPaths = ["."]
106
- reportMissingParameterType = false
107
- reportOptionalMemberAccess = false
108
-
109
- [[tool.basedpyright.executionEnvironments]]
110
- root = "tests/bench"
111
- extraPaths = ["."]
112
- reportMissingParameterType = false
113
- reportOptionalMemberAccess = false
114
- reportInvalidCast = "error"
115
- reportDeprecated = "error"
116
- reportUnsafeMultipleInheritance = "error"
117
- reportTypeCommentUsage = "error"
118
- reportSelfClsParameterName = "error"
119
-
120
75
  [tool.mutmut]
121
76
  source_paths = ["src"]
122
77
  only_mutate = [
@@ -140,9 +95,10 @@ mutate_only_covered_lines = false
140
95
 
141
96
  [dependency-groups]
142
97
  dev = [
143
- "basedpyright==1.39.9",
144
98
  "catppuccin[pygments]>=2.5.0",
145
99
  "types-lxml>=2025.3.30",
100
+ "types-jsonschema>=4.21.0",
101
+ "types-PyYAML>=6.0.12",
146
102
  "mkdocs-material>=9.7.6",
147
103
  "mkdocstrings[python]>=1.0.3",
148
104
  "polars>=1.39.3",
@@ -150,6 +106,7 @@ dev = [
150
106
  "jsonschema>=4.21.0",
151
107
  "pytest-benchmark>=4.0.0",
152
108
  "pytest-cov>=7.1.0",
109
+ "pytest-xdist>=3.8.0",
153
110
  "pytest-memray>=1.7.0",
154
111
  "pytest-recording>=0.13.4",
155
112
  "pydoclint>=0.9.1,<0.10",
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sdmxlib"
3
- version = "0.65.0"
3
+ version = "0.67.0"
4
4
  description = "SDMX structural metadata library for Python"
5
5
  readme = "README.md"
6
6
  license = { text = "Apache-2.0" }
@@ -92,126 +92,6 @@ exclude_lines = [
92
92
  "@overload",
93
93
  ]
94
94
 
95
- [tool.basedpyright]
96
- pythonVersion = "3.13"
97
- pythonPlatform = "Linux"
98
- venvPath = "."
99
- venv = ".venv"
100
- # "standard" rather than "strict" because the strict-only rules
101
- # (reportUnknownMemberType, reportUnknownArgumentType,
102
- # reportUnknownVariableType) fire on every duckdb / lxml / pyarrow
103
- # touch — third-party stubs we don't control. Standard mode still
104
- # catches the bugs we care about (assignments, attribute access,
105
- # return types, missing arguments). Strict-mode rules that *are*
106
- # enforceable on this codebase are turned on manually via
107
- # `# pyright: ignore[...]` opt-outs at the call sites that need them.
108
- typeCheckingMode = "standard"
109
- # `pytest.raises(...)` doesn't bind a name — without this every
110
- # `with pytest.raises(...):` block would need `# pyright: ignore`.
111
- # Off project-wide since the only places that consume return values
112
- # we *don't* care about are tests.
113
- reportUnusedCallResult = false
114
-
115
- # Strict-mode rules that ARE clean on this codebase (0 findings), turned on
116
- # to catch regressions the flood-prone `reportUnknown*` rules would bury.
117
- # The first two are basedpyright's Any-degradation catchers — the closest it
118
- # gets to pyrefly's `implicit-any` family without the duckdb/pyarrow noise:
119
- # * reportMissingTypeArgument — a bare generic (`list` not `list[str]`) → Any.
120
- # * reportMissingParameterType — an untyped parameter → Any.
121
- # The rest are pure hardening: dead casts (aligns with "narrow, don't cast"),
122
- # getter/setter type drift, __init__/__new__ disagreement, mutable-default
123
- # gotchas, and always-true/false `in` checks.
124
- reportMissingTypeArgument = "error"
125
- reportMissingParameterType = "error"
126
- reportUnnecessaryCast = "error"
127
- reportPropertyTypeMismatch = "error"
128
- reportInconsistentConstructor = "error"
129
- reportCallInDefaultInitializer = "error"
130
- reportUnnecessaryContains = "error"
131
-
132
- # Suppression hygiene. This is the rule that keeps the "suppressions require
133
- # approval" policy honest in the other direction: an ignore that outlived its
134
- # defect is a lie the next reader believes. It found 10 dead ones the day it
135
- # went in. Note that neither checker can adjudicate a shared `# type: ignore`
136
- # alone — pyrefly honours the same comment — which is why checker-specific
137
- # suppressions are written as `# pyright: ignore[rule]` and
138
- # `# pyrefly: ignore[kind]`, and why pyrefly's `unused-type-ignore` is off.
139
- reportUnnecessaryTypeIgnoreComment = "error"
140
-
141
- # ── Test directories: two rules subtracted, with reasons (#347) ────────────
142
- #
143
- # `tests/` was outside both checkers, which is what let 59 dead suppressions
144
- # accumulate there — a suppression written against a checker that never runs
145
- # can be neither validated nor invalidated. It is all in now: !433 brought
146
- # `tests/support`, !469 `tests/integration` / `tests/docs` / `tests/bench`,
147
- # and this pass `tests/unit`, which was the last holdout at 1,050 findings.
148
- #
149
- # Two rules are off for these paths, and only these paths. Both are measured
150
- # across the whole of `tests/` and both are *style*, not safety:
151
- #
152
- # * reportMissingParameterType (919) — pytest injects fixtures by name, so
153
- # `def test_x(bundle_conn)` is how every test in the suite is written.
154
- # Annotating them all would be mechanical, would bind each test to its
155
- # fixture's concrete type, and would buy nothing: the fixture's own return
156
- # type is checked where it is defined. Where a fixture's type *does* carry
157
- # through to many call sites it is annotated anyway — `tests/bench` proved
158
- # the cost of not doing so, hiding four `str` URNs from `just check` until
159
- # the bundle tier caught them.
160
- # * reportOptionalMemberAccess (227) — `reg.get(urn).id` in a test that added
161
- # `urn` three lines earlier. The Optional is real and the test is the proof
162
- # it is not None; asserting that first in 227 places is noise. It covers
163
- # attribute access only, so `len(x)`, `x in y` and passing the value on
164
- # still need the assert spelled out, and do have it.
165
- #
166
- # `extraPaths = ["."]` appears in all four because `root` re-bases import
167
- # resolution: without it, `from tests.support …` stops resolving.
168
- #
169
- # Everything else is on, which is the point.
170
-
171
- [[tool.basedpyright.executionEnvironments]]
172
- root = "tests/integration"
173
- extraPaths = ["."]
174
- reportMissingParameterType = false
175
- reportOptionalMemberAccess = false
176
-
177
- [[tool.basedpyright.executionEnvironments]]
178
- root = "tests/docs"
179
- extraPaths = ["."]
180
- reportMissingParameterType = false
181
- reportOptionalMemberAccess = false
182
-
183
- [[tool.basedpyright.executionEnvironments]]
184
- root = "tests/unit"
185
- extraPaths = ["."]
186
- reportMissingParameterType = false
187
- reportOptionalMemberAccess = false
188
-
189
- [[tool.basedpyright.executionEnvironments]]
190
- root = "tests/bench"
191
- extraPaths = ["."]
192
- reportMissingParameterType = false
193
- reportOptionalMemberAccess = false
194
-
195
- # Zero findings when enabled; on as regression guards. reportInvalidCast in
196
- # particular backs the "narrow, don't cast" rule — it fires when a cast asserts
197
- # something the checker knows to be false.
198
- reportInvalidCast = "error"
199
- reportDeprecated = "error"
200
- reportUnsafeMultipleInheritance = "error"
201
- reportTypeCommentUsage = "error"
202
- reportSelfClsParameterName = "error"
203
-
204
- # NOT enabled, measured rather than assumed:
205
- # * reportImplicitOverride (26) — after pyrefly's `missing-override-decorator`
206
- # took the 29 real base-class overrides, every one left is a `__repr__` /
207
- # `__eq__` / `__hash__` / `__str__` overriding `object`. Decorating those
208
- # adds ceremony, not safety. One owner per check; pyrefly owns this one.
209
- # * reportPrivateUsage (340) — same population as the SLF001 noqas.
210
- # * reportUnnecessaryIsInstance (14) — half are pyright generic artifacts
211
- # ("Ref[Code] is always an instance of Ref[Unknown]").
212
- # * reportUnnecessaryComparison (7) — all `str`/`None` guards in `model/`
213
- # that validate untyped caller input at runtime. Deliberate.
214
-
215
95
  [dependency-groups]
216
96
  dev = [
217
97
  # Checker versions carry UPPER bounds. uv.lock is gitignored (published
@@ -220,18 +100,13 @@ dev = [
220
100
  # ruff on `select = ["ALL"]` that is not hypothetical: ruff 0.16.0
221
101
  # stabilised 12 preview rules and would have turned CI red with no commit
222
102
  # of ours in between. Bump these deliberately, in their own MR.
223
- #
224
- # basedpyright is pinned *exactly*, because the bound above was not enough:
225
- # 1.39.10 is a patch release built on pyright 1.1.412, which stopped solving
226
- # a generic class passed as a `Callable[[], R]` factory from the declared
227
- # type — `x: Box[int] = make(Box)` is an error there and fine on 1.1.411.
228
- # Every `field(factory=ItemList, ...)` in the model is that shape, so a fresh
229
- # environment failed `just check` with 44 errors and no commit of ours in
230
- # between. Still broken on pyright 1.1.414 / basedpyright 1.40.2. The pin
231
- # goes with basedpyright itself in the pyrefly-only migration.
232
- "basedpyright==1.39.9",
233
103
  "catppuccin[pygments]>=2.5.0",
234
104
  "types-lxml>=2025.3.30",
105
+ # Stubs for the two untyped test-only imports, so pyrefly checks tests/ without
106
+ # `untyped-import` findings (#425): jsonschema (tests/support/json_schema.py)
107
+ # and PyYAML (test_query_closure). Dev group only — nothing published needs them.
108
+ "types-jsonschema>=4.21.0",
109
+ "types-PyYAML>=6.0.12",
235
110
  "mkdocs-material>=9.7.6",
236
111
  "mkdocstrings[python]>=1.0.3",
237
112
  "polars>=1.39.3",
@@ -239,6 +114,7 @@ dev = [
239
114
  "jsonschema>=4.21.0",
240
115
  "pytest-benchmark>=4.0.0",
241
116
  "pytest-cov>=7.1.0",
117
+ "pytest-xdist>=3.8.0",
242
118
  "pytest-memray>=1.7.0",
243
119
  "pytest-recording>=0.13.4",
244
120
  "pydoclint>=0.9.1,<0.10",
@@ -26,7 +26,7 @@ class _DimExpr:
26
26
  def __init__(self, id: str) -> None:
27
27
  self._id = id
28
28
 
29
- def __eq__(self, other: object) -> DimFilter: # pyright: ignore[reportIncompatibleMethodOverride] # pyrefly: ignore[bad-override]
29
+ def __eq__(self, other: object) -> DimFilter: # pyrefly: ignore[bad-override]
30
30
  if not isinstance(other, str):
31
31
  msg = f"Expected a code string, got {type(other).__name__!r}"
32
32
  raise TypeError(msg)
@@ -140,7 +140,7 @@ def _flow_urn(flow: "Dataflow | Ref[Dataflow] | SdmxUrn[Dataflow]") -> "SdmxUrn[
140
140
  if isinstance(flow, Ref):
141
141
  return flow.urn
142
142
  if isinstance(flow, Dataflow):
143
- return artefact_urn(flow)
143
+ return flow.urn
144
144
  return flow
145
145
 
146
146
 
@@ -137,7 +137,7 @@ class Session:
137
137
  """
138
138
  hit = self._map.get(str(urn))
139
139
  if hit is not None:
140
- return hit # pyright: ignore[reportReturnType] # pyrefly: ignore[bad-return]
140
+ return hit # pyrefly: ignore[bad-return]
141
141
  return None
142
142
 
143
143
  def get_at_least[T](self, urn: SdmxUrn[T], completeness: Completeness) -> T | None:
@@ -154,7 +154,7 @@ class Session:
154
154
  return None
155
155
  hit = self._map.get(key)
156
156
  if hit is not None:
157
- return hit # pyright: ignore[reportReturnType] # pyrefly: ignore[bad-return]
157
+ return hit # pyrefly: ignore[bad-return]
158
158
  return None
159
159
 
160
160
  def completeness_for(self, urn: SdmxUrn[object]) -> Completeness:
@@ -0,0 +1,28 @@
1
+ """Arrow record batch → polars frame, on the fast path (#427).
2
+
3
+ The data writers convert every batch they encode, so the conversion is on the
4
+ hot path of every bulk download. `pl.from_arrow(batch)` is the zero-copy
5
+ conversion; the `pl.DataFrame(batch)` constructor takes a slower general path —
6
+ 2.4x slower to build the frame on polars 1.39, and 1.33x slower for SDMX-CSV's
7
+ whole per-batch encode. 4f65fd7 switched to the constructor for its return type
8
+ and cost a consumer ~17% on large CSV downloads.
9
+
10
+ `from_arrow` is typed `DataFrame | Series`, and since polars 2.0 really returns
11
+ a `Series` for stream-only inputs. A `cast` would pass that through unchecked,
12
+ so the result is narrowed at runtime instead: a record batch takes the fast
13
+ branch, and anything that ever comes back as a `Series` falls back to the
14
+ constructor rather than being mislabelled.
15
+
16
+ Shared by the SDMX-CSV and SDMX-JSON data writers, hence `sdmxlib.formats`.
17
+ """
18
+
19
+ import polars as pl
20
+ import pyarrow as pa
21
+
22
+
23
+ def frame_of(batch: pa.RecordBatch) -> pl.DataFrame:
24
+ """The batch as a polars frame, zero-copy where polars allows it."""
25
+ df = pl.from_arrow(batch)
26
+ if isinstance(df, pl.DataFrame):
27
+ return df
28
+ return pl.DataFrame(batch)
@@ -23,7 +23,7 @@ column maps in their dict-insertion order, which matches the order in
23
23
  which they were declared on the structure.
24
24
 
25
25
  Performance: rows are serialised via Polars' Rust CSV writer
26
- (``pl.DataFrame(batch).write_csv``) rather than Python's `csv`,
26
+ (``pl.from_arrow(batch).write_csv``) rather than Python's `csv`,
27
27
  which makes the writer fast enough that bulk SDMX-CSV downloads are
28
28
  network-bound rather than writer-bound (#61).
29
29
 
@@ -47,6 +47,7 @@ from typing import TYPE_CHECKING
47
47
 
48
48
  import polars as pl
49
49
 
50
+ from sdmxlib.formats._arrow import frame_of
50
51
  from sdmxlib.formats.sdmx_csv.reader import STRUCTURE_KINDS
51
52
  from sdmxlib.model.dataflow import Dataflow
52
53
  from sdmxlib.model.datastructure import DataStructure
@@ -143,11 +144,9 @@ def write_sdmx_csv(
143
144
  )
144
145
  raise KeyError(msg)
145
146
 
146
- # The constructor rather than `pl.from_arrow`, which is typed
147
- # `DataFrame | Series` and since polars 2.0 really does return a Series
148
- # for stream-only inputs — a `cast` here would have hidden that.
147
+ # `frame_of`, not the `pl.DataFrame` constructor: 1.33x per batch (#427).
149
148
  df = (
150
- pl.DataFrame(batch)
149
+ frame_of(batch)
151
150
  .select(data_cols)
152
151
  .with_columns(
153
152
  pl.lit(structure).alias("STRUCTURE"),
@@ -28,6 +28,7 @@ if TYPE_CHECKING:
28
28
  from sdmxlib.model.datastructure import Component, DataStructure, Dimension
29
29
  from sdmxlib.model.istring import InternationalString
30
30
 
31
+ from sdmxlib.formats._arrow import frame_of
31
32
  from sdmxlib.formats._coerce import to_int
32
33
  from sdmxlib.model.errors import SdmxParseError
33
34
  from sdmxlib.model.ref import Ref
@@ -640,11 +641,8 @@ def encode_batch(
640
641
  )
641
642
  raise KeyError(msg)
642
643
 
643
- # The constructor, not `pl.from_arrow`: the same frame for a RecordBatch, but
644
- # typed `DataFrame` rather than `DataFrame | Series` — and polars 2.0 made
645
- # `from_arrow` return a Series for stream-only inputs, which a `cast` would
646
- # have passed through unchecked.
647
- df = pl.DataFrame(batch)
644
+ # `frame_of`, not the `pl.DataFrame` constructor: 2.4x on the conversion (#427).
645
+ df = frame_of(batch)
648
646
  n = df.height
649
647
  if n == 0:
650
648
  return b""
@@ -12,10 +12,30 @@ from __future__ import annotations
12
12
 
13
13
  from sdmxlib.formats._index import lookup
14
14
  from sdmxlib.model.ref import Ref
15
- from sdmxlib.model.urn import SdmxUrn
15
+ from sdmxlib.model.urn import ParsesUrn, SdmxUrn
16
16
 
17
17
 
18
- def resolve_urn[T](urn_val: object, index: dict[str, object], cls: type[T]) -> Ref[T] | None:
18
+ def resolve_any_urn(urn_val: object, index: dict[str, object]) -> Ref[object] | None:
19
+ """`resolve_urn` for a reference that may name any artefact.
20
+
21
+ A categorisation's source and target, or a constraint's attachment, can be
22
+ any maintainable, so there is no class to parse the URN as or to guard the
23
+ resolved object with. Split from `resolve_urn` when `SdmxUrn.parse` became
24
+ `SdmxUrn[object]` (#399): the typed path parses on the class
25
+ (`cls.parse_urn`), which `object` has not got, and saying "any" by name
26
+ here is clearer than an `object` witness there.
27
+ """
28
+ if not isinstance(urn_val, str) or not urn_val.startswith("urn:"):
29
+ return None
30
+ try:
31
+ urn = SdmxUrn.parse(urn_val)
32
+ except (ValueError, TypeError):
33
+ return None
34
+ obj = lookup(index, urn)
35
+ return Ref.of(urn, obj) if obj is not None else Ref.unresolved(urn)
36
+
37
+
38
+ def resolve_urn[T: ParsesUrn](urn_val: object, index: dict[str, object], cls: type[T]) -> Ref[T] | None:
19
39
  """Parse a URN string and resolve it against the artefact index.
20
40
 
21
41
  *cls* is both a type witness and a runtime guard: the indexed object is
@@ -27,7 +47,7 @@ def resolve_urn[T](urn_val: object, index: dict[str, object], cls: type[T]) -> R
27
47
  if not isinstance(urn_val, str) or not urn_val.startswith("urn:"):
28
48
  return None
29
49
  try:
30
- urn = SdmxUrn.parse(urn_val)
50
+ urn = cls.parse_urn(urn_val)
31
51
  except (ValueError, TypeError):
32
52
  return None
33
53
  obj = lookup(index, urn)
@@ -19,7 +19,7 @@ from datetime import date, datetime
19
19
 
20
20
  from sdmxlib.formats._coerce import to_date, to_datetime, to_int
21
21
  from sdmxlib.formats.sdmx_json._dto import ingest_annotations, ingest_istr
22
- from sdmxlib.formats.sdmx_json._resolve import resolve_urn
22
+ from sdmxlib.formats.sdmx_json._resolve import resolve_any_urn, resolve_urn
23
23
  from sdmxlib.formats.sdmx_json._structure_dto import (
24
24
  AgencySchemeJson,
25
25
  AttributeJson,
@@ -552,8 +552,8 @@ def from_categorisation(cat: CategorisationJson, maintainer: Agency, index: dict
552
552
  description=ingest_istr(cat.description, cat.descriptions),
553
553
  annotations=ingest_annotations(cat.annotations),
554
554
  uri=read_uri(cat.links),
555
- source=resolve_urn(cat.source, index, object),
556
- target=resolve_urn(cat.target, index, object),
555
+ source=resolve_any_urn(cat.source, index),
556
+ target=resolve_any_urn(cat.target, index),
557
557
  valid_from=parse_dt(cat.valid_from),
558
558
  valid_to=parse_dt(cat.valid_to),
559
559
  )
@@ -571,7 +571,7 @@ def from_provision_agreement(
571
571
  annotations=ingest_annotations(pa.annotations),
572
572
  uri=read_uri(pa.links),
573
573
  dataflow=resolve_urn(pa.dataflow, index, Dataflow),
574
- data_provider=resolve_urn(pa.data_provider, index, object),
574
+ data_provider=resolve_any_urn(pa.data_provider, index),
575
575
  valid_from=parse_dt(pa.valid_from),
576
576
  valid_to=parse_dt(pa.valid_to),
577
577
  )
@@ -585,13 +585,13 @@ def from_constraint_attachment(ca: ConstraintAttachmentJson | None, index: dict[
585
585
  if ca is None:
586
586
  return ConstraintAttachment()
587
587
  if ca.dataflows:
588
- return ConstraintAttachment(dataflow=resolve_urn(ca.dataflows[0], index, object))
588
+ return ConstraintAttachment(dataflow=resolve_any_urn(ca.dataflows[0], index))
589
589
  if ca.data_structures:
590
- return ConstraintAttachment(data_structure=resolve_urn(ca.data_structures[0], index, object))
590
+ return ConstraintAttachment(data_structure=resolve_any_urn(ca.data_structures[0], index))
591
591
  if ca.provision_agreements:
592
- return ConstraintAttachment(provision_agreement=resolve_urn(ca.provision_agreements[0], index, object))
592
+ return ConstraintAttachment(provision_agreement=resolve_any_urn(ca.provision_agreements[0], index))
593
593
  if ca.data_provider is not None:
594
- return ConstraintAttachment(data_provider=resolve_urn(ca.data_provider, index, object))
594
+ return ConstraintAttachment(data_provider=resolve_any_urn(ca.data_provider, index))
595
595
  return ConstraintAttachment()
596
596
 
597
597
 
@@ -637,7 +637,7 @@ def _hcode_ref(urn_val: str | None) -> Ref[Code] | None:
637
637
  if not isinstance(urn_val, str) or not urn_val.startswith("urn:"):
638
638
  return None
639
639
  try:
640
- return Ref[Code].unresolved(SdmxUrn.parse(urn_val))
640
+ return Ref[Code].unresolved(Code.parse_urn(urn_val))
641
641
  except (ValueError, TypeError) as exc:
642
642
  warnings.warn(
643
643
  f"Skipping unparseable code URN in hierarchical code: {urn_val!r} ({exc})",
@@ -719,8 +719,8 @@ def from_hierarchy_association(
719
719
  valid_from=parse_dt(ha.valid_from),
720
720
  valid_to=parse_dt(ha.valid_to),
721
721
  linked_hierarchy=resolve_urn(ha.linked_hierarchy, index, Hierarchy),
722
- linked_object=resolve_urn(ha.linked_object, index, object),
723
- context_object=resolve_urn(ha.context_object, index, object),
722
+ linked_object=resolve_any_urn(ha.linked_object, index),
723
+ context_object=resolve_any_urn(ha.context_object, index),
724
724
  )
725
725
 
726
726
 
@@ -888,8 +888,8 @@ def from_concept_scheme_map(
888
888
  version=csm.version,
889
889
  name=ingest_istr(csm.name, csm.names),
890
890
  description=ingest_istr(csm.description, csm.descriptions),
891
- source=resolve_urn(csm.source, index, object),
892
- target=resolve_urn(csm.target, index, object),
891
+ source=resolve_any_urn(csm.source, index),
892
+ target=resolve_any_urn(csm.target, index),
893
893
  maps=tuple(maps),
894
894
  annotations=ingest_annotations(csm.annotations),
895
895
  uri=read_uri(csm.links),
@@ -908,8 +908,8 @@ def from_category_scheme_map(
908
908
  version=catm.version,
909
909
  name=ingest_istr(catm.name, catm.names),
910
910
  description=ingest_istr(catm.description, catm.descriptions),
911
- source=resolve_urn(catm.source, index, object),
912
- target=resolve_urn(catm.target, index, object),
911
+ source=resolve_any_urn(catm.source, index),
912
+ target=resolve_any_urn(catm.target, index),
913
913
  maps=tuple(maps),
914
914
  annotations=ingest_annotations(catm.annotations),
915
915
  uri=read_uri(catm.links),
@@ -926,8 +926,8 @@ def from_structure_map(sm: StructureMapJson, maintainer: Agency, index: dict[str
926
926
  version=sm.version,
927
927
  name=ingest_istr(sm.name, sm.names),
928
928
  description=ingest_istr(sm.description, sm.descriptions),
929
- source=resolve_urn(sm.source, index, object),
930
- target=resolve_urn(sm.target, index, object),
929
+ source=resolve_any_urn(sm.source, index),
930
+ target=resolve_any_urn(sm.target, index),
931
931
  maps=tuple(maps),
932
932
  annotations=ingest_annotations(sm.annotations),
933
933
  uri=read_uri(sm.links),
@@ -15,10 +15,12 @@ from typing import Any, NamedTuple
15
15
 
16
16
  import msgspec
17
17
 
18
+ from sdmxlib.model.base import MaintainableArtefact
18
19
  from sdmxlib.model.constraint import DataConstraint
19
20
  from sdmxlib.model.message import StructureMessage
20
21
  from sdmxlib.model.ref import artefact_urn
21
22
  from sdmxlib.model.urn import SdmxUrn
23
+ from sdmxlib.model.version import check_writable
22
24
 
23
25
  from ._dto import ENCODER, Link, Meta, Sender, WireStruct
24
26
  from ._structure_dto import (
@@ -187,6 +189,8 @@ def write(
187
189
 
188
190
  Raises:
189
191
  ValueError: If ``metadata_links`` is given without ``base_url``.
192
+ VersionFormatError: If an artefact's version is one the structure
193
+ schema's `VersionType` (SDMX-ML 3.0's grammar) does not accept.
190
194
  """
191
195
  link_base: str | None = None
192
196
  if metadata_links:
@@ -195,6 +199,15 @@ def write(
195
199
  raise ValueError(msg_err)
196
200
  link_base = base_url
197
201
 
202
+ # The structure schema's `VersionType` is SDMX-ML 3.0's, so a version 3.0
203
+ # refuses would be written here schema-invalid (#417). Checked over every
204
+ # maintainable before anything is encoded, as the SDMX-ML writers do.
205
+ for artefact in msg.all_artefacts():
206
+ if isinstance(artefact, MaintainableArtefact):
207
+ check_writable(
208
+ artefact.version, "json", artefact=f"{type(artefact).__name__} {artefact.agency_id}:{artefact.id}"
209
+ )
210
+
198
211
  data: dict[str, object] = {}
199
212
 
200
213
  for sec in _SECTIONS:
@@ -51,7 +51,7 @@ from sdmxlib.model.organisation import Agency
51
51
  from sdmxlib.model.provision import ProvisionAgreement
52
52
  from sdmxlib.model.ref import Ref, SupportsUrn
53
53
  from sdmxlib.model.representation import Facet
54
- from sdmxlib.model.urn import SdmxUrn
54
+ from sdmxlib.model.urn import ParsesUrn, SdmxUrn
55
55
  from sdmxlib.model.validation import SdmxConformanceError
56
56
  from sdmxlib.model.version import check_writable
57
57
 
@@ -506,7 +506,7 @@ class ReaderHelpers:
506
506
  el = local_rep_el.find("str:EnumerationFormat", namespaces=self._ns)
507
507
  return self.read_text_format(el) if el is not None else None
508
508
 
509
- def read_representation[T](
509
+ def read_representation[T: ParsesUrn](
510
510
  self,
511
511
  local_rep_el: etree._Element,
512
512
  index: dict[str, object] | None = None,
@@ -542,7 +542,7 @@ class ReaderHelpers:
542
542
  return Ref[T].unresolved(urn)
543
543
  text = (enum_el.text or "").strip()
544
544
  if text.startswith("urn:"):
545
- urn = SdmxUrn.parse(text)
545
+ urn = enum_cls.parse_urn(text)
546
546
  _require_urn_class(urn, enum_cls, tag)
547
547
  return Ref[T].unresolved(urn)
548
548
  tf_el = local_rep_el.find("str:TextFormat", namespaces=self._ns)
@@ -998,7 +998,7 @@ def _read_codelist_extensions(cl_el: etree._Element, ns: dict[str, str]) -> tupl
998
998
  urn_text = cl_ref_el.text.strip()
999
999
  if not urn_text.startswith("urn:"):
1000
1000
  continue
1001
- codelist_ref = Ref[Codelist].unresolved(SdmxUrn.parse(urn_text))
1001
+ codelist_ref = Ref[Codelist].unresolved(Codelist.parse_urn(urn_text))
1002
1002
 
1003
1003
  selection: CodeSelection | None = None
1004
1004
  sel_el = ext_el.find("str:InclusiveCodeSelection", namespaces=ns)
@@ -945,33 +945,18 @@ class LocalRegistry(SessionFactory):
945
945
  For Codelists, returns a ``LazyCodelist`` by default (codes not
946
946
  materialized). Pass ``eager=True`` to get a real ``Codelist``.
947
947
 
948
- **A `SdmxUrn[Any]` reads as `LazyCodelist | None` under basedpyright,
949
- whatever the URN names — and that is the checker, not this ordering
950
- (#399).** Typing-spec step 5 makes the call *ambiguous*: more than one
951
- overload still matches and their return types are not equivalent, so a
952
- checker must infer a gradual type assignable to each. pyrefly answers
953
- `Any`. basedpyright applies the rule for an argument that *is* `Any`
954
- and not for one nested inside a generic, which is what `SdmxUrn[Any]`
955
- is — the case the spec illustrates with its own `list[Any]` example.
956
-
957
- Two things not to do about it. **Reordering does not help:** put the
958
- generic overload first and `get(Codelist.urn_for(...))` goes back to
959
- claiming `Codelist` for a `LazyCodelist`, the silent lie #397 removed,
960
- and the explicit spelling is the common one. **Narrowing
961
- `SdmxUrn.parse` to `SdmxUrn[object]`** — #399's option 2 — measures at
962
- 52 basedpyright + 27 pyrefly findings, every one the same fact: a
963
- `Ref[object]` does not go where a `Ref[Codelist]` is wanted. `Ref` is
964
- *covariant* (see `Ref`'s own class comment and #289), so the assignment
965
- that works is the other direction; narrowing `parse` asks for this one,
966
- which covariance does not give. They are sites of correct code, not
967
- defects.
968
-
969
- Pass a typed URN where you have one; narrow with `isinstance` where you
970
- do not. `tests/typing/probe_urn_any.py` pins the declared types and
971
- `tests/unit/test_overload_any_ambiguity.py` the disagreement, so this
972
- caveat comes due for deletion on its own when basedpyright conforms.
973
-
974
- **`expect=` is the way out that does not wait on any of that (#406).**
948
+ **An untyped URN answers `object | None`.** `SdmxUrn.parse(text)` and
949
+ `artefact_urn(x)` are `SdmxUrn[object]` (#399): a URN string can name
950
+ anything, so this cannot say which class comes back, and the caller
951
+ narrows. A typed URN — `Codelist.urn_for(...)`, `Codelist.parse_urn(text)`,
952
+ `artefact.urn` — picks the matching overload and its class.
953
+
954
+ Until #399 those were `SdmxUrn[Any]`, which made the call ambiguous
955
+ across these overloads; basedpyright then answered a confident, wrong
956
+ `LazyCodelist | None` and pyrefly an unchecked `Any | None`.
957
+ `tests/typing/probe_urn_any.py` pins what is declared now.
958
+
959
+ **`expect=` names the class when the URN does not carry it (#406).**
975
960
  It names the class, so `T` comes from the argument rather than from the
976
961
  URN's phantom, and the ambiguity above never arises:
977
962
 
@@ -1336,7 +1321,7 @@ class LocalRegistry(SessionFactory):
1336
1321
  `tests/unit/test_query_applicability.py` compares them.
1337
1322
  """
1338
1323
  rows = self._applicability(dataflow, dim_id)
1339
- return [Ref[Hierarchy].unresolved(SdmxUrn.parse(urn)) for urn, _ha, _scoped in rows]
1324
+ return [Ref[Hierarchy].unresolved(Hierarchy.parse_urn(urn)) for urn, _ha, _scoped in rows]
1340
1325
 
1341
1326
  def unique_hierarchy(
1342
1327
  self,
@@ -1378,7 +1363,7 @@ class LocalRegistry(SessionFactory):
1378
1363
  dataflow_urn=str(artefact_urn(dataflow)),
1379
1364
  ha_urns=sorted(association for _h, association, _s in chosen),
1380
1365
  )
1381
- return Ref[Hierarchy].unresolved(SdmxUrn.parse(rows[0][0]))
1366
+ return Ref[Hierarchy].unresolved(Hierarchy.parse_urn(rows[0][0]))
1382
1367
 
1383
1368
  # ── The data plane ────────────────────────────────────────────────────
1384
1369