openhandle 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,10 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ .mypy_cache/
5
+ .pytest_cache/
6
+ .ruff_cache/
7
+ dist/
8
+ build/
9
+ *.egg-info/
10
+ .DS_Store
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OpenHandle
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,189 @@
1
+ Metadata-Version: 2.5
2
+ Name: openhandle
3
+ Version: 1.0.0
4
+ Summary: Official Python SDK for OpenHandle
5
+ Project-URL: Homepage, https://openhandle.dev
6
+ Project-URL: Documentation, https://openhandle.dev/docs/sdks
7
+ Project-URL: Repository, https://github.com/openhandlehq/openhandle-python
8
+ Project-URL: Issues, https://github.com/openhandlehq/openhandle-python/issues
9
+ Author-email: OpenHandle <support@openhandle.dev>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: api,openhandle,sdk,social-media
13
+ Classifier: Development Status :: 5 - Production/Stable
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: httpx>=0.27
24
+ Requires-Dist: typing-extensions>=4.10
25
+ Description-Content-Type: text/markdown
26
+
27
+ # OpenHandle Python SDK
28
+
29
+ The official Python client for the OpenHandle API.
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ pip install openhandle
35
+ ```
36
+
37
+ The package supports Python 3.10 and newer and ships full type annotations.
38
+
39
+ ## Usage
40
+
41
+ Create a Test key in the [Openhandle dashboard](https://app.openhandle.dev),
42
+ store it as `OPENHANDLE_TEST_KEY`, and create one reusable client:
43
+
44
+ ```python
45
+ import os
46
+
47
+ from openhandle import OpenHandle
48
+
49
+ openhandle = OpenHandle(api_key=os.environ["OPENHANDLE_TEST_KEY"])
50
+ profile = openhandle.instagram.profile("northstar_forge_test")
51
+
52
+ response = profile.get()
53
+ posts = profile.posts.list(freshness="24h")
54
+
55
+ print(response.data["handle"], len(posts.data))
56
+ ```
57
+
58
+ The key selects the environment. `oh_test_` keys return deterministic synthetic
59
+ data with a `$0.000` actual charge; `oh_live_` keys use real public identifiers
60
+ and normal billing. Never expose an API key in client-side code.
61
+
62
+ See the [API reference](https://openhandle.dev/docs/api-reference) for a typed
63
+ SDK example for every operation.
64
+
65
+ ## Resource selection
66
+
67
+ The SDK follows one predictable grammar:
68
+
69
+ ```text
70
+ openhandle.<platform>.<resource>(reference).<subresource>.<operation>(options)
71
+ ```
72
+
73
+ Only terminal operations such as `get`, `list`, `search`, and `fetch` perform
74
+ network requests. Selecting a resource is synchronous and reusable:
75
+
76
+ ```python
77
+ post = openhandle.instagram.post("Db04otPRpRH")
78
+
79
+ response = post.get()
80
+ comments = post.comments.list()
81
+ ```
82
+
83
+ A profile selector accepts a username shorthand or an explicit reference:
84
+
85
+ ```python
86
+ openhandle.instagram.profile("openai")
87
+ openhandle.instagram.profile("https://www.instagram.com/openai/")
88
+ openhandle.instagram.profile(username="12356")
89
+ openhandle.instagram.profile(id="25025320")
90
+ openhandle.instagram.profile(url="https://www.instagram.com/openai/")
91
+ ```
92
+
93
+ A raw string is never treated as a platform ID. `profile("12356")` selects the
94
+ username `12356`; `profile(id="12356")` selects platform ID `12356`. Numeric
95
+ reference values are rejected because platform IDs are opaque strings.
96
+
97
+ Call `openhandle.fetch(url)` when you do not know which resource a supported
98
+ social URL represents.
99
+
100
+ ## Pagination
101
+
102
+ A list or search operation returns one typed page:
103
+
104
+ ```python
105
+ page = openhandle.instagram.profile("northstar_forge_test").posts.list()
106
+
107
+ print(page.data, page.has_next_page, page.next_cursor)
108
+ next_page = page.next()
109
+ ```
110
+
111
+ `items()` iterates lazily across pages, one request per page:
112
+
113
+ ```python
114
+ for post in openhandle.instagram.profile("northstar_forge_test").posts.items():
115
+ print(post["id"])
116
+ ```
117
+
118
+ ## Async client
119
+
120
+ `AsyncOpenHandle` exposes the same resource graph with `async` terminal
121
+ operations:
122
+
123
+ ```python
124
+ import asyncio
125
+ import os
126
+
127
+ from openhandle import AsyncOpenHandle
128
+
129
+
130
+ async def main() -> None:
131
+ async with AsyncOpenHandle(api_key=os.environ["OPENHANDLE_TEST_KEY"]) as openhandle:
132
+ response = await openhandle.instagram.profile("northstar_forge_test").get()
133
+ print(response.data["handle"])
134
+
135
+ async for post in openhandle.instagram.profile("northstar_forge_test").posts.items():
136
+ print(post["id"])
137
+
138
+
139
+ asyncio.run(main())
140
+ ```
141
+
142
+ ## Responses
143
+
144
+ Every response preserves the public envelope. `data` stays typed through the
145
+ generated models in `openhandle.models`, and metadata is available on the
146
+ response object:
147
+
148
+ ```python
149
+ response = openhandle.instagram.profile("northstar_forge_test").get()
150
+
151
+ response.platform # "instagram"
152
+ response.resource # "profile"
153
+ response.captured_at # datetime
154
+ response.source # "live" or "cache"
155
+ response.request_id # stable request identifier for logs and support
156
+ response.billing.cost # authoritative charge as a decimal string
157
+ ```
158
+
159
+ A missing metric is `None`. It is never `0`.
160
+
161
+ ## Errors and retries
162
+
163
+ The SDK raises `OpenHandleError` with the documented fields. Branch on `code`,
164
+ never on `message`:
165
+
166
+ ```python
167
+ from openhandle import OpenHandle, OpenHandleError
168
+
169
+ try:
170
+ response = openhandle.instagram.profile("private_account").get()
171
+ except OpenHandleError as error:
172
+ print(error.code, error.request_id, error.retryable)
173
+ ```
174
+
175
+ Retryable failures are retried automatically with capped exponential backoff
176
+ and `Retry-After` support. Configure the client, or override per request:
177
+
178
+ ```python
179
+ openhandle = OpenHandle(api_key="...", timeout=10.0, max_retries=1)
180
+ openhandle.twitter.profile("openai").get(timeout=5.0, max_retries=0)
181
+ ```
182
+
183
+ Locally invalid references raise `OpenHandleReferenceError` before any request
184
+ is made. `ReferenceMismatchError` reports a social URL that belongs to a
185
+ different platform or resource than the selector.
186
+
187
+ ## License
188
+
189
+ [MIT](./LICENSE)
@@ -0,0 +1,163 @@
1
+ # OpenHandle Python SDK
2
+
3
+ The official Python client for the OpenHandle API.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install openhandle
9
+ ```
10
+
11
+ The package supports Python 3.10 and newer and ships full type annotations.
12
+
13
+ ## Usage
14
+
15
+ Create a Test key in the [Openhandle dashboard](https://app.openhandle.dev),
16
+ store it as `OPENHANDLE_TEST_KEY`, and create one reusable client:
17
+
18
+ ```python
19
+ import os
20
+
21
+ from openhandle import OpenHandle
22
+
23
+ openhandle = OpenHandle(api_key=os.environ["OPENHANDLE_TEST_KEY"])
24
+ profile = openhandle.instagram.profile("northstar_forge_test")
25
+
26
+ response = profile.get()
27
+ posts = profile.posts.list(freshness="24h")
28
+
29
+ print(response.data["handle"], len(posts.data))
30
+ ```
31
+
32
+ The key selects the environment. `oh_test_` keys return deterministic synthetic
33
+ data with a `$0.000` actual charge; `oh_live_` keys use real public identifiers
34
+ and normal billing. Never expose an API key in client-side code.
35
+
36
+ See the [API reference](https://openhandle.dev/docs/api-reference) for a typed
37
+ SDK example for every operation.
38
+
39
+ ## Resource selection
40
+
41
+ The SDK follows one predictable grammar:
42
+
43
+ ```text
44
+ openhandle.<platform>.<resource>(reference).<subresource>.<operation>(options)
45
+ ```
46
+
47
+ Only terminal operations such as `get`, `list`, `search`, and `fetch` perform
48
+ network requests. Selecting a resource is synchronous and reusable:
49
+
50
+ ```python
51
+ post = openhandle.instagram.post("Db04otPRpRH")
52
+
53
+ response = post.get()
54
+ comments = post.comments.list()
55
+ ```
56
+
57
+ A profile selector accepts a username shorthand or an explicit reference:
58
+
59
+ ```python
60
+ openhandle.instagram.profile("openai")
61
+ openhandle.instagram.profile("https://www.instagram.com/openai/")
62
+ openhandle.instagram.profile(username="12356")
63
+ openhandle.instagram.profile(id="25025320")
64
+ openhandle.instagram.profile(url="https://www.instagram.com/openai/")
65
+ ```
66
+
67
+ A raw string is never treated as a platform ID. `profile("12356")` selects the
68
+ username `12356`; `profile(id="12356")` selects platform ID `12356`. Numeric
69
+ reference values are rejected because platform IDs are opaque strings.
70
+
71
+ Call `openhandle.fetch(url)` when you do not know which resource a supported
72
+ social URL represents.
73
+
74
+ ## Pagination
75
+
76
+ A list or search operation returns one typed page:
77
+
78
+ ```python
79
+ page = openhandle.instagram.profile("northstar_forge_test").posts.list()
80
+
81
+ print(page.data, page.has_next_page, page.next_cursor)
82
+ next_page = page.next()
83
+ ```
84
+
85
+ `items()` iterates lazily across pages, one request per page:
86
+
87
+ ```python
88
+ for post in openhandle.instagram.profile("northstar_forge_test").posts.items():
89
+ print(post["id"])
90
+ ```
91
+
92
+ ## Async client
93
+
94
+ `AsyncOpenHandle` exposes the same resource graph with `async` terminal
95
+ operations:
96
+
97
+ ```python
98
+ import asyncio
99
+ import os
100
+
101
+ from openhandle import AsyncOpenHandle
102
+
103
+
104
+ async def main() -> None:
105
+ async with AsyncOpenHandle(api_key=os.environ["OPENHANDLE_TEST_KEY"]) as openhandle:
106
+ response = await openhandle.instagram.profile("northstar_forge_test").get()
107
+ print(response.data["handle"])
108
+
109
+ async for post in openhandle.instagram.profile("northstar_forge_test").posts.items():
110
+ print(post["id"])
111
+
112
+
113
+ asyncio.run(main())
114
+ ```
115
+
116
+ ## Responses
117
+
118
+ Every response preserves the public envelope. `data` stays typed through the
119
+ generated models in `openhandle.models`, and metadata is available on the
120
+ response object:
121
+
122
+ ```python
123
+ response = openhandle.instagram.profile("northstar_forge_test").get()
124
+
125
+ response.platform # "instagram"
126
+ response.resource # "profile"
127
+ response.captured_at # datetime
128
+ response.source # "live" or "cache"
129
+ response.request_id # stable request identifier for logs and support
130
+ response.billing.cost # authoritative charge as a decimal string
131
+ ```
132
+
133
+ A missing metric is `None`. It is never `0`.
134
+
135
+ ## Errors and retries
136
+
137
+ The SDK raises `OpenHandleError` with the documented fields. Branch on `code`,
138
+ never on `message`:
139
+
140
+ ```python
141
+ from openhandle import OpenHandle, OpenHandleError
142
+
143
+ try:
144
+ response = openhandle.instagram.profile("private_account").get()
145
+ except OpenHandleError as error:
146
+ print(error.code, error.request_id, error.retryable)
147
+ ```
148
+
149
+ Retryable failures are retried automatically with capped exponential backoff
150
+ and `Retry-After` support. Configure the client, or override per request:
151
+
152
+ ```python
153
+ openhandle = OpenHandle(api_key="...", timeout=10.0, max_retries=1)
154
+ openhandle.twitter.profile("openai").get(timeout=5.0, max_retries=0)
155
+ ```
156
+
157
+ Locally invalid references raise `OpenHandleReferenceError` before any request
158
+ is made. `ReferenceMismatchError` reports a social URL that belongs to a
159
+ different platform or resource than the selector.
160
+
161
+ ## License
162
+
163
+ [MIT](./LICENSE)
@@ -0,0 +1,29 @@
1
+ # Agent usability eval
2
+
3
+ This eval measures whether a coding agent can discover the public SDK from the
4
+ installed package without reading SDK source files.
5
+
6
+ Give the agent the ten prompts in `tasks.json` and ask it to write one Python
7
+ file per task, named `<task-id>.py`. The agent may inspect the installed
8
+ package's type annotations and the package README, but not `src`, `openapi`,
9
+ tests, generator code, or the reference answers.
10
+
11
+ Score an answer directory with:
12
+
13
+ ```bash
14
+ python scripts/generate.py
15
+ python scripts/score_agent_eval.py path/to/answers
16
+ ```
17
+
18
+ The scorer reports two independent results per task:
19
+
20
+ - `compile`: the answer passes `mypy --strict` against the installed package.
21
+ - `semantic`: lightweight required/forbidden markers indicate that it used the
22
+ intended resource, terminal operation, and behavior.
23
+
24
+ The semantic checks are intentionally conservative heuristics, not a substitute
25
+ for human review. Review incorrect answers for invented methods, raw HTTP paths,
26
+ misunderstood references, eager pagination, and branching on error messages.
27
+
28
+ `evals/reference` is a checked-in 10/10 baseline that proves the tasks and
29
+ scorer remain compatible with the current package. It is not an agent score.
@@ -0,0 +1,7 @@
1
+ # OpenAPI input
2
+
3
+ `openhandle.json` is the exact public API contract used to generate this SDK.
4
+ It is committed so generation and releases remain reproducible.
5
+
6
+ Updates arrive through automated pull requests from the OpenHandle API
7
+ repository. Do not edit the snapshot or generated Python files by hand.
@@ -0,0 +1,79 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "openhandle"
7
+ version = "1.0.0"
8
+ description = "Official Python SDK for OpenHandle"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ authors = [{ name = "OpenHandle", email = "support@openhandle.dev" }]
13
+ requires-python = ">=3.10"
14
+ dependencies = ["httpx>=0.27", "typing-extensions>=4.10"]
15
+ keywords = ["openhandle", "social-media", "api", "sdk"]
16
+ classifiers = [
17
+ "Development Status :: 5 - Production/Stable",
18
+ "Intended Audience :: Developers",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Typing :: Typed",
26
+ ]
27
+
28
+ [project.urls]
29
+ Homepage = "https://openhandle.dev"
30
+ Documentation = "https://openhandle.dev/docs/sdks"
31
+ Repository = "https://github.com/openhandlehq/openhandle-python"
32
+ Issues = "https://github.com/openhandlehq/openhandle-python/issues"
33
+
34
+ [dependency-groups]
35
+ dev = [
36
+ "mypy>=1.14",
37
+ "pytest>=8.3",
38
+ "pytest-asyncio>=0.25",
39
+ "ruff>=0.9",
40
+ ]
41
+
42
+ [tool.hatch.build.targets.sdist]
43
+ include = ["src/openhandle", "README.md", "LICENSE"]
44
+
45
+ [tool.hatch.build.targets.wheel]
46
+ packages = ["src/openhandle"]
47
+
48
+ [tool.ruff]
49
+ line-length = 120
50
+ target-version = "py310"
51
+
52
+ [tool.ruff.lint]
53
+ select = ["E", "F", "W", "I", "UP", "B", "SIM", "RUF"]
54
+ ignore = ["SIM108"]
55
+
56
+ [tool.ruff.lint.per-file-ignores]
57
+ "scripts/generate.py" = ["E501"]
58
+ "src/openhandle/models.py" = ["E501"]
59
+ "src/openhandle/_operations.py" = ["E501"]
60
+ "src/openhandle/_sync_resources.py" = ["E501"]
61
+ "src/openhandle/_async_resources.py" = ["E501"]
62
+
63
+ [tool.ruff.format]
64
+ exclude = [
65
+ "src/openhandle/models.py",
66
+ "src/openhandle/_operations.py",
67
+ "src/openhandle/_sync_resources.py",
68
+ "src/openhandle/_async_resources.py",
69
+ ]
70
+
71
+ [tool.mypy]
72
+ strict = true
73
+ python_version = "3.10"
74
+ mypy_path = "src"
75
+ files = ["src/openhandle", "scripts", "tests"]
76
+
77
+ [tool.pytest.ini_options]
78
+ testpaths = ["tests"]
79
+ asyncio_mode = "auto"
@@ -0,0 +1,21 @@
1
+ from openhandle._client import AsyncOpenHandle, OpenHandle
2
+ from openhandle._errors import OpenHandleError, OpenHandleReferenceError, ReferenceMismatchError
3
+ from openhandle._response import AsyncPage, Billing, Page, Response
4
+ from openhandle._transport import sdk_version
5
+ from openhandle._types import Freshness
6
+
7
+ __version__ = sdk_version()
8
+
9
+ __all__ = [
10
+ "AsyncOpenHandle",
11
+ "AsyncPage",
12
+ "Billing",
13
+ "Freshness",
14
+ "OpenHandle",
15
+ "OpenHandleError",
16
+ "OpenHandleReferenceError",
17
+ "Page",
18
+ "ReferenceMismatchError",
19
+ "Response",
20
+ "__version__",
21
+ ]