Metadata-Version: 2.4
Name: hantaviruspy
Version: 0.1.1
Summary: A Python library for fetching hanta virus info from hantavirus osint.
Project-URL: Repository, https://github.com/grMLEqomlkkU5Eeinz4brIrOVCUCkJuN/hantaviruspy
Author: grml
License: ISC
License-File: LICENSE
Keywords: hantavirus,library,python,template
Requires-Python: >=3.12
Requires-Dist: httpx
Provides-Extra: dev
Requires-Dist: git-cliff>=2.13.1; extra == 'dev'
Requires-Dist: lefthook>=2.1.9; extra == 'dev'
Requires-Dist: mypy>=2.1.0; extra == 'dev'
Requires-Dist: ruff>=0.15.18; extra == 'dev'
Description-Content-Type: text/markdown

# hantaviruspy

A typed async Python library for fetching and querying hantavirus outbreak data from the [hantaosint.com](https://hantaosint.com) API.

> Publishing is handled manually (custom settings), so there's no publish
> workflow here, just a `release.sh` that bumps the version, regenerates the
> changelog, and tags.

## What's inside

- **HTTP client**: async `httpx`-based client targeting `https://hantaosint.com/api/v1`, with built-in rate limiter integration.
- **Rate limiter**: token-bucket `RateLimiter` (60 req/min, 120 burst by default). Raises `RateLimiterError` with a `retryAfterMs` value when the bucket is empty.
- **Cache**: `CachedFetcher` wraps the public-JSON fetch with a configurable TTL (default 5 min). Call `invalidate()` to bust it early.
- **Types**: frozen dataclasses for the full API surface — `PublicJsonResponse`, `Country`, `Outbreak`, `MapMarker`, `Brief`, `Stats`, `Meta`, and `GenericRequestError`.
- **Service helpers**: filter/search functions for countries and outbreaks (by status, trend, strain, origin, ISO code, slug, and name).
- **Packaging**: [hatchling](https://hatch.pypa.io) build backend, a `src`-less layout (`hantaviruspy/`), a `py.typed` marker, and a thin `__init__.py` barrel that re-exports the public API.
- **Type checking**: [mypy](https://mypy-lang.org) in `strict` mode, configured in `pyproject.toml`.
- **Linting & formatting**: [ruff](https://docs.astral.sh/ruff) for both lint and format (tab indentation, 88-col, `E/F/I/UP/ANN/B/SIM` rule sets, with type-hint enforcement relaxed for tests).
- **Testing**: the standard library's [`unittest`](https://docs.python.org/3/library/unittest.html) runner. Test modules live under `tests/` and are named after the area they cover.
- **Conventional Commits**: a commit-msg hook validates the [Conventional Commits](https://www.conventionalcommits.org) format with a regex (no Node toolchain), and [git-cliff](https://git-cliff.org) turns that history into a `CHANGELOG.md`.
- **Git hooks**: [lefthook](https://lefthook.dev) runs ruff + mypy on staged files before commit and lints the commit message.
- **CI**: `.github/workflows/` runs lint + type-check on one job and the test suite across Linux/macOS/Windows on Python 3.12 & 3.13.
- **Editor config**: `.vscode/` recommends the Ruff + Python + mypy extensions and wires up format-on-save and import sorting via Ruff.
- **Dependabot**: daily pip + GitHub Actions update PRs.

## Getting started

1. Create a virtual environment and install the dev dependencies:
   ```bash
   python -m venv .venv
   source .venv/bin/activate        # Windows: .venv\Scripts\activate
   pip install -e ".[dev]"
   ```
2. Install the git hooks: `lefthook install`.

## Usage

```python
import asyncio
from hantaviruspy.cache import create_cached_fetcher
from hantaviruspy.service.filterCountries import getCountriesByStatus, searchCountriesByName
from hantaviruspy.service.filterOutbreak import getOutbreaksByStatus

fetcher = create_cached_fetcher(ttl_s=300)

async def main() -> None:
    data = await fetcher()

    active = getCountriesByStatus(data, "active")
    print(f"{len(active)} countries with active cases")

    results = searchCountriesByName(data, "germany")
    print(results)

    outbreaks = getOutbreaksByStatus(data, "active")
    print(outbreaks)

asyncio.run(main())
```

## API reference

### `hanta_virus_client(endpoint, path=None, request_type="GET")`

Low-level async client. Consumes one rate-limiter token per call and raises `GenericRequestError` on any failure.

### `RateLimiter(max_per_minute=60, burst=120)`

Token-bucket rate limiter. The module-level `rateLimiter` instance is used automatically by the client.

| Method / property | Description |
| --- | --- |
| `consume()` | Deduct one token; raises `RateLimiterError` if empty |
| `canRequest()` | `True` if at least one token is available |
| `retryAfterMs` | Milliseconds until the next token is available |

### `CachedFetcher(ttl_s=300)`

Callable that returns a cached `PublicJsonResponse`, re-fetching when the TTL expires.

| Method | Description |
| --- | --- |
| `await fetcher()` | Return cached data (or fetch if stale) |
| `fetcher.invalidate()` | Clear the cache immediately |

Use `create_cached_fetcher(ttl_s=...)` to construct one with a custom TTL.

### Service helpers — countries (`hantaviruspy.service.filterCountries`)

| Function | Description |
| --- | --- |
| `getCountryByCode(data, code)` | Look up a country by its 2-letter code |
| `getCountryByIso(data, iso)` | Look up a country by ISO A3 code |
| `getCountriesByStatus(data, status)` | Filter by status (`"active"`, `"suspected"`, `"lockdown"`, `"historical"`) |
| `getCountriesByTrend(data, trend)` | Filter by trend (`"up"`, `"down"`, `"flat"`) |
| `searchCountriesByName(data, name)` | Case-insensitive substring search on country name |

### Service helpers — outbreaks (`hantaviruspy.service.filterOutbreak`)

| Function | Description |
| --- | --- |
| `getOutbreakBySlug(data, slug)` | Look up an outbreak by its slug |
| `getOutbreaksByStatus(data, status)` | Filter by status (`"active"`, `"resolved"`) |
| `getOutbreaksByStrain(data, strain)` | Case-insensitive substring match on strain |
| `searchOutbreaksByName(data, name)` | Case-insensitive substring search on outbreak name |
| `getOutbreaksByOrigin(data, origin)` | Case-insensitive substring match on origin |

### Types (`hantaviruspy._types`)

| Type | Description |
| --- | --- |
| `PublicJsonResponse` | Full API response (meta, stats, countries, map_markers, briefs, outbreaks) |
| `Country` | Per-country data (code, iso_a3, status, cases, trend, …) |
| `Outbreak` | Outbreak record (slug, strain, origin, cases, deaths, …) |
| `MapMarker` | Geographic marker (lat/lng, city, category, strain, …) |
| `Brief` | News brief (tag, source, title, url, …) |
| `Meta` | Response metadata (tier, delay, generated_at, license, docs) |
| `Stats` | Global aggregate counts |
| `GenericRequestError` | Raised on any API or HTTP failure |

## Commands

| Command | What it does |
| --- | --- |
| `python -m unittest discover -s tests -p "*.py"` | Run the test suite |
| `python -m unittest tests.cache` | Run a single test module |
| `mypy` | Type-check the package (strict) |
| `ruff check .` | Lint |
| `ruff check . --fix` | Lint and auto-fix what it can |
| `ruff format .` | Format with tabs |
| `git-cliff -o CHANGELOG.md` | Regenerate the changelog from commit history |
| `./release.sh v[X.Y.Z]` | Bump version, regenerate changelog, commit, tag |

> Test files are named after the area they cover (e.g. `cache.py`) rather than
> `test_*.py`, so discovery needs the explicit `-p "*.py"` pattern.

## Conventional commits & git hooks

Commits follow [Conventional Commits](https://www.conventionalcommits.org)
(`feat:`, `fix:`, `chore:`, etc.). After installing the dev dependencies, run
`lefthook install` once to wire up the hooks:

- **pre-commit**: runs `ruff check`, `ruff format --check` on staged Python files and `mypy` on the package.
- **commit-msg**: validates the message format with a regex (the rule lives in `lefthook.yml`; no Node/commitlint dependency).

Because the history is conventional, `git-cliff` can regenerate `CHANGELOG.md` automatically.

## Project layout

```
.
├── hantaviruspy/               # implementation
│   ├── __init__.py             # public barrel — re-exports the full API
│   ├── _types.py               # dataclasses and literal types
│   ├── client.py               # async httpx client
│   ├── rateLimiter.py          # token-bucket rate limiter
│   ├── cache.py                # TTL-based cached fetcher
│   ├── py.typed                # ships type information to consumers
│   └── service/
│       ├── getPublicJsonData.py
│       ├── filterCountries.py
│       └── filterOutbreak.py
├── tests/                      # unittest modules, run via discovery
│   ├── __init__.py
│   ├── cache.py
│   ├── filterCountries.py
│   ├── filterOutbreaks.py
│   ├── getPublicJsonData.py
│   └── rateLimit.py
├── pyproject.toml              # packaging + mypy + ruff config
├── cliff.toml                  # git-cliff changelog config
├── lefthook.yml                # git hooks (ruff + mypy + commit-msg check)
├── release.sh                  # version bump + changelog + annotated tag
├── LICENSE
├── .vscode/                    # recommended extensions + editor settings
└── .github/
    ├── ISSUE_TEMPLATE/         # bug report + feature request
    ├── workflows/              # lint.yml + test.yml
    └── dependabot.yml
```

## Releasing

`./release.sh v[X.Y.Z]` bumps the `version` in `pyproject.toml`, regenerates
`CHANGELOG.md`, commits the result as `chore(release): prepare for v[X.Y.Z]`,
and creates an annotated tag whose message is the changelog for the new
version. Then push with `git push && git push --tags`.

Publishing is intentionally left manual. Build with `python -m build` (or
`hatch build`) and upload with whatever registry/auth settings you use.

## License

ISC