lsst-felis 26.2024.1700__py3-none-any.whl → 27.0.0rc1__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.

Potentially problematic release.


This version of lsst-felis might be problematic. Click here for more details.

felis/cli.py CHANGED
@@ -183,7 +183,6 @@ def init_tap(
183
183
  @click.option("--tap-columns-table", help="Alt Table Name for TAP_SCHEMA.columns")
184
184
  @click.option("--tap-keys-table", help="Alt Table Name for TAP_SCHEMA.keys")
185
185
  @click.option("--tap-key-columns-table", help="Alt Table Name for TAP_SCHEMA.key_columns")
186
- @click.option("--tap-schema-index", type=int, help="TAP_SCHEMA index of the schema")
187
186
  @click.argument("file", type=click.File())
188
187
  def load_tap(
189
188
  engine_url: str,
@@ -197,7 +196,6 @@ def load_tap(
197
196
  tap_columns_table: str,
198
197
  tap_keys_table: str,
199
198
  tap_key_columns_table: str,
200
- tap_schema_index: int,
201
199
  file: io.TextIOBase,
202
200
  ) -> None:
203
201
  """Load TAP metadata from a Felis FILE.
@@ -205,8 +203,28 @@ def load_tap(
205
203
  This command loads the associated TAP metadata from a Felis FILE
206
204
  to the TAP_SCHEMA tables.
207
205
  """
208
- yaml_data = yaml.load(file, Loader=yaml.SafeLoader)
209
- schema = Schema.model_validate(yaml_data)
206
+ top_level_object = yaml.load(file, Loader=yaml.SafeLoader)
207
+ schema_obj: dict
208
+ if isinstance(top_level_object, dict):
209
+ schema_obj = top_level_object
210
+ if "@graph" not in schema_obj:
211
+ schema_obj["@type"] = "felis:Schema"
212
+ schema_obj["@context"] = DEFAULT_CONTEXT
213
+ elif isinstance(top_level_object, list):
214
+ schema_obj = {"@context": DEFAULT_CONTEXT, "@graph": top_level_object}
215
+ else:
216
+ logger.error("Schema object not of recognizable type")
217
+ raise click.exceptions.Exit(1)
218
+
219
+ normalized = _normalize(schema_obj, embed="@always")
220
+ if len(normalized["@graph"]) > 1 and (schema_name or catalog_name):
221
+ logger.error("--schema-name and --catalog-name incompatible with multiple schemas")
222
+ raise click.exceptions.Exit(1)
223
+
224
+ # Force normalized["@graph"] to a list, which is what happens when there's
225
+ # multiple schemas
226
+ if isinstance(normalized["@graph"], dict):
227
+ normalized["@graph"] = [normalized["@graph"]]
210
228
 
211
229
  tap_tables = init_tables(
212
230
  tap_schema_name,
@@ -225,28 +243,28 @@ def load_tap(
225
243
  # In Memory SQLite - Mostly used to test
226
244
  Tap11Base.metadata.create_all(engine)
227
245
 
228
- tap_visitor = TapLoadingVisitor(
229
- engine,
230
- catalog_name=catalog_name,
231
- schema_name=schema_name,
232
- tap_tables=tap_tables,
233
- tap_schema_index=tap_schema_index,
234
- )
235
- tap_visitor.visit_schema(schema)
246
+ for schema in normalized["@graph"]:
247
+ tap_visitor = TapLoadingVisitor(
248
+ engine,
249
+ catalog_name=catalog_name,
250
+ schema_name=schema_name,
251
+ tap_tables=tap_tables,
252
+ )
253
+ tap_visitor.visit_schema(schema)
236
254
  else:
237
255
  _insert_dump = InsertDump()
238
256
  conn = create_mock_engine(make_url(engine_url), executor=_insert_dump.dump, paramstyle="pyformat")
239
257
  # After the engine is created, update the executor with the dialect
240
258
  _insert_dump.dialect = conn.dialect
241
259
 
242
- tap_visitor = TapLoadingVisitor.from_mock_connection(
243
- conn,
244
- catalog_name=catalog_name,
245
- schema_name=schema_name,
246
- tap_tables=tap_tables,
247
- tap_schema_index=tap_schema_index,
248
- )
249
- tap_visitor.visit_schema(schema)
260
+ for schema in normalized["@graph"]:
261
+ tap_visitor = TapLoadingVisitor.from_mock_connection(
262
+ conn,
263
+ catalog_name=catalog_name,
264
+ schema_name=schema_name,
265
+ tap_tables=tap_tables,
266
+ )
267
+ tap_visitor.visit_schema(schema)
250
268
 
251
269
 
252
270
  @cli.command("modify-tap")
felis/datamodel.py CHANGED
@@ -93,20 +93,18 @@ class BaseObject(BaseModel):
93
93
  description: DescriptionStr | None = None
94
94
  """A description of the database object."""
95
95
 
96
- votable_utype: str | None = Field(None, alias="votable:utype")
97
- """The VOTable utype (usage-specific or unique type) of the object."""
98
-
99
- @model_validator(mode="after")
100
- def check_description(self, info: ValidationInfo) -> BaseObject:
96
+ @model_validator(mode="after") # type: ignore[arg-type]
97
+ @classmethod
98
+ def check_description(cls, object: BaseObject, info: ValidationInfo) -> BaseObject:
101
99
  """Check that the description is present if required."""
102
100
  context = info.context
103
101
  if not context or not context.get("require_description", False):
104
- return self
105
- if self.description is None or self.description == "":
102
+ return object
103
+ if object.description is None or object.description == "":
106
104
  raise ValueError("Description is required and must be non-empty")
107
- if len(self.description) < DESCR_MIN_LENGTH:
105
+ if len(object.description) < DESCR_MIN_LENGTH:
108
106
  raise ValueError(f"Description must be at least {DESCR_MIN_LENGTH} characters long")
109
- return self
107
+ return object
110
108
 
111
109
 
112
110
  class DataType(StrEnum):
@@ -181,8 +179,13 @@ class Column(BaseObject):
181
179
  length: int | None = None
182
180
  """The length of the column."""
183
181
 
184
- nullable: bool = True
185
- """Whether the column can be ``NULL``."""
182
+ nullable: bool | None = None
183
+ """Whether the column can be ``NULL``.
184
+
185
+ If `None`, this value was not set explicitly in the YAML data. In this
186
+ case, it will be set to `False` for columns with numeric types and `True`
187
+ otherwise.
188
+ """
186
189
 
187
190
  value: Any = None
188
191
  """The default value of the column."""
@@ -219,12 +222,12 @@ class Column(BaseObject):
219
222
  """TAP_SCHEMA indication that this column is defined by an IVOA standard.
220
223
  """
221
224
 
225
+ votable_utype: str | None = Field(None, alias="votable:utype")
226
+ """The VOTable utype (usage-specific or unique type) of the column."""
227
+
222
228
  votable_xtype: str | None = Field(None, alias="votable:xtype")
223
229
  """The VOTable xtype (extended type) of the column."""
224
230
 
225
- votable_datatype: str | None = Field(None, alias="votable:datatype")
226
- """The VOTable datatype of the column."""
227
-
228
231
  @field_validator("ivoa_ucd")
229
232
  @classmethod
230
233
  def check_ivoa_ucd(cls, ivoa_ucd: str) -> str:
@@ -284,26 +287,22 @@ class Column(BaseObject):
284
287
  db_datatype_obj = string_to_typeengine(datatype_string, dialect, length)
285
288
  if datatype_obj.compile(dialect) == db_datatype_obj.compile(dialect):
286
289
  raise ValueError(
287
- "'{}: {}' is a redundant override of 'datatype: {}' in column '{}'{}".format(
288
- db_annotation,
289
- datatype_string,
290
- col.datatype,
291
- col.id,
292
- "" if length is None else f" with length {length}",
290
+ "'{}: {}' is the same as 'datatype: {}' in column '{}'".format(
291
+ db_annotation, datatype_string, col.datatype, col.id
293
292
  )
294
293
  )
295
294
  else:
296
295
  logger.debug(
297
- "Type override of 'datatype: {}' with '{}: {}' in column '{}' "
298
- "compiled to '{}' and '{}'".format(
299
- col.datatype,
300
- db_annotation,
301
- datatype_string,
302
- col.id,
303
- datatype_obj.compile(dialect),
304
- db_datatype_obj.compile(dialect),
296
+ "Valid type override of 'datatype: {}' with '{}: {}' in column '{}'".format(
297
+ col.datatype, db_annotation, datatype_string, col.id
305
298
  )
306
299
  )
300
+ logger.debug(
301
+ "Compiled datatype '{}' with {} compiled override '{}'".format(
302
+ datatype_obj.compile(dialect), dialect_name, db_datatype_obj.compile(dialect)
303
+ )
304
+ )
305
+
307
306
  return col
308
307
 
309
308
 
felis/db/sqltypes.py CHANGED
@@ -47,7 +47,7 @@ def compile_tinyint(type_: Any, compiler: Any, **kw: Any) -> str:
47
47
 
48
48
  _TypeMap = Mapping[str, types.TypeEngine | type[types.TypeEngine]]
49
49
 
50
- boolean_map: _TypeMap = {MYSQL: mysql.BOOLEAN, ORACLE: oracle.NUMBER(1), POSTGRES: postgresql.BOOLEAN()}
50
+ boolean_map: _TypeMap = {MYSQL: mysql.BIT(1), ORACLE: oracle.NUMBER(1), POSTGRES: postgresql.BOOLEAN()}
51
51
 
52
52
  byte_map: _TypeMap = {
53
53
  MYSQL: mysql.TINYINT(),
felis/metadata.py CHANGED
@@ -34,6 +34,7 @@ from sqlalchemy import (
34
34
  ForeignKeyConstraint,
35
35
  Index,
36
36
  MetaData,
37
+ Numeric,
37
38
  PrimaryKeyConstraint,
38
39
  ResultProxy,
39
40
  Table,
@@ -264,12 +265,17 @@ class MetaDataBuilder:
264
265
  id = column_obj.id
265
266
  description = column_obj.description
266
267
  default = column_obj.value
267
- nullable = column_obj.nullable
268
268
 
269
- # Get datatype, handling variant overrides such as "mysql:datatype".
269
+ # Handle variant overrides for the column (e.g., "mysql:datatype").
270
270
  datatype = get_datatype_with_variants(column_obj)
271
271
 
272
- # Set autoincrement, depending on if it was provided explicitly.
272
+ # Set default value of nullable based on column type and then whether
273
+ # it was explicitly provided in the schema data.
274
+ nullable = column_obj.nullable
275
+ if nullable is None:
276
+ nullable = False if isinstance(datatype, Numeric) else True
277
+
278
+ # Set autoincrement depending on if it was provided explicitly.
273
279
  autoincrement: Literal["auto"] | bool = (
274
280
  column_obj.autoincrement if column_obj.autoincrement is not None else "auto"
275
281
  )
felis/tap.py CHANGED
@@ -24,7 +24,7 @@ from __future__ import annotations
24
24
  __all__ = ["Tap11Base", "TapLoadingVisitor", "init_tables"]
25
25
 
26
26
  import logging
27
- from collections.abc import Iterable, MutableMapping
27
+ from collections.abc import Iterable, Mapping, MutableMapping
28
28
  from typing import Any
29
29
 
30
30
  from sqlalchemy import Column, Integer, String
@@ -34,13 +34,14 @@ from sqlalchemy.orm import Session, declarative_base, sessionmaker
34
34
  from sqlalchemy.schema import MetaData
35
35
  from sqlalchemy.sql.expression import Insert, insert
36
36
 
37
- from felis import datamodel
38
-
39
- from .datamodel import Constraint, Index, Schema, Table
37
+ from .check import FelisValidator
40
38
  from .types import FelisType
39
+ from .visitor import Visitor
40
+
41
+ _Mapping = Mapping[str, Any]
41
42
 
42
43
  Tap11Base: Any = declarative_base() # Any to avoid mypy mess with SA 2
43
- logger = logging.getLogger(__name__)
44
+ logger = logging.getLogger("felis")
44
45
 
45
46
  IDENTIFIER_LENGTH = 128
46
47
  SMALL_FIELD_LENGTH = 32
@@ -132,7 +133,7 @@ def init_tables(
132
133
  )
133
134
 
134
135
 
135
- class TapLoadingVisitor:
136
+ class TapLoadingVisitor(Visitor[None, tuple, Tap11Base, None, tuple, None, None]):
136
137
  """Felis schema visitor for generating TAP schema.
137
138
 
138
139
  Parameters
@@ -153,7 +154,6 @@ class TapLoadingVisitor:
153
154
  catalog_name: str | None = None,
154
155
  schema_name: str | None = None,
155
156
  tap_tables: MutableMapping[str, Any] | None = None,
156
- tap_schema_index: int | None = None,
157
157
  ):
158
158
  self.graph_index: MutableMapping[str, Any] = {}
159
159
  self.catalog_name = catalog_name
@@ -161,7 +161,7 @@ class TapLoadingVisitor:
161
161
  self.engine = engine
162
162
  self._mock_connection: MockConnection | None = None
163
163
  self.tables = tap_tables or init_tables()
164
- self.tap_schema_index = tap_schema_index
164
+ self.checker = FelisValidator()
165
165
 
166
166
  @classmethod
167
167
  def from_mock_connection(
@@ -170,30 +170,30 @@ class TapLoadingVisitor:
170
170
  catalog_name: str | None = None,
171
171
  schema_name: str | None = None,
172
172
  tap_tables: MutableMapping[str, Any] | None = None,
173
- tap_schema_index: int | None = None,
174
173
  ) -> TapLoadingVisitor:
175
174
  visitor = cls(engine=None, catalog_name=catalog_name, schema_name=schema_name, tap_tables=tap_tables)
176
175
  visitor._mock_connection = mock_connection
177
- visitor.tap_schema_index = tap_schema_index
178
176
  return visitor
179
177
 
180
- def visit_schema(self, schema_obj: Schema) -> None:
178
+ def visit_schema(self, schema_obj: _Mapping) -> None:
179
+ self.checker.check_schema(schema_obj)
180
+ if (version_obj := schema_obj.get("version")) is not None:
181
+ self.visit_schema_version(version_obj, schema_obj)
181
182
  schema = self.tables["schemas"]()
182
183
  # Override with default
183
- self.schema_name = self.schema_name or schema_obj.name
184
+ self.schema_name = self.schema_name or schema_obj["name"]
184
185
 
185
186
  schema.schema_name = self._schema_name()
186
- schema.description = schema_obj.description
187
- schema.utype = schema_obj.votable_utype
188
- schema.schema_index = self.tap_schema_index
189
- logger.debug("Set TAP_SCHEMA index: {}".format(self.tap_schema_index))
187
+ schema.description = schema_obj.get("description")
188
+ schema.utype = schema_obj.get("votable:utype")
189
+ schema.schema_index = int(schema_obj.get("tap:schema_index", 0))
190
190
 
191
191
  if self.engine is not None:
192
192
  session: Session = sessionmaker(self.engine)()
193
193
 
194
194
  session.add(schema)
195
195
 
196
- for table_obj in schema_obj.tables:
196
+ for table_obj in schema_obj["tables"]:
197
197
  table, columns = self.visit_table(table_obj, schema_obj)
198
198
  session.add(table)
199
199
  session.add_all(columns)
@@ -202,8 +202,6 @@ class TapLoadingVisitor:
202
202
  session.add_all(keys)
203
203
  session.add_all(key_columns)
204
204
 
205
- logger.debug("Committing TAP schema: %s", schema_obj.name)
206
- logger.debug("TAP tables: %s", len(self.tables))
207
205
  session.commit()
208
206
  else:
209
207
  logger.info("Dry run, not inserting into database")
@@ -213,7 +211,7 @@ class TapLoadingVisitor:
213
211
  conn = self._mock_connection
214
212
  conn.execute(_insert(self.tables["schemas"], schema))
215
213
 
216
- for table_obj in schema_obj.tables:
214
+ for table_obj in schema_obj["tables"]:
217
215
  table, columns = self.visit_table(table_obj, schema_obj)
218
216
  conn.execute(_insert(self.tables["tables"], table))
219
217
  for column in columns:
@@ -225,45 +223,56 @@ class TapLoadingVisitor:
225
223
  for key_column in key_columns:
226
224
  conn.execute(_insert(self.tables["key_columns"], key_column))
227
225
 
228
- def visit_constraints(self, schema_obj: Schema) -> tuple:
226
+ def visit_constraints(self, schema_obj: _Mapping) -> tuple:
229
227
  all_keys = []
230
228
  all_key_columns = []
231
- for table_obj in schema_obj.tables:
232
- for c in table_obj.constraints:
233
- key, key_columns = self.visit_constraint(c)
229
+ for table_obj in schema_obj["tables"]:
230
+ for c in table_obj.get("constraints", []):
231
+ key, key_columns = self.visit_constraint(c, table_obj)
234
232
  if not key:
235
233
  continue
236
234
  all_keys.append(key)
237
235
  all_key_columns += key_columns
238
236
  return all_keys, all_key_columns
239
237
 
240
- def visit_table(self, table_obj: Table, schema_obj: Schema) -> tuple:
241
- table_id = table_obj.id
238
+ def visit_schema_version(
239
+ self, version_obj: str | Mapping[str, Any], schema_obj: Mapping[str, Any]
240
+ ) -> None:
241
+ # Docstring is inherited.
242
+
243
+ # For now we ignore schema versioning completely, still do some checks.
244
+ self.checker.check_schema_version(version_obj, schema_obj)
245
+
246
+ def visit_table(self, table_obj: _Mapping, schema_obj: _Mapping) -> tuple:
247
+ self.checker.check_table(table_obj, schema_obj)
248
+ table_id = table_obj["@id"]
242
249
  table = self.tables["tables"]()
243
250
  table.schema_name = self._schema_name()
244
- table.table_name = self._table_name(table_obj.name)
251
+ table.table_name = self._table_name(table_obj["name"])
245
252
  table.table_type = "table"
246
- table.utype = table_obj.votable_utype
247
- table.description = table_obj.description
248
- table.table_index = 0 if table_obj.tap_table_index is None else table_obj.tap_table_index
253
+ table.utype = table_obj.get("votable:utype")
254
+ table.description = table_obj.get("description")
255
+ table.table_index = int(table_obj.get("tap:table_index", 0))
249
256
 
250
- columns = [self.visit_column(c, table_obj) for c in table_obj.columns]
251
- self.visit_primary_key(table_obj.primary_key, table_obj)
257
+ columns = [self.visit_column(c, table_obj) for c in table_obj["columns"]]
258
+ self.visit_primary_key(table_obj.get("primaryKey", []), table_obj)
252
259
 
253
- for i in table_obj.indexes:
260
+ for i in table_obj.get("indexes", []):
254
261
  self.visit_index(i, table)
255
262
 
256
263
  self.graph_index[table_id] = table
257
264
  return table, columns
258
265
 
259
- def check_column(self, column_obj: datamodel.Column) -> None:
260
- _id = column_obj.id
261
- datatype_name = column_obj.datatype
262
- felis_type = FelisType.felis_type(datatype_name.value)
266
+ def check_column(self, column_obj: _Mapping, table_obj: _Mapping) -> None:
267
+ self.checker.check_column(column_obj, table_obj)
268
+ _id = column_obj["@id"]
269
+ # Guaranteed to exist at this point, for mypy use "" as default
270
+ datatype_name = column_obj.get("datatype", "")
271
+ felis_type = FelisType.felis_type(datatype_name)
263
272
  if felis_type.is_sized:
264
273
  # It is expected that both arraysize and length are fine for
265
274
  # length types.
266
- arraysize = column_obj.votable_arraysize or column_obj.length
275
+ arraysize = column_obj.get("votable:arraysize", column_obj.get("length"))
267
276
  if arraysize is None:
268
277
  logger.warning(
269
278
  f"votable:arraysize and length for {_id} are None for type {datatype_name}. "
@@ -274,7 +283,7 @@ class TapLoadingVisitor:
274
283
  # datetime types really should have a votable:arraysize, because
275
284
  # they are converted to strings and the `length` is loosely to the
276
285
  # string size
277
- if not column_obj.votable_arraysize:
286
+ if "votable:arraysize" not in column_obj:
278
287
  logger.warning(
279
288
  f"votable:arraysize for {_id} is None for type {datatype_name}. "
280
289
  f'Using length "*". '
@@ -282,45 +291,47 @@ class TapLoadingVisitor:
282
291
  "materialized datetime/timestamp strings."
283
292
  )
284
293
 
285
- def visit_column(self, column_obj: datamodel.Column, table_obj: Table) -> Tap11Base:
286
- self.check_column(column_obj)
287
- column_id = column_obj.id
288
- table_name = self._table_name(table_obj.name)
294
+ def visit_column(self, column_obj: _Mapping, table_obj: _Mapping) -> Tap11Base:
295
+ self.check_column(column_obj, table_obj)
296
+ column_id = column_obj["@id"]
297
+ table_name = self._table_name(table_obj["name"])
289
298
 
290
299
  column = self.tables["columns"]()
291
300
  column.table_name = table_name
292
- column.column_name = column_obj.name
301
+ column.column_name = column_obj["name"]
293
302
 
294
- felis_datatype = column_obj.datatype
295
- felis_type = FelisType.felis_type(felis_datatype.value)
296
- column.datatype = column_obj.votable_datatype or felis_type.votable_name
303
+ felis_datatype = column_obj["datatype"]
304
+ felis_type = FelisType.felis_type(felis_datatype)
305
+ column.datatype = column_obj.get("votable:datatype", felis_type.votable_name)
297
306
 
298
307
  arraysize = None
299
308
  if felis_type.is_sized:
300
- arraysize = column_obj.votable_arraysize or column_obj.length or "*"
309
+ # prefer votable:arraysize to length, fall back to `*`
310
+ arraysize = column_obj.get("votable:arraysize", column_obj.get("length", "*"))
301
311
  if felis_type.is_timestamp:
302
- arraysize = column_obj.votable_arraysize or "*"
312
+ arraysize = column_obj.get("votable:arraysize", "*")
303
313
  column.arraysize = arraysize
304
314
 
305
- column.xtype = column_obj.votable_xtype
306
- column.description = column_obj.description
307
- column.utype = column_obj.votable_utype
315
+ column.xtype = column_obj.get("votable:xtype")
316
+ column.description = column_obj.get("description")
317
+ column.utype = column_obj.get("votable:utype")
308
318
 
309
- unit = column_obj.ivoa_unit or column_obj.fits_tunit
319
+ unit = column_obj.get("ivoa:unit") or column_obj.get("fits:tunit")
310
320
  column.unit = unit
311
- column.ucd = column_obj.ivoa_ucd
321
+ column.ucd = column_obj.get("ivoa:ucd")
312
322
 
313
323
  # We modify this after we process columns
314
324
  column.indexed = 0
315
325
 
316
- column.principal = column_obj.tap_principal
317
- column.std = column_obj.tap_std
318
- column.column_index = column_obj.tap_column_index
326
+ column.principal = column_obj.get("tap:principal", 0)
327
+ column.std = column_obj.get("tap:std", 0)
328
+ column.column_index = column_obj.get("tap:column_index")
319
329
 
320
330
  self.graph_index[column_id] = column
321
331
  return column
322
332
 
323
- def visit_primary_key(self, primary_key_obj: str | Iterable[str] | None, table_obj: Table) -> None:
333
+ def visit_primary_key(self, primary_key_obj: str | Iterable[str], table_obj: _Mapping) -> None:
334
+ self.checker.check_primary_key(primary_key_obj, table_obj)
324
335
  if primary_key_obj:
325
336
  if isinstance(primary_key_obj, str):
326
337
  primary_key_obj = [primary_key_obj]
@@ -330,18 +341,19 @@ class TapLoadingVisitor:
330
341
  columns[0].indexed = 1
331
342
  return None
332
343
 
333
- def visit_constraint(self, constraint_obj: Constraint) -> tuple:
334
- constraint_type = constraint_obj.type
344
+ def visit_constraint(self, constraint_obj: _Mapping, table_obj: _Mapping) -> tuple:
345
+ self.checker.check_constraint(constraint_obj, table_obj)
346
+ constraint_type = constraint_obj["@type"]
335
347
  key = None
336
348
  key_columns = []
337
349
  if constraint_type == "ForeignKey":
338
- constraint_name = constraint_obj.name
339
- description = constraint_obj.description
340
- utype = constraint_obj.votable_utype
350
+ constraint_name = constraint_obj["name"]
351
+ description = constraint_obj.get("description")
352
+ utype = constraint_obj.get("votable:utype")
341
353
 
342
- columns = [self.graph_index[col_id] for col_id in getattr(constraint_obj, "columns", [])]
354
+ columns = [self.graph_index[col["@id"]] for col in constraint_obj.get("columns", [])]
343
355
  refcolumns = [
344
- self.graph_index[refcol_id] for refcol_id in getattr(constraint_obj, "referenced_columns", [])
356
+ self.graph_index[refcol["@id"]] for refcol in constraint_obj.get("referencedColumns", [])
345
357
  ]
346
358
 
347
359
  table_name = None
@@ -374,8 +386,9 @@ class TapLoadingVisitor:
374
386
  key_columns.append(key_column)
375
387
  return key, key_columns
376
388
 
377
- def visit_index(self, index_obj: Index, table_obj: Table) -> None:
378
- columns = [self.graph_index[col_id] for col_id in getattr(index_obj, "columns", [])]
389
+ def visit_index(self, index_obj: _Mapping, table_obj: _Mapping) -> None:
390
+ self.checker.check_index(index_obj, table_obj)
391
+ columns = [self.graph_index[col["@id"]] for col in index_obj.get("columns", [])]
379
392
  # if just one column and it's indexed, update the object
380
393
  if len(columns) == 1:
381
394
  columns[0].indexed = 1
felis/version.py CHANGED
@@ -1,2 +1,2 @@
1
1
  __all__ = ["__version__"]
2
- __version__ = "26.2024.1700"
2
+ __version__ = "27.0.0rc1"
@@ -1,11 +1,10 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lsst-felis
3
- Version: 26.2024.1700
3
+ Version: 27.0.0rc1
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+)
7
- Project-URL: Homepage, https://felis.lsst.io
8
- Project-URL: Source, https://github.com/lsst/felis
7
+ Project-URL: Homepage, https://github.com/lsst/felis
9
8
  Keywords: lsst
10
9
  Classifier: Intended Audience :: Science/Research
11
10
  Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
@@ -25,8 +24,6 @@ Requires-Dist: pyyaml >=6
25
24
  Requires-Dist: pyld >=2
26
25
  Requires-Dist: pydantic <3,>=2
27
26
  Requires-Dist: lsst-utils
28
- Provides-Extra: dev
29
- Requires-Dist: documenteer[guide] ; extra == 'dev'
30
27
  Provides-Extra: test
31
28
  Requires-Dist: pytest >=3.2 ; extra == 'test'
32
29
 
@@ -0,0 +1,24 @@
1
+ felis/__init__.py,sha256=_Pw-QKMYj0WRgE8fW2N2pBXJUj-Pjv8dSKJBzykjyZU,1842
2
+ felis/check.py,sha256=RBxXq7XwPGIucrs1PPgPtgk8MrWAJlOmoxCNySEz9-I,13892
3
+ felis/cli.py,sha256=IG_G41LRcEa7Y_6iFQ4i44LmyARhiqiv_0gFvLeJzpA,17041
4
+ felis/datamodel.py,sha256=Wv7SAOTahIMWWpYFT_VxBD4WDRYlcm83SGAAy30Ly0c,19293
5
+ felis/metadata.py,sha256=5DE2YMnu6YuhwntBSe-OheCD7C2-vA4yb64BpjTC68A,18542
6
+ felis/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ felis/simple.py,sha256=yzv_aoZrZhfakd1Xm7gLDeVKyJjCDZ7wAyYYp-l_Sxs,14414
8
+ felis/tap.py,sha256=RBwEKyU3S0oXSNIMoI2WRAuC9WB5eai9BdQQUYN5Qdc,17704
9
+ felis/types.py,sha256=z_ECfSxpqiFSGppjxKwCO4fPP7TLBaIN3Qo1AGF16Go,4418
10
+ felis/utils.py,sha256=tYxr0xFdPN4gDHibeAD9d5DFgU8hKlSZVKmZoDzi4e8,4164
11
+ felis/validation.py,sha256=f9VKvp7q-cnim2D5voTKwCdt0NRsYBpTwom1Z_3OKkc,3469
12
+ felis/version.py,sha256=JOI1Xea54VxWcOeyBpnkANENYD4iRqhpyFoGJEsZd2g,52
13
+ felis/visitor.py,sha256=EazU4nYbkKBj3mCZYvsTCBTNmh0qRaUNZIzCcM3dqOQ,6439
14
+ felis/db/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ felis/db/_variants.py,sha256=zCuXDgU_x_pTZcWkBLgqQCiOhlA6y2tBt-PUQfafwmM,3368
16
+ felis/db/sqltypes.py,sha256=-YPkU5yS7udcPZTg__G_85tH6oMqVm6hEcHUgYpgiT8,5734
17
+ lsst_felis-27.0.0rc1.dist-info/COPYRIGHT,sha256=bUmNy19uUxqITMpjeHFe69q3IzQpjxvvBw6oV7kR7ho,129
18
+ lsst_felis-27.0.0rc1.dist-info/LICENSE,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
19
+ lsst_felis-27.0.0rc1.dist-info/METADATA,sha256=W3MNx0jyzLcT4ccHR30iQZQ6W26iycKsv5RTJyaSu3Y,1098
20
+ lsst_felis-27.0.0rc1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
21
+ lsst_felis-27.0.0rc1.dist-info/entry_points.txt,sha256=Gk2XFujA_Gp52VBk45g5kim8TDoMDJFPctsMqiq72EM,40
22
+ lsst_felis-27.0.0rc1.dist-info/top_level.txt,sha256=F4SvPip3iZRVyISi50CHhwTIAokAhSxjWiVcn4IVWRI,6
23
+ lsst_felis-27.0.0rc1.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
24
+ lsst_felis-27.0.0rc1.dist-info/RECORD,,
@@ -1,24 +0,0 @@
1
- felis/__init__.py,sha256=_Pw-QKMYj0WRgE8fW2N2pBXJUj-Pjv8dSKJBzykjyZU,1842
2
- felis/check.py,sha256=RBxXq7XwPGIucrs1PPgPtgk8MrWAJlOmoxCNySEz9-I,13892
3
- felis/cli.py,sha256=VBlEoo65Y5lPf7wzokuZeW1HL-ObfIrrVzo155RZK0Q,16202
4
- felis/datamodel.py,sha256=VEymalSBkVmTPWL-xm1DPp6fAR2ze0KvFRGwVf2YR08,19315
5
- felis/metadata.py,sha256=gcBjpB_JpEfKXpKc7hMwG11PizqeDR8IJaFfhFtBIEw,18294
6
- felis/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- felis/simple.py,sha256=yzv_aoZrZhfakd1Xm7gLDeVKyJjCDZ7wAyYYp-l_Sxs,14414
8
- felis/tap.py,sha256=MOqe9_K6KK9oUqEuKyKtSLFrzTluohbcvQ59DeoRiLY,16803
9
- felis/types.py,sha256=z_ECfSxpqiFSGppjxKwCO4fPP7TLBaIN3Qo1AGF16Go,4418
10
- felis/utils.py,sha256=tYxr0xFdPN4gDHibeAD9d5DFgU8hKlSZVKmZoDzi4e8,4164
11
- felis/validation.py,sha256=f9VKvp7q-cnim2D5voTKwCdt0NRsYBpTwom1Z_3OKkc,3469
12
- felis/version.py,sha256=PH-FIbvh4hvlcfNHl8Bt-EVFDlE5_dasGcUm02k5uxI,55
13
- felis/visitor.py,sha256=EazU4nYbkKBj3mCZYvsTCBTNmh0qRaUNZIzCcM3dqOQ,6439
14
- felis/db/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
- felis/db/_variants.py,sha256=zCuXDgU_x_pTZcWkBLgqQCiOhlA6y2tBt-PUQfafwmM,3368
16
- felis/db/sqltypes.py,sha256=n6E1K-Hvdt62GVBQedefzTqBpNS7ks92lchRM5BF_Oo,5735
17
- lsst_felis-26.2024.1700.dist-info/COPYRIGHT,sha256=bUmNy19uUxqITMpjeHFe69q3IzQpjxvvBw6oV7kR7ho,129
18
- lsst_felis-26.2024.1700.dist-info/LICENSE,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
19
- lsst_felis-26.2024.1700.dist-info/METADATA,sha256=e4j5d28w8iR-ZzYbhvD_tSKHAIC7Gx07rSW4llYDaa8,1215
20
- lsst_felis-26.2024.1700.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
21
- lsst_felis-26.2024.1700.dist-info/entry_points.txt,sha256=Gk2XFujA_Gp52VBk45g5kim8TDoMDJFPctsMqiq72EM,40
22
- lsst_felis-26.2024.1700.dist-info/top_level.txt,sha256=F4SvPip3iZRVyISi50CHhwTIAokAhSxjWiVcn4IVWRI,6
23
- lsst_felis-26.2024.1700.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
24
- lsst_felis-26.2024.1700.dist-info/RECORD,,