diff --git a/src/apify_client/clients/resource_clients/dataset.py b/src/apify_client/clients/resource_clients/dataset.py index 078e568a..87d6aab5 100644 --- a/src/apify_client/clients/resource_clients/dataset.py +++ b/src/apify_client/clients/resource_clients/dataset.py @@ -85,6 +85,7 @@ def list_items( skip_hidden: bool | None = None, flatten: list[str] | None = None, view: str | None = None, + signature: str | None = None, ) -> ListPage: """List the items of the dataset. @@ -116,6 +117,7 @@ def list_items( the # character. flatten: A list of fields that should be flattened. view: Name of the dataset view to be used. + signature: Signature used to access the items. Returns: A page of the list of dataset items according to the specified filters. @@ -132,6 +134,7 @@ def list_items( skipHidden=skip_hidden, flatten=flatten, view=view, + signature=signature, ) response = self.http_client.call( @@ -169,6 +172,7 @@ def iterate_items( unwind: list[str] | None = None, skip_empty: bool | None = None, skip_hidden: bool | None = None, + signature: str | None = None, ) -> Iterator[dict]: """Iterate over the items in the dataset. @@ -198,6 +202,7 @@ def iterate_items( contain less items than the limit value. skip_hidden: If True, then hidden fields are skipped from the output, i.e. fields starting with the # character. + signature: Signature used to access the items. Yields: An item from the dataset. @@ -227,6 +232,7 @@ def iterate_items( unwind=unwind, skip_empty=skip_empty, skip_hidden=skip_hidden, + signature=signature, ) yield from current_items_page.items @@ -256,6 +262,7 @@ def download_items( xml_root: str | None = None, xml_row: str | None = None, flatten: list[str] | None = None, + signature: str | None = None, ) -> bytes: """Get the items in the dataset as raw bytes. @@ -300,6 +307,7 @@ def download_items( xml_row: Overrides default element name that wraps each page or page function result object in xml output. By default the element name is item. flatten: A list of fields that should be flattened. + signature: Signature used to access the items. Returns: The dataset items as raw bytes. @@ -327,6 +335,7 @@ def download_items( xml_root=xml_root, xml_row=xml_row, flatten=flatten, + signature=signature, ) def get_items_as_bytes( @@ -348,6 +357,7 @@ def get_items_as_bytes( xml_root: str | None = None, xml_row: str | None = None, flatten: list[str] | None = None, + signature: str | None = None, ) -> bytes: """Get the items in the dataset as raw bytes. @@ -390,6 +400,7 @@ def get_items_as_bytes( xml_row: Overrides default element name that wraps each page or page function result object in xml output. By default the element name is item. flatten: A list of fields that should be flattened. + signature: Signature used to access the items. Returns: The dataset items as raw bytes. @@ -411,6 +422,7 @@ def get_items_as_bytes( xmlRoot=xml_root, xmlRow=xml_row, flatten=flatten, + signature=signature, ) response = self.http_client.call( @@ -440,6 +452,7 @@ def stream_items( skip_hidden: bool | None = None, xml_root: str | None = None, xml_row: str | None = None, + signature: str | None = None, ) -> Iterator[impit.Response]: """Retrieve the items in the dataset as a stream. @@ -481,6 +494,7 @@ def stream_items( xml_root: Overrides default root element name of xml output. By default the root element is items. xml_row: Overrides default element name that wraps each page or page function result object in xml output. By default the element name is item. + signature: Signature used to access the items. Returns: The dataset items as a context-managed streaming `Response`. @@ -503,6 +517,7 @@ def stream_items( skipHidden=skip_hidden, xmlRoot=xml_root, xmlRow=xml_row, + signature=signature, ) response = self.http_client.call( @@ -683,6 +698,7 @@ async def list_items( skip_hidden: bool | None = None, flatten: list[str] | None = None, view: str | None = None, + signature: str | None = None, ) -> ListPage: """List the items of the dataset. @@ -714,6 +730,7 @@ async def list_items( the # character. flatten: A list of fields that should be flattened. view: Name of the dataset view to be used. + signature: Signature used to access the items. Returns: A page of the list of dataset items according to the specified filters. @@ -730,6 +747,7 @@ async def list_items( skipHidden=skip_hidden, flatten=flatten, view=view, + signature=signature, ) response = await self.http_client.call( @@ -767,6 +785,7 @@ async def iterate_items( unwind: list[str] | None = None, skip_empty: bool | None = None, skip_hidden: bool | None = None, + signature: str | None = None, ) -> AsyncIterator[dict]: """Iterate over the items in the dataset. @@ -796,6 +815,7 @@ async def iterate_items( contain less items than the limit value. skip_hidden: If True, then hidden fields are skipped from the output, i.e. fields starting with the # character. + signature: Signature used to access the items. Yields: An item from the dataset. @@ -825,6 +845,7 @@ async def iterate_items( unwind=unwind, skip_empty=skip_empty, skip_hidden=skip_hidden, + signature=signature, ) for item in current_items_page.items: @@ -855,6 +876,7 @@ async def get_items_as_bytes( xml_root: str | None = None, xml_row: str | None = None, flatten: list[str] | None = None, + signature: str | None = None, ) -> bytes: """Get the items in the dataset as raw bytes. @@ -897,6 +919,7 @@ async def get_items_as_bytes( xml_row: Overrides default element name that wraps each page or page function result object in xml output. By default the element name is item. flatten: A list of fields that should be flattened. + signature: Signature used to access the items. Returns: The dataset items as raw bytes. @@ -918,6 +941,7 @@ async def get_items_as_bytes( xmlRoot=xml_root, xmlRow=xml_row, flatten=flatten, + signature=signature, ) response = await self.http_client.call( @@ -947,6 +971,7 @@ async def stream_items( skip_hidden: bool | None = None, xml_root: str | None = None, xml_row: str | None = None, + signature: str | None = None, ) -> AsyncIterator[impit.Response]: """Retrieve the items in the dataset as a stream. @@ -988,6 +1013,7 @@ async def stream_items( xml_root: Overrides default root element name of xml output. By default the root element is items. xml_row: Overrides default element name that wraps each page or page function result object in xml output. By default the element name is item. + signature: Signature used to access the items. Returns: The dataset items as a context-managed streaming `Response`. @@ -1010,6 +1036,7 @@ async def stream_items( skipHidden=skip_hidden, xmlRoot=xml_root, xmlRow=xml_row, + signature=signature, ) response = await self.http_client.call( diff --git a/src/apify_client/clients/resource_clients/key_value_store.py b/src/apify_client/clients/resource_clients/key_value_store.py index f1d03e96..46d415f1 100644 --- a/src/apify_client/clients/resource_clients/key_value_store.py +++ b/src/apify_client/clients/resource_clients/key_value_store.py @@ -77,6 +77,7 @@ def list_keys( exclusive_start_key: str | None = None, collection: str | None = None, prefix: str | None = None, + signature: str | None = None, ) -> dict: """List the keys in the key-value store. @@ -87,6 +88,7 @@ def list_keys( exclusive_start_key: All keys up to this one (including) are skipped from the result. collection: The name of the collection in store schema to list keys from. prefix: The prefix of the keys to be listed. + signature: Signature used to access the items. Returns: The list of keys in the key-value store matching the given arguments. @@ -96,6 +98,7 @@ def list_keys( exclusiveStartKey=exclusive_start_key, collection=collection, prefix=prefix, + signature=signature, ) response = self.http_client.call( @@ -107,13 +110,14 @@ def list_keys( return parse_date_fields(pluck_data(response.json())) - def get_record(self, key: str) -> dict | None: + def get_record(self, key: str, signature: str | None = None) -> dict | None: """Retrieve the given record from the key-value store. https://docs.apify.com/api/v2#/reference/key-value-stores/record/get-record Args: key: Key of the record to retrieve. + signature: Signature used to access the items. Returns: The requested record, or None, if the record does not exist. @@ -122,7 +126,7 @@ def get_record(self, key: str) -> dict | None: response = self.http_client.call( url=self._url(f'records/{key}'), method='GET', - params=self._params(), + params=self._params(signature=signature), ) return { @@ -161,13 +165,14 @@ def record_exists(self, key: str) -> bool: return response.status_code == HTTPStatus.OK - def get_record_as_bytes(self, key: str) -> dict | None: + def get_record_as_bytes(self, key: str, signature: str | None = None) -> dict | None: """Retrieve the given record from the key-value store, without parsing it. https://docs.apify.com/api/v2#/reference/key-value-stores/record/get-record Args: key: Key of the record to retrieve. + signature: Signature used to access the items. Returns: The requested record, or None, if the record does not exist. @@ -176,7 +181,7 @@ def get_record_as_bytes(self, key: str) -> dict | None: response = self.http_client.call( url=self._url(f'records/{key}'), method='GET', - params=self._params(), + params=self._params(signature=signature), ) return { @@ -191,13 +196,14 @@ def get_record_as_bytes(self, key: str) -> dict | None: return None @contextmanager - def stream_record(self, key: str) -> Iterator[dict | None]: + def stream_record(self, key: str, signature: str | None = None) -> Iterator[dict | None]: """Retrieve the given record from the key-value store, as a stream. https://docs.apify.com/api/v2#/reference/key-value-stores/record/get-record Args: key: Key of the record to retrieve. + signature: Signature used to access the items. Returns: The requested record as a context-managed streaming Response, or None, if the record does not exist. @@ -207,7 +213,7 @@ def stream_record(self, key: str) -> Iterator[dict | None]: response = self.http_client.call( url=self._url(f'records/{key}'), method='GET', - params=self._params(), + params=self._params(signature=signature), stream=True, ) @@ -395,6 +401,7 @@ async def list_keys( exclusive_start_key: str | None = None, collection: str | None = None, prefix: str | None = None, + signature: str | None = None, ) -> dict: """List the keys in the key-value store. @@ -405,6 +412,7 @@ async def list_keys( exclusive_start_key: All keys up to this one (including) are skipped from the result. collection: The name of the collection in store schema to list keys from. prefix: The prefix of the keys to be listed. + signature: Signature used to access the items. Returns: The list of keys in the key-value store matching the given arguments. @@ -414,6 +422,7 @@ async def list_keys( exclusiveStartKey=exclusive_start_key, collection=collection, prefix=prefix, + signature=signature, ) response = await self.http_client.call( @@ -425,13 +434,14 @@ async def list_keys( return parse_date_fields(pluck_data(response.json())) - async def get_record(self, key: str) -> dict | None: + async def get_record(self, key: str, signature: str | None = None) -> dict | None: """Retrieve the given record from the key-value store. https://docs.apify.com/api/v2#/reference/key-value-stores/record/get-record Args: key: Key of the record to retrieve. + signature: Signature used to access the items. Returns: The requested record, or None, if the record does not exist. @@ -440,7 +450,7 @@ async def get_record(self, key: str) -> dict | None: response = await self.http_client.call( url=self._url(f'records/{key}'), method='GET', - params=self._params(), + params=self._params(signature=signature), ) return { @@ -479,13 +489,14 @@ async def record_exists(self, key: str) -> bool: return response.status_code == HTTPStatus.OK - async def get_record_as_bytes(self, key: str) -> dict | None: + async def get_record_as_bytes(self, key: str, signature: str | None = None) -> dict | None: """Retrieve the given record from the key-value store, without parsing it. https://docs.apify.com/api/v2#/reference/key-value-stores/record/get-record Args: key: Key of the record to retrieve. + signature: Signature used to access the items. Returns: The requested record, or None, if the record does not exist. @@ -494,7 +505,7 @@ async def get_record_as_bytes(self, key: str) -> dict | None: response = await self.http_client.call( url=self._url(f'records/{key}'), method='GET', - params=self._params(), + params=self._params(signature=signature), ) return { @@ -509,13 +520,14 @@ async def get_record_as_bytes(self, key: str) -> dict | None: return None @asynccontextmanager - async def stream_record(self, key: str) -> AsyncIterator[dict | None]: + async def stream_record(self, key: str, signature: str | None = None) -> AsyncIterator[dict | None]: """Retrieve the given record from the key-value store, as a stream. https://docs.apify.com/api/v2#/reference/key-value-stores/record/get-record Args: key: Key of the record to retrieve. + signature: Signature used to access the items. Returns: The requested record as a context-managed streaming Response, or None, if the record does not exist. @@ -525,7 +537,7 @@ async def stream_record(self, key: str) -> AsyncIterator[dict | None]: response = await self.http_client.call( url=self._url(f'records/{key}'), method='GET', - params=self._params(), + params=self._params(signature=signature), stream=True, ) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 79e9e397..ad5759db 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -2,6 +2,7 @@ import pytest +from .integration_test_utils import TestDataset, TestKvs from apify_client import ApifyClient, ApifyClientAsync TOKEN_ENV_VAR = 'APIFY_TEST_USER_API_TOKEN' @@ -31,3 +32,28 @@ def apify_client(api_token: str) -> ApifyClient: def apify_client_async(api_token: str) -> ApifyClientAsync: api_url = os.getenv(API_URL_ENV_VAR) return ApifyClientAsync(api_token, api_url=api_url) + + +@pytest.fixture +def test_dataset_of_another_user() -> TestDataset: + """Pre-existing dataset of another test user with restricted access.""" + return TestDataset( + id='InrsNvJNGwJMFAR2l', + signature='MC4wLjFGbVN3UjB5T0xvMU1hU0lFQjZCMQ', + expected_content=[{'item1': 1, 'item2': 2, 'item3': 3}, {'item1': 4, 'item2': 5, 'item3': 6}], + ) + + +@pytest.fixture +def test_kvs_of_another_user() -> TestKvs: + """Pre-existing key value store of another test user with restricted access.""" + return TestKvs( + id='0SWREKM4yzKnpQRGA', + signature='MC4wLjVKVmlMSVpDNEhaazg1Z1VXTnBP', + expected_content={'key1': 1, 'key2': 2, 'key3': 3}, + keys_signature={ + 'key1': 'qrQL9pHpiok99v9kWhKx', + 'key2': '1BhGTfsLvpsF8aPiIgoBt', + 'key3': 'rPPqxmTNcxvvpvO0Bx5s', + }, + ) diff --git a/tests/integration/integration_test_utils.py b/tests/integration/integration_test_utils.py index 28705761..6d7fc6bb 100644 --- a/tests/integration/integration_test_utils.py +++ b/tests/integration/integration_test_utils.py @@ -1,5 +1,7 @@ +import dataclasses import secrets import string +from typing import Any import pytest @@ -24,3 +26,20 @@ def random_resource_name(resource: str) -> str: ('http://10.0.88.214:8010', None), ], ) + + +@dataclasses.dataclass +class TestStorage: + id: str + signature: str + + +@dataclasses.dataclass +class TestDataset(TestStorage): + expected_content: list + + +@dataclasses.dataclass +class TestKvs(TestStorage): + expected_content: dict[str, Any] + keys_signature: dict[str, str] diff --git a/tests/integration/test_dataset.py b/tests/integration/test_dataset.py index 6e3c8eab..cb33f426 100644 --- a/tests/integration/test_dataset.py +++ b/tests/integration/test_dataset.py @@ -5,11 +5,13 @@ from unittest.mock import Mock import impit +import pytest -from integration.integration_test_utils import parametrized_api_urls, random_resource_name +from integration.integration_test_utils import TestDataset, parametrized_api_urls, random_resource_name from apify_client import ApifyClient, ApifyClientAsync from apify_client.client import DEFAULT_API_URL +from apify_client.errors import ApifyApiError MOCKED_API_DATASET_RESPONSE = """{ "data": { @@ -90,6 +92,58 @@ def test_public_url(self, api_token: str, api_url: str, api_public_url: str) -> f'someID/items?signature={public_url.split("signature=")[1]}' ) + def test_list_items_signature(self, apify_client: ApifyClient, test_dataset_of_another_user: TestDataset) -> None: + dataset = apify_client.dataset(dataset_id=test_dataset_of_another_user.id) + + # Permission error without valid signature + with pytest.raises( + ApifyApiError, + match=r"Insufficient permissions for the dataset. Make sure you're passing a " + r'correct API token and that it has the required permissions.', + ): + dataset.list_items() + + # Dataset content retrieved with correct signature + assert ( + test_dataset_of_another_user.expected_content + == dataset.list_items(signature=test_dataset_of_another_user.signature).items + ) + + def test_iterate_items_signature( + self, apify_client: ApifyClient, test_dataset_of_another_user: TestDataset + ) -> None: + dataset = apify_client.dataset(dataset_id=test_dataset_of_another_user.id) + + # Permission error without valid signature + with pytest.raises( + ApifyApiError, + match=r"Insufficient permissions for the dataset. Make sure you're passing a " + r'correct API token and that it has the required permissions.', + ): + list(dataset.iterate_items()) + + # Dataset content retrieved with correct signature + assert test_dataset_of_another_user.expected_content == list( + dataset.iterate_items(signature=test_dataset_of_another_user.signature) + ) + + def test_get_items_as_bytes_signature( + self, apify_client: ApifyClient, test_dataset_of_another_user: TestDataset + ) -> None: + dataset = apify_client.dataset(dataset_id=test_dataset_of_another_user.id) + + # Permission error without valid signature + with pytest.raises( + ApifyApiError, + match=r"Insufficient permissions for the dataset. Make sure you're passing a " + r'correct API token and that it has the required permissions.', + ): + dataset.get_items_as_bytes() + + # Dataset content retrieved with correct signature + raw_data = dataset.get_items_as_bytes(signature=test_dataset_of_another_user.signature) + assert test_dataset_of_another_user.expected_content == json.loads(raw_data.decode('utf-8')) + class TestDatasetAsync: async def test_dataset_should_create_public_items_expiring_url_with_params( @@ -146,3 +200,57 @@ async def test_public_url(self, api_token: str, api_url: str, api_public_url: st f'{(api_public_url or DEFAULT_API_URL).strip("/")}/v2/datasets/' f'someID/items?signature={public_url.split("signature=")[1]}' ) + + async def test_list_items_signature( + self, apify_client_async: ApifyClientAsync, test_dataset_of_another_user: TestDataset + ) -> None: + dataset = apify_client_async.dataset(dataset_id=test_dataset_of_another_user.id) + + # Permission error without valid signature + with pytest.raises( + ApifyApiError, + match=r"Insufficient permissions for the dataset. Make sure you're passing a " + r'correct API token and that it has the required permissions.', + ): + await dataset.list_items() + + # Dataset content retrieved with correct signature + assert ( + test_dataset_of_another_user.expected_content + == (await dataset.list_items(signature=test_dataset_of_another_user.signature)).items + ) + + async def test_iterate_items_signature( + self, apify_client_async: ApifyClientAsync, test_dataset_of_another_user: TestDataset + ) -> None: + dataset = apify_client_async.dataset(dataset_id=test_dataset_of_another_user.id) + + # Permission error without valid signature + with pytest.raises( + ApifyApiError, + match=r"Insufficient permissions for the dataset. Make sure you're passing a " + r'correct API token and that it has the required permissions.', + ): + [item async for item in dataset.iterate_items()] + + # Dataset content retrieved with correct signature + assert test_dataset_of_another_user.expected_content == [ + item async for item in dataset.iterate_items(signature=test_dataset_of_another_user.signature) + ] + + async def test_get_items_as_bytes_signature( + self, apify_client_async: ApifyClientAsync, test_dataset_of_another_user: TestDataset + ) -> None: + dataset = apify_client_async.dataset(dataset_id=test_dataset_of_another_user.id) + + # Permission error without valid signature + with pytest.raises( + ApifyApiError, + match=r"Insufficient permissions for the dataset. Make sure you're passing a " + r'correct API token and that it has the required permissions.', + ): + await dataset.get_items_as_bytes() + + # Dataset content retrieved with correct signature + raw_data = await dataset.get_items_as_bytes(signature=test_dataset_of_another_user.signature) + assert test_dataset_of_another_user.expected_content == json.loads(raw_data.decode('utf-8')) diff --git a/tests/integration/test_key_value_store.py b/tests/integration/test_key_value_store.py index 298e14a6..470d8ec8 100644 --- a/tests/integration/test_key_value_store.py +++ b/tests/integration/test_key_value_store.py @@ -1,5 +1,6 @@ from __future__ import annotations +import json from unittest import mock from unittest.mock import Mock @@ -7,10 +8,10 @@ import pytest from apify_shared.utils import create_hmac_signature, create_storage_content_signature -from integration.integration_test_utils import parametrized_api_urls, random_resource_name - +from .integration_test_utils import TestKvs, parametrized_api_urls, random_resource_name from apify_client import ApifyClient, ApifyClientAsync from apify_client.client import DEFAULT_API_URL +from apify_client.errors import ApifyApiError MOCKED_ID = 'someID' @@ -122,6 +123,77 @@ def test_record_public_url(self, api_token: str, api_url: str, api_public_url: s f'records/{key}{expected_signature}' ) + def test_list_keys_signature(self, apify_client: ApifyClient, test_kvs_of_another_user: TestKvs) -> None: + kvs = apify_client.key_value_store(key_value_store_id=test_kvs_of_another_user.id) + + # Permission error without valid signature + with pytest.raises( + ApifyApiError, + match=r"Insufficient permissions for the key-value store. Make sure you're passing a correct" + r' API token and that it has the required permissions.', + ): + kvs.list_keys() + + # Kvs content retrieved with correct signature + raw_items = kvs.list_keys(signature=test_kvs_of_another_user.signature)['items'] + + assert set(test_kvs_of_another_user.expected_content) == {item['key'] for item in raw_items} + + def test_get_record_signature(self, apify_client: ApifyClient, test_kvs_of_another_user: TestKvs) -> None: + key = 'key1' + kvs = apify_client.key_value_store(key_value_store_id=test_kvs_of_another_user.id) + + # Permission error without valid signature + with pytest.raises( + ApifyApiError, + match=r"Insufficient permissions for the key-value store. Make sure you're passing a correct" + r' API token and that it has the required permissions.', + ): + kvs.get_record(key=key) + + # Kvs content retrieved with correct signature + record = kvs.get_record(key=key, signature=test_kvs_of_another_user.keys_signature[key]) + assert record + assert test_kvs_of_another_user.expected_content[key] == record['value'] + + def test_get_record_as_bytes_signature(self, apify_client: ApifyClient, test_kvs_of_another_user: TestKvs) -> None: + key = 'key1' + kvs = apify_client.key_value_store(key_value_store_id=test_kvs_of_another_user.id) + + # Permission error without valid signature + with pytest.raises( + ApifyApiError, + match=r"Insufficient permissions for the key-value store. Make sure you're passing a correct" + r' API token and that it has the required permissions.', + ): + kvs.get_record_as_bytes(key=key) + + # Kvs content retrieved with correct signature + item = kvs.get_record_as_bytes(key=key, signature=test_kvs_of_another_user.keys_signature[key]) + assert item + assert test_kvs_of_another_user.expected_content[key] == json.loads(item['value'].decode('utf-8')) + + def test_stream_record_signature(self, apify_client: ApifyClient, test_kvs_of_another_user: TestKvs) -> None: + key = 'key1' + kvs = apify_client.key_value_store(key_value_store_id=test_kvs_of_another_user.id) + + # Permission error without valid signature + with ( + pytest.raises( + ApifyApiError, + match=r"Insufficient permissions for the key-value store. Make sure you're passing a correct" + r' API token and that it has the required permissions.', + ), + kvs.stream_record(key=key), + ): + pass + + # Kvs content retrieved with correct signature + with kvs.stream_record(key=key, signature=test_kvs_of_another_user.keys_signature[key]) as stream: + assert stream + value = json.loads(stream['value'].content.decode('utf-8')) + assert test_kvs_of_another_user.expected_content[key] == value + class TestKeyValueStoreAsync: async def test_key_value_store_should_create_expiring_keys_public_url_with_params( @@ -209,3 +281,80 @@ async def test_record_public_url(self, api_token: str, api_url: str, api_public_ f'{(api_public_url or DEFAULT_API_URL).strip("/")}/v2/key-value-stores/someID/' f'records/{key}{expected_signature}' ) + + async def test_list_keys_signature( + self, apify_client_async: ApifyClientAsync, test_kvs_of_another_user: TestKvs + ) -> None: + kvs = apify_client_async.key_value_store(key_value_store_id=test_kvs_of_another_user.id) + + # Permission error without valid signature + with pytest.raises( + ApifyApiError, + match=r"Insufficient permissions for the key-value store. Make sure you're passing a correct" + r' API token and that it has the required permissions.', + ): + await kvs.list_keys() + + # Kvs content retrieved with correct signature + raw_items = (await kvs.list_keys(signature=test_kvs_of_another_user.signature))['items'] + + assert set(test_kvs_of_another_user.expected_content) == {item['key'] for item in raw_items} + + async def test_get_record_signature( + self, apify_client_async: ApifyClientAsync, test_kvs_of_another_user: TestKvs + ) -> None: + key = 'key1' + kvs = apify_client_async.key_value_store(key_value_store_id=test_kvs_of_another_user.id) + + # Permission error without valid signature + with pytest.raises( + ApifyApiError, + match=r"Insufficient permissions for the key-value store. Make sure you're passing a correct" + r' API token and that it has the required permissions.', + ): + await kvs.get_record(key=key) + + # Kvs content retrieved with correct signature + record = await kvs.get_record(key=key, signature=test_kvs_of_another_user.keys_signature[key]) + assert record + assert test_kvs_of_another_user.expected_content[key] == record['value'] + + async def test_get_record_as_bytes_signature( + self, apify_client_async: ApifyClientAsync, test_kvs_of_another_user: TestKvs + ) -> None: + key = 'key1' + kvs = apify_client_async.key_value_store(key_value_store_id=test_kvs_of_another_user.id) + + # Permission error without valid signature + with pytest.raises( + ApifyApiError, + match=r"Insufficient permissions for the key-value store. Make sure you're passing a correct" + r' API token and that it has the required permissions.', + ): + await kvs.get_record_as_bytes(key=key) + + # Kvs content retrieved with correct signature + item = await kvs.get_record_as_bytes(key=key, signature=test_kvs_of_another_user.keys_signature[key]) + assert item + assert test_kvs_of_another_user.expected_content[key] == json.loads(item['value'].decode('utf-8')) + + async def test_stream_record_signature( + self, apify_client_async: ApifyClientAsync, test_kvs_of_another_user: TestKvs + ) -> None: + key = 'key1' + kvs = apify_client_async.key_value_store(key_value_store_id=test_kvs_of_another_user.id) + + # Permission error without valid signature + with pytest.raises( + ApifyApiError, + match=r"Insufficient permissions for the key-value store. Make sure you're passing a correct" + r' API token and that it has the required permissions.', + ): + async with kvs.stream_record(key=key): + pass + + # Kvs content retrieved with correct signature + async with kvs.stream_record(key=key, signature=test_kvs_of_another_user.keys_signature[key]) as stream: + assert stream + value = json.loads(stream['value'].content.decode('utf-8')) + assert test_kvs_of_another_user.expected_content[key] == value