lsst-felis 26.2024.1700__tar.gz → 27.0.0rc1__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.
Potentially problematic release.
This version of lsst-felis might be problematic. Click here for more details.
- {lsst_felis-26.2024.1700/python/lsst_felis.egg-info → lsst_felis-27.0.0rc1}/PKG-INFO +2 -5
- {lsst_felis-26.2024.1700 → lsst_felis-27.0.0rc1}/pyproject.toml +6 -5
- {lsst_felis-26.2024.1700 → lsst_felis-27.0.0rc1}/python/felis/cli.py +38 -20
- {lsst_felis-26.2024.1700 → lsst_felis-27.0.0rc1}/python/felis/datamodel.py +27 -28
- {lsst_felis-26.2024.1700 → lsst_felis-27.0.0rc1}/python/felis/db/sqltypes.py +1 -1
- {lsst_felis-26.2024.1700 → lsst_felis-27.0.0rc1}/python/felis/metadata.py +9 -3
- {lsst_felis-26.2024.1700 → lsst_felis-27.0.0rc1}/python/felis/tap.py +80 -67
- lsst_felis-27.0.0rc1/python/felis/version.py +2 -0
- {lsst_felis-26.2024.1700 → lsst_felis-27.0.0rc1/python/lsst_felis.egg-info}/PKG-INFO +2 -5
- {lsst_felis-26.2024.1700 → lsst_felis-27.0.0rc1}/python/lsst_felis.egg-info/requires.txt +0 -3
- {lsst_felis-26.2024.1700 → lsst_felis-27.0.0rc1}/tests/test_datatypes.py +6 -5
- {lsst_felis-26.2024.1700 → lsst_felis-27.0.0rc1}/tests/test_tap.py +10 -5
- lsst_felis-26.2024.1700/python/felis/version.py +0 -2
- {lsst_felis-26.2024.1700 → lsst_felis-27.0.0rc1}/COPYRIGHT +0 -0
- {lsst_felis-26.2024.1700 → lsst_felis-27.0.0rc1}/LICENSE +0 -0
- {lsst_felis-26.2024.1700 → lsst_felis-27.0.0rc1}/README.rst +0 -0
- {lsst_felis-26.2024.1700 → lsst_felis-27.0.0rc1}/python/felis/__init__.py +0 -0
- {lsst_felis-26.2024.1700 → lsst_felis-27.0.0rc1}/python/felis/check.py +0 -0
- {lsst_felis-26.2024.1700 → lsst_felis-27.0.0rc1}/python/felis/db/__init__.py +0 -0
- {lsst_felis-26.2024.1700 → lsst_felis-27.0.0rc1}/python/felis/db/_variants.py +0 -0
- {lsst_felis-26.2024.1700 → lsst_felis-27.0.0rc1}/python/felis/py.typed +0 -0
- {lsst_felis-26.2024.1700 → lsst_felis-27.0.0rc1}/python/felis/simple.py +0 -0
- {lsst_felis-26.2024.1700 → lsst_felis-27.0.0rc1}/python/felis/types.py +0 -0
- {lsst_felis-26.2024.1700 → lsst_felis-27.0.0rc1}/python/felis/utils.py +0 -0
- {lsst_felis-26.2024.1700 → lsst_felis-27.0.0rc1}/python/felis/validation.py +0 -0
- {lsst_felis-26.2024.1700 → lsst_felis-27.0.0rc1}/python/felis/visitor.py +0 -0
- {lsst_felis-26.2024.1700 → lsst_felis-27.0.0rc1}/python/lsst_felis.egg-info/SOURCES.txt +0 -0
- {lsst_felis-26.2024.1700 → lsst_felis-27.0.0rc1}/python/lsst_felis.egg-info/dependency_links.txt +0 -0
- {lsst_felis-26.2024.1700 → lsst_felis-27.0.0rc1}/python/lsst_felis.egg-info/entry_points.txt +0 -0
- {lsst_felis-26.2024.1700 → lsst_felis-27.0.0rc1}/python/lsst_felis.egg-info/top_level.txt +0 -0
- {lsst_felis-26.2024.1700 → lsst_felis-27.0.0rc1}/python/lsst_felis.egg-info/zip-safe +0 -0
- {lsst_felis-26.2024.1700 → lsst_felis-27.0.0rc1}/setup.cfg +0 -0
- {lsst_felis-26.2024.1700 → lsst_felis-27.0.0rc1}/tests/test_check.py +0 -0
- {lsst_felis-26.2024.1700 → lsst_felis-27.0.0rc1}/tests/test_cli.py +0 -0
- {lsst_felis-26.2024.1700 → lsst_felis-27.0.0rc1}/tests/test_datamodel.py +0 -0
- {lsst_felis-26.2024.1700 → lsst_felis-27.0.0rc1}/tests/test_metadata.py +0 -0
- {lsst_felis-26.2024.1700 → lsst_felis-27.0.0rc1}/tests/test_simple.py +0 -0
- {lsst_felis-26.2024.1700 → lsst_felis-27.0.0rc1}/tests/test_utils.py +0 -0
- {lsst_felis-26.2024.1700 → lsst_felis-27.0.0rc1}/tests/test_validation.py +0 -0
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: lsst-felis
|
|
3
|
-
Version:
|
|
3
|
+
Version: 27.0.0rc1
|
|
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
6
|
License: GNU General Public License v3 or later (GPLv3+)
|
|
7
|
-
Project-URL: Homepage, https://
|
|
8
|
-
Project-URL: Source, https://github.com/lsst/felis
|
|
7
|
+
Project-URL: Homepage, https://github.com/lsst/felis
|
|
9
8
|
Keywords: lsst
|
|
10
9
|
Classifier: Intended Audience :: Science/Research
|
|
11
10
|
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
@@ -27,5 +26,3 @@ Requires-Dist: pydantic<3,>=2
|
|
|
27
26
|
Requires-Dist: lsst-utils
|
|
28
27
|
Provides-Extra: test
|
|
29
28
|
Requires-Dist: pytest>=3.2; extra == "test"
|
|
30
|
-
Provides-Extra: dev
|
|
31
|
-
Requires-Dist: documenteer[guide]; extra == "dev"
|
|
@@ -33,16 +33,12 @@ requires-python = ">=3.11.0"
|
|
|
33
33
|
dynamic = ["version"]
|
|
34
34
|
|
|
35
35
|
[project.urls]
|
|
36
|
-
Homepage = "https://
|
|
37
|
-
Source = "https://github.com/lsst/felis"
|
|
36
|
+
"Homepage" = "https://github.com/lsst/felis"
|
|
38
37
|
|
|
39
38
|
[project.optional-dependencies]
|
|
40
39
|
test = [
|
|
41
40
|
"pytest >= 3.2"
|
|
42
41
|
]
|
|
43
|
-
dev = [
|
|
44
|
-
"documenteer[guide]"
|
|
45
|
-
]
|
|
46
42
|
|
|
47
43
|
[tool.pytest.ini_options]
|
|
48
44
|
|
|
@@ -147,6 +143,11 @@ select = [
|
|
|
147
143
|
"D", # pydocstyle
|
|
148
144
|
]
|
|
149
145
|
target-version = "py311"
|
|
146
|
+
# Commented out to suppress "unused noqa" in jenkins which has older ruff not
|
|
147
|
+
# generating E721.
|
|
148
|
+
extend-select = [
|
|
149
|
+
"RUF100", # Warn about unused noqa
|
|
150
|
+
]
|
|
150
151
|
|
|
151
152
|
[tool.pydeps]
|
|
152
153
|
max_bacon = 2
|
|
@@ -183,7 +183,6 @@ def init_tap(
|
|
|
183
183
|
@click.option("--tap-columns-table", help="Alt Table Name for TAP_SCHEMA.columns")
|
|
184
184
|
@click.option("--tap-keys-table", help="Alt Table Name for TAP_SCHEMA.keys")
|
|
185
185
|
@click.option("--tap-key-columns-table", help="Alt Table Name for TAP_SCHEMA.key_columns")
|
|
186
|
-
@click.option("--tap-schema-index", type=int, help="TAP_SCHEMA index of the schema")
|
|
187
186
|
@click.argument("file", type=click.File())
|
|
188
187
|
def load_tap(
|
|
189
188
|
engine_url: str,
|
|
@@ -197,7 +196,6 @@ def load_tap(
|
|
|
197
196
|
tap_columns_table: str,
|
|
198
197
|
tap_keys_table: str,
|
|
199
198
|
tap_key_columns_table: str,
|
|
200
|
-
tap_schema_index: int,
|
|
201
199
|
file: io.TextIOBase,
|
|
202
200
|
) -> None:
|
|
203
201
|
"""Load TAP metadata from a Felis FILE.
|
|
@@ -205,8 +203,28 @@ def load_tap(
|
|
|
205
203
|
This command loads the associated TAP metadata from a Felis FILE
|
|
206
204
|
to the TAP_SCHEMA tables.
|
|
207
205
|
"""
|
|
208
|
-
|
|
209
|
-
|
|
206
|
+
top_level_object = yaml.load(file, Loader=yaml.SafeLoader)
|
|
207
|
+
schema_obj: dict
|
|
208
|
+
if isinstance(top_level_object, dict):
|
|
209
|
+
schema_obj = top_level_object
|
|
210
|
+
if "@graph" not in schema_obj:
|
|
211
|
+
schema_obj["@type"] = "felis:Schema"
|
|
212
|
+
schema_obj["@context"] = DEFAULT_CONTEXT
|
|
213
|
+
elif isinstance(top_level_object, list):
|
|
214
|
+
schema_obj = {"@context": DEFAULT_CONTEXT, "@graph": top_level_object}
|
|
215
|
+
else:
|
|
216
|
+
logger.error("Schema object not of recognizable type")
|
|
217
|
+
raise click.exceptions.Exit(1)
|
|
218
|
+
|
|
219
|
+
normalized = _normalize(schema_obj, embed="@always")
|
|
220
|
+
if len(normalized["@graph"]) > 1 and (schema_name or catalog_name):
|
|
221
|
+
logger.error("--schema-name and --catalog-name incompatible with multiple schemas")
|
|
222
|
+
raise click.exceptions.Exit(1)
|
|
223
|
+
|
|
224
|
+
# Force normalized["@graph"] to a list, which is what happens when there's
|
|
225
|
+
# multiple schemas
|
|
226
|
+
if isinstance(normalized["@graph"], dict):
|
|
227
|
+
normalized["@graph"] = [normalized["@graph"]]
|
|
210
228
|
|
|
211
229
|
tap_tables = init_tables(
|
|
212
230
|
tap_schema_name,
|
|
@@ -225,28 +243,28 @@ def load_tap(
|
|
|
225
243
|
# In Memory SQLite - Mostly used to test
|
|
226
244
|
Tap11Base.metadata.create_all(engine)
|
|
227
245
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
246
|
+
for schema in normalized["@graph"]:
|
|
247
|
+
tap_visitor = TapLoadingVisitor(
|
|
248
|
+
engine,
|
|
249
|
+
catalog_name=catalog_name,
|
|
250
|
+
schema_name=schema_name,
|
|
251
|
+
tap_tables=tap_tables,
|
|
252
|
+
)
|
|
253
|
+
tap_visitor.visit_schema(schema)
|
|
236
254
|
else:
|
|
237
255
|
_insert_dump = InsertDump()
|
|
238
256
|
conn = create_mock_engine(make_url(engine_url), executor=_insert_dump.dump, paramstyle="pyformat")
|
|
239
257
|
# After the engine is created, update the executor with the dialect
|
|
240
258
|
_insert_dump.dialect = conn.dialect
|
|
241
259
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
260
|
+
for schema in normalized["@graph"]:
|
|
261
|
+
tap_visitor = TapLoadingVisitor.from_mock_connection(
|
|
262
|
+
conn,
|
|
263
|
+
catalog_name=catalog_name,
|
|
264
|
+
schema_name=schema_name,
|
|
265
|
+
tap_tables=tap_tables,
|
|
266
|
+
)
|
|
267
|
+
tap_visitor.visit_schema(schema)
|
|
250
268
|
|
|
251
269
|
|
|
252
270
|
@cli.command("modify-tap")
|
|
@@ -93,20 +93,18 @@ class BaseObject(BaseModel):
|
|
|
93
93
|
description: DescriptionStr | None = None
|
|
94
94
|
"""A description of the database object."""
|
|
95
95
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
@model_validator(mode="after")
|
|
100
|
-
def check_description(self, info: ValidationInfo) -> BaseObject:
|
|
96
|
+
@model_validator(mode="after") # type: ignore[arg-type]
|
|
97
|
+
@classmethod
|
|
98
|
+
def check_description(cls, object: BaseObject, info: ValidationInfo) -> BaseObject:
|
|
101
99
|
"""Check that the description is present if required."""
|
|
102
100
|
context = info.context
|
|
103
101
|
if not context or not context.get("require_description", False):
|
|
104
|
-
return
|
|
105
|
-
if
|
|
102
|
+
return object
|
|
103
|
+
if object.description is None or object.description == "":
|
|
106
104
|
raise ValueError("Description is required and must be non-empty")
|
|
107
|
-
if len(
|
|
105
|
+
if len(object.description) < DESCR_MIN_LENGTH:
|
|
108
106
|
raise ValueError(f"Description must be at least {DESCR_MIN_LENGTH} characters long")
|
|
109
|
-
return
|
|
107
|
+
return object
|
|
110
108
|
|
|
111
109
|
|
|
112
110
|
class DataType(StrEnum):
|
|
@@ -181,8 +179,13 @@ class Column(BaseObject):
|
|
|
181
179
|
length: int | None = None
|
|
182
180
|
"""The length of the column."""
|
|
183
181
|
|
|
184
|
-
nullable: bool =
|
|
185
|
-
"""Whether the column can be ``NULL``.
|
|
182
|
+
nullable: bool | None = None
|
|
183
|
+
"""Whether the column can be ``NULL``.
|
|
184
|
+
|
|
185
|
+
If `None`, this value was not set explicitly in the YAML data. In this
|
|
186
|
+
case, it will be set to `False` for columns with numeric types and `True`
|
|
187
|
+
otherwise.
|
|
188
|
+
"""
|
|
186
189
|
|
|
187
190
|
value: Any = None
|
|
188
191
|
"""The default value of the column."""
|
|
@@ -219,12 +222,12 @@ class Column(BaseObject):
|
|
|
219
222
|
"""TAP_SCHEMA indication that this column is defined by an IVOA standard.
|
|
220
223
|
"""
|
|
221
224
|
|
|
225
|
+
votable_utype: str | None = Field(None, alias="votable:utype")
|
|
226
|
+
"""The VOTable utype (usage-specific or unique type) of the column."""
|
|
227
|
+
|
|
222
228
|
votable_xtype: str | None = Field(None, alias="votable:xtype")
|
|
223
229
|
"""The VOTable xtype (extended type) of the column."""
|
|
224
230
|
|
|
225
|
-
votable_datatype: str | None = Field(None, alias="votable:datatype")
|
|
226
|
-
"""The VOTable datatype of the column."""
|
|
227
|
-
|
|
228
231
|
@field_validator("ivoa_ucd")
|
|
229
232
|
@classmethod
|
|
230
233
|
def check_ivoa_ucd(cls, ivoa_ucd: str) -> str:
|
|
@@ -284,26 +287,22 @@ class Column(BaseObject):
|
|
|
284
287
|
db_datatype_obj = string_to_typeengine(datatype_string, dialect, length)
|
|
285
288
|
if datatype_obj.compile(dialect) == db_datatype_obj.compile(dialect):
|
|
286
289
|
raise ValueError(
|
|
287
|
-
"'{}: {}' is
|
|
288
|
-
db_annotation,
|
|
289
|
-
datatype_string,
|
|
290
|
-
col.datatype,
|
|
291
|
-
col.id,
|
|
292
|
-
"" if length is None else f" with length {length}",
|
|
290
|
+
"'{}: {}' is the same as 'datatype: {}' in column '{}'".format(
|
|
291
|
+
db_annotation, datatype_string, col.datatype, col.id
|
|
293
292
|
)
|
|
294
293
|
)
|
|
295
294
|
else:
|
|
296
295
|
logger.debug(
|
|
297
|
-
"
|
|
298
|
-
|
|
299
|
-
col.datatype,
|
|
300
|
-
db_annotation,
|
|
301
|
-
datatype_string,
|
|
302
|
-
col.id,
|
|
303
|
-
datatype_obj.compile(dialect),
|
|
304
|
-
db_datatype_obj.compile(dialect),
|
|
296
|
+
"Valid type override of 'datatype: {}' with '{}: {}' in column '{}'".format(
|
|
297
|
+
col.datatype, db_annotation, datatype_string, col.id
|
|
305
298
|
)
|
|
306
299
|
)
|
|
300
|
+
logger.debug(
|
|
301
|
+
"Compiled datatype '{}' with {} compiled override '{}'".format(
|
|
302
|
+
datatype_obj.compile(dialect), dialect_name, db_datatype_obj.compile(dialect)
|
|
303
|
+
)
|
|
304
|
+
)
|
|
305
|
+
|
|
307
306
|
return col
|
|
308
307
|
|
|
309
308
|
|
|
@@ -47,7 +47,7 @@ def compile_tinyint(type_: Any, compiler: Any, **kw: Any) -> str:
|
|
|
47
47
|
|
|
48
48
|
_TypeMap = Mapping[str, types.TypeEngine | type[types.TypeEngine]]
|
|
49
49
|
|
|
50
|
-
boolean_map: _TypeMap = {MYSQL: mysql.
|
|
50
|
+
boolean_map: _TypeMap = {MYSQL: mysql.BIT(1), ORACLE: oracle.NUMBER(1), POSTGRES: postgresql.BOOLEAN()}
|
|
51
51
|
|
|
52
52
|
byte_map: _TypeMap = {
|
|
53
53
|
MYSQL: mysql.TINYINT(),
|
|
@@ -34,6 +34,7 @@ from sqlalchemy import (
|
|
|
34
34
|
ForeignKeyConstraint,
|
|
35
35
|
Index,
|
|
36
36
|
MetaData,
|
|
37
|
+
Numeric,
|
|
37
38
|
PrimaryKeyConstraint,
|
|
38
39
|
ResultProxy,
|
|
39
40
|
Table,
|
|
@@ -264,12 +265,17 @@ class MetaDataBuilder:
|
|
|
264
265
|
id = column_obj.id
|
|
265
266
|
description = column_obj.description
|
|
266
267
|
default = column_obj.value
|
|
267
|
-
nullable = column_obj.nullable
|
|
268
268
|
|
|
269
|
-
#
|
|
269
|
+
# Handle variant overrides for the column (e.g., "mysql:datatype").
|
|
270
270
|
datatype = get_datatype_with_variants(column_obj)
|
|
271
271
|
|
|
272
|
-
# Set
|
|
272
|
+
# Set default value of nullable based on column type and then whether
|
|
273
|
+
# it was explicitly provided in the schema data.
|
|
274
|
+
nullable = column_obj.nullable
|
|
275
|
+
if nullable is None:
|
|
276
|
+
nullable = False if isinstance(datatype, Numeric) else True
|
|
277
|
+
|
|
278
|
+
# Set autoincrement depending on if it was provided explicitly.
|
|
273
279
|
autoincrement: Literal["auto"] | bool = (
|
|
274
280
|
column_obj.autoincrement if column_obj.autoincrement is not None else "auto"
|
|
275
281
|
)
|
|
@@ -24,7 +24,7 @@ from __future__ import annotations
|
|
|
24
24
|
__all__ = ["Tap11Base", "TapLoadingVisitor", "init_tables"]
|
|
25
25
|
|
|
26
26
|
import logging
|
|
27
|
-
from collections.abc import Iterable, MutableMapping
|
|
27
|
+
from collections.abc import Iterable, Mapping, MutableMapping
|
|
28
28
|
from typing import Any
|
|
29
29
|
|
|
30
30
|
from sqlalchemy import Column, Integer, String
|
|
@@ -34,13 +34,14 @@ from sqlalchemy.orm import Session, declarative_base, sessionmaker
|
|
|
34
34
|
from sqlalchemy.schema import MetaData
|
|
35
35
|
from sqlalchemy.sql.expression import Insert, insert
|
|
36
36
|
|
|
37
|
-
from
|
|
38
|
-
|
|
39
|
-
from .datamodel import Constraint, Index, Schema, Table
|
|
37
|
+
from .check import FelisValidator
|
|
40
38
|
from .types import FelisType
|
|
39
|
+
from .visitor import Visitor
|
|
40
|
+
|
|
41
|
+
_Mapping = Mapping[str, Any]
|
|
41
42
|
|
|
42
43
|
Tap11Base: Any = declarative_base() # Any to avoid mypy mess with SA 2
|
|
43
|
-
logger = logging.getLogger(
|
|
44
|
+
logger = logging.getLogger("felis")
|
|
44
45
|
|
|
45
46
|
IDENTIFIER_LENGTH = 128
|
|
46
47
|
SMALL_FIELD_LENGTH = 32
|
|
@@ -132,7 +133,7 @@ def init_tables(
|
|
|
132
133
|
)
|
|
133
134
|
|
|
134
135
|
|
|
135
|
-
class TapLoadingVisitor:
|
|
136
|
+
class TapLoadingVisitor(Visitor[None, tuple, Tap11Base, None, tuple, None, None]):
|
|
136
137
|
"""Felis schema visitor for generating TAP schema.
|
|
137
138
|
|
|
138
139
|
Parameters
|
|
@@ -153,7 +154,6 @@ class TapLoadingVisitor:
|
|
|
153
154
|
catalog_name: str | None = None,
|
|
154
155
|
schema_name: str | None = None,
|
|
155
156
|
tap_tables: MutableMapping[str, Any] | None = None,
|
|
156
|
-
tap_schema_index: int | None = None,
|
|
157
157
|
):
|
|
158
158
|
self.graph_index: MutableMapping[str, Any] = {}
|
|
159
159
|
self.catalog_name = catalog_name
|
|
@@ -161,7 +161,7 @@ class TapLoadingVisitor:
|
|
|
161
161
|
self.engine = engine
|
|
162
162
|
self._mock_connection: MockConnection | None = None
|
|
163
163
|
self.tables = tap_tables or init_tables()
|
|
164
|
-
self.
|
|
164
|
+
self.checker = FelisValidator()
|
|
165
165
|
|
|
166
166
|
@classmethod
|
|
167
167
|
def from_mock_connection(
|
|
@@ -170,30 +170,30 @@ class TapLoadingVisitor:
|
|
|
170
170
|
catalog_name: str | None = None,
|
|
171
171
|
schema_name: str | None = None,
|
|
172
172
|
tap_tables: MutableMapping[str, Any] | None = None,
|
|
173
|
-
tap_schema_index: int | None = None,
|
|
174
173
|
) -> TapLoadingVisitor:
|
|
175
174
|
visitor = cls(engine=None, catalog_name=catalog_name, schema_name=schema_name, tap_tables=tap_tables)
|
|
176
175
|
visitor._mock_connection = mock_connection
|
|
177
|
-
visitor.tap_schema_index = tap_schema_index
|
|
178
176
|
return visitor
|
|
179
177
|
|
|
180
|
-
def visit_schema(self, schema_obj:
|
|
178
|
+
def visit_schema(self, schema_obj: _Mapping) -> None:
|
|
179
|
+
self.checker.check_schema(schema_obj)
|
|
180
|
+
if (version_obj := schema_obj.get("version")) is not None:
|
|
181
|
+
self.visit_schema_version(version_obj, schema_obj)
|
|
181
182
|
schema = self.tables["schemas"]()
|
|
182
183
|
# Override with default
|
|
183
|
-
self.schema_name = self.schema_name or schema_obj
|
|
184
|
+
self.schema_name = self.schema_name or schema_obj["name"]
|
|
184
185
|
|
|
185
186
|
schema.schema_name = self._schema_name()
|
|
186
|
-
schema.description = schema_obj.description
|
|
187
|
-
schema.utype = schema_obj.
|
|
188
|
-
schema.schema_index =
|
|
189
|
-
logger.debug("Set TAP_SCHEMA index: {}".format(self.tap_schema_index))
|
|
187
|
+
schema.description = schema_obj.get("description")
|
|
188
|
+
schema.utype = schema_obj.get("votable:utype")
|
|
189
|
+
schema.schema_index = int(schema_obj.get("tap:schema_index", 0))
|
|
190
190
|
|
|
191
191
|
if self.engine is not None:
|
|
192
192
|
session: Session = sessionmaker(self.engine)()
|
|
193
193
|
|
|
194
194
|
session.add(schema)
|
|
195
195
|
|
|
196
|
-
for table_obj in schema_obj
|
|
196
|
+
for table_obj in schema_obj["tables"]:
|
|
197
197
|
table, columns = self.visit_table(table_obj, schema_obj)
|
|
198
198
|
session.add(table)
|
|
199
199
|
session.add_all(columns)
|
|
@@ -202,8 +202,6 @@ class TapLoadingVisitor:
|
|
|
202
202
|
session.add_all(keys)
|
|
203
203
|
session.add_all(key_columns)
|
|
204
204
|
|
|
205
|
-
logger.debug("Committing TAP schema: %s", schema_obj.name)
|
|
206
|
-
logger.debug("TAP tables: %s", len(self.tables))
|
|
207
205
|
session.commit()
|
|
208
206
|
else:
|
|
209
207
|
logger.info("Dry run, not inserting into database")
|
|
@@ -213,7 +211,7 @@ class TapLoadingVisitor:
|
|
|
213
211
|
conn = self._mock_connection
|
|
214
212
|
conn.execute(_insert(self.tables["schemas"], schema))
|
|
215
213
|
|
|
216
|
-
for table_obj in schema_obj
|
|
214
|
+
for table_obj in schema_obj["tables"]:
|
|
217
215
|
table, columns = self.visit_table(table_obj, schema_obj)
|
|
218
216
|
conn.execute(_insert(self.tables["tables"], table))
|
|
219
217
|
for column in columns:
|
|
@@ -225,45 +223,56 @@ class TapLoadingVisitor:
|
|
|
225
223
|
for key_column in key_columns:
|
|
226
224
|
conn.execute(_insert(self.tables["key_columns"], key_column))
|
|
227
225
|
|
|
228
|
-
def visit_constraints(self, schema_obj:
|
|
226
|
+
def visit_constraints(self, schema_obj: _Mapping) -> tuple:
|
|
229
227
|
all_keys = []
|
|
230
228
|
all_key_columns = []
|
|
231
|
-
for table_obj in schema_obj
|
|
232
|
-
for c in table_obj.constraints:
|
|
233
|
-
key, key_columns = self.visit_constraint(c)
|
|
229
|
+
for table_obj in schema_obj["tables"]:
|
|
230
|
+
for c in table_obj.get("constraints", []):
|
|
231
|
+
key, key_columns = self.visit_constraint(c, table_obj)
|
|
234
232
|
if not key:
|
|
235
233
|
continue
|
|
236
234
|
all_keys.append(key)
|
|
237
235
|
all_key_columns += key_columns
|
|
238
236
|
return all_keys, all_key_columns
|
|
239
237
|
|
|
240
|
-
def
|
|
241
|
-
|
|
238
|
+
def visit_schema_version(
|
|
239
|
+
self, version_obj: str | Mapping[str, Any], schema_obj: Mapping[str, Any]
|
|
240
|
+
) -> None:
|
|
241
|
+
# Docstring is inherited.
|
|
242
|
+
|
|
243
|
+
# For now we ignore schema versioning completely, still do some checks.
|
|
244
|
+
self.checker.check_schema_version(version_obj, schema_obj)
|
|
245
|
+
|
|
246
|
+
def visit_table(self, table_obj: _Mapping, schema_obj: _Mapping) -> tuple:
|
|
247
|
+
self.checker.check_table(table_obj, schema_obj)
|
|
248
|
+
table_id = table_obj["@id"]
|
|
242
249
|
table = self.tables["tables"]()
|
|
243
250
|
table.schema_name = self._schema_name()
|
|
244
|
-
table.table_name = self._table_name(table_obj
|
|
251
|
+
table.table_name = self._table_name(table_obj["name"])
|
|
245
252
|
table.table_type = "table"
|
|
246
|
-
table.utype = table_obj.
|
|
247
|
-
table.description = table_obj.description
|
|
248
|
-
table.table_index =
|
|
253
|
+
table.utype = table_obj.get("votable:utype")
|
|
254
|
+
table.description = table_obj.get("description")
|
|
255
|
+
table.table_index = int(table_obj.get("tap:table_index", 0))
|
|
249
256
|
|
|
250
|
-
columns = [self.visit_column(c, table_obj) for c in table_obj
|
|
251
|
-
self.visit_primary_key(table_obj.
|
|
257
|
+
columns = [self.visit_column(c, table_obj) for c in table_obj["columns"]]
|
|
258
|
+
self.visit_primary_key(table_obj.get("primaryKey", []), table_obj)
|
|
252
259
|
|
|
253
|
-
for i in table_obj.indexes:
|
|
260
|
+
for i in table_obj.get("indexes", []):
|
|
254
261
|
self.visit_index(i, table)
|
|
255
262
|
|
|
256
263
|
self.graph_index[table_id] = table
|
|
257
264
|
return table, columns
|
|
258
265
|
|
|
259
|
-
def check_column(self, column_obj:
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
266
|
+
def check_column(self, column_obj: _Mapping, table_obj: _Mapping) -> None:
|
|
267
|
+
self.checker.check_column(column_obj, table_obj)
|
|
268
|
+
_id = column_obj["@id"]
|
|
269
|
+
# Guaranteed to exist at this point, for mypy use "" as default
|
|
270
|
+
datatype_name = column_obj.get("datatype", "")
|
|
271
|
+
felis_type = FelisType.felis_type(datatype_name)
|
|
263
272
|
if felis_type.is_sized:
|
|
264
273
|
# It is expected that both arraysize and length are fine for
|
|
265
274
|
# length types.
|
|
266
|
-
arraysize = column_obj.
|
|
275
|
+
arraysize = column_obj.get("votable:arraysize", column_obj.get("length"))
|
|
267
276
|
if arraysize is None:
|
|
268
277
|
logger.warning(
|
|
269
278
|
f"votable:arraysize and length for {_id} are None for type {datatype_name}. "
|
|
@@ -274,7 +283,7 @@ class TapLoadingVisitor:
|
|
|
274
283
|
# datetime types really should have a votable:arraysize, because
|
|
275
284
|
# they are converted to strings and the `length` is loosely to the
|
|
276
285
|
# string size
|
|
277
|
-
if not column_obj
|
|
286
|
+
if "votable:arraysize" not in column_obj:
|
|
278
287
|
logger.warning(
|
|
279
288
|
f"votable:arraysize for {_id} is None for type {datatype_name}. "
|
|
280
289
|
f'Using length "*". '
|
|
@@ -282,45 +291,47 @@ class TapLoadingVisitor:
|
|
|
282
291
|
"materialized datetime/timestamp strings."
|
|
283
292
|
)
|
|
284
293
|
|
|
285
|
-
def visit_column(self, column_obj:
|
|
286
|
-
self.check_column(column_obj)
|
|
287
|
-
column_id = column_obj
|
|
288
|
-
table_name = self._table_name(table_obj
|
|
294
|
+
def visit_column(self, column_obj: _Mapping, table_obj: _Mapping) -> Tap11Base:
|
|
295
|
+
self.check_column(column_obj, table_obj)
|
|
296
|
+
column_id = column_obj["@id"]
|
|
297
|
+
table_name = self._table_name(table_obj["name"])
|
|
289
298
|
|
|
290
299
|
column = self.tables["columns"]()
|
|
291
300
|
column.table_name = table_name
|
|
292
|
-
column.column_name = column_obj
|
|
301
|
+
column.column_name = column_obj["name"]
|
|
293
302
|
|
|
294
|
-
felis_datatype = column_obj
|
|
295
|
-
felis_type = FelisType.felis_type(felis_datatype
|
|
296
|
-
column.datatype = column_obj.
|
|
303
|
+
felis_datatype = column_obj["datatype"]
|
|
304
|
+
felis_type = FelisType.felis_type(felis_datatype)
|
|
305
|
+
column.datatype = column_obj.get("votable:datatype", felis_type.votable_name)
|
|
297
306
|
|
|
298
307
|
arraysize = None
|
|
299
308
|
if felis_type.is_sized:
|
|
300
|
-
|
|
309
|
+
# prefer votable:arraysize to length, fall back to `*`
|
|
310
|
+
arraysize = column_obj.get("votable:arraysize", column_obj.get("length", "*"))
|
|
301
311
|
if felis_type.is_timestamp:
|
|
302
|
-
arraysize = column_obj.
|
|
312
|
+
arraysize = column_obj.get("votable:arraysize", "*")
|
|
303
313
|
column.arraysize = arraysize
|
|
304
314
|
|
|
305
|
-
column.xtype = column_obj.
|
|
306
|
-
column.description = column_obj.description
|
|
307
|
-
column.utype = column_obj.
|
|
315
|
+
column.xtype = column_obj.get("votable:xtype")
|
|
316
|
+
column.description = column_obj.get("description")
|
|
317
|
+
column.utype = column_obj.get("votable:utype")
|
|
308
318
|
|
|
309
|
-
unit = column_obj.
|
|
319
|
+
unit = column_obj.get("ivoa:unit") or column_obj.get("fits:tunit")
|
|
310
320
|
column.unit = unit
|
|
311
|
-
column.ucd = column_obj.
|
|
321
|
+
column.ucd = column_obj.get("ivoa:ucd")
|
|
312
322
|
|
|
313
323
|
# We modify this after we process columns
|
|
314
324
|
column.indexed = 0
|
|
315
325
|
|
|
316
|
-
column.principal = column_obj.
|
|
317
|
-
column.std = column_obj.
|
|
318
|
-
column.column_index = column_obj.
|
|
326
|
+
column.principal = column_obj.get("tap:principal", 0)
|
|
327
|
+
column.std = column_obj.get("tap:std", 0)
|
|
328
|
+
column.column_index = column_obj.get("tap:column_index")
|
|
319
329
|
|
|
320
330
|
self.graph_index[column_id] = column
|
|
321
331
|
return column
|
|
322
332
|
|
|
323
|
-
def visit_primary_key(self, primary_key_obj: str | Iterable[str]
|
|
333
|
+
def visit_primary_key(self, primary_key_obj: str | Iterable[str], table_obj: _Mapping) -> None:
|
|
334
|
+
self.checker.check_primary_key(primary_key_obj, table_obj)
|
|
324
335
|
if primary_key_obj:
|
|
325
336
|
if isinstance(primary_key_obj, str):
|
|
326
337
|
primary_key_obj = [primary_key_obj]
|
|
@@ -330,18 +341,19 @@ class TapLoadingVisitor:
|
|
|
330
341
|
columns[0].indexed = 1
|
|
331
342
|
return None
|
|
332
343
|
|
|
333
|
-
def visit_constraint(self, constraint_obj:
|
|
334
|
-
|
|
344
|
+
def visit_constraint(self, constraint_obj: _Mapping, table_obj: _Mapping) -> tuple:
|
|
345
|
+
self.checker.check_constraint(constraint_obj, table_obj)
|
|
346
|
+
constraint_type = constraint_obj["@type"]
|
|
335
347
|
key = None
|
|
336
348
|
key_columns = []
|
|
337
349
|
if constraint_type == "ForeignKey":
|
|
338
|
-
constraint_name = constraint_obj
|
|
339
|
-
description = constraint_obj.description
|
|
340
|
-
utype = constraint_obj.
|
|
350
|
+
constraint_name = constraint_obj["name"]
|
|
351
|
+
description = constraint_obj.get("description")
|
|
352
|
+
utype = constraint_obj.get("votable:utype")
|
|
341
353
|
|
|
342
|
-
columns = [self.graph_index[
|
|
354
|
+
columns = [self.graph_index[col["@id"]] for col in constraint_obj.get("columns", [])]
|
|
343
355
|
refcolumns = [
|
|
344
|
-
self.graph_index[
|
|
356
|
+
self.graph_index[refcol["@id"]] for refcol in constraint_obj.get("referencedColumns", [])
|
|
345
357
|
]
|
|
346
358
|
|
|
347
359
|
table_name = None
|
|
@@ -374,8 +386,9 @@ class TapLoadingVisitor:
|
|
|
374
386
|
key_columns.append(key_column)
|
|
375
387
|
return key, key_columns
|
|
376
388
|
|
|
377
|
-
def visit_index(self, index_obj:
|
|
378
|
-
|
|
389
|
+
def visit_index(self, index_obj: _Mapping, table_obj: _Mapping) -> None:
|
|
390
|
+
self.checker.check_index(index_obj, table_obj)
|
|
391
|
+
columns = [self.graph_index[col["@id"]] for col in index_obj.get("columns", [])]
|
|
379
392
|
# if just one column and it's indexed, update the object
|
|
380
393
|
if len(columns) == 1:
|
|
381
394
|
columns[0].indexed = 1
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: lsst-felis
|
|
3
|
-
Version:
|
|
3
|
+
Version: 27.0.0rc1
|
|
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
6
|
License: GNU General Public License v3 or later (GPLv3+)
|
|
7
|
-
Project-URL: Homepage, https://
|
|
8
|
-
Project-URL: Source, https://github.com/lsst/felis
|
|
7
|
+
Project-URL: Homepage, https://github.com/lsst/felis
|
|
9
8
|
Keywords: lsst
|
|
10
9
|
Classifier: Intended Audience :: Science/Research
|
|
11
10
|
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
@@ -27,5 +26,3 @@ Requires-Dist: pydantic<3,>=2
|
|
|
27
26
|
Requires-Dist: lsst-utils
|
|
28
27
|
Provides-Extra: test
|
|
29
28
|
Requires-Dist: pytest>=3.2; extra == "test"
|
|
30
|
-
Provides-Extra: dev
|
|
31
|
-
Requires-Dist: documenteer[guide]; extra == "dev"
|
|
@@ -61,6 +61,9 @@ class RedundantDatatypesTest(unittest.TestCase):
|
|
|
61
61
|
with self.assertRaises(ValidationError):
|
|
62
62
|
coldata.col("int", "INTEGER")
|
|
63
63
|
|
|
64
|
+
with self.assertRaises(ValidationError):
|
|
65
|
+
coldata.col("boolean", "BIT(1)")
|
|
66
|
+
|
|
64
67
|
with self.assertRaises(ValidationError):
|
|
65
68
|
coldata.col("float", "FLOAT")
|
|
66
69
|
|
|
@@ -79,8 +82,9 @@ class RedundantDatatypesTest(unittest.TestCase):
|
|
|
79
82
|
with self.assertRaises(ValidationError):
|
|
80
83
|
coldata.col("long", "BIGINT")
|
|
81
84
|
|
|
82
|
-
|
|
83
|
-
|
|
85
|
+
# These look like they should be equivalent, but the default is
|
|
86
|
+
# actually ``BIT(1)`` for MySQL.
|
|
87
|
+
coldata.col("boolean", "BOOLEAN")
|
|
84
88
|
|
|
85
89
|
with self.assertRaises(ValidationError):
|
|
86
90
|
coldata.col("unicode", "NVARCHAR", length=32)
|
|
@@ -98,9 +102,6 @@ class RedundantDatatypesTest(unittest.TestCase):
|
|
|
98
102
|
# Same type and length
|
|
99
103
|
coldata.col("string", "VARCHAR(128)", length=128)
|
|
100
104
|
|
|
101
|
-
# Check the old type mapping for MySQL, which is now okay
|
|
102
|
-
coldata.col("boolean", "BIT(1)")
|
|
103
|
-
|
|
104
105
|
# Different types, which is okay
|
|
105
106
|
coldata.col("double", "FLOAT")
|
|
106
107
|
|
|
@@ -29,7 +29,8 @@ from typing import Any
|
|
|
29
29
|
import sqlalchemy
|
|
30
30
|
import yaml
|
|
31
31
|
|
|
32
|
-
from felis
|
|
32
|
+
from felis import DEFAULT_FRAME
|
|
33
|
+
from felis.cli import _normalize
|
|
33
34
|
from felis.tap import Tap11Base, TapLoadingVisitor, init_tables
|
|
34
35
|
|
|
35
36
|
TESTDIR = os.path.abspath(os.path.dirname(__file__))
|
|
@@ -44,8 +45,8 @@ class VisitorTestCase(unittest.TestCase):
|
|
|
44
45
|
def setUp(self) -> None:
|
|
45
46
|
"""Load data from test file."""
|
|
46
47
|
with open(TEST_YAML) as test_yaml:
|
|
47
|
-
|
|
48
|
-
self.schema_obj
|
|
48
|
+
self.schema_obj = yaml.load(test_yaml, Loader=yaml.SafeLoader)
|
|
49
|
+
self.schema_obj.update(DEFAULT_FRAME)
|
|
49
50
|
self.tmpdir = tempfile.mkdtemp(dir=TESTDIR)
|
|
50
51
|
|
|
51
52
|
def tearDown(self) -> None:
|
|
@@ -59,8 +60,12 @@ class VisitorTestCase(unittest.TestCase):
|
|
|
59
60
|
Tap11Base.metadata.create_all(engine)
|
|
60
61
|
|
|
61
62
|
# This repeats logic from cli.py.
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
normalized = _normalize(self.schema_obj, embed="@always")
|
|
64
|
+
if isinstance(normalized["@graph"], dict):
|
|
65
|
+
normalized["@graph"] = [normalized["@graph"]]
|
|
66
|
+
for schema in normalized["@graph"]:
|
|
67
|
+
tap_visitor = TapLoadingVisitor(engine, tap_tables=tap_tables)
|
|
68
|
+
tap_visitor.visit_schema(schema)
|
|
64
69
|
|
|
65
70
|
|
|
66
71
|
if __name__ == "__main__":
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lsst_felis-26.2024.1700 → lsst_felis-27.0.0rc1}/python/lsst_felis.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{lsst_felis-26.2024.1700 → lsst_felis-27.0.0rc1}/python/lsst_felis.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|