lsst-felis 26.2024.1600__py3-none-any.whl → 26.2024.1700__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,6 +183,7 @@ 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")
186
187
  @click.argument("file", type=click.File())
187
188
  def load_tap(
188
189
  engine_url: str,
@@ -196,6 +197,7 @@ def load_tap(
196
197
  tap_columns_table: str,
197
198
  tap_keys_table: str,
198
199
  tap_key_columns_table: str,
200
+ tap_schema_index: int,
199
201
  file: io.TextIOBase,
200
202
  ) -> None:
201
203
  """Load TAP metadata from a Felis FILE.
@@ -203,28 +205,8 @@ def load_tap(
203
205
  This command loads the associated TAP metadata from a Felis FILE
204
206
  to the TAP_SCHEMA tables.
205
207
  """
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"]]
208
+ yaml_data = yaml.load(file, Loader=yaml.SafeLoader)
209
+ schema = Schema.model_validate(yaml_data)
228
210
 
229
211
  tap_tables = init_tables(
230
212
  tap_schema_name,
@@ -243,28 +225,28 @@ def load_tap(
243
225
  # In Memory SQLite - Mostly used to test
244
226
  Tap11Base.metadata.create_all(engine)
245
227
 
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)
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)
254
236
  else:
255
237
  _insert_dump = InsertDump()
256
238
  conn = create_mock_engine(make_url(engine_url), executor=_insert_dump.dump, paramstyle="pyformat")
257
239
  # After the engine is created, update the executor with the dialect
258
240
  _insert_dump.dialect = conn.dialect
259
241
 
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)
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)
268
250
 
269
251
 
270
252
  @cli.command("modify-tap")
felis/datamodel.py CHANGED
@@ -93,18 +93,20 @@ class BaseObject(BaseModel):
93
93
  description: DescriptionStr | None = None
94
94
  """A description of the database object."""
95
95
 
96
- @model_validator(mode="after") # type: ignore[arg-type]
97
- @classmethod
98
- def check_description(cls, object: BaseObject, info: ValidationInfo) -> BaseObject:
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:
99
101
  """Check that the description is present if required."""
100
102
  context = info.context
101
103
  if not context or not context.get("require_description", False):
102
- return object
103
- if object.description is None or object.description == "":
104
+ return self
105
+ if self.description is None or self.description == "":
104
106
  raise ValueError("Description is required and must be non-empty")
105
- if len(object.description) < DESCR_MIN_LENGTH:
107
+ if len(self.description) < DESCR_MIN_LENGTH:
106
108
  raise ValueError(f"Description must be at least {DESCR_MIN_LENGTH} characters long")
107
- return object
109
+ return self
108
110
 
109
111
 
110
112
  class DataType(StrEnum):
@@ -179,13 +181,8 @@ class Column(BaseObject):
179
181
  length: int | None = None
180
182
  """The length of the column."""
181
183
 
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
- """
184
+ nullable: bool = True
185
+ """Whether the column can be ``NULL``."""
189
186
 
190
187
  value: Any = None
191
188
  """The default value of the column."""
@@ -222,12 +219,12 @@ class Column(BaseObject):
222
219
  """TAP_SCHEMA indication that this column is defined by an IVOA standard.
223
220
  """
224
221
 
225
- votable_utype: str | None = Field(None, alias="votable:utype")
226
- """The VOTable utype (usage-specific or unique type) of the column."""
227
-
228
222
  votable_xtype: str | None = Field(None, alias="votable:xtype")
229
223
  """The VOTable xtype (extended type) of the column."""
230
224
 
225
+ votable_datatype: str | None = Field(None, alias="votable:datatype")
226
+ """The VOTable datatype of the column."""
227
+
231
228
  @field_validator("ivoa_ucd")
232
229
  @classmethod
233
230
  def check_ivoa_ucd(cls, ivoa_ucd: str) -> str:
@@ -287,22 +284,26 @@ class Column(BaseObject):
287
284
  db_datatype_obj = string_to_typeengine(datatype_string, dialect, length)
288
285
  if datatype_obj.compile(dialect) == db_datatype_obj.compile(dialect):
289
286
  raise ValueError(
290
- "'{}: {}' is the same as 'datatype: {}' in column '{}'".format(
291
- db_annotation, datatype_string, col.datatype, col.id
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}",
292
293
  )
293
294
  )
294
295
  else:
295
296
  logger.debug(
296
- "Valid type override of 'datatype: {}' with '{}: {}' in column '{}'".format(
297
- col.datatype, db_annotation, datatype_string, col.id
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),
298
305
  )
299
306
  )
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
-
306
307
  return col
307
308
 
308
309
 
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.BIT(1), ORACLE: oracle.NUMBER(1), POSTGRES: postgresql.BOOLEAN()}
50
+ boolean_map: _TypeMap = {MYSQL: mysql.BOOLEAN, ORACLE: oracle.NUMBER(1), POSTGRES: postgresql.BOOLEAN()}
51
51
 
52
52
  byte_map: _TypeMap = {
53
53
  MYSQL: mysql.TINYINT(),
felis/metadata.py CHANGED
@@ -34,7 +34,6 @@ from sqlalchemy import (
34
34
  ForeignKeyConstraint,
35
35
  Index,
36
36
  MetaData,
37
- Numeric,
38
37
  PrimaryKeyConstraint,
39
38
  ResultProxy,
40
39
  Table,
@@ -265,17 +264,12 @@ class MetaDataBuilder:
265
264
  id = column_obj.id
266
265
  description = column_obj.description
267
266
  default = column_obj.value
267
+ nullable = column_obj.nullable
268
268
 
269
- # Handle variant overrides for the column (e.g., "mysql:datatype").
269
+ # Get datatype, handling variant overrides such as "mysql:datatype".
270
270
  datatype = get_datatype_with_variants(column_obj)
271
271
 
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.
272
+ # Set autoincrement, depending on if it was provided explicitly.
279
273
  autoincrement: Literal["auto"] | bool = (
280
274
  column_obj.autoincrement if column_obj.autoincrement is not None else "auto"
281
275
  )
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, Mapping, MutableMapping
27
+ from collections.abc import Iterable, MutableMapping
28
28
  from typing import Any
29
29
 
30
30
  from sqlalchemy import Column, Integer, String
@@ -34,14 +34,13 @@ 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 .check import FelisValidator
38
- from .types import FelisType
39
- from .visitor import Visitor
37
+ from felis import datamodel
40
38
 
41
- _Mapping = Mapping[str, Any]
39
+ from .datamodel import Constraint, Index, Schema, Table
40
+ from .types import FelisType
42
41
 
43
42
  Tap11Base: Any = declarative_base() # Any to avoid mypy mess with SA 2
44
- logger = logging.getLogger("felis")
43
+ logger = logging.getLogger(__name__)
45
44
 
46
45
  IDENTIFIER_LENGTH = 128
47
46
  SMALL_FIELD_LENGTH = 32
@@ -133,7 +132,7 @@ def init_tables(
133
132
  )
134
133
 
135
134
 
136
- class TapLoadingVisitor(Visitor[None, tuple, Tap11Base, None, tuple, None, None]):
135
+ class TapLoadingVisitor:
137
136
  """Felis schema visitor for generating TAP schema.
138
137
 
139
138
  Parameters
@@ -154,6 +153,7 @@ class TapLoadingVisitor(Visitor[None, tuple, Tap11Base, None, tuple, None, None]
154
153
  catalog_name: str | None = None,
155
154
  schema_name: str | None = None,
156
155
  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(Visitor[None, tuple, Tap11Base, None, tuple, None, None]
161
161
  self.engine = engine
162
162
  self._mock_connection: MockConnection | None = None
163
163
  self.tables = tap_tables or init_tables()
164
- self.checker = FelisValidator()
164
+ self.tap_schema_index = tap_schema_index
165
165
 
166
166
  @classmethod
167
167
  def from_mock_connection(
@@ -170,30 +170,30 @@ class TapLoadingVisitor(Visitor[None, tuple, Tap11Base, None, tuple, None, None]
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,
173
174
  ) -> TapLoadingVisitor:
174
175
  visitor = cls(engine=None, catalog_name=catalog_name, schema_name=schema_name, tap_tables=tap_tables)
175
176
  visitor._mock_connection = mock_connection
177
+ visitor.tap_schema_index = tap_schema_index
176
178
  return visitor
177
179
 
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)
180
+ def visit_schema(self, schema_obj: Schema) -> None:
182
181
  schema = self.tables["schemas"]()
183
182
  # Override with default
184
- self.schema_name = self.schema_name or schema_obj["name"]
183
+ self.schema_name = self.schema_name or schema_obj.name
185
184
 
186
185
  schema.schema_name = self._schema_name()
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))
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))
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,6 +202,8 @@ class TapLoadingVisitor(Visitor[None, tuple, Tap11Base, None, tuple, None, None]
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))
205
207
  session.commit()
206
208
  else:
207
209
  logger.info("Dry run, not inserting into database")
@@ -211,7 +213,7 @@ class TapLoadingVisitor(Visitor[None, tuple, Tap11Base, None, tuple, None, None]
211
213
  conn = self._mock_connection
212
214
  conn.execute(_insert(self.tables["schemas"], schema))
213
215
 
214
- for table_obj in schema_obj["tables"]:
216
+ for table_obj in schema_obj.tables:
215
217
  table, columns = self.visit_table(table_obj, schema_obj)
216
218
  conn.execute(_insert(self.tables["tables"], table))
217
219
  for column in columns:
@@ -223,56 +225,45 @@ class TapLoadingVisitor(Visitor[None, tuple, Tap11Base, None, tuple, None, None]
223
225
  for key_column in key_columns:
224
226
  conn.execute(_insert(self.tables["key_columns"], key_column))
225
227
 
226
- def visit_constraints(self, schema_obj: _Mapping) -> tuple:
228
+ def visit_constraints(self, schema_obj: Schema) -> tuple:
227
229
  all_keys = []
228
230
  all_key_columns = []
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)
231
+ for table_obj in schema_obj.tables:
232
+ for c in table_obj.constraints:
233
+ key, key_columns = self.visit_constraint(c)
232
234
  if not key:
233
235
  continue
234
236
  all_keys.append(key)
235
237
  all_key_columns += key_columns
236
238
  return all_keys, all_key_columns
237
239
 
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"]
240
+ def visit_table(self, table_obj: Table, schema_obj: Schema) -> tuple:
241
+ table_id = table_obj.id
249
242
  table = self.tables["tables"]()
250
243
  table.schema_name = self._schema_name()
251
- table.table_name = self._table_name(table_obj["name"])
244
+ table.table_name = self._table_name(table_obj.name)
252
245
  table.table_type = "table"
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))
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
256
249
 
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)
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)
259
252
 
260
- for i in table_obj.get("indexes", []):
253
+ for i in table_obj.indexes:
261
254
  self.visit_index(i, table)
262
255
 
263
256
  self.graph_index[table_id] = table
264
257
  return table, columns
265
258
 
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)
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)
272
263
  if felis_type.is_sized:
273
264
  # It is expected that both arraysize and length are fine for
274
265
  # length types.
275
- arraysize = column_obj.get("votable:arraysize", column_obj.get("length"))
266
+ arraysize = column_obj.votable_arraysize or column_obj.length
276
267
  if arraysize is None:
277
268
  logger.warning(
278
269
  f"votable:arraysize and length for {_id} are None for type {datatype_name}. "
@@ -283,7 +274,7 @@ class TapLoadingVisitor(Visitor[None, tuple, Tap11Base, None, tuple, None, None]
283
274
  # datetime types really should have a votable:arraysize, because
284
275
  # they are converted to strings and the `length` is loosely to the
285
276
  # string size
286
- if "votable:arraysize" not in column_obj:
277
+ if not column_obj.votable_arraysize:
287
278
  logger.warning(
288
279
  f"votable:arraysize for {_id} is None for type {datatype_name}. "
289
280
  f'Using length "*". '
@@ -291,47 +282,45 @@ class TapLoadingVisitor(Visitor[None, tuple, Tap11Base, None, tuple, None, None]
291
282
  "materialized datetime/timestamp strings."
292
283
  )
293
284
 
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"])
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)
298
289
 
299
290
  column = self.tables["columns"]()
300
291
  column.table_name = table_name
301
- column.column_name = column_obj["name"]
292
+ column.column_name = column_obj.name
302
293
 
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)
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
306
297
 
307
298
  arraysize = None
308
299
  if felis_type.is_sized:
309
- # prefer votable:arraysize to length, fall back to `*`
310
- arraysize = column_obj.get("votable:arraysize", column_obj.get("length", "*"))
300
+ arraysize = column_obj.votable_arraysize or column_obj.length or "*"
311
301
  if felis_type.is_timestamp:
312
- arraysize = column_obj.get("votable:arraysize", "*")
302
+ arraysize = column_obj.votable_arraysize or "*"
313
303
  column.arraysize = arraysize
314
304
 
315
- column.xtype = column_obj.get("votable:xtype")
316
- column.description = column_obj.get("description")
317
- column.utype = column_obj.get("votable:utype")
305
+ column.xtype = column_obj.votable_xtype
306
+ column.description = column_obj.description
307
+ column.utype = column_obj.votable_utype
318
308
 
319
- unit = column_obj.get("ivoa:unit") or column_obj.get("fits:tunit")
309
+ unit = column_obj.ivoa_unit or column_obj.fits_tunit
320
310
  column.unit = unit
321
- column.ucd = column_obj.get("ivoa:ucd")
311
+ column.ucd = column_obj.ivoa_ucd
322
312
 
323
313
  # We modify this after we process columns
324
314
  column.indexed = 0
325
315
 
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")
316
+ column.principal = column_obj.tap_principal
317
+ column.std = column_obj.tap_std
318
+ column.column_index = column_obj.tap_column_index
329
319
 
330
320
  self.graph_index[column_id] = column
331
321
  return column
332
322
 
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)
323
+ def visit_primary_key(self, primary_key_obj: str | Iterable[str] | None, table_obj: Table) -> None:
335
324
  if primary_key_obj:
336
325
  if isinstance(primary_key_obj, str):
337
326
  primary_key_obj = [primary_key_obj]
@@ -341,19 +330,18 @@ class TapLoadingVisitor(Visitor[None, tuple, Tap11Base, None, tuple, None, None]
341
330
  columns[0].indexed = 1
342
331
  return None
343
332
 
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"]
333
+ def visit_constraint(self, constraint_obj: Constraint) -> tuple:
334
+ constraint_type = constraint_obj.type
347
335
  key = None
348
336
  key_columns = []
349
337
  if constraint_type == "ForeignKey":
350
- constraint_name = constraint_obj["name"]
351
- description = constraint_obj.get("description")
352
- utype = constraint_obj.get("votable:utype")
338
+ constraint_name = constraint_obj.name
339
+ description = constraint_obj.description
340
+ utype = constraint_obj.votable_utype
353
341
 
354
- columns = [self.graph_index[col["@id"]] for col in constraint_obj.get("columns", [])]
342
+ columns = [self.graph_index[col_id] for col_id in getattr(constraint_obj, "columns", [])]
355
343
  refcolumns = [
356
- self.graph_index[refcol["@id"]] for refcol in constraint_obj.get("referencedColumns", [])
344
+ self.graph_index[refcol_id] for refcol_id in getattr(constraint_obj, "referenced_columns", [])
357
345
  ]
358
346
 
359
347
  table_name = None
@@ -386,9 +374,8 @@ class TapLoadingVisitor(Visitor[None, tuple, Tap11Base, None, tuple, None, None]
386
374
  key_columns.append(key_column)
387
375
  return key, key_columns
388
376
 
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", [])]
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", [])]
392
379
  # if just one column and it's indexed, update the object
393
380
  if len(columns) == 1:
394
381
  columns[0].indexed = 1
felis/version.py CHANGED
@@ -1,2 +1,2 @@
1
1
  __all__ = ["__version__"]
2
- __version__ = "26.2024.1600"
2
+ __version__ = "26.2024.1700"
@@ -1,10 +1,11 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lsst-felis
3
- Version: 26.2024.1600
3
+ Version: 26.2024.1700
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://github.com/lsst/felis
7
+ Project-URL: Homepage, https://felis.lsst.io
8
+ Project-URL: Source, https://github.com/lsst/felis
8
9
  Keywords: lsst
9
10
  Classifier: Intended Audience :: Science/Research
10
11
  Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
@@ -24,6 +25,8 @@ Requires-Dist: pyyaml >=6
24
25
  Requires-Dist: pyld >=2
25
26
  Requires-Dist: pydantic <3,>=2
26
27
  Requires-Dist: lsst-utils
28
+ Provides-Extra: dev
29
+ Requires-Dist: documenteer[guide] ; extra == 'dev'
27
30
  Provides-Extra: test
28
31
  Requires-Dist: pytest >=3.2 ; extra == 'test'
29
32
 
@@ -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=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,,
@@ -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=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=H15ZOvw6LpBJgcJl5X5QV2o57W94MWuGRM3CPP_z0Xk,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=-YPkU5yS7udcPZTg__G_85tH6oMqVm6hEcHUgYpgiT8,5734
17
- lsst_felis-26.2024.1600.dist-info/COPYRIGHT,sha256=bUmNy19uUxqITMpjeHFe69q3IzQpjxvvBw6oV7kR7ho,129
18
- lsst_felis-26.2024.1600.dist-info/LICENSE,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
19
- lsst_felis-26.2024.1600.dist-info/METADATA,sha256=7F-m_tITJUAB08GAsCdFA2b7kmga_AZqtk8-B0uTQ3g,1101
20
- lsst_felis-26.2024.1600.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
21
- lsst_felis-26.2024.1600.dist-info/entry_points.txt,sha256=Gk2XFujA_Gp52VBk45g5kim8TDoMDJFPctsMqiq72EM,40
22
- lsst_felis-26.2024.1600.dist-info/top_level.txt,sha256=F4SvPip3iZRVyISi50CHhwTIAokAhSxjWiVcn4IVWRI,6
23
- lsst_felis-26.2024.1600.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
24
- lsst_felis-26.2024.1600.dist-info/RECORD,,