exadata-validator 1.1.0__tar.gz → 1.2.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.
- exadata_validator-1.2.0/.gitignore +156 -0
- exadata_validator-1.2.0/AGENTS.md +47 -0
- {exadata_validator-1.1.0 → exadata_validator-1.2.0}/PKG-INFO +5 -8
- exadata_validator-1.2.0/pyproject.toml +31 -0
- exadata_validator-1.2.0/tests/conftest.py +26 -0
- exadata_validator-1.2.0/tests/data/fail/data_model_longitudinal_v_1_0/CDEsMetadata.json +45 -0
- exadata_validator-1.2.0/tests/data/fail/data_model_longitudinal_v_1_0/dataset.csv +5 -0
- exadata_validator-1.2.0/tests/data/fail/data_model_v_1_0/CDEsMetadata.json +88 -0
- exadata_validator-1.2.0/tests/data/fail/data_model_v_1_0/column_not_present_in_cdes.csv +6 -0
- exadata_validator-1.2.0/tests/data/fail/data_model_v_1_0/dataset_exceeds_max.csv +6 -0
- exadata_validator-1.2.0/tests/data/fail/data_model_v_1_0/dataset_exceeds_min.csv +6 -0
- exadata_validator-1.2.0/tests/data/fail/data_model_v_1_0/dataset_is_not_unique.csv +6 -0
- exadata_validator-1.2.0/tests/data/fail/data_model_v_1_0/duplication_column_row_id.csv +6 -0
- exadata_validator-1.2.0/tests/data/fail/data_model_v_1_0/invalid_enum.csv +6 -0
- exadata_validator-1.2.0/tests/data/fail/data_model_v_1_0/invalid_type1.csv +6 -0
- exadata_validator-1.2.0/tests/data/fail/data_model_v_1_0/missing_column_dataset.csv +6 -0
- exadata_validator-1.2.0/tests/data/fail/data_model_v_1_0/missing_column_row_id.csv +6 -0
- exadata_validator-1.2.0/tests/data/fail/data_model_v_1_0/missing_column_subjectcode.csv +6 -0
- exadata_validator-1.2.0/tests/data/success/data_model1_v_1_0/CDEsMetadata.json +88 -0
- exadata_validator-1.2.0/tests/data/success/data_model1_v_1_0/dataset10.csv +6 -0
- exadata_validator-1.2.0/tests/data/success/data_model1_v_1_0/dataset20.csv +6 -0
- exadata_validator-1.2.0/tests/data/success/data_model_longitudinal_v_1_0/CDEsMetadata.json +45 -0
- exadata_validator-1.2.0/tests/data/success/data_model_longitudinal_v_1_0/dataset.csv +4 -0
- exadata_validator-1.2.0/tests/data/success/data_model_v_1_0/CDEsMetadata.json +90 -0
- exadata_validator-1.2.0/tests/data/success/data_model_v_1_0/dataset.csv +6 -0
- exadata_validator-1.2.0/tests/data/success/data_model_v_1_0/dataset1.csv +6 -0
- exadata_validator-1.2.0/tests/data/success/data_model_v_1_0/dataset10.csv +6 -0
- exadata_validator-1.2.0/tests/data/success/data_model_v_1_0/dataset123.csv +6 -0
- exadata_validator-1.2.0/tests/data/success/data_model_v_1_0/dataset2.csv +6 -0
- exadata_validator-1.2.0/tests/test_commands.py +158 -0
- exadata_validator-1.2.0/tests/test_dataelements.py +28 -0
- exadata_validator-1.2.0/tests/test_duckdb_validator.py +57 -0
- exadata_validator-1.2.0/uv.lock +147 -0
- exadata_validator-1.1.0/pyproject.toml +0 -27
- {exadata_validator-1.1.0 → exadata_validator-1.2.0}/README.md +0 -0
- {exadata_validator-1.1.0 → exadata_validator-1.2.0}/validator/__init__.py +0 -0
- {exadata_validator-1.1.0 → exadata_validator-1.2.0}/validator/__main__.py +0 -0
- {exadata_validator-1.1.0 → exadata_validator-1.2.0}/validator/commands.py +0 -0
- {exadata_validator-1.1.0 → exadata_validator-1.2.0}/validator/dataelements.py +0 -0
- {exadata_validator-1.1.0 → exadata_validator-1.2.0}/validator/duckdb_validator.py +0 -0
- {exadata_validator-1.1.0 → exadata_validator-1.2.0}/validator/exceptions.py +0 -0
- {exadata_validator-1.1.0 → exadata_validator-1.2.0}/validator/reporting.py +0 -0
- {exadata_validator-1.1.0 → exadata_validator-1.2.0}/validator/usecases.py +0 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Intelij
|
|
2
|
+
.idea/
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
# Byte-compiled / optimized / DLL files
|
|
6
|
+
__pycache__/
|
|
7
|
+
*.py[cod]
|
|
8
|
+
*$py.class
|
|
9
|
+
|
|
10
|
+
# C extensions
|
|
11
|
+
*.so
|
|
12
|
+
|
|
13
|
+
# Distribution / packaging
|
|
14
|
+
.Python
|
|
15
|
+
build/
|
|
16
|
+
develop-eggs/
|
|
17
|
+
dist/
|
|
18
|
+
downloads/
|
|
19
|
+
eggs/
|
|
20
|
+
.eggs/
|
|
21
|
+
lib/
|
|
22
|
+
lib64/
|
|
23
|
+
parts/
|
|
24
|
+
sdist/
|
|
25
|
+
var/
|
|
26
|
+
wheels/
|
|
27
|
+
pip-wheel-metadata/
|
|
28
|
+
share/python-wheels/
|
|
29
|
+
*.egg-info/
|
|
30
|
+
.installed.cfg
|
|
31
|
+
*.egg
|
|
32
|
+
MANIFEST
|
|
33
|
+
|
|
34
|
+
# PyInstaller
|
|
35
|
+
# Usually these files are written by a python script from a template
|
|
36
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
37
|
+
*.manifest
|
|
38
|
+
*.spec
|
|
39
|
+
|
|
40
|
+
# Installer logs
|
|
41
|
+
pip-log.txt
|
|
42
|
+
pip-delete-this-directory.txt
|
|
43
|
+
|
|
44
|
+
# Unit test / coverage reports
|
|
45
|
+
htmlcov/
|
|
46
|
+
.tox/
|
|
47
|
+
.nox/
|
|
48
|
+
.coverage
|
|
49
|
+
.coverage.*
|
|
50
|
+
.cache
|
|
51
|
+
nosetests.xml
|
|
52
|
+
coverage.xml
|
|
53
|
+
*.cover
|
|
54
|
+
*.py,cover
|
|
55
|
+
.hypothesis/
|
|
56
|
+
.pytest_cache/
|
|
57
|
+
|
|
58
|
+
# Translations
|
|
59
|
+
*.mo
|
|
60
|
+
*.pot
|
|
61
|
+
|
|
62
|
+
# Django stuff:
|
|
63
|
+
*.log
|
|
64
|
+
local_settings.py
|
|
65
|
+
db.sqlite3
|
|
66
|
+
db.sqlite3-journal
|
|
67
|
+
|
|
68
|
+
# Flask stuff:
|
|
69
|
+
instance/
|
|
70
|
+
.webassets-cache
|
|
71
|
+
|
|
72
|
+
# Scrapy stuff:
|
|
73
|
+
.scrapy
|
|
74
|
+
|
|
75
|
+
# Sphinx documentation
|
|
76
|
+
docs/_build/
|
|
77
|
+
|
|
78
|
+
# PyBuilder
|
|
79
|
+
target/
|
|
80
|
+
|
|
81
|
+
# Jupyter Notebook
|
|
82
|
+
.ipynb_checkpoints
|
|
83
|
+
|
|
84
|
+
# IPython
|
|
85
|
+
profile_default/
|
|
86
|
+
ipython_config.py
|
|
87
|
+
|
|
88
|
+
# pyenv
|
|
89
|
+
.python-version
|
|
90
|
+
|
|
91
|
+
# pipenv
|
|
92
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
93
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
94
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
95
|
+
# install all needed dependencies.
|
|
96
|
+
#Pipfile.lock
|
|
97
|
+
|
|
98
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
|
99
|
+
__pypackages__/
|
|
100
|
+
|
|
101
|
+
# SageMath parsed files
|
|
102
|
+
*.sage.py
|
|
103
|
+
|
|
104
|
+
# Environments
|
|
105
|
+
.env
|
|
106
|
+
.venv
|
|
107
|
+
env/
|
|
108
|
+
venv/
|
|
109
|
+
ENV/
|
|
110
|
+
env.bak/
|
|
111
|
+
venv.bak/
|
|
112
|
+
|
|
113
|
+
# Spyder project settings
|
|
114
|
+
.spyderproject
|
|
115
|
+
.spyproject
|
|
116
|
+
|
|
117
|
+
# Rope project settings
|
|
118
|
+
.ropeproject
|
|
119
|
+
|
|
120
|
+
# mkdocs documentation
|
|
121
|
+
/site
|
|
122
|
+
|
|
123
|
+
# mypy
|
|
124
|
+
.mypy_cache/
|
|
125
|
+
.dmypy.json
|
|
126
|
+
dmypy.json
|
|
127
|
+
|
|
128
|
+
# Pyre type checker
|
|
129
|
+
.pyre/
|
|
130
|
+
|
|
131
|
+
#temp files
|
|
132
|
+
*~
|
|
133
|
+
*.swp
|
|
134
|
+
*.swo
|
|
135
|
+
*#*
|
|
136
|
+
|
|
137
|
+
# vim stuff
|
|
138
|
+
.vim/
|
|
139
|
+
pyrightconfig.json
|
|
140
|
+
.vimspector.json
|
|
141
|
+
|
|
142
|
+
# macos
|
|
143
|
+
**/.DS_Store
|
|
144
|
+
|
|
145
|
+
# mip deployment config file
|
|
146
|
+
.deployment.toml
|
|
147
|
+
/configs
|
|
148
|
+
/smpc_configs
|
|
149
|
+
.stored_data/
|
|
150
|
+
*.db
|
|
151
|
+
/tests/test_data/.data_paths/
|
|
152
|
+
/tests/test_data/localworker*/
|
|
153
|
+
/logs/
|
|
154
|
+
|
|
155
|
+
# LLMs
|
|
156
|
+
.codex
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Agent Instructions: exadata-validator
|
|
2
|
+
|
|
3
|
+
This directory is a separate uv-managed package for validating data-model folders
|
|
4
|
+
using DuckDB. Follow the root `AGENTS.md` first, then these package-specific
|
|
5
|
+
rules.
|
|
6
|
+
|
|
7
|
+
## Package Overview
|
|
8
|
+
|
|
9
|
+
`exadata-validator` exposes a CLI through `validator.commands:entry` and can
|
|
10
|
+
also run as `python -m validator`.
|
|
11
|
+
|
|
12
|
+
## Commands
|
|
13
|
+
|
|
14
|
+
Run commands from `exadata-validator/`.
|
|
15
|
+
|
|
16
|
+
Install:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
uv sync --all-groups
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Test:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
uv run pytest -q
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Build:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
uv build
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Smoke test an installed package as CI does:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
python -m validator validate-data-model tests/data/success/data_model_longitudinal_v_1_0
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Rules
|
|
41
|
+
|
|
42
|
+
- Keep dependency and lockfile changes local to this package unless the root
|
|
43
|
+
package also intentionally changes.
|
|
44
|
+
- Preserve the CLI contract documented in `README.md`.
|
|
45
|
+
- Add or update tests under `tests/` for validation behavior changes.
|
|
46
|
+
- Treat CSV/CDE validation semantics as public behavior for package users.
|
|
47
|
+
- Do not mix Exaflow service deployment concerns into this package.
|
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: exadata-validator
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Validate data-model folders using DuckDB.
|
|
5
5
|
Author: Exaflow Team
|
|
6
|
-
Requires-Python:
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
Requires-Dist: click (>=8.1,<8.2)
|
|
10
|
-
Requires-Dist: duckdb (>=1.1,<1.2)
|
|
6
|
+
Requires-Python: <3.11,>=3.10
|
|
7
|
+
Requires-Dist: click<8.2.0,>=8.1.0
|
|
8
|
+
Requires-Dist: duckdb<1.2.0,>=1.1.0
|
|
11
9
|
Description-Content-Type: text/markdown
|
|
12
10
|
|
|
13
11
|
# exadata-validator
|
|
@@ -58,4 +56,3 @@ Use `exadata-validator validate-data-model --help` for reporting, output, and th
|
|
|
58
56
|
|
|
59
57
|
- CSV validation queries files directly with DuckDB and uses fused aggregate checks to reduce scan overhead.
|
|
60
58
|
- Folder-level dataset uniqueness is enforced across all CSV files via SQL using normalized codes (`trim + lower`).
|
|
61
|
-
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "exadata-validator"
|
|
3
|
+
version = "1.2.0"
|
|
4
|
+
description = "Validate data-model folders using DuckDB."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Exaflow Team" },
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.10,<3.11"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"click>=8.1.0,<8.2.0",
|
|
12
|
+
"duckdb>=1.1.0,<1.2.0",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
exadata-validator = "validator.commands:entry"
|
|
17
|
+
|
|
18
|
+
[dependency-groups]
|
|
19
|
+
dev = [
|
|
20
|
+
"pytest>=8.4.0,<8.5.0",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[build-system]
|
|
24
|
+
requires = ["hatchling"]
|
|
25
|
+
build-backend = "hatchling.build"
|
|
26
|
+
|
|
27
|
+
[tool.hatch.build.targets.wheel]
|
|
28
|
+
packages = ["validator"]
|
|
29
|
+
|
|
30
|
+
[tool.pytest.ini_options]
|
|
31
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
TEST_DIR = Path(__file__).resolve().parent
|
|
7
|
+
DATA_DIR = TEST_DIR / "data"
|
|
8
|
+
|
|
9
|
+
SUCCESS_MODEL_DIR = DATA_DIR / "success" / "data_model_v_1_0"
|
|
10
|
+
FAIL_MODEL_DIR = DATA_DIR / "fail" / "data_model_v_1_0"
|
|
11
|
+
|
|
12
|
+
SUCCESS_METADATA_PATH = SUCCESS_MODEL_DIR / "CDEsMetadata.json"
|
|
13
|
+
FAIL_METADATA_PATH = FAIL_MODEL_DIR / "CDEsMetadata.json"
|
|
14
|
+
|
|
15
|
+
LONGITUDINAL_SUCCESS_DIR = DATA_DIR / "success" / "data_model_longitudinal_v_1_0"
|
|
16
|
+
LONGITUDINAL_FAIL_DIR = DATA_DIR / "fail" / "data_model_longitudinal_v_1_0"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.fixture
|
|
20
|
+
def success_metadata():
|
|
21
|
+
return json.loads(SUCCESS_METADATA_PATH.read_text(encoding="utf-8"))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@pytest.fixture
|
|
25
|
+
def fail_metadata():
|
|
26
|
+
return json.loads(FAIL_METADATA_PATH.read_text(encoding="utf-8"))
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"code": "data_model_longitudinal",
|
|
3
|
+
"label": "The Data Model Longitudinal",
|
|
4
|
+
"version": "1.0",
|
|
5
|
+
"longitudinal": true,
|
|
6
|
+
"variables": [
|
|
7
|
+
{
|
|
8
|
+
"isCategorical": true,
|
|
9
|
+
"code": "dataset",
|
|
10
|
+
"sql_type": "text",
|
|
11
|
+
"description": "",
|
|
12
|
+
"enumerations": [
|
|
13
|
+
{"code": "dataset_longitudinal", "label": "Dataset Longitudinal"}
|
|
14
|
+
],
|
|
15
|
+
"label": "Dataset",
|
|
16
|
+
"methodology": "",
|
|
17
|
+
"type": "nominal"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"isCategorical": false,
|
|
21
|
+
"code": "subjectid",
|
|
22
|
+
"sql_type": "text",
|
|
23
|
+
"description": "",
|
|
24
|
+
"label": "subjectid",
|
|
25
|
+
"methodology": "",
|
|
26
|
+
"type": "nominal"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"isCategorical": true,
|
|
30
|
+
"label": "visitid",
|
|
31
|
+
"code": "visitid",
|
|
32
|
+
"sql_type": "text",
|
|
33
|
+
"enumerations": [
|
|
34
|
+
{"code": "BL", "label": "Base line"},
|
|
35
|
+
{"code": "FL1", "label": "Follow up 1"},
|
|
36
|
+
{"code": "FL2", "label": "Follow up 2"}
|
|
37
|
+
],
|
|
38
|
+
"description": "",
|
|
39
|
+
"methodology": "",
|
|
40
|
+
"type": "nominal"
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
"groups": [
|
|
44
|
+
]
|
|
45
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
{
|
|
2
|
+
"code": "data_model",
|
|
3
|
+
"label": "The Data Model",
|
|
4
|
+
"version": "1.0",
|
|
5
|
+
"variables": [
|
|
6
|
+
{
|
|
7
|
+
"isCategorical": false,
|
|
8
|
+
"code": "var1",
|
|
9
|
+
"sql_type": "text",
|
|
10
|
+
"description": "",
|
|
11
|
+
"label": "Variable 1",
|
|
12
|
+
"methodology": "",
|
|
13
|
+
"type": "nominal"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"isCategorical": false,
|
|
17
|
+
"label": "subjectcode",
|
|
18
|
+
"code": "subjectcode",
|
|
19
|
+
"sql_type": "text",
|
|
20
|
+
"description": "",
|
|
21
|
+
"methodology": "",
|
|
22
|
+
"type": "nominal"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"isCategorical": true,
|
|
26
|
+
"code": "var2",
|
|
27
|
+
"sql_type": "text",
|
|
28
|
+
"description": "",
|
|
29
|
+
"enumerations": [
|
|
30
|
+
{"code": "l1", "label": "Level1"},
|
|
31
|
+
{"code": "l2", "label": "Level2"}
|
|
32
|
+
],
|
|
33
|
+
"label": "Variable 2",
|
|
34
|
+
"methodology": "",
|
|
35
|
+
"type": "nominal"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"isCategorical": true,
|
|
39
|
+
"code": "dataset",
|
|
40
|
+
"sql_type": "text",
|
|
41
|
+
"description": "",
|
|
42
|
+
"enumerations": [
|
|
43
|
+
{"code": "valid_dataset", "label": "Valid dataset"},
|
|
44
|
+
{"code": "dataset_is_not_unique", "label": "Dataset with more one value"}
|
|
45
|
+
],
|
|
46
|
+
"label": "Dataset",
|
|
47
|
+
"methodology": "",
|
|
48
|
+
"type": "nominal"
|
|
49
|
+
}
|
|
50
|
+
],
|
|
51
|
+
"groups": [
|
|
52
|
+
{
|
|
53
|
+
"name": "group",
|
|
54
|
+
"label": "The Group",
|
|
55
|
+
"variables": [
|
|
56
|
+
{
|
|
57
|
+
"isCategorical": false,
|
|
58
|
+
"code": "var3",
|
|
59
|
+
"sql_type": "real",
|
|
60
|
+
"minValue": 5,
|
|
61
|
+
"maxValue": 60,
|
|
62
|
+
"description": "",
|
|
63
|
+
"label": "Variable 3",
|
|
64
|
+
"methodology": "",
|
|
65
|
+
"type": "nominal"
|
|
66
|
+
}
|
|
67
|
+
],
|
|
68
|
+
"groups": [
|
|
69
|
+
{
|
|
70
|
+
"name": "inner_group",
|
|
71
|
+
"label": "The Inner Group",
|
|
72
|
+
"variables": [
|
|
73
|
+
{
|
|
74
|
+
"isCategorical": false,
|
|
75
|
+
"code": "var4",
|
|
76
|
+
"sql_type": "int",
|
|
77
|
+
"units": "years",
|
|
78
|
+
"description": "",
|
|
79
|
+
"label": "Variable 4",
|
|
80
|
+
"methodology": "",
|
|
81
|
+
"type": "nominal"
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
{
|
|
2
|
+
"code": "data_model1",
|
|
3
|
+
"label": "The Data Model",
|
|
4
|
+
"version": "1.0",
|
|
5
|
+
"variables": [
|
|
6
|
+
{
|
|
7
|
+
"isCategorical": false,
|
|
8
|
+
"code": "var1",
|
|
9
|
+
"sql_type": "text",
|
|
10
|
+
"description": "",
|
|
11
|
+
"label": "Variable 1",
|
|
12
|
+
"methodology": "",
|
|
13
|
+
"type": "nominal"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"isCategorical": false,
|
|
17
|
+
"label": "subjectcode",
|
|
18
|
+
"code": "subjectcode",
|
|
19
|
+
"sql_type": "text",
|
|
20
|
+
"description": "",
|
|
21
|
+
"methodology": "",
|
|
22
|
+
"type": "nominal"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"isCategorical": true,
|
|
26
|
+
"code": "var2",
|
|
27
|
+
"sql_type": "text",
|
|
28
|
+
"description": "",
|
|
29
|
+
"enumerations": [
|
|
30
|
+
{"code": "l10", "label": "Level10"},
|
|
31
|
+
{"code": "l20", "label": "Level20"}
|
|
32
|
+
],
|
|
33
|
+
"label": "Variable 2",
|
|
34
|
+
"methodology": "",
|
|
35
|
+
"type": "nominal"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"isCategorical": true,
|
|
39
|
+
"code": "dataset",
|
|
40
|
+
"sql_type": "text",
|
|
41
|
+
"description": "",
|
|
42
|
+
"enumerations": [
|
|
43
|
+
{"code": "dataset10", "label": "Dataset 10"},
|
|
44
|
+
{"code": "dataset20", "label": "Dataset 20"}
|
|
45
|
+
],
|
|
46
|
+
"label": "Dataset",
|
|
47
|
+
"methodology": "",
|
|
48
|
+
"type": "nominal"
|
|
49
|
+
}
|
|
50
|
+
],
|
|
51
|
+
"groups": [
|
|
52
|
+
{
|
|
53
|
+
"name": "group",
|
|
54
|
+
"label": "The Group",
|
|
55
|
+
"variables": [
|
|
56
|
+
{
|
|
57
|
+
"isCategorical": false,
|
|
58
|
+
"code": "var3",
|
|
59
|
+
"sql_type": "real",
|
|
60
|
+
"minValue": 0,
|
|
61
|
+
"maxValue": 100,
|
|
62
|
+
"description": "",
|
|
63
|
+
"label": "Variable 3",
|
|
64
|
+
"methodology": "",
|
|
65
|
+
"type": "nominal"
|
|
66
|
+
}
|
|
67
|
+
],
|
|
68
|
+
"groups": [
|
|
69
|
+
{
|
|
70
|
+
"name": "inner_group",
|
|
71
|
+
"label": "The Inner Group",
|
|
72
|
+
"variables": [
|
|
73
|
+
{
|
|
74
|
+
"isCategorical": false,
|
|
75
|
+
"code": "var4",
|
|
76
|
+
"sql_type": "int",
|
|
77
|
+
"units": "years",
|
|
78
|
+
"description": "",
|
|
79
|
+
"label": "Variable 4",
|
|
80
|
+
"methodology": "",
|
|
81
|
+
"type": "nominal"
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"code": "data_model_longitudinal",
|
|
3
|
+
"label": "The Data Model Longitudinal",
|
|
4
|
+
"version": "2.0",
|
|
5
|
+
"longitudinal": true,
|
|
6
|
+
"variables": [
|
|
7
|
+
{
|
|
8
|
+
"isCategorical": true,
|
|
9
|
+
"code": "dataset",
|
|
10
|
+
"sql_type": "text",
|
|
11
|
+
"description": "",
|
|
12
|
+
"enumerations": [
|
|
13
|
+
{"code": "dataset_longitudinal", "label": "Dataset Longitudinal"}
|
|
14
|
+
],
|
|
15
|
+
"label": "Dataset",
|
|
16
|
+
"methodology": "",
|
|
17
|
+
"type": "nominal"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"isCategorical": false,
|
|
21
|
+
"code": "subjectid",
|
|
22
|
+
"sql_type": "text",
|
|
23
|
+
"description": "",
|
|
24
|
+
"label": "subjectid",
|
|
25
|
+
"methodology": "",
|
|
26
|
+
"type": "nominal"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"isCategorical": true,
|
|
30
|
+
"label": "visitid",
|
|
31
|
+
"code": "visitid",
|
|
32
|
+
"sql_type": "text",
|
|
33
|
+
"enumerations": [
|
|
34
|
+
{"code": "BL", "label": "Base line"},
|
|
35
|
+
{"code": "FL1", "label": "Follow up 1"},
|
|
36
|
+
{"code": "FL2", "label": "Follow up 2"}
|
|
37
|
+
],
|
|
38
|
+
"description": "",
|
|
39
|
+
"methodology": "",
|
|
40
|
+
"type": "nominal"
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
"groups": [
|
|
44
|
+
]
|
|
45
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
{
|
|
2
|
+
"code": "data_model",
|
|
3
|
+
"label": "The Data Model",
|
|
4
|
+
"version": "1.0",
|
|
5
|
+
"variables": [
|
|
6
|
+
{
|
|
7
|
+
"isCategorical": false,
|
|
8
|
+
"code": "var1",
|
|
9
|
+
"sql_type": "text",
|
|
10
|
+
"description": "",
|
|
11
|
+
"label": "Variable 1",
|
|
12
|
+
"methodology": "",
|
|
13
|
+
"type": "nominal"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"isCategorical": false,
|
|
17
|
+
"label": "subjectcode",
|
|
18
|
+
"code": "subjectcode",
|
|
19
|
+
"sql_type": "text",
|
|
20
|
+
"description": "",
|
|
21
|
+
"methodology": "",
|
|
22
|
+
"type": "nominal"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"isCategorical": true,
|
|
26
|
+
"code": "var2",
|
|
27
|
+
"sql_type": "text",
|
|
28
|
+
"description": "",
|
|
29
|
+
"enumerations": [
|
|
30
|
+
{"code": "1", "label": "Leve1"},
|
|
31
|
+
{"code": "2.0", "label": "Leve2"}
|
|
32
|
+
],
|
|
33
|
+
"label": "Variable 2",
|
|
34
|
+
"methodology": "",
|
|
35
|
+
"type": "nominal"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"isCategorical": true,
|
|
39
|
+
"code": "dataset",
|
|
40
|
+
"sql_type": "text",
|
|
41
|
+
"description": "",
|
|
42
|
+
"enumerations": [
|
|
43
|
+
{"code": "dataset", "label": "Dataset"},
|
|
44
|
+
{"code": "dataset1", "label": "Dataset 1"},
|
|
45
|
+
{"code": "dataset2", "label": "Dataset 2"},
|
|
46
|
+
{"code": "dataset10", "label": "Dataset 10"}
|
|
47
|
+
],
|
|
48
|
+
"label": "Dataset",
|
|
49
|
+
"methodology": "",
|
|
50
|
+
"type": "nominal"
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
"groups": [
|
|
54
|
+
{
|
|
55
|
+
"name": "group",
|
|
56
|
+
"label": "The Group",
|
|
57
|
+
"variables": [
|
|
58
|
+
{
|
|
59
|
+
"isCategorical": false,
|
|
60
|
+
"code": "var3",
|
|
61
|
+
"sql_type": "real",
|
|
62
|
+
"minValue": 0,
|
|
63
|
+
"maxValue": 100,
|
|
64
|
+
"description": "",
|
|
65
|
+
"label": "Variable 3",
|
|
66
|
+
"methodology": "",
|
|
67
|
+
"type": "nominal"
|
|
68
|
+
}
|
|
69
|
+
],
|
|
70
|
+
"groups": [
|
|
71
|
+
{
|
|
72
|
+
"name": "inner_group",
|
|
73
|
+
"label": "The Inner Group",
|
|
74
|
+
"variables": [
|
|
75
|
+
{
|
|
76
|
+
"isCategorical": false,
|
|
77
|
+
"code": "var4",
|
|
78
|
+
"sql_type": "int",
|
|
79
|
+
"units": "years",
|
|
80
|
+
"description": "",
|
|
81
|
+
"label": "Variable 4",
|
|
82
|
+
"methodology": "",
|
|
83
|
+
"type": "nominal"
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from click.testing import CliRunner
|
|
5
|
+
from validator import commands
|
|
6
|
+
from validator.commands import entry as cli
|
|
7
|
+
from validator.exceptions import ExitCode
|
|
8
|
+
|
|
9
|
+
TEST_DIR = Path(__file__).resolve().parent
|
|
10
|
+
DATA_DIR = TEST_DIR / "data"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def run_cli(*args):
|
|
14
|
+
runner = CliRunner()
|
|
15
|
+
return runner.invoke(cli, [*args])
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_validate_data_model_command_success():
|
|
19
|
+
folder = DATA_DIR / "success" / "data_model1_v_1_0"
|
|
20
|
+
result = run_cli("validate-data-model", str(folder))
|
|
21
|
+
assert result.exit_code == ExitCode.OK
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_validate_data_model_command_failure():
|
|
25
|
+
folder = DATA_DIR / "fail" / "data_model_v_1_0"
|
|
26
|
+
result = run_cli("validate-data-model", str(folder))
|
|
27
|
+
assert result.exit_code == ExitCode.FILE_ERROR
|
|
28
|
+
assert "Validation report for:" in result.output
|
|
29
|
+
assert "[columns.unknown]" in result.output
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_validate_data_model_command_fail_fast():
|
|
33
|
+
folder = DATA_DIR / "fail" / "data_model_v_1_0"
|
|
34
|
+
result = run_cli("validate-data-model", str(folder), "--fail-fast")
|
|
35
|
+
assert result.exit_code == ExitCode.FILE_ERROR
|
|
36
|
+
assert "Validation error" in result.output
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_validate_data_model_command_no_csv(tmp_path):
|
|
40
|
+
folder = tmp_path / "data_model"
|
|
41
|
+
folder.mkdir()
|
|
42
|
+
(folder / "CDEsMetadata.json").write_text(
|
|
43
|
+
(DATA_DIR / "success" / "data_model_v_1_0" / "CDEsMetadata.json").read_text(),
|
|
44
|
+
encoding="utf-8",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
result = run_cli("validate-data-model", str(folder))
|
|
48
|
+
assert result.exit_code == ExitCode.USER_ERROR
|
|
49
|
+
assert "No CSV files found" in result.output
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_validate_data_model_command_duplicate_dataset_across_csv_files(tmp_path):
|
|
53
|
+
source_folder = DATA_DIR / "success" / "data_model_v_1_0"
|
|
54
|
+
folder = tmp_path / "data_model"
|
|
55
|
+
folder.mkdir()
|
|
56
|
+
|
|
57
|
+
(folder / "CDEsMetadata.json").write_text(
|
|
58
|
+
(source_folder / "CDEsMetadata.json").read_text(),
|
|
59
|
+
encoding="utf-8",
|
|
60
|
+
)
|
|
61
|
+
(folder / "dataset1.csv").write_text(
|
|
62
|
+
(source_folder / "dataset1.csv").read_text(),
|
|
63
|
+
encoding="utf-8",
|
|
64
|
+
)
|
|
65
|
+
(folder / "dataset2.csv").write_text(
|
|
66
|
+
(source_folder / "dataset2.csv").read_text().replace("dataset2", "dataset1"),
|
|
67
|
+
encoding="utf-8",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
result = run_cli("validate-data-model", str(folder))
|
|
71
|
+
assert result.exit_code == ExitCode.FILE_ERROR
|
|
72
|
+
assert "Dataset code collision after normalization" in result.output
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_validate_data_model_command_rejects_unsupported_format():
|
|
76
|
+
folder = DATA_DIR / "fail" / "data_model_v_1_0"
|
|
77
|
+
result = run_cli("validate-data-model", str(folder), "--format", "ndjson")
|
|
78
|
+
assert result.exit_code == 2
|
|
79
|
+
assert "Invalid value for '--format'" in result.output
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_validate_data_model_command_report_all_html_output_file(tmp_path):
|
|
83
|
+
folder = DATA_DIR / "fail" / "data_model_v_1_0"
|
|
84
|
+
output_path = tmp_path / "report.html"
|
|
85
|
+
result = run_cli(
|
|
86
|
+
"validate-data-model",
|
|
87
|
+
str(folder),
|
|
88
|
+
"--format",
|
|
89
|
+
"html",
|
|
90
|
+
"--output",
|
|
91
|
+
str(output_path),
|
|
92
|
+
)
|
|
93
|
+
assert result.exit_code == ExitCode.FILE_ERROR
|
|
94
|
+
assert output_path.exists()
|
|
95
|
+
assert "<table>" in output_path.read_text(encoding="utf-8")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_validate_data_model_command_report_all_html_defaults_to_tmp(
|
|
99
|
+
monkeypatch, tmp_path
|
|
100
|
+
):
|
|
101
|
+
folder = DATA_DIR / "fail" / "data_model_v_1_0"
|
|
102
|
+
auto_output_path = tmp_path / "report-auto.html"
|
|
103
|
+
monkeypatch.setattr(commands, "_default_html_report_path", lambda: auto_output_path)
|
|
104
|
+
|
|
105
|
+
result = run_cli("validate-data-model", str(folder), "--format", "html")
|
|
106
|
+
|
|
107
|
+
assert result.exit_code == ExitCode.FILE_ERROR
|
|
108
|
+
assert auto_output_path.exists()
|
|
109
|
+
assert "<table>" in auto_output_path.read_text(encoding="utf-8")
|
|
110
|
+
assert "Report written to" in result.output
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_validate_data_model_command_duplicate_dataset_normalization(tmp_path):
|
|
114
|
+
source_folder = DATA_DIR / "success" / "data_model_v_1_0"
|
|
115
|
+
folder = tmp_path / "data_model"
|
|
116
|
+
folder.mkdir()
|
|
117
|
+
|
|
118
|
+
metadata = json.loads(
|
|
119
|
+
(source_folder / "CDEsMetadata.json").read_text(encoding="utf-8")
|
|
120
|
+
)
|
|
121
|
+
dataset_cde = next(cde for cde in metadata["variables"] if cde["code"] == "dataset")
|
|
122
|
+
dataset_cde["enumerations"].append({"code": "DATASET1", "label": "Dataset 1 Upper"})
|
|
123
|
+
(folder / "CDEsMetadata.json").write_text(
|
|
124
|
+
json.dumps(metadata),
|
|
125
|
+
encoding="utf-8",
|
|
126
|
+
)
|
|
127
|
+
(folder / "dataset1.csv").write_text(
|
|
128
|
+
(source_folder / "dataset1.csv").read_text(),
|
|
129
|
+
encoding="utf-8",
|
|
130
|
+
)
|
|
131
|
+
(folder / "dataset2.csv").write_text(
|
|
132
|
+
(source_folder / "dataset2.csv").read_text().replace("dataset2", "DATASET1"),
|
|
133
|
+
encoding="utf-8",
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
result = run_cli("validate-data-model", str(folder))
|
|
137
|
+
assert result.exit_code == ExitCode.FILE_ERROR
|
|
138
|
+
assert "collision after normalization (trim+lower)" in result.output
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_validate_data_model_command_report_all_text_includes_line_number(tmp_path):
|
|
142
|
+
source_folder = DATA_DIR / "success" / "data_model_v_1_0"
|
|
143
|
+
folder = tmp_path / "data_model"
|
|
144
|
+
folder.mkdir()
|
|
145
|
+
|
|
146
|
+
(folder / "CDEsMetadata.json").write_text(
|
|
147
|
+
(source_folder / "CDEsMetadata.json").read_text(),
|
|
148
|
+
encoding="utf-8",
|
|
149
|
+
)
|
|
150
|
+
(folder / "dataset.csv").write_text(
|
|
151
|
+
"subjectcode,var1,var3,dataset\n10,1,2000,dataset1\n",
|
|
152
|
+
encoding="utf-8",
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
result = run_cli("validate-data-model", str(folder), "--format", "text")
|
|
156
|
+
assert result.exit_code == ExitCode.FILE_ERROR
|
|
157
|
+
assert "[range.max]" in result.output
|
|
158
|
+
assert "line=2" in result.output
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from validator.dataelements import CommonDataElement
|
|
3
|
+
from validator.dataelements import flatten_cdes
|
|
4
|
+
from validator.dataelements import validate_dataset_present_on_cdes_with_proper_format
|
|
5
|
+
from validator.dataelements import validate_longitudinal_data_model
|
|
6
|
+
from validator.exceptions import InvalidDataModelError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_flatten_cdes_success(success_metadata):
|
|
10
|
+
cdes = flatten_cdes(success_metadata)
|
|
11
|
+
assert len(cdes) == 6
|
|
12
|
+
assert all(isinstance(cde, CommonDataElement) for cde in cdes)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_dataset_cde_validation_success(success_metadata):
|
|
16
|
+
cdes = flatten_cdes(success_metadata)
|
|
17
|
+
validate_dataset_present_on_cdes_with_proper_format(cdes)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_dataset_cde_missing_raises(success_metadata):
|
|
21
|
+
cdes = [cde for cde in flatten_cdes(success_metadata) if cde.code != "dataset"]
|
|
22
|
+
with pytest.raises(InvalidDataModelError, match="There is no 'dataset' CDE"):
|
|
23
|
+
validate_dataset_present_on_cdes_with_proper_format(cdes)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_longitudinal_validation_requires_subjectid_and_visitid():
|
|
27
|
+
with pytest.raises(InvalidDataModelError, match="subjectid"):
|
|
28
|
+
validate_longitudinal_data_model([])
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
from validator.duckdb_validator import DuckDBDatasetValidator
|
|
6
|
+
from validator.exceptions import InvalidDatasetError
|
|
7
|
+
|
|
8
|
+
TEST_DIR = Path(__file__).resolve().parent
|
|
9
|
+
DATA_DIR = TEST_DIR / "data"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_validate_csv_success(success_metadata):
|
|
13
|
+
csv_path = DATA_DIR / "success" / "data_model_v_1_0" / "dataset.csv"
|
|
14
|
+
DuckDBDatasetValidator(success_metadata).validate_csv(csv_path)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_validate_csv_invalid_type(fail_metadata):
|
|
18
|
+
csv_path = DATA_DIR / "fail" / "data_model_v_1_0" / "invalid_type1.csv"
|
|
19
|
+
with pytest.raises(
|
|
20
|
+
InvalidDatasetError, match="Column 'var3' has invalid real values"
|
|
21
|
+
):
|
|
22
|
+
DuckDBDatasetValidator(fail_metadata).validate_csv(csv_path)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_validate_csv_missing_dataset_column(fail_metadata):
|
|
26
|
+
csv_path = DATA_DIR / "fail" / "data_model_v_1_0" / "missing_column_dataset.csv"
|
|
27
|
+
with pytest.raises(InvalidDatasetError, match="'dataset' column"):
|
|
28
|
+
DuckDBDatasetValidator(fail_metadata).validate_csv(csv_path)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_validate_csv_invalid_enum(fail_metadata):
|
|
32
|
+
csv_path = DATA_DIR / "fail" / "data_model_v_1_0" / "invalid_enum.csv"
|
|
33
|
+
with pytest.raises(InvalidDatasetError, match="has invalid categorical value"):
|
|
34
|
+
DuckDBDatasetValidator(fail_metadata).validate_csv(csv_path)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_validate_csv_unknown_column_suggests_closest_cde(success_metadata, tmp_path):
|
|
38
|
+
csv_path = tmp_path / "dataset.csv"
|
|
39
|
+
csv_path.write_text(
|
|
40
|
+
"dataset,vra1\ndataset,10\n",
|
|
41
|
+
encoding="utf-8",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
with pytest.raises(InvalidDatasetError, match="Did you mean 'var1'"):
|
|
45
|
+
DuckDBDatasetValidator(success_metadata).validate_csv(csv_path)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_validate_csv_longitudinal_duplicate_pair():
|
|
49
|
+
metadata = json.loads(
|
|
50
|
+
(
|
|
51
|
+
DATA_DIR / "fail" / "data_model_longitudinal_v_1_0" / "CDEsMetadata.json"
|
|
52
|
+
).read_text(encoding="utf-8")
|
|
53
|
+
)
|
|
54
|
+
csv_path = DATA_DIR / "fail" / "data_model_longitudinal_v_1_0" / "dataset.csv"
|
|
55
|
+
|
|
56
|
+
with pytest.raises(InvalidDatasetError, match=r"duplicate \(visitid, subjectid\)"):
|
|
57
|
+
DuckDBDatasetValidator(metadata).validate_csv(csv_path)
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
version = 1
|
|
2
|
+
revision = 3
|
|
3
|
+
requires-python = "==3.10.*"
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "click"
|
|
7
|
+
version = "8.1.8"
|
|
8
|
+
source = { registry = "https://pypi.org/simple" }
|
|
9
|
+
dependencies = [
|
|
10
|
+
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
11
|
+
]
|
|
12
|
+
sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" }
|
|
13
|
+
wheels = [
|
|
14
|
+
{ url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" },
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[[package]]
|
|
18
|
+
name = "colorama"
|
|
19
|
+
version = "0.4.6"
|
|
20
|
+
source = { registry = "https://pypi.org/simple" }
|
|
21
|
+
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
|
22
|
+
wheels = [
|
|
23
|
+
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[[package]]
|
|
27
|
+
name = "duckdb"
|
|
28
|
+
version = "1.1.3"
|
|
29
|
+
source = { registry = "https://pypi.org/simple" }
|
|
30
|
+
sdist = { url = "https://files.pythonhosted.org/packages/a0/d7/ec014b351b6bb026d5f473b1d0ec6bd6ba40786b9abbf530b4c9041d9895/duckdb-1.1.3.tar.gz", hash = "sha256:68c3a46ab08836fe041d15dcbf838f74a990d551db47cb24ab1c4576fc19351c", size = 12240672, upload-time = "2024-11-04T14:03:28.533Z" }
|
|
31
|
+
wheels = [
|
|
32
|
+
{ url = "https://files.pythonhosted.org/packages/de/7e/aef0fa22a80939edb04f66152a1fd5ce7257931576be192a8068e74f0892/duckdb-1.1.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:1c0226dc43e2ee4cc3a5a4672fddb2d76fd2cf2694443f395c02dd1bea0b7fce", size = 15469781, upload-time = "2024-11-04T14:01:05.885Z" },
|
|
33
|
+
{ url = "https://files.pythonhosted.org/packages/38/22/df548714ddd915929ebbba9699e8614655ed93cd367f5849f6dbd1b3e160/duckdb-1.1.3-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:7c71169fa804c0b65e49afe423ddc2dc83e198640e3b041028da8110f7cd16f7", size = 32313005, upload-time = "2024-11-04T14:01:09.089Z" },
|
|
34
|
+
{ url = "https://files.pythonhosted.org/packages/9f/38/8de640857f4c55df870faf025835e09c69222d365dc773507e934cee3376/duckdb-1.1.3-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:872d38b65b66e3219d2400c732585c5b4d11b13d7a36cd97908d7981526e9898", size = 16931481, upload-time = "2024-11-04T14:01:12.047Z" },
|
|
35
|
+
{ url = "https://files.pythonhosted.org/packages/41/9b/87fff1341a9f57ab75284d79f902fee8cd6ef3a9135af4c723c90384d307/duckdb-1.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25fb02629418c0d4d94a2bc1776edaa33f6f6ccaa00bd84eb96ecb97ae4b50e9", size = 18491670, upload-time = "2024-11-04T14:01:14.378Z" },
|
|
36
|
+
{ url = "https://files.pythonhosted.org/packages/3e/ee/8f74ccecbafd14e257c634f0f2cdebbc35634d9d74f04bb7ad8a0e142bf8/duckdb-1.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e3f5cd604e7c39527e6060f430769b72234345baaa0987f9500988b2814f5e4", size = 20144774, upload-time = "2024-11-04T14:01:17.218Z" },
|
|
37
|
+
{ url = "https://files.pythonhosted.org/packages/36/7b/edffb833b8569a7fc1799ceb4392911e0082f18a6076225441e954a95853/duckdb-1.1.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08935700e49c187fe0e9b2b86b5aad8a2ccd661069053e38bfaed3b9ff795efd", size = 18287084, upload-time = "2024-11-04T14:01:19.921Z" },
|
|
38
|
+
{ url = "https://files.pythonhosted.org/packages/a9/ab/6367e8c98b3331260bb4389c6b80deef96614c1e21edcdba23a882e45ab0/duckdb-1.1.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f9b47036945e1db32d70e414a10b1593aec641bd4c5e2056873d971cc21e978b", size = 21614877, upload-time = "2024-11-04T14:01:22.998Z" },
|
|
39
|
+
{ url = "https://files.pythonhosted.org/packages/03/d8/89b1c5f1dbd16342640742f6f6d3f1c827d1a1b966d674774ddfe6a385e2/duckdb-1.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:35c420f58abc79a68a286a20fd6265636175fadeca1ce964fc8ef159f3acc289", size = 10954044, upload-time = "2024-11-04T14:01:26.091Z" },
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[[package]]
|
|
43
|
+
name = "exadata-validator"
|
|
44
|
+
version = "1.2.0"
|
|
45
|
+
source = { editable = "." }
|
|
46
|
+
dependencies = [
|
|
47
|
+
{ name = "click" },
|
|
48
|
+
{ name = "duckdb" },
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
[package.dev-dependencies]
|
|
52
|
+
dev = [
|
|
53
|
+
{ name = "pytest" },
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
[package.metadata]
|
|
57
|
+
requires-dist = [
|
|
58
|
+
{ name = "click", specifier = ">=8.1.0,<8.2.0" },
|
|
59
|
+
{ name = "duckdb", specifier = ">=1.1.0,<1.2.0" },
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
[package.metadata.requires-dev]
|
|
63
|
+
dev = [{ name = "pytest", specifier = ">=8.4.0,<8.5.0" }]
|
|
64
|
+
|
|
65
|
+
[[package]]
|
|
66
|
+
name = "exceptiongroup"
|
|
67
|
+
version = "1.3.1"
|
|
68
|
+
source = { registry = "https://pypi.org/simple" }
|
|
69
|
+
dependencies = [
|
|
70
|
+
{ name = "typing-extensions" },
|
|
71
|
+
]
|
|
72
|
+
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
|
|
73
|
+
wheels = [
|
|
74
|
+
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
[[package]]
|
|
78
|
+
name = "iniconfig"
|
|
79
|
+
version = "2.3.0"
|
|
80
|
+
source = { registry = "https://pypi.org/simple" }
|
|
81
|
+
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
|
82
|
+
wheels = [
|
|
83
|
+
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
[[package]]
|
|
87
|
+
name = "packaging"
|
|
88
|
+
version = "26.2"
|
|
89
|
+
source = { registry = "https://pypi.org/simple" }
|
|
90
|
+
sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
|
|
91
|
+
wheels = [
|
|
92
|
+
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
[[package]]
|
|
96
|
+
name = "pluggy"
|
|
97
|
+
version = "1.6.0"
|
|
98
|
+
source = { registry = "https://pypi.org/simple" }
|
|
99
|
+
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
|
100
|
+
wheels = [
|
|
101
|
+
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
[[package]]
|
|
105
|
+
name = "pygments"
|
|
106
|
+
version = "2.20.0"
|
|
107
|
+
source = { registry = "https://pypi.org/simple" }
|
|
108
|
+
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
|
|
109
|
+
wheels = [
|
|
110
|
+
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
[[package]]
|
|
114
|
+
name = "pytest"
|
|
115
|
+
version = "8.4.2"
|
|
116
|
+
source = { registry = "https://pypi.org/simple" }
|
|
117
|
+
dependencies = [
|
|
118
|
+
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
119
|
+
{ name = "exceptiongroup" },
|
|
120
|
+
{ name = "iniconfig" },
|
|
121
|
+
{ name = "packaging" },
|
|
122
|
+
{ name = "pluggy" },
|
|
123
|
+
{ name = "pygments" },
|
|
124
|
+
{ name = "tomli" },
|
|
125
|
+
]
|
|
126
|
+
sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
|
|
127
|
+
wheels = [
|
|
128
|
+
{ url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
[[package]]
|
|
132
|
+
name = "tomli"
|
|
133
|
+
version = "2.4.1"
|
|
134
|
+
source = { registry = "https://pypi.org/simple" }
|
|
135
|
+
sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" }
|
|
136
|
+
wheels = [
|
|
137
|
+
{ url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" },
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
[[package]]
|
|
141
|
+
name = "typing-extensions"
|
|
142
|
+
version = "4.15.0"
|
|
143
|
+
source = { registry = "https://pypi.org/simple" }
|
|
144
|
+
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
|
145
|
+
wheels = [
|
|
146
|
+
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
|
147
|
+
]
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
[tool.poetry]
|
|
2
|
-
name = "exadata-validator"
|
|
3
|
-
version = "1.1.0"
|
|
4
|
-
description = "Validate data-model folders using DuckDB."
|
|
5
|
-
authors = ["Exaflow Team"]
|
|
6
|
-
readme = "README.md"
|
|
7
|
-
packages = [
|
|
8
|
-
{ include = "validator" },
|
|
9
|
-
]
|
|
10
|
-
|
|
11
|
-
[tool.poetry.dependencies]
|
|
12
|
-
python = ">=3.10,<3.11"
|
|
13
|
-
click = "~8.1"
|
|
14
|
-
duckdb = "~1.1"
|
|
15
|
-
|
|
16
|
-
[tool.poetry.group.dev.dependencies]
|
|
17
|
-
pytest = "~8.4"
|
|
18
|
-
|
|
19
|
-
[tool.poetry.scripts]
|
|
20
|
-
exadata-validator = "validator.commands:entry"
|
|
21
|
-
|
|
22
|
-
[tool.pytest.ini_options]
|
|
23
|
-
testpaths = ["tests"]
|
|
24
|
-
|
|
25
|
-
[build-system]
|
|
26
|
-
requires = ["poetry-core>=1.8.0"]
|
|
27
|
-
build-backend = "poetry.core.masonry.api"
|
|
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
|