Skip to content

Commit 2225548

Browse files
authored
x402 support (#261)
1 parent fe819a2 commit 2225548

File tree

9 files changed

+318
-130
lines changed

9 files changed

+318
-130
lines changed

.github/workflows/test.yml

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,34 +18,35 @@ jobs:
1818
matrix:
1919
include:
2020
- python-version: '3.9'
21-
toxenv: pinned-scrapy-2x0
21+
toxenv: min-scrapy-2x0
2222
- python-version: '3.9'
23-
toxenv: pinned-scrapy-2x1
23+
toxenv: min-scrapy-2x1
2424
- python-version: '3.9'
25-
toxenv: pinned-scrapy-2x3
25+
toxenv: min-scrapy-2x3
2626
- python-version: '3.9'
27-
toxenv: pinned-scrapy-2x4
27+
toxenv: min-scrapy-2x4
2828
- python-version: '3.9'
29-
toxenv: pinned-scrapy-2x5
29+
toxenv: min-scrapy-2x5
3030
- python-version: '3.9'
31-
toxenv: pinned-scrapy-2x6
31+
toxenv: min-scrapy-2x6
3232
- python-version: '3.9'
33-
toxenv: pinned-scrapy-2x7
33+
toxenv: min-scrapy-2x7
34+
- python-version: '3.9'
35+
toxenv: min-extra
36+
- python-version: '3.9'
37+
toxenv: min-provider
38+
- python-version: '3.10'
39+
toxenv: min-x402
3440
- python-version: '3.10'
3541
- python-version: '3.11'
3642
- python-version: '3.12'
3743
- python-version: '3.13'
38-
39-
- python-version: '3.9'
40-
toxenv: pinned-provider
44+
- python-version: '3.13'
45+
toxenv: extra
4146
- python-version: '3.13'
4247
toxenv: provider
43-
44-
- python-version: '3.9'
45-
toxenv: pinned-extra
4648
- python-version: '3.13'
47-
toxenv: extra
48-
49+
toxenv: x402
4950
steps:
5051
- uses: actions/checkout@v4
5152
- name: Set up Python ${{ matrix.python-version }}
@@ -70,7 +71,7 @@ jobs:
7071
fail-fast: false
7172
matrix:
7273
python-version: ["3.12"] # Keep in sync with .readthedocs.yml
73-
tox-job: ["mypy", "pre-commit", "twine-check", "docs"]
74+
tox-job: ["pre-commit", "mypy", "twine", "docs"]
7475

7576
steps:
7677
- uses: actions/checkout@v4

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.coverage
1+
.coverage*
22
.mypy_cache/
33
.tox/
44
dist/
@@ -8,4 +8,4 @@ docs/_build
88
*.egg-info/
99
__pycache__/
1010
/test-results/
11-
build/
11+
build/

CHANGES.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
Changes
22
=======
33

4+
0.31.0 (unreleased)
5+
-------------------
6+
7+
- Added :ref:`x402 support <x402>`.
8+
9+
410
0.30.0 (2025-05-13)
511
-------------------
612

docs/setup.rst

Lines changed: 97 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@ You need at least:
2222

2323
- Scrapy 2.0.1+
2424

25-
:doc:`scrapy-poet <scrapy-poet:index>` integration requires higher versions:
25+
:doc:`scrapy-poet <scrapy-poet:index>` integration requires Scrapy 2.6+.
2626

27-
- Scrapy 2.6+
27+
:ref:`x402 support <x402>` requires Python 3.10+.
2828

2929

30+
.. _install:
31+
3032
Installation
3133
============
3234

@@ -42,31 +44,63 @@ For :ref:`scrapy-poet integration <scrapy-poet>`:
4244
4345
pip install scrapy-zyte-api[provider]
4446
47+
For :ref:`x402 support <x402>`, make sure you have Python 3.10+ and install
48+
the ``x402`` extra:
49+
50+
.. code-block:: shell
51+
52+
pip install scrapy-zyte-api[x402]
53+
54+
Note that you can install multiple extras_:
55+
56+
.. _extras: https://setuptools.pypa.io/en/latest/userguide/dependency_management.html#optional-dependencies
57+
58+
.. code-block:: shell
59+
60+
pip install scrapy-zyte-api[provider,x402]
61+
4562
4663
Configuration
4764
=============
4865

49-
To configure scrapy-zyte-api, :ref:`set your API key <config-api-key>` and
50-
either :ref:`enable the add-on <config-addon>` (Scrapy ≥ 2.10) or
51-
:ref:`configure all components separately <config-components>`.
66+
To configure scrapy-zyte-api, :ref:`set up authentication <auth>` and either
67+
:ref:`enable the add-on <config-addon>` (Scrapy ≥ 2.10) or :ref:`configure all
68+
components separately <config-components>`.
5269

5370
.. warning:: :ref:`reactor-change`.
5471

72+
.. _auth:
5573
.. _config-api-key:
5674

57-
Setting your API key
58-
--------------------
75+
Authentication
76+
--------------
5977

60-
Add your `Zyte API key`_, and add it to your project ``settings.py``:
78+
`Sign up for a Zyte API account
79+
<https://app.zyte.com/account/signup/zyteapi>`_, copy `your API key
80+
<https://app.zyte.com/o/zyte-api/api-access>`_ and do either of the following:
6181

62-
.. _Zyte API key: https://app.zyte.com/o/zyte-api/api-access
82+
- Define an environment variable named ``ZYTE_API_KEY`` with your API key:
6383

64-
.. code-block:: python
84+
- On Windows’ CMD:
6585

66-
ZYTE_API_KEY = "YOUR_API_KEY"
86+
.. code-block:: shell
6787
68-
Alternatively, you can set your API key in the ``ZYTE_API_KEY`` environment
69-
variable instead.
88+
> set ZYTE_API_KEY=YOUR_API_KEY
89+
90+
- On macOS and Linux:
91+
92+
.. code-block:: shell
93+
94+
$ export ZYTE_API_KEY=YOUR_API_KEY
95+
96+
- Add your API key to your setting module:
97+
98+
.. code-block:: python
99+
:caption: settings.py
100+
101+
ZYTE_API_KEY = "YOUR_API_KEY"
102+
103+
To use `x402 <https://www.x402.org/>`__ instead, see :ref:`x402`.
70104

71105

72106
.. _config-addon:
@@ -175,3 +209,53 @@ your existing code may need changes, such as:
175209
some Scrapy functions and methods. For example, when you yield the
176210
return value of ``self.crawler.engine.download()`` from a spider
177211
callback, you are yielding a Deferred.
212+
213+
214+
.. _x402:
215+
216+
x402
217+
====
218+
219+
It is possible to use :ref:`Zyte API <zyte-api>` without a Zyte API account by
220+
using the `x402 <https://www.x402.org/>`__ protocol to handle payments:
221+
222+
#. Read the `Zyte Terms of Service`_. By using Zyte API, you are accepting
223+
them.
224+
225+
.. _Zyte Terms of Service: https://www.zyte.com/terms-policies/terms-of-service/
226+
227+
#. During :ref:`installation <install>`, make sure to install the ``x402``
228+
extra.
229+
230+
#. :ref:`Configure <eth-key>` the *private* key of your Ethereum_ account to
231+
authorize payments.
232+
233+
.. _Ethereum: https://ethereum.org/
234+
235+
.. _eth-key:
236+
237+
Configuring your Ethereum private key
238+
-------------------------------------
239+
240+
It is recommended to configure your Ethereum private key through an environment
241+
variable, so that it also works when you use :doc:`python-zyte-api
242+
<python-zyte-api:index>`:
243+
244+
- On Windows’ CMD:
245+
246+
.. code-block:: shell
247+
248+
> set ZYTE_API_ETH_KEY=YOUR_ETH_PRIVATE_KEY
249+
250+
- On macOS and Linux:
251+
252+
.. code-block:: shell
253+
254+
$ export ZYTE_API_ETH_KEY=YOUR_ETH_PRIVATE_KEY
255+
256+
Alternatively, you can add your Ethereum private key to the settings module:
257+
258+
.. code-block:: python
259+
:caption: settings.py
260+
261+
ZYTE_API_ETH_KEY = "YOUR_ETH_PRIVATE_KEY"

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ provider = [
3838
"web-poet>=0.17.0",
3939
"zyte-common-items>=0.27.0",
4040
]
41+
x402 = [
42+
"zyte-api[x402]>=0.8.0",
43+
]
4144

4245
[project.urls]
4346
source = "https://github.com/scrapy-plugins/scrapy-zyte-api"

scrapy_zyte_api/handler.py

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from copy import deepcopy
55
from typing import Any, Generator, Optional, Union
66

7+
78
from scrapy import Spider, signals
89
from scrapy.crawler import Crawler
910
from scrapy.exceptions import NotConfigured
@@ -15,12 +16,12 @@
1516
from twisted.internet.defer import Deferred, inlineCallbacks
1617
from zyte_api import AsyncZyteAPI, RequestError
1718
from zyte_api.apikey import NoApiKey
18-
from zyte_api.constants import API_URL
1919

2020
from ._params import _ParamParser
2121
from .responses import ZyteAPIResponse, ZyteAPITextResponse, _process_response
2222
from .utils import (
2323
_AUTOTHROTTLE_DONT_ADJUST_DELAY_SUPPORT,
24+
_X402_SUPPORT,
2425
USER_AGENT,
2526
_build_from_crawler,
2627
)
@@ -95,7 +96,7 @@ def __init__(
9596
# https://github.com/scrapy-plugins/scrapy-zyte-api/issues/58
9697
crawler.zyte_api_client = client # type: ignore[attr-defined]
9798
self._client: AsyncZyteAPI = crawler.zyte_api_client # type: ignore[attr-defined]
98-
logger.info("Using a Zyte API key starting with %r", self._client.api_key[:7])
99+
self._log_auth()
99100
verify_installed_reactor(
100101
"twisted.internet.asyncioreactor.AsyncioSelectorReactor"
101102
)
@@ -133,6 +134,21 @@ def __init__(
133134
def from_crawler(cls, crawler):
134135
return cls(crawler.settings, crawler)
135136

137+
def _log_auth(self):
138+
if _X402_SUPPORT:
139+
auth_type = (
140+
"a Zyte API key"
141+
if self._client.auth.type == "zyte"
142+
else "an Ethereum private key"
143+
)
144+
logger.info(
145+
f"Using {auth_type} starting with {self._client.auth.key[:7]!r}"
146+
)
147+
else:
148+
logger.info(
149+
f"Using a Zyte API key starting with {self._client.api_key[:7]!r}"
150+
)
151+
136152
async def engine_started(self):
137153
self._session = self._client.session(trust_env=self._trust_env)
138154
if not self._cookies_enabled:
@@ -153,27 +169,26 @@ async def engine_started(self):
153169

154170
@staticmethod
155171
def _build_client(settings):
172+
kwargs = {}
173+
if api_key := settings.get("ZYTE_API_KEY"):
174+
kwargs["api_key"] = api_key
175+
if _X402_SUPPORT and (eth_key := settings.get("ZYTE_API_ETH_KEY")):
176+
kwargs["eth_key"] = eth_key
177+
if api_url := settings.get("ZYTE_API_URL"):
178+
kwargs["api_url"] = api_url
156179
try:
157180
return AsyncZyteAPI(
158-
# To allow users to have a key defined in Scrapy settings and
159-
# in a environment variable, and be able to cause the
160-
# environment variable to be used instead of the setting by
161-
# overriding the setting on the command-line to be an empty
162-
# string, we do not support setting empty string keys through
163-
# settings.
164-
api_key=settings.get("ZYTE_API_KEY") or None,
165-
api_url=settings.get("ZYTE_API_URL") or API_URL,
166181
n_conn=settings.getint("CONCURRENT_REQUESTS"),
167-
user_agent=settings.get("_ZYTE_API_USER_AGENT", default=USER_AGENT),
182+
user_agent=settings.get("_ZYTE_API_USER_AGENT", USER_AGENT),
183+
**kwargs,
168184
)
169185
except NoApiKey:
170-
logger.warning(
171-
"'ZYTE_API_KEY' must be set in the spider settings or env var "
172-
"in order for ScrapyZyteAPIDownloadHandler to work."
173-
)
174-
raise NotConfigured(
175-
"Your Zyte API key is not set. Set ZYTE_API_KEY to your API key."
186+
message = (
187+
"No authentication data provided. See "
188+
"https://scrapy-zyte-api.readthedocs.io/en/latest/setup.html#auth"
176189
)
190+
logger.warning(message)
191+
raise NotConfigured(message)
177192

178193
def _create_handler(self, path: Any) -> Any:
179194
dhcls = load_object(path)

scrapy_zyte_api/utils.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,10 @@ def _build_from_crawler(
6262
_SCRAPY_POET_VERSION = Version(version("scrapy-poet"))
6363
_SCRAPY_POET_0_26_0 = Version("0.26.0")
6464
_POET_ADDON_SUPPORT = _SCRAPY_POET_VERSION >= _SCRAPY_POET_0_26_0
65+
66+
try:
67+
from zyte_api import AuthInfo # noqa: F401
68+
except ImportError:
69+
_X402_SUPPORT = False
70+
else:
71+
_X402_SUPPORT = True

0 commit comments

Comments
 (0)