datakiln 0.3.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.
- datakiln-0.3.0/.gitignore +170 -0
- datakiln-0.3.0/LICENSE +21 -0
- datakiln-0.3.0/PKG-INFO +475 -0
- datakiln-0.3.0/README.md +438 -0
- datakiln-0.3.0/datakiln/__init__.py +1 -0
- datakiln-0.3.0/datakiln/__main__.py +7 -0
- datakiln-0.3.0/datakiln/arguments.py +76 -0
- datakiln-0.3.0/datakiln/build_database.py +110 -0
- datakiln-0.3.0/datakiln/build_dbms.py +136 -0
- datakiln-0.3.0/datakiln/build_metadata.py +165 -0
- datakiln-0.3.0/datakiln/build_redcap.py +401 -0
- datakiln-0.3.0/datakiln/cli.py +70 -0
- datakiln-0.3.0/datakiln/extraction/__init__.py +3 -0
- datakiln-0.3.0/datakiln/extraction/exporters.py +213 -0
- datakiln-0.3.0/datakiln/extraction/models.py +712 -0
- datakiln-0.3.0/datakiln/extraction/renderers.py +131 -0
- datakiln-0.3.0/datakiln/extraction/strategies.py +916 -0
- datakiln-0.3.0/datakiln/io.py +57 -0
- datakiln-0.3.0/datakiln/metadata/__init__.py +0 -0
- datakiln-0.3.0/datakiln/metadata/column.py +120 -0
- datakiln-0.3.0/datakiln/metadata/database.py +571 -0
- datakiln-0.3.0/datakiln/metadata/dialect.py +23 -0
- datakiln-0.3.0/datakiln/metadata/files.py +119 -0
- datakiln-0.3.0/datakiln/metadata/foreign_key.py +151 -0
- datakiln-0.3.0/datakiln/metadata/graph.py +237 -0
- datakiln-0.3.0/datakiln/metadata/location.py +223 -0
- datakiln-0.3.0/datakiln/metadata/migrations/__init__.py +52 -0
- datakiln-0.3.0/datakiln/metadata/migrations/env.py +17 -0
- datakiln-0.3.0/datakiln/metadata/migrations/versions/__init__.py +0 -0
- datakiln-0.3.0/datakiln/metadata/migrations/versions/v0001_baseline.py +47 -0
- datakiln-0.3.0/datakiln/metadata/migrations/versions/v0002_objects.py +158 -0
- datakiln-0.3.0/datakiln/metadata/script.py +356 -0
- datakiln-0.3.0/datakiln/metadata/table.py +894 -0
- datakiln-0.3.0/datakiln/metadata/unpivot.py +179 -0
- datakiln-0.3.0/datakiln/show_graph.py +265 -0
- datakiln-0.3.0/datakiln/test.py +88 -0
- datakiln-0.3.0/docs/conf.py +14 -0
- datakiln-0.3.0/docs/index.rst +13 -0
- datakiln-0.3.0/pyproject.toml +63 -0
- datakiln-0.3.0/tests/__init__.py +0 -0
- datakiln-0.3.0/tests/data/foreign_key.csv +3 -0
- datakiln-0.3.0/tests/data/test.csv +3 -0
- datakiln-0.3.0/tests/metadata/foreign_key.json +45 -0
- datakiln-0.3.0/tests/metadata/test.json +28 -0
- datakiln-0.3.0/tests/unit/test_load.py +98 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py,cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
#Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# poetry
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
102
|
+
#poetry.lock
|
|
103
|
+
|
|
104
|
+
# pdm
|
|
105
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
106
|
+
#pdm.lock
|
|
107
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
108
|
+
# in version control.
|
|
109
|
+
# https://pdm.fming.dev/#use-with-ide
|
|
110
|
+
.pdm.toml
|
|
111
|
+
|
|
112
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
113
|
+
__pypackages__/
|
|
114
|
+
|
|
115
|
+
# Celery stuff
|
|
116
|
+
celerybeat-schedule
|
|
117
|
+
celerybeat.pid
|
|
118
|
+
|
|
119
|
+
# SageMath parsed files
|
|
120
|
+
*.sage.py
|
|
121
|
+
|
|
122
|
+
# Environments
|
|
123
|
+
.env
|
|
124
|
+
.venv
|
|
125
|
+
env/
|
|
126
|
+
venv/
|
|
127
|
+
ENV/
|
|
128
|
+
env.bak/
|
|
129
|
+
venv.bak/
|
|
130
|
+
|
|
131
|
+
# Spyder project settings
|
|
132
|
+
.spyderproject
|
|
133
|
+
.spyproject
|
|
134
|
+
|
|
135
|
+
# Rope project settings
|
|
136
|
+
.ropeproject
|
|
137
|
+
|
|
138
|
+
# mkdocs documentation
|
|
139
|
+
/site
|
|
140
|
+
|
|
141
|
+
# mypy
|
|
142
|
+
.mypy_cache/
|
|
143
|
+
.dmypy.json
|
|
144
|
+
dmypy.json
|
|
145
|
+
|
|
146
|
+
# Pyre type checker
|
|
147
|
+
.pyre/
|
|
148
|
+
|
|
149
|
+
# pytype static type analyzer
|
|
150
|
+
.pytype/
|
|
151
|
+
|
|
152
|
+
# Cython debug symbols
|
|
153
|
+
cython_debug/
|
|
154
|
+
|
|
155
|
+
# PyCharm
|
|
156
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
157
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
158
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
159
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
160
|
+
#.idea/
|
|
161
|
+
|
|
162
|
+
# Other
|
|
163
|
+
.vscode/
|
|
164
|
+
|
|
165
|
+
# Local notes
|
|
166
|
+
TODO.md
|
|
167
|
+
|
|
168
|
+
# Generated pages of the API documentation
|
|
169
|
+
docs/api/
|
|
170
|
+
_site/
|
datakiln-0.3.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Lucas Bulgarelli
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
datakiln-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: datakiln
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Build a database from a repository of files.
|
|
5
|
+
Author-email: Lucas Bulgarelli <lucas.bulgarelli@hotmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: database,utilities
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Requires-Dist: alembic>=1.13
|
|
11
|
+
Requires-Dist: dvc[ssh]>=3.60.1
|
|
12
|
+
Requires-Dist: more-itertools>=10.0.0
|
|
13
|
+
Requires-Dist: networkx>=3.0
|
|
14
|
+
Requires-Dist: openpyxl>=3.1.0
|
|
15
|
+
Requires-Dist: pandas>=2.1.0
|
|
16
|
+
Requires-Dist: paramiko>=4.0.0
|
|
17
|
+
Requires-Dist: psycopg2-binary>=2.9
|
|
18
|
+
Requires-Dist: pycap>=2.6.0
|
|
19
|
+
Requires-Dist: pytest>=8.1
|
|
20
|
+
Requires-Dist: python-decouple>=3.8
|
|
21
|
+
Requires-Dist: python-slugify>=8.0.0
|
|
22
|
+
Requires-Dist: sqlalchemy-utils>=0.41.2
|
|
23
|
+
Requires-Dist: sqlalchemy<2.0.36
|
|
24
|
+
Requires-Dist: sqlglot>=30.0.1
|
|
25
|
+
Requires-Dist: sqlparse>=0.5.5
|
|
26
|
+
Requires-Dist: tenacity>=8.0.0
|
|
27
|
+
Provides-Extra: docs
|
|
28
|
+
Requires-Dist: sphinx>=7.2; extra == 'docs'
|
|
29
|
+
Provides-Extra: lint
|
|
30
|
+
Requires-Dist: dlint>=0.14; extra == 'lint'
|
|
31
|
+
Requires-Dist: flake8>=7.0; extra == 'lint'
|
|
32
|
+
Requires-Dist: pylint>=3.0; extra == 'lint'
|
|
33
|
+
Provides-Extra: test
|
|
34
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'test'
|
|
35
|
+
Requires-Dist: pytest-xdist>=3.5; extra == 'test'
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
[](https://github.com/lbulgarelli/datakiln/actions/workflows/lint.yml)
|
|
39
|
+
[](https://github.com/lbulgarelli/datakiln/actions/workflows/test.yml)
|
|
40
|
+
[](https://lbulgarelli.github.io/datakiln/coverage/)
|
|
41
|
+
[](https://lbulgarelli.github.io/datakiln/)
|
|
42
|
+
[](https://www.python.org/)
|
|
43
|
+
[](LICENSE)
|
|
44
|
+
[](https://github.com/lbulgarelli/datakiln/releases)
|
|
45
|
+
|
|
46
|
+
# datakiln
|
|
47
|
+
|
|
48
|
+
datakiln builds a database from a repository of files, so that any
|
|
49
|
+
version of a dataset can be rebuilt later with one command.
|
|
50
|
+
|
|
51
|
+
Research datasets can span dozens of tables, with views and
|
|
52
|
+
functions that people build on top of them. Each new release of the data, each
|
|
53
|
+
fix to a view and each change to a column type leaves a slightly different
|
|
54
|
+
database behind, and months later, when someone has to reproduce a result,
|
|
55
|
+
nothing in the database says which version of which file it came from.
|
|
56
|
+
|
|
57
|
+
Here the data files are versioned, and the metadata keeps track of the source
|
|
58
|
+
tables, their columns and their constraints. Together with the scripts for the
|
|
59
|
+
views and functions, that describes the whole database, so a version of the
|
|
60
|
+
dataset is a version of the repository. Because it remembers what it built last
|
|
61
|
+
time, moving to another version only reloads what changed and reruns only what
|
|
62
|
+
depends on it.
|
|
63
|
+
|
|
64
|
+
## Why use it
|
|
65
|
+
|
|
66
|
+
- To get the dataset exactly as it was when a paper was written, go back to
|
|
67
|
+
that version of the repository and build again.
|
|
68
|
+
- A change to a column type, a key or a view is recorded like any change to
|
|
69
|
+
code, so others can review it and it can be undone.
|
|
70
|
+
- A data pipeline is a chain of steps that depend on one another, and those
|
|
71
|
+
dependencies usually live in someone's head or in conventions that drift.
|
|
72
|
+
Here they are inferred from the repository's own files, so every step runs
|
|
73
|
+
after what it depends on and a change reaches everything built on it.
|
|
74
|
+
- How the tables and views depend on each other can be drawn from the
|
|
75
|
+
repository itself, so whoever picks up a dataset they did not build can see
|
|
76
|
+
how it fits together, and the picture cannot drift from the code.
|
|
77
|
+
|
|
78
|
+
## Features
|
|
79
|
+
|
|
80
|
+
- **Tables from files.** Files of several types are loaded into database
|
|
81
|
+
tables as their metadata describes: types, required columns, primary,
|
|
82
|
+
unique and foreign keys, indexes, values read as `NULL`, and columns
|
|
83
|
+
unpivoted from wide data.
|
|
84
|
+
- **Incremental builds.** A table is rebuilt when its metadata or its data
|
|
85
|
+
file changes, or when a table it references is rebuilt. A script that
|
|
86
|
+
builds views or functions runs again when its content changes or what it
|
|
87
|
+
uses is rebuilt.
|
|
88
|
+
- **DVC data files.** The data files can be tracked by DVC, and the ones a
|
|
89
|
+
build needs are pulled from the remote.
|
|
90
|
+
- **Views and functions in dependency order.** Each SQL script creates one
|
|
91
|
+
object (a view, function, type...) from the tables or from other objects.
|
|
92
|
+
The tools read the SQL to find what each script uses, so the scripts need no
|
|
93
|
+
numbering, and the order comes from the dependencies.
|
|
94
|
+
- **Dependency graph.** The graph of the tables and scripts can be drawn as a
|
|
95
|
+
Mermaid flowchart.
|
|
96
|
+
- **Metadata generation.** Table metadata can be generated from sample data
|
|
97
|
+
files, from a source database in one of several DBMS, or from the forms of a
|
|
98
|
+
REDCap project.
|
|
99
|
+
- **Extraction from a database.** Data can be extracted from a source database
|
|
100
|
+
for the group of people a study follows (its cohort). A table is limited to
|
|
101
|
+
the rows that relate to the cohort or to another extracted table, directly
|
|
102
|
+
or through a chain of joins, and the tables are extracted in the order that
|
|
103
|
+
those relations require.
|
|
104
|
+
- **Pruning and permissions.** Objects removed from the repository are listed
|
|
105
|
+
or dropped, and read access is granted to a database role and its users.
|
|
106
|
+
|
|
107
|
+
## Installation
|
|
108
|
+
|
|
109
|
+
It needs Python and a database server. The metadata gives a column type for
|
|
110
|
+
each DBMS, so the tool is meant to work with more than one. From a clone of the
|
|
111
|
+
repository:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
pip install .
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
This installs one command, `datakiln`, with the subcommands `build`,
|
|
118
|
+
`metadata`, `graph` and `extract` (`extract dbms` and `extract redcap`). Run
|
|
119
|
+
`datakiln --help`, or `--help` after a subcommand, to see all of its options.
|
|
120
|
+
|
|
121
|
+
## Dataset layout
|
|
122
|
+
|
|
123
|
+
Every command takes the directory of the dataset:
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
dataset/
|
|
127
|
+
├── .env # Database connection
|
|
128
|
+
├── .dataignore # Files to skip (optional)
|
|
129
|
+
├── data/ # Data files, one for each table
|
|
130
|
+
├── metadata/ # Table metadata, one JSON file for each table
|
|
131
|
+
├── transform/ # SQL scripts that run on the loaded data
|
|
132
|
+
├── derive/ # SQL scripts that create views, functions...
|
|
133
|
+
└── extract/ # Extraction metadata (only to extract from a DBMS)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`.dataignore` lists files, one for each line, relative to the dataset
|
|
137
|
+
directory (`derive/old.sql`). They are left out of the build.
|
|
138
|
+
|
|
139
|
+
### Environment
|
|
140
|
+
|
|
141
|
+
The connection to the database is set in the `.env` file of the dataset
|
|
142
|
+
directory. All of the variables are optional and have the defaults below, but
|
|
143
|
+
the file itself must exist to build the database.
|
|
144
|
+
|
|
145
|
+
| Variable | Default | |
|
|
146
|
+
| --- | --- | --- |
|
|
147
|
+
| `DB_USER` | the current user | |
|
|
148
|
+
| `DB_HOST` | `localhost` | |
|
|
149
|
+
| `DB_PORT` | `5432` | |
|
|
150
|
+
| `DB_NAME` | `postgres` | Created if it does not exist. |
|
|
151
|
+
| `DB_DIALECT` | `postgresql` | The DBMS of the database to build. |
|
|
152
|
+
| `DB_ISOLATION_LEVEL` | `READ COMMITTED` | |
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
DB_USER=analyst
|
|
156
|
+
DB_HOST=db.example.org
|
|
157
|
+
DB_NAME=study
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
There is no variable for the database password. The connection uses the
|
|
161
|
+
password mechanism of the DBMS, for example:
|
|
162
|
+
|
|
163
|
+
- PostgreSQL: a `~/.pgpass` file or the `PGPASSWORD` environment variable.
|
|
164
|
+
|
|
165
|
+
`DB_NAME` also names the read role the build creates (see
|
|
166
|
+
[Permissions](#permissions)).
|
|
167
|
+
|
|
168
|
+
### Data files and DVC
|
|
169
|
+
|
|
170
|
+
Each table reads the file in `data/` that has its name, ignoring the case and
|
|
171
|
+
the suffixes (`.csv`, `.csv.gz`, `.xlsx`). The table of `metadata/patient.json`
|
|
172
|
+
reads `data/patient.csv`.
|
|
173
|
+
|
|
174
|
+
If `data/` is a DVC repository (it has a `data/.dvc` directory), the files are
|
|
175
|
+
the ones DVC tracks. A build pulls the files of the tables it builds from the
|
|
176
|
+
DVC remote, and tells a changed file by its checksum. Otherwise `data/` is
|
|
177
|
+
read as a plain folder and nothing is pulled.
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
cd dataset/data
|
|
181
|
+
dvc init --subdir
|
|
182
|
+
dvc remote add -d storage ssh://user@example.org/path/to/storage
|
|
183
|
+
dvc add patient.csv
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The DVC remote of a pipeline that runs without a terminal needs credentials
|
|
187
|
+
that do not ask for a password. For an SSH remote, that is a private key in
|
|
188
|
+
`~/.ssh`, or one named with `dvc remote modify --local <remote> keyfile <path>`.
|
|
189
|
+
|
|
190
|
+
## Usage
|
|
191
|
+
|
|
192
|
+
### Generate the metadata of the tables
|
|
193
|
+
|
|
194
|
+
From sample data files, it reads the first 10,000 rows of each file in `data/`
|
|
195
|
+
and writes a JSON file for each table to `metadata/`:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
datakiln metadata dataset
|
|
199
|
+
datakiln metadata dataset --delimiter ";"
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
The columns are named from the headers, the types are guessed from the
|
|
203
|
+
data, and the primary key is a `meta_row_id` column. The schema is `public`.
|
|
204
|
+
Edit each file after: set the schema, the key, the real types and the
|
|
205
|
+
descriptions.
|
|
206
|
+
|
|
207
|
+
From a REDCap project, it downloads the data of each form to `data/` and, with
|
|
208
|
+
`--metadata`, writes its metadata:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
datakiln extract redcap dataset --api-url https://redcap.example.org/api/ \
|
|
212
|
+
--api-key "$REDCAP_TOKEN" --metadata --schema redcap \
|
|
213
|
+
--include demographics labs --exclude consent
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
From a source database, see [Extract from a database](#extract-from-a-database).
|
|
217
|
+
|
|
218
|
+
### Build the database
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
datakiln build dataset
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
This does the following:
|
|
225
|
+
|
|
226
|
+
1. Creates the database if it does not exist, and the schema that records what
|
|
227
|
+
was built (the checksums of the tables and scripts).
|
|
228
|
+
2. Loads the data of each table that is new or changed, or that references a
|
|
229
|
+
table that changed, into a staging schema (`_<schema>`), and runs the
|
|
230
|
+
`transform/` scripts on it.
|
|
231
|
+
3. Moves the rows that satisfy the constraints of each table to its final
|
|
232
|
+
table, which has its types, keys and indexes. The rows that do not (a
|
|
233
|
+
required value is missing, a foreign key has no match or a key is
|
|
234
|
+
duplicated) stay in the staging table, with the reason in its `meta_log`
|
|
235
|
+
column, and a warning gives their count.
|
|
236
|
+
4. Runs the `derive/` scripts that changed, each one after the scripts that
|
|
237
|
+
create what it uses.
|
|
238
|
+
5. Lists what was built before and is no longer in the repository, and grants
|
|
239
|
+
read access.
|
|
240
|
+
|
|
241
|
+
| Option | |
|
|
242
|
+
| --- | --- |
|
|
243
|
+
| `--include name...` | Build only these tables and scripts, and what they use. |
|
|
244
|
+
| `--rebuild-all` | Rebuild everything, even what is up to date. |
|
|
245
|
+
| `--prune` | Drop what was removed from the repository. Without it, the objects are only listed. |
|
|
246
|
+
| `--grant user...` | Give read access to these database users. |
|
|
247
|
+
| `--stdout` | Show the log in the terminal. It is always written to `build.log`. |
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
# Only one view, with the tables and scripts it needs
|
|
251
|
+
datakiln build dataset --include study_derived.labs
|
|
252
|
+
|
|
253
|
+
# Everything again, and remove what is gone from the repository
|
|
254
|
+
datakiln build dataset --rebuild-all --prune
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
A build stops with an error if what is in the repository needs an object that
|
|
258
|
+
was removed from it.
|
|
259
|
+
|
|
260
|
+
### Go back to an earlier version
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
git checkout <commit>
|
|
264
|
+
datakiln build dataset
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
The build compares the checksums of the files and metadata at that commit with
|
|
268
|
+
what is in the database, reloads the tables that differ, and reruns the
|
|
269
|
+
scripts built on them. Add `--prune` to drop the objects that did not exist
|
|
270
|
+
yet at that commit.
|
|
271
|
+
|
|
272
|
+
### Table metadata
|
|
273
|
+
|
|
274
|
+
A table is described by a JSON file in `metadata/`:
|
|
275
|
+
|
|
276
|
+
```json
|
|
277
|
+
{
|
|
278
|
+
"schema": "study",
|
|
279
|
+
"name": "visit",
|
|
280
|
+
"encoding": "utf-8",
|
|
281
|
+
"delimiter": ",",
|
|
282
|
+
"na_values": ["N/A", "unknown"],
|
|
283
|
+
"columns": [
|
|
284
|
+
{
|
|
285
|
+
"name": "Visit ID",
|
|
286
|
+
"rename": "visit_id",
|
|
287
|
+
"description": "Identifier of the visit.",
|
|
288
|
+
"dtype": {"postgresql": "INTEGER"},
|
|
289
|
+
"required": true
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
"name": "Patient ID",
|
|
293
|
+
"rename": "patient_id",
|
|
294
|
+
"dtype": {"postgresql": "INTEGER"},
|
|
295
|
+
"required": true
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
"name": "Visit date",
|
|
299
|
+
"rename": "visit_date",
|
|
300
|
+
"dtype": {"postgresql": "DATE"}
|
|
301
|
+
}
|
|
302
|
+
],
|
|
303
|
+
"constraints": {
|
|
304
|
+
"primary_key": ["visit_id"],
|
|
305
|
+
"uniques": [["patient_id", "visit_date"]],
|
|
306
|
+
"foreign_keys": [
|
|
307
|
+
{
|
|
308
|
+
"columns": ["patient_id"],
|
|
309
|
+
"reference": {
|
|
310
|
+
"schema": "study",
|
|
311
|
+
"table": "patient",
|
|
312
|
+
"columns": ["patient_id"]
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
]
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
- `name` is the header of the column in the data file, and `rename` is its
|
|
321
|
+
name in the database.
|
|
322
|
+
- `dtype` has the type for each dialect.
|
|
323
|
+
- A column with `"transform": true` is not in the data file. A `transform/`
|
|
324
|
+
script fills it.
|
|
325
|
+
- A foreign key makes the table depend on the one it references, which
|
|
326
|
+
therefore is built first.
|
|
327
|
+
- Tables can also have `indexes` and `unpivot` (to turn columns into rows).
|
|
328
|
+
The metadata generated by the commands above, and the code of
|
|
329
|
+
[`TableMeta`](datakiln/metadata/table.py), show their format.
|
|
330
|
+
|
|
331
|
+
### Transform scripts
|
|
332
|
+
|
|
333
|
+
The `.sql` files in `transform/` run, in the order of their names, after the
|
|
334
|
+
data is loaded into the staging tables and before it moves to the final ones.
|
|
335
|
+
They are the place to clean values or to fill the columns marked `transform`.
|
|
336
|
+
|
|
337
|
+
### Derive scripts
|
|
338
|
+
|
|
339
|
+
The `.sql` files in `derive/` (in any subfolder) create the objects built from
|
|
340
|
+
the tables: views, materialized views, tables, indexes, functions,
|
|
341
|
+
procedures, aggregates, types, sequences and schemas.
|
|
342
|
+
|
|
343
|
+
```sql
|
|
344
|
+
-- derive/labs.sql
|
|
345
|
+
CREATE MATERIALIZED VIEW study_derived.labs AS
|
|
346
|
+
SELECT v.patient_id, v.visit_date, l.test, l.value
|
|
347
|
+
FROM study.visit AS v
|
|
348
|
+
JOIN study.lab AS l USING (visit_id);
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
The tools read each script to find the objects it uses, so the scripts do not
|
|
352
|
+
need numbered names. The rules are:
|
|
353
|
+
|
|
354
|
+
- A script creates exactly one object, and nothing else.
|
|
355
|
+
- Tables and views are in a schema that is not `public`.
|
|
356
|
+
- Functions, procedures and aggregates are created and called without a
|
|
357
|
+
schema, so they live in the default schema of the target database. They are
|
|
358
|
+
told apart by the types of their parameters, so overloads work.
|
|
359
|
+
- A script that creates a schema (`CREATE SCHEMA study_derived`) is required
|
|
360
|
+
by every script that creates something in it.
|
|
361
|
+
- A query given as text is read too, as the ones a `crosstab` takes, and so
|
|
362
|
+
are the strings in the body of a function. They follow the same rules as the
|
|
363
|
+
code around them.
|
|
364
|
+
- A script whose SQL cannot be parsed, so its dependencies cannot be worked
|
|
365
|
+
out, is an error that names the file.
|
|
366
|
+
|
|
367
|
+
### Dependency graph
|
|
368
|
+
|
|
369
|
+
```bash
|
|
370
|
+
datakiln graph dataset # Mermaid text
|
|
371
|
+
datakiln graph dataset --output graph.md # Markdown with a Mermaid diagram
|
|
372
|
+
datakiln graph dataset --output graph.html # A page that any browser draws
|
|
373
|
+
datakiln graph dataset --direction TB --include study_derived.labs
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
It draws the tables and the scripts as a diagram, with an arrow from what is
|
|
377
|
+
needed to what needs it. The extension of the output file tells the format,
|
|
378
|
+
and `--include` limits the graph to some objects and what they need.
|
|
379
|
+
|
|
380
|
+
### Extract from a database
|
|
381
|
+
|
|
382
|
+
`datakiln extract dbms` downloads data from a source database to `data/`, and can write the table metadata from the source's columns. Each
|
|
383
|
+
table is described by a JSON file in `extract/`:
|
|
384
|
+
|
|
385
|
+
```json
|
|
386
|
+
{
|
|
387
|
+
"schema": "source",
|
|
388
|
+
"table": "visit",
|
|
389
|
+
"filters": [
|
|
390
|
+
{
|
|
391
|
+
"columns": ["patient_id"],
|
|
392
|
+
"reference": {
|
|
393
|
+
"schema": "study",
|
|
394
|
+
"table": "cohort",
|
|
395
|
+
"columns": ["patient_id"]
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
]
|
|
399
|
+
}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
A filter limits the rows to the ones that match a reference table, which is
|
|
403
|
+
the cohort or another extracted table. Chains of `join`, comparison
|
|
404
|
+
`operators`, cohorts given by a `.sql` file and the `chunk_size` of the
|
|
405
|
+
download are also available. The format is documented in
|
|
406
|
+
[`datakiln/extraction/models.py`](datakiln/extraction/models.py).
|
|
407
|
+
|
|
408
|
+
```bash
|
|
409
|
+
datakiln extract dbms dataset --dialect oracle --host source.example.org --port 1521 \
|
|
410
|
+
--database ORCL --user analyst --metadata
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
| Option | |
|
|
414
|
+
| --- | --- |
|
|
415
|
+
| `--dialect`, `--host`, `--port`, `--database` | The source DBMS, all required. |
|
|
416
|
+
| `--user`, `--password` | The default user is the current one. |
|
|
417
|
+
| `--metadata` | Also write the JSON metadata of the tables to `metadata/`. |
|
|
418
|
+
| `--no-data` | Only write the metadata, without downloading data. |
|
|
419
|
+
| `--include table...` | Extract only these tables, and what they need. |
|
|
420
|
+
| `--read-only` | Stream the data, without creating temporary tables in the source. |
|
|
421
|
+
| `--parallel N` | Number of extractions at once. Default is 4. |
|
|
422
|
+
|
|
423
|
+
The metadata is written in the dialect of the target database, set in the
|
|
424
|
+
`.env` file of the dataset. `extract redcap` takes the same `--data` and
|
|
425
|
+
`--metadata` options, and `--include` and `--exclude` for forms.
|
|
426
|
+
|
|
427
|
+
### Permissions
|
|
428
|
+
|
|
429
|
+
The build creates a database role named after the database (`DB_NAME`). It
|
|
430
|
+
gets read access to the schemas of the tables and scripts, which leaves out
|
|
431
|
+
the staging schemas and the metadata. `--grant` gives that role to users:
|
|
432
|
+
|
|
433
|
+
```bash
|
|
434
|
+
datakiln build dataset --grant alice bob
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### Logs
|
|
438
|
+
|
|
439
|
+
`build`, `metadata` and `extract` write a log to the directory they run in:
|
|
440
|
+
`build.log`, `metadata.log` or `extract.log` (for both sources). The log is
|
|
441
|
+
overwritten on each run, and `--stdout` also prints it. `graph` writes no log.
|
|
442
|
+
|
|
443
|
+
## Development
|
|
444
|
+
|
|
445
|
+
The unit tests need a database server that the user in `.env` can use, for
|
|
446
|
+
example:
|
|
447
|
+
|
|
448
|
+
```bash
|
|
449
|
+
DB_USER=test
|
|
450
|
+
DB_HOST=localhost
|
|
451
|
+
DB_NAME=test
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
```bash
|
|
455
|
+
pip install -e .
|
|
456
|
+
pytest tests
|
|
457
|
+
flake8 datakiln tests
|
|
458
|
+
pylint datakiln
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
The tests of the script reader (`tests/unit/test_script.py`) do not need the
|
|
462
|
+
database.
|
|
463
|
+
|
|
464
|
+
A dataset repository can test its own metadata and scripts by running the
|
|
465
|
+
tests in `datakiln/test.py` from its directory. They create a `__test`
|
|
466
|
+
database, load the tables, run the transform and `derive/` scripts, and roll
|
|
467
|
+
everything back.
|
|
468
|
+
|
|
469
|
+
```bash
|
|
470
|
+
pytest --pyargs datakiln.test
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
## License
|
|
474
|
+
|
|
475
|
+
See [LICENSE](LICENSE).
|