Coverage for middle_layer/common/application_layer/orm_repositories/orm_types.py: 78.16%
87 statements
« prev ^ index » next coverage.py v7.10.5, created at 2026-03-09 06:13 +0000
« prev ^ index » next coverage.py v7.10.5, created at 2026-03-09 06:13 +0000
1# Copyright 2024 Associated Universities, Inc.
2#
3# This file is part of Telescope Time Allocation Tools (TTAT).
4#
5# TTAT is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# any later version.
9#
10# TTAT is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with TTAT. If not, see <https://www.gnu.org/licenses/>.
17#
18from typing import Any, Type, override
20import sqlalchemy.types as types
21from astropy.units import Quantity, Unit, degree, s
23from common.domain_layer import JSON
26class QuantitySeconds(types.TypeDecorator):
27 """converts an astropy Quantity in seconds into Numeric for storage."""
29 impl = types.Numeric
31 cache_ok = True
33 @override
34 def process_bind_param(self, value: Quantity["time"], dialect):
35 return value.to_value(s)
37 @override
38 def process_result_value(self, value, dialect) -> Quantity[s]:
39 # convert the numeric from the db into Quantity in seconds:
40 return Quantity(value, s)
42 @property
43 @override
44 def python_type(self) -> type:
45 return Quantity
48class QuantityDegrees(types.TypeDecorator):
49 """converts an astropy Quantity in degrees into Numeric for storage."""
51 impl = types.Numeric
53 cache_ok = True
55 @override
56 def process_bind_param(self, value, dialect):
57 # check that the input is a Quantity in seconds:
58 if not (isinstance(value, Quantity) and value.unit == Unit("degree")):
59 try:
60 value = value * Unit("degree")
61 except ValueError:
62 raise TypeError(f"value {value} must be a Quantity in degrees.")
63 # return just the numeric part of the value
64 return value.value
66 @override
67 def process_result_value(self, value, dialect):
68 # convert the numeric from the db into Quantity in seconds:
69 return Quantity(value, degree)
71 @property
72 @override
73 def python_type(self) -> Type[Any]:
74 return Quantity
77class JSONList(types.TypeDecorator):
78 """Convert a list of JSON values into a single JSON blob for storage, and vice versa"""
80 impl = types.JSON(none_as_null=True)
81 cache_ok = True
83 @override
84 def process_bind_param(self, value: list[JSON], dialect) -> JSON:
85 return value
87 @override
88 def process_result_value(self, value: JSON, dialect) -> list[JSON]:
89 return value
92class CSV(types.TypeDecorator):
93 """Convert a list of strings into a single String for storage, and vice versa"""
95 SEPARATOR = ","
96 impl = types.String
97 cache_ok = True
99 @override
100 def process_bind_param(self, value: list[str], dialect) -> str:
101 return self.SEPARATOR.join(value)
103 @override
104 def process_result_value(self, value: str | None, dialect) -> list[str]:
105 return value.split(self.SEPARATOR) if value is not None else []
107 @property
108 @override
109 def python_type(self) -> Type[Any]:
110 return list[str]
113class CSVFloat(types.TypeDecorator):
114 """Convert a list of floats into a single String for storage, and vice versa"""
116 SEPARATOR = ","
117 impl = types.String
118 cache_ok = True
120 @override
121 def process_bind_param(self, value: list[float], dialect) -> str:
122 return self.SEPARATOR.join([str(i) for i in value])
124 @override
125 def process_result_value(self, value: str | None, dialect) -> list[float]:
126 return [float(x) for x in value.split(self.SEPARATOR)] if value is not None else []
128 @property
129 @override
130 def python_type(self) -> Type[Any]:
131 return list[float]
134class CSVInt(types.TypeDecorator):
135 """Convert a list of floats into a single String for storage, and vice versa"""
137 "It would be great to combine CSV, CSVFloat, and CSVInt into one type, if possible."
139 SEPARATOR = ","
140 impl = types.String
141 cache_ok = True
143 @override
144 def process_bind_param(self, value: list[int], dialect) -> str:
145 return self.SEPARATOR.join([str(i) for i in value])
147 @override
148 def process_result_value(self, value: str | None, dialect) -> list[int]:
149 return [int(x) for x in value.split(self.SEPARATOR)] if value is not None else []
151 @property
152 @override
153 def python_type(self) -> Type[Any]:
154 return list[int]