lsst-felis 29.2025.1900__tar.gz → 29.2025.2100__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 (39) hide show
  1. {lsst_felis-29.2025.1900/python/lsst_felis.egg-info → lsst_felis-29.2025.2100}/PKG-INFO +1 -1
  2. {lsst_felis-29.2025.1900 → lsst_felis-29.2025.2100}/python/felis/cli.py +14 -1
  3. {lsst_felis-29.2025.1900 → lsst_felis-29.2025.2100}/python/felis/datamodel.py +12 -2
  4. {lsst_felis-29.2025.1900 → lsst_felis-29.2025.2100/python/lsst_felis.egg-info}/PKG-INFO +1 -1
  5. {lsst_felis-29.2025.1900 → lsst_felis-29.2025.2100}/tests/test_tap_schema.py +80 -27
  6. {lsst_felis-29.2025.1900 → lsst_felis-29.2025.2100}/COPYRIGHT +0 -0
  7. {lsst_felis-29.2025.1900 → lsst_felis-29.2025.2100}/LICENSE +0 -0
  8. {lsst_felis-29.2025.1900 → lsst_felis-29.2025.2100}/README.rst +0 -0
  9. {lsst_felis-29.2025.1900 → lsst_felis-29.2025.2100}/pyproject.toml +0 -0
  10. {lsst_felis-29.2025.1900 → lsst_felis-29.2025.2100}/python/felis/__init__.py +0 -0
  11. {lsst_felis-29.2025.1900 → lsst_felis-29.2025.2100}/python/felis/db/__init__.py +0 -0
  12. {lsst_felis-29.2025.1900 → lsst_felis-29.2025.2100}/python/felis/db/dialects.py +0 -0
  13. {lsst_felis-29.2025.1900 → lsst_felis-29.2025.2100}/python/felis/db/schema.py +0 -0
  14. {lsst_felis-29.2025.1900 → lsst_felis-29.2025.2100}/python/felis/db/sqltypes.py +0 -0
  15. {lsst_felis-29.2025.1900 → lsst_felis-29.2025.2100}/python/felis/db/utils.py +0 -0
  16. {lsst_felis-29.2025.1900 → lsst_felis-29.2025.2100}/python/felis/db/variants.py +0 -0
  17. {lsst_felis-29.2025.1900 → lsst_felis-29.2025.2100}/python/felis/diff.py +0 -0
  18. {lsst_felis-29.2025.1900 → lsst_felis-29.2025.2100}/python/felis/metadata.py +0 -0
  19. {lsst_felis-29.2025.1900 → lsst_felis-29.2025.2100}/python/felis/py.typed +0 -0
  20. {lsst_felis-29.2025.1900 → lsst_felis-29.2025.2100}/python/felis/schemas/tap_schema_std.yaml +0 -0
  21. {lsst_felis-29.2025.1900 → lsst_felis-29.2025.2100}/python/felis/tap_schema.py +0 -0
  22. {lsst_felis-29.2025.1900 → lsst_felis-29.2025.2100}/python/felis/tests/__init__.py +0 -0
  23. {lsst_felis-29.2025.1900 → lsst_felis-29.2025.2100}/python/felis/tests/postgresql.py +0 -0
  24. {lsst_felis-29.2025.1900 → lsst_felis-29.2025.2100}/python/felis/tests/run_cli.py +0 -0
  25. {lsst_felis-29.2025.1900 → lsst_felis-29.2025.2100}/python/felis/types.py +0 -0
  26. {lsst_felis-29.2025.1900 → lsst_felis-29.2025.2100}/python/lsst_felis.egg-info/SOURCES.txt +0 -0
  27. {lsst_felis-29.2025.1900 → lsst_felis-29.2025.2100}/python/lsst_felis.egg-info/dependency_links.txt +0 -0
  28. {lsst_felis-29.2025.1900 → lsst_felis-29.2025.2100}/python/lsst_felis.egg-info/entry_points.txt +0 -0
  29. {lsst_felis-29.2025.1900 → lsst_felis-29.2025.2100}/python/lsst_felis.egg-info/requires.txt +0 -0
  30. {lsst_felis-29.2025.1900 → lsst_felis-29.2025.2100}/python/lsst_felis.egg-info/top_level.txt +0 -0
  31. {lsst_felis-29.2025.1900 → lsst_felis-29.2025.2100}/python/lsst_felis.egg-info/zip-safe +0 -0
  32. {lsst_felis-29.2025.1900 → lsst_felis-29.2025.2100}/setup.cfg +0 -0
  33. {lsst_felis-29.2025.1900 → lsst_felis-29.2025.2100}/tests/test_cli.py +0 -0
  34. {lsst_felis-29.2025.1900 → lsst_felis-29.2025.2100}/tests/test_datamodel.py +0 -0
  35. {lsst_felis-29.2025.1900 → lsst_felis-29.2025.2100}/tests/test_db.py +0 -0
  36. {lsst_felis-29.2025.1900 → lsst_felis-29.2025.2100}/tests/test_diff.py +0 -0
  37. {lsst_felis-29.2025.1900 → lsst_felis-29.2025.2100}/tests/test_metadata.py +0 -0
  38. {lsst_felis-29.2025.1900 → lsst_felis-29.2025.2100}/tests/test_postgres.py +0 -0
  39. {lsst_felis-29.2025.1900 → lsst_felis-29.2025.2100}/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.1900
3
+ Version: 29.2025.2100
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+)
@@ -198,6 +198,12 @@ def create(
198
198
  @click.option("--dry-run", "-D", is_flag=True, help="Execute dry run only. Does not insert any data.")
199
199
  @click.option("--echo", "-e", is_flag=True, help="Print out the generated insert statements to stdout")
200
200
  @click.option("--output-file", "-o", type=click.Path(), help="Write SQL commands to a file")
201
+ @click.option(
202
+ "--force-unbounded-arraysize",
203
+ is_flag=True,
204
+ help="Use unbounded arraysize by default for all variable length string columns"
205
+ ", e.g., ``votable:arraysize: *`` (workaround for astropy bug #18099)",
206
+ ) # DM-50899: Variable-length bounded strings are not handled correctly in astropy
201
207
  @click.option(
202
208
  "--unique-keys",
203
209
  "-u",
@@ -216,6 +222,7 @@ def load_tap_schema(
216
222
  dry_run: bool,
217
223
  echo: bool,
218
224
  output_file: str | None,
225
+ force_unbounded_arraysize: bool,
219
226
  unique_keys: bool,
220
227
  file: IO[str],
221
228
  ) -> None:
@@ -256,7 +263,13 @@ def load_tap_schema(
256
263
  table_name_postfix=tap_tables_postfix,
257
264
  )
258
265
 
259
- schema = Schema.from_stream(file, context={"id_generation": ctx.obj["id_generation"]})
266
+ schema = Schema.from_stream(
267
+ file,
268
+ context={
269
+ "id_generation": ctx.obj["id_generation"],
270
+ "force_unbounded_arraysize": force_unbounded_arraysize,
271
+ },
272
+ )
260
273
 
261
274
  DataLoader(
262
275
  schema,
@@ -421,7 +421,7 @@ class Column(BaseObject):
421
421
 
422
422
  @model_validator(mode="before")
423
423
  @classmethod
424
- def check_votable_arraysize(cls, values: dict[str, Any]) -> dict[str, Any]:
424
+ def check_votable_arraysize(cls, values: dict[str, Any], info: ValidationInfo) -> dict[str, Any]:
425
425
  """Set the default value for the ``votable_arraysize`` field, which
426
426
  corresponds to ``arraysize`` in the IVOA VOTable standard.
427
427
 
@@ -429,6 +429,8 @@ class Column(BaseObject):
429
429
  ----------
430
430
  values
431
431
  Values of the column.
432
+ info
433
+ Validation context used to determine if the check is enabled.
432
434
 
433
435
  Returns
434
436
  -------
@@ -443,6 +445,7 @@ class Column(BaseObject):
443
445
  if values.get("name", None) is None or values.get("datatype", None) is None:
444
446
  # Skip bad column data that will not validate
445
447
  return values
448
+ context = info.context if info.context else {}
446
449
  arraysize = values.get("votable:arraysize", None)
447
450
  if arraysize is None:
448
451
  length = values.get("length", None)
@@ -452,7 +455,14 @@ class Column(BaseObject):
452
455
  if datatype == "char":
453
456
  arraysize = str(length)
454
457
  elif datatype in ("string", "unicode", "binary"):
455
- arraysize = f"{length}*"
458
+ if context.get("force_unbounded_arraysize", False):
459
+ arraysize = "*"
460
+ logger.debug(
461
+ f"Forced VOTable's 'arraysize' to '*' on column '{values['name']}' with datatype "
462
+ + f"'{values['datatype']}' and length '{length}'"
463
+ )
464
+ else:
465
+ arraysize = f"{length}*"
456
466
  elif datatype in ("timestamp", "text"):
457
467
  arraysize = "*"
458
468
  if arraysize is not None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lsst-felis
3
- Version: 29.2025.1900
3
+ Version: 29.2025.2100
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 MetaData, create_engine, select
28
+ from sqlalchemy import create_engine, select
29
29
 
30
30
  from felis.datamodel import Schema
31
31
  from felis.tap_schema import DataLoader, TableManager
@@ -153,29 +153,54 @@ def _find_row(rows: list[dict[str, Any]], column_name: str, value: str) -> dict[
153
153
  return next_row
154
154
 
155
155
 
156
- class TapSchemaDataTest(unittest.TestCase):
157
- """Test the validity of generated TAP SCHEMA data."""
156
+ class TapSchemaSqliteSetup:
157
+ """Set up the TAP_SCHEMA SQLite database for testing.
158
158
 
159
- def setUp(self) -> None:
160
- """Set up the test case."""
159
+ Parameters
160
+ ----------
161
+ context : `dict`
162
+ Context for the schema. Default is an empty dictionary.
163
+ """
164
+
165
+ def __init__(self, context: dict = {}) -> None:
161
166
  with open(TEST_TAP_SCHEMA) as test_file:
162
- self.schema = Schema.from_stream(test_file, context={"id_generation": True})
167
+ self._schema = Schema.from_stream(test_file, context=context)
163
168
 
164
- self.engine = create_engine("sqlite:///:memory:")
169
+ self._engine = create_engine("sqlite:///:memory:")
165
170
 
166
171
  mgr = TableManager(apply_schema_to_metadata=False)
167
- mgr.initialize_database(self.engine)
168
- self.mgr = mgr
172
+ mgr.initialize_database(self._engine)
173
+ self._mgr = mgr
169
174
 
170
- loader = DataLoader(self.schema, mgr, self.engine, tap_schema_index=2)
175
+ loader = DataLoader(self._schema, mgr, self._engine, tap_schema_index=2)
171
176
  loader.load()
172
177
 
173
- self.md = MetaData()
174
- self.md.reflect(self.engine)
178
+ @property
179
+ def schema(self) -> Schema:
180
+ """Return the schema."""
181
+ return self._schema
182
+
183
+ @property
184
+ def engine(self) -> Any:
185
+ """Return the engine."""
186
+ return self._engine
187
+
188
+ @property
189
+ def mgr(self) -> TableManager:
190
+ """Return the table manager."""
191
+ return self._mgr
192
+
193
+
194
+ class TapSchemaDataTest(unittest.TestCase):
195
+ """Test the validity of generated TAP SCHEMA data."""
196
+
197
+ def setUp(self) -> None:
198
+ """Set up the test case."""
199
+ self.tap_schema_setup = TapSchemaSqliteSetup(context={"id_generation": True})
175
200
 
176
201
  def test_schemas(self) -> None:
177
- schemas_table = self.mgr["schemas"]
178
- with self.engine.connect() as connection:
202
+ schemas_table = self.tap_schema_setup.mgr["schemas"]
203
+ with self.tap_schema_setup.engine.connect() as connection:
179
204
  result = connection.execute(select(schemas_table))
180
205
  schema_data = [row._asdict() for row in result]
181
206
 
@@ -188,8 +213,8 @@ class TapSchemaDataTest(unittest.TestCase):
188
213
  self.assertEqual(schema["schema_index"], 2)
189
214
 
190
215
  def test_tables(self) -> None:
191
- tables_table = self.mgr["tables"]
192
- with self.engine.connect() as connection:
216
+ tables_table = self.tap_schema_setup.mgr["tables"]
217
+ with self.tap_schema_setup.engine.connect() as connection:
193
218
  result = connection.execute(select(tables_table))
194
219
  table_data = [row._asdict() for row in result]
195
220
 
@@ -198,19 +223,21 @@ class TapSchemaDataTest(unittest.TestCase):
198
223
  table = table_data[0]
199
224
  assert isinstance(table, dict)
200
225
  self.assertEqual(table["schema_name"], "test_schema")
201
- self.assertEqual(table["table_name"], f"{self.schema.name}.table1")
226
+ self.assertEqual(table["table_name"], f"{self.tap_schema_setup.schema.name}.table1")
202
227
  self.assertEqual(table["table_type"], "table")
203
228
  self.assertEqual(table["utype"], "Table")
204
229
  self.assertEqual(table["description"], "Test table 1")
205
230
  self.assertEqual(table["table_index"], 2)
206
231
 
207
232
  def test_columns(self) -> None:
208
- columns_table = self.mgr["columns"]
209
- with self.engine.connect() as connection:
233
+ columns_table = self.tap_schema_setup.mgr["columns"]
234
+ with self.tap_schema_setup.engine.connect() as connection:
210
235
  result = connection.execute(select(columns_table))
211
236
  column_data = [row._asdict() for row in result]
212
237
 
213
- table1_rows = [row for row in column_data if row["table_name"] == f"{self.schema.name}.table1"]
238
+ table1_rows = [
239
+ row for row in column_data if row["table_name"] == f"{self.tap_schema_setup.schema.name}.table1"
240
+ ]
214
241
  self.assertNotEqual(len(table1_rows), 0)
215
242
 
216
243
  boolean_col = _find_row(table1_rows, "column_name", "boolean_field")
@@ -275,8 +302,8 @@ class TapSchemaDataTest(unittest.TestCase):
275
302
  self.assertEqual(txt_col["arraysize"], "*")
276
303
 
277
304
  def test_keys(self) -> None:
278
- keys_table = self.mgr["keys"]
279
- with self.engine.connect() as connection:
305
+ keys_table = self.tap_schema_setup.mgr["keys"]
306
+ with self.tap_schema_setup.engine.connect() as connection:
280
307
  result = connection.execute(select(keys_table))
281
308
  key_data = [row._asdict() for row in result]
282
309
 
@@ -286,14 +313,14 @@ class TapSchemaDataTest(unittest.TestCase):
286
313
  assert isinstance(key, dict)
287
314
 
288
315
  self.assertEqual(key["key_id"], "fk_table1_to_table2")
289
- self.assertEqual(key["from_table"], f"{self.schema.name}.table1")
290
- self.assertEqual(key["target_table"], f"{self.schema.name}.table2")
316
+ self.assertEqual(key["from_table"], f"{self.tap_schema_setup.schema.name}.table1")
317
+ self.assertEqual(key["target_table"], f"{self.tap_schema_setup.schema.name}.table2")
291
318
  self.assertEqual(key["description"], "Foreign key from table1 to table2")
292
319
  self.assertEqual(key["utype"], "ForeignKey")
293
320
 
294
321
  def test_key_columns(self) -> None:
295
- key_columns_table = self.mgr["key_columns"]
296
- with self.engine.connect() as connection:
322
+ key_columns_table = self.tap_schema_setup.mgr["key_columns"]
323
+ with self.tap_schema_setup.engine.connect() as connection:
297
324
  result = connection.execute(select(key_columns_table))
298
325
  key_column_data = [row._asdict() for row in result]
299
326
 
@@ -309,7 +336,33 @@ class TapSchemaDataTest(unittest.TestCase):
309
336
  def test_bad_table_name(self) -> None:
310
337
  """Test getting a bad TAP_SCHEMA table name."""
311
338
  with self.assertRaises(KeyError):
312
- self.mgr["bad_table"]
339
+ self.tap_schema_setup.mgr["bad_table"]
340
+
341
+
342
+ class ForceUnboundArraySizeTest(unittest.TestCase):
343
+ """Test that arraysize for appropriate types is set to '*' when the
344
+ ``force_unboundeded_arraysize`` context flag is set to ``True``.
345
+ """
346
+
347
+ def setUp(self) -> None:
348
+ """Set up the test case."""
349
+ self.tap_schema_setup = TapSchemaSqliteSetup(
350
+ context={"id_generation": True, "force_unbounded_arraysize": True}
351
+ )
352
+
353
+ def test_force_unbounded_arraysize(self) -> None:
354
+ """Test that unbounded arraysize is set to None."""
355
+ columns_table = self.tap_schema_setup.mgr["columns"]
356
+ with self.tap_schema_setup.engine.connect() as connection:
357
+ result = connection.execute(select(columns_table))
358
+ column_data = [row._asdict() for row in result]
359
+
360
+ table1_rows = [
361
+ row for row in column_data if row["table_name"] == f"{self.tap_schema_setup.schema.name}.table1"
362
+ ]
363
+ for row in table1_rows:
364
+ if row["column_name"] in ["string_field", "text_field", "unicode_field", "binary_field"]:
365
+ self.assertEqual(row["arraysize"], "*")
313
366
 
314
367
 
315
368
  if __name__ == "__main__":