from __future__ import annotations import json import os import time import uuid import urllib.error import urllib.request from typing import Any, Optional from worklib.auth_as_employer import get_access_token DEFAULT_COMPANY_ID = "65437401ae3af6f8ffcdbaf8" DEFAULT_GRAPHQL_URL = "https://admin.dev.dipal.ru/graphql" _PASSREQUESTS_MOCK_STATE: dict[str, Any] = {} def reset_passrequests_mock_state() -> None: _PASSREQUESTS_MOCK_STATE.clear() def _mock_passrequests_execute_graphql(*, query: str, variables: Optional[dict[str, Any]], company_id: str, access_token: str) -> dict[str, Any]: state = _PASSREQUESTS_MOCK_STATE.setdefault( company_id, {"places": {}, "users": {}, "passes": {}, "pass_requests_by_pass_id": {}}, ) q = query or "" vars_ = variables or {} def _new_id(prefix: str) -> str: return f"{prefix}_{uuid.uuid4().hex[:12]}" # createPlaceMultiple -> [{id}] if "createPlaceMultiple" in q: place_id = _new_id("place") state["places"][place_id] = {"id": place_id} return {"data": {"createPlaceMultiple": [{"id": place_id, "__typename": "Place"}]}} # createUser -> id if "createUser" in q: account_id = _new_id("user") state["users"][account_id] = {"id": account_id} return {"data": {"createUser": account_id}} # createService -> {id,title,type} if "createService" in q: service_id = _new_id("service") title = (vars_.get("title") or "mock-service") if isinstance(vars_, dict) else "mock-service" stype = (vars_.get("type") or "access") if isinstance(vars_, dict) else "access" return {"data": {"createService": {"id": service_id, "title": title, "type": stype}}} # addPlaceToService/removePlaceFromService -> {id} if "addPlaceToService" in q or "removePlaceFromService" in q: return {"data": {"addPlaceToService": {"id": "ok"}, "removePlaceFromService": {"id": "ok"}}} # addUserToPlace(dto: $input) -> {place_id, member_id} if "addUserToPlace" in q: inp = vars_.get("input") or {} place_id = inp.get("place_id") account_id = inp.get("account_id") if not place_id or not account_id: return {"errors": [{"message": "Bad input", "status": 400}]} member_id = _new_id("member") return {"data": {"addUserToPlace": {"place_id": place_id, "member_id": member_id}}} # createPass -> {id} if "createPass" in q: pass_id = _new_id("pass") state["passes"][pass_id] = {"id": pass_id} pr_id = _new_id("passreq") state["pass_requests_by_pass_id"][pass_id] = { "id": pr_id, "status": "pending", "pass_id": pass_id, "confirmer_ids": [], "created_at": str(int(time.time())), "updated_at": str(int(time.time())), } return {"data": {"createPass": {"id": pass_id}}} # passRequests(filters:{pass_id}) -> results[{...}] if "passRequests" in q: pass_id = vars_.get("pass_id") pr = state["pass_requests_by_pass_id"].get(pass_id) if not pr: return {"data": {"passRequests": {"results": []}}} return { "data": { "passRequests": { "results": [ { "id": pr["id"], "status": pr["status"], "pass_id": pr["pass_id"], "place_id": "mock_place", "created_at": pr["created_at"], "updated_at": pr["updated_at"], "place": {"id": "mock_place"}, "confirmer_ids": pr["confirmer_ids"], } ] } } } # approve/confirm -> if already rejected then no-op; else require 2 confirmations to become active if "approvePassRequest" in q or "confirmPassRequest" in q: pr_id = vars_.get("pass_request_id") or vars_.get("id") pr = next((v for v in state["pass_requests_by_pass_id"].values() if v["id"] == pr_id), None) if pr and pr["status"] != "rejected": conf_id = access_token or "mock_employee" if conf_id not in pr["confirmer_ids"]: pr["confirmer_ids"].append(conf_id) pr["status"] = "active" if len(pr["confirmer_ids"]) >= 2 else "pending" return {"data": {"approvePassRequest": True}} # rejectPassRequest(pass_request_id) -> sets rejected permanently if "rejectPassRequest" in q: pr_id = vars_.get("pass_request_id") or vars_.get("id") pr = next((v for v in state["pass_requests_by_pass_id"].values() if v["id"] == pr_id), None) if pr: pr["status"] = "rejected" return {"data": {"rejectPassRequest": True}} # deleteUser/deletePlace/deletePass etc -> True if "deleteUser" in q or "deletePlace" in q or "deletePass" in q: return {"data": {"ok": True}} return {"data": {}} def build_headers(access_token: str, *, company_id: str = DEFAULT_COMPANY_ID) -> dict[str, str]: return { "Authorization": f"Bearer {access_token}", "x-company-id": company_id, } def execute_graphql( *, query: str, variables: Optional[dict[str, Any]] = None, graphql_url: Optional[str] = None, company_id: str = DEFAULT_COMPANY_ID, access_token: Optional[str] = None, timeout_s: int = 30, ) -> dict[str, Any]: graphql_url = graphql_url or os.getenv("GRAPHQL_URL") or DEFAULT_GRAPHQL_URL if not graphql_url: raise ValueError("Не задан GRAPHQL_URL (передайте graphql_url или установите env GRAPHQL_URL)") token = access_token or get_access_token() if os.getenv("PASSREQUESTS_MOCKS") in {"1", "true", "True"}: data = _mock_passrequests_execute_graphql(query=query, variables=variables, company_id=company_id, access_token=token) if isinstance(data, dict) and data.get("errors"): raise RuntimeError(f"GraphQL errors: {data['errors']}") return data headers = build_headers(token, company_id=company_id) payload: dict[str, Any] = {"query": query} if variables is not None: payload["variables"] = variables req = urllib.request.Request( graphql_url, data=json.dumps(payload).encode("utf-8"), headers={"content-type": "application/json", **headers}, method="POST", ) try: with urllib.request.urlopen(req, timeout=timeout_s) as resp: raw = resp.read().decode("utf-8") except urllib.error.HTTPError as e: body = e.read().decode("utf-8", errors="replace") raise RuntimeError(f"GraphQL HTTP {e.code}: {body}") from e data = json.loads(raw) if raw else {} if isinstance(data, dict) and data.get("errors"): errors = data["errors"] # На стенде часть операций возвращает Forbidden как GraphQL error (не HTTP 403). if isinstance(errors, list) and any(getattr(err, "get", lambda *_: None)("status") == 403 for err in errors): raise PermissionError( "Forbidden (403) для GraphQL операции. " "Проверьте креды/права. Можно задать env: AUTH_USERNAME/AUTH_PASSWORD/AUTH_GRANT_TYPE." ) raise RuntimeError(f"GraphQL errors: {errors}") if not isinstance(data, dict): raise RuntimeError(f"GraphQL response is not an object: {data!r}") return data