datakiln 0.3.0__py3-none-any.whl
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/__init__.py +1 -0
- datakiln/__main__.py +7 -0
- datakiln/arguments.py +76 -0
- datakiln/build_database.py +110 -0
- datakiln/build_dbms.py +136 -0
- datakiln/build_metadata.py +165 -0
- datakiln/build_redcap.py +401 -0
- datakiln/cli.py +70 -0
- datakiln/extraction/__init__.py +3 -0
- datakiln/extraction/exporters.py +213 -0
- datakiln/extraction/models.py +712 -0
- datakiln/extraction/renderers.py +131 -0
- datakiln/extraction/strategies.py +916 -0
- datakiln/io.py +57 -0
- datakiln/metadata/__init__.py +0 -0
- datakiln/metadata/column.py +120 -0
- datakiln/metadata/database.py +571 -0
- datakiln/metadata/dialect.py +23 -0
- datakiln/metadata/files.py +119 -0
- datakiln/metadata/foreign_key.py +151 -0
- datakiln/metadata/graph.py +237 -0
- datakiln/metadata/location.py +223 -0
- datakiln/metadata/migrations/__init__.py +52 -0
- datakiln/metadata/migrations/env.py +17 -0
- datakiln/metadata/migrations/versions/__init__.py +0 -0
- datakiln/metadata/migrations/versions/v0001_baseline.py +47 -0
- datakiln/metadata/migrations/versions/v0002_objects.py +158 -0
- datakiln/metadata/script.py +356 -0
- datakiln/metadata/table.py +894 -0
- datakiln/metadata/unpivot.py +179 -0
- datakiln/show_graph.py +265 -0
- datakiln/test.py +88 -0
- datakiln-0.3.0.dist-info/METADATA +475 -0
- datakiln-0.3.0.dist-info/RECORD +37 -0
- datakiln-0.3.0.dist-info/WHEEL +4 -0
- datakiln-0.3.0.dist-info/entry_points.txt +2 -0
- datakiln-0.3.0.dist-info/licenses/LICENSE +21 -0
datakiln/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Build a database from a repository of files."""
|
datakiln/__main__.py
ADDED
datakiln/arguments.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Options that the commands share, and the log they write.
|
|
3
|
+
"""
|
|
4
|
+
import logging
|
|
5
|
+
import sys
|
|
6
|
+
from argparse import ArgumentParser, BooleanOptionalAction
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def add_directory(parser: ArgumentParser):
|
|
10
|
+
"""Adds the directory of the dataset, which every command takes.
|
|
11
|
+
|
|
12
|
+
:param parser: Parser of a command.
|
|
13
|
+
:type parser: argparse.ArgumentParser
|
|
14
|
+
"""
|
|
15
|
+
parser.add_argument("directory", metavar="directory", type=str,
|
|
16
|
+
help="Path of the directory of the dataset.")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def add_log(parser: ArgumentParser):
|
|
20
|
+
"""Adds the option to show the log, which commands write to a file.
|
|
21
|
+
|
|
22
|
+
:param parser: Parser of a command.
|
|
23
|
+
:type parser: argparse.ArgumentParser
|
|
24
|
+
"""
|
|
25
|
+
parser.add_argument("--stdout", action="store_true",
|
|
26
|
+
help="Output logs to standard output as well as to "
|
|
27
|
+
"the log file.")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def add_include(parser: ArgumentParser, what: str):
|
|
31
|
+
"""Adds the option that limits a command to some of the things it works
|
|
32
|
+
on.
|
|
33
|
+
|
|
34
|
+
:param parser: Parser of a command.
|
|
35
|
+
:type parser: argparse.ArgumentParser
|
|
36
|
+
|
|
37
|
+
:param what: What the command does with the names, as a sentence.
|
|
38
|
+
:type what: str
|
|
39
|
+
"""
|
|
40
|
+
parser.add_argument("--include", metavar="name", type=str, nargs="+",
|
|
41
|
+
help=what)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def add_export(parser: ArgumentParser):
|
|
45
|
+
"""Adds the options that tell what a command that reads from a source
|
|
46
|
+
writes to the dataset.
|
|
47
|
+
|
|
48
|
+
:param parser: Parser of a command.
|
|
49
|
+
:type parser: argparse.ArgumentParser
|
|
50
|
+
"""
|
|
51
|
+
parser.add_argument("--data", action=BooleanOptionalAction, default=True,
|
|
52
|
+
help="Whether to write the data files.")
|
|
53
|
+
parser.add_argument("--metadata", action=BooleanOptionalAction,
|
|
54
|
+
default=False,
|
|
55
|
+
help="Whether to write the metadata files of the "
|
|
56
|
+
"tables.")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def setup_logging(stdout: bool, log_file: str):
|
|
60
|
+
"""Sets up logging for a command.
|
|
61
|
+
|
|
62
|
+
:param stdout: Whether to output logs to standard output.
|
|
63
|
+
:type stdout: bool
|
|
64
|
+
|
|
65
|
+
:param log_file: Path to the log file.
|
|
66
|
+
:type log_file: str
|
|
67
|
+
"""
|
|
68
|
+
handlers = [logging.FileHandler(log_file, mode="w")]
|
|
69
|
+
if stdout:
|
|
70
|
+
handlers.append(logging.StreamHandler(sys.stdout))
|
|
71
|
+
|
|
72
|
+
logging.basicConfig(
|
|
73
|
+
level=logging.INFO, datefmt="%Y-%m-%d %H:%M:%S",
|
|
74
|
+
format="%(asctime)s %(name)-12s %(levelname)-8s %(message)s",
|
|
75
|
+
handlers=handlers
|
|
76
|
+
)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Creates table and loads data.
|
|
3
|
+
"""
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from datakiln.arguments import add_directory, add_include, add_log
|
|
8
|
+
from datakiln.metadata.database import DatabaseMeta, Session
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def add_arguments(parser):
|
|
15
|
+
"""Adds the options of the command.
|
|
16
|
+
|
|
17
|
+
:param parser: Parser of the command.
|
|
18
|
+
:type parser: argparse.ArgumentParser
|
|
19
|
+
"""
|
|
20
|
+
add_directory(parser)
|
|
21
|
+
add_log(parser)
|
|
22
|
+
add_include(parser, "Build only these tables and scripts, and "
|
|
23
|
+
"what they need.")
|
|
24
|
+
parser.add_argument("--grant", metavar="grant", type=str, nargs='+',
|
|
25
|
+
help="Grant read permissions to specified users.")
|
|
26
|
+
parser.add_argument("--rebuild-all", action="store_true",
|
|
27
|
+
help="Rebuild all tables and scripts, even if "
|
|
28
|
+
"up to date.")
|
|
29
|
+
parser.add_argument("--prune", action="store_true",
|
|
30
|
+
help="Drop the objects removed from the repo.")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def run(args):
|
|
34
|
+
"""Creates the tables, loads the data and runs the scripts.
|
|
35
|
+
|
|
36
|
+
:param args: Arguments of the command.
|
|
37
|
+
:type args: argparse.Namespace
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
# Get absolute of specified path
|
|
41
|
+
directory = Path(args.directory).absolute()
|
|
42
|
+
|
|
43
|
+
# Get database parameters
|
|
44
|
+
db_params = DatabaseMeta.load_env(directory)
|
|
45
|
+
|
|
46
|
+
# Tables and scripts to build, with what they use
|
|
47
|
+
database = DatabaseMeta(directory, db_params["database"])
|
|
48
|
+
include = None
|
|
49
|
+
if args.include:
|
|
50
|
+
selected = database.graph.find(args.include)
|
|
51
|
+
include = database.graph.requirements(selected)
|
|
52
|
+
|
|
53
|
+
# Connect to database
|
|
54
|
+
session = Session.create(**db_params)
|
|
55
|
+
|
|
56
|
+
# Create metadata
|
|
57
|
+
database.create_metadata(session)
|
|
58
|
+
database.load_stale(session)
|
|
59
|
+
|
|
60
|
+
# Create role for database read access
|
|
61
|
+
database.create_role(session)
|
|
62
|
+
|
|
63
|
+
# Stage and flush to database
|
|
64
|
+
updated = []
|
|
65
|
+
for table in database.graph.tables:
|
|
66
|
+
if include is not None and table not in include:
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
# Skip the tables that did not change, unless all are rebuilt. A table
|
|
70
|
+
# is also rebuilt if a table it references is, since that drops its
|
|
71
|
+
# foreign keys
|
|
72
|
+
if (
|
|
73
|
+
not args.rebuild_all
|
|
74
|
+
and not table.needs_update(session)
|
|
75
|
+
and database.graph.dependencies(table).isdisjoint(updated)
|
|
76
|
+
):
|
|
77
|
+
logger.info("Table '%s'.'%s': Already up to date.",
|
|
78
|
+
table.schema, table.name)
|
|
79
|
+
continue
|
|
80
|
+
table.pull()
|
|
81
|
+
table.create(session)
|
|
82
|
+
table.stage_meta.load(session)
|
|
83
|
+
updated.append(table)
|
|
84
|
+
|
|
85
|
+
# Run transforms
|
|
86
|
+
database.execute_directory("transform", session, max_statements=0)
|
|
87
|
+
|
|
88
|
+
# Load to final schema and flush to database
|
|
89
|
+
for table in updated:
|
|
90
|
+
table.resolve_columns(session)
|
|
91
|
+
table.create_uniques(session)
|
|
92
|
+
table.load(session)
|
|
93
|
+
table.enforce_fks(session)
|
|
94
|
+
table.create_indexes(session)
|
|
95
|
+
table.create_metadata(session)
|
|
96
|
+
database.forget_dependents(session, [table])
|
|
97
|
+
session.commit()
|
|
98
|
+
|
|
99
|
+
# Run views
|
|
100
|
+
session.close()
|
|
101
|
+
database.execute_derive(
|
|
102
|
+
session, force=args.rebuild_all, include=include)
|
|
103
|
+
|
|
104
|
+
# Objects whose metadata or script was removed
|
|
105
|
+
database.prune(session, drop=args.prune)
|
|
106
|
+
|
|
107
|
+
# Grant read permissions on entire database
|
|
108
|
+
database.grant(session, args.grant)
|
|
109
|
+
session.commit()
|
|
110
|
+
session.close()
|
datakiln/build_dbms.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Builds table metadata from DBMS.
|
|
3
|
+
"""
|
|
4
|
+
import logging
|
|
5
|
+
from getpass import getuser
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from datakiln.metadata.database import DatabaseMeta, DRIVER
|
|
9
|
+
from datakiln.arguments import (
|
|
10
|
+
add_directory, add_export, add_include, add_log
|
|
11
|
+
)
|
|
12
|
+
from datakiln.extraction.models import ExtractMeta, ExtractGraph
|
|
13
|
+
from datakiln.extraction.strategies import (
|
|
14
|
+
StreamStrategy, TempTableStrategy, ParallelExecutor
|
|
15
|
+
)
|
|
16
|
+
from datakiln.extraction.exporters import MetadataExtractor
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def add_arguments(parser):
|
|
23
|
+
"""Adds the options of the command.
|
|
24
|
+
|
|
25
|
+
:param parser: Parser of the command.
|
|
26
|
+
:type parser: argparse.ArgumentParser
|
|
27
|
+
"""
|
|
28
|
+
add_directory(parser)
|
|
29
|
+
add_log(parser)
|
|
30
|
+
add_include(parser, "Extract only these tables, and the ones they "
|
|
31
|
+
"need.")
|
|
32
|
+
add_export(parser)
|
|
33
|
+
parser.add_argument('--dialect', metavar="dialect", type=str,
|
|
34
|
+
required=True, choices=DRIVER.keys(),
|
|
35
|
+
help='Dialect of the source DBMS.')
|
|
36
|
+
parser.add_argument('--host', metavar="host", type=str,
|
|
37
|
+
required=True, help='Hostname of the source DBMS.')
|
|
38
|
+
parser.add_argument('--port', metavar="port", type=str,
|
|
39
|
+
required=True,
|
|
40
|
+
help='Port to use when connecting to the source '
|
|
41
|
+
'DBMS.')
|
|
42
|
+
parser.add_argument('--user', metavar="user", type=str,
|
|
43
|
+
required=False, default=getuser(),
|
|
44
|
+
help='Username to use when connecting to the source '
|
|
45
|
+
'DBMS.')
|
|
46
|
+
parser.add_argument('--password', metavar="password", type=str,
|
|
47
|
+
required=False,
|
|
48
|
+
help='Password to use when connecting to the source '
|
|
49
|
+
'DBMS.')
|
|
50
|
+
parser.add_argument('--database', metavar="database", type=str,
|
|
51
|
+
required=True,
|
|
52
|
+
help='Database to connect to in the source DBMS.')
|
|
53
|
+
parser.add_argument('--read-only', action='store_true', default=False,
|
|
54
|
+
help='Run in read-only mode (no temp tables).')
|
|
55
|
+
parser.add_argument('--parallel', type=int, default=4,
|
|
56
|
+
help='Number of parallel extraction jobs.')
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def run(args):
|
|
60
|
+
# pylint: disable=too-many-locals, too-many-branches, too-many-statements
|
|
61
|
+
"""Downloads data and creates sample metadata for tables.
|
|
62
|
+
|
|
63
|
+
:param args: Arguments of the command.
|
|
64
|
+
:type args: argparse.Namespace
|
|
65
|
+
"""
|
|
66
|
+
directory = Path(args.directory).absolute()
|
|
67
|
+
|
|
68
|
+
# Create output directory
|
|
69
|
+
meta_dir = Path(directory) / "metadata"
|
|
70
|
+
meta_dir.mkdir(exist_ok=True)
|
|
71
|
+
|
|
72
|
+
# Connect parameters
|
|
73
|
+
db_params = {
|
|
74
|
+
"dialect": args.dialect,
|
|
75
|
+
"user": args.user,
|
|
76
|
+
"password": args.password,
|
|
77
|
+
"host": args.host,
|
|
78
|
+
"database": args.database,
|
|
79
|
+
"port": args.port
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# Load all extraction metadata
|
|
83
|
+
tasks = []
|
|
84
|
+
for file in sorted(directory.glob("extract/*.json")):
|
|
85
|
+
tasks.append(ExtractMeta.from_json(file))
|
|
86
|
+
|
|
87
|
+
# Build dependency graph of the tables to export, and what they need
|
|
88
|
+
graph = ExtractGraph.select(tasks, args.include)
|
|
89
|
+
|
|
90
|
+
# Initialize execution strategy
|
|
91
|
+
if args.read_only:
|
|
92
|
+
strategy = StreamStrategy(db_params, directory)
|
|
93
|
+
else:
|
|
94
|
+
strategy = TempTableStrategy(db_params, directory)
|
|
95
|
+
|
|
96
|
+
# Track exported tables for metadata generation
|
|
97
|
+
exported_schemas = {}
|
|
98
|
+
for task in graph.sort():
|
|
99
|
+
if task in graph.export:
|
|
100
|
+
if task.schema not in exported_schemas:
|
|
101
|
+
exported_schemas[task.schema] = []
|
|
102
|
+
exported_schemas[task.schema].append(task.table)
|
|
103
|
+
|
|
104
|
+
if args.data:
|
|
105
|
+
# Create data directory if not exists
|
|
106
|
+
data_dir = directory / "data"
|
|
107
|
+
data_dir.mkdir(exist_ok=True)
|
|
108
|
+
|
|
109
|
+
# Initialize and run parallel executor
|
|
110
|
+
executor = ParallelExecutor(strategy, graph, max_workers=args.parallel)
|
|
111
|
+
try:
|
|
112
|
+
executor.execute(data_dir=data_dir)
|
|
113
|
+
finally:
|
|
114
|
+
strategy.cleanup()
|
|
115
|
+
|
|
116
|
+
# Skip metadata generation
|
|
117
|
+
if not args.metadata:
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
# Get target database params
|
|
121
|
+
db_params = DatabaseMeta.load_env(directory)
|
|
122
|
+
|
|
123
|
+
# Generate table metadata
|
|
124
|
+
with strategy.session() as session:
|
|
125
|
+
for schema, tables in exported_schemas.items():
|
|
126
|
+
# Retrieve metadata from DBMS
|
|
127
|
+
metadata = MetadataExtractor.get_metadata(session, schema, tables)
|
|
128
|
+
|
|
129
|
+
for _, table in metadata.tables.items():
|
|
130
|
+
# Generate table metadata
|
|
131
|
+
logger.info("Table '%s': Generating metadata.", table)
|
|
132
|
+
MetadataExtractor.generate_table_meta(
|
|
133
|
+
meta_dir,
|
|
134
|
+
table,
|
|
135
|
+
db_params["dialect"]
|
|
136
|
+
)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Generate metadata from data file.
|
|
3
|
+
"""
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from itertools import starmap
|
|
8
|
+
from functools import partial
|
|
9
|
+
|
|
10
|
+
import pandas as pd
|
|
11
|
+
from slugify import slugify
|
|
12
|
+
|
|
13
|
+
from datakiln.arguments import add_directory, add_log
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
SUPPORTED_EXTENSIONS = {
|
|
19
|
+
".csv.gz": partial(pd.read_csv, engine="python", nrows=10000),
|
|
20
|
+
".csv": partial(pd.read_csv, engine="python", nrows=10000),
|
|
21
|
+
".xlsx": partial(pd.read_excel, sheet_name=None, nrows=10000)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def dtype_to_psql(dtype):
|
|
26
|
+
"""Define PostgreSQL type from pandas inferred type.
|
|
27
|
+
|
|
28
|
+
:param dtype: Type inferred by pandas.
|
|
29
|
+
:type dtype: type
|
|
30
|
+
|
|
31
|
+
:return: PostgreSQL data type.
|
|
32
|
+
:rtype: str
|
|
33
|
+
"""
|
|
34
|
+
if dtype == object:
|
|
35
|
+
return "TEXT"
|
|
36
|
+
if dtype == int:
|
|
37
|
+
return "INTEGER"
|
|
38
|
+
if dtype == float:
|
|
39
|
+
return "DOUBLE PRECISION"
|
|
40
|
+
if dtype == bool:
|
|
41
|
+
return "BOOLEAN"
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def dtype_to_meta(column, dtype):
|
|
46
|
+
"""Generates metadata for column from tuple.
|
|
47
|
+
|
|
48
|
+
:param column: Column name as it is in data file.
|
|
49
|
+
:type column: str
|
|
50
|
+
|
|
51
|
+
:param dtype: Type inferred by pandas.
|
|
52
|
+
:type dtype: type
|
|
53
|
+
|
|
54
|
+
:return: Column metadata.
|
|
55
|
+
:rtype: dict
|
|
56
|
+
"""
|
|
57
|
+
return {
|
|
58
|
+
"name": column,
|
|
59
|
+
"rename": slugify(column, separator="_"),
|
|
60
|
+
"dtype": {
|
|
61
|
+
"postgresql": dtype_to_psql(dtype)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def generate_table_meta(meta_dir, table_name, data):
|
|
67
|
+
"""Generates metadata for column from tuple.
|
|
68
|
+
|
|
69
|
+
:param meta_dir: Medatada directory.
|
|
70
|
+
:type meta_dir: Path
|
|
71
|
+
|
|
72
|
+
:param table_name: Name of the table.
|
|
73
|
+
:type table_name: str
|
|
74
|
+
|
|
75
|
+
:param data: DataFrame containing sample data for the table.
|
|
76
|
+
:type data: pandas.DataFrame
|
|
77
|
+
|
|
78
|
+
:return: None
|
|
79
|
+
"""
|
|
80
|
+
# Ensure table name is database friendly
|
|
81
|
+
table_name = slugify(table_name, separator="_")
|
|
82
|
+
|
|
83
|
+
# Remove trailing white spaces from data
|
|
84
|
+
data = data.map(lambda x: x.strip() if isinstance(x, str) else x)
|
|
85
|
+
|
|
86
|
+
# Generate metadata
|
|
87
|
+
meta = {
|
|
88
|
+
"name": table_name,
|
|
89
|
+
"schema": "public",
|
|
90
|
+
"encoding": "utf-8",
|
|
91
|
+
"columns": list(starmap(dtype_to_meta, data.dtypes.items())),
|
|
92
|
+
"constraints": {
|
|
93
|
+
"primary_key": ["meta_row_id"]
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# Write metadata to file
|
|
98
|
+
meta_path = meta_dir / f"{table_name}.json"
|
|
99
|
+
with open(meta_path, "w", encoding="utf-8") as file:
|
|
100
|
+
json.dump(meta, file, indent=4)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def add_arguments(parser):
|
|
104
|
+
"""Adds the options of the command.
|
|
105
|
+
|
|
106
|
+
:param parser: Parser of the command.
|
|
107
|
+
:type parser: argparse.ArgumentParser
|
|
108
|
+
"""
|
|
109
|
+
add_directory(parser)
|
|
110
|
+
add_log(parser)
|
|
111
|
+
parser.add_argument('--delimiter', metavar="delimiter", type=str,
|
|
112
|
+
required=False,
|
|
113
|
+
help='Character to treat as delimiter.')
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def run(args):
|
|
117
|
+
"""Generates metadata from the data files.
|
|
118
|
+
|
|
119
|
+
:param args: Arguments of the command.
|
|
120
|
+
:type args: argparse.Namespace
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
# Get absolute path for specified directory
|
|
124
|
+
directory = Path(args.directory).absolute()
|
|
125
|
+
|
|
126
|
+
# Ensure metadata directory exists
|
|
127
|
+
meta_dir = directory / "metadata"
|
|
128
|
+
meta_dir.mkdir(exist_ok=True)
|
|
129
|
+
|
|
130
|
+
# Generate metadata for each data file
|
|
131
|
+
for data_path in directory.glob("data/*.*"):
|
|
132
|
+
# Skip if the extension is not supported
|
|
133
|
+
extension = "".join(data_path.suffixes).lower()
|
|
134
|
+
if extension not in SUPPORTED_EXTENSIONS:
|
|
135
|
+
continue
|
|
136
|
+
logger.info("File '%s': Generating metadata.", data_path)
|
|
137
|
+
|
|
138
|
+
# Read file
|
|
139
|
+
params = {}
|
|
140
|
+
reader = SUPPORTED_EXTENSIONS[extension]
|
|
141
|
+
if args.delimiter and reader.func == pd.read_csv:
|
|
142
|
+
params["delimiter"] = args.delimiter
|
|
143
|
+
data = reader(data_path.absolute(), **params)
|
|
144
|
+
|
|
145
|
+
# Excel files can have more than one sheet
|
|
146
|
+
# so we handle the data as a dictionary
|
|
147
|
+
table_name = data_path.name[:-len(extension)]
|
|
148
|
+
if isinstance(data, pd.DataFrame):
|
|
149
|
+
data = {table_name: data}
|
|
150
|
+
# Only consider tables with header row
|
|
151
|
+
# when multiple tables are returned
|
|
152
|
+
elif isinstance(data, dict):
|
|
153
|
+
data = {
|
|
154
|
+
name: df
|
|
155
|
+
for name, df in data.items()
|
|
156
|
+
if not df.columns.str.startswith("Unnamed:").all()
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
# If only one table is left, use file name
|
|
160
|
+
if len(data) == 1:
|
|
161
|
+
data = {data_path.stem: list(data.values())[0]}
|
|
162
|
+
|
|
163
|
+
# Generate metadata
|
|
164
|
+
for table_name, df in data.items():
|
|
165
|
+
generate_table_meta(meta_dir, table_name, df)
|