Skip to content

Commit 6504e47

Browse files
Add CLOB support.
Cleanup BLOB support to match CLOB implementation. list command has left-justified output for easier reading.
1 parent 431c268 commit 6504e47

5 files changed

Lines changed: 399 additions & 103 deletions

File tree

cwmscli/__main__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ def cli():
1313
cli.add_command(commands_cwms.shefcritimport)
1414
cli.add_command(commands_cwms.csv2cwms_cmd)
1515
cli.add_command(commands_cwms.blob_group)
16+
cli.add_command(commands_cwms.clob_group)

cwmscli/commands/blob.py

Lines changed: 6 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -90,107 +90,6 @@ def _save_base64(
9090
return dest
9191

9292

93-
def store_blob(**kwargs):
94-
file_data = kwargs.get("file_data")
95-
blob_id = kwargs.get("blob_id", "").upper()
96-
# Attempt to determine what media type should be used for the mime-type if one is not presented based on the file extension
97-
media = kwargs.get("media_type") or get_media_type(kwargs.get("input_file"))
98-
99-
logging.debug(
100-
f"Office: {kwargs.get('office')} Output ID: {blob_id} Media: {media}"
101-
)
102-
103-
blob = {
104-
"office-id": kwargs.get("office"),
105-
"id": blob_id,
106-
"description": json.dumps(kwargs.get("description")),
107-
"media-type-id": media,
108-
"value": base64.b64encode(file_data).decode("utf-8"),
109-
}
110-
111-
params = {"fail-if-exists": not kwargs.get("overwrite")}
112-
113-
if kwargs.get("dry_run"):
114-
logging.info(
115-
f"--dry-run enabled. Would POST to {kwargs.get('api_root')}/blobs with params={params}"
116-
)
117-
logging.info(
118-
f"Blob payload summary: office-id={kwargs.get('office')}, id={blob_id}, media={media}",
119-
)
120-
logging.info(
121-
json.dumps(
122-
{
123-
"url": f"{kwargs.get('api_root')}blobs",
124-
"params": params,
125-
"blob": {**blob, "value": f"<base64:{len(blob['value'])} chars>"},
126-
},
127-
indent=2,
128-
)
129-
)
130-
sys.exit(0)
131-
132-
try:
133-
cwms.store_blobs(blob, fail_if_exists=kwargs.get("overwrite"))
134-
logging.info(f"Successfully stored blob with ID: {blob_id}")
135-
logging.info(
136-
f"View: {kwargs.get('api_root')}blobs/{blob_id}?office={kwargs.get('office')}"
137-
)
138-
except requests.HTTPError as e:
139-
# Include response text when available
140-
detail = getattr(e.response, "text", "") or str(e)
141-
logging.error(f"Failed to store blob (HTTP): {detail}")
142-
sys.exit(1)
143-
except Exception as e:
144-
logging.error(f"Failed to store blob: {e}")
145-
sys.exit(1)
146-
147-
148-
def retrieve_blob(**kwargs):
149-
blob_id = kwargs.get("blob_id", "").upper()
150-
if not blob_id:
151-
logging.warning(
152-
"Valid blob_id required to download a blob. cwms-cli blob download --blob-id=myid. Run the list directive to see options for your office."
153-
)
154-
sys.exit(0)
155-
logging.debug(f"Office: {kwargs.get('office')} Blob ID: {blob_id}")
156-
try:
157-
blob = cwms.get_blob(
158-
office_id=kwargs.get("office"),
159-
blob_id=blob_id,
160-
)
161-
logging.info(
162-
f"Successfully retrieved blob with ID: {blob_id}",
163-
)
164-
_save_base64(blob, dest=blob_id)
165-
logging.info(f"Downloaded blob to: {blob_id}")
166-
except requests.HTTPError as e:
167-
detail = getattr(e.response, "text", "") or str(e)
168-
logging.error(f"Failed to retrieve blob (HTTP): {detail}")
169-
sys.exit(1)
170-
except Exception as e:
171-
logging.error(f"Failed to retrieve blob: {e}")
172-
sys.exit(1)
173-
174-
175-
def delete_blob(**kwargs):
176-
blob_id = kwargs.get("blob_id").upper()
177-
logging.debug(f"Office: {kwargs.get('office')} Blob ID: {blob_id}")
178-
179-
try:
180-
# cwms.delete_blob(
181-
# office_id=kwargs.get("office"),
182-
# blob_id=kwargs.get("blob_id").upper(),
183-
# )
184-
logging.info(f"Successfully deleted blob with ID: {blob_id}")
185-
except requests.HTTPError as e:
186-
details = getattr(e.response, "text", "") or str(e)
187-
logging.error(f"Failed to delete blob (HTTP): {details}")
188-
sys.exit(1)
189-
except Exception as e:
190-
logging.error(f"Failed to delete blob: {e}")
191-
sys.exit(1)
192-
193-
19493
def list_blobs(
19594
office: Optional[str] = None,
19695
blob_id_like: Optional[str] = None,
@@ -295,7 +194,10 @@ def upload_cmd(
295194
try:
296195
cwms.store_blobs(blob, fail_if_exists=not overwrite)
297196
logging.info(f"Uploaded blob: {blob_id_up}")
298-
logging.info(f"View: {api_root}blobs/{blob_id_up}?office={office}")
197+
if has_invalid_chars(blob_id_up):
198+
logging.info(f"View: {api_root}blobs/ignored?blob-id={blob_id_up}&office={office}")
199+
else:
200+
logging.info(f"View: {api_root}blobs/{blob_id_up}?office={office}")
299201
except requests.HTTPError as e:
300202
detail = getattr(e.response, "text", "") or str(e)
301203
logging.error(f"Failed to upload (HTTP): {detail}")
@@ -415,4 +317,5 @@ def list_cmd(
415317
else:
416318
# Friendly console preview
417319
with pd.option_context("display.max_rows", 500, "display.max_columns", None):
418-
logging.info(df.to_string(index=False))
320+
# Left-align all columns
321+
logging.info("\n" + df.apply(lambda s: (s:=s.astype(str).str.strip()).str.ljust(s.str.len().max())).to_string(index=False, justify='left'))

cwmscli/commands/clob.py

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
import base64
2+
import json
3+
import logging
4+
import mimetypes
5+
import os
6+
import re
7+
import sys
8+
from typing import Optional, Sequence
9+
10+
import cwms
11+
import pandas as pd
12+
import requests
13+
14+
from cwmscli.utils import get_api_key, has_invalid_chars
15+
16+
def list_clobs(
17+
office: Optional[str] = None,
18+
clob_id_like: Optional[str] = None,
19+
columns: Optional[Sequence[str]] = None,
20+
sort_by: Optional[Sequence[str]] = None,
21+
ascending: bool = True,
22+
limit: Optional[int] = None,
23+
) -> pd.DataFrame:
24+
logging.info(f"Listing clobs for office: {office!r}...")
25+
result = cwms.get_clobs(office_id=office, clob_id_like=clob_id_like)
26+
27+
# Accept either a DataFrame or a JSON/dict-like response
28+
if isinstance(result, pd.DataFrame):
29+
df = result.copy()
30+
else:
31+
# Expecting normal clob return structure
32+
data = getattr(result, "json", None)
33+
if callable(data):
34+
data = result.json()
35+
df = pd.DataFrame((data or {}).get("clobs", []))
36+
37+
# Allow column filtering
38+
if columns:
39+
keep = [c for c in columns if c in df.columns]
40+
if keep:
41+
df = df[keep]
42+
43+
# Sort by option
44+
if sort_by:
45+
by = [c for c in sort_by if c in df.columns]
46+
if by:
47+
df = df.sort_values(by=by, ascending=ascending, kind="stable")
48+
49+
# Optional limit
50+
if limit is not None:
51+
df = df.head(limit)
52+
53+
logging.info(f"Found {len(df):,} clob(s)")
54+
# List the clobs in the logger
55+
for _, row in df.iterrows():
56+
logging.info(f"clob ID: {row['id']}, Description: {row.get('description')}")
57+
return df
58+
59+
60+
def upload_cmd(
61+
input_file: str,
62+
clob_id: str,
63+
description: str,
64+
media_type: str,
65+
overwrite: bool,
66+
dry_run: bool,
67+
office: str,
68+
api_root: str,
69+
api_key: str,
70+
):
71+
cwms.init_session(api_root=api_root, api_key=get_api_key(api_key, ""))
72+
try:
73+
file_size = os.path.getsize(input_file)
74+
with open(input_file, "r") as f:
75+
file_data = f.read()
76+
logging.info(f"Read file: {input_file} ({file_size} bytes)")
77+
except Exception as e:
78+
logging.error(f"Failed to read file: {e}")
79+
sys.exit(1)
80+
81+
clob_id_up = clob_id.upper()
82+
logging.debug(f"Office={office} clobID={clob_id_up}")
83+
84+
clob = {
85+
"office-id": office,
86+
"id": clob_id_up,
87+
"description": (
88+
json.dumps(description)
89+
if isinstance(description, (dict, list))
90+
else description
91+
),
92+
"value": file_data,
93+
}
94+
params = {"fail-if-exists": not overwrite}
95+
96+
if dry_run:
97+
logging.info(f"DRY RUN: would POST {api_root}clobs with params={params}")
98+
logging.info(
99+
json.dumps(
100+
{
101+
"url": f"{api_root}clobs",
102+
"params": params,
103+
"clob": {**clob, "value": f'<{len(clob["value"])} chars>'},
104+
},
105+
indent=2,
106+
)
107+
)
108+
return
109+
110+
try:
111+
cwms.store_clobs(clob, fail_if_exists=not overwrite)
112+
logging.info(f"Uploaded clob: {clob_id_up}")
113+
# IDs with / can't be used directly in the path
114+
# TODO: check for other disallowed characters
115+
if has_invalid_chars(clob_id_up):
116+
logging.info(f"View: {api_root}clobs/ignored?clob-id={clob_id_up}&office={office}")
117+
else:
118+
logging.info(f"View: {api_root}clobs/{clob_id_up}?office={office}")
119+
except requests.HTTPError as e:
120+
detail = getattr(e.response, "text", "") or str(e)
121+
logging.error(f"Failed to upload (HTTP): {detail}")
122+
sys.exit(1)
123+
except Exception as e:
124+
logging.error(f"Failed to upload: {e}")
125+
sys.exit(1)
126+
127+
128+
def download_cmd(
129+
clob_id: str, dest: str, office: str, api_root: str, api_key: str, dry_run: bool
130+
):
131+
if dry_run:
132+
logging.info(
133+
f"DRY RUN: would GET {api_root} clob with clob-id={clob_id} office={office}."
134+
)
135+
return
136+
cwms.init_session(api_root=api_root, api_key=get_api_key(api_key, ""))
137+
bid = clob_id.upper()
138+
logging.debug(f"Office={office} clobID={bid}")
139+
140+
try:
141+
clob = cwms.get_clob(office_id=office, clob_id=bid)
142+
os.makedirs(os.path.dirname(dest) or ".", exist_ok=True)
143+
sys.stderr.write(repr(clob.json) + "\n")
144+
with open(dest, "wt") as f:
145+
f.write(clob.json["value"])
146+
147+
logging.info(f"Downloaded clob to: {dest}")
148+
except requests.HTTPError as e:
149+
detail = getattr(e.response, "text", "") or str(e)
150+
logging.error(f"Failed to download (HTTP): {detail}")
151+
sys.exit(1)
152+
except Exception as e:
153+
logging.error(f"Failed to download: {e}")
154+
sys.exit(1)
155+
156+
157+
def delete_cmd(clob_id: str, office: str, api_root: str, api_key: str, dry_run: bool):
158+
159+
if dry_run:
160+
logging.info(
161+
f"DRY RUN: would DELETE {api_root} clob with clob-id={clob_id} office={office}"
162+
)
163+
return
164+
cwms.init_session(api_root=api_root, api_key=api_key)
165+
cwms.delete_clob(office_id=office, clob_id=clob_id)
166+
logging.info(f"Deleted clob: {clob_id} for office: {office}")
167+
168+
169+
def update_cmd(
170+
input_file: str,
171+
clob_id: str,
172+
description: str,
173+
ignore_nulls: bool,
174+
dry_run: bool,
175+
office: str,
176+
api_root: str,
177+
api_key: str,
178+
):
179+
if dry_run:
180+
logging.info(
181+
f"DRY RUN: would PATCH {api_root} clob with clob-id={clob_id} office={office}"
182+
)
183+
return
184+
file_data = None
185+
if input_file:
186+
try:
187+
file_size = os.path.getsize(input_file)
188+
with open(input_file, "r") as f:
189+
file_data = f.read()
190+
logging.info(f"Read file: {input_file} ({file_size} bytes)")
191+
except Exception as e:
192+
logging.error(f"Failed to read file: {e}")
193+
sys.exit(1)
194+
# Setup minimum required payload
195+
clob = {"office-id": office, "id": clob_id.upper()}
196+
if description:
197+
clob["description"] = description
198+
199+
if file_data:
200+
clob["value"] = file_data
201+
cwms.init_session(api_root=api_root, api_key=api_key)
202+
cwms.update_clob(clob, clob_id.upper(), ignore_nulls=ignore_nulls)
203+
204+
205+
def list_cmd(
206+
clob_id_like: str,
207+
columns: list[str],
208+
sort_by: list[str],
209+
desc: bool,
210+
limit: int,
211+
to_csv: str,
212+
office: str,
213+
api_root: str,
214+
api_key: str,
215+
):
216+
cwms.init_session(api_root=api_root, api_key=get_api_key(api_key, None))
217+
df = list_clobs(
218+
office=office,
219+
clob_id_like=clob_id_like,
220+
columns=columns,
221+
sort_by=sort_by,
222+
ascending=not desc,
223+
limit=limit,
224+
)
225+
if to_csv:
226+
df.to_csv(to_csv, index=False)
227+
logging.info(f"Wrote {len(df)} rows to {to_csv}")
228+
else:
229+
# Friendly console preview
230+
with pd.option_context("display.max_rows", 500, "display.max_columns", None):
231+
# Left-align all columns
232+
logging.info("\n" + df.apply(lambda s: (s:=s.astype(str).str.strip()).str.ljust(s.str.len().max())).to_string(index=False, justify='left'))

0 commit comments

Comments
 (0)