opensampl 1.2.2__tar.gz → 1.2.3__tar.gz
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.
- {opensampl-1.2.2 → opensampl-1.2.3}/PKG-INFO +2 -2
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/cli.py +9 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/config/server.py +1 -1
- opensampl-1.2.3/opensampl/helpers/convert_value.py +253 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/load_data.py +1 -1
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/backend/main.py +4 -1
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/docker-compose.dev.yaml +19 -3
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/docker-compose.yaml +17 -3
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/migrations/Dockerfile +1 -1
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/migrations/_migrations/env.py +5 -1
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/migrations/_migrations/versions/2026_09_11_1445_update_probe_data_types.py +0 -12
- {opensampl-1.2.2 → opensampl-1.2.3}/pyproject.toml +2 -2
- {opensampl-1.2.2 → opensampl-1.2.3}/.gitignore +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/LICENSE +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/README.md +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/__init__.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/collect/__init__.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/collect/cli.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/collect/microchip/__init__.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/collect/microchip/tp4100/__init__.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/collect/microchip/tp4100/collect_4100.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/collect/microchip/twst/__init__.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/collect/microchip/twst/context.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/collect/microchip/twst/generate_twst_files.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/collect/microchip/twst/readings.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/collect/modem.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/config/__init__.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/config/base.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/config/tp4100.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/create/__init__.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/create/create_vendor.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/create/insert_markers.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/create/templates/collect_mixin_template.txt +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/create/templates/compound_reference_template.txt +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/create/templates/metric_template.txt +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/create/templates/orm_metadata.txt +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/create/templates/parser_template.txt +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/create/templates/reference_template.txt +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/create/templates/vendor_template.txt +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/db/__init__.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/db/access_orm.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/db/orm.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/helpers/__init__.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/helpers/geolocator.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/load/__init__.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/load/data.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/load/routing.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/load/table_factory.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/metrics.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/mixins/__init__.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/mixins/collect.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/mixins/random_data.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/references.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/__init__.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/backend/Dockerfile +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/backend/__init__.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/cli.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/cli2.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/default.env +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/grafana/Dockerfile +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/grafana/grafana-dashboards/ntp_dash.json +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/grafana/grafana-dashboards/public-timing-dashboard.json +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/grafana/grafana-dashboards/public_twst_dash.json +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/grafana/grafana-dashboards/timing-dashboard2.json +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/grafana/grafana-dashboards/twst_dash.json +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/grafana/grafana-provisioning/dashboards/dashboard.yaml +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/grafana/grafana-provisioning/datasources/postgresql.yaml +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/grafana/grafana.env +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/migrations/_migrations/README +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/migrations/_migrations/script.py.mako +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/migrations/_migrations/versions/2024_03_26_1145_create_schema_initialize_orm.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/migrations/_migrations/versions/2024_12_04_1155_update_db_tables.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/migrations/_migrations/versions/2025_01_28_2212_create_time_buckets.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/migrations/_migrations/versions/2025_01_29_0909_updating_location_and_test_tables.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/migrations/_migrations/versions/2025_03_05_0958_add_grafana_user_access.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/migrations/_migrations/versions/2025_03_26_0743_create_campus_view.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/migrations/_migrations/versions/2025_04_14_1231_update_retention_policy.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/migrations/_migrations/versions/2025_04_22_1531_add_access_tokens.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/migrations/_migrations/versions/2025_04_22_1650_turn_off_matviews.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/migrations/_migrations/versions/2025_06_03_1223_add_column_comments.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/migrations/_migrations/versions/2025_06_03_1235_create_reference_and_metric_tables.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/migrations/_migrations/versions/2025_06_03_1254_update_probe_data_to_take_reference_and_.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/migrations/_migrations/versions/2025_06_03_1318_adding_freeform_metadata_to_adva.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/migrations/_migrations/versions/2025_06_03_1358_setting_default_reference_and_metric.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/migrations/_migrations/versions/2025_06_03_1539_making_default_trigger_functions.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/migrations/_migrations/versions/2025_06_23_0825_data_filtering_functions.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/migrations/_migrations/versions/2025_06_23_1654_making_microsemi_table.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/migrations/_migrations/versions/2025_08_15_0840_create_microchip_tp400_metadata.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/migrations/_migrations/versions/2025_09_22_0915_campus_view_not_forced.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/migrations/_migrations/versions/2026_04_17_1243_add_ntp_values.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/migrations/_migrations/versions/2026_04_17_1254_add_reference_view.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/migrations/alembic.ini +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/vendors/__init__.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/vendors/adva.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/vendors/base_probe.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/vendors/constants.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/vendors/microchip/__init__.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/vendors/microchip/tp4100.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/vendors/microchip/twst.py +0 -0
- {opensampl-1.2.2 → opensampl-1.2.3}/opensampl/vendors/ntp.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: opensampl
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.3
|
|
4
4
|
Summary: Python tools for adding clock data to a timescale db.
|
|
5
5
|
Project-URL: Homepage, https://github.com/ORNL/OpenSAMPL
|
|
6
6
|
Project-URL: Repository, https://github.com/ORNL/OpenSAMPL
|
|
@@ -60,7 +60,7 @@ Requires-Python: <3.15,>=3.10
|
|
|
60
60
|
Requires-Dist: allantools
|
|
61
61
|
Requires-Dist: astor
|
|
62
62
|
Requires-Dist: click<9,>=8.0.0
|
|
63
|
-
Requires-Dist: geoalchemy2
|
|
63
|
+
Requires-Dist: geoalchemy2<1,>=0.18.0
|
|
64
64
|
Requires-Dist: jinja2>=3.1.6
|
|
65
65
|
Requires-Dist: libcst
|
|
66
66
|
Requires-Dist: loguru<0.8,>=0.7.0
|
|
@@ -18,6 +18,7 @@ from loguru import logger
|
|
|
18
18
|
|
|
19
19
|
from opensampl.config.base import BaseConfig as CLIConfig
|
|
20
20
|
from opensampl.db.orm import get_table_names
|
|
21
|
+
from opensampl.helpers.convert_value import value_backfill
|
|
21
22
|
from opensampl.load_data import create_new_tables, write_to_table
|
|
22
23
|
from opensampl.mixins.collect import CollectMixin
|
|
23
24
|
from opensampl.mixins.random_data import RandomDataMixin
|
|
@@ -176,6 +177,14 @@ def config_set(ctx: click.Context, name: str, value: str):
|
|
|
176
177
|
conf.set_by_name(name=name, value=value)
|
|
177
178
|
|
|
178
179
|
|
|
180
|
+
@cli.group(cls=CaseInsensitiveGroup)
|
|
181
|
+
def maintenance():
|
|
182
|
+
"""Run direct-database maintenance operations."""
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
maintenance.add_command(value_backfill)
|
|
186
|
+
|
|
187
|
+
|
|
179
188
|
@cli.group(cls=CaseInsensitiveGroup)
|
|
180
189
|
def load():
|
|
181
190
|
"""Load data into database"""
|
|
@@ -133,5 +133,5 @@ class ServerConfig(BaseConfig):
|
|
|
133
133
|
password = self.docker_env_values.get("POSTGRES_PASSWORD")
|
|
134
134
|
db = self.docker_env_values.get("POSTGRES_DB")
|
|
135
135
|
if all(x is not None for x in [user, password, db]):
|
|
136
|
-
return f"postgresql://{user}:{password}@localhost:5415/{db}"
|
|
136
|
+
return f"postgresql+psycopg2://{user}:{password}@localhost:5415/{db}"
|
|
137
137
|
raise ValueError("Database environment variables POSTGRES_USER, POSTGRES_PASSWORD, or POSTGRES_DB are not set.")
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""Backfill numeric probe values after the probe-data type migration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
8
|
+
from datetime import datetime, timedelta
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
from loguru import logger
|
|
13
|
+
from sqlalchemy import create_engine, text
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from collections.abc import Callable, Iterable
|
|
17
|
+
|
|
18
|
+
from sqlalchemy import Engine
|
|
19
|
+
|
|
20
|
+
DEFAULT_BATCH_SIZE = "1d"
|
|
21
|
+
_DURATION_PATTERN = re.compile(r"^(?P<value>(?:\d+(?:\.\d*)?|\.\d+))(?P<unit>[mhdw])$", re.IGNORECASE)
|
|
22
|
+
_DURATION_SECONDS = {
|
|
23
|
+
"m": 60,
|
|
24
|
+
"h": 60 * 60,
|
|
25
|
+
"d": 24 * 60 * 60,
|
|
26
|
+
"w": 7 * 24 * 60 * 60,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
METRIC_TYPES_SQL = text("""
|
|
30
|
+
SELECT uuid
|
|
31
|
+
FROM castdb.metric_type
|
|
32
|
+
WHERE value_type IN ('float', 'int')
|
|
33
|
+
""")
|
|
34
|
+
|
|
35
|
+
SCHEMA_COLUMNS_SQL = text("""
|
|
36
|
+
SELECT column_name
|
|
37
|
+
FROM information_schema.columns
|
|
38
|
+
WHERE table_schema = 'castdb'
|
|
39
|
+
AND table_name = 'probe_data'
|
|
40
|
+
AND column_name IN ('value_jsonb', 'value_float')
|
|
41
|
+
""")
|
|
42
|
+
|
|
43
|
+
RANGE_SQL = text("""
|
|
44
|
+
SELECT
|
|
45
|
+
min(pd.time) AS start_time,
|
|
46
|
+
max(pd.time) AS end_time
|
|
47
|
+
FROM castdb.probe_data pd
|
|
48
|
+
WHERE pd.metric_type_uuid = ANY(:metric_type_uuids)
|
|
49
|
+
AND pd.value_float IS NULL
|
|
50
|
+
AND pd.value_jsonb IS NOT NULL
|
|
51
|
+
""")
|
|
52
|
+
|
|
53
|
+
UPDATE_SQL = text("""
|
|
54
|
+
UPDATE castdb.probe_data
|
|
55
|
+
SET value_float = (value_jsonb #>> '{}')::double precision
|
|
56
|
+
WHERE metric_type_uuid = ANY(:metric_type_uuids)
|
|
57
|
+
AND value_float IS NULL
|
|
58
|
+
AND value_jsonb IS NOT NULL
|
|
59
|
+
AND time >= :start_time
|
|
60
|
+
AND time < :end_time
|
|
61
|
+
""")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def parse_duration(value: str) -> timedelta:
|
|
65
|
+
"""Parse a positive duration expressed in minutes, hours, days, or weeks."""
|
|
66
|
+
match = _DURATION_PATTERN.fullmatch(value.strip())
|
|
67
|
+
if match is None:
|
|
68
|
+
raise click.BadParameter("must be a positive duration such as 30m, 12h, 1d, or 2w")
|
|
69
|
+
|
|
70
|
+
seconds = float(match.group("value")) * _DURATION_SECONDS[match.group("unit").lower()]
|
|
71
|
+
if seconds <= 0:
|
|
72
|
+
raise click.BadParameter("must be greater than zero")
|
|
73
|
+
return timedelta(seconds=seconds)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def resolve_workers(workers: int | None) -> int:
|
|
77
|
+
"""Resolve worker count from the option, environment, or local CPU count."""
|
|
78
|
+
if workers is not None:
|
|
79
|
+
return workers
|
|
80
|
+
|
|
81
|
+
env_workers = os.getenv("WORKERS")
|
|
82
|
+
if env_workers is not None:
|
|
83
|
+
try:
|
|
84
|
+
resolved = int(env_workers)
|
|
85
|
+
except ValueError as exc:
|
|
86
|
+
raise click.BadParameter("WORKERS must be a positive integer", param_hint="--workers") from exc
|
|
87
|
+
if resolved <= 0:
|
|
88
|
+
raise click.BadParameter("WORKERS must be a positive integer", param_hint="--workers")
|
|
89
|
+
return resolved
|
|
90
|
+
|
|
91
|
+
return os.cpu_count() or 4
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def build_windows(
|
|
95
|
+
start_time: datetime,
|
|
96
|
+
end_time: datetime,
|
|
97
|
+
batch_size: timedelta,
|
|
98
|
+
) -> list[tuple[datetime, datetime]]:
|
|
99
|
+
"""Build newest-first half-open time windows that include the final row."""
|
|
100
|
+
end_exclusive = end_time + timedelta(microseconds=1)
|
|
101
|
+
windows: list[tuple[datetime, datetime]] = []
|
|
102
|
+
current_start = start_time
|
|
103
|
+
while current_start < end_exclusive:
|
|
104
|
+
current_end = min(current_start + batch_size, end_exclusive)
|
|
105
|
+
windows.append((current_start, current_end))
|
|
106
|
+
current_start = current_end
|
|
107
|
+
windows.reverse()
|
|
108
|
+
return windows
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _validate_schema(engine: Engine) -> None:
|
|
112
|
+
"""Ensure the value-type migration has created both required columns."""
|
|
113
|
+
with engine.connect() as conn:
|
|
114
|
+
columns = set(conn.execute(SCHEMA_COLUMNS_SQL).scalars().all())
|
|
115
|
+
missing = {"value_jsonb", "value_float"} - columns
|
|
116
|
+
if missing:
|
|
117
|
+
missing_names = ", ".join(sorted(missing))
|
|
118
|
+
raise RuntimeError(
|
|
119
|
+
f"The probe-data type migration has not been applied; missing castdb.probe_data column(s): {missing_names}"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _iter_waves(
|
|
124
|
+
windows: list[tuple[datetime, datetime]],
|
|
125
|
+
vacuum_every_batches: int,
|
|
126
|
+
) -> Iterable[list[tuple[datetime, datetime]]]:
|
|
127
|
+
"""Split windows at vacuum boundaries, or return one wave when disabled."""
|
|
128
|
+
wave_size = vacuum_every_batches or len(windows)
|
|
129
|
+
for index in range(0, len(windows), wave_size):
|
|
130
|
+
yield windows[index : index + wave_size]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def run_backfill(
|
|
134
|
+
database_url: str,
|
|
135
|
+
workers: int,
|
|
136
|
+
batch_size: timedelta,
|
|
137
|
+
vacuum_every_batches: int,
|
|
138
|
+
*,
|
|
139
|
+
engine_factory: Callable[..., Engine] | None = None,
|
|
140
|
+
) -> int:
|
|
141
|
+
"""Backfill numeric values and return the number of rows updated."""
|
|
142
|
+
factory = engine_factory or create_engine
|
|
143
|
+
engine = factory(database_url, pool_size=workers + 1)
|
|
144
|
+
logger.info("Using database: {}", engine.url)
|
|
145
|
+
logger.info("Workers: {}", workers)
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
_validate_schema(engine)
|
|
149
|
+
with engine.connect() as conn:
|
|
150
|
+
metric_type_uuids = conn.execute(METRIC_TYPES_SQL).scalars().all()
|
|
151
|
+
|
|
152
|
+
if not metric_type_uuids:
|
|
153
|
+
logger.info("No float/int metric types found; nothing to map.")
|
|
154
|
+
return 0
|
|
155
|
+
|
|
156
|
+
with engine.connect() as conn:
|
|
157
|
+
time_range = conn.execute(
|
|
158
|
+
RANGE_SQL,
|
|
159
|
+
{"metric_type_uuids": metric_type_uuids},
|
|
160
|
+
).one()
|
|
161
|
+
|
|
162
|
+
if time_range.start_time is None or time_range.end_time is None:
|
|
163
|
+
logger.info("No values require backfilling.")
|
|
164
|
+
return 0
|
|
165
|
+
|
|
166
|
+
windows = build_windows(time_range.start_time, time_range.end_time, batch_size)
|
|
167
|
+
logger.info("Backfilling values from {} through {}", time_range.start_time, time_range.end_time)
|
|
168
|
+
|
|
169
|
+
def run_batch(window: tuple[datetime, datetime]) -> int:
|
|
170
|
+
batch_start, batch_end = window
|
|
171
|
+
with engine.begin() as conn:
|
|
172
|
+
conn.execute(text("SET LOCAL synchronous_commit = off"))
|
|
173
|
+
result = conn.execute(
|
|
174
|
+
UPDATE_SQL,
|
|
175
|
+
{
|
|
176
|
+
"metric_type_uuids": metric_type_uuids,
|
|
177
|
+
"start_time": batch_start,
|
|
178
|
+
"end_time": batch_end,
|
|
179
|
+
},
|
|
180
|
+
)
|
|
181
|
+
logger.info("{} -> {}: committed {:,} rows", batch_start, batch_end, result.rowcount)
|
|
182
|
+
return result.rowcount
|
|
183
|
+
|
|
184
|
+
total_updated = 0
|
|
185
|
+
vacuum_engine = engine.execution_options(isolation_level="AUTOCOMMIT")
|
|
186
|
+
with ThreadPoolExecutor(max_workers=workers) as pool:
|
|
187
|
+
for wave in _iter_waves(windows, vacuum_every_batches):
|
|
188
|
+
total_updated += sum(pool.map(run_batch, wave))
|
|
189
|
+
logger.info("Vacuuming probe_data...")
|
|
190
|
+
with vacuum_engine.connect() as conn:
|
|
191
|
+
conn.execute(text("VACUUM castdb.probe_data"))
|
|
192
|
+
|
|
193
|
+
logger.info("Value backfill complete; updated {:,} rows.", total_updated)
|
|
194
|
+
return total_updated
|
|
195
|
+
finally:
|
|
196
|
+
engine.dispose()
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@click.command("value-backfill")
|
|
200
|
+
@click.option(
|
|
201
|
+
"--workers",
|
|
202
|
+
type=click.IntRange(min=1),
|
|
203
|
+
default=None,
|
|
204
|
+
help="Parallel database workers. Defaults to WORKERS or the local CPU count.",
|
|
205
|
+
)
|
|
206
|
+
@click.option(
|
|
207
|
+
"--batch-size",
|
|
208
|
+
type=str,
|
|
209
|
+
default=DEFAULT_BATCH_SIZE,
|
|
210
|
+
show_default=True,
|
|
211
|
+
metavar="DURATION",
|
|
212
|
+
help="Time span per batch, using m, h, d, or w units.",
|
|
213
|
+
)
|
|
214
|
+
@click.option(
|
|
215
|
+
"--vacuum-every-batches",
|
|
216
|
+
type=click.IntRange(min=0),
|
|
217
|
+
default=None,
|
|
218
|
+
metavar="INTEGER",
|
|
219
|
+
help="Vacuum after this many batches; defaults to workers * 2. Use 0 for only a final vacuum.",
|
|
220
|
+
)
|
|
221
|
+
@click.pass_obj
|
|
222
|
+
def value_backfill(
|
|
223
|
+
obj: dict[str, Any],
|
|
224
|
+
workers: int | None,
|
|
225
|
+
batch_size: str,
|
|
226
|
+
vacuum_every_batches: int | None,
|
|
227
|
+
) -> None:
|
|
228
|
+
"""Map numeric JSON values into the optimized floating-point column."""
|
|
229
|
+
config = obj["conf"]
|
|
230
|
+
if config.ROUTE_TO_BACKEND:
|
|
231
|
+
click.echo(
|
|
232
|
+
"Warning: value-backfill requires a direct database connection and cannot run when ROUTE_TO_BACKEND=true.",
|
|
233
|
+
err=True,
|
|
234
|
+
)
|
|
235
|
+
raise click.UsageError("Disable ROUTE_TO_BACKEND before running this command.")
|
|
236
|
+
if not config.DATABASE_URL:
|
|
237
|
+
raise click.UsageError("DATABASE_URL must be configured to run value-backfill.")
|
|
238
|
+
|
|
239
|
+
resolved_workers = resolve_workers(workers)
|
|
240
|
+
resolved_batch_size = parse_duration(batch_size)
|
|
241
|
+
resolved_vacuum_cadence = resolved_workers * 2 if vacuum_every_batches is None else vacuum_every_batches
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
run_backfill(
|
|
245
|
+
config.DATABASE_URL,
|
|
246
|
+
resolved_workers,
|
|
247
|
+
resolved_batch_size,
|
|
248
|
+
resolved_vacuum_cadence,
|
|
249
|
+
)
|
|
250
|
+
except click.ClickException:
|
|
251
|
+
raise
|
|
252
|
+
except Exception as exc:
|
|
253
|
+
raise click.ClickException(f"Value backfill failed: {exc}") from exc
|
|
@@ -142,7 +142,7 @@ def load_time_data(
|
|
|
142
142
|
# Ensure correct time dtypes
|
|
143
143
|
df["time"] = pd.to_datetime(df["time"], format="mixed", utc=True, errors="raise")
|
|
144
144
|
|
|
145
|
-
if
|
|
145
|
+
if metric_type.is_numeric():
|
|
146
146
|
df["value_float"] = pd.to_numeric(df["value"], errors="raise")
|
|
147
147
|
df["value_jsonb"] = None
|
|
148
148
|
else:
|
|
@@ -53,6 +53,9 @@ class ProbeMetadataPayload(BaseModel):
|
|
|
53
53
|
|
|
54
54
|
|
|
55
55
|
DATABASE_URI = os.getenv("DATABASE_URL")
|
|
56
|
+
if DATABASE_URI and DATABASE_URI.startswith("postgresql://"):
|
|
57
|
+
DATABASE_URI = DATABASE_URI.replace("postgresql://", "postgresql+psycopg2://", 1)
|
|
58
|
+
|
|
56
59
|
engine = create_engine(DATABASE_URI)
|
|
57
60
|
|
|
58
61
|
loglevel = os.getenv("BACKEND_LOG_LEVEL", "INFO")
|
|
@@ -93,7 +96,7 @@ def get_keys():
|
|
|
93
96
|
Session = sessionmaker(bind=engine) # noqa: N806
|
|
94
97
|
with Session() as session:
|
|
95
98
|
now = datetime.now(tz=UTC)
|
|
96
|
-
stmt = select(APIAccessKey.key).where(or_(APIAccessKey.expires_at
|
|
99
|
+
stmt = select(APIAccessKey.key).where(or_(APIAccessKey.expires_at.is_(None), APIAccessKey.expires_at > now))
|
|
97
100
|
result = session.execute(stmt)
|
|
98
101
|
keys = [row[0] for row in result.all()]
|
|
99
102
|
logger.debug("api access keys loaded from db")
|
|
@@ -44,11 +44,27 @@ services:
|
|
|
44
44
|
context: migrations
|
|
45
45
|
restart: "no"
|
|
46
46
|
environment:
|
|
47
|
-
- DB_URI=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
|
47
|
+
- DB_URI=postgresql+psycopg2://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
|
48
48
|
depends_on:
|
|
49
49
|
db:
|
|
50
50
|
condition: service_healthy
|
|
51
51
|
|
|
52
|
+
value-backfill:
|
|
53
|
+
image: savannah.ornl.gov/opensampl/migrations:latest
|
|
54
|
+
build:
|
|
55
|
+
context: migrations
|
|
56
|
+
profiles: ["maintenance"]
|
|
57
|
+
restart: "no"
|
|
58
|
+
command: ["opensampl", "maintenance", "value-backfill"]
|
|
59
|
+
environment:
|
|
60
|
+
- DATABASE_URL=postgresql+psycopg2://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
|
61
|
+
- ROUTE_TO_BACKEND=false
|
|
62
|
+
depends_on:
|
|
63
|
+
db:
|
|
64
|
+
condition: service_healthy
|
|
65
|
+
migrations:
|
|
66
|
+
condition: service_completed_successfully
|
|
67
|
+
|
|
52
68
|
|
|
53
69
|
backend:
|
|
54
70
|
image: savannah.ornl.gov/opensampl/backend:latest
|
|
@@ -59,7 +75,7 @@ services:
|
|
|
59
75
|
- "8015:8000"
|
|
60
76
|
restart: unless-stopped
|
|
61
77
|
environment:
|
|
62
|
-
- DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
|
78
|
+
- DATABASE_URL=postgresql+psycopg2://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
|
63
79
|
- ROUTE_TO_BACKEND=false
|
|
64
80
|
- BACKEND_LOG_LEVEL=${BACKEND_LOG_LEVEL}
|
|
65
81
|
- USE_API_KEY=${USE_API_KEY}
|
|
@@ -72,4 +88,4 @@ services:
|
|
|
72
88
|
|
|
73
89
|
volumes:
|
|
74
90
|
castdb:
|
|
75
|
-
grafana-data:
|
|
91
|
+
grafana-data:
|
|
@@ -40,13 +40,27 @@ services:
|
|
|
40
40
|
context: ./migrations
|
|
41
41
|
restart: "no"
|
|
42
42
|
environment:
|
|
43
|
-
- DB_URI=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
|
43
|
+
- DB_URI=postgresql+psycopg2://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
|
44
44
|
- CHUNK_INTERVAL=${CHUNK_INTERVAL}
|
|
45
45
|
- RETENTION_POLICY=${RETENTION_POLICY}
|
|
46
46
|
depends_on:
|
|
47
47
|
db:
|
|
48
48
|
condition: service_healthy
|
|
49
49
|
|
|
50
|
+
value-backfill:
|
|
51
|
+
image: savannah.ornl.gov/opensampl/migrations:latest
|
|
52
|
+
profiles: ["maintenance"]
|
|
53
|
+
restart: "no"
|
|
54
|
+
command: ["opensampl", "maintenance", "value-backfill"]
|
|
55
|
+
environment:
|
|
56
|
+
- DATABASE_URL=postgresql+psycopg2://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
|
57
|
+
- ROUTE_TO_BACKEND=false
|
|
58
|
+
depends_on:
|
|
59
|
+
db:
|
|
60
|
+
condition: service_healthy
|
|
61
|
+
migrations:
|
|
62
|
+
condition: service_completed_successfully
|
|
63
|
+
|
|
50
64
|
backend:
|
|
51
65
|
image: savannah.ornl.gov/opensampl/backend:latest
|
|
52
66
|
build:
|
|
@@ -56,7 +70,7 @@ services:
|
|
|
56
70
|
- "8015:8000"
|
|
57
71
|
restart: always
|
|
58
72
|
environment:
|
|
59
|
-
- DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
|
73
|
+
- DATABASE_URL=postgresql+psycopg2://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
|
60
74
|
- BACKEND_LOG_LEVEL=${BACKEND_LOG_LEVEL:-INFO}
|
|
61
75
|
- USE_API_KEY=${USE_API_KEY:-false}
|
|
62
76
|
- API_KEY=${API_KEY:-}
|
|
@@ -66,4 +80,4 @@ services:
|
|
|
66
80
|
|
|
67
81
|
volumes:
|
|
68
82
|
castdb:
|
|
69
|
-
grafana-data:
|
|
83
|
+
grafana-data:
|
|
@@ -3,7 +3,7 @@ FROM python:3.12
|
|
|
3
3
|
ARG OPENSAMPL_VERSION=1.1.5
|
|
4
4
|
|
|
5
5
|
WORKDIR /
|
|
6
|
-
RUN pip install --no-cache-dir "opensampl[migrations]==${OPENSAMPL_VERSION}"
|
|
6
|
+
RUN pip install --no-cache-dir "opensampl[migrations]==${OPENSAMPL_VERSION}"
|
|
7
7
|
|
|
8
8
|
RUN useradd -m alembic
|
|
9
9
|
USER alembic
|
|
@@ -6,7 +6,11 @@ from sqlalchemy import pool
|
|
|
6
6
|
|
|
7
7
|
from alembic import context
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
db_uri = os.environ.get('DB_URI')
|
|
10
|
+
if db_uri and db_uri.startswith('postgresql://'):
|
|
11
|
+
db_uri = db_uri.replace('postgresql://', 'postgresql+psycopg2://', 1)
|
|
12
|
+
|
|
13
|
+
sqlalchemy_url = db_uri
|
|
10
14
|
# this is the Alembic Config object, which provides
|
|
11
15
|
# access to the values within the .ini file in use.
|
|
12
16
|
config = context.config
|
|
@@ -30,18 +30,6 @@ def upgrade() -> None:
|
|
|
30
30
|
sa.Column("value_float", sa.Float(), nullable=True),
|
|
31
31
|
schema=SCHEMA)
|
|
32
32
|
|
|
33
|
-
# Backfill value_float from the numeric jsonb values
|
|
34
|
-
op.execute(
|
|
35
|
-
"""
|
|
36
|
-
UPDATE castdb.probe_data pd
|
|
37
|
-
SET value_float = (pd.value_jsonb #>> '{}')::double precision
|
|
38
|
-
FROM castdb.metric_type mt
|
|
39
|
-
WHERE pd.metric_type_uuid = mt.uuid
|
|
40
|
-
AND mt.value_type IN ('float'
|
|
41
|
-
, 'int')
|
|
42
|
-
"""
|
|
43
|
-
)
|
|
44
|
-
|
|
45
33
|
|
|
46
34
|
def downgrade() -> None:
|
|
47
35
|
op.execute(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "opensampl"
|
|
3
|
-
version = "1.2.
|
|
3
|
+
version = "1.2.3"
|
|
4
4
|
description = "Python tools for adding clock data to a timescale db."
|
|
5
5
|
license = {file = "LICENSE"}
|
|
6
6
|
authors = [
|
|
@@ -43,7 +43,7 @@ dependencies = [
|
|
|
43
43
|
"pydantic>=2.10.3,<3",
|
|
44
44
|
"pydantic-settings>=2.9.0",
|
|
45
45
|
"sqlalchemy>=2.0.39,<3",
|
|
46
|
-
"geoalchemy2
|
|
46
|
+
"geoalchemy2>=0.18.0,<1",
|
|
47
47
|
"click>=8.0.0,<9",
|
|
48
48
|
"pandas>=2.2.1,<3",
|
|
49
49
|
"tqdm>=4.66.2,<5",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{opensampl-1.2.2 → opensampl-1.2.3}/opensampl/create/templates/compound_reference_template.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/grafana/grafana-dashboards/ntp_dash.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{opensampl-1.2.2 → opensampl-1.2.3}/opensampl/server/grafana/grafana-dashboards/twst_dash.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|