lsst-felis 27.2024.2500__py3-none-any.whl → 27.2024.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/cli.py +145 -29
- felis/datamodel.py +334 -89
- felis/db/dialects.py +65 -12
- felis/db/sqltypes.py +255 -16
- felis/db/utils.py +108 -52
- felis/db/variants.py +66 -8
- felis/metadata.py +70 -54
- felis/tap.py +180 -18
- felis/types.py +56 -8
- felis/version.py +1 -1
- {lsst_felis-27.2024.2500.dist-info → lsst_felis-27.2024.2700.dist-info}/COPYRIGHT +1 -1
- {lsst_felis-27.2024.2500.dist-info → lsst_felis-27.2024.2700.dist-info}/METADATA +4 -2
- lsst_felis-27.2024.2700.dist-info/RECORD +21 -0
- {lsst_felis-27.2024.2500.dist-info → lsst_felis-27.2024.2700.dist-info}/WHEEL +1 -1
- lsst_felis-27.2024.2500.dist-info/RECORD +0 -21
- {lsst_felis-27.2024.2500.dist-info → lsst_felis-27.2024.2700.dist-info}/LICENSE +0 -0
- {lsst_felis-27.2024.2500.dist-info → lsst_felis-27.2024.2700.dist-info}/entry_points.txt +0 -0
- {lsst_felis-27.2024.2500.dist-info → lsst_felis-27.2024.2700.dist-info}/top_level.txt +0 -0
- {lsst_felis-27.2024.2500.dist-info → lsst_felis-27.2024.2700.dist-info}/zip-safe +0 -0
felis/datamodel.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Define Pydantic data models for Felis."""
|
|
2
|
+
|
|
1
3
|
# This file is part of felis.
|
|
2
4
|
#
|
|
3
5
|
# Developed for the LSST Data Management System.
|
|
@@ -42,7 +44,6 @@ __all__ = (
|
|
|
42
44
|
"Column",
|
|
43
45
|
"CheckConstraint",
|
|
44
46
|
"Constraint",
|
|
45
|
-
"DescriptionStr",
|
|
46
47
|
"ForeignKeyConstraint",
|
|
47
48
|
"Index",
|
|
48
49
|
"Schema",
|
|
@@ -64,37 +65,45 @@ DESCR_MIN_LENGTH = 3
|
|
|
64
65
|
"""Minimum length for a description field."""
|
|
65
66
|
|
|
66
67
|
DescriptionStr: TypeAlias = Annotated[str, Field(min_length=DESCR_MIN_LENGTH)]
|
|
67
|
-
"""
|
|
68
|
-
characters long. Stripping of whitespace is done globally on all str fields."""
|
|
68
|
+
"""Type for a description, which must be three or more characters long."""
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
class BaseObject(BaseModel):
|
|
72
|
-
"""Base
|
|
72
|
+
"""Base model.
|
|
73
|
+
|
|
74
|
+
All classes representing objects in the Felis data model should inherit
|
|
75
|
+
from this class.
|
|
76
|
+
"""
|
|
73
77
|
|
|
74
78
|
model_config = CONFIG
|
|
75
79
|
"""Pydantic model configuration."""
|
|
76
80
|
|
|
77
81
|
name: str
|
|
78
|
-
"""
|
|
79
|
-
|
|
80
|
-
All Felis database objects must have a name.
|
|
81
|
-
"""
|
|
82
|
+
"""Name of the database object."""
|
|
82
83
|
|
|
83
84
|
id: str = Field(alias="@id")
|
|
84
|
-
"""
|
|
85
|
-
|
|
86
|
-
All Felis database objects must have a unique identifier.
|
|
87
|
-
"""
|
|
85
|
+
"""Unique identifier of the database object."""
|
|
88
86
|
|
|
89
87
|
description: DescriptionStr | None = None
|
|
90
|
-
"""
|
|
88
|
+
"""Description of the database object."""
|
|
91
89
|
|
|
92
90
|
votable_utype: str | None = Field(None, alias="votable:utype")
|
|
93
|
-
"""
|
|
91
|
+
"""VOTable utype (usage-specific or unique type) of the object."""
|
|
94
92
|
|
|
95
93
|
@model_validator(mode="after")
|
|
96
94
|
def check_description(self, info: ValidationInfo) -> BaseObject:
|
|
97
|
-
"""Check that the description is present if required.
|
|
95
|
+
"""Check that the description is present if required.
|
|
96
|
+
|
|
97
|
+
Parameters
|
|
98
|
+
----------
|
|
99
|
+
info
|
|
100
|
+
Validation context used to determine if the check is enabled.
|
|
101
|
+
|
|
102
|
+
Returns
|
|
103
|
+
-------
|
|
104
|
+
`BaseObject`
|
|
105
|
+
The object being validated.
|
|
106
|
+
"""
|
|
98
107
|
context = info.context
|
|
99
108
|
if not context or not context.get("check_description", False):
|
|
100
109
|
return self
|
|
@@ -124,61 +133,66 @@ class DataType(StrEnum):
|
|
|
124
133
|
|
|
125
134
|
|
|
126
135
|
class Column(BaseObject):
|
|
127
|
-
"""
|
|
136
|
+
"""Column model."""
|
|
128
137
|
|
|
129
138
|
datatype: DataType
|
|
130
|
-
"""
|
|
139
|
+
"""Datatype of the column."""
|
|
131
140
|
|
|
132
141
|
length: int | None = Field(None, gt=0)
|
|
133
|
-
"""
|
|
142
|
+
"""Length of the column."""
|
|
134
143
|
|
|
135
144
|
nullable: bool = True
|
|
136
145
|
"""Whether the column can be ``NULL``."""
|
|
137
146
|
|
|
138
147
|
value: str | int | float | bool | None = None
|
|
139
|
-
"""
|
|
148
|
+
"""Default value of the column."""
|
|
140
149
|
|
|
141
150
|
autoincrement: bool | None = None
|
|
142
151
|
"""Whether the column is autoincremented."""
|
|
143
152
|
|
|
144
153
|
mysql_datatype: str | None = Field(None, alias="mysql:datatype")
|
|
145
|
-
"""
|
|
154
|
+
"""MySQL datatype override on the column."""
|
|
146
155
|
|
|
147
156
|
postgresql_datatype: str | None = Field(None, alias="postgresql:datatype")
|
|
148
|
-
"""
|
|
157
|
+
"""PostgreSQL datatype override on the column."""
|
|
149
158
|
|
|
150
159
|
ivoa_ucd: str | None = Field(None, alias="ivoa:ucd")
|
|
151
|
-
"""
|
|
160
|
+
"""IVOA UCD of the column."""
|
|
152
161
|
|
|
153
162
|
fits_tunit: str | None = Field(None, alias="fits:tunit")
|
|
154
|
-
"""
|
|
163
|
+
"""FITS TUNIT of the column."""
|
|
155
164
|
|
|
156
165
|
ivoa_unit: str | None = Field(None, alias="ivoa:unit")
|
|
157
|
-
"""
|
|
166
|
+
"""IVOA unit of the column."""
|
|
158
167
|
|
|
159
168
|
tap_column_index: int | None = Field(None, alias="tap:column_index")
|
|
160
|
-
"""
|
|
169
|
+
"""TAP_SCHEMA column index of the column."""
|
|
161
170
|
|
|
162
171
|
tap_principal: int | None = Field(0, alias="tap:principal", ge=0, le=1)
|
|
163
|
-
"""Whether this is a TAP_SCHEMA principal column
|
|
164
|
-
"""
|
|
172
|
+
"""Whether this is a TAP_SCHEMA principal column."""
|
|
165
173
|
|
|
166
174
|
votable_arraysize: int | Literal["*"] | None = Field(None, alias="votable:arraysize")
|
|
167
|
-
"""
|
|
175
|
+
"""VOTable arraysize of the column."""
|
|
168
176
|
|
|
169
177
|
tap_std: int | None = Field(0, alias="tap:std", ge=0, le=1)
|
|
170
178
|
"""TAP_SCHEMA indication that this column is defined by an IVOA standard.
|
|
171
179
|
"""
|
|
172
180
|
|
|
173
181
|
votable_xtype: str | None = Field(None, alias="votable:xtype")
|
|
174
|
-
"""
|
|
182
|
+
"""VOTable xtype (extended type) of the column."""
|
|
175
183
|
|
|
176
184
|
votable_datatype: str | None = Field(None, alias="votable:datatype")
|
|
177
|
-
"""
|
|
185
|
+
"""VOTable datatype of the column."""
|
|
178
186
|
|
|
179
187
|
@model_validator(mode="after")
|
|
180
188
|
def check_value(self) -> Column:
|
|
181
|
-
"""Check that the default value is valid.
|
|
189
|
+
"""Check that the default value is valid.
|
|
190
|
+
|
|
191
|
+
Returns
|
|
192
|
+
-------
|
|
193
|
+
`Column`
|
|
194
|
+
The column being validated.
|
|
195
|
+
"""
|
|
182
196
|
if (value := self.value) is not None:
|
|
183
197
|
if value is not None and self.autoincrement is True:
|
|
184
198
|
raise ValueError("Column cannot have both a default value and be autoincremented")
|
|
@@ -200,7 +214,18 @@ class Column(BaseObject):
|
|
|
200
214
|
@field_validator("ivoa_ucd")
|
|
201
215
|
@classmethod
|
|
202
216
|
def check_ivoa_ucd(cls, ivoa_ucd: str) -> str:
|
|
203
|
-
"""Check that IVOA UCD values are valid.
|
|
217
|
+
"""Check that IVOA UCD values are valid.
|
|
218
|
+
|
|
219
|
+
Parameters
|
|
220
|
+
----------
|
|
221
|
+
ivoa_ucd
|
|
222
|
+
IVOA UCD value to check.
|
|
223
|
+
|
|
224
|
+
Returns
|
|
225
|
+
-------
|
|
226
|
+
`str`
|
|
227
|
+
The IVOA UCD value if it is valid.
|
|
228
|
+
"""
|
|
204
229
|
if ivoa_ucd is not None:
|
|
205
230
|
try:
|
|
206
231
|
ucd.parse_ucd(ivoa_ucd, check_controlled_vocabulary=True, has_colon=";" in ivoa_ucd)
|
|
@@ -210,7 +235,20 @@ class Column(BaseObject):
|
|
|
210
235
|
|
|
211
236
|
@model_validator(mode="after")
|
|
212
237
|
def check_units(self) -> Column:
|
|
213
|
-
"""Check that
|
|
238
|
+
"""Check that the ``fits:tunit`` or ``ivoa:unit`` field has valid
|
|
239
|
+
units according to astropy. Only one may be provided.
|
|
240
|
+
|
|
241
|
+
Returns
|
|
242
|
+
-------
|
|
243
|
+
`Column`
|
|
244
|
+
The column being validated.
|
|
245
|
+
|
|
246
|
+
Raises
|
|
247
|
+
------
|
|
248
|
+
ValueError
|
|
249
|
+
If both FITS and IVOA units are provided, or if the unit is
|
|
250
|
+
invalid.
|
|
251
|
+
"""
|
|
214
252
|
fits_unit = self.fits_tunit
|
|
215
253
|
ivoa_unit = self.ivoa_unit
|
|
216
254
|
|
|
@@ -229,7 +267,23 @@ class Column(BaseObject):
|
|
|
229
267
|
@model_validator(mode="before")
|
|
230
268
|
@classmethod
|
|
231
269
|
def check_length(cls, values: dict[str, Any]) -> dict[str, Any]:
|
|
232
|
-
"""Check that a valid length is provided for sized types.
|
|
270
|
+
"""Check that a valid length is provided for sized types.
|
|
271
|
+
|
|
272
|
+
Parameters
|
|
273
|
+
----------
|
|
274
|
+
values
|
|
275
|
+
Values of the column.
|
|
276
|
+
|
|
277
|
+
Returns
|
|
278
|
+
-------
|
|
279
|
+
`dict` [ `str`, `Any` ]
|
|
280
|
+
The values of the column.
|
|
281
|
+
|
|
282
|
+
Raises
|
|
283
|
+
------
|
|
284
|
+
ValueError
|
|
285
|
+
If a length is not provided for a sized type.
|
|
286
|
+
"""
|
|
233
287
|
datatype = values.get("datatype")
|
|
234
288
|
if datatype is None:
|
|
235
289
|
# Skip this validation if datatype is not provided
|
|
@@ -250,7 +304,23 @@ class Column(BaseObject):
|
|
|
250
304
|
|
|
251
305
|
@model_validator(mode="after")
|
|
252
306
|
def check_redundant_datatypes(self, info: ValidationInfo) -> Column:
|
|
253
|
-
"""Check for redundant datatypes on columns.
|
|
307
|
+
"""Check for redundant datatypes on columns.
|
|
308
|
+
|
|
309
|
+
Parameters
|
|
310
|
+
----------
|
|
311
|
+
info
|
|
312
|
+
Validation context used to determine if the check is enabled.
|
|
313
|
+
|
|
314
|
+
Returns
|
|
315
|
+
-------
|
|
316
|
+
`Column`
|
|
317
|
+
The column being validated.
|
|
318
|
+
|
|
319
|
+
Raises
|
|
320
|
+
------
|
|
321
|
+
ValueError
|
|
322
|
+
If a datatype override is redundant.
|
|
323
|
+
"""
|
|
254
324
|
context = info.context
|
|
255
325
|
if not context or not context.get("check_redundant_datatypes", False):
|
|
256
326
|
return self
|
|
@@ -295,51 +365,69 @@ class Column(BaseObject):
|
|
|
295
365
|
|
|
296
366
|
|
|
297
367
|
class Constraint(BaseObject):
|
|
298
|
-
"""
|
|
368
|
+
"""Table constraint model."""
|
|
299
369
|
|
|
300
370
|
deferrable: bool = False
|
|
301
|
-
"""
|
|
371
|
+
"""Whether this constraint will be declared as deferrable."""
|
|
302
372
|
|
|
303
373
|
initially: str | None = None
|
|
304
|
-
"""Value for ``INITIALLY`` clause
|
|
374
|
+
"""Value for ``INITIALLY`` clause; only used if `deferrable` is
|
|
375
|
+
`True`."""
|
|
305
376
|
|
|
306
377
|
annotations: Mapping[str, Any] = Field(default_factory=dict)
|
|
307
378
|
"""Additional annotations for this constraint."""
|
|
308
379
|
|
|
309
380
|
type: str | None = Field(None, alias="@type")
|
|
310
|
-
"""
|
|
381
|
+
"""Type of the constraint."""
|
|
311
382
|
|
|
312
383
|
|
|
313
384
|
class CheckConstraint(Constraint):
|
|
314
|
-
"""
|
|
385
|
+
"""Table check constraint model."""
|
|
315
386
|
|
|
316
387
|
expression: str
|
|
317
|
-
"""
|
|
388
|
+
"""Expression for the check constraint."""
|
|
318
389
|
|
|
319
390
|
|
|
320
391
|
class UniqueConstraint(Constraint):
|
|
321
|
-
"""
|
|
392
|
+
"""Table unique constraint model."""
|
|
322
393
|
|
|
323
394
|
columns: list[str]
|
|
324
|
-
"""
|
|
395
|
+
"""Columns in the unique constraint."""
|
|
325
396
|
|
|
326
397
|
|
|
327
398
|
class Index(BaseObject):
|
|
328
|
-
"""
|
|
399
|
+
"""Table index model.
|
|
329
400
|
|
|
330
401
|
An index can be defined on either columns or expressions, but not both.
|
|
331
402
|
"""
|
|
332
403
|
|
|
333
404
|
columns: list[str] | None = None
|
|
334
|
-
"""
|
|
405
|
+
"""Columns in the index."""
|
|
335
406
|
|
|
336
407
|
expressions: list[str] | None = None
|
|
337
|
-
"""
|
|
408
|
+
"""Expressions in the index."""
|
|
338
409
|
|
|
339
410
|
@model_validator(mode="before")
|
|
340
411
|
@classmethod
|
|
341
412
|
def check_columns_or_expressions(cls, values: dict[str, Any]) -> dict[str, Any]:
|
|
342
|
-
"""Check that columns or expressions are specified, but not both.
|
|
413
|
+
"""Check that columns or expressions are specified, but not both.
|
|
414
|
+
|
|
415
|
+
Parameters
|
|
416
|
+
----------
|
|
417
|
+
values
|
|
418
|
+
Values of the index.
|
|
419
|
+
|
|
420
|
+
Returns
|
|
421
|
+
-------
|
|
422
|
+
`dict` [ `str`, `Any` ]
|
|
423
|
+
The values of the index.
|
|
424
|
+
|
|
425
|
+
Raises
|
|
426
|
+
------
|
|
427
|
+
ValueError
|
|
428
|
+
If both columns and expressions are specified, or if neither are
|
|
429
|
+
specified.
|
|
430
|
+
"""
|
|
343
431
|
if "columns" in values and "expressions" in values:
|
|
344
432
|
raise ValueError("Defining columns and expressions is not valid")
|
|
345
433
|
elif "columns" not in values and "expressions" not in values:
|
|
@@ -348,9 +436,15 @@ class Index(BaseObject):
|
|
|
348
436
|
|
|
349
437
|
|
|
350
438
|
class ForeignKeyConstraint(Constraint):
|
|
351
|
-
"""
|
|
439
|
+
"""Table foreign key constraint model.
|
|
440
|
+
|
|
441
|
+
This constraint is used to define a foreign key relationship between two
|
|
442
|
+
tables in the schema.
|
|
352
443
|
|
|
353
|
-
|
|
444
|
+
Notes
|
|
445
|
+
-----
|
|
446
|
+
These relationships will be reflected in the TAP_SCHEMA ``keys`` and
|
|
447
|
+
``key_columns`` data.
|
|
354
448
|
"""
|
|
355
449
|
|
|
356
450
|
columns: list[str]
|
|
@@ -361,41 +455,46 @@ class ForeignKeyConstraint(Constraint):
|
|
|
361
455
|
|
|
362
456
|
|
|
363
457
|
class Table(BaseObject):
|
|
364
|
-
"""
|
|
458
|
+
"""Table model."""
|
|
365
459
|
|
|
366
460
|
columns: Sequence[Column]
|
|
367
|
-
"""
|
|
461
|
+
"""Columns in the table."""
|
|
368
462
|
|
|
369
463
|
constraints: list[Constraint] = Field(default_factory=list)
|
|
370
|
-
"""
|
|
464
|
+
"""Constraints on the table."""
|
|
371
465
|
|
|
372
466
|
indexes: list[Index] = Field(default_factory=list)
|
|
373
|
-
"""
|
|
467
|
+
"""Indexes on the table."""
|
|
374
468
|
|
|
375
469
|
primary_key: str | list[str] | None = Field(None, alias="primaryKey")
|
|
376
|
-
"""
|
|
470
|
+
"""Primary key of the table."""
|
|
377
471
|
|
|
378
472
|
tap_table_index: int | None = Field(None, alias="tap:table_index")
|
|
379
|
-
"""
|
|
473
|
+
"""IVOA TAP_SCHEMA table index of the table."""
|
|
380
474
|
|
|
381
475
|
mysql_engine: str | None = Field(None, alias="mysql:engine")
|
|
382
|
-
"""
|
|
383
|
-
|
|
384
|
-
For now this is a freeform string but it could be constrained to a list of
|
|
385
|
-
known engines in the future.
|
|
386
|
-
"""
|
|
476
|
+
"""MySQL engine to use for the table."""
|
|
387
477
|
|
|
388
478
|
mysql_charset: str | None = Field(None, alias="mysql:charset")
|
|
389
|
-
"""
|
|
390
|
-
|
|
391
|
-
For now this is a freeform string but it could be constrained to a list of
|
|
392
|
-
known charsets in the future.
|
|
393
|
-
"""
|
|
479
|
+
"""MySQL charset to use for the table."""
|
|
394
480
|
|
|
395
481
|
@model_validator(mode="before")
|
|
396
482
|
@classmethod
|
|
397
483
|
def create_constraints(cls, values: dict[str, Any]) -> dict[str, Any]:
|
|
398
|
-
"""Create
|
|
484
|
+
"""Create specific constraint types from the data in the
|
|
485
|
+
``constraints`` field of a table.
|
|
486
|
+
|
|
487
|
+
Parameters
|
|
488
|
+
----------
|
|
489
|
+
values
|
|
490
|
+
The values of the table containing the constraint data.
|
|
491
|
+
|
|
492
|
+
Returns
|
|
493
|
+
-------
|
|
494
|
+
`dict` [ `str`, `Any` ]
|
|
495
|
+
The values of the table with the constraints converted to their
|
|
496
|
+
respective types.
|
|
497
|
+
"""
|
|
399
498
|
if "constraints" in values:
|
|
400
499
|
new_constraints: list[Constraint] = []
|
|
401
500
|
for item in values["constraints"]:
|
|
@@ -413,14 +512,46 @@ class Table(BaseObject):
|
|
|
413
512
|
@field_validator("columns", mode="after")
|
|
414
513
|
@classmethod
|
|
415
514
|
def check_unique_column_names(cls, columns: list[Column]) -> list[Column]:
|
|
416
|
-
"""Check that column names are unique.
|
|
515
|
+
"""Check that column names are unique.
|
|
516
|
+
|
|
517
|
+
Parameters
|
|
518
|
+
----------
|
|
519
|
+
columns
|
|
520
|
+
The columns to check.
|
|
521
|
+
|
|
522
|
+
Returns
|
|
523
|
+
-------
|
|
524
|
+
`list` [ `Column` ]
|
|
525
|
+
The columns if they are unique.
|
|
526
|
+
|
|
527
|
+
Raises
|
|
528
|
+
------
|
|
529
|
+
ValueError
|
|
530
|
+
If column names are not unique.
|
|
531
|
+
"""
|
|
417
532
|
if len(columns) != len(set(column.name for column in columns)):
|
|
418
533
|
raise ValueError("Column names must be unique")
|
|
419
534
|
return columns
|
|
420
535
|
|
|
421
536
|
@model_validator(mode="after")
|
|
422
537
|
def check_tap_table_index(self, info: ValidationInfo) -> Table:
|
|
423
|
-
"""Check that the table has a TAP table index.
|
|
538
|
+
"""Check that the table has a TAP table index.
|
|
539
|
+
|
|
540
|
+
Parameters
|
|
541
|
+
----------
|
|
542
|
+
info
|
|
543
|
+
Validation context used to determine if the check is enabled.
|
|
544
|
+
|
|
545
|
+
Returns
|
|
546
|
+
-------
|
|
547
|
+
`Table`
|
|
548
|
+
The table being validated.
|
|
549
|
+
|
|
550
|
+
Raises
|
|
551
|
+
------
|
|
552
|
+
ValueError
|
|
553
|
+
If the table is missing a TAP table index.
|
|
554
|
+
"""
|
|
424
555
|
context = info.context
|
|
425
556
|
if not context or not context.get("check_tap_table_indexes", False):
|
|
426
557
|
return self
|
|
@@ -432,6 +563,21 @@ class Table(BaseObject):
|
|
|
432
563
|
def check_tap_principal(self, info: ValidationInfo) -> Table:
|
|
433
564
|
"""Check that at least one column is flagged as 'principal' for TAP
|
|
434
565
|
purposes.
|
|
566
|
+
|
|
567
|
+
Parameters
|
|
568
|
+
----------
|
|
569
|
+
info
|
|
570
|
+
Validation context used to determine if the check is enabled.
|
|
571
|
+
|
|
572
|
+
Returns
|
|
573
|
+
-------
|
|
574
|
+
`Table`
|
|
575
|
+
The table being validated.
|
|
576
|
+
|
|
577
|
+
Raises
|
|
578
|
+
------
|
|
579
|
+
ValueError
|
|
580
|
+
If the table is missing a column flagged as 'principal'.
|
|
435
581
|
"""
|
|
436
582
|
context = info.context
|
|
437
583
|
if not context or not context.get("check_tap_principal", False):
|
|
@@ -443,7 +589,7 @@ class Table(BaseObject):
|
|
|
443
589
|
|
|
444
590
|
|
|
445
591
|
class SchemaVersion(BaseModel):
|
|
446
|
-
"""
|
|
592
|
+
"""Schema version model."""
|
|
447
593
|
|
|
448
594
|
current: str
|
|
449
595
|
"""The current version of the schema."""
|
|
@@ -456,15 +602,16 @@ class SchemaVersion(BaseModel):
|
|
|
456
602
|
|
|
457
603
|
|
|
458
604
|
class SchemaIdVisitor:
|
|
459
|
-
"""
|
|
605
|
+
"""Visit a schema and build the map of IDs to objects.
|
|
460
606
|
|
|
607
|
+
Notes
|
|
608
|
+
-----
|
|
461
609
|
Duplicates are added to a set when they are encountered, which can be
|
|
462
|
-
accessed via the
|
|
610
|
+
accessed via the ``duplicates`` attribute. The presence of duplicates will
|
|
463
611
|
not throw an error. Only the first object with a given ID will be added to
|
|
464
|
-
the map, but this should not matter, since a ValidationError will be
|
|
465
|
-
by the
|
|
466
|
-
|
|
467
|
-
This class is intended for internal use only.
|
|
612
|
+
the map, but this should not matter, since a ``ValidationError`` will be
|
|
613
|
+
thrown by the ``model_validator`` method if any duplicates are found in the
|
|
614
|
+
schema.
|
|
468
615
|
"""
|
|
469
616
|
|
|
470
617
|
def __init__(self) -> None:
|
|
@@ -473,7 +620,13 @@ class SchemaIdVisitor:
|
|
|
473
620
|
self.duplicates: set[str] = set()
|
|
474
621
|
|
|
475
622
|
def add(self, obj: BaseObject) -> None:
|
|
476
|
-
"""Add an object to the ID map.
|
|
623
|
+
"""Add an object to the ID map.
|
|
624
|
+
|
|
625
|
+
Parameters
|
|
626
|
+
----------
|
|
627
|
+
obj
|
|
628
|
+
The object to add to the ID map.
|
|
629
|
+
"""
|
|
477
630
|
if hasattr(obj, "id"):
|
|
478
631
|
obj_id = getattr(obj, "id")
|
|
479
632
|
if self.schema is not None:
|
|
@@ -483,8 +636,15 @@ class SchemaIdVisitor:
|
|
|
483
636
|
self.schema.id_map[obj_id] = obj
|
|
484
637
|
|
|
485
638
|
def visit_schema(self, schema: Schema) -> None:
|
|
486
|
-
"""Visit the schema
|
|
639
|
+
"""Visit the objects in a schema and build the ID map.
|
|
487
640
|
|
|
641
|
+
Parameters
|
|
642
|
+
----------
|
|
643
|
+
schema
|
|
644
|
+
The schema object to visit.
|
|
645
|
+
|
|
646
|
+
Notes
|
|
647
|
+
-----
|
|
488
648
|
This will set an internal variable pointing to the schema object.
|
|
489
649
|
"""
|
|
490
650
|
self.schema = schema
|
|
@@ -494,7 +654,13 @@ class SchemaIdVisitor:
|
|
|
494
654
|
self.visit_table(table)
|
|
495
655
|
|
|
496
656
|
def visit_table(self, table: Table) -> None:
|
|
497
|
-
"""Visit a table object.
|
|
657
|
+
"""Visit a table object.
|
|
658
|
+
|
|
659
|
+
Parameters
|
|
660
|
+
----------
|
|
661
|
+
table
|
|
662
|
+
The table object to visit.
|
|
663
|
+
"""
|
|
498
664
|
self.add(table)
|
|
499
665
|
for column in table.columns:
|
|
500
666
|
self.visit_column(column)
|
|
@@ -502,16 +668,31 @@ class SchemaIdVisitor:
|
|
|
502
668
|
self.visit_constraint(constraint)
|
|
503
669
|
|
|
504
670
|
def visit_column(self, column: Column) -> None:
|
|
505
|
-
"""Visit a column object.
|
|
671
|
+
"""Visit a column object.
|
|
672
|
+
|
|
673
|
+
Parameters
|
|
674
|
+
----------
|
|
675
|
+
column
|
|
676
|
+
The column object to visit.
|
|
677
|
+
"""
|
|
506
678
|
self.add(column)
|
|
507
679
|
|
|
508
680
|
def visit_constraint(self, constraint: Constraint) -> None:
|
|
509
|
-
"""Visit a constraint object.
|
|
681
|
+
"""Visit a constraint object.
|
|
682
|
+
|
|
683
|
+
Parameters
|
|
684
|
+
----------
|
|
685
|
+
constraint
|
|
686
|
+
The constraint object to visit.
|
|
687
|
+
"""
|
|
510
688
|
self.add(constraint)
|
|
511
689
|
|
|
512
690
|
|
|
513
691
|
class Schema(BaseObject):
|
|
514
|
-
"""
|
|
692
|
+
"""Database schema model.
|
|
693
|
+
|
|
694
|
+
This is the root object of the Felis data model.
|
|
695
|
+
"""
|
|
515
696
|
|
|
516
697
|
version: SchemaVersion | str | None = None
|
|
517
698
|
"""The version of the schema."""
|
|
@@ -525,14 +706,41 @@ class Schema(BaseObject):
|
|
|
525
706
|
@field_validator("tables", mode="after")
|
|
526
707
|
@classmethod
|
|
527
708
|
def check_unique_table_names(cls, tables: list[Table]) -> list[Table]:
|
|
528
|
-
"""Check that table names are unique.
|
|
709
|
+
"""Check that table names are unique.
|
|
710
|
+
|
|
711
|
+
Parameters
|
|
712
|
+
----------
|
|
713
|
+
tables
|
|
714
|
+
The tables to check.
|
|
715
|
+
|
|
716
|
+
Returns
|
|
717
|
+
-------
|
|
718
|
+
`list` [ `Table` ]
|
|
719
|
+
The tables if they are unique.
|
|
720
|
+
|
|
721
|
+
Raises
|
|
722
|
+
------
|
|
723
|
+
ValueError
|
|
724
|
+
If table names are not unique.
|
|
725
|
+
"""
|
|
529
726
|
if len(tables) != len(set(table.name for table in tables)):
|
|
530
727
|
raise ValueError("Table names must be unique")
|
|
531
728
|
return tables
|
|
532
729
|
|
|
533
730
|
@model_validator(mode="after")
|
|
534
731
|
def check_tap_table_indexes(self, info: ValidationInfo) -> Schema:
|
|
535
|
-
"""Check that the TAP table indexes are unique.
|
|
732
|
+
"""Check that the TAP table indexes are unique.
|
|
733
|
+
|
|
734
|
+
Parameters
|
|
735
|
+
----------
|
|
736
|
+
info
|
|
737
|
+
The validation context used to determine if the check is enabled.
|
|
738
|
+
|
|
739
|
+
Returns
|
|
740
|
+
-------
|
|
741
|
+
`Schema`
|
|
742
|
+
The schema being validated.
|
|
743
|
+
"""
|
|
536
744
|
context = info.context
|
|
537
745
|
if not context or not context.get("check_tap_table_indexes", False):
|
|
538
746
|
return self
|
|
@@ -548,9 +756,15 @@ class Schema(BaseObject):
|
|
|
548
756
|
def _create_id_map(self: Schema) -> Schema:
|
|
549
757
|
"""Create a map of IDs to objects.
|
|
550
758
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
759
|
+
Raises
|
|
760
|
+
------
|
|
761
|
+
ValueError
|
|
762
|
+
If duplicate IDs are found in the schema.
|
|
763
|
+
|
|
764
|
+
Notes
|
|
765
|
+
-----
|
|
766
|
+
This is called automatically by the `model_post_init` method. If the
|
|
767
|
+
ID map is already populated, this method will return immediately.
|
|
554
768
|
"""
|
|
555
769
|
if len(self.id_map):
|
|
556
770
|
logger.debug("Ignoring call to create_id_map() - ID map was already populated")
|
|
@@ -564,15 +778,46 @@ class Schema(BaseObject):
|
|
|
564
778
|
return self
|
|
565
779
|
|
|
566
780
|
def model_post_init(self, ctx: Any) -> None:
|
|
567
|
-
"""Post-initialization hook for the model.
|
|
781
|
+
"""Post-initialization hook for the model.
|
|
782
|
+
|
|
783
|
+
Parameters
|
|
784
|
+
----------
|
|
785
|
+
ctx
|
|
786
|
+
The context object which was passed to the model.
|
|
787
|
+
|
|
788
|
+
Notes
|
|
789
|
+
-----
|
|
790
|
+
This method is called automatically by Pydantic after the model is
|
|
791
|
+
initialized. It is used to create the ID map for the schema.
|
|
792
|
+
|
|
793
|
+
The ``ctx`` argument has the type `Any` because this is the function
|
|
794
|
+
signature in Pydantic itself.
|
|
795
|
+
"""
|
|
568
796
|
self._create_id_map()
|
|
569
797
|
|
|
570
798
|
def __getitem__(self, id: str) -> BaseObject:
|
|
571
|
-
"""Get an object by its ID.
|
|
799
|
+
"""Get an object by its ID.
|
|
800
|
+
|
|
801
|
+
Parameters
|
|
802
|
+
----------
|
|
803
|
+
id
|
|
804
|
+
The ID of the object to get.
|
|
805
|
+
|
|
806
|
+
Raises
|
|
807
|
+
------
|
|
808
|
+
KeyError
|
|
809
|
+
If the object with the given ID is not found in the schema.
|
|
810
|
+
"""
|
|
572
811
|
if id not in self:
|
|
573
812
|
raise KeyError(f"Object with ID '{id}' not found in schema")
|
|
574
813
|
return self.id_map[id]
|
|
575
814
|
|
|
576
815
|
def __contains__(self, id: str) -> bool:
|
|
577
|
-
"""Check if an object with the given ID is in the schema.
|
|
816
|
+
"""Check if an object with the given ID is in the schema.
|
|
817
|
+
|
|
818
|
+
Parameters
|
|
819
|
+
----------
|
|
820
|
+
id
|
|
821
|
+
The ID of the object to check.
|
|
822
|
+
"""
|
|
578
823
|
return id in self.id_map
|