lsst-felis 26.2024.900__py3-none-any.whl → 29.2025.4500__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.
Files changed (39) hide show
  1. felis/__init__.py +10 -24
  2. felis/cli.py +437 -341
  3. felis/config/tap_schema/columns.csv +33 -0
  4. felis/config/tap_schema/key_columns.csv +8 -0
  5. felis/config/tap_schema/keys.csv +8 -0
  6. felis/config/tap_schema/schemas.csv +2 -0
  7. felis/config/tap_schema/tables.csv +6 -0
  8. felis/config/tap_schema/tap_schema_std.yaml +273 -0
  9. felis/datamodel.py +1386 -193
  10. felis/db/dialects.py +116 -0
  11. felis/db/schema.py +62 -0
  12. felis/db/sqltypes.py +275 -48
  13. felis/db/utils.py +409 -0
  14. felis/db/variants.py +159 -0
  15. felis/diff.py +234 -0
  16. felis/metadata.py +385 -0
  17. felis/tap_schema.py +767 -0
  18. felis/tests/__init__.py +0 -0
  19. felis/tests/postgresql.py +134 -0
  20. felis/tests/run_cli.py +79 -0
  21. felis/types.py +57 -9
  22. lsst_felis-29.2025.4500.dist-info/METADATA +38 -0
  23. lsst_felis-29.2025.4500.dist-info/RECORD +31 -0
  24. {lsst_felis-26.2024.900.dist-info → lsst_felis-29.2025.4500.dist-info}/WHEEL +1 -1
  25. {lsst_felis-26.2024.900.dist-info → lsst_felis-29.2025.4500.dist-info/licenses}/COPYRIGHT +1 -1
  26. felis/check.py +0 -381
  27. felis/simple.py +0 -424
  28. felis/sql.py +0 -275
  29. felis/tap.py +0 -433
  30. felis/utils.py +0 -100
  31. felis/validation.py +0 -103
  32. felis/version.py +0 -2
  33. felis/visitor.py +0 -180
  34. lsst_felis-26.2024.900.dist-info/METADATA +0 -28
  35. lsst_felis-26.2024.900.dist-info/RECORD +0 -23
  36. {lsst_felis-26.2024.900.dist-info → lsst_felis-29.2025.4500.dist-info}/entry_points.txt +0 -0
  37. {lsst_felis-26.2024.900.dist-info → lsst_felis-29.2025.4500.dist-info/licenses}/LICENSE +0 -0
  38. {lsst_felis-26.2024.900.dist-info → lsst_felis-29.2025.4500.dist-info}/top_level.txt +0 -0
  39. {lsst_felis-26.2024.900.dist-info → lsst_felis-29.2025.4500.dist-info}/zip-safe +0 -0
felis/db/dialects.py ADDED
@@ -0,0 +1,116 @@
1
+ """Get SQLAlchemy dialects and their type modules."""
2
+
3
+ # This file is part of felis.
4
+ #
5
+ # Developed for the LSST Data Management System.
6
+ # This product includes software developed by the LSST Project
7
+ # (https://www.lsst.org).
8
+ # See the COPYRIGHT file at the top-level directory of this distribution
9
+ # for details of code ownership.
10
+ #
11
+ # This program is free software: you can redistribute it and/or modify
12
+ # it under the terms of the GNU General Public License as published by
13
+ # the Free Software Foundation, either version 3 of the License, or
14
+ # (at your option) any later version.
15
+ #
16
+ # This program is distributed in the hope that it will be useful,
17
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
18
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19
+ # GNU General Public License for more details.
20
+ #
21
+ # You should have received a copy of the GNU General Public License
22
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
23
+
24
+ from __future__ import annotations
25
+
26
+ from collections.abc import Mapping
27
+ from types import MappingProxyType, ModuleType
28
+
29
+ from sqlalchemy import dialects
30
+ from sqlalchemy.engine import Dialect
31
+ from sqlalchemy.engine.mock import create_mock_engine
32
+
33
+ from .sqltypes import MYSQL, POSTGRES, SQLITE
34
+
35
+ __all__ = ["get_dialect_module", "get_supported_dialects"]
36
+
37
+ _DIALECT_NAMES = (MYSQL, POSTGRES, SQLITE)
38
+ """List of supported dialect names.
39
+
40
+ This list is used to create the dialect and module dictionaries.
41
+ """
42
+
43
+
44
+ def _dialect(dialect_name: str) -> Dialect:
45
+ """Create the SQLAlchemy dialect for the given name using a mock engine.
46
+
47
+ Parameters
48
+ ----------
49
+ dialect_name
50
+ The name of the dialect to create.
51
+
52
+ Returns
53
+ -------
54
+ `~sqlalchemy.engine.Dialect`
55
+ The SQLAlchemy dialect.
56
+ """
57
+ return create_mock_engine(f"{dialect_name}://", executor=None).dialect
58
+
59
+
60
+ _DIALECTS = MappingProxyType({name: _dialect(name) for name in _DIALECT_NAMES})
61
+ """Dictionary of dialect names to SQLAlchemy dialects."""
62
+
63
+
64
+ def get_supported_dialects() -> Mapping[str, Dialect]:
65
+ """Get a dictionary of the supported SQLAlchemy dialects.
66
+
67
+ Returns
68
+ -------
69
+ `dict` [ `str`, `~sqlalchemy.engine.Dialect`]
70
+ A dictionary of the supported SQLAlchemy dialects.
71
+
72
+ Notes
73
+ -----
74
+ The dictionary is keyed by the dialect name and the value is the SQLAlchemy
75
+ dialect object. This function is intended as the primary interface for
76
+ getting the supported dialects.
77
+ """
78
+ return _DIALECTS
79
+
80
+
81
+ def _dialect_module(dialect_name: str) -> ModuleType:
82
+ """Get the SQLAlchemy dialect module for the given name.
83
+
84
+ Parameters
85
+ ----------
86
+ dialect_name
87
+ The name of the dialect module to get from the SQLAlchemy package.
88
+ """
89
+ return getattr(dialects, dialect_name)
90
+
91
+
92
+ _DIALECT_MODULES = MappingProxyType({name: _dialect_module(name) for name in _DIALECT_NAMES})
93
+ """Dictionary of dialect names to SQLAlchemy modules."""
94
+
95
+
96
+ def get_dialect_module(dialect_name: str) -> ModuleType:
97
+ """Get the SQLAlchemy dialect module for the given name.
98
+
99
+ Parameters
100
+ ----------
101
+ dialect_name
102
+ The name of the dialect module to get from the SQLAlchemy package.
103
+
104
+ Returns
105
+ -------
106
+ `~types.ModuleType`
107
+ The SQLAlchemy dialect module.
108
+
109
+ Raises
110
+ ------
111
+ ValueError
112
+ Raised if the dialect name is not supported.
113
+ """
114
+ if dialect_name not in _DIALECT_MODULES:
115
+ raise ValueError(f"Unsupported dialect: {dialect_name}")
116
+ return _DIALECT_MODULES[dialect_name]
felis/db/schema.py ADDED
@@ -0,0 +1,62 @@
1
+ """Database utilities for Felis schemas."""
2
+
3
+ # This file is part of felis.
4
+ #
5
+ # Developed for the LSST Data Management System.
6
+ # This product includes software developed by the LSST Project
7
+ # (https://www.lsst.org).
8
+ # See the COPYRIGHT file at the top-level directory of this distribution
9
+ # for details of code ownership.
10
+ #
11
+ # This program is free software: you can redistribute it and/or modify
12
+ # it under the terms of the GNU General Public License as published by
13
+ # the Free Software Foundation, either version 3 of the License, or
14
+ # (at your option) any later version.
15
+ #
16
+ # This program is distributed in the hope that it will be useful,
17
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
18
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19
+ # GNU General Public License for more details.
20
+ #
21
+ # You should have received a copy of the GNU General Public License
22
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
23
+
24
+ from sqlalchemy import Engine, create_engine
25
+
26
+ from ..datamodel import Schema
27
+ from ..metadata import MetaDataBuilder
28
+ from .utils import DatabaseContext
29
+
30
+ __all__ = ["create_database"]
31
+
32
+
33
+ def create_database(schema: Schema, engine_or_url_str: Engine | str | None = None) -> DatabaseContext:
34
+ """
35
+ Create a database from the specified `Schema`.
36
+
37
+ Parameters
38
+ ----------
39
+ schema
40
+ The schema to create.
41
+ engine_or_url_str
42
+ The SQLAlchemy engine or URL to use for database creation.
43
+ If None, an in-memory SQLite database will be created.
44
+
45
+ Returns
46
+ -------
47
+ `DatabaseContext`
48
+ The database context object.
49
+ """
50
+ if engine_or_url_str is not None:
51
+ engine = (
52
+ engine_or_url_str if isinstance(engine_or_url_str, Engine) else create_engine(engine_or_url_str)
53
+ )
54
+ else:
55
+ engine = create_engine("sqlite:///:memory:")
56
+ metadata = MetaDataBuilder(
57
+ schema, apply_schema_to_metadata=False if engine.url.drivername == "sqlite" else True
58
+ ).build()
59
+ ctx = DatabaseContext(metadata, engine)
60
+ ctx.initialize()
61
+ ctx.create_all()
62
+ return ctx
felis/db/sqltypes.py CHANGED
@@ -1,3 +1,5 @@
1
+ """Map Felis types to SQLAlchemy types."""
2
+
1
3
  # This file is part of felis.
2
4
  #
3
5
  # Developed for the LSST Data Management System.
@@ -19,16 +21,34 @@
19
21
  # You should have received a copy of the GNU General Public License
20
22
  # along with this program. If not, see <https://www.gnu.org/licenses/>.
21
23
 
24
+ from __future__ import annotations
25
+
22
26
  import builtins
23
- from collections.abc import Mapping
27
+ from collections.abc import Callable, Mapping
24
28
  from typing import Any
25
29
 
26
- from sqlalchemy import Float, SmallInteger, types
27
- from sqlalchemy.dialects import mysql, oracle, postgresql
30
+ from sqlalchemy import SmallInteger, types
31
+ from sqlalchemy.dialects import mysql, postgresql
28
32
  from sqlalchemy.ext.compiler import compiles
29
33
 
34
+ __all__ = [
35
+ "binary",
36
+ "boolean",
37
+ "byte",
38
+ "char",
39
+ "double",
40
+ "float",
41
+ "get_type_func",
42
+ "int",
43
+ "long",
44
+ "short",
45
+ "string",
46
+ "text",
47
+ "timestamp",
48
+ "unicode",
49
+ ]
50
+
30
51
  MYSQL = "mysql"
31
- ORACLE = "oracle"
32
52
  POSTGRES = "postgresql"
33
53
  SQLITE = "sqlite"
34
54
 
@@ -39,41 +59,46 @@ class TINYINT(SmallInteger):
39
59
  __visit_name__ = "TINYINT"
40
60
 
41
61
 
42
- class DOUBLE(Float):
43
- """The non-standard DOUBLE type."""
44
-
45
- __visit_name__ = "DOUBLE"
46
-
47
-
48
62
  @compiles(TINYINT)
49
- def compile_tinyint(type_: Any, compiler: Any, **kw: Any) -> str:
50
- """Return type name for TINYINT."""
63
+ def compile_tinyint(type_: Any, compiler: Any, **kwargs: Any) -> str:
64
+ """Compile the non-standard ``TINYINT`` type to SQL.
65
+
66
+ Parameters
67
+ ----------
68
+ type_
69
+ The type object.
70
+ compiler
71
+ The compiler object.
72
+ **kwargs
73
+ Additional keyword arguments.
74
+
75
+ Returns
76
+ -------
77
+ `str`
78
+ The compiled SQL for TINYINT.
79
+
80
+ Notes
81
+ -----
82
+ This function returns the SQL for the the TINYINT type. The function
83
+ signature and parameters are defined by SQLAlchemy.
84
+ """
51
85
  return "TINYINT"
52
86
 
53
87
 
54
- @compiles(DOUBLE)
55
- def compile_double(type_: Any, compiler: Any, **kw: Any) -> str:
56
- """Return type name for double precision type."""
57
- return "DOUBLE"
58
-
59
-
60
88
  _TypeMap = Mapping[str, types.TypeEngine | type[types.TypeEngine]]
61
89
 
62
- boolean_map: _TypeMap = {MYSQL: mysql.BIT(1), ORACLE: oracle.NUMBER(1), POSTGRES: postgresql.BOOLEAN()}
90
+ boolean_map: _TypeMap = {MYSQL: mysql.BOOLEAN, POSTGRES: postgresql.BOOLEAN()}
63
91
 
64
92
  byte_map: _TypeMap = {
65
93
  MYSQL: mysql.TINYINT(),
66
- ORACLE: oracle.NUMBER(3),
67
94
  POSTGRES: postgresql.SMALLINT(),
68
95
  }
69
96
 
70
97
  short_map: _TypeMap = {
71
98
  MYSQL: mysql.SMALLINT(),
72
- ORACLE: oracle.NUMBER(5),
73
99
  POSTGRES: postgresql.SMALLINT(),
74
100
  }
75
101
 
76
- # Skip Oracle
77
102
  int_map: _TypeMap = {
78
103
  MYSQL: mysql.INTEGER(),
79
104
  POSTGRES: postgresql.INTEGER(),
@@ -81,116 +106,293 @@ int_map: _TypeMap = {
81
106
 
82
107
  long_map: _TypeMap = {
83
108
  MYSQL: mysql.BIGINT(),
84
- ORACLE: oracle.NUMBER(38, 0),
85
109
  POSTGRES: postgresql.BIGINT(),
86
110
  }
87
111
 
88
112
  float_map: _TypeMap = {
89
113
  MYSQL: mysql.FLOAT(),
90
- ORACLE: oracle.BINARY_FLOAT(),
91
114
  POSTGRES: postgresql.FLOAT(),
92
115
  }
93
116
 
94
117
  double_map: _TypeMap = {
95
118
  MYSQL: mysql.DOUBLE(),
96
- ORACLE: oracle.BINARY_DOUBLE(),
97
119
  POSTGRES: postgresql.DOUBLE_PRECISION(),
98
120
  }
99
121
 
100
122
  char_map: _TypeMap = {
101
123
  MYSQL: mysql.CHAR,
102
- ORACLE: oracle.CHAR,
103
124
  POSTGRES: postgresql.CHAR,
104
125
  }
105
126
 
106
127
  string_map: _TypeMap = {
107
128
  MYSQL: mysql.VARCHAR,
108
- ORACLE: oracle.VARCHAR2,
109
129
  POSTGRES: postgresql.VARCHAR,
110
130
  }
111
131
 
112
132
  unicode_map: _TypeMap = {
113
133
  MYSQL: mysql.NVARCHAR,
114
- ORACLE: oracle.NVARCHAR2,
115
134
  POSTGRES: postgresql.VARCHAR,
116
135
  }
117
136
 
118
137
  text_map: _TypeMap = {
119
138
  MYSQL: mysql.LONGTEXT,
120
- ORACLE: oracle.CLOB,
121
139
  POSTGRES: postgresql.TEXT,
122
140
  }
123
141
 
124
142
  binary_map: _TypeMap = {
125
143
  MYSQL: mysql.LONGBLOB,
126
- ORACLE: oracle.BLOB,
127
144
  POSTGRES: postgresql.BYTEA,
128
145
  }
129
146
 
147
+ timestamp_map: _TypeMap = {
148
+ MYSQL: mysql.DATETIME(timezone=False),
149
+ POSTGRES: postgresql.TIMESTAMP(timezone=False),
150
+ }
151
+
130
152
 
131
153
  def boolean(**kwargs: Any) -> types.TypeEngine:
132
- """Return SQLAlchemy type for boolean."""
154
+ """Get the SQL type for Felis `~felis.types.Boolean` with variants.
155
+
156
+ Parameters
157
+ ----------
158
+ **kwargs
159
+ Additional keyword arguments to pass to the type object.
160
+
161
+ Returns
162
+ -------
163
+ `~sqlalchemy.types.TypeEngine`
164
+ The SQL type for a Felis boolean.
165
+ """
133
166
  return _vary(types.BOOLEAN(), boolean_map, kwargs)
134
167
 
135
168
 
136
169
  def byte(**kwargs: Any) -> types.TypeEngine:
137
- """Return SQLAlchemy type for byte."""
170
+ """Get the SQL type for Felis `~felis.types.Byte` with variants.
171
+
172
+ Parameters
173
+ ----------
174
+ **kwargs
175
+ Additional keyword arguments to pass to the type object.
176
+
177
+ Returns
178
+ -------
179
+ `~sqlalchemy.types.TypeEngine`
180
+ The SQL type for a Felis byte.
181
+ """
138
182
  return _vary(TINYINT(), byte_map, kwargs)
139
183
 
140
184
 
141
185
  def short(**kwargs: Any) -> types.TypeEngine:
142
- """Return SQLAlchemy type for short integer."""
186
+ """Get the SQL type for Felis `~felis.types.Short` with variants.
187
+
188
+ Parameters
189
+ ----------
190
+ **kwargs
191
+ Additional keyword arguments to pass to the type object.
192
+
193
+ Returns
194
+ -------
195
+ `~sqlalchemy.types.TypeEngine`
196
+ The SQL type for a Felis short.
197
+ """
143
198
  return _vary(types.SMALLINT(), short_map, kwargs)
144
199
 
145
200
 
146
201
  def int(**kwargs: Any) -> types.TypeEngine:
147
- """Return SQLAlchemy type for integer."""
202
+ """Get the SQL type for Felis `~felis.types.Int` with variants.
203
+
204
+ Parameters
205
+ ----------
206
+ **kwargs
207
+ Additional keyword arguments to pass to the type object.
208
+
209
+ Returns
210
+ -------
211
+ `~sqlalchemy.types.TypeEngine`
212
+ The SQL type for a Felis int.
213
+ """
148
214
  return _vary(types.INTEGER(), int_map, kwargs)
149
215
 
150
216
 
151
217
  def long(**kwargs: Any) -> types.TypeEngine:
152
- """Return SQLAlchemy type for long integer."""
218
+ """Get the SQL type for Felis `~felis.types.Long` with variants.
219
+
220
+ Parameters
221
+ ----------
222
+ **kwargs
223
+ Additional keyword arguments to pass to the type object.
224
+
225
+ Returns
226
+ -------
227
+ `~sqlalchemy.types.TypeEngine`
228
+ The SQL type for a Felis long.
229
+ """
153
230
  return _vary(types.BIGINT(), long_map, kwargs)
154
231
 
155
232
 
156
233
  def float(**kwargs: Any) -> types.TypeEngine:
157
- """Return SQLAlchemy type for single precision float."""
234
+ """Get the SQL type for Felis `~felis.types.Float` with variants.
235
+
236
+ Parameters
237
+ ----------
238
+ **kwargs
239
+ Additional keyword arguments to pass to the type object.
240
+
241
+ Returns
242
+ -------
243
+ `~sqlalchemy.types.TypeEngine`
244
+ The SQL type for a Felis float.
245
+ """
158
246
  return _vary(types.FLOAT(), float_map, kwargs)
159
247
 
160
248
 
161
249
  def double(**kwargs: Any) -> types.TypeEngine:
162
- """Return SQLAlchemy type for double precision float."""
163
- return _vary(DOUBLE(), double_map, kwargs)
250
+ """Get the SQL type for Felis `~felis.types.Double` with variants.
251
+
252
+ Parameters
253
+ ----------
254
+ **kwargs
255
+ Additional keyword arguments to pass to the type object.
256
+
257
+ Returns
258
+ -------
259
+ `~sqlalchemy.types.TypeEngine`
260
+ The SQL type for a Felis double.
261
+ """
262
+ return _vary(types.DOUBLE(), double_map, kwargs)
164
263
 
165
264
 
166
265
  def char(length: builtins.int, **kwargs: Any) -> types.TypeEngine:
167
- """Return SQLAlchemy type for character."""
266
+ """Get the SQL type for Felis `~felis.types.Char` with variants.
267
+
268
+ Parameters
269
+ ----------
270
+ length
271
+ The length of the character field.
272
+ **kwargs
273
+ Additional keyword arguments to pass to the type object.
274
+
275
+ Returns
276
+ -------
277
+ `~sqlalchemy.types.TypeEngine`
278
+ The SQL type for a Felis char.
279
+ """
168
280
  return _vary(types.CHAR(length), char_map, kwargs, length)
169
281
 
170
282
 
171
283
  def string(length: builtins.int, **kwargs: Any) -> types.TypeEngine:
172
- """Return SQLAlchemy type for string."""
284
+ """Get the SQL type for Felis `~felis.types.String` with variants.
285
+
286
+ Parameters
287
+ ----------
288
+ length
289
+ The length of the string field.
290
+ **kwargs
291
+ Additional keyword arguments to pass to the type object.
292
+
293
+ Returns
294
+ -------
295
+ `~sqlalchemy.types.TypeEngine`
296
+ The SQL type for a Felis string.
297
+ """
173
298
  return _vary(types.VARCHAR(length), string_map, kwargs, length)
174
299
 
175
300
 
176
301
  def unicode(length: builtins.int, **kwargs: Any) -> types.TypeEngine:
177
- """Return SQLAlchemy type for unicode string."""
302
+ """Get the SQL type for Felis `~felis.types.Unicode` with variants.
303
+
304
+ Parameters
305
+ ----------
306
+ length
307
+ The length of the unicode string field.
308
+ **kwargs
309
+ Additional keyword arguments to pass to the type object.
310
+
311
+ Returns
312
+ -------
313
+ `~sqlalchemy.types.TypeEngine`
314
+ The SQL type for a Felis unicode string.
315
+ """
178
316
  return _vary(types.NVARCHAR(length), unicode_map, kwargs, length)
179
317
 
180
318
 
181
- def text(length: builtins.int, **kwargs: Any) -> types.TypeEngine:
182
- """Return SQLAlchemy type for text."""
183
- return _vary(types.CLOB(length), text_map, kwargs, length)
319
+ def text(**kwargs: Any) -> types.TypeEngine:
320
+ """Get the SQL type for Felis `~felis.types.Text` with variants.
321
+
322
+ Parameters
323
+ ----------
324
+ **kwargs
325
+ Additional keyword arguments to pass to the type object.
326
+
327
+ Returns
328
+ -------
329
+ `~sqlalchemy.types.TypeEngine`
330
+ The SQL type for Felis text.
331
+ """
332
+ return _vary(types.TEXT(), text_map, kwargs)
184
333
 
185
334
 
186
335
  def binary(length: builtins.int, **kwargs: Any) -> types.TypeEngine:
187
- """Return SQLAlchemy type for binary."""
336
+ """Get the SQL type for Felis `~felis.types.Binary` with variants.
337
+
338
+ Parameters
339
+ ----------
340
+ length
341
+ The length of the binary field.
342
+ **kwargs
343
+ Additional keyword arguments to pass to the type object.
344
+
345
+ Returns
346
+ -------
347
+ `~sqlalchemy.types.TypeEngine`
348
+ The SQL type for Felis binary.
349
+ """
188
350
  return _vary(types.BLOB(length), binary_map, kwargs, length)
189
351
 
190
352
 
191
353
  def timestamp(**kwargs: Any) -> types.TypeEngine:
192
- """Return SQLAlchemy type for timestamp."""
193
- return types.TIMESTAMP()
354
+ """Get the SQL type for Felis `~felis.types.Timestamp` with variants.
355
+
356
+ Parameters
357
+ ----------
358
+ **kwargs
359
+ Additional keyword arguments to pass to the type object.
360
+
361
+ Returns
362
+ -------
363
+ `~sqlalchemy.types.TypeEngine`
364
+ The SQL type for a Felis timestamp.
365
+ """
366
+ return _vary(types.TIMESTAMP(timezone=False), timestamp_map, kwargs)
367
+
368
+
369
+ def get_type_func(type_name: str) -> Callable:
370
+ """Find the function which creates a specific SQL type by its Felis type
371
+ name.
372
+
373
+ Parameters
374
+ ----------
375
+ type_name
376
+ The name of the type function to get.
377
+
378
+ Returns
379
+ -------
380
+ `Callable`
381
+ The function for the type.
382
+
383
+ Raises
384
+ ------
385
+ ValueError
386
+ Raised if the type name is not recognized.
387
+
388
+ Notes
389
+ -----
390
+ This maps the type name to the function that creates the SQL type. This is
391
+ the main way to get the type functions from the type names.
392
+ """
393
+ if type_name not in globals():
394
+ raise ValueError(f"Unknown type: {type_name}")
395
+ return globals()[type_name]
194
396
 
195
397
 
196
398
  def _vary(
@@ -199,11 +401,36 @@ def _vary(
199
401
  overrides: _TypeMap,
200
402
  *args: Any,
201
403
  ) -> types.TypeEngine:
404
+ """Add datatype variants and overrides to a SQLAlchemy type.
405
+
406
+ Parameters
407
+ ----------
408
+ type_
409
+ The base SQLAlchemy type object. This is essentially a default
410
+ SQLAlchemy ``TypeEngine`` object, which will apply if there is no
411
+ variant or type override from the schema.
412
+ variant_map
413
+ The dictionary of dialects to types. Each key is a string representing
414
+ a dialect name, and each value is either an instance of
415
+ ``TypeEngine`` representing the variant type object or a callable
416
+ reference to its class type that will be instantiated later.
417
+ overrides
418
+ The dictionary of dialects to types to override the defaults. Each key
419
+ is a string representing a dialect name and type with a similar
420
+ structure as the `variant_map`.
421
+ args
422
+ The extra arguments to pass to the type object.
423
+
424
+ Notes
425
+ -----
426
+ This function is intended for internal use only. It builds a SQLAlchemy
427
+ ``TypeEngine`` that includes variants and overrides defined by Felis.
428
+ """
202
429
  variants: dict[str, types.TypeEngine | type[types.TypeEngine]] = dict(variant_map)
203
430
  variants.update(overrides)
204
431
  for dialect, variant in variants.items():
205
432
  # If this is a class and not an instance, instantiate
206
- if isinstance(variant, type):
433
+ if callable(variant):
207
434
  variant = variant(*args)
208
435
  type_ = type_.with_variant(variant, dialect)
209
436
  return type_