lsst-felis 29.2025.2700__tar.gz → 29.2025.2900__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 (44) hide show
  1. {lsst_felis-29.2025.2700/python/lsst_felis.egg-info → lsst_felis-29.2025.2900}/PKG-INFO +1 -1
  2. {lsst_felis-29.2025.2700 → lsst_felis-29.2025.2900}/python/felis/datamodel.py +27 -3
  3. {lsst_felis-29.2025.2700 → lsst_felis-29.2025.2900}/python/felis/tap_schema.py +24 -14
  4. {lsst_felis-29.2025.2700 → lsst_felis-29.2025.2900/python/lsst_felis.egg-info}/PKG-INFO +1 -1
  5. {lsst_felis-29.2025.2700 → lsst_felis-29.2025.2900}/tests/test_datamodel.py +25 -0
  6. {lsst_felis-29.2025.2700 → lsst_felis-29.2025.2900}/tests/test_tap_schema.py +64 -5
  7. {lsst_felis-29.2025.2700 → lsst_felis-29.2025.2900}/COPYRIGHT +0 -0
  8. {lsst_felis-29.2025.2700 → lsst_felis-29.2025.2900}/LICENSE +0 -0
  9. {lsst_felis-29.2025.2700 → lsst_felis-29.2025.2900}/README.rst +0 -0
  10. {lsst_felis-29.2025.2700 → lsst_felis-29.2025.2900}/pyproject.toml +0 -0
  11. {lsst_felis-29.2025.2700 → lsst_felis-29.2025.2900}/python/felis/__init__.py +0 -0
  12. {lsst_felis-29.2025.2700 → lsst_felis-29.2025.2900}/python/felis/cli.py +0 -0
  13. {lsst_felis-29.2025.2700 → lsst_felis-29.2025.2900}/python/felis/config/tap_schema/columns.csv +0 -0
  14. {lsst_felis-29.2025.2700 → lsst_felis-29.2025.2900}/python/felis/config/tap_schema/key_columns.csv +0 -0
  15. {lsst_felis-29.2025.2700 → lsst_felis-29.2025.2900}/python/felis/config/tap_schema/keys.csv +0 -0
  16. {lsst_felis-29.2025.2700 → lsst_felis-29.2025.2900}/python/felis/config/tap_schema/schemas.csv +0 -0
  17. {lsst_felis-29.2025.2700 → lsst_felis-29.2025.2900}/python/felis/config/tap_schema/tables.csv +0 -0
  18. {lsst_felis-29.2025.2700 → lsst_felis-29.2025.2900}/python/felis/config/tap_schema/tap_schema_std.yaml +0 -0
  19. {lsst_felis-29.2025.2700 → lsst_felis-29.2025.2900}/python/felis/db/__init__.py +0 -0
  20. {lsst_felis-29.2025.2700 → lsst_felis-29.2025.2900}/python/felis/db/dialects.py +0 -0
  21. {lsst_felis-29.2025.2700 → lsst_felis-29.2025.2900}/python/felis/db/schema.py +0 -0
  22. {lsst_felis-29.2025.2700 → lsst_felis-29.2025.2900}/python/felis/db/sqltypes.py +0 -0
  23. {lsst_felis-29.2025.2700 → lsst_felis-29.2025.2900}/python/felis/db/utils.py +0 -0
  24. {lsst_felis-29.2025.2700 → lsst_felis-29.2025.2900}/python/felis/db/variants.py +0 -0
  25. {lsst_felis-29.2025.2700 → lsst_felis-29.2025.2900}/python/felis/diff.py +0 -0
  26. {lsst_felis-29.2025.2700 → lsst_felis-29.2025.2900}/python/felis/metadata.py +0 -0
  27. {lsst_felis-29.2025.2700 → lsst_felis-29.2025.2900}/python/felis/py.typed +0 -0
  28. {lsst_felis-29.2025.2700 → lsst_felis-29.2025.2900}/python/felis/tests/__init__.py +0 -0
  29. {lsst_felis-29.2025.2700 → lsst_felis-29.2025.2900}/python/felis/tests/postgresql.py +0 -0
  30. {lsst_felis-29.2025.2700 → lsst_felis-29.2025.2900}/python/felis/tests/run_cli.py +0 -0
  31. {lsst_felis-29.2025.2700 → lsst_felis-29.2025.2900}/python/felis/types.py +0 -0
  32. {lsst_felis-29.2025.2700 → lsst_felis-29.2025.2900}/python/lsst_felis.egg-info/SOURCES.txt +0 -0
  33. {lsst_felis-29.2025.2700 → lsst_felis-29.2025.2900}/python/lsst_felis.egg-info/dependency_links.txt +0 -0
  34. {lsst_felis-29.2025.2700 → lsst_felis-29.2025.2900}/python/lsst_felis.egg-info/entry_points.txt +0 -0
  35. {lsst_felis-29.2025.2700 → lsst_felis-29.2025.2900}/python/lsst_felis.egg-info/requires.txt +0 -0
  36. {lsst_felis-29.2025.2700 → lsst_felis-29.2025.2900}/python/lsst_felis.egg-info/top_level.txt +0 -0
  37. {lsst_felis-29.2025.2700 → lsst_felis-29.2025.2900}/python/lsst_felis.egg-info/zip-safe +0 -0
  38. {lsst_felis-29.2025.2700 → lsst_felis-29.2025.2900}/setup.cfg +0 -0
  39. {lsst_felis-29.2025.2700 → lsst_felis-29.2025.2900}/tests/test_cli.py +0 -0
  40. {lsst_felis-29.2025.2700 → lsst_felis-29.2025.2900}/tests/test_db.py +0 -0
  41. {lsst_felis-29.2025.2700 → lsst_felis-29.2025.2900}/tests/test_diff.py +0 -0
  42. {lsst_felis-29.2025.2700 → lsst_felis-29.2025.2900}/tests/test_metadata.py +0 -0
  43. {lsst_felis-29.2025.2700 → lsst_felis-29.2025.2900}/tests/test_postgres.py +0 -0
  44. {lsst_felis-29.2025.2700 → lsst_felis-29.2025.2900}/tests/test_tap_schema_postgres.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lsst-felis
3
- Version: 29.2025.2700
3
+ Version: 29.2025.2900
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+)
@@ -618,7 +618,9 @@ class ForeignKeyConstraint(Constraint):
618
618
  """Table foreign key constraint model.
619
619
 
620
620
  This constraint is used to define a foreign key relationship between two
621
- tables in the schema.
621
+ tables in the schema. There must be at least one column in the
622
+ `columns` list, and at least one column in the `referenced_columns` list
623
+ or a validation error will be raised.
622
624
 
623
625
  Notes
624
626
  -----
@@ -629,10 +631,10 @@ class ForeignKeyConstraint(Constraint):
629
631
  type: Literal["ForeignKey"] = Field("ForeignKey", alias="@type")
630
632
  """Type of the constraint."""
631
633
 
632
- columns: list[str]
634
+ columns: list[str] = Field(min_length=1)
633
635
  """The columns comprising the foreign key."""
634
636
 
635
- referenced_columns: list[str] = Field(alias="referencedColumns")
637
+ referenced_columns: list[str] = Field(alias="referencedColumns", min_length=1)
636
638
  """The columns referenced by the foreign key."""
637
639
 
638
640
  on_delete: Literal["CASCADE", "SET NULL", "SET DEFAULT", "RESTRICT", "NO ACTION"] | None = None
@@ -657,6 +659,28 @@ class ForeignKeyConstraint(Constraint):
657
659
  """
658
660
  return value
659
661
 
662
+ @model_validator(mode="after")
663
+ def check_column_lengths(self) -> ForeignKeyConstraint:
664
+ """Check that the `columns` and `referenced_columns` lists have the
665
+ same length.
666
+
667
+ Returns
668
+ -------
669
+ `ForeignKeyConstraint`
670
+ The foreign key constraint being validated.
671
+
672
+ Raises
673
+ ------
674
+ ValueError
675
+ Raised if the `columns` and `referenced_columns` lists do not have
676
+ the same length.
677
+ """
678
+ if len(self.columns) != len(self.referenced_columns):
679
+ raise ValueError(
680
+ "Columns and referencedColumns must have the same length for a ForeignKey constraint"
681
+ )
682
+ return self
683
+
660
684
 
661
685
  _ConstraintType = Annotated[
662
686
  CheckConstraint | ForeignKeyConstraint | UniqueConstraint, Field(discriminator="type")
@@ -470,7 +470,7 @@ class DataLoader:
470
470
  logger.info("Dry run - not loading data into database")
471
471
 
472
472
  def _insert_schemas(self) -> None:
473
- """Insert the schema data into the schemas table."""
473
+ """Insert the schema data into the ``schemas`` table."""
474
474
  schema_record = {
475
475
  "schema_name": self.schema.name,
476
476
  "utype": self.schema.votable_utype,
@@ -495,7 +495,7 @@ class DataLoader:
495
495
  return f"{self.schema.name}.{table.name}"
496
496
 
497
497
  def _insert_tables(self) -> None:
498
- """Insert the table data into the tables table."""
498
+ """Insert the table data into the ``tables`` table."""
499
499
  for table in self.schema.tables:
500
500
  table_record = {
501
501
  "schema_name": self.schema.name,
@@ -508,7 +508,7 @@ class DataLoader:
508
508
  self._insert("tables", table_record)
509
509
 
510
510
  def _insert_columns(self) -> None:
511
- """Insert the column data into the columns table."""
511
+ """Insert the column data into the ``columns`` table."""
512
512
  for table in self.schema.tables:
513
513
  for column in table.columns:
514
514
  felis_type = FelisType.felis_type(column.datatype.value)
@@ -563,11 +563,15 @@ class DataLoader:
563
563
  return key_id
564
564
 
565
565
  def _insert_keys(self) -> None:
566
- """Insert the foreign keys into the keys and key_columns tables."""
566
+ """Insert the foreign keys into the ``keys`` and ``key_columns``
567
+ tables.
568
+ """
567
569
  for table in self.schema.tables:
568
570
  for constraint in table.constraints:
569
571
  if isinstance(constraint, datamodel.ForeignKeyConstraint):
572
+ ###########################################################
570
573
  # Handle keys table
574
+ ###########################################################
571
575
  referenced_column = self.schema.find_object_by_id(
572
576
  constraint.referenced_columns[0], datamodel.Column
573
577
  )
@@ -582,17 +586,23 @@ class DataLoader:
582
586
  }
583
587
  self._insert("keys", key_record)
584
588
 
589
+ ###########################################################
585
590
  # Handle key_columns table
586
- from_column = self.schema.find_object_by_id(constraint.columns[0], datamodel.Column)
587
- target_column = self.schema.find_object_by_id(
588
- constraint.referenced_columns[0], datamodel.Column
589
- )
590
- key_columns_record = {
591
- "key_id": key_id,
592
- "from_column": from_column.name,
593
- "target_column": target_column.name,
594
- }
595
- self._insert("key_columns", key_columns_record)
591
+ ###########################################################
592
+ # Loop over the corresponding columns and referenced
593
+ # columns and insert a record for each pair. This is
594
+ # necessary for proper handling of composite keys.
595
+ for from_column_id, target_column_id in zip(
596
+ constraint.columns, constraint.referenced_columns
597
+ ):
598
+ from_column = self.schema.find_object_by_id(from_column_id, datamodel.Column)
599
+ target_column = self.schema.find_object_by_id(target_column_id, datamodel.Column)
600
+ key_columns_record = {
601
+ "key_id": key_id,
602
+ "from_column": from_column.name,
603
+ "target_column": target_column.name,
604
+ }
605
+ self._insert("key_columns", key_columns_record)
596
606
 
597
607
  def _generate_all_inserts(self) -> None:
598
608
  """Generate the inserts for all the data."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lsst-felis
3
- Version: 29.2025.2700
3
+ Version: 29.2025.2900
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+)
@@ -422,6 +422,31 @@ class ConstraintTestCase(unittest.TestCase):
422
422
  constraint.referenced_columns, ["test_column"], "referenced_columns should be ['test_column']"
423
423
  )
424
424
 
425
+ # Creating a foreign key constraint with no columns should raise an
426
+ # exception.
427
+ with self.assertRaises(ValidationError):
428
+ ForeignKeyConstraint(
429
+ name="fk_test", id="#fk_test", columns=[], referenced_columns=["test_column"]
430
+ )
431
+
432
+ # Creating a foreign key constraint with no referenced columns should
433
+ # raise an exception.
434
+ with self.assertRaises(ValidationError):
435
+ ForeignKeyConstraint(
436
+ name="fk_test", id="#fk_test", columns=["test_column"], referenced_columns=[]
437
+ )
438
+
439
+ # Creating a foreign key constraint where the number of foreign key
440
+ # columns does not match the number of referenced columns should raise
441
+ # an exception.
442
+ with self.assertRaises(ValidationError):
443
+ ForeignKeyConstraint(
444
+ name="fk_test",
445
+ id="#fk_test",
446
+ columns=["test_column", "test_column2"],
447
+ referenced_columns=["test_column"],
448
+ )
449
+
425
450
  def test_check_constraint(self) -> None:
426
451
  """Test validation of check constraints."""
427
452
  # Setting name and id should throw an exception from missing
@@ -33,6 +33,7 @@ from felis.tap_schema import DataLoader, TableManager
33
33
  TEST_DIR = os.path.dirname(__file__)
34
34
  TEST_SALES = os.path.join(TEST_DIR, "data", "sales.yaml")
35
35
  TEST_TAP_SCHEMA = os.path.join(TEST_DIR, "data", "test_tap_schema.yaml")
36
+ TEST_COMPOSITE_KEYS = os.path.join(TEST_DIR, "data", "test_composite_keys.yaml")
36
37
 
37
38
 
38
39
  class TableManagerTestCase(unittest.TestCase):
@@ -158,12 +159,15 @@ class TapSchemaSqliteSetup:
158
159
 
159
160
  Parameters
160
161
  ----------
161
- context : `dict`
162
+ test_file_path:
163
+ Path to the TAP_SCHEMA test file.
164
+
165
+ context
162
166
  Context for the schema. Default is an empty dictionary.
163
167
  """
164
168
 
165
- def __init__(self, context: dict = {}) -> None:
166
- with open(TEST_TAP_SCHEMA) as test_file:
169
+ def __init__(self, test_file_path: str, context: dict = {}) -> None:
170
+ with open(test_file_path) as test_file:
167
171
  self._schema = Schema.from_stream(test_file, context=context)
168
172
 
169
173
  self._engine = create_engine("sqlite:///:memory:")
@@ -196,7 +200,7 @@ class TapSchemaDataTest(unittest.TestCase):
196
200
 
197
201
  def setUp(self) -> None:
198
202
  """Set up the test case."""
199
- self.tap_schema_setup = TapSchemaSqliteSetup(context={"id_generation": True})
203
+ self.tap_schema_setup = TapSchemaSqliteSetup(TEST_TAP_SCHEMA, context={"id_generation": True})
200
204
 
201
205
  def test_schemas(self) -> None:
202
206
  schemas_table = self.tap_schema_setup.mgr["schemas"]
@@ -347,7 +351,7 @@ class ForceUnboundArraySizeTest(unittest.TestCase):
347
351
  def setUp(self) -> None:
348
352
  """Set up the test case."""
349
353
  self.tap_schema_setup = TapSchemaSqliteSetup(
350
- context={"id_generation": True, "force_unbounded_arraysize": True}
354
+ TEST_TAP_SCHEMA, context={"id_generation": True, "force_unbounded_arraysize": True}
351
355
  )
352
356
 
353
357
  def test_force_unbounded_arraysize(self) -> None:
@@ -365,5 +369,60 @@ class ForceUnboundArraySizeTest(unittest.TestCase):
365
369
  self.assertEqual(row["arraysize"], "*")
366
370
 
367
371
 
372
+ class CompositeKeysTestCase(unittest.TestCase):
373
+ """Test the handling of composite foreign keys."""
374
+
375
+ def setUp(self) -> None:
376
+ """Set up the test case."""
377
+ self.tap_schema_setup = TapSchemaSqliteSetup(TEST_COMPOSITE_KEYS, context={"id_generation": True})
378
+
379
+ # Fetch the keys and key_columns data from the TAP_SCHEMA tables.
380
+ keys_table = self.tap_schema_setup.mgr["keys"]
381
+ key_columns_table = self.tap_schema_setup.mgr["key_columns"]
382
+ with self.tap_schema_setup.engine.connect() as connection:
383
+ key_columns_result = connection.execute(select(key_columns_table))
384
+ self.key_columns_data = [row._asdict() for row in key_columns_result]
385
+
386
+ keys_result = connection.execute(select(keys_table))
387
+ self.keys_data = [row._asdict() for row in keys_result]
388
+
389
+ def test_keys(self) -> None:
390
+ """Test that composite keys are handled correctly by inspecting the
391
+ data in the generated TAP_SCHEMA ``keys`` table.
392
+ """
393
+ print(f"\nComposite keys data: {self.keys_data}")
394
+
395
+ self.assertEqual(len(self.keys_data), 1)
396
+
397
+ self.assertEqual(
398
+ self.keys_data[0],
399
+ {
400
+ "key_id": "fk_composite",
401
+ "from_table": "test_composite_keys.table1",
402
+ "target_table": "test_composite_keys.table2",
403
+ "utype": "ForeignKey",
404
+ "description": "Composite foreign key from table1 to table2",
405
+ },
406
+ )
407
+
408
+ def test_key_columns(self) -> None:
409
+ """Test that composite keys are handled correctly by inspecting the
410
+ data in the generated TAP_SCHEMA ``key_columns`` table.
411
+ """
412
+ print(f"\nComposite key columns data: {self.key_columns_data}")
413
+
414
+ self.assertEqual(len(self.key_columns_data), 2)
415
+
416
+ key_columns_row1 = self.key_columns_data[0]
417
+ self.assertEqual(
418
+ key_columns_row1, {"key_id": "fk_composite", "from_column": "id1", "target_column": "id1"}
419
+ )
420
+
421
+ key_columns_row2 = self.key_columns_data[1]
422
+ self.assertEqual(
423
+ key_columns_row2, {"key_id": "fk_composite", "from_column": "id2", "target_column": "id2"}
424
+ )
425
+
426
+
368
427
  if __name__ == "__main__":
369
428
  unittest.main()