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.
Files changed (43) hide show
  1. exadata_validator-1.2.0/.gitignore +156 -0
  2. exadata_validator-1.2.0/AGENTS.md +47 -0
  3. {exadata_validator-1.1.0 → exadata_validator-1.2.0}/PKG-INFO +5 -8
  4. exadata_validator-1.2.0/pyproject.toml +31 -0
  5. exadata_validator-1.2.0/tests/conftest.py +26 -0
  6. exadata_validator-1.2.0/tests/data/fail/data_model_longitudinal_v_1_0/CDEsMetadata.json +45 -0
  7. exadata_validator-1.2.0/tests/data/fail/data_model_longitudinal_v_1_0/dataset.csv +5 -0
  8. exadata_validator-1.2.0/tests/data/fail/data_model_v_1_0/CDEsMetadata.json +88 -0
  9. exadata_validator-1.2.0/tests/data/fail/data_model_v_1_0/column_not_present_in_cdes.csv +6 -0
  10. exadata_validator-1.2.0/tests/data/fail/data_model_v_1_0/dataset_exceeds_max.csv +6 -0
  11. exadata_validator-1.2.0/tests/data/fail/data_model_v_1_0/dataset_exceeds_min.csv +6 -0
  12. exadata_validator-1.2.0/tests/data/fail/data_model_v_1_0/dataset_is_not_unique.csv +6 -0
  13. exadata_validator-1.2.0/tests/data/fail/data_model_v_1_0/duplication_column_row_id.csv +6 -0
  14. exadata_validator-1.2.0/tests/data/fail/data_model_v_1_0/invalid_enum.csv +6 -0
  15. exadata_validator-1.2.0/tests/data/fail/data_model_v_1_0/invalid_type1.csv +6 -0
  16. exadata_validator-1.2.0/tests/data/fail/data_model_v_1_0/missing_column_dataset.csv +6 -0
  17. exadata_validator-1.2.0/tests/data/fail/data_model_v_1_0/missing_column_row_id.csv +6 -0
  18. exadata_validator-1.2.0/tests/data/fail/data_model_v_1_0/missing_column_subjectcode.csv +6 -0
  19. exadata_validator-1.2.0/tests/data/success/data_model1_v_1_0/CDEsMetadata.json +88 -0
  20. exadata_validator-1.2.0/tests/data/success/data_model1_v_1_0/dataset10.csv +6 -0
  21. exadata_validator-1.2.0/tests/data/success/data_model1_v_1_0/dataset20.csv +6 -0
  22. exadata_validator-1.2.0/tests/data/success/data_model_longitudinal_v_1_0/CDEsMetadata.json +45 -0
  23. exadata_validator-1.2.0/tests/data/success/data_model_longitudinal_v_1_0/dataset.csv +4 -0
  24. exadata_validator-1.2.0/tests/data/success/data_model_v_1_0/CDEsMetadata.json +90 -0
  25. exadata_validator-1.2.0/tests/data/success/data_model_v_1_0/dataset.csv +6 -0
  26. exadata_validator-1.2.0/tests/data/success/data_model_v_1_0/dataset1.csv +6 -0
  27. exadata_validator-1.2.0/tests/data/success/data_model_v_1_0/dataset10.csv +6 -0
  28. exadata_validator-1.2.0/tests/data/success/data_model_v_1_0/dataset123.csv +6 -0
  29. exadata_validator-1.2.0/tests/data/success/data_model_v_1_0/dataset2.csv +6 -0
  30. exadata_validator-1.2.0/tests/test_commands.py +158 -0
  31. exadata_validator-1.2.0/tests/test_dataelements.py +28 -0
  32. exadata_validator-1.2.0/tests/test_duckdb_validator.py +57 -0
  33. exadata_validator-1.2.0/uv.lock +147 -0
  34. exadata_validator-1.1.0/pyproject.toml +0 -27
  35. {exadata_validator-1.1.0 → exadata_validator-1.2.0}/README.md +0 -0
  36. {exadata_validator-1.1.0 → exadata_validator-1.2.0}/validator/__init__.py +0 -0
  37. {exadata_validator-1.1.0 → exadata_validator-1.2.0}/validator/__main__.py +0 -0
  38. {exadata_validator-1.1.0 → exadata_validator-1.2.0}/validator/commands.py +0 -0
  39. {exadata_validator-1.1.0 → exadata_validator-1.2.0}/validator/dataelements.py +0 -0
  40. {exadata_validator-1.1.0 → exadata_validator-1.2.0}/validator/duckdb_validator.py +0 -0
  41. {exadata_validator-1.1.0 → exadata_validator-1.2.0}/validator/exceptions.py +0 -0
  42. {exadata_validator-1.1.0 → exadata_validator-1.2.0}/validator/reporting.py +0 -0
  43. {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.4
1
+ Metadata-Version: 2.5
2
2
  Name: exadata-validator
3
- Version: 1.1.0
3
+ Version: 1.2.0
4
4
  Summary: Validate data-model folders using DuckDB.
5
5
  Author: Exaflow Team
6
- Requires-Python: >=3.10,<3.11
7
- Classifier: Programming Language :: Python :: 3
8
- Classifier: Programming Language :: Python :: 3.10
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,5 @@
1
+ subjectid,visitid,dataset
2
+ "subjectid1","BL","dataset_longitudinal"
3
+ "subjectid2","FL1","dataset_longitudinal"
4
+ "subjectid2","FL1","dataset_longitudinal"
5
+ "subjectid2","FL2","dataset_longitudinal"
@@ -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,6 @@
1
+ row_id,var1,var3,dataset,non_existing_col
2
+ 1,l1,,valid_dataset,1
3
+ 2,l2,22,valid_dataset,2
4
+ 3,l1,23,valid_dataset,3
5
+ 4,l1,24,valid_dataset,4
6
+ 5,l2,25,valid_dataset,5
@@ -0,0 +1,6 @@
1
+ subjectcode,var2,var3,var4,dataset
2
+ 1,l1,100,,valid_dataset
3
+ 2,l2,12,22,valid_dataset
4
+ 3,l1,13,23,valid_dataset
5
+ 4,l1,14,24,valid_dataset
6
+ 5,l2,15,25,valid_dataset
@@ -0,0 +1,6 @@
1
+ subjectcode,var2,var3,var4,dataset
2
+ 1,l1,0,,valid_dataset
3
+ 2,l2,12,22,valid_dataset
4
+ 3,l1,13,23,valid_dataset
5
+ 4,l1,14,24,valid_dataset
6
+ 5,l2,15,25,valid_dataset
@@ -0,0 +1,6 @@
1
+ subjectcode,var1,var3,dataset
2
+ 1,l1,,valid_dataset
3
+ 2,l2,22,valid_dataset
4
+ 3,l1,23,valid_dataset
5
+ 4,l1,24,valid_dataset
6
+ 5,l2,25,dataset_is_not_unique
@@ -0,0 +1,6 @@
1
+ subjectcode,var1,var3,dataset
2
+ 1,l1,,valid_dataset
3
+ 2,l2,22,valid_dataset
4
+ 3,l1,23,valid_dataset
5
+ 4,l1,24,valid_dataset
6
+ 1,l2,25,valid_dataset
@@ -0,0 +1,6 @@
1
+ subjectcode,var2,var3,dataset
2
+ 1,l3,,valid_dataset
3
+ 2,l2,22,valid_dataset
4
+ 3,l1,23,valid_dataset
5
+ 4,l1,24,valid_dataset
6
+ 5,l2,25,valid_dataset
@@ -0,0 +1,6 @@
1
+ subjectcode,var1,var3,dataset
2
+ 1,l10,,valid_dataset
3
+ 2,l20,invalid,valid_dataset
4
+ 3,l10,23,valid_dataset
5
+ 4,l10,24,valid_dataset
6
+ 5,l20,25,valid_dataset
@@ -0,0 +1,6 @@
1
+ subjectcode,var1,var3
2
+ 1,l1,
3
+ 2,l2,22
4
+ 3,l1,23
5
+ 4,l1,24
6
+ 5,l2,25
@@ -0,0 +1,6 @@
1
+ subjectcode,var1,var3,dataset
2
+ 1,l1,,valid_dataset
3
+ 2,l2,22,valid_dataset
4
+ 2,l1,23,valid_dataset
5
+ 2,l1,24,valid_dataset
6
+ 3,l2,25,valid_dataset
@@ -0,0 +1,6 @@
1
+ row_id,var1,var3,dataset
2
+ 1,l1,,valid_dataset
3
+ 2,l2,22,valid_dataset
4
+ 3,l1,23,valid_dataset
5
+ 4,l1,24,valid_dataset
6
+ 5,l2,25,valid_dataset
@@ -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,6 @@
1
+ subjectcode,var1,var3,dataset
2
+ 1,l10,,dataset10
3
+ 1,l20,22,dataset10
4
+ 2,l10,23,dataset10
5
+ 4,l10,24,dataset10
6
+ 5,l20,25,dataset10
@@ -0,0 +1,6 @@
1
+ subjectcode,var2,var3,var4,dataset
2
+ 1,l10,11,,dataset20
3
+ 2,l20,12,22,dataset20
4
+ 2,l10,13,23,dataset20
5
+ 4,l10,14,24,dataset20
6
+ 4,l20,15,25,dataset20
@@ -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,4 @@
1
+ subjectid,visitid,dataset
2
+ "subjectid1","BL","dataset_longitudinal"
3
+ "subjectid2","FL1","dataset_longitudinal"
4
+ "subjectid2","FL2","dataset_longitudinal"
@@ -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,6 @@
1
+ subjectcode,var1,var2,var3,var4,dataset
2
+ 2,1,,,,dataset
3
+ 2,1,2.0,12,22,dataset
4
+ 2,1,1,13,23,dataset
5
+ 3,1,1,14,24,dataset
6
+ 3,1,2.0,15,25,dataset
@@ -0,0 +1,6 @@
1
+ subjectcode,var1,var3,dataset
2
+ 2,1,,dataset1
3
+ 2,2,22,dataset1
4
+ 2,1,23,dataset1
5
+ 5,1,24,dataset1
6
+ 5,2,25,dataset1
@@ -0,0 +1,6 @@
1
+ subjectcode,var1,var3,dataset
2
+ 2,1,,dataset10
3
+ 2,2,22,dataset10
4
+ 2,1,23,dataset10
5
+ 5,1,24,dataset10
6
+ 5,2,25,dataset10
@@ -0,0 +1,6 @@
1
+ subjectcode,var2,var3,var4,dataset
2
+ 2,1,11,,dataset
3
+ 2,2.0,12,22,dataset1
4
+ 3,1,13,23,dataset2
5
+ 4,1,14,24,dataset1
6
+ 3,2.0,15,25,dataset2
@@ -0,0 +1,6 @@
1
+ subjectcode,var2,var3,var4,dataset
2
+ 2,1,11,,dataset2
3
+ 2,2.0,12,22,dataset2
4
+ 3,1,13,23,dataset2
5
+ 4,1,14,24,dataset2
6
+ 3,2.0,15,25,dataset2
@@ -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"