lsst-felis 28.2025.402__tar.gz → 28.2025.600__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.

Files changed (42) hide show
  1. {lsst_felis-28.2025.402/python/lsst_felis.egg-info → lsst_felis-28.2025.600}/PKG-INFO +1 -1
  2. {lsst_felis-28.2025.402 → lsst_felis-28.2025.600}/python/felis/cli.py +35 -167
  3. {lsst_felis-28.2025.402 → lsst_felis-28.2025.600}/python/felis/datamodel.py +131 -6
  4. {lsst_felis-28.2025.402 → lsst_felis-28.2025.600}/python/felis/metadata.py +5 -1
  5. {lsst_felis-28.2025.402 → lsst_felis-28.2025.600}/python/felis/tap_schema.py +13 -5
  6. lsst_felis-28.2025.600/python/felis/version.py +2 -0
  7. {lsst_felis-28.2025.402 → lsst_felis-28.2025.600/python/lsst_felis.egg-info}/PKG-INFO +1 -1
  8. {lsst_felis-28.2025.402 → lsst_felis-28.2025.600}/python/lsst_felis.egg-info/SOURCES.txt +0 -2
  9. {lsst_felis-28.2025.402 → lsst_felis-28.2025.600}/tests/test_cli.py +17 -35
  10. {lsst_felis-28.2025.402 → lsst_felis-28.2025.600}/tests/test_datamodel.py +71 -0
  11. {lsst_felis-28.2025.402 → lsst_felis-28.2025.600}/tests/test_metadata.py +9 -0
  12. {lsst_felis-28.2025.402 → lsst_felis-28.2025.600}/tests/test_tap_schema.py +6 -44
  13. lsst_felis-28.2025.402/python/felis/tap.py +0 -597
  14. lsst_felis-28.2025.402/python/felis/version.py +0 -2
  15. lsst_felis-28.2025.402/tests/test_tap.py +0 -66
  16. {lsst_felis-28.2025.402 → lsst_felis-28.2025.600}/COPYRIGHT +0 -0
  17. {lsst_felis-28.2025.402 → lsst_felis-28.2025.600}/LICENSE +0 -0
  18. {lsst_felis-28.2025.402 → lsst_felis-28.2025.600}/README.rst +0 -0
  19. {lsst_felis-28.2025.402 → lsst_felis-28.2025.600}/pyproject.toml +0 -0
  20. {lsst_felis-28.2025.402 → lsst_felis-28.2025.600}/python/felis/__init__.py +0 -0
  21. {lsst_felis-28.2025.402 → lsst_felis-28.2025.600}/python/felis/db/__init__.py +0 -0
  22. {lsst_felis-28.2025.402 → lsst_felis-28.2025.600}/python/felis/db/dialects.py +0 -0
  23. {lsst_felis-28.2025.402 → lsst_felis-28.2025.600}/python/felis/db/schema.py +0 -0
  24. {lsst_felis-28.2025.402 → lsst_felis-28.2025.600}/python/felis/db/sqltypes.py +0 -0
  25. {lsst_felis-28.2025.402 → lsst_felis-28.2025.600}/python/felis/db/utils.py +0 -0
  26. {lsst_felis-28.2025.402 → lsst_felis-28.2025.600}/python/felis/db/variants.py +0 -0
  27. {lsst_felis-28.2025.402 → lsst_felis-28.2025.600}/python/felis/diff.py +0 -0
  28. {lsst_felis-28.2025.402 → lsst_felis-28.2025.600}/python/felis/py.typed +0 -0
  29. {lsst_felis-28.2025.402 → lsst_felis-28.2025.600}/python/felis/schemas/tap_schema_std.yaml +0 -0
  30. {lsst_felis-28.2025.402 → lsst_felis-28.2025.600}/python/felis/tests/__init__.py +0 -0
  31. {lsst_felis-28.2025.402 → lsst_felis-28.2025.600}/python/felis/tests/postgresql.py +0 -0
  32. {lsst_felis-28.2025.402 → lsst_felis-28.2025.600}/python/felis/types.py +0 -0
  33. {lsst_felis-28.2025.402 → lsst_felis-28.2025.600}/python/lsst_felis.egg-info/dependency_links.txt +0 -0
  34. {lsst_felis-28.2025.402 → lsst_felis-28.2025.600}/python/lsst_felis.egg-info/entry_points.txt +0 -0
  35. {lsst_felis-28.2025.402 → lsst_felis-28.2025.600}/python/lsst_felis.egg-info/requires.txt +0 -0
  36. {lsst_felis-28.2025.402 → lsst_felis-28.2025.600}/python/lsst_felis.egg-info/top_level.txt +0 -0
  37. {lsst_felis-28.2025.402 → lsst_felis-28.2025.600}/python/lsst_felis.egg-info/zip-safe +0 -0
  38. {lsst_felis-28.2025.402 → lsst_felis-28.2025.600}/setup.cfg +0 -0
  39. {lsst_felis-28.2025.402 → lsst_felis-28.2025.600}/tests/test_db.py +0 -0
  40. {lsst_felis-28.2025.402 → lsst_felis-28.2025.600}/tests/test_diff.py +0 -0
  41. {lsst_felis-28.2025.402 → lsst_felis-28.2025.600}/tests/test_postgres.py +0 -0
  42. {lsst_felis-28.2025.402 → lsst_felis-28.2025.600}/tests/test_tap_schema_postgres.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: lsst-felis
3
- Version: 28.2025.402
3
+ Version: 28.2025.600
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+)
@@ -38,7 +38,6 @@ from .db.schema import create_database
38
38
  from .db.utils import DatabaseContext, is_mock_url
39
39
  from .diff import DatabaseDiff, FormattedSchemaDiff, SchemaDiff
40
40
  from .metadata import MetaDataBuilder
41
- from .tap import Tap11Base, TapLoadingVisitor, init_tables
42
41
  from .tap_schema import DataLoader, TableManager
43
42
 
44
43
  __all__ = ["cli"]
@@ -179,174 +178,9 @@ def create(
179
178
  raise click.ClickException(str(e))
180
179
 
181
180
 
182
- @cli.command("init-tap", help="Initialize TAP_SCHEMA objects in the database")
183
- @click.option("--tap-schema-name", help="Alternate database schema name for 'TAP_SCHEMA'")
184
- @click.option("--tap-schemas-table", help="Alternate table name for 'schemas'")
185
- @click.option("--tap-tables-table", help="Alternate table name for 'tables'")
186
- @click.option("--tap-columns-table", help="Alternate table name for 'columns'")
187
- @click.option("--tap-keys-table", help="Alternate table name for 'keys'")
188
- @click.option("--tap-key-columns-table", help="Alternate table name for 'key_columns'")
189
- @click.argument("engine-url")
190
- def init_tap(
191
- engine_url: str,
192
- tap_schema_name: str,
193
- tap_schemas_table: str,
194
- tap_tables_table: str,
195
- tap_columns_table: str,
196
- tap_keys_table: str,
197
- tap_key_columns_table: str,
198
- ) -> None:
199
- """Initialize TAP_SCHEMA objects in the database.
200
-
201
- Parameters
202
- ----------
203
- engine_url
204
- SQLAlchemy Engine URL. The target PostgreSQL schema or MySQL database
205
- must already exist and be referenced in the URL.
206
- tap_schema_name
207
- Alterate name for the database schema ``TAP_SCHEMA``.
208
- tap_schemas_table
209
- Alterate table name for ``schemas``.
210
- tap_tables_table
211
- Alterate table name for ``tables``.
212
- tap_columns_table
213
- Alterate table name for ``columns``.
214
- tap_keys_table
215
- Alterate table name for ``keys``.
216
- tap_key_columns_table
217
- Alterate table name for ``key_columns``.
218
-
219
- Notes
220
- -----
221
- The supported version of TAP_SCHEMA in the SQLAlchemy metadata is 1.1. The
222
- tables are created in the database schema specified by the engine URL,
223
- which must be a PostgreSQL schema or MySQL database that already exists.
224
- """
225
- engine = create_engine(engine_url)
226
- init_tables(
227
- tap_schema_name,
228
- tap_schemas_table,
229
- tap_tables_table,
230
- tap_columns_table,
231
- tap_keys_table,
232
- tap_key_columns_table,
233
- )
234
- Tap11Base.metadata.create_all(engine)
235
-
236
-
237
- @cli.command("load-tap", help="Load metadata from a Felis file into a TAP_SCHEMA database")
238
- @click.option("--engine-url", envvar="FELIS_ENGINE_URL", help="SQLAlchemy Engine URL")
239
- @click.option("--schema-name", help="Alternate Schema Name for Felis file")
240
- @click.option("--catalog-name", help="Catalog Name for Schema")
241
- @click.option("--dry-run", is_flag=True, help="Dry Run Only. Prints out the DDL that would be executed")
242
- @click.option("--tap-schema-name", help="Alternate schema name for 'TAP_SCHEMA'")
243
- @click.option("--tap-tables-postfix", help="Postfix for TAP_SCHEMA table names")
244
- @click.option("--tap-schemas-table", help="Alternate table name for 'schemas'")
245
- @click.option("--tap-tables-table", help="Alternate table name for 'tables'")
246
- @click.option("--tap-columns-table", help="Alternate table name for 'columns'")
247
- @click.option("--tap-keys-table", help="Alternate table name for 'keys'")
248
- @click.option("--tap-key-columns-table", help="Alternate table name for 'key_columns'")
249
- @click.option("--tap-schema-index", type=int, help="TAP_SCHEMA index of the schema in this environment")
250
- @click.argument("file", type=click.File())
251
- def load_tap(
252
- engine_url: str,
253
- schema_name: str,
254
- catalog_name: str,
255
- dry_run: bool,
256
- tap_schema_name: str,
257
- tap_tables_postfix: str,
258
- tap_schemas_table: str,
259
- tap_tables_table: str,
260
- tap_columns_table: str,
261
- tap_keys_table: str,
262
- tap_key_columns_table: str,
263
- tap_schema_index: int,
264
- file: IO[str],
265
- ) -> None:
266
- """Load TAP metadata from a Felis file.
267
-
268
- This command loads the associated TAP metadata from a Felis YAML file
269
- into the TAP_SCHEMA tables.
270
-
271
- Parameters
272
- ----------
273
- engine_url
274
- SQLAlchemy Engine URL to catalog.
275
- schema_name
276
- Alternate schema name. This overrides the schema name in the
277
- ``catalog`` field of the Felis file.
278
- catalog_name
279
- Catalog name for the schema. This possibly duplicates the
280
- ``tap_schema_name`` argument (DM-44870).
281
- dry_run
282
- Dry run only to print out commands instead of executing.
283
- tap_schema_name
284
- Alternate name for the schema of TAP_SCHEMA in the database.
285
- tap_tables_postfix
286
- Postfix for TAP table names that will be automatically appended.
287
- tap_schemas_table
288
- Alternate table name for ``schemas``.
289
- tap_tables_table
290
- Alternate table name for ``tables``.
291
- tap_columns_table
292
- Alternate table name for ``columns``.
293
- tap_keys_table
294
- Alternate table name for ``keys``.
295
- tap_key_columns_table
296
- Alternate table name for ``key_columns``.
297
- tap_schema_index
298
- TAP_SCHEMA index of the schema in this TAP environment.
299
- file
300
- Felis file to read.
301
-
302
- Notes
303
- -----
304
- The data will be loaded into the TAP_SCHEMA from the engine URL. The
305
- tables must have already been initialized or an error will occur.
306
- """
307
- schema = Schema.from_stream(file)
308
-
309
- tap_tables = init_tables(
310
- tap_schema_name,
311
- tap_tables_postfix,
312
- tap_schemas_table,
313
- tap_tables_table,
314
- tap_columns_table,
315
- tap_keys_table,
316
- tap_key_columns_table,
317
- )
318
-
319
- if not dry_run:
320
- engine = create_engine(engine_url)
321
-
322
- if engine_url == "sqlite://" and not dry_run:
323
- # In Memory SQLite - Mostly used to test
324
- Tap11Base.metadata.create_all(engine)
325
-
326
- tap_visitor = TapLoadingVisitor(
327
- engine,
328
- catalog_name=catalog_name,
329
- schema_name=schema_name,
330
- tap_tables=tap_tables,
331
- tap_schema_index=tap_schema_index,
332
- )
333
- tap_visitor.visit_schema(schema)
334
- else:
335
- conn = DatabaseContext.create_mock_engine(engine_url)
336
-
337
- tap_visitor = TapLoadingVisitor.from_mock_connection(
338
- conn,
339
- catalog_name=catalog_name,
340
- schema_name=schema_name,
341
- tap_tables=tap_tables,
342
- tap_schema_index=tap_schema_index,
343
- )
344
- tap_visitor.visit_schema(schema)
345
-
346
-
347
181
  @cli.command("load-tap-schema", help="Load metadata from a Felis file into a TAP_SCHEMA database")
348
182
  @click.option("--engine-url", envvar="FELIS_ENGINE_URL", help="SQLAlchemy Engine URL")
349
- @click.option("--tap-schema-name", help="Name of the TAP_SCHEMA schema in the database")
183
+ @click.option("--tap-schema-name", help="Name of the TAP_SCHEMA schema in the database (default: TAP_SCHEMA)")
350
184
  @click.option(
351
185
  "--tap-tables-postfix", help="Postfix which is applied to standard TAP_SCHEMA table names", default=""
352
186
  )
@@ -417,6 +251,40 @@ def load_tap_schema(
417
251
  ).load()
418
252
 
419
253
 
254
+ @cli.command("init-tap-schema", help="Initialize a standard TAP_SCHEMA database")
255
+ @click.option("--engine-url", envvar="FELIS_ENGINE_URL", help="SQLAlchemy Engine URL")
256
+ @click.option("--tap-schema-name", help="Name of the TAP_SCHEMA schema in the database")
257
+ @click.option(
258
+ "--tap-tables-postfix", help="Postfix which is applied to standard TAP_SCHEMA table names", default=""
259
+ )
260
+ @click.pass_context
261
+ def init_tap_schema(
262
+ ctx: click.Context, engine_url: str, tap_schema_name: str, tap_tables_postfix: str
263
+ ) -> None:
264
+ """Initialize a standard TAP_SCHEMA database.
265
+
266
+ Parameters
267
+ ----------
268
+ engine_url
269
+ SQLAlchemy Engine URL.
270
+ tap_schema_name
271
+ Name of the TAP_SCHEMA schema in the database.
272
+ tap_tables_postfix
273
+ Postfix which is applied to standard TAP_SCHEMA table names.
274
+ """
275
+ url = make_url(engine_url)
276
+ engine: Engine | MockConnection
277
+ if is_mock_url(url):
278
+ raise click.ClickException("Mock engine URL is not supported for this command")
279
+ engine = create_engine(engine_url)
280
+ mgr = TableManager(
281
+ apply_schema_to_metadata=False if engine.dialect.name == "sqlite" else True,
282
+ schema_name=tap_schema_name,
283
+ table_name_postfix=tap_tables_postfix,
284
+ )
285
+ mgr.initialize_database(engine)
286
+
287
+
420
288
  @cli.command("validate", help="Validate one or more Felis YAML files")
421
289
  @click.option(
422
290
  "--check-description", is_flag=True, help="Check that all objects have a description", default=False
@@ -134,6 +134,32 @@ class DataType(StrEnum):
134
134
  timestamp = auto()
135
135
 
136
136
 
137
+ def validate_ivoa_ucd(ivoa_ucd: str) -> str:
138
+ """Validate IVOA UCD values.
139
+
140
+ Parameters
141
+ ----------
142
+ ivoa_ucd
143
+ IVOA UCD value to check.
144
+
145
+ Returns
146
+ -------
147
+ `str`
148
+ The IVOA UCD value if it is valid.
149
+
150
+ Raises
151
+ ------
152
+ ValueError
153
+ If the IVOA UCD value is invalid.
154
+ """
155
+ if ivoa_ucd is not None:
156
+ try:
157
+ ucd.parse_ucd(ivoa_ucd, check_controlled_vocabulary=True, has_colon=";" in ivoa_ucd)
158
+ except ValueError as e:
159
+ raise ValueError(f"Invalid IVOA UCD: {e}")
160
+ return ivoa_ucd
161
+
162
+
137
163
  class Column(BaseObject):
138
164
  """Column model."""
139
165
 
@@ -235,12 +261,7 @@ class Column(BaseObject):
235
261
  `str`
236
262
  The IVOA UCD value if it is valid.
237
263
  """
238
- if ivoa_ucd is not None:
239
- try:
240
- ucd.parse_ucd(ivoa_ucd, check_controlled_vocabulary=True, has_colon=";" in ivoa_ucd)
241
- except ValueError as e:
242
- raise ValueError(f"Invalid IVOA UCD: {e}")
243
- return ivoa_ucd
264
+ return validate_ivoa_ucd(ivoa_ucd)
244
265
 
245
266
  @model_validator(mode="after")
246
267
  def check_units(self) -> Column:
@@ -551,6 +572,70 @@ _ConstraintType = Annotated[
551
572
  """Type alias for a constraint type."""
552
573
 
553
574
 
575
+ ColumnRef: TypeAlias = str
576
+ """Type alias for a column reference."""
577
+
578
+
579
+ class ColumnGroup(BaseObject):
580
+ """Column group model."""
581
+
582
+ columns: list[ColumnRef | Column] = Field(..., min_length=1)
583
+ """Columns in the group."""
584
+
585
+ ivoa_ucd: str | None = Field(None, alias="ivoa:ucd")
586
+ """IVOA UCD of the column."""
587
+
588
+ table: Table | None = None
589
+ """Reference to the parent table."""
590
+
591
+ @field_validator("ivoa_ucd")
592
+ @classmethod
593
+ def check_ivoa_ucd(cls, ivoa_ucd: str) -> str:
594
+ """Check that IVOA UCD values are valid.
595
+
596
+ Parameters
597
+ ----------
598
+ ivoa_ucd
599
+ IVOA UCD value to check.
600
+
601
+ Returns
602
+ -------
603
+ `str`
604
+ The IVOA UCD value if it is valid.
605
+ """
606
+ return validate_ivoa_ucd(ivoa_ucd)
607
+
608
+ @model_validator(mode="after")
609
+ def check_unique_columns(self) -> ColumnGroup:
610
+ """Check that the columns list contains unique items.
611
+
612
+ Returns
613
+ -------
614
+ `ColumnGroup`
615
+ The column group being validated.
616
+ """
617
+ column_ids = [col if isinstance(col, str) else col.id for col in self.columns]
618
+ if len(column_ids) != len(set(column_ids)):
619
+ raise ValueError("Columns in the group must be unique")
620
+ return self
621
+
622
+ def _dereference_columns(self) -> None:
623
+ """Dereference ColumnRef to Column objects."""
624
+ if self.table is None:
625
+ raise ValueError("ColumnGroup must have a reference to its parent table")
626
+
627
+ dereferenced_columns: list[ColumnRef | Column] = []
628
+ for col in self.columns:
629
+ if isinstance(col, str):
630
+ # Dereference ColumnRef to Column object
631
+ col_obj = self.table._find_column_by_id(col)
632
+ dereferenced_columns.append(col_obj)
633
+ else:
634
+ dereferenced_columns.append(col)
635
+
636
+ self.columns = dereferenced_columns
637
+
638
+
554
639
  class Table(BaseObject):
555
640
  """Table model."""
556
641
 
@@ -563,6 +648,9 @@ class Table(BaseObject):
563
648
  indexes: list[Index] = Field(default_factory=list)
564
649
  """Indexes on the table."""
565
650
 
651
+ column_groups: list[ColumnGroup] = Field(default_factory=list, alias="columnGroups")
652
+ """Column groups in the table."""
653
+
566
654
  primary_key: str | list[str] | None = Field(None, alias="primaryKey")
567
655
  """Primary key of the table."""
568
656
 
@@ -653,6 +741,43 @@ class Table(BaseObject):
653
741
  return self
654
742
  raise ValueError(f"Table '{self.name}' is missing at least one column designated as 'tap:principal'")
655
743
 
744
+ def _find_column_by_id(self, id: str) -> Column:
745
+ """Find a column by ID.
746
+
747
+ Parameters
748
+ ----------
749
+ id
750
+ The ID of the column to find.
751
+
752
+ Returns
753
+ -------
754
+ `Column`
755
+ The column with the given ID.
756
+
757
+ Raises
758
+ ------
759
+ ValueError
760
+ Raised if the column is not found.
761
+ """
762
+ for column in self.columns:
763
+ if column.id == id:
764
+ return column
765
+ raise ValueError(f"Column '{id}' not found in table '{self.name}'")
766
+
767
+ @model_validator(mode="after")
768
+ def dereference_column_groups(self: Table) -> Table:
769
+ """Dereference columns in column groups.
770
+
771
+ Returns
772
+ -------
773
+ `Table`
774
+ The table with dereferenced column groups.
775
+ """
776
+ for group in self.column_groups:
777
+ group.table = self
778
+ group._dereference_columns()
779
+ return self
780
+
656
781
 
657
782
  class SchemaVersion(BaseModel):
658
783
  """Schema version model."""
@@ -127,6 +127,8 @@ class MetaDataBuilder:
127
127
  Whether to apply the schema name to the metadata object.
128
128
  ignore_constraints
129
129
  Whether to ignore constraints when building the metadata.
130
+ table_name_postfix
131
+ A string to append to the table names when building the metadata.
130
132
  """
131
133
 
132
134
  def __init__(
@@ -134,6 +136,7 @@ class MetaDataBuilder:
134
136
  schema: Schema,
135
137
  apply_schema_to_metadata: bool = True,
136
138
  ignore_constraints: bool = False,
139
+ table_name_postfix: str = "",
137
140
  ) -> None:
138
141
  """Initialize the metadata builder."""
139
142
  self.schema = schema
@@ -142,6 +145,7 @@ class MetaDataBuilder:
142
145
  self.metadata = MetaData(schema=schema.name if apply_schema_to_metadata else None)
143
146
  self._objects: dict[str, Any] = {}
144
147
  self.ignore_constraints = ignore_constraints
148
+ self.table_name_postfix = table_name_postfix
145
149
 
146
150
  def build(self) -> MetaData:
147
151
  """Build the SQLAlchemy tables and constraints from the schema.
@@ -225,7 +229,7 @@ class MetaDataBuilder:
225
229
  description = table_obj.description
226
230
  columns = [self.build_column(column) for column in table_obj.columns]
227
231
  table = Table(
228
- name,
232
+ name + self.table_name_postfix,
229
233
  self.metadata,
230
234
  *columns,
231
235
  comment=description,
@@ -91,9 +91,15 @@ class TableManager:
91
91
  self.table_name_postfix = table_name_postfix
92
92
  self.apply_schema_to_metadata = apply_schema_to_metadata
93
93
  self.schema_name = schema_name or TableManager._SCHEMA_NAME_STD
94
+ self.table_name_postfix = table_name_postfix
94
95
 
95
96
  if is_valid_engine(engine):
96
97
  assert isinstance(engine, Engine)
98
+ if table_name_postfix != "":
99
+ logger.warning(
100
+ "Table name postfix '%s' will be ignored when reflecting TAP_SCHEMA database",
101
+ table_name_postfix,
102
+ )
97
103
  logger.debug(
98
104
  "Reflecting TAP_SCHEMA database from existing database at %s",
99
105
  engine.url._replace(password="***"),
@@ -131,7 +137,9 @@ class TableManager:
131
137
  self.schema_name = self.schema.name
132
138
 
133
139
  self._metadata = MetaDataBuilder(
134
- self.schema, apply_schema_to_metadata=self.apply_schema_to_metadata
140
+ self.schema,
141
+ apply_schema_to_metadata=self.apply_schema_to_metadata,
142
+ table_name_postfix=self.table_name_postfix,
135
143
  ).build()
136
144
 
137
145
  logger.debug("Loaded TAP_SCHEMA '%s' from YAML resource", self.schema_name)
@@ -353,7 +361,7 @@ class TableManager:
353
361
  engine
354
362
  The SQLAlchemy engine to use to create the tables.
355
363
  """
356
- logger.info("Creating TAP_SCHEMA database '%s'", self.metadata.schema)
364
+ logger.info("Creating TAP_SCHEMA database '%s'", self.schema_name)
357
365
  self._create_schema(engine)
358
366
  self.metadata.create_all(engine)
359
367
 
@@ -424,7 +432,7 @@ class DataLoader:
424
432
  # Execute the inserts if not in dry run mode.
425
433
  self._execute_inserts()
426
434
  else:
427
- logger.info("Dry run: not loading data into database")
435
+ logger.info("Dry run - not loading data into database")
428
436
 
429
437
  def _insert_schemas(self) -> None:
430
438
  """Insert the schema data into the schemas table."""
@@ -565,7 +573,7 @@ class DataLoader:
565
573
  def _print_sql(self) -> None:
566
574
  """Print the generated inserts to stdout."""
567
575
  for insert_str in self._compiled_inserts():
568
- print(insert_str)
576
+ print(insert_str + ";")
569
577
 
570
578
  def _write_sql_to_file(self) -> None:
571
579
  """Write the generated insert statements to a file."""
@@ -573,7 +581,7 @@ class DataLoader:
573
581
  raise ValueError("No output path specified")
574
582
  with open(self.output_path, "w") as outfile:
575
583
  for insert_str in self._compiled_inserts():
576
- outfile.write(insert_str + "\n")
584
+ outfile.write(insert_str + ";" + "\n")
577
585
 
578
586
  def _insert(self, table_name: str, record: list[Any] | dict[str, Any]) -> None:
579
587
  """Generate an insert statement for a record.
@@ -0,0 +1,2 @@
1
+ __all__ = ["__version__"]
2
+ __version__ = "28.2025.600"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: lsst-felis
3
- Version: 28.2025.402
3
+ Version: 28.2025.600
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+)
@@ -9,7 +9,6 @@ python/felis/datamodel.py
9
9
  python/felis/diff.py
10
10
  python/felis/metadata.py
11
11
  python/felis/py.typed
12
- python/felis/tap.py
13
12
  python/felis/tap_schema.py
14
13
  python/felis/types.py
15
14
  python/felis/version.py
@@ -35,6 +34,5 @@ tests/test_db.py
35
34
  tests/test_diff.py
36
35
  tests/test_metadata.py
37
36
  tests/test_postgres.py
38
- tests/test_tap.py
39
37
  tests/test_tap_schema.py
40
38
  tests/test_tap_schema_postgres.py
@@ -30,7 +30,6 @@ from sqlalchemy import create_engine
30
30
  import felis.tap_schema as tap_schema
31
31
  from felis.cli import cli
32
32
  from felis.datamodel import Schema
33
- from felis.db.dialects import get_supported_dialects
34
33
  from felis.metadata import MetaDataBuilder
35
34
 
36
35
  TESTDIR = os.path.abspath(os.path.dirname(__file__))
@@ -91,38 +90,6 @@ class CliTestCase(unittest.TestCase):
91
90
  )
92
91
  self.assertEqual(result.exit_code, 0)
93
92
 
94
- def test_init_tap(self) -> None:
95
- """Test for ``init-tap`` command."""
96
- url = f"sqlite:///{self.tmpdir}/tap.sqlite3"
97
- runner = CliRunner()
98
- result = runner.invoke(cli, ["init-tap", url], catch_exceptions=False)
99
- self.assertEqual(result.exit_code, 0)
100
-
101
- def test_load_tap(self) -> None:
102
- """Test for ``load-tap`` command."""
103
- # Cannot use the same url for both init-tap and load-tap in the same
104
- # process.
105
- url = f"sqlite:///{self.tmpdir}/tap.sqlite3"
106
-
107
- # Need to run init-tap first.
108
- runner = CliRunner()
109
- result = runner.invoke(cli, ["init-tap", url])
110
- self.assertEqual(result.exit_code, 0)
111
-
112
- result = runner.invoke(cli, ["load-tap", f"--engine-url={url}", TEST_YAML], catch_exceptions=False)
113
- self.assertEqual(result.exit_code, 0)
114
-
115
- def test_load_tap_mock(self) -> None:
116
- """Test ``load-tap --dry-run`` command on supported dialects."""
117
- urls = [f"{dialect_name}://" for dialect_name in get_supported_dialects().keys()]
118
-
119
- for url in urls:
120
- runner = CliRunner()
121
- result = runner.invoke(
122
- cli, ["load-tap", f"--engine-url={url}", "--dry-run", TEST_YAML], catch_exceptions=False
123
- )
124
- self.assertEqual(result.exit_code, 0)
125
-
126
93
  def test_validate_default(self) -> None:
127
94
  """Test validate command."""
128
95
  runner = CliRunner()
@@ -172,9 +139,9 @@ class CliTestCase(unittest.TestCase):
172
139
  self.assertTrue(result.exit_code != 0)
173
140
 
174
141
  def test_load_tap_schema(self) -> None:
175
- """Test for ``load-tap-schema`` command."""
142
+ """Test load-tap-schema command."""
176
143
  # Create the TAP_SCHEMA database.
177
- url = f"sqlite:///{self.tmpdir}/tap_schema.sqlite3"
144
+ url = f"sqlite:///{self.tmpdir}/load_tap_schema.sqlite3"
178
145
  runner = CliRunner()
179
146
  tap_schema_path = tap_schema.TableManager.get_tap_schema_std_path()
180
147
  result = runner.invoke(
@@ -191,6 +158,21 @@ class CliTestCase(unittest.TestCase):
191
158
  )
192
159
  self.assertEqual(result.exit_code, 0)
193
160
 
161
+ def test_init_tap_schema(self) -> None:
162
+ """Test init-tap-schema command."""
163
+ url = f"sqlite:///{self.tmpdir}/init_tap_schema.sqlite3"
164
+ runner = CliRunner()
165
+ result = runner.invoke(cli, ["init-tap-schema", f"--engine-url={url}"], catch_exceptions=False)
166
+ self.assertEqual(result.exit_code, 0)
167
+
168
+ def test_init_tap_schema_mock(self) -> None:
169
+ """Test init-tap-schema command with a mock URL, which should throw
170
+ an error, as this is not supported.
171
+ """
172
+ runner = CliRunner()
173
+ result = runner.invoke(cli, ["init-tap-schema", "sqlite://"], catch_exceptions=False)
174
+ self.assertNotEqual(result.exit_code, 0)
175
+
194
176
  def test_diff(self) -> None:
195
177
  """Test for ``diff`` command."""
196
178
  test_diff1 = os.path.join(TESTDIR, "data", "test_diff1.yaml")