outcrawl 0.1.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.
- outcrawl-0.1.0/.gitignore +22 -0
- outcrawl-0.1.0/LICENSE +21 -0
- outcrawl-0.1.0/PKG-INFO +156 -0
- outcrawl-0.1.0/README.md +126 -0
- outcrawl-0.1.0/pyproject.toml +72 -0
- outcrawl-0.1.0/scripts/gen_error_parity.ts +95 -0
- outcrawl-0.1.0/scripts/gen_registry.py +387 -0
- outcrawl-0.1.0/src/outcrawl/__init__.py +177 -0
- outcrawl-0.1.0/src/outcrawl/agent.py +697 -0
- outcrawl-0.1.0/src/outcrawl/availability.py +198 -0
- outcrawl-0.1.0/src/outcrawl/client.py +158 -0
- outcrawl-0.1.0/src/outcrawl/errors.py +1082 -0
- outcrawl-0.1.0/src/outcrawl/py.typed +0 -0
- outcrawl-0.1.0/src/outcrawl/registry.py +453 -0
- outcrawl-0.1.0/src/outcrawl/resources.py +447 -0
- outcrawl-0.1.0/src/outcrawl/result.py +182 -0
- outcrawl-0.1.0/src/outcrawl/scrape.py +212 -0
- outcrawl-0.1.0/src/outcrawl/transport.py +278 -0
- outcrawl-0.1.0/tests/conftest.py +138 -0
- outcrawl-0.1.0/tests/error_parity.json +492 -0
- outcrawl-0.1.0/tests/test_agent.py +303 -0
- outcrawl-0.1.0/tests/test_crawl.py +53 -0
- outcrawl-0.1.0/tests/test_errors.py +236 -0
- outcrawl-0.1.0/tests/test_registry.py +249 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Build and cache artefacts for this package.
|
|
2
|
+
#
|
|
3
|
+
# This file exists for a second, non-obvious reason: hatchling FORCE-INCLUDES
|
|
4
|
+
# the VCS exclusion file it used into the sdist, so that a rebuild from an
|
|
5
|
+
# unpacked sdist applies the same exclusions. Without a `.gitignore` here it
|
|
6
|
+
# walked up and shipped the MONOREPO's root one — `node_modules/`, the 126 GB
|
|
7
|
+
# Chromium checkout path, our credential rules — at the root of a distribution
|
|
8
|
+
# a customer unpacks. `exclude` in pyproject.toml cannot remove it, because
|
|
9
|
+
# `force_include` runs after exclusions by design. A local file is the fix.
|
|
10
|
+
dist/
|
|
11
|
+
__pycache__/
|
|
12
|
+
*.py[cod]
|
|
13
|
+
.pytest_cache/
|
|
14
|
+
*.egg-info/
|
|
15
|
+
.venv/
|
|
16
|
+
|
|
17
|
+
# `uv run` writes this. Deliberately NOT tracked: this is a LIBRARY, so the
|
|
18
|
+
# resolution a customer gets is decided by their own resolver against
|
|
19
|
+
# `dependencies`, and a committed lock would pin only OUR dev environment while
|
|
20
|
+
# generating churn on every `uv run`. An application in this repo would commit
|
|
21
|
+
# one; a library published to PyPI should not.
|
|
22
|
+
uv.lock
|
outcrawl-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aeonmind LLC
|
|
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.
|
outcrawl-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: outcrawl
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The Python client for Outcrawl: scrape, crawl, search and run agent browser tasks on one credit balance.
|
|
5
|
+
Project-URL: Homepage, https://outcrawl.ai
|
|
6
|
+
Project-URL: Repository, https://github.com/aeonmindai/outcrawl-browser
|
|
7
|
+
Project-URL: Issues, https://github.com/aeonmindai/outcrawl-browser/issues
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: agent,browser-automation,crawler,outcrawl,scraping,stealth,web-search
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
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: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
22
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Requires-Dist: httpx<1,>=0.27
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# outcrawl
|
|
32
|
+
|
|
33
|
+
The Python client for [Outcrawl](https://api.outcrawl.ai): scrape, crawl, search and run agent browser
|
|
34
|
+
tasks, on one credit balance. Pure Python — one `py3-none-any` wheel for every operating system,
|
|
35
|
+
and no compiler on any of them.
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
pip install outcrawl
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Requires Python 3.10 or newer. One runtime dependency, [`httpx`](https://www.python-httpx.org/),
|
|
42
|
+
which is pure Python, as is everything it pulls.
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
import asyncio
|
|
48
|
+
from outcrawl import Outcrawl
|
|
49
|
+
|
|
50
|
+
async def main() -> None:
|
|
51
|
+
async with Outcrawl() as oc: # reads OUTCRAWL_API_KEY
|
|
52
|
+
doc = await oc.scrape("https://example.com", formats=["markdown"])
|
|
53
|
+
print(doc.markdown) # or doc["markdown"] — it is the wire object
|
|
54
|
+
print(doc.usage) # every call returns what it cost
|
|
55
|
+
|
|
56
|
+
job = oc.agent(
|
|
57
|
+
task="Report the title of the top story on Hacker News.",
|
|
58
|
+
caps={"budget": "2.00"}, # the one ceiling a submit must carry
|
|
59
|
+
)
|
|
60
|
+
async for record in job: # the event cursor, while it runs
|
|
61
|
+
print(record["kind"], record.get("text"))
|
|
62
|
+
run = await job # and the settled run
|
|
63
|
+
print(run["status"], run["data"])
|
|
64
|
+
|
|
65
|
+
asyncio.run(main())
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`await` and `async for` on **one** job is one run and not two: they are separate requests against
|
|
69
|
+
one durable row, so watching a run and having its answer is not a choice.
|
|
70
|
+
|
|
71
|
+
## Configuration
|
|
72
|
+
|
|
73
|
+
| Variable | Meaning | Default |
|
|
74
|
+
| --- | --- | --- |
|
|
75
|
+
| `OUTCRAWL_API_KEY` | your key | required |
|
|
76
|
+
| `OUTCRAWL_API_URL` | base url | `https://api.outcrawl.ai` |
|
|
77
|
+
|
|
78
|
+
The same two names `@outcrawl/sdk`, the `outcrawl` CLI and the Outcrawl MCP server read. Pass
|
|
79
|
+
`Outcrawl(api_key=..., base_url=...)` to override them per client.
|
|
80
|
+
|
|
81
|
+
## Surface
|
|
82
|
+
|
|
83
|
+
Every one of the 34 capabilities in the registry is reachable, and `tests/test_registry.py` fails
|
|
84
|
+
when one is not.
|
|
85
|
+
|
|
86
|
+
- `oc.scrape(url, **options)` / `oc.crawl(url, **options)` / `oc.search(query, **options)` —
|
|
87
|
+
`crawl` is async-iterable and single-pass.
|
|
88
|
+
- `oc.crawl.job(id)` — the same crawl by id, with `status()`, `results()`, `cancel()`, and
|
|
89
|
+
`oc.crawl.get(id)` / `.results(id)` / `.cancel(id)` for an id on its own. A streaming crawl
|
|
90
|
+
belongs to its connection, so hanging up cancels it: the row settles `cancelled` with every
|
|
91
|
+
page it had already delivered, and these routes are how you read the pages you paid for. The
|
|
92
|
+
id is on `handle.job["id"]` from the first progress frame — store it before you need it.
|
|
93
|
+
- `oc.agent(task=..., caps=...)` — `caps` is required at runtime and `caps.budget` is required
|
|
94
|
+
inside it; `caps.steps` and `caps.duration` are optional and unset unless you name them, and
|
|
95
|
+
all three are hard stops when present. The job is awaitable, async-iterable, and carries
|
|
96
|
+
everything a handle does: `status()`, `results()`, `events()`, `cancel()`, `control()`,
|
|
97
|
+
`answer()`, `add_file()`. `oc.agent.get(id)` and friends reach a run by id — an id from a
|
|
98
|
+
webhook needs no submit. `schema` accepts a JSON Schema object and constrains `run["data"]` to
|
|
99
|
+
it, the same typed submit `@outcrawl/sdk` documents.
|
|
100
|
+
- `oc.profiles` / `oc.secrets` / `oc.rules` / `oc.integrations` / `oc.sessions` / `oc.monitors`.
|
|
101
|
+
`oc.secrets` has deliberately **no `get`**: there is no route to widen. A run reaches a value
|
|
102
|
+
only by naming its HANDLE in the submit, and the substitution happens below the model.
|
|
103
|
+
- `oc.usage(**query)` and `oc.credits()` — every credit figure is a decimal **string**; use
|
|
104
|
+
`decimal.Decimal`, never `float()`.
|
|
105
|
+
- `CAPABILITY_AVAILABILITY` — whether anything is SERVED behind a declared route. A capability
|
|
106
|
+
nothing serves is refused in your own process, with what is missing, rather than as a 503.
|
|
107
|
+
|
|
108
|
+
## Errors
|
|
109
|
+
|
|
110
|
+
Identical to the TypeScript SDK's: same classes, same fields, same sentences. Branch on
|
|
111
|
+
`error.code`, never on `str(error)`.
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from outcrawl import ProfileInUseError, QuotaExceededError
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
...
|
|
118
|
+
except ProfileInUseError as leased:
|
|
119
|
+
retry_at = leased.held_until # a retry that knows when to retry
|
|
120
|
+
except QuotaExceededError as over:
|
|
121
|
+
await asyncio.sleep(over.retry_after_seconds)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`tests/error_parity.json` is emitted from the TypeScript SDK's own error reconstruction and read by
|
|
125
|
+
both test suites, so neither surface can drift from the other without turning its own suite red.
|
|
126
|
+
|
|
127
|
+
## What is not here
|
|
128
|
+
|
|
129
|
+
`oc.browser()`. The live browser needs a CDP connection driven in the caller's own process, which
|
|
130
|
+
is why it is deliberately not a registry capability and why the TypeScript SDK is the one place it
|
|
131
|
+
exists. The same page capabilities are reachable through `agent` and `scrape`, which run the loop
|
|
132
|
+
on our side. That is a difference in kind, not an omission.
|
|
133
|
+
|
|
134
|
+
The same reason takes `Session.take_control` with it: the TypeScript SDK's `takeControl()` opens a
|
|
135
|
+
CDP connection to a running session's own page so a person can drive it directly, and that
|
|
136
|
+
connection has to live in the caller's process for the same reason `oc.browser()` does. Everything
|
|
137
|
+
else on a session — `get`, `list`, `export`, `live()` — is a row read or a held stream and is here.
|
|
138
|
+
|
|
139
|
+
## Development
|
|
140
|
+
|
|
141
|
+
```sh
|
|
142
|
+
pip install -e '.[dev]' # pytest + pytest-asyncio, never runtime dependencies
|
|
143
|
+
python3 scripts/gen_registry.py # regenerate registry.py from packages/core/src/registry.ts
|
|
144
|
+
python3 scripts/gen_registry.py --check # or just fail if it is stale
|
|
145
|
+
bun ../sdk-python/scripts/gen_error_parity.ts # regenerate the shared error contract
|
|
146
|
+
python3 -m pytest tests -q # the drift, parity and ergonomics tests
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
From the repo root, `npm run test:python` runs the staleness check and the suite together on
|
|
150
|
+
the SUPPORTED FLOOR — `uv run --extra dev --python 3.10 pytest` — so the gate exercises 3.10
|
|
151
|
+
rather than whatever `python3` happens to be. Without `uv`: `pip install -e '.[dev]'` and
|
|
152
|
+
`python3 -m pytest tests -q`.
|
|
153
|
+
|
|
154
|
+
The two registry-versus-TypeScript tests skip when run from an unpacked sdist, which has no
|
|
155
|
+
TypeScript to compare against, and run whenever the monorepo is present. The reachability test —
|
|
156
|
+
the one that fails when a registry row has no method — has no such dependency and always runs.
|
outcrawl-0.1.0/README.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# outcrawl
|
|
2
|
+
|
|
3
|
+
The Python client for [Outcrawl](https://api.outcrawl.ai): scrape, crawl, search and run agent browser
|
|
4
|
+
tasks, on one credit balance. Pure Python — one `py3-none-any` wheel for every operating system,
|
|
5
|
+
and no compiler on any of them.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pip install outcrawl
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires Python 3.10 or newer. One runtime dependency, [`httpx`](https://www.python-httpx.org/),
|
|
12
|
+
which is pure Python, as is everything it pulls.
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
import asyncio
|
|
18
|
+
from outcrawl import Outcrawl
|
|
19
|
+
|
|
20
|
+
async def main() -> None:
|
|
21
|
+
async with Outcrawl() as oc: # reads OUTCRAWL_API_KEY
|
|
22
|
+
doc = await oc.scrape("https://example.com", formats=["markdown"])
|
|
23
|
+
print(doc.markdown) # or doc["markdown"] — it is the wire object
|
|
24
|
+
print(doc.usage) # every call returns what it cost
|
|
25
|
+
|
|
26
|
+
job = oc.agent(
|
|
27
|
+
task="Report the title of the top story on Hacker News.",
|
|
28
|
+
caps={"budget": "2.00"}, # the one ceiling a submit must carry
|
|
29
|
+
)
|
|
30
|
+
async for record in job: # the event cursor, while it runs
|
|
31
|
+
print(record["kind"], record.get("text"))
|
|
32
|
+
run = await job # and the settled run
|
|
33
|
+
print(run["status"], run["data"])
|
|
34
|
+
|
|
35
|
+
asyncio.run(main())
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`await` and `async for` on **one** job is one run and not two: they are separate requests against
|
|
39
|
+
one durable row, so watching a run and having its answer is not a choice.
|
|
40
|
+
|
|
41
|
+
## Configuration
|
|
42
|
+
|
|
43
|
+
| Variable | Meaning | Default |
|
|
44
|
+
| --- | --- | --- |
|
|
45
|
+
| `OUTCRAWL_API_KEY` | your key | required |
|
|
46
|
+
| `OUTCRAWL_API_URL` | base url | `https://api.outcrawl.ai` |
|
|
47
|
+
|
|
48
|
+
The same two names `@outcrawl/sdk`, the `outcrawl` CLI and the Outcrawl MCP server read. Pass
|
|
49
|
+
`Outcrawl(api_key=..., base_url=...)` to override them per client.
|
|
50
|
+
|
|
51
|
+
## Surface
|
|
52
|
+
|
|
53
|
+
Every one of the 34 capabilities in the registry is reachable, and `tests/test_registry.py` fails
|
|
54
|
+
when one is not.
|
|
55
|
+
|
|
56
|
+
- `oc.scrape(url, **options)` / `oc.crawl(url, **options)` / `oc.search(query, **options)` —
|
|
57
|
+
`crawl` is async-iterable and single-pass.
|
|
58
|
+
- `oc.crawl.job(id)` — the same crawl by id, with `status()`, `results()`, `cancel()`, and
|
|
59
|
+
`oc.crawl.get(id)` / `.results(id)` / `.cancel(id)` for an id on its own. A streaming crawl
|
|
60
|
+
belongs to its connection, so hanging up cancels it: the row settles `cancelled` with every
|
|
61
|
+
page it had already delivered, and these routes are how you read the pages you paid for. The
|
|
62
|
+
id is on `handle.job["id"]` from the first progress frame — store it before you need it.
|
|
63
|
+
- `oc.agent(task=..., caps=...)` — `caps` is required at runtime and `caps.budget` is required
|
|
64
|
+
inside it; `caps.steps` and `caps.duration` are optional and unset unless you name them, and
|
|
65
|
+
all three are hard stops when present. The job is awaitable, async-iterable, and carries
|
|
66
|
+
everything a handle does: `status()`, `results()`, `events()`, `cancel()`, `control()`,
|
|
67
|
+
`answer()`, `add_file()`. `oc.agent.get(id)` and friends reach a run by id — an id from a
|
|
68
|
+
webhook needs no submit. `schema` accepts a JSON Schema object and constrains `run["data"]` to
|
|
69
|
+
it, the same typed submit `@outcrawl/sdk` documents.
|
|
70
|
+
- `oc.profiles` / `oc.secrets` / `oc.rules` / `oc.integrations` / `oc.sessions` / `oc.monitors`.
|
|
71
|
+
`oc.secrets` has deliberately **no `get`**: there is no route to widen. A run reaches a value
|
|
72
|
+
only by naming its HANDLE in the submit, and the substitution happens below the model.
|
|
73
|
+
- `oc.usage(**query)` and `oc.credits()` — every credit figure is a decimal **string**; use
|
|
74
|
+
`decimal.Decimal`, never `float()`.
|
|
75
|
+
- `CAPABILITY_AVAILABILITY` — whether anything is SERVED behind a declared route. A capability
|
|
76
|
+
nothing serves is refused in your own process, with what is missing, rather than as a 503.
|
|
77
|
+
|
|
78
|
+
## Errors
|
|
79
|
+
|
|
80
|
+
Identical to the TypeScript SDK's: same classes, same fields, same sentences. Branch on
|
|
81
|
+
`error.code`, never on `str(error)`.
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from outcrawl import ProfileInUseError, QuotaExceededError
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
...
|
|
88
|
+
except ProfileInUseError as leased:
|
|
89
|
+
retry_at = leased.held_until # a retry that knows when to retry
|
|
90
|
+
except QuotaExceededError as over:
|
|
91
|
+
await asyncio.sleep(over.retry_after_seconds)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`tests/error_parity.json` is emitted from the TypeScript SDK's own error reconstruction and read by
|
|
95
|
+
both test suites, so neither surface can drift from the other without turning its own suite red.
|
|
96
|
+
|
|
97
|
+
## What is not here
|
|
98
|
+
|
|
99
|
+
`oc.browser()`. The live browser needs a CDP connection driven in the caller's own process, which
|
|
100
|
+
is why it is deliberately not a registry capability and why the TypeScript SDK is the one place it
|
|
101
|
+
exists. The same page capabilities are reachable through `agent` and `scrape`, which run the loop
|
|
102
|
+
on our side. That is a difference in kind, not an omission.
|
|
103
|
+
|
|
104
|
+
The same reason takes `Session.take_control` with it: the TypeScript SDK's `takeControl()` opens a
|
|
105
|
+
CDP connection to a running session's own page so a person can drive it directly, and that
|
|
106
|
+
connection has to live in the caller's process for the same reason `oc.browser()` does. Everything
|
|
107
|
+
else on a session — `get`, `list`, `export`, `live()` — is a row read or a held stream and is here.
|
|
108
|
+
|
|
109
|
+
## Development
|
|
110
|
+
|
|
111
|
+
```sh
|
|
112
|
+
pip install -e '.[dev]' # pytest + pytest-asyncio, never runtime dependencies
|
|
113
|
+
python3 scripts/gen_registry.py # regenerate registry.py from packages/core/src/registry.ts
|
|
114
|
+
python3 scripts/gen_registry.py --check # or just fail if it is stale
|
|
115
|
+
bun ../sdk-python/scripts/gen_error_parity.ts # regenerate the shared error contract
|
|
116
|
+
python3 -m pytest tests -q # the drift, parity and ergonomics tests
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
From the repo root, `npm run test:python` runs the staleness check and the suite together on
|
|
120
|
+
the SUPPORTED FLOOR — `uv run --extra dev --python 3.10 pytest` — so the gate exercises 3.10
|
|
121
|
+
rather than whatever `python3` happens to be. Without `uv`: `pip install -e '.[dev]'` and
|
|
122
|
+
`python3 -m pytest tests -q`.
|
|
123
|
+
|
|
124
|
+
The two registry-versus-TypeScript tests skip when run from an unpacked sdist, which has no
|
|
125
|
+
TypeScript to compare against, and run whenever the monorepo is present. The reachability test —
|
|
126
|
+
the one that fails when a registry row has no method — has no such dependency and always runs.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.25"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "outcrawl"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "The Python client for Outcrawl: scrape, crawl, search and run agent browser tasks on one credit balance."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
keywords = [
|
|
14
|
+
"outcrawl",
|
|
15
|
+
"scraping",
|
|
16
|
+
"crawler",
|
|
17
|
+
"web-search",
|
|
18
|
+
"browser-automation",
|
|
19
|
+
"agent",
|
|
20
|
+
"stealth",
|
|
21
|
+
]
|
|
22
|
+
classifiers = [
|
|
23
|
+
"Development Status :: 4 - Beta",
|
|
24
|
+
"Intended Audience :: Developers",
|
|
25
|
+
"License :: OSI Approved :: MIT License",
|
|
26
|
+
"Operating System :: OS Independent",
|
|
27
|
+
"Programming Language :: Python :: 3",
|
|
28
|
+
"Programming Language :: Python :: 3.10",
|
|
29
|
+
"Programming Language :: Python :: 3.11",
|
|
30
|
+
"Programming Language :: Python :: 3.12",
|
|
31
|
+
"Programming Language :: Python :: 3.13",
|
|
32
|
+
"Programming Language :: Python :: 3.14",
|
|
33
|
+
"Programming Language :: Python :: Implementation :: CPython",
|
|
34
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
35
|
+
"Typing :: Typed",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
# The one runtime dependency, and it is pure Python — as is everything it pulls
|
|
39
|
+
# (httpcore, h11, certifi, idna, anyio, sniffio). That is what keeps this wheel
|
|
40
|
+
# `py3-none-any` and makes "supports every OS" a fact about the artefact rather
|
|
41
|
+
# than a hope about the user's compiler.
|
|
42
|
+
dependencies = ["httpx>=0.27,<1"]
|
|
43
|
+
|
|
44
|
+
[project.urls]
|
|
45
|
+
Homepage = "https://outcrawl.ai"
|
|
46
|
+
Repository = "https://github.com/aeonmindai/outcrawl-browser"
|
|
47
|
+
Issues = "https://github.com/aeonmindai/outcrawl-browser/issues"
|
|
48
|
+
|
|
49
|
+
[project.optional-dependencies]
|
|
50
|
+
# Test-only. Never a runtime dependency: a customer installing this package must
|
|
51
|
+
# not be handed a test runner.
|
|
52
|
+
dev = ["pytest>=8", "pytest-asyncio>=0.24"]
|
|
53
|
+
|
|
54
|
+
[tool.hatch.build.targets.wheel]
|
|
55
|
+
packages = ["src/outcrawl"]
|
|
56
|
+
|
|
57
|
+
[tool.hatch.build.targets.sdist]
|
|
58
|
+
# The sdist carries the tests and the generator, so `pytest` works from an
|
|
59
|
+
# unpacked source distribution — including the registry drift test, which needs
|
|
60
|
+
# the TypeScript registry and therefore reports honestly that it cannot run
|
|
61
|
+
# rather than passing silently.
|
|
62
|
+
include = ["src", "tests", "scripts", "README.md", "pyproject.toml", "LICENSE"]
|
|
63
|
+
# Hatchling FORCE-INCLUDES the VCS exclusion file it used, so a rebuild from an
|
|
64
|
+
# unpacked sdist applies the same exclusions — and `exclude` cannot remove a
|
|
65
|
+
# force-included path. With no `.gitignore` in this directory it walked up and
|
|
66
|
+
# shipped the MONOREPO's root one, whose contents are `node_modules/`, the
|
|
67
|
+
# Chromium checkout path and our credential rules. `packages/sdk-python/.gitignore`
|
|
68
|
+
# is what makes the shipped one this package's own; see the note in it.
|
|
69
|
+
|
|
70
|
+
[tool.pytest.ini_options]
|
|
71
|
+
testpaths = ["tests"]
|
|
72
|
+
asyncio_mode = "auto"
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Emit the cross-language error contract both SDKs are held to.
|
|
3
|
+
*
|
|
4
|
+
* THE PROBLEM. "Auth, base URL and error shapes identical across both" is a
|
|
5
|
+
* claim about two codebases in two languages, and the only way it stays true is
|
|
6
|
+
* if one artefact pins it and BOTH suites read that artefact. A Python mirror
|
|
7
|
+
* checked only by Python tests is a mirror that drifts the first time a
|
|
8
|
+
* TypeScript message is reworded, and the customer who notices is the one
|
|
9
|
+
* quoting one sentence to support while their logs contain the other.
|
|
10
|
+
*
|
|
11
|
+
* So this writes `tests/error_parity.json`: for every code in
|
|
12
|
+
* `OUTCRAWL_ERROR_CODES`, the wire body the platform sends and exactly what the
|
|
13
|
+
* TypeScript SDK's `errorFromWire` reconstructs from it — class, code, status,
|
|
14
|
+
* message, details.
|
|
15
|
+
*
|
|
16
|
+
* - `packages/sdk-python/tests/test_errors.py` asserts Python rebuilds the
|
|
17
|
+
* same thing from the same body.
|
|
18
|
+
* - `packages/sdk/test/error-parity.test.ts` asserts TypeScript still does,
|
|
19
|
+
* so a reworded message turns the TS suite red rather than silently
|
|
20
|
+
* invalidating the fixture the Python suite trusts.
|
|
21
|
+
*
|
|
22
|
+
* The fixture lives in the Python package because it must ship inside the
|
|
23
|
+
* sdist: `pytest` from an unpacked source distribution has no TypeScript to
|
|
24
|
+
* read, and a parity test that skips when it cannot find the other language is
|
|
25
|
+
* a parity test that passes while the two disagree.
|
|
26
|
+
*
|
|
27
|
+
* bun packages/sdk-python/scripts/gen_error_parity.ts
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { writeFileSync } from 'node:fs';
|
|
31
|
+
import { join } from 'node:path';
|
|
32
|
+
|
|
33
|
+
import { OUTCRAWL_ERROR_CODES, isOutcrawlError } from '../../core/src/errors';
|
|
34
|
+
import { ERROR_SAMPLES } from '../../api/src/errors';
|
|
35
|
+
import { errorFromWire } from '../../sdk/src/transport';
|
|
36
|
+
|
|
37
|
+
interface Rebuilt {
|
|
38
|
+
readonly class: string;
|
|
39
|
+
readonly code: string | null;
|
|
40
|
+
readonly status: number | null;
|
|
41
|
+
readonly message: string;
|
|
42
|
+
readonly details: Record<string, unknown> | null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface ParityRow {
|
|
46
|
+
readonly capabilityCode: string;
|
|
47
|
+
readonly status: number;
|
|
48
|
+
readonly wire: { code: string; message: string; details: Record<string, unknown> };
|
|
49
|
+
readonly rebuilt: Rebuilt;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function describe(error: Error): Rebuilt {
|
|
53
|
+
if (!isOutcrawlError(error)) {
|
|
54
|
+
// Every declared code must reconstruct into a typed error; an
|
|
55
|
+
// `OutcrawlApiError` here is the drop this file exists to catch, and it is
|
|
56
|
+
// recorded rather than thrown on so the fixture shows the truth.
|
|
57
|
+
return {
|
|
58
|
+
class: error.constructor.name,
|
|
59
|
+
code: null,
|
|
60
|
+
status: null,
|
|
61
|
+
message: error.message,
|
|
62
|
+
details: null,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
class: error.constructor.name,
|
|
67
|
+
code: error.code,
|
|
68
|
+
status: error.status,
|
|
69
|
+
message: error.message,
|
|
70
|
+
details: { ...error.details },
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const rows: ParityRow[] = OUTCRAWL_ERROR_CODES.map((code) => {
|
|
75
|
+
const sample = ERROR_SAMPLES[code];
|
|
76
|
+
const wire = sample.toJSON();
|
|
77
|
+
return {
|
|
78
|
+
capabilityCode: code,
|
|
79
|
+
status: sample.status,
|
|
80
|
+
wire,
|
|
81
|
+
rebuilt: describe(errorFromWire(sample.status, wire)),
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const dropped = rows.filter((row) => row.rebuilt.code === null);
|
|
86
|
+
if (dropped.length > 0) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`errorFromWire drops ${dropped.length} declared code(s) to an untyped error: ` +
|
|
89
|
+
`${dropped.map((row) => row.capabilityCode).join(', ')}. Map them before regenerating.`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const target = join(import.meta.dir, '..', 'tests', 'error_parity.json');
|
|
94
|
+
writeFileSync(target, `${JSON.stringify({ rows }, null, 2)}\n`, 'utf8');
|
|
95
|
+
console.log(`wrote ${target} (${rows.length} codes)`);
|