ctao-calibpipe 0.1.0__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.

Files changed (93) hide show
  1. calibpipe/__init__.py +5 -0
  2. calibpipe/_dev_version/__init__.py +9 -0
  3. calibpipe/_version.py +21 -0
  4. calibpipe/atmosphere/__init__.py +1 -0
  5. calibpipe/atmosphere/atmosphere_containers.py +109 -0
  6. calibpipe/atmosphere/meteo_data_handlers.py +485 -0
  7. calibpipe/atmosphere/models/README.md +14 -0
  8. calibpipe/atmosphere/models/__init__.py +1 -0
  9. calibpipe/atmosphere/models/macobac.ecsv +23 -0
  10. calibpipe/atmosphere/models/reference_MDPs/__init__.py +1 -0
  11. calibpipe/atmosphere/models/reference_MDPs/ref_density_at_15km_ctao-north_intermediate.ecsv +8 -0
  12. calibpipe/atmosphere/models/reference_MDPs/ref_density_at_15km_ctao-north_summer.ecsv +8 -0
  13. calibpipe/atmosphere/models/reference_MDPs/ref_density_at_15km_ctao-north_winter.ecsv +8 -0
  14. calibpipe/atmosphere/models/reference_MDPs/ref_density_at_15km_ctao-south_summer.ecsv +8 -0
  15. calibpipe/atmosphere/models/reference_MDPs/ref_density_at_15km_ctao-south_winter.ecsv +8 -0
  16. calibpipe/atmosphere/models/reference_atmospheres/__init__.py +1 -0
  17. calibpipe/atmosphere/models/reference_atmospheres/reference_atmo_model_v0_ctao-north_intermediate.ecsv +73 -0
  18. calibpipe/atmosphere/models/reference_atmospheres/reference_atmo_model_v0_ctao-north_summer.ecsv +73 -0
  19. calibpipe/atmosphere/models/reference_atmospheres/reference_atmo_model_v0_ctao-north_winter.ecsv +73 -0
  20. calibpipe/atmosphere/models/reference_atmospheres/reference_atmo_model_v0_ctao-south_summer.ecsv +73 -0
  21. calibpipe/atmosphere/models/reference_atmospheres/reference_atmo_model_v0_ctao-south_winter.ecsv +73 -0
  22. calibpipe/atmosphere/models/reference_rayleigh_scattering_profiles/__init__.py +1 -0
  23. calibpipe/atmosphere/models/reference_rayleigh_scattering_profiles/reference_rayleigh_extinction_profile_v0_ctao-north_intermediate.ecsv +857 -0
  24. calibpipe/atmosphere/models/reference_rayleigh_scattering_profiles/reference_rayleigh_extinction_profile_v0_ctao-north_summer.ecsv +857 -0
  25. calibpipe/atmosphere/models/reference_rayleigh_scattering_profiles/reference_rayleigh_extinction_profile_v0_ctao-north_winter.ecsv +857 -0
  26. calibpipe/atmosphere/models/reference_rayleigh_scattering_profiles/reference_rayleigh_extinction_profile_v0_ctao-south_summer.ecsv +857 -0
  27. calibpipe/atmosphere/models/reference_rayleigh_scattering_profiles/reference_rayleigh_extinction_profile_v0_ctao-south_winter.ecsv +857 -0
  28. calibpipe/atmosphere/templates/request_templates/__init__.py +1 -0
  29. calibpipe/atmosphere/templates/request_templates/copernicus.json +11 -0
  30. calibpipe/atmosphere/templates/request_templates/gdas.json +12 -0
  31. calibpipe/core/__init__.py +39 -0
  32. calibpipe/core/common_metadata_containers.py +195 -0
  33. calibpipe/core/exceptions.py +87 -0
  34. calibpipe/database/__init__.py +24 -0
  35. calibpipe/database/adapter/__init__.py +23 -0
  36. calibpipe/database/adapter/adapter.py +80 -0
  37. calibpipe/database/adapter/database_containers/__init__.py +61 -0
  38. calibpipe/database/adapter/database_containers/atmosphere.py +199 -0
  39. calibpipe/database/adapter/database_containers/common_metadata.py +148 -0
  40. calibpipe/database/adapter/database_containers/container_map.py +59 -0
  41. calibpipe/database/adapter/database_containers/observatory.py +61 -0
  42. calibpipe/database/adapter/database_containers/table_version_manager.py +39 -0
  43. calibpipe/database/adapter/database_containers/version_control.py +17 -0
  44. calibpipe/database/connections/__init__.py +28 -0
  45. calibpipe/database/connections/calibpipe_database.py +60 -0
  46. calibpipe/database/connections/postgres_utils.py +97 -0
  47. calibpipe/database/connections/sql_connection.py +103 -0
  48. calibpipe/database/connections/user_confirmation.py +19 -0
  49. calibpipe/database/interfaces/__init__.py +71 -0
  50. calibpipe/database/interfaces/hashable_row_data.py +54 -0
  51. calibpipe/database/interfaces/queries.py +180 -0
  52. calibpipe/database/interfaces/sql_column_info.py +67 -0
  53. calibpipe/database/interfaces/sql_metadata.py +6 -0
  54. calibpipe/database/interfaces/sql_table_info.py +131 -0
  55. calibpipe/database/interfaces/table_handler.py +351 -0
  56. calibpipe/database/interfaces/types.py +96 -0
  57. calibpipe/tests/data/atmosphere/molecular_atmosphere/__init__.py +0 -0
  58. calibpipe/tests/data/atmosphere/molecular_atmosphere/contemporary_MDP.ecsv +34 -0
  59. calibpipe/tests/data/atmosphere/molecular_atmosphere/macobac.csv +852 -0
  60. calibpipe/tests/data/atmosphere/molecular_atmosphere/macobac.ecsv +23 -0
  61. calibpipe/tests/data/atmosphere/molecular_atmosphere/merged_file.ecsv +1082 -0
  62. calibpipe/tests/data/atmosphere/molecular_atmosphere/meteo_data_copernicus.ecsv +1082 -0
  63. calibpipe/tests/data/atmosphere/molecular_atmosphere/meteo_data_gdas.ecsv +66 -0
  64. calibpipe/tests/data/atmosphere/molecular_atmosphere/observatory_configurations.json +71 -0
  65. calibpipe/tests/data/utils/__init__.py +0 -0
  66. calibpipe/tests/data/utils/meteo_data_winter_and_summer.ecsv +12992 -0
  67. calibpipe/tests/unittests/atmosphere/astral_testing.py +107 -0
  68. calibpipe/tests/unittests/atmosphere/test_meteo_data_handler.py +775 -0
  69. calibpipe/tests/unittests/atmosphere/test_molecular_atmosphere.py +327 -0
  70. calibpipe/tests/unittests/database/test_table_handler.py +66 -0
  71. calibpipe/tests/unittests/database/test_types.py +38 -0
  72. calibpipe/tests/unittests/test_bootstrap_db.py +79 -0
  73. calibpipe/tests/unittests/utils/test_observatory.py +309 -0
  74. calibpipe/tools/atmospheric_base_tool.py +78 -0
  75. calibpipe/tools/atmospheric_model_db_loader.py +181 -0
  76. calibpipe/tools/basic_tool_with_db.py +38 -0
  77. calibpipe/tools/contemporary_mdp_producer.py +87 -0
  78. calibpipe/tools/init_db.py +37 -0
  79. calibpipe/tools/macobac_calculator.py +82 -0
  80. calibpipe/tools/molecular_atmospheric_model_producer.py +197 -0
  81. calibpipe/tools/observatory_data_db_loader.py +71 -0
  82. calibpipe/tools/reference_atmospheric_model_selector.py +201 -0
  83. calibpipe/utils/__init__.py +10 -0
  84. calibpipe/utils/observatory.py +486 -0
  85. calibpipe/utils/observatory_containers.py +26 -0
  86. calibpipe/version.py +24 -0
  87. ctao_calibpipe-0.1.0.dist-info/METADATA +86 -0
  88. ctao_calibpipe-0.1.0.dist-info/RECORD +93 -0
  89. ctao_calibpipe-0.1.0.dist-info/WHEEL +5 -0
  90. ctao_calibpipe-0.1.0.dist-info/entry_points.txt +8 -0
  91. ctao_calibpipe-0.1.0.dist-info/licenses/AUTHORS.md +13 -0
  92. ctao_calibpipe-0.1.0.dist-info/licenses/LICENSE +21 -0
  93. ctao_calibpipe-0.1.0.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
@@ -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