manifest-api 0.1.0__py3-none-any.whl
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.
- manifest_api/__init__.py +28 -0
- manifest_api/client.py +237 -0
- manifest_api-0.1.0.dist-info/METADATA +112 -0
- manifest_api-0.1.0.dist-info/RECORD +5 -0
- manifest_api-0.1.0.dist-info/WHEEL +4 -0
manifest_api/__init__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""manifest-api — Python SDK for the Manifest API."""
|
|
2
|
+
|
|
3
|
+
from .client import (
|
|
4
|
+
Action,
|
|
5
|
+
APIError,
|
|
6
|
+
AsyncManifestClient,
|
|
7
|
+
AuthenticationError,
|
|
8
|
+
Manifest,
|
|
9
|
+
ManifestClient,
|
|
10
|
+
ManifestError,
|
|
11
|
+
NavLink,
|
|
12
|
+
RateLimitError,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__version__ = "0.1.0"
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"__version__",
|
|
19
|
+
"ManifestClient",
|
|
20
|
+
"AsyncManifestClient",
|
|
21
|
+
"Manifest",
|
|
22
|
+
"Action",
|
|
23
|
+
"NavLink",
|
|
24
|
+
"ManifestError",
|
|
25
|
+
"AuthenticationError",
|
|
26
|
+
"RateLimitError",
|
|
27
|
+
"APIError",
|
|
28
|
+
]
|
manifest_api/client.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""Manifest API client — sync and async."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import List, Optional
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
_BASE_URL = "https://manifest.omfang.io"
|
|
12
|
+
_DEFAULT_TIMEOUT = 30.0
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
# Exceptions
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
class ManifestError(Exception):
|
|
20
|
+
"""Base exception for all Manifest API errors."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AuthenticationError(ManifestError):
|
|
24
|
+
"""Missing or invalid API key."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class RateLimitError(ManifestError):
|
|
28
|
+
"""Rate limit exceeded (429)."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class APIError(ManifestError):
|
|
32
|
+
"""Unexpected API error (5xx or other)."""
|
|
33
|
+
def __init__(self, message: str, status_code: int) -> None:
|
|
34
|
+
super().__init__(message)
|
|
35
|
+
self.status_code = status_code
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
# Data models
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class Action:
|
|
44
|
+
id: str
|
|
45
|
+
label: str
|
|
46
|
+
type: str
|
|
47
|
+
description: str
|
|
48
|
+
required: bool
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def _from_dict(cls, d: dict) -> "Action":
|
|
52
|
+
return cls(
|
|
53
|
+
id=d["id"],
|
|
54
|
+
label=d["label"],
|
|
55
|
+
type=d["type"],
|
|
56
|
+
description=d.get("description", ""),
|
|
57
|
+
required=d.get("required", False),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class NavLink:
|
|
63
|
+
label: str
|
|
64
|
+
url: str
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def _from_dict(cls, d: dict) -> "NavLink":
|
|
68
|
+
return cls(label=d["label"], url=d["url"])
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class Manifest:
|
|
73
|
+
url: str
|
|
74
|
+
authenticated_user: Optional[str]
|
|
75
|
+
current_page_state: str
|
|
76
|
+
actions: List[Action] = field(default_factory=list)
|
|
77
|
+
navigation: List[NavLink] = field(default_factory=list)
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def _from_dict(cls, d: dict) -> "Manifest":
|
|
81
|
+
return cls(
|
|
82
|
+
url=d["url"],
|
|
83
|
+
authenticated_user=d.get("authenticated_user"),
|
|
84
|
+
current_page_state=d.get("current_page_state", ""),
|
|
85
|
+
actions=[Action._from_dict(a) for a in d.get("actions", [])],
|
|
86
|
+
navigation=[NavLink._from_dict(n) for n in d.get("navigation", [])],
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def action(self, id: str) -> Optional[Action]:
|
|
90
|
+
"""Return the action with the given id, or None."""
|
|
91
|
+
return next((a for a in self.actions if a.id == id), None)
|
|
92
|
+
|
|
93
|
+
def actions_of_type(self, type: str) -> List[Action]:
|
|
94
|
+
"""Return all actions matching the given type."""
|
|
95
|
+
return [a for a in self.actions if a.type == type]
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def required_actions(self) -> List[Action]:
|
|
99
|
+
"""Return all actions where required=True."""
|
|
100
|
+
return [a for a in self.actions if a.required]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
# Shared helpers
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
def _resolve_api_key(api_key: Optional[str]) -> str:
|
|
108
|
+
key = api_key or os.environ.get("MANIFEST_API_KEY", "")
|
|
109
|
+
if not key:
|
|
110
|
+
raise AuthenticationError(
|
|
111
|
+
"No API key provided. Pass api_key= or set MANIFEST_API_KEY."
|
|
112
|
+
)
|
|
113
|
+
return key
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _headers(api_key: str) -> dict:
|
|
117
|
+
return {"X-API-Key": api_key, "Content-Type": "application/json"}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _raise_for_status(response: httpx.Response) -> None:
|
|
121
|
+
if response.status_code == 401:
|
|
122
|
+
raise AuthenticationError("Invalid or missing API key (401).")
|
|
123
|
+
if response.status_code == 429:
|
|
124
|
+
raise RateLimitError("Rate limit exceeded (429). Slow down requests.")
|
|
125
|
+
if response.status_code >= 500:
|
|
126
|
+
raise APIError(
|
|
127
|
+
f"Server error: {response.text}", status_code=response.status_code
|
|
128
|
+
)
|
|
129
|
+
if response.status_code >= 400:
|
|
130
|
+
raise APIError(
|
|
131
|
+
f"Client error {response.status_code}: {response.text}",
|
|
132
|
+
status_code=response.status_code,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ---------------------------------------------------------------------------
|
|
137
|
+
# Sync client
|
|
138
|
+
# ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
class ManifestClient:
|
|
141
|
+
"""Synchronous Manifest API client."""
|
|
142
|
+
|
|
143
|
+
def __init__(
|
|
144
|
+
self,
|
|
145
|
+
api_key: Optional[str] = None,
|
|
146
|
+
base_url: str = _BASE_URL,
|
|
147
|
+
timeout: float = _DEFAULT_TIMEOUT,
|
|
148
|
+
) -> None:
|
|
149
|
+
self._api_key = _resolve_api_key(api_key)
|
|
150
|
+
self._base_url = base_url.rstrip("/")
|
|
151
|
+
self._client = httpx.Client(timeout=timeout)
|
|
152
|
+
|
|
153
|
+
def get(self, url: str) -> Manifest:
|
|
154
|
+
"""Fetch an action manifest for the given URL."""
|
|
155
|
+
response = self._client.post(
|
|
156
|
+
f"{self._base_url}/manifest",
|
|
157
|
+
json={"url": url},
|
|
158
|
+
headers=_headers(self._api_key),
|
|
159
|
+
)
|
|
160
|
+
_raise_for_status(response)
|
|
161
|
+
return Manifest._from_dict(response.json())
|
|
162
|
+
|
|
163
|
+
def health(self) -> dict:
|
|
164
|
+
"""Check API health. Returns the raw JSON response."""
|
|
165
|
+
response = self._client.get(f"{self._base_url}/health")
|
|
166
|
+
_raise_for_status(response)
|
|
167
|
+
return response.json()
|
|
168
|
+
|
|
169
|
+
def session_valid(self) -> bool:
|
|
170
|
+
"""Return True if the current session cookie is still valid."""
|
|
171
|
+
response = self._client.get(
|
|
172
|
+
f"{self._base_url}/session-status",
|
|
173
|
+
headers=_headers(self._api_key),
|
|
174
|
+
)
|
|
175
|
+
_raise_for_status(response)
|
|
176
|
+
return response.json().get("valid", False)
|
|
177
|
+
|
|
178
|
+
def close(self) -> None:
|
|
179
|
+
self._client.close()
|
|
180
|
+
|
|
181
|
+
def __enter__(self) -> "ManifestClient":
|
|
182
|
+
return self
|
|
183
|
+
|
|
184
|
+
def __exit__(self, *_: object) -> None:
|
|
185
|
+
self.close()
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# ---------------------------------------------------------------------------
|
|
189
|
+
# Async client
|
|
190
|
+
# ---------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
class AsyncManifestClient:
|
|
193
|
+
"""Asynchronous Manifest API client."""
|
|
194
|
+
|
|
195
|
+
def __init__(
|
|
196
|
+
self,
|
|
197
|
+
api_key: Optional[str] = None,
|
|
198
|
+
base_url: str = _BASE_URL,
|
|
199
|
+
timeout: float = _DEFAULT_TIMEOUT,
|
|
200
|
+
) -> None:
|
|
201
|
+
self._api_key = _resolve_api_key(api_key)
|
|
202
|
+
self._base_url = base_url.rstrip("/")
|
|
203
|
+
self._client = httpx.AsyncClient(timeout=timeout)
|
|
204
|
+
|
|
205
|
+
async def get(self, url: str) -> Manifest:
|
|
206
|
+
"""Fetch an action manifest for the given URL."""
|
|
207
|
+
response = await self._client.post(
|
|
208
|
+
f"{self._base_url}/manifest",
|
|
209
|
+
json={"url": url},
|
|
210
|
+
headers=_headers(self._api_key),
|
|
211
|
+
)
|
|
212
|
+
_raise_for_status(response)
|
|
213
|
+
return Manifest._from_dict(response.json())
|
|
214
|
+
|
|
215
|
+
async def health(self) -> dict:
|
|
216
|
+
"""Check API health. Returns the raw JSON response."""
|
|
217
|
+
response = await self._client.get(f"{self._base_url}/health")
|
|
218
|
+
_raise_for_status(response)
|
|
219
|
+
return response.json()
|
|
220
|
+
|
|
221
|
+
async def session_valid(self) -> bool:
|
|
222
|
+
"""Return True if the current session cookie is still valid."""
|
|
223
|
+
response = await self._client.get(
|
|
224
|
+
f"{self._base_url}/session-status",
|
|
225
|
+
headers=_headers(self._api_key),
|
|
226
|
+
)
|
|
227
|
+
_raise_for_status(response)
|
|
228
|
+
return response.json().get("valid", False)
|
|
229
|
+
|
|
230
|
+
async def close(self) -> None:
|
|
231
|
+
await self._client.aclose()
|
|
232
|
+
|
|
233
|
+
async def __aenter__(self) -> "AsyncManifestClient":
|
|
234
|
+
return self
|
|
235
|
+
|
|
236
|
+
async def __aexit__(self, *_: object) -> None:
|
|
237
|
+
await self.close()
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: manifest-api
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the Manifest API — structured action manifests for AI agents
|
|
5
|
+
Project-URL: Homepage, https://omfang.io/docs
|
|
6
|
+
Project-URL: Repository, https://github.com/omfang/manifest-api
|
|
7
|
+
Project-URL: Bug Tracker, https://github.com/omfang/manifest-api/issues
|
|
8
|
+
Author-email: Omfang AB <hello@omfang.io>
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: agents,ai,automation,browser,manifest,web
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Requires-Dist: httpx>=0.27
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# manifest-api
|
|
32
|
+
|
|
33
|
+
Python SDK for the [Manifest API](https://omfang.io/docs) — extracts structured action manifests from web pages so AI agents know *what they can do*, not just what's on screen.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install manifest-api
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quickstart
|
|
42
|
+
|
|
43
|
+
### Sync
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from manifest_api import ManifestClient
|
|
47
|
+
|
|
48
|
+
client = ManifestClient(api_key="your-key") # or set MANIFEST_API_KEY env var
|
|
49
|
+
manifest = client.get("https://example.com")
|
|
50
|
+
|
|
51
|
+
print(manifest.current_page_state)
|
|
52
|
+
print(manifest.actions)
|
|
53
|
+
|
|
54
|
+
# Convenience helpers
|
|
55
|
+
action = manifest.action("submit-form")
|
|
56
|
+
inputs = manifest.actions_of_type("input")
|
|
57
|
+
required = manifest.required_actions
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Async
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
import asyncio
|
|
64
|
+
from manifest_api import AsyncManifestClient
|
|
65
|
+
|
|
66
|
+
async def main():
|
|
67
|
+
async with AsyncManifestClient(api_key="your-key") as client:
|
|
68
|
+
manifest = await client.get("https://example.com")
|
|
69
|
+
print(manifest.current_page_state)
|
|
70
|
+
|
|
71
|
+
asyncio.run(main())
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## All methods
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
# Both ManifestClient and AsyncManifestClient expose:
|
|
78
|
+
manifest = client.get("https://example.com") # POST /manifest → Manifest
|
|
79
|
+
health = client.health() # GET /health → dict
|
|
80
|
+
valid = client.session_valid() # GET /session-status → bool
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Manifest helpers
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
manifest.action("id") # → Action | None
|
|
87
|
+
manifest.actions_of_type("input") # → list[Action]
|
|
88
|
+
manifest.required_actions # → list[Action]
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Action types
|
|
92
|
+
|
|
93
|
+
`button` · `input` · `textarea` · `select` · `checkbox` · `radio` · `other`
|
|
94
|
+
|
|
95
|
+
## Error handling
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from manifest_api import AuthenticationError, RateLimitError, APIError
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
manifest = client.get("https://example.com")
|
|
102
|
+
except AuthenticationError:
|
|
103
|
+
print("Check your API key")
|
|
104
|
+
except RateLimitError:
|
|
105
|
+
print("Slow down — rate limit hit")
|
|
106
|
+
except APIError as e:
|
|
107
|
+
print(f"Server error {e.status_code}")
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Docs
|
|
111
|
+
|
|
112
|
+
[https://omfang.io/docs](https://omfang.io/docs)
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
manifest_api/__init__.py,sha256=iXoIEdeApwl1H7KCSo4VoVBGPK-8IyL7UjxBPkNqmcg,480
|
|
2
|
+
manifest_api/client.py,sha256=MpTr-dxBcWlmFKfoVKZAQ-pk2sfUJSRj5Q-543ZWyuc,7323
|
|
3
|
+
manifest_api-0.1.0.dist-info/METADATA,sha256=7wEIulFMPCYjxVAKHrcIwg5AU_3pJNVb-GCqstqvys8,3233
|
|
4
|
+
manifest_api-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
5
|
+
manifest_api-0.1.0.dist-info/RECORD,,
|