lsst-felis 29.2025.1000__tar.gz → 29.2025.1100__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 (38) hide show
  1. {lsst_felis-29.2025.1000/python/lsst_felis.egg-info → lsst_felis-29.2025.1100}/PKG-INFO +1 -1
  2. {lsst_felis-29.2025.1000 → lsst_felis-29.2025.1100}/python/felis/cli.py +20 -6
  3. {lsst_felis-29.2025.1000 → lsst_felis-29.2025.1100}/python/felis/tap_schema.py +65 -5
  4. {lsst_felis-29.2025.1000 → lsst_felis-29.2025.1100/python/lsst_felis.egg-info}/PKG-INFO +1 -1
  5. {lsst_felis-29.2025.1000 → lsst_felis-29.2025.1100}/tests/test_tap_schema.py +32 -14
  6. {lsst_felis-29.2025.1000 → lsst_felis-29.2025.1100}/COPYRIGHT +0 -0
  7. {lsst_felis-29.2025.1000 → lsst_felis-29.2025.1100}/LICENSE +0 -0
  8. {lsst_felis-29.2025.1000 → lsst_felis-29.2025.1100}/README.rst +0 -0
  9. {lsst_felis-29.2025.1000 → lsst_felis-29.2025.1100}/pyproject.toml +0 -0
  10. {lsst_felis-29.2025.1000 → lsst_felis-29.2025.1100}/python/felis/__init__.py +0 -0
  11. {lsst_felis-29.2025.1000 → lsst_felis-29.2025.1100}/python/felis/datamodel.py +0 -0
  12. {lsst_felis-29.2025.1000 → lsst_felis-29.2025.1100}/python/felis/db/__init__.py +0 -0
  13. {lsst_felis-29.2025.1000 → lsst_felis-29.2025.1100}/python/felis/db/dialects.py +0 -0
  14. {lsst_felis-29.2025.1000 → lsst_felis-29.2025.1100}/python/felis/db/schema.py +0 -0
  15. {lsst_felis-29.2025.1000 → lsst_felis-29.2025.1100}/python/felis/db/sqltypes.py +0 -0
  16. {lsst_felis-29.2025.1000 → lsst_felis-29.2025.1100}/python/felis/db/utils.py +0 -0
  17. {lsst_felis-29.2025.1000 → lsst_felis-29.2025.1100}/python/felis/db/variants.py +0 -0
  18. {lsst_felis-29.2025.1000 → lsst_felis-29.2025.1100}/python/felis/diff.py +0 -0
  19. {lsst_felis-29.2025.1000 → lsst_felis-29.2025.1100}/python/felis/metadata.py +0 -0
  20. {lsst_felis-29.2025.1000 → lsst_felis-29.2025.1100}/python/felis/py.typed +0 -0
  21. {lsst_felis-29.2025.1000 → lsst_felis-29.2025.1100}/python/felis/schemas/tap_schema_std.yaml +0 -0
  22. {lsst_felis-29.2025.1000 → lsst_felis-29.2025.1100}/python/felis/tests/__init__.py +0 -0
  23. {lsst_felis-29.2025.1000 → lsst_felis-29.2025.1100}/python/felis/tests/postgresql.py +0 -0
  24. {lsst_felis-29.2025.1000 → lsst_felis-29.2025.1100}/python/felis/types.py +0 -0
  25. {lsst_felis-29.2025.1000 → lsst_felis-29.2025.1100}/python/lsst_felis.egg-info/SOURCES.txt +0 -0
  26. {lsst_felis-29.2025.1000 → lsst_felis-29.2025.1100}/python/lsst_felis.egg-info/dependency_links.txt +0 -0
  27. {lsst_felis-29.2025.1000 → lsst_felis-29.2025.1100}/python/lsst_felis.egg-info/entry_points.txt +0 -0
  28. {lsst_felis-29.2025.1000 → lsst_felis-29.2025.1100}/python/lsst_felis.egg-info/requires.txt +0 -0
  29. {lsst_felis-29.2025.1000 → lsst_felis-29.2025.1100}/python/lsst_felis.egg-info/top_level.txt +0 -0
  30. {lsst_felis-29.2025.1000 → lsst_felis-29.2025.1100}/python/lsst_felis.egg-info/zip-safe +0 -0
  31. {lsst_felis-29.2025.1000 → lsst_felis-29.2025.1100}/setup.cfg +0 -0
  32. {lsst_felis-29.2025.1000 → lsst_felis-29.2025.1100}/tests/test_cli.py +0 -0
  33. {lsst_felis-29.2025.1000 → lsst_felis-29.2025.1100}/tests/test_datamodel.py +0 -0
  34. {lsst_felis-29.2025.1000 → lsst_felis-29.2025.1100}/tests/test_db.py +0 -0
  35. {lsst_felis-29.2025.1000 → lsst_felis-29.2025.1100}/tests/test_diff.py +0 -0
  36. {lsst_felis-29.2025.1000 → lsst_felis-29.2025.1100}/tests/test_metadata.py +0 -0
  37. {lsst_felis-29.2025.1000 → lsst_felis-29.2025.1100}/tests/test_postgres.py +0 -0
  38. {lsst_felis-29.2025.1000 → lsst_felis-29.2025.1100}/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: 29.2025.1000
3
+ Version: 29.2025.1100
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+)
@@ -180,14 +180,26 @@ def create(
180
180
 
181
181
  @cli.command("load-tap-schema", help="Load metadata from a Felis file into a TAP_SCHEMA database")
182
182
  @click.option("--engine-url", envvar="FELIS_ENGINE_URL", help="SQLAlchemy Engine URL")
183
- @click.option("--tap-schema-name", help="Name of the TAP_SCHEMA schema in the database (default: TAP_SCHEMA)")
184
183
  @click.option(
185
- "--tap-tables-postfix", help="Postfix which is applied to standard TAP_SCHEMA table names", default=""
184
+ "--tap-schema-name", "-n", help="Name of the TAP_SCHEMA schema in the database (default: TAP_SCHEMA)"
185
+ )
186
+ @click.option(
187
+ "--tap-tables-postfix",
188
+ "-p",
189
+ help="Postfix which is applied to standard TAP_SCHEMA table names",
190
+ default="",
191
+ )
192
+ @click.option("--tap-schema-index", "-i", type=int, help="TAP_SCHEMA index of the schema in this environment")
193
+ @click.option("--dry-run", "-D", is_flag=True, help="Execute dry run only. Does not insert any data.")
194
+ @click.option("--echo", "-e", is_flag=True, help="Print out the generated insert statements to stdout")
195
+ @click.option("--output-file", "-o", type=click.Path(), help="Write SQL commands to a file")
196
+ @click.option(
197
+ "--unique-keys",
198
+ "-u",
199
+ is_flag=True,
200
+ help="Generate unique key_id values for keys and key_columns tables by prepending the schema name",
201
+ default=False,
186
202
  )
187
- @click.option("--tap-schema-index", type=int, help="TAP_SCHEMA index of the schema in this environment")
188
- @click.option("--dry-run", is_flag=True, help="Execute dry run only. Does not insert any data.")
189
- @click.option("--echo", is_flag=True, help="Print out the generated insert statements to stdout")
190
- @click.option("--output-file", type=click.Path(), help="Write SQL commands to a file")
191
203
  @click.argument("file", type=click.File())
192
204
  @click.pass_context
193
205
  def load_tap_schema(
@@ -199,6 +211,7 @@ def load_tap_schema(
199
211
  dry_run: bool,
200
212
  echo: bool,
201
213
  output_file: str | None,
214
+ unique_keys: bool,
202
215
  file: IO[str],
203
216
  ) -> None:
204
217
  """Load TAP metadata from a Felis file.
@@ -248,6 +261,7 @@ def load_tap_schema(
248
261
  dry_run=dry_run,
249
262
  print_sql=echo,
250
263
  output_path=output_file,
264
+ unique_keys=unique_keys,
251
265
  ).load()
252
266
 
253
267
 
@@ -27,7 +27,7 @@ import re
27
27
  from typing import Any
28
28
 
29
29
  from lsst.resources import ResourcePath
30
- from sqlalchemy import MetaData, Table, text
30
+ from sqlalchemy import MetaData, Table, select, text
31
31
  from sqlalchemy.engine import Connection, Engine
32
32
  from sqlalchemy.engine.mock import MockConnection
33
33
  from sqlalchemy.exc import SQLAlchemyError
@@ -35,7 +35,7 @@ from sqlalchemy.schema import CreateSchema
35
35
  from sqlalchemy.sql.dml import Insert
36
36
 
37
37
  from felis import datamodel
38
- from felis.datamodel import Schema
38
+ from felis.datamodel import Constraint, Schema
39
39
  from felis.db.utils import is_valid_engine
40
40
  from felis.metadata import MetaDataBuilder
41
41
 
@@ -163,7 +163,7 @@ class TableManager:
163
163
  tables to be accessed by their standard TAP_SCHEMA names.
164
164
  """
165
165
  if table_name not in self._table_map:
166
- raise KeyError(f"Table '{table_name}' not found in table map")
166
+ raise KeyError(f"Table '{table_name}' not found in TAP_SCHEMA")
167
167
  return self.metadata.tables[self._table_map[table_name]]
168
168
 
169
169
  @property
@@ -365,6 +365,34 @@ class TableManager:
365
365
  self._create_schema(engine)
366
366
  self.metadata.create_all(engine)
367
367
 
368
+ def select(self, engine: Engine, table_name: str, filter_condition: str = "") -> list[dict[str, Any]]:
369
+ """Select all rows from a TAP_SCHEMA table with an optional filter
370
+ condition.
371
+
372
+ Parameters
373
+ ----------
374
+ engine
375
+ The SQLAlchemy engine to use to connect to the database.
376
+ table_name
377
+ The name of the table to select from.
378
+ filter_condition
379
+ The filter condition as a string. If empty, no filter will be
380
+ applied.
381
+
382
+ Returns
383
+ -------
384
+ list
385
+ A list of dictionaries containing the rows from the table.
386
+ """
387
+ table = self[table_name]
388
+ query = select(table)
389
+ if filter_condition:
390
+ query = query.where(text(filter_condition))
391
+ with engine.connect() as connection:
392
+ result = connection.execute(query)
393
+ rows = [dict(row._mapping) for row in result]
394
+ return rows
395
+
368
396
 
369
397
  class DataLoader:
370
398
  """Load data into the TAP_SCHEMA tables.
@@ -386,6 +414,9 @@ class DataLoader:
386
414
  If True, print the SQL statements that will be executed.
387
415
  dry_run
388
416
  If True, the data will not be loaded into the database.
417
+ unique_keys
418
+ If True, prepend the schema name to the key name to make it unique
419
+ when loading data into the keys and key_columns tables.
389
420
  """
390
421
 
391
422
  def __init__(
@@ -397,6 +428,7 @@ class DataLoader:
397
428
  output_path: str | None = None,
398
429
  print_sql: bool = False,
399
430
  dry_run: bool = False,
431
+ unique_keys: bool = False,
400
432
  ):
401
433
  self.schema = schema
402
434
  self.mgr = mgr
@@ -406,6 +438,7 @@ class DataLoader:
406
438
  self.output_path = output_path
407
439
  self.print_sql = print_sql
408
440
  self.dry_run = dry_run
441
+ self.unique_keys = unique_keys
409
442
 
410
443
  def load(self) -> None:
411
444
  """Load the schema data into the TAP_SCHEMA tables.
@@ -501,6 +534,32 @@ class DataLoader:
501
534
  }
502
535
  self._insert("columns", column_record)
503
536
 
537
+ def _get_key(self, constraint: Constraint) -> str:
538
+ """Get the key name for a constraint.
539
+
540
+ Parameters
541
+ ----------
542
+ constraint
543
+ The constraint to get the key name for.
544
+
545
+ Returns
546
+ -------
547
+ str
548
+ The key name for the constraint.
549
+
550
+ Notes
551
+ -----
552
+ This will prepend the name of the schema to the key name if the
553
+ `unique_keys` attribute is set to True. Otherwise, it will just return
554
+ the name of the constraint.
555
+ """
556
+ if self.unique_keys:
557
+ key_id = f"{self.schema.name}_{constraint.name}"
558
+ logger.debug("Generated unique key_id: %s -> %s", constraint.name, key_id)
559
+ else:
560
+ key_id = constraint.name
561
+ return key_id
562
+
504
563
  def _insert_keys(self) -> None:
505
564
  """Insert the foreign keys into the keys and key_columns tables."""
506
565
  for table in self.schema.tables:
@@ -511,8 +570,9 @@ class DataLoader:
511
570
  constraint.referenced_columns[0], datamodel.Column
512
571
  )
513
572
  referenced_table = self.schema.get_table_by_column(referenced_column)
573
+ key_id = self._get_key(constraint)
514
574
  key_record = {
515
- "key_id": constraint.name,
575
+ "key_id": key_id,
516
576
  "from_table": self._get_table_name(table),
517
577
  "target_table": self._get_table_name(referenced_table),
518
578
  "description": constraint.description,
@@ -526,7 +586,7 @@ class DataLoader:
526
586
  constraint.referenced_columns[0], datamodel.Column
527
587
  )
528
588
  key_columns_record = {
529
- "key_id": constraint.name,
589
+ "key_id": key_id,
530
590
  "from_column": from_column.name,
531
591
  "target_column": target_column.name,
532
592
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: lsst-felis
3
- Version: 29.2025.1000
3
+ Version: 29.2025.1100
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+)
@@ -25,7 +25,7 @@ import tempfile
25
25
  import unittest
26
26
  from typing import Any
27
27
 
28
- from sqlalchemy import Engine, MetaData, create_engine, select
28
+ from sqlalchemy import MetaData, create_engine, select
29
29
 
30
30
  from felis.datamodel import Schema
31
31
  from felis.tap_schema import DataLoader, TableManager
@@ -111,6 +111,37 @@ class DataLoaderTestCase(unittest.TestCase):
111
111
  f"Expected 22 'INSERT INTO' statements, found {insert_count}",
112
112
  )
113
113
 
114
+ def test_unique_keys(self) -> None:
115
+ """Test generation of unique foreign keys."""
116
+ engine = create_engine("sqlite:///:memory:")
117
+
118
+ mgr = TableManager(apply_schema_to_metadata=False)
119
+ mgr.initialize_database(engine)
120
+
121
+ loader = DataLoader(self.schema, mgr, engine, unique_keys=True)
122
+ loader.load()
123
+
124
+ keys_data = mgr.select(engine, "keys")
125
+ self.assertGreaterEqual(len(keys_data), 1)
126
+ for row in keys_data:
127
+ self.assertTrue(row["key_id"].startswith(f"{self.schema.name}_"))
128
+
129
+ key_columns_data = mgr.select(engine, "key_columns")
130
+ self.assertGreaterEqual(len(key_columns_data), 1)
131
+ for row in key_columns_data:
132
+ self.assertTrue(row["key_id"].startswith(f"{self.schema.name}_"))
133
+
134
+ def test_select_with_filter(self) -> None:
135
+ """Test selecting rows with a filter."""
136
+ engine = create_engine("sqlite:///:memory:")
137
+ mgr = TableManager(apply_schema_to_metadata=False)
138
+ mgr.initialize_database(engine)
139
+ loader = DataLoader(self.schema, mgr, engine, unique_keys=True)
140
+ loader.load()
141
+
142
+ rows = mgr.select(engine, "columns", "table_name = 'test_schema.table1'")
143
+ self.assertEqual(len(rows), 16)
144
+
114
145
 
115
146
  def _find_row(rows: list[dict[str, Any]], column_name: str, value: str) -> dict[str, Any]:
116
147
  next_row = next(
@@ -122,19 +153,6 @@ def _find_row(rows: list[dict[str, Any]], column_name: str, value: str) -> dict[
122
153
  return next_row
123
154
 
124
155
 
125
- def _fetch_results(_engine: Engine, _metadata: MetaData) -> dict:
126
- results: dict[str, Any] = {}
127
- with _engine.connect() as connection:
128
- for table_name in TableManager.get_table_names_std():
129
- tap_table = _metadata.tables[table_name]
130
- primary_key_columns = tap_table.primary_key.columns
131
- stmt = select(tap_table).order_by(*primary_key_columns)
132
- result = connection.execute(stmt)
133
- column_data = [row._asdict() for row in result]
134
- results[table_name] = column_data
135
- return results
136
-
137
-
138
156
  class TapSchemaDataTest(unittest.TestCase):
139
157
  """Test the validity of generated TAP SCHEMA data."""
140
158