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/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 .db import sqltypes
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 felis import datamodel
40
- from felis.datamodel import Constraint, Schema
41
- from felis.db.utils import is_valid_engine
42
- from felis.metadata import MetaDataBuilder
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 creation of TAP_SCHEMA tables.
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
- engine
57
- The SQLAlchemy engine for reflecting the TAP_SCHEMA tables from an
58
- existing database.
59
- This can be a mock connection or None, in which case the internal
60
- TAP_SCHEMA schema will be used by loading an internal YAML file.
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 the TAP_SCHEMA tables.
63
- Leave as None to use the standard name of "TAP_SCHEMA".
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 all the standard table names.
69
- This needs to be used in a way such that the resultant table names
70
- map to tables within the TAP_SCHEMA database.
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 TAP_SCHEMA schema must either have been created already, in which case
75
- the ``engine`` should be provided. Or the internal TAP_SCHEMA schema will
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
- engine: Engine | MockConnection | None = None,
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.apply_schema_to_metadata = apply_schema_to_metadata
95
- self.schema_name = schema_name or TableManager._SCHEMA_NAME_STD
96
- self.table_name_postfix = table_name_postfix
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 is_valid_engine(engine):
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._reflect(engine)
113
+ self._reflect_from_database(db_context)
110
114
  else:
111
- self._load_yaml()
115
+ self._load_from_yaml()
112
116
 
113
117
  self._create_table_map()
114
118
  self._check_tables()
115
119
 
116
- def _reflect(self, engine: Engine) -> None:
117
- """Reflect the TAP_SCHEMA database tables into the metadata.
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
- engine
122
- The SQLAlchemy engine to use to reflect the tables.
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.metadata.reflect(bind=engine)
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 _load_yaml(self) -> None:
132
- """Load the standard TAP_SCHEMA schema from a Felis package
133
- resource.
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._load_schema()
136
- if self.schema_name != TableManager._SCHEMA_NAME_STD:
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
- self._metadata = MetaDataBuilder(
142
- self.schema,
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
- logger.debug("Loaded TAP_SCHEMA '%s' from YAML resource", self.schema_name)
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 _create_schema(self, engine: Engine) -> None:
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
- engine
364
- The SQLAlchemy engine to use to create the tables.
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
- self._create_schema(engine)
368
- self.metadata.create_all(engine)
343
+ db_context.initialize()
344
+ db_context.create_all()
369
345
 
370
- def select(self, engine: Engine, table_name: str, filter_condition: str = "") -> list[dict[str, Any]]:
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
- engine
377
- The SQLAlchemy engine to use to connect to the database.
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
- engine
409
- The SQLAlchemy engine to use to connect to the database.
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
- output_path
413
- The file to write the SQL statements to. If None, printing will be
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
- engine: Engine | MockConnection,
409
+ db_context: DatabaseContext,
429
410
  tap_schema_index: int = 0,
430
- output_path: str | None = None,
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.engine = engine
418
+ self._db_context = db_context
438
419
  self.tap_schema_index = tap_schema_index
439
420
  self.inserts: list[Insert] = []
440
- self.output_path = output_path
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.output_path:
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 - not loading data into database")
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
- if isinstance(self.engine, Engine):
621
- with self.engine.connect() as connection:
622
- transaction = connection.begin()
623
- try:
624
- for insert in self.inserts:
625
- connection.execute(insert)
626
- transaction.commit()
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(insert.compile(self.engine, compile_kwargs={"literal_binds": True}))
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.output_path:
653
- raise ValueError("No output path specified")
654
- with open(self.output_path, "w") as outfile:
655
- for insert_str in self._compiled_inserts():
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
- engine
733
- The engine for connecting to the TAP_SCHEMA database.
713
+ db_context
714
+ The database context for connecting to the TAP_SCHEMA database.
734
715
  """
735
716
 
736
- def __init__(self, mgr: TableManager, engine: Engine):
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
- engine
744
- The SQLAlchemy engine for connecting to the database.
724
+ db_context
725
+ The database context for connecting to the database.
745
726
  """
746
727
  self._mgr = mgr
747
- self._engine = engine
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
- for table_name in self._mgr.get_table_names_std():
752
- table = self._mgr[table_name]
753
- csv_bytes = ResourcePath(f"resource://felis/config/tap_schema/{table_name}.csv").read()
754
- text_stream = io.TextIOWrapper(io.BytesIO(csv_bytes), encoding="utf-8")
755
- reader = csv.reader(text_stream)
756
- headers = next(reader)
757
- rows = [
758
- {key: None if value == "\\N" else value for key, value in zip(headers, row)} for row in reader
759
- ]
760
- logger.debug(
761
- "Inserting %d rows into table '%s' with headers: %s",
762
- len(rows),
763
- table_name,
764
- headers,
765
- )
766
- with self._engine.begin() as conn:
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
@@ -130,5 +130,5 @@ def setup_postgres_test_db() -> Iterator[TemporaryPostgresInstance]:
130
130
 
131
131
  # Clean up any lingering SQLAlchemy engines/connections
132
132
  # so they're closed before we shut down the server.
133
- gc.collect()
134
133
  engine.dispose()
134
+ gc.collect()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lsst-felis
3
- Version: 29.2025.4500
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