Skip to content

Commit b999bc8

Browse files
committed
Finalize for kvs
1 parent fbff42d commit b999bc8

File tree

2 files changed

+82
-15
lines changed

2 files changed

+82
-15
lines changed

src/apify_client/clients/resource_clients/key_value_store.py

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ def delete_record(self, key: str) -> None:
267267
timeout_secs=_SMALL_TIMEOUT,
268268
)
269269

270-
def get_record_public_rul(self, key: str) -> str:
270+
def get_record_public_url(self, key: str) -> str:
271271
"""Generate a URL that can be used to access key-value store record.
272272
273273
If the client has permission to access the key-value store's URL signing key, the URL will include a signature
@@ -282,13 +282,20 @@ def get_record_public_rul(self, key: str) -> str:
282282
if self.resource_id is None:
283283
raise ValueError('resource_id cannot be None when generating a public URL')
284284

285-
public_url = f'{self._api_public_base_url}/key-value-stores/{self.resource_id}/records/{key}'
286-
metadata = self.get_metadata()
285+
metadata = self.get()
287286

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))
287+
request_params = self._params()
290288

291-
return str(public_url)
289+
if metadata and 'urlSigningSecretKey' in metadata:
290+
request_params['signature'] = create_hmac_signature(metadata['urlSigningSecretKey'], key)
291+
292+
key_public_url = urlparse(self._url(f'records/{key}', public=True))
293+
filtered_params = {k: v for k, v in request_params.items() if v is not None}
294+
295+
if filtered_params:
296+
key_public_url = key_public_url._replace(query=urlencode(filtered_params))
297+
298+
return urlunparse(key_public_url)
292299

293300
def create_keys_public_url(
294301
self,
@@ -313,7 +320,7 @@ def create_keys_public_url(
313320
Returns:
314321
The public key-value store keys URL.
315322
"""
316-
store = self.get()
323+
metadata = self.get()
317324

318325
request_params = self._params(
319326
limit=limit,
@@ -322,10 +329,10 @@ def create_keys_public_url(
322329
prefix=prefix,
323330
)
324331

325-
if store and 'urlSigningSecretKey' in store:
332+
if metadata and 'urlSigningSecretKey' in metadata:
326333
signature = create_storage_content_signature(
327-
resource_id=store['id'],
328-
url_signing_secret_key=store['urlSigningSecretKey'],
334+
resource_id=metadata['id'],
335+
url_signing_secret_key=metadata['urlSigningSecretKey'],
329336
expires_in_millis=expires_in_secs * 1000 if expires_in_secs is not None else None,
330337
)
331338
request_params['signature'] = signature
@@ -578,6 +585,36 @@ async def delete_record(self, key: str) -> None:
578585
timeout_secs=_SMALL_TIMEOUT,
579586
)
580587

588+
async def get_record_public_url(self, key: str) -> str:
589+
"""Generate a URL that can be used to access key-value store record.
590+
591+
If the client has permission to access the key-value store's URL signing key, the URL will include a signature
592+
to verify its authenticity.
593+
594+
Args:
595+
key: The key for which the URL should be generated.
596+
597+
Returns:
598+
A public URL that can be used to access the value of the given key in the KVS.
599+
"""
600+
if self.resource_id is None:
601+
raise ValueError('resource_id cannot be None when generating a public URL')
602+
603+
metadata = await self.get()
604+
605+
request_params = self._params()
606+
607+
if metadata and 'urlSigningSecretKey' in metadata:
608+
request_params['signature'] = create_hmac_signature(metadata['urlSigningSecretKey'], key)
609+
610+
key_public_url = urlparse(self._url(f'records/{key}', public=True))
611+
filtered_params = {k: v for k, v in request_params.items() if v is not None}
612+
613+
if filtered_params:
614+
key_public_url = key_public_url._replace(query=urlencode(filtered_params))
615+
616+
return urlunparse(key_public_url)
617+
581618
async def create_keys_public_url(
582619
self,
583620
*,
@@ -601,7 +638,7 @@ async def create_keys_public_url(
601638
Returns:
602639
The public key-value store keys URL.
603640
"""
604-
store = await self.get()
641+
metadata = await self.get()
605642

606643
keys_public_url = urlparse(self._url('keys'))
607644

@@ -612,10 +649,10 @@ async def create_keys_public_url(
612649
prefix=prefix,
613650
)
614651

615-
if store and 'urlSigningSecretKey' in store:
652+
if metadata and 'urlSigningSecretKey' in metadata:
616653
signature = create_storage_content_signature(
617-
resource_id=store['id'],
618-
url_signing_secret_key=store['urlSigningSecretKey'],
654+
resource_id=metadata['id'],
655+
url_signing_secret_key=metadata['urlSigningSecretKey'],
619656
expires_in_millis=expires_in_secs * 1000 if expires_in_secs is not None else None,
620657
)
621658
request_params['signature'] = signature

tests/integration/test_key_value_store.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,21 @@ def test_public_url(self, api_token: str, api_url: str, api_public_url: str, sig
9292
f'{(api_public_url or DEFAULT_API_URL).strip("/")}/v2/key-value-stores/someID/keys{expected_signature}'
9393
)
9494

95+
@pytest.mark.parametrize('signature', [None, 'custom-signature'])
96+
def test_record_public_url(self, api_token: str, signature: str) -> None:
97+
apify_client = ApifyClient(token=api_token)
98+
kvs = apify_client.key_value_store('someID')
99+
100+
# Mock the API call to return predefined response
101+
with mock.patch.object(
102+
apify_client.http_client,
103+
'call',
104+
return_value=Mock(text=_get_mocked_api_kvs_response(signing_key=signature)),
105+
):
106+
public_url = kvs.get_record_public_url(key='key')
107+
expected_signature = f'?signature={public_url.split("signature=")[1]}' if signature else ''
108+
assert public_url == (f'{DEFAULT_API_URL}/v2/key-value-stores/someID/records/key{expected_signature}')
109+
95110

96111
class TestKeyValueStoreAsync:
97112
async def test_key_value_store_should_create_expiring_keys_public_url_with_params(
@@ -138,7 +153,7 @@ async def test_key_value_store_should_create_public_keys_non_expiring_url(
138153

139154
@pytest.mark.parametrize('signature', [None, 'custom-signature'])
140155
@parametrized_api_urls
141-
async def test_public_url(self, api_token: str, api_url: str, api_public_url: str, signature: str) -> None:
156+
async def test_record_public_url(self, api_token: str, api_url: str, api_public_url: str, signature: str) -> None:
142157
apify_client = ApifyClientAsync(token=api_token, api_url=api_url, api_public_url=api_public_url)
143158
kvs = apify_client.key_value_store('someID')
144159

@@ -153,3 +168,18 @@ async def test_public_url(self, api_token: str, api_url: str, api_public_url: st
153168
assert public_url == (
154169
f'{(api_public_url or DEFAULT_API_URL).strip("/")}/v2/key-value-stores/someID/keys{expected_signature}'
155170
)
171+
172+
@pytest.mark.parametrize('signature', [None, 'custom-signature'])
173+
async def test_public_url(self, api_token: str, signature: str) -> None:
174+
apify_client = ApifyClientAsync(token=api_token)
175+
kvs = apify_client.key_value_store('someID')
176+
177+
# Mock the API call to return predefined response
178+
with mock.patch.object(
179+
apify_client.http_client,
180+
'call',
181+
return_value=Mock(text=_get_mocked_api_kvs_response(signing_key=signature)),
182+
):
183+
public_url = await kvs.get_record_public_url(key='key')
184+
expected_signature = f'?signature={public_url.split("signature=")[1]}' if signature else ''
185+
assert public_url == (f'{DEFAULT_API_URL}/v2/key-value-stores/someID/records/key{expected_signature}')

0 commit comments

Comments
 (0)