Noob_test/worklib/graphql_client.py
2026-05-15 11:34:24 +03:00

319 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
import json
import os
import ssl
import time
import uuid
import urllib.error
import urllib.request
import urllib.parse
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 _build_ssl_context(url: str) -> ssl.SSLContext | None:
"""
На dev-стендах сертификат иногда не проходит системную валидацию на Windows.
По умолчанию для *.dev.dipal.ru отключаем verify, но это можно переопределить env SSL_VERIFY=1.
"""
try:
parsed = urllib.parse.urlparse(url)
except Exception:
return None
if (os.getenv("SSL_VERIFY") or "").lower() in {"1", "true", "yes"}:
return None
if (parsed.scheme or "").lower() != "https":
return None
host = (parsed.hostname or "").lower()
if host.endswith(".dev.dipal.ru") or host == "dev.dipal.ru":
return ssl._create_unverified_context() # noqa: SLF001
return None
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": {}, "members_by_place": {}},
)
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")
parent_id = None
if isinstance(vars_, dict):
parent_id = vars_.get("parent_id")
state["places"][place_id] = {"id": place_id, "parent_id": parent_id}
return {"data": {"createPlaceMultiple": [{"id": place_id, "__typename": "Place"}]}}
# createEntrance -> JSONObject (mocked as object with id)
if "createEntrance" in q:
entrance_id = _new_id("entrance")
return {"data": {"createEntrance": {"id": entrance_id}}}
# 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")
members = state["members_by_place"].setdefault(str(place_id), [])
raw_privs = inp.get("privileges") or inp.get("privilege") or []
if isinstance(raw_privs, str):
privs = [raw_privs]
elif isinstance(raw_privs, list):
privs = raw_privs
else:
privs = []
members.append(
{
"id": member_id,
"status": "accepted",
"privileges": privs,
"user": {"id": str(account_id)},
}
)
return {"data": {"addUserToPlace": {"place_id": place_id, "member_id": member_id}}}
# createPass -> {id}
if "createPass" in q:
pass_id = _new_id("pass")
place_id = None
if isinstance(vars_, dict):
place_id = vars_.get("place_id") or (vars_.get("dto") or {}).get("place_id")
state["passes"][pass_id] = {"id": pass_id, "place_id": place_id}
pr_id = _new_id("passreq")
parent_place_id = None
if place_id and isinstance(state.get("places"), dict):
parent_place_id = (state["places"].get(str(place_id)) or {}).get("parent_id")
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())),
"place_id": parent_place_id or place_id or "mock_parent_place",
}
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": pr.get("place_id") or "mock_parent_place",
"created_at": pr["created_at"],
"updated_at": pr["updated_at"],
"place": {"id": pr.get("place_id") or "mock_parent_place"},
"confirmer_ids": pr["confirmer_ids"],
}
]
}
}
}
# members(filters:{place_id}) -> results[{id,status,privileges,user{id}}]
# Важно: place(...) query тоже выбирает поле members, поэтому матчим только конкретные members-запросы.
if ("membersByPlace" in q or "query members" in q) and "members(" in q:
place_id = None
if isinstance(vars_, dict):
place_id = vars_.get("place_id")
results = state["members_by_place"].get(str(place_id), []) if place_id else []
return {"data": {"members": {"results": results}}}
# setUserPlaces -> {id}
if "setUserPlaces" in q:
dto = None
if isinstance(vars_, dict):
dto = vars_.get("dto") or vars_
account_id = (dto or {}).get("account_id")
place_ids = (dto or {}).get("place_ids") or []
extra_privileges = (dto or {}).get("extra_privileges") or []
for pid in place_ids:
members = state["members_by_place"].setdefault(str(pid), [])
members.append(
{
"id": _new_id("member"),
"status": "accepted",
"privileges": extra_privileges,
"user": {"id": str(account_id)},
}
)
return {"data": {"setUserPlaces": {"id": "ok"}}}
# place(filters:{member_ids/member_id/user_ids}) -> results[{id,members{...}}]
if "place(" in q and "filters" in q:
member_ids = []
if isinstance(vars_, dict):
member_ids = vars_.get("member_ids") or vars_.get("user_ids") or []
mid = vars_.get("member_id")
if mid:
member_ids = [mid]
rows = []
for pid, members in (state.get("members_by_place") or {}).items():
if member_ids:
found = False
for m in members:
u = (m or {}).get("user") if isinstance(m, dict) else None
if isinstance(u, dict) and u.get("id") in member_ids:
found = True
break
if not found:
continue
rows.append({"id": pid, "members": members})
return {"data": {"place": {"results": rows}}}
# 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/deleteTicket etc -> True
if "deleteTicket" in q:
return {"data": {"deleteTicket": 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("SUBSCRIBE_BUNDLE_MOCKS") in {"1", "true", "True"}:
from worklib.subscribe_bundle_graphql_mock import execute_subscribe_bundle_mock_graphql
data = execute_subscribe_bundle_mock_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
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:
ctx = _build_ssl_context(graphql_url)
with urllib.request.urlopen(req, timeout=timeout_s, context=ctx) 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