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
45 changes: 40 additions & 5 deletions src/apify/_actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

from apify_client import ApifyClientAsync
from apify_shared.consts import ActorEnvVars, ActorExitCodes, ApifyEnvVars
from apify_shared.utils import ignore_docs, maybe_extract_enum_member_value
from apify_shared.utils import maybe_extract_enum_member_value
from crawlee import service_locator
from crawlee.events import (
Event,
Expand Down Expand Up @@ -54,9 +54,46 @@


@docs_name('Actor')
@docs_group('Classes')
@docs_group('Actor')
class _ActorType:
"""The class of `Actor`. Only make a new instance if you're absolutely sure you need to."""
"""The core class for building Actors on the Apify platform.

Actors are serverless programs running in the cloud that can perform anything from simple actions
(such as filling out a web form or sending an email) to complex operations (such as crawling an
entire website or removing duplicates from a large dataset). They are packaged as Docker containers
which accept well-defined JSON input, perform an action, and optionally produce well-defined output.

### References

- Apify platform documentation: https://docs.apify.com/platform/actors
- Actor whitepaper: https://whitepaper.actor/

### Usage

```python
import asyncio

import httpx
from apify import Actor
from bs4 import BeautifulSoup


async def main() -> None:
async with Actor:
actor_input = await Actor.get_input()
async with httpx.AsyncClient() as client:
response = await client.get(actor_input['url'])
soup = BeautifulSoup(response.content, 'html.parser')
data = {
'url': actor_input['url'],
'title': soup.title.string if soup.title else None,
}
await Actor.push_data(data)

if __name__ == '__main__':
asyncio.run(main())
```
"""

_is_rebooting = False
_is_any_instance_initialized = False
Expand Down Expand Up @@ -108,7 +145,6 @@ def __init__(

self._is_initialized = False

@ignore_docs
async def __aenter__(self) -> Self:
"""Initialize the Actor.

Expand All @@ -120,7 +156,6 @@ async def __aenter__(self) -> Self:
await self.init()
return self

@ignore_docs
async def __aexit__(
self,
_exc_type: type[BaseException] | None,
Expand Down
19 changes: 13 additions & 6 deletions src/apify/_charging.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

from pydantic import TypeAdapter

from apify_shared.utils import ignore_docs
from crawlee._utils.context import ensure_context

from apify._models import ActorRun, PricingModel
Expand All @@ -26,9 +25,18 @@
run_validator = TypeAdapter[ActorRun | None](ActorRun | None)


@docs_group('Interfaces')
@docs_group('Charging')
class ChargingManager(Protocol):
"""Provides fine-grained access to pay-per-event functionality."""
"""Provides fine-grained access to pay-per-event functionality.

The ChargingManager allows you to charge for specific events in your Actor when using
the pay-per-event pricing model. This enables precise cost control and transparent
billing for different operations within your Actor.

### References

- Apify platform documentation: https://docs.apify.com/platform/actors/publishing/monetize
"""

async def charge(self, event_name: str, count: int = 1) -> ChargeResult:
"""Charge for a specified number of events - sub-operations of the Actor.
Expand Down Expand Up @@ -57,7 +65,7 @@ def get_pricing_info(self) -> ActorPricingInfo:
"""


@docs_group('Data structures')
@docs_group('Charging')
@dataclass(frozen=True)
class ChargeResult:
"""Result of the `ChargingManager.charge` method."""
Expand All @@ -72,7 +80,7 @@ class ChargeResult:
"""How many events of each known type can still be charged within the limit."""


@docs_group('Data structures')
@docs_group('Charging')
@dataclass
class ActorPricingInfo:
"""Result of the `ChargingManager.get_pricing_info` method."""
Expand All @@ -90,7 +98,6 @@ class ActorPricingInfo:
"""Price of every known event type."""


@ignore_docs
class ChargingManagerImplementation(ChargingManager):
"""Implementation of the `ChargingManager` Protocol - this is only meant to be instantiated internally."""

Expand Down
2 changes: 1 addition & 1 deletion src/apify/_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def _transform_to_list(value: Any) -> list[str] | None:
return value if isinstance(value, list) else str(value).split(',')


@docs_group('Classes')
@docs_group('Configuration')
class Configuration(CrawleeConfiguration):
"""A class for specifying the configuration of an Actor.

Expand Down
6 changes: 0 additions & 6 deletions src/apify/_crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
from cryptography.hazmat.primitives.asymmetric import padding, rsa
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes

from apify_shared.utils import ignore_docs
from crawlee._utils.crypto import crypto_random_object_id

from apify._consts import ENCRYPTED_INPUT_VALUE_REGEXP, ENCRYPTED_JSON_VALUE_PREFIX, ENCRYPTED_STRING_VALUE_PREFIX
Expand All @@ -22,7 +21,6 @@
ENCRYPTION_AUTH_TAG_LENGTH = 16


@ignore_docs
def public_encrypt(value: str, *, public_key: rsa.RSAPublicKey) -> dict:
"""Encrypts the given value using AES cipher and the password for encryption using the public key.

Expand Down Expand Up @@ -66,7 +64,6 @@ def public_encrypt(value: str, *, public_key: rsa.RSAPublicKey) -> dict:
}


@ignore_docs
def private_decrypt(
encrypted_password: str,
encrypted_value: str,
Expand Down Expand Up @@ -118,7 +115,6 @@ def private_decrypt(
return decipher_bytes.decode('utf-8')


@ignore_docs
def load_private_key(private_key_file_base64: str, private_key_password: str) -> rsa.RSAPrivateKey:
private_key = serialization.load_pem_private_key(
base64.b64decode(private_key_file_base64.encode('utf-8')),
Expand All @@ -138,7 +134,6 @@ def _load_public_key(public_key_file_base64: str) -> rsa.RSAPublicKey:
return public_key


@ignore_docs
def decrypt_input_secrets(private_key: rsa.RSAPrivateKey, input_data: Any) -> Any:
"""Decrypt input secrets."""
if not isinstance(input_data, dict):
Expand Down Expand Up @@ -180,7 +175,6 @@ def encode_base62(num: int) -> str:
return res


@ignore_docs
def create_hmac_signature(secret_key: str, message: str) -> str:
"""Generate an HMAC signature and encodes it using Base62. Base62 encoding reduces the signature length.

Expand Down
12 changes: 6 additions & 6 deletions src/apify/_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from typing import TypeAlias


@docs_group('Data structures')
@docs_group('Actor')
class Webhook(BaseModel):
__model_config__ = ConfigDict(populate_by_name=True)

Expand All @@ -35,14 +35,14 @@ class Webhook(BaseModel):
] = None


@docs_group('Data structures')
@docs_group('Actor')
class ActorRunMeta(BaseModel):
__model_config__ = ConfigDict(populate_by_name=True)

origin: Annotated[MetaOrigin, Field()]


@docs_group('Data structures')
@docs_group('Actor')
class ActorRunStats(BaseModel):
__model_config__ = ConfigDict(populate_by_name=True)

Expand All @@ -63,7 +63,7 @@ class ActorRunStats(BaseModel):
compute_units: Annotated[float, Field(alias='computeUnits')]


@docs_group('Data structures')
@docs_group('Actor')
class ActorRunOptions(BaseModel):
__model_config__ = ConfigDict(populate_by_name=True)

Expand All @@ -74,7 +74,7 @@ class ActorRunOptions(BaseModel):
max_total_charge_usd: Annotated[Decimal | None, Field(alias='maxTotalChargeUsd')] = None


@docs_group('Data structures')
@docs_group('Actor')
class ActorRunUsage(BaseModel):
__model_config__ = ConfigDict(populate_by_name=True)

Expand All @@ -92,7 +92,7 @@ class ActorRunUsage(BaseModel):
proxy_serps: Annotated[float | None, Field(alias='PROXY_SERPS')] = None


@docs_group('Data structures')
@docs_group('Actor')
class ActorRun(BaseModel):
__model_config__ = ConfigDict(populate_by_name=True)

Expand Down
31 changes: 15 additions & 16 deletions src/apify/_platform_event_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,10 @@

from apify._configuration import Configuration


__all__ = ['EventManager', 'LocalEventManager', 'PlatformEventManager']


@docs_group('Data structures')
class PersistStateEvent(BaseModel):
name: Literal[Event.PERSIST_STATE]
data: Annotated[EventPersistStateData, Field(default_factory=lambda: EventPersistStateData(is_migrating=False))]


@docs_group('Data structures')
@docs_group('Event data')
class SystemInfoEventData(BaseModel):
mem_avg_bytes: Annotated[float, Field(alias='memAvgBytes')]
mem_current_bytes: Annotated[float, Field(alias='memCurrentBytes')]
Expand All @@ -64,31 +57,37 @@ def to_crawlee_format(self, dedicated_cpus: float) -> EventSystemInfoData:
)


@docs_group('Data structures')
@docs_group('Events')
class PersistStateEvent(BaseModel):
name: Literal[Event.PERSIST_STATE]
data: Annotated[EventPersistStateData, Field(default_factory=lambda: EventPersistStateData(is_migrating=False))]


@docs_group('Events')
class SystemInfoEvent(BaseModel):
name: Literal[Event.SYSTEM_INFO]
data: SystemInfoEventData


@docs_group('Data structures')
@docs_group('Events')
class MigratingEvent(BaseModel):
name: Literal[Event.MIGRATING]
data: Annotated[EventMigratingData, Field(default_factory=EventMigratingData)]


@docs_group('Data structures')
@docs_group('Events')
class AbortingEvent(BaseModel):
name: Literal[Event.ABORTING]
data: Annotated[EventAbortingData, Field(default_factory=EventAbortingData)]


@docs_group('Data structures')
@docs_group('Events')
class ExitEvent(BaseModel):
name: Literal[Event.EXIT]
data: Annotated[EventExitData, Field(default_factory=EventExitData)]


@docs_group('Data structures')
@docs_group('Events')
class EventWithoutData(BaseModel):
name: Literal[
Event.SESSION_RETIRED,
Expand All @@ -101,13 +100,13 @@ class EventWithoutData(BaseModel):
data: Any = None


@docs_group('Data structures')
@docs_group('Events')
class DeprecatedEvent(BaseModel):
name: Literal['cpuInfo']
data: Annotated[dict[str, Any], Field(default_factory=dict)]


@docs_group('Data structures')
@docs_group('Events')
class UnknownEvent(BaseModel):
name: str
data: Annotated[dict[str, Any], Field(default_factory=dict)]
Expand All @@ -120,7 +119,7 @@ class UnknownEvent(BaseModel):
)


@docs_group('Classes')
@docs_group('Event managers')
class PlatformEventManager(EventManager):
"""A class for managing Actor events.

Expand Down
7 changes: 2 additions & 5 deletions src/apify/_proxy_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import httpx

from apify_shared.consts import ApifyEnvVars
from apify_shared.utils import ignore_docs
from crawlee.proxy_configuration import ProxyConfiguration as CrawleeProxyConfiguration
from crawlee.proxy_configuration import ProxyInfo as CrawleeProxyInfo
from crawlee.proxy_configuration import _NewUrlFunction
Expand All @@ -28,7 +27,6 @@
SESSION_ID_MAX_LENGTH = 50


@ignore_docs
def is_url(url: str) -> bool:
"""Check if the given string is a valid URL."""
try:
Expand Down Expand Up @@ -69,7 +67,7 @@ def _check(
raise ValueError(f'{error_str} does not match pattern {pattern.pattern!r}')


@docs_group('Classes')
@docs_group('Configuration')
@dataclass
class ProxyInfo(CrawleeProxyInfo):
"""Provides information about a proxy connection that is used for requests."""
Expand All @@ -89,7 +87,7 @@ class ProxyInfo(CrawleeProxyInfo):
"""


@docs_group('Classes')
@docs_group('Configuration')
class ProxyConfiguration(CrawleeProxyConfiguration):
"""Configures a connection to a proxy server with the provided options.

Expand All @@ -104,7 +102,6 @@ class ProxyConfiguration(CrawleeProxyConfiguration):

_configuration: Configuration

@ignore_docs
def __init__(
self,
*,
Expand Down
14 changes: 13 additions & 1 deletion src/apify/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,19 @@ def is_running_in_ipython() -> bool:
return getattr(builtins, '__IPYTHON__', False)


GroupName = Literal['Classes', 'Abstract classes', 'Interfaces', 'Data structures', 'Errors', 'Functions']
# The order of the rendered API groups is defined in the website/docusaurus.config.js file.
GroupName = Literal[
'Actor',
'Charging',
'Configuration',
'Event data',
'Event managers',
'Events',
'Request loaders',
'Storage clients',
'Storage data',
'Storages',
]


def docs_group(group_name: GroupName) -> Callable: # noqa: ARG001
Expand Down
2 changes: 1 addition & 1 deletion src/apify/apify_storage_client/_apify_storage_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from apify._configuration import Configuration


@docs_group('Classes')
@docs_group('Storage clients')
class ApifyStorageClient(StorageClient):
"""A storage client implementation based on the Apify platform storage."""

Expand Down
Loading
Loading