lsst-felis 27.2024.2700__tar.gz → 27.2024.2900__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-27.2024.2700/python/lsst_felis.egg-info → lsst_felis-27.2024.2900}/PKG-INFO +1 -1
- {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/README.rst +1 -1
- {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/python/felis/datamodel.py +20 -0
- {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/python/felis/db/dialects.py +2 -2
- {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/python/felis/db/sqltypes.py +8 -15
- {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/python/felis/db/utils.py +1 -1
- {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/python/felis/metadata.py +34 -6
- lsst_felis-27.2024.2900/python/felis/version.py +2 -0
- {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900/python/lsst_felis.egg-info}/PKG-INFO +1 -1
- {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/tests/test_cli.py +10 -8
- {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/tests/test_datamodel.py +8 -1
- {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/tests/test_metadata.py +22 -0
- lsst_felis-27.2024.2700/python/felis/version.py +0 -2
- {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/COPYRIGHT +0 -0
- {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/LICENSE +0 -0
- {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/pyproject.toml +0 -0
- {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/python/felis/__init__.py +0 -0
- {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/python/felis/cli.py +0 -0
- {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/python/felis/db/__init__.py +0 -0
- {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/python/felis/db/variants.py +0 -0
- {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/python/felis/py.typed +0 -0
- {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/python/felis/tap.py +0 -0
- {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/python/felis/types.py +0 -0
- {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/python/lsst_felis.egg-info/SOURCES.txt +0 -0
- {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/python/lsst_felis.egg-info/dependency_links.txt +0 -0
- {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/python/lsst_felis.egg-info/entry_points.txt +0 -0
- {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/python/lsst_felis.egg-info/requires.txt +0 -0
- {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/python/lsst_felis.egg-info/top_level.txt +0 -0
- {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/python/lsst_felis.egg-info/zip-safe +0 -0
- {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/setup.cfg +0 -0
- {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/tests/test_tap.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: lsst-felis
|
|
3
|
-
Version: 27.2024.
|
|
3
|
+
Version: 27.2024.2900
|
|
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,7 +23,7 @@ validity using an internal `Pydantic <https://docs.pydantic.dev/latest/>`_ data
|
|
|
23
23
|
model to ensure strict conformance to the format. SQL Data Definition language
|
|
24
24
|
(DDL) statements can be generated to instantiate corresponding database
|
|
25
25
|
objects, such as tables and columns, in a number of different database
|
|
26
|
-
variants, including MySQL, PostgreSQL,
|
|
26
|
+
variants, including MySQL, PostgreSQL, and SQLite. The schema can also
|
|
27
27
|
be used to update the TAP schema information in a
|
|
28
28
|
`TAP <https://www.ivoa.net/documents/TAP/>`_ service.
|
|
29
29
|
|
|
@@ -141,6 +141,13 @@ class Column(BaseObject):
|
|
|
141
141
|
length: int | None = Field(None, gt=0)
|
|
142
142
|
"""Length of the column."""
|
|
143
143
|
|
|
144
|
+
precision: int | None = Field(None, ge=0)
|
|
145
|
+
"""The numerical precision of the column.
|
|
146
|
+
|
|
147
|
+
For timestamps, this is the number of fractional digits retained in the
|
|
148
|
+
seconds field.
|
|
149
|
+
"""
|
|
150
|
+
|
|
144
151
|
nullable: bool = True
|
|
145
152
|
"""Whether the column can be ``NULL``."""
|
|
146
153
|
|
|
@@ -363,6 +370,19 @@ class Column(BaseObject):
|
|
|
363
370
|
)
|
|
364
371
|
return self
|
|
365
372
|
|
|
373
|
+
@model_validator(mode="after")
|
|
374
|
+
def check_precision(self) -> Column:
|
|
375
|
+
"""Check that precision is only valid for timestamp columns.
|
|
376
|
+
|
|
377
|
+
Returns
|
|
378
|
+
-------
|
|
379
|
+
`Column`
|
|
380
|
+
The column being validated.
|
|
381
|
+
"""
|
|
382
|
+
if self.precision is not None and self.datatype != "timestamp":
|
|
383
|
+
raise ValueError("Precision is only valid for timestamp columns")
|
|
384
|
+
return self
|
|
385
|
+
|
|
366
386
|
|
|
367
387
|
class Constraint(BaseObject):
|
|
368
388
|
"""Table constraint model."""
|
|
@@ -30,11 +30,11 @@ from sqlalchemy import dialects
|
|
|
30
30
|
from sqlalchemy.engine import Dialect
|
|
31
31
|
from sqlalchemy.engine.mock import create_mock_engine
|
|
32
32
|
|
|
33
|
-
from .sqltypes import MYSQL,
|
|
33
|
+
from .sqltypes import MYSQL, POSTGRES, SQLITE
|
|
34
34
|
|
|
35
35
|
__all__ = ["get_supported_dialects", "get_dialect_module"]
|
|
36
36
|
|
|
37
|
-
_DIALECT_NAMES = (MYSQL, POSTGRES, SQLITE
|
|
37
|
+
_DIALECT_NAMES = (MYSQL, POSTGRES, SQLITE)
|
|
38
38
|
"""List of supported dialect names.
|
|
39
39
|
|
|
40
40
|
This list is used to create the dialect and module dictionaries.
|
|
@@ -28,7 +28,7 @@ from collections.abc import Callable, Mapping
|
|
|
28
28
|
from typing import Any
|
|
29
29
|
|
|
30
30
|
from sqlalchemy import SmallInteger, types
|
|
31
|
-
from sqlalchemy.dialects import mysql,
|
|
31
|
+
from sqlalchemy.dialects import mysql, postgresql
|
|
32
32
|
from sqlalchemy.ext.compiler import compiles
|
|
33
33
|
|
|
34
34
|
__all__ = [
|
|
@@ -49,7 +49,6 @@ __all__ = [
|
|
|
49
49
|
]
|
|
50
50
|
|
|
51
51
|
MYSQL = "mysql"
|
|
52
|
-
ORACLE = "oracle"
|
|
53
52
|
POSTGRES = "postgresql"
|
|
54
53
|
SQLITE = "sqlite"
|
|
55
54
|
|
|
@@ -88,21 +87,18 @@ def compile_tinyint(type_: Any, compiler: Any, **kwargs: Any) -> str:
|
|
|
88
87
|
|
|
89
88
|
_TypeMap = Mapping[str, types.TypeEngine | type[types.TypeEngine]]
|
|
90
89
|
|
|
91
|
-
boolean_map: _TypeMap = {MYSQL: mysql.BOOLEAN,
|
|
90
|
+
boolean_map: _TypeMap = {MYSQL: mysql.BOOLEAN, POSTGRES: postgresql.BOOLEAN()}
|
|
92
91
|
|
|
93
92
|
byte_map: _TypeMap = {
|
|
94
93
|
MYSQL: mysql.TINYINT(),
|
|
95
|
-
ORACLE: oracle.NUMBER(3),
|
|
96
94
|
POSTGRES: postgresql.SMALLINT(),
|
|
97
95
|
}
|
|
98
96
|
|
|
99
97
|
short_map: _TypeMap = {
|
|
100
98
|
MYSQL: mysql.SMALLINT(),
|
|
101
|
-
ORACLE: oracle.NUMBER(5),
|
|
102
99
|
POSTGRES: postgresql.SMALLINT(),
|
|
103
100
|
}
|
|
104
101
|
|
|
105
|
-
# Skip Oracle
|
|
106
102
|
int_map: _TypeMap = {
|
|
107
103
|
MYSQL: mysql.INTEGER(),
|
|
108
104
|
POSTGRES: postgresql.INTEGER(),
|
|
@@ -110,52 +106,49 @@ int_map: _TypeMap = {
|
|
|
110
106
|
|
|
111
107
|
long_map: _TypeMap = {
|
|
112
108
|
MYSQL: mysql.BIGINT(),
|
|
113
|
-
ORACLE: oracle.NUMBER(38, 0),
|
|
114
109
|
POSTGRES: postgresql.BIGINT(),
|
|
115
110
|
}
|
|
116
111
|
|
|
117
112
|
float_map: _TypeMap = {
|
|
118
113
|
MYSQL: mysql.FLOAT(),
|
|
119
|
-
ORACLE: oracle.BINARY_FLOAT(),
|
|
120
114
|
POSTGRES: postgresql.FLOAT(),
|
|
121
115
|
}
|
|
122
116
|
|
|
123
117
|
double_map: _TypeMap = {
|
|
124
118
|
MYSQL: mysql.DOUBLE(),
|
|
125
|
-
ORACLE: oracle.BINARY_DOUBLE(),
|
|
126
119
|
POSTGRES: postgresql.DOUBLE_PRECISION(),
|
|
127
120
|
}
|
|
128
121
|
|
|
129
122
|
char_map: _TypeMap = {
|
|
130
123
|
MYSQL: mysql.CHAR,
|
|
131
|
-
ORACLE: oracle.CHAR,
|
|
132
124
|
POSTGRES: postgresql.CHAR,
|
|
133
125
|
}
|
|
134
126
|
|
|
135
127
|
string_map: _TypeMap = {
|
|
136
128
|
MYSQL: mysql.VARCHAR,
|
|
137
|
-
ORACLE: oracle.VARCHAR2,
|
|
138
129
|
POSTGRES: postgresql.VARCHAR,
|
|
139
130
|
}
|
|
140
131
|
|
|
141
132
|
unicode_map: _TypeMap = {
|
|
142
133
|
MYSQL: mysql.NVARCHAR,
|
|
143
|
-
ORACLE: oracle.NVARCHAR2,
|
|
144
134
|
POSTGRES: postgresql.VARCHAR,
|
|
145
135
|
}
|
|
146
136
|
|
|
147
137
|
text_map: _TypeMap = {
|
|
148
138
|
MYSQL: mysql.LONGTEXT,
|
|
149
|
-
ORACLE: oracle.CLOB,
|
|
150
139
|
POSTGRES: postgresql.TEXT,
|
|
151
140
|
}
|
|
152
141
|
|
|
153
142
|
binary_map: _TypeMap = {
|
|
154
143
|
MYSQL: mysql.LONGBLOB,
|
|
155
|
-
ORACLE: oracle.BLOB,
|
|
156
144
|
POSTGRES: postgresql.BYTEA,
|
|
157
145
|
}
|
|
158
146
|
|
|
147
|
+
timestamp_map: _TypeMap = {
|
|
148
|
+
MYSQL: mysql.DATETIME(timezone=False),
|
|
149
|
+
POSTGRES: postgresql.TIMESTAMP(timezone=False),
|
|
150
|
+
}
|
|
151
|
+
|
|
159
152
|
|
|
160
153
|
def boolean(**kwargs: Any) -> types.TypeEngine:
|
|
161
154
|
"""Get the SQL type for Felis `~felis.types.Boolean` with variants.
|
|
@@ -370,7 +363,7 @@ def timestamp(**kwargs: Any) -> types.TypeEngine:
|
|
|
370
363
|
`~sqlalchemy.types.TypeEngine`
|
|
371
364
|
The SQL type for a Felis timestamp.
|
|
372
365
|
"""
|
|
373
|
-
return types.TIMESTAMP()
|
|
366
|
+
return _vary(types.TIMESTAMP(timezone=False), timestamp_map, kwargs)
|
|
374
367
|
|
|
375
368
|
|
|
376
369
|
def get_type_func(type_name: str) -> Callable:
|
|
@@ -299,6 +299,6 @@ class DatabaseContext:
|
|
|
299
299
|
The mock connection object.
|
|
300
300
|
"""
|
|
301
301
|
writer = SQLWriter(output_file)
|
|
302
|
-
engine = create_mock_engine(engine_url, executor=writer.write)
|
|
302
|
+
engine = create_mock_engine(engine_url, executor=writer.write, paramstyle="pyformat")
|
|
303
303
|
writer.dialect = engine.dialect
|
|
304
304
|
return engine
|
|
@@ -40,6 +40,7 @@ from sqlalchemy import (
|
|
|
40
40
|
UniqueConstraint,
|
|
41
41
|
text,
|
|
42
42
|
)
|
|
43
|
+
from sqlalchemy.dialects import mysql, postgresql
|
|
43
44
|
from sqlalchemy.types import TypeEngine
|
|
44
45
|
|
|
45
46
|
from felis.datamodel import Schema
|
|
@@ -54,6 +55,28 @@ __all__ = ("MetaDataBuilder", "get_datatype_with_variants")
|
|
|
54
55
|
logger = logging.getLogger(__name__)
|
|
55
56
|
|
|
56
57
|
|
|
58
|
+
def _handle_timestamp_column(column_obj: datamodel.Column, variant_dict: dict[str, TypeEngine[Any]]) -> None:
|
|
59
|
+
"""Handle columns with the timestamp datatype.
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
column_obj
|
|
64
|
+
The column object representing the timestamp.
|
|
65
|
+
variant_dict
|
|
66
|
+
The dictionary of variant overrides for the datatype.
|
|
67
|
+
|
|
68
|
+
Notes
|
|
69
|
+
-----
|
|
70
|
+
This function updates the variant dictionary with the appropriate
|
|
71
|
+
timestamp type for the column object but only if the precision is set.
|
|
72
|
+
Otherwise, the default timestamp objects defined in the Felis type system
|
|
73
|
+
will be used instead.
|
|
74
|
+
"""
|
|
75
|
+
if column_obj.precision is not None:
|
|
76
|
+
args: Any = [False, column_obj.precision] # Turn off timezone.
|
|
77
|
+
variant_dict.update({"postgresql": postgresql.TIMESTAMP(*args), "mysql": mysql.DATETIME(*args)})
|
|
78
|
+
|
|
79
|
+
|
|
57
80
|
def get_datatype_with_variants(column_obj: datamodel.Column) -> TypeEngine:
|
|
58
81
|
"""Use the Felis type system to get a SQLAlchemy datatype with variant
|
|
59
82
|
overrides from the information in a Felis column object.
|
|
@@ -71,18 +94,23 @@ def get_datatype_with_variants(column_obj: datamodel.Column) -> TypeEngine:
|
|
|
71
94
|
Raises
|
|
72
95
|
------
|
|
73
96
|
ValueError
|
|
74
|
-
If the column has a sized type but no length
|
|
97
|
+
If the column has a sized type but no length or if the datatype is
|
|
98
|
+
invalid.
|
|
75
99
|
"""
|
|
76
100
|
variant_dict = make_variant_dict(column_obj)
|
|
77
101
|
felis_type = FelisType.felis_type(column_obj.datatype.value)
|
|
78
|
-
datatype_fun = getattr(sqltypes, column_obj.datatype.value)
|
|
102
|
+
datatype_fun = getattr(sqltypes, column_obj.datatype.value, None)
|
|
103
|
+
if datatype_fun is None:
|
|
104
|
+
raise ValueError(f"Unknown datatype: {column_obj.datatype.value}")
|
|
105
|
+
args = []
|
|
79
106
|
if felis_type.is_sized:
|
|
107
|
+
# Add length argument for size types.
|
|
80
108
|
if not column_obj.length:
|
|
81
109
|
raise ValueError(f"Column {column_obj.name} has sized type '{column_obj.datatype}' but no length")
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return
|
|
110
|
+
args = [column_obj.length]
|
|
111
|
+
if felis_type.is_timestamp:
|
|
112
|
+
_handle_timestamp_column(column_obj, variant_dict)
|
|
113
|
+
return datatype_fun(*args, **variant_dict)
|
|
86
114
|
|
|
87
115
|
|
|
88
116
|
_VALID_SERVER_DEFAULTS = ("CURRENT_TIMESTAMP", "NOW()", "LOCALTIMESTAMP", "NULL")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: lsst-felis
|
|
3
|
-
Version: 27.2024.
|
|
3
|
+
Version: 27.2024.2900
|
|
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+)
|
|
@@ -27,6 +27,7 @@ import unittest
|
|
|
27
27
|
from click.testing import CliRunner
|
|
28
28
|
|
|
29
29
|
from felis.cli import cli
|
|
30
|
+
from felis.db.dialects import get_supported_dialects
|
|
30
31
|
|
|
31
32
|
TESTDIR = os.path.abspath(os.path.dirname(__file__))
|
|
32
33
|
TEST_YAML = os.path.join(TESTDIR, "data", "test.yml")
|
|
@@ -90,14 +91,15 @@ class CliTestCase(unittest.TestCase):
|
|
|
90
91
|
self.assertEqual(result.exit_code, 0)
|
|
91
92
|
|
|
92
93
|
def test_load_tap_mock(self) -> None:
|
|
93
|
-
"""Test
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
94
|
+
"""Test ``load-tap --dry-run`` command on supported dialects."""
|
|
95
|
+
urls = [f"{dialect_name}://" for dialect_name in get_supported_dialects().keys()]
|
|
96
|
+
|
|
97
|
+
for url in urls:
|
|
98
|
+
runner = CliRunner()
|
|
99
|
+
result = runner.invoke(
|
|
100
|
+
cli, ["load-tap", f"--engine-url={url}", "--dry-run", TEST_YAML], catch_exceptions=False
|
|
101
|
+
)
|
|
102
|
+
self.assertEqual(result.exit_code, 0)
|
|
101
103
|
|
|
102
104
|
def test_validate_default(self) -> None:
|
|
103
105
|
"""Test validate command."""
|
|
@@ -664,7 +664,7 @@ class RedundantDatatypesTest(unittest.TestCase):
|
|
|
664
664
|
coldata.col("unicode", "NVARCHAR", length=32)
|
|
665
665
|
|
|
666
666
|
with self.assertRaises(ValidationError):
|
|
667
|
-
coldata.col("timestamp", "
|
|
667
|
+
coldata.col("timestamp", "DATETIME")
|
|
668
668
|
|
|
669
669
|
# DM-42257: Felis does not handle unbounded text types properly.
|
|
670
670
|
# coldata.col("text", "TEXT", length=32)
|
|
@@ -689,6 +689,13 @@ class RedundantDatatypesTest(unittest.TestCase):
|
|
|
689
689
|
coldata.col("string", "CHAR", length=32)
|
|
690
690
|
coldata.col("unicode", "CHAR", length=32)
|
|
691
691
|
|
|
692
|
+
def test_precision(self) -> None:
|
|
693
|
+
"""Test that precision is not allowed for datatypes other than
|
|
694
|
+
timestamp.
|
|
695
|
+
"""
|
|
696
|
+
with self.assertRaises(ValidationError):
|
|
697
|
+
Column(**{"name": "testColumn", "@id": "#test_col_id", "datatype": "double", "precision": 6})
|
|
698
|
+
|
|
692
699
|
|
|
693
700
|
if __name__ == "__main__":
|
|
694
701
|
unittest.main()
|
|
@@ -188,6 +188,28 @@ class MetaDataTestCase(unittest.TestCase):
|
|
|
188
188
|
for primary_key in primary_keys:
|
|
189
189
|
self.assertTrue(md_table.columns[primary_key].primary_key)
|
|
190
190
|
|
|
191
|
+
def test_timestamp(self):
|
|
192
|
+
"""Test that the `timestamp` datatype is created correctly."""
|
|
193
|
+
for precision in [None, 6]:
|
|
194
|
+
col = dm.Column(
|
|
195
|
+
**{
|
|
196
|
+
"name": "timestamp_test",
|
|
197
|
+
"id": "#timestamp_test",
|
|
198
|
+
"datatype": "timestamp",
|
|
199
|
+
"precision": precision,
|
|
200
|
+
}
|
|
201
|
+
)
|
|
202
|
+
datatype = get_datatype_with_variants(col)
|
|
203
|
+
variant_dict = datatype._variant_mapping
|
|
204
|
+
self.assertTrue("mysql" in variant_dict)
|
|
205
|
+
self.assertTrue("postgresql" in variant_dict)
|
|
206
|
+
pg_timestamp = variant_dict["postgresql"]
|
|
207
|
+
self.assertEqual(pg_timestamp.timezone, False)
|
|
208
|
+
self.assertEqual(pg_timestamp.precision, precision)
|
|
209
|
+
mysql_timestamp = variant_dict["mysql"]
|
|
210
|
+
self.assertEqual(mysql_timestamp.timezone, False)
|
|
211
|
+
self.assertEqual(mysql_timestamp.fsp, precision)
|
|
212
|
+
|
|
191
213
|
|
|
192
214
|
if __name__ == "__main__":
|
|
193
215
|
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/python/lsst_felis.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/python/lsst_felis.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
{lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/python/lsst_felis.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|