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
66 changes: 22 additions & 44 deletions .github/workflows/tests-python.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
name: Tests (Python)
name: Tests (uv)

on:
push:
branches: [main]
pull_request:
branches: [main]

# Allow job to be triggered manually.
workflow_dispatch:

# Cancel in-progress jobs when pushing to the same branch.
Expand All @@ -20,60 +18,40 @@ jobs:
strategy:
fail-fast: true
matrix:
os: ["ubuntu-latest"]
python-version: ["3.11", "3.12", "3.13"]
# In order to save resources, only run particular
# matrix slots on other OS than Linux.
include:
- os: "macos-latest"
python-version: "3.12"
- os: "windows-latest"
- os: ubuntu-latest
python-version: "3.11"
- os: ubuntu-latest
python-version: "3.12"
- os: ubuntu-latest
python-version: "3.13"
- os: macos-latest
python-version: "3.13"
continue-on-error: true
- os: windows-latest
python-version: "3.13"
continue-on-error: true

env:
OS: ${{ matrix.os }}
PYTHON: ${{ matrix.python-version }}
name: Python ${{ matrix.python-version }} • ${{ matrix.os }}

defaults:
run:
shell: bash -el {0}

name: Python ${{ matrix.python-version }} on OS ${{ matrix.os }}
steps:
- name: Acquire sources
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 2

- name: Setup Python
uses: actions/setup-python@v5
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
python-version: ${{ matrix.python-version }}
architecture: x64
cache: "pip"
cache-dependency-path: "setup.cfg"

- name: Install project (Linux)
if: runner.os == 'Linux'
run: |
pip3 install --requirement=requirements-test.txt
pip3 install --editable='.'

- name: Install project (macOS)
if: runner.os == 'macOS'
run: |
pip3 install --break-system-packages --requirement=requirements-test.txt
pip3 install --break-system-packages --editable='.'

- name: Install project (Windows)
if: runner.os == 'Windows'
run: |
pip3 install --requirement=requirements-test.txt
pip3 install --editable='.'
- name: Install the project
shell: bash -el {0}
run: uv sync

- name: Run tests
shell: bash -el {0}
env:
TMPDIR: ${{ runner.temp }}
SYNOPTIC_TOKEN: ${{ secrets.SYNOPTIC_TOKEN }}
run: |
pytest
TMPDIR: ${{ runner.temp }}
run: uv run pytest
32 changes: 13 additions & 19 deletions .readthedocs.yml
Original file line number Diff line number Diff line change
@@ -1,37 +1,31 @@
# .readthedocs.yml
# Read the Docs configuration file

# Details
# - https://docs.readthedocs.io/en/stable/config-file/v2.html
# - https://github.com/astral-sh/uv/issues/10074
# - https://docs.readthedocs.com/platform/stable/build-customization.html#install-dependencies-with-uv

# Required
version: 2

build:
os: "ubuntu-22.04"
tools:
python: "3.12"

# Work around timeout error.
# ReadTimeoutError: HTTPSConnectionPool(host='files.pythonhosted.org', port=443): Read timed out.
# https://stackoverflow.com/questions/43298872/how-to-solve-readtimeouterror-httpsconnectionpoolhost-pypi-python-org-port#comment110786026_43560499
# https://github.com/readthedocs/readthedocs.org/issues/6311#issuecomment-1324426604
# https://docs.readthedocs.io/en/stable/config-file/v2.html#build-jobs
python: "3.13"
jobs:
post_checkout:
- echo "export PIP_DEFAULT_TIMEOUT=100" >> ~/.profile
create_environment:
- asdf plugin add uv
- asdf install uv latest
- asdf global uv latest
- uv venv
- UV_PROJECT_ENVIRONMENT=$READTHEDOCS_VIRTUALENV_PATH uv sync --all-extras --group docs
install:
- "true"

# Build documentation in the docs/ directory with Sphinx
sphinx:
configuration: docs/conf.py

python:
install:
- method: pip
path: .
extra_requirements:
- docs

# Optionally build your docs in additional formats such as PDF
#formats:
# - pdf
formats:
- pdf
72 changes: 38 additions & 34 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@ name = "SynopticPy"
description = "Retrieve mesonet weather data as Polars DataFrames from Synoptic's Weather API."
readme = "README.md"
requires-python = ">=3.11"
# NOTE: 3.9 doesn't work because I'm using some new typing syntax
# NOTE: 3.10 doesn't work because I'm using `from datetime import UTC`
# NOTE: 3.10 doesn't work because I'm using tomllib, which was introduced in 3.11
license = { file = "LICENSE" }
authors = [{ name = "Brian K. Blaylock", email = "blaylockbk@gmail.com" }]
maintainers = [{ name = "Brian K. Blaylock", email = "blaylockbk@gmail.com" }]
Expand All @@ -24,10 +21,10 @@ classifiers = [
]
keywords = ["weather", "meteorology", "mesonet", "atmosphere"]
dependencies = [
"numpy",
"polars[style,plot,timezone]>=1.9.0",
"requests",
"toml",
"numpy>=2.3.2",
"polars[plot,style,timezone]>=1.33.0",
"requests>=2.32.5",
"toml>=0.10.2",
]
dynamic = ["version"]

Expand All @@ -39,40 +36,22 @@ dynamic = ["version"]
"Bug Tracker" = "https://github.com/blaylockbk/SynopticPy/issues"

[project.optional-dependencies]
extras = [
"altair", # Plotting
"cartopy", # Plotting
"herbie-data", # Need the Cartopy plotting EasyMap
"matplotlib", # Plotting
"metpy",
"pyarrow", # Write to Parquet with pyarrow
"seaborn", # Plotting
plot = [
"altair>=5.5.0",
"cartopy>=0.25.0",
"herbie-data>=2025.7.0",
"matplotlib>=3.10.6",
"seaborn>=0.13.2",
]

docs = [
"autodocsumm",
"esbonio",
"ipython",
"linkify-it-py",
"myst-parser",
"nbconvert",
"nbsphinx",
"sphinx-copybutton",
"pydata-sphinx-theme",
"recommonmark",
"sphinx",
"sphinx-autosummary-accessors",
"sphinx-design",
"sphinx-markdown-tables",
"sphinxcontrib-mermaid",
pandas = [
"pandas>=2.3.2",
"pyarrow>=21.0.0",
]
test = ["pytest", "pytest-cov", "ruff"]

[build-system]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"


[tool.hatch]

[tool.hatch.version]
Expand Down Expand Up @@ -106,3 +85,28 @@ log_level = "DEBUG"
testpaths = ["tests"]
xfail_strict = true
markers = []

[dependency-groups]
dev = [
"ipykernel>=6.30.1",
"pytest>=8.4.2",
"pytest-cov>=6.2.1",
"ruff>=0.12.12",
]
docs = [
"autodocsumm>=0.2.14",
"esbonio>=0.16.5",
"ipython>=9.5.0",
"linkify-it-py>=2.0.3",
"myst-parser>=4.0.1",
"nbconvert>=7.16.6",
"nbsphinx>=0.9.7",
"pydata-sphinx-theme>=0.16.1",
"recommonmark>=0.7.1",
"sphinx>=8.1.3",
"sphinx-autosummary-accessors>=2025.3.1",
"sphinx-copybutton>=0.5.2",
"sphinx-design>=0.6.1",
"sphinx-markdown-tables>=0.0.17",
"sphinxcontrib-mermaid>=1.0.0",
]
12 changes: 8 additions & 4 deletions src/synoptic/json_parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,9 @@ def parse_stations_timeseries(S: "SynopticAPI") -> pl.DataFrame:
)

# Cast 'date_time' column from string to datetime
observed = observed.with_columns(pl.col("date_time").str.to_datetime())
observed = observed.with_columns(
pl.col("date_time").str.to_datetime(time_zone="UTC")
)

# Parse the variable name
observed = observed.pipe(parse_raw_variable_column)
Expand Down Expand Up @@ -283,7 +285,7 @@ def parse_stations_latest_nearesttime(S: "SynopticAPI") -> pl.DataFrame:
## BUG: Synoptic API -- This doesn't seem to be an issue anymore
## The ozone_concentration_value_1 value is returned as string but should
## be a float.
#if "ozone_concentration_value_1" in df.columns:
# if "ozone_concentration_value_1" in df.columns:
# df = df.with_columns(
# pl.struct(
# [
Expand Down Expand Up @@ -364,7 +366,9 @@ def parse_stations_latest_nearesttime(S: "SynopticAPI") -> pl.DataFrame:
observed = pl.concat(to_concat, how="diagonal_relaxed")

# Cast 'date_time' column from string to datetime
observed = observed.with_columns(pl.col("date_time").str.to_datetime())
observed = observed.with_columns(
pl.col("date_time").str.to_datetime(time_zone="UTC")
)

# Parse the variable name
observed = observed.pipe(parse_raw_variable_column)
Expand Down Expand Up @@ -409,7 +413,7 @@ def parse_stations_precipitation(S: "SynopticAPI") -> pl.DataFrame:
.explode("precipitation")
.unnest("precipitation")
.with_columns(
pl.col("first_report", "last_report").str.to_datetime(),
pl.col("first_report", "last_report").str.to_datetime(time_zone="UTC"),
pl.lit(S.UNITS["precipitation"]).alias("units"),
)
)
Expand Down
5 changes: 5 additions & 0 deletions src/synoptic/params.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,5 +165,10 @@ def validate_params(service, **params):
f"'{key}' is not an expected API parameter for the {service} service.",
UserWarning,
)
if key in {"obtimezone"}:
warnings.warn(
f"The '{key}' key is ignored by SynopticPy because a Polars DataFrame can't have mixed timezones in a column.",
UserWarning,
)
if key in {"timeformat", "output", "fields", "obtimezone"}:
warnings.warn(f"The '{key}' key is ignored by SynopticPy.", UserWarning)
2 changes: 1 addition & 1 deletion src/synoptic/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -704,7 +704,7 @@ def df(self) -> pl.DataFrame:
pl.concat([pl.DataFrame(i) for i in self.MNET], how="diagonal_relaxed")
.with_columns(
pl.col("ID", "CATEGORY").cast(pl.UInt32),
pl.col("LAST_OBSERVATION").str.to_datetime(),
pl.col("LAST_OBSERVATION").str.to_datetime(time_zone="UTC"),
pl.struct(
pl.col("PERIOD_OF_RECORD")
.struct.field("start")
Expand Down
31 changes: 18 additions & 13 deletions tests/test_metadata_tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,27 @@ def test_Variables():
assert len(s.df())


def test_Networks():
"""Get DataFrame of all networks."""
s = ss.Networks()
def test_NetworkTypes():
"""Get DataFrame of all network types."""
s = ss.NetworkTypes()
assert len(s.df())

s = ss.Networks(id=1)
assert len(s.df()) == 1

s = ss.Networks(id=[1, 2, 3])
assert len(s.df()) == 3
class TestNetworks:
"""Get DataFrame of all networks."""

def test_networks_default(self):
s = ss.Networks()
assert len(s.df())

s = ss.Networks(shortname="uunet,raws")
assert len(s.df()) == 2
def test_networks_1(self):
s = ss.Networks(id=1)
assert len(s.df()) == 1

def test_networks_123(self):
s = ss.Networks(id=[1, 2, 3])
assert len(s.df()) == 3

def test_NetworkTypes():
"""Get DataFrame of all network types."""
s = ss.NetworkTypes()
assert len(s.df())
def test_networks_shortname(self):
s = ss.Networks(shortname="uunet,raws")
assert len(s.df()) == 2
10 changes: 7 additions & 3 deletions tests/test_services/test_Metadata.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Tests for Metadata Class."""

from datetime import datetime, date
from datetime import date, datetime

import pytest

from synoptic.services import Metadata

Expand All @@ -11,19 +13,21 @@ def test_all_stations():
assert len(s.df())


@pytest.mark.skipif(True, reason="This times out on my personal computer.")
def test_all_stations_complete():
"""Get complete metadata for all stations."""
s = Metadata(complete=1)
assert len(s.df())


@pytest.mark.skipif(True, reason="This times out on my personal computer.")
def test_all_stations_obrange():
"""Get metadata for an obrange."""
s = Metadata(
state="UT",
obrange=(
datetime(2000, 1, 1),
datetime(2001, 1, 1),
datetime(2000, 1, 5),
),
)
assert len(s.df())
Expand All @@ -32,7 +36,7 @@ def test_all_stations_obrange():
def test_obrange_as_date():
"""Get metadata for an obrange, using datetime.date."""
s = Metadata(
state="UT",
state="RI",
obrange=(
date(2024, 1, 1),
date(2024, 1, 2),
Expand Down
Loading
Loading