From f01e3b904023bd5efd3990a08ea0dac3c60ab070 Mon Sep 17 00:00:00 2001 From: Damir Date: Fri, 10 Apr 2026 00:01:42 +0300 Subject: [PATCH] update --- backend/auth.py | 8 +- backend/main.py | 6 + backend/requirements-backend.txt | 2 + frontend/ui.py | 240 +++++++++++++++++++------------ 4 files changed, 161 insertions(+), 95 deletions(-) diff --git a/backend/auth.py b/backend/auth.py index e57c1da..8699322 100644 --- a/backend/auth.py +++ b/backend/auth.py @@ -12,10 +12,10 @@ from sqlalchemy.orm import Session from backend.db import get_db from backend.models import User -pwd_context = CryptContext(schemes=["argon2"], deprecated="auto") +pwd_context = CryptContext(schemes=["argon2", "bcrypt"], deprecated="auto") bearer = HTTPBearer(auto_error=False) -AUTH_VERSION = "argon2-2026-04-07" +AUTH_VERSION = "argon2-bcrypt-compat-2026-04-07" def _env(name: str, default: str) -> str: @@ -48,6 +48,10 @@ def verify_password(password: str, password_hash: str) -> bool: ) from e +def password_needs_update(password_hash: str) -> bool: + return pwd_context.needs_update(password_hash) + + def create_access_token(*, user_id: str) -> str: now = datetime.now(timezone.utc) payload = { diff --git a/backend/main.py b/backend/main.py index 50e4291..3a69fc6 100644 --- a/backend/main.py +++ b/backend/main.py @@ -27,6 +27,7 @@ from backend.auth import ( # noqa: E402 create_access_token, get_current_user, hash_password, + password_needs_update, verify_password, ) from backend.db import engine, get_db # noqa: E402 @@ -130,6 +131,11 @@ def login(req: LoginRequest, db: Session = Depends(get_db)): if not user.is_active: raise HTTPException(status_code=403, detail="User is inactive") + if password_needs_update(user.password_hash): + user.password_hash = hash_password(req.password) + db.add(user) + db.commit() + token = create_access_token(user_id=str(user.id)) return {"access_token": token, "token_type": "bearer"} except HTTPException: diff --git a/backend/requirements-backend.txt b/backend/requirements-backend.txt index dec289f..9015974 100644 --- a/backend/requirements-backend.txt +++ b/backend/requirements-backend.txt @@ -6,5 +6,7 @@ sqlalchemy psycopg[binary] passlib[argon2] argon2-cffi +passlib[bcrypt] +bcrypt python-jose[cryptography] pydantic[email] diff --git a/frontend/ui.py b/frontend/ui.py index de10ec9..d172d59 100644 --- a/frontend/ui.py +++ b/frontend/ui.py @@ -4,10 +4,21 @@ from datetime import datetime, timezone import requests import streamlit as st -st.set_page_config(page_title="DevOps AI Assistant", page_icon="πŸš€") +# Π’ Π΄ΡƒΡ…Π΅ https://github.com/streamlit/demo-ai-assistant β€” ΠΊΡ€ΡƒΠΏΠ½Ρ‹ΠΉ Π³Π΅Ρ€ΠΎΠΉ, Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ Π² ряд с дСйствиСм +st.set_page_config( + page_title="DevOps AI Assistant", + page_icon="✨", + layout="wide", + initial_sidebar_state="expanded", +) -st.title("DevOps AI Assistant") -st.markdown("ΠŸΠΎΡ€Ρ‚Π°Π» с Π΄Π°Π½Π½Ρ‹ΠΌΠΈ ΠΈ инструмСнтами для АБ/ЀП.") +_HERO_HTML = """ + +
❉
+""" def _env(name: str, default: str) -> str: @@ -24,14 +35,36 @@ def _normalize_base_url(value: str) -> str: v = v[: -len("/generate")] return v.rstrip("/") + +def _title_bar(*, show_logout: bool): + st.html(_HERO_HTML) + row = st.container(horizontal=True, vertical_alignment="bottom") + with row: + st.title("DevOps AI Assistant", anchor=False) + if show_logout: + + def _logout(): + st.session_state.token = None + st.rerun() + + st.button( + "Π’Ρ‹ΠΉΡ‚ΠΈ", + icon=":material/logout:", + on_click=_logout, + help="Π’Ρ‹ΠΉΡ‚ΠΈ ΠΈΠ· ΠΏΠΎΡ€Ρ‚Π°Π»Π°", + ) + + with st.sidebar: - st.header("Настройки") + st.markdown("#### Настройки") backend_base_url = _normalize_base_url(st.text_input("Backend URL", default_backend_base_url)) model = st.selectbox( - "LLM МодСль", + "LLM модСль", ["deepseek-r1:1.5b", "qwen2.5-coder:1.5b", "llama3.1:8b", "Arena Model"], ) platform = st.radio("ΠŸΠ»Π°Ρ‚Ρ„ΠΎΡ€ΠΌΠ°", ["PV DOT CI", "PV DOT CD", "DPM CI", "DPM CD"]) + st.divider() + st.caption("ΠŸΠΎΡ€Ρ‚Π°Π» АБ/ЀП Β· ΠΊΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΠΈ Π² Ρ€Π°Π·Π΄Π΅Π»Π΅ Β«Π˜Π½ΡΡ‚Ρ€ΡƒΠΌΠ΅Π½Ρ‚Ρ‹Β»") if "history" not in st.session_state: st.session_state.history = [] @@ -67,6 +100,7 @@ def _auth_headers(): return {} return {"Authorization": f"Bearer {st.session_state.token}"} + def _detail_or_text(resp: requests.Response) -> str: try: payload = resp.json() @@ -78,35 +112,37 @@ def _detail_or_text(resp: requests.Response) -> str: def _show_account(): - st.subheader("АутСнтификация") + _title_bar(show_logout=bool(st.session_state.token)) + st.caption("Π’ΠΎΠΉΠ΄ΠΈΡ‚Π΅ ΠΈΠ»ΠΈ создайтС ΡƒΡ‡Ρ‘Ρ‚Π½ΡƒΡŽ запись, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΠΎΡ‚ΠΊΡ€Ρ‹Ρ‚ΡŒ Ρ€Π°Π·Π΄Π΅Π»Ρ‹ ΠΏΠΎΡ€Ρ‚Π°Π»Π°.") - c1, c2 = st.columns(2) + # Π”Π²Π° Π±Π»ΠΎΠΊΠ° с Ρ€Π°ΠΌΠΊΠΎΠΉ β€” Π±Π»ΠΈΠΆΠ΅ ΠΊ Β«ΠΊΠ°Ρ€Ρ‚ΠΎΡ‡Π½ΠΎΠΌΡƒΒ» Π²ΠΈΠ΄Ρƒ Π΄Π΅ΠΌΠΎ + c1, c2 = st.columns(2, gap="large") with c1: - st.markdown("**РСгистрация**") - reg_email = st.text_input("Email", key="reg_email") - reg_full_name = st.text_input("Имя (ΠΎΠΏΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½ΠΎ)", key="reg_full_name") - reg_password = st.text_input("ΠŸΠ°Ρ€ΠΎΠ»ΡŒ", type="password", key="reg_password") - if st.button("Π‘ΠΎΠ·Π΄Π°Ρ‚ΡŒ Π°ΠΊΠΊΠ°ΡƒΠ½Ρ‚", key="btn_register"): - try: - r = requests.post( - f"{backend_base_url}/auth/register", - json={"email": reg_email, "password": reg_password, "full_name": reg_full_name or None}, - timeout=30, - ) - if r.status_code >= 400: - st.error(f"Ошибка рСгистрации: {_detail_or_text(r)}") - else: - st.success("Аккаунт создан. Π’Π΅ΠΏΠ΅Ρ€ΡŒ ΠΌΠΎΠΆΠ½ΠΎ Π²ΠΎΠΉΡ‚ΠΈ.") - except Exception as e: - st.error(f"Ошибка ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΡ ΠΊ Π±Π΅ΠΊΠ΅Π½Π΄Ρƒ: {e}") + with st.container(border=True): + st.markdown("**РСгистрация**") + reg_email = st.text_input("Email", key="reg_email") + reg_full_name = st.text_input("Имя (ΠΎΠΏΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½ΠΎ)", key="reg_full_name") + reg_password = st.text_input("ΠŸΠ°Ρ€ΠΎΠ»ΡŒ", type="password", key="reg_password") + if st.button("Π‘ΠΎΠ·Π΄Π°Ρ‚ΡŒ Π°ΠΊΠΊΠ°ΡƒΠ½Ρ‚", key="btn_register", use_container_width=True): + try: + r = requests.post( + f"{backend_base_url}/auth/register", + json={"email": reg_email, "password": reg_password, "full_name": reg_full_name or None}, + timeout=30, + ) + if r.status_code >= 400: + st.error(f"Ошибка рСгистрации: {_detail_or_text(r)}") + else: + st.success("Аккаунт создан. Π’Π΅ΠΏΠ΅Ρ€ΡŒ ΠΌΠΎΠΆΠ½ΠΎ Π²ΠΎΠΉΡ‚ΠΈ.") + except Exception as e: + st.error(f"Ошибка ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΡ ΠΊ Π±Π΅ΠΊΠ΅Π½Π΄Ρƒ: {e}") with c2: - st.markdown("**Π’Ρ…ΠΎΠ΄**") - login_email = st.text_input("Email ", key="login_email") - login_password = st.text_input("ΠŸΠ°Ρ€ΠΎΠ»ΡŒ ", type="password", key="login_password") - login_col_a, login_col_b = st.columns([1, 1]) - with login_col_a: - if st.button("Π’ΠΎΠΉΡ‚ΠΈ", type="primary", key="btn_login"): + with st.container(border=True): + st.markdown("**Π’Ρ…ΠΎΠ΄**") + login_email = st.text_input("Email ", key="login_email") + login_password = st.text_input("ΠŸΠ°Ρ€ΠΎΠ»ΡŒ ", type="password", key="login_password") + if st.button("Π’ΠΎΠΉΡ‚ΠΈ", type="primary", key="btn_login", use_container_width=True): try: r = requests.post( f"{backend_base_url}/auth/login", @@ -121,10 +157,6 @@ def _show_account(): st.rerun() except Exception as e: st.error(f"Ошибка ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΡ ΠΊ Π±Π΅ΠΊΠ΅Π½Π΄Ρƒ: {e}") - with login_col_b: - if st.button("Π’Ρ‹ΠΉΡ‚ΠΈ", disabled=not bool(st.session_state.token), key="btn_logout"): - st.session_state.token = None - st.rerun() st.divider() if st.session_state.token: @@ -135,16 +167,20 @@ def _show_account(): st.warning("БСссия истСкла ΠΈΠ»ΠΈ Ρ‚ΠΎΠΊΠ΅Π½ Π½Π΅Π²Π΅Ρ€Π½Ρ‹ΠΉ. Π’ΠΎΠΉΠ΄ΠΈΡ‚Π΅ Π·Π°Π½ΠΎΠ²ΠΎ.") st.rerun() else: - st.json(r.json()) + with st.expander("ΠŸΡ€ΠΎΡ„ΠΈΠ»ΡŒ", expanded=False): + st.json(r.json()) except Exception as e: st.error(f"Ошибка ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΡ ΠΊ Π±Π΅ΠΊΠ΅Π½Π΄Ρƒ: {e}") -# Auth wall: ΠΏΠΎΠΊΠ° ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ Π½Π΅ Π²ΠΎΡˆΡ‘Π» β€” ΠΏΠΎΠΊΠ°Π·Ρ‹Π²Π°Π΅ΠΌ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ экран Π²Ρ…ΠΎΠ΄Π°/рСгистрации. +# Auth wall if not st.session_state.token: _show_account() st.stop() +# ПослС Π²Ρ…ΠΎΠ΄Π° β€” Ρ‚ΠΎΡ‚ ΠΆΠ΅ Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ + Π²ΠΊΠ»Π°Π΄ΠΊΠΈ +_title_bar(show_logout=True) +st.caption("ΠŸΠΎΡ€Ρ‚Π°Π» с Π΄Π°Π½Π½Ρ‹ΠΌΠΈ ΠΈ инструмСнтами для АБ/ЀП.") tab_info, tab_autonomous, tab_infra, tab_tools, tab_news = st.tabs( [ @@ -158,79 +194,97 @@ tab_info, tab_autonomous, tab_infra, tab_tools, tab_news = st.tabs( with tab_info: st.subheader("БвСдСния ΠΎΠ± АБ/ЀП") - st.caption("Π§Π΅Ρ€Π½ΠΎΠ²ΠΈΠΊ Ρ„ΠΎΡ€ΠΌΡ‹. Π”Π°Π»ΡŒΡˆΠ΅ ΠΌΠΎΠΆΠ½ΠΎ ΠΏΡ€ΠΈΠ²ΡΠ·Π°Ρ‚ΡŒ ΠΊ Postgres ΠΈ Ρ…Ρ€Π°Π½ΠΈΡ‚ΡŒ ΠΊΠ°ΠΊ ΡΡƒΡ‰Π½ΠΎΡΡ‚ΡŒ.") - st.session_state.asfp_info["name"] = st.text_input("НазваниС", st.session_state.asfp_info["name"]) - st.session_state.asfp_info["owner"] = st.text_input("Π’Π»Π°Π΄Π΅Π»Π΅Ρ†", st.session_state.asfp_info["owner"]) - st.session_state.asfp_info["contacts"] = st.text_input("ΠšΠΎΠ½Ρ‚Π°ΠΊΡ‚Ρ‹", st.session_state.asfp_info["contacts"]) - st.session_state.asfp_info["description"] = st.text_area( - "ОписаниС", st.session_state.asfp_info["description"], height=120 - ) - st.divider() - st.json(st.session_state.asfp_info) + st.caption("Π§Π΅Ρ€Π½ΠΎΠ²ΠΈΠΊ Ρ„ΠΎΡ€ΠΌΡ‹. Π”Π°Π»ΡŒΡˆΠ΅ ΠΌΠΎΠΆΠ½ΠΎ ΠΏΡ€ΠΈΠ²ΡΠ·Π°Ρ‚ΡŒ ΠΊ Postgres.") + with st.container(border=True): + st.session_state.asfp_info["name"] = st.text_input("НазваниС", st.session_state.asfp_info["name"]) + st.session_state.asfp_info["owner"] = st.text_input("Π’Π»Π°Π΄Π΅Π»Π΅Ρ†", st.session_state.asfp_info["owner"]) + st.session_state.asfp_info["contacts"] = st.text_input("ΠšΠΎΠ½Ρ‚Π°ΠΊΡ‚Ρ‹", st.session_state.asfp_info["contacts"]) + st.session_state.asfp_info["description"] = st.text_area( + "ОписаниС", st.session_state.asfp_info["description"], height=120 + ) + with st.expander("JSON", expanded=False): + st.json(st.session_state.asfp_info) with tab_autonomous: st.subheader("АвтономныС внСдрСния") - st.info("Бюда ΠΌΠΎΠΆΠ½ΠΎ Π΄ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ список Π²Π½Π΅Π΄Ρ€Π΅Π½ΠΈΠΉ, статус, Π΄Π°Ρ‚Ρ‹, отвСтствСнных ΠΈ ссылки Π½Π° Ρ€Π΅Π»ΠΈΠ·Ρ‹/Ρ‚ΠΈΠΊΠ΅Ρ‚Ρ‹.") + with st.container(border=True): + st.info( + "Бюда ΠΌΠΎΠΆΠ½ΠΎ Π΄ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ список Π²Π½Π΅Π΄Ρ€Π΅Π½ΠΈΠΉ, статус, Π΄Π°Ρ‚Ρ‹, отвСтствСнных ΠΈ ссылки Π½Π° Ρ€Π΅Π»ΠΈΠ·Ρ‹/Ρ‚ΠΈΠΊΠ΅Ρ‚Ρ‹." + ) with tab_infra: st.subheader("Π˜Π½Ρ„Ρ€Π°ΡΡ‚Ρ€ΡƒΠΊΡ‚ΡƒΡ€Π° АБ/ЀП") - st.info("Бюда ΠΌΠΎΠΆΠ½ΠΎ Π΄ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ окруТСния, кластСра, сСрвисы, рСсурсы, сСтСвыС зависимости, сСкрСты ΠΈ Ρ‚.Π΄.") + with st.container(border=True): + st.info( + "Бюда ΠΌΠΎΠΆΠ½ΠΎ Π΄ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ окруТСния, кластСра, сСрвисы, рСсурсы, сСтСвыС зависимости ΠΈ Ρ‚.Π΄." + ) with tab_tools: st.subheader("Π˜Π½ΡΡ‚Ρ€ΡƒΠΌΠ΅Π½Ρ‚Ρ‹") - st.caption(f"Backend URL: {backend_base_url}") + st.caption(f"Backend: `{backend_base_url}`") - st.markdown("### Pipeline generator") - user_input = st.text_area( - "ΠžΠΏΠΈΡˆΠΈΡ‚Π΅ ΠΏΠ°ΠΉΠΏΠ»Π°ΠΉΠ½ (Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€: 'Python app with pytest and docker build')", - height=150, - key="generator_prompt", - ) + with st.container(border=True): + st.markdown("#### Pipeline generator") + user_input = st.text_area( + "ΠžΠΏΠΈΡˆΠΈΡ‚Π΅ ΠΏΠ°ΠΉΠΏΠ»Π°ΠΉΠ½ (Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€: 'Python app with pytest and docker build')", + height=150, + key="generator_prompt", + ) - col_a, col_b = st.columns([1, 3]) - with col_a: - generate_clicked = st.button("Π‘Π³Π΅Π½Π΅Ρ€ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ ΠΊΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΡŽ", type="primary") - with col_b: - st.caption("Подсказка: Ρ‡Π΅ΠΌ ΠΊΠΎΠ½ΠΊΡ€Π΅Ρ‚Π½Π΅Π΅ трСбования, Ρ‚Π΅ΠΌ Π²Π°Π»ΠΈΠ΄Π½Π΅Π΅ YAML.") + col_a, col_b = st.columns([1, 3]) + with col_a: + generate_clicked = st.button("Π‘Π³Π΅Π½Π΅Ρ€ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ", type="primary", use_container_width=True) + with col_b: + st.caption("Π§Π΅ΠΌ ΠΊΠΎΠ½ΠΊΡ€Π΅Ρ‚Π½Π΅Π΅ трСбования, Ρ‚Π΅ΠΌ ΡΡ‚Π°Π±ΠΈΠ»ΡŒΠ½Π΅Π΅ YAML.") - if generate_clicked: - if user_input.strip(): - with st.spinner("LLM Π΄ΡƒΠΌΠ°Π΅Ρ‚..."): - try: - res = requests.post( - f"{backend_base_url}/generate", - json={"description": user_input, "platform": platform, "model": model}, - headers=_auth_headers(), - timeout=90, - ) - res.raise_for_status() - data = res.json() + if generate_clicked: + if user_input.strip(): + with st.spinner("LLM Π΄ΡƒΠΌΠ°Π΅Ρ‚..."): + try: + res = requests.post( + f"{backend_base_url}/generate", + json={"description": user_input, "platform": platform, "model": model}, + headers=_auth_headers(), + timeout=90, + ) + res.raise_for_status() + data = res.json() - status = data.get("status", "error") - config = data.get("config", "") - err = data.get("validation_error") + status = data.get("status", "error") + config = data.get("config", "") + err = data.get("validation_error") - _push_history( - platform=platform, - model=model, - prompt=user_input, - status=status, - config=config, - error=err, - ) + _push_history( + platform=platform, + model=model, + prompt=user_input, + status=status, + config=config, + error=err, + ) - if status == "ok": - st.success("ΠšΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΡ Π²Π°Π»ΠΈΠ΄Π½Π°.") - else: - st.error(f"Ошибка Π²Π°Π»ΠΈΠ΄Π°Ρ†ΠΈΠΈ: {err}") + if status == "ok": + st.success("ΠšΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΡ Π²Π°Π»ΠΈΠ΄Π½Π°.") + else: + st.error(f"Ошибка Π²Π°Π»ΠΈΠ΄Π°Ρ†ΠΈΠΈ: {err}") - st.code(config, language="yaml") - st.download_button("Π‘ΠΊΠ°Ρ‡Π°Ρ‚ΡŒ .yml", config, file_name="pipeline.yml") - except Exception as e: - st.error(f"Ошибка ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΡ ΠΊ Π±Π΅ΠΊΠ΅Π½Π΄Ρƒ: {e}") - else: - st.warning("ΠŸΠΎΠΆΠ°Π»ΡƒΠΉΡΡ‚Π°, Π²Π²Π΅Π΄ΠΈΡ‚Π΅ описаниС.") + st.code(config, language="yaml") + st.download_button("Π‘ΠΊΠ°Ρ‡Π°Ρ‚ΡŒ .yml", config, file_name="pipeline.yml") + except Exception as e: + st.error(f"Ошибка ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΡ ΠΊ Π±Π΅ΠΊΠ΅Π½Π΄Ρƒ: {e}") + else: + st.warning("ΠŸΠΎΠΆΠ°Π»ΡƒΠΉΡΡ‚Π°, Π²Π²Π΅Π΄ΠΈΡ‚Π΅ описаниС.") with tab_news: st.subheader("Новости") - st.info("Бюда ΠΌΠΎΠΆΠ½ΠΎ вывСсти новости/измСнСния: Ρ€Π΅Π»ΠΈΠ·Ρ‹, ΠΈΠ½Ρ†ΠΈΠ΄Π΅Π½Ρ‚Ρ‹, Ρ€Π΅Π³Π»Π°ΠΌΠ΅Π½Ρ‚Π½Ρ‹Π΅ Ρ€Π°Π±ΠΎΡ‚Ρ‹, обновлСния инструмСнтов.") + with st.container(border=True): + st.info( + "Π Π΅Π»ΠΈΠ·Ρ‹, ΠΈΠ½Ρ†ΠΈΠ΄Π΅Π½Ρ‚Ρ‹, Ρ€Π΅Π³Π»Π°ΠΌΠ΅Π½Ρ‚Π½Ρ‹Π΅ Ρ€Π°Π±ΠΎΡ‚Ρ‹, обновлСния инструмСнтов β€” сюда ΠΌΠΎΠΆΠ½ΠΎ вывСсти Π»Π΅Π½Ρ‚Ρƒ ΠΏΠΎΠ·ΠΆΠ΅." + ) + +st.divider() +st.button( + " :small[:gray[:material/balance: О ΠΏΠΎΡ€Ρ‚Π°Π»Π΅]]", + type="tertiary", + help="DevOps AI Assistant β€” Π²Π½ΡƒΡ‚Ρ€Π΅Π½Π½ΠΈΠΉ ΠΏΠΎΡ€Ρ‚Π°Π».", +)