lsst-felis 29.0.1__tar.gz → 30.0.0rc3__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.
- {lsst_felis-29.0.1/python/lsst_felis.egg-info → lsst_felis-30.0.0rc3}/PKG-INFO +5 -5
- {lsst_felis-29.0.1 → lsst_felis-30.0.0rc3}/pyproject.toml +50 -32
- {lsst_felis-29.0.1 → lsst_felis-30.0.0rc3}/python/felis/__init__.py +9 -5
- {lsst_felis-29.0.1 → lsst_felis-30.0.0rc3}/python/felis/cli.py +256 -85
- lsst_felis-30.0.0rc3/python/felis/config/tap_schema/columns.csv +33 -0
- lsst_felis-30.0.0rc3/python/felis/config/tap_schema/key_columns.csv +8 -0
- lsst_felis-30.0.0rc3/python/felis/config/tap_schema/keys.csv +8 -0
- lsst_felis-30.0.0rc3/python/felis/config/tap_schema/schemas.csv +2 -0
- lsst_felis-30.0.0rc3/python/felis/config/tap_schema/tables.csv +6 -0
- lsst_felis-30.0.0rc3/python/felis/config/tap_schema/tap_schema_extensions.yaml +73 -0
- {lsst_felis-29.0.1 → lsst_felis-30.0.0rc3}/python/felis/datamodel.py +476 -61
- lsst_felis-29.0.1/python/felis/db/dialects.py → lsst_felis-30.0.0rc3/python/felis/db/_dialects.py +69 -4
- lsst_felis-29.0.1/python/felis/db/variants.py → lsst_felis-30.0.0rc3/python/felis/db/_variants.py +1 -1
- lsst_felis-30.0.0rc3/python/felis/db/database_context.py +917 -0
- {lsst_felis-29.0.1 → lsst_felis-30.0.0rc3}/python/felis/diff.py +23 -18
- {lsst_felis-29.0.1 → lsst_felis-30.0.0rc3}/python/felis/metadata.py +84 -11
- {lsst_felis-29.0.1 → lsst_felis-30.0.0rc3}/python/felis/tap_schema.py +265 -166
- lsst_felis-30.0.0rc3/python/felis/tests/run_cli.py +79 -0
- {lsst_felis-29.0.1 → lsst_felis-30.0.0rc3/python/lsst_felis.egg-info}/PKG-INFO +5 -5
- {lsst_felis-29.0.1 → lsst_felis-30.0.0rc3}/python/lsst_felis.egg-info/SOURCES.txt +14 -8
- {lsst_felis-29.0.1 → lsst_felis-30.0.0rc3}/python/lsst_felis.egg-info/requires.txt +2 -2
- lsst_felis-30.0.0rc3/tests/test_cli.py +362 -0
- lsst_felis-30.0.0rc3/tests/test_database_context.py +264 -0
- {lsst_felis-29.0.1 → lsst_felis-30.0.0rc3}/tests/test_datamodel.py +178 -6
- {lsst_felis-29.0.1 → lsst_felis-30.0.0rc3}/tests/test_diff.py +2 -0
- lsst_felis-30.0.0rc3/tests/test_import_modules.py +36 -0
- {lsst_felis-29.0.1 → lsst_felis-30.0.0rc3}/tests/test_metadata.py +84 -69
- {lsst_felis-29.0.1 → lsst_felis-30.0.0rc3}/tests/test_postgres.py +76 -3
- lsst_felis-30.0.0rc3/tests/test_tap_schema.py +801 -0
- {lsst_felis-29.0.1 → lsst_felis-30.0.0rc3}/tests/test_tap_schema_postgres.py +48 -40
- lsst_felis-29.0.1/python/felis/db/schema.py +0 -62
- lsst_felis-29.0.1/python/felis/db/utils.py +0 -409
- lsst_felis-29.0.1/python/felis/version.py +0 -2
- lsst_felis-29.0.1/tests/test_cli.py +0 -232
- lsst_felis-29.0.1/tests/test_db.py +0 -79
- lsst_felis-29.0.1/tests/test_tap_schema.py +0 -298
- {lsst_felis-29.0.1 → lsst_felis-30.0.0rc3}/COPYRIGHT +0 -0
- {lsst_felis-29.0.1 → lsst_felis-30.0.0rc3}/LICENSE +0 -0
- {lsst_felis-29.0.1 → lsst_felis-30.0.0rc3}/README.rst +0 -0
- {lsst_felis-29.0.1/python/felis/schemas → lsst_felis-30.0.0rc3/python/felis/config/tap_schema}/tap_schema_std.yaml +0 -0
- {lsst_felis-29.0.1 → lsst_felis-30.0.0rc3}/python/felis/db/__init__.py +0 -0
- lsst_felis-29.0.1/python/felis/db/sqltypes.py → lsst_felis-30.0.0rc3/python/felis/db/_sqltypes.py +7 -7
- {lsst_felis-29.0.1 → lsst_felis-30.0.0rc3}/python/felis/py.typed +0 -0
- {lsst_felis-29.0.1 → lsst_felis-30.0.0rc3}/python/felis/tests/__init__.py +0 -0
- {lsst_felis-29.0.1 → lsst_felis-30.0.0rc3}/python/felis/tests/postgresql.py +1 -1
- {lsst_felis-29.0.1 → lsst_felis-30.0.0rc3}/python/felis/types.py +7 -7
- {lsst_felis-29.0.1 → lsst_felis-30.0.0rc3}/python/lsst_felis.egg-info/dependency_links.txt +0 -0
- {lsst_felis-29.0.1 → lsst_felis-30.0.0rc3}/python/lsst_felis.egg-info/entry_points.txt +0 -0
- {lsst_felis-29.0.1 → lsst_felis-30.0.0rc3}/python/lsst_felis.egg-info/top_level.txt +0 -0
- {lsst_felis-29.0.1 → lsst_felis-30.0.0rc3}/python/lsst_felis.egg-info/zip-safe +0 -0
- {lsst_felis-29.0.1 → lsst_felis-30.0.0rc3}/setup.cfg +0 -0
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lsst-felis
|
|
3
|
-
Version:
|
|
3
|
+
Version: 30.0.0rc3
|
|
4
4
|
Summary: A vocabulary for describing catalogs and acting on those descriptions
|
|
5
5
|
Author-email: Rubin Observatory Data Management <dm-admin@lists.lsst.org>
|
|
6
|
-
License:
|
|
6
|
+
License-Expression: GPL-3.0-or-later
|
|
7
7
|
Project-URL: Homepage, https://felis.lsst.io
|
|
8
8
|
Project-URL: Source, https://github.com/lsst/felis
|
|
9
9
|
Keywords: lsst
|
|
10
10
|
Classifier: Intended Audience :: Science/Research
|
|
11
|
-
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
12
11
|
Classifier: Operating System :: OS Independent
|
|
13
12
|
Classifier: Programming Language :: Python :: 3
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.11
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.12
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
17
|
Classifier: Topic :: Scientific/Engineering :: Astronomy
|
|
18
18
|
Requires-Python: >=3.11.0
|
|
19
19
|
Description-Content-Type: text/markdown
|
|
@@ -23,8 +23,8 @@ Requires-Dist: alembic
|
|
|
23
23
|
Requires-Dist: astropy
|
|
24
24
|
Requires-Dist: click
|
|
25
25
|
Requires-Dist: deepdiff
|
|
26
|
-
Requires-Dist: lsst-resources
|
|
27
|
-
Requires-Dist: lsst-utils
|
|
26
|
+
Requires-Dist: lsst-resources
|
|
27
|
+
Requires-Dist: lsst-utils
|
|
28
28
|
Requires-Dist: numpy
|
|
29
29
|
Requires-Dist: pydantic<3,>=2
|
|
30
30
|
Requires-Dist: pyyaml
|
|
@@ -5,19 +5,20 @@ build-backend = "setuptools.build_meta"
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "lsst-felis"
|
|
7
7
|
description = "A vocabulary for describing catalogs and acting on those descriptions"
|
|
8
|
-
license =
|
|
8
|
+
license = "GPL-3.0-or-later"
|
|
9
|
+
license-files = ["COPYRIGHT", "LICENSE"]
|
|
9
10
|
readme = "README.md"
|
|
10
11
|
authors = [
|
|
11
12
|
{name="Rubin Observatory Data Management", email="dm-admin@lists.lsst.org"},
|
|
12
13
|
]
|
|
13
14
|
classifiers = [
|
|
14
15
|
"Intended Audience :: Science/Research",
|
|
15
|
-
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
|
|
16
16
|
"Operating System :: OS Independent",
|
|
17
17
|
"Programming Language :: Python :: 3",
|
|
18
18
|
"Programming Language :: Python :: 3.11",
|
|
19
19
|
"Programming Language :: Python :: 3.12",
|
|
20
20
|
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Programming Language :: Python :: 3.14",
|
|
21
22
|
"Topic :: Scientific/Engineering :: Astronomy"
|
|
22
23
|
]
|
|
23
24
|
keywords = ["lsst"]
|
|
@@ -26,8 +27,8 @@ dependencies = [
|
|
|
26
27
|
"astropy",
|
|
27
28
|
"click",
|
|
28
29
|
"deepdiff",
|
|
29
|
-
"lsst-resources
|
|
30
|
-
"lsst-utils
|
|
30
|
+
"lsst-resources",
|
|
31
|
+
"lsst-utils",
|
|
31
32
|
"numpy",
|
|
32
33
|
"pydantic >=2,<3",
|
|
33
34
|
"pyyaml",
|
|
@@ -57,10 +58,9 @@ where = ["python"]
|
|
|
57
58
|
|
|
58
59
|
[tool.setuptools]
|
|
59
60
|
zip-safe = true
|
|
60
|
-
license-files = ["COPYRIGHT", "LICENSE"]
|
|
61
61
|
|
|
62
62
|
[tool.setuptools.package-data]
|
|
63
|
-
"felis" = ["py.typed", "
|
|
63
|
+
"felis" = ["py.typed", "config/tap_schema/*.yaml", "config/tap_schema/*.csv"]
|
|
64
64
|
|
|
65
65
|
[tool.setuptools.dynamic]
|
|
66
66
|
version = { attr = "lsst_versions.get_lsst_version" }
|
|
@@ -120,31 +120,15 @@ target-version = ["py311"]
|
|
|
120
120
|
profile = "black"
|
|
121
121
|
line_length = 110
|
|
122
122
|
|
|
123
|
-
[tool.lsst_versions]
|
|
124
|
-
write_to = "python/felis/version.py"
|
|
125
|
-
|
|
126
123
|
[tool.ruff]
|
|
127
124
|
line-length = 110
|
|
128
125
|
target-version = "py311"
|
|
129
126
|
exclude = [
|
|
130
|
-
"__init__.py"
|
|
131
|
-
"lex.py",
|
|
132
|
-
"yacc.py",
|
|
127
|
+
"__init__.py"
|
|
133
128
|
]
|
|
134
129
|
|
|
135
130
|
[tool.ruff.lint]
|
|
136
131
|
ignore = [
|
|
137
|
-
"D100",
|
|
138
|
-
"D102",
|
|
139
|
-
"D104",
|
|
140
|
-
"D105",
|
|
141
|
-
"D107",
|
|
142
|
-
"D200",
|
|
143
|
-
"D203",
|
|
144
|
-
"D205",
|
|
145
|
-
"D213",
|
|
146
|
-
"D400",
|
|
147
|
-
"D413",
|
|
148
132
|
"N802",
|
|
149
133
|
"N803",
|
|
150
134
|
"N806",
|
|
@@ -152,7 +136,14 @@ ignore = [
|
|
|
152
136
|
"N815",
|
|
153
137
|
"N816",
|
|
154
138
|
"N999",
|
|
155
|
-
"
|
|
139
|
+
"D107",
|
|
140
|
+
"D105",
|
|
141
|
+
"D102",
|
|
142
|
+
"D104",
|
|
143
|
+
"D100",
|
|
144
|
+
"D200",
|
|
145
|
+
"D205",
|
|
146
|
+
"D400",
|
|
156
147
|
]
|
|
157
148
|
select = [
|
|
158
149
|
"E", # pycodestyle
|
|
@@ -161,8 +152,29 @@ select = [
|
|
|
161
152
|
"W", # pycodestyle
|
|
162
153
|
"D", # pydocstyle
|
|
163
154
|
"UP", # pyupgrade
|
|
155
|
+
"I", # isort
|
|
156
|
+
"RUF022", # sort __all__
|
|
157
|
+
]
|
|
158
|
+
# Commented out to suppress "unused noqa" in jenkins which has older ruff not
|
|
159
|
+
# generating E721.
|
|
160
|
+
extend-select = [
|
|
161
|
+
"RUF100", # Warn about unused noqa
|
|
164
162
|
]
|
|
165
163
|
|
|
164
|
+
[tool.ruff.lint.isort]
|
|
165
|
+
known-first-party = ["felis"]
|
|
166
|
+
|
|
167
|
+
[tool.ruff.lint.pycodestyle]
|
|
168
|
+
max-doc-length = 79
|
|
169
|
+
|
|
170
|
+
[tool.ruff.lint.pydocstyle]
|
|
171
|
+
convention = "numpy"
|
|
172
|
+
|
|
173
|
+
[tool.ruff.format]
|
|
174
|
+
docstring-code-format = true
|
|
175
|
+
# Formatter does not know about indenting.
|
|
176
|
+
docstring-code-line-length = 69
|
|
177
|
+
|
|
166
178
|
[tool.pydocstyle]
|
|
167
179
|
convention = "numpy"
|
|
168
180
|
# Our coding style does not require docstrings for magic methods (D105)
|
|
@@ -171,13 +183,16 @@ convention = "numpy"
|
|
|
171
183
|
# Docstring at the very first line is not required
|
|
172
184
|
# D200, D205 and D400 all complain if the first sentence of the docstring does
|
|
173
185
|
# not fit on one line. We do not require docstrings in __init__ files (D104).
|
|
174
|
-
add-ignore = [
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
186
|
+
add-ignore = [
|
|
187
|
+
"D107",
|
|
188
|
+
"D105",
|
|
189
|
+
"D102",
|
|
190
|
+
"D100",
|
|
191
|
+
"D200",
|
|
192
|
+
"D205",
|
|
193
|
+
"D400",
|
|
194
|
+
"D104",
|
|
195
|
+
]
|
|
181
196
|
|
|
182
197
|
[tool.numpydoc_validation]
|
|
183
198
|
checks = [
|
|
@@ -189,11 +204,14 @@ checks = [
|
|
|
189
204
|
"GL01", # Summary text can start on same line as """
|
|
190
205
|
"GL08", # Do not require docstring.
|
|
191
206
|
"ES01", # No extended summary required.
|
|
207
|
+
"PR04", # Do not require types on parameters.
|
|
208
|
+
"RT01", # Unfortunately our @property trigger this.
|
|
209
|
+
"RT02", # Does not want named return value. DM style says we do.
|
|
192
210
|
"SS05", # pydocstyle is better at finding infinitive verb.
|
|
193
|
-
"PR04", # Sphinx does not require parameter type.
|
|
194
211
|
]
|
|
195
212
|
exclude = [
|
|
196
213
|
"^test_.*", # Do not test docstrings in test code.
|
|
214
|
+
'^cli', # This is the main click command
|
|
197
215
|
'^__init__$',
|
|
198
216
|
'\._[a-zA-Z_]+$', # Private methods.
|
|
199
217
|
]
|
|
@@ -19,8 +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 .datamodel import
|
|
23
|
-
|
|
24
|
-
from .
|
|
25
|
-
|
|
26
|
-
|
|
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"
|
|
@@ -29,16 +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 .db.utils import DatabaseContext, is_mock_url
|
|
35
|
+
from .db.database_context import create_database_context
|
|
39
36
|
from .diff import DatabaseDiff, FormattedSchemaDiff, SchemaDiff
|
|
40
|
-
from .metadata import
|
|
41
|
-
from .tap_schema import DataLoader, TableManager
|
|
37
|
+
from .metadata import create_metadata
|
|
38
|
+
from .tap_schema import DataLoader, MetadataInserter, TableManager
|
|
42
39
|
|
|
43
40
|
__all__ = ["cli"]
|
|
44
41
|
|
|
@@ -63,7 +60,10 @@ loglevel_choices = ["CRITICAL", "FATAL", "ERROR", "WARNING", "INFO", "DEBUG"]
|
|
|
63
60
|
help="Felis log file path",
|
|
64
61
|
)
|
|
65
62
|
@click.option(
|
|
66
|
-
"--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,
|
|
67
67
|
)
|
|
68
68
|
@click.pass_context
|
|
69
69
|
def cli(ctx: click.Context, log_level: str, log_file: str | None, id_generation: bool) -> None:
|
|
@@ -72,6 +72,8 @@ def cli(ctx: click.Context, log_level: str, log_file: str | None, id_generation:
|
|
|
72
72
|
ctx.obj["id_generation"] = id_generation
|
|
73
73
|
if ctx.obj["id_generation"]:
|
|
74
74
|
logger.info("ID generation is enabled")
|
|
75
|
+
else:
|
|
76
|
+
logger.info("ID generation is disabled")
|
|
75
77
|
if log_file:
|
|
76
78
|
logging.basicConfig(filename=log_file, level=log_level)
|
|
77
79
|
else:
|
|
@@ -95,6 +97,7 @@ def cli(ctx: click.Context, log_level: str, log_file: str | None, id_generation:
|
|
|
95
97
|
"--output-file", "-o", type=click.File(mode="w"), help="Write SQL commands to a file instead of executing"
|
|
96
98
|
)
|
|
97
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")
|
|
98
101
|
@click.argument("file", type=click.File())
|
|
99
102
|
@click.pass_context
|
|
100
103
|
def create(
|
|
@@ -107,6 +110,7 @@ def create(
|
|
|
107
110
|
dry_run: bool,
|
|
108
111
|
output_file: IO[str] | None,
|
|
109
112
|
ignore_constraints: bool,
|
|
113
|
+
skip_indexes: bool,
|
|
110
114
|
file: IO[str],
|
|
111
115
|
) -> None:
|
|
112
116
|
"""Create database objects from the Felis file.
|
|
@@ -129,65 +133,141 @@ def create(
|
|
|
129
133
|
Write SQL commands to a file instead of executing.
|
|
130
134
|
ignore_constraints
|
|
131
135
|
Ignore constraints when creating tables.
|
|
136
|
+
skip_indexes
|
|
137
|
+
Skip creating indexes when building metadata.
|
|
132
138
|
file
|
|
133
139
|
Felis file to read.
|
|
134
140
|
"""
|
|
135
141
|
try:
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if not url.host and not url.drivername == "sqlite":
|
|
145
|
-
dry_run = True
|
|
146
|
-
logger.info("Forcing dry run for non-sqlite engine URL with no host")
|
|
147
|
-
|
|
148
|
-
metadata = MetaDataBuilder(schema, ignore_constraints=ignore_constraints).build()
|
|
149
|
-
logger.debug(f"Created metadata with schema name: {metadata.schema}")
|
|
150
|
-
|
|
151
|
-
engine: Engine | MockConnection
|
|
152
|
-
if not dry_run and not output_file:
|
|
153
|
-
engine = create_engine(url, echo=echo)
|
|
154
|
-
else:
|
|
155
|
-
if dry_run:
|
|
156
|
-
logger.info("Dry run will be executed")
|
|
157
|
-
engine = DatabaseContext.create_mock_engine(url, output_file)
|
|
158
|
-
if output_file:
|
|
159
|
-
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
|
+
)
|
|
160
150
|
|
|
161
|
-
|
|
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")
|
|
162
160
|
|
|
163
|
-
|
|
164
|
-
|
|
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.
|
|
165
165
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
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()
|
|
170
169
|
|
|
171
|
-
|
|
172
|
-
logger.debug("Creating schema if not exists")
|
|
173
|
-
context.initialize()
|
|
170
|
+
db_ctx.create_all()
|
|
174
171
|
|
|
175
|
-
context.create_all()
|
|
176
172
|
except Exception as e:
|
|
177
173
|
logger.exception(e)
|
|
178
174
|
raise click.ClickException(str(e))
|
|
179
175
|
|
|
180
176
|
|
|
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,
|
|
184
|
+
engine_url: str,
|
|
185
|
+
schema_name: str | None,
|
|
186
|
+
file: IO[str],
|
|
187
|
+
) -> None:
|
|
188
|
+
"""Create indexes from a Felis YAML file in a target database.
|
|
189
|
+
|
|
190
|
+
Parameters
|
|
191
|
+
----------
|
|
192
|
+
engine_url
|
|
193
|
+
SQLAlchemy Engine URL.
|
|
194
|
+
file
|
|
195
|
+
Felis file to read.
|
|
196
|
+
"""
|
|
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))
|
|
206
|
+
|
|
207
|
+
|
|
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")
|
|
211
|
+
@click.argument("file", type=click.File())
|
|
212
|
+
@click.pass_context
|
|
213
|
+
def drop_indexes(
|
|
214
|
+
ctx: click.Context,
|
|
215
|
+
engine_url: str,
|
|
216
|
+
schema_name: str | None,
|
|
217
|
+
file: IO[str],
|
|
218
|
+
) -> None:
|
|
219
|
+
"""Drop indexes from a Felis YAML file in a target database.
|
|
220
|
+
|
|
221
|
+
Parameters
|
|
222
|
+
----------
|
|
223
|
+
engine_url
|
|
224
|
+
SQLAlchemy Engine URL.
|
|
225
|
+
schema-name
|
|
226
|
+
Alternate schema name to override Felis file.
|
|
227
|
+
file
|
|
228
|
+
Felis file to read.
|
|
229
|
+
"""
|
|
230
|
+
try:
|
|
231
|
+
metadata = create_metadata(
|
|
232
|
+
file, id_generation=ctx.obj["id_generation"], schema_name=schema_name, engine_url=engine_url
|
|
233
|
+
)
|
|
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))
|
|
239
|
+
|
|
240
|
+
|
|
181
241
|
@cli.command("load-tap-schema", help="Load metadata from a Felis file into a TAP_SCHEMA database")
|
|
182
242
|
@click.option("--engine-url", envvar="FELIS_ENGINE_URL", help="SQLAlchemy Engine URL")
|
|
183
|
-
@click.option("--tap-schema-name", help="Name of the TAP_SCHEMA schema in the database (default: TAP_SCHEMA)")
|
|
184
243
|
@click.option(
|
|
185
|
-
"--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,
|
|
186
270
|
)
|
|
187
|
-
@click.option("--tap-schema-index", type=int, help="TAP_SCHEMA index of the schema in this environment")
|
|
188
|
-
@click.option("--dry-run", is_flag=True, help="Execute dry run only. Does not insert any data.")
|
|
189
|
-
@click.option("--echo", is_flag=True, help="Print out the generated insert statements to stdout")
|
|
190
|
-
@click.option("--output-file", type=click.Path(), help="Write SQL commands to a file")
|
|
191
271
|
@click.argument("file", type=click.File())
|
|
192
272
|
@click.pass_context
|
|
193
273
|
def load_tap_schema(
|
|
@@ -198,7 +278,9 @@ def load_tap_schema(
|
|
|
198
278
|
tap_schema_index: int,
|
|
199
279
|
dry_run: bool,
|
|
200
280
|
echo: bool,
|
|
201
|
-
output_file: str | None,
|
|
281
|
+
output_file: IO[str] | None,
|
|
282
|
+
force_unbounded_arraysize: bool,
|
|
283
|
+
unique_keys: bool,
|
|
202
284
|
file: IO[str],
|
|
203
285
|
) -> None:
|
|
204
286
|
"""Load TAP metadata from a Felis file.
|
|
@@ -225,41 +307,68 @@ def load_tap_schema(
|
|
|
225
307
|
The TAP_SCHEMA database must already exist or the command will fail. This
|
|
226
308
|
command will not initialize the TAP_SCHEMA tables.
|
|
227
309
|
"""
|
|
228
|
-
|
|
229
|
-
engine: Engine | MockConnection
|
|
230
|
-
if dry_run or is_mock_url(url):
|
|
231
|
-
engine = create_mock_engine(url, executor=None)
|
|
232
|
-
else:
|
|
233
|
-
engine = create_engine(engine_url)
|
|
310
|
+
# Create TableManager with automatic dialect detection
|
|
234
311
|
mgr = TableManager(
|
|
235
|
-
|
|
236
|
-
apply_schema_to_metadata=False if engine.dialect.name == "sqlite" else True,
|
|
312
|
+
engine_url=engine_url,
|
|
237
313
|
schema_name=tap_schema_name,
|
|
238
314
|
table_name_postfix=tap_tables_postfix,
|
|
239
315
|
)
|
|
240
316
|
|
|
241
|
-
|
|
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
|
+
)
|
|
242
328
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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()
|
|
252
339
|
|
|
253
340
|
|
|
254
341
|
@cli.command("init-tap-schema", help="Initialize a standard TAP_SCHEMA database")
|
|
255
|
-
@click.option("--engine-url", envvar="FELIS_ENGINE_URL", help="SQLAlchemy Engine URL")
|
|
342
|
+
@click.option("--engine-url", envvar="FELIS_ENGINE_URL", help="SQLAlchemy Engine URL", required=True)
|
|
256
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
|
+
)
|
|
257
355
|
@click.option(
|
|
258
356
|
"--tap-tables-postfix", help="Postfix which is applied to standard TAP_SCHEMA table names", default=""
|
|
259
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
|
+
)
|
|
260
364
|
@click.pass_context
|
|
261
365
|
def init_tap_schema(
|
|
262
|
-
ctx: click.Context,
|
|
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,
|
|
263
372
|
) -> None:
|
|
264
373
|
"""Initialize a standard TAP_SCHEMA database.
|
|
265
374
|
|
|
@@ -269,20 +378,28 @@ def init_tap_schema(
|
|
|
269
378
|
SQLAlchemy Engine URL.
|
|
270
379
|
tap_schema_name
|
|
271
380
|
Name of the TAP_SCHEMA schema in the database.
|
|
381
|
+
extensions
|
|
382
|
+
Extensions YAML file.
|
|
272
383
|
tap_tables_postfix
|
|
273
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.
|
|
274
389
|
"""
|
|
275
|
-
|
|
276
|
-
engine: Engine | MockConnection
|
|
277
|
-
if is_mock_url(url):
|
|
278
|
-
raise click.ClickException("Mock engine URL is not supported for this command")
|
|
279
|
-
engine = create_engine(engine_url)
|
|
390
|
+
# Create TableManager with automatic dialect detection
|
|
280
391
|
mgr = TableManager(
|
|
281
|
-
|
|
392
|
+
engine_url=engine_url,
|
|
282
393
|
schema_name=tap_schema_name,
|
|
283
394
|
table_name_postfix=tap_tables_postfix,
|
|
395
|
+
extensions_path=extensions,
|
|
284
396
|
)
|
|
285
|
-
|
|
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()
|
|
286
403
|
|
|
287
404
|
|
|
288
405
|
@cli.command("validate", help="Validate one or more Felis YAML files")
|
|
@@ -395,24 +512,37 @@ def diff(
|
|
|
395
512
|
error_on_change: bool,
|
|
396
513
|
files: Iterable[IO[str]],
|
|
397
514
|
) -> None:
|
|
515
|
+
files_list = list(files)
|
|
398
516
|
schemas = [
|
|
399
|
-
Schema.from_stream(file, context={"id_generation": ctx.obj["id_generation"]}) for file in
|
|
517
|
+
Schema.from_stream(file, context={"id_generation": ctx.obj["id_generation"]}) for file in files_list
|
|
400
518
|
]
|
|
401
|
-
|
|
402
519
|
diff: SchemaDiff
|
|
403
|
-
if len(schemas) == 2
|
|
520
|
+
if len(schemas) == 2:
|
|
404
521
|
if comparator == "alembic":
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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)
|
|
408
533
|
else:
|
|
409
534
|
diff = FormattedSchemaDiff(schemas[0], schemas[1])
|
|
410
535
|
elif len(schemas) == 1 and engine_url is not None:
|
|
411
|
-
|
|
412
|
-
|
|
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)
|
|
413
543
|
else:
|
|
414
544
|
raise click.ClickException(
|
|
415
|
-
"Invalid arguments - provide two schemas or a schema and a database engine URL"
|
|
545
|
+
"Invalid arguments - provide two schemas or a single schema and a database engine URL"
|
|
416
546
|
)
|
|
417
547
|
|
|
418
548
|
diff.print()
|
|
@@ -421,5 +551,46 @@ def diff(
|
|
|
421
551
|
raise click.ClickException("Schema was changed")
|
|
422
552
|
|
|
423
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
|
+
|
|
424
595
|
if __name__ == "__main__":
|
|
425
596
|
cli()
|