lsst-felis 27.2024.2200__py3-none-any.whl → 27.2024.2400__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 +10 -14
- felis/datamodel.py +7 -54
- felis/db/dialects.py +63 -0
- felis/db/utils.py +248 -0
- felis/db/{_variants.py → variants.py} +29 -22
- felis/metadata.py +2 -185
- felis/version.py +1 -1
- {lsst_felis-27.2024.2200.dist-info → lsst_felis-27.2024.2400.dist-info}/METADATA +1 -1
- lsst_felis-27.2024.2400.dist-info/RECORD +22 -0
- lsst_felis-27.2024.2200.dist-info/RECORD +0 -20
- {lsst_felis-27.2024.2200.dist-info → lsst_felis-27.2024.2400.dist-info}/COPYRIGHT +0 -0
- {lsst_felis-27.2024.2200.dist-info → lsst_felis-27.2024.2400.dist-info}/LICENSE +0 -0
- {lsst_felis-27.2024.2200.dist-info → lsst_felis-27.2024.2400.dist-info}/WHEEL +0 -0
- {lsst_felis-27.2024.2200.dist-info → lsst_felis-27.2024.2400.dist-info}/entry_points.txt +0 -0
- {lsst_felis-27.2024.2200.dist-info → lsst_felis-27.2024.2400.dist-info}/top_level.txt +0 -0
- {lsst_felis-27.2024.2200.dist-info → lsst_felis-27.2024.2400.dist-info}/zip-safe +0 -0
felis/cli.py
CHANGED
|
@@ -29,12 +29,13 @@ from typing import IO
|
|
|
29
29
|
import click
|
|
30
30
|
import yaml
|
|
31
31
|
from pydantic import ValidationError
|
|
32
|
-
from sqlalchemy.engine import Engine, create_engine,
|
|
32
|
+
from sqlalchemy.engine import Engine, create_engine, make_url
|
|
33
33
|
from sqlalchemy.engine.mock import MockConnection
|
|
34
34
|
|
|
35
35
|
from . import __version__
|
|
36
36
|
from .datamodel import Schema
|
|
37
|
-
from .
|
|
37
|
+
from .db.utils import DatabaseContext
|
|
38
|
+
from .metadata import MetaDataBuilder
|
|
38
39
|
from .tap import Tap11Base, TapLoadingVisitor, init_tables
|
|
39
40
|
from .validation import get_schema
|
|
40
41
|
|
|
@@ -92,29 +93,27 @@ def create(
|
|
|
92
93
|
"""Create database objects from the Felis file."""
|
|
93
94
|
yaml_data = yaml.safe_load(file)
|
|
94
95
|
schema = Schema.model_validate(yaml_data)
|
|
95
|
-
|
|
96
|
+
url = make_url(engine_url)
|
|
96
97
|
if schema_name:
|
|
97
98
|
logger.info(f"Overriding schema name with: {schema_name}")
|
|
98
99
|
schema.name = schema_name
|
|
99
|
-
elif
|
|
100
|
+
elif url.drivername == "sqlite":
|
|
100
101
|
logger.info("Overriding schema name for sqlite with: main")
|
|
101
102
|
schema.name = "main"
|
|
102
|
-
if not
|
|
103
|
+
if not url.host and not url.drivername == "sqlite":
|
|
103
104
|
dry_run = True
|
|
104
105
|
logger.info("Forcing dry run for non-sqlite engine URL with no host")
|
|
105
106
|
|
|
106
|
-
|
|
107
|
-
builder.build()
|
|
108
|
-
metadata = builder.metadata
|
|
107
|
+
metadata = MetaDataBuilder(schema).build()
|
|
109
108
|
logger.debug(f"Created metadata with schema name: {metadata.schema}")
|
|
110
109
|
|
|
111
110
|
engine: Engine | MockConnection
|
|
112
111
|
if not dry_run and not output_file:
|
|
113
|
-
engine = create_engine(
|
|
112
|
+
engine = create_engine(url, echo=echo)
|
|
114
113
|
else:
|
|
115
114
|
if dry_run:
|
|
116
115
|
logger.info("Dry run will be executed")
|
|
117
|
-
engine = DatabaseContext.create_mock_engine(
|
|
116
|
+
engine = DatabaseContext.create_mock_engine(url, output_file)
|
|
118
117
|
if output_file:
|
|
119
118
|
logger.info("Writing SQL output to: " + output_file.name)
|
|
120
119
|
|
|
@@ -229,10 +228,7 @@ def load_tap(
|
|
|
229
228
|
)
|
|
230
229
|
tap_visitor.visit_schema(schema)
|
|
231
230
|
else:
|
|
232
|
-
|
|
233
|
-
conn = create_mock_engine(make_url(engine_url), executor=_insert_dump.dump, paramstyle="pyformat")
|
|
234
|
-
# After the engine is created, update the executor with the dialect
|
|
235
|
-
_insert_dump.dialect = conn.dialect
|
|
231
|
+
conn = DatabaseContext.create_mock_engine(engine_url)
|
|
236
232
|
|
|
237
233
|
tap_visitor = TapLoadingVisitor.from_mock_connection(
|
|
238
234
|
conn,
|
felis/datamodel.py
CHANGED
|
@@ -22,7 +22,6 @@
|
|
|
22
22
|
from __future__ import annotations
|
|
23
23
|
|
|
24
24
|
import logging
|
|
25
|
-
import re
|
|
26
25
|
from collections.abc import Mapping, Sequence
|
|
27
26
|
from enum import StrEnum, auto
|
|
28
27
|
from typing import Annotated, Any, Literal, TypeAlias
|
|
@@ -30,13 +29,10 @@ from typing import Annotated, Any, Literal, TypeAlias
|
|
|
30
29
|
from astropy import units as units # type: ignore
|
|
31
30
|
from astropy.io.votable import ucd # type: ignore
|
|
32
31
|
from pydantic import BaseModel, ConfigDict, Field, ValidationInfo, field_validator, model_validator
|
|
33
|
-
from sqlalchemy import dialects
|
|
34
|
-
from sqlalchemy import types as sqa_types
|
|
35
|
-
from sqlalchemy.engine import create_mock_engine
|
|
36
|
-
from sqlalchemy.engine.interfaces import Dialect
|
|
37
|
-
from sqlalchemy.types import TypeEngine
|
|
38
32
|
|
|
33
|
+
from .db.dialects import get_supported_dialects
|
|
39
34
|
from .db.sqltypes import get_type_func
|
|
35
|
+
from .db.utils import string_to_typeengine
|
|
40
36
|
from .types import Boolean, Byte, Char, Double, FelisType, Float, Int, Long, Short, String, Text, Unicode
|
|
41
37
|
|
|
42
38
|
logger = logging.getLogger(__name__)
|
|
@@ -127,51 +123,6 @@ class DataType(StrEnum):
|
|
|
127
123
|
timestamp = auto()
|
|
128
124
|
|
|
129
125
|
|
|
130
|
-
_DIALECTS = {
|
|
131
|
-
"mysql": create_mock_engine("mysql://", executor=None).dialect,
|
|
132
|
-
"postgresql": create_mock_engine("postgresql://", executor=None).dialect,
|
|
133
|
-
}
|
|
134
|
-
"""Dictionary of dialect names to SQLAlchemy dialects."""
|
|
135
|
-
|
|
136
|
-
_DIALECT_MODULES = {"mysql": getattr(dialects, "mysql"), "postgresql": getattr(dialects, "postgresql")}
|
|
137
|
-
"""Dictionary of dialect names to SQLAlchemy dialect modules."""
|
|
138
|
-
|
|
139
|
-
_DATATYPE_REGEXP = re.compile(r"(\w+)(\((.*)\))?")
|
|
140
|
-
"""Regular expression to match data types in the form "type(length)"""
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
def string_to_typeengine(
|
|
144
|
-
type_string: str, dialect: Dialect | None = None, length: int | None = None
|
|
145
|
-
) -> TypeEngine:
|
|
146
|
-
match = _DATATYPE_REGEXP.search(type_string)
|
|
147
|
-
if not match:
|
|
148
|
-
raise ValueError(f"Invalid type string: {type_string}")
|
|
149
|
-
|
|
150
|
-
type_name, _, params = match.groups()
|
|
151
|
-
if dialect is None:
|
|
152
|
-
type_class = getattr(sqa_types, type_name.upper(), None)
|
|
153
|
-
else:
|
|
154
|
-
try:
|
|
155
|
-
dialect_module = _DIALECT_MODULES[dialect.name]
|
|
156
|
-
except KeyError:
|
|
157
|
-
raise ValueError(f"Unsupported dialect: {dialect}")
|
|
158
|
-
type_class = getattr(dialect_module, type_name.upper(), None)
|
|
159
|
-
|
|
160
|
-
if not type_class:
|
|
161
|
-
raise ValueError(f"Unsupported type: {type_class}")
|
|
162
|
-
|
|
163
|
-
if params:
|
|
164
|
-
params = [int(param) if param.isdigit() else param for param in params.split(",")]
|
|
165
|
-
type_obj = type_class(*params)
|
|
166
|
-
else:
|
|
167
|
-
type_obj = type_class()
|
|
168
|
-
|
|
169
|
-
if hasattr(type_obj, "length") and getattr(type_obj, "length") is None and length is not None:
|
|
170
|
-
type_obj.length = length
|
|
171
|
-
|
|
172
|
-
return type_obj
|
|
173
|
-
|
|
174
|
-
|
|
175
126
|
class Column(BaseObject):
|
|
176
127
|
"""A column in a table."""
|
|
177
128
|
|
|
@@ -304,7 +255,10 @@ class Column(BaseObject):
|
|
|
304
255
|
context = info.context
|
|
305
256
|
if not context or not context.get("check_redundant_datatypes", False):
|
|
306
257
|
return self
|
|
307
|
-
if all(
|
|
258
|
+
if all(
|
|
259
|
+
getattr(self, f"{dialect}:datatype", None) is not None
|
|
260
|
+
for dialect in get_supported_dialects().keys()
|
|
261
|
+
):
|
|
308
262
|
return self
|
|
309
263
|
|
|
310
264
|
datatype = self.datatype
|
|
@@ -317,7 +271,7 @@ class Column(BaseObject):
|
|
|
317
271
|
else:
|
|
318
272
|
datatype_obj = datatype_func()
|
|
319
273
|
|
|
320
|
-
for dialect_name, dialect in
|
|
274
|
+
for dialect_name, dialect in get_supported_dialects().items():
|
|
321
275
|
db_annotation = f"{dialect_name}_datatype"
|
|
322
276
|
if datatype_string := self.model_dump().get(db_annotation):
|
|
323
277
|
db_datatype_obj = string_to_typeengine(datatype_string, dialect, length)
|
|
@@ -566,7 +520,6 @@ class Schema(BaseObject):
|
|
|
566
520
|
return self
|
|
567
521
|
visitor: SchemaIdVisitor = SchemaIdVisitor()
|
|
568
522
|
visitor.visit_schema(self)
|
|
569
|
-
logger.debug(f"Created schema ID map with {len(self.id_map.keys())} objects")
|
|
570
523
|
if len(visitor.duplicates):
|
|
571
524
|
raise ValueError(
|
|
572
525
|
"Duplicate IDs found in schema:\n " + "\n ".join(visitor.duplicates) + "\n"
|
felis/db/dialects.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
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 logging
|
|
23
|
+
from types import ModuleType
|
|
24
|
+
|
|
25
|
+
from sqlalchemy import dialects
|
|
26
|
+
from sqlalchemy.engine import Dialect
|
|
27
|
+
from sqlalchemy.engine.mock import create_mock_engine
|
|
28
|
+
|
|
29
|
+
from .sqltypes import MYSQL, ORACLE, POSTGRES, SQLITE
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
_DIALECT_NAMES = [MYSQL, POSTGRES, SQLITE, ORACLE]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _dialect(dialect_name: str) -> Dialect:
|
|
37
|
+
"""Create the SQLAlchemy dialect for the given name."""
|
|
38
|
+
return create_mock_engine(f"{dialect_name}://", executor=None).dialect
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
_DIALECTS = {name: _dialect(name) for name in _DIALECT_NAMES}
|
|
42
|
+
"""Dictionary of dialect names to SQLAlchemy dialects."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_supported_dialects() -> dict[str, Dialect]:
|
|
46
|
+
"""Get a dictionary of the supported SQLAlchemy dialects."""
|
|
47
|
+
return _DIALECTS
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _dialect_module(dialect_name: str) -> ModuleType:
|
|
51
|
+
"""Get the SQLAlchemy dialect module for the given name."""
|
|
52
|
+
return getattr(dialects, dialect_name)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
_DIALECT_MODULES = {name: _dialect_module(name) for name in _DIALECT_NAMES}
|
|
56
|
+
"""Dictionary of dialect names to SQLAlchemy modules for type instantiation."""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def get_dialect_module(dialect_name: str) -> ModuleType:
|
|
60
|
+
"""Get the SQLAlchemy dialect module for the given name."""
|
|
61
|
+
if dialect_name not in _DIALECT_MODULES:
|
|
62
|
+
raise ValueError(f"Unsupported dialect: {dialect_name}")
|
|
63
|
+
return _DIALECT_MODULES[dialect_name]
|
felis/db/utils.py
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
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
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import logging
|
|
25
|
+
import re
|
|
26
|
+
from typing import IO, Any
|
|
27
|
+
|
|
28
|
+
from sqlalchemy import MetaData, types
|
|
29
|
+
from sqlalchemy.engine import Dialect, Engine, ResultProxy
|
|
30
|
+
from sqlalchemy.engine.mock import MockConnection, create_mock_engine
|
|
31
|
+
from sqlalchemy.engine.url import URL
|
|
32
|
+
from sqlalchemy.exc import SQLAlchemyError
|
|
33
|
+
from sqlalchemy.schema import CreateSchema, DropSchema
|
|
34
|
+
from sqlalchemy.sql import text
|
|
35
|
+
from sqlalchemy.types import TypeEngine
|
|
36
|
+
|
|
37
|
+
from .dialects import get_dialect_module
|
|
38
|
+
|
|
39
|
+
logger = logging.getLogger("felis")
|
|
40
|
+
|
|
41
|
+
_DATATYPE_REGEXP = re.compile(r"(\w+)(\((.*)\))?")
|
|
42
|
+
"""Regular expression to match data types in the form "type(length)"""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def string_to_typeengine(
|
|
46
|
+
type_string: str, dialect: Dialect | None = None, length: int | None = None
|
|
47
|
+
) -> TypeEngine:
|
|
48
|
+
"""Convert a string representation of a data type to a SQLAlchemy
|
|
49
|
+
TypeEngine.
|
|
50
|
+
"""
|
|
51
|
+
match = _DATATYPE_REGEXP.search(type_string)
|
|
52
|
+
if not match:
|
|
53
|
+
raise ValueError(f"Invalid type string: {type_string}")
|
|
54
|
+
|
|
55
|
+
type_name, _, params = match.groups()
|
|
56
|
+
if dialect is None:
|
|
57
|
+
type_class = getattr(types, type_name.upper(), None)
|
|
58
|
+
else:
|
|
59
|
+
try:
|
|
60
|
+
dialect_module = get_dialect_module(dialect.name)
|
|
61
|
+
except KeyError:
|
|
62
|
+
raise ValueError(f"Unsupported dialect: {dialect}")
|
|
63
|
+
type_class = getattr(dialect_module, type_name.upper(), None)
|
|
64
|
+
|
|
65
|
+
if not type_class:
|
|
66
|
+
raise ValueError(f"Unsupported type: {type_class}")
|
|
67
|
+
|
|
68
|
+
if params:
|
|
69
|
+
params = [int(param) if param.isdigit() else param for param in params.split(",")]
|
|
70
|
+
type_obj = type_class(*params)
|
|
71
|
+
else:
|
|
72
|
+
type_obj = type_class()
|
|
73
|
+
|
|
74
|
+
if hasattr(type_obj, "length") and getattr(type_obj, "length") is None and length is not None:
|
|
75
|
+
type_obj.length = length
|
|
76
|
+
|
|
77
|
+
return type_obj
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class SQLWriter:
|
|
81
|
+
"""Writes SQL statements to stdout or a file."""
|
|
82
|
+
|
|
83
|
+
def __init__(self, file: IO[str] | None = None) -> None:
|
|
84
|
+
"""Initialize the SQL writer.
|
|
85
|
+
|
|
86
|
+
Parameters
|
|
87
|
+
----------
|
|
88
|
+
file : `io.TextIOBase` or `None`, optional
|
|
89
|
+
The file to write the SQL statements to. If None, the statements
|
|
90
|
+
will be written to stdout.
|
|
91
|
+
"""
|
|
92
|
+
self.file = file
|
|
93
|
+
self.dialect: Dialect | None = None
|
|
94
|
+
|
|
95
|
+
def write(self, sql: Any, *multiparams: Any, **params: Any) -> None:
|
|
96
|
+
"""Write the SQL statement to a file or stdout.
|
|
97
|
+
|
|
98
|
+
Statements with parameters will be formatted with the values
|
|
99
|
+
inserted into the resultant SQL output.
|
|
100
|
+
|
|
101
|
+
Parameters
|
|
102
|
+
----------
|
|
103
|
+
sql : `typing.Any`
|
|
104
|
+
The SQL statement to write.
|
|
105
|
+
multiparams : `typing.Any`
|
|
106
|
+
The multiparams to use for the SQL statement.
|
|
107
|
+
params : `typing.Any`
|
|
108
|
+
The params to use for the SQL statement.
|
|
109
|
+
"""
|
|
110
|
+
compiled = sql.compile(dialect=self.dialect)
|
|
111
|
+
sql_str = str(compiled) + ";"
|
|
112
|
+
params_list = [compiled.params]
|
|
113
|
+
for params in params_list:
|
|
114
|
+
if not params:
|
|
115
|
+
print(sql_str, file=self.file)
|
|
116
|
+
continue
|
|
117
|
+
new_params = {}
|
|
118
|
+
for key, value in params.items():
|
|
119
|
+
if isinstance(value, str):
|
|
120
|
+
new_params[key] = f"'{value}'"
|
|
121
|
+
elif value is None:
|
|
122
|
+
new_params[key] = "null"
|
|
123
|
+
else:
|
|
124
|
+
new_params[key] = value
|
|
125
|
+
print(sql_str % new_params, file=self.file)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class ConnectionWrapper:
|
|
129
|
+
"""A wrapper for a SQLAlchemy engine or mock connection which provides a
|
|
130
|
+
consistent interface for executing SQL statements.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
def __init__(self, engine: Engine | MockConnection):
|
|
134
|
+
"""Initialize the connection wrapper.
|
|
135
|
+
|
|
136
|
+
Parameters
|
|
137
|
+
----------
|
|
138
|
+
engine : `sqlalchemy.Engine` or `sqlalchemy.MockConnection`
|
|
139
|
+
The SQLAlchemy engine or mock connection to wrap.
|
|
140
|
+
"""
|
|
141
|
+
self.engine = engine
|
|
142
|
+
|
|
143
|
+
def execute(self, statement: Any) -> ResultProxy:
|
|
144
|
+
"""Execute a SQL statement on the engine and return the result."""
|
|
145
|
+
if isinstance(statement, str):
|
|
146
|
+
statement = text(statement)
|
|
147
|
+
if isinstance(self.engine, MockConnection):
|
|
148
|
+
return self.engine.connect().execute(statement)
|
|
149
|
+
else:
|
|
150
|
+
with self.engine.begin() as connection:
|
|
151
|
+
result = connection.execute(statement)
|
|
152
|
+
return result
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class DatabaseContext:
|
|
156
|
+
"""A class for managing the schema and its database connection."""
|
|
157
|
+
|
|
158
|
+
def __init__(self, metadata: MetaData, engine: Engine | MockConnection):
|
|
159
|
+
"""Initialize the database context.
|
|
160
|
+
|
|
161
|
+
Parameters
|
|
162
|
+
----------
|
|
163
|
+
metadata : `sqlalchemy.MetaData`
|
|
164
|
+
The SQLAlchemy metadata object.
|
|
165
|
+
|
|
166
|
+
engine : `sqlalchemy.Engine` or `sqlalchemy.MockConnection`
|
|
167
|
+
The SQLAlchemy engine or mock connection object.
|
|
168
|
+
"""
|
|
169
|
+
self.engine = engine
|
|
170
|
+
self.dialect_name = engine.dialect.name
|
|
171
|
+
self.metadata = metadata
|
|
172
|
+
self.conn = ConnectionWrapper(engine)
|
|
173
|
+
|
|
174
|
+
def create_if_not_exists(self) -> None:
|
|
175
|
+
"""Create the schema in the database if it does not exist.
|
|
176
|
+
|
|
177
|
+
In MySQL, this will create a new database. In PostgreSQL, it will
|
|
178
|
+
create a new schema. For other variants, this is an unsupported
|
|
179
|
+
operation.
|
|
180
|
+
|
|
181
|
+
Parameters
|
|
182
|
+
----------
|
|
183
|
+
engine: `sqlalchemy.Engine`
|
|
184
|
+
The SQLAlchemy engine object.
|
|
185
|
+
schema_name: `str`
|
|
186
|
+
The name of the schema (or database) to create.
|
|
187
|
+
"""
|
|
188
|
+
schema_name = self.metadata.schema
|
|
189
|
+
try:
|
|
190
|
+
if self.dialect_name == "mysql":
|
|
191
|
+
logger.debug(f"Creating MySQL database: {schema_name}")
|
|
192
|
+
self.conn.execute(text(f"CREATE DATABASE IF NOT EXISTS {schema_name}"))
|
|
193
|
+
elif self.dialect_name == "postgresql":
|
|
194
|
+
logger.debug(f"Creating PG schema: {schema_name}")
|
|
195
|
+
self.conn.execute(CreateSchema(schema_name, if_not_exists=True))
|
|
196
|
+
else:
|
|
197
|
+
raise ValueError("Unsupported database type:" + self.dialect_name)
|
|
198
|
+
except SQLAlchemyError as e:
|
|
199
|
+
logger.error(f"Error creating schema: {e}")
|
|
200
|
+
raise
|
|
201
|
+
|
|
202
|
+
def drop_if_exists(self) -> None:
|
|
203
|
+
"""Drop the schema in the database if it exists.
|
|
204
|
+
|
|
205
|
+
In MySQL, this will drop a database. In PostgreSQL, it will drop a
|
|
206
|
+
schema. For other variants, this is unsupported for now.
|
|
207
|
+
|
|
208
|
+
Parameters
|
|
209
|
+
----------
|
|
210
|
+
engine: `sqlalchemy.Engine`
|
|
211
|
+
The SQLAlchemy engine object.
|
|
212
|
+
schema_name: `str`
|
|
213
|
+
The name of the schema (or database) to drop.
|
|
214
|
+
"""
|
|
215
|
+
schema_name = self.metadata.schema
|
|
216
|
+
try:
|
|
217
|
+
if self.dialect_name == "mysql":
|
|
218
|
+
logger.debug(f"Dropping MySQL database if exists: {schema_name}")
|
|
219
|
+
self.conn.execute(text(f"DROP DATABASE IF EXISTS {schema_name}"))
|
|
220
|
+
elif self.dialect_name == "postgresql":
|
|
221
|
+
logger.debug(f"Dropping PostgreSQL schema if exists: {schema_name}")
|
|
222
|
+
self.conn.execute(DropSchema(schema_name, if_exists=True, cascade=True))
|
|
223
|
+
else:
|
|
224
|
+
raise ValueError(f"Unsupported database type: {self.dialect_name}")
|
|
225
|
+
except SQLAlchemyError as e:
|
|
226
|
+
logger.error(f"Error dropping schema: {e}")
|
|
227
|
+
raise
|
|
228
|
+
|
|
229
|
+
def create_all(self) -> None:
|
|
230
|
+
"""Create all tables in the schema using the metadata object."""
|
|
231
|
+
self.metadata.create_all(self.engine)
|
|
232
|
+
|
|
233
|
+
@staticmethod
|
|
234
|
+
def create_mock_engine(engine_url: str | URL, output_file: IO[str] | None = None) -> MockConnection:
|
|
235
|
+
"""Create a mock engine for testing or dumping DDL statements.
|
|
236
|
+
|
|
237
|
+
Parameters
|
|
238
|
+
----------
|
|
239
|
+
engine_url : `sqlalchemy.engine.url.URL`
|
|
240
|
+
The SQLAlchemy engine URL.
|
|
241
|
+
output_file : `typing.IO` [ `str` ] or `None`, optional
|
|
242
|
+
The file to write the SQL statements to. If None, the statements
|
|
243
|
+
will be written to stdout.
|
|
244
|
+
"""
|
|
245
|
+
writer = SQLWriter(output_file)
|
|
246
|
+
engine = create_mock_engine(engine_url, executor=writer.write)
|
|
247
|
+
writer.dialect = engine.dialect
|
|
248
|
+
return engine
|
|
@@ -23,38 +23,44 @@ import re
|
|
|
23
23
|
from typing import Any
|
|
24
24
|
|
|
25
25
|
from sqlalchemy import types
|
|
26
|
-
from sqlalchemy.dialects import mysql, oracle, postgresql, sqlite
|
|
27
26
|
from sqlalchemy.types import TypeEngine
|
|
28
27
|
|
|
29
28
|
from ..datamodel import Column
|
|
29
|
+
from .dialects import get_dialect_module, get_supported_dialects
|
|
30
30
|
|
|
31
|
-
MYSQL = "mysql"
|
|
32
|
-
ORACLE = "oracle"
|
|
33
|
-
POSTGRES = "postgresql"
|
|
34
|
-
SQLITE = "sqlite"
|
|
35
31
|
|
|
36
|
-
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
32
|
+
def _create_column_variant_overrides() -> dict[str, str]:
|
|
33
|
+
"""Create a dictionary of column variant overrides."""
|
|
34
|
+
column_variant_overrides = {}
|
|
35
|
+
for dialect_name in get_supported_dialects().keys():
|
|
36
|
+
column_variant_overrides[f"{dialect_name}_datatype"] = dialect_name
|
|
37
|
+
return column_variant_overrides
|
|
41
38
|
|
|
42
|
-
COLUMN_VARIANT_OVERRIDE = {
|
|
43
|
-
"mysql_datatype": "mysql",
|
|
44
|
-
"oracle_datatype": "oracle",
|
|
45
|
-
"postgresql_datatype": "postgresql",
|
|
46
|
-
"sqlite_datatype": "sqlite",
|
|
47
|
-
}
|
|
48
39
|
|
|
49
|
-
|
|
40
|
+
_COLUMN_VARIANT_OVERRIDES = _create_column_variant_overrides()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _get_column_variant_overrides() -> dict[str, str]:
|
|
44
|
+
"""Return a dictionary of column variant overrides."""
|
|
45
|
+
return _COLUMN_VARIANT_OVERRIDES
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _get_column_variant_override(field_name: str) -> str:
|
|
49
|
+
"""Return the dialect name from an override field name on the column like
|
|
50
|
+
``mysql_datatype``.
|
|
51
|
+
"""
|
|
52
|
+
if field_name not in _COLUMN_VARIANT_OVERRIDES:
|
|
53
|
+
raise ValueError(f"Field name {field_name} not found in column variant overrides")
|
|
54
|
+
return _COLUMN_VARIANT_OVERRIDES[field_name]
|
|
55
|
+
|
|
50
56
|
|
|
51
57
|
_length_regex = re.compile(r"\((\d+)\)")
|
|
52
58
|
"""A regular expression that is looking for numbers within parentheses."""
|
|
53
59
|
|
|
54
60
|
|
|
55
|
-
def
|
|
61
|
+
def _process_variant_override(dialect_name: str, variant_override_str: str) -> types.TypeEngine:
|
|
56
62
|
"""Return variant type for given dialect."""
|
|
57
|
-
dialect =
|
|
63
|
+
dialect = get_dialect_module(dialect_name)
|
|
58
64
|
variant_type_name = variant_override_str.split("(")[0]
|
|
59
65
|
|
|
60
66
|
# Process Variant Type
|
|
@@ -86,9 +92,10 @@ def make_variant_dict(column_obj: Column) -> dict[str, TypeEngine[Any]]:
|
|
|
86
92
|
variant datatype information (e.g., for mysql, postgresql, etc).
|
|
87
93
|
"""
|
|
88
94
|
variant_dict = {}
|
|
95
|
+
variant_overrides = _get_column_variant_overrides()
|
|
89
96
|
for field_name, value in iter(column_obj):
|
|
90
|
-
if field_name in
|
|
91
|
-
dialect =
|
|
92
|
-
variant: TypeEngine =
|
|
97
|
+
if field_name in variant_overrides and value is not None:
|
|
98
|
+
dialect = _get_column_variant_override(field_name)
|
|
99
|
+
variant: TypeEngine = _process_variant_override(dialect, value)
|
|
93
100
|
variant_dict[dialect] = variant
|
|
94
101
|
return variant_dict
|
felis/metadata.py
CHANGED
|
@@ -22,35 +22,26 @@
|
|
|
22
22
|
from __future__ import annotations
|
|
23
23
|
|
|
24
24
|
import logging
|
|
25
|
-
from typing import
|
|
25
|
+
from typing import Any, Literal
|
|
26
26
|
|
|
27
|
-
import sqlalchemy.schema as sqa_schema
|
|
28
27
|
from lsst.utils.iteration import ensure_iterable
|
|
29
28
|
from sqlalchemy import (
|
|
30
29
|
CheckConstraint,
|
|
31
30
|
Column,
|
|
32
31
|
Constraint,
|
|
33
|
-
Engine,
|
|
34
32
|
ForeignKeyConstraint,
|
|
35
33
|
Index,
|
|
36
34
|
MetaData,
|
|
37
35
|
PrimaryKeyConstraint,
|
|
38
|
-
ResultProxy,
|
|
39
36
|
Table,
|
|
40
37
|
TextClause,
|
|
41
38
|
UniqueConstraint,
|
|
42
|
-
create_mock_engine,
|
|
43
|
-
make_url,
|
|
44
39
|
text,
|
|
45
40
|
)
|
|
46
|
-
from sqlalchemy.engine.interfaces import Dialect
|
|
47
|
-
from sqlalchemy.engine.mock import MockConnection
|
|
48
|
-
from sqlalchemy.engine.url import URL
|
|
49
|
-
from sqlalchemy.exc import SQLAlchemyError
|
|
50
41
|
from sqlalchemy.types import TypeEngine
|
|
51
42
|
|
|
52
43
|
from felis.datamodel import Schema
|
|
53
|
-
from felis.db.
|
|
44
|
+
from felis.db.variants import make_variant_dict
|
|
54
45
|
|
|
55
46
|
from . import datamodel
|
|
56
47
|
from .db import sqltypes
|
|
@@ -59,56 +50,6 @@ from .types import FelisType
|
|
|
59
50
|
logger = logging.getLogger(__name__)
|
|
60
51
|
|
|
61
52
|
|
|
62
|
-
class InsertDump:
|
|
63
|
-
"""An Insert Dumper for SQL statements which supports writing messages
|
|
64
|
-
to stdout or a file.
|
|
65
|
-
"""
|
|
66
|
-
|
|
67
|
-
def __init__(self, file: IO[str] | None = None) -> None:
|
|
68
|
-
"""Initialize the insert dumper.
|
|
69
|
-
|
|
70
|
-
Parameters
|
|
71
|
-
----------
|
|
72
|
-
file : `io.TextIOBase` or `None`, optional
|
|
73
|
-
The file to write the SQL statements to. If None, the statements
|
|
74
|
-
will be written to stdout.
|
|
75
|
-
"""
|
|
76
|
-
self.file = file
|
|
77
|
-
self.dialect: Dialect | None = None
|
|
78
|
-
|
|
79
|
-
def dump(self, sql: Any, *multiparams: Any, **params: Any) -> None:
|
|
80
|
-
"""Dump the SQL statement to a file or stdout.
|
|
81
|
-
|
|
82
|
-
Statements with parameters will be formatted with the values
|
|
83
|
-
inserted into the resultant SQL output.
|
|
84
|
-
|
|
85
|
-
Parameters
|
|
86
|
-
----------
|
|
87
|
-
sql : `typing.Any`
|
|
88
|
-
The SQL statement to dump.
|
|
89
|
-
multiparams : `typing.Any`
|
|
90
|
-
The multiparams to use for the SQL statement.
|
|
91
|
-
params : `typing.Any`
|
|
92
|
-
The params to use for the SQL statement.
|
|
93
|
-
"""
|
|
94
|
-
compiled = sql.compile(dialect=self.dialect)
|
|
95
|
-
sql_str = str(compiled) + ";"
|
|
96
|
-
params_list = [compiled.params]
|
|
97
|
-
for params in params_list:
|
|
98
|
-
if not params:
|
|
99
|
-
print(sql_str, file=self.file)
|
|
100
|
-
continue
|
|
101
|
-
new_params = {}
|
|
102
|
-
for key, value in params.items():
|
|
103
|
-
if isinstance(value, str):
|
|
104
|
-
new_params[key] = f"'{value}'"
|
|
105
|
-
elif value is None:
|
|
106
|
-
new_params[key] = "null"
|
|
107
|
-
else:
|
|
108
|
-
new_params[key] = value
|
|
109
|
-
print(sql_str % new_params, file=self.file)
|
|
110
|
-
|
|
111
|
-
|
|
112
53
|
def get_datatype_with_variants(column_obj: datamodel.Column) -> TypeEngine:
|
|
113
54
|
"""Use the Felis type system to get a SQLAlchemy datatype with variant
|
|
114
55
|
overrides from the information in a `Column` object.
|
|
@@ -387,127 +328,3 @@ class MetaDataBuilder:
|
|
|
387
328
|
index = Index(index_obj.name, *columns, *expressions)
|
|
388
329
|
self._objects[index_obj.id] = index
|
|
389
330
|
return index
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
class ConnectionWrapper:
|
|
393
|
-
"""A wrapper for a SQLAlchemy engine or mock connection which provides a
|
|
394
|
-
consistent interface for executing SQL statements.
|
|
395
|
-
"""
|
|
396
|
-
|
|
397
|
-
def __init__(self, engine: Engine | MockConnection):
|
|
398
|
-
"""Initialize the connection wrapper.
|
|
399
|
-
|
|
400
|
-
Parameters
|
|
401
|
-
----------
|
|
402
|
-
engine : `sqlalchemy.Engine` or `sqlalchemy.MockConnection`
|
|
403
|
-
The SQLAlchemy engine or mock connection to wrap.
|
|
404
|
-
"""
|
|
405
|
-
self.engine = engine
|
|
406
|
-
|
|
407
|
-
def execute(self, statement: Any) -> ResultProxy:
|
|
408
|
-
"""Execute a SQL statement on the engine and return the result."""
|
|
409
|
-
if isinstance(statement, str):
|
|
410
|
-
statement = text(statement)
|
|
411
|
-
if isinstance(self.engine, MockConnection):
|
|
412
|
-
return self.engine.connect().execute(statement)
|
|
413
|
-
else:
|
|
414
|
-
with self.engine.begin() as connection:
|
|
415
|
-
result = connection.execute(statement)
|
|
416
|
-
return result
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
class DatabaseContext:
|
|
420
|
-
"""A class for managing the schema and its database connection."""
|
|
421
|
-
|
|
422
|
-
def __init__(self, metadata: MetaData, engine: Engine | MockConnection):
|
|
423
|
-
"""Initialize the database context.
|
|
424
|
-
|
|
425
|
-
Parameters
|
|
426
|
-
----------
|
|
427
|
-
metadata : `sqlalchemy.MetaData`
|
|
428
|
-
The SQLAlchemy metadata object.
|
|
429
|
-
|
|
430
|
-
engine : `sqlalchemy.Engine` or `sqlalchemy.MockConnection`
|
|
431
|
-
The SQLAlchemy engine or mock connection object.
|
|
432
|
-
"""
|
|
433
|
-
self.engine = engine
|
|
434
|
-
self.metadata = metadata
|
|
435
|
-
self.connection = ConnectionWrapper(engine)
|
|
436
|
-
|
|
437
|
-
def create_if_not_exists(self) -> None:
|
|
438
|
-
"""Create the schema in the database if it does not exist.
|
|
439
|
-
|
|
440
|
-
In MySQL, this will create a new database. In PostgreSQL, it will
|
|
441
|
-
create a new schema. For other variants, this is an unsupported
|
|
442
|
-
operation.
|
|
443
|
-
|
|
444
|
-
Parameters
|
|
445
|
-
----------
|
|
446
|
-
engine: `sqlalchemy.Engine`
|
|
447
|
-
The SQLAlchemy engine object.
|
|
448
|
-
schema_name: `str`
|
|
449
|
-
The name of the schema (or database) to create.
|
|
450
|
-
"""
|
|
451
|
-
db_type = self.engine.dialect.name
|
|
452
|
-
schema_name = self.metadata.schema
|
|
453
|
-
try:
|
|
454
|
-
if db_type == "mysql":
|
|
455
|
-
logger.info(f"Creating MySQL database: {schema_name}")
|
|
456
|
-
self.connection.execute(text(f"CREATE DATABASE IF NOT EXISTS {schema_name}"))
|
|
457
|
-
elif db_type == "postgresql":
|
|
458
|
-
logger.info(f"Creating PG schema: {schema_name}")
|
|
459
|
-
self.connection.execute(sqa_schema.CreateSchema(schema_name, if_not_exists=True))
|
|
460
|
-
else:
|
|
461
|
-
raise ValueError("Unsupported database type:" + db_type)
|
|
462
|
-
except SQLAlchemyError as e:
|
|
463
|
-
logger.error(f"Error creating schema: {e}")
|
|
464
|
-
raise
|
|
465
|
-
|
|
466
|
-
def drop_if_exists(self) -> None:
|
|
467
|
-
"""Drop the schema in the database if it exists.
|
|
468
|
-
|
|
469
|
-
In MySQL, this will drop a database. In PostgreSQL, it will drop a
|
|
470
|
-
schema. For other variants, this is unsupported for now.
|
|
471
|
-
|
|
472
|
-
Parameters
|
|
473
|
-
----------
|
|
474
|
-
engine: `sqlalchemy.Engine`
|
|
475
|
-
The SQLAlchemy engine object.
|
|
476
|
-
schema_name: `str`
|
|
477
|
-
The name of the schema (or database) to drop.
|
|
478
|
-
"""
|
|
479
|
-
db_type = self.engine.dialect.name
|
|
480
|
-
schema_name = self.metadata.schema
|
|
481
|
-
try:
|
|
482
|
-
if db_type == "mysql":
|
|
483
|
-
logger.info(f"Dropping MySQL database if exists: {schema_name}")
|
|
484
|
-
self.connection.execute(text(f"DROP DATABASE IF EXISTS {schema_name}"))
|
|
485
|
-
elif db_type == "postgresql":
|
|
486
|
-
logger.info(f"Dropping PostgreSQL schema if exists: {schema_name}")
|
|
487
|
-
self.connection.execute(sqa_schema.DropSchema(schema_name, if_exists=True, cascade=True))
|
|
488
|
-
else:
|
|
489
|
-
raise ValueError(f"Unsupported database type: {db_type}")
|
|
490
|
-
except SQLAlchemyError as e:
|
|
491
|
-
logger.error(f"Error dropping schema: {e}")
|
|
492
|
-
raise
|
|
493
|
-
|
|
494
|
-
def create_all(self) -> None:
|
|
495
|
-
"""Create all tables in the schema using the metadata object."""
|
|
496
|
-
self.metadata.create_all(self.engine)
|
|
497
|
-
|
|
498
|
-
@staticmethod
|
|
499
|
-
def create_mock_engine(engine_url: URL, output_file: IO[str] | None = None) -> MockConnection:
|
|
500
|
-
"""Create a mock engine for testing or dumping DDL statements.
|
|
501
|
-
|
|
502
|
-
Parameters
|
|
503
|
-
----------
|
|
504
|
-
engine_url : `sqlalchemy.engine.url.URL`
|
|
505
|
-
The SQLAlchemy engine URL.
|
|
506
|
-
output_file : `typing.IO` [ `str` ] or `None`, optional
|
|
507
|
-
The file to write the SQL statements to. If None, the statements
|
|
508
|
-
will be written to stdout.
|
|
509
|
-
"""
|
|
510
|
-
dumper = InsertDump(output_file)
|
|
511
|
-
engine = create_mock_engine(make_url(engine_url), executor=dumper.dump)
|
|
512
|
-
dumper.dialect = engine.dialect
|
|
513
|
-
return engine
|
felis/version.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
__all__ = ["__version__"]
|
|
2
|
-
__version__ = "27.2024.
|
|
2
|
+
__version__ = "27.2024.2400"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: lsst-felis
|
|
3
|
-
Version: 27.2024.
|
|
3
|
+
Version: 27.2024.2400
|
|
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+)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
felis/__init__.py,sha256=THmRg3ylB4E73XhFjJX7YlnV_CM3lr_gZO_HqQFzIQ4,937
|
|
2
|
+
felis/cli.py,sha256=wBALFf9bMYpL6-A58I3JtozaiMSSoi7Gu7YyGuUk8Uo,9997
|
|
3
|
+
felis/datamodel.py,sha256=ECXmd78fufrYDBK4S0n3-wSQytWmz-vGLkNRjtrHPCE,19627
|
|
4
|
+
felis/metadata.py,sha256=lSgKy9CksmyJp9zUkzkwOAzjk2Hzivb9VVbYfTPNmvY,12140
|
|
5
|
+
felis/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
felis/tap.py,sha256=fVYvvIFk_vccXqbcFYdgK2yOfp4P5E4guvsSGktsNxs,16795
|
|
7
|
+
felis/types.py,sha256=z_ECfSxpqiFSGppjxKwCO4fPP7TLBaIN3Qo1AGF16Go,4418
|
|
8
|
+
felis/validation.py,sha256=Zq0gyCvPCwRlhfQ-w_p6ccDTkjcyhxSA1-Gr5plXiZI,3465
|
|
9
|
+
felis/version.py,sha256=SKE5LZn31z5ONJXoQypgUH-1aa2Ar0owtNYSiqyaKog,55
|
|
10
|
+
felis/db/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
felis/db/dialects.py,sha256=mSYgS8gqUJQGgZw3IA-zBfKlrJ2r4nEUR94wKZNRKZg,2267
|
|
12
|
+
felis/db/sqltypes.py,sha256=yFlautQ1hv21MHF4AIfBp7_2m1-exKBfc76xYsMHBgk,5735
|
|
13
|
+
felis/db/utils.py,sha256=oYPGOt5K_82GnScJOg8WarOyvval-Cu1nX7CduFxKss,9122
|
|
14
|
+
felis/db/variants.py,sha256=Ti2oZf7nFTe8aFyG-GeFSW4bIb5ClNikm9xOJtRcxLY,3862
|
|
15
|
+
lsst_felis-27.2024.2400.dist-info/COPYRIGHT,sha256=bUmNy19uUxqITMpjeHFe69q3IzQpjxvvBw6oV7kR7ho,129
|
|
16
|
+
lsst_felis-27.2024.2400.dist-info/LICENSE,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
|
|
17
|
+
lsst_felis-27.2024.2400.dist-info/METADATA,sha256=agi49bvO-Q3nN0a2chNJcvsKtz1mi9J_Az02n4wh_Og,1191
|
|
18
|
+
lsst_felis-27.2024.2400.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
19
|
+
lsst_felis-27.2024.2400.dist-info/entry_points.txt,sha256=Gk2XFujA_Gp52VBk45g5kim8TDoMDJFPctsMqiq72EM,40
|
|
20
|
+
lsst_felis-27.2024.2400.dist-info/top_level.txt,sha256=F4SvPip3iZRVyISi50CHhwTIAokAhSxjWiVcn4IVWRI,6
|
|
21
|
+
lsst_felis-27.2024.2400.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
22
|
+
lsst_felis-27.2024.2400.dist-info/RECORD,,
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
felis/__init__.py,sha256=THmRg3ylB4E73XhFjJX7YlnV_CM3lr_gZO_HqQFzIQ4,937
|
|
2
|
-
felis/cli.py,sha256=l_4srdXPghBLAVuOvfJVdyIVhq45kTV5KYskmYSsIUA,10279
|
|
3
|
-
felis/datamodel.py,sha256=0LWiqjQgsgn0do4YjVWcf-_5JyJVT6UoFDuVMAULhPI,21351
|
|
4
|
-
felis/metadata.py,sha256=df64ep8F7nY7wiO-Myo1jUJNaNOq12qctSWmyIjGN5k,18910
|
|
5
|
-
felis/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
-
felis/tap.py,sha256=fVYvvIFk_vccXqbcFYdgK2yOfp4P5E4guvsSGktsNxs,16795
|
|
7
|
-
felis/types.py,sha256=z_ECfSxpqiFSGppjxKwCO4fPP7TLBaIN3Qo1AGF16Go,4418
|
|
8
|
-
felis/validation.py,sha256=Zq0gyCvPCwRlhfQ-w_p6ccDTkjcyhxSA1-Gr5plXiZI,3465
|
|
9
|
-
felis/version.py,sha256=nASiJwiqeqo2GROuDTWi-OYW1YS1-Qkrocag2n37gpQ,55
|
|
10
|
-
felis/db/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
-
felis/db/_variants.py,sha256=zCuXDgU_x_pTZcWkBLgqQCiOhlA6y2tBt-PUQfafwmM,3368
|
|
12
|
-
felis/db/sqltypes.py,sha256=yFlautQ1hv21MHF4AIfBp7_2m1-exKBfc76xYsMHBgk,5735
|
|
13
|
-
lsst_felis-27.2024.2200.dist-info/COPYRIGHT,sha256=bUmNy19uUxqITMpjeHFe69q3IzQpjxvvBw6oV7kR7ho,129
|
|
14
|
-
lsst_felis-27.2024.2200.dist-info/LICENSE,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
|
|
15
|
-
lsst_felis-27.2024.2200.dist-info/METADATA,sha256=FiVIJ3ddBfqH4kcd6YwoiYo-0hcPU_ZR-KMhqEcCztY,1191
|
|
16
|
-
lsst_felis-27.2024.2200.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
17
|
-
lsst_felis-27.2024.2200.dist-info/entry_points.txt,sha256=Gk2XFujA_Gp52VBk45g5kim8TDoMDJFPctsMqiq72EM,40
|
|
18
|
-
lsst_felis-27.2024.2200.dist-info/top_level.txt,sha256=F4SvPip3iZRVyISi50CHhwTIAokAhSxjWiVcn4IVWRI,6
|
|
19
|
-
lsst_felis-27.2024.2200.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
20
|
-
lsst_felis-27.2024.2200.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|