|
9 | 9 | from ._utils import get_random_resource_name, maybe_await |
10 | 10 | from apify_client._models import ( |
11 | 11 | Actor, |
| 12 | + ActorChargeEvent, |
12 | 13 | ActorShort, |
13 | 14 | Build, |
| 15 | + FlatPricePerMonthActorPricingInfo, |
14 | 16 | ListOfActors, |
15 | 17 | ListOfWebhooks, |
16 | 18 | PayPerEventActorPricingInfo, |
|
22 | 24 | if TYPE_CHECKING: |
23 | 25 | from apify_client import ApifyClient, ApifyClientAsync |
24 | 26 |
|
| 27 | +# Actor carrying pricing-info entries for every non-trivial variant: FLAT_PRICE_PER_MONTH, |
| 28 | +# both flat and tiered PRICE_PER_DATASET_ITEM, and tiered PAY_PER_EVENT with |
| 29 | +# `isPrimaryEvent` / `isOneTimeEvent` fields. |
| 30 | +ALL_PRICING_VARIANTS_ACTOR = 'apify/facebook-pages-scraper' |
| 31 | + |
25 | 32 |
|
26 | 33 | async def test_get_public_actor(client: ApifyClient | ApifyClientAsync) -> None: |
27 | 34 | """Test getting a public Actor by ID.""" |
@@ -368,40 +375,95 @@ async def test_actor_default_build_wait_for_finish(client: ApifyClient | ApifyCl |
368 | 375 | assert isinstance(build, Build) |
369 | 376 |
|
370 | 377 |
|
371 | | -async def test_get_actor_with_tiered_pricing(client: ApifyClient | ApifyClientAsync) -> None: |
372 | | - """Regression test for apify/apify-client-python#811. |
| 378 | +async def test_actor_get_parses_tiered_price_per_dataset_item(client: ApifyClient | ApifyClientAsync) -> None: |
| 379 | + """Test that actor.get() parses PRICE_PER_DATASET_ITEM entries with tieredPricing. |
373 | 380 |
|
374 | | - `apify/facebook-pages-scraper` historically returns `pricingInfos` entries that use tiered |
375 | | - pricing — both `PRICE_PER_DATASET_ITEM` with `tieredPricing` (instead of `pricePerUnitUsd`) |
376 | | - and `PAY_PER_EVENT` with `eventTieredPricingUsd` (instead of `eventPriceUsd`). Earlier |
377 | | - versions of the Pydantic models required the flat-price fields and rejected the response. |
| 381 | + Tiered-PPD actors return `tieredPricing: {FREE, BRONZE, SILVER, GOLD, PLATINUM, DIAMOND}` |
| 382 | + instead of a flat `pricePerUnitUsd`. |
378 | 383 | """ |
379 | | - actor = await maybe_await(client.actor('apify/facebook-pages-scraper').get()) |
| 384 | + actor = await maybe_await(client.actor(ALL_PRICING_VARIANTS_ACTOR).get()) |
380 | 385 | assert isinstance(actor, Actor) |
381 | | - assert actor.pricing_infos is not None |
| 386 | + assert actor.pricing_infos |
382 | 387 |
|
383 | | - tiered_ppr = [ |
384 | | - pi |
385 | | - for pi in actor.pricing_infos |
386 | | - if isinstance(pi, PricePerDatasetItemActorPricingInfo) and pi.tiered_pricing is not None |
387 | | - ] |
388 | | - assert tiered_ppr, 'expected at least one PRICE_PER_DATASET_ITEM entry with tiered_pricing' |
389 | | - tiered_pricing = tiered_ppr[0].tiered_pricing |
390 | | - assert tiered_pricing is not None |
391 | | - tiered_entry = next(iter(tiered_pricing.values())) |
392 | | - assert tiered_entry.tiered_price_per_unit_usd >= 0 |
393 | | - |
394 | | - tiered_ppe_events = [ |
395 | | - event |
396 | | - for pi in actor.pricing_infos |
397 | | - if isinstance(pi, PayPerEventActorPricingInfo) |
398 | | - and pi.pricing_per_event is not None |
399 | | - and pi.pricing_per_event.actor_charge_events is not None |
400 | | - for event in pi.pricing_per_event.actor_charge_events.values() |
401 | | - if event.event_tiered_pricing_usd is not None |
| 388 | + tiered_ppd_entries = [ |
| 389 | + info |
| 390 | + for info in actor.pricing_infos |
| 391 | + if isinstance(info, PricePerDatasetItemActorPricingInfo) and info.tiered_pricing is not None |
402 | 392 | ] |
403 | | - assert tiered_ppe_events, 'expected at least one PAY_PER_EVENT charge event with event_tiered_pricing_usd' |
404 | | - event_tiered_pricing = tiered_ppe_events[0].event_tiered_pricing_usd |
405 | | - assert event_tiered_pricing is not None |
406 | | - tiered_event_entry = next(iter(event_tiered_pricing.values())) |
407 | | - assert tiered_event_entry.tiered_event_price_usd >= 0 |
| 393 | + assert tiered_ppd_entries, ( |
| 394 | + f'{ALL_PRICING_VARIANTS_ACTOR} should have at least one tiered PRICE_PER_DATASET_ITEM entry — ' |
| 395 | + 'pick a different actor if pricing changed.' |
| 396 | + ) |
| 397 | + |
| 398 | + # Fixture-drift guard: tiered pricing is only meaningful when it has more than one tier |
| 399 | + # and the tiers actually differ in price. A degenerate single-tier or all-zero payload |
| 400 | + # would silently look like flat pricing. |
| 401 | + for entry in tiered_ppd_entries: |
| 402 | + assert entry.tiered_pricing is not None |
| 403 | + assert len(entry.tiered_pricing) >= 2, ( |
| 404 | + f'{ALL_PRICING_VARIANTS_ACTOR} tiered PPD entry has only {len(entry.tiered_pricing)} tier(s); ' |
| 405 | + 'expected multiple tiers (e.g. FREE/BRONZE/SILVER/GOLD/PLATINUM/DIAMOND).' |
| 406 | + ) |
| 407 | + distinct_prices = {t.tiered_price_per_unit_usd for t in entry.tiered_pricing.values()} |
| 408 | + assert len(distinct_prices) >= 2, ( |
| 409 | + f'{ALL_PRICING_VARIANTS_ACTOR} tiered PPD entry has all-identical prices ({distinct_prices}); ' |
| 410 | + 'tiers should differ.' |
| 411 | + ) |
| 412 | + |
| 413 | + |
| 414 | +async def test_actor_get_parses_tiered_pay_per_event(client: ApifyClient | ApifyClientAsync) -> None: |
| 415 | + """Test that actor.get() parses PAY_PER_EVENT events with eventTieredPricingUsd, |
| 416 | + isPrimaryEvent and isOneTimeEvent. |
| 417 | +
|
| 418 | + Tiered PPE events use `eventTieredPricingUsd` instead of `eventPriceUsd`, plus the |
| 419 | + `isPrimaryEvent` / `isOneTimeEvent` flags. |
| 420 | + """ |
| 421 | + actor = await maybe_await(client.actor(ALL_PRICING_VARIANTS_ACTOR).get()) |
| 422 | + assert isinstance(actor, Actor) |
| 423 | + assert actor.pricing_infos |
| 424 | + |
| 425 | + tiered_ppe_events: list[ActorChargeEvent] = [] |
| 426 | + for info in actor.pricing_infos: |
| 427 | + if not isinstance(info, PayPerEventActorPricingInfo): |
| 428 | + continue |
| 429 | + events = info.pricing_per_event.actor_charge_events or {} |
| 430 | + tiered_ppe_events.extend(event for event in events.values() if event.event_tiered_pricing_usd is not None) |
| 431 | + |
| 432 | + assert tiered_ppe_events, ( |
| 433 | + f'{ALL_PRICING_VARIANTS_ACTOR} should have at least one tiered PAY_PER_EVENT event — ' |
| 434 | + 'pick a different actor if pricing changed.' |
| 435 | + ) |
| 436 | + # Because every model uses `extra='allow'`, a regenerator that drops either alias would |
| 437 | + # silently absorb the JSON key into `model_extra`. Asserting the typed attribute is |
| 438 | + # populated catches that drift. |
| 439 | + assert any(event.is_primary_event is True for event in tiered_ppe_events), ( |
| 440 | + f'{ALL_PRICING_VARIANTS_ACTOR}: no tiered PPE event has is_primary_event == True. ' |
| 441 | + 'The isPrimaryEvent alias may have been dropped from the model.' |
| 442 | + ) |
| 443 | + assert any(event.is_one_time_event is not None for event in tiered_ppe_events), ( |
| 444 | + f'{ALL_PRICING_VARIANTS_ACTOR}: no tiered PPE event has is_one_time_event populated. ' |
| 445 | + 'The isOneTimeEvent alias may have been dropped from the model.' |
| 446 | + ) |
| 447 | + |
| 448 | + |
| 449 | +async def test_actor_pricing_infos_includes_expected_variants(client: ApifyClient | ApifyClientAsync) -> None: |
| 450 | + """Test that actor.get() parses pricing_infos for PPE, PPD and FLAT_PRICE_PER_MONTH variants. |
| 451 | +
|
| 452 | + Fixture sanity check. If the actor stops publishing one of these variants, the sibling |
| 453 | + tiered_* tests no longer exercise their respective code paths and this test fails loudly — |
| 454 | + pointing maintainers to pick a different fixture. |
| 455 | + """ |
| 456 | + actor = await maybe_await(client.actor(ALL_PRICING_VARIANTS_ACTOR).get()) |
| 457 | + assert isinstance(actor, Actor) |
| 458 | + assert actor.pricing_infos |
| 459 | + |
| 460 | + seen = {type(entry) for entry in actor.pricing_infos} |
| 461 | + expected = { |
| 462 | + PayPerEventActorPricingInfo, |
| 463 | + PricePerDatasetItemActorPricingInfo, |
| 464 | + FlatPricePerMonthActorPricingInfo, |
| 465 | + } |
| 466 | + missing = expected - seen |
| 467 | + assert not missing, ( |
| 468 | + f'{ALL_PRICING_VARIANTS_ACTOR} no longer carries pricing_infos for {missing}; pick a different fixture.' |
| 469 | + ) |
0 commit comments