lsst-felis 27.2024.3300__py3-none-any.whl → 27.2024.3500__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
@@ -62,8 +62,16 @@ loglevel_choices = ["CRITICAL", "FATAL", "ERROR", "WARNING", "INFO", "DEBUG"]
62
62
  envvar="FELIS_LOGFILE",
63
63
  help="Felis log file path",
64
64
  )
65
- def cli(log_level: str, log_file: str | None) -> None:
65
+ @click.option(
66
+ "--id-generation", is_flag=True, help="Generate IDs for all objects that do not have them", default=False
67
+ )
68
+ @click.pass_context
69
+ def cli(ctx: click.Context, log_level: str, log_file: str | None, id_generation: bool) -> None:
66
70
  """Felis command line tools"""
71
+ ctx.ensure_object(dict)
72
+ ctx.obj["id_generation"] = id_generation
73
+ if ctx.obj["id_generation"]:
74
+ logger.info("ID generation is enabled")
67
75
  if log_file:
68
76
  logging.basicConfig(filename=log_file, level=log_level)
69
77
  else:
@@ -88,7 +96,9 @@ def cli(log_level: str, log_file: str | None) -> None:
88
96
  )
89
97
  @click.option("--ignore-constraints", is_flag=True, help="Ignore constraints when creating tables")
90
98
  @click.argument("file", type=click.File())
99
+ @click.pass_context
91
100
  def create(
101
+ ctx: click.Context,
92
102
  engine_url: str,
93
103
  schema_name: str | None,
94
104
  initialize: bool,
@@ -124,7 +134,7 @@ def create(
124
134
  """
125
135
  try:
126
136
  yaml_data = yaml.safe_load(file)
127
- schema = Schema.model_validate(yaml_data)
137
+ schema = Schema.model_validate(yaml_data, context={"id_generation": ctx.obj["id_generation"]})
128
138
  url = make_url(engine_url)
129
139
  if schema_name:
130
140
  logger.info(f"Overriding schema name with: {schema_name}")
@@ -355,7 +365,9 @@ def load_tap(
355
365
  default=False,
356
366
  )
357
367
  @click.argument("files", nargs=-1, type=click.File())
368
+ @click.pass_context
358
369
  def validate(
370
+ ctx: click.Context,
359
371
  check_description: bool,
360
372
  check_redundant_datatypes: bool,
361
373
  check_tap_table_indexes: bool,
@@ -402,6 +414,7 @@ def validate(
402
414
  "check_redundant_datatypes": check_redundant_datatypes,
403
415
  "check_tap_table_indexes": check_tap_table_indexes,
404
416
  "check_tap_principal": check_tap_principal,
417
+ "id_generation": ctx.obj["id_generation"],
405
418
  },
406
419
  )
407
420
  except ValidationError as e:
felis/datamodel.py CHANGED
@@ -24,9 +24,9 @@
24
24
  from __future__ import annotations
25
25
 
26
26
  import logging
27
- from collections.abc import Mapping, Sequence
27
+ from collections.abc import Sequence
28
28
  from enum import StrEnum, auto
29
- from typing import Annotated, Any, TypeAlias
29
+ from typing import Annotated, Any, Literal, TypeAlias, Union
30
30
 
31
31
  from astropy import units as units # type: ignore
32
32
  from astropy.io.votable import ucd # type: ignore
@@ -390,20 +390,31 @@ class Constraint(BaseObject):
390
390
  deferrable: bool = False
391
391
  """Whether this constraint will be declared as deferrable."""
392
392
 
393
- initially: str | None = None
393
+ initially: Literal["IMMEDIATE", "DEFERRED"] | None = None
394
394
  """Value for ``INITIALLY`` clause; only used if `deferrable` is
395
395
  `True`."""
396
396
 
397
- annotations: Mapping[str, Any] = Field(default_factory=dict)
398
- """Additional annotations for this constraint."""
397
+ @model_validator(mode="after")
398
+ def check_deferrable(self) -> Constraint:
399
+ """Check that the ``INITIALLY`` clause is only used if `deferrable` is
400
+ `True`.
399
401
 
400
- type: str | None = Field(None, alias="@type")
401
- """Type of the constraint."""
402
+ Returns
403
+ -------
404
+ `Constraint`
405
+ The constraint being validated.
406
+ """
407
+ if self.initially is not None and not self.deferrable:
408
+ raise ValueError("INITIALLY clause can only be used if deferrable is True")
409
+ return self
402
410
 
403
411
 
404
412
  class CheckConstraint(Constraint):
405
413
  """Table check constraint model."""
406
414
 
415
+ type: Literal["Check"] = Field("Check", alias="@type")
416
+ """Type of the constraint."""
417
+
407
418
  expression: str
408
419
  """Expression for the check constraint."""
409
420
 
@@ -411,10 +422,35 @@ class CheckConstraint(Constraint):
411
422
  class UniqueConstraint(Constraint):
412
423
  """Table unique constraint model."""
413
424
 
425
+ type: Literal["Unique"] = Field("Unique", alias="@type")
426
+ """Type of the constraint."""
427
+
414
428
  columns: list[str]
415
429
  """Columns in the unique constraint."""
416
430
 
417
431
 
432
+ class ForeignKeyConstraint(Constraint):
433
+ """Table foreign key constraint model.
434
+
435
+ This constraint is used to define a foreign key relationship between two
436
+ tables in the schema.
437
+
438
+ Notes
439
+ -----
440
+ These relationships will be reflected in the TAP_SCHEMA ``keys`` and
441
+ ``key_columns`` data.
442
+ """
443
+
444
+ type: Literal["ForeignKey"] = Field("ForeignKey", alias="@type")
445
+ """Type of the constraint."""
446
+
447
+ columns: list[str]
448
+ """The columns comprising the foreign key."""
449
+
450
+ referenced_columns: list[str] = Field(alias="referencedColumns")
451
+ """The columns referenced by the foreign key."""
452
+
453
+
418
454
  class Index(BaseObject):
419
455
  """Table index model.
420
456
 
@@ -455,23 +491,10 @@ class Index(BaseObject):
455
491
  return values
456
492
 
457
493
 
458
- class ForeignKeyConstraint(Constraint):
459
- """Table foreign key constraint model.
460
-
461
- This constraint is used to define a foreign key relationship between two
462
- tables in the schema.
463
-
464
- Notes
465
- -----
466
- These relationships will be reflected in the TAP_SCHEMA ``keys`` and
467
- ``key_columns`` data.
468
- """
469
-
470
- columns: list[str]
471
- """The columns comprising the foreign key."""
472
-
473
- referenced_columns: list[str] = Field(alias="referencedColumns")
474
- """The columns referenced by the foreign key."""
494
+ _ConstraintType = Annotated[
495
+ Union[CheckConstraint, ForeignKeyConstraint, UniqueConstraint], Field(discriminator="type")
496
+ ]
497
+ """Type alias for a constraint type."""
475
498
 
476
499
 
477
500
  class Table(BaseObject):
@@ -480,7 +503,7 @@ class Table(BaseObject):
480
503
  columns: Sequence[Column]
481
504
  """Columns in the table."""
482
505
 
483
- constraints: list[Constraint] = Field(default_factory=list)
506
+ constraints: list[_ConstraintType] = Field(default_factory=list, discriminator="type")
484
507
  """Constraints on the table."""
485
508
 
486
509
  indexes: list[Index] = Field(default_factory=list)
@@ -492,43 +515,12 @@ class Table(BaseObject):
492
515
  tap_table_index: int | None = Field(None, alias="tap:table_index")
493
516
  """IVOA TAP_SCHEMA table index of the table."""
494
517
 
495
- mysql_engine: str | None = Field(None, alias="mysql:engine")
518
+ mysql_engine: str | None = Field("MyISAM", alias="mysql:engine")
496
519
  """MySQL engine to use for the table."""
497
520
 
498
521
  mysql_charset: str | None = Field(None, alias="mysql:charset")
499
522
  """MySQL charset to use for the table."""
500
523
 
501
- @model_validator(mode="before")
502
- @classmethod
503
- def create_constraints(cls, values: dict[str, Any]) -> dict[str, Any]:
504
- """Create specific constraint types from the data in the
505
- ``constraints`` field of a table.
506
-
507
- Parameters
508
- ----------
509
- values
510
- The values of the table containing the constraint data.
511
-
512
- Returns
513
- -------
514
- `dict` [ `str`, `Any` ]
515
- The values of the table with the constraints converted to their
516
- respective types.
517
- """
518
- if "constraints" in values:
519
- new_constraints: list[Constraint] = []
520
- for item in values["constraints"]:
521
- if item["@type"] == "ForeignKey":
522
- new_constraints.append(ForeignKeyConstraint(**item))
523
- elif item["@type"] == "Unique":
524
- new_constraints.append(UniqueConstraint(**item))
525
- elif item["@type"] == "Check":
526
- new_constraints.append(CheckConstraint(**item))
527
- else:
528
- raise ValueError(f"Unknown constraint type: {item['@type']}")
529
- values["constraints"] = new_constraints
530
- return values
531
-
532
524
  @field_validator("columns", mode="after")
533
525
  @classmethod
534
526
  def check_unique_column_names(cls, columns: list[Column]) -> list[Column]:
@@ -723,6 +715,55 @@ class Schema(BaseObject):
723
715
  id_map: dict[str, Any] = Field(default_factory=dict, exclude=True)
724
716
  """Map of IDs to objects."""
725
717
 
718
+ @model_validator(mode="before")
719
+ @classmethod
720
+ def generate_ids(cls, values: dict[str, Any], info: ValidationInfo) -> dict[str, Any]:
721
+ """Generate IDs for objects that do not have them.
722
+
723
+ Parameters
724
+ ----------
725
+ values
726
+ The values of the schema.
727
+ info
728
+ Validation context used to determine if ID generation is enabled.
729
+
730
+ Returns
731
+ -------
732
+ `dict` [ `str`, `Any` ]
733
+ The values of the schema with generated IDs.
734
+ """
735
+ context = info.context
736
+ if not context or not context.get("id_generation", False):
737
+ logger.debug("Skipping ID generation")
738
+ return values
739
+ schema_name = values["name"]
740
+ if "@id" not in values:
741
+ values["@id"] = f"#{schema_name}"
742
+ logger.debug(f"Generated ID '{values['@id']}' for schema '{schema_name}'")
743
+ if "tables" in values:
744
+ for table in values["tables"]:
745
+ if "@id" not in table:
746
+ table["@id"] = f"#{table['name']}"
747
+ logger.debug(f"Generated ID '{table['@id']}' for table '{table['name']}'")
748
+ if "columns" in table:
749
+ for column in table["columns"]:
750
+ if "@id" not in column:
751
+ column["@id"] = f"#{table['name']}.{column['name']}"
752
+ logger.debug(f"Generated ID '{column['@id']}' for column '{column['name']}'")
753
+ if "constraints" in table:
754
+ for constraint in table["constraints"]:
755
+ if "@id" not in constraint:
756
+ constraint["@id"] = f"#{constraint['name']}"
757
+ logger.debug(
758
+ f"Generated ID '{constraint['@id']}' for constraint '{constraint['name']}'"
759
+ )
760
+ if "indexes" in table:
761
+ for index in table["indexes"]:
762
+ if "@id" not in index:
763
+ index["@id"] = f"#{index['name']}"
764
+ logger.debug(f"Generated ID '{index['@id']}' for index '{index['name']}'")
765
+ return values
766
+
726
767
  @field_validator("tables", mode="after")
727
768
  @classmethod
728
769
  def check_unique_table_names(cls, tables: list[Table]) -> list[Table]:
@@ -773,6 +814,66 @@ class Schema(BaseObject):
773
814
  table_indicies.add(table_index)
774
815
  return self
775
816
 
817
+ @model_validator(mode="after")
818
+ def check_unique_constraint_names(self: Schema) -> Schema:
819
+ """Check for duplicate constraint names in the schema.
820
+
821
+ Returns
822
+ -------
823
+ `Schema`
824
+ The schema being validated.
825
+
826
+ Raises
827
+ ------
828
+ ValueError
829
+ Raised if duplicate constraint names are found in the schema.
830
+ """
831
+ constraint_names = set()
832
+ duplicate_names = []
833
+
834
+ for table in self.tables:
835
+ for constraint in table.constraints:
836
+ constraint_name = constraint.name
837
+ if constraint_name in constraint_names:
838
+ duplicate_names.append(constraint_name)
839
+ else:
840
+ constraint_names.add(constraint_name)
841
+
842
+ if duplicate_names:
843
+ raise ValueError(f"Duplicate constraint names found in schema: {duplicate_names}")
844
+
845
+ return self
846
+
847
+ @model_validator(mode="after")
848
+ def check_unique_index_names(self: Schema) -> Schema:
849
+ """Check for duplicate index names in the schema.
850
+
851
+ Returns
852
+ -------
853
+ `Schema`
854
+ The schema being validated.
855
+
856
+ Raises
857
+ ------
858
+ ValueError
859
+ Raised if duplicate index names are found in the schema.
860
+ """
861
+ index_names = set()
862
+ duplicate_names = []
863
+
864
+ for table in self.tables:
865
+ for index in table.indexes:
866
+ index_name = index.name
867
+ if index_name in index_names:
868
+ duplicate_names.append(index_name)
869
+ else:
870
+ index_names.add(index_name)
871
+
872
+ if duplicate_names:
873
+ raise ValueError(f"Duplicate index names found in schema: {duplicate_names}")
874
+
875
+ return self
876
+
776
877
  def _create_id_map(self: Schema) -> Schema:
777
878
  """Create a map of IDs to objects.
778
879
 
felis/metadata.py CHANGED
@@ -342,7 +342,6 @@ class MetaDataBuilder:
342
342
  "initially": constraint_obj.initially or None,
343
343
  }
344
344
  constraint: Constraint
345
- constraint_type = constraint_obj.type
346
345
 
347
346
  if isinstance(constraint_obj, datamodel.ForeignKeyConstraint):
348
347
  fk_obj: datamodel.ForeignKeyConstraint = constraint_obj
@@ -358,7 +357,7 @@ class MetaDataBuilder:
358
357
  columns = [self._objects[column_id] for column_id in uniq_obj.columns]
359
358
  constraint = UniqueConstraint(*columns, **args)
360
359
  else:
361
- raise ValueError(f"Unknown constraint type: {constraint_type}")
360
+ raise ValueError(f"Unknown constraint type: {type(constraint_obj)}")
362
361
 
363
362
  self._objects[constraint_obj.id] = constraint
364
363
 
felis/tap.py CHANGED
@@ -37,7 +37,7 @@ from sqlalchemy.sql.expression import Insert, insert
37
37
 
38
38
  from felis import datamodel
39
39
 
40
- from .datamodel import Constraint, Index, Schema, Table
40
+ from .datamodel import Constraint, ForeignKeyConstraint, Index, Schema, Table
41
41
  from .types import FelisType
42
42
 
43
43
  __all__ = ["TapLoadingVisitor", "init_tables"]
@@ -480,10 +480,9 @@ class TapLoadingVisitor:
480
480
  A tuple of the SQLAlchemy ORM objects for the TAP_SCHEMA ``key``
481
481
  and ``key_columns`` data.
482
482
  """
483
- constraint_type = constraint_obj.type
484
483
  key = None
485
484
  key_columns = []
486
- if constraint_type == "ForeignKey":
485
+ if isinstance(constraint_obj, ForeignKeyConstraint):
487
486
  constraint_name = constraint_obj.name
488
487
  description = constraint_obj.description
489
488
  utype = constraint_obj.votable_utype
felis/version.py CHANGED
@@ -1,2 +1,2 @@
1
1
  __all__ = ["__version__"]
2
- __version__ = "27.2024.3300"
2
+ __version__ = "27.2024.3500"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lsst-felis
3
- Version: 27.2024.3300
3
+ Version: 27.2024.3500
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,11 +1,11 @@
1
1
  felis/__init__.py,sha256=THmRg3ylB4E73XhFjJX7YlnV_CM3lr_gZO_HqQFzIQ4,937
2
- felis/cli.py,sha256=iXZ3ViMKAC45OHOT5OSPW0aqSxOTG2Oa0qrgbADKD7A,14378
3
- felis/datamodel.py,sha256=f0iHz2N0mXOWNBUPfmXO9bDmALKG1UUJPvFp_oPo4jU,26598
4
- felis/metadata.py,sha256=JgqZeFqGFuuahOKL2jLrh3KDZhjILQJoS2_p3P8lKNE,13739
2
+ felis/cli.py,sha256=0qKk1cGsGcVXJL4fStvWLN5CPHgNwZdS8B2E9Y08wq8,14924
3
+ felis/datamodel.py,sha256=etwLIgzc8RyTVOunkfr7qQJWKqLJ5_HVBJT8jFHzjEY,30174
4
+ felis/metadata.py,sha256=MqNlBoFPMq18Kp0WUfyC8RtlxyBHTL_0SmHanAJgZWk,13698
5
5
  felis/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- felis/tap.py,sha256=pyLHBWzXCw4F1wDbQiyxyJYPQcye4It2yRuVFoQQK5I,21746
6
+ felis/tap.py,sha256=uVmrVnxnVIIdt7MdIv3LhysuEIUJWYvRQLJKvSD03u0,21739
7
7
  felis/types.py,sha256=m80GSGfNHQ3-NzRuTzKOyRXLJboPxdk9kzpp1SO8XdY,5510
8
- felis/version.py,sha256=NiQ2xf9osfwJG1W0J5AFtqbuQIt9vDj9eILHTVVltKg,55
8
+ felis/version.py,sha256=UJEaGxD7ag5N-gxngXVq2kv55Dxj7g0WkaPo_JF-goQ,55
9
9
  felis/db/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  felis/db/dialects.py,sha256=n5La-shu-8fHLIyf8rrazHDyrzATmMCdELtKV_0ymxI,3517
11
11
  felis/db/sqltypes.py,sha256=JJy97U8KzAOg5pFi2xZgSjvU8CXXgrzkvCsmo6FLRG4,11060
@@ -13,11 +13,11 @@ felis/db/utils.py,sha256=I8t9Yui98T8iPyrs0Q-qyjjaxVyX8kQkyizvzv-p4FE,11526
13
13
  felis/db/variants.py,sha256=eahthrbVeV8ZdGamWQccNmWgx6CCscGrU0vQRs5HZK8,5260
14
14
  felis/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
15
  felis/tests/postgresql.py,sha256=B_xk4fLual5-viGDqP20r94okuc0pbSvytRH_L0fvMs,4035
16
- lsst_felis-27.2024.3300.dist-info/COPYRIGHT,sha256=vJAFLFTSF1mhy9eIuA3P6R-3yxTWKQgpig88P-1IzRw,129
17
- lsst_felis-27.2024.3300.dist-info/LICENSE,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
18
- lsst_felis-27.2024.3300.dist-info/METADATA,sha256=zMf5z3224Qn-U88B1dZT6DWJBwEOOHsOMVOKNXj7S28,1288
19
- lsst_felis-27.2024.3300.dist-info/WHEEL,sha256=HiCZjzuy6Dw0hdX5R3LCFPDmFS4BWl8H-8W39XfmgX4,91
20
- lsst_felis-27.2024.3300.dist-info/entry_points.txt,sha256=Gk2XFujA_Gp52VBk45g5kim8TDoMDJFPctsMqiq72EM,40
21
- lsst_felis-27.2024.3300.dist-info/top_level.txt,sha256=F4SvPip3iZRVyISi50CHhwTIAokAhSxjWiVcn4IVWRI,6
22
- lsst_felis-27.2024.3300.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
23
- lsst_felis-27.2024.3300.dist-info/RECORD,,
16
+ lsst_felis-27.2024.3500.dist-info/COPYRIGHT,sha256=vJAFLFTSF1mhy9eIuA3P6R-3yxTWKQgpig88P-1IzRw,129
17
+ lsst_felis-27.2024.3500.dist-info/LICENSE,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
18
+ lsst_felis-27.2024.3500.dist-info/METADATA,sha256=XsAXbdKyRCr6P9j6mRgvRtzw5860yG6Ol3sd0FiD_kM,1288
19
+ lsst_felis-27.2024.3500.dist-info/WHEEL,sha256=UvcQYKBHoFqaQd6LKyqHw9fxEolWLQnlzP0h_LgJAfI,91
20
+ lsst_felis-27.2024.3500.dist-info/entry_points.txt,sha256=Gk2XFujA_Gp52VBk45g5kim8TDoMDJFPctsMqiq72EM,40
21
+ lsst_felis-27.2024.3500.dist-info/top_level.txt,sha256=F4SvPip3iZRVyISi50CHhwTIAokAhSxjWiVcn4IVWRI,6
22
+ lsst_felis-27.2024.3500.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
23
+ lsst_felis-27.2024.3500.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (72.2.0)
2
+ Generator: setuptools (74.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5