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.

Files changed (31) hide show
  1. {lsst_felis-27.2024.2700/python/lsst_felis.egg-info → lsst_felis-27.2024.2900}/PKG-INFO +1 -1
  2. {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/README.rst +1 -1
  3. {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/python/felis/datamodel.py +20 -0
  4. {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/python/felis/db/dialects.py +2 -2
  5. {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/python/felis/db/sqltypes.py +8 -15
  6. {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/python/felis/db/utils.py +1 -1
  7. {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/python/felis/metadata.py +34 -6
  8. lsst_felis-27.2024.2900/python/felis/version.py +2 -0
  9. {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900/python/lsst_felis.egg-info}/PKG-INFO +1 -1
  10. {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/tests/test_cli.py +10 -8
  11. {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/tests/test_datamodel.py +8 -1
  12. {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/tests/test_metadata.py +22 -0
  13. lsst_felis-27.2024.2700/python/felis/version.py +0 -2
  14. {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/COPYRIGHT +0 -0
  15. {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/LICENSE +0 -0
  16. {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/pyproject.toml +0 -0
  17. {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/python/felis/__init__.py +0 -0
  18. {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/python/felis/cli.py +0 -0
  19. {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/python/felis/db/__init__.py +0 -0
  20. {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/python/felis/db/variants.py +0 -0
  21. {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/python/felis/py.typed +0 -0
  22. {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/python/felis/tap.py +0 -0
  23. {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/python/felis/types.py +0 -0
  24. {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/python/lsst_felis.egg-info/SOURCES.txt +0 -0
  25. {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/python/lsst_felis.egg-info/dependency_links.txt +0 -0
  26. {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/python/lsst_felis.egg-info/entry_points.txt +0 -0
  27. {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/python/lsst_felis.egg-info/requires.txt +0 -0
  28. {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/python/lsst_felis.egg-info/top_level.txt +0 -0
  29. {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/python/lsst_felis.egg-info/zip-safe +0 -0
  30. {lsst_felis-27.2024.2700 → lsst_felis-27.2024.2900}/setup.cfg +0 -0
  31. {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.2700
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, Oracle, and SQLite. The schema can also
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, ORACLE, POSTGRES, SQLITE
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, ORACLE)
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, oracle, postgresql
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, ORACLE: oracle.NUMBER(1), POSTGRES: postgresql.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
- datatype = datatype_fun(column_obj.length, **variant_dict)
83
- else:
84
- datatype = datatype_fun(**variant_dict)
85
- return datatype
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")
@@ -0,0 +1,2 @@
1
+ __all__ = ["__version__"]
2
+ __version__ = "27.2024.2900"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lsst-felis
3
- Version: 27.2024.2700
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 for ``load-tap --dry-run`` command."""
94
- url = "postgresql+psycopg2://"
95
-
96
- runner = CliRunner()
97
- result = runner.invoke(
98
- cli, ["load-tap", f"--engine-url={url}", "--dry-run", TEST_YAML], catch_exceptions=False
99
- )
100
- self.assertEqual(result.exit_code, 0)
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", "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()
@@ -1,2 +0,0 @@
1
- __all__ = ["__version__"]
2
- __version__ = "27.2024.2700"