# Кэширование и мониторинг

Кэширование METAR и TAF в продакшне — правила актуальности, TTL, заголовки квоты и мониторинг.



Резервная обработка ответов 404 для [METAR](https://en.wikipedia.org/wiki/METAR)/[TAF](https://en.wikipedia.org/wiki/Terminal_aerodrome_forecast): [Обработка ошибок](/docs/v31/weather-integration-errors). HTTP-ошибки платформы и повторные попытки: [Обработка ошибок](/docs/errors).

### Актуальность данных [#data-freshness]

| Тип данных            | График обновления                                                    | Примечания                                                                       |
| --------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| METAR                 | Каждые 30 мин (крупные аэропорты), каждые 60 мин (небольшие станции) | Сводки SPECI выпускаются немедленно при значительном изменении условий           |
| TAF                   | 4 раза в сутки — 00Z, 06Z, 12Z, 18Z                                  | Действует 24 или 30 часов; TAF остаётся актуальным между выпусками               |
| Ветер на высотах (FB) | Фиксированный график продукта FAA                                    | Только США; `valid_time` в ответе указывает срок действия прогноза               |
| PIREP                 | Пилоты подают донесения непрерывно                                   | Быстро устаревают — используйте короткий период ретроспективы (параметр `hours`) |
| AIRMET/SIGMET         | Выпускаются и обновляются по графику FAA                             | Отбрасывайте истёкшие на стороне клиента по `start_time` / `end_time`            |

Поле `timestamp` в каждом ответе — это момент, когда API получил данные; оно меняется при каждом запросе, даже если само наблюдение не изменилось. Для определения &#x2A;*возраста [METAR](https://en.wikipedia.org/wiki/METAR)** используйте `parsed.time&#x60; (время наблюдения из заголовка сводки). Для &#x2A;*[TAF](https://en.wikipedia.org/wiki/Terminal_aerodrome_forecast)** не используйте `parsed.start_time` как показатель свежести сводки — это начало периода действия прогноза. Между четырьмя ежедневными выпусками один и тот же [TAF](https://en.wikipedia.org/wiki/Terminal_aerodrome_forecast) остаётся актуальным; опрашивайте не чаще чем раз в 15–30 минут или после ожидаемого времени выпуска (00Z, 06Z, 12Z, 18Z).

```python
import requests
from datetime import datetime, timezone

HEADERS = {
    "X-RapidAPI-Key": "YOUR_RAPIDAPI_KEY",
    "X-RapidAPI-Host": "skylink-api.p.rapidapi.com",
}
BASE = "https://skylink-api.p.rapidapi.com"


def parse_observation_time(iso: str) -> datetime:
    return datetime.fromisoformat(iso.replace("Z", "+00:00"))


def observation_age_minutes(metar: dict) -> float:
    obs_time = parse_observation_time(metar["parsed"]["time"])
    now = datetime.now(timezone.utc)
    return (now - obs_time).total_seconds() / 60


def fetch_metar(icao: str) -> dict:
    r = requests.get(
        f"{BASE}/weather/metar/{icao}",
        headers=HEADERS,
        params={"parsed": "true"},
        timeout=(10, 15),
    )
    r.raise_for_status()
    return r.json()


# age = observation_age_minutes(data)
# if age > 90:
#     print(f"Warning: observation is {age:.0f} minutes old - station may be offline")
```

Если `observation_age_minutes` возвращает больше 90 минут, станция, скорее всего, отключена или передаёт данные с перебоями. Данные всё равно можно показывать, но пометьте их как возможно устаревшие.

### Кэширование [#caching]

Ответы [METAR](https://en.wikipedia.org/wiki/METAR) и [TAF](https://en.wikipedia.org/wiki/Terminal_aerodrome_forecast) меняются медленно по сравнению с частотой обновления интерфейса. Кэшируйте [METAR](https://en.wikipedia.org/wiki/METAR) на 3–5 минут, а [TAF](https://en.wikipedia.org/wiki/Terminal_aerodrome_forecast) — на 15–30 минут, вместо того чтобы запрашивать данные заново при каждой отрисовке.

| Эндпоинт                                | Рекомендуемый TTL | Ключ кэша                         |
| --------------------------------------- | ----------------- | --------------------------------- |
| `GET /weather/metar/{icao}`             | 3–5 минут         | `metar:{icao}`                    |
| `GET /weather/metar/{icao}?parsed=true` | 3–5 минут         | `metar:{icao}:parsed`             |
| `GET /weather/taf/{icao}`               | 15–30 минут       | `taf:{icao}`                      |
| `GET /weather/taf/{icao}?parsed=true`   | 15–30 минут       | `taf:{icao}:parsed`               |
| `GET /weather/winds-aloft?bbox=…`       | 30–60 минут       | `winds:{bbox}:{forecast}:{level}` |
| `GET /weather/pireps?bbox=…`            | 5–15 минут        | `pirep:{bbox}:{hours}`            |
| `GET /weather/airsigmet?bbox=…`         | 10–15 минут       | `airsig:{bbox}:{type}`            |

```python
import time
import requests

HEADERS = {
    "X-RapidAPI-Key": "YOUR_RAPIDAPI_KEY",
    "X-RapidAPI-Host": "skylink-api.p.rapidapi.com",
}
BASE = "https://skylink-api.p.rapidapi.com"

_cache: dict[str, tuple[dict, float]] = {}


def cached_get(url: str, params: dict, ttl_seconds: int) -> dict | None:
    key = url + str(sorted(params.items()))
    cached = _cache.get(key)
    if cached and time.time() < cached[1]:
        return cached[0]

    r = requests.get(url, headers=HEADERS, params=params, timeout=(10, 15))
    if r.status_code == 404:
        return None
    r.raise_for_status()
    data = r.json()
    _cache[key] = (data, time.time() + ttl_seconds)
    return data


def get_metar(icao: str) -> dict | None:
    return cached_get(f"{BASE}/weather/metar/{icao}", {"parsed": "true"}, ttl_seconds=300)


def get_taf(icao: str) -> dict | None:
    return cached_get(f"{BASE}/weather/taf/{icao}", {"parsed": "true"}, ttl_seconds=1800)
```

В продакшне замените кэш на основе словаря на Redis или Memcached, чтобы кэш сохранялся после перезапусков и был общим для всех экземпляров.

### Ограничения частоты запросов [#rate-limits]

Каждый ответ содержит заголовки rate limit:

| Заголовок                        | Значение                              |
| -------------------------------- | ------------------------------------- |
| `X-RateLimit-Requests-Limit`     | Месячная квота запросов вашего тарифа |
| `X-RateLimit-Requests-Remaining` | Оставшиеся запросы в этом месяце      |
| `X-RateLimit-Requests-Reset`     | Секунды до сброса квоты               |

Квоты тарифов:

| Тариф | Запросов в месяц |
| ----- | ---------------- |
| Free  | 1000             |
| Basic | 5000             |
| Pro   | 50 000           |
| Ultra | 200 000          |
| Mega  | 600 000          |

```python
import requests

HEADERS = {
    "X-RapidAPI-Key": "YOUR_RAPIDAPI_KEY",
    "X-RapidAPI-Host": "skylink-api.p.rapidapi.com",
}


def check_quota() -> dict:
    r = requests.get(
        "https://skylink-api.p.rapidapi.com/weather/metar/KJFK",
        headers=HEADERS,
        timeout=(10, 15),
    )
    r.raise_for_status()

    remaining = int(r.headers.get("X-RateLimit-Requests-Remaining", 0))
    limit      = int(r.headers.get("X-RateLimit-Requests-Limit", 0))
    reset_in   = int(r.headers.get("X-RateLimit-Requests-Reset", 0))

    return {
        "remaining": remaining,
        "limit": limit,
        "reset_in": reset_in,
        "low_quota": remaining < 100,
    }


# quota = check_quota()
# print(f"Quota: {quota['remaining']}/{quota['limit']} remaining, resets in {quota['reset_in']}s")
#
# if quota["low_quota"]:
#     print("Warning: approaching quota limit - reduce polling frequency or upgrade plan")
```

При получении 429 тело ответа имеет вид `{"message": "Too many requests"}` (формат шлюза RapidAPI). Подождите и повторите попытку — см. [вспомогательную функцию повторных попыток](/docs/errors#retry-rules) на странице «Обработка ошибок» платформы.

### Мониторинг и логирование [#monitoring-and-logging]

Логируйте при каждом запросе погоды:

```python
import logging
import time
import requests
from datetime import datetime, timezone

logger = logging.getLogger("weather")
HEADERS = {
    "X-RapidAPI-Key": "YOUR_RAPIDAPI_KEY",
    "X-RapidAPI-Host": "skylink-api.p.rapidapi.com",
}
BASE = "https://skylink-api.p.rapidapi.com"


def fetch_metar_logged(icao: str) -> dict | None:
    start = time.monotonic()
    r = requests.get(f"{BASE}/weather/metar/{icao}", headers=HEADERS, params={"parsed": "true"}, timeout=(10, 15))
    elapsed_ms = (time.monotonic() - start) * 1000

    log_data = {
        "endpoint": "metar",
        "icao": icao,
        "status": r.status_code,
        "elapsed_ms": round(elapsed_ms),
        "quota_remaining": r.headers.get("X-RateLimit-Requests-Remaining"),
    }

    if r.status_code == 200:
        data = r.json()
        if "parsed" in data:
            obs_time = datetime.fromisoformat(data["parsed"]["time"].replace("Z", "+00:00"))
            age_min = (datetime.now(timezone.utc) - obs_time).total_seconds() / 60
            log_data["observation_age_min"] = round(age_min, 1)
            log_data["flight_rules"] = data["parsed"]["flight_rules"]
        logger.info("metar_fetch", extra=log_data)
        return data
    elif r.status_code == 404:
        logger.info("metar_not_found", extra=log_data)
        return None
    else:
        logger.warning("metar_error", extra=log_data)
        r.raise_for_status()
```

**Настройте оповещения на:**

* `observation_age_min > 90` — станция, скорее всего, отключена или обновления задерживаются
* `status = 429` более двух раз за 5 минут — частота опроса слишком высока для вашего тарифа
* повторяющиеся `elapsed_ms > 5000` — проблема с задержкой у поставщика данных; рассмотрите таймаут и резервный вариант
* `quota_remaining < 200` — вы близки к месячному лимиту

***

См. также: [METAR](/docs/v31/weather-metar) · [TAF](/docs/v31/weather-taf) · [Ветер на высотах](/docs/v31/winds-aloft) · [PIREP](/docs/v31/pireps) · [AIRMET/SIGMET](/docs/v31/airsigmet) · [Обработка ошибок](/docs/v31/weather-integration-errors) · [Сценарии для аэропортов](/docs/v31/weather-integration-airport) · [Сценарии на маршруте](/docs/v31/weather-integration-enroute)
