statcatlib 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.
- statcatlib/__init__.py +187 -0
- statcatlib/bundle.py +565 -0
- statcatlib/cli/__init__.py +546 -0
- statcatlib/expr.py +167 -0
- statcatlib/model/__init__.py +56 -0
- statcatlib/model/agents.py +32 -0
- statcatlib/model/catalog.py +71 -0
- statcatlib/model/classification.py +19 -0
- statcatlib/model/dataset.py +198 -0
- statcatlib/model/distribution.py +50 -0
- statcatlib/model/licence.py +18 -0
- statcatlib/model/quality.py +31 -0
- statcatlib/model/skos.py +141 -0
- statcatlib/model/spatial.py +19 -0
- statcatlib/model/temporal.py +33 -0
- statcatlib/model/types.py +5 -0
- statcatlib/py.typed +0 -0
- statcatlib/query.py +924 -0
- statcatlib/sources/__init__.py +13 -0
- statcatlib/sources/ckan.py +490 -0
- statcatlib/sources/sdmx.py +321 -0
- statcatlib/store/__init__.py +38 -0
- statcatlib/store/lance.py +1494 -0
- statcatlib/store/row.py +30 -0
- statcatlib/store/schema.py +403 -0
- statcatlib/store/scopes.py +1419 -0
- statcatlib/store/sentinel.py +99 -0
- statcatlib/store/store.py +6836 -0
- statcatlib/store/xhtml.py +206 -0
- statcatlib/urn.py +108 -0
- statcatlib-0.8.0.dist-info/METADATA +429 -0
- statcatlib-0.8.0.dist-info/RECORD +35 -0
- statcatlib-0.8.0.dist-info/WHEEL +4 -0
- statcatlib-0.8.0.dist-info/entry_points.txt +3 -0
- statcatlib-0.8.0.dist-info/licenses/LICENSE +180 -0
statcatlib/expr.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Sub-traversal expression language for scope-based graph queries.
|
|
2
|
+
|
|
3
|
+
Expressions describe computations over related graph data without executing
|
|
4
|
+
any SQL. They are context-free objects — like ``pl.col()`` in Polars — and
|
|
5
|
+
can be reused across ``.enrich()``, ``.filter()``, and ``.sort_by()`` on any
|
|
6
|
+
collection scope.
|
|
7
|
+
|
|
8
|
+
Usage::
|
|
9
|
+
|
|
10
|
+
import statcatlib as sc
|
|
11
|
+
|
|
12
|
+
n_datasets = sc.each("datasets").count()
|
|
13
|
+
has_data = sc.each("datasets").exists()
|
|
14
|
+
|
|
15
|
+
store.themes["subjects"].enrich(n=n_datasets).filter(has_data).collect()
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from attrs import define
|
|
21
|
+
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
# Base expression types
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@define(frozen=True)
|
|
28
|
+
class Expr:
|
|
29
|
+
"""Base class for all expression nodes."""
|
|
30
|
+
|
|
31
|
+
def alias(self, name: str) -> AliasedExpr:
|
|
32
|
+
"""Name this expression result (like Polars ``.alias()``)."""
|
|
33
|
+
return AliasedExpr(expr=self, name=name)
|
|
34
|
+
|
|
35
|
+
def count(self) -> CountExpr:
|
|
36
|
+
"""Count items at the far end of the traversal."""
|
|
37
|
+
return CountExpr(source=self)
|
|
38
|
+
|
|
39
|
+
def exists(self) -> ExistsExpr:
|
|
40
|
+
"""Check if any items exist at the far end."""
|
|
41
|
+
return ExistsExpr(source=self)
|
|
42
|
+
|
|
43
|
+
def sum(self, field: str) -> SumExpr:
|
|
44
|
+
"""Sum a numeric field at the far end."""
|
|
45
|
+
return SumExpr(source=self, field=field)
|
|
46
|
+
|
|
47
|
+
def ids(self) -> IdsExpr:
|
|
48
|
+
"""Collect identifiers at the far end."""
|
|
49
|
+
return IdsExpr(source=self)
|
|
50
|
+
|
|
51
|
+
def distinct(self, field: str) -> DistinctExpr:
|
|
52
|
+
"""Select distinct values of a field at the far end."""
|
|
53
|
+
return DistinctExpr(source=self, field=field)
|
|
54
|
+
|
|
55
|
+
def filter(self, **kwargs: object) -> FilteredExpr:
|
|
56
|
+
"""Filter far-side nodes by attribute values."""
|
|
57
|
+
return FilteredExpr(source=self, conditions=kwargs)
|
|
58
|
+
|
|
59
|
+
def count_distinct(self) -> CountDistinctExpr:
|
|
60
|
+
"""Count distinct items at the far end."""
|
|
61
|
+
return CountDistinctExpr(source=self)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@define(frozen=True)
|
|
65
|
+
class TraversalExpr(Expr):
|
|
66
|
+
"""A traversal along one or more edges from the current scope position.
|
|
67
|
+
|
|
68
|
+
Created by ``sc.each("edge1", "edge2", ...)``. Strings are always
|
|
69
|
+
edge labels (table names or relationship names in the graph).
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
edges: tuple[str, ...]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@define(frozen=True)
|
|
76
|
+
class AliasedExpr:
|
|
77
|
+
"""An expression with a named output column.
|
|
78
|
+
|
|
79
|
+
Created by ``expr.alias("name")`` or via ``**kwargs`` in ``.enrich()``.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
expr: Expr
|
|
83
|
+
name: str
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
# Terminal reduction expressions
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@define(frozen=True)
|
|
92
|
+
class CountExpr(Expr):
|
|
93
|
+
"""COUNT(*) at the far end of a traversal."""
|
|
94
|
+
|
|
95
|
+
source: Expr
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@define(frozen=True)
|
|
99
|
+
class ExistsExpr(Expr):
|
|
100
|
+
"""EXISTS(...) — boolean, true if any far-side items exist."""
|
|
101
|
+
|
|
102
|
+
source: Expr
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@define(frozen=True)
|
|
106
|
+
class SumExpr(Expr):
|
|
107
|
+
"""SUM(field) at the far end of a traversal."""
|
|
108
|
+
|
|
109
|
+
source: Expr
|
|
110
|
+
field: str
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@define(frozen=True)
|
|
114
|
+
class IdsExpr(Expr):
|
|
115
|
+
"""LIST(id) — collect identifiers at the far end."""
|
|
116
|
+
|
|
117
|
+
source: Expr
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@define(frozen=True)
|
|
121
|
+
class DistinctExpr(Expr):
|
|
122
|
+
"""SELECT DISTINCT field — narrows to unique values."""
|
|
123
|
+
|
|
124
|
+
source: Expr
|
|
125
|
+
field: str
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@define(frozen=True)
|
|
129
|
+
class CountDistinctExpr(Expr):
|
|
130
|
+
"""COUNT(DISTINCT ...) at the far end."""
|
|
131
|
+
|
|
132
|
+
source: Expr
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@define(frozen=True)
|
|
136
|
+
class FilteredExpr(Expr):
|
|
137
|
+
"""A traversal with far-side filtering applied.
|
|
138
|
+
|
|
139
|
+
Created by ``sc.each("datasets").filter(status="current")``.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
source: Expr
|
|
143
|
+
conditions: dict[str, object]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# ---------------------------------------------------------------------------
|
|
147
|
+
# Public entry point
|
|
148
|
+
# ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def each(*edges: str) -> TraversalExpr:
|
|
152
|
+
"""Create a sub-traversal expression starting from the current scope.
|
|
153
|
+
|
|
154
|
+
Each argument is an edge label (relationship name). Multiple arguments
|
|
155
|
+
describe a multi-hop path::
|
|
156
|
+
|
|
157
|
+
sc.each("datasets") # one hop
|
|
158
|
+
sc.each("datasets", "dimensions") # two hops
|
|
159
|
+
sc.each("datasets", "dimensions", "codelist") # three hops
|
|
160
|
+
|
|
161
|
+
The returned expression is context-free and can be used in ``.enrich()``,
|
|
162
|
+
``.filter()``, and ``.sort_by()`` on any collection scope.
|
|
163
|
+
"""
|
|
164
|
+
if not edges:
|
|
165
|
+
msg = "each() requires at least one edge label"
|
|
166
|
+
raise ValueError(msg)
|
|
167
|
+
return TraversalExpr(edges=edges)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Domain model for statcatlib."""
|
|
2
|
+
|
|
3
|
+
from statcatlib.model.agents import Agent, ContactPoint
|
|
4
|
+
from statcatlib.model.catalog import CatalogDescription, CatalogRecord
|
|
5
|
+
from statcatlib.model.classification import Standard
|
|
6
|
+
from statcatlib.model.dataset import (
|
|
7
|
+
AggregateDataset,
|
|
8
|
+
BaseDataset,
|
|
9
|
+
Dataset,
|
|
10
|
+
DatasetT,
|
|
11
|
+
GeospatialDataset,
|
|
12
|
+
MicrodataDataset,
|
|
13
|
+
OpenDataset,
|
|
14
|
+
PublicationDataset,
|
|
15
|
+
StatisticalDataset,
|
|
16
|
+
Variable,
|
|
17
|
+
)
|
|
18
|
+
from statcatlib.model.distribution import DataService, Distribution
|
|
19
|
+
from statcatlib.model.licence import LicenceDocument
|
|
20
|
+
from statcatlib.model.quality import ProvenanceStatement, QualityAnnotation
|
|
21
|
+
from statcatlib.model.spatial import SpatialCoverage
|
|
22
|
+
from statcatlib.model.temporal import Frequency, TemporalCoverage
|
|
23
|
+
from statcatlib.model.skos import Concept, ConceptRef, ConceptScheme, ConceptSchemeRef, is_concept_in_scheme
|
|
24
|
+
from statcatlib.model.types import InternationalString
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"Agent",
|
|
28
|
+
"AggregateDataset",
|
|
29
|
+
"BaseDataset",
|
|
30
|
+
"CatalogDescription",
|
|
31
|
+
"CatalogRecord",
|
|
32
|
+
"Concept",
|
|
33
|
+
"ConceptRef",
|
|
34
|
+
"ConceptScheme",
|
|
35
|
+
"ConceptSchemeRef",
|
|
36
|
+
"ContactPoint",
|
|
37
|
+
"DataService",
|
|
38
|
+
"Dataset",
|
|
39
|
+
"DatasetT",
|
|
40
|
+
"Distribution",
|
|
41
|
+
"Frequency",
|
|
42
|
+
"GeospatialDataset",
|
|
43
|
+
"InternationalString",
|
|
44
|
+
"LicenceDocument",
|
|
45
|
+
"MicrodataDataset",
|
|
46
|
+
"OpenDataset",
|
|
47
|
+
"ProvenanceStatement",
|
|
48
|
+
"PublicationDataset",
|
|
49
|
+
"QualityAnnotation",
|
|
50
|
+
"SpatialCoverage",
|
|
51
|
+
"Standard",
|
|
52
|
+
"StatisticalDataset",
|
|
53
|
+
"TemporalCoverage",
|
|
54
|
+
"Variable",
|
|
55
|
+
"is_concept_in_scheme",
|
|
56
|
+
]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Agent and contact types for the statcatlib domain model."""
|
|
2
|
+
|
|
3
|
+
from attrs import define
|
|
4
|
+
from sdmxlib import InternationalString
|
|
5
|
+
|
|
6
|
+
__all__ = ["Agent", "ContactPoint"]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@define
|
|
10
|
+
class Agent:
|
|
11
|
+
"""An organization or person responsible for a dataset.
|
|
12
|
+
|
|
13
|
+
Maps to foaf:Agent / dct:publisher / dct:creator in DCAT-AP.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
name: InternationalString
|
|
17
|
+
id: str | None = None
|
|
18
|
+
type: str | None = None
|
|
19
|
+
homepage: str | None = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@define
|
|
23
|
+
class ContactPoint:
|
|
24
|
+
"""Contact information for a dataset.
|
|
25
|
+
|
|
26
|
+
Maps to dcat:contactPoint → vcard:Kind in DCAT-AP.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
name: InternationalString | None = None
|
|
30
|
+
email: str | None = None
|
|
31
|
+
phone: str | None = None
|
|
32
|
+
url: str | None = None
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""CatalogDescription and CatalogRecord types for the statcatlib domain model.
|
|
2
|
+
|
|
3
|
+
DCAT-AP describes a catalog as a top-level container of datasets. In the
|
|
4
|
+
statcatlib runtime, a *catalog* is the DuckDB-backed `statcatlib.Catalog`
|
|
5
|
+
store. The DCAT-style metadata record that describes such a catalog (its
|
|
6
|
+
title, publisher, list of datasets…) is `CatalogDescription` here.
|
|
7
|
+
This split avoids the name collision between "DCAT Catalog the model
|
|
8
|
+
record" and "Catalog the store".
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
|
|
13
|
+
from attrs import Factory, define
|
|
14
|
+
from sdmxlib import InternationalString
|
|
15
|
+
|
|
16
|
+
from statcatlib.model.agents import Agent
|
|
17
|
+
from statcatlib.model.classification import Standard
|
|
18
|
+
from statcatlib.model.dataset import BaseDataset
|
|
19
|
+
from statcatlib.model.distribution import DataService
|
|
20
|
+
from statcatlib.model.licence import LicenceDocument
|
|
21
|
+
from statcatlib.model.skos import ConceptSchemeRef
|
|
22
|
+
from statcatlib.model.spatial import SpatialCoverage
|
|
23
|
+
from statcatlib.model.temporal import TemporalCoverage
|
|
24
|
+
|
|
25
|
+
__all__ = ["CatalogDescription", "CatalogRecord"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@define
|
|
29
|
+
class CatalogRecord:
|
|
30
|
+
"""Metadata about a dataset's entry in the catalog.
|
|
31
|
+
|
|
32
|
+
Maps to dcat:CatalogRecord in DCAT-AP. This is metadata about
|
|
33
|
+
the catalog entry itself — when it was listed, where it was
|
|
34
|
+
harvested from, etc. Optional — not all catalogs use this.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
identifier: str
|
|
38
|
+
primary_topic: BaseDataset
|
|
39
|
+
listing_date: datetime | None = None
|
|
40
|
+
modified: datetime | None = None
|
|
41
|
+
source_catalog: str | None = None
|
|
42
|
+
conforms_to: Standard | None = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@define
|
|
46
|
+
class CatalogDescription:
|
|
47
|
+
"""A DCAT-style description record for a catalog of datasets.
|
|
48
|
+
|
|
49
|
+
Maps to dcat:Catalog in DCAT-AP. This is metadata describing the
|
|
50
|
+
catalog as a published artefact — title, publisher, list of datasets
|
|
51
|
+
and services, licence, etc. The runtime store with the same
|
|
52
|
+
DCAT-shaped role is `statcatlib.Catalog`; the two are separate
|
|
53
|
+
concerns. Use this when serialising to / from DCAT-AP RDF or when
|
|
54
|
+
a catalog needs a self-describing manifest record.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
identifier: str
|
|
58
|
+
title: InternationalString
|
|
59
|
+
description: InternationalString
|
|
60
|
+
publisher: Agent
|
|
61
|
+
datasets: list[BaseDataset] = Factory(list)
|
|
62
|
+
services: list[DataService] = Factory(list)
|
|
63
|
+
records: list[CatalogRecord] = Factory(list)
|
|
64
|
+
homepage: str | None = None
|
|
65
|
+
language: list[str] = Factory(list)
|
|
66
|
+
licence: LicenceDocument | None = None
|
|
67
|
+
spatial_coverage: list[SpatialCoverage] = Factory(list)
|
|
68
|
+
temporal_coverage: TemporalCoverage | None = None
|
|
69
|
+
theme_taxonomy: list[ConceptSchemeRef] = Factory(list)
|
|
70
|
+
issued: datetime | None = None
|
|
71
|
+
modified: datetime | None = None
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Classification types for the statcatlib domain model."""
|
|
2
|
+
|
|
3
|
+
from attrs import define
|
|
4
|
+
from sdmxlib import InternationalString
|
|
5
|
+
|
|
6
|
+
__all__ = ["Standard"]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@define
|
|
10
|
+
class Standard:
|
|
11
|
+
"""A standard or specification that the dataset conforms to.
|
|
12
|
+
|
|
13
|
+
Maps to dct:conformsTo in DCAT-AP. Used to link to SDMX DSDs,
|
|
14
|
+
CSV schemas, API specifications, etc.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
id: str
|
|
18
|
+
label: InternationalString | None = None
|
|
19
|
+
uri: str | None = None
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Core dataset types for the statcatlib domain model."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import ClassVar, Literal, TypeVar
|
|
7
|
+
|
|
8
|
+
from attrs import Factory, define
|
|
9
|
+
from sdmxlib import InternationalString
|
|
10
|
+
|
|
11
|
+
from statcatlib.model.agents import Agent, ContactPoint
|
|
12
|
+
from statcatlib.model.classification import Standard
|
|
13
|
+
from statcatlib.model.distribution import Distribution
|
|
14
|
+
from statcatlib.model.licence import LicenceDocument
|
|
15
|
+
from statcatlib.model.quality import ProvenanceStatement, QualityAnnotation
|
|
16
|
+
from statcatlib.model.skos import ConceptRef
|
|
17
|
+
from statcatlib.model.spatial import SpatialCoverage
|
|
18
|
+
from statcatlib.model.temporal import Frequency, TemporalCoverage
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"DATASET_KINDS",
|
|
22
|
+
"AggregateDataset",
|
|
23
|
+
"BaseDataset",
|
|
24
|
+
"Dataset",
|
|
25
|
+
"DatasetKind",
|
|
26
|
+
"DatasetT",
|
|
27
|
+
"GeospatialDataset",
|
|
28
|
+
"MicrodataDataset",
|
|
29
|
+
"OpenDataset",
|
|
30
|
+
"PublicationDataset",
|
|
31
|
+
"StatisticalDataset",
|
|
32
|
+
"Variable",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
#: The domain of the ``dataset.kind`` column. `BaseDataset` deliberately has
|
|
37
|
+
#: no ``kind``: it is never instantiated, so it carries no tag — only the five
|
|
38
|
+
#: concrete variants do, each annotated with its own single-member `Literal`.
|
|
39
|
+
#: Annotating a variant with `DatasetKind` itself would widen it back to "any
|
|
40
|
+
#: of the five" and lose the narrowing that makes the tag useful.
|
|
41
|
+
type DatasetKind = Literal["open", "aggregate", "microdata", "geospatial", "publication"]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@define
|
|
45
|
+
class BaseDataset:
|
|
46
|
+
"""Common fields for all dataset variants. Maps to DCAT-AP 3.0.
|
|
47
|
+
|
|
48
|
+
Not intended for direct construction — use one of the five concrete
|
|
49
|
+
variants: OpenDataset, AggregateDataset, MicrodataDataset,
|
|
50
|
+
GeospatialDataset, PublicationDataset.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
# --- Identity (always required) ---
|
|
54
|
+
identifier: str
|
|
55
|
+
title: InternationalString
|
|
56
|
+
description: InternationalString
|
|
57
|
+
|
|
58
|
+
# --- Core catalog metadata ---
|
|
59
|
+
publisher: Agent | None = None
|
|
60
|
+
contact_point: ContactPoint | None = None
|
|
61
|
+
keywords: list[InternationalString] = Factory(list)
|
|
62
|
+
themes: list[ConceptRef] = Factory(list) # dcat:theme — URI refs, not owned
|
|
63
|
+
subject: list[ConceptRef] = Factory(list) # dcterms:subject — URI refs
|
|
64
|
+
dataset_type: ConceptRef | None = None # dcterms:type — URI ref
|
|
65
|
+
issued: datetime | None = None
|
|
66
|
+
modified: datetime | None = None
|
|
67
|
+
frequency: Frequency | None = None
|
|
68
|
+
spatial_coverage: list[SpatialCoverage] = Factory(list)
|
|
69
|
+
temporal_coverage: TemporalCoverage | None = None
|
|
70
|
+
distributions: list[Distribution] = Factory(list)
|
|
71
|
+
licence: LicenceDocument | None = None
|
|
72
|
+
language: list[str] = Factory(list)
|
|
73
|
+
|
|
74
|
+
# --- Extended metadata ---
|
|
75
|
+
landing_page: str | None = None
|
|
76
|
+
version: str | None = None
|
|
77
|
+
version_notes: InternationalString | None = None
|
|
78
|
+
conforms_to: list[Standard] = Factory(list)
|
|
79
|
+
creator: Agent | None = None
|
|
80
|
+
access_rights: str | None = None
|
|
81
|
+
status: str | None = None
|
|
82
|
+
provenance: list[ProvenanceStatement] = Factory(list)
|
|
83
|
+
quality_annotations: list[QualityAnnotation] = Factory(list)
|
|
84
|
+
|
|
85
|
+
# --- Extensibility ---
|
|
86
|
+
#: Free-form key/value pairs, both sides strings.
|
|
87
|
+
#:
|
|
88
|
+
#: Stored in a ``MAP(VARCHAR, VARCHAR)`` column, and declared to match
|
|
89
|
+
#: rather than to flatter: as ``dict[str, object]`` it accepted values
|
|
90
|
+
#: it could not return, so `None` read back as the *string* ``"None"``
|
|
91
|
+
#: — present and truthy where an absence was stored (#114). This is
|
|
92
|
+
#: also the shape the vocabulary uses; CKAN's package extras are
|
|
93
|
+
#: string-valued key/value pairs.
|
|
94
|
+
#:
|
|
95
|
+
#: Structured values are the caller's to encode — ``json.dumps`` on
|
|
96
|
+
#: the way in, ``json.loads`` on the way out — so that the lossy step
|
|
97
|
+
#: is one they chose and can reverse.
|
|
98
|
+
extras: dict[str, str] = Factory(dict)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@define
|
|
102
|
+
class OpenDataset(BaseDataset):
|
|
103
|
+
"""Generic open dataset — the default bucket for non-specialized data."""
|
|
104
|
+
|
|
105
|
+
kind: ClassVar[Literal["open"]] = "open"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@define
|
|
109
|
+
class AggregateDataset(BaseDataset):
|
|
110
|
+
"""A statistical aggregate dataset (tabular, with dimensions and measures).
|
|
111
|
+
|
|
112
|
+
Extends BaseDataset with StatDCAT-AP discovery fields and an optional link
|
|
113
|
+
to the full SDMX structural record in sdmxlib's shared DuckDB tables.
|
|
114
|
+
dimension_names carries enough info for catalog search without a JOIN.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
kind: ClassVar[Literal["aggregate"]] = "aggregate"
|
|
118
|
+
dimension_names: list[InternationalString] = Factory(list)
|
|
119
|
+
num_series: int | None = None
|
|
120
|
+
# SDMX link: FK into sdmxlib's dataflows.urn in the shared DuckDB
|
|
121
|
+
sdmx_dataflow_urn: str | None = None
|
|
122
|
+
# Pivot axis dimension for ingest_data(). None = auto-detect from DSD.
|
|
123
|
+
measure_dim: str | None = None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@define
|
|
127
|
+
class Variable:
|
|
128
|
+
"""A variable in a microdata dataset."""
|
|
129
|
+
|
|
130
|
+
id: str
|
|
131
|
+
label: InternationalString
|
|
132
|
+
description: InternationalString | None = None
|
|
133
|
+
value_type: str | None = None
|
|
134
|
+
classification_id: str | None = None
|
|
135
|
+
universe: str | None = None
|
|
136
|
+
question_text: str | None = None
|
|
137
|
+
valid_range: str | None = None
|
|
138
|
+
derivation: str | None = None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@define
|
|
142
|
+
class MicrodataDataset(BaseDataset):
|
|
143
|
+
"""A microdata (survey/record-level) dataset."""
|
|
144
|
+
|
|
145
|
+
kind: ClassVar[Literal["microdata"]] = "microdata"
|
|
146
|
+
variables: list[Variable] = Factory(list)
|
|
147
|
+
sample_size: int | None = None
|
|
148
|
+
universe: str | None = None
|
|
149
|
+
collection_method: str | None = None
|
|
150
|
+
anonymization: str | None = None
|
|
151
|
+
survey_id: str | None = None
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@define
|
|
155
|
+
class GeospatialDataset(BaseDataset):
|
|
156
|
+
"""A geospatial dataset."""
|
|
157
|
+
|
|
158
|
+
kind: ClassVar[Literal["geospatial"]] = "geospatial"
|
|
159
|
+
crs: str | None = None
|
|
160
|
+
spatial_resolution: str | None = None
|
|
161
|
+
feature_types: list[str] = Factory(list)
|
|
162
|
+
scale: str | None = None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@define
|
|
166
|
+
class PublicationDataset(BaseDataset):
|
|
167
|
+
"""A publication (article, report, monograph)."""
|
|
168
|
+
|
|
169
|
+
kind: ClassVar[Literal["publication"]] = "publication"
|
|
170
|
+
authors: list[str] = Factory(list)
|
|
171
|
+
doi: str | None = None
|
|
172
|
+
isbn: str | None = None
|
|
173
|
+
publication_type: str | None = None
|
|
174
|
+
pages: int | None = None
|
|
175
|
+
series_title: InternationalString | None = None
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
#: Every tag, in declaration order. Built from the classes rather than from
|
|
179
|
+
#: `DatasetKind`, so each variant's `Literal` is checked to be a member of the
|
|
180
|
+
#: alias — a typo'd tag is a type error here, not a silent mismatch with the
|
|
181
|
+
#: ``dataset.kind`` column.
|
|
182
|
+
DATASET_KINDS: tuple[DatasetKind, ...] = (
|
|
183
|
+
OpenDataset.kind,
|
|
184
|
+
AggregateDataset.kind,
|
|
185
|
+
MicrodataDataset.kind,
|
|
186
|
+
GeospatialDataset.kind,
|
|
187
|
+
PublicationDataset.kind,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Public union type alias — the type you work with day-to-day.
|
|
191
|
+
# Use BaseDataset as the generic bound in query() and other generic contexts.
|
|
192
|
+
type Dataset = OpenDataset | AggregateDataset | MicrodataDataset | GeospatialDataset | PublicationDataset
|
|
193
|
+
|
|
194
|
+
# TypeVar for pre-PEP-695 generic code
|
|
195
|
+
DatasetT = TypeVar("DatasetT", bound=BaseDataset)
|
|
196
|
+
|
|
197
|
+
# Backward compatibility alias — AggregateDataset is the successor
|
|
198
|
+
StatisticalDataset = AggregateDataset
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Distribution and DataService types for the statcatlib domain model."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from attrs import Factory, define
|
|
6
|
+
from sdmxlib import InternationalString
|
|
7
|
+
|
|
8
|
+
from statcatlib.model.classification import Standard
|
|
9
|
+
from statcatlib.model.licence import LicenceDocument
|
|
10
|
+
|
|
11
|
+
__all__ = ["DataService", "Distribution"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@define
|
|
15
|
+
class DataService:
|
|
16
|
+
"""An API endpoint that serves one or more datasets.
|
|
17
|
+
|
|
18
|
+
Maps to dcat:DataService in DCAT-AP. For SDMX sources, the
|
|
19
|
+
endpoint_url is the SDMX REST base URL.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
id: str
|
|
23
|
+
title: InternationalString
|
|
24
|
+
endpoint_url: str
|
|
25
|
+
description: InternationalString | None = None
|
|
26
|
+
serves_datasets: list[str] = Factory(list)
|
|
27
|
+
conforms_to: list[Standard] = Factory(list)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@define
|
|
31
|
+
class Distribution:
|
|
32
|
+
"""A specific available form of a dataset.
|
|
33
|
+
|
|
34
|
+
Maps to dcat:Distribution in DCAT-AP. A dataset may have
|
|
35
|
+
multiple distributions: CSV download, API endpoint, visualization, etc.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
id: str
|
|
39
|
+
title: InternationalString | None = None
|
|
40
|
+
description: InternationalString | None = None
|
|
41
|
+
access_url: str | None = None
|
|
42
|
+
download_url: str | None = None
|
|
43
|
+
media_type: str | None = None
|
|
44
|
+
format: str | None = None
|
|
45
|
+
byte_size: int | None = None
|
|
46
|
+
issued: datetime | None = None
|
|
47
|
+
modified: datetime | None = None
|
|
48
|
+
licence: LicenceDocument | None = None
|
|
49
|
+
access_service: DataService | None = None
|
|
50
|
+
distribution_type: str | None = None
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Licence types for the statcatlib domain model."""
|
|
2
|
+
|
|
3
|
+
from attrs import define
|
|
4
|
+
from sdmxlib import InternationalString
|
|
5
|
+
|
|
6
|
+
__all__ = ["LicenceDocument"]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@define
|
|
10
|
+
class LicenceDocument:
|
|
11
|
+
"""A licence under which the dataset is made available.
|
|
12
|
+
|
|
13
|
+
Maps to dct:license → dct:LicenseDocument in DCAT-AP.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
id: str
|
|
17
|
+
label: InternationalString | None = None
|
|
18
|
+
uri: str | None = None
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Quality and provenance types for the statcatlib domain model."""
|
|
2
|
+
|
|
3
|
+
from attrs import define
|
|
4
|
+
from sdmxlib import InternationalString
|
|
5
|
+
|
|
6
|
+
__all__ = ["ProvenanceStatement", "QualityAnnotation"]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@define
|
|
10
|
+
class QualityAnnotation:
|
|
11
|
+
"""A quality statement about the dataset.
|
|
12
|
+
|
|
13
|
+
Maps to dqv:hasQualityAnnotation in StatDCAT-AP. Intentionally
|
|
14
|
+
simple — a label + optional body. Richer quality modelling
|
|
15
|
+
can be added later.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
dimension: str | None = None
|
|
19
|
+
label: InternationalString | None = None
|
|
20
|
+
body: InternationalString | None = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@define
|
|
24
|
+
class ProvenanceStatement:
|
|
25
|
+
"""A statement about the origin/methodology of the dataset.
|
|
26
|
+
|
|
27
|
+
Maps to dct:provenance in DCAT-AP.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
label: InternationalString
|
|
31
|
+
uri: str | None = None
|