lsst-felis 28.2024.4500__py3-none-any.whl → 30.0.0rc3__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.
- felis/__init__.py +9 -1
- felis/cli.py +308 -209
- felis/config/tap_schema/columns.csv +33 -0
- felis/config/tap_schema/key_columns.csv +8 -0
- felis/config/tap_schema/keys.csv +8 -0
- felis/config/tap_schema/schemas.csv +2 -0
- felis/config/tap_schema/tables.csv +6 -0
- felis/config/tap_schema/tap_schema_extensions.yaml +73 -0
- felis/datamodel.py +599 -59
- felis/db/{dialects.py → _dialects.py} +69 -4
- felis/db/{variants.py → _variants.py} +1 -1
- felis/db/database_context.py +917 -0
- felis/diff.py +234 -0
- felis/metadata.py +89 -19
- felis/tap_schema.py +271 -166
- felis/tests/postgresql.py +1 -1
- felis/tests/run_cli.py +79 -0
- felis/types.py +7 -7
- {lsst_felis-28.2024.4500.dist-info → lsst_felis-30.0.0rc3.dist-info}/METADATA +20 -16
- lsst_felis-30.0.0rc3.dist-info/RECORD +31 -0
- {lsst_felis-28.2024.4500.dist-info → lsst_felis-30.0.0rc3.dist-info}/WHEEL +1 -1
- felis/db/utils.py +0 -409
- felis/tap.py +0 -597
- felis/tests/utils.py +0 -122
- felis/version.py +0 -2
- lsst_felis-28.2024.4500.dist-info/RECORD +0 -26
- felis/{schemas → config/tap_schema}/tap_schema_std.yaml +0 -0
- felis/db/{sqltypes.py → _sqltypes.py} +7 -7
- {lsst_felis-28.2024.4500.dist-info → lsst_felis-30.0.0rc3.dist-info}/entry_points.txt +0 -0
- {lsst_felis-28.2024.4500.dist-info → lsst_felis-30.0.0rc3.dist-info/licenses}/COPYRIGHT +0 -0
- {lsst_felis-28.2024.4500.dist-info → lsst_felis-30.0.0rc3.dist-info/licenses}/LICENSE +0 -0
- {lsst_felis-28.2024.4500.dist-info → lsst_felis-30.0.0rc3.dist-info}/top_level.txt +0 -0
- {lsst_felis-28.2024.4500.dist-info → lsst_felis-30.0.0rc3.dist-info}/zip-safe +0 -0
felis/__init__.py
CHANGED
|
@@ -19,4 +19,12 @@
|
|
|
19
19
|
# You should have received a copy of the GNU General Public License
|
|
20
20
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
21
21
|
|
|
22
|
-
from .
|
|
22
|
+
from .datamodel import *
|
|
23
|
+
|
|
24
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
__version__ = version("lsst-felis")
|
|
28
|
+
except PackageNotFoundError:
|
|
29
|
+
# Package not installed or scons not run.
|
|
30
|
+
__version__ = "0.0.0"
|
felis/cli.py
CHANGED
|
@@ -29,15 +29,13 @@ from typing import IO
|
|
|
29
29
|
|
|
30
30
|
import click
|
|
31
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
32
|
|
|
35
33
|
from . import __version__
|
|
36
34
|
from .datamodel import Schema
|
|
37
|
-
from .db.
|
|
38
|
-
from .
|
|
39
|
-
from .
|
|
40
|
-
from .tap_schema import DataLoader, TableManager
|
|
35
|
+
from .db.database_context import create_database_context
|
|
36
|
+
from .diff import DatabaseDiff, FormattedSchemaDiff, SchemaDiff
|
|
37
|
+
from .metadata import create_metadata
|
|
38
|
+
from .tap_schema import DataLoader, MetadataInserter, TableManager
|
|
41
39
|
|
|
42
40
|
__all__ = ["cli"]
|
|
43
41
|
|
|
@@ -62,7 +60,10 @@ loglevel_choices = ["CRITICAL", "FATAL", "ERROR", "WARNING", "INFO", "DEBUG"]
|
|
|
62
60
|
help="Felis log file path",
|
|
63
61
|
)
|
|
64
62
|
@click.option(
|
|
65
|
-
"--id-generation",
|
|
63
|
+
"--id-generation/--no-id-generation",
|
|
64
|
+
is_flag=True,
|
|
65
|
+
help="Generate IDs for all objects that do not have them",
|
|
66
|
+
default=True,
|
|
66
67
|
)
|
|
67
68
|
@click.pass_context
|
|
68
69
|
def cli(ctx: click.Context, log_level: str, log_file: str | None, id_generation: bool) -> None:
|
|
@@ -71,6 +72,8 @@ def cli(ctx: click.Context, log_level: str, log_file: str | None, id_generation:
|
|
|
71
72
|
ctx.obj["id_generation"] = id_generation
|
|
72
73
|
if ctx.obj["id_generation"]:
|
|
73
74
|
logger.info("ID generation is enabled")
|
|
75
|
+
else:
|
|
76
|
+
logger.info("ID generation is disabled")
|
|
74
77
|
if log_file:
|
|
75
78
|
logging.basicConfig(filename=log_file, level=log_level)
|
|
76
79
|
else:
|
|
@@ -94,6 +97,7 @@ def cli(ctx: click.Context, log_level: str, log_file: str | None, id_generation:
|
|
|
94
97
|
"--output-file", "-o", type=click.File(mode="w"), help="Write SQL commands to a file instead of executing"
|
|
95
98
|
)
|
|
96
99
|
@click.option("--ignore-constraints", is_flag=True, help="Ignore constraints when creating tables")
|
|
100
|
+
@click.option("--skip-indexes", is_flag=True, help="Skip creating indexes when building metadata")
|
|
97
101
|
@click.argument("file", type=click.File())
|
|
98
102
|
@click.pass_context
|
|
99
103
|
def create(
|
|
@@ -106,6 +110,7 @@ def create(
|
|
|
106
110
|
dry_run: bool,
|
|
107
111
|
output_file: IO[str] | None,
|
|
108
112
|
ignore_constraints: bool,
|
|
113
|
+
skip_indexes: bool,
|
|
109
114
|
file: IO[str],
|
|
110
115
|
) -> None:
|
|
111
116
|
"""Create database objects from the Felis file.
|
|
@@ -128,230 +133,141 @@ def create(
|
|
|
128
133
|
Write SQL commands to a file instead of executing.
|
|
129
134
|
ignore_constraints
|
|
130
135
|
Ignore constraints when creating tables.
|
|
136
|
+
skip_indexes
|
|
137
|
+
Skip creating indexes when building metadata.
|
|
131
138
|
file
|
|
132
139
|
Felis file to read.
|
|
133
140
|
"""
|
|
134
141
|
try:
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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)
|
|
142
|
+
metadata = create_metadata(
|
|
143
|
+
file,
|
|
144
|
+
id_generation=ctx.obj["id_generation"],
|
|
145
|
+
schema_name=schema_name,
|
|
146
|
+
ignore_constraints=ignore_constraints,
|
|
147
|
+
skip_indexes=skip_indexes,
|
|
148
|
+
engine_url=engine_url,
|
|
149
|
+
)
|
|
159
150
|
|
|
160
|
-
|
|
151
|
+
with create_database_context(
|
|
152
|
+
engine_url,
|
|
153
|
+
metadata,
|
|
154
|
+
echo=echo,
|
|
155
|
+
dry_run=dry_run,
|
|
156
|
+
output_file=output_file,
|
|
157
|
+
) as db_ctx:
|
|
158
|
+
if drop and initialize:
|
|
159
|
+
raise ValueError("Cannot drop and initialize schema at the same time")
|
|
161
160
|
|
|
162
|
-
|
|
163
|
-
|
|
161
|
+
if drop:
|
|
162
|
+
logger.debug("Dropping schema if it exists")
|
|
163
|
+
db_ctx.drop()
|
|
164
|
+
initialize = True # If schema is dropped, it needs to be recreated.
|
|
164
165
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
initialize = True # If schema is dropped, it needs to be recreated.
|
|
166
|
+
if initialize:
|
|
167
|
+
logger.debug("Creating schema if not exists")
|
|
168
|
+
db_ctx.initialize()
|
|
169
169
|
|
|
170
|
-
|
|
171
|
-
logger.debug("Creating schema if not exists")
|
|
172
|
-
context.initialize()
|
|
170
|
+
db_ctx.create_all()
|
|
173
171
|
|
|
174
|
-
context.create_all()
|
|
175
172
|
except Exception as e:
|
|
176
173
|
logger.exception(e)
|
|
177
174
|
raise click.ClickException(str(e))
|
|
178
175
|
|
|
179
176
|
|
|
180
|
-
@cli.command("
|
|
181
|
-
@click.option("--
|
|
182
|
-
@click.option("--
|
|
183
|
-
@click.
|
|
184
|
-
@click.
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
@click.argument("engine-url")
|
|
188
|
-
def init_tap(
|
|
177
|
+
@cli.command("create-indexes", help="Create database indexes defined in the Felis file")
|
|
178
|
+
@click.option("--engine-url", envvar="FELIS_ENGINE_URL", help="SQLAlchemy Engine URL", default="sqlite://")
|
|
179
|
+
@click.option("--schema-name", help="Alternate schema name to override Felis file")
|
|
180
|
+
@click.argument("file", type=click.File())
|
|
181
|
+
@click.pass_context
|
|
182
|
+
def create_indexes(
|
|
183
|
+
ctx: click.Context,
|
|
189
184
|
engine_url: str,
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
tap_tables_table: str,
|
|
193
|
-
tap_columns_table: str,
|
|
194
|
-
tap_keys_table: str,
|
|
195
|
-
tap_key_columns_table: str,
|
|
185
|
+
schema_name: str | None,
|
|
186
|
+
file: IO[str],
|
|
196
187
|
) -> None:
|
|
197
|
-
"""
|
|
188
|
+
"""Create indexes from a Felis YAML file in a target database.
|
|
198
189
|
|
|
199
190
|
Parameters
|
|
200
191
|
----------
|
|
201
192
|
engine_url
|
|
202
|
-
SQLAlchemy Engine URL.
|
|
203
|
-
|
|
204
|
-
|
|
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.
|
|
193
|
+
SQLAlchemy Engine URL.
|
|
194
|
+
file
|
|
195
|
+
Felis file to read.
|
|
222
196
|
"""
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
Tap11Base.metadata.create_all(engine)
|
|
197
|
+
try:
|
|
198
|
+
metadata = create_metadata(
|
|
199
|
+
file, id_generation=ctx.obj["id_generation"], schema_name=schema_name, engine_url=engine_url
|
|
200
|
+
)
|
|
201
|
+
with create_database_context(engine_url, metadata) as db_ctx:
|
|
202
|
+
db_ctx.create_indexes()
|
|
203
|
+
except Exception as e:
|
|
204
|
+
logger.exception(e)
|
|
205
|
+
raise click.ClickException("Error creating indexes: " + str(e))
|
|
233
206
|
|
|
234
207
|
|
|
235
|
-
@cli.command("
|
|
236
|
-
@click.option("--engine-url", envvar="FELIS_ENGINE_URL", help="SQLAlchemy Engine URL")
|
|
237
|
-
@click.option("--schema-name", help="Alternate
|
|
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")
|
|
208
|
+
@cli.command("drop-indexes", help="Drop database indexes defined in the Felis file")
|
|
209
|
+
@click.option("--engine-url", envvar="FELIS_ENGINE_URL", help="SQLAlchemy Engine URL", default="sqlite://")
|
|
210
|
+
@click.option("--schema-name", help="Alternate schema name to override Felis file")
|
|
248
211
|
@click.argument("file", type=click.File())
|
|
249
|
-
|
|
212
|
+
@click.pass_context
|
|
213
|
+
def drop_indexes(
|
|
214
|
+
ctx: click.Context,
|
|
250
215
|
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,
|
|
216
|
+
schema_name: str | None,
|
|
262
217
|
file: IO[str],
|
|
263
218
|
) -> None:
|
|
264
|
-
"""
|
|
265
|
-
|
|
266
|
-
This command loads the associated TAP metadata from a Felis YAML file
|
|
267
|
-
into the TAP_SCHEMA tables.
|
|
219
|
+
"""Drop indexes from a Felis YAML file in a target database.
|
|
268
220
|
|
|
269
221
|
Parameters
|
|
270
222
|
----------
|
|
271
223
|
engine_url
|
|
272
|
-
SQLAlchemy Engine URL
|
|
273
|
-
|
|
274
|
-
Alternate schema name
|
|
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.
|
|
224
|
+
SQLAlchemy Engine URL.
|
|
225
|
+
schema-name
|
|
226
|
+
Alternate schema name to override Felis file.
|
|
297
227
|
file
|
|
298
228
|
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
229
|
"""
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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,
|
|
230
|
+
try:
|
|
231
|
+
metadata = create_metadata(
|
|
232
|
+
file, id_generation=ctx.obj["id_generation"], schema_name=schema_name, engine_url=engine_url
|
|
341
233
|
)
|
|
342
|
-
|
|
234
|
+
with create_database_context(engine_url, metadata) as db:
|
|
235
|
+
db.drop_indexes()
|
|
236
|
+
except Exception as e:
|
|
237
|
+
logger.exception(e)
|
|
238
|
+
raise click.ClickException("Error dropping indexes: " + str(e))
|
|
343
239
|
|
|
344
240
|
|
|
345
241
|
@cli.command("load-tap-schema", help="Load metadata from a Felis file into a TAP_SCHEMA database")
|
|
346
242
|
@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
243
|
@click.option(
|
|
349
|
-
"--tap-
|
|
244
|
+
"--tap-schema-name", "-n", help="Name of the TAP_SCHEMA schema in the database (default: TAP_SCHEMA)"
|
|
245
|
+
)
|
|
246
|
+
@click.option(
|
|
247
|
+
"--tap-tables-postfix",
|
|
248
|
+
"-p",
|
|
249
|
+
help="Postfix which is applied to standard TAP_SCHEMA table names",
|
|
250
|
+
default="",
|
|
251
|
+
)
|
|
252
|
+
@click.option("--tap-schema-index", "-i", type=int, help="TAP_SCHEMA index of the schema in this environment")
|
|
253
|
+
@click.option("--dry-run", "-D", is_flag=True, help="Execute dry run only. Does not insert any data.")
|
|
254
|
+
@click.option("--echo", "-e", is_flag=True, help="Print out the generated insert statements to stdout")
|
|
255
|
+
@click.option(
|
|
256
|
+
"--output-file", "-o", type=click.File(mode="w"), help="Write SQL commands to a file instead of executing"
|
|
257
|
+
)
|
|
258
|
+
@click.option(
|
|
259
|
+
"--force-unbounded-arraysize",
|
|
260
|
+
is_flag=True,
|
|
261
|
+
help="Use unbounded arraysize by default for all variable length string columns"
|
|
262
|
+
", e.g., ``votable:arraysize: *`` (workaround for astropy bug #18099)",
|
|
263
|
+
) # DM-50899: Variable-length bounded strings are not handled correctly in astropy
|
|
264
|
+
@click.option(
|
|
265
|
+
"--unique-keys",
|
|
266
|
+
"-u",
|
|
267
|
+
is_flag=True,
|
|
268
|
+
help="Generate unique key_id values for keys and key_columns tables by prepending the schema name",
|
|
269
|
+
default=False,
|
|
350
270
|
)
|
|
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
271
|
@click.argument("file", type=click.File())
|
|
356
272
|
@click.pass_context
|
|
357
273
|
def load_tap_schema(
|
|
@@ -362,7 +278,9 @@ def load_tap_schema(
|
|
|
362
278
|
tap_schema_index: int,
|
|
363
279
|
dry_run: bool,
|
|
364
280
|
echo: bool,
|
|
365
|
-
output_file: str | None,
|
|
281
|
+
output_file: IO[str] | None,
|
|
282
|
+
force_unbounded_arraysize: bool,
|
|
283
|
+
unique_keys: bool,
|
|
366
284
|
file: IO[str],
|
|
367
285
|
) -> None:
|
|
368
286
|
"""Load TAP metadata from a Felis file.
|
|
@@ -389,30 +307,99 @@ def load_tap_schema(
|
|
|
389
307
|
The TAP_SCHEMA database must already exist or the command will fail. This
|
|
390
308
|
command will not initialize the TAP_SCHEMA tables.
|
|
391
309
|
"""
|
|
392
|
-
|
|
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)
|
|
310
|
+
# Create TableManager with automatic dialect detection
|
|
398
311
|
mgr = TableManager(
|
|
399
|
-
|
|
400
|
-
apply_schema_to_metadata=False if engine.dialect.name == "sqlite" else True,
|
|
312
|
+
engine_url=engine_url,
|
|
401
313
|
schema_name=tap_schema_name,
|
|
402
314
|
table_name_postfix=tap_tables_postfix,
|
|
403
315
|
)
|
|
404
316
|
|
|
405
|
-
|
|
317
|
+
# Create DatabaseContext using TableManager's metadata
|
|
318
|
+
with create_database_context(
|
|
319
|
+
engine_url, mgr.metadata, echo=echo, dry_run=dry_run, output_file=output_file
|
|
320
|
+
) as db_ctx:
|
|
321
|
+
schema = Schema.from_stream(
|
|
322
|
+
file,
|
|
323
|
+
context={
|
|
324
|
+
"id_generation": ctx.obj["id_generation"],
|
|
325
|
+
"force_unbounded_arraysize": force_unbounded_arraysize,
|
|
326
|
+
},
|
|
327
|
+
)
|
|
406
328
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
329
|
+
DataLoader(
|
|
330
|
+
schema,
|
|
331
|
+
mgr,
|
|
332
|
+
db_context=db_ctx,
|
|
333
|
+
tap_schema_index=tap_schema_index,
|
|
334
|
+
dry_run=dry_run,
|
|
335
|
+
print_sql=echo,
|
|
336
|
+
output_file=output_file,
|
|
337
|
+
unique_keys=unique_keys,
|
|
338
|
+
).load()
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
@cli.command("init-tap-schema", help="Initialize a standard TAP_SCHEMA database")
|
|
342
|
+
@click.option("--engine-url", envvar="FELIS_ENGINE_URL", help="SQLAlchemy Engine URL", required=True)
|
|
343
|
+
@click.option("--tap-schema-name", help="Name of the TAP_SCHEMA schema in the database")
|
|
344
|
+
@click.option(
|
|
345
|
+
"--extensions",
|
|
346
|
+
type=str,
|
|
347
|
+
default=None,
|
|
348
|
+
help=(
|
|
349
|
+
"Optional path to extensions YAML file (system path or resource:// URI). "
|
|
350
|
+
"If not provided, no extensions will be applied. "
|
|
351
|
+
"Example (default packaged extensions): "
|
|
352
|
+
"--extensions resource://felis/config/tap_schema/tap_schema_extensions.yaml"
|
|
353
|
+
),
|
|
354
|
+
)
|
|
355
|
+
@click.option(
|
|
356
|
+
"--tap-tables-postfix", help="Postfix which is applied to standard TAP_SCHEMA table names", default=""
|
|
357
|
+
)
|
|
358
|
+
@click.option(
|
|
359
|
+
"--insert-metadata/--no-insert-metadata",
|
|
360
|
+
is_flag=True,
|
|
361
|
+
help="Insert metadata describing TAP_SCHEMA itself",
|
|
362
|
+
default=True,
|
|
363
|
+
)
|
|
364
|
+
@click.pass_context
|
|
365
|
+
def init_tap_schema(
|
|
366
|
+
ctx: click.Context,
|
|
367
|
+
engine_url: str,
|
|
368
|
+
tap_schema_name: str,
|
|
369
|
+
extensions: str | None,
|
|
370
|
+
tap_tables_postfix: str,
|
|
371
|
+
insert_metadata: bool,
|
|
372
|
+
) -> None:
|
|
373
|
+
"""Initialize a standard TAP_SCHEMA database.
|
|
374
|
+
|
|
375
|
+
Parameters
|
|
376
|
+
----------
|
|
377
|
+
engine_url
|
|
378
|
+
SQLAlchemy Engine URL.
|
|
379
|
+
tap_schema_name
|
|
380
|
+
Name of the TAP_SCHEMA schema in the database.
|
|
381
|
+
extensions
|
|
382
|
+
Extensions YAML file.
|
|
383
|
+
tap_tables_postfix
|
|
384
|
+
Postfix which is applied to standard TAP_SCHEMA table names.
|
|
385
|
+
insert_metadata
|
|
386
|
+
Insert metadata describing TAP_SCHEMA itself.
|
|
387
|
+
If set to False, only the TAP_SCHEMA tables will be created, but no
|
|
388
|
+
metadata will be inserted.
|
|
389
|
+
"""
|
|
390
|
+
# Create TableManager with automatic dialect detection
|
|
391
|
+
mgr = TableManager(
|
|
392
|
+
engine_url=engine_url,
|
|
393
|
+
schema_name=tap_schema_name,
|
|
394
|
+
table_name_postfix=tap_tables_postfix,
|
|
395
|
+
extensions_path=extensions,
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
# Create DatabaseContext using TableManager's metadata
|
|
399
|
+
with create_database_context(engine_url, mgr.metadata) as db_ctx:
|
|
400
|
+
mgr.initialize_database(db_context=db_ctx)
|
|
401
|
+
if insert_metadata:
|
|
402
|
+
MetadataInserter(mgr, db_context=db_ctx).insert_metadata()
|
|
416
403
|
|
|
417
404
|
|
|
418
405
|
@cli.command("validate", help="Validate one or more Felis YAML files")
|
|
@@ -493,5 +480,117 @@ def validate(
|
|
|
493
480
|
raise click.exceptions.Exit(rc)
|
|
494
481
|
|
|
495
482
|
|
|
483
|
+
@cli.command(
|
|
484
|
+
"diff",
|
|
485
|
+
help="""
|
|
486
|
+
Compare two schemas or a schema and a database for changes
|
|
487
|
+
|
|
488
|
+
Examples:
|
|
489
|
+
|
|
490
|
+
felis diff schema1.yaml schema2.yaml
|
|
491
|
+
|
|
492
|
+
felis diff -c alembic schema1.yaml schema2.yaml
|
|
493
|
+
|
|
494
|
+
felis diff --engine-url sqlite:///test.db schema.yaml
|
|
495
|
+
""",
|
|
496
|
+
)
|
|
497
|
+
@click.option("--engine-url", envvar="FELIS_ENGINE_URL", help="SQLAlchemy Engine URL")
|
|
498
|
+
@click.option(
|
|
499
|
+
"-c",
|
|
500
|
+
"--comparator",
|
|
501
|
+
type=click.Choice(["alembic", "deepdiff"], case_sensitive=False),
|
|
502
|
+
help="Comparator to use for schema comparison",
|
|
503
|
+
default="deepdiff",
|
|
504
|
+
)
|
|
505
|
+
@click.option("-E", "--error-on-change", is_flag=True, help="Exit with error code if schemas are different")
|
|
506
|
+
@click.argument("files", nargs=-1, type=click.File())
|
|
507
|
+
@click.pass_context
|
|
508
|
+
def diff(
|
|
509
|
+
ctx: click.Context,
|
|
510
|
+
engine_url: str | None,
|
|
511
|
+
comparator: str,
|
|
512
|
+
error_on_change: bool,
|
|
513
|
+
files: Iterable[IO[str]],
|
|
514
|
+
) -> None:
|
|
515
|
+
files_list = list(files)
|
|
516
|
+
schemas = [
|
|
517
|
+
Schema.from_stream(file, context={"id_generation": ctx.obj["id_generation"]}) for file in files_list
|
|
518
|
+
]
|
|
519
|
+
diff: SchemaDiff
|
|
520
|
+
if len(schemas) == 2:
|
|
521
|
+
if comparator == "alembic":
|
|
522
|
+
# Reset file stream to beginning before re-reading
|
|
523
|
+
files_list[0].seek(0)
|
|
524
|
+
metadata = create_metadata(
|
|
525
|
+
files_list[0], id_generation=ctx.obj["id_generation"], engine_url=engine_url
|
|
526
|
+
)
|
|
527
|
+
with create_database_context(
|
|
528
|
+
engine_url if engine_url else "sqlite:///:memory:", metadata
|
|
529
|
+
) as db_ctx:
|
|
530
|
+
db_ctx.initialize()
|
|
531
|
+
db_ctx.create_all()
|
|
532
|
+
diff = DatabaseDiff(schemas[1], db_ctx.engine)
|
|
533
|
+
else:
|
|
534
|
+
diff = FormattedSchemaDiff(schemas[0], schemas[1])
|
|
535
|
+
elif len(schemas) == 1 and engine_url is not None:
|
|
536
|
+
# Create minimal metadata for the context manager
|
|
537
|
+
from sqlalchemy import MetaData
|
|
538
|
+
|
|
539
|
+
metadata = MetaData()
|
|
540
|
+
|
|
541
|
+
with create_database_context(engine_url, metadata) as db_ctx:
|
|
542
|
+
diff = DatabaseDiff(schemas[0], db_ctx.engine)
|
|
543
|
+
else:
|
|
544
|
+
raise click.ClickException(
|
|
545
|
+
"Invalid arguments - provide two schemas or a single schema and a database engine URL"
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
diff.print()
|
|
549
|
+
|
|
550
|
+
if diff.has_changes and error_on_change:
|
|
551
|
+
raise click.ClickException("Schema was changed")
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
@cli.command(
|
|
555
|
+
"dump",
|
|
556
|
+
help="""
|
|
557
|
+
Dump a schema file to YAML or JSON format
|
|
558
|
+
|
|
559
|
+
Example:
|
|
560
|
+
|
|
561
|
+
felis dump schema.yaml schema.json
|
|
562
|
+
|
|
563
|
+
felis dump schema.yaml schema_dump.yaml
|
|
564
|
+
""",
|
|
565
|
+
)
|
|
566
|
+
@click.option(
|
|
567
|
+
"--strip-ids/--no-strip-ids",
|
|
568
|
+
is_flag=True,
|
|
569
|
+
help="Strip IDs from the output schema",
|
|
570
|
+
default=False,
|
|
571
|
+
)
|
|
572
|
+
@click.argument("files", nargs=2, type=click.Path())
|
|
573
|
+
@click.pass_context
|
|
574
|
+
def dump(
|
|
575
|
+
ctx: click.Context,
|
|
576
|
+
strip_ids: bool,
|
|
577
|
+
files: list[str],
|
|
578
|
+
) -> None:
|
|
579
|
+
if strip_ids:
|
|
580
|
+
logger.info("Stripping IDs from the output schema")
|
|
581
|
+
if files[1].endswith(".json"):
|
|
582
|
+
format = "json"
|
|
583
|
+
elif files[1].endswith(".yaml"):
|
|
584
|
+
format = "yaml"
|
|
585
|
+
else:
|
|
586
|
+
raise click.ClickException("Output file must have a .json or .yaml extension")
|
|
587
|
+
schema = Schema.from_uri(files[0], context={"id_generation": ctx.obj["id_generation"]})
|
|
588
|
+
with open(files[1], "w") as f:
|
|
589
|
+
if format == "yaml":
|
|
590
|
+
schema.dump_yaml(f, strip_ids=strip_ids)
|
|
591
|
+
elif format == "json":
|
|
592
|
+
schema.dump_json(f, strip_ids=strip_ids)
|
|
593
|
+
|
|
594
|
+
|
|
496
595
|
if __name__ == "__main__":
|
|
497
596
|
cli()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
table_name,column_name,utype,ucd,unit,description,datatype,arraysize,xtype,size,principal,indexed,std,column_index
|
|
2
|
+
tap_schema.columns,"""size""",\N,\N,\N,deprecated: use arraysize,int,\N,\N,\N,1,0,1,9
|
|
3
|
+
tap_schema.columns,arraysize,\N,\N,\N,lists the size of variable-length columns in the tableset,char,16*,\N,16,1,0,1,8
|
|
4
|
+
tap_schema.columns,column_index,\N,\N,\N,recommended sort order when listing columns of a table,int,\N,\N,\N,1,0,1,13
|
|
5
|
+
tap_schema.columns,column_name,\N,\N,\N,the column name,char,64*,\N,64,1,0,1,2
|
|
6
|
+
tap_schema.columns,datatype,\N,\N,\N,lists the ADQL datatype of columns in the tableset,char,64*,\N,64,1,0,1,7
|
|
7
|
+
tap_schema.columns,description,\N,\N,\N,describes the columns in the tableset,char,512*,\N,512,1,0,1,6
|
|
8
|
+
tap_schema.columns,indexed,\N,\N,\N,"an indexed column; 1 means 1, 0 means 0",int,\N,\N,\N,1,0,1,11
|
|
9
|
+
tap_schema.columns,principal,\N,\N,\N,"a principal column; 1 means 1, 0 means 0",int,\N,\N,\N,1,0,1,10
|
|
10
|
+
tap_schema.columns,std,\N,\N,\N,"a standard column; 1 means 1, 0 means 0",int,\N,\N,\N,1,0,1,12
|
|
11
|
+
tap_schema.columns,table_name,\N,\N,\N,the table this column belongs to,char,64*,\N,64,1,0,1,1
|
|
12
|
+
tap_schema.columns,ucd,\N,\N,\N,lists the UCDs of columns in the tableset,char,64*,\N,64,1,0,1,4
|
|
13
|
+
tap_schema.columns,unit,\N,\N,\N,lists the unit used for column values in the tableset,char,64*,\N,64,1,0,1,5
|
|
14
|
+
tap_schema.columns,utype,\N,\N,\N,lists the utypes of columns in the tableset,char,512*,\N,512,1,0,1,3
|
|
15
|
+
tap_schema.columns,xtype,\N,\N,\N,a DALI or custom extended type annotation,char,64*,\N,64,1,0,1,7
|
|
16
|
+
tap_schema.key_columns,from_column,\N,\N,\N,column in the from_table,char,64*,\N,64,1,0,1,2
|
|
17
|
+
tap_schema.key_columns,key_id,\N,\N,\N,key to join to tap_schema.keys,char,64*,\N,64,1,0,1,1
|
|
18
|
+
tap_schema.key_columns,target_column,\N,\N,\N,column in the target_table,char,64*,\N,64,1,0,1,3
|
|
19
|
+
tap_schema.keys,description,\N,\N,\N,describes keys in the tableset,char,512*,\N,512,1,0,1,5
|
|
20
|
+
tap_schema.keys,from_table,\N,\N,\N,the table with the foreign key,char,64*,\N,64,1,0,1,2
|
|
21
|
+
tap_schema.keys,key_id,\N,\N,\N,unique key to join to tap_schema.key_columns,char,64*,\N,64,1,0,1,1
|
|
22
|
+
tap_schema.keys,target_table,\N,\N,\N,the table with the primary key,char,64*,\N,64,1,0,1,3
|
|
23
|
+
tap_schema.keys,utype,\N,\N,\N,lists the utype of keys in the tableset,char,512*,\N,512,1,0,1,4
|
|
24
|
+
tap_schema.schemas,description,\N,\N,\N,describes schemas in the tableset,char,512*,\N,512,1,0,1,3
|
|
25
|
+
tap_schema.schemas,schema_index,\N,\N,\N,recommended sort order when listing schemas,int,\N,\N,\N,1,0,1,4
|
|
26
|
+
tap_schema.schemas,schema_name,\N,\N,\N,schema name for reference to tap_schema.schemas,char,64*,\N,64,1,0,1,1
|
|
27
|
+
tap_schema.schemas,utype,\N,\N,\N,lists the utypes of schemas in the tableset,char,512*,\N,512,1,0,1,2
|
|
28
|
+
tap_schema.tables,description,\N,\N,\N,describes tables in the tableset,char,512*,\N,512,1,0,1,5
|
|
29
|
+
tap_schema.tables,schema_name,\N,\N,\N,the schema this table belongs to,char,512*,\N,512,1,0,1,1
|
|
30
|
+
tap_schema.tables,table_index,\N,\N,\N,recommended sort order when listing tables,int,\N,\N,\N,1,0,1,6
|
|
31
|
+
tap_schema.tables,table_name,\N,\N,\N,the fully qualified table name,char,64*,\N,64,1,0,1,2
|
|
32
|
+
tap_schema.tables,table_type,\N,\N,\N,one of: table view,char,8*,\N,8,1,0,1,3
|
|
33
|
+
tap_schema.tables,utype,\N,\N,\N,lists the utype of tables in the tableset,char,512*,\N,512,1,0,1,4
|