[build-system] requires = ["hatchling >= 1.26"] build-backend = "hatchling.build" [project] name = "foehn" version = "0.4.0" description = "Python toolkit for Swiss weather data — download, query, and analyse MeteoSwiss Open Government Data" readme = "README.md" license = "MIT" license-files = ["LICENSE"] authors = [ { name = "Kay Hendriksen" }, ] requires-python = ">=3.11" dependencies = [ "requests>=2.32", "polars>=1.18", # Imported directly in fetch.py (Retry). It arrives via requests today, but # relying on a transitive dep breaks silently if requests ever swaps backends. # Floor is where Retry gained allowed_methods. "urllib3>=1.26", ] keywords = ["meteoswiss", "weather", "parquet", "delta", "open-data", "switzerland"] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Science/Research", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Topic :: Scientific/Engineering :: Atmospheric Science", "Topic :: Scientific/Engineering :: GIS", "Typing :: Typed", ] [project.urls] Homepage = "https://github.com/kayhendriksen/foehn" Repository = "https://github.com/kayhendriksen/foehn" Issues = "https://github.com/kayhendriksen/foehn/issues" [project.optional-dependencies] databricks = [ "polars-lts-cpu>=1.18", "pyspark>=3.5,<5.0", "delta-spark>=2.4,<5.0", ] mcp = [ "mcp[cli]>=2.0", # mcp_server.py imports BaseModel/Field directly rather than only through mcp. "pydantic>=2.0", ] # Everything for the gridded (xarray) read path, across all formats: # NetCDF (climate grids) -> netCDF4, h5netcdf, h5py # GRIB2 (forecasts) -> cfgrib, eccodes (eccodes ships a system C lib) # HDF5 (ODIM radar) -> h5py, pyproj # plus zarr for to_zarr(). One extra keeps install simple; note eccodes can be # the fragile piece on some platforms. grids = [ "xarray>=2024.1", # Imported directly in grids.py; arrives via xarray/netCDF4 but is ours to declare. "numpy>=1.24", "netCDF4>=1.6", "h5netcdf>=1.0", "h5py>=3.0", "zarr>=3.0", "cfgrib>=0.9.12", "eccodes>=1.7", "pyproj>=3.0", ] dev = [ "ruff>=0.9", # Pinned, not floored: ty typechecks src/ in CI, it is pre-1.0, and a version # bump can turn a green PR red on its own. Dependabot bumps this weekly, so # the upgrade arrives as its own reviewable PR rather than landing on whoever # opens the next one. (ruff and zizmor are pinned in .pre-commit-config.yaml, # which CI reads — see scripts/hook_version.sh.) "ty==0.0.78", "numpy>=1.24", "pydantic>=2.0", # Strict YAML parsing for the workflow-structure test: PyYAML silently # tolerates the duplicate keys and reparenting that break a workflow. "ruamel.yaml>=0.18", "pytest>=8.0", "pytest-cov>=5.0", "pyarrow>=14.0", "mcp[cli]>=2.0", "xarray>=2024.1", "netCDF4>=1.6", "h5netcdf>=1.0", "zarr>=3.0", "cfgrib>=0.9.12", "eccodes>=1.7", "h5py>=3.0", "pyproj>=3.0", ] [project.scripts] foehn = "foehn.cli:main" [tool.ruff] line-length = 120 target-version = "py311" [tool.ruff.lint] select = [ "E", "F", "I", "W", "UP", "B", "C4", "SIM", "RUF", "A", "PIE", "PERF", "T20", "S", # flake8-bandit — inline security lint (bandit still runs in CI for the deeper pass) "DTZ", # naive datetimes — a weather library should never build one by accident "PTH", # pathlib over os.path ] ignore = [ "E731", # lambda assignment — sometimes fine "SIM108", # ternary operator — can hurt readability "RUF002", # en dash in docstrings — intentional for ranges "RUF003", # en dash in comments — intentional for ranges ] [tool.ruff.lint.per-file-ignores] "src/foehn/cli.py" = ["T201"] "src/foehn/convert.py" = ["T201"] "src/foehn/mcp_server.py" = ["T201"] "scripts/*" = ["T201", "PERF203"] # Tests assert freely, build naive datetimes as fixture values, and open() by # path is clearer than Path.open() when the path is a plain string literal. "tests/*" = ["S101", "DTZ001", "PTH123"] [tool.coverage.run] source = ["foehn"] branch = true [tool.coverage.report] # Branch coverage sits at ~99%. What is left is three kinds of unreachable: an # import-time guard (foehn/__init__.py's polars smoke test, exercised by # executing its source rather than re-importing the package), a branch needing an # optional dependency the suite does not install (to_zarr's dask rechunk), and a # handful of defensive branches on library output. Kept a couple of points below # so ordinary churn doesn't go red, but a real regression does. # # precision is set because the gate is compared against the *rounded* total. At # the default precision of 0 a run of 96.95% rounded to 97 and passed, while # pytest-cov printed "FAIL Required test coverage of 97.0% not reached" from its # own unrounded comparison — a green job with a red line in its log, and half a # point of slack the floor did not intend to give. precision = 2 fail_under = 97 show_missing = true [tool.pytest.ini_options] testpaths = ["tests"] addopts = "-ra -m 'not live'" pythonpath = ["."] filterwarnings = [ # Synthetic NetCDF fixtures must not regress to xarray's netCDF4 writer, # which triggers NumPy 2.5's deprecated array-shape assignment. "error:Setting the shape on a NumPy array has been deprecated.*:DeprecationWarning", ] markers = [ "live: opt-in tests that hit the live MeteoSwiss STAC/S3 (run with -m live)", ] [tool.hatch.build.targets.wheel] packages = ["src/foehn"]