From fde68cd2f09e74a088adb185159d5066b39f8427 Mon Sep 17 00:00:00 2001 From: Jan Kuzelik Date: Tue, 3 Dec 2024 14:20:44 +0100 Subject: [PATCH 01/12] Add charge to run client --- .../clients/resource_clients/run.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/apify_client/clients/resource_clients/run.py b/src/apify_client/clients/resource_clients/run.py index d835d28f..7f6f4975 100644 --- a/src/apify_client/clients/resource_clients/run.py +++ b/src/apify_client/clients/resource_clients/run.py @@ -1,6 +1,7 @@ from __future__ import annotations from typing import Any +import time from apify_shared.utils import filter_out_none_values_recursively, ignore_docs, parse_date_fields @@ -11,6 +12,7 @@ from apify_client.clients.resource_clients.log import LogClient, LogClientAsync from apify_client.clients.resource_clients.request_queue import RequestQueueClient, RequestQueueClientAsync +RUN_CHARGE_IDEMPOTENCY_HEADER = 'idempotency-key' class RunClient(ActorJobBaseClient): """Sub-client for manipulating a single Actor run.""" @@ -440,3 +442,37 @@ def log(self: RunClientAsync) -> LogClientAsync: return LogClientAsync( **self._sub_resource_init_options(resource_path='log'), ) + + async def charge( + self: RunClient, + eventName: str, + count: int | None = None, + idempotencyKey: str | None = None, + ) -> dict: + """Charge for an event of a Pay-Per-Event Actor run. + + TODO: docs url + + Returns: + dict: Status and message of the charge event. + """ + + if not eventName: + raise ValueError("eventName is required for charging an event") + + idempotencyKey = idempotencyKey or "{runId}-{eventName}-{timestamp}".format( + runId=self.resource_id, + eventName=eventName, + timestamp=int(time.time() * 1000), + ) + + response = await self.http_client.call( + url=self._url('charge'), + method='POST', + headers={RUN_CHARGE_IDEMPOTENCY_HEADER: idempotencyKey}, + data={ + 'eventName': eventName, + 'count': count or 1, + } + ) + return response From 037090f74fab42e9eaa51a490b4514c0b212fd70 Mon Sep 17 00:00:00 2001 From: Jan Kuzelik Date: Tue, 3 Dec 2024 16:17:28 +0100 Subject: [PATCH 02/12] Add sync method and make them work --- .../clients/resource_clients/run.py | 62 +++++++++++++------ 1 file changed, 43 insertions(+), 19 deletions(-) diff --git a/src/apify_client/clients/resource_clients/run.py b/src/apify_client/clients/resource_clients/run.py index 7f6f4975..a28a8b62 100644 --- a/src/apify_client/clients/resource_clients/run.py +++ b/src/apify_client/clients/resource_clients/run.py @@ -1,7 +1,8 @@ from __future__ import annotations -from typing import Any +import json import time +from typing import Any from apify_shared.utils import filter_out_none_values_recursively, ignore_docs, parse_date_fields @@ -12,7 +13,6 @@ from apify_client.clients.resource_clients.log import LogClient, LogClientAsync from apify_client.clients.resource_clients.request_queue import RequestQueueClient, RequestQueueClientAsync -RUN_CHARGE_IDEMPOTENCY_HEADER = 'idempotency-key' class RunClient(ActorJobBaseClient): """Sub-client for manipulating a single Actor run.""" @@ -228,6 +228,35 @@ 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. + + TODO: docs url + + Returns: + dict: Status and message of the charge event. + """ + if not event_name: + raise ValueError('eventName is required for charging an event') + + return 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, + }) + ) + class RunClientAsync(ActorJobBaseClientAsync): """Async sub-client for manipulating a single Actor run.""" @@ -445,9 +474,9 @@ def log(self: RunClientAsync) -> LogClientAsync: async def charge( self: RunClient, - eventName: str, + event_name: str, count: int | None = None, - idempotencyKey: str | None = None, + idempotency_key: str | None = None, ) -> dict: """Charge for an event of a Pay-Per-Event Actor run. @@ -456,23 +485,18 @@ async def charge( Returns: dict: Status and message of the charge event. """ + if not event_name: + raise ValueError('eventName is required for charging an event') - if not eventName: - raise ValueError("eventName is required for charging an event") - - idempotencyKey = idempotencyKey or "{runId}-{eventName}-{timestamp}".format( - runId=self.resource_id, - eventName=eventName, - timestamp=int(time.time() * 1000), - ) - - response = await self.http_client.call( + return await self.http_client.call( url=self._url('charge'), method='POST', - headers={RUN_CHARGE_IDEMPOTENCY_HEADER: idempotencyKey}, - data={ - 'eventName': eventName, + 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 response From 01b09ba165e221bc1c2b4e35498351d4a6601cac Mon Sep 17 00:00:00 2001 From: Jan Kuzelik Date: Tue, 3 Dec 2024 16:24:35 +0100 Subject: [PATCH 03/12] response.json --- src/apify_client/clients/resource_clients/run.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/apify_client/clients/resource_clients/run.py b/src/apify_client/clients/resource_clients/run.py index a28a8b62..d71bcf88 100644 --- a/src/apify_client/clients/resource_clients/run.py +++ b/src/apify_client/clients/resource_clients/run.py @@ -244,7 +244,7 @@ def charge( if not event_name: raise ValueError('eventName is required for charging an event') - return self.http_client.call( + response = self.http_client.call( url=self._url('charge'), method='POST', headers={ @@ -256,6 +256,7 @@ def charge( 'count': count or 1, }) ) + return response.json() class RunClientAsync(ActorJobBaseClientAsync): @@ -488,7 +489,7 @@ async def charge( if not event_name: raise ValueError('eventName is required for charging an event') - return await self.http_client.call( + response = await self.http_client.call( url=self._url('charge'), method='POST', headers={ @@ -500,3 +501,4 @@ async def charge( 'count': count or 1, }) ) + return response.json() From 7f18ac14f2eebbb46aafa0ffd07c09f1caff14ff Mon Sep 17 00:00:00 2001 From: Jan Kuzelik Date: Tue, 3 Dec 2024 16:36:04 +0100 Subject: [PATCH 04/12] format --- .../clients/resource_clients/run.py | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/src/apify_client/clients/resource_clients/run.py b/src/apify_client/clients/resource_clients/run.py index d71bcf88..46ffb685 100644 --- a/src/apify_client/clients/resource_clients/run.py +++ b/src/apify_client/clients/resource_clients/run.py @@ -229,11 +229,11 @@ def log(self: RunClient) -> LogClient: ) def charge( - self: RunClient, - event_name: str, - count: int | None = None, - idempotency_key: str | None = None, - ) -> dict: + 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. TODO: docs url @@ -251,10 +251,12 @@ def charge( '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, - }) + data=json.dumps( + { + 'eventName': event_name, + 'count': count or 1, + } + ), ) return response.json() @@ -474,11 +476,11 @@ def log(self: RunClientAsync) -> LogClientAsync: ) async def charge( - self: RunClient, - event_name: str, - count: int | None = None, - idempotency_key: str | None = None, - ) -> dict: + 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. TODO: docs url @@ -496,9 +498,11 @@ async def charge( '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, - }) + data=json.dumps( + { + 'eventName': event_name, + 'count': count or 1, + } + ), ) return response.json() From d912d6e801d8c32be32ecff0d2f4de01c954016f Mon Sep 17 00:00:00 2001 From: Jan Kuzelik Date: Tue, 3 Dec 2024 16:38:02 +0100 Subject: [PATCH 05/12] correct self class --- src/apify_client/clients/resource_clients/run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apify_client/clients/resource_clients/run.py b/src/apify_client/clients/resource_clients/run.py index 46ffb685..85157d16 100644 --- a/src/apify_client/clients/resource_clients/run.py +++ b/src/apify_client/clients/resource_clients/run.py @@ -476,7 +476,7 @@ def log(self: RunClientAsync) -> LogClientAsync: ) async def charge( - self: RunClient, + self: RunClientAsync, event_name: str, count: int | None = None, idempotency_key: str | None = None, From 25e7e00c028f48e40dfa7db7a0b3db8cb802724c Mon Sep 17 00:00:00 2001 From: Jan Kuzelik Date: Tue, 3 Dec 2024 16:57:19 +0100 Subject: [PATCH 06/12] Fix types, add todo url --- src/apify_client/clients/resource_clients/run.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/apify_client/clients/resource_clients/run.py b/src/apify_client/clients/resource_clients/run.py index 85157d16..e61a984b 100644 --- a/src/apify_client/clients/resource_clients/run.py +++ b/src/apify_client/clients/resource_clients/run.py @@ -237,6 +237,7 @@ def charge( """Charge for an event of a Pay-Per-Event Actor run. TODO: docs url + https://github.com/apify/apify-client-python/issues/305 Returns: dict: Status and message of the charge event. @@ -258,7 +259,7 @@ def charge( } ), ) - return response.json() + return parse_date_fields(pluck_data(response.json())) class RunClientAsync(ActorJobBaseClientAsync): @@ -484,6 +485,7 @@ async def charge( """Charge for an event of a Pay-Per-Event Actor run. TODO: docs url + https://github.com/apify/apify-client-python/issues/305 Returns: dict: Status and message of the charge event. @@ -505,4 +507,4 @@ async def charge( } ), ) - return response.json() + return parse_date_fields(pluck_data(response.json())) From 4b656a804db9ce5aeac307b4c04f878e25a02d6d Mon Sep 17 00:00:00 2001 From: Jan Kuzelik Date: Wed, 4 Dec 2024 11:14:50 +0100 Subject: [PATCH 07/12] Guess the docs URL --- src/apify_client/clients/resource_clients/run.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/apify_client/clients/resource_clients/run.py b/src/apify_client/clients/resource_clients/run.py index e61a984b..9987c1a1 100644 --- a/src/apify_client/clients/resource_clients/run.py +++ b/src/apify_client/clients/resource_clients/run.py @@ -236,8 +236,7 @@ def charge( ) -> dict: """Charge for an event of a Pay-Per-Event Actor run. - TODO: docs url - https://github.com/apify/apify-client-python/issues/305 + https://docs.apify.com/api/v2#/reference/actor-runs/charge-run/charge-run Returns: dict: Status and message of the charge event. @@ -484,8 +483,7 @@ async def charge( ) -> dict: """Charge for an event of a Pay-Per-Event Actor run. - TODO: docs url - https://github.com/apify/apify-client-python/issues/305 + https://docs.apify.com/api/v2#/reference/actor-runs/charge-run/charge-run Returns: dict: Status and message of the charge event. From aec0b3c9c56c5f923b3e357f6e9595d2029d1711 Mon Sep 17 00:00:00 2001 From: Jkuzz <68104924+Jkuzz@users.noreply.github.com> Date: Wed, 4 Dec 2024 13:37:56 +0100 Subject: [PATCH 08/12] Update src/apify_client/clients/resource_clients/run.py Co-authored-by: Vlada Dusek --- src/apify_client/clients/resource_clients/run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apify_client/clients/resource_clients/run.py b/src/apify_client/clients/resource_clients/run.py index 9987c1a1..91a36d9c 100644 --- a/src/apify_client/clients/resource_clients/run.py +++ b/src/apify_client/clients/resource_clients/run.py @@ -242,7 +242,7 @@ def charge( dict: Status and message of the charge event. """ if not event_name: - raise ValueError('eventName is required for charging an event') + raise ValueError('event_name is required for charging an event') response = self.http_client.call( url=self._url('charge'), From 0bf8a553c4e1c7fd47829cb634533542d227a273 Mon Sep 17 00:00:00 2001 From: Jkuzz <68104924+Jkuzz@users.noreply.github.com> Date: Wed, 4 Dec 2024 13:38:03 +0100 Subject: [PATCH 09/12] Update src/apify_client/clients/resource_clients/run.py Co-authored-by: Vlada Dusek --- src/apify_client/clients/resource_clients/run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apify_client/clients/resource_clients/run.py b/src/apify_client/clients/resource_clients/run.py index 91a36d9c..f1021669 100644 --- a/src/apify_client/clients/resource_clients/run.py +++ b/src/apify_client/clients/resource_clients/run.py @@ -489,7 +489,7 @@ async def charge( dict: Status and message of the charge event. """ if not event_name: - raise ValueError('eventName is required for charging an event') + raise ValueError('event_name is required for charging an event') response = await self.http_client.call( url=self._url('charge'), From f074945214bc00dffde7c9d6098b2e2a57b4d94b Mon Sep 17 00:00:00 2001 From: Jan Kuzelik Date: Thu, 5 Dec 2024 12:12:51 +0100 Subject: [PATCH 10/12] Add suffix to idempotency key --- src/apify_client/clients/resource_clients/run.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/apify_client/clients/resource_clients/run.py b/src/apify_client/clients/resource_clients/run.py index f1021669..7f499d6f 100644 --- a/src/apify_client/clients/resource_clients/run.py +++ b/src/apify_client/clients/resource_clients/run.py @@ -1,6 +1,8 @@ from __future__ import annotations import json +import random +import string import time from typing import Any @@ -491,11 +493,15 @@ async def charge( if not event_name: raise ValueError('event_name is required for charging an event') + idempotency_key = idempotency_key or ( + f'{self.resource_id}-{event_name}-{int(time.time() * 1000)}-{''.join(random.choices(string.ascii_letters + string.digits, k=6))}' + ) + 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)}', + 'idempotency-key': idempotency_key, 'content-type': 'application/json', }, data=json.dumps( From 58d2674cfd5223a3a0a7255fdf50692d3277fc42 Mon Sep 17 00:00:00 2001 From: Jan Kuzelik Date: Thu, 5 Dec 2024 13:08:15 +0100 Subject: [PATCH 11/12] fix f string --- src/apify_client/clients/resource_clients/run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apify_client/clients/resource_clients/run.py b/src/apify_client/clients/resource_clients/run.py index 7f499d6f..4a6530df 100644 --- a/src/apify_client/clients/resource_clients/run.py +++ b/src/apify_client/clients/resource_clients/run.py @@ -494,7 +494,7 @@ async def charge( raise ValueError('event_name is required for charging an event') idempotency_key = idempotency_key or ( - f'{self.resource_id}-{event_name}-{int(time.time() * 1000)}-{''.join(random.choices(string.ascii_letters + string.digits, k=6))}' + f'{self.resource_id}-{event_name}-{int(time.time() * 1000)}-{"".join(random.choices(string.ascii_letters + string.digits, k=6))}' ) response = await self.http_client.call( From f3ddf5945fa49ad6d3707a6acd9c17e8e92f098e Mon Sep 17 00:00:00 2001 From: Jan Kuzelik Date: Thu, 5 Dec 2024 13:13:35 +0100 Subject: [PATCH 12/12] Also change sync method --- src/apify_client/clients/resource_clients/run.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/apify_client/clients/resource_clients/run.py b/src/apify_client/clients/resource_clients/run.py index 4a6530df..73263c1c 100644 --- a/src/apify_client/clients/resource_clients/run.py +++ b/src/apify_client/clients/resource_clients/run.py @@ -246,11 +246,15 @@ def charge( if not event_name: raise ValueError('event_name is required for charging an event') + idempotency_key = idempotency_key or ( + f'{self.resource_id}-{event_name}-{int(time.time() * 1000)}-{"".join(random.choices(string.ascii_letters + string.digits, k=6))}' + ) + 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)}', + 'idempotency-key': idempotency_key, 'content-type': 'application/json', }, data=json.dumps(