Skip to content

Commit fbff42d

Browse files
committed
Update existing tests, add draft of the function
1 parent f33cbe0 commit fbff42d

File tree

2 files changed

+67
-33
lines changed

2 files changed

+67
-33
lines changed

src/apify_client/clients/resource_clients/key_value_store.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from typing import TYPE_CHECKING, Any
77
from urllib.parse import urlencode, urlparse, urlunparse
88

9-
from apify_shared.utils import create_storage_content_signature
9+
from apify_shared.utils import create_hmac_signature, create_storage_content_signature
1010

1111
from apify_client._utils import (
1212
catch_not_found_or_throw,
@@ -267,6 +267,29 @@ def delete_record(self, key: str) -> None:
267267
timeout_secs=_SMALL_TIMEOUT,
268268
)
269269

270+
def get_record_public_rul(self, key: str) -> str:
271+
"""Generate a URL that can be used to access key-value store record.
272+
273+
If the client has permission to access the key-value store's URL signing key, the URL will include a signature
274+
to verify its authenticity.
275+
276+
Args:
277+
key: The key for which the URL should be generated.
278+
279+
Returns:
280+
A public URL that can be used to access the value of the given key in the KVS.
281+
"""
282+
if self.resource_id is None:
283+
raise ValueError('resource_id cannot be None when generating a public URL')
284+
285+
public_url = f'{self._api_public_base_url}/key-value-stores/{self.resource_id}/records/{key}'
286+
metadata = self.get_metadata()
287+
288+
if metadata.url_signing_secret_key is not None:
289+
public_url = public_url.with_query(signature=create_hmac_signature(metadata.url_signing_secret_key, key))
290+
291+
return str(public_url)
292+
270293
def create_keys_public_url(
271294
self,
272295
*,

tests/integration/test_key_value_store.py

Lines changed: 43 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,40 @@
11
from __future__ import annotations
22

3+
import json
34
from unittest import mock
45
from unittest.mock import Mock
56

67
import impit
8+
import pytest
79

810
from integration.integration_test_utils import parametrized_api_urls, random_resource_name
911

1012
from apify_client import ApifyClient, ApifyClientAsync
1113
from apify_client.client import DEFAULT_API_URL
1214

13-
MOCKED_API_KVS_RESPONSE = """{
14-
"data": {
15-
"id": "someID",
16-
"name": "name",
17-
"userId": "userId",
18-
"createdAt": "2025-09-11T08:48:51.806Z",
19-
"modifiedAt": "2025-09-11T08:48:51.806Z",
20-
"accessedAt": "2025-09-11T08:48:51.806Z",
21-
"actId": null,
22-
"actRunId": null,
23-
"schema": null,
24-
"stats": {
25-
"readCount": 0,
26-
"writeCount": 0,
27-
"deleteCount": 0,
28-
"listCount": 0,
29-
"storageBytes": 0
30-
},
31-
"consoleUrl": "https://console.apify.com/storage/key-value-stores/someID",
32-
"keysPublicUrl": "https://api.apify.com/v2/key-value-stores/someID/keys",
33-
"generalAccess": "FOLLOW_USER_SETTING",
34-
"urlSigningSecretKey": "urlSigningSecretKey"
35-
}
36-
}"""
15+
16+
def _get_mocked_api_kvs_response(signing_key: str | None = None) -> str:
17+
response_data = {
18+
'data': {
19+
'id': 'someID',
20+
'name': 'name',
21+
'userId': 'userId',
22+
'createdAt': '2025-09-11T08:48:51.806Z',
23+
'modifiedAt': '2025-09-11T08:48:51.806Z',
24+
'accessedAt': '2025-09-11T08:48:51.806Z',
25+
'actId': None,
26+
'actRunId': None,
27+
'schema': None,
28+
'stats': {'readCount': 0, 'writeCount': 0, 'deleteCount': 0, 'listCount': 0, 'storageBytes': 0},
29+
'consoleUrl': 'https://console.apify.com/storage/key-value-stores/someID',
30+
'keysPublicUrl': 'https://api.apify.com/v2/key-value-stores/someID/keys',
31+
'generalAccess': 'FOLLOW_USER_SETTING',
32+
}
33+
}
34+
if signing_key:
35+
response_data['data']['urlSigningSecretKey'] = signing_key
36+
37+
return json.dumps(response_data)
3738

3839

3940
class TestKeyValueStoreSync:
@@ -73,17 +74,22 @@ def test_key_value_store_should_create_public_keys_non_expiring_url(self, apify_
7374
store.delete()
7475
assert apify_client.key_value_store(created_store['id']).get() is None
7576

77+
@pytest.mark.parametrize('signature', [None, 'custom-signature'])
7678
@parametrized_api_urls
77-
def test_public_url(self, api_token: str, api_url: str, api_public_url: str) -> None:
79+
def test_public_url(self, api_token: str, api_url: str, api_public_url: str, signature: str) -> None:
7880
apify_client = ApifyClient(token=api_token, api_url=api_url, api_public_url=api_public_url)
7981
kvs = apify_client.key_value_store('someID')
8082

8183
# Mock the API call to return predefined response
82-
with mock.patch.object(apify_client.http_client, 'call', return_value=Mock(text=MOCKED_API_KVS_RESPONSE)):
84+
with mock.patch.object(
85+
apify_client.http_client,
86+
'call',
87+
return_value=Mock(text=_get_mocked_api_kvs_response(signing_key=signature)),
88+
):
8389
public_url = kvs.create_keys_public_url()
90+
expected_signature = f'?signature={public_url.split("signature=")[1]}' if signature else ''
8491
assert public_url == (
85-
f'{(api_public_url or DEFAULT_API_URL).strip("/")}/v2/key-value-stores/'
86-
f'someID/keys?signature={public_url.split("signature=")[1]}'
92+
f'{(api_public_url or DEFAULT_API_URL).strip("/")}/v2/key-value-stores/someID/keys{expected_signature}'
8793
)
8894

8995

@@ -130,15 +136,20 @@ async def test_key_value_store_should_create_public_keys_non_expiring_url(
130136
await store.delete()
131137
assert await apify_client_async.key_value_store(created_store['id']).get() is None
132138

139+
@pytest.mark.parametrize('signature', [None, 'custom-signature'])
133140
@parametrized_api_urls
134-
async def test_public_url(self, api_token: str, api_url: str, api_public_url: str) -> None:
141+
async def test_public_url(self, api_token: str, api_url: str, api_public_url: str, signature: str) -> None:
135142
apify_client = ApifyClientAsync(token=api_token, api_url=api_url, api_public_url=api_public_url)
136143
kvs = apify_client.key_value_store('someID')
137144

138145
# Mock the API call to return predefined response
139-
with mock.patch.object(apify_client.http_client, 'call', return_value=Mock(text=MOCKED_API_KVS_RESPONSE)):
146+
with mock.patch.object(
147+
apify_client.http_client,
148+
'call',
149+
return_value=Mock(text=_get_mocked_api_kvs_response(signing_key=signature)),
150+
):
140151
public_url = await kvs.create_keys_public_url()
152+
expected_signature = f'?signature={public_url.split("signature=")[1]}' if signature else ''
141153
assert public_url == (
142-
f'{(api_public_url or DEFAULT_API_URL).strip("/")}/v2/key-value-stores/'
143-
f'someID/keys?signature={public_url.split("signature=")[1]}'
154+
f'{(api_public_url or DEFAULT_API_URL).strip("/")}/v2/key-value-stores/someID/keys{expected_signature}'
144155
)

0 commit comments

Comments
 (0)