lsst-felis 26.2024.400__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/__init__.py +47 -0
- felis/check.py +381 -0
- felis/cli.py +398 -0
- felis/datamodel.py +409 -0
- felis/db/__init__.py +0 -0
- felis/db/sqltypes.py +209 -0
- felis/py.typed +0 -0
- felis/simple.py +424 -0
- felis/sql.py +264 -0
- felis/tap.py +434 -0
- felis/types.py +137 -0
- felis/utils.py +98 -0
- felis/version.py +2 -0
- felis/visitor.py +180 -0
- lsst_felis-26.2024.400.dist-info/COPYRIGHT +1 -0
- lsst_felis-26.2024.400.dist-info/LICENSE +674 -0
- lsst_felis-26.2024.400.dist-info/METADATA +1064 -0
- lsst_felis-26.2024.400.dist-info/RECORD +22 -0
- lsst_felis-26.2024.400.dist-info/WHEEL +5 -0
- lsst_felis-26.2024.400.dist-info/entry_points.txt +2 -0
- lsst_felis-26.2024.400.dist-info/top_level.txt +1 -0
- lsst_felis-26.2024.400.dist-info/zip-safe +1 -0
felis/datamodel.py
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
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 collections.abc import Mapping
|
|
24
|
+
from enum import Enum
|
|
25
|
+
from typing import Any, Literal
|
|
26
|
+
|
|
27
|
+
from astropy import units as units # type: ignore
|
|
28
|
+
from astropy.io.votable import ucd # type: ignore
|
|
29
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
# logger.setLevel(logging.DEBUG)
|
|
33
|
+
|
|
34
|
+
__all__ = (
|
|
35
|
+
"BaseObject",
|
|
36
|
+
"Column",
|
|
37
|
+
"Constraint",
|
|
38
|
+
"CheckConstraint",
|
|
39
|
+
"UniqueConstraint",
|
|
40
|
+
"Index",
|
|
41
|
+
"ForeignKeyConstraint",
|
|
42
|
+
"Table",
|
|
43
|
+
"SchemaVersion",
|
|
44
|
+
"Schema",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class BaseObject(BaseModel):
|
|
49
|
+
"""Base class for all Felis objects."""
|
|
50
|
+
|
|
51
|
+
model_config = ConfigDict(populate_by_name=True, extra="forbid", use_enum_values=True)
|
|
52
|
+
"""Configuration for the `BaseModel` class.
|
|
53
|
+
|
|
54
|
+
Allow attributes to be populated by name and forbid extra attributes.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
name: str
|
|
58
|
+
"""The name of the database object.
|
|
59
|
+
|
|
60
|
+
All Felis database objects must have a name.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
id: str = Field(alias="@id")
|
|
64
|
+
"""The unique identifier of the database object.
|
|
65
|
+
|
|
66
|
+
All Felis database objects must have a unique identifier.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
description: str | None = None
|
|
70
|
+
"""A description of the database object.
|
|
71
|
+
|
|
72
|
+
The description is optional.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class DataType(Enum):
|
|
77
|
+
"""`Enum` representing the data types supported by Felis."""
|
|
78
|
+
|
|
79
|
+
BOOLEAN = "boolean"
|
|
80
|
+
BYTE = "byte"
|
|
81
|
+
SHORT = "short"
|
|
82
|
+
INT = "int"
|
|
83
|
+
LONG = "long"
|
|
84
|
+
FLOAT = "float"
|
|
85
|
+
DOUBLE = "double"
|
|
86
|
+
CHAR = "char"
|
|
87
|
+
STRING = "string"
|
|
88
|
+
UNICODE = "unicode"
|
|
89
|
+
TEXT = "text"
|
|
90
|
+
BINARY = "binary"
|
|
91
|
+
TIMESTAMP = "timestamp"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class Column(BaseObject):
|
|
95
|
+
"""A column in a table."""
|
|
96
|
+
|
|
97
|
+
datatype: DataType
|
|
98
|
+
"""The datatype of the column."""
|
|
99
|
+
|
|
100
|
+
length: int | None = None
|
|
101
|
+
"""The length of the column."""
|
|
102
|
+
|
|
103
|
+
nullable: bool = True
|
|
104
|
+
"""Whether the column can be `NULL`."""
|
|
105
|
+
|
|
106
|
+
value: Any = None
|
|
107
|
+
"""The default value of the column."""
|
|
108
|
+
|
|
109
|
+
autoincrement: bool | None = None
|
|
110
|
+
"""Whether the column is autoincremented."""
|
|
111
|
+
|
|
112
|
+
mysql_datatype: str | None = Field(None, alias="mysql:datatype")
|
|
113
|
+
"""The MySQL datatype of the column."""
|
|
114
|
+
|
|
115
|
+
ivoa_ucd: str | None = Field(None, alias="ivoa:ucd")
|
|
116
|
+
"""The IVOA UCD of the column."""
|
|
117
|
+
|
|
118
|
+
fits_tunit: str | None = Field(None, alias="fits:tunit")
|
|
119
|
+
"""The FITS TUNIT of the column."""
|
|
120
|
+
|
|
121
|
+
ivoa_unit: str | None = Field(None, alias="ivoa:unit")
|
|
122
|
+
"""The IVOA unit of the column."""
|
|
123
|
+
|
|
124
|
+
tap_column_index: int | None = Field(None, alias="tap:column_index")
|
|
125
|
+
"""The TAP_SCHEMA column index of the column."""
|
|
126
|
+
|
|
127
|
+
tap_principal: int | None = Field(0, alias="tap:principal", ge=0, le=1)
|
|
128
|
+
"""Whether this is a TAP_SCHEMA principal column; can be either 0 or 1.
|
|
129
|
+
|
|
130
|
+
This could be a boolean instead of 0 or 1.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
votable_arraysize: int | Literal["*"] | None = Field(None, alias="votable:arraysize")
|
|
134
|
+
"""The VOTable arraysize of the column."""
|
|
135
|
+
|
|
136
|
+
tap_std: int | None = Field(0, alias="tap:std", ge=0, le=1)
|
|
137
|
+
"""TAP_SCHEMA indication that this column is defined by an IVOA standard.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
votable_utype: str | None = Field(None, alias="votable:utype")
|
|
141
|
+
"""The VOTable utype (usage-specific or unique type) of the column."""
|
|
142
|
+
|
|
143
|
+
votable_xtype: str | None = Field(None, alias="votable:xtype")
|
|
144
|
+
"""The VOTable xtype (extended type) of the column."""
|
|
145
|
+
|
|
146
|
+
@field_validator("ivoa_ucd")
|
|
147
|
+
@classmethod
|
|
148
|
+
def check_ivoa_ucd(cls, ivoa_ucd: str) -> str:
|
|
149
|
+
"""Check that IVOA UCD values are valid."""
|
|
150
|
+
if ivoa_ucd is not None:
|
|
151
|
+
try:
|
|
152
|
+
ucd.parse_ucd(ivoa_ucd, check_controlled_vocabulary=True, has_colon=";" in ivoa_ucd)
|
|
153
|
+
except ValueError as e:
|
|
154
|
+
raise ValueError(f"Invalid IVOA UCD: {e}")
|
|
155
|
+
return ivoa_ucd
|
|
156
|
+
|
|
157
|
+
@model_validator(mode="before")
|
|
158
|
+
@classmethod
|
|
159
|
+
def check_units(cls, values: dict[str, Any]) -> dict[str, Any]:
|
|
160
|
+
"""Check that units are valid."""
|
|
161
|
+
fits_unit = values.get("fits:tunit")
|
|
162
|
+
ivoa_unit = values.get("ivoa:unit")
|
|
163
|
+
|
|
164
|
+
if fits_unit and ivoa_unit:
|
|
165
|
+
raise ValueError("Column cannot have both FITS and IVOA units")
|
|
166
|
+
unit = fits_unit or ivoa_unit
|
|
167
|
+
|
|
168
|
+
if unit is not None:
|
|
169
|
+
try:
|
|
170
|
+
units.Unit(unit)
|
|
171
|
+
except ValueError as e:
|
|
172
|
+
raise ValueError(f"Invalid unit: {e}")
|
|
173
|
+
|
|
174
|
+
return values
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class Constraint(BaseObject):
|
|
178
|
+
"""A database table constraint."""
|
|
179
|
+
|
|
180
|
+
deferrable: bool = False
|
|
181
|
+
"""If `True` then this constraint will be declared as deferrable."""
|
|
182
|
+
|
|
183
|
+
initially: str | None = None
|
|
184
|
+
"""Value for ``INITIALLY`` clause, only used if ``deferrable`` is True."""
|
|
185
|
+
|
|
186
|
+
annotations: Mapping[str, Any] = Field(default_factory=dict)
|
|
187
|
+
"""Additional annotations for this constraint."""
|
|
188
|
+
|
|
189
|
+
type: str | None = Field(None, alias="@type")
|
|
190
|
+
"""The type of the constraint."""
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class CheckConstraint(Constraint):
|
|
194
|
+
"""A check constraint on a table."""
|
|
195
|
+
|
|
196
|
+
expression: str
|
|
197
|
+
"""The expression for the check constraint."""
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class UniqueConstraint(Constraint):
|
|
201
|
+
"""A unique constraint on a table."""
|
|
202
|
+
|
|
203
|
+
columns: list[str]
|
|
204
|
+
"""The columns in the unique constraint."""
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class Index(BaseObject):
|
|
208
|
+
"""A database table index.
|
|
209
|
+
|
|
210
|
+
An index can be defined on either columns or expressions, but not both.
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
columns: list[str] | None = None
|
|
214
|
+
"""The columns in the index."""
|
|
215
|
+
|
|
216
|
+
expressions: list[str] | None = None
|
|
217
|
+
"""The expressions in the index."""
|
|
218
|
+
|
|
219
|
+
@model_validator(mode="before")
|
|
220
|
+
@classmethod
|
|
221
|
+
def check_columns_or_expressions(cls, values: dict[str, Any]) -> dict[str, Any]:
|
|
222
|
+
"""Check that columns or expressions are specified, but not both."""
|
|
223
|
+
if "columns" in values and "expressions" in values:
|
|
224
|
+
raise ValueError("Defining columns and expressions is not valid")
|
|
225
|
+
elif "columns" not in values and "expressions" not in values:
|
|
226
|
+
raise ValueError("Must define columns or expressions")
|
|
227
|
+
return values
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class ForeignKeyConstraint(Constraint):
|
|
231
|
+
"""A foreign key constraint on a table.
|
|
232
|
+
|
|
233
|
+
These will be reflected in the TAP_SCHEMA keys and key_columns data.
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
columns: list[str]
|
|
237
|
+
"""The columns comprising the foreign key."""
|
|
238
|
+
|
|
239
|
+
referenced_columns: list[str] = Field(alias="referencedColumns")
|
|
240
|
+
"""The columns referenced by the foreign key."""
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class Table(BaseObject):
|
|
244
|
+
"""A database table."""
|
|
245
|
+
|
|
246
|
+
columns: list[Column]
|
|
247
|
+
"""The columns in the table."""
|
|
248
|
+
|
|
249
|
+
constraints: list[Constraint] = Field(default_factory=list)
|
|
250
|
+
"""The constraints on the table."""
|
|
251
|
+
|
|
252
|
+
indexes: list[Index] = Field(default_factory=list)
|
|
253
|
+
"""The indexes on the table."""
|
|
254
|
+
|
|
255
|
+
primaryKey: str | list[str] | None = None
|
|
256
|
+
"""The primary key of the table."""
|
|
257
|
+
|
|
258
|
+
tap_table_index: int | None = Field(None, alias="tap:table_index")
|
|
259
|
+
"""The IVOA TAP_SCHEMA table index of the table."""
|
|
260
|
+
|
|
261
|
+
mysql_engine: str | None = Field(None, alias="mysql:engine")
|
|
262
|
+
"""The mysql engine to use for the table.
|
|
263
|
+
|
|
264
|
+
For now this is a freeform string but it could be constrained to a list of
|
|
265
|
+
known engines in the future.
|
|
266
|
+
"""
|
|
267
|
+
|
|
268
|
+
mysql_charset: str | None = Field(None, alias="mysql:charset")
|
|
269
|
+
"""The mysql charset to use for the table.
|
|
270
|
+
|
|
271
|
+
For now this is a freeform string but it could be constrained to a list of
|
|
272
|
+
known charsets in the future.
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
@model_validator(mode="before")
|
|
276
|
+
@classmethod
|
|
277
|
+
def create_constraints(cls, values: dict[str, Any]) -> dict[str, Any]:
|
|
278
|
+
"""Create constraints from the ``constraints`` field."""
|
|
279
|
+
if "constraints" in values:
|
|
280
|
+
new_constraints: list[Constraint] = []
|
|
281
|
+
for item in values["constraints"]:
|
|
282
|
+
if item["@type"] == "ForeignKey":
|
|
283
|
+
new_constraints.append(ForeignKeyConstraint(**item))
|
|
284
|
+
elif item["@type"] == "Unique":
|
|
285
|
+
new_constraints.append(UniqueConstraint(**item))
|
|
286
|
+
elif item["@type"] == "Check":
|
|
287
|
+
new_constraints.append(CheckConstraint(**item))
|
|
288
|
+
else:
|
|
289
|
+
raise ValueError(f"Unknown constraint type: {item['@type']}")
|
|
290
|
+
values["constraints"] = new_constraints
|
|
291
|
+
return values
|
|
292
|
+
|
|
293
|
+
@field_validator("columns", mode="after")
|
|
294
|
+
@classmethod
|
|
295
|
+
def check_unique_column_names(cls, columns: list[Column]) -> list[Column]:
|
|
296
|
+
"""Check that column names are unique."""
|
|
297
|
+
if len(columns) != len(set(column.name for column in columns)):
|
|
298
|
+
raise ValueError("Column names must be unique")
|
|
299
|
+
return columns
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class SchemaVersion(BaseModel):
|
|
303
|
+
"""The version of the schema."""
|
|
304
|
+
|
|
305
|
+
current: str
|
|
306
|
+
"""The current version of the schema."""
|
|
307
|
+
|
|
308
|
+
compatible: list[str] | None = None
|
|
309
|
+
"""The compatible versions of the schema."""
|
|
310
|
+
|
|
311
|
+
read_compatible: list[str] | None = None
|
|
312
|
+
"""The read compatible versions of the schema."""
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
class SchemaVisitor:
|
|
316
|
+
"""Visitor to build a Schema object's map of IDs to objects.
|
|
317
|
+
|
|
318
|
+
Duplicates are added to a set when they are encountered, which can be
|
|
319
|
+
accessed via the `duplicates` attribute. The presence of duplicates will
|
|
320
|
+
not throw an error. Only the first object with a given ID will be added to
|
|
321
|
+
the map, but this should not matter, since a ValidationError will be thrown
|
|
322
|
+
by the `model_validator` method if any duplicates are found in the schema.
|
|
323
|
+
|
|
324
|
+
This class is intended for internal use only.
|
|
325
|
+
"""
|
|
326
|
+
|
|
327
|
+
def __init__(self) -> None:
|
|
328
|
+
"""Create a new SchemaVisitor."""
|
|
329
|
+
self.schema: "Schema" | None = None
|
|
330
|
+
self.duplicates: set[str] = set()
|
|
331
|
+
|
|
332
|
+
def add(self, obj: BaseObject) -> None:
|
|
333
|
+
"""Add an object to the ID map."""
|
|
334
|
+
if hasattr(obj, "id"):
|
|
335
|
+
obj_id = getattr(obj, "id")
|
|
336
|
+
if self.schema is not None:
|
|
337
|
+
if obj_id in self.schema.id_map:
|
|
338
|
+
self.duplicates.add(obj_id)
|
|
339
|
+
else:
|
|
340
|
+
self.schema.id_map[obj_id] = obj
|
|
341
|
+
|
|
342
|
+
def visit_schema(self, schema: "Schema") -> None:
|
|
343
|
+
"""Visit the schema object that was added during initialization.
|
|
344
|
+
|
|
345
|
+
This will set an internal variable pointing to the schema object.
|
|
346
|
+
"""
|
|
347
|
+
self.schema = schema
|
|
348
|
+
self.duplicates.clear()
|
|
349
|
+
self.add(self.schema)
|
|
350
|
+
for table in self.schema.tables:
|
|
351
|
+
self.visit_table(table)
|
|
352
|
+
|
|
353
|
+
def visit_table(self, table: Table) -> None:
|
|
354
|
+
"""Visit a table object."""
|
|
355
|
+
self.add(table)
|
|
356
|
+
for column in table.columns:
|
|
357
|
+
self.visit_column(column)
|
|
358
|
+
for constraint in table.constraints:
|
|
359
|
+
self.visit_constraint(constraint)
|
|
360
|
+
|
|
361
|
+
def visit_column(self, column: Column) -> None:
|
|
362
|
+
"""Visit a column object."""
|
|
363
|
+
self.add(column)
|
|
364
|
+
|
|
365
|
+
def visit_constraint(self, constraint: Constraint) -> None:
|
|
366
|
+
"""Visit a constraint object."""
|
|
367
|
+
self.add(constraint)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
class Schema(BaseObject):
|
|
371
|
+
"""The database schema."""
|
|
372
|
+
|
|
373
|
+
version: SchemaVersion | str | None = None
|
|
374
|
+
"""The version of the schema."""
|
|
375
|
+
|
|
376
|
+
tables: list[Table]
|
|
377
|
+
"""The tables in the schema."""
|
|
378
|
+
|
|
379
|
+
id_map: dict[str, Any] = Field(default_factory=dict, exclude=True)
|
|
380
|
+
"""Map of IDs to objects."""
|
|
381
|
+
|
|
382
|
+
@field_validator("tables", mode="after")
|
|
383
|
+
@classmethod
|
|
384
|
+
def check_unique_table_names(cls, tables: list[Table]) -> list[Table]:
|
|
385
|
+
"""Check that table names are unique."""
|
|
386
|
+
if len(tables) != len(set(table.name for table in tables)):
|
|
387
|
+
raise ValueError("Table names must be unique")
|
|
388
|
+
return tables
|
|
389
|
+
|
|
390
|
+
@model_validator(mode="after")
|
|
391
|
+
def create_id_map(self) -> "Schema":
|
|
392
|
+
"""Create a map of IDs to objects."""
|
|
393
|
+
visitor: SchemaVisitor = SchemaVisitor()
|
|
394
|
+
visitor.visit_schema(self)
|
|
395
|
+
logger.debug(f"ID map contains {len(self.id_map.keys())} objects")
|
|
396
|
+
if len(visitor.duplicates):
|
|
397
|
+
raise ValueError(
|
|
398
|
+
"Duplicate IDs found in schema:\n " + "\n ".join(visitor.duplicates) + "\n"
|
|
399
|
+
)
|
|
400
|
+
return self
|
|
401
|
+
|
|
402
|
+
def get_object_by_id(self, id: str) -> BaseObject:
|
|
403
|
+
"""Get an object by its unique "@id" field value.
|
|
404
|
+
|
|
405
|
+
An error will be thrown if the object is not found.
|
|
406
|
+
"""
|
|
407
|
+
if id not in self.id_map:
|
|
408
|
+
raise ValueError(f"Object with ID {id} not found in schema")
|
|
409
|
+
return self.id_map[id]
|
felis/db/__init__.py
ADDED
|
File without changes
|
felis/db/sqltypes.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
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 builtins
|
|
23
|
+
from collections.abc import Mapping
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from sqlalchemy import Float, SmallInteger, types
|
|
27
|
+
from sqlalchemy.dialects import mysql, oracle, postgresql
|
|
28
|
+
from sqlalchemy.ext.compiler import compiles
|
|
29
|
+
|
|
30
|
+
MYSQL = "mysql"
|
|
31
|
+
ORACLE = "oracle"
|
|
32
|
+
POSTGRES = "postgresql"
|
|
33
|
+
SQLITE = "sqlite"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TINYINT(SmallInteger):
|
|
37
|
+
"""The non-standard TINYINT type."""
|
|
38
|
+
|
|
39
|
+
__visit_name__ = "TINYINT"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class DOUBLE(Float):
|
|
43
|
+
"""The non-standard DOUBLE type."""
|
|
44
|
+
|
|
45
|
+
__visit_name__ = "DOUBLE"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@compiles(TINYINT)
|
|
49
|
+
def compile_tinyint(type_: Any, compiler: Any, **kw: Any) -> str:
|
|
50
|
+
"""Return type name for TINYINT."""
|
|
51
|
+
return "TINYINT"
|
|
52
|
+
|
|
53
|
+
|
|
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
|
+
_TypeMap = Mapping[str, types.TypeEngine | type[types.TypeEngine]]
|
|
61
|
+
|
|
62
|
+
boolean_map: _TypeMap = {MYSQL: mysql.BIT(1), ORACLE: oracle.NUMBER(1), POSTGRES: postgresql.BOOLEAN()}
|
|
63
|
+
|
|
64
|
+
byte_map: _TypeMap = {
|
|
65
|
+
MYSQL: mysql.TINYINT(),
|
|
66
|
+
ORACLE: oracle.NUMBER(3),
|
|
67
|
+
POSTGRES: postgresql.SMALLINT(),
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
short_map: _TypeMap = {
|
|
71
|
+
MYSQL: mysql.SMALLINT(),
|
|
72
|
+
ORACLE: oracle.NUMBER(5),
|
|
73
|
+
POSTGRES: postgresql.SMALLINT(),
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# Skip Oracle
|
|
77
|
+
int_map: _TypeMap = {
|
|
78
|
+
MYSQL: mysql.INTEGER(),
|
|
79
|
+
POSTGRES: postgresql.INTEGER(),
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
long_map: _TypeMap = {
|
|
83
|
+
MYSQL: mysql.BIGINT(),
|
|
84
|
+
ORACLE: oracle.NUMBER(38, 0),
|
|
85
|
+
POSTGRES: postgresql.BIGINT(),
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
float_map: _TypeMap = {
|
|
89
|
+
MYSQL: mysql.FLOAT(),
|
|
90
|
+
ORACLE: oracle.BINARY_FLOAT(),
|
|
91
|
+
POSTGRES: postgresql.FLOAT(),
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
double_map: _TypeMap = {
|
|
95
|
+
MYSQL: mysql.DOUBLE(),
|
|
96
|
+
ORACLE: oracle.BINARY_DOUBLE(),
|
|
97
|
+
POSTGRES: postgresql.DOUBLE_PRECISION(),
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
char_map: _TypeMap = {
|
|
101
|
+
MYSQL: mysql.CHAR,
|
|
102
|
+
ORACLE: oracle.CHAR,
|
|
103
|
+
POSTGRES: postgresql.CHAR,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
string_map: _TypeMap = {
|
|
107
|
+
MYSQL: mysql.VARCHAR,
|
|
108
|
+
ORACLE: oracle.VARCHAR2,
|
|
109
|
+
POSTGRES: postgresql.VARCHAR,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
unicode_map: _TypeMap = {
|
|
113
|
+
MYSQL: mysql.NVARCHAR,
|
|
114
|
+
ORACLE: oracle.NVARCHAR2,
|
|
115
|
+
POSTGRES: postgresql.VARCHAR,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
text_map: _TypeMap = {
|
|
119
|
+
MYSQL: mysql.LONGTEXT,
|
|
120
|
+
ORACLE: oracle.CLOB,
|
|
121
|
+
POSTGRES: postgresql.TEXT,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
binary_map: _TypeMap = {
|
|
125
|
+
MYSQL: mysql.LONGBLOB,
|
|
126
|
+
ORACLE: oracle.BLOB,
|
|
127
|
+
POSTGRES: postgresql.BYTEA,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def boolean(**kwargs: Any) -> types.TypeEngine:
|
|
132
|
+
"""Return SQLAlchemy type for boolean."""
|
|
133
|
+
return _vary(types.BOOLEAN(), boolean_map, kwargs)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def byte(**kwargs: Any) -> types.TypeEngine:
|
|
137
|
+
"""Return SQLAlchemy type for byte."""
|
|
138
|
+
return _vary(TINYINT(), byte_map, kwargs)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def short(**kwargs: Any) -> types.TypeEngine:
|
|
142
|
+
"""Return SQLAlchemy type for short integer."""
|
|
143
|
+
return _vary(types.SMALLINT(), short_map, kwargs)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def int(**kwargs: Any) -> types.TypeEngine:
|
|
147
|
+
"""Return SQLAlchemy type for integer."""
|
|
148
|
+
return _vary(types.INTEGER(), int_map, kwargs)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def long(**kwargs: Any) -> types.TypeEngine:
|
|
152
|
+
"""Return SQLAlchemy type for long integer."""
|
|
153
|
+
return _vary(types.BIGINT(), long_map, kwargs)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def float(**kwargs: Any) -> types.TypeEngine:
|
|
157
|
+
"""Return SQLAlchemy type for single precision float."""
|
|
158
|
+
return _vary(types.FLOAT(), float_map, kwargs)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def double(**kwargs: Any) -> types.TypeEngine:
|
|
162
|
+
"""Return SQLAlchemy type for double precision float."""
|
|
163
|
+
return _vary(DOUBLE(), double_map, kwargs)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def char(length: builtins.int, **kwargs: Any) -> types.TypeEngine:
|
|
167
|
+
"""Return SQLAlchemy type for character."""
|
|
168
|
+
return _vary(types.CHAR(length), char_map, kwargs, length)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def string(length: builtins.int, **kwargs: Any) -> types.TypeEngine:
|
|
172
|
+
"""Return SQLAlchemy type for string."""
|
|
173
|
+
return _vary(types.VARCHAR(length), string_map, kwargs, length)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def unicode(length: builtins.int, **kwargs: Any) -> types.TypeEngine:
|
|
177
|
+
"""Return SQLAlchemy type for unicode string."""
|
|
178
|
+
return _vary(types.NVARCHAR(length), unicode_map, kwargs, length)
|
|
179
|
+
|
|
180
|
+
|
|
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)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def binary(length: builtins.int, **kwargs: Any) -> types.TypeEngine:
|
|
187
|
+
"""Return SQLAlchemy type for binary."""
|
|
188
|
+
return _vary(types.BLOB(length), binary_map, kwargs, length)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def timestamp(**kwargs: Any) -> types.TypeEngine:
|
|
192
|
+
"""Return SQLAlchemy type for timestamp."""
|
|
193
|
+
return types.TIMESTAMP()
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _vary(
|
|
197
|
+
type_: types.TypeEngine,
|
|
198
|
+
variant_map: _TypeMap,
|
|
199
|
+
overrides: _TypeMap,
|
|
200
|
+
*args: Any,
|
|
201
|
+
) -> types.TypeEngine:
|
|
202
|
+
variants: dict[str, types.TypeEngine | type[types.TypeEngine]] = dict(variant_map)
|
|
203
|
+
variants.update(overrides)
|
|
204
|
+
for dialect, variant in variants.items():
|
|
205
|
+
# If this is a class and not an instance, instantiate
|
|
206
|
+
if isinstance(variant, type):
|
|
207
|
+
variant = variant(*args)
|
|
208
|
+
type_ = type_.with_variant(variant, dialect)
|
|
209
|
+
return type_
|
felis/py.typed
ADDED
|
File without changes
|