lsst-felis 26.2024.1200__tar.gz → 26.2024.1500__tar.gz

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.

Files changed (40) hide show
  1. {lsst-felis-26.2024.1200/python/lsst_felis.egg-info → lsst-felis-26.2024.1500}/PKG-INFO +2 -1
  2. {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1500}/pyproject.toml +12 -1
  3. {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1500}/python/felis/cli.py +62 -44
  4. {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1500}/python/felis/datamodel.py +14 -5
  5. lsst-felis-26.2024.1500/python/felis/db/_variants.py +94 -0
  6. lsst-felis-26.2024.1500/python/felis/metadata.py +504 -0
  7. lsst-felis-26.2024.1500/python/felis/version.py +2 -0
  8. {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1500/python/lsst_felis.egg-info}/PKG-INFO +2 -1
  9. {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1500}/python/lsst_felis.egg-info/SOURCES.txt +3 -2
  10. {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1500}/python/lsst_felis.egg-info/requires.txt +1 -0
  11. {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1500}/tests/test_cli.py +4 -4
  12. {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1500}/tests/test_datamodel.py +2 -2
  13. lsst-felis-26.2024.1500/tests/test_metadata.py +193 -0
  14. lsst-felis-26.2024.1200/python/felis/sql.py +0 -275
  15. lsst-felis-26.2024.1200/python/felis/version.py +0 -2
  16. lsst-felis-26.2024.1200/tests/test_sql.py +0 -190
  17. {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1500}/COPYRIGHT +0 -0
  18. {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1500}/LICENSE +0 -0
  19. {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1500}/README.rst +0 -0
  20. {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1500}/python/felis/__init__.py +0 -0
  21. {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1500}/python/felis/check.py +0 -0
  22. {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1500}/python/felis/db/__init__.py +0 -0
  23. {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1500}/python/felis/db/sqltypes.py +0 -0
  24. {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1500}/python/felis/py.typed +0 -0
  25. {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1500}/python/felis/simple.py +0 -0
  26. {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1500}/python/felis/tap.py +0 -0
  27. {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1500}/python/felis/types.py +0 -0
  28. {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1500}/python/felis/utils.py +0 -0
  29. {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1500}/python/felis/validation.py +0 -0
  30. {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1500}/python/felis/visitor.py +0 -0
  31. {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1500}/python/lsst_felis.egg-info/dependency_links.txt +0 -0
  32. {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1500}/python/lsst_felis.egg-info/entry_points.txt +0 -0
  33. {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1500}/python/lsst_felis.egg-info/top_level.txt +0 -0
  34. {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1500}/python/lsst_felis.egg-info/zip-safe +0 -0
  35. {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1500}/setup.cfg +0 -0
  36. {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1500}/tests/test_check.py +0 -0
  37. {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1500}/tests/test_simple.py +0 -0
  38. {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1500}/tests/test_tap.py +0 -0
  39. {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1500}/tests/test_utils.py +0 -0
  40. {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1500}/tests/test_validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lsst-felis
3
- Version: 26.2024.1200
3
+ Version: 26.2024.1500
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+)
@@ -23,5 +23,6 @@ Requires-Dist: click>=7
23
23
  Requires-Dist: pyyaml>=6
24
24
  Requires-Dist: pyld>=2
25
25
  Requires-Dist: pydantic<3,>=2
26
+ Requires-Dist: lsst-utils
26
27
  Provides-Extra: test
27
28
  Requires-Dist: pytest>=3.2; extra == "test"
@@ -26,7 +26,8 @@ dependencies = [
26
26
  "click >= 7",
27
27
  "pyyaml >= 6",
28
28
  "pyld >= 2",
29
- "pydantic >= 2, < 3"
29
+ "pydantic >= 2, < 3",
30
+ "lsst-utils"
30
31
  ]
31
32
  requires-python = ">=3.11.0"
32
33
  dynamic = ["version"]
@@ -147,3 +148,13 @@ target-version = "py311"
147
148
  extend-select = [
148
149
  "RUF100", # Warn about unused noqa
149
150
  ]
151
+
152
+ [tool.pydeps]
153
+ max_bacon = 2
154
+ no_show = true
155
+ verbose = 0
156
+ pylib = false
157
+ format = "png"
158
+ exclude = [
159
+ "sqlalchemy"
160
+ ]
@@ -26,7 +26,7 @@ import json
26
26
  import logging
27
27
  import sys
28
28
  from collections.abc import Iterable, Mapping, MutableMapping
29
- from typing import Any
29
+ from typing import IO, Any
30
30
 
31
31
  import click
32
32
  import yaml
@@ -38,7 +38,7 @@ from sqlalchemy.engine.mock import MockConnection
38
38
  from . import DEFAULT_CONTEXT, DEFAULT_FRAME, __version__
39
39
  from .check import CheckingVisitor
40
40
  from .datamodel import Schema
41
- from .sql import SQLVisitor
41
+ from .metadata import DatabaseContext, InsertDump, MetaDataBuilder
42
42
  from .tap import Tap11Base, TapLoadingVisitor, init_tables
43
43
  from .utils import ReorderingVisitor
44
44
  from .validation import get_schema
@@ -71,27 +71,70 @@ def cli(log_level: str, log_file: str | None) -> None:
71
71
  logging.basicConfig(level=log_level)
72
72
 
73
73
 
74
- @cli.command("create-all")
75
- @click.option("--engine-url", envvar="ENGINE_URL", help="SQLAlchemy Engine URL")
76
- @click.option("--schema-name", help="Alternate Schema Name for Felis File")
77
- @click.option("--dry-run", is_flag=True, help="Dry Run Only. Prints out the DDL that would be executed")
74
+ @cli.command("create")
75
+ @click.option("--engine-url", envvar="ENGINE_URL", help="SQLAlchemy Engine URL", default="sqlite://")
76
+ @click.option("--schema-name", help="Alternate schema name to override Felis file")
77
+ @click.option(
78
+ "--create-if-not-exists", is_flag=True, help="Create the schema in the database if it does not exist"
79
+ )
80
+ @click.option("--drop-if-exists", is_flag=True, help="Drop schema if it already exists in the database")
81
+ @click.option("--echo", is_flag=True, help="Echo database commands as they are executed")
82
+ @click.option("--dry-run", is_flag=True, help="Dry run only to print out commands instead of executing")
83
+ @click.option(
84
+ "--output-file", "-o", type=click.File(mode="w"), help="Write SQL commands to a file instead of executing"
85
+ )
78
86
  @click.argument("file", type=click.File())
79
- def create_all(engine_url: str, schema_name: str, dry_run: bool, file: io.TextIOBase) -> None:
80
- """Create schema objects from the Felis FILE."""
81
- schema_obj = yaml.load(file, Loader=yaml.SafeLoader)
82
- visitor = SQLVisitor(schema_name=schema_name)
83
- schema = visitor.visit_schema(schema_obj)
84
-
85
- metadata = schema.metadata
87
+ def create(
88
+ engine_url: str,
89
+ schema_name: str | None,
90
+ create_if_not_exists: bool,
91
+ drop_if_exists: bool,
92
+ echo: bool,
93
+ dry_run: bool,
94
+ output_file: IO[str] | None,
95
+ file: IO,
96
+ ) -> None:
97
+ """Create database objects from the Felis file."""
98
+ yaml_data = yaml.safe_load(file)
99
+ schema = Schema.model_validate(yaml_data)
100
+ url_obj = make_url(engine_url)
101
+ if schema_name:
102
+ logger.info(f"Overriding schema name with: {schema_name}")
103
+ schema.name = schema_name
104
+ elif url_obj.drivername == "sqlite":
105
+ logger.info("Overriding schema name for sqlite with: main")
106
+ schema.name = "main"
107
+ if not url_obj.host and not url_obj.drivername == "sqlite":
108
+ dry_run = True
109
+ logger.info("Forcing dry run for non-sqlite engine URL with no host")
110
+
111
+ builder = MetaDataBuilder(schema)
112
+ builder.build()
113
+ metadata = builder.metadata
114
+ logger.debug(f"Created metadata with schema name: {metadata.schema}")
86
115
 
87
116
  engine: Engine | MockConnection
88
- if not dry_run:
89
- engine = create_engine(engine_url)
117
+ if not dry_run and not output_file:
118
+ engine = create_engine(engine_url, echo=echo)
90
119
  else:
91
- _insert_dump = InsertDump()
92
- engine = create_mock_engine(make_url(engine_url), executor=_insert_dump.dump)
93
- _insert_dump.dialect = engine.dialect
94
- metadata.create_all(engine)
120
+ if dry_run:
121
+ logger.info("Dry run will be executed")
122
+ engine = DatabaseContext.create_mock_engine(url_obj, output_file)
123
+ if output_file:
124
+ logger.info("Writing SQL output to: " + output_file.name)
125
+
126
+ context = DatabaseContext(metadata, engine)
127
+
128
+ if drop_if_exists:
129
+ logger.debug("Dropping schema if it exists")
130
+ context.drop_if_exists()
131
+ create_if_not_exists = True # If schema is dropped, it needs to be recreated.
132
+
133
+ if create_if_not_exists:
134
+ logger.debug("Creating schema if not exists")
135
+ context.create_if_not_exists()
136
+
137
+ context.create_all()
95
138
 
96
139
 
97
140
  @cli.command("init-tap")
@@ -404,30 +447,5 @@ def _normalize(schema_obj: Mapping[str, Any], embed: str = "@last") -> MutableMa
404
447
  return compacted
405
448
 
406
449
 
407
- class InsertDump:
408
- """An Insert Dumper for SQL statements."""
409
-
410
- dialect: Any = None
411
-
412
- def dump(self, sql: Any, *multiparams: Any, **params: Any) -> None:
413
- compiled = sql.compile(dialect=self.dialect)
414
- sql_str = str(compiled) + ";"
415
- params_list = [compiled.params]
416
- for params in params_list:
417
- if not params:
418
- print(sql_str)
419
- continue
420
- new_params = {}
421
- for key, value in params.items():
422
- if isinstance(value, str):
423
- new_params[key] = f"'{value}'"
424
- elif value is None:
425
- new_params[key] = "null"
426
- else:
427
- new_params[key] = value
428
-
429
- print(sql_str % new_params)
430
-
431
-
432
450
  if __name__ == "__main__":
433
451
  cli()
@@ -31,7 +31,6 @@ from astropy.io.votable import ucd # type: ignore
31
31
  from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
32
32
 
33
33
  logger = logging.getLogger(__name__)
34
- # logger.setLevel(logging.DEBUG)
35
34
 
36
35
  __all__ = (
37
36
  "BaseObject",
@@ -50,7 +49,6 @@ __all__ = (
50
49
  CONFIG = ConfigDict(
51
50
  populate_by_name=True, # Populate attributes by name.
52
51
  extra="forbid", # Do not allow extra fields.
53
- use_enum_values=True, # Use enum values instead of names.
54
52
  validate_assignment=True, # Validate assignments after model is created.
55
53
  str_strip_whitespace=True, # Strip whitespace from string fields.
56
54
  )
@@ -130,8 +128,13 @@ class Column(BaseObject):
130
128
  length: int | None = None
131
129
  """The length of the column."""
132
130
 
133
- nullable: bool = True
134
- """Whether the column can be `NULL`."""
131
+ nullable: bool | None = None
132
+ """Whether the column can be ``NULL``.
133
+
134
+ If `None`, this value was not set explicitly in the YAML data. In this
135
+ case, it will be set to `False` for columns with numeric types and `True`
136
+ otherwise.
137
+ """
135
138
 
136
139
  value: Any = None
137
140
  """The default value of the column."""
@@ -142,6 +145,9 @@ class Column(BaseObject):
142
145
  mysql_datatype: str | None = Field(None, alias="mysql:datatype")
143
146
  """The MySQL datatype of the column."""
144
147
 
148
+ postgresql_datatype: str | None = Field(None, alias="postgresql:datatype")
149
+ """The PostgreSQL datatype of the column."""
150
+
145
151
  ivoa_ucd: str | None = Field(None, alias="ivoa:ucd")
146
152
  """The IVOA UCD of the column."""
147
153
 
@@ -280,7 +286,7 @@ class Table(BaseObject):
280
286
  indexes: list[Index] = Field(default_factory=list)
281
287
  """The indexes on the table."""
282
288
 
283
- primaryKey: str | list[str] | None = None
289
+ primary_key: str | list[str] | None = Field(None, alias="primaryKey")
284
290
  """The primary key of the table."""
285
291
 
286
292
  tap_table_index: int | None = Field(None, alias="tap:table_index")
@@ -427,6 +433,9 @@ class Schema(BaseObject):
427
433
  @model_validator(mode="after")
428
434
  def create_id_map(self: Schema) -> Schema:
429
435
  """Create a map of IDs to objects."""
436
+ if len(self.id_map):
437
+ logger.debug("ID map was already populated")
438
+ return self
430
439
  visitor: SchemaIdVisitor = SchemaIdVisitor()
431
440
  visitor.visit_schema(self)
432
441
  logger.debug(f"ID map contains {len(self.id_map.keys())} objects")
@@ -0,0 +1,94 @@
1
+ # This file is part of felis.
2
+ #
3
+ # Developed for the LSST Data Management System.
4
+ # This product includes software developed by the LSST Project
5
+ # (https://www.lsst.org).
6
+ # See the COPYRIGHT file at the top-level directory of this distribution
7
+ # for details of code ownership.
8
+ #
9
+ # This program is free software: you can redistribute it and/or modify
10
+ # it under the terms of the GNU General Public License as published by
11
+ # the Free Software Foundation, either version 3 of the License, or
12
+ # (at your option) any later version.
13
+ #
14
+ # This program is distributed in the hope that it will be useful,
15
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
+ # GNU General Public License for more details.
18
+ #
19
+ # You should have received a copy of the GNU General Public License
20
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
21
+
22
+ import re
23
+ from typing import Any
24
+
25
+ from sqlalchemy import types
26
+ from sqlalchemy.dialects import mysql, oracle, postgresql, sqlite
27
+ from sqlalchemy.types import TypeEngine
28
+
29
+ from ..datamodel import Column
30
+
31
+ MYSQL = "mysql"
32
+ ORACLE = "oracle"
33
+ POSTGRES = "postgresql"
34
+ SQLITE = "sqlite"
35
+
36
+ TABLE_OPTS = {
37
+ "mysql:engine": "mysql_engine",
38
+ "mysql:charset": "mysql_charset",
39
+ "oracle:compress": "oracle_compress",
40
+ }
41
+
42
+ COLUMN_VARIANT_OVERRIDE = {
43
+ "mysql:datatype": "mysql",
44
+ "oracle:datatype": "oracle",
45
+ "postgresql:datatype": "postgresql",
46
+ "sqlite:datatype": "sqlite",
47
+ }
48
+
49
+ DIALECT_MODULES = {MYSQL: mysql, ORACLE: oracle, SQLITE: sqlite, POSTGRES: postgresql}
50
+
51
+ _length_regex = re.compile(r"\((\d+)\)")
52
+ """A regular expression that is looking for numbers within parentheses."""
53
+
54
+
55
+ def process_variant_override(dialect_name: str, variant_override_str: str) -> types.TypeEngine:
56
+ """Return variant type for given dialect."""
57
+ dialect = DIALECT_MODULES[dialect_name]
58
+ variant_type_name = variant_override_str.split("(")[0]
59
+
60
+ # Process Variant Type
61
+ if variant_type_name not in dir(dialect):
62
+ raise ValueError(f"Type {variant_type_name} not found in dialect {dialect_name}")
63
+ variant_type = getattr(dialect, variant_type_name)
64
+ length_params = []
65
+ if match := _length_regex.search(variant_override_str):
66
+ length_params.extend([int(i) for i in match.group(1).split(",")])
67
+ return variant_type(*length_params)
68
+
69
+
70
+ def make_variant_dict(column_obj: Column) -> dict[str, TypeEngine[Any]]:
71
+ """Handle variant overrides for a `felis.datamodel.Column`.
72
+
73
+ This function will return a dictionary of `str` to
74
+ `sqlalchemy.types.TypeEngine` containing variant datatype information
75
+ (e.g., for mysql, postgresql, etc).
76
+
77
+ Parameters
78
+ ----------
79
+ column_obj : `felis.datamodel.Column`
80
+ The column object from which to build the variant dictionary.
81
+
82
+ Returns
83
+ -------
84
+ variant_dict : `dict`
85
+ The dictionary of `str` to `sqlalchemy.types.TypeEngine` containing
86
+ variant datatype information (e.g., for mysql, postgresql, etc).
87
+ """
88
+ variant_dict = {}
89
+ for field_name, value in iter(column_obj):
90
+ if field_name in COLUMN_VARIANT_OVERRIDE:
91
+ dialect = COLUMN_VARIANT_OVERRIDE[field_name]
92
+ variant: TypeEngine = process_variant_override(dialect, value)
93
+ variant_dict[dialect] = variant
94
+ return variant_dict