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.
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any