import os import httpx API_URL = os.getenv("API_URL", "http://api:8000") _TIMEOUT = httpx.Timeout(15.0, connect=5.0) # Один клиент на весь процесс — TCP-соединения переиспользуются (keep-alive) _client: httpx.AsyncClient | None = None def _get() -> httpx.AsyncClient: global _client if _client is None or _client.is_closed: _client = httpx.AsyncClient( base_url=API_URL, timeout=_TIMEOUT, limits=httpx.Limits(max_keepalive_connections=5, max_connections=10), ) return _client def _headers(token: str | None = None) -> dict: h = {"Content-Type": "application/json"} if token: h["Authorization"] = f"Bearer {token}" return h # ─── Public ────────────────────────────────────────────────────────────────── async def get_news( offset: int = 0, limit: int = 20, category: str | None = None, tag: str | None = None, date_from: str | None = None, date_to: str | None = None, ) -> dict | None: params: dict = {"offset": offset, "limit": limit} if category: params["category"] = category if tag: params["tag"] = tag if date_from: params["date_from"] = date_from if date_to: params["date_to"] = date_to try: r = await _get().get("/news", params=params) if r.status_code == 200: return r.json() except httpx.RequestError: pass return None async def get_article(slug: str) -> dict | None: try: r = await _get().get(f"/news/{slug}") if r.status_code == 200: return r.json() except httpx.RequestError: pass return None async def get_categories() -> list: try: r = await _get().get("/news/categories") if r.status_code == 200: return r.json() except httpx.RequestError: pass return [] async def get_tags(limit: int = 30) -> list: try: r = await _get().get("/news/tags", params={"limit": limit}) if r.status_code == 200: return r.json() except httpx.RequestError: pass return [] # ─── Admin: Auth ───────────────────────────────────────────────────────────── async def admin_login(username: str, password: str) -> str | None: try: r = await _get().post( "/admin/login", json={"username": username, "password": password}, ) if r.status_code == 200: return r.json().get("access_token") return None except httpx.RequestError: return None # ─── Admin: Articles ────────────────────────────────────────────────────────── async def admin_list_articles(token: str, offset: int = 0, limit: int = 50, status: str | None = None) -> dict | None: params: dict = {"offset": offset, "limit": limit} if status: params["status"] = status try: r = await _get().get("/admin/articles", params=params, headers=_headers(token)) if r.status_code == 200: return r.json() except httpx.RequestError: pass return None async def admin_get_article(token: str, article_id: str) -> dict | None: try: r = await _get().get(f"/admin/articles/{article_id}", headers=_headers(token)) if r.status_code == 200: return r.json() except httpx.RequestError: pass return None async def admin_create_article(token: str, data: dict) -> dict | None: try: r = await _get().post("/admin/articles", json=data, headers=_headers(token)) if r.status_code == 200: return r.json() except httpx.RequestError: pass return None async def admin_update_article(token: str, article_id: str, data: dict) -> dict | None: try: r = await _get().put(f"/admin/articles/{article_id}", json=data, headers=_headers(token)) if r.status_code == 200: return r.json() except httpx.RequestError: pass return None async def admin_delete_article(token: str, article_id: str) -> bool: try: r = await _get().delete(f"/admin/articles/{article_id}", headers=_headers(token)) return r.status_code == 200 except httpx.RequestError: return False async def admin_publish_article(token: str, article_id: str) -> bool: try: r = await _get().post(f"/admin/articles/{article_id}/publish", headers=_headers(token)) return r.status_code == 200 except httpx.RequestError: return False # ─── Admin: Categories ──────────────────────────────────────────────────────── async def admin_list_categories(token: str) -> list: try: r = await _get().get("/admin/categories", headers=_headers(token)) if r.status_code == 200: return r.json() except httpx.RequestError: pass return [] async def admin_create_category(token: str, name: str, slug: str = "") -> dict | None: try: r = await _get().post( "/admin/categories", json={"name": name, "slug": slug or None}, headers=_headers(token), ) if r.status_code == 200: return r.json() except httpx.RequestError: pass return None async def admin_update_category(token: str, cat_id: str, name: str, slug: str = "") -> dict | None: try: r = await _get().put( f"/admin/categories/{cat_id}", json={"name": name, "slug": slug or None}, headers=_headers(token), ) if r.status_code == 200: return r.json() except httpx.RequestError: pass return None async def admin_delete_category(token: str, cat_id: str) -> bool: try: r = await _get().delete(f"/admin/categories/{cat_id}", headers=_headers(token)) return r.status_code == 200 except httpx.RequestError: return False # ─── Admin: Media ───────────────────────────────────────────────────────────── async def admin_upload_media(token: str, file_bytes: bytes, filename: str, content_type: str) -> dict | None: try: r = await _get().post( "/admin/media/upload", files={"file": (filename, file_bytes, content_type)}, headers={"Authorization": f"Bearer {token}"}, ) if r.status_code == 200: return r.json() except httpx.RequestError: pass return None async def admin_list_media(token: str, offset: int = 0, limit: int = 50, media_type: str | None = None) -> list: params: dict = {"offset": offset, "limit": limit} if media_type: params["media_type"] = media_type try: r = await _get().get("/admin/media", params=params, headers=_headers(token)) if r.status_code == 200: return r.json() except httpx.RequestError: pass return [] async def admin_delete_media(token: str, media_id: str) -> bool: try: r = await _get().delete(f"/admin/media/{media_id}", headers=_headers(token)) return r.status_code == 200 except httpx.RequestError: return False # ─── Admin: VK Sources ──────────────────────────────────────────────────────── async def admin_vk_status(token: str) -> dict: try: r = await _get().get("/admin/vk/status", headers=_headers(token)) if r.status_code == 200: return r.json() except httpx.RequestError: pass return {"configured": False} async def admin_vk_list_sources(token: str) -> list: try: r = await _get().get("/admin/vk/sources", headers=_headers(token)) if r.status_code == 200: return r.json() except httpx.RequestError: pass return [] async def admin_vk_add_source(token: str, group_id: str, group_name: str, enabled: bool = True) -> dict | None: try: r = await _get().post( "/admin/vk/sources", json={"group_id": group_id, "group_name": group_name, "enabled": enabled}, headers=_headers(token), ) if r.status_code == 201: return r.json() return {"error": r.json().get("detail", "Ошибка")} except httpx.RequestError: return None async def admin_vk_update_source(token: str, source_id: str, group_id: str, group_name: str, enabled: bool) -> dict | None: try: r = await _get().patch( f"/admin/vk/sources/{source_id}", json={"group_id": group_id, "group_name": group_name, "enabled": enabled}, headers=_headers(token), ) if r.status_code == 200: return r.json() except httpx.RequestError: pass return None async def admin_vk_delete_source(token: str, source_id: str) -> bool: try: r = await _get().delete(f"/admin/vk/sources/{source_id}", headers=_headers(token)) return r.status_code == 204 except httpx.RequestError: return False async def admin_vk_import(token: str) -> dict | None: try: r = await _get().post("/admin/vk/import", headers=_headers(token)) if r.status_code == 200: return r.json() except httpx.RequestError: pass return None async def admin_vk_import_history(token: str, source_id: str | None = None) -> dict | None: url = "/admin/vk/import/history" if source_id: url += f"/{source_id}" try: r = await _get().post(url, headers=_headers(token)) if r.status_code == 200: return r.json() except httpx.RequestError: pass return None