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.
Files changed (45) hide show
  1. datakiln-0.3.0/.gitignore +170 -0
  2. datakiln-0.3.0/LICENSE +21 -0
  3. datakiln-0.3.0/PKG-INFO +475 -0
  4. datakiln-0.3.0/README.md +438 -0
  5. datakiln-0.3.0/datakiln/__init__.py +1 -0
  6. datakiln-0.3.0/datakiln/__main__.py +7 -0
  7. datakiln-0.3.0/datakiln/arguments.py +76 -0
  8. datakiln-0.3.0/datakiln/build_database.py +110 -0
  9. datakiln-0.3.0/datakiln/build_dbms.py +136 -0
  10. datakiln-0.3.0/datakiln/build_metadata.py +165 -0
  11. datakiln-0.3.0/datakiln/build_redcap.py +401 -0
  12. datakiln-0.3.0/datakiln/cli.py +70 -0
  13. datakiln-0.3.0/datakiln/extraction/__init__.py +3 -0
  14. datakiln-0.3.0/datakiln/extraction/exporters.py +213 -0
  15. datakiln-0.3.0/datakiln/extraction/models.py +712 -0
  16. datakiln-0.3.0/datakiln/extraction/renderers.py +131 -0
  17. datakiln-0.3.0/datakiln/extraction/strategies.py +916 -0
  18. datakiln-0.3.0/datakiln/io.py +57 -0
  19. datakiln-0.3.0/datakiln/metadata/__init__.py +0 -0
  20. datakiln-0.3.0/datakiln/metadata/column.py +120 -0
  21. datakiln-0.3.0/datakiln/metadata/database.py +571 -0
  22. datakiln-0.3.0/datakiln/metadata/dialect.py +23 -0
  23. datakiln-0.3.0/datakiln/metadata/files.py +119 -0
  24. datakiln-0.3.0/datakiln/metadata/foreign_key.py +151 -0
  25. datakiln-0.3.0/datakiln/metadata/graph.py +237 -0
  26. datakiln-0.3.0/datakiln/metadata/location.py +223 -0
  27. datakiln-0.3.0/datakiln/metadata/migrations/__init__.py +52 -0
  28. datakiln-0.3.0/datakiln/metadata/migrations/env.py +17 -0
  29. datakiln-0.3.0/datakiln/metadata/migrations/versions/__init__.py +0 -0
  30. datakiln-0.3.0/datakiln/metadata/migrations/versions/v0001_baseline.py +47 -0
  31. datakiln-0.3.0/datakiln/metadata/migrations/versions/v0002_objects.py +158 -0
  32. datakiln-0.3.0/datakiln/metadata/script.py +356 -0
  33. datakiln-0.3.0/datakiln/metadata/table.py +894 -0
  34. datakiln-0.3.0/datakiln/metadata/unpivot.py +179 -0
  35. datakiln-0.3.0/datakiln/show_graph.py +265 -0
  36. datakiln-0.3.0/datakiln/test.py +88 -0
  37. datakiln-0.3.0/docs/conf.py +14 -0
  38. datakiln-0.3.0/docs/index.rst +13 -0
  39. datakiln-0.3.0/pyproject.toml +63 -0
  40. datakiln-0.3.0/tests/__init__.py +0 -0
  41. datakiln-0.3.0/tests/data/foreign_key.csv +3 -0
  42. datakiln-0.3.0/tests/data/test.csv +3 -0
  43. datakiln-0.3.0/tests/metadata/foreign_key.json +45 -0
  44. datakiln-0.3.0/tests/metadata/test.json +28 -0
  45. 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.
@@ -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
+ [![Lint](https://github.com/lbulgarelli/datakiln/actions/workflows/lint.yml/badge.svg)](https://github.com/lbulgarelli/datakiln/actions/workflows/lint.yml)
39
+ [![Test](https://github.com/lbulgarelli/datakiln/actions/workflows/test.yml/badge.svg)](https://github.com/lbulgarelli/datakiln/actions/workflows/test.yml)
40
+ [![Coverage](https://img.shields.io/endpoint?url=https%3A%2F%2Flbulgarelli.github.io%2Fdatakiln%2Fcoverage.json)](https://lbulgarelli.github.io/datakiln/coverage/)
41
+ [![Docs](https://img.shields.io/badge/docs-latest-blue)](https://lbulgarelli.github.io/datakiln/)
42
+ [![Python 3.12](https://img.shields.io/badge/python-3.12-blue)](https://www.python.org/)
43
+ [![License: MIT](https://img.shields.io/github/license/lbulgarelli/datakiln)](LICENSE)
44
+ [![Release](https://img.shields.io/github/v/release/lbulgarelli/datakiln)](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).