Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add charge method to the run client for "pay per event" #304

Merged
merged 12 commits into from
Dec 5, 2024
66 changes: 66 additions & 0 deletions src/apify_client/clients/resource_clients/run.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

import json
import time
from typing import Any

from apify_shared.utils import filter_out_none_values_recursively, ignore_docs, parse_date_fields
Expand Down Expand Up @@ -226,6 +228,38 @@ def log(self: RunClient) -> LogClient:
**self._sub_resource_init_options(resource_path='log'),
)

def charge(
self: RunClient,
event_name: str,
count: int | None = None,
idempotency_key: str | None = None,
) -> dict:
"""Charge for an event of a Pay-Per-Event Actor run.

https://docs.apify.com/api/v2#/reference/actor-runs/charge-run/charge-run

Returns:
dict: Status and message of the charge event.
"""
if not event_name:
raise ValueError('eventName is required for charging an event')
Jkuzz marked this conversation as resolved.
Show resolved Hide resolved

response = self.http_client.call(
url=self._url('charge'),
method='POST',
headers={
'idempotency-key': idempotency_key or f'{self.resource_id}-{event_name}-{int(time.time() * 1000)}',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if someone tries to charge for two occurences of one event twice in the same millisecond? Then the idempotency key will be the same and one charge will get ignored. Some unique key is needed here, I think a random string would be enough.

There's a crypto_random_object_id implementation in Crawlee for Python, perhaps we could move it to apify-shared-python and use it here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took the idempotency key generation from https://github.com/apify-store/contact-info/blob/master/code/src/charging_manager.ts but it could be specific to their actors 🤔 I can move crypto_random_object_id 👍

'content-type': 'application/json',
},
data=json.dumps(
{
'eventName': event_name,
'count': count or 1,
}
),
)
return parse_date_fields(pluck_data(response.json()))


class RunClientAsync(ActorJobBaseClientAsync):
"""Async sub-client for manipulating a single Actor run."""
Expand Down Expand Up @@ -440,3 +474,35 @@ def log(self: RunClientAsync) -> LogClientAsync:
return LogClientAsync(
**self._sub_resource_init_options(resource_path='log'),
)

async def charge(
self: RunClientAsync,
event_name: str,
count: int | None = None,
idempotency_key: str | None = None,
) -> dict:
"""Charge for an event of a Pay-Per-Event Actor run.

https://docs.apify.com/api/v2#/reference/actor-runs/charge-run/charge-run

Returns:
dict: Status and message of the charge event.
"""
if not event_name:
raise ValueError('eventName is required for charging an event')
Jkuzz marked this conversation as resolved.
Show resolved Hide resolved

response = await self.http_client.call(
url=self._url('charge'),
method='POST',
headers={
'idempotency-key': idempotency_key or f'{self.resource_id}-{event_name}-{int(time.time() * 1000)}',
'content-type': 'application/json',
},
data=json.dumps(
{
'eventName': event_name,
'count': count or 1,
}
),
)
return parse_date_fields(pluck_data(response.json()))
Loading