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