Skip to content

Commit e261c9e

Browse files
[REFACTOR] Add logging, os-->Path, streamlined loading
* [REFACTOR] Better readers to check existence of local file * [REFACTOR] Streamlined readers with utilities.resolve_file_path() * [REFACTOR] Add logging and fix tests, change local to data/
1 parent 411977b commit e261c9e

19 files changed

Lines changed: 1320 additions & 392 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
amocarray/_version.py
2+
setup.sh
3+
ts_gridded.nc
24

35
# VSCode settings
46
.vscode/*

.pre-commit-config.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ repos:
3636
entry: pytest -q
3737
language: system
3838
types: [python]
39-
files: ^(tests/.*\\.py$|amocarray/.*\\.py$|notebooks/.*\\.ipynb$)
4039
exclude: ^data/
4140

4241
- repo: https://github.com/charliermarsh/ruff-pre-commit

README.md

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
# MOC transports
22

3-
Amocarray is a python package for reading AMOC transport data from transport mooring arrays. It does not modify, fix or grid data. Functionality currently includes:
3+
**Clean, modular loading of AMOC observing array datasets, with optional structured logging and metadata enrichment.**
44

5-
- loading data from the RAPID 26°N array, OSNAP array, MOVE 16°N array and SAMBA 34.5°S array.
5+
> AMOCarray provides a unified system to access and process data from major Atlantic Meridional Overturning Circulation (AMOC) observing arrays:
6+
> - MOVE (16°N)
7+
> - RAPID (26°N)
8+
> - OSNAP (Subpolar North Atlantic)
9+
> - SAMBA (34.5°S)
10+
11+
The project emphasizes clarity, reproducibility, and modular design, with per-dataset logging, metadata handling, and testable utilities.
612

713
This is a work in progress, all contributions welcome!
814

@@ -21,6 +27,56 @@ Check out the demo notebook `notebooks/demo.ipynb` for example functionality.
2127

2228
As input, amocarray downloads data from the observing arrays.
2329

30+
### Quickstart
31+
32+
#### Load a sample dataset
33+
```python
34+
from amocarray import readers
35+
36+
# Load RAPID sample dataset
37+
ds = readers.load_sample_dataset("rapid")
38+
print(ds)
39+
```
40+
41+
#### Load a full dataset
42+
43+
```python
44+
from amocarray import readers
45+
46+
datasets = readers.load_dataset("osnap")
47+
for ds in datasets:
48+
print(ds)
49+
```
50+
A `*.log` file will be written to `logs/` by default.
51+
52+
Data will be cached in `~/.amocarray_data/` unless you specify a custom location.
53+
54+
### Project structure
55+
56+
```
57+
amocarray/
58+
59+
├── readers.py # Orchestrator for loading datasets
60+
├── read_move.py # MOVE reader
61+
├── read_rapid.py # RAPID reader
62+
├── read_osnap.py # OSNAP reader
63+
├── read_samba.py # SAMBA reader
64+
65+
├── utilities.py # Shared utilities (downloads, parsing, etc.)
66+
├── logger.py # Structured logging setup
67+
68+
└── tests/ # Unit tests
69+
```
70+
71+
### Roadmap
72+
73+
- [] Add test coverage for utilities and readers
74+
- [] Add dataset summary output at end of load_dataset()
75+
- [] Optional global logging helpers (disable_logging(), enable_logging())
76+
- [] Extend load_sample_dataset() to support all arrays
77+
- [] Metadata enrichment (source paths, processing dates)
78+
79+
2480
### Contributing
2581

2682
All contributions are welcome! See [contributing](CONTRIBUTING.md) for more details.
@@ -44,9 +100,21 @@ pytest --cov=amocarray --cov-report term-missing tests/
44100

45101
Try to ensure that all the lines of your contribution are covered in the tests.
46102

103+
47104
### Initial plans
48105

49106

50107
The **initial plan** for this repository is to simply load the volume transports as published by different AMOC observing arrays and replicate (update) the figure from Frajka-Williams et al. (2019) [10.3389/fmars.2019.00260](https://doi.org/10.3389/fmars.2019.00260).
51108

52109
<img width="358" alt="image" src="https://github.com/user-attachments/assets/fb35a276-a41e-4cef-b78f-9c3c46710466" />
110+
111+
112+
113+
## Acknowledgements
114+
115+
- MOVE data: NOAA Climate Program Office and German Bundesministerium für Bildung und Forschung.
116+
- OSNAP data: www.o-snap.org
117+
- SAMBA data: NOAA AOML, Met Office, and associated research projects.
118+
- RAPID data: RAPID-MOCHA-WBTS collaboration.
119+
120+
Dataset access and processing via [AMOCarray](https://github.com/AMOCcommunity/amocarray).

amocarray/logger.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# amocarray/logger.py
2+
import logging
3+
import datetime
4+
from pathlib import Path
5+
6+
7+
# Global logger instance (will be configured by setup_logger)
8+
log = logging.getLogger("amocarray")
9+
log.setLevel(logging.DEBUG) # capture everything; handlers filter later
10+
11+
# Global logging flag
12+
# Set to True to enable logging, False to disable
13+
LOGGING_ENABLED = True
14+
15+
16+
def enable_logging():
17+
"""Enable logging globally."""
18+
global LOGGING_ENABLED
19+
LOGGING_ENABLED = True
20+
21+
22+
def disable_logging():
23+
"""Disable logging globally."""
24+
global LOGGING_ENABLED
25+
LOGGING_ENABLED = False
26+
27+
28+
def log_info(message, *args):
29+
"""Log an info message, if logging is enabled."""
30+
if LOGGING_ENABLED:
31+
log.info(message, *args)
32+
33+
34+
def log_warning(message, *args):
35+
"""Log a warning message, if logging is enabled."""
36+
if LOGGING_ENABLED:
37+
log.warning(message, *args)
38+
39+
40+
def log_error(message, *args):
41+
"""Log an error message, if logging is enabled."""
42+
if LOGGING_ENABLED:
43+
log.error(message, *args)
44+
45+
46+
def log_debug(message, *args):
47+
"""Log a debug message, if logging is enabled."""
48+
if LOGGING_ENABLED:
49+
log.debug(message, *args)
50+
51+
52+
def setup_logger(array_name: str, output_dir: str = "logs") -> None:
53+
"""
54+
Configure the global logger to output to a file for the given array.
55+
56+
Parameters
57+
----------
58+
array_name : str
59+
Name of the observing array (e.g., 'move', 'rapid', etc.).
60+
output_dir : str
61+
Directory to save log files.
62+
"""
63+
if not LOGGING_ENABLED:
64+
return
65+
# Resolve output directory to project root
66+
project_root = Path(__file__).resolve().parent.parent
67+
output_path = project_root / output_dir
68+
output_path.mkdir(parents=True, exist_ok=True)
69+
70+
timestamp = datetime.datetime.now().strftime("%Y%m%dT%H")
71+
log_filename = f"{array_name.upper()}_{timestamp}_read.log"
72+
log_path = output_path / log_filename
73+
74+
# Prevent duplicate handlers in case of multiple calls
75+
if not any(
76+
isinstance(h, logging.FileHandler) and h.baseFilename == log_path
77+
for h in log.handlers
78+
):
79+
file_handler = logging.FileHandler(log_path, encoding="utf-8", mode="w")
80+
formatter = logging.Formatter(
81+
fmt="%(asctime)s %(levelname)-8s %(funcName)s %(message)s",
82+
datefmt="%Y%m%dT%H%M%S",
83+
)
84+
file_handler.setFormatter(formatter)
85+
86+
# Optional: console handler
87+
console_handler = logging.StreamHandler()
88+
console_handler.setFormatter(formatter)
89+
90+
# Clear existing handlers first
91+
log.handlers.clear()
92+
93+
log.addHandler(file_handler)
94+
log.addHandler(console_handler)
95+
96+
log.info(f"Logger initialized for array: {array_name}, writing to {log_path}")

amocarray/read_move.py

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
import os
1+
from pathlib import Path
2+
from typing import Union
23

34
import xarray as xr
45

5-
from amocarray import utilities
6+
from amocarray import utilities, logger
67
from amocarray.utilities import apply_defaults
78

9+
log = logger.log # ✅ use the global logger
10+
811
# Default source and file list
912
MOVE_DEFAULT_SOURCE = "https://mooring.ucsd.edu/move/nc/"
1013
MOVE_DEFAULT_FILES = ["OS_MOVE_TRANSPORTS.nc"]
@@ -16,7 +19,7 @@
1619
"project": "Meridional Overturning Variability Experiment (MOVE)",
1720
"weblink": "https://mooring.ucsd.edu/move/",
1821
"comment": "Dataset accessed and processed via http://github.com/AMOCcommunity/amocarray",
19-
# Acknowledgement and DOI can be added here when available
22+
# DOI can be added here when available
2023
"acknowledgement": "The MOVE project is made possible with funding from the NOAA Climate Program Office. Initial funding came from the German Bundesministerium fuer Bildung und Forschung.",
2124
}
2225

@@ -34,8 +37,8 @@ def read_move(
3437
source: str,
3538
file_list: str | list[str],
3639
transport_only: bool = True,
37-
data_dir=None,
38-
redownload=False,
40+
data_dir: Union[str, Path, None] = None,
41+
redownload: bool = False,
3942
) -> list[xr.Dataset]:
4043
"""
4144
Load the MOVE transport dataset from a URL or local file path into xarray Datasets.
@@ -48,6 +51,12 @@ def read_move(
4851
file_list : str or list of str, optional
4952
Filename or list of filenames to process.
5053
Defaults to MOVE_DEFAULT_FILES.
54+
transport_only : bool, optional
55+
If True, restrict to transport files only.
56+
data_dir : str, Path or None, optional
57+
Optional local data directory.
58+
redownload : bool, optional
59+
If True, force redownload of the data.
5160
5261
Returns
5362
-------
@@ -61,45 +70,51 @@ def read_move(
6170
FileNotFoundError
6271
If the file cannot be downloaded or does not exist locally.
6372
"""
64-
source = utilities.get_local_file(source, data_dir, redownload)
73+
log.info("Starting to read MOVE dataset")
74+
6575
if transport_only:
6676
file_list = MOVE_TRANSPORT_FILES
6777
if isinstance(file_list, str):
6878
file_list = [file_list]
79+
80+
local_data_dir = Path(data_dir) if data_dir else utilities.get_default_data_dir()
81+
local_data_dir.mkdir(parents=True, exist_ok=True)
82+
6983
datasets = []
7084

7185
for file in file_list:
7286
if not file.lower().endswith(".nc"):
87+
log.warning("Skipping non-NetCDF file: %s", file)
7388
continue
7489

75-
# Validate source
76-
if utilities._is_valid_url(source):
77-
file_url = f"{source.rstrip('/')}/{file}"
78-
dest_folder = os.path.join(os.path.expanduser("~"), ".amocarray_data")
79-
try:
80-
file_path = utilities.download_file(file_url, dest_folder)
81-
except Exception as e:
82-
raise FileNotFoundError(f"Failed to download {file_url}: {e}")
83-
elif os.path.isdir(source):
84-
file_path = os.path.join(source, file)
85-
if not os.path.exists(file_path):
86-
raise FileNotFoundError(f"Local file not found: {file_path}")
87-
else:
88-
raise ValueError("Source must be a valid URL or directory path.")
90+
download_url = (
91+
f"{source.rstrip('/')}/{file}" if utilities._is_valid_url(source) else None
92+
)
93+
94+
file_path = utilities.resolve_file_path(
95+
file_name=file,
96+
source=source,
97+
download_url=download_url,
98+
local_data_dir=local_data_dir,
99+
redownload=redownload,
100+
)
89101

90102
# Open dataset
91103
try:
104+
log.info("Opening MOVE dataset: %s", file_path)
92105
ds = xr.open_dataset(file_path)
93106
except Exception as e:
107+
log.error("Failed to open NetCDF file: %s: %s", file_path, e)
94108
raise FileNotFoundError(f"Failed to open NetCDF file: {file_path}: {e}")
95109

96110
# Attach metadata
97111
file_metadata = MOVE_FILE_METADATA.get(file, {})
112+
log.info("Attaching metadata to dataset from file: %s", file)
98113
utilities.safe_update_attrs(
99114
ds,
100115
{
101116
"source_file": file,
102-
"source_path": source,
117+
"source_path": str(file_path),
103118
**MOVE_METADATA,
104119
**file_metadata,
105120
},
@@ -108,6 +123,8 @@ def read_move(
108123
datasets.append(ds)
109124

110125
if not datasets:
126+
log.error("No valid NetCDF files found in %s", file_list)
111127
raise FileNotFoundError(f"No valid NetCDF files found in {file_list}")
112128

129+
log.info("Successfully loaded %d MOVE dataset(s)", len(datasets))
113130
return datasets

0 commit comments

Comments
 (0)