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
1,349 changes: 1,083 additions & 266 deletions docs/docs.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/res/format_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
(r'\*\*Parameters\*\*\n\n \*\*', '**Parameters**\n\n * **'), # workaround for sphinx bug with single parameter not being rendered as list
(r'\(`(.*), optional`\)', r'(`\1`, *optional*)'), # workaround for bug formatting parameter types
# (r'\*\*\[\*\*(\w+)\*\*\]', r'[\1]'), # workaround for bug formatting parameter types
(r' \\\*', r' *'), # workaround for bug with nested lists in argument descriptions
(r'`(\w+)`\[`(\w+)`\]', r'`\1[\2]`'), # workaround for complex parameter types with backticks
(\
r'`Union`\[`str`, `int`, `float`, `bool`, `None`, `Dict`\[`str`, `Any`\], `List\[Any\]`\]', \
Expand Down
10 changes: 10 additions & 0 deletions src/apify_client/_consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ class ActorJobStatus(Enum):
ABORTED = 'ABORTED' # aborted by user


class ActorSourceType(Enum):
"""Available source types for actors."""

SOURCE_CODE = 'SOURCE_CODE' # Source code is a single JavaScript/Node.js file
SOURCE_FILES = 'SOURCE_FILES' # Source code is comprised of multiple files
GIT_REPO = 'GIT_REPO' # Source code is cloned from a Git repository
TARBALL = 'TARBALL' # Source code is downloaded using a tarball or Zip file
GITHUB_GIST = 'GITHUB_GIST' # Source code is taken from a GitHub Gist


class WebhookEventType(Enum):
"""Events that can trigger a webhook."""

Expand Down
18 changes: 16 additions & 2 deletions src/apify_client/_http_client.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import gzip
import json as jsonlib
import os
import sys
from http import HTTPStatus
Expand Down Expand Up @@ -36,14 +38,27 @@ def call(
url: str,
headers: Optional[Dict] = None,
params: Optional[Dict] = None,
data: Optional[str] = None,
data: Optional[Any] = None,
json: Optional[JSONSerializable] = None,
stream: Optional[bool] = None,
parse_response: Optional[bool] = True,
) -> requests.models.Response:
request_params = self._parse_params(params)
requests_session = self.requests_session

if not headers:
headers = {}

if json and not data:
data = jsonlib.dumps(json, ensure_ascii=False).encode('utf-8')
headers['Content-Type'] = 'application/json'

if isinstance(data, (str, bytes, bytearray)):
if isinstance(data, str):
data = data.encode('utf-8')
data = gzip.compress(data)
headers['Content-Encoding'] = 'gzip'

def _make_request(bail: Callable, attempt: int) -> requests.models.Response: # type: ignore[return]
try:
response = requests_session.request(
Expand All @@ -52,7 +67,6 @@ def _make_request(bail: Callable, attempt: int) -> requests.models.Response: #
headers=headers,
params=request_params,
data=data,
json=json,
stream=stream,
)
if response.status_code < 300:
Expand Down
53 changes: 28 additions & 25 deletions src/apify_client/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@
import time
from datetime import datetime, timezone
from http import HTTPStatus
from typing import Any, Callable, Dict, List, TypeVar, cast
from typing import Any, Callable, Dict, List, Optional, Tuple, TypeVar, cast

from ._errors import ApifyApiError
from ._types import JSONSerializable

PARSE_DATE_FIELDS_MAX_DEPTH = 3
PARSE_DATE_FIELDS_KEY_SUFFIX = 'At'
Expand Down Expand Up @@ -134,30 +133,19 @@ def _catch_not_found_or_throw(exc: ApifyApiError) -> None:
return None


def _encode_json_to_base64(data: JSONSerializable) -> bytes:
"""Encode JSON serializable data to base64 string."""
return base64.b64encode(json.dumps(data).encode("utf-8"))

def _encode_webhook_list_to_base64(webhooks: List[Dict]) -> bytes:
"""Encode a list of dictionaries representing webhooks to their base64-encoded representation for the API."""
data = []
for webhook in webhooks:
webhook_representation = {
'eventTypes': webhook['event_types'],
'requestUrl': webhook['request_url'],
}
if 'payload_template' in webhook:
webhook_representation['payloadTemplate'] = webhook['payload_template']
data.append(webhook_representation)

def _decode_base64_to_json(encoded_data: bytes) -> JSONSerializable:
"""Decode base64 string into JSONSerializable data.

>>> _decode_base64_to_json(_encode_json_to_base64(1))
1
>>> _decode_base64_to_json(_encode_json_to_base64(1.1))
1.1
>>> _decode_base64_to_json(_encode_json_to_base64("apify"))
'apify'
>>> _decode_base64_to_json(_encode_json_to_base64(True))
True
>>> _decode_base64_to_json(_encode_json_to_base64(None)) is None
True
>>> _decode_base64_to_json(_encode_json_to_base64([1, 2, 3]))
[1, 2, 3]
>>> _decode_base64_to_json(_encode_json_to_base64({"apify": "rocks"}))
{'apify': 'rocks'}
"""
return cast(JSONSerializable, json.loads(base64.b64decode(encoded_data).decode("utf-8")))
return base64.b64encode(json.dumps(data).encode("utf-8"))


def _filter_out_none_values(dictionary: Dict) -> Dict:
Expand Down Expand Up @@ -198,3 +186,18 @@ def _snake_case_to_camel_case(str_snake_case: str) -> str:
part.capitalize() if i > 0 else part
for i, part in enumerate(str_snake_case.split("_"))
])


def _encode_key_value_store_record_value(value: Any, content_type: Optional[str] = None) -> Tuple[Any, str]:
if not content_type:
if _is_file_or_bytes(value):
content_type = 'application/octet-stream'
elif isinstance(value, str):
content_type = 'text/plain; charset=utf-8'
else:
content_type = 'application/json; charset=utf-8'

if 'application/json' in content_type and not _is_file_or_bytes(value) and not isinstance(value, str):
value = json.dumps(value, ensure_ascii=False, indent=2).encode("utf-8")
Comment thread
fnesveda marked this conversation as resolved.
Resolved

return (value, content_type)
31 changes: 30 additions & 1 deletion src/apify_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from ._http_client import _HTTPClient
from .clients import (
ActorClient,
ActorCollectionClient,
BuildClient,
BuildCollectionClient,
DatasetClient,
Expand All @@ -11,6 +13,8 @@
LogClient,
RequestQueueClient,
RequestQueueCollectionClient,
RunClient,
RunCollectionClient,
ScheduleClient,
ScheduleCollectionClient,
TaskClient,
Expand Down Expand Up @@ -56,13 +60,26 @@ def __init__(

def _options(self) -> Dict:
return {
'root_client': self,
'base_url': self.base_url,
'http_client': self.http_client,
'params': {
'token': self.token,
},
}

def actor(self, actor_id: str) -> ActorClient:
"""Retrieve the sub-client for manipulating a single actor.

Args:
actor_id (str): ID of the actor to be manipulated
"""
return ActorClient(resource_id=actor_id, **self._options())

def actors(self) -> ActorCollectionClient:
"""Retrieve the sub-client for manipulating actors."""
return ActorCollectionClient(**self._options())

def build(self, build_id: str) -> BuildClient:
"""Retrieve the sub-client for manipulating a single actor build.

Expand All @@ -75,6 +92,18 @@ def builds(self) -> BuildCollectionClient:
"""Retrieve the sub-client for querying multiple builds of a user."""
return BuildCollectionClient(**self._options())

def run(self, run_id: str) -> RunClient:
"""Retrieve the sub-client for manipulating a single actor run.

Args:
run_id (str): ID of the actor run to be manipulated
"""
return RunClient(resource_id=run_id, **self._options())

def runs(self) -> RunCollectionClient:
"""Retrieve the sub-client for querying multiple actor runs of a user."""
return RunCollectionClient(**self._options())

def dataset(self, dataset_id: str) -> DatasetClient:
"""Retrieve the sub-client for manipulating a single dataset.

Expand Down Expand Up @@ -165,7 +194,7 @@ def task(self, task_id: str) -> TaskClient:
return TaskClient(resource_id=task_id, **self._options())

def tasks(self) -> TaskCollectionClient:
"""Retrieve the sub-client for retrieving tasks."""
"""Retrieve the sub-client for manipulating tasks."""
return TaskCollectionClient(**self._options())

def user(self, user_id: Optional[str] = None) -> UserClient:
Expand Down
12 changes: 12 additions & 0 deletions src/apify_client/clients/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
from .base import ActorJobBaseClient, BaseClient, ResourceClient, ResourceCollectionClient
from .resource_clients import (
ActorClient,
ActorCollectionClient,
ActorVersionClient,
ActorVersionCollectionClient,
BuildClient,
BuildCollectionClient,
DatasetClient,
Expand All @@ -9,6 +13,8 @@
LogClient,
RequestQueueClient,
RequestQueueCollectionClient,
RunClient,
RunCollectionClient,
ScheduleClient,
ScheduleCollectionClient,
TaskClient,
Expand All @@ -25,6 +31,10 @@
'BaseClient',
'ResourceClient',
'ResourceCollectionClient',
'ActorClient',
'ActorCollectionClient',
'ActorVersionClient',
'ActorVersionCollectionClient',
'BuildClient',
'BuildCollectionClient',
'DatasetClient',
Expand All @@ -34,6 +44,8 @@
'LogClient',
'RequestQueueClient',
'RequestQueueCollectionClient',
'RunClient',
'RunCollectionClient',
'ScheduleClient',
'ScheduleCollectionClient',
'TaskClient',
Expand Down
12 changes: 11 additions & 1 deletion src/apify_client/clients/base/base_client.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
from typing import Any, Dict, Optional
from __future__ import annotations

from typing import TYPE_CHECKING, Any, Dict, Optional

from ..._http_client import _HTTPClient
from ..._utils import _to_safe_id

# Conditional import only executed when type checking, otherwise we'd get circular dependency issues
if TYPE_CHECKING:
from ...client import ApifyClient
Comment thread
fnesveda marked this conversation as resolved.
Resolved


class BaseClient:
"""Base class for sub-clients."""
Expand All @@ -11,6 +17,7 @@ def __init__(
self,
*,
base_url: str,
root_client: ApifyClient,
http_client: _HTTPClient,
resource_id: Optional[str] = None,
resource_path: str,
Expand All @@ -20,6 +27,7 @@ def __init__(

Args:
base_url (str): Base URL of the API server
root_client (ApifyClient): The ApifyClient instance under which this resource client exists
http_client (_HTTPClient): The _HTTPClient instance to be used in this client
resource_id (str): ID of the manipulated resource, in case of a single-resource client
resource_path (str): Path to the resource's endpoint on the API server
Expand All @@ -29,6 +37,7 @@ def __init__(
raise ValueError('resource_path must not end with "/"')

self.base_url = base_url
self.root_client = root_client
self.http_client = http_client
self.params = params or {}
self.resource_path = resource_path
Expand All @@ -54,6 +63,7 @@ def _sub_resource_init_options(self, **kwargs: Any) -> Dict:
"base_url": self.url,
"http_client": self.http_client,
"params": self.params,
"root_client": self.root_client,
}

return {
Expand Down
24 changes: 18 additions & 6 deletions src/apify_client/clients/resource_clients/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
from .actor import ActorClient
from .actor_collection import ActorCollectionClient
from .actor_version import ActorVersionClient
from .actor_version_collection import ActorVersionCollectionClient
from .build import BuildClient
from .build_collection import BuildCollectionClient
from .dataset import DatasetClient
Expand All @@ -7,6 +11,8 @@
from .log import LogClient
from .request_queue import RequestQueueClient
from .request_queue_collection import RequestQueueCollectionClient
from .run import RunClient
from .run_collection import RunCollectionClient
from .schedule import ScheduleClient
from .schedule_collection import ScheduleCollectionClient
from .task import TaskClient
Expand All @@ -18,22 +24,28 @@
from .webhook_dispatch_collection import WebhookDispatchCollectionClient

__all__ = [
'ActorClient',
'ActorCollectionClient',
'ActorVersionClient',
'ActorVersionCollectionClient',
'RunClient',
'RunCollectionClient',
'BuildClient',
'BuildCollectionClient',
'DatasetClient',
'DatasetCollectionClient',
'KeyValueStoreClient',
'KeyValueStoreCollectionClient',
'LogClient',
'RequestQueueClient',
'RequestQueueCollectionClient',
'ScheduleClient',
'ScheduleCollectionClient',
'TaskClient',
'TaskCollectionClient',
'UserClient',
'LogClient',
'WebhookClient',
'WebhookCollectionClient',
'WebhookDispatchClient',
'WebhookDispatchCollectionClient',
'TaskClient',
'TaskCollectionClient',
'ScheduleClient',
'ScheduleCollectionClient',
'UserClient',
]
Loading