Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 pytest
pip install flake8 pytest pytest-cov
pip install .
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Lint with flake8
Expand All @@ -35,6 +35,11 @@ jobs:
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
- name: Test with coverage
run: |
pytest
pytest --cov=pyo_oracle --cov-report=html --cov-report=term-missing tests/
- name: Upload coverage report
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: htmlcov/
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,7 @@
**/*.ini
build/
pyo_oracle/data
.coverage
coverage.*
htmlcov/
.venv/
1 change: 1 addition & 0 deletions environment-dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ dependencies:
- httpx
- ipython
- pytest
- pytest-cov
21 changes: 19 additions & 2 deletions pyo_oracle/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,21 @@
from .main import download_layers, list_layers, list_local_data
from .config import config
from . import _config as config_module

__all__ = ["download_layers", "list_layers", "list_local_data", "config"]
# Re-export helpers so users can call pyo.create_config(), etc.
create_config = config_module.create_config
get_config_path = config_module.get_config_path
print_config_values = config_module.print_config_values
update_setting = config_module.update_setting

config = config_module.config

__all__ = [
"download_layers",
"list_layers",
"list_local_data",
"config",
"create_config",
"get_config_path",
"print_config_values",
"update_setting",
]
11 changes: 6 additions & 5 deletions pyo_oracle/config.py → pyo_oracle/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,17 @@ def create_config(default_config: dict = _default_config, path: str = None) -> N
"""
config = configparser.ConfigParser()
config["DEFAULT"] = default_config
if config_file.exists():
target_path = Path(path) if path else config_file
if target_path.exists():
response = input(
f"Config file '{config_file}' already exists, overwrite it? y/N \n"
f"Config file '{target_path}' already exists, overwrite it? y/N \n"
)
if (response.lower() not in "y yes") or (not response):
print("Operation cancelled.\n")
return
with open(config_file, "w") as f:
with open(target_path, "w") as f:
config.write(f)
print(f"Created configuration at '{config_file}'.")
print(f"Created configuration at '{target_path}'.")


def _get_default_config() -> configparser.ConfigParser:
Expand Down Expand Up @@ -61,7 +62,7 @@ def get_config_path() -> Path:
else:
print("Config file doesn't exist, creating it.")
create_config()
get_config_path()
return get_config_path()


def print_config_values() -> None:
Expand Down
74 changes: 50 additions & 24 deletions pyo_oracle/main.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
"""
Main module with library functions.
"""
import re
from functools import lru_cache
from glob import glob
from pathlib import Path
from typing import Optional, Union, List, Dict

import pandas as pd

from pyo_oracle.config import config
from pyo_oracle._config import config
from pyo_oracle.utils import (
_format_args,
_download_layer,
Expand Down Expand Up @@ -95,22 +96,26 @@ def download_layers(
@_format_args
@lru_cache(8)
def list_layers(
search: str or list = None,
variables: str or list = None,
ssp: str or list = None,
time_period: str = None,
depth: str or list = None,
dataframe: bool = True,
simplify = False,
_include_allDatasets: bool = False,
) -> pd.DataFrame or list:
"""
Lists available layers in the Bio-ORACLE server.

Args:
search (str|list): Natural text search term, eg. 'Temperature', 'Oxygen'.
variables (str|list): Variables to filter from. Valid values are ['po4','o2','si','ph','sws','phyc','so','thetao','dfe','no3','sithick','tas','siconc','chl','mlotst','clt','terrain'].
ssp (str|list): Future scenario to choose from. Valid values are ['ssp119', 'ssp126', 'ssp370', 'ssp585', 'ssp460', 'ssp245', 'baseline'].
time_period (str): Time period to choose from. Valid values are either 'present' or 'future'.
depth (str|list): Depth category to choose from. Valid values are ['min', 'mean', 'max', 'surf'].
dataframe (bool): Whether to return a Pandas DataFrame. If False, will return a list.
simplify (bool): Whether to simplify the output. If True, will return only dataset ID and dataset title. If dataframe=False, this doesn't do anything.
_include_allDatasets (bool): Internal flag for including all datasets.

Returns:
Expand All @@ -128,36 +133,54 @@ def list_layers(
# List layers for specific variables and future scenarios
filtered_layers = list_layers(variables=['po4', 'o2'], ssp='ssp585', dataframe=True)
"""
valid_variables = [
"po4",
"o2",
"si",
"ph",
"sws",
"phyc",
"so",
"thetao",
"dfe",
"no3",
"sithick",
"tas",
"siconc",
"chl",
"mlotst",
"clt",
"terrain",
]
valid_ssp = ["ssp119", "ssp126", "ssp370", "ssp585", "ssp460", "ssp245", "baseline"]
valid_time_period = ["present", "future"]
valid_depth = ["min", "mean", "max", "surf"]
valid_args = {
"valid_variables": [
"po4",
"o2",
"si",
"ph",
"sws",
"phyc",
"so",
"thetao",
"dfe",
"no3",
"sithick",
"tas",
"siconc",
"chl",
"mlotst",
"clt",
"terrain",
],
"valid_ssp": ["ssp119", "ssp126", "ssp370", "ssp585", "ssp460", "ssp245", "baseline"],
"valid_time_period": ["present", "future"],
"valid_depth": ["min", "mean", "max", "surf"],
}

# Validate the provided arguments against valid values
for arg in ("variables", "ssp", "time_period", "depth"):
_validate_argument(arg, eval(arg), eval(f"valid_{arg}"))
_validate_argument(arg, eval(arg), valid_args[f"valid_{arg}"])

# Fetch the dataframe containing layer information
_dataframe = _layer_dataframe(_include_allDatasets)

if search:
search_terms = search if isinstance(search, (tuple, list)) else (search,)
pattern = "|".join(re.escape(term) for term in search_terms)
searchable_columns = [
col
for col in ("datasetID", "title", "long_name", "standard_name")
if col in _dataframe.columns
]
if not searchable_columns:
searchable_columns = ["datasetID"]

mask = pd.Series(False, index=_dataframe.index)
for col in searchable_columns:
mask = mask | _dataframe[col].str.contains(pattern, case=False, na=False)
_dataframe = _dataframe[mask]

# Filter the resulting dataframe based on provided filters
if variables:
_dataframe = pd.concat(
Expand Down Expand Up @@ -197,6 +220,9 @@ def list_layers(

# Convert to list if needed
if dataframe:
# Simplify option
if simplify:
_dataframe = _dataframe[["datasetID", "title"]]
return _dataframe.reset_index(drop=True)
else:
return _dataframe["datasetID"].to_list()
Expand Down
2 changes: 1 addition & 1 deletion pyo_oracle/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import httpx
import pandas as pd

from pyo_oracle.config import default_server, config
from pyo_oracle._config import default_server, config


def convert_bytes(num: float) -> str:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ packages = ["pyo_oracle"]

[project]
name = "pyo_oracle"
version = "0.0.5"
version = "0.1.0"
authors = [
{ name="Vini Salazar", email="vinicius.salazar@unimelb.edu.au" },
]
Expand Down
68 changes: 68 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import configparser

import pytest

import pyo_oracle._config as config_module


@pytest.fixture
def temp_config_path(tmp_path, monkeypatch):
"""Use a temporary config file to avoid mutating the real one."""
path = tmp_path / "config.ini"
monkeypatch.setattr(config_module, "config_file", path)
return path


def _read_config(path):
parser = configparser.ConfigParser()
parser.read(path)
return parser


def test_create_config_creates_file_with_defaults(temp_config_path):
config_module.create_config()

parser = _read_config(temp_config_path)
assert temp_config_path.exists()
assert parser["DEFAULT"]["data_directory"] == config_module._default_config["data_directory"]
assert parser["DEFAULT"]["erddap_server"] == config_module._default_config["erddap_server"]
assert parser["DEFAULT"]["skip_confirmation"] == str(
config_module._default_config["skip_confirmation"]
)


def test_create_config_does_not_overwrite_without_confirmation(
temp_config_path, monkeypatch
):
temp_config_path.write_text("[DEFAULT]\nkey=original\n")
monkeypatch.setattr("builtins.input", lambda *_: "n")

config_module.create_config(default_config={"key": "new-value"})

parser = _read_config(temp_config_path)
assert parser["DEFAULT"]["key"] == "original"


def test_get_config_path_creates_missing_file(temp_config_path):
created_path = config_module.get_config_path()

assert created_path == temp_config_path
assert temp_config_path.exists()


def test_update_setting_writes_changes(temp_config_path):
config_module.create_config()

config_module.update_setting("custom_key", "custom_value")
parser = _read_config(temp_config_path)
assert parser["DEFAULT"]["custom_key"] == "custom_value"


def test_print_config_values_outputs_defaults(temp_config_path, capsys):
config_module.create_config()

config_module.print_config_values()
output = capsys.readouterr().out
assert str(temp_config_path) in output
for key in ("data_directory", "erddap_server", "skip_confirmation"):
assert key in output
10 changes: 9 additions & 1 deletion tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def test_list_layers():
assert len(layers_df_filter) < len(layers_df_all)

# Test depth
layers_df_filter = pyo.list_layers(depth=["mean", "surf"])
layers_df_filter = pyo.list_layers(depth=["mean", "surf"], simplify=True)
assert isinstance(layers_df_filter, pd.DataFrame)
assert layers_df_filter.empty is False
assert len(layers_df_filter) < len(layers_df_all)
Expand All @@ -80,6 +80,14 @@ def test_list_layers():
assert isinstance(layers_list, list)
assert len(layers_list) > 0

# Test search
layers_search = pyo.list_layers(
search=["temperature"]
)
assert isinstance(layers_search, pd.DataFrame)
assert layers_search.empty is False
assert len(layers_search) < len(layers_df_all)


def test_download_layers(layer, constraints, test_data_dir):
pyo.download_layers(
Expand Down