lsst-felis 27.2024.2500__py3-none-any.whl → 27.2024.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
@@ -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
- """Define a type for a description string, which must be three or more
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 class for all Felis objects."""
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
- """The name of the database object.
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
- """The unique identifier of the database object.
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
- """A description of the database object."""
88
+ """Description of the database object."""
91
89
 
92
90
  votable_utype: str | None = Field(None, alias="votable:utype")
93
- """The VOTable utype (usage-specific or unique type) of the object."""
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
- """A column in a table."""
136
+ """Column model."""
128
137
 
129
138
  datatype: DataType
130
- """The datatype of the column."""
139
+ """Datatype of the column."""
131
140
 
132
141
  length: int | None = Field(None, gt=0)
133
- """The length of the column."""
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
- """The default value of the column."""
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
- """The MySQL datatype of the column."""
154
+ """MySQL datatype override on the column."""
146
155
 
147
156
  postgresql_datatype: str | None = Field(None, alias="postgresql:datatype")
148
- """The PostgreSQL datatype of the column."""
157
+ """PostgreSQL datatype override on the column."""
149
158
 
150
159
  ivoa_ucd: str | None = Field(None, alias="ivoa:ucd")
151
- """The IVOA UCD of the column."""
160
+ """IVOA UCD of the column."""
152
161
 
153
162
  fits_tunit: str | None = Field(None, alias="fits:tunit")
154
- """The FITS TUNIT of the column."""
163
+ """FITS TUNIT of the column."""
155
164
 
156
165
  ivoa_unit: str | None = Field(None, alias="ivoa:unit")
157
- """The IVOA unit of the column."""
166
+ """IVOA unit of the column."""
158
167
 
159
168
  tap_column_index: int | None = Field(None, alias="tap:column_index")
160
- """The TAP_SCHEMA column index of the column."""
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; can be either 0 or 1.
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
- """The VOTable arraysize of the column."""
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
- """The VOTable xtype (extended type) of the column."""
182
+ """VOTable xtype (extended type) of the column."""
175
183
 
176
184
  votable_datatype: str | None = Field(None, alias="votable:datatype")
177
- """The VOTable datatype of the column."""
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 units are valid."""
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
- """A database table constraint."""
368
+ """Table constraint model."""
299
369
 
300
370
  deferrable: bool = False
301
- """If `True` then this constraint will be declared as deferrable."""
371
+ """Whether this constraint will be declared as deferrable."""
302
372
 
303
373
  initially: str | None = None
304
- """Value for ``INITIALLY`` clause, only used if ``deferrable`` is True."""
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
- """The type of the constraint."""
381
+ """Type of the constraint."""
311
382
 
312
383
 
313
384
  class CheckConstraint(Constraint):
314
- """A check constraint on a table."""
385
+ """Table check constraint model."""
315
386
 
316
387
  expression: str
317
- """The expression for the check constraint."""
388
+ """Expression for the check constraint."""
318
389
 
319
390
 
320
391
  class UniqueConstraint(Constraint):
321
- """A unique constraint on a table."""
392
+ """Table unique constraint model."""
322
393
 
323
394
  columns: list[str]
324
- """The columns in the unique constraint."""
395
+ """Columns in the unique constraint."""
325
396
 
326
397
 
327
398
  class Index(BaseObject):
328
- """A database table index.
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
- """The columns in the index."""
405
+ """Columns in the index."""
335
406
 
336
407
  expressions: list[str] | None = None
337
- """The expressions in the index."""
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
- """A foreign key constraint on a table.
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
- These will be reflected in the TAP_SCHEMA keys and key_columns data.
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
- """A database table."""
458
+ """Table model."""
365
459
 
366
460
  columns: Sequence[Column]
367
- """The columns in the table."""
461
+ """Columns in the table."""
368
462
 
369
463
  constraints: list[Constraint] = Field(default_factory=list)
370
- """The constraints on the table."""
464
+ """Constraints on the table."""
371
465
 
372
466
  indexes: list[Index] = Field(default_factory=list)
373
- """The indexes on the table."""
467
+ """Indexes on the table."""
374
468
 
375
469
  primary_key: str | list[str] | None = Field(None, alias="primaryKey")
376
- """The primary key of the table."""
470
+ """Primary key of the table."""
377
471
 
378
472
  tap_table_index: int | None = Field(None, alias="tap:table_index")
379
- """The IVOA TAP_SCHEMA table index of the table."""
473
+ """IVOA TAP_SCHEMA table index of the table."""
380
474
 
381
475
  mysql_engine: str | None = Field(None, alias="mysql:engine")
382
- """The mysql engine to use for the table.
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
- """The mysql charset to use for the table.
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 constraints from the ``constraints`` field."""
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
- """The version of the schema."""
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
- """Visitor to build a Schema object's map of IDs to objects.
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 `duplicates` attribute. The presence of duplicates will
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 thrown
465
- by the `model_validator` method if any duplicates are found in the schema.
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 object that was added during initialization.
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
- """The database schema containing the tables."""
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
- This method should not be called by users. It is called automatically
552
- by the ``model_post_init()`` method. If the ID map is already
553
- populated, this method will return immediately.
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