Spaces:
Sleeping
Sleeping
File size: 7,323 Bytes
399944f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 | """
Aura API helper utilities to detect paused instances and resume them.
This prevents Neo4j driver failures like:
- socket.gaierror: [Errno -2] Name or service not known
- Cannot resolve address <dbid>.databases.neo4j.io:7687
Those typically happen when AuraDB Free is auto-paused and the bolt hostname
stops resolving.
"""
from __future__ import annotations
import os
import time
from dataclasses import dataclass
from typing import Any, Optional
import requests
import rich
@dataclass(frozen=True)
class AuraCredentials:
"""OAuth client credentials for the Neo4j Aura API."""
client_id: str
client_secret: str
class AuraAPIError(RuntimeError):
"""Raised when Aura API interactions fail."""
class AuraAPI:
"""
Minimal Aura API client for:
- fetching an OAuth bearer token
- reading instance status
- resuming an instance if paused
Docs:
- Base URL: https://api.neo4j.io
- Token endpoint: POST https://api.neo4j.io/oauth/token
"""
def __init__(
self,
creds: AuraCredentials,
*,
base_url: str = "https://api.neo4j.io"
) -> None:
self._creds = creds
self._base_url = base_url.rstrip("/") # remove trailing slash if provided
self._token: Optional[str] = None
self._token_expires_at: float = 0.0
def _get_token(self) -> str:
"""
Fetch and cache a bearer token (client_credentials flow).
The Aura docs specify:
POST https://api.neo4j.io/oauth/token
grant_type=client_credentials
Basic auth: <client_id>:<client_secret>
"""
now = time.time()
if self._token and now < self._token_expires_at - 30:
return self._token
resp = requests.post(
f"{self._base_url}/oauth/token",
auth=(self._creds.client_id, self._creds.client_secret),
headers={"Content-Type": "application/x-www-form-urlencoded"},
data={"grant_type": "client_credentials"},
timeout=30,
)
if resp.status_code >= 400:
raise AuraAPIError(f"Failed to obtain Aura API token: {resp.status_code} {resp.text}")
payload = resp.json()
access_token = payload["access_token"]
expires_in = float(payload.get("expires_in", 3600))
self._token = access_token
self._token_expires_at = now + expires_in
return access_token
def _headers(self) -> dict[str, str]:
return {"Authorization": f"Bearer {self._get_token()}"}
def get_instance(self, instance_id: str) -> dict[str, Any]:
"""
Get instance details (includes status).
NOTE: Endpoint path may evolve across Aura API versions.
If your tenant uses a different path, adjust according to the API spec.
"""
url = f"{self._base_url}/v1/instances/{instance_id}"
resp = requests.get(url, headers=self._headers(), timeout=30)
if resp.status_code >= 400:
raise AuraAPIError(f"Failed to get instance {instance_id}: {resp.status_code} {resp.text}")
return resp.json()
def get_instance_status(self, instance_id: str) -> str:
"""
Get the status of an Aura instance.
Expected values (observed):
- "running"
- "paused"
- "resuming"
- "provisioning"
Returns
-------
str
Lowercased status string.
Raises
------
AuraAPIError
If the response does not contain a status field.
"""
info = self.get_instance(instance_id)
try:
status = info["data"]["status"]
except KeyError as e:
raise AuraAPIError(
f"Unexpected Aura API response shape; missing status.\n"
f"Response keys: {list(info.keys())}\n"
f"Full response: {info}"
) from e
return str(status).lower()
def resume_instance(self, instance_id: str) -> None:
"""
Trigger a resume of a paused instance.
NOTE: Endpoint path may evolve across Aura API versions.
Adjust according to the API spec if needed.
"""
url = f"{self._base_url}/v1/instances/{instance_id}/resume"
resp = requests.post(url, headers=self._headers(), timeout=30)
resp = requests.post(
url,
headers={
**self._headers(),
"Content-Type": "application/json", # needed here
},
json={}, # optional but helps make intent explicit
timeout=30,
)
if resp.status_code >= 400:
raise AuraAPIError(f"Failed to resume instance {instance_id}: {resp.status_code} {resp.text}")
def ensure_running(
self,
instance_id: str,
*,
poll_seconds: float = 5.0, # time to wait between status checks while polling
timeout_seconds: float = 180.0,
verbose: bool = True,
) -> None:
"""
If instance is paused, resume it and wait until status is Running.
This is intended to run BEFORE creating a Neo4j driver session.
"""
start = time.time()
status = self.get_instance_status(instance_id)
if verbose:
rich.print(f"💧 [kbdebugger] Aura instance {instance_id}: status={status!r}")
if "paused" in status:
if verbose:
rich.print(f"⚠️ [kbdebugger] Aura instance {instance_id} is paused → resuming via Aura API...")
self.resume_instance(instance_id)
# Poll until running (or timeout)
while True:
if time.time() - start > timeout_seconds:
raise AuraAPIError(
f"🛑☹️ Timed out waiting for Aura instance {instance_id} to become running "
f"(waited {timeout_seconds}s)."
)
status = self.get_instance_status(instance_id)
if "running" in status:
if verbose:
rich.print(f"🏃💧 [kbdebugger] Aura instance {instance_id} is running.")
return
if verbose:
rich.print(f"[kbdebugger] Aura instance {instance_id} not running yet (status={status!r}); polling...")
time.sleep(poll_seconds)
def ensure_aura_running_from_env(*, verbose: bool = True) -> None:
"""
Convenience entrypoint: uses env vars to ensure the Aura instance is running.
Required env vars:
- AURA_API_CLIENT_ID
- AURA_API_CLIENT_SECRET
- AURA_INSTANCE_ID
"""
client_id = os.getenv("AURA_API_CLIENT_ID", "").strip()
client_secret = os.getenv("AURA_API_CLIENT_SECRET", "").strip()
instance_id = os.getenv("AURA_INSTANCE_ID", "").strip()
if not (client_id and client_secret and instance_id):
# If you want this to be mandatory, raise instead of returning.
if verbose:
rich.print("⚠️ [kbdebugger] Aura auto-resume is not configured; skipping (missing env vars).")
return
api = AuraAPI(
AuraCredentials(client_id=client_id, client_secret=client_secret)
)
api.ensure_running(instance_id, verbose=verbose)
|