Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,12 @@ jobs:
permissions:
id-token: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: actions/checkout@v5
- uses: actions/setup-python@v6
with:
python-version: 3.13
- run: |
python -m pip install --upgrade build
python -m build
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_TOKEN }}
12 changes: 6 additions & 6 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ jobs:
tox: x402

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
Expand All @@ -44,7 +44,7 @@ jobs:
tox -e ${{ matrix.tox || 'py' }}
- name: coverage
if: ${{ success() }}
uses: codecov/codecov-action@v4.0.1
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}

Expand All @@ -53,13 +53,13 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ['3.12'] # Keep in sync with .readthedocs.yml
python-version: ["3.13"] # Keep in sync with .readthedocs.yml
tox-job: ["mypy", "docs"]

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
Expand Down
17 changes: 13 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.6
rev: v0.13.3
hooks:
- id: ruff
- id: ruff-check
args: [ --fix ]
- id: ruff-format
- repo: https://github.com/adamchainz/blacken-docs
rev: 1.19.0
rev: 1.20.0
hooks:
- id: blacken-docs
additional_dependencies:
- black==25.1.0
- black==25.9.0
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/sphinx-contrib/sphinx-lint
rev: v1.0.0
hooks:
- id: sphinx-lint
4 changes: 2 additions & 2 deletions .readthedocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ formats: all
sphinx:
configuration: docs/conf.py
build:
os: ubuntu-22.04
os: ubuntu-24.04
tools:
# For available versions, see:
# https://docs.readthedocs.io/en/stable/config-file/v2.html#build-tools-python
python: "3.12" # Keep in sync with .github/workflows/test.yml
python: "3.13" # Keep in sync with .github/workflows/test.yml
python:
install:
- requirements: docs/requirements.txt
Expand Down
2 changes: 1 addition & 1 deletion docs/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ help:
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
# -- Project information -----------------------------------------------------

project = "python-zyte-api"
copyright = "2021, Zyte Group Ltd"
project_copyright = "2021, Zyte Group Ltd"
author = "Zyte Group Ltd"

# The short X.Y version
Expand Down
2 changes: 1 addition & 1 deletion docs/use/x402.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
x402
====

It is possible to use :ref:`Zyte API <zyte-api>` without a Zyte API account by
It is possible to use :ref:`Zyte API <zyte-api>` without a Zyte API account by
using the x402_ protocol to handle payments:

#. Read the `Zyte Terms of Service`_. By using Zyte API, you are accepting
Expand Down
50 changes: 38 additions & 12 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,39 @@ filename = "zyte_api/__version__.py"

[tool.coverage.run]
branch = true

[tool.coverage.report]
exclude_also = [
"if TYPE_CHECKING:",
patch = [
"subprocess",
]

[tool.mypy]
allow_untyped_defs = false
implicit_reexport = false

[[tool.mypy.overrides]]
module = "runstats"
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = "tests.*"
allow_untyped_defs = true

[tool.pytest.ini_options]
filterwarnings = [
"ignore:The zyte_api\\.aio module is deprecated:DeprecationWarning"
]

[tool.ruff.lint]
extend-select = [
# flake8-builtins
"A",
# flake8-async
"ASYNC",
# flake8-bugbear
"B",
# flake8-comprehensions
"C4",
# flake8-commas
"COM",
# pydocstyle
"D",
# flake8-future-annotations
Expand Down Expand Up @@ -84,6 +100,8 @@ extend-select = [
"T10",
# flake8-type-checking
"TC",
# flake8-tidy-imports
"TID",
# pyupgrade
"UP",
# pycodestyle warnings
Expand All @@ -92,6 +110,8 @@ extend-select = [
"YTT",
]
ignore = [
# Trailing comma missing
"COM812",
# Missing docstring in public module
"D100",
# Missing docstring in public class
Expand Down Expand Up @@ -144,21 +164,27 @@ ignore = [
"S101",
]

[tool.ruff.lint.flake8-pytest-style]
parametrize-values-type = "tuple"

[tool.ruff.lint.flake8-tidy-imports]
banned-module-level-imports = ["twisted.internet.reactor"]

[tool.ruff.lint.flake8-type-checking]
runtime-evaluated-decorators = ["attr.s"]

[tool.ruff.lint.isort]
split-on-trailing-comma = false

[tool.ruff.lint.per-file-ignores]
"zyte_api/__init__.py" = ["F401"]
"zyte_api/aio/errors.py" = ["F401"]
"zyte_api/aio/retry.py" = ["F401"]
"tests/*" = ["S"]
"docs/**" = ["B006"]
# Skip PEP 604 suggestions for files with attr classes
"zyte_api/errors.py" = ["UP007"]
"zyte_api/stats.py" = ["UP007"]

[tool.ruff.lint.flake8-pytest-style]
parametrize-values-type = "tuple"

[tool.ruff.lint.flake8-type-checking]
runtime-evaluated-decorators = ["attr.s"]
"zyte_api/errors.py" = ["UP007", "UP045"]
"zyte_api/stats.py" = ["UP007", "UP045"]

[tool.ruff.lint.pydocstyle]
convention = "pep257"
4 changes: 4 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
author_email="opensource@zyte.com",
url="https://github.com/zytedata/python-zyte-api",
packages=find_packages(exclude=["tests", "examples"]),
package_data={
"zyte_api": ["py.typed"],
},
include_package_data=True,
entry_points={
"console_scripts": ["zyte-api=zyte_api.__main__:_main"],
},
Expand Down
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

@pytest.fixture(scope="session")
def mockserver():
from .mockserver import MockServer
from .mockserver import MockServer # noqa: PLC0415

with MockServer() as server:
yield server
16 changes: 10 additions & 6 deletions tests/mockserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
from typing import Any
from urllib.parse import urlparse

from twisted.internet import reactor
from twisted.internet.task import deferLater
from twisted.web.resource import Resource
from twisted.web.server import NOT_DONE_YET, Site
Expand All @@ -22,11 +21,11 @@


# https://github.com/scrapy/scrapy/blob/02b97f98e74a994ad3e4d74e7ed55207e508a576/tests/mockserver.py#L27C1-L33C19
def getarg(request, name, default=None, type=None):
def getarg(request, name, default=None, type_=None):
if name in request.args:
value = request.args[name][0]
if type is not None:
value = type(value)
if type_ is not None:
value = type_(value)
return value
return default

Expand All @@ -41,6 +40,8 @@ class DropResource(Resource):
isLeaf = True

def deferRequest(self, request, delay, f, *a, **kw):
from twisted.internet import reactor

def _cancelrequest(_):
# silence CancelledError
d.addErrback(lambda _: None)
Expand All @@ -56,7 +57,7 @@ def render_POST(self, request):
return NOT_DONE_YET

def _delayedRender(self, request):
abort = getarg(request, b"abort", 0, type=int)
abort = getarg(request, b"abort", 0, type_=int)
request.write(b"this connection will be dropped\n")
tr = request.channel.transport
try:
Expand Down Expand Up @@ -107,6 +108,7 @@ def render_POST(self, request):
)

request_data = json.loads(request.content.read())
response_data: dict[str, Any]

url = request_data["url"]
domain = urlparse(url).netloc
Expand Down Expand Up @@ -214,7 +216,7 @@ def render_POST(self, request):
}
return json.dumps(response_data).encode()

response_data: dict[str, Any] = {
response_data = {
"url": url,
}

Expand Down Expand Up @@ -269,6 +271,8 @@ def urljoin(self, path):


def main():
from twisted.internet import reactor

parser = argparse.ArgumentParser()
parser.add_argument("resource")
parser.add_argument("--port", type=int)
Expand Down
20 changes: 15 additions & 5 deletions tests/test_async.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from __future__ import annotations

import asyncio
from typing import TYPE_CHECKING, Any
from unittest.mock import AsyncMock

import pytest
Expand All @@ -9,6 +12,9 @@
from zyte_api.errors import ParsedError
from zyte_api.utils import USER_AGENT

if TYPE_CHECKING:
from tests.mockserver import MockServer


@pytest.mark.parametrize(
"client_cls",
Expand Down Expand Up @@ -218,7 +224,7 @@ async def test_semaphore(client_cls, get_method, iter_method, mockserver):


@pytest.mark.asyncio
async def test_session_context_manager(mockserver):
async def test_session_context_manager(mockserver: MockServer) -> None:
client = AsyncZyteAPI(api_key="a", api_url=mockserver.urljoin("/"))
queries = [
{"url": "https://a.example", "httpResponseBody": True},
Expand All @@ -236,11 +242,13 @@ async def test_session_context_manager(mockserver):
"httpResponseBody": "PGh0bWw+PGJvZHk+SGVsbG88aDE+V29ybGQhPC9oMT48L2JvZHk+PC9odG1sPg==",
},
]
actual_results = []
actual_results: list[dict[str, Any] | Exception] = []
async with client.session() as session:
assert session._session.connector is not None
assert session._session.connector.limit == client.n_conn
actual_results.append(await session.get(queries[0]))
for future in session.iter(queries[1:]):
result: dict[str, Any] | Exception
try:
result = await future
except Exception as e:
Expand All @@ -266,7 +274,7 @@ async def test_session_context_manager(mockserver):


@pytest.mark.asyncio
async def test_session_no_context_manager(mockserver):
async def test_session_no_context_manager(mockserver: MockServer) -> None:
client = AsyncZyteAPI(api_key="a", api_url=mockserver.urljoin("/"))
queries = [
{"url": "https://a.example", "httpResponseBody": True},
Expand All @@ -284,8 +292,10 @@ async def test_session_no_context_manager(mockserver):
"httpResponseBody": "PGh0bWw+PGJvZHk+SGVsbG88aDE+V29ybGQhPC9oMT48L2JvZHk+PC9odG1sPg==",
},
]
actual_results = []
actual_results: list[dict[str, Any] | Exception] = []
result: dict[str, Any] | Exception
session = client.session()
assert session._session.connector is not None
assert session._session.connector.limit == client.n_conn
actual_results.append(await session.get(queries[0]))
for future in session.iter(queries[1:]):
Expand Down Expand Up @@ -318,4 +328,4 @@ def test_retrying_class():
"""A descriptive exception is raised when creating a client with an
AsyncRetrying subclass or similar instead of an instance of it."""
with pytest.raises(ValueError, match="must be an instance of AsyncRetrying"):
AsyncZyteAPI(api_key="foo", retrying=AggressiveRetryFactory)
AsyncZyteAPI(api_key="foo", retrying=AggressiveRetryFactory) # type: ignore[arg-type]
Loading