5 Commits
Author SHA1 Message Date
ZacharyZhang-NY 4a05ba80e1 docs: add AGit pull request instructions 2026-10-09 19:46:40 +00:00
雷电芽衣 4f68bf0f9a chore(python): release 2.1.1
2.1.0 was tagged but never published. This release carries the
deep-health 503 handling, the restored issue_token aliases, the rotation
grace deadline, and agent-id path encoding.

Claude-Session: https://claude.ai/code/session_01Agoc9bRVE9KL7mmWtey9Vq
2026-09-03 21:46:48 -04:00
雷电芽衣 11a152bab8 fix: expose the rotation grace deadline and encode agent ids in paths
Add previous_token_grace_until to RotateApiTokenResponse and
percent-encode the agent id path segment in both clients.

Claude-Session: https://claude.ai/code/session_01Agoc9bRVE9KL7mmWtey9Vq
2026-09-03 20:12:50 -04:00
雷电芽衣 7b3ea2922c fix: restore the deprecated issue_token aliases
Published 2.0.2 exposed ElydoraClient.issue_token(),
AsyncElydoraClient.issue_token(), and the IssueTokenResponse type alias.
The audit removed them, which breaks 2.0.x callers on a minor upgrade.
Restore all three, now raising DeprecationWarning.

Claude-Session: https://claude.ai/code/session_01Agoc9bRVE9KL7mmWtey9Vq
2026-09-03 19:55:45 -04:00
雷电芽衣 b438ead6ae fix: return the degraded deep-health report instead of raising
The API answers GET /v1/health/deep with 503 and a full report when a
dependency is down. _handle_response raised for every status at or above
400, so deep_health() raised exactly when callers need the dependency
details. Both the sync and async clients now accept 503 for this endpoint.

Claude-Session: https://claude.ai/code/session_01Agoc9bRVE9KL7mmWtey9Vq
2026-09-03 19:36:25 -04:00
6 changed files with 65 additions and 17 deletions

No files matched your search

+15
View File
@@ -0,0 +1,15 @@
# Contributing
## Opening a pull request on git.zacharyzhang.com
Forking is disabled on this server. Open pull requests with AGit instead:
1. Register an account at https://git.zacharyzhang.com
2. Clone, commit your changes, then push to a `refs/for/<target-branch>` ref:
```bash
git clone https://git.zacharyzhang.com/Elydora-Infrastructure/Elydora-Python-SDK.git
git push origin HEAD:refs/for/main -o topic=<short-name> -o title="<PR title>" -o description="<PR body>"
```
3. To update the pull request, push again with the same `topic`.
+2
View File
@@ -68,6 +68,7 @@ from .types import (
AuthLoginResponse, AuthLoginResponse,
GetMeResponse, GetMeResponse,
IssueApiTokenResponse, IssueApiTokenResponse,
IssueTokenResponse,
RotateApiTokenResponse, RotateApiTokenResponse,
HealthResponse, HealthResponse,
DeepHealthResponse, DeepHealthResponse,
@@ -142,6 +143,7 @@ __all__ = [
"AuthLoginResponse", "AuthLoginResponse",
"GetMeResponse", "GetMeResponse",
"IssueApiTokenResponse", "IssueApiTokenResponse",
"IssueTokenResponse",
"RotateApiTokenResponse", "RotateApiTokenResponse",
"HealthResponse", "HealthResponse",
"DeepHealthResponse", "DeepHealthResponse",
+1 -1
View File
@@ -1,3 +1,3 @@
"""Canonical Elydora package version.""" """Canonical Elydora package version."""
__version__ = "2.1.0" __version__ = "2.1.1"
+21 -8
View File
@@ -3,6 +3,7 @@
from __future__ import annotations from __future__ import annotations
import warnings import warnings
from urllib.parse import quote
from typing import Any, Dict, List, Optional, Union from typing import Any, Dict, List, Optional, Union
import aiohttp import aiohttp
@@ -130,9 +131,11 @@ class AsyncElydoraClient:
headers=build_headers(self.token), headers=build_headers(self.token),
) )
async def _get_public(self, path: str) -> Any: async def _get_public(self, path: str, *, accept_status: Optional[int] = None) -> Any:
session = await self._get_session() session = await self._get_session()
async with session.get(f"{self.base_url}{path}", timeout=TIMEOUT) as response: async with session.get(f"{self.base_url}{path}", timeout=TIMEOUT) as response:
if accept_status is not None and response.status == accept_status:
return await response.json()
return await self._handle_response(response) return await self._handle_response(response)
@staticmethod @staticmethod
@@ -193,6 +196,15 @@ class AsyncElydoraClient:
"POST", "/v1/auth/token", json_body=api_token_body(ttl_seconds) "POST", "/v1/auth/token", json_body=api_token_body(ttl_seconds)
) )
async def issue_token(self, ttl_seconds: Optional[int] = None) -> IssueApiTokenResponse:
"""Deprecated alias for issue_api_token()."""
warnings.warn(
"AsyncElydoraClient.issue_token() is deprecated. Use issue_api_token().",
DeprecationWarning,
stacklevel=2,
)
return await self.issue_api_token(ttl_seconds=ttl_seconds)
async def rotate_api_token(self) -> RotateApiTokenResponse: async def rotate_api_token(self) -> RotateApiTokenResponse:
return await self._request("POST", "/v1/auth/rotate", json_body={}) return await self._request("POST", "/v1/auth/rotate", json_body={})
@@ -201,19 +213,19 @@ class AsyncElydoraClient:
return await self._request("POST", "/v1/agents/register", json_body=request) return await self._request("POST", "/v1/agents/register", json_body=request)
async def get_agent(self, agent_id: str) -> GetAgentResponse: async def get_agent(self, agent_id: str) -> GetAgentResponse:
return await self._request("GET", f"/v1/agents/{agent_id}") return await self._request("GET", f"/v1/agents/{quote(agent_id, safe='')}")
async def list_agents(self) -> ListAgentsResponse: async def list_agents(self) -> ListAgentsResponse:
return await self._request("GET", "/v1/agents") return await self._request("GET", "/v1/agents")
async def freeze_agent(self, agent_id: str, reason: str) -> FreezeAgentResponse: async def freeze_agent(self, agent_id: str, reason: str) -> FreezeAgentResponse:
return await self._request( return await self._request(
"POST", f"/v1/agents/{agent_id}/freeze", json_body={"reason": reason} "POST", f"/v1/agents/{quote(agent_id, safe='')}/freeze", json_body={"reason": reason}
) )
async def unfreeze_agent(self, agent_id: str, reason: str) -> UnfreezeAgentResponse: async def unfreeze_agent(self, agent_id: str, reason: str) -> UnfreezeAgentResponse:
return await self._request( return await self._request(
"POST", f"/v1/agents/{agent_id}/unfreeze", json_body={"reason": reason} "POST", f"/v1/agents/{quote(agent_id, safe='')}/unfreeze", json_body={"reason": reason}
) )
async def update_agent( async def update_agent(
@@ -221,15 +233,15 @@ class AsyncElydoraClient:
) -> UpdateAgentResponse: ) -> UpdateAgentResponse:
require_integration_type(integration_type) require_integration_type(integration_type)
return await self._request( return await self._request(
"PATCH", f"/v1/agents/{agent_id}", json_body={"integration_type": integration_type} "PATCH", f"/v1/agents/{quote(agent_id, safe='')}", json_body={"integration_type": integration_type}
) )
async def delete_agent(self, agent_id: str) -> DeleteAgentResponse: async def delete_agent(self, agent_id: str) -> DeleteAgentResponse:
return await self._request("DELETE", f"/v1/agents/{agent_id}") return await self._request("DELETE", f"/v1/agents/{quote(agent_id, safe='')}")
async def revoke_key(self, agent_id: str, kid: str, reason: str) -> None: async def revoke_key(self, agent_id: str, kid: str, reason: str) -> None:
await self._request( await self._request(
"POST", f"/v1/agents/{agent_id}/revoke", json_body={"kid": kid, "reason": reason} "POST", f"/v1/agents/{quote(agent_id, safe='')}/revoke", json_body={"kid": kid, "reason": reason}
) )
def create_operation( def create_operation(
@@ -349,4 +361,5 @@ class AsyncElydoraClient:
return await self._get_public("/v1/health") return await self._get_public("/v1/health")
async def deep_health(self) -> DeepHealthResponse: async def deep_health(self) -> DeepHealthResponse:
return await self._get_public("/v1/health/deep") """503 carries the degraded report, not an error."""
return await self._get_public("/v1/health/deep", accept_status=503)
+21 -8
View File
@@ -3,6 +3,7 @@
from __future__ import annotations from __future__ import annotations
import warnings import warnings
from urllib.parse import quote
from typing import Any, Dict, List, Optional, Union from typing import Any, Dict, List, Optional, Union
import requests import requests
@@ -119,8 +120,10 @@ class ElydoraClient:
headers=build_headers(self.token), headers=build_headers(self.token),
) )
def _get_public(self, path: str) -> Any: def _get_public(self, path: str, *, accept_status: Optional[int] = None) -> Any:
response = self._session.get(f"{self.base_url}{path}", timeout=REQUEST_TIMEOUT_SECONDS) response = self._session.get(f"{self.base_url}{path}", timeout=REQUEST_TIMEOUT_SECONDS)
if accept_status is not None and response.status_code == accept_status:
return response.json()
return self._handle_response(response) return self._handle_response(response)
@staticmethod @staticmethod
@@ -179,6 +182,15 @@ class ElydoraClient:
def issue_api_token(self, ttl_seconds: Optional[int] = None) -> IssueApiTokenResponse: def issue_api_token(self, ttl_seconds: Optional[int] = None) -> IssueApiTokenResponse:
return self._request("POST", "/v1/auth/token", json_body=api_token_body(ttl_seconds)) return self._request("POST", "/v1/auth/token", json_body=api_token_body(ttl_seconds))
def issue_token(self, ttl_seconds: Optional[int] = None) -> IssueApiTokenResponse:
"""Deprecated alias for issue_api_token()."""
warnings.warn(
"ElydoraClient.issue_token() is deprecated. Use issue_api_token().",
DeprecationWarning,
stacklevel=2,
)
return self.issue_api_token(ttl_seconds=ttl_seconds)
def rotate_api_token(self) -> RotateApiTokenResponse: def rotate_api_token(self) -> RotateApiTokenResponse:
return self._request("POST", "/v1/auth/rotate", json_body={}) return self._request("POST", "/v1/auth/rotate", json_body={})
@@ -187,29 +199,29 @@ class ElydoraClient:
return self._request("POST", "/v1/agents/register", json_body=request) return self._request("POST", "/v1/agents/register", json_body=request)
def get_agent(self, agent_id: str) -> GetAgentResponse: def get_agent(self, agent_id: str) -> GetAgentResponse:
return self._request("GET", f"/v1/agents/{agent_id}") return self._request("GET", f"/v1/agents/{quote(agent_id, safe='')}")
def list_agents(self) -> ListAgentsResponse: def list_agents(self) -> ListAgentsResponse:
return self._request("GET", "/v1/agents") return self._request("GET", "/v1/agents")
def freeze_agent(self, agent_id: str, reason: str) -> FreezeAgentResponse: def freeze_agent(self, agent_id: str, reason: str) -> FreezeAgentResponse:
return self._request("POST", f"/v1/agents/{agent_id}/freeze", json_body={"reason": reason}) return self._request("POST", f"/v1/agents/{quote(agent_id, safe='')}/freeze", json_body={"reason": reason})
def unfreeze_agent(self, agent_id: str, reason: str) -> UnfreezeAgentResponse: def unfreeze_agent(self, agent_id: str, reason: str) -> UnfreezeAgentResponse:
return self._request("POST", f"/v1/agents/{agent_id}/unfreeze", json_body={"reason": reason}) return self._request("POST", f"/v1/agents/{quote(agent_id, safe='')}/unfreeze", json_body={"reason": reason})
def update_agent(self, agent_id: str, integration_type: IntegrationType) -> UpdateAgentResponse: def update_agent(self, agent_id: str, integration_type: IntegrationType) -> UpdateAgentResponse:
require_integration_type(integration_type) require_integration_type(integration_type)
return self._request( return self._request(
"PATCH", f"/v1/agents/{agent_id}", json_body={"integration_type": integration_type} "PATCH", f"/v1/agents/{quote(agent_id, safe='')}", json_body={"integration_type": integration_type}
) )
def delete_agent(self, agent_id: str) -> DeleteAgentResponse: def delete_agent(self, agent_id: str) -> DeleteAgentResponse:
return self._request("DELETE", f"/v1/agents/{agent_id}") return self._request("DELETE", f"/v1/agents/{quote(agent_id, safe='')}")
def revoke_key(self, agent_id: str, kid: str, reason: str) -> None: def revoke_key(self, agent_id: str, kid: str, reason: str) -> None:
self._request( self._request(
"POST", f"/v1/agents/{agent_id}/revoke", json_body={"kid": kid, "reason": reason} "POST", f"/v1/agents/{quote(agent_id, safe='')}/revoke", json_body={"kid": kid, "reason": reason}
) )
def create_operation( def create_operation(
@@ -324,4 +336,5 @@ class ElydoraClient:
return self._get_public("/v1/health") return self._get_public("/v1/health")
def deep_health(self) -> DeepHealthResponse: def deep_health(self) -> DeepHealthResponse:
return self._get_public("/v1/health/deep") """503 carries the degraded report, not an error."""
return self._get_public("/v1/health/deep", accept_status=503)
+5
View File
@@ -351,10 +351,15 @@ class IssueApiTokenResponse(TypedDict):
token_id: str token_id: str
# Deprecated alias for IssueApiTokenResponse.
IssueTokenResponse = IssueApiTokenResponse
class RotateApiTokenResponse(TypedDict): class RotateApiTokenResponse(TypedDict):
token: str token: str
expires_at: Optional[int] expires_at: Optional[int]
token_id: str token_id: str
previous_token_grace_until: int
class HealthResponse(TypedDict): class HealthResponse(TypedDict):