diff --git a/src/apify/_charging.py b/src/apify/_charging.py index 68182443..0a6a95fc 100644 --- a/src/apify/_charging.py +++ b/src/apify/_charging.py @@ -7,7 +7,7 @@ from decimal import Decimal from typing import TYPE_CHECKING, Annotated, Literal, Protocol, TypedDict -from pydantic import BaseModel, ConfigDict, Field +from pydantic import Field import apify_client._models as _client_models from apify_client._models import ActorChargeEvent as ClientActorChargeEvent @@ -28,14 +28,17 @@ from apify._configuration import Configuration -PricingModel = Literal['PAY_PER_EVENT', 'PRICE_PER_DATASET_ITEM', 'FLAT_PRICE_PER_MONTH', 'FREE'] -"""Pricing model for an Actor.""" +charging_manager_ctx: ContextVar[ChargingManager | None] = ContextVar('charging_manager_ctx', default=None) +"""Holds the current `ChargingManager` instance, if any. + +Allows PPE-aware dataset clients to access the charging manager without needing to pass it explicitly. +""" DEFAULT_DATASET_ITEM_EVENT = 'apify-default-dataset-item' +"""Name of the synthetic event charged for each item pushed to the default dataset.""" -# Context variable to hold the current `ChargingManager` instance, if any. This allows PPE-aware dataset clients to -# access the charging manager without needing to pass it explicitly. -charging_manager_ctx: ContextVar[ChargingManager | None] = ContextVar('charging_manager_ctx', default=None) +PricingModel = Literal['PAY_PER_EVENT', 'PRICE_PER_DATASET_ITEM', 'FLAT_PRICE_PER_MONTH', 'FREE'] +"""Pricing model for an Actor.""" _ensure_context = ensure_context('active') @@ -49,48 +52,91 @@ # `apify-client` instance) flows through the same code paths without conversion. -class _RelaxedPricingMetadata(BaseModel): - """Mixin relaxing the `CommonActorPricingInfo` metadata fields the platform env var omits.""" - - model_config = ConfigDict(populate_by_name=True, extra='allow') - - apify_margin_percentage: Annotated[float | None, Field(alias='apifyMarginPercentage')] = None - created_at: Annotated[datetime | None, Field(alias='createdAt')] = None - started_at: Annotated[datetime | None, Field(alias='startedAt')] = None - - @docs_group('Charging') class ActorChargeEvent(ClientActorChargeEvent): - # `event_description` is required in apify-client but omitted from the env var. + """Definition of a single chargeable event in the pay-per-event pricing model.""" + event_description: Annotated[str | None, Field(alias='eventDescription')] = None + """Human-readable description of the event. + + Required in apify-client but omitted from the env var, so it is relaxed to optional. + """ @docs_group('Charging') class PricingPerEvent(ClientPricingPerEvent): + """Pay-per-event pricing details - the chargeable events and their prices.""" + actor_charge_events: Annotated[dict[str, ActorChargeEvent] | None, Field(alias='actorChargeEvents')] = None + """Mapping of event name to its charge definition.""" @docs_group('Charging') -class FreeActorPricingInfo(_RelaxedPricingMetadata, ClientFree): - pass +class FreeActorPricingInfo(ClientFree): + """Pricing info for an Actor offered free of charge.""" + + apify_margin_percentage: Annotated[float | None, Field(alias='apifyMarginPercentage')] = None + """Apify's margin on the price, as a percentage.""" + + created_at: Annotated[datetime | None, Field(alias='createdAt')] = None + """Timestamp when this pricing info was created.""" + + started_at: Annotated[datetime | None, Field(alias='startedAt')] = None + """Timestamp when this pricing became effective.""" @docs_group('Charging') -class FlatPricePerMonthActorPricingInfo(_RelaxedPricingMetadata, ClientFlatPricePerMonth): +class FlatPricePerMonthActorPricingInfo(ClientFlatPricePerMonth): + """Pricing info for an Actor billed at a flat monthly price.""" + + apify_margin_percentage: Annotated[float | None, Field(alias='apifyMarginPercentage')] = None + """Apify's margin on the price, as a percentage.""" + + created_at: Annotated[datetime | None, Field(alias='createdAt')] = None + """Timestamp when this pricing info was created.""" + + started_at: Annotated[datetime | None, Field(alias='startedAt')] = None + """Timestamp when this pricing became effective.""" + trial_minutes: Annotated[int | None, Field(alias='trialMinutes')] = None + """Length of the free trial period, in minutes.""" + price_per_unit_usd: Annotated[float | None, Field(alias='pricePerUnitUsd')] = None + """Price per unit, in USD.""" @docs_group('Charging') -class PricePerDatasetItemActorPricingInfo(_RelaxedPricingMetadata, ClientPricePerDatasetItem): +class PricePerDatasetItemActorPricingInfo(ClientPricePerDatasetItem): + """Pricing info for an Actor billed per dataset item produced.""" + + apify_margin_percentage: Annotated[float | None, Field(alias='apifyMarginPercentage')] = None + """Apify's margin on the price, as a percentage.""" + + created_at: Annotated[datetime | None, Field(alias='createdAt')] = None + """Timestamp when this pricing info was created.""" + + started_at: Annotated[datetime | None, Field(alias='startedAt')] = None + """Timestamp when this pricing became effective.""" + unit_name: Annotated[str | None, Field(alias='unitName')] = None - # `price_per_unit_usd` is already optional in apify-client - inherited. + """Name of the billed unit.""" @docs_group('Charging') -class PayPerEventActorPricingInfo(_RelaxedPricingMetadata, ClientPayPerEvent): - # Re-typed to the relaxed element so an omitted `eventDescription` validates; the field stays required. +class PayPerEventActorPricingInfo(ClientPayPerEvent): + """Pricing info for an Actor billed per charged event.""" + + apify_margin_percentage: Annotated[float | None, Field(alias='apifyMarginPercentage')] = None + """Apify's margin on the price, as a percentage.""" + + created_at: Annotated[datetime | None, Field(alias='createdAt')] = None + """Timestamp when this pricing info was created.""" + + started_at: Annotated[datetime | None, Field(alias='startedAt')] = None + """Timestamp when this pricing became effective.""" + pricing_per_event: Annotated[PricingPerEvent, Field(alias='pricingPerEvent')] + """The pay-per-event pricing details.""" ActorPricingInfoModel = ClientFree | ClientFlatPricePerMonth | ClientPricePerDatasetItem | ClientPayPerEvent diff --git a/src/apify/events/_types.py b/src/apify/events/_types.py index d702fa32..e5d16011 100644 --- a/src/apify/events/_types.py +++ b/src/apify/events/_types.py @@ -27,14 +27,31 @@ @docs_group('Event data') class SystemInfoEventData(BaseModel): + """Resource usage metrics carried by a `systemInfo` event.""" + mem_avg_bytes: Annotated[float, Field(alias='memAvgBytes')] + """Average memory usage over the measured interval, in bytes.""" + mem_current_bytes: Annotated[float, Field(alias='memCurrentBytes')] + """Current memory usage, in bytes.""" + mem_max_bytes: Annotated[float, Field(alias='memMaxBytes')] + """Peak memory usage observed so far, in bytes.""" + cpu_avg_usage: Annotated[float, Field(alias='cpuAvgUsage')] + """Average CPU usage over the measured interval, in percent.""" + cpu_max_usage: Annotated[float, Field(alias='cpuMaxUsage')] + """Peak CPU usage observed so far, in percent.""" + cpu_current_usage: Annotated[float, Field(alias='cpuCurrentUsage')] + """Current CPU usage, in percent.""" + is_cpu_overloaded: Annotated[bool, Field(alias='isCpuOverloaded')] + """Whether the CPU is currently overloaded.""" + created_at: Annotated[datetime, Field(alias='createdAt')] + """Timestamp when the metrics were collected.""" def to_crawlee_format(self, dedicated_cpus: float) -> EventSystemInfoData: return EventSystemInfoData.model_validate( @@ -54,36 +71,63 @@ def to_crawlee_format(self, dedicated_cpus: float) -> EventSystemInfoData: @docs_group('Events') class PersistStateEvent(BaseModel): + """A `persistState` event instructing the Actor to persist its state.""" + name: Literal[Event.PERSIST_STATE] + """The event name.""" + data: Annotated[EventPersistStateData, Field(default_factory=lambda: EventPersistStateData(is_migrating=False))] + """The event payload.""" @docs_group('Events') class SystemInfoEvent(BaseModel): + """A `systemInfo` event carrying the Actor's resource usage metrics.""" + name: Literal[Event.SYSTEM_INFO] + """The event name.""" + data: SystemInfoEventData + """The event payload.""" @docs_group('Events') class MigratingEvent(BaseModel): + """A `migrating` event signalling the Actor is about to be migrated to another host.""" + name: Literal[Event.MIGRATING] + """The event name.""" + data: Annotated[EventMigratingData, Field(default_factory=EventMigratingData)] + """The event payload.""" @docs_group('Events') class AbortingEvent(BaseModel): + """An `aborting` event signalling the Actor run is being aborted.""" + name: Literal[Event.ABORTING] + """The event name.""" + data: Annotated[EventAbortingData, Field(default_factory=EventAbortingData)] + """The event payload.""" @docs_group('Events') class ExitEvent(BaseModel): + """An `exit` event signalling the Actor process is about to exit.""" + name: Literal[Event.EXIT] + """The event name.""" + data: Annotated[EventExitData, Field(default_factory=EventExitData)] + """The event payload.""" @docs_group('Events') class EventWithoutData(BaseModel): + """A framework-level event that carries no payload (e.g. browser and page lifecycle events).""" + name: Literal[ Event.SESSION_RETIRED, Event.BROWSER_LAUNCHED, @@ -92,19 +136,32 @@ class EventWithoutData(BaseModel): Event.PAGE_CREATED, Event.PAGE_CLOSED, ] + """The event name.""" + data: Any = None + """The event payload, always empty for this event.""" @docs_group('Events') class DeprecatedEvent(BaseModel): + """A deprecated event kept for backward compatibility (e.g. `cpuInfo`).""" + name: Literal['cpuInfo'] + """The event name.""" + data: Annotated[dict[str, Any], Field(default_factory=dict)] + """The event payload.""" @docs_group('Events') class UnknownEvent(BaseModel): + """A fallback for any event whose name is not recognized by the SDK.""" + name: str + """The event name.""" + data: Annotated[dict[str, Any], Field(default_factory=dict)] + """The event payload.""" EventMessage = PersistStateEvent | SystemInfoEvent | MigratingEvent | AbortingEvent | ExitEvent | EventWithoutData