lsst-felis 29.2025.2500__py3-none-any.whl → 29.2025.2700__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 +182 -19
- felis/diff.py +14 -4
- {lsst_felis-29.2025.2500.dist-info → lsst_felis-29.2025.2700.dist-info}/METADATA +1 -1
- {lsst_felis-29.2025.2500.dist-info → lsst_felis-29.2025.2700.dist-info}/RECORD +10 -10
- {lsst_felis-29.2025.2500.dist-info → lsst_felis-29.2025.2700.dist-info}/WHEEL +0 -0
- {lsst_felis-29.2025.2500.dist-info → lsst_felis-29.2025.2700.dist-info}/entry_points.txt +0 -0
- {lsst_felis-29.2025.2500.dist-info → lsst_felis-29.2025.2700.dist-info}/licenses/COPYRIGHT +0 -0
- {lsst_felis-29.2025.2500.dist-info → lsst_felis-29.2025.2700.dist-info}/licenses/LICENSE +0 -0
- {lsst_felis-29.2025.2500.dist-info → lsst_felis-29.2025.2700.dist-info}/top_level.txt +0 -0
- {lsst_felis-29.2025.2500.dist-info → lsst_felis-29.2025.2700.dist-info}/zip-safe +0 -0
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
1258
|
-
|
|
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
|
-
|
|
1263
|
-
The
|
|
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
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
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
|
-
|
|
1271
|
-
|
|
1369
|
+
Returns
|
|
1370
|
+
-------
|
|
1371
|
+
`Schema`
|
|
1372
|
+
The schema being validated.
|
|
1272
1373
|
"""
|
|
1273
|
-
|
|
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.
|
felis/diff.py
CHANGED
|
@@ -112,8 +112,10 @@ class FormattedSchemaDiff(SchemaDiff):
|
|
|
112
112
|
handler(self.diff[change_type])
|
|
113
113
|
|
|
114
114
|
def _print_header(self, id_dict: dict[str, Any], keys: list[int | str]) -> None:
|
|
115
|
-
id = self._get_id(id_dict, keys)
|
|
116
|
-
|
|
115
|
+
# id = self._get_id(id_dict, keys)
|
|
116
|
+
# Don't display ID here for now; it is always just the schema ID.
|
|
117
|
+
print(f"{self._get_key_display(keys)}")
|
|
118
|
+
# print(f"{id} @ {self._get_key_display(keys)}")
|
|
117
119
|
|
|
118
120
|
def _handle_values_changed(self, changes: dict[str, Any]) -> None:
|
|
119
121
|
for key in changes:
|
|
@@ -156,13 +158,21 @@ class FormattedSchemaDiff(SchemaDiff):
|
|
|
156
158
|
|
|
157
159
|
@staticmethod
|
|
158
160
|
def _get_id(values: dict, keys: list[str | int]) -> str:
|
|
159
|
-
|
|
161
|
+
# Unused for now, pending updates to diff tool in DM-49446.
|
|
162
|
+
value: list | dict = values
|
|
160
163
|
last_id = None
|
|
161
164
|
|
|
162
165
|
for key in keys:
|
|
166
|
+
logger.debug(f"Processing key <{key}> with type {type(key)}")
|
|
167
|
+
logger.debug(f"Type of value: {type(value)}")
|
|
163
168
|
if isinstance(value, dict) and "id" in value:
|
|
164
169
|
last_id = value["id"]
|
|
165
|
-
value
|
|
170
|
+
elif isinstance(value, list) and isinstance(key, int):
|
|
171
|
+
if 0 <= key < len(value):
|
|
172
|
+
value = value[key]
|
|
173
|
+
else:
|
|
174
|
+
raise ValueError(f"Index '{key}' is out of range for list of length {len(value)}")
|
|
175
|
+
value = value[key]
|
|
166
176
|
|
|
167
177
|
if isinstance(value, dict) and "id" in value:
|
|
168
178
|
last_id = value["id"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lsst-felis
|
|
3
|
-
Version: 29.2025.
|
|
3
|
+
Version: 29.2025.2700
|
|
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,7 +1,7 @@
|
|
|
1
1
|
felis/__init__.py,sha256=HnwWzLaPOSnPzAoppSIHzTrGfixEgvkzJdBxa8-03cw,1294
|
|
2
2
|
felis/cli.py,sha256=g6OrBrIylNLiflSvrLlef86BjoiehV3L5eAvVPrxPog,16911
|
|
3
|
-
felis/datamodel.py,sha256=
|
|
4
|
-
felis/diff.py,sha256=
|
|
3
|
+
felis/datamodel.py,sha256=HKg4Ut0qPX7sV6q-Mw9U3BKgjVQFAnhhAUmo9Woh7v8,51228
|
|
4
|
+
felis/diff.py,sha256=ZzjOJ57p5ZwFn6eem7CYoPjSnxti5OZY33B6Ds5Q-Rg,7797
|
|
5
5
|
felis/metadata.py,sha256=79YcaIqeFP-pj9zhWpqXlvw_piUTUwuLrV5_8eVYalQ,13763
|
|
6
6
|
felis/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
7
|
felis/tap_schema.py,sha256=uu2imuzxyuIUbmW4liC6_h4TW5MW2t4SI9rjtMxcyfI,26372
|
|
@@ -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.
|
|
25
|
-
lsst_felis-29.2025.
|
|
26
|
-
lsst_felis-29.2025.
|
|
27
|
-
lsst_felis-29.2025.
|
|
28
|
-
lsst_felis-29.2025.
|
|
29
|
-
lsst_felis-29.2025.
|
|
30
|
-
lsst_felis-29.2025.
|
|
31
|
-
lsst_felis-29.2025.
|
|
24
|
+
lsst_felis-29.2025.2700.dist-info/licenses/COPYRIGHT,sha256=vJAFLFTSF1mhy9eIuA3P6R-3yxTWKQgpig88P-1IzRw,129
|
|
25
|
+
lsst_felis-29.2025.2700.dist-info/licenses/LICENSE,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
|
|
26
|
+
lsst_felis-29.2025.2700.dist-info/METADATA,sha256=DIXrsGLUC-sBW8-SY7BkhybbS4ETFl_1osQttfriFMc,1433
|
|
27
|
+
lsst_felis-29.2025.2700.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
28
|
+
lsst_felis-29.2025.2700.dist-info/entry_points.txt,sha256=Gk2XFujA_Gp52VBk45g5kim8TDoMDJFPctsMqiq72EM,40
|
|
29
|
+
lsst_felis-29.2025.2700.dist-info/top_level.txt,sha256=F4SvPip3iZRVyISi50CHhwTIAokAhSxjWiVcn4IVWRI,6
|
|
30
|
+
lsst_felis-29.2025.2700.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
31
|
+
lsst_felis-29.2025.2700.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|