Skip to content
Open
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
71 changes: 71 additions & 0 deletions tests/e2e/test_scrapy/test_ppe_spider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from __future__ import annotations

from decimal import Decimal
from typing import TYPE_CHECKING

from ..._utils import poll_until_condition
from .conftest import get_scrapy_source_files

if TYPE_CHECKING:
from apify_client import ApifyClientAsync

from ..conftest import MakeActorFunction, RunActorFunction


async def test_ppe_spider_charges_pushed_items_within_budget(
make_actor: MakeActorFunction,
run_actor: RunActorFunction,
apify_client_async: ApifyClientAsync,
) -> None:
"""A pay-per-event Scrapy Actor charges the synthetic dataset-item event and stops pushing at the budget cap."""
actor = await make_actor(
label='scrapy-ppe',
source_files=get_scrapy_source_files('spider_basic.py', 'BasicSpider'),
additional_requirements=['scrapy>=2.14.0'],
)

await actor.update(
pricing_infos=[
{
'pricingModel': 'PAY_PER_EVENT',
'apifyMarginPercentage': 0.0,
'createdAt': '2024-01-01T00:00:00.000Z',
'startedAt': '2024-01-01T00:00:00.000Z',
'pricingPerEvent': {
'actorChargeEvents': {
'apify-default-dataset-item': {
'eventTitle': 'Default dataset item',
'eventPriceUsd': 0.05,
'eventDescription': 'One item written to the default dataset',
},
},
},
},
]
)

# The spider scrapes three products. At $0.05 an item, a $0.125 budget covers only two of them, so the
# third push has to be dropped and never charged. The budget deliberately does not divide evenly into the
# item price - hitting the cap exactly would let the platform auto-abort the run and race the clean exit.
run = await run_actor(actor, max_total_charge_usd=Decimal('0.125'))

assert run.status == 'SUCCEEDED'

expected_charges = {'apify-default-dataset-item': 2}

# The SDK only tracks the synthetic event internally - the platform derives the charges from dataset writes
# asynchronously, so they can lag behind the run reaching a terminal status. Refetch until they propagate.
charged_run = await poll_until_condition(
apify_client_async.run(run.id).get,
lambda refetched: refetched is not None and refetched.charged_event_counts == expected_charges,
timeout=120,
poll_interval=1,
)

assert charged_run is not None
assert charged_run.charged_event_counts == expected_charges

# `total` counts the whole dataset, unlike `count`, which only covers the items returned in this page and
# can still lag right after a run finishes.
items = await actor.last_run().dataset().list_items()
assert items.total == 2
Loading