asiflow-hyper 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,37 @@
1
+ Metadata-Version: 2.4
2
+ Name: asiflow-hyper
3
+ Version: 1.0.0
4
+ Summary: Python client for the Hyper Agent API (OAuth 2.1, REST v1): delegate objectives, run and verify work, read evidence.
5
+ Author: ASIFlow
6
+ License: MIT
7
+ Project-URL: Homepage, https://hyper.asiflow.ai
8
+ Project-URL: Documentation, https://hyper.asiflow.ai/docs/platform/agent-api
9
+ Project-URL: OpenAPI, https://api.hyper.asiflow.ai/openapi.json
10
+ Keywords: hyper,agent,ai software engineer,oauth,mcp
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+ Requires-Dist: httpx>=0.25
17
+
18
+ # asiflow-hyper
19
+
20
+ Python client for the [Hyper Agent API](https://hyper.asiflow.ai/docs/platform/agent-api): delegate a software objective to Hyper, run and verify the work, read the evidence. Thin and honest: every method is one documented REST call (`https://api.hyper.asiflow.ai/openapi.json`); errors are the API's RFC 9457 problems.
21
+
22
+ ```python
23
+ from asiflow_hyper import HyperClient, device_login
24
+
25
+ token = device_login(client_name="my-agent", scope="objectives:read objectives:write runs:write evidence:read deploy:verify offline_access")
26
+ hyper = HyperClient(token=token.access_token)
27
+
28
+ job = hyper.create_objective(name="Bakery site", objective="A one-page site for Nour's bakery with a contact form.")
29
+ job = hyper.wait_job(job["id"])
30
+ run = hyper.start_run(job["result"]["project_id"], policy={"budget_cap_usd": 10})
31
+ run = hyper.wait_run(run["result"]["run_id"], on_needs_you=lambda card: "approved")
32
+ for attempt in run["attempts"]:
33
+ if attempt["status"] == "verified":
34
+ print(hyper.attempt_evidence(attempt["id"]))
35
+ ```
36
+
37
+ The `agent-interop-e2e` lane runs this client against production on every Hyper deploy.
@@ -0,0 +1,20 @@
1
+ # asiflow-hyper
2
+
3
+ Python client for the [Hyper Agent API](https://hyper.asiflow.ai/docs/platform/agent-api): delegate a software objective to Hyper, run and verify the work, read the evidence. Thin and honest: every method is one documented REST call (`https://api.hyper.asiflow.ai/openapi.json`); errors are the API's RFC 9457 problems.
4
+
5
+ ```python
6
+ from asiflow_hyper import HyperClient, device_login
7
+
8
+ token = device_login(client_name="my-agent", scope="objectives:read objectives:write runs:write evidence:read deploy:verify offline_access")
9
+ hyper = HyperClient(token=token.access_token)
10
+
11
+ job = hyper.create_objective(name="Bakery site", objective="A one-page site for Nour's bakery with a contact form.")
12
+ job = hyper.wait_job(job["id"])
13
+ run = hyper.start_run(job["result"]["project_id"], policy={"budget_cap_usd": 10})
14
+ run = hyper.wait_run(run["result"]["run_id"], on_needs_you=lambda card: "approved")
15
+ for attempt in run["attempts"]:
16
+ if attempt["status"] == "verified":
17
+ print(hyper.attempt_evidence(attempt["id"]))
18
+ ```
19
+
20
+ The `agent-interop-e2e` lane runs this client against production on every Hyper deploy.
@@ -0,0 +1,6 @@
1
+ """asiflow-hyper — Python client for the Hyper Agent API."""
2
+ from .auth import DeviceToken, TokenSet, device_login, jwt_bearer_login, refresh, register_client
3
+ from .client import HyperClient, HyperError
4
+
5
+ __all__ = ["HyperClient", "HyperError", "DeviceToken", "TokenSet", "device_login", "jwt_bearer_login", "refresh", "register_client"]
6
+ __version__ = "1.0.0"
@@ -0,0 +1,91 @@
1
+ """OAuth 2.1 helpers for the Hyper authorization server (RFC 7591 registration,
2
+ RFC 8628 device grant, RFC 7523 jwt-bearer, rotating refresh). Human-facing:
3
+ ``device_login`` prints the code the person types at hyper.asiflow.ai."""
4
+ from __future__ import annotations
5
+
6
+ import time
7
+ from dataclasses import dataclass
8
+ from typing import Callable, Dict, List, Optional
9
+
10
+ import httpx
11
+
12
+ DEFAULT_BASE = "https://api.hyper.asiflow.ai"
13
+ DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code"
14
+ JWT_BEARER_GRANT = "urn:ietf:params:oauth:grant-type:jwt-bearer"
15
+
16
+
17
+ @dataclass
18
+ class TokenSet:
19
+ access_token: str
20
+ refresh_token: Optional[str]
21
+ expires_in: int
22
+ scope: str
23
+ token_type: str = "Bearer"
24
+
25
+
26
+ @dataclass
27
+ class DeviceToken(TokenSet):
28
+ client_id: str = ""
29
+
30
+
31
+ class AuthError(Exception):
32
+ pass
33
+
34
+
35
+ def _post_form(base: str, path: str, data: Dict[str, str], transport: Optional[httpx.BaseTransport] = None) -> Dict:
36
+ with httpx.Client(base_url=base.rstrip("/"), timeout=30.0, transport=transport) as c:
37
+ r = c.post(path, data=data, headers={"Accept": "application/json"})
38
+ body = r.json() if r.content else {}
39
+ if r.status_code >= 400:
40
+ raise AuthError(f"{r.status_code} {body.get('error')}: {body.get('error_description') or body}")
41
+ return body
42
+
43
+
44
+ def register_client(*, client_name: str, scope: str, grant_types: Optional[List[str]] = None,
45
+ redirect_uris: Optional[List[str]] = None, base: str = DEFAULT_BASE, transport: Optional[httpx.BaseTransport] = None) -> Dict:
46
+ """RFC 7591 dynamic registration. Unverified clients receive read-only scopes until reviewed."""
47
+ body = {"client_name": client_name, "scope": scope, "grant_types": grant_types or [DEVICE_GRANT, "refresh_token"],
48
+ "redirect_uris": redirect_uris or ["https://hyper.asiflow.ai/oauth/callback"]}
49
+ with httpx.Client(base_url=base.rstrip("/"), timeout=30.0, transport=transport) as c:
50
+ r = c.post("/oauth/register", json=body, headers={"Accept": "application/json"})
51
+ out = r.json() if r.content else {}
52
+ if r.status_code != 201:
53
+ raise AuthError(f"register → {r.status_code} {out}")
54
+ return out
55
+
56
+
57
+ def device_login(*, client_name: str, scope: str, client_id: Optional[str] = None, base: str = DEFAULT_BASE,
58
+ prompt: Callable[[str, str], None] = None, transport: Optional[httpx.BaseTransport] = None, timeout_s: float = 600) -> DeviceToken:
59
+ """Register (unless client_id is given), start the device grant, show the person the
60
+ code, poll until they approve. ``prompt(verification_uri, user_code)`` defaults to print."""
61
+ cid = client_id or register_client(client_name=client_name, scope=scope, base=base, transport=transport)["client_id"]
62
+ dev = _post_form(base, "/oauth/device", {"client_id": cid, "scope": scope}, transport)
63
+ (prompt or (lambda uri, code: print(f"Open {uri} and enter the code {code}")))(dev.get("verification_uri_complete") or dev["verification_uri"], dev["user_code"])
64
+ interval, deadline = float(dev.get("interval") or 5), time.time() + timeout_s
65
+ while time.time() < deadline:
66
+ time.sleep(interval)
67
+ try:
68
+ tok = _post_form(base, "/oauth/token", {"grant_type": DEVICE_GRANT, "client_id": cid, "device_code": dev["device_code"]}, transport)
69
+ except AuthError as exc:
70
+ msg = str(exc)
71
+ if "authorization_pending" in msg:
72
+ continue
73
+ if "slow_down" in msg:
74
+ interval += 5
75
+ continue
76
+ raise
77
+ return DeviceToken(access_token=tok["access_token"], refresh_token=tok.get("refresh_token"), expires_in=int(tok.get("expires_in") or 900),
78
+ scope=str(tok.get("scope") or scope), client_id=cid)
79
+ raise AuthError("the person did not approve the device code in time")
80
+
81
+
82
+ def jwt_bearer_login(*, client_id: str, assertion: str, scope: str, base: str = DEFAULT_BASE, transport: Optional[httpx.BaseTransport] = None) -> TokenSet:
83
+ """RFC 7523: exchange a signed assertion (a key Hyper trusts for this client) for an access token."""
84
+ tok = _post_form(base, "/oauth/token", {"grant_type": JWT_BEARER_GRANT, "client_id": client_id, "assertion": assertion, "scope": scope}, transport)
85
+ return TokenSet(access_token=tok["access_token"], refresh_token=tok.get("refresh_token"), expires_in=int(tok.get("expires_in") or 900), scope=str(tok.get("scope") or scope))
86
+
87
+
88
+ def refresh(*, client_id: str, refresh_token: str, base: str = DEFAULT_BASE, transport: Optional[httpx.BaseTransport] = None) -> TokenSet:
89
+ """Rotating refresh: the old token is invalid after this call; reuse revokes the family."""
90
+ tok = _post_form(base, "/oauth/token", {"grant_type": "refresh_token", "client_id": client_id, "refresh_token": refresh_token}, transport)
91
+ return TokenSet(access_token=tok["access_token"], refresh_token=tok.get("refresh_token"), expires_in=int(tok.get("expires_in") or 900), scope=str(tok.get("scope") or ""))
@@ -0,0 +1,125 @@
1
+ """The REST v1 client. One method per documented call; nothing invented."""
2
+ from __future__ import annotations
3
+
4
+ import time
5
+ import uuid
6
+ from typing import Any, Callable, Dict, List, Optional
7
+
8
+ import httpx
9
+
10
+ DEFAULT_BASE = "https://api.hyper.asiflow.ai"
11
+ TERMINAL_JOB = ("succeeded", "failed", "cancelled")
12
+ TERMINAL_RUN = ("completed", "failed", "stopped")
13
+
14
+
15
+ class HyperError(Exception):
16
+ """An RFC 9457 problem from the API: ``status``, ``type``, ``title``, ``detail``."""
17
+
18
+ def __init__(self, status: int, problem: Dict[str, Any]):
19
+ self.status, self.problem = status, problem
20
+ super().__init__(f"{status} {problem.get('title') or ''}: {problem.get('detail') or problem}")
21
+
22
+
23
+ class HyperClient:
24
+ def __init__(self, token: str, *, base_url: str = DEFAULT_BASE, timeout: float = 30.0, transport: Optional[httpx.BaseTransport] = None,
25
+ user_agent: str = "asiflow-hyper-python/1.0.0"):
26
+ self.base_url = base_url.rstrip("/")
27
+ self._http = httpx.Client(base_url=self.base_url, timeout=timeout, transport=transport,
28
+ headers={"Authorization": f"Bearer {token}", "User-Agent": user_agent, "Accept": "application/json"})
29
+
30
+ # ── transport ──────────────────────────────────────────────────────────
31
+ def _call(self, method: str, path: str, *, json: Any = None, idempotent: bool = False) -> Any:
32
+ headers = {"Idempotency-Key": str(uuid.uuid4())} if idempotent else {}
33
+ r = self._http.request(method, path, json=json, headers=headers)
34
+ if r.status_code >= 400:
35
+ try:
36
+ problem = r.json()
37
+ except ValueError:
38
+ problem = {"title": r.reason_phrase, "detail": r.text[:300]}
39
+ raise HyperError(r.status_code, problem)
40
+ return r.json() if r.content else None
41
+
42
+ def close(self) -> None:
43
+ self._http.close()
44
+
45
+ # ── identity ───────────────────────────────────────────────────────────
46
+ def me(self) -> Dict[str, Any]:
47
+ return self._call("GET", "/v1/me")
48
+
49
+ # ── objectives and jobs ────────────────────────────────────────────────
50
+ def create_objective(self, *, name: str, objective: str, tasks: Optional[List[Dict[str, Any]]] = None,
51
+ autonomy: str = "balanced", repo: Optional[str] = None) -> Dict[str, Any]:
52
+ body: Dict[str, Any] = {"name": name, "objective": objective, "autonomy": autonomy}
53
+ if tasks is not None:
54
+ body["tasks"] = tasks
55
+ if repo:
56
+ body["repo"] = repo
57
+ return self._call("POST", "/v1/objectives", json=body, idempotent=True)
58
+
59
+ def get_job(self, job_id: str) -> Dict[str, Any]:
60
+ return self._call("GET", f"/v1/jobs/{job_id}")
61
+
62
+ def cancel_job(self, job_id: str) -> Dict[str, Any]:
63
+ return self._call("POST", f"/v1/jobs/{job_id}/cancel")
64
+
65
+ def wait_job(self, job_id: str, *, timeout_s: float = 900, interval_s: float = 5.0) -> Dict[str, Any]:
66
+ deadline = time.time() + timeout_s
67
+ while True:
68
+ job = self.get_job(job_id)
69
+ if job.get("terminal") or job.get("status") in TERMINAL_JOB + ("waiting_for_acceptance",):
70
+ return job
71
+ if time.time() > deadline:
72
+ raise TimeoutError(f"job {job_id} still {job.get('status')} after {timeout_s}s")
73
+ time.sleep(interval_s)
74
+
75
+ # ── projects, repos, runs ──────────────────────────────────────────────
76
+ def list_projects(self) -> Dict[str, Any]:
77
+ return self._call("GET", "/v1/projects")
78
+
79
+ def get_project(self, project_id: str) -> Dict[str, Any]:
80
+ return self._call("GET", f"/v1/projects/{project_id}")
81
+
82
+ def connect_repo(self, project_id: str, repo: str) -> Dict[str, Any]:
83
+ return self._call("POST", f"/v1/projects/{project_id}/repo", json={"repo": repo})
84
+
85
+ def start_run(self, project_id: str, *, scope: Optional[Dict[str, Any]] = None, policy: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
86
+ return self._call("POST", f"/v1/projects/{project_id}/runs", json={"scope": scope or {"kind": "project"}, "policy": policy or {}}, idempotent=True)
87
+
88
+ def get_run(self, run_id: str) -> Dict[str, Any]:
89
+ return self._call("GET", f"/v1/runs/{run_id}")
90
+
91
+ def control_run(self, run_id: str, action: str) -> Dict[str, Any]:
92
+ if action not in ("pause", "resume", "stop"):
93
+ raise ValueError("action must be pause, resume or stop")
94
+ return self._call("POST", f"/v1/runs/{run_id}/{action}")
95
+
96
+ def wait_run(self, run_id: str, *, timeout_s: float = 1800, interval_s: float = 10.0,
97
+ on_needs_you: Optional[Callable[[Dict[str, Any]], Any]] = None) -> Dict[str, Any]:
98
+ """Poll until the run ends. When it blocks on a card and ``on_needs_you`` is given,
99
+ the callback returns "approved" | "rejected" | "changes_requested" or (status, note)."""
100
+ deadline = time.time() + timeout_s
101
+ while True:
102
+ run = self.get_run(run_id)
103
+ if run.get("status") in TERMINAL_RUN:
104
+ return run
105
+ if run.get("status") == "blocked" and run.get("needs_you") and on_needs_you is not None:
106
+ for card in run["needs_you"]:
107
+ answer = on_needs_you(card)
108
+ status, note = (answer, None) if isinstance(answer, str) else (answer[0], answer[1])
109
+ self.decide(card["id"], status=status, note=note)
110
+ if time.time() > deadline:
111
+ raise TimeoutError(f"run {run_id} still {run.get('status')} after {timeout_s}s")
112
+ time.sleep(interval_s)
113
+
114
+ # ── approvals and evidence ─────────────────────────────────────────────
115
+ def list_approvals(self, project_id: str) -> Dict[str, Any]:
116
+ return self._call("GET", f"/v1/projects/{project_id}/approvals")
117
+
118
+ def decide(self, approval_id: str, *, status: str, note: Optional[str] = None) -> Dict[str, Any]:
119
+ return self._call("POST", f"/v1/approvals/{approval_id}/decide", json={"status": status, "note": note})
120
+
121
+ def attempt_evidence(self, attempt_id: str) -> Dict[str, Any]:
122
+ return self._call("GET", f"/v1/attempts/{attempt_id}/evidence")
123
+
124
+ def verify_deployment(self, url: str, *, expect_text: Optional[str] = None) -> Dict[str, Any]:
125
+ return self._call("POST", "/v1/verify/deployment", json={"url": url, "expect_text": expect_text})
@@ -0,0 +1,37 @@
1
+ Metadata-Version: 2.4
2
+ Name: asiflow-hyper
3
+ Version: 1.0.0
4
+ Summary: Python client for the Hyper Agent API (OAuth 2.1, REST v1): delegate objectives, run and verify work, read evidence.
5
+ Author: ASIFlow
6
+ License: MIT
7
+ Project-URL: Homepage, https://hyper.asiflow.ai
8
+ Project-URL: Documentation, https://hyper.asiflow.ai/docs/platform/agent-api
9
+ Project-URL: OpenAPI, https://api.hyper.asiflow.ai/openapi.json
10
+ Keywords: hyper,agent,ai software engineer,oauth,mcp
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+ Requires-Dist: httpx>=0.25
17
+
18
+ # asiflow-hyper
19
+
20
+ Python client for the [Hyper Agent API](https://hyper.asiflow.ai/docs/platform/agent-api): delegate a software objective to Hyper, run and verify the work, read the evidence. Thin and honest: every method is one documented REST call (`https://api.hyper.asiflow.ai/openapi.json`); errors are the API's RFC 9457 problems.
21
+
22
+ ```python
23
+ from asiflow_hyper import HyperClient, device_login
24
+
25
+ token = device_login(client_name="my-agent", scope="objectives:read objectives:write runs:write evidence:read deploy:verify offline_access")
26
+ hyper = HyperClient(token=token.access_token)
27
+
28
+ job = hyper.create_objective(name="Bakery site", objective="A one-page site for Nour's bakery with a contact form.")
29
+ job = hyper.wait_job(job["id"])
30
+ run = hyper.start_run(job["result"]["project_id"], policy={"budget_cap_usd": 10})
31
+ run = hyper.wait_run(run["result"]["run_id"], on_needs_you=lambda card: "approved")
32
+ for attempt in run["attempts"]:
33
+ if attempt["status"] == "verified":
34
+ print(hyper.attempt_evidence(attempt["id"]))
35
+ ```
36
+
37
+ The `agent-interop-e2e` lane runs this client against production on every Hyper deploy.
@@ -0,0 +1,10 @@
1
+ README.md
2
+ pyproject.toml
3
+ asiflow_hyper/__init__.py
4
+ asiflow_hyper/auth.py
5
+ asiflow_hyper/client.py
6
+ asiflow_hyper.egg-info/PKG-INFO
7
+ asiflow_hyper.egg-info/SOURCES.txt
8
+ asiflow_hyper.egg-info/dependency_links.txt
9
+ asiflow_hyper.egg-info/requires.txt
10
+ asiflow_hyper.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ httpx>=0.25
@@ -0,0 +1 @@
1
+ asiflow_hyper
@@ -0,0 +1,23 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "asiflow-hyper"
7
+ version = "1.0.0"
8
+ description = "Python client for the Hyper Agent API (OAuth 2.1, REST v1): delegate objectives, run and verify work, read evidence."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.9"
12
+ authors = [{ name = "ASIFlow" }]
13
+ dependencies = ["httpx>=0.25"]
14
+ keywords = ["hyper", "agent", "ai software engineer", "oauth", "mcp"]
15
+ classifiers = ["Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent"]
16
+
17
+ [project.urls]
18
+ Homepage = "https://hyper.asiflow.ai"
19
+ Documentation = "https://hyper.asiflow.ai/docs/platform/agent-api"
20
+ OpenAPI = "https://api.hyper.asiflow.ai/openapi.json"
21
+
22
+ [tool.setuptools.packages.find]
23
+ include = ["asiflow_hyper*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+