diff --git a/src/apify_client/_resource_clients/run.py b/src/apify_client/_resource_clients/run.py index 9f8a69c4..ffcea254 100644 --- a/src/apify_client/_resource_clients/run.py +++ b/src/apify_client/_resource_clients/run.py @@ -385,7 +385,8 @@ def charge( Args: event_name: The name of the event to charge for. - count: The number of events to charge. Defaults to 1 if not provided. + count: The number of events to charge. Defaults to 1 when `None`; other values, + including 0, are sent to the server as-is. idempotency_key: A unique key to ensure idempotent charging. If not provided, one will be auto-generated. timeout: Timeout for the API HTTP request. @@ -411,7 +412,7 @@ def charge( data=json.dumps( { 'eventName': event_name, - 'count': count or 1, + 'count': count if count is not None else 1, } ), timeout=timeout, @@ -811,7 +812,8 @@ async def charge( Args: event_name: The name of the event to charge for. - count: The number of events to charge. Defaults to 1 if not provided. + count: The number of events to charge. Defaults to 1 when `None`; other values, + including 0, are sent to the server as-is. idempotency_key: A unique key to ensure idempotent charging. If not provided, one will be auto-generated. timeout: Timeout for the API HTTP request. @@ -837,7 +839,7 @@ async def charge( data=json.dumps( { 'eventName': event_name, - 'count': count or 1, + 'count': count if count is not None else 1, } ), timeout=timeout, diff --git a/tests/unit/test_run_charge.py b/tests/unit/test_run_charge.py new file mode 100644 index 00000000..d007033e --- /dev/null +++ b/tests/unit/test_run_charge.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +import gzip +import json +from typing import TYPE_CHECKING + +import pytest +from werkzeug import Request, Response + +from apify_client import ApifyClient, ApifyClientAsync + +if TYPE_CHECKING: + from pytest_httpserver import HTTPServer + +_MOCKED_RUN_ID = 'test_run_id' +_CHARGE_PATH = f'/v2/actor-runs/{_MOCKED_RUN_ID}/charge' + + +def _decode_body(request: Request) -> dict: + raw = request.get_data() + if request.headers.get('Content-Encoding') == 'gzip': + raw = gzip.decompress(raw) + return json.loads(raw) + + +@pytest.mark.parametrize( + ('count', 'expected'), + [ + (None, 1), + (0, 0), + (1, 1), + (5, 5), + ], +) +def test_run_charge_preserves_count_sync( + httpserver: HTTPServer, + count: int | None, + expected: int, +) -> None: + """Ensure `count` is sent as-is; only `None` falls back to 1 (in particular, `0` is preserved).""" + captured_requests: list[Request] = [] + + def capture_request(request: Request) -> Response: + captured_requests.append(request) + return Response(status=200, mimetype='application/json') + + httpserver.expect_request(_CHARGE_PATH, method='POST').respond_with_handler(capture_request) + + api_url = httpserver.url_for('/').removesuffix('/') + client = ApifyClient(token='test_token', api_url=api_url) + + client.run(_MOCKED_RUN_ID).charge(event_name='test-event', count=count) + + assert len(captured_requests) == 1 + body = _decode_body(captured_requests[0]) + assert body['count'] == expected + + +@pytest.mark.parametrize( + ('count', 'expected'), + [ + (None, 1), + (0, 0), + (1, 1), + (5, 5), + ], +) +async def test_run_charge_preserves_count_async( + httpserver: HTTPServer, + count: int | None, + expected: int, +) -> None: + """Async variant of `test_run_charge_preserves_count_sync`.""" + captured_requests: list[Request] = [] + + def capture_request(request: Request) -> Response: + captured_requests.append(request) + return Response(status=200, mimetype='application/json') + + httpserver.expect_request(_CHARGE_PATH, method='POST').respond_with_handler(capture_request) + + api_url = httpserver.url_for('/').removesuffix('/') + client = ApifyClientAsync(token='test_token', api_url=api_url) + + await client.run(_MOCKED_RUN_ID).charge(event_name='test-event', count=count) + + assert len(captured_requests) == 1 + body = _decode_body(captured_requests[0]) + assert body['count'] == expected