forktex-scraping
Acquire public data, and be able to prove what you acquired.
pip install forktex-scraping[http]
An acquisition framework, not a scraper. Scraping a rendered page is its lowest tier, used when nothing better exists — and across the four sources it was built against, two are official JSON APIs and only one needs a browser at all.
A remote that answers
200to a request it did not understand is the adversary. Everything here exists to turn a silently wrong answer into a loud one.
The problem it solves
A public procurement portal was measured to do all of this at once:
- reject any request without a
Referer, with a message and no status code to match; - answer every unfiltered query with
total: 3000, searchTooLong: true— a cap, not a count; - accept a filter field it did not recognise, ignore it, and return
200with the capped, unfiltered set — byte-identical to sending no filter at all.
That last one is the reason this library exists. A guessed field name does not fail. It produces a confidently wrong dataset, which is worse than an error, and no amount of care in the calling code detects it.
What it does about that
A request contract is captured, not guessed. A browser drives the site's own filter controls once, and the wire name of every filter it honours is read off the traffic. That contract is data; replaying it never needs a browser again.
Every filter is verified on every page. A contract's filter declares where its effect must
appear in a returned row. A row that violates it raises ContractDriftError — the remote answered
a different question than the one asked, and that is not something to patch client-side.
Coverage is a claim that has to be earned. A capped query is split along its axis until the remote answers honestly. A window that cannot be split further and is still capped is neither dropped nor quietly accepted:
coverage = await harvest(source, contract, transport=..., politeness=..., sink=..., run_id=...)
if not coverage.complete:
for window in coverage.truncated: # named, not silently missing
...
Politeness is a refusal, not a log line. robots.txt is honoured — and it fails open, since
absence is not prohibition. The rate limiter is per-origin and shared across callers, so eight
concurrent workers at "one request per second" produce one request per second, not eight.
Retry-After wins over the computed backoff.
There is no stealth, no user-agent rotation and no proxy pool. The agent string says who you are.
The seam: stateless versus stateful
The central distinction is not HTTP-versus-browser; it is whether a thing can be replayed.
Transport |
Session |
|
|---|---|---|
| snapshot and replay | yes — a request hashes to a stable identity | no — order is the state |
| retry, pagination, cap-splitting | yes | not applicable |
| safe to run concurrently | yes, under the pacer | one session, one caller |
The rule that follows: a session's output is never a result. It is data — a RequestContract
or a SourceDefinition — which the stateless path then replays forever. An agent explores a site
statefully; what comes out is never records. That is what keeps a harvest reproducible.
Sources are data
A source is a JSON file naming the model it loads into. Adding a source is a data change; adding a kind is a code change, because a kind with no runner acquires nothing, silently.
{ "$model": "ApiSource",
"id": "source.example.notices",
"tier": "captured_contract",
"contract": "example/notices.json",
"partition": {"axis": "publishedAt", "start_field": "from", "end_field": "to",
"initial": "P1M", "floor": "P1D", "end_bound": "inclusive_day"},
"politeness": {"rate_per_second": 2.0, "respect_robots": true} }
Five kinds, each earned by a real source: ApiSource, BatchKeySource, CatalogSource,
FormStateSource, SitemapSource.
Driving it from an agent
forktex_scraping.operations describes the library as typed operations whose JSON Schemas come
free from Pydantic — with no MCP, agent-framework or SDK dependency. A harness binds them:
from forktex_scraping import OPERATIONS, schemas
for op in OPERATIONS:
register(name=op.name, description=op.summary, parameters=op.schema())
# op.holds_resource marks the session group — scope those, or you leak browsers
Install
| Extra | Pulls | For |
|---|---|---|
[http] |
httpx |
the stateless transport — almost everything |
[browser] |
playwright |
capturing a contract, once per source |
[html] |
selectolax |
parsing a server-rendered table |
Extras import lazily: import forktex_scraping works with none of them installed, and asking for
one you did not install raises an ImportError that names the extra.
Documentation
One page per module in docs/, plus DESIGN.md for why it is shaped this
way and docs/development.md for the gate.
Licence
Dual-licensed: AGPL-3.0-or-later, or a commercial licence for use in proprietary products and
SaaS deployments where AGPL obligations cannot be met — info@forktex.com. See
LICENSE and NOTICE.
Metadata
Release files for forktex-scraping 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| forktex_scraping-0.1.0.tar.gz | 48.8 kB | Without provenance | Details for forktex_scraping-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| forktex_scraping-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for forktex_scraping-0.1.0-py3-none-any.whl |
Total release size: 111.9 kB
Release files / forktex_scraping-0.1.0.tar.gz
| Download URL | forktex_scraping-0.1.0.tar.gz |
|---|---|
| Size | 48.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e42925a9b4d163ee320436f28c0ac016eb8befc747ab76f6544cd637d2b5ca70
|
|
BLAKE2b-256 checksum How to use checksums |
ea151624b199b4519b0ad177c0f455cbc5f8a9c55a93fbbb726360f8c0a0bdeb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.6
|
Release files / forktex_scraping-0.1.0-py3-none-any.whl
| Download URL | forktex_scraping-0.1.0-py3-none-any.whl |
|---|---|
| Size | 63.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
06dc3f31c11c9dbbc3e882ed3032df2280b373a6d5543944145ec4b0e21e9874
|
|
BLAKE2b-256 checksum How to use checksums |
59556635d85f9f086868f831b1bdcc99b950397cfbb7e518d05bb2f96f0f1d65
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.6
|
Security for forktex-scraping 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.