lsst-felis 24.1.6rc1__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.
Potentially problematic release.
This version of lsst-felis might be problematic. Click here for more details.
- felis/__init__.py +22 -0
- felis/cli.py +497 -0
- felis/datamodel.py +1116 -0
- felis/db/__init__.py +0 -0
- felis/db/dialects.py +116 -0
- felis/db/sqltypes.py +436 -0
- felis/db/utils.py +409 -0
- felis/db/variants.py +159 -0
- felis/metadata.py +383 -0
- felis/py.typed +0 -0
- felis/schemas/tap_schema_std.yaml +273 -0
- felis/tap.py +597 -0
- felis/tap_schema.py +644 -0
- felis/tests/__init__.py +0 -0
- felis/tests/postgresql.py +134 -0
- felis/tests/utils.py +122 -0
- felis/types.py +185 -0
- felis/version.py +2 -0
- lsst_felis-24.1.6rc1.dist-info/COPYRIGHT +1 -0
- lsst_felis-24.1.6rc1.dist-info/LICENSE +674 -0
- lsst_felis-24.1.6rc1.dist-info/METADATA +34 -0
- lsst_felis-24.1.6rc1.dist-info/RECORD +26 -0
- lsst_felis-24.1.6rc1.dist-info/WHEEL +5 -0
- lsst_felis-24.1.6rc1.dist-info/entry_points.txt +2 -0
- lsst_felis-24.1.6rc1.dist-info/top_level.txt +1 -0
- lsst_felis-24.1.6rc1.dist-info/zip-safe +1 -0
felis/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# This file is part of felis.
|
|
2
|
+
#
|
|
3
|
+
# Developed for the LSST Data Management System.
|
|
4
|
+
# This product includes software developed by the LSST Project
|
|
5
|
+
# (https://www.lsst.org).
|
|
6
|
+
# See the COPYRIGHT file at the top-level directory of this distribution
|
|
7
|
+
# for details of code ownership.
|
|
8
|
+
#
|
|
9
|
+
# This program is free software: you can redistribute it and/or modify
|
|
10
|
+
# it under the terms of the GNU General Public License as published by
|
|
11
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
12
|
+
# (at your option) any later version.
|
|
13
|
+
#
|
|
14
|
+
# This program is distributed in the hope that it will be useful,
|
|
15
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
16
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
17
|
+
# GNU General Public License for more details.
|
|
18
|
+
#
|
|
19
|
+
# You should have received a copy of the GNU General Public License
|
|
20
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
21
|
+
|
|
22
|
+
from .version import *
|
felis/cli.py
ADDED
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
"""Click command line interface."""
|
|
2
|
+
|
|
3
|
+
# This file is part of felis.
|
|
4
|
+
#
|
|
5
|
+
# Developed for the LSST Data Management System.
|
|
6
|
+
# This product includes software developed by the LSST Project
|
|
7
|
+
# (https://www.lsst.org).
|
|
8
|
+
# See the COPYRIGHT file at the top-level directory of this distribution
|
|
9
|
+
# for details of code ownership.
|
|
10
|
+
#
|
|
11
|
+
# This program is free software: you can redistribute it and/or modify
|
|
12
|
+
# it under the terms of the GNU General Public License as published by
|
|
13
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
14
|
+
# (at your option) any later version.
|
|
15
|
+
#
|
|
16
|
+
# This program is distributed in the hope that it will be useful,
|
|
17
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
18
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
19
|
+
# GNU General Public License for more details.
|
|
20
|
+
#
|
|
21
|
+
# You should have received a copy of the GNU General Public License
|
|
22
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import logging
|
|
27
|
+
from collections.abc import Iterable
|
|
28
|
+
from typing import IO
|
|
29
|
+
|
|
30
|
+
import click
|
|
31
|
+
from pydantic import ValidationError
|
|
32
|
+
from sqlalchemy.engine import Engine, create_engine, make_url
|
|
33
|
+
from sqlalchemy.engine.mock import MockConnection, create_mock_engine
|
|
34
|
+
|
|
35
|
+
from . import __version__
|
|
36
|
+
from .datamodel import Schema
|
|
37
|
+
from .db.utils import DatabaseContext, is_mock_url
|
|
38
|
+
from .metadata import MetaDataBuilder
|
|
39
|
+
from .tap import Tap11Base, TapLoadingVisitor, init_tables
|
|
40
|
+
from .tap_schema import DataLoader, TableManager
|
|
41
|
+
|
|
42
|
+
__all__ = ["cli"]
|
|
43
|
+
|
|
44
|
+
logger = logging.getLogger("felis")
|
|
45
|
+
|
|
46
|
+
loglevel_choices = ["CRITICAL", "FATAL", "ERROR", "WARNING", "INFO", "DEBUG"]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@click.group()
|
|
50
|
+
@click.version_option(__version__)
|
|
51
|
+
@click.option(
|
|
52
|
+
"--log-level",
|
|
53
|
+
type=click.Choice(loglevel_choices),
|
|
54
|
+
envvar="FELIS_LOGLEVEL",
|
|
55
|
+
help="Felis log level",
|
|
56
|
+
default=logging.getLevelName(logging.INFO),
|
|
57
|
+
)
|
|
58
|
+
@click.option(
|
|
59
|
+
"--log-file",
|
|
60
|
+
type=click.Path(),
|
|
61
|
+
envvar="FELIS_LOGFILE",
|
|
62
|
+
help="Felis log file path",
|
|
63
|
+
)
|
|
64
|
+
@click.option(
|
|
65
|
+
"--id-generation", is_flag=True, help="Generate IDs for all objects that do not have them", default=False
|
|
66
|
+
)
|
|
67
|
+
@click.pass_context
|
|
68
|
+
def cli(ctx: click.Context, log_level: str, log_file: str | None, id_generation: bool) -> None:
|
|
69
|
+
"""Felis command line tools"""
|
|
70
|
+
ctx.ensure_object(dict)
|
|
71
|
+
ctx.obj["id_generation"] = id_generation
|
|
72
|
+
if ctx.obj["id_generation"]:
|
|
73
|
+
logger.info("ID generation is enabled")
|
|
74
|
+
if log_file:
|
|
75
|
+
logging.basicConfig(filename=log_file, level=log_level)
|
|
76
|
+
else:
|
|
77
|
+
logging.basicConfig(level=log_level)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@cli.command("create", help="Create database objects from the Felis file")
|
|
81
|
+
@click.option("--engine-url", envvar="FELIS_ENGINE_URL", help="SQLAlchemy Engine URL", default="sqlite://")
|
|
82
|
+
@click.option("--schema-name", help="Alternate schema name to override Felis file")
|
|
83
|
+
@click.option(
|
|
84
|
+
"--initialize",
|
|
85
|
+
is_flag=True,
|
|
86
|
+
help="Create the schema in the database if it does not exist (error if already exists)",
|
|
87
|
+
)
|
|
88
|
+
@click.option(
|
|
89
|
+
"--drop", is_flag=True, help="Drop schema if it already exists in the database (implies --initialize)"
|
|
90
|
+
)
|
|
91
|
+
@click.option("--echo", is_flag=True, help="Echo database commands as they are executed")
|
|
92
|
+
@click.option("--dry-run", is_flag=True, help="Dry run only to print out commands instead of executing")
|
|
93
|
+
@click.option(
|
|
94
|
+
"--output-file", "-o", type=click.File(mode="w"), help="Write SQL commands to a file instead of executing"
|
|
95
|
+
)
|
|
96
|
+
@click.option("--ignore-constraints", is_flag=True, help="Ignore constraints when creating tables")
|
|
97
|
+
@click.argument("file", type=click.File())
|
|
98
|
+
@click.pass_context
|
|
99
|
+
def create(
|
|
100
|
+
ctx: click.Context,
|
|
101
|
+
engine_url: str,
|
|
102
|
+
schema_name: str | None,
|
|
103
|
+
initialize: bool,
|
|
104
|
+
drop: bool,
|
|
105
|
+
echo: bool,
|
|
106
|
+
dry_run: bool,
|
|
107
|
+
output_file: IO[str] | None,
|
|
108
|
+
ignore_constraints: bool,
|
|
109
|
+
file: IO[str],
|
|
110
|
+
) -> None:
|
|
111
|
+
"""Create database objects from the Felis file.
|
|
112
|
+
|
|
113
|
+
Parameters
|
|
114
|
+
----------
|
|
115
|
+
engine_url
|
|
116
|
+
SQLAlchemy Engine URL.
|
|
117
|
+
schema_name
|
|
118
|
+
Alternate schema name to override Felis file.
|
|
119
|
+
initialize
|
|
120
|
+
Create the schema in the database if it does not exist.
|
|
121
|
+
drop
|
|
122
|
+
Drop schema if it already exists in the database.
|
|
123
|
+
echo
|
|
124
|
+
Echo database commands as they are executed.
|
|
125
|
+
dry_run
|
|
126
|
+
Dry run only to print out commands instead of executing.
|
|
127
|
+
output_file
|
|
128
|
+
Write SQL commands to a file instead of executing.
|
|
129
|
+
ignore_constraints
|
|
130
|
+
Ignore constraints when creating tables.
|
|
131
|
+
file
|
|
132
|
+
Felis file to read.
|
|
133
|
+
"""
|
|
134
|
+
try:
|
|
135
|
+
schema = Schema.from_stream(file, context={"id_generation": ctx.obj["id_generation"]})
|
|
136
|
+
url = make_url(engine_url)
|
|
137
|
+
if schema_name:
|
|
138
|
+
logger.info(f"Overriding schema name with: {schema_name}")
|
|
139
|
+
schema.name = schema_name
|
|
140
|
+
elif url.drivername == "sqlite":
|
|
141
|
+
logger.info("Overriding schema name for sqlite with: main")
|
|
142
|
+
schema.name = "main"
|
|
143
|
+
if not url.host and not url.drivername == "sqlite":
|
|
144
|
+
dry_run = True
|
|
145
|
+
logger.info("Forcing dry run for non-sqlite engine URL with no host")
|
|
146
|
+
|
|
147
|
+
metadata = MetaDataBuilder(schema, ignore_constraints=ignore_constraints).build()
|
|
148
|
+
logger.debug(f"Created metadata with schema name: {metadata.schema}")
|
|
149
|
+
|
|
150
|
+
engine: Engine | MockConnection
|
|
151
|
+
if not dry_run and not output_file:
|
|
152
|
+
engine = create_engine(url, echo=echo)
|
|
153
|
+
else:
|
|
154
|
+
if dry_run:
|
|
155
|
+
logger.info("Dry run will be executed")
|
|
156
|
+
engine = DatabaseContext.create_mock_engine(url, output_file)
|
|
157
|
+
if output_file:
|
|
158
|
+
logger.info("Writing SQL output to: " + output_file.name)
|
|
159
|
+
|
|
160
|
+
context = DatabaseContext(metadata, engine)
|
|
161
|
+
|
|
162
|
+
if drop and initialize:
|
|
163
|
+
raise ValueError("Cannot drop and initialize schema at the same time")
|
|
164
|
+
|
|
165
|
+
if drop:
|
|
166
|
+
logger.debug("Dropping schema if it exists")
|
|
167
|
+
context.drop()
|
|
168
|
+
initialize = True # If schema is dropped, it needs to be recreated.
|
|
169
|
+
|
|
170
|
+
if initialize:
|
|
171
|
+
logger.debug("Creating schema if not exists")
|
|
172
|
+
context.initialize()
|
|
173
|
+
|
|
174
|
+
context.create_all()
|
|
175
|
+
except Exception as e:
|
|
176
|
+
logger.exception(e)
|
|
177
|
+
raise click.ClickException(str(e))
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@cli.command("init-tap", help="Initialize TAP_SCHEMA objects in the database")
|
|
181
|
+
@click.option("--tap-schema-name", help="Alternate database schema name for 'TAP_SCHEMA'")
|
|
182
|
+
@click.option("--tap-schemas-table", help="Alternate table name for 'schemas'")
|
|
183
|
+
@click.option("--tap-tables-table", help="Alternate table name for 'tables'")
|
|
184
|
+
@click.option("--tap-columns-table", help="Alternate table name for 'columns'")
|
|
185
|
+
@click.option("--tap-keys-table", help="Alternate table name for 'keys'")
|
|
186
|
+
@click.option("--tap-key-columns-table", help="Alternate table name for 'key_columns'")
|
|
187
|
+
@click.argument("engine-url")
|
|
188
|
+
def init_tap(
|
|
189
|
+
engine_url: str,
|
|
190
|
+
tap_schema_name: str,
|
|
191
|
+
tap_schemas_table: str,
|
|
192
|
+
tap_tables_table: str,
|
|
193
|
+
tap_columns_table: str,
|
|
194
|
+
tap_keys_table: str,
|
|
195
|
+
tap_key_columns_table: str,
|
|
196
|
+
) -> None:
|
|
197
|
+
"""Initialize TAP_SCHEMA objects in the database.
|
|
198
|
+
|
|
199
|
+
Parameters
|
|
200
|
+
----------
|
|
201
|
+
engine_url
|
|
202
|
+
SQLAlchemy Engine URL. The target PostgreSQL schema or MySQL database
|
|
203
|
+
must already exist and be referenced in the URL.
|
|
204
|
+
tap_schema_name
|
|
205
|
+
Alterate name for the database schema ``TAP_SCHEMA``.
|
|
206
|
+
tap_schemas_table
|
|
207
|
+
Alterate table name for ``schemas``.
|
|
208
|
+
tap_tables_table
|
|
209
|
+
Alterate table name for ``tables``.
|
|
210
|
+
tap_columns_table
|
|
211
|
+
Alterate table name for ``columns``.
|
|
212
|
+
tap_keys_table
|
|
213
|
+
Alterate table name for ``keys``.
|
|
214
|
+
tap_key_columns_table
|
|
215
|
+
Alterate table name for ``key_columns``.
|
|
216
|
+
|
|
217
|
+
Notes
|
|
218
|
+
-----
|
|
219
|
+
The supported version of TAP_SCHEMA in the SQLAlchemy metadata is 1.1. The
|
|
220
|
+
tables are created in the database schema specified by the engine URL,
|
|
221
|
+
which must be a PostgreSQL schema or MySQL database that already exists.
|
|
222
|
+
"""
|
|
223
|
+
engine = create_engine(engine_url)
|
|
224
|
+
init_tables(
|
|
225
|
+
tap_schema_name,
|
|
226
|
+
tap_schemas_table,
|
|
227
|
+
tap_tables_table,
|
|
228
|
+
tap_columns_table,
|
|
229
|
+
tap_keys_table,
|
|
230
|
+
tap_key_columns_table,
|
|
231
|
+
)
|
|
232
|
+
Tap11Base.metadata.create_all(engine)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@cli.command("load-tap", help="Load metadata from a Felis file into a TAP_SCHEMA database")
|
|
236
|
+
@click.option("--engine-url", envvar="FELIS_ENGINE_URL", help="SQLAlchemy Engine URL")
|
|
237
|
+
@click.option("--schema-name", help="Alternate Schema Name for Felis file")
|
|
238
|
+
@click.option("--catalog-name", help="Catalog Name for Schema")
|
|
239
|
+
@click.option("--dry-run", is_flag=True, help="Dry Run Only. Prints out the DDL that would be executed")
|
|
240
|
+
@click.option("--tap-schema-name", help="Alternate schema name for 'TAP_SCHEMA'")
|
|
241
|
+
@click.option("--tap-tables-postfix", help="Postfix for TAP_SCHEMA table names")
|
|
242
|
+
@click.option("--tap-schemas-table", help="Alternate table name for 'schemas'")
|
|
243
|
+
@click.option("--tap-tables-table", help="Alternate table name for 'tables'")
|
|
244
|
+
@click.option("--tap-columns-table", help="Alternate table name for 'columns'")
|
|
245
|
+
@click.option("--tap-keys-table", help="Alternate table name for 'keys'")
|
|
246
|
+
@click.option("--tap-key-columns-table", help="Alternate table name for 'key_columns'")
|
|
247
|
+
@click.option("--tap-schema-index", type=int, help="TAP_SCHEMA index of the schema in this environment")
|
|
248
|
+
@click.argument("file", type=click.File())
|
|
249
|
+
def load_tap(
|
|
250
|
+
engine_url: str,
|
|
251
|
+
schema_name: str,
|
|
252
|
+
catalog_name: str,
|
|
253
|
+
dry_run: bool,
|
|
254
|
+
tap_schema_name: str,
|
|
255
|
+
tap_tables_postfix: str,
|
|
256
|
+
tap_schemas_table: str,
|
|
257
|
+
tap_tables_table: str,
|
|
258
|
+
tap_columns_table: str,
|
|
259
|
+
tap_keys_table: str,
|
|
260
|
+
tap_key_columns_table: str,
|
|
261
|
+
tap_schema_index: int,
|
|
262
|
+
file: IO[str],
|
|
263
|
+
) -> None:
|
|
264
|
+
"""Load TAP metadata from a Felis file.
|
|
265
|
+
|
|
266
|
+
This command loads the associated TAP metadata from a Felis YAML file
|
|
267
|
+
into the TAP_SCHEMA tables.
|
|
268
|
+
|
|
269
|
+
Parameters
|
|
270
|
+
----------
|
|
271
|
+
engine_url
|
|
272
|
+
SQLAlchemy Engine URL to catalog.
|
|
273
|
+
schema_name
|
|
274
|
+
Alternate schema name. This overrides the schema name in the
|
|
275
|
+
``catalog`` field of the Felis file.
|
|
276
|
+
catalog_name
|
|
277
|
+
Catalog name for the schema. This possibly duplicates the
|
|
278
|
+
``tap_schema_name`` argument (DM-44870).
|
|
279
|
+
dry_run
|
|
280
|
+
Dry run only to print out commands instead of executing.
|
|
281
|
+
tap_schema_name
|
|
282
|
+
Alternate name for the schema of TAP_SCHEMA in the database.
|
|
283
|
+
tap_tables_postfix
|
|
284
|
+
Postfix for TAP table names that will be automatically appended.
|
|
285
|
+
tap_schemas_table
|
|
286
|
+
Alternate table name for ``schemas``.
|
|
287
|
+
tap_tables_table
|
|
288
|
+
Alternate table name for ``tables``.
|
|
289
|
+
tap_columns_table
|
|
290
|
+
Alternate table name for ``columns``.
|
|
291
|
+
tap_keys_table
|
|
292
|
+
Alternate table name for ``keys``.
|
|
293
|
+
tap_key_columns_table
|
|
294
|
+
Alternate table name for ``key_columns``.
|
|
295
|
+
tap_schema_index
|
|
296
|
+
TAP_SCHEMA index of the schema in this TAP environment.
|
|
297
|
+
file
|
|
298
|
+
Felis file to read.
|
|
299
|
+
|
|
300
|
+
Notes
|
|
301
|
+
-----
|
|
302
|
+
The data will be loaded into the TAP_SCHEMA from the engine URL. The
|
|
303
|
+
tables must have already been initialized or an error will occur.
|
|
304
|
+
"""
|
|
305
|
+
schema = Schema.from_stream(file)
|
|
306
|
+
|
|
307
|
+
tap_tables = init_tables(
|
|
308
|
+
tap_schema_name,
|
|
309
|
+
tap_tables_postfix,
|
|
310
|
+
tap_schemas_table,
|
|
311
|
+
tap_tables_table,
|
|
312
|
+
tap_columns_table,
|
|
313
|
+
tap_keys_table,
|
|
314
|
+
tap_key_columns_table,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
if not dry_run:
|
|
318
|
+
engine = create_engine(engine_url)
|
|
319
|
+
|
|
320
|
+
if engine_url == "sqlite://" and not dry_run:
|
|
321
|
+
# In Memory SQLite - Mostly used to test
|
|
322
|
+
Tap11Base.metadata.create_all(engine)
|
|
323
|
+
|
|
324
|
+
tap_visitor = TapLoadingVisitor(
|
|
325
|
+
engine,
|
|
326
|
+
catalog_name=catalog_name,
|
|
327
|
+
schema_name=schema_name,
|
|
328
|
+
tap_tables=tap_tables,
|
|
329
|
+
tap_schema_index=tap_schema_index,
|
|
330
|
+
)
|
|
331
|
+
tap_visitor.visit_schema(schema)
|
|
332
|
+
else:
|
|
333
|
+
conn = DatabaseContext.create_mock_engine(engine_url)
|
|
334
|
+
|
|
335
|
+
tap_visitor = TapLoadingVisitor.from_mock_connection(
|
|
336
|
+
conn,
|
|
337
|
+
catalog_name=catalog_name,
|
|
338
|
+
schema_name=schema_name,
|
|
339
|
+
tap_tables=tap_tables,
|
|
340
|
+
tap_schema_index=tap_schema_index,
|
|
341
|
+
)
|
|
342
|
+
tap_visitor.visit_schema(schema)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
@cli.command("load-tap-schema", help="Load metadata from a Felis file into a TAP_SCHEMA database")
|
|
346
|
+
@click.option("--engine-url", envvar="FELIS_ENGINE_URL", help="SQLAlchemy Engine URL")
|
|
347
|
+
@click.option("--tap-schema-name", help="Name of the TAP_SCHEMA schema in the database")
|
|
348
|
+
@click.option(
|
|
349
|
+
"--tap-tables-postfix", help="Postfix which is applied to standard TAP_SCHEMA table names", default=""
|
|
350
|
+
)
|
|
351
|
+
@click.option("--tap-schema-index", type=int, help="TAP_SCHEMA index of the schema in this environment")
|
|
352
|
+
@click.option("--dry-run", is_flag=True, help="Execute dry run only. Does not insert any data.")
|
|
353
|
+
@click.option("--echo", is_flag=True, help="Print out the generated insert statements to stdout")
|
|
354
|
+
@click.option("--output-file", type=click.Path(), help="Write SQL commands to a file")
|
|
355
|
+
@click.argument("file", type=click.File())
|
|
356
|
+
@click.pass_context
|
|
357
|
+
def load_tap_schema(
|
|
358
|
+
ctx: click.Context,
|
|
359
|
+
engine_url: str,
|
|
360
|
+
tap_schema_name: str,
|
|
361
|
+
tap_tables_postfix: str,
|
|
362
|
+
tap_schema_index: int,
|
|
363
|
+
dry_run: bool,
|
|
364
|
+
echo: bool,
|
|
365
|
+
output_file: str | None,
|
|
366
|
+
file: IO[str],
|
|
367
|
+
) -> None:
|
|
368
|
+
"""Load TAP metadata from a Felis file.
|
|
369
|
+
|
|
370
|
+
Parameters
|
|
371
|
+
----------
|
|
372
|
+
engine_url
|
|
373
|
+
SQLAlchemy Engine URL.
|
|
374
|
+
tap_tables_postfix
|
|
375
|
+
Postfix which is applied to standard TAP_SCHEMA table names.
|
|
376
|
+
tap_schema_index
|
|
377
|
+
TAP_SCHEMA index of the schema in this environment.
|
|
378
|
+
dry_run
|
|
379
|
+
Execute dry run only. Does not insert any data.
|
|
380
|
+
echo
|
|
381
|
+
Print out the generated insert statements to stdout.
|
|
382
|
+
output_file
|
|
383
|
+
Output file for writing generated SQL.
|
|
384
|
+
file
|
|
385
|
+
Felis file to read.
|
|
386
|
+
|
|
387
|
+
Notes
|
|
388
|
+
-----
|
|
389
|
+
The TAP_SCHEMA database must already exist or the command will fail. This
|
|
390
|
+
command will not initialize the TAP_SCHEMA tables.
|
|
391
|
+
"""
|
|
392
|
+
url = make_url(engine_url)
|
|
393
|
+
engine: Engine | MockConnection
|
|
394
|
+
if dry_run or is_mock_url(url):
|
|
395
|
+
engine = create_mock_engine(url, executor=None)
|
|
396
|
+
else:
|
|
397
|
+
engine = create_engine(engine_url)
|
|
398
|
+
mgr = TableManager(
|
|
399
|
+
engine=engine,
|
|
400
|
+
apply_schema_to_metadata=False if engine.dialect.name == "sqlite" else True,
|
|
401
|
+
schema_name=tap_schema_name,
|
|
402
|
+
table_name_postfix=tap_tables_postfix,
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
schema = Schema.from_stream(file, context={"id_generation": ctx.obj["id_generation"]})
|
|
406
|
+
|
|
407
|
+
DataLoader(
|
|
408
|
+
schema,
|
|
409
|
+
mgr,
|
|
410
|
+
engine,
|
|
411
|
+
tap_schema_index=tap_schema_index,
|
|
412
|
+
dry_run=dry_run,
|
|
413
|
+
print_sql=echo,
|
|
414
|
+
output_path=output_file,
|
|
415
|
+
).load()
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
@cli.command("validate", help="Validate one or more Felis YAML files")
|
|
419
|
+
@click.option(
|
|
420
|
+
"--check-description", is_flag=True, help="Check that all objects have a description", default=False
|
|
421
|
+
)
|
|
422
|
+
@click.option(
|
|
423
|
+
"--check-redundant-datatypes", is_flag=True, help="Check for redundant datatype overrides", default=False
|
|
424
|
+
)
|
|
425
|
+
@click.option(
|
|
426
|
+
"--check-tap-table-indexes",
|
|
427
|
+
is_flag=True,
|
|
428
|
+
help="Check that every table has a unique TAP table index",
|
|
429
|
+
default=False,
|
|
430
|
+
)
|
|
431
|
+
@click.option(
|
|
432
|
+
"--check-tap-principal",
|
|
433
|
+
is_flag=True,
|
|
434
|
+
help="Check that at least one column per table is flagged as TAP principal",
|
|
435
|
+
default=False,
|
|
436
|
+
)
|
|
437
|
+
@click.argument("files", nargs=-1, type=click.File())
|
|
438
|
+
@click.pass_context
|
|
439
|
+
def validate(
|
|
440
|
+
ctx: click.Context,
|
|
441
|
+
check_description: bool,
|
|
442
|
+
check_redundant_datatypes: bool,
|
|
443
|
+
check_tap_table_indexes: bool,
|
|
444
|
+
check_tap_principal: bool,
|
|
445
|
+
files: Iterable[IO[str]],
|
|
446
|
+
) -> None:
|
|
447
|
+
"""Validate one or more felis YAML files.
|
|
448
|
+
|
|
449
|
+
Parameters
|
|
450
|
+
----------
|
|
451
|
+
check_description
|
|
452
|
+
Check that all objects have a valid description.
|
|
453
|
+
check_redundant_datatypes
|
|
454
|
+
Check for redundant type overrides.
|
|
455
|
+
check_tap_table_indexes
|
|
456
|
+
Check that every table has a unique TAP table index.
|
|
457
|
+
check_tap_principal
|
|
458
|
+
Check that at least one column per table is flagged as TAP principal.
|
|
459
|
+
files
|
|
460
|
+
The Felis YAML files to validate.
|
|
461
|
+
|
|
462
|
+
Raises
|
|
463
|
+
------
|
|
464
|
+
click.exceptions.Exit
|
|
465
|
+
Raised if any validation errors are found. The ``ValidationError``
|
|
466
|
+
which is thrown when a schema fails to validate will be logged as an
|
|
467
|
+
error message.
|
|
468
|
+
|
|
469
|
+
Notes
|
|
470
|
+
-----
|
|
471
|
+
All of the ``check`` flags are turned off by default and represent
|
|
472
|
+
optional validations controlled by the Pydantic context.
|
|
473
|
+
"""
|
|
474
|
+
rc = 0
|
|
475
|
+
for file in files:
|
|
476
|
+
file_name = getattr(file, "name", None)
|
|
477
|
+
logger.info(f"Validating {file_name}")
|
|
478
|
+
try:
|
|
479
|
+
Schema.from_stream(
|
|
480
|
+
file,
|
|
481
|
+
context={
|
|
482
|
+
"check_description": check_description,
|
|
483
|
+
"check_redundant_datatypes": check_redundant_datatypes,
|
|
484
|
+
"check_tap_table_indexes": check_tap_table_indexes,
|
|
485
|
+
"check_tap_principal": check_tap_principal,
|
|
486
|
+
"id_generation": ctx.obj["id_generation"],
|
|
487
|
+
},
|
|
488
|
+
)
|
|
489
|
+
except ValidationError as e:
|
|
490
|
+
logger.error(e)
|
|
491
|
+
rc = 1
|
|
492
|
+
if rc:
|
|
493
|
+
raise click.exceptions.Exit(rc)
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
if __name__ == "__main__":
|
|
497
|
+
cli()
|