Metadata-Version: 2.4
Name: ym-api-client
Version: 0.2.0
Summary: Fast, modern Python client for Yandex Metrika APIs (Logs, Management, Stats) with built-in rate limiting and parallel execution
Author-email: im01zhas <im01zhas@users.noreply.github.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/01zhas/ym-api-client
Project-URL: Source, https://github.com/01zhas/ym-api-client
Project-URL: Tracker, https://github.com/01zhas/ym-api-client/issues
Project-URL: Documentation, https://github.com/01zhas/ym-api-client#readme
Keywords: yandex,metrika,api,client,logs,analytics
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Internet :: WWW/HTTP
Requires-Python: >=3.7
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.25.0
Provides-Extra: async
Requires-Dist: httpx>=0.27.0; extra == "async"
Requires-Dist: httpx[http2]>=0.27.0; extra == "async"
Provides-Extra: dev
Requires-Dist: pytest>=6.0; extra == "dev"
Requires-Dist: pytest-cov>=2.0; extra == "dev"
Requires-Dist: ruff>=0.1.0; extra == "dev"
Requires-Dist: build>=0.7.0; extra == "dev"
Requires-Dist: twine>=3.8.0; extra == "dev"
Dynamic: license-file

<img src="https://raw.githubusercontent.com/01zhas/ym-api-client/main/assets/logo.svg" alt="ym-api-client" width="64" height="64">

# ym-api-client

> Fast, modern Python client for [Yandex Metrika APIs](https://yandex.ru/dev/metrika/doc/api2/concept/about-docpage/) — with automatic rate limiting, parallel execution, exponential backoff, and optional async+HTTP/2 support.

---

[![PyPI](https://img.shields.io/pypi/v/ym-api-client.svg)](https://pypi.org/project/ym-api-client/)
[![Python Versions](https://img.shields.io/pypi/pyversions/ym-api-client.svg)](https://pypi.org/project/ym-api-client/)
[![License](https://img.shields.io/pypi/l/ym-api-client.svg)](https://github.com/01zhas/ym-api-client/blob/main/LICENSE)
[![Code style: ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)](https://github.com/astral-sh/ruff)

## Features

- **⚡ Fast** — up to **8.8× faster** than `tapi-yandex-metrika` in real-world benchmarks
- **⏱ Auto rate limiting** — token bucket algorithm prevents 429 errors (default 8 req/s)
- **🔄 Exponential backoff** — smart retry with jitter for transient failures
- **🧵 Parallel execution** — download from multiple counters concurrently via `ThreadPoolExecutor`
- **🔩 Automatic field chunking** — respects Yandex's 3000‑character `fields` limit, splits and merges transparently
- **🧹 Auto‑cleanup** — removes processed log requests to stay within the 10 GB storage quota
- **🌐 Async + HTTP/2** — optional `ym-api-client[async]` with `httpx.AsyncClient` for high‑throughput concurrency
- **🛡️ Error classification** — `YandexMetrikaAuthError` (403), `YandexMetrikaRateLimitError` (429), `YandexMetrikaApiError` (4xx/5xx)

## Installation

```bash
pip install ym-api-client
```

For async/HTTP/2 support:

```bash
pip install ym-api-client[async]
```

## Quick Start

### Fetch visits for one day

```python
from ym_api_client import YandexMetrikaClient

client = YandexMetrikaClient(access_token="your_oauth_token")

data = client.logs.download_visits(
    counter_id="12345678",
    date1="2026-06-01",
    date2="2026-06-01",
)

print(f"Loaded {len(data)} visits")
# → Loaded 1523 visits
```

### Parallel fetch from multiple counters

```python
from ym_api_client import YandexMetrikaClient

client = YandexMetrikaClient(access_token="token")

results = client.logs.fetch_visits_parallel(
    counter_ids=["111", "222", "333"],
    date1="2026-06-01",
    date2="2026-06-07",
    max_workers=5,
)

for counter_id, rows in results.items():
    print(f"Counter {counter_id}: {len(rows)} rows")
```

### List counters (Management API)

```python
from ym_api_client import YandexMetrikaClient

client = YandexMetrikaClient(access_token="token")
counters = client.management.list_counters()

for c in counters:
    print(f"{c['id']}: {c['name']} — {c['site']}")
```

### Low-level Logs API (step‑by‑step)

```python
# 1. Create a log request
request = client.logs.create_request(
    counter_id="12345678",
    date1="2026-06-01",
    date2="2026-06-01",
    fields=["ym:s:visitID", "ym:s:date", "ym:s:pageViews"],
    source="visits",
)
request_id = request["log_request"]["request_id"]

# 2. Wait for Yandex to process it (exponential backoff)
client.logs.wait_for_request(counter_id="12345678", request_id=request_id)

# 3. Download all parts
rows = client.logs.download_request(counter_id="12345678", request_id=request_id)
```

### Async with HTTP/2

```python
import asyncio
from ym_api_client import AsyncYandexMetrikaClient

async def main():
    async with AsyncYandexMetrikaClient(access_token="token") as client:
        data = await client.logs.download_visits(
            counter_id="12345678",
            date1="2026-06-01",
            date2="2026-06-07",
        )
        print(f"Loaded {len(data)} visits")

asyncio.run(main())
```

## Why not `tapi-yandex-metrika`?

`tapi-yandex-metrika` was the go‑to library for years, but it's been stagnant since 2022. `ym-api-client` is a complete rewrite that fixes its fundamental performance problems.

| Feature | `tapi-yandex-metrika` | `ym-api-client` |
|---|---|---|
| HTTP layer | `tapioca-wrapper` (dynamic proxy, ~2.5 × overhead) | `requests.Session` (direct, pooled) |
| Rate limiting | ❌ None | ✅ Token bucket (8 req/s default, configurable) |
| Parallel downloads | ❌ Sequential only | ✅ `ThreadPoolExecutor` (up to YM's 3‑request limit) |
| Retry logic | ❌ Fixed 10‑second sleep | ✅ Exponential backoff + jitter (2 s → 30 s) |
| Field chunking | ❌ Manual | ✅ Automatic (respects 3000‑char limit, merges transparently) |
| Async / HTTP/2 | ❌ | ✅ Optional `[async]` extra |
| Error handling | Basic | Classified: 403 auth, 429 rate limit, 4xx/5xx API errors |
| Last release | April 2022 | 2026 (active) |
| Python support | 3.5+ | 3.7–3.12 |

### Real‑world benchmark

| Metric | `tapi-yandex-metrika` | `ym-api-client` | Speed‑up |
|---|---|---|---|
| **Total time** (40 counters, 1 day) | 58.89 s | **6.67 s** | **8.8 ×** |
| Wait for YM processing | 58.11 s | **5.52 s** | **10.5 ×** |

> Benchmark details: fetching visits from 40 Yandex Metrika counters for the same date. `ym-api-client` upgrades HTTP/1.1 keep‑alive + eliminates `tapioca-wrapper`'s dynamic proxy overhead. The 10.5 × speedup on the wait phase comes from exponential polling (requests that finish early are detected much sooner than a fixed 10‑second interval).

## API Overview

### `YandexMetrikaClient`

The main client that provides access to all sub‑APIs.

| Sub‑API | Attribute | Description |
|---|---|---|
| Logs API | `.logs` | Download non‑aggregated visit and hit data |
| Management API | `.management` | List counters, goals, filters, operations |

### Logs API methods

| Method | Description |
|---|---|
| `create_request(counter_id, date1, date2, fields, source)` | Create a new log request |
| `wait_for_request(counter_id, request_id)` | Poll until processed (exponential backoff) |
| `download_request(counter_id, request_id)` | Download all parts → `List[Dict]` |
| `get_request_info(counter_id, request_id)` | Check status of a request |
| `evaluate_request(counter_id, date1, date2, fields, source)` | Pre‑flight check (dry run) |
| `clean_request(counter_id, request_id)` | Delete request, free storage |
| `list_requests(counter_id)` | List all log requests for a counter |
| **High‑level:** | |
| `download_visits(counter_id, date1, date2, ...)` | Create → wait → download → cleanup |
| `download_hits(counter_id, date1, date2, ...)` | Same flow, source = `"hits"` |
| `fetch_visits_parallel(counter_ids, date1, date2, ...)` | Multi‑counter parallel visits |
| `fetch_hits_parallel(counter_ids, date1, date2, ...)` | Multi‑counter parallel hits |
| `fetch_missing_dates_parallel(counter_missing, ...)` | Incremental load for gaps |

### Async counterpart

When you install `ym-api-client[async]`, `AsyncYandexMetrikaClient` is available with the same interface but async methods. All parallel operations use `asyncio.gather` + `httpx.AsyncClient` with HTTP/2 multiplexing.

## Architecture

```
ym_api_client/
├── __init__.py           # Public exports, optional async imports
├── client.py             # YandexMetrikaClient — main entry point
├── base.py               # BaseApiClient — HTTP session, auth, retry
├── rate_limiter.py       # Token bucket rate limiter
├── logs_api.py           # Logs API — visits, hits, parallel downloads
├── management_api.py     # Management API — counters, goals
├── exceptions.py         # Typed exceptions
│
├── async_base.py         # [optional] httpx.AsyncClient + HTTP/2
└── async_logs_api.py     # [optional] Async Logs API client
```

## Rate Limiting

The built‑in token bucket prevents 429 errors:

- **Default**: 8 requests/second (safe under the 10 req/s Logs API limit)
- **Burst**: same as the configured rate (instant burst up to 8)
- **If 429 still occurs**: exponential backoff with jitter (2–10 s random sleep)
- **Configurable**: `YandexMetrikaClient(rate_limit_rps=5.0)`

## Development

```bash
git clone https://github.com/01zhas/ym-api-client.git
cd ym-api-client
pip install -e ".[dev]"
pytest tests/
```

### Releasing

```bash
pip install -e ".[dev]"    # ensures build + twine
python -m build            # builds dist/*.whl and dist/*.tar.gz
twine check dist/*         # validates the packages
twine upload dist/*        # uploads to PyPI
```

## License

[MIT](LICENSE) © 2026 im01zhas
