ctao-calibpipe 0.1.0rc7__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 ctao-calibpipe might be problematic. Click here for more details.
- calibpipe/__init__.py +5 -0
- calibpipe/_dev_version/__init__.py +9 -0
- calibpipe/_version.py +21 -0
- calibpipe/atmosphere/__init__.py +1 -0
- calibpipe/atmosphere/atmosphere_containers.py +109 -0
- calibpipe/atmosphere/meteo_data_handlers.py +485 -0
- calibpipe/atmosphere/models/README.md +14 -0
- calibpipe/atmosphere/models/__init__.py +1 -0
- calibpipe/atmosphere/models/macobac.ecsv +23 -0
- calibpipe/atmosphere/models/reference_MDPs/__init__.py +1 -0
- calibpipe/atmosphere/models/reference_MDPs/ref_density_at_15km_ctao-north_intermediate.ecsv +8 -0
- calibpipe/atmosphere/models/reference_MDPs/ref_density_at_15km_ctao-north_summer.ecsv +8 -0
- calibpipe/atmosphere/models/reference_MDPs/ref_density_at_15km_ctao-north_winter.ecsv +8 -0
- calibpipe/atmosphere/models/reference_MDPs/ref_density_at_15km_ctao-south_summer.ecsv +8 -0
- calibpipe/atmosphere/models/reference_MDPs/ref_density_at_15km_ctao-south_winter.ecsv +8 -0
- calibpipe/atmosphere/models/reference_atmospheres/__init__.py +1 -0
- calibpipe/atmosphere/models/reference_atmospheres/reference_atmo_model_v0_ctao-north_intermediate.ecsv +73 -0
- calibpipe/atmosphere/models/reference_atmospheres/reference_atmo_model_v0_ctao-north_summer.ecsv +73 -0
- calibpipe/atmosphere/models/reference_atmospheres/reference_atmo_model_v0_ctao-north_winter.ecsv +73 -0
- calibpipe/atmosphere/models/reference_atmospheres/reference_atmo_model_v0_ctao-south_summer.ecsv +73 -0
- calibpipe/atmosphere/models/reference_atmospheres/reference_atmo_model_v0_ctao-south_winter.ecsv +73 -0
- calibpipe/atmosphere/models/reference_rayleigh_scattering_profiles/__init__.py +1 -0
- calibpipe/atmosphere/models/reference_rayleigh_scattering_profiles/reference_rayleigh_extinction_profile_v0_ctao-north_intermediate.ecsv +857 -0
- calibpipe/atmosphere/models/reference_rayleigh_scattering_profiles/reference_rayleigh_extinction_profile_v0_ctao-north_summer.ecsv +857 -0
- calibpipe/atmosphere/models/reference_rayleigh_scattering_profiles/reference_rayleigh_extinction_profile_v0_ctao-north_winter.ecsv +857 -0
- calibpipe/atmosphere/models/reference_rayleigh_scattering_profiles/reference_rayleigh_extinction_profile_v0_ctao-south_summer.ecsv +857 -0
- calibpipe/atmosphere/models/reference_rayleigh_scattering_profiles/reference_rayleigh_extinction_profile_v0_ctao-south_winter.ecsv +857 -0
- calibpipe/atmosphere/templates/request_templates/__init__.py +1 -0
- calibpipe/atmosphere/templates/request_templates/copernicus.json +11 -0
- calibpipe/atmosphere/templates/request_templates/gdas.json +12 -0
- calibpipe/core/__init__.py +39 -0
- calibpipe/core/common_metadata_containers.py +195 -0
- calibpipe/core/exceptions.py +87 -0
- calibpipe/database/__init__.py +24 -0
- calibpipe/database/adapter/__init__.py +23 -0
- calibpipe/database/adapter/adapter.py +80 -0
- calibpipe/database/adapter/database_containers/__init__.py +61 -0
- calibpipe/database/adapter/database_containers/atmosphere.py +199 -0
- calibpipe/database/adapter/database_containers/common_metadata.py +148 -0
- calibpipe/database/adapter/database_containers/container_map.py +59 -0
- calibpipe/database/adapter/database_containers/observatory.py +61 -0
- calibpipe/database/adapter/database_containers/table_version_manager.py +39 -0
- calibpipe/database/adapter/database_containers/version_control.py +17 -0
- calibpipe/database/connections/__init__.py +28 -0
- calibpipe/database/connections/calibpipe_database.py +60 -0
- calibpipe/database/connections/postgres_utils.py +97 -0
- calibpipe/database/connections/sql_connection.py +103 -0
- calibpipe/database/connections/user_confirmation.py +19 -0
- calibpipe/database/interfaces/__init__.py +71 -0
- calibpipe/database/interfaces/hashable_row_data.py +54 -0
- calibpipe/database/interfaces/queries.py +180 -0
- calibpipe/database/interfaces/sql_column_info.py +67 -0
- calibpipe/database/interfaces/sql_metadata.py +6 -0
- calibpipe/database/interfaces/sql_table_info.py +131 -0
- calibpipe/database/interfaces/table_handler.py +351 -0
- calibpipe/database/interfaces/types.py +96 -0
- calibpipe/tests/data/atmosphere/molecular_atmosphere/__init__.py +0 -0
- calibpipe/tests/data/atmosphere/molecular_atmosphere/contemporary_MDP.ecsv +34 -0
- calibpipe/tests/data/atmosphere/molecular_atmosphere/macobac.csv +852 -0
- calibpipe/tests/data/atmosphere/molecular_atmosphere/macobac.ecsv +23 -0
- calibpipe/tests/data/atmosphere/molecular_atmosphere/merged_file.ecsv +1082 -0
- calibpipe/tests/data/atmosphere/molecular_atmosphere/meteo_data_copernicus.ecsv +1082 -0
- calibpipe/tests/data/atmosphere/molecular_atmosphere/meteo_data_gdas.ecsv +66 -0
- calibpipe/tests/data/atmosphere/molecular_atmosphere/observatory_configurations.json +71 -0
- calibpipe/tests/data/utils/__init__.py +0 -0
- calibpipe/tests/data/utils/meteo_data_winter_and_summer.ecsv +12992 -0
- calibpipe/tests/unittests/atmosphere/astral_testing.py +107 -0
- calibpipe/tests/unittests/atmosphere/test_meteo_data_handler.py +775 -0
- calibpipe/tests/unittests/atmosphere/test_molecular_atmosphere.py +327 -0
- calibpipe/tests/unittests/database/test_table_handler.py +66 -0
- calibpipe/tests/unittests/database/test_types.py +38 -0
- calibpipe/tests/unittests/test_bootstrap_db.py +79 -0
- calibpipe/tests/unittests/utils/test_observatory.py +309 -0
- calibpipe/tools/atmospheric_base_tool.py +78 -0
- calibpipe/tools/atmospheric_model_db_loader.py +181 -0
- calibpipe/tools/basic_tool_with_db.py +38 -0
- calibpipe/tools/contemporary_mdp_producer.py +87 -0
- calibpipe/tools/init_db.py +37 -0
- calibpipe/tools/macobac_calculator.py +82 -0
- calibpipe/tools/molecular_atmospheric_model_producer.py +197 -0
- calibpipe/tools/observatory_data_db_loader.py +71 -0
- calibpipe/tools/reference_atmospheric_model_selector.py +201 -0
- calibpipe/utils/__init__.py +10 -0
- calibpipe/utils/observatory.py +486 -0
- calibpipe/utils/observatory_containers.py +26 -0
- calibpipe/version.py +24 -0
- ctao_calibpipe-0.1.0rc7.dist-info/METADATA +86 -0
- ctao_calibpipe-0.1.0rc7.dist-info/RECORD +93 -0
- ctao_calibpipe-0.1.0rc7.dist-info/WHEEL +5 -0
- ctao_calibpipe-0.1.0rc7.dist-info/entry_points.txt +8 -0
- ctao_calibpipe-0.1.0rc7.dist-info/licenses/AUTHORS.md +13 -0
- ctao_calibpipe-0.1.0rc7.dist-info/licenses/LICENSE +21 -0
- ctao_calibpipe-0.1.0rc7.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""SQLTableInfo class."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sqlalchemy as sa
|
|
6
|
+
from sqlalchemy.orm import declarative_base
|
|
7
|
+
from sqlalchemy.schema import (
|
|
8
|
+
CheckConstraint,
|
|
9
|
+
ForeignKeyConstraint,
|
|
10
|
+
PrimaryKeyConstraint,
|
|
11
|
+
UniqueConstraint,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from ..interfaces import sql_metadata
|
|
15
|
+
from .sql_column_info import SQLColumnInfo
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class InvalidTableError(Exception):
|
|
19
|
+
"""Raised when a table is invalid e.g. has no primary key."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SQLTableInfo:
|
|
23
|
+
"""
|
|
24
|
+
Collection of attributes defining a Table's columns.
|
|
25
|
+
|
|
26
|
+
The class contains the column information (`SQLColumnInfo`)
|
|
27
|
+
and additional arguments required to build the sqlalchemy
|
|
28
|
+
table when the `get_table()` method is called.
|
|
29
|
+
|
|
30
|
+
This class can provide useful information on the corresponding
|
|
31
|
+
table. For example the primary-key or the list of undeferred
|
|
32
|
+
and deferred columns, i.e. that must be loaded directly or
|
|
33
|
+
looked up in a cache system (if implemented) respectively.
|
|
34
|
+
Note that no cache implementation lies here, only the information
|
|
35
|
+
that some columns must be deferred if possible.
|
|
36
|
+
|
|
37
|
+
The `SQLTableInfo` also can manage several tables of the same type
|
|
38
|
+
(e.g. for versioning, table_A_v1 && table_A_v2). When calling
|
|
39
|
+
the `get_table()` method, a custom table name can be given. The
|
|
40
|
+
object will ensure that only one table is created for a given
|
|
41
|
+
name (otherwise `sqlalchemy` cannot work properly).
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
table_base_class = declarative_base()
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
table_name: str,
|
|
49
|
+
metadata: sql_metadata,
|
|
50
|
+
columns: list[SQLColumnInfo],
|
|
51
|
+
constraints: list[
|
|
52
|
+
ForeignKeyConstraint
|
|
53
|
+
| UniqueConstraint
|
|
54
|
+
| CheckConstraint
|
|
55
|
+
| PrimaryKeyConstraint
|
|
56
|
+
]
|
|
57
|
+
| None = None,
|
|
58
|
+
) -> None:
|
|
59
|
+
"""Initialize the table data and sqlachemy metadata."""
|
|
60
|
+
self.table_name = table_name
|
|
61
|
+
self.metadata = metadata
|
|
62
|
+
self.columns = columns
|
|
63
|
+
self.constraints = constraints if constraints else []
|
|
64
|
+
self._table_instances: dict[str, sa.Table] = {}
|
|
65
|
+
|
|
66
|
+
def get_primary_keys(self) -> list[SQLColumnInfo]:
|
|
67
|
+
"""Get list of primary keys for the table.
|
|
68
|
+
|
|
69
|
+
Returns
|
|
70
|
+
-------
|
|
71
|
+
list
|
|
72
|
+
list with SQLColumnInfo objects that are the primary keys
|
|
73
|
+
|
|
74
|
+
Raises
|
|
75
|
+
------
|
|
76
|
+
InvalidTableError
|
|
77
|
+
If there are no primary key in the table
|
|
78
|
+
"""
|
|
79
|
+
pk_columns = []
|
|
80
|
+
for column in self.columns:
|
|
81
|
+
if column.is_primary_key():
|
|
82
|
+
pk_columns.append(column)
|
|
83
|
+
if pk_columns:
|
|
84
|
+
return pk_columns
|
|
85
|
+
raise InvalidTableError(f"Table {self.table_name!r} has no primary key.")
|
|
86
|
+
|
|
87
|
+
def get_deferred_columns(self) -> list[SQLColumnInfo]:
|
|
88
|
+
"""
|
|
89
|
+
Return the columns that must be deferred.
|
|
90
|
+
|
|
91
|
+
Deferred columns won't be loaded directly when queried.
|
|
92
|
+
"""
|
|
93
|
+
return [column for column in self.columns if column.is_deferred]
|
|
94
|
+
|
|
95
|
+
def get_undeferred_columns(self) -> list[SQLColumnInfo]:
|
|
96
|
+
"""Return the columns that must not be deferred.
|
|
97
|
+
|
|
98
|
+
These columns are loaded directly when queried.
|
|
99
|
+
"""
|
|
100
|
+
return [column for column in self.columns if not column.is_deferred]
|
|
101
|
+
|
|
102
|
+
def get_table(self, table_name: str | None = None) -> sa.Table:
|
|
103
|
+
"""
|
|
104
|
+
Return a table with a given name, create it if necessary.
|
|
105
|
+
|
|
106
|
+
Parameters
|
|
107
|
+
----------
|
|
108
|
+
table_name: str (optional, default=None)
|
|
109
|
+
Name of the table to create. If not given, the `table_name`
|
|
110
|
+
attribute is used. If the table with the given name has
|
|
111
|
+
already been created it is returned and no new table
|
|
112
|
+
is generated.
|
|
113
|
+
"""
|
|
114
|
+
table_name = table_name or self.table_name
|
|
115
|
+
if table_name not in self._table_instances:
|
|
116
|
+
if table_name in self.metadata.tables:
|
|
117
|
+
self._table_instances[table_name] = sa.Table(table_name, self.metadata)
|
|
118
|
+
else:
|
|
119
|
+
self._table_instances[table_name] = self._generate_table(
|
|
120
|
+
table_name=table_name
|
|
121
|
+
)
|
|
122
|
+
return self._table_instances[table_name]
|
|
123
|
+
|
|
124
|
+
def _generate_table(self, table_name: str) -> sa.Table:
|
|
125
|
+
"""Generate a table corresponding to the info with a specific name."""
|
|
126
|
+
return sa.Table(
|
|
127
|
+
table_name,
|
|
128
|
+
self.metadata,
|
|
129
|
+
*[col.generate_column() for col in self.columns],
|
|
130
|
+
*self.constraints,
|
|
131
|
+
)
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"""Utilities for CalibPipe data."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import astropy.units as u
|
|
7
|
+
import numpy as np # # noqa: F401
|
|
8
|
+
import sqlalchemy as sa
|
|
9
|
+
from astropy.table import QTable
|
|
10
|
+
from ctapipe.core import Container
|
|
11
|
+
|
|
12
|
+
import calibpipe.core.common_metadata_containers as common_metadata_module
|
|
13
|
+
|
|
14
|
+
from ...core.exceptions import DBStorageError
|
|
15
|
+
from ..adapter.adapter import Adapter
|
|
16
|
+
from ..adapter.database_containers.container_map import ContainerMap
|
|
17
|
+
from ..adapter.database_containers.table_version_manager import TableVersionManager
|
|
18
|
+
from ..connections import CalibPipeDatabase
|
|
19
|
+
from ..interfaces import sql_metadata
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TableHandler:
|
|
23
|
+
"""
|
|
24
|
+
Handles tables in CalibPipe DataBase.
|
|
25
|
+
|
|
26
|
+
The first method returns a valid insertion for a DB, made by the table instance
|
|
27
|
+
and the values to be inserted. The second method just insert values in a DB,
|
|
28
|
+
provided the DB connection, the table and the values.
|
|
29
|
+
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
def get_database_table_insertion(
|
|
34
|
+
container: Container,
|
|
35
|
+
version: str | None = None,
|
|
36
|
+
) -> tuple[sa.Table, dict[str, Any]]:
|
|
37
|
+
"""Return a valid insertion for a DB made by the table instance, and the values to insert."""
|
|
38
|
+
table, kwargs = Adapter.to_postgres(container, version=version)
|
|
39
|
+
if table is None:
|
|
40
|
+
raise TypeError(f"Table cannot be created for {type(container)}.")
|
|
41
|
+
return table, kwargs
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def insert_row_in_database(
|
|
45
|
+
table: sa.Table,
|
|
46
|
+
kwargs: dict[str, Any],
|
|
47
|
+
connection: CalibPipeDatabase,
|
|
48
|
+
) -> None:
|
|
49
|
+
"""Insert values in a DB table as a row."""
|
|
50
|
+
connection.execute(sa.insert(table).values(**kwargs))
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def read_table_from_database(
|
|
54
|
+
container: Container,
|
|
55
|
+
connection: CalibPipeDatabase,
|
|
56
|
+
condition: str | None = None,
|
|
57
|
+
) -> QTable:
|
|
58
|
+
"""
|
|
59
|
+
Read a table from the DB and return it as a QTable object.
|
|
60
|
+
|
|
61
|
+
An optional argument `condition` shall have the following form:
|
|
62
|
+
`c.<column_name> <operator> <value>`
|
|
63
|
+
or a combination of thereof using `&` and `|` operators.
|
|
64
|
+
In case of compound condition, every singleton must be contained in parentheses.
|
|
65
|
+
"""
|
|
66
|
+
table = ContainerMap.map_to_db_container(container).get_table()
|
|
67
|
+
if condition:
|
|
68
|
+
query = table.select().where(
|
|
69
|
+
eval(condition.replace("c.", "table.c.")) # pylint: disable=eval-used
|
|
70
|
+
)
|
|
71
|
+
else:
|
|
72
|
+
query = table.select()
|
|
73
|
+
rows = connection.execute(query).fetchall()
|
|
74
|
+
if not rows:
|
|
75
|
+
return QTable(
|
|
76
|
+
names=table.columns.keys(),
|
|
77
|
+
units=[
|
|
78
|
+
1 * u.Unit(c.comment) if c.comment else None for c in table.columns
|
|
79
|
+
],
|
|
80
|
+
)
|
|
81
|
+
return QTable(
|
|
82
|
+
rows=rows,
|
|
83
|
+
names=table.columns.keys(),
|
|
84
|
+
units=[1 * u.Unit(c.comment) if c.comment else None for c in table.columns],
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def get_compatible_version(
|
|
89
|
+
version_table: sa.Table,
|
|
90
|
+
table_name: str,
|
|
91
|
+
version: str,
|
|
92
|
+
connection: CalibPipeDatabase,
|
|
93
|
+
) -> str:
|
|
94
|
+
"""
|
|
95
|
+
Get a compatible version for a certain table from the version table.
|
|
96
|
+
|
|
97
|
+
If no compatible version of the table is available, the new version
|
|
98
|
+
the table will be added to the version table.
|
|
99
|
+
"""
|
|
100
|
+
version_major = version.split(".")[0]
|
|
101
|
+
query = sa.select(version_table.c.version).where(
|
|
102
|
+
version_table.c.version.like(f"{version_major}%"),
|
|
103
|
+
version_table.c.name == table_name,
|
|
104
|
+
)
|
|
105
|
+
query_results = connection.execute(query).first()
|
|
106
|
+
if query_results is None:
|
|
107
|
+
vals = {
|
|
108
|
+
"name": table_name,
|
|
109
|
+
"version": version,
|
|
110
|
+
"validity_start": datetime(2023, 1, 1, 0, 0, 1, tzinfo=timezone.utc),
|
|
111
|
+
"validity_end": datetime(2023, 1, 1, 0, 0, 2, tzinfo=timezone.utc),
|
|
112
|
+
}
|
|
113
|
+
TableHandler.insert_row_in_database(
|
|
114
|
+
version_table,
|
|
115
|
+
vals,
|
|
116
|
+
connection=connection,
|
|
117
|
+
)
|
|
118
|
+
return version
|
|
119
|
+
comp_version = query_results[0]
|
|
120
|
+
return comp_version
|
|
121
|
+
|
|
122
|
+
@staticmethod
|
|
123
|
+
def update_tables_info(
|
|
124
|
+
table: sa.Table,
|
|
125
|
+
version_table: sa.Table,
|
|
126
|
+
table_name: str,
|
|
127
|
+
comp_version: str,
|
|
128
|
+
table_version: str,
|
|
129
|
+
connection: CalibPipeDatabase,
|
|
130
|
+
) -> str:
|
|
131
|
+
"""
|
|
132
|
+
Update the tables' info.
|
|
133
|
+
|
|
134
|
+
Updated min and max timestamps are taken from the data table,
|
|
135
|
+
and a check on version is performed to update the version table.
|
|
136
|
+
Also, the name of the table is updated accordingly if version has changed.
|
|
137
|
+
"""
|
|
138
|
+
msg = "DB tables have been updated successfully."
|
|
139
|
+
query = sa.select(
|
|
140
|
+
sa.func.min(table.c.validity_start).label("min_time"),
|
|
141
|
+
sa.func.max(table.c.validity_end).label("max_time"),
|
|
142
|
+
)
|
|
143
|
+
results = connection.execute(query).first()
|
|
144
|
+
|
|
145
|
+
if float(table_version.split(".")[1]) > float(comp_version.split(".")[1]):
|
|
146
|
+
TableHandler.update_version_table(
|
|
147
|
+
version_table,
|
|
148
|
+
table_name,
|
|
149
|
+
comp_version,
|
|
150
|
+
table_version,
|
|
151
|
+
results.min_time,
|
|
152
|
+
results.max_time,
|
|
153
|
+
connection,
|
|
154
|
+
)
|
|
155
|
+
TableHandler.update_table_name(table, table_version, connection)
|
|
156
|
+
return (
|
|
157
|
+
msg
|
|
158
|
+
+ f" Version has been updated from v{comp_version} to v{table_version}."
|
|
159
|
+
)
|
|
160
|
+
TableHandler.update_version_table(
|
|
161
|
+
version_table,
|
|
162
|
+
table_name,
|
|
163
|
+
comp_version,
|
|
164
|
+
comp_version,
|
|
165
|
+
results.min_time,
|
|
166
|
+
results.max_time,
|
|
167
|
+
connection,
|
|
168
|
+
)
|
|
169
|
+
return msg
|
|
170
|
+
|
|
171
|
+
@staticmethod
|
|
172
|
+
def update_version_table(
|
|
173
|
+
version_table: sa.Table,
|
|
174
|
+
table_name: str,
|
|
175
|
+
old_version: str,
|
|
176
|
+
new_version: str,
|
|
177
|
+
min_time: datetime,
|
|
178
|
+
max_time: datetime,
|
|
179
|
+
connection: CalibPipeDatabase,
|
|
180
|
+
) -> None:
|
|
181
|
+
"""Update the version of a table with the new version in the version table of the DB."""
|
|
182
|
+
stmt = (
|
|
183
|
+
sa.update(version_table)
|
|
184
|
+
.where(
|
|
185
|
+
version_table.c.name == table_name,
|
|
186
|
+
version_table.c.version == old_version,
|
|
187
|
+
)
|
|
188
|
+
.values(version=new_version, validity_start=min_time, validity_end=max_time)
|
|
189
|
+
)
|
|
190
|
+
connection.execute(stmt)
|
|
191
|
+
|
|
192
|
+
@staticmethod
|
|
193
|
+
def update_table_name(
|
|
194
|
+
table: sa.Table,
|
|
195
|
+
version: str,
|
|
196
|
+
connection: CalibPipeDatabase,
|
|
197
|
+
) -> None:
|
|
198
|
+
"""Update the name of a table with the new version."""
|
|
199
|
+
new_table_name = TableVersionManager.update_version(table.name, version)
|
|
200
|
+
stmt = sa.text(f"ALTER TABLE {table} RENAME TO {new_table_name};")
|
|
201
|
+
connection.execute(stmt)
|
|
202
|
+
|
|
203
|
+
@staticmethod
|
|
204
|
+
def prepare_db_tables(containers, db_config):
|
|
205
|
+
"""
|
|
206
|
+
Create and upload to the CalibPipe DB empty tables for selected calibration containers.
|
|
207
|
+
|
|
208
|
+
Parameters
|
|
209
|
+
----------
|
|
210
|
+
containers : list[Container]
|
|
211
|
+
list of calibpipe containers or ContainerMeta instances
|
|
212
|
+
that will be created as empty tables in the DB
|
|
213
|
+
|
|
214
|
+
config_data : dict
|
|
215
|
+
Calibpipe configuration with database connection configuration
|
|
216
|
+
"""
|
|
217
|
+
try:
|
|
218
|
+
with CalibPipeDatabase(**db_config) as connection:
|
|
219
|
+
sql_metadata.reflect(bind=connection.engine, extend_existing=True)
|
|
220
|
+
|
|
221
|
+
# Create empty main data tables
|
|
222
|
+
for cp_container in containers:
|
|
223
|
+
if isinstance(cp_container, Container):
|
|
224
|
+
db_container = ContainerMap.map_to_db_container(
|
|
225
|
+
type(cp_container)
|
|
226
|
+
)
|
|
227
|
+
else:
|
|
228
|
+
db_container = ContainerMap.map_to_db_container(cp_container)
|
|
229
|
+
if not sa.inspect(connection.engine).has_table(
|
|
230
|
+
db_container.table_name
|
|
231
|
+
):
|
|
232
|
+
db_container.get_table()
|
|
233
|
+
sql_metadata.create_all(bind=connection.engine)
|
|
234
|
+
except sa.exc.DatabaseError:
|
|
235
|
+
raise DBStorageError("Issues with connection to the CalibPipe DB")
|
|
236
|
+
|
|
237
|
+
@staticmethod
|
|
238
|
+
def upload_data(calibpipe_data_container, config_data):
|
|
239
|
+
"""
|
|
240
|
+
Universal function to upload data and metadata to the DB.
|
|
241
|
+
|
|
242
|
+
Metadata is uploaded based on values in the dictionary config_data.
|
|
243
|
+
It is possible to update fields in the dictionary while performing calibration,
|
|
244
|
+
and transfer the final metadata collection to this function.
|
|
245
|
+
|
|
246
|
+
Parameters
|
|
247
|
+
----------
|
|
248
|
+
calibpipe_data_container : ctapipe.container
|
|
249
|
+
calibpipe container with data that will be uploaded to the main table of DB
|
|
250
|
+
|
|
251
|
+
config_data : dict
|
|
252
|
+
dictionary with configurable values,
|
|
253
|
+
should contain at least DB configuration
|
|
254
|
+
and metadata information for each metadata table.
|
|
255
|
+
|
|
256
|
+
Returns
|
|
257
|
+
-------
|
|
258
|
+
insertion_list : list
|
|
259
|
+
list of metadata dictionaries that were uploaded to DB
|
|
260
|
+
"""
|
|
261
|
+
insertion_list = []
|
|
262
|
+
metadata_dict = {
|
|
263
|
+
container: values
|
|
264
|
+
for container, values in config_data.items()
|
|
265
|
+
if "Reference" in container
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
data_db_container = ContainerMap.map_to_db_container(
|
|
269
|
+
type(calibpipe_data_container)
|
|
270
|
+
)
|
|
271
|
+
has_autoincrement_pk = any(
|
|
272
|
+
col.autoincrement for col in data_db_container.get_table().c
|
|
273
|
+
)
|
|
274
|
+
is_single_pk = len(data_db_container.get_primary_keys()) == 1
|
|
275
|
+
# Check if there are only one autoincremented pk in the table
|
|
276
|
+
if has_autoincrement_pk and is_single_pk:
|
|
277
|
+
pk_name = data_db_container.get_primary_keys()[0].name
|
|
278
|
+
try:
|
|
279
|
+
with CalibPipeDatabase(
|
|
280
|
+
**config_data["database_configuration"]
|
|
281
|
+
) as connection:
|
|
282
|
+
TableHandler.insert_row_in_database(
|
|
283
|
+
data_db_container.get_table(),
|
|
284
|
+
calibpipe_data_container,
|
|
285
|
+
connection,
|
|
286
|
+
)
|
|
287
|
+
# Get the last uploaded DB record,
|
|
288
|
+
# to which all metadata will be attached
|
|
289
|
+
stmt = (
|
|
290
|
+
sa.select(data_db_container.get_table())
|
|
291
|
+
.order_by(sa.desc(data_db_container.get_table().c[pk_name]))
|
|
292
|
+
.limit(1)
|
|
293
|
+
)
|
|
294
|
+
last_db_record = connection.execute(stmt).fetchone()
|
|
295
|
+
data_pk_value = last_db_record._asdict()[pk_name]
|
|
296
|
+
|
|
297
|
+
# We should process Reference metadata separately,
|
|
298
|
+
# because it contains autoincremented PK
|
|
299
|
+
# to which all other metadata are connected
|
|
300
|
+
cp_container = getattr(
|
|
301
|
+
common_metadata_module, "ReferenceMetadataContainer"
|
|
302
|
+
)
|
|
303
|
+
db_container = ContainerMap.map_to_db_container(cp_container)
|
|
304
|
+
reference_meta_insertion = cp_container(
|
|
305
|
+
ID_optical_throughput=data_pk_value,
|
|
306
|
+
**config_data["ReferenceMetadataContainer"],
|
|
307
|
+
)
|
|
308
|
+
TableHandler.insert_row_in_database(
|
|
309
|
+
db_container.get_table(), reference_meta_insertion, connection
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
# Extract value of the Reference metadata PK,
|
|
313
|
+
# and connect to it all other metadata tables
|
|
314
|
+
stmt = (
|
|
315
|
+
sa.select(db_container.get_table())
|
|
316
|
+
.order_by(sa.desc(db_container.get_table().c.ID))
|
|
317
|
+
.limit(1)
|
|
318
|
+
)
|
|
319
|
+
metadata_id = connection.execute(stmt).fetchone()
|
|
320
|
+
|
|
321
|
+
# Remove Reference metadata from the dict to not process it second time
|
|
322
|
+
metadata_dict.pop("ReferenceMetadataContainer", None)
|
|
323
|
+
|
|
324
|
+
# Create list with values that should be inserted
|
|
325
|
+
# to the metadata tables in the DB
|
|
326
|
+
for container in metadata_dict.keys():
|
|
327
|
+
cp_container = getattr(common_metadata_module, container)
|
|
328
|
+
insertion_list.append(
|
|
329
|
+
cp_container(ID=metadata_id.ID, **config_data[container])
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
# Upload metadata values to the DB
|
|
333
|
+
for insertion, container in zip(
|
|
334
|
+
insertion_list, metadata_dict.keys()
|
|
335
|
+
):
|
|
336
|
+
cp_container = getattr(common_metadata_module, container)
|
|
337
|
+
db_container = ContainerMap.map_to_db_container(cp_container)
|
|
338
|
+
TableHandler.insert_row_in_database(
|
|
339
|
+
db_container.get_table(), insertion, connection
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
insertion_list = [reference_meta_insertion] + insertion_list
|
|
343
|
+
except sa.exc.DatabaseError:
|
|
344
|
+
raise DBStorageError("Issues with connection to the CalibPipe DB")
|
|
345
|
+
else:
|
|
346
|
+
raise ValueError(
|
|
347
|
+
f"Table '{data_db_container.table_name}' "
|
|
348
|
+
"doesn't contain single autoincremented primary key."
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
return insertion_list
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Type definitions for SQLAlchemy.
|
|
3
|
+
|
|
4
|
+
These type definitions allow us to define database fields and
|
|
5
|
+
containers being almost completely decoupled from SQLAlchemy
|
|
6
|
+
(without direct coupling).
|
|
7
|
+
|
|
8
|
+
In particular, SQLColumnInfo and SQLTableInfo use these generic
|
|
9
|
+
types and not the sqlalchemy types directly.
|
|
10
|
+
|
|
11
|
+
The NDArray type is defined explicitly to implemented the
|
|
12
|
+
serialization/deserialization np.ndarray <-> bytes and the
|
|
13
|
+
(optional) zlib compression/decompression on the byte data.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import pickle
|
|
18
|
+
import zlib
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
import sqlalchemy as sa
|
|
22
|
+
import sqlalchemy.sql.sqltypes
|
|
23
|
+
from sqlalchemy.dialects.postgresql import ARRAY, DOUBLE_PRECISION
|
|
24
|
+
|
|
25
|
+
ColumnType = sqlalchemy.sql.sqltypes.TypeEngine
|
|
26
|
+
|
|
27
|
+
Boolean: ColumnType = sa.Boolean
|
|
28
|
+
|
|
29
|
+
SmallInteger: ColumnType = sa.SmallInteger
|
|
30
|
+
Integer: ColumnType = sa.Integer
|
|
31
|
+
BigInteger: ColumnType = sa.BigInteger
|
|
32
|
+
Float: ColumnType = sa.Float
|
|
33
|
+
Double: ColumnType = DOUBLE_PRECISION
|
|
34
|
+
Numeric: ColumnType = sa.Numeric
|
|
35
|
+
Binary: ColumnType = sa.types.LargeBinary
|
|
36
|
+
String: ColumnType = sa.String
|
|
37
|
+
|
|
38
|
+
ArrayF1D: ColumnType = ARRAY(Float, dimensions=1)
|
|
39
|
+
ArrayF2D: ColumnType = ARRAY(Float, dimensions=2)
|
|
40
|
+
ArrayF3D: ColumnType = ARRAY(Float, dimensions=3)
|
|
41
|
+
|
|
42
|
+
Date: ColumnType = sa.Date
|
|
43
|
+
Time: ColumnType = sa.Time
|
|
44
|
+
DateTime: ColumnType = sa.DateTime
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class NDArray(sa.types.TypeDecorator): # pylint: disable=too-many-ancestors
|
|
48
|
+
"""
|
|
49
|
+
Type for numpy.ndarray binding, include data compression.
|
|
50
|
+
|
|
51
|
+
The array is stored as a compressed byte string in the database.
|
|
52
|
+
The class implements the binding between the `np.ndarray` in the
|
|
53
|
+
program memory and the byte string stored in the DB.
|
|
54
|
+
|
|
55
|
+
Compression can be removed or modified, but the two process methods
|
|
56
|
+
should be the opposite of each other for the binding to work.
|
|
57
|
+
Ignoring the dialect parameter that is anyway not used, this means
|
|
58
|
+
that the following assertion should always pass::
|
|
59
|
+
|
|
60
|
+
db_arr: NDArray
|
|
61
|
+
arr: np.ndarray
|
|
62
|
+
arr_bytes: bytes = db_arr.process_bind_param(arr)
|
|
63
|
+
recov_arr: np.ndarray = db_arr.process_result_value(arr_bytes)
|
|
64
|
+
assert(arr == recov_arr)
|
|
65
|
+
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
impl = sa.types.LargeBinary # Byte storage in the DB
|
|
69
|
+
cache_ok: bool = True # Results of process methods can be cached
|
|
70
|
+
|
|
71
|
+
def process_bind_param(self, value: np.ndarray, dialect) -> bytes:
|
|
72
|
+
"""
|
|
73
|
+
Serialize a np.ndarray into a byte object to store in the DB.
|
|
74
|
+
|
|
75
|
+
The array is first serialized into bytes and compressed using
|
|
76
|
+
the default zlib compression algorithm.
|
|
77
|
+
"""
|
|
78
|
+
return zlib.compress(pickle.dumps(value))
|
|
79
|
+
|
|
80
|
+
def process_result_value(self, value: bytes, dialect) -> np.ndarray:
|
|
81
|
+
"""
|
|
82
|
+
Deserialize a np.ndarray from bytes read in the DB.
|
|
83
|
+
|
|
84
|
+
The bytes are first decompressed and the array is loaded from
|
|
85
|
+
the decompressed byte string.
|
|
86
|
+
"""
|
|
87
|
+
return pickle.loads(zlib.decompress(value))
|
|
88
|
+
|
|
89
|
+
def process_literal_param(self, value: np.ndarray, dialect) -> str:
|
|
90
|
+
"""Representation of the NDArray object."""
|
|
91
|
+
return f"NDArray(shape={value.shape}, dtype={value.dtype})"
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def python_type(self) -> type:
|
|
95
|
+
"""Return the python type of the underlying object represented by the byte string."""
|
|
96
|
+
return np.ndarray
|
|
File without changes
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# %ECSV 1.0
|
|
2
|
+
# ---
|
|
3
|
+
# datatype:
|
|
4
|
+
# - {name: height, unit: m, datatype: float64}
|
|
5
|
+
# - {name: number density, unit: 1 / cm3, datatype: float64}
|
|
6
|
+
# schema: astropy-2.0
|
|
7
|
+
height "number density"
|
|
8
|
+
0.0 2.5226221253975482e+19
|
|
9
|
+
1000.0 2.2612248701943214e+19
|
|
10
|
+
2000.0 2.0188159791161926e+19
|
|
11
|
+
3000.0 1.821777495861036e+19
|
|
12
|
+
4000.0 1.6455659215053523e+19
|
|
13
|
+
5000.0 1.4943555611850443e+19
|
|
14
|
+
6000.0 1.3555512867555471e+19
|
|
15
|
+
7000.0 1.2239497020511496e+19
|
|
16
|
+
8000.0 1.1015931343575489e+19
|
|
17
|
+
9000.0 9.89813592093043e+18
|
|
18
|
+
10000.0 8.860096921932773e+18
|
|
19
|
+
11000.0 7.904336449894107e+18
|
|
20
|
+
12000.0 6.990997567438649e+18
|
|
21
|
+
13000.0 6.139328580924725e+18
|
|
22
|
+
14000.0 5.360396027080355e+18
|
|
23
|
+
15000.0 4.644145761612367e+18
|
|
24
|
+
16000.0 3.989701117077186e+18
|
|
25
|
+
17000.0 3.4018252254753705e+18
|
|
26
|
+
18000.0 2.88252588630463e+18
|
|
27
|
+
19000.0 2.428328176278362e+18
|
|
28
|
+
20000.0 2.035126555710463e+18
|
|
29
|
+
21000.0 1.6988154849148237e+18
|
|
30
|
+
22000.0 1.4152894242053386e+18
|
|
31
|
+
23000.0 1.1804428338959007e+18
|
|
32
|
+
24000.0 9.901701743004028e+17
|
|
33
|
+
25000.0 8.403659057327388e+17
|
|
34
|
+
26000.0 7.269244885068019e+17
|