lsst-felis 29.2025.4500__py3-none-any.whl → 30.0.0rc3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- felis/__init__.py +1 -4
- felis/cli.py +172 -87
- felis/config/tap_schema/tap_schema_extensions.yaml +73 -0
- felis/datamodel.py +2 -3
- felis/db/{dialects.py → _dialects.py} +69 -4
- felis/db/{variants.py → _variants.py} +1 -1
- felis/db/database_context.py +917 -0
- felis/metadata.py +79 -11
- felis/tap_schema.py +159 -177
- felis/tests/postgresql.py +1 -1
- {lsst_felis-29.2025.4500.dist-info → lsst_felis-30.0.0rc3.dist-info}/METADATA +1 -1
- lsst_felis-30.0.0rc3.dist-info/RECORD +31 -0
- felis/db/schema.py +0 -62
- felis/db/utils.py +0 -409
- lsst_felis-29.2025.4500.dist-info/RECORD +0 -31
- /felis/db/{sqltypes.py → _sqltypes.py} +0 -0
- {lsst_felis-29.2025.4500.dist-info → lsst_felis-30.0.0rc3.dist-info}/WHEEL +0 -0
- {lsst_felis-29.2025.4500.dist-info → lsst_felis-30.0.0rc3.dist-info}/entry_points.txt +0 -0
- {lsst_felis-29.2025.4500.dist-info → lsst_felis-30.0.0rc3.dist-info}/licenses/COPYRIGHT +0 -0
- {lsst_felis-29.2025.4500.dist-info → lsst_felis-30.0.0rc3.dist-info}/licenses/LICENSE +0 -0
- {lsst_felis-29.2025.4500.dist-info → lsst_felis-30.0.0rc3.dist-info}/top_level.txt +0 -0
- {lsst_felis-29.2025.4500.dist-info → lsst_felis-30.0.0rc3.dist-info}/zip-safe +0 -0
felis/metadata.py
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
from __future__ import annotations
|
|
25
25
|
|
|
26
26
|
import logging
|
|
27
|
-
from typing import Any, Literal
|
|
27
|
+
from typing import IO, Any, Literal
|
|
28
28
|
|
|
29
29
|
from lsst.utils.iteration import ensure_iterable
|
|
30
30
|
from sqlalchemy import (
|
|
@@ -43,11 +43,11 @@ from sqlalchemy import (
|
|
|
43
43
|
from sqlalchemy.dialects import mysql, postgresql
|
|
44
44
|
from sqlalchemy.types import TypeEngine
|
|
45
45
|
|
|
46
|
-
from felis.datamodel import Schema
|
|
47
|
-
from felis.db.variants import make_variant_dict
|
|
48
|
-
|
|
49
46
|
from . import datamodel
|
|
50
|
-
from .
|
|
47
|
+
from .datamodel import Schema
|
|
48
|
+
from .db import _sqltypes as sqltypes
|
|
49
|
+
from .db._variants import make_variant_dict
|
|
50
|
+
from .db.database_context import is_sqlite_url
|
|
51
51
|
from .types import FelisType
|
|
52
52
|
|
|
53
53
|
__all__ = ("MetaDataBuilder", "get_datatype_with_variants")
|
|
@@ -129,6 +129,8 @@ class MetaDataBuilder:
|
|
|
129
129
|
Whether to ignore constraints when building the metadata.
|
|
130
130
|
table_name_postfix
|
|
131
131
|
A string to append to the table names when building the metadata.
|
|
132
|
+
skip_indexes
|
|
133
|
+
Skip indexes when building the metadata.
|
|
132
134
|
"""
|
|
133
135
|
|
|
134
136
|
def __init__(
|
|
@@ -137,6 +139,7 @@ class MetaDataBuilder:
|
|
|
137
139
|
apply_schema_to_metadata: bool = True,
|
|
138
140
|
ignore_constraints: bool = False,
|
|
139
141
|
table_name_postfix: str = "",
|
|
142
|
+
skip_indexes: bool = False,
|
|
140
143
|
) -> None:
|
|
141
144
|
"""Initialize the metadata builder."""
|
|
142
145
|
self.schema = schema
|
|
@@ -146,6 +149,7 @@ class MetaDataBuilder:
|
|
|
146
149
|
self._objects: dict[str, Any] = {}
|
|
147
150
|
self.ignore_constraints = ignore_constraints
|
|
148
151
|
self.table_name_postfix = table_name_postfix
|
|
152
|
+
self.skip_indexes = skip_indexes
|
|
149
153
|
|
|
150
154
|
def build(self) -> MetaData:
|
|
151
155
|
"""Build the SQLAlchemy tables and constraints from the schema.
|
|
@@ -162,6 +166,10 @@ class MetaDataBuilder:
|
|
|
162
166
|
The SQLAlchemy metadata object.
|
|
163
167
|
"""
|
|
164
168
|
self.build_tables()
|
|
169
|
+
if not self.skip_indexes:
|
|
170
|
+
self.build_indexes()
|
|
171
|
+
else:
|
|
172
|
+
logger.warning("Ignoring indexes")
|
|
165
173
|
if not self.ignore_constraints:
|
|
166
174
|
self.build_constraints()
|
|
167
175
|
else:
|
|
@@ -236,12 +244,6 @@ class MetaDataBuilder:
|
|
|
236
244
|
**optargs, # type: ignore[arg-type]
|
|
237
245
|
)
|
|
238
246
|
|
|
239
|
-
# Create the indexes and add them to the table.
|
|
240
|
-
indexes = [self.build_index(index) for index in table_obj.indexes]
|
|
241
|
-
for index in indexes:
|
|
242
|
-
index._set_parent(table)
|
|
243
|
-
table.indexes.add(index)
|
|
244
|
-
|
|
245
247
|
self._objects[id] = table
|
|
246
248
|
|
|
247
249
|
def build_column(self, column_obj: datamodel.Column) -> Column:
|
|
@@ -383,3 +385,69 @@ class MetaDataBuilder:
|
|
|
383
385
|
index = Index(index_obj.name, *columns, *expressions)
|
|
384
386
|
self._objects[index_obj.id] = index
|
|
385
387
|
return index
|
|
388
|
+
|
|
389
|
+
def build_indexes(self) -> None:
|
|
390
|
+
"""Build the SQLAlchemy indexes from the Felis schema and add them to
|
|
391
|
+
the associated table in the metadata.
|
|
392
|
+
"""
|
|
393
|
+
for table in self.schema.tables:
|
|
394
|
+
md_table = self._objects.get(table.id, None)
|
|
395
|
+
if md_table is None:
|
|
396
|
+
raise KeyError(f"Table with ID '{table.id}' not found in objects map")
|
|
397
|
+
if not isinstance(md_table, Table):
|
|
398
|
+
raise TypeError(f"Expected Table object, got {type(md_table)}")
|
|
399
|
+
indexes = [self.build_index(index) for index in table.indexes]
|
|
400
|
+
for index in indexes:
|
|
401
|
+
index._set_parent(md_table)
|
|
402
|
+
md_table.indexes.add(index)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def create_metadata(
|
|
406
|
+
felis_file: IO[str],
|
|
407
|
+
schema_name: str | None = None,
|
|
408
|
+
id_generation: bool = True,
|
|
409
|
+
ignore_constraints: bool = False,
|
|
410
|
+
skip_indexes: bool = False,
|
|
411
|
+
engine_url: str | None = None,
|
|
412
|
+
) -> MetaData:
|
|
413
|
+
"""Create SQLAlchemy metadata from a Felis schema file.
|
|
414
|
+
|
|
415
|
+
Parameters
|
|
416
|
+
----------
|
|
417
|
+
felis_file
|
|
418
|
+
The Felis schema file to read.
|
|
419
|
+
schema_name
|
|
420
|
+
Optional schema name to override the one in the file.
|
|
421
|
+
id_generation
|
|
422
|
+
Whether to generate IDs for all objects in the schema that do not have
|
|
423
|
+
them.
|
|
424
|
+
ignore_constraints
|
|
425
|
+
Whether to ignore constraints when building metadata.
|
|
426
|
+
skip_indexes
|
|
427
|
+
Whether to skip creating indexes when building metadata.
|
|
428
|
+
engine_url
|
|
429
|
+
Engine URL to determine if SQLite-specific handling is needed.
|
|
430
|
+
|
|
431
|
+
Returns
|
|
432
|
+
-------
|
|
433
|
+
MetaData
|
|
434
|
+
The SQLAlchemy metadata object with proper schema handling.
|
|
435
|
+
"""
|
|
436
|
+
schema = Schema.from_stream(felis_file, context={"id_generation": id_generation})
|
|
437
|
+
if schema_name:
|
|
438
|
+
logger.info(f"Overriding schema name with: {schema_name}")
|
|
439
|
+
schema.name = schema_name
|
|
440
|
+
|
|
441
|
+
# Determine if we need SQLite-specific handling
|
|
442
|
+
apply_schema = True
|
|
443
|
+
if engine_url:
|
|
444
|
+
if is_sqlite_url(engine_url):
|
|
445
|
+
apply_schema = False
|
|
446
|
+
logger.debug("SQLite detected: schema name will not be applied to metadata")
|
|
447
|
+
|
|
448
|
+
return MetaDataBuilder(
|
|
449
|
+
schema,
|
|
450
|
+
ignore_constraints=ignore_constraints,
|
|
451
|
+
skip_indexes=skip_indexes,
|
|
452
|
+
apply_schema_to_metadata=apply_schema,
|
|
453
|
+
).build()
|
felis/tap_schema.py
CHANGED
|
@@ -26,54 +26,51 @@ import io
|
|
|
26
26
|
import logging
|
|
27
27
|
import os
|
|
28
28
|
import re
|
|
29
|
-
from typing import Any
|
|
29
|
+
from typing import IO, Any
|
|
30
30
|
|
|
31
31
|
from lsst.resources import ResourcePath
|
|
32
32
|
from sqlalchemy import MetaData, Table, select, text
|
|
33
|
-
from sqlalchemy.engine import Connection, Engine
|
|
34
|
-
from sqlalchemy.engine.mock import MockConnection
|
|
35
33
|
from sqlalchemy.exc import SQLAlchemyError
|
|
36
|
-
from sqlalchemy.schema import CreateSchema
|
|
37
34
|
from sqlalchemy.sql.dml import Insert
|
|
38
35
|
|
|
39
|
-
from
|
|
40
|
-
from
|
|
41
|
-
from
|
|
42
|
-
from
|
|
43
|
-
|
|
36
|
+
from . import datamodel
|
|
37
|
+
from .datamodel import Constraint, Schema
|
|
38
|
+
from .db.database_context import DatabaseContext, is_sqlite_url
|
|
39
|
+
from .metadata import MetaDataBuilder
|
|
44
40
|
from .types import FelisType
|
|
45
41
|
|
|
46
|
-
__all__ = ["DataLoader", "TableManager"]
|
|
42
|
+
__all__ = ["DataLoader", "MetadataInserter", "TableManager"]
|
|
47
43
|
|
|
48
44
|
logger = logging.getLogger(__name__)
|
|
49
45
|
|
|
50
46
|
|
|
51
47
|
class TableManager:
|
|
52
|
-
"""Manage
|
|
48
|
+
"""Manage TAP_SCHEMA table definitions and access.
|
|
49
|
+
|
|
50
|
+
This class provides a streamlined interface for managing TAP_SCHEMA tables,
|
|
51
|
+
automatically handling dialect-specific requirements and providing
|
|
52
|
+
consistent access to TAP_SCHEMA tables through a dictionary-like interface.
|
|
53
53
|
|
|
54
54
|
Parameters
|
|
55
55
|
----------
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
engine_url
|
|
57
|
+
Database engine URL for automatic dialect detection and schema
|
|
58
|
+
handling.
|
|
59
|
+
db_context
|
|
60
|
+
Optional database context for reflecting existing TAP_SCHEMA tables.
|
|
61
|
+
If None, loads from internal YAML schema.
|
|
61
62
|
schema_name
|
|
62
|
-
The name of the schema to use for
|
|
63
|
-
|
|
64
|
-
apply_schema_to_metadata
|
|
65
|
-
If True, apply the schema to the metadata as well as the tables.
|
|
66
|
-
If False, these will be set to None, e.g., for sqlite.
|
|
63
|
+
The name of the schema to use for TAP_SCHEMA tables.
|
|
64
|
+
Defaults to "TAP_SCHEMA".
|
|
67
65
|
table_name_postfix
|
|
68
|
-
A string to append to
|
|
69
|
-
|
|
70
|
-
|
|
66
|
+
A string to append to standard table names for customization.
|
|
67
|
+
extensions_path
|
|
68
|
+
Path to additional TAP_SCHEMA table definitions.
|
|
71
69
|
|
|
72
70
|
Notes
|
|
73
71
|
-----
|
|
74
|
-
The
|
|
75
|
-
|
|
76
|
-
be used if ``engine`` is None or a ``MockConnection``.
|
|
72
|
+
The TableManager automatically detects SQLite vs. schema-supporting
|
|
73
|
+
databases and handles schema application appropriately.
|
|
77
74
|
"""
|
|
78
75
|
|
|
79
76
|
_TABLE_NAMES_STD = ["schemas", "tables", "columns", "keys", "key_columns"]
|
|
@@ -84,19 +81,26 @@ class TableManager:
|
|
|
84
81
|
|
|
85
82
|
def __init__(
|
|
86
83
|
self,
|
|
87
|
-
|
|
84
|
+
engine_url: str | None = None,
|
|
85
|
+
db_context: DatabaseContext | None = None,
|
|
88
86
|
schema_name: str | None = None,
|
|
89
|
-
apply_schema_to_metadata: bool = True,
|
|
90
87
|
table_name_postfix: str = "",
|
|
88
|
+
extensions_path: str | None = None,
|
|
91
89
|
):
|
|
92
90
|
"""Initialize the table manager."""
|
|
93
91
|
self.table_name_postfix = table_name_postfix
|
|
94
|
-
self.
|
|
95
|
-
self.
|
|
96
|
-
|
|
92
|
+
self.schema_name = schema_name or self._SCHEMA_NAME_STD
|
|
93
|
+
self.extensions_path = extensions_path
|
|
94
|
+
|
|
95
|
+
# Automatic dialect detection from engine URL
|
|
96
|
+
if engine_url is not None:
|
|
97
|
+
self.apply_schema_to_metadata = not is_sqlite_url(engine_url)
|
|
98
|
+
else:
|
|
99
|
+
# Default case: assume SQLite
|
|
100
|
+
engine_url = "sqlite:///:memory:"
|
|
101
|
+
self.apply_schema_to_metadata = False
|
|
97
102
|
|
|
98
|
-
if
|
|
99
|
-
assert isinstance(engine, Engine)
|
|
103
|
+
if db_context is not None:
|
|
100
104
|
if table_name_postfix != "":
|
|
101
105
|
logger.warning(
|
|
102
106
|
"Table name postfix '%s' will be ignored when reflecting TAP_SCHEMA database",
|
|
@@ -104,47 +108,80 @@ class TableManager:
|
|
|
104
108
|
)
|
|
105
109
|
logger.debug(
|
|
106
110
|
"Reflecting TAP_SCHEMA database from existing database at %s",
|
|
107
|
-
engine.url._replace(password="***"),
|
|
111
|
+
db_context.engine.url._replace(password="***"),
|
|
108
112
|
)
|
|
109
|
-
self.
|
|
113
|
+
self._reflect_from_database(db_context)
|
|
110
114
|
else:
|
|
111
|
-
self.
|
|
115
|
+
self._load_from_yaml()
|
|
112
116
|
|
|
113
117
|
self._create_table_map()
|
|
114
118
|
self._check_tables()
|
|
115
119
|
|
|
116
|
-
def
|
|
117
|
-
"""
|
|
120
|
+
def _load_from_yaml(self) -> None:
|
|
121
|
+
"""Load TAP_SCHEMA from YAML resources and build metadata."""
|
|
122
|
+
# Load the base schema
|
|
123
|
+
self._schema = self.load_schema_resource()
|
|
124
|
+
|
|
125
|
+
# Override schema name if specified
|
|
126
|
+
if self.schema_name != self._SCHEMA_NAME_STD:
|
|
127
|
+
self._schema.name = self.schema_name
|
|
128
|
+
else:
|
|
129
|
+
self.schema_name = self._schema.name
|
|
130
|
+
|
|
131
|
+
# Apply any extensions
|
|
132
|
+
self._apply_extensions()
|
|
133
|
+
|
|
134
|
+
# Build metadata using streamlined approach
|
|
135
|
+
self._metadata = MetaDataBuilder(
|
|
136
|
+
self._schema,
|
|
137
|
+
apply_schema_to_metadata=self.apply_schema_to_metadata,
|
|
138
|
+
table_name_postfix=self.table_name_postfix,
|
|
139
|
+
).build()
|
|
140
|
+
|
|
141
|
+
logger.debug("Loaded TAP_SCHEMA '%s' from YAML resource", self.schema_name)
|
|
142
|
+
|
|
143
|
+
def _reflect_from_database(self, db_context: DatabaseContext) -> None:
|
|
144
|
+
"""Reflect TAP_SCHEMA tables from an existing database.
|
|
118
145
|
|
|
119
146
|
Parameters
|
|
120
147
|
----------
|
|
121
|
-
|
|
122
|
-
The
|
|
148
|
+
db_context
|
|
149
|
+
The database context to use for reflection.
|
|
123
150
|
"""
|
|
124
151
|
self._metadata = MetaData(schema=self.schema_name if self.apply_schema_to_metadata else None)
|
|
125
152
|
try:
|
|
126
|
-
self.
|
|
153
|
+
self._metadata.reflect(bind=db_context.engine)
|
|
127
154
|
except SQLAlchemyError as e:
|
|
128
155
|
logger.error("Error reflecting TAP_SCHEMA database: %s", e)
|
|
129
156
|
raise
|
|
130
157
|
|
|
131
|
-
def
|
|
132
|
-
"""
|
|
133
|
-
|
|
158
|
+
def _apply_extensions(self) -> None:
|
|
159
|
+
"""Apply extensions from a YAML file to the TAP_SCHEMA schema.
|
|
160
|
+
|
|
161
|
+
This method loads extension column definitions from a YAML file and
|
|
162
|
+
adds them to the appropriate TAP_SCHEMA tables.
|
|
134
163
|
"""
|
|
135
|
-
self.
|
|
136
|
-
|
|
137
|
-
self.schema.name = self.schema_name
|
|
138
|
-
else:
|
|
139
|
-
self.schema_name = self.schema.name
|
|
164
|
+
if not self.extensions_path:
|
|
165
|
+
return
|
|
140
166
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
apply_schema_to_metadata=self.apply_schema_to_metadata,
|
|
144
|
-
table_name_postfix=self.table_name_postfix,
|
|
145
|
-
).build()
|
|
167
|
+
logger.info("Loading TAP_SCHEMA extensions from: %s", self.extensions_path)
|
|
168
|
+
extensions_schema = Schema.from_uri(self.extensions_path, context={"id_generation": True})
|
|
146
169
|
|
|
147
|
-
|
|
170
|
+
if not extensions_schema.tables:
|
|
171
|
+
logger.warning("Extensions schema does not contain any tables, no extensions applied")
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
extension_count = 0
|
|
175
|
+
extension_tables = {table.name: table.columns for table in extensions_schema.tables}
|
|
176
|
+
|
|
177
|
+
for table in self.schema.tables:
|
|
178
|
+
extension_columns = extension_tables.get(table.name)
|
|
179
|
+
if extension_columns:
|
|
180
|
+
table.columns = list(table.columns) + list(extension_columns)
|
|
181
|
+
extension_count += len(extension_columns)
|
|
182
|
+
logger.debug("Added %d extension columns to table '%s'", len(extension_columns), table.name)
|
|
183
|
+
|
|
184
|
+
logger.info("Applied %d extension columns to TAP_SCHEMA", extension_count)
|
|
148
185
|
|
|
149
186
|
def __getitem__(self, table_name: str) -> Table:
|
|
150
187
|
"""Get one of the TAP_SCHEMA tables by its standard TAP_SCHEMA name.
|
|
@@ -266,19 +303,13 @@ class TableManager:
|
|
|
266
303
|
"""Create a mapping of standard table names to the table names modified
|
|
267
304
|
with a postfix, as well as the prepended schema name if it is set.
|
|
268
305
|
|
|
269
|
-
Returns
|
|
270
|
-
-------
|
|
271
|
-
dict
|
|
272
|
-
A dictionary mapping the standard table names to the modified
|
|
273
|
-
table names.
|
|
274
|
-
|
|
275
306
|
Notes
|
|
276
307
|
-----
|
|
277
308
|
This is a private method that is called during initialization, allowing
|
|
278
309
|
us to use table names like ``schemas11`` such as those used by the CADC
|
|
279
310
|
TAP library instead of the standard table names. It also maps between
|
|
280
311
|
the standard table names and those with the schema name prepended like
|
|
281
|
-
SQLAlchemy uses.
|
|
312
|
+
SQLAlchemy uses. The mapping is stored in ``self._table_map``.
|
|
282
313
|
"""
|
|
283
314
|
self._table_map = {
|
|
284
315
|
table_name: (
|
|
@@ -300,81 +331,31 @@ class TableManager:
|
|
|
300
331
|
for table_name in TableManager.get_table_names_std():
|
|
301
332
|
self[table_name]
|
|
302
333
|
|
|
303
|
-
def
|
|
304
|
-
"""Create the database schema for TAP_SCHEMA if it does not already
|
|
305
|
-
exist.
|
|
306
|
-
|
|
307
|
-
Parameters
|
|
308
|
-
----------
|
|
309
|
-
engine
|
|
310
|
-
The SQLAlchemy engine to use to create the schema.
|
|
311
|
-
|
|
312
|
-
Notes
|
|
313
|
-
-----
|
|
314
|
-
This method only creates the schema in the database. It does not create
|
|
315
|
-
the tables.
|
|
316
|
-
"""
|
|
317
|
-
create_schema_functions = {
|
|
318
|
-
"postgresql": self._create_schema_postgresql,
|
|
319
|
-
"mysql": self._create_schema_mysql,
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
dialect_name = engine.dialect.name
|
|
323
|
-
if dialect_name == "sqlite":
|
|
324
|
-
# SQLite doesn't have schemas.
|
|
325
|
-
return
|
|
326
|
-
|
|
327
|
-
create_function = create_schema_functions.get(dialect_name)
|
|
328
|
-
|
|
329
|
-
if create_function:
|
|
330
|
-
with engine.begin() as connection:
|
|
331
|
-
create_function(connection)
|
|
332
|
-
else:
|
|
333
|
-
# Some other database engine we don't currently know how to handle.
|
|
334
|
-
raise NotImplementedError(
|
|
335
|
-
f"Database engine '{engine.dialect.name}' is not supported for schema creation"
|
|
336
|
-
)
|
|
337
|
-
|
|
338
|
-
def _create_schema_postgresql(self, connection: Connection) -> None:
|
|
339
|
-
"""Create the schema in a PostgreSQL database.
|
|
340
|
-
|
|
341
|
-
Parameters
|
|
342
|
-
----------
|
|
343
|
-
connection
|
|
344
|
-
The SQLAlchemy connection to use to create the schema.
|
|
345
|
-
"""
|
|
346
|
-
connection.execute(CreateSchema(self.schema_name, if_not_exists=True))
|
|
347
|
-
|
|
348
|
-
def _create_schema_mysql(self, connection: Connection) -> None:
|
|
349
|
-
"""Create the schema in a MySQL database.
|
|
350
|
-
|
|
351
|
-
Parameters
|
|
352
|
-
----------
|
|
353
|
-
connection
|
|
354
|
-
The SQLAlchemy connection to use to create the schema.
|
|
355
|
-
"""
|
|
356
|
-
connection.execute(text(f"CREATE DATABASE IF NOT EXISTS {self.schema_name}"))
|
|
357
|
-
|
|
358
|
-
def initialize_database(self, engine: Engine) -> None:
|
|
334
|
+
def initialize_database(self, db_context: DatabaseContext) -> None:
|
|
359
335
|
"""Initialize a database with the TAP_SCHEMA tables.
|
|
360
336
|
|
|
361
337
|
Parameters
|
|
362
338
|
----------
|
|
363
|
-
|
|
364
|
-
The
|
|
339
|
+
db_context
|
|
340
|
+
The database context to use to create the tables.
|
|
365
341
|
"""
|
|
366
342
|
logger.info("Creating TAP_SCHEMA database '%s'", self.schema_name)
|
|
367
|
-
|
|
368
|
-
|
|
343
|
+
db_context.initialize()
|
|
344
|
+
db_context.create_all()
|
|
369
345
|
|
|
370
|
-
def select(
|
|
346
|
+
def select(
|
|
347
|
+
self,
|
|
348
|
+
db_context: DatabaseContext,
|
|
349
|
+
table_name: str,
|
|
350
|
+
filter_condition: str = "",
|
|
351
|
+
) -> list[dict[str, Any]]:
|
|
371
352
|
"""Select all rows from a TAP_SCHEMA table with an optional filter
|
|
372
353
|
condition.
|
|
373
354
|
|
|
374
355
|
Parameters
|
|
375
356
|
----------
|
|
376
|
-
|
|
377
|
-
The
|
|
357
|
+
db_context
|
|
358
|
+
The database context to use to connect to the database.
|
|
378
359
|
table_name
|
|
379
360
|
The name of the table to select from.
|
|
380
361
|
filter_condition
|
|
@@ -390,7 +371,7 @@ class TableManager:
|
|
|
390
371
|
query = select(table)
|
|
391
372
|
if filter_condition:
|
|
392
373
|
query = query.where(text(filter_condition))
|
|
393
|
-
with engine.connect() as connection:
|
|
374
|
+
with db_context.engine.connect() as connection:
|
|
394
375
|
result = connection.execute(query)
|
|
395
376
|
rows = [dict(row._mapping) for row in result]
|
|
396
377
|
return rows
|
|
@@ -405,13 +386,13 @@ class DataLoader:
|
|
|
405
386
|
The Felis ``Schema`` to load into the TAP_SCHEMA tables.
|
|
406
387
|
mgr
|
|
407
388
|
The table manager that contains the TAP_SCHEMA tables.
|
|
408
|
-
|
|
409
|
-
The
|
|
389
|
+
db_context
|
|
390
|
+
The database context to use to connect to the database.
|
|
410
391
|
tap_schema_index
|
|
411
392
|
The index of the schema in the TAP_SCHEMA database.
|
|
412
|
-
|
|
413
|
-
The file to write the SQL statements to. If None,
|
|
414
|
-
suppressed.
|
|
393
|
+
output_file
|
|
394
|
+
The file object to write the SQL statements to. If None, file output
|
|
395
|
+
will be suppressed.
|
|
415
396
|
print_sql
|
|
416
397
|
If True, print the SQL statements that will be executed.
|
|
417
398
|
dry_run
|
|
@@ -425,19 +406,19 @@ class DataLoader:
|
|
|
425
406
|
self,
|
|
426
407
|
schema: Schema,
|
|
427
408
|
mgr: TableManager,
|
|
428
|
-
|
|
409
|
+
db_context: DatabaseContext,
|
|
429
410
|
tap_schema_index: int = 0,
|
|
430
|
-
|
|
411
|
+
output_file: IO[str] | None = None,
|
|
431
412
|
print_sql: bool = False,
|
|
432
413
|
dry_run: bool = False,
|
|
433
414
|
unique_keys: bool = False,
|
|
434
415
|
):
|
|
435
416
|
self.schema = schema
|
|
436
417
|
self.mgr = mgr
|
|
437
|
-
self.
|
|
418
|
+
self._db_context = db_context
|
|
438
419
|
self.tap_schema_index = tap_schema_index
|
|
439
420
|
self.inserts: list[Insert] = []
|
|
440
|
-
self.
|
|
421
|
+
self.output_file = output_file
|
|
441
422
|
self.print_sql = print_sql
|
|
442
423
|
self.dry_run = dry_run
|
|
443
424
|
self.unique_keys = unique_keys
|
|
@@ -460,14 +441,14 @@ class DataLoader:
|
|
|
460
441
|
if self.print_sql:
|
|
461
442
|
# Print to stdout.
|
|
462
443
|
self._print_sql()
|
|
463
|
-
if self.
|
|
444
|
+
if self.output_file:
|
|
464
445
|
# Print to an output file.
|
|
465
446
|
self._write_sql_to_file()
|
|
466
447
|
if not self.dry_run:
|
|
467
448
|
# Execute the inserts if not in dry run mode.
|
|
468
449
|
self._execute_inserts()
|
|
469
450
|
else:
|
|
470
|
-
logger.info("Dry run -
|
|
451
|
+
logger.info("Dry run - skipped loading into database")
|
|
471
452
|
|
|
472
453
|
def _insert_schemas(self) -> None:
|
|
473
454
|
"""Insert the schema data into the ``schemas`` table."""
|
|
@@ -617,17 +598,13 @@ class DataLoader:
|
|
|
617
598
|
"""Load the `~felis.datamodel.Schema` data into the TAP_SCHEMA
|
|
618
599
|
tables.
|
|
619
600
|
"""
|
|
620
|
-
|
|
621
|
-
with self.engine.
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
except Exception as e:
|
|
628
|
-
logger.error("Error loading data into database: %s", e)
|
|
629
|
-
transaction.rollback()
|
|
630
|
-
raise
|
|
601
|
+
try:
|
|
602
|
+
with self._db_context.engine.begin() as connection:
|
|
603
|
+
for insert in self.inserts:
|
|
604
|
+
connection.execute(insert)
|
|
605
|
+
except Exception as e:
|
|
606
|
+
logger.error("Error loading data into database: %s", e)
|
|
607
|
+
raise
|
|
631
608
|
|
|
632
609
|
def _compiled_inserts(self) -> list[str]:
|
|
633
610
|
"""Compile the inserts to SQL.
|
|
@@ -638,7 +615,12 @@ class DataLoader:
|
|
|
638
615
|
A list of the compiled insert statements.
|
|
639
616
|
"""
|
|
640
617
|
return [
|
|
641
|
-
str(
|
|
618
|
+
str(
|
|
619
|
+
insert.compile(
|
|
620
|
+
dialect=self._db_context.dialect,
|
|
621
|
+
compile_kwargs={"literal_binds": True},
|
|
622
|
+
),
|
|
623
|
+
)
|
|
642
624
|
for insert in self.inserts
|
|
643
625
|
]
|
|
644
626
|
|
|
@@ -649,11 +631,10 @@ class DataLoader:
|
|
|
649
631
|
|
|
650
632
|
def _write_sql_to_file(self) -> None:
|
|
651
633
|
"""Write the generated insert statements to a file."""
|
|
652
|
-
if not self.
|
|
653
|
-
raise ValueError("No output
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
outfile.write(insert_str + ";" + "\n")
|
|
634
|
+
if not self.output_file:
|
|
635
|
+
raise ValueError("No output file specified")
|
|
636
|
+
for insert_str in self._compiled_inserts():
|
|
637
|
+
self.output_file.write(insert_str + ";" + "\n")
|
|
657
638
|
|
|
658
639
|
def _insert(self, table_name: str, record: list[Any] | dict[str, Any]) -> None:
|
|
659
640
|
"""Generate an insert statement for a record.
|
|
@@ -729,39 +710,40 @@ class MetadataInserter:
|
|
|
729
710
|
----------
|
|
730
711
|
mgr
|
|
731
712
|
The table manager that contains the TAP_SCHEMA tables.
|
|
732
|
-
|
|
733
|
-
The
|
|
713
|
+
db_context
|
|
714
|
+
The database context for connecting to the TAP_SCHEMA database.
|
|
734
715
|
"""
|
|
735
716
|
|
|
736
|
-
def __init__(self, mgr: TableManager,
|
|
717
|
+
def __init__(self, mgr: TableManager, db_context: DatabaseContext):
|
|
737
718
|
"""Initialize the metadata inserter.
|
|
738
719
|
|
|
739
720
|
Parameters
|
|
740
721
|
----------
|
|
741
722
|
mgr
|
|
742
723
|
The table manager representing the TAP_SCHEMA tables.
|
|
743
|
-
|
|
744
|
-
The
|
|
724
|
+
db_context
|
|
725
|
+
The database context for connecting to the database.
|
|
745
726
|
"""
|
|
746
727
|
self._mgr = mgr
|
|
747
|
-
self.
|
|
728
|
+
self._db_context = db_context
|
|
748
729
|
|
|
749
730
|
def insert_metadata(self) -> None:
|
|
750
731
|
"""Insert the TAP_SCHEMA metadata into the database."""
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
732
|
+
with self._db_context.engine.begin() as conn:
|
|
733
|
+
for table_name in self._mgr.get_table_names_std():
|
|
734
|
+
table = self._mgr[table_name]
|
|
735
|
+
csv_bytes = ResourcePath(f"resource://felis/config/tap_schema/{table_name}.csv").read()
|
|
736
|
+
text_stream = io.TextIOWrapper(io.BytesIO(csv_bytes), encoding="utf-8")
|
|
737
|
+
reader = csv.reader(text_stream)
|
|
738
|
+
headers = next(reader)
|
|
739
|
+
rows = [
|
|
740
|
+
{key: None if value == "\\N" else value for key, value in zip(headers, row)}
|
|
741
|
+
for row in reader
|
|
742
|
+
]
|
|
743
|
+
logger.debug(
|
|
744
|
+
"Inserting %d rows into table '%s' with headers: %s",
|
|
745
|
+
len(rows),
|
|
746
|
+
table_name,
|
|
747
|
+
headers,
|
|
748
|
+
)
|
|
767
749
|
conn.execute(table.insert(), rows)
|
felis/tests/postgresql.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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
6
|
License-Expression: GPL-3.0-or-later
|