lsst-felis 29.2025.2500__py3-none-any.whl → 29.2025.2600__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/datamodel.py CHANGED
@@ -39,11 +39,13 @@ from pydantic import (
39
39
  ConfigDict,
40
40
  Field,
41
41
  PrivateAttr,
42
+ ValidationError,
42
43
  ValidationInfo,
43
44
  field_serializer,
44
45
  field_validator,
45
46
  model_validator,
46
47
  )
48
+ from pydantic_core import InitErrorDetails
47
49
 
48
50
  from .db.dialects import get_supported_dialects
49
51
  from .db.sqltypes import get_type_func
@@ -758,7 +760,10 @@ class ColumnGroup(BaseObject):
758
760
  for col in self.columns:
759
761
  if isinstance(col, str):
760
762
  # Dereference ColumnRef to Column object
761
- col_obj = self.table._find_column_by_id(col)
763
+ try:
764
+ col_obj = self.table._find_column_by_id(col)
765
+ except KeyError as e:
766
+ raise ValueError(f"Column '{col}' not found in table '{self.table.name}'") from e
762
767
  dereferenced_columns.append(col_obj)
763
768
  else:
764
769
  dereferenced_columns.append(col)
@@ -908,7 +913,7 @@ class Table(BaseObject):
908
913
  for column in self.columns:
909
914
  if column.id == id:
910
915
  return column
911
- raise ValueError(f"Column '{id}' not found in table '{self.name}'")
916
+ raise KeyError(f"Column '{id}' not found in table '{self.name}'")
912
917
 
913
918
  @model_validator(mode="after")
914
919
  def dereference_column_groups(self: Table) -> Table:
@@ -1048,6 +1053,36 @@ def _strip_ids(data: Any) -> Any:
1048
1053
  return data
1049
1054
 
1050
1055
 
1056
+ def _append_error(
1057
+ errors: list[InitErrorDetails],
1058
+ loc: tuple,
1059
+ input_value: Any,
1060
+ error_message: str,
1061
+ error_type: str = "value_error",
1062
+ ) -> None:
1063
+ """Append an error to the errors list.
1064
+
1065
+ Parameters
1066
+ ----------
1067
+ errors : list[InitErrorDetails]
1068
+ The list of errors to append to.
1069
+ loc : tuple
1070
+ The location of the error in the schema.
1071
+ input_value : Any
1072
+ The input value that caused the error.
1073
+ error_message : str
1074
+ The error message to include in the context.
1075
+ """
1076
+ errors.append(
1077
+ {
1078
+ "type": error_type,
1079
+ "loc": loc,
1080
+ "input": input_value,
1081
+ "ctx": {"error": error_message},
1082
+ }
1083
+ )
1084
+
1085
+
1051
1086
  class Schema(BaseObject, Generic[T]):
1052
1087
  """Database schema model.
1053
1088
 
@@ -1230,18 +1265,19 @@ class Schema(BaseObject, Generic[T]):
1230
1265
 
1231
1266
  return self
1232
1267
 
1233
- def _create_id_map(self: Schema) -> Schema:
1268
+ @model_validator(mode="after")
1269
+ def create_id_map(self: Schema) -> Schema:
1234
1270
  """Create a map of IDs to objects.
1235
1271
 
1272
+ Returns
1273
+ -------
1274
+ `Schema`
1275
+ The schema with the ID map created.
1276
+
1236
1277
  Raises
1237
1278
  ------
1238
1279
  ValueError
1239
1280
  Raised if duplicate identifiers are found in the schema.
1240
-
1241
- Notes
1242
- -----
1243
- This is called automatically by the `model_post_init` method. If the
1244
- ID map is already populated, this method will return immediately.
1245
1281
  """
1246
1282
  if self._id_map:
1247
1283
  logger.debug("Ignoring call to create_id_map() - ID map was already populated")
@@ -1252,25 +1288,152 @@ class Schema(BaseObject, Generic[T]):
1252
1288
  raise ValueError(
1253
1289
  "Duplicate IDs found in schema:\n " + "\n ".join(visitor.duplicates) + "\n"
1254
1290
  )
1291
+ logger.debug("Created ID map with %d entries", len(self._id_map))
1255
1292
  return self
1256
1293
 
1257
- def model_post_init(self, ctx: Any) -> None:
1258
- """Post-initialization hook for the model.
1294
+ def _validate_column_id(
1295
+ self: Schema,
1296
+ column_id: str,
1297
+ loc: tuple,
1298
+ errors: list[InitErrorDetails],
1299
+ ) -> None:
1300
+ """Validate a column ID from a constraint and append errors if invalid.
1259
1301
 
1260
1302
  Parameters
1261
1303
  ----------
1262
- ctx
1263
- The context object which was passed to the model.
1304
+ schema : Schema
1305
+ The schema being validated.
1306
+ column_id : str
1307
+ The column ID to validate.
1308
+ loc : tuple
1309
+ The location of the error in the schema.
1310
+ errors : list[InitErrorDetails]
1311
+ The list of errors to append to.
1312
+ """
1313
+ if column_id not in self:
1314
+ _append_error(
1315
+ errors,
1316
+ loc,
1317
+ column_id,
1318
+ f"Column ID '{column_id}' not found in schema",
1319
+ )
1320
+ elif not isinstance(self[column_id], Column):
1321
+ _append_error(
1322
+ errors,
1323
+ loc,
1324
+ column_id,
1325
+ f"ID '{column_id}' does not refer to a Column object",
1326
+ )
1264
1327
 
1265
- Notes
1266
- -----
1267
- This method is called automatically by Pydantic after the model is
1268
- initialized. It is used to create the ID map for the schema.
1328
+ def _validate_foreign_key_column(
1329
+ self: Schema,
1330
+ column_id: str,
1331
+ table: Table,
1332
+ loc: tuple,
1333
+ errors: list[InitErrorDetails],
1334
+ ) -> None:
1335
+ """Validate a foreign key column ID from a constraint and append errors
1336
+ if invalid.
1337
+
1338
+ Parameters
1339
+ ----------
1340
+ schema : Schema
1341
+ The schema being validated.
1342
+ column_id : str
1343
+ The foreign key column ID to validate.
1344
+ loc : tuple
1345
+ The location of the error in the schema.
1346
+ errors : list[InitErrorDetails]
1347
+ The list of errors to append to.
1348
+ """
1349
+ try:
1350
+ table._find_column_by_id(column_id)
1351
+ except KeyError:
1352
+ _append_error(
1353
+ errors,
1354
+ loc,
1355
+ column_id,
1356
+ f"Column '{column_id}' not found in table '{table.name}'",
1357
+ )
1358
+
1359
+ @model_validator(mode="after")
1360
+ def check_constraints(self: Schema) -> Schema:
1361
+ """Check constraint objects for validity. This needs to be deferred
1362
+ until after the schema is fully loaded and the ID map is created.
1363
+
1364
+ Raises
1365
+ ------
1366
+ pydantic.ValidationError
1367
+ Raised if any constraints are invalid.
1269
1368
 
1270
- The ``ctx`` argument has the type `Any` because this is the function
1271
- signature in Pydantic itself.
1369
+ Returns
1370
+ -------
1371
+ `Schema`
1372
+ The schema being validated.
1272
1373
  """
1273
- self._create_id_map()
1374
+ errors: list[InitErrorDetails] = []
1375
+
1376
+ for table_index, table in enumerate(self.tables):
1377
+ for constraint_index, constraint in enumerate(table.constraints):
1378
+ column_ids: list[str] = []
1379
+ referenced_column_ids: list[str] = []
1380
+
1381
+ if isinstance(constraint, ForeignKeyConstraint):
1382
+ column_ids += constraint.columns
1383
+ referenced_column_ids += constraint.referenced_columns
1384
+ elif isinstance(constraint, UniqueConstraint):
1385
+ column_ids += constraint.columns
1386
+ # No extra checks are required on CheckConstraint objects.
1387
+
1388
+ # Validate the foreign key columns
1389
+ for column_id in column_ids:
1390
+ self._validate_column_id(
1391
+ column_id,
1392
+ (
1393
+ "tables",
1394
+ table_index,
1395
+ "constraints",
1396
+ constraint_index,
1397
+ "columns",
1398
+ column_id,
1399
+ ),
1400
+ errors,
1401
+ )
1402
+ # Check that the foreign key column is within the source
1403
+ # table.
1404
+ self._validate_foreign_key_column(
1405
+ column_id,
1406
+ table,
1407
+ (
1408
+ "tables",
1409
+ table_index,
1410
+ "constraints",
1411
+ constraint_index,
1412
+ "columns",
1413
+ column_id,
1414
+ ),
1415
+ errors,
1416
+ )
1417
+
1418
+ # Validate the primary key (reference) columns
1419
+ for referenced_column_id in referenced_column_ids:
1420
+ self._validate_column_id(
1421
+ referenced_column_id,
1422
+ (
1423
+ "tables",
1424
+ table_index,
1425
+ "constraints",
1426
+ constraint_index,
1427
+ "referenced_columns",
1428
+ referenced_column_id,
1429
+ ),
1430
+ errors,
1431
+ )
1432
+
1433
+ if errors:
1434
+ raise ValidationError.from_exception_data("Schema validation failed", errors)
1435
+
1436
+ return self
1274
1437
 
1275
1438
  def __getitem__(self, id: str) -> BaseObject:
1276
1439
  """Get an object by its ID.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lsst-felis
3
- Version: 29.2025.2500
3
+ Version: 29.2025.2600
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+)
@@ -1,6 +1,6 @@
1
1
  felis/__init__.py,sha256=HnwWzLaPOSnPzAoppSIHzTrGfixEgvkzJdBxa8-03cw,1294
2
2
  felis/cli.py,sha256=g6OrBrIylNLiflSvrLlef86BjoiehV3L5eAvVPrxPog,16911
3
- felis/datamodel.py,sha256=COXmk1p8qiwdiOrPkTJNCSbg27FIvosF3pFIFusAsnA,45926
3
+ felis/datamodel.py,sha256=HKg4Ut0qPX7sV6q-Mw9U3BKgjVQFAnhhAUmo9Woh7v8,51228
4
4
  felis/diff.py,sha256=z4ZzUocFYVa2y22BWUAMkeeLORmMtaWIDGTVaUE1OIM,7181
5
5
  felis/metadata.py,sha256=79YcaIqeFP-pj9zhWpqXlvw_piUTUwuLrV5_8eVYalQ,13763
6
6
  felis/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -21,11 +21,11 @@ felis/db/variants.py,sha256=eahthrbVeV8ZdGamWQccNmWgx6CCscGrU0vQRs5HZK8,5260
21
21
  felis/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
22
  felis/tests/postgresql.py,sha256=B_xk4fLual5-viGDqP20r94okuc0pbSvytRH_L0fvMs,4035
23
23
  felis/tests/run_cli.py,sha256=Gg8loUIGj9t6KlkRKrEc9Z9b5dtlkpJy94ORuj4BrxU,2503
24
- lsst_felis-29.2025.2500.dist-info/licenses/COPYRIGHT,sha256=vJAFLFTSF1mhy9eIuA3P6R-3yxTWKQgpig88P-1IzRw,129
25
- lsst_felis-29.2025.2500.dist-info/licenses/LICENSE,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
26
- lsst_felis-29.2025.2500.dist-info/METADATA,sha256=waHHJI3LP891BnULx3aiSr0f8DaPPKf9a3lHVkZ_Djo,1433
27
- lsst_felis-29.2025.2500.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
28
- lsst_felis-29.2025.2500.dist-info/entry_points.txt,sha256=Gk2XFujA_Gp52VBk45g5kim8TDoMDJFPctsMqiq72EM,40
29
- lsst_felis-29.2025.2500.dist-info/top_level.txt,sha256=F4SvPip3iZRVyISi50CHhwTIAokAhSxjWiVcn4IVWRI,6
30
- lsst_felis-29.2025.2500.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
31
- lsst_felis-29.2025.2500.dist-info/RECORD,,
24
+ lsst_felis-29.2025.2600.dist-info/licenses/COPYRIGHT,sha256=vJAFLFTSF1mhy9eIuA3P6R-3yxTWKQgpig88P-1IzRw,129
25
+ lsst_felis-29.2025.2600.dist-info/licenses/LICENSE,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
26
+ lsst_felis-29.2025.2600.dist-info/METADATA,sha256=olXZcNPImhvS5QjMr8uoEyq_Br5GjLRaBjE4OsujeJM,1433
27
+ lsst_felis-29.2025.2600.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
28
+ lsst_felis-29.2025.2600.dist-info/entry_points.txt,sha256=Gk2XFujA_Gp52VBk45g5kim8TDoMDJFPctsMqiq72EM,40
29
+ lsst_felis-29.2025.2600.dist-info/top_level.txt,sha256=F4SvPip3iZRVyISi50CHhwTIAokAhSxjWiVcn4IVWRI,6
30
+ lsst_felis-29.2025.2600.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
31
+ lsst_felis-29.2025.2600.dist-info/RECORD,,