Skip to content

Commit 3af3882

Browse files
authored
Feature/improvements (#26)
- reduce number of log messages - enable logging to a timestamped file at the root of git repository - improve plotting - refactor use of earthaccess - Discontinue python 3.9 - update github actions; refactor to resolve import problems in python 3.10 --------- Co-authored-by: kaza <kaza@dhigroup.com>
1 parent be3c400 commit 3af3882

30 files changed

Lines changed: 789 additions & 3374 deletions

.github/workflows/ci.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jobs:
1010
lint:
1111
runs-on: ubuntu-latest
1212
steps:
13-
- uses: actions/checkout@v4
13+
- uses: actions/checkout@v6
1414
- uses: chartboost/ruff-action@v1
1515

1616
unit-tests:
@@ -20,10 +20,10 @@ jobs:
2020
fail-fast: false
2121
matrix:
2222
os: [ubuntu-latest, windows-latest]
23-
python-version: ["3.9", "3.12"]
23+
python-version: ["3.10", "3.11", "3.12"]
2424

2525
steps:
26-
- uses: actions/checkout@v4
26+
- uses: actions/checkout@v6
2727

2828
- name: Set up Python ${{ matrix.python-version }}
2929
uses: actions/setup-python@v5
@@ -56,7 +56,7 @@ jobs:
5656
vars.RUN_INTEGRATION_TESTS == 'true'
5757
5858
steps:
59-
- uses: actions/checkout@v4
59+
- uses: actions/checkout@v6
6060

6161
- name: Set up Python 3.12
6262
uses: actions/setup-python@v5

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ dmypy.json
142142
/data/*
143143
/quarto/*
144144
/development/*
145-
145+
/logs/*
146146

147147
# development notebooks
148148
/notebooks/river_development.ipynb

HydroEO/downloaders/creodias.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ def _token_age(session_start_time):
344344
logger.info("Generating session token")
345345
token = _get_token(username, password)
346346
session_start_time = time.time()
347-
logger.info("Session token age: %.2f minutes", _token_age(session_start_time))
347+
logger.debug("Session token age: %.2f minutes", _token_age(session_start_time))
348348

349349
if show_progress:
350350
if len(uids) > 0:
@@ -357,7 +357,7 @@ def _token_age(session_start_time):
357357
for uid in uids:
358358
# assess age of token, (Expires every ten minutes so we refresh every 9 minutes)
359359
if _token_age(session_start_time) > 9:
360-
logger.info(
360+
logger.debug(
361361
"Session token age: %.2f minutes. Refreshing session token now.",
362362
_token_age(session_start_time),
363363
)

HydroEO/flows.py

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,12 @@ def initialize_rivers(prj: "Project") -> None:
147147

148148
id_label = "node" if prj.rivers.target_id_col == "node_id" else "reach"
149149
logger.info(
150-
"Initialized river %s ids: %s",
150+
"Found river %s %s ids",
151+
len(prj.rivers.target_ids),
152+
id_label,
153+
)
154+
logger.debug(
155+
"Found river %s ids: %s",
151156
id_label,
152157
", ".join(str(target_id) for target_id in prj.rivers.target_ids),
153158
)
@@ -217,15 +222,15 @@ def _ensure_sword_database(prj: "Project") -> str:
217222
"SWORD_v17b database not found in %s. Downloading and extracting it now.",
218223
sword_dir,
219224
)
220-
general.ifnotmakedirs(sword_dir)
225+
general.ifnotmakedirs(os.path.join(prj.dirs["main"], "SWORD_v17b_gpkg"))
221226

222227
zip_path = os.path.join(prj.dirs["main"], "SWORD_v17b_gpkg.zip")
223228
url_request.urlretrieve(SWORD_V17B_ZIP_URL, zip_path)
224229

225230
with zipfile.ZipFile(zip_path, "r") as zip_ref:
226-
zip_ref.extractall(sword_dir)
231+
zip_ref.extractall(os.path.join(prj.dirs["main"], "SWORD_v17b_gpkg"))
227232

228-
os.remove(zip_path)
233+
# os.remove(zip_path)
229234
return sword_dir
230235

231236

@@ -484,13 +489,14 @@ def _download_swot_hydrocron_timeseries(prj: "Project", startdate, enddate) -> N
484489
waterbody_groups = _group_river_targets_by_waterbody(prj)
485490

486491
summary = {
487-
"requested": len(waterbody_groups),
492+
"requested": 0,
488493
"successful": 0,
489494
"failed": 0,
490495
"empty_after_filter": 0,
491496
}
492497

493498
for wb_id, target_ids in waterbody_groups.items():
499+
summary["requested"] = summary["requested"] + len(target_ids)
494500
output_path = os.path.join(
495501
prj.dirs["swot"], "rivers", str(wb_id), "timeseries.csv"
496502
)
@@ -501,8 +507,16 @@ def _download_swot_hydrocron_timeseries(prj: "Project", startdate, enddate) -> N
501507
if latest_obs is not None:
502508
wb_startdate = latest_obs
503509

510+
deferred_warnings = []
511+
512+
def _defer_warning(message, *args):
513+
if args:
514+
deferred_warnings.append(message % args)
515+
else:
516+
deferred_warnings.append(message)
517+
504518
frames = []
505-
for target_id in target_ids:
519+
for target_id in tqdm(target_ids, desc="Downloading hydrocron data"):
506520
try:
507521
query_params = {
508522
"feature": feature,
@@ -520,7 +534,7 @@ def _download_swot_hydrocron_timeseries(prj: "Project", startdate, enddate) -> N
520534
status_code = getattr(response, "status", response.getcode())
521535
payload = json.loads(response.read().decode("utf-8"))
522536
except Exception as exc:
523-
logger.warning(
537+
_defer_warning(
524538
"Hydrocron request failed for %s %s: %s",
525539
prj.rivers.target_id_col,
526540
target_id,
@@ -535,7 +549,7 @@ def _download_swot_hydrocron_timeseries(prj: "Project", startdate, enddate) -> N
535549
else None
536550
)
537551
if status_code != 200 or not csv_payload:
538-
logger.warning(
552+
_defer_warning(
539553
"Hydrocron returned status %s for %s %s",
540554
status_code,
541555
prj.rivers.target_id_col,
@@ -547,7 +561,7 @@ def _download_swot_hydrocron_timeseries(prj: "Project", startdate, enddate) -> N
547561
try:
548562
df = pd.read_csv(StringIO(csv_payload))
549563
except Exception as exc:
550-
logger.warning(
564+
_defer_warning(
551565
"Failed to parse CSV for %s %s: %s",
552566
prj.rivers.target_id_col,
553567
target_id,
@@ -558,13 +572,13 @@ def _download_swot_hydrocron_timeseries(prj: "Project", startdate, enddate) -> N
558572

559573
if df.empty or quality_column not in df.columns:
560574
if df.empty:
561-
logger.info(
575+
_defer_warning(
562576
"Hydrocron returned no data for %s %s",
563577
prj.rivers.target_id_col,
564578
target_id,
565579
)
566580
else:
567-
logger.warning(
581+
_defer_warning(
568582
"Quality column %s not in Hydrocron response for %s %s",
569583
quality_column,
570584
prj.rivers.target_id_col,
@@ -574,7 +588,7 @@ def _download_swot_hydrocron_timeseries(prj: "Project", startdate, enddate) -> N
574588

575589
df = df[df[quality_column] <= max_q]
576590
if df.empty:
577-
logger.info(
591+
_defer_warning(
578592
"All Hydrocron observations filtered for %s %s (quality > %s)",
579593
prj.rivers.target_id_col,
580594
target_id,
@@ -590,8 +604,11 @@ def _download_swot_hydrocron_timeseries(prj: "Project", startdate, enddate) -> N
590604
combined = pd.concat(frames, ignore_index=True)
591605
combined.to_csv(output_path, index=False)
592606

607+
for warning in deferred_warnings:
608+
logger.debug(warning)
609+
593610
logger.info(
594-
"Hydrocron download complete: %s requested, %s successful, %s failed, %s empty after filtering",
611+
"Hydrocron download complete: %s requested, %s successful, %s failed, %s empty after filtering. See file logs for more info.",
595612
summary["requested"],
596613
summary["successful"],
597614
summary["failed"],

HydroEO/logging_config.py

Lines changed: 60 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,81 @@
11
"""Logging configuration for HydroEO application."""
22

33
import logging
4+
from datetime import datetime
5+
from pathlib import Path
46

57

6-
def setup_logging(level=logging.INFO):
7-
"""Configure logging for HydroEO to display messages in terminal.
8+
def _ensure_log_dir():
9+
"""Create logs directory at project root if it doesn't exist.
810
9-
Configures the HydroEO logger to output to console with a standard format.
11+
Returns
12+
-------
13+
Path
14+
Path to the logs directory.
15+
"""
16+
project_root = Path(
17+
__file__
18+
).parent.parent # HydroEO/logging_config.py -> HydroEO/ -> project root
19+
logs_dir = project_root / "logs"
20+
logs_dir.mkdir(exist_ok=True)
21+
return logs_dir
22+
23+
24+
def _get_log_filepath():
25+
"""Generate a timestamped log file path.
26+
27+
Returns
28+
-------
29+
Path
30+
Path to the log file with timestamp.
31+
"""
32+
logs_dir = _ensure_log_dir()
33+
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
34+
return logs_dir / f"HydroEO_{timestamp}.log"
35+
36+
37+
def setup_logging(level=logging.INFO, enable_file_logging=True):
38+
"""Configure logging for HydroEO with console and optional file output.
39+
40+
Configures the HydroEO logger to output to console at the specified level.
41+
If file logging is enabled, also logs to a timestamped file at DEBUG level.
1042
This should be called at application startup before running any HydroEO operations.
1143
1244
Parameters
1345
----------
1446
level : int
15-
Logging level (logging.DEBUG, logging.INFO, logging.WARNING, etc.)
47+
Logging level for console output (logging.DEBUG, logging.INFO, logging.WARNING, etc.)
1648
Default is logging.INFO.
49+
enable_file_logging : bool
50+
If True, also logs to a timestamped file at DEBUG level.
51+
Default is True.
1752
1853
Examples
1954
--------
2055
>>> from HydroEO.logging_config import setup_logging
21-
>>> setup_logging(logging.DEBUG) # Enable debug messages
56+
>>> setup_logging(logging.DEBUG) # Console at DEBUG, file at DEBUG
57+
>>> setup_logging(logging.INFO, enable_file_logging=False) # Console only
2258
"""
2359
logger = logging.getLogger("HydroEO")
24-
logger.setLevel(level)
60+
logger.setLevel(logging.DEBUG) # Logger itself should be at DEBUG to capture all
61+
62+
# Create formatter
63+
formatter = logging.Formatter(
64+
"%(asctime)s - %(name)s - %(levelname)s - %(message)s",
65+
datefmt="%Y-%m-%d %H:%M:%S",
66+
)
2567

2668
# Create console handler if one doesn't exist
2769
if not logger.handlers:
28-
handler = logging.StreamHandler()
29-
handler.setLevel(level)
30-
31-
# Create formatter
32-
formatter = logging.Formatter(
33-
"%(asctime)s - %(name)s - %(levelname)s - %(message)s",
34-
datefmt="%Y-%m-%d %H:%M:%S",
35-
)
36-
handler.setFormatter(formatter)
37-
logger.addHandler(handler)
70+
console_handler = logging.StreamHandler()
71+
console_handler.setLevel(level)
72+
console_handler.setFormatter(formatter)
73+
logger.addHandler(console_handler)
74+
75+
# Create file handler if enabled
76+
if enable_file_logging:
77+
log_filepath = _get_log_filepath()
78+
file_handler = logging.FileHandler(log_filepath)
79+
file_handler.setLevel(logging.DEBUG)
80+
file_handler.setFormatter(formatter)
81+
logger.addHandler(file_handler)

0 commit comments

Comments
 (0)