Skip to content

Commit 6c198fb

Browse files
committed
Add a TS Data loading script
1 parent b7d0a8d commit 6c198fb

2 files changed

Lines changed: 201 additions & 0 deletions

File tree

cwmscli/load/timeseries/timeseries.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from datetime import datetime, timedelta
12
from typing import Optional
23

34
import click
@@ -53,3 +54,94 @@ def load_timeseries_ids_all(
5354
timeseries_id_regex=timeseries_id_regex,
5455
dry_run=dry_run,
5556
)
57+
58+
59+
@timeseries.command(
60+
"data",
61+
help="Copy timeseries data for a single location in a target CDA from a source CDA.",
62+
)
63+
@shared_source_target_options
64+
@click.option(
65+
"--ts-id",
66+
"ts_id",
67+
default=None,
68+
type=str,
69+
help="ID of the timeseries to copy (e.g. 'LocID.*').",
70+
)
71+
@click.option(
72+
"--ts-group",
73+
"ts_group",
74+
default=None,
75+
type=str,
76+
help="ID of the timeseries group to copy (e.g. 'GroupID.*').",
77+
)
78+
@click.option(
79+
"--ts-group-category-id",
80+
"ts_group_category_id",
81+
default=None,
82+
type=str,
83+
help="ID of the timeseries group category to copy.",
84+
)
85+
@click.option(
86+
"--ts-group-category-office-id",
87+
"ts_group_category_office_id",
88+
default=None,
89+
type=str,
90+
help="ID of the timeseries group category office to copy.",
91+
)
92+
@click.option(
93+
"--begin",
94+
"begin",
95+
default=(datetime.now().astimezone() - timedelta(days=1)).replace(microsecond=0),
96+
type=click.DateTime(formats=["%Y-%m-%dT%H:%M:%S%z", "%Y-%m-%dT%H:%M:%S %Z"]),
97+
help="Start date for timeseries data (e.g. '2022-01-01T00:00:00-06:00').",
98+
)
99+
@click.option(
100+
"--end",
101+
"end",
102+
default=datetime.now().astimezone().replace(microsecond=0),
103+
type=click.DateTime(formats=["%Y-%m-%dT%H:%M:%S%z", "%Y-%m-%dT%H:%M:%S %Z"]),
104+
help="End date for timeseries data (e.g. '2022-01-31T00:00:00-0600').",
105+
)
106+
@requires(reqs.cwms)
107+
@validate_cda_targets
108+
def load_timeseries_data(
109+
source_cda: str,
110+
source_office: str,
111+
target_cda: str,
112+
target_api_key: Optional[str],
113+
verbose: int,
114+
ts_id: Optional[str],
115+
dry_run: bool,
116+
ts_group: Optional[str],
117+
ts_group_category_id: Optional[str],
118+
ts_group_category_office_id: Optional[str],
119+
begin: Optional[datetime] = None,
120+
end: Optional[datetime] = None,
121+
):
122+
if (ts_id is None) == (ts_group is None):
123+
raise click.UsageError("Exactly one of --ts-id or --ts-group must be provided.")
124+
if ts_group and not ts_group_category_id:
125+
raise click.UsageError(
126+
"When specifying --ts-group, --ts-group-category-id must also be provided."
127+
)
128+
if ts_group and not ts_group_category_office_id:
129+
raise click.UsageError(
130+
"When specifying --ts-group, --ts-group-category-office-id must also be provided."
131+
)
132+
from cwmscli.load.timeseries.timeseries_data import _load_timeseries_data
133+
134+
_load_timeseries_data(
135+
source_cda=source_cda,
136+
source_office=source_office,
137+
target_cda=target_cda,
138+
target_api_key=target_api_key,
139+
verbose=verbose,
140+
dry_run=dry_run,
141+
ts_id=ts_id,
142+
ts_group=ts_group,
143+
ts_group_category_id=ts_group_category_id,
144+
ts_group_category_office_id=ts_group_category_office_id,
145+
begin=begin,
146+
end=end,
147+
)
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# cwmscli/load/timeseries_ids.py
2+
import json
3+
import logging
4+
from datetime import datetime
5+
from typing import Optional
6+
7+
import click
8+
import pandas as pd
9+
from cwms import JSON
10+
11+
12+
def _load_timeseries_data(
13+
source_cda: str,
14+
source_office: str,
15+
target_cda: str,
16+
target_api_key: Optional[str],
17+
verbose: int,
18+
dry_run: bool,
19+
ts_id: str,
20+
ts_group: str,
21+
ts_group_category_id: str,
22+
ts_group_category_office_id: str,
23+
begin: Optional[datetime] = None,
24+
end: Optional[datetime] = None,
25+
):
26+
import cwms
27+
28+
if verbose:
29+
click.echo(
30+
f"Loading timeseries data from source CDA '{source_cda}' (office '{source_office}') "
31+
f"to target CDA '{target_cda}'."
32+
)
33+
cwms.init_session(api_root=source_cda, api_key=None)
34+
# User has a ts_id
35+
if ts_id and not ts_group:
36+
ts_data = [
37+
cwms.get_timeseries(
38+
ts_id=ts_id, office_id=source_office, begin=begin, end=end
39+
)
40+
]
41+
# only grab time_ids for locations that are in the target database
42+
try:
43+
if dry_run:
44+
click.echo("Dry run enabled. No changes will be made.")
45+
logging.debug(f"Would store {ts_data} for {ts_id}({source_office})")
46+
else:
47+
cwms.init_session(api_root=target_cda, api_key=target_api_key)
48+
cwms.store_timeseries(
49+
data=ts_data.json,
50+
store_rule="REPLACE_ALL",
51+
override_protection=False,
52+
)
53+
except Exception as e:
54+
click.echo(f"Error storing timeseries ({ts_id}) data: {e}", err=True)
55+
# User did not have a ts_id but has a ts_group
56+
if ts_group:
57+
ts_ids = cwms.get_timeseries_group(
58+
group_id=ts_group,
59+
category_id=ts_group_category_id,
60+
office_id=source_office,
61+
category_office_id=ts_group_category_office_id,
62+
)
63+
logging.info(
64+
f"Found {len(ts_ids.json.get('assigned-time-series', []))} timeseries in group {ts_group}."
65+
)
66+
logging.info(f"Storing TSID from begin: {begin} to end: {end}")
67+
for ts in ts_ids.json.get("assigned-time-series", []):
68+
try:
69+
if dry_run:
70+
click.echo(
71+
f"Would store timeseries data for {ts['timeseries-id']}({ts['office-id']})"
72+
)
73+
else:
74+
cwms.init_session(api_root=source_cda, api_key=None)
75+
ts_data = cwms.get_timeseries(
76+
ts_id=ts["timeseries-id"],
77+
office_id=ts["office-id"],
78+
begin=begin,
79+
end=end,
80+
)
81+
cwms.init_session(api_root=target_cda, api_key=target_api_key)
82+
# Convert the TS Values to a format CDA expects
83+
ts_cda_format_values = [
84+
[
85+
pd.to_datetime(value["date-time"]).isoformat(),
86+
value["value"],
87+
value["quality-code"],
88+
]
89+
for value in ts_data.json.get("values", [])
90+
]
91+
# Build a payload dict with formatted values instead of copying the response object
92+
ts_data_copy = ts_data.json.copy()
93+
ts_data_copy["values"] = ts_cda_format_values
94+
cwms.store_timeseries(
95+
data=ts_data_copy,
96+
store_rule="REPLACE_ALL",
97+
override_protection=False,
98+
)
99+
click.echo(
100+
f"Wrote {len(ts_data_copy.get('values', []))} values to {ts['timeseries-id']}."
101+
)
102+
except Exception as e:
103+
logging.warning(
104+
f"Error storing timeseries ({ts['timeseries-id']}) data: {e}",
105+
exc_info=True,
106+
)
107+
108+
if verbose:
109+
click.echo("Timeseries ID copy operation completed.")

0 commit comments

Comments
 (0)