Metadata-Version: 2.5
Name: iso8601-intervals
Version: 0.4.0
Summary: ISO 8601 interval navigation, shard keys, and viewport state for time series
Project-URL: Homepage, https://github.com/rolveb/iso8601-intervals
Author: Rolv Erlend Bredesen
License-Expression: BSD-3-Clause
License-File: LICENSE
Keywords: WMO,intervals,iso8601,isoweek,navigation,sharding,time-series
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: BSD License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Database
Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
Requires-Python: >=3.11
Requires-Dist: isodate>=0.7
Provides-Extra: all
Requires-Dist: blosc2>=3.12; extra == 'all'
Requires-Dist: ipywidgets>=8; extra == 'all'
Requires-Dist: numpy>=1.26; extra == 'all'
Requires-Dist: pandas; extra == 'all'
Requires-Dist: xarray>=2024.9; extra == 'all'
Provides-Extra: dev
Requires-Dist: blosc2>=3.12; extra == 'dev'
Requires-Dist: ipywidgets>=8; extra == 'dev'
Requires-Dist: numpy>=1.26; extra == 'dev'
Requires-Dist: pandas; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff; extra == 'dev'
Requires-Dist: xarray>=2024.9; extra == 'dev'
Provides-Extra: widgets
Requires-Dist: ipywidgets>=8; extra == 'widgets'
Provides-Extra: xarray
Requires-Dist: blosc2>=3.12; extra == 'xarray'
Requires-Dist: numpy>=1.26; extra == 'xarray'
Requires-Dist: pandas; extra == 'xarray'
Requires-Dist: xarray>=2024.9; extra == 'xarray'
Description-Content-Type: text/markdown

# iso8601-intervals

ISO 8601 interval navigation for time series dashboards, shard key computation, every extraction, and bookmarkable 3D viewport state.

**No existing package does this.** Parsing libraries handle strings. This package navigates data: URL query params → time ranges → shard keys → stride indices → viewport state.

## Install

```bash
pip install iso8601-intervals              # core (isodate only)
pip install iso8601-intervals[xarray]      # + ShardNavigator
pip install iso8601-intervals[widgets]     # + ◀ ▶ widget
pip install iso8601-intervals[all]         # everything
```

### Installing from TestPyPI

The package is also published to TestPyPI for pre-release testing. Use
both indexes so the `isodate` dependency (which only exists on real
PyPI) resolves correctly:

```bash
pip install \
    --index-url https://test.pypi.org/simple/ \
    --extra-index-url https://pypi.org/simple/ \
    iso8601-intervals
```

Or copy `iso8601_intervals.py` — single file, works in Pyodide.

## Quick start

### URL → Time Range → Shard Keys

```python
from iso8601_intervals import ISOInterval

iv = ISOInterval.from_url("?offset=-1&duration=P2W&every=PT6H")
s, e, rs = iv.resolve()
keys = iv.shard_keys()         # ['2026W14', '2026W15']
print(iv.label())              # '2026W13–W14'
```

### Generalized shard keys

```python
from datetime import datetime
from iso8601_intervals import shard_key

shard_key(datetime(2026, 4, 15, 12), "P1W")   # '2026W16' (isoweek)
shard_key(datetime(2026, 4, 15, 12), "P1D")   # '20260415' (daily)
shard_key(datetime(2026, 4, 15, 12), "P1M")   # '202604' (monthly)
```

### Every — stride + navigation step

```python
# 8 weeks of 1-min DFOS: 80,640 timesteps
iv = ISOInterval(duration="P8W", every="PT6H")
indices = iv.stride_indices(80640, "PT1M")  # [0, 360, 720, ...] → 224 picks
```

### ViewState — 3D viewport in URL

```python
from iso8601_intervals import ViewState

vs = ViewState(elev=25, azim=-55, clim=(-30, 30))
vs.to_query_params()  # {'elev': '25', 'azim': '-55', 'clim': '-30,30'}

# Embedded in ISOInterval
iv = ISOInterval.from_url("?offset=0&elev=25&clim=-30,30&var=strain")
iv.view.apply_to_axes(ax)  # matplotlib 3D axes
```

### ISO 8601 parsing

```python
from iso8601_intervals import parse_iso_time, parse_iso_duration

parse_iso_time("2026-W16-1")    # extended week date
parse_iso_time("2026W161")      # shorthand (basic)
parse_iso_time("2026-W16")      # whole week
parse_iso_time("2026-105")      # ordinal
parse_iso_time("2026-04-15")    # calendar

parse_iso_duration("P2W")       # timedelta(days=14)
parse_iso_duration("PT6H")      # timedelta(hours=6)
```

## Query param schema

| Param | Role | Example | Default |
|:------|:-----|:--------|:--------|
| `offset` | relative window index | `-1` | `0` |
| `start` | absolute start | `2026-W15-1` | — |
| `duration` | window width | `P2W` | `P2W` |
| `every` | stride + ◀ ▶ step | `PT6H` | — |
| `window` | sample width at every | `PT10M` | — |
| `resample` | aggregation | `PT1H` | — |
| `elev`/`azim`/`roll` | 3D viewport | `25`/`-55`/`0` | `30`/`-60`/`0` |
| `xlim`/`ylim`/`zlim` | axis limits | `5,15` | auto |
| `clim` | color limits | `-30,30` | auto |
| `cmap` | colormap | `jet` | `jet` |
| `var`/`channel` | variable selection | `strain`/`1` | — |

## Testing

```bash
python iso8601_intervals.py          # inline tests
pytest tests/ -v                     # 41 pytest tests
```

## Changelog

### 0.4.0 (2026-08-16)

- **`parse_iso_duration`**: cast `d.months` / `d.years` to `float` before
  multiplying by the days-per-month / days-per-year constants. isodate
  returns `decimal.Decimal` for `P1M` / `P1Y` etc.; the previous code
  raised `TypeError` on those inputs. `P1M` now resolves to `30 d 10:30`,
  `P1Y` to `365 d 6:00`.
- **`ISOInterval.resolve`**: new branch for `end + duration → start = end − duration`.
  Handles the `?t_to=…&duration=…` URL shape that the datashader viewer
  writes. Prior behaviour silently anchored to `now − duration`.
- **New `ISOInterval.every_stride(grid_step)` method** and
  `stride_indices` refactor. Single source of truth for `every` → stride,
  grid-aware (pass the grid's measured step instead of a declared
  native-resolution string).

### 0.3.0

Initial TestPyPI release. See git log for prior history.

## License

BSD-3-Clause
