lsst-felis 26.2024.1200__tar.gz → 26.2024.1400__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.
- {lsst-felis-26.2024.1200/python/lsst_felis.egg-info → lsst-felis-26.2024.1400}/PKG-INFO +2 -1
- {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1400}/pyproject.toml +12 -1
- {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1400}/python/felis/cli.py +62 -44
- {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1400}/python/felis/datamodel.py +14 -5
- lsst-felis-26.2024.1400/python/felis/db/_variants.py +94 -0
- lsst-felis-26.2024.1400/python/felis/metadata.py +504 -0
- lsst-felis-26.2024.1400/python/felis/version.py +2 -0
- {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1400/python/lsst_felis.egg-info}/PKG-INFO +2 -1
- {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1400}/python/lsst_felis.egg-info/SOURCES.txt +3 -2
- {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1400}/python/lsst_felis.egg-info/requires.txt +1 -0
- {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1400}/tests/test_cli.py +4 -4
- {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1400}/tests/test_datamodel.py +2 -2
- lsst-felis-26.2024.1400/tests/test_metadata.py +193 -0
- lsst-felis-26.2024.1200/python/felis/sql.py +0 -275
- lsst-felis-26.2024.1200/python/felis/version.py +0 -2
- lsst-felis-26.2024.1200/tests/test_sql.py +0 -190
- {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1400}/COPYRIGHT +0 -0
- {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1400}/LICENSE +0 -0
- {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1400}/README.rst +0 -0
- {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1400}/python/felis/__init__.py +0 -0
- {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1400}/python/felis/check.py +0 -0
- {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1400}/python/felis/db/__init__.py +0 -0
- {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1400}/python/felis/db/sqltypes.py +0 -0
- {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1400}/python/felis/py.typed +0 -0
- {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1400}/python/felis/simple.py +0 -0
- {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1400}/python/felis/tap.py +0 -0
- {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1400}/python/felis/types.py +0 -0
- {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1400}/python/felis/utils.py +0 -0
- {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1400}/python/felis/validation.py +0 -0
- {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1400}/python/felis/visitor.py +0 -0
- {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1400}/python/lsst_felis.egg-info/dependency_links.txt +0 -0
- {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1400}/python/lsst_felis.egg-info/entry_points.txt +0 -0
- {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1400}/python/lsst_felis.egg-info/top_level.txt +0 -0
- {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1400}/python/lsst_felis.egg-info/zip-safe +0 -0
- {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1400}/setup.cfg +0 -0
- {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1400}/tests/test_check.py +0 -0
- {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1400}/tests/test_simple.py +0 -0
- {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1400}/tests/test_tap.py +0 -0
- {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1400}/tests/test_utils.py +0 -0
- {lsst-felis-26.2024.1200 → lsst-felis-26.2024.1400}/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.
|
|
3
|
+
Version: 26.2024.1400
|
|
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 .
|
|
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
|
|
75
|
-
@click.option("--engine-url", envvar="ENGINE_URL", help="SQLAlchemy Engine URL")
|
|
76
|
-
@click.option("--schema-name", help="Alternate
|
|
77
|
-
@click.option(
|
|
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
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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 =
|
|
134
|
-
"""Whether the column can be
|
|
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
|
-
|
|
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
|