Files
fquiz/api/app/services/line_service.py
T
2026-04-26 00:14:25 +08:00

820 lines
27 KiB
Python

from __future__ import annotations
import asyncio
import csv
import io
from dataclasses import dataclass
from datetime import datetime
from typing import Any
from fastapi import HTTPException, UploadFile, status
from sqlalchemy import func, or_, select
from sqlalchemy.orm import Session
from ..models.base import utcnow
from ..models.line import Line
from ..models.line_tower import LineTower
from ..schemas.line import (
LineCreateRequest,
LineListResponse,
LineSummary,
LineTowerCreateRequest,
LineTowerImportResponse,
LineTowerListResponse,
LineTowerSummary,
LineTowerUpdateRequest,
LineUpdateRequest,
)
from .push_service import publish_topic
LINE_TOPIC = "admin.power-lines"
CSV_ENCODINGS = ("utf-8-sig", "utf-8", "gbk", "latin-1")
@dataclass
class CsvImportStats:
imported_count: int = 0
updated_count: int = 0
skipped_count: int = 0
warnings: list[str] | None = None
def __post_init__(self) -> None:
if self.warnings is None:
self.warnings = []
def serialize_line(line: Line, *, tower_count: int = 0) -> LineSummary:
return LineSummary(
id=line.id,
code=line.code,
name=line.name,
voltage_kv=line.voltage_kv,
tower_shape=line.tower_shape,
phase_sequence_json=line.phase_sequence_json or {},
arrester_install_json=line.arrester_install_json or {},
lightning_param_json=line.lightning_param_json or {},
status=line.status, # type: ignore[arg-type]
tower_count=tower_count,
create_date=line.create_date,
create_user=line.create_user,
update_date=line.update_date,
update_user=line.update_user,
)
def serialize_line_tower(item: LineTower) -> LineTowerSummary:
return LineTowerSummary(
id=item.id,
line_id=item.line_id,
seq_no=item.seq_no,
tower_no=item.tower_no,
tower_model=item.tower_model,
tower_type=item.tower_type,
longitude=item.longitude,
latitude=item.latitude,
altitude_m=item.altitude_m,
terrain=item.terrain,
ground_resistance_ohm=item.ground_resistance_ohm,
lightning_density=item.lightning_density,
span_small_m=item.span_small_m,
span_large_m=item.span_large_m,
slope_1=item.slope_1,
slope_2=item.slope_2,
risk_level=item.risk_level,
circuit_geometry_json=item.circuit_geometry_json or {},
lightning_result_json=item.lightning_result_json or {},
raw_extra_json=item.raw_extra_json or {},
create_date=item.create_date,
create_user=item.create_user,
update_date=item.update_date,
update_user=item.update_user,
)
def list_lines(
db: Session,
*,
keyword: str | None,
status_filter: str | None,
) -> LineListResponse:
stmt = select(Line)
total_stmt = select(func.count()).select_from(Line)
normalized = (keyword or "").strip()
if normalized:
like = f"%{normalized}%"
predicate = or_(Line.code.ilike(like), Line.name.ilike(like))
stmt = stmt.where(predicate)
total_stmt = total_stmt.where(predicate)
if status_filter in {"enabled", "disabled"}:
stmt = stmt.where(Line.status == status_filter)
total_stmt = total_stmt.where(Line.status == status_filter)
total = int(db.scalar(total_stmt) or 0)
items = db.execute(stmt.order_by(Line.update_date.desc(), Line.code.asc())).scalars().all()
line_ids = [item.id for item in items]
tower_count_map = _load_tower_counts(db, line_ids)
return LineListResponse(
items=[serialize_line(item, tower_count=tower_count_map.get(item.id, 0)) for item in items],
total=total,
)
def get_line_by_id(db: Session, line_id: str) -> Line | None:
return db.execute(select(Line).where(Line.id == line_id)).scalar_one_or_none()
def get_line_by_code(db: Session, code: str) -> Line | None:
normalized = code.strip()
if not normalized:
return None
return db.execute(select(Line).where(func.lower(Line.code) == normalized.lower())).scalar_one_or_none()
def create_line(
db: Session,
payload: LineCreateRequest,
*,
actor_user_id: str,
) -> LineSummary | None:
if get_line_by_code(db, payload.code):
return None
now = utcnow()
line = Line(
code=payload.code.strip(),
name=payload.name.strip(),
voltage_kv=payload.voltage_kv,
tower_shape=_normalize_str(payload.tower_shape),
phase_sequence_json=payload.phase_sequence_json,
arrester_install_json=payload.arrester_install_json,
lightning_param_json=payload.lightning_param_json,
status=payload.status,
create_user=actor_user_id,
update_user=actor_user_id,
create_date=now,
update_date=now,
)
db.add(line)
db.commit()
saved = get_line_by_id(db, line.id)
if not saved:
return None
_publish_line_change("power-lines.created", {"action": "created", "line_id": saved.id})
return serialize_line(saved, tower_count=0)
def update_line(
db: Session,
line_id: str,
payload: LineUpdateRequest,
*,
actor_user_id: str,
) -> LineSummary | None:
line = get_line_by_id(db, line_id)
if not line:
return None
update_data = payload.model_dump(exclude_unset=True)
if "name" in update_data and update_data["name"] is not None:
line.name = str(update_data["name"]).strip()
if "voltage_kv" in update_data:
line.voltage_kv = update_data["voltage_kv"]
if "tower_shape" in update_data:
line.tower_shape = _normalize_str(update_data["tower_shape"])
if "phase_sequence_json" in update_data and update_data["phase_sequence_json"] is not None:
line.phase_sequence_json = dict(update_data["phase_sequence_json"])
if "arrester_install_json" in update_data and update_data["arrester_install_json"] is not None:
line.arrester_install_json = dict(update_data["arrester_install_json"])
if "lightning_param_json" in update_data and update_data["lightning_param_json"] is not None:
line.lightning_param_json = dict(update_data["lightning_param_json"])
if "status" in update_data and update_data["status"] is not None:
line.status = str(update_data["status"])
line.update_user = actor_user_id
line.update_date = utcnow()
db.commit()
saved = get_line_by_id(db, line_id)
if not saved:
return None
tower_count = int(db.scalar(select(func.count()).select_from(LineTower).where(LineTower.line_id == line_id)) or 0)
_publish_line_change("power-lines.updated", {"action": "updated", "line_id": line_id})
return serialize_line(saved, tower_count=tower_count)
def delete_line(db: Session, line_id: str) -> tuple[bool, int]:
line = get_line_by_id(db, line_id)
if not line:
return False, 0
tower_count = int(db.scalar(select(func.count()).select_from(LineTower).where(LineTower.line_id == line_id)) or 0)
if tower_count > 0:
return False, tower_count
db.delete(line)
db.commit()
_publish_line_change("power-lines.deleted", {"action": "deleted", "line_id": line_id})
return True, 0
def list_line_towers(
db: Session,
*,
line_id: str,
keyword: str | None,
tower_type: str | None,
risk_level: str | None,
limit: int,
offset: int,
) -> LineTowerListResponse:
filters: list[Any] = [LineTower.line_id == line_id]
normalized_keyword = (keyword or "").strip()
if normalized_keyword:
like = f"%{normalized_keyword}%"
filters.append(or_(LineTower.tower_no.ilike(like), LineTower.tower_model.ilike(like)))
if tower_type:
filters.append(LineTower.tower_type == tower_type)
if risk_level:
filters.append(LineTower.risk_level == risk_level)
total = int(db.scalar(select(func.count()).select_from(LineTower).where(*filters)) or 0)
items = db.execute(
select(LineTower)
.where(*filters)
.order_by(LineTower.seq_no.asc(), LineTower.id.asc())
.offset(offset)
.limit(limit)
).scalars().all()
return LineTowerListResponse(
items=[serialize_line_tower(item) for item in items],
total=total,
)
def get_line_tower_by_id(db: Session, tower_id: str) -> LineTower | None:
return db.execute(select(LineTower).where(LineTower.id == tower_id)).scalar_one_or_none()
def create_line_tower(
db: Session,
line_id: str,
payload: LineTowerCreateRequest,
*,
actor_user_id: str,
) -> LineTowerSummary | None:
if not get_line_by_id(db, line_id):
return None
existed = db.execute(
select(LineTower).where(
LineTower.line_id == line_id,
or_(LineTower.seq_no == payload.seq_no, LineTower.tower_no == payload.tower_no.strip()),
)
).scalar_one_or_none()
if existed:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Tower sequence or tower number already exists")
now = utcnow()
item = LineTower(
line_id=line_id,
seq_no=payload.seq_no,
tower_no=payload.tower_no.strip(),
tower_model=_normalize_str(payload.tower_model),
tower_type=_normalize_str(payload.tower_type),
longitude=payload.longitude,
latitude=payload.latitude,
altitude_m=payload.altitude_m,
terrain=_normalize_str(payload.terrain),
ground_resistance_ohm=payload.ground_resistance_ohm,
lightning_density=payload.lightning_density,
span_small_m=payload.span_small_m,
span_large_m=payload.span_large_m,
slope_1=payload.slope_1,
slope_2=payload.slope_2,
risk_level=_normalize_str(payload.risk_level),
circuit_geometry_json=payload.circuit_geometry_json,
lightning_result_json=payload.lightning_result_json,
raw_extra_json=payload.raw_extra_json,
create_user=actor_user_id,
update_user=actor_user_id,
create_date=now,
update_date=now,
)
db.add(item)
db.commit()
saved = get_line_tower_by_id(db, item.id)
if not saved:
return None
_publish_line_change("power-lines.towers.created", {"action": "tower_created", "line_id": line_id, "tower_id": saved.id})
return serialize_line_tower(saved)
def update_line_tower(
db: Session,
tower_id: str,
payload: LineTowerUpdateRequest,
*,
actor_user_id: str,
) -> LineTowerSummary | None:
tower = get_line_tower_by_id(db, tower_id)
if not tower:
return None
update_data = payload.model_dump(exclude_unset=True)
next_seq_no = tower.seq_no
if "seq_no" in update_data and update_data["seq_no"] is not None:
next_seq_no = int(update_data["seq_no"])
next_tower_no = tower.tower_no
if "tower_no" in update_data:
normalized_tower_no = _normalize_str(update_data["tower_no"])
if normalized_tower_no is None:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="tower_no cannot be empty")
next_tower_no = normalized_tower_no
conflict = db.execute(
select(LineTower).where(
LineTower.line_id == tower.line_id,
LineTower.id != tower.id,
or_(LineTower.seq_no == next_seq_no, LineTower.tower_no == next_tower_no),
)
).scalar_one_or_none()
if conflict:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Tower sequence or tower number already exists")
for field in (
"seq_no",
"longitude",
"latitude",
"altitude_m",
"ground_resistance_ohm",
"lightning_density",
"span_small_m",
"span_large_m",
"slope_1",
"slope_2",
):
if field in update_data and update_data[field] is not None:
setattr(tower, field, update_data[field])
if "tower_no" in update_data:
tower.tower_no = next_tower_no
for field in ("tower_model", "tower_type", "terrain", "risk_level"):
if field in update_data:
setattr(tower, field, _normalize_str(update_data[field]))
if "circuit_geometry_json" in update_data and update_data["circuit_geometry_json"] is not None:
tower.circuit_geometry_json = dict(update_data["circuit_geometry_json"])
if "lightning_result_json" in update_data and update_data["lightning_result_json"] is not None:
tower.lightning_result_json = dict(update_data["lightning_result_json"])
if "raw_extra_json" in update_data and update_data["raw_extra_json"] is not None:
tower.raw_extra_json = dict(update_data["raw_extra_json"])
tower.update_user = actor_user_id
tower.update_date = utcnow()
db.commit()
saved = get_line_tower_by_id(db, tower_id)
if not saved:
return None
_publish_line_change(
"power-lines.towers.updated",
{"action": "tower_updated", "line_id": saved.line_id, "tower_id": saved.id},
)
return serialize_line_tower(saved)
def delete_line_tower(db: Session, tower_id: str) -> bool:
item = get_line_tower_by_id(db, tower_id)
if not item:
return False
line_id = item.line_id
db.delete(item)
db.commit()
_publish_line_change("power-lines.towers.deleted", {"action": "tower_deleted", "line_id": line_id, "tower_id": tower_id})
return True
def import_line_towers_from_csv(
db: Session,
*,
line: Line,
file: UploadFile,
actor_user_id: str,
) -> LineTowerImportResponse:
content = file.file.read()
if not content:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="CSV file is empty")
text = _decode_csv_bytes(content)
rows = list(csv.reader(io.StringIO(text)))
if not rows:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="CSV file has no rows")
raw_header = rows[0]
if not raw_header:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="CSV header is empty")
max_row_len = max((len(row) for row in rows[1:]), default=len(raw_header))
header = list(raw_header)
extra_headers: list[str] = []
if max_row_len > len(header):
extra_count = max_row_len - len(header)
extra_headers = [f"__extra_col_{idx}" for idx in range(1, extra_count + 1)]
header.extend(extra_headers)
stats = CsvImportStats()
existing_towers = db.execute(select(LineTower).where(LineTower.line_id == line.id)).scalars().all()
tower_by_seq = {item.seq_no: item for item in existing_towers}
tower_by_no = {item.tower_no: item for item in existing_towers}
first_data_row: dict[str, str] | None = None
for row_index, source_row in enumerate(rows[1:], start=2):
row_values = source_row + [""] * (len(header) - len(source_row))
row = {name: row_values[idx] for idx, name in enumerate(header)}
if all(not str(value).strip() for value in row.values()):
continue
if first_data_row is None:
first_data_row = row
seq_no = _parse_int(row.get("序号"))
tower_no = _normalize_str(row.get("塔号"))
if seq_no is None or not tower_no:
stats.skipped_count += 1
if stats.warnings is not None:
stats.warnings.append(f"第 {row_index} 行缺少有效的序号/塔号,已跳过")
continue
tower_from_seq = tower_by_seq.get(seq_no)
tower_from_no = tower_by_no.get(tower_no)
if tower_from_seq and tower_from_no and tower_from_seq.id != tower_from_no.id:
stats.skipped_count += 1
if stats.warnings is not None:
stats.warnings.append(f"第 {row_index} 行序号/塔号映射冲突,已跳过")
continue
tower = tower_from_seq or tower_from_no
is_created = tower is None
if tower is None:
tower = LineTower(
line_id=line.id,
seq_no=seq_no,
tower_no=tower_no,
create_user=actor_user_id,
create_date=utcnow(),
)
db.add(tower)
tower.seq_no = seq_no
tower.tower_no = tower_no
tower.tower_model = _normalize_str(row.get("杆塔模型"))
tower.tower_type = _normalize_str(row.get("直线或耐张杆塔"))
tower.longitude = _parse_float(row.get("经度"))
tower.latitude = _parse_float(row.get("纬度"))
tower.altitude_m = _parse_float(row.get("海拔m"))
tower.terrain = _normalize_str(row.get("地形"))
tower.ground_resistance_ohm = _parse_float(row.get("接地电阻"))
tower.lightning_density = _parse_float(row.get("地闪密度"))
tower.span_small_m = _parse_float(row.get("小号侧档距"))
tower.span_large_m = _parse_float(row.get("大号侧档距"))
tower.slope_1 = _parse_float(row.get("地面倾角1"))
tower.slope_2 = _parse_float(row.get("地面倾角2"))
tower.risk_level = _normalize_str(row.get("雷击风险等级"))
tower.circuit_geometry_json = _build_circuit_geometry(row)
tower.lightning_result_json = _build_lightning_result(row)
tower.raw_extra_json = _extract_extra_values(row, extra_headers)
tower.update_user = actor_user_id
tower.update_date = utcnow()
tower_by_seq[tower.seq_no] = tower
tower_by_no[tower.tower_no] = tower
if is_created:
stats.imported_count += 1
else:
stats.updated_count += 1
_apply_line_metadata_from_csv(
line,
first_data_row,
actor_user_id=actor_user_id,
warnings=stats.warnings if stats.warnings is not None else [],
)
db.commit()
tower_count = int(db.scalar(select(func.count()).select_from(LineTower).where(LineTower.line_id == line.id)) or 0)
_publish_line_change(
"power-lines.towers.imported",
{
"action": "tower_imported",
"line_id": line.id,
"imported_count": stats.imported_count,
"updated_count": stats.updated_count,
"skipped_count": stats.skipped_count,
},
)
return LineTowerImportResponse(
line=serialize_line(line, tower_count=tower_count),
imported_count=stats.imported_count,
updated_count=stats.updated_count,
skipped_count=stats.skipped_count,
warning_count=len(stats.warnings or []),
warnings=stats.warnings or [],
)
def export_line_towers_to_csv(db: Session, *, line: Line) -> tuple[str, bytes]:
towers = db.execute(
select(LineTower)
.where(LineTower.line_id == line.id)
.order_by(LineTower.seq_no.asc(), LineTower.id.asc())
).scalars().all()
headers = [
"线路编号",
"线路名称",
"电压等级",
"塔形",
"序号",
"塔号",
"杆塔模型",
"直线或耐张杆塔",
"经度",
"纬度",
"海拔m",
"接地电阻",
"地闪密度",
"小号侧档距",
"大号侧档距",
"地面倾角1",
"地面倾角2",
"地形",
"雷击风险等级",
"几何参数JSON",
"雷电参数JSON",
"额外字段JSON",
]
output = io.StringIO()
writer = csv.writer(output)
writer.writerow(headers)
for tower in towers:
writer.writerow(
[
line.code,
line.name,
line.voltage_kv or "",
line.tower_shape or "",
tower.seq_no,
tower.tower_no,
tower.tower_model or "",
tower.tower_type or "",
_to_csv_number(tower.longitude),
_to_csv_number(tower.latitude),
_to_csv_number(tower.altitude_m),
_to_csv_number(tower.ground_resistance_ohm),
_to_csv_number(tower.lightning_density),
_to_csv_number(tower.span_small_m),
_to_csv_number(tower.span_large_m),
_to_csv_number(tower.slope_1),
_to_csv_number(tower.slope_2),
tower.terrain or "",
tower.risk_level or "",
_json_dumps_compact(tower.circuit_geometry_json or {}),
_json_dumps_compact(tower.lightning_result_json or {}),
_json_dumps_compact(tower.raw_extra_json or {}),
]
)
filename = f"{line.code}_towers_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
return filename, output.getvalue().encode("utf-8-sig")
def _load_tower_counts(db: Session, line_ids: list[str]) -> dict[str, int]:
if not line_ids:
return {}
rows = db.execute(
select(LineTower.line_id, func.count())
.where(LineTower.line_id.in_(line_ids))
.group_by(LineTower.line_id)
).all()
return {str(row[0]): int(row[1] or 0) for row in rows}
def _apply_line_metadata_from_csv(
line: Line,
row: dict[str, str] | None,
*,
actor_user_id: str,
warnings: list[str],
) -> None:
if not row:
return
csv_line_code = _normalize_str(row.get("线路编号"))
if csv_line_code and csv_line_code != line.code:
warnings.append(f"CSV 线路编号 {csv_line_code} 与当前线路编码 {line.code} 不一致,已保留当前线路编码")
csv_line_name = _normalize_str(row.get("线路名称"))
if csv_line_name:
line.name = csv_line_name
voltage = _parse_int(row.get("电压等级"))
if voltage is not None:
line.voltage_kv = voltage
tower_shape = _normalize_str(row.get("塔形"))
if tower_shape:
line.tower_shape = tower_shape
line.phase_sequence_json = {
"I": _normalize_str(row.get("I回相序")),
"II": _normalize_str(row.get("II回相序")),
"III": _normalize_str(row.get("III回相序")),
"IV": _normalize_str(row.get("IV回相序")),
}
line.arrester_install_json = {
"A": _normalize_str(row.get("A相是否安装避雷器")),
"B": _normalize_str(row.get("B相是否安装避雷器")),
"C": _normalize_str(row.get("C相是否安装避雷器")),
}
line.lightning_param_json = {
"电角度": _parse_float(row.get("电角度")),
"雷电流幅值a": _parse_float(row.get("雷电流幅值a")),
"雷电流幅值b": _parse_float(row.get("雷电流幅值b")),
}
line.update_user = actor_user_id
line.update_date = utcnow()
def _build_circuit_geometry(row: dict[str, str]) -> dict[str, Any]:
def value(key: str) -> float | None:
return _parse_float(row.get(key))
return {
"I": {
"phase_spacing_m": {
"upper": value("I回上相中距m"),
"middle": value("I回中相中距m"),
"lower": value("I回下相中距m"),
},
"phase_height_m": {
"upper": value("I回上相高度m"),
"middle": value("I回中相高度m"),
"lower": value("I回下相高度m"),
},
},
"II": {
"phase_spacing_m": {
"upper": value("II回上相中距m"),
"middle": value("II回中相中距m"),
"lower": value("II回下相中距m"),
},
"phase_height_m": {
"upper": value("II回上相高度m"),
"middle": value("II回中相高度m"),
"lower": value("II回下相高度m"),
},
},
"III": {
"phase_spacing_m": {
"upper": value("III回上相中距m"),
"middle": value("III回中相中距m"),
"lower": value("III回下相中距m"),
},
"phase_height_m": {
"upper": value("III回上相高度m"),
"middle": value("III回中相高度m"),
"lower": value("III回下相高度m"),
},
},
"IV": {
"phase_spacing_m": {
"upper": value("IV回上相中距m"),
"middle": value("IV回中相中距m"),
"lower": value("IV回下相中距m"),
},
"phase_height_m": {
"upper": value("IV回上相高度m"),
"middle": value("IV回中相高度m"),
"lower": value("IV回下相高度m"),
},
},
"insulator_length_mm": _parse_float(row.get("绝缘子串长度mm")),
"tower_height_m": _parse_float(row.get("杆塔呼高m")),
"lightning_wire": {
"left_mid_distance_m": _parse_float(row.get("左避雷中距m")),
"right_mid_distance_m": _parse_float(row.get("右避雷中距m")),
"height_m": _parse_float(row.get("避雷线高度m")),
},
}
def _build_lightning_result(row: dict[str, str]) -> dict[str, Any]:
return {
"counterstroke_indicator": _parse_float(row.get("绕击反击")),
"counterstroke_withstand_ka": _parse_float(row.get("反击耐雷水平kA")),
"counterstroke_trip_rate": _parse_float(row.get("反击跳闸率(次/100km.a)")),
"shielding_withstand_ka": _parse_float(row.get("绕击耐雷水平kA")),
"shielding_trip_rate": _parse_float(row.get("绕击跳闸率(次/100km.a)")),
"risk_level": _normalize_str(row.get("雷击风险等级")),
}
def _extract_extra_values(row: dict[str, str], extra_headers: list[str]) -> dict[str, Any]:
result: dict[str, Any] = {}
for key in extra_headers:
value = _normalize_str(row.get(key))
if value is not None:
result[key] = value
return result
def _decode_csv_bytes(content: bytes) -> str:
for encoding in CSV_ENCODINGS:
try:
return content.decode(encoding)
except UnicodeDecodeError:
continue
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="CSV encoding is not supported")
def _normalize_str(value: Any) -> str | None:
if value is None:
return None
text = str(value).strip()
if not text:
return None
if text in {"-1", "-1.0"}:
return None
return text
def _parse_float(value: Any) -> float | None:
normalized = _normalize_str(value)
if normalized is None:
return None
try:
return float(normalized)
except ValueError:
return None
def _parse_int(value: Any) -> int | None:
normalized = _normalize_str(value)
if normalized is None:
return None
try:
return int(float(normalized))
except ValueError:
return None
def _json_dumps_compact(payload: dict[str, Any]) -> str:
import json
return json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
def _to_csv_number(value: float | None) -> str:
if value is None:
return ""
if float(value).is_integer():
return str(int(value))
return f"{value:.6f}".rstrip("0").rstrip(".")
def _publish_line_change(event_name: str, payload: dict[str, Any]) -> None:
_fire_and_forget(
publish_topic(
LINE_TOPIC,
name=event_name,
payload=payload,
requires_refetch=["/api/v1/lines"],
dedupe_key=f"{event_name}:{payload.get('line_id', 'unknown')}",
)
)
def _fire_and_forget(coro: object) -> None:
try:
loop = asyncio.get_running_loop()
except RuntimeError:
return
loop.create_task(coro)