feat: ж/д слой + cant_swim в скоринг + профили вне модели

Определение специфических рекомендаций (матрица профилей §8):

1. Ж/д слой (закрыт мёртвый railway ×2.5 у РАС):
   - /api/v1/water/{case_id} отдаёт railway=rail как LineString
     (без service/industrial/military веток), кэш общий v2;
   - railway_warning «перекрыть/проверить немедленно» по профилям;
   - SearchMap: Polyline слой ж/д (тёмно-красный), счётчики 💧/🚂.

2. cant_swim → профиль не_умеет_плавать (water ×3.0, без изменения
   радиуса, critical_warning «обследовать водоёмы НЕМЕДЛЕННО»):
   - раньше чекбокс влиял только на текст, в скоринге был пробел;
   - derive в analyze._derive_profiles — работает и для closed_cases.

3. unmodeled_profiles: ДЦП/слабое зрение/слух — честная пометка
   «вне поведенческой модели» с пояснением (vector_tasks B12:
   профили без аналога не выдавать за учтённые); блок на фронте
   в карточке здоровья.

Площадь воды: сферический эксцесс, проверен на квадрате 53° (744017 м²
vs 743272 точного). Тесты: 202 passed (новый test_cant_swim_profile).
This commit is contained in:
2026-09-09 13:12:55 +03:00
parent 98e8e58023
commit f20080305d
2069 changed files with 803865 additions and 97 deletions
@@ -0,0 +1,132 @@
"""Command-line interface for the :mod:`idna` package.
Invoked via ``python -m idna``. See :func:`main` for the entry point.
"""
from __future__ import annotations
import argparse
import sys
from itertools import chain
from typing import IO, TYPE_CHECKING
from . import IDNAError, decode, encode, unicode_version
from .core import _alabel_prefix, _unicode_dots_re
from .package_data import __version__
if TYPE_CHECKING:
from collections.abc import Iterable
def _looks_like_alabel(s: str) -> bool:
"""Return True if any label in ``s`` carries the ``xn--`` ACE prefix."""
prefix = _alabel_prefix.decode("ascii")
return any(label.lower().startswith(prefix) for label in _unicode_dots_re.split(s))
def _build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="python -m idna",
description=(
"Convert a domain name between its Unicode (U-label) and "
"ASCII-compatible (A-label) forms. With no mode flag, the "
"direction is chosen from the first input — if it contains "
"an xn-- label the stream is decoded, otherwise it is "
"encoded — and the same mode is applied to every remaining "
"input. UTS #46 mapping is applied by default; pass "
"--strict to disable it. When no domains are given on the "
"command line and stdin is piped, one domain per line is "
"read from stdin."
),
)
mode = parser.add_mutually_exclusive_group()
mode.add_argument(
"-e",
"--encode",
dest="mode",
action="store_const",
const="encode",
help="Encode the input to its ASCII A-label form.",
)
mode.add_argument(
"-d",
"--decode",
dest="mode",
action="store_const",
const="decode",
help="Decode the input from its ASCII A-label form.",
)
parser.add_argument(
"--strict",
action="store_true",
help="Disable the default UTS #46 mapping and apply IDNA 2008 rules verbatim.",
)
parser.add_argument(
"--version",
action="version",
version=f"idna {__version__} (Unicode {unicode_version})",
)
parser.add_argument(
"domain",
nargs="*",
help="One or more domain names to convert. Omit to read from stdin.",
)
return parser
def _iter_stdin(stream: IO[str]) -> Iterable[str]:
"""Yield non-empty stripped lines from ``stream``, ignoring blanks."""
for line in stream:
stripped = line.strip()
if stripped:
yield stripped
def _convert_one(domain: str, mode: str, uts46: bool) -> bool:
"""Convert ``domain`` and write the result; return ``False`` on failure."""
try:
if mode == "decode":
print(decode(domain, uts46=uts46))
else:
print(encode(domain, uts46=uts46).decode("ascii"))
except IDNAError as err:
print(f"idna: {mode} failed for {domain!r}: {err}", file=sys.stderr)
return False
return True
def main(argv: list[str] | None = None) -> int:
"""Entry point for ``python -m idna``.
When more than one domain is supplied (via positional arguments or
piped stdin) and no mode flag is given, the first input determines
the direction and that mode is applied uniformly to the rest.
:param argv: Argument list excluding the program name. Defaults to
:data:`sys.argv` when ``None``.
:returns: ``0`` on success, ``1`` if any conversion fails.
"""
parser = _build_parser()
args = parser.parse_args(argv)
uts46 = not args.strict
if args.domain:
domains: Iterable[str] = args.domain
elif not sys.stdin.isatty():
domains = _iter_stdin(sys.stdin)
else:
parser.error("a domain argument is required when stdin is a terminal")
iterator = iter(domains)
first = next(iterator, None)
if first is None:
return 0
mode = args.mode or ("decode" if _looks_like_alabel(first) else "encode")
results = [_convert_one(domain, mode, uts46) for domain in chain([first], iterator)]
return 0 if all(results) else 1
if __name__ == "__main__":
sys.exit(main())