microdata-tools 2.0.1__tar.gz → 2.1.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.
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/.gitignore +4 -10
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/PKG-INFO +4 -2
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/validation/__init__.py +11 -6
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/validation/adapter/local_storage.py +1 -0
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/validation/steps/data_reader.py +81 -34
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/validation/steps/dataset_validator.py +136 -117
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/pyproject.toml +12 -10
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/LICENSE.md +0 -0
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/README.md +0 -0
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/__init__.py +0 -0
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/packaging/__init__.py +0 -0
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/packaging/_crypto_config.py +0 -0
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/packaging/_decrypt.py +0 -0
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/packaging/_encrypt.py +0 -0
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/packaging/_utils.py +0 -0
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/packaging/exceptions/__init__.py +0 -0
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/packaging/keys.py +0 -0
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/validation/components/temporal_attributes.py +0 -0
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/validation/components/unit_id_types.py +0 -0
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/validation/components/unit_type_variables/BEDRIFT.json +0 -0
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/validation/components/unit_type_variables/BK_HELSESTASJONSKONSULTASJON.json +0 -0
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/validation/components/unit_type_variables/BRUKSENHET.json +0 -0
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/validation/components/unit_type_variables/FAMILIE.json +0 -0
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/validation/components/unit_type_variables/FENGSLINGER_TILGANG.json +0 -0
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/validation/components/unit_type_variables/FORETAK.json +0 -0
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/validation/components/unit_type_variables/HKDIR_HYU_OPPMELDING.json +0 -0
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/validation/components/unit_type_variables/HKDIR_STUDIESOKNAD.json +0 -0
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/validation/components/unit_type_variables/HUSHOLDNING.json +0 -0
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/validation/components/unit_type_variables/JOBB.json +0 -0
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/validation/components/unit_type_variables/KJORETOY.json +0 -0
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/validation/components/unit_type_variables/KOMMUNE.json +0 -0
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/validation/components/unit_type_variables/KRG_KREFTTILFELLE.json +0 -0
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/validation/components/unit_type_variables/KURS.json +0 -0
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/validation/components/unit_type_variables/MALEPUNKT.json +0 -0
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/validation/components/unit_type_variables/NPR_EPISODE.json +0 -0
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/validation/components/unit_type_variables/NPR_PROSEDYRE.json +0 -0
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/validation/components/unit_type_variables/NPR_SYKEHUSOPPHOLD.json +0 -0
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/validation/components/unit_type_variables/PERSON.json +0 -0
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/validation/components/unit_type_variables/SOESKEN.json +0 -0
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/validation/components/unit_type_variables/SOSTIL.json +0 -0
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/validation/components/unit_type_variables/TRAFIKKULYKKE.json +0 -0
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/validation/components/unit_type_variables/TRAFIKKULYKKE_PERSON.json +0 -0
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/validation/components/unit_type_variables/VIRKSOMHET.json +0 -0
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/validation/components/unit_type_variables/__init__.py +0 -0
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/validation/exceptions/__init__.py +0 -0
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/validation/model/__init__.py +0 -0
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/validation/model/metadata.py +0 -0
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/validation/steps/metadata_enricher.py +0 -0
- {microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/validation/steps/metadata_reader.py +0 -0
|
@@ -60,17 +60,11 @@ coverage.xml
|
|
|
60
60
|
.pytest_cache/
|
|
61
61
|
cover/
|
|
62
62
|
|
|
63
|
-
tests/resources/validation/validate_dataset/big_datasets
|
|
64
|
-
tests/resources/validation/validate_dataset/big_datasets
|
|
63
|
+
tests/resources/validation/validate_dataset/big_datasets/*/*.csv
|
|
64
|
+
tests/resources/validation/validate_dataset/big_datasets/*/*.json
|
|
65
|
+
tests/resources/validation/validate_dataset/big_datasets/*/*.csv.rowcount
|
|
66
|
+
tests/resources/validation/steps/dataset_validator/parquet/*.parquet
|
|
65
67
|
|
|
66
|
-
tests/resources/validation/validate_dataset/big_datasets/EVENT_DS/EVENT_DS.csv
|
|
67
|
-
tests/resources/validation/validate_dataset/big_datasets/EVENT_DS/EVENT_DS.csv.rowcount
|
|
68
|
-
|
|
69
|
-
tests/resources/validation/validate_dataset/big_datasets/FIXED_DS/FIXED_DS.csv
|
|
70
|
-
tests/resources/validation/validate_dataset/big_datasets/FIXED_DS/FIXED_DS.csv.rowcount
|
|
71
|
-
|
|
72
|
-
tests/resources/validation/validate_dataset/big_datasets/STATUS_DS/STATUS_DS.csv
|
|
73
|
-
tests/resources/validation/validate_dataset/big_datasets/STATUS_DS/STATUS_DS.csv.rowcount
|
|
74
68
|
|
|
75
69
|
# Translations
|
|
76
70
|
*.mo
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: microdata-tools
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: Tools for the microdata.no platform
|
|
5
5
|
Author: microdata-developers
|
|
6
6
|
License: MIT
|
|
7
7
|
License-File: LICENSE.md
|
|
8
8
|
Requires-Python: >=3.10
|
|
9
|
+
Requires-Dist: adbc-driver-manager==1.10.0
|
|
10
|
+
Requires-Dist: adbc-driver-sqlite==1.10.0
|
|
9
11
|
Requires-Dist: cryptography>=50.0.0
|
|
10
12
|
Requires-Dist: numpy>=2.2.0
|
|
11
13
|
Requires-Dist: pyarrow==23.0.1
|
|
@@ -88,12 +88,16 @@ def validate_dataset(
|
|
|
88
88
|
|
|
89
89
|
# Read data
|
|
90
90
|
parquet_path = working_directory_path / f"{dataset_name}.parquet"
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
91
|
+
sqlite_path = working_directory_path / f"{dataset_name}.sqlite3.db"
|
|
92
|
+
filesystem_dataset = (
|
|
93
|
+
data_reader.read_and_sanitize_csv_write_sqlite_and_parquet(
|
|
94
|
+
input_data_path,
|
|
95
|
+
parquet_path,
|
|
96
|
+
sqlite_path,
|
|
97
|
+
identifier_data_type,
|
|
98
|
+
measure_data_type,
|
|
99
|
+
temporality_type,
|
|
100
|
+
)
|
|
97
101
|
)
|
|
98
102
|
|
|
99
103
|
# Enrich metadata with temporal data
|
|
@@ -112,6 +116,7 @@ def validate_dataset(
|
|
|
112
116
|
# Validate data
|
|
113
117
|
dataset_validator.validate_dataset(
|
|
114
118
|
pyarrow.dataset.dataset(parquet_path),
|
|
119
|
+
sqlite_path,
|
|
115
120
|
measure_data_type,
|
|
116
121
|
code_list,
|
|
117
122
|
sentinel_list,
|
{microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/validation/steps/data_reader.py
RENAMED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
# pyright: reportAttributeAccessIssue=false
|
|
2
2
|
import logging
|
|
3
|
+
import os.path
|
|
4
|
+
import sqlite3
|
|
3
5
|
from datetime import datetime, timedelta
|
|
4
6
|
from pathlib import Path
|
|
5
7
|
from typing import Dict, Tuple
|
|
6
8
|
|
|
9
|
+
import adbc_driver_sqlite.dbapi
|
|
7
10
|
import pyarrow
|
|
8
11
|
import pyarrow.dataset
|
|
9
12
|
import pyarrow.dataset as ds
|
|
@@ -74,40 +77,57 @@ def _get_csv_convert_options(
|
|
|
74
77
|
)
|
|
75
78
|
|
|
76
79
|
|
|
77
|
-
def
|
|
80
|
+
def _csv_stream_to_sqlite_and_parquet(
|
|
78
81
|
identifier_data_type: str,
|
|
79
82
|
measure_data_type: str,
|
|
80
83
|
temporality_type: str,
|
|
81
84
|
reader: pyarrow.csv.CSVStreamingReader,
|
|
82
85
|
writer: pyarrow.parquet.ParquetWriter,
|
|
86
|
+
conn: sqlite3.Connection,
|
|
83
87
|
) -> None:
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
88
|
+
with conn.cursor() as cursor:
|
|
89
|
+
while True:
|
|
90
|
+
try:
|
|
91
|
+
batch = reader.read_next_batch()
|
|
92
|
+
except StopIteration:
|
|
93
|
+
logger.debug("End of file reached for CSV file")
|
|
94
|
+
break
|
|
95
|
+
table = pyarrow.Table.from_batches([batch])
|
|
96
|
+
unit_id = _sanitize_unit_id(table, identifier_data_type)
|
|
97
|
+
value = _sanitize_value(table, measure_data_type)
|
|
98
|
+
epoch_start = _cast_to_epoch_date(table, "start")
|
|
99
|
+
epoch_stop = _cast_to_epoch_date(table, "stop")
|
|
100
|
+
columns = [unit_id, value, epoch_start, epoch_stop]
|
|
101
|
+
column_names = [
|
|
102
|
+
"unit_id",
|
|
103
|
+
"value",
|
|
104
|
+
"start_epoch_days",
|
|
105
|
+
"stop_epoch_days",
|
|
106
|
+
]
|
|
107
|
+
if temporality_type in ["STATUS", "ACCUMULATED"]:
|
|
108
|
+
columns.append(_generate_start_year(table))
|
|
109
|
+
column_names.append("start_year")
|
|
110
|
+
table = pyarrow.Table.from_arrays(columns, column_names)
|
|
111
|
+
writer.write_table(table)
|
|
112
|
+
|
|
113
|
+
column_names2 = [
|
|
114
|
+
"unit_id",
|
|
115
|
+
"start_epoch_days",
|
|
116
|
+
"stop_epoch_days",
|
|
117
|
+
]
|
|
118
|
+
columns2 = [unit_id, epoch_start, epoch_stop]
|
|
119
|
+
cursor.adbc_ingest(
|
|
120
|
+
"dataset",
|
|
121
|
+
pyarrow.Table.from_arrays(columns2, column_names2),
|
|
122
|
+
mode="append",
|
|
123
|
+
)
|
|
124
|
+
conn.commit()
|
|
106
125
|
|
|
107
126
|
|
|
108
|
-
def
|
|
127
|
+
def _csv_to_sqlite_and_parquet(
|
|
109
128
|
input_csv_path: Path,
|
|
110
|
-
output_parquet_path:
|
|
129
|
+
output_parquet_path: Path,
|
|
130
|
+
conn: sqlite3.Connection,
|
|
111
131
|
identifier_data_type: str,
|
|
112
132
|
measure_data_type: str,
|
|
113
133
|
temporality_type: str,
|
|
@@ -141,13 +161,20 @@ def _csv_to_parquet(
|
|
|
141
161
|
schema_list.append(("start_year", pyarrow.string()))
|
|
142
162
|
schema = pyarrow.schema(schema_list)
|
|
143
163
|
with parquet.ParquetWriter(output_parquet_path, schema) as writer:
|
|
144
|
-
|
|
164
|
+
_csv_stream_to_sqlite_and_parquet(
|
|
145
165
|
identifier_data_type,
|
|
146
166
|
measure_data_type,
|
|
147
167
|
temporality_type,
|
|
148
168
|
reader,
|
|
149
169
|
writer,
|
|
170
|
+
conn,
|
|
150
171
|
)
|
|
172
|
+
logger.debug("Done streaming, writing parquet metadata")
|
|
173
|
+
logger.debug("Creating sqlite index ...")
|
|
174
|
+
conn.execute(
|
|
175
|
+
"CREATE INDEX IF NOT EXISTS index_unit_id ON dataset(unit_id)"
|
|
176
|
+
)
|
|
177
|
+
conn.commit()
|
|
151
178
|
return pyarrow.dataset.dataset(output_parquet_path)
|
|
152
179
|
except ArrowInvalid as e:
|
|
153
180
|
raise ValidationError(
|
|
@@ -200,9 +227,10 @@ def _generate_start_year(table: pyarrow.Table) -> pyarrow.Array:
|
|
|
200
227
|
)
|
|
201
228
|
|
|
202
229
|
|
|
203
|
-
def
|
|
230
|
+
def read_and_sanitize_csv_write_sqlite_and_parquet(
|
|
204
231
|
input_data_path: Path,
|
|
205
232
|
output_parquet_path: Path,
|
|
233
|
+
sqlite_path: Path,
|
|
206
234
|
identifier_data_type: str,
|
|
207
235
|
measure_data_type: str,
|
|
208
236
|
temporality_type: str,
|
|
@@ -212,13 +240,32 @@ def read_and_sanitize_csv_write_parquet(
|
|
|
212
240
|
ensures the input csv data follows the requirements for the
|
|
213
241
|
microdata data model.
|
|
214
242
|
"""
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
243
|
+
if os.path.exists(sqlite_path):
|
|
244
|
+
os.remove(sqlite_path)
|
|
245
|
+
with adbc_driver_sqlite.dbapi.connect(
|
|
246
|
+
sqlite_path, autocommit=False
|
|
247
|
+
) as conn:
|
|
248
|
+
conn.execute(
|
|
249
|
+
"""CREATE TABLE dataset
|
|
250
|
+
(
|
|
251
|
+
unit_id VARCHAR,
|
|
252
|
+
start_epoch_days INTEGER,
|
|
253
|
+
stop_epoch_days INTEGER
|
|
254
|
+
)"""
|
|
255
|
+
)
|
|
256
|
+
conn.commit()
|
|
257
|
+
# row_count = get_row_count(input_data_path, show_progress)
|
|
258
|
+
logger.debug("Streaming to sqlite and parquet ...")
|
|
259
|
+
filesystem_dataset = _csv_to_sqlite_and_parquet(
|
|
260
|
+
input_data_path,
|
|
261
|
+
output_parquet_path,
|
|
262
|
+
conn,
|
|
263
|
+
identifier_data_type,
|
|
264
|
+
measure_data_type,
|
|
265
|
+
temporality_type,
|
|
266
|
+
)
|
|
267
|
+
logger.debug("Returning FileSystemDataset")
|
|
268
|
+
return filesystem_dataset
|
|
222
269
|
|
|
223
270
|
|
|
224
271
|
def _min_max(
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# pyright: reportAttributeAccessIssue=false
|
|
2
|
+
import os.path
|
|
3
|
+
import sqlite3
|
|
2
4
|
from datetime import datetime
|
|
3
|
-
from
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import List, Union
|
|
4
7
|
|
|
5
|
-
from pyarrow import Table,
|
|
8
|
+
from pyarrow import Table, dataset
|
|
6
9
|
from pyarrow.dataset import FileSystemDataset
|
|
7
10
|
|
|
8
11
|
from microdata_tools.validation.exceptions import ValidationError
|
|
@@ -208,49 +211,35 @@ def _accumulated_temporal_variables_check(data: FileSystemDataset) -> None:
|
|
|
208
211
|
)
|
|
209
212
|
|
|
210
213
|
|
|
211
|
-
def _only_unique_identifiers_check(
|
|
214
|
+
def _only_unique_identifiers_check(conn: sqlite3.Connection) -> None:
|
|
212
215
|
"""
|
|
213
216
|
A table with temporalityType=FIXED is only valid if all
|
|
214
217
|
cells in the unit_id column are unique.
|
|
215
218
|
"""
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
),
|
|
222
|
-
identifiers["unit_id"],
|
|
223
|
-
],
|
|
224
|
-
names=["bucket", "unit_id"],
|
|
225
|
-
)
|
|
226
|
-
unique_buckets = compute.unique(identifiers["bucket"])
|
|
227
|
-
for unique_bucket in unique_buckets:
|
|
228
|
-
bucket_table = identifiers.filter(
|
|
229
|
-
dataset.field("bucket") == unique_bucket
|
|
230
|
-
)
|
|
231
|
-
bucket_row_count = len(bucket_table)
|
|
232
|
-
unique_identifiers_count = len(compute.unique(bucket_table["unit_id"]))
|
|
233
|
-
if unique_identifiers_count != bucket_row_count:
|
|
219
|
+
prev_unit_id = None
|
|
220
|
+
for (unit_id,) in conn.execute(
|
|
221
|
+
"SELECT unit_id FROM dataset ORDER BY unit_id ASC"
|
|
222
|
+
):
|
|
223
|
+
if unit_id == prev_unit_id:
|
|
234
224
|
raise ValidationError(
|
|
235
225
|
"#1 column",
|
|
236
226
|
errors=["Duplicate identifiers in #1 column"],
|
|
237
227
|
)
|
|
228
|
+
else:
|
|
229
|
+
prev_unit_id = unit_id
|
|
238
230
|
|
|
239
231
|
|
|
240
|
-
def _status_uniquesness_check(
|
|
232
|
+
def _status_uniquesness_check(conn: sqlite3.Connection) -> None:
|
|
241
233
|
"""
|
|
242
234
|
A table with temporalityType=STATUS is valid only if all
|
|
243
235
|
cells in the unit_id column are unique per status date.
|
|
244
236
|
"""
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
)
|
|
252
|
-
unique_identifiers = compute.unique(status_table["unit_id"])
|
|
253
|
-
if len(unique_identifiers) != len(status_table):
|
|
237
|
+
prev = None, None
|
|
238
|
+
for curr in conn.execute(
|
|
239
|
+
"SELECT unit_id, start_epoch_days FROM dataset "
|
|
240
|
+
+ "ORDER BY unit_id ASC, start_epoch_days ASC"
|
|
241
|
+
):
|
|
242
|
+
if curr == prev:
|
|
254
243
|
raise ValidationError(
|
|
255
244
|
"#1, #3 and #4 columns",
|
|
256
245
|
errors=[
|
|
@@ -258,110 +247,140 @@ def _status_uniquesness_check(data: FileSystemDataset) -> None:
|
|
|
258
247
|
"(#3 and #4 column)"
|
|
259
248
|
],
|
|
260
249
|
)
|
|
250
|
+
else:
|
|
251
|
+
prev = curr
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _from_epoch_days_to_date(epoch_days: Union[int, None]) -> str:
|
|
255
|
+
return (
|
|
256
|
+
""
|
|
257
|
+
if epoch_days is None
|
|
258
|
+
else datetime.fromtimestamp(epoch_days * 24 * 60 * 60).strftime(
|
|
259
|
+
"%Y-%m-%d"
|
|
260
|
+
)
|
|
261
|
+
)
|
|
261
262
|
|
|
262
263
|
|
|
263
|
-
def
|
|
264
|
+
def _find_overlap(start_list: list, stop_list: list) -> Union[str, None]:
|
|
264
265
|
"""
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
266
|
+
Looks for overlapping timespans where each timespan
|
|
267
|
+
is defined by a start_date at an index from the start_list,
|
|
268
|
+
and a stop_date at the same index from the stop_list.
|
|
268
269
|
"""
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
"
|
|
270
|
+
for i in range(len(start_list) - 1):
|
|
271
|
+
if stop_list[i] is None:
|
|
272
|
+
return (
|
|
273
|
+
f"timespan: ({_from_epoch_days_to_date(start_list[i])} - "
|
|
274
|
+
") overlaps with "
|
|
275
|
+
f"timespan: "
|
|
276
|
+
f"({_from_epoch_days_to_date(start_list[i + 1])} - "
|
|
277
|
+
f"{_from_epoch_days_to_date(stop_list[i + 1])})"
|
|
276
278
|
)
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
if stop_list[i] is None:
|
|
287
|
-
return (
|
|
288
|
-
f"timespan: ({from_epoch_days_to_date(start_list[i])} - "
|
|
289
|
-
") overlaps with "
|
|
290
|
-
f"timespan: "
|
|
291
|
-
f"({from_epoch_days_to_date(start_list[i + 1])} - "
|
|
292
|
-
f"{from_epoch_days_to_date(stop_list[i + 1])})"
|
|
293
|
-
)
|
|
294
|
-
if stop_list[i] >= start_list[i + 1]:
|
|
295
|
-
return (
|
|
296
|
-
f"timespan: ({from_epoch_days_to_date(start_list[i])} - "
|
|
297
|
-
f"{from_epoch_days_to_date(stop_list[i])}) "
|
|
298
|
-
f"overlaps with timespan: "
|
|
299
|
-
f"({from_epoch_days_to_date(start_list[i + 1])} - "
|
|
300
|
-
f"{from_epoch_days_to_date(stop_list[i + 1])})"
|
|
301
|
-
)
|
|
302
|
-
return None
|
|
279
|
+
if stop_list[i] >= start_list[i + 1]:
|
|
280
|
+
return (
|
|
281
|
+
f"timespan: ({_from_epoch_days_to_date(start_list[i])} - "
|
|
282
|
+
f"{_from_epoch_days_to_date(stop_list[i])}) "
|
|
283
|
+
f"overlaps with timespan: "
|
|
284
|
+
f"({_from_epoch_days_to_date(start_list[i + 1])} - "
|
|
285
|
+
f"{_from_epoch_days_to_date(stop_list[i + 1])})"
|
|
286
|
+
)
|
|
287
|
+
return None
|
|
303
288
|
|
|
304
|
-
def batch(iterable: Sequence, batch_size: int) -> Iterator:
|
|
305
|
-
for index in range(0, len(iterable), batch_size):
|
|
306
|
-
yield iterable[index : index + batch_size]
|
|
307
289
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
)
|
|
319
|
-
identifier_time_spans = identifier_time_spans.group_by(
|
|
320
|
-
"unit_id", use_threads=False
|
|
321
|
-
).aggregate([("start_epoch_days", "list"), ("stop_epoch_days", "list")])
|
|
322
|
-
for i in range(len(identifier_time_spans)):
|
|
323
|
-
overlap_message = find_overlap(
|
|
324
|
-
identifier_time_spans["start_epoch_days_list"][i].as_py(),
|
|
325
|
-
identifier_time_spans["stop_epoch_days_list"][i].as_py(),
|
|
290
|
+
def _check_overlap(
|
|
291
|
+
error_list: list[str], unit_id: str, start_list: list, stop_list: list
|
|
292
|
+
) -> None:
|
|
293
|
+
overlap_message = _find_overlap(start_list, stop_list)
|
|
294
|
+
if overlap_message is not None:
|
|
295
|
+
error_list.append(
|
|
296
|
+
(
|
|
297
|
+
"Invalid overlapping timespans for identifier"
|
|
298
|
+
f' "{unit_id}":'
|
|
299
|
+
f" {overlap_message}"
|
|
326
300
|
)
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
(
|
|
330
|
-
"Invalid overlapping timespans for identifier"
|
|
331
|
-
f' "{identifier_time_spans["unit_id"][i]}":'
|
|
332
|
-
f" {overlap_message}"
|
|
333
|
-
)
|
|
334
|
-
)
|
|
335
|
-
if len(error_list) > 49:
|
|
336
|
-
raise ValidationError(
|
|
337
|
-
"#1, #3 and #4 columns",
|
|
338
|
-
errors=error_list,
|
|
339
|
-
)
|
|
340
|
-
if error_list:
|
|
301
|
+
)
|
|
302
|
+
if len(error_list) > 49:
|
|
341
303
|
raise ValidationError(
|
|
342
304
|
"#1, #3 and #4 columns",
|
|
343
305
|
errors=error_list,
|
|
344
306
|
)
|
|
345
307
|
|
|
346
308
|
|
|
309
|
+
def _no_overlapping_timespans_check(
|
|
310
|
+
conn: sqlite3.Connection,
|
|
311
|
+
) -> None:
|
|
312
|
+
"""
|
|
313
|
+
A table with temporalityType=(EVENT|ACCUMULATED) is valid
|
|
314
|
+
only if all rows for a given identifier contains no overlapping
|
|
315
|
+
timespans in the start_epoch_days and stop_epoch_days columns.
|
|
316
|
+
"""
|
|
317
|
+
error_list = []
|
|
318
|
+
cursor = conn.cursor()
|
|
319
|
+
cursor.execute(
|
|
320
|
+
"SELECT unit_id, start_epoch_days, stop_epoch_days "
|
|
321
|
+
+ "FROM dataset "
|
|
322
|
+
+ "ORDER BY unit_id, start_epoch_days"
|
|
323
|
+
)
|
|
324
|
+
curr_unit_id = None
|
|
325
|
+
start_list = []
|
|
326
|
+
stop_list = []
|
|
327
|
+
while True:
|
|
328
|
+
res = cursor.fetchone()
|
|
329
|
+
if res is None:
|
|
330
|
+
if curr_unit_id is not None:
|
|
331
|
+
_check_overlap(error_list, curr_unit_id, start_list, stop_list)
|
|
332
|
+
break
|
|
333
|
+
else:
|
|
334
|
+
unit_id, start_epoch_days, stop_epoch_days = res
|
|
335
|
+
if curr_unit_id is None:
|
|
336
|
+
curr_unit_id = unit_id
|
|
337
|
+
start_list.append(start_epoch_days)
|
|
338
|
+
stop_list.append(stop_epoch_days)
|
|
339
|
+
elif curr_unit_id == unit_id:
|
|
340
|
+
start_list.append(start_epoch_days)
|
|
341
|
+
stop_list.append(stop_epoch_days)
|
|
342
|
+
elif curr_unit_id != unit_id:
|
|
343
|
+
_check_overlap(error_list, curr_unit_id, start_list, stop_list)
|
|
344
|
+
curr_unit_id = unit_id
|
|
345
|
+
start_list = [start_epoch_days]
|
|
346
|
+
stop_list = [stop_epoch_days]
|
|
347
|
+
else:
|
|
348
|
+
raise RuntimeError("Unhandled state!")
|
|
349
|
+
if error_list:
|
|
350
|
+
raise ValidationError(
|
|
351
|
+
"#1, #3 and #4 columns",
|
|
352
|
+
errors=error_list,
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
|
|
347
356
|
def validate_dataset(
|
|
348
357
|
data: FileSystemDataset,
|
|
358
|
+
sqlite_path: Path,
|
|
349
359
|
measure_data_type: str,
|
|
350
360
|
code_list: Union[List, None],
|
|
351
361
|
sentinel_list: Union[List, None],
|
|
352
362
|
temporality_type: str,
|
|
353
363
|
) -> None:
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
364
|
+
assert os.path.exists(sqlite_path)
|
|
365
|
+
conn = sqlite3.connect(sqlite_path)
|
|
366
|
+
try:
|
|
367
|
+
_valid_unit_id_check(data)
|
|
368
|
+
_valid_value_column_check(
|
|
369
|
+
data, measure_data_type, code_list, sentinel_list
|
|
370
|
+
)
|
|
371
|
+
if temporality_type == "FIXED":
|
|
372
|
+
_fixed_temporal_variables_check(data)
|
|
373
|
+
_only_unique_identifiers_check(conn)
|
|
374
|
+
elif temporality_type == "STATUS":
|
|
375
|
+
_status_temporal_variables_check(data)
|
|
376
|
+
_status_uniquesness_check(conn)
|
|
377
|
+
elif temporality_type == "ACCUMULATED":
|
|
378
|
+
_accumulated_temporal_variables_check(data)
|
|
379
|
+
_no_overlapping_timespans_check(conn)
|
|
380
|
+
elif temporality_type == "EVENT":
|
|
381
|
+
_event_temporal_variables_check(data)
|
|
382
|
+
_no_overlapping_timespans_check(conn)
|
|
383
|
+
else:
|
|
384
|
+
raise RuntimeError(f"Unknown temporality type '{temporality_type}'")
|
|
385
|
+
finally:
|
|
386
|
+
conn.close()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "microdata-tools"
|
|
3
|
-
version = "2.0
|
|
3
|
+
version = "2.1.0"
|
|
4
4
|
description = "Tools for the microdata.no platform"
|
|
5
5
|
authors = [{ name = "microdata-developers" }]
|
|
6
6
|
license = { text = "MIT" }
|
|
@@ -11,6 +11,8 @@ dependencies = [
|
|
|
11
11
|
"numpy>=2.2.0",
|
|
12
12
|
"pyarrow==23.0.1",
|
|
13
13
|
"pydantic>=2.11.7",
|
|
14
|
+
"adbc-driver-manager==1.10.0",
|
|
15
|
+
"adbc-driver-sqlite==1.10.0"
|
|
14
16
|
]
|
|
15
17
|
|
|
16
18
|
[dependency-groups]
|
|
@@ -27,30 +29,30 @@ exclude-newer = "7 days"
|
|
|
27
29
|
required-version = ">=0.7.0"
|
|
28
30
|
|
|
29
31
|
[build-system]
|
|
30
|
-
build-backend
|
|
31
|
-
requires
|
|
32
|
+
build-backend = 'hatchling.build'
|
|
33
|
+
requires = ['hatchling']
|
|
32
34
|
|
|
33
35
|
[tool.hatch.build.targets.sdist]
|
|
34
36
|
packages = ["microdata_tools"]
|
|
35
37
|
include = [
|
|
36
|
-
|
|
38
|
+
"microdata_tools/validation/components/unit_type_variables/*.json"
|
|
37
39
|
]
|
|
38
40
|
|
|
39
41
|
[tool.hatch.build.targets.wheel]
|
|
40
42
|
packages = ["microdata_tools"]
|
|
41
43
|
include = [
|
|
42
|
-
|
|
44
|
+
"microdata_tools/validation/components/unit_type_variables/*.json"
|
|
43
45
|
]
|
|
44
46
|
|
|
45
47
|
[tool.ruff]
|
|
46
48
|
line-length = 80
|
|
47
49
|
target-version = "py313"
|
|
48
50
|
lint.select = [
|
|
49
|
-
"F",
|
|
50
|
-
"T",
|
|
51
|
-
"E",
|
|
52
|
-
"W",
|
|
53
|
-
"I",
|
|
51
|
+
"F", # Pyflakes errors (including unused imports, undefined names)
|
|
52
|
+
"T", # flake8-type-checking (imports used only for typing)
|
|
53
|
+
"E", # pycodestyle errors (style errors)
|
|
54
|
+
"W", # pycodestyle warnings
|
|
55
|
+
"I", # isort import ordering
|
|
54
56
|
"ANN", # type annotation related rules
|
|
55
57
|
]
|
|
56
58
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/packaging/exceptions/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/validation/exceptions/__init__.py
RENAMED
|
File without changes
|
{microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/validation/model/__init__.py
RENAMED
|
File without changes
|
{microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/validation/model/metadata.py
RENAMED
|
File without changes
|
|
File without changes
|
{microdata_tools-2.0.1 → microdata_tools-2.1.0}/microdata_tools/validation/steps/metadata_reader.py
RENAMED
|
File without changes
|