auto-sync: 2026-04-22 00:10:01
This commit is contained in:
@@ -212,8 +212,8 @@ def find_fr24_track(conn, flight_number: str, flight_date: date,
|
||||
if orig_iata == origin_iata and dest_iata == destination_iata:
|
||||
return (row[0], row[1])
|
||||
|
||||
# Soft match: only origin known (destination_iata is NULL in schedule)
|
||||
if origin_iata and not destination_iata:
|
||||
# Fallback: match by origin only (full route match failed)
|
||||
if origin_iata:
|
||||
for row in rows:
|
||||
orig_iata = ICAO_TO_IATA.get(row[2])
|
||||
if orig_iata == origin_iata:
|
||||
@@ -280,8 +280,8 @@ def find_fa_track(conn, flight_number: str, flight_date: date,
|
||||
if orig_iata == origin_iata and dest_iata == destination_iata:
|
||||
return (row[0], row[1])
|
||||
|
||||
# Soft match: only origin known (destination_iata is NULL in schedule)
|
||||
if origin_iata and not destination_iata:
|
||||
# Fallback: match by origin only (full route match failed)
|
||||
if origin_iata:
|
||||
for row in rows:
|
||||
orig_iata = ICAO_TO_IATA.get(row[2])
|
||||
if orig_iata == origin_iata:
|
||||
|
||||
@@ -297,6 +297,116 @@ def enrich_schedule(conn, target_date: date) -> int:
|
||||
return updated
|
||||
|
||||
|
||||
# ── Supplement schedule with FR24 flights not in Yandex ─────────────────────
|
||||
|
||||
# Moscow airports: ICAO → IATA (for supplement_schedule)
|
||||
_MOSCOW_ICAO_TO_IATA: Dict[str, str] = {
|
||||
"UUEE": "SVO",
|
||||
"UUDD": "DME",
|
||||
"UUWW": "VKO",
|
||||
"UUBW": "ZIA",
|
||||
}
|
||||
_MOSCOW_ICAO_SET = set(_MOSCOW_ICAO_TO_IATA.keys())
|
||||
|
||||
|
||||
def supplement_schedule(conn, target_date: date) -> int:
|
||||
"""
|
||||
Insert into fr24_ext.schedule flights from flight_actual
|
||||
that have no matching schedule record.
|
||||
|
||||
Source: fr24_ext.flight_actual
|
||||
Target: fr24_ext.schedule (source='fr24')
|
||||
|
||||
Returns: number of rows inserted
|
||||
"""
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO fr24_ext.schedule
|
||||
(flight_date, airport_iata, direction, flight_number,
|
||||
airline_iata, origin_iata, destination_iata,
|
||||
aircraft_type, scheduled_at,
|
||||
actual_takeoff, actual_landed,
|
||||
status, source, fr24_id)
|
||||
SELECT
|
||||
fa.flight_date,
|
||||
-- airport_iata: the Moscow airport involved in this flight
|
||||
CASE
|
||||
WHEN fa.origin_icao IN ('UUEE','UUDD','UUWW','UUBW')
|
||||
THEN CASE fa.origin_icao
|
||||
WHEN 'UUEE' THEN 'SVO'
|
||||
WHEN 'UUDD' THEN 'DME'
|
||||
WHEN 'UUWW' THEN 'VKO'
|
||||
WHEN 'UUBW' THEN 'ZIA'
|
||||
END
|
||||
ELSE CASE fa.dest_icao
|
||||
WHEN 'UUEE' THEN 'SVO'
|
||||
WHEN 'UUDD' THEN 'DME'
|
||||
WHEN 'UUWW' THEN 'VKO'
|
||||
WHEN 'UUBW' THEN 'ZIA'
|
||||
END
|
||||
END AS airport_iata,
|
||||
-- direction: departure if origin is Moscow, otherwise arrival
|
||||
CASE
|
||||
WHEN fa.origin_icao IN ('UUEE','UUDD','UUWW','UUBW')
|
||||
THEN 'departure'
|
||||
ELSE 'arrival'
|
||||
END AS direction,
|
||||
-- flight_number: normalised (strip spaces)
|
||||
UPPER(REGEXP_REPLACE(fa.flight, '\\s+', '', 'g')) AS flight_number,
|
||||
-- airline_iata: leading letter prefix of the flight number
|
||||
UPPER(SUBSTRING(REGEXP_REPLACE(fa.flight, '\\s+', '', 'g') FROM '^([A-Z]{2,3})')) AS airline_iata,
|
||||
-- origin_iata: map known Moscow ICAOs, others NULL
|
||||
CASE fa.origin_icao
|
||||
WHEN 'UUEE' THEN 'SVO'
|
||||
WHEN 'UUDD' THEN 'DME'
|
||||
WHEN 'UUWW' THEN 'VKO'
|
||||
WHEN 'UUBW' THEN 'ZIA'
|
||||
ELSE NULL
|
||||
END AS origin_iata,
|
||||
-- destination_iata: map known Moscow ICAOs, others NULL
|
||||
CASE fa.dest_icao
|
||||
WHEN 'UUEE' THEN 'SVO'
|
||||
WHEN 'UUDD' THEN 'DME'
|
||||
WHEN 'UUWW' THEN 'VKO'
|
||||
WHEN 'UUBW' THEN 'ZIA'
|
||||
ELSE NULL
|
||||
END AS destination_iata,
|
||||
NULL AS aircraft_type,
|
||||
-- scheduled_at: takeoff time for departures, landed for arrivals
|
||||
CASE
|
||||
WHEN fa.origin_icao IN ('UUEE','UUDD','UUWW','UUBW')
|
||||
THEN fa.datetime_takeoff
|
||||
ELSE fa.datetime_landed
|
||||
END AS scheduled_at,
|
||||
fa.datetime_takeoff AS actual_takeoff,
|
||||
fa.datetime_landed AS actual_landed,
|
||||
'actual' AS status,
|
||||
'fr24' AS source,
|
||||
fa.fr24_id
|
||||
FROM fr24_ext.flight_actual fa
|
||||
WHERE fa.flight_date = %(date)s
|
||||
AND fa.flight IS NOT NULL
|
||||
AND fa.flight != ''
|
||||
-- Must involve at least one Moscow airport
|
||||
AND (
|
||||
fa.origin_icao IN ('UUEE','UUDD','UUWW','UUBW')
|
||||
OR fa.dest_icao IN ('UUEE','UUDD','UUWW','UUBW')
|
||||
)
|
||||
-- Skip flights already present in schedule
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM fr24_ext.schedule s
|
||||
WHERE UPPER(REPLACE(s.flight_number, ' ', ''))
|
||||
= UPPER(REPLACE(fa.flight, ' ', ''))
|
||||
AND s.flight_date = fa.flight_date
|
||||
)
|
||||
ON CONFLICT (flight_number, airport_iata, scheduled_at, direction) DO NOTHING
|
||||
""",
|
||||
{"date": target_date},
|
||||
)
|
||||
return cur.rowcount
|
||||
|
||||
|
||||
# ── Main entry ───────────────────────────────────────────────────────────────
|
||||
|
||||
def run(target_date: date, conn) -> Dict:
|
||||
@@ -360,5 +470,16 @@ def run(target_date: date, conn) -> Dict:
|
||||
log.error("FR24 worker: schedule enrichment failed: %s", e)
|
||||
stats["errors"] += 1
|
||||
|
||||
# 4. Supplement schedule with flights from FR24 not in Yandex
|
||||
try:
|
||||
supplemented = supplement_schedule(conn, target_date)
|
||||
conn.commit()
|
||||
stats["schedule_supplemented"] = supplemented
|
||||
log.info("FR24 worker: supplemented %d new schedule rows", supplemented)
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
log.error("FR24 worker: supplement_schedule failed: %s", e)
|
||||
stats["errors"] += 1
|
||||
|
||||
log.info("FR24 worker done: %s", stats)
|
||||
return stats
|
||||
|
||||
165
tasks/flightradar24/reports/TZ-fr24-schedule-supplement.md
Normal file
165
tasks/flightradar24/reports/TZ-fr24-schedule-supplement.md
Normal file
@@ -0,0 +1,165 @@
|
||||
# ТЗ: Дополнение расписания из FR24 flight-summary/full
|
||||
|
||||
**Дата:** 2026-04-21
|
||||
**Статус:** READY FOR DEV
|
||||
**Приоритет:** Высокий
|
||||
|
||||
---
|
||||
|
||||
## Контекст
|
||||
|
||||
FR24 `/api/flight-summary/full` уже используется в `fr24_worker.py` и возвращает
|
||||
**все реальные рейсы** через московские аэропорты за сутки (до 20 000 записей).
|
||||
|
||||
Текущее поведение:
|
||||
- `enrich_schedule()` — только **UPDATE** существующих строк в `fr24_ext.schedule`
|
||||
- Рейсы из `flight_actual`, которых нет в Яндекс.Расписании, **теряются**
|
||||
|
||||
Проблема: Яндекс не покрывает 100% рейсов (чартеры, грузовые, технические, задержанные).
|
||||
FR24 имеет полные данные. Нужно использовать `flight_actual` как дополнительный источник расписания.
|
||||
|
||||
---
|
||||
|
||||
## Что нужно сделать
|
||||
|
||||
### 1. Функция `supplement_schedule()` в `fr24_worker.py`
|
||||
|
||||
```python
|
||||
def supplement_schedule(conn, target_date: date) -> int:
|
||||
"""
|
||||
Insert into fr24_ext.schedule flights from flight_actual
|
||||
that have no matching schedule record.
|
||||
|
||||
Source: fr24_ext.flight_actual
|
||||
Target: fr24_ext.schedule (source='fr24')
|
||||
|
||||
Returns: number of rows inserted
|
||||
"""
|
||||
```
|
||||
|
||||
**Логика:**
|
||||
1. Найти все рейсы в `flight_actual` за дату, у которых нет соответствия в `schedule`
|
||||
2. Определить direction: если `origin_icao` в московских аэропортах → departure, иначе arrival
|
||||
3. INSERT в `schedule` с `source='fr24'`, `status='actual'`
|
||||
|
||||
**SQL-скелет для поиска незаматченных:**
|
||||
```sql
|
||||
SELECT fa.*
|
||||
FROM fr24_ext.flight_actual fa
|
||||
WHERE fa.flight_date = %(date)s
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM fr24_ext.schedule s
|
||||
WHERE UPPER(REPLACE(s.flight_number,' ','')) = UPPER(REPLACE(fa.flight,' ',''))
|
||||
AND s.flight_date = fa.flight_date
|
||||
)
|
||||
AND fa.flight IS NOT NULL
|
||||
AND fa.flight != ''
|
||||
```
|
||||
|
||||
**Поля для INSERT в schedule:**
|
||||
```
|
||||
flight_date = target_date
|
||||
airport_iata = ICAO_TO_IATA[origin_icao или dest_icao] (московский аэропорт)
|
||||
direction = 'departure' если origin_icao московский, иначе 'arrival'
|
||||
flight_number = fa.flight (нормализовать: убрать лишние пробелы)
|
||||
airline_iata = первые 2 символа IATA (если можно определить)
|
||||
origin_iata = ICAO_TO_IATA.get(fa.origin_icao)
|
||||
destination_iata = ICAO_TO_IATA.get(fa.dest_icao)
|
||||
aircraft_type = NULL (нет в flight_actual)
|
||||
scheduled_at = fa.datetime_takeoff (или datetime_landed для arrival)
|
||||
actual_takeoff = fa.datetime_takeoff
|
||||
actual_landed = fa.datetime_landed
|
||||
status = 'actual'
|
||||
source = 'fr24'
|
||||
fr24_id = fa.fr24_id
|
||||
```
|
||||
|
||||
**ON CONFLICT:** `(flight_number, airport_iata, scheduled_at, direction)` — уже есть.
|
||||
|
||||
### 2. Также исправить баги из ревью
|
||||
|
||||
#### Баг A: ZBAD/PKX не в словаре ICAO_TO_IATA
|
||||
Файл: `build_mart.py`
|
||||
Добавить в словарь `ICAO_TO_IATA`:
|
||||
```python
|
||||
"ZBAD": "PKX", # Beijing Daxing
|
||||
"RKSI": "ICN", # Seoul Incheon (уже есть, проверить)
|
||||
```
|
||||
|
||||
#### Баг B: Soft match для FA/FR24 когда destination не совпадает
|
||||
Файл: `build_mart.py`, функции `find_fa_track()` и `find_fr24_track()`
|
||||
|
||||
Текущая логика:
|
||||
```python
|
||||
if origin_iata and not destination_iata: # только если dest == NULL
|
||||
# soft match by origin only
|
||||
```
|
||||
|
||||
Нужно изменить на:
|
||||
```python
|
||||
# Если полный матч не нашёл → fallback на origin only
|
||||
if origin_iata:
|
||||
for row in rows:
|
||||
orig_iata = ICAO_TO_IATA.get(row[2])
|
||||
if orig_iata == origin_iata:
|
||||
return (row[0], row[1])
|
||||
```
|
||||
(убрать условие `not destination_iata`)
|
||||
|
||||
### 3. Вызов `supplement_schedule()` в `run()` fr24_worker.py
|
||||
|
||||
После `enrich_schedule()`:
|
||||
```python
|
||||
# 4. Supplement schedule with flights from FR24 not in Yandex
|
||||
try:
|
||||
supplemented = supplement_schedule(conn, target_date)
|
||||
conn.commit()
|
||||
stats["schedule_supplemented"] = supplemented
|
||||
log.info("FR24 worker: supplemented %d new schedule rows", supplemented)
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
log.error("FR24 worker: supplement_schedule failed: %s", e)
|
||||
stats["errors"] += 1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Что НЕ нужно делать
|
||||
|
||||
- НЕ менять DDL/схему БД (колонки `fr24_id`, `actual_takeoff`, `actual_landed` в schedule уже есть или добавить как nullable)
|
||||
- НЕ трогать RTL-SDR матч — он работает корректно
|
||||
- НЕ запускать backfill — только текущая логика для новых дат
|
||||
|
||||
---
|
||||
|
||||
## Проверка результата
|
||||
|
||||
1. Запустить build для 19.04 или 20.04 (данные уже есть в `flight_actual`)
|
||||
2. Проверить:
|
||||
```sql
|
||||
SELECT source, count(*) FROM fr24_ext.schedule
|
||||
WHERE flight_date = '2026-04-19' GROUP BY source;
|
||||
-- Ожидаемо: yandex=~1570, fr24=N (новые)
|
||||
|
||||
SELECT count(*) FROM fr24_ext.schedule s
|
||||
JOIN fr24_ext.flight_actual fa ON UPPER(REPLACE(fa.flight,' ','')) = UPPER(REPLACE(s.flight_number,' ',''))
|
||||
WHERE s.flight_date = '2026-04-19' AND s.source = 'fr24';
|
||||
```
|
||||
3. Перезапустить mart build для даты — посмотреть увеличение `schedule_flights` и `with_track`
|
||||
|
||||
---
|
||||
|
||||
## Файлы для изменения
|
||||
|
||||
| Файл | Изменение |
|
||||
|------|-----------|
|
||||
| `ingest/tracks_fr24/fr24_worker.py` | Добавить `supplement_schedule()`, вызвать в `run()` |
|
||||
| `ingest/mart/build_mart.py` | Добавить `ZBAD→PKX` в словарь, исправить soft match |
|
||||
|
||||
---
|
||||
|
||||
## Путь к проекту на VM
|
||||
|
||||
`/home/fr24/fr24/ingest/`
|
||||
|
||||
Деплой: `docker cp <file> fr24-tracks-fr24:/app/<file> && docker restart fr24-tracks-fr24`
|
||||
@@ -0,0 +1,76 @@
|
||||
# DEV REPORT: FR24 Schedule Supplement
|
||||
|
||||
**Дата:** 2026-04-21
|
||||
**Статус:** ✅ DONE
|
||||
**Задача:** TZ-fr24-schedule-supplement
|
||||
|
||||
---
|
||||
|
||||
## Прогресс
|
||||
|
||||
- [x] Создан отчёт
|
||||
- [x] Прочитаны исходники
|
||||
- [x] Реализована `supplement_schedule()` в `fr24_worker.py`
|
||||
- [x] Баг A (ZBAD/PKX) — уже был исправлен в коде (строка 65 build_mart.py), изменений не потребовалось
|
||||
- [x] Исправлен баг B (soft match) в `build_mart.py` — оба find_fr24_track и find_fa_track
|
||||
- [x] Добавлен вызов `supplement_schedule()` в `run()` fr24_worker.py (шаг 4)
|
||||
- [x] Синтаксис проверен: оба файла проходят `python3 -m py_compile` без ошибок
|
||||
|
||||
---
|
||||
|
||||
## Изменённые файлы
|
||||
|
||||
### 1. `ingest/tracks_fr24/fr24_worker.py`
|
||||
|
||||
**Добавлено:**
|
||||
- Константы `_MOSCOW_ICAO_TO_IATA` и `_MOSCOW_ICAO_SET` (строки ~302-308)
|
||||
- Функция `supplement_schedule(conn, target_date: date) -> int` (строки 312-400)
|
||||
- Вызов `supplement_schedule()` в `run()` как шаг 4 (строки 475-481)
|
||||
- Новый ключ в `stats`: `"schedule_supplemented"`
|
||||
|
||||
**Логика `supplement_schedule()`:**
|
||||
- Одним INSERT...SELECT вставляет в `fr24_ext.schedule` все рейсы из `flight_actual`, у которых нет совпадений по нормализованному номеру рейса + дате
|
||||
- Определяет `direction` по московскому аэропорту: origin → departure, dest → arrival
|
||||
- `airport_iata` = московский аэропорт рейса (CASE WHEN по 4 ICAO)
|
||||
- `flight_number` = нормализован (пробелы убраны, UPPER)
|
||||
- `airline_iata` = буквенный префикс номера рейса
|
||||
- `scheduled_at` = datetime_takeoff для вылетов, datetime_landed для прилётов
|
||||
- `source = 'fr24'`, `status = 'actual'`
|
||||
- `ON CONFLICT (flight_number, airport_iata, scheduled_at, direction) DO NOTHING`
|
||||
|
||||
### 2. `ingest/mart/build_mart.py`
|
||||
|
||||
**Баг A:** `ZBAD→PKX` уже присутствовал в `ICAO_TO_IATA` (строка 65). Изменений не потребовалось.
|
||||
|
||||
**Баг B (исправлен в двух местах):**
|
||||
- `find_fr24_track()` строка ~215: убрано условие `not destination_iata` — soft match по origin теперь работает как fallback даже когда destination известен
|
||||
- `find_fa_track()` строка ~283: аналогично
|
||||
|
||||
Было:
|
||||
```python
|
||||
if origin_iata and not destination_iata:
|
||||
```
|
||||
Стало:
|
||||
```python
|
||||
# Fallback: match by origin only (full route match failed)
|
||||
if origin_iata:
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Результат проверки синтаксиса
|
||||
|
||||
```
|
||||
fr24_worker.py: OK
|
||||
build_mart.py: OK
|
||||
```
|
||||
|
||||
`python3 -m py_compile` — оба файла без ошибок.
|
||||
|
||||
---
|
||||
|
||||
## Примечания
|
||||
|
||||
- Деплой НЕ выполнялся (согласно ТЗ)
|
||||
- Non-Moscow аэропорты в `origin_iata`/`destination_iata` будут NULL — это допустимо, т.к. полный ICAO→IATA словарь в fr24_worker не дублировался
|
||||
- При желании можно расширить CASE WHEN или вынести общий словарь в `config.py` / `constants.py`
|
||||
Reference in New Issue
Block a user