from __future__ import annotations import json from typing import TYPE_CHECKING import pytest from crawlee import service_locator from crawlee.configuration import Configuration from crawlee.storage_clients import FileSystemStorageClient, MemoryStorageClient from crawlee.storages import Dataset, KeyValueStore from crawlee.storages._storage_instance_manager import StorageInstanceManager if TYPE_CHECKING: from collections.abc import AsyncGenerator from pathlib import Path from typing import Any from crawlee.storage_clients import StorageClient @pytest.fixture async def dataset( storage_client: StorageClient, ) -> AsyncGenerator[Dataset, None]: """Fixture that provides a dataset instance for each test.""" dataset = await Dataset.open( storage_client=storage_client, ) yield dataset await dataset.drop() async def test_open_creates_new_dataset( storage_client: StorageClient, ) -> None: """Test that open() creates a new dataset with proper metadata.""" dataset = await Dataset.open( name='new-dataset', storage_client=storage_client, ) # Verify dataset properties assert dataset.id is not None assert dataset.name == 'new-dataset' metadata = await dataset.get_metadata() assert metadata.item_count == 0 await dataset.drop() async def test_reopen_default( storage_client: StorageClient, ) -> None: """Test reopening a dataset with default parameters.""" # Create a first dataset instance with default parameters dataset_1 = await Dataset.open( storage_client=storage_client, ) # Verify default properties assert dataset_1.id is not None metadata_1 = await dataset_1.get_metadata() assert metadata_1.item_count == 0 # Add an item await dataset_1.push_data({'key': 'value'}) metadata_1 = await dataset_1.get_metadata() assert metadata_1.item_count == 1 # Reopen the same dataset dataset_2 = await Dataset.open( storage_client=storage_client, ) # Verify both instances reference the same dataset assert dataset_2.id == dataset_1.id assert dataset_2.name == dataset_1.name metadata_1 = await dataset_1.get_metadata() metadata_2 = await dataset_2.get_metadata() assert metadata_2.item_count == metadata_1.item_count == 1 # Verify they are the same object (cached) assert id(dataset_1) == id(dataset_2) # Clean up await dataset_1.drop() async def test_open_by_id( storage_client: StorageClient, ) -> None: """Test opening a dataset by its ID.""" # First create a dataset by name dataset1 = await Dataset.open( name='dataset-by-id-test', storage_client=storage_client, ) # Add some data to identify it test_item = {'test': 'opening_by_id', 'timestamp': 12345} await dataset1.push_data(test_item) # Open the dataset by ID dataset2 = await Dataset.open( id=dataset1.id, storage_client=storage_client, ) # Verify it's the same dataset assert dataset2.id == dataset1.id assert dataset2.name == 'dataset-by-id-test' # Verify the data is still there data = await dataset2.get_data() assert data.count == 1 assert data.items[0]['test'] == 'opening_by_id' assert data.items[0]['timestamp'] == 12345 # Clean up await dataset2.drop() async def test_open_existing_dataset( dataset: Dataset, ) -> None: """Test that open() loads an existing dataset correctly.""" # Open the same dataset again reopened_dataset = await Dataset.open( name=dataset.name, ) # Verify dataset properties assert dataset.id == reopened_dataset.id assert dataset.name == reopened_dataset.name metadata = await dataset.get_metadata() reopened_metadata = await reopened_dataset.get_metadata() assert metadata.item_count == reopened_metadata.item_count # Verify they are the same object (from cache) assert id(dataset) == id(reopened_dataset) async def test_open_with_id_and_name( storage_client: StorageClient, ) -> None: """Test that open() raises an error when both id and name are provided.""" with pytest.raises( ValueError, match=r'Only one of "id", "name", "alias" can be specified, but following arguments ' r'were specified: "id", "name".', ): await Dataset.open( id='some-id', name='some-name', storage_client=storage_client, ) async def test_push_data_single_item(dataset: Dataset) -> None: """Test pushing a single item to the dataset.""" item = {'key': 'value', 'number': 42} await dataset.push_data(item) # Verify item was stored result = await dataset.get_data() assert result.count == 1 assert result.items[0] == item async def test_push_data_multiple_items(dataset: Dataset) -> None: """Test pushing multiple items to the dataset.""" items = [ {'id': 1, 'name': 'Item 1'}, {'id': 2, 'name': 'Item 2'}, {'id': 3, 'name': 'Item 3'}, ] await dataset.push_data(items) # Verify items were stored result = await dataset.get_data() assert result.count == 3 assert result.items == items async def test_get_data_empty_dataset(dataset: Dataset) -> None: """Test getting data from an empty dataset returns empty results.""" result = await dataset.get_data() assert result.count == 0 assert result.total == 0 assert result.items == [] async def test_get_data_with_pagination(dataset: Dataset) -> None: """Test getting data with offset and limit parameters for pagination.""" # Add some items items = [{'id': i} for i in range(1, 11)] # 10 items await dataset.push_data(items) # Test offset result = await dataset.get_data(offset=3) assert result.count == 7 assert result.offset == 3 assert result.items[0]['id'] == 4 # Test limit result = await dataset.get_data(limit=5) assert result.count == 5 assert result.limit == 5 assert result.items[-1]['id'] == 5 # Test both offset and limit result = await dataset.get_data(offset=2, limit=3) assert result.count == 3 assert result.offset == 2 assert result.limit == 3 assert result.items[0]['id'] == 3 assert result.items[-1]['id'] == 5 async def test_get_data_descending_order(dataset: Dataset) -> None: """Test getting data in descending order reverses the item order.""" # Add some items items = [{'id': i} for i in range(1, 6)] # 5 items await dataset.push_data(items) # Get items in descending order result = await dataset.get_data(desc=True) assert result.desc is True assert result.items[0]['id'] == 5 assert result.items[-1]['id'] == 1 async def test_get_data_skip_empty(dataset: Dataset) -> None: """Test getting data with skip_empty option filters out empty items.""" # Add some items including an empty one items = [ {'id': 1, 'name': 'Item 1'}, {}, # Empty item {'id': 3, 'name': 'Item 3'}, ] await dataset.push_data(items) # Get all items result = await dataset.get_data() assert result.count == 3 # Get non-empty items result = await dataset.get_data(skip_empty=True) assert result.count == 2 assert all(item != {} for item in result.items) async def test_iterate_items(dataset: Dataset) -> None: """Test iterating over dataset items yields each item in the correct order.""" # Add some items items = [{'id': i} for i in range(1, 6)] # 5 items await dataset.push_data(items) # Iterate over all items collected_items = [item async for item in dataset.iterate_items()] assert len(collected_items) == 5 assert collected_items[0]['id'] == 1 assert collected_items[-1]['id'] == 5 async def test_iterate_items_with_options(dataset: Dataset) -> None: """Test iterating with offset, limit and desc parameters.""" # Add some items items = [{'id': i} for i in range(1, 11)] # 10 items await dataset.push_data(items) # Test with offset and limit collected_items = [item async for item in dataset.iterate_items(offset=3, limit=3)] assert len(collected_items) == 3 assert collected_items[0]['id'] == 4 assert collected_items[-1]['id'] == 6 # Test with descending order collected_items = [] async for item in dataset.iterate_items(desc=True, limit=3): collected_items.append(item) assert len(collected_items) == 3 assert collected_items[0]['id'] == 10 assert collected_items[-1]['id'] == 8 async def test_list_items(dataset: Dataset) -> None: """Test that list_items returns all dataset items as a list.""" # Add some items items = [{'id': i} for i in range(1, 6)] # 5 items await dataset.push_data(items) # Get all items as a list collected_items = await dataset.list_items() assert len(collected_items) == 5 assert collected_items[0]['id'] == 1 assert collected_items[-1]['id'] == 5 async def test_list_items_with_options(dataset: Dataset) -> None: """Test that list_items respects filtering options.""" # Add some items items: list[dict[str, Any]] = [ {'id': 1, 'name': 'Item 1'}, {'id': 2, 'name': 'Item 2'}, {'id': 3}, # Item with missing 'name' field {}, # Empty item {'id': 5, 'name': 'Item 5'}, ] await dataset.push_data(items) # Test with offset and limit collected_items = await dataset.list_items(offset=1, limit=2) assert len(collected_items) == 2 assert collected_items[0]['id'] == 2 assert collected_items[1]['id'] == 3 # Test with descending order - skip empty items to avoid KeyError collected_items = await dataset.list_items(desc=True, skip_empty=True) # Filter items that have an 'id' field items_with_ids = [item for item in collected_items if 'id' in item] id_values = [item['id'] for item in items_with_ids] # Verify the list is sorted in descending order assert sorted(id_values, reverse=True) == id_values, f'IDs should be in descending order. Got {id_values}' # Verify key IDs are present and in the right order if 5 in id_values and 3 in id_values: assert id_values.index(5) < id_values.index(3), 'ID 5 should come before ID 3 in descending order' # Test with skip_empty collected_items = await dataset.list_items(skip_empty=True) assert len(collected_items) == 4 # Should skip the empty item assert all(item != {} for item in collected_items) # Test with fields - manually filter since 'fields' parameter is not supported # Get all items first collected_items = await dataset.list_items() assert len(collected_items) == 5 # Manually extract only the 'id' field from each item filtered_items = [{key: item[key] for key in ['id'] if key in item} for item in collected_items] # Verify 'name' field is not present in any item assert all('name' not in item for item in filtered_items) # Test clean functionality manually instead of using the clean parameter # Get all items collected_items = await dataset.list_items() # Manually filter out empty items as 'clean' would do clean_items = [item for item in collected_items if item != {}] assert len(clean_items) == 4 # Should have 4 non-empty items assert all(item != {} for item in clean_items) async def test_drop( storage_client: StorageClient, ) -> None: """Test dropping a dataset removes it from cache and clears its data.""" dataset = await Dataset.open( name='drop-test', storage_client=storage_client, ) # Add some data await dataset.push_data({'test': 'data'}) # Drop the dataset await dataset.drop() # Verify dataset is empty (by creating a new one with the same name) new_dataset = await Dataset.open( name='drop-test', storage_client=storage_client, ) result = await new_dataset.get_data() assert result.count == 0 await new_dataset.drop() async def test_export_to_json( dataset: Dataset, storage_client: StorageClient, ) -> None: """Test exporting dataset to JSON format.""" # Create a key-value store for export kvs = await KeyValueStore.open( name='export-kvs', ) # Add some items to the dataset items = [ {'id': 1, 'name': 'Item 1'}, {'id': 2, 'name': 'Item 2'}, {'id': 3, 'name': 'Item 3'}, ] await dataset.push_data(items) # Export to JSON await dataset.export_to( key='dataset_export.json', content_type='json', to_kvs_name='export-kvs', to_kvs_storage_client=storage_client, ) # Retrieve the exported file record = await kvs.get_value(key='dataset_export.json') assert record is not None # Verify content has all the items assert '"id": 1' in record assert '"id": 2' in record assert '"id": 3' in record await kvs.drop() async def test_export_to_csv( dataset: Dataset, storage_client: StorageClient, ) -> None: """Test exporting dataset to CSV format.""" # Create a key-value store for export kvs = await KeyValueStore.open( name='export-kvs', storage_client=storage_client, ) # Add some items to the dataset items = [ {'id': 1, 'name': 'Item 1'}, {'id': 2, 'name': 'Item 2'}, {'id': 3, 'name': 'Item 3'}, ] await dataset.push_data(items) # Export to CSV await dataset.export_to( key='dataset_export.csv', content_type='csv', to_kvs_name='export-kvs', to_kvs_storage_client=storage_client, ) # Retrieve the exported file record = await kvs.get_value(key='dataset_export.csv') assert record is not None # Verify content has all the items assert 'id,name' in record assert '1,Item 1' in record assert '2,Item 2' in record assert '3,Item 3' in record await kvs.drop() async def test_export_to_invalid_content_type(dataset: Dataset) -> None: """Test exporting dataset with invalid content type raises error.""" with pytest.raises(ValueError, match=r'Unsupported content type'): await dataset.export_to(key='invalid_export', content_type='invalid') # ty: ignore[no-matching-overload] async def test_export_with_multiple_kwargs(dataset: Dataset, tmp_path: Path) -> None: """Test exporting dataset using many optional arguments together.""" target_kvs_name = 'some-kvs' target_storage_client = FileSystemStorageClient() export_key = 'exported_dataset' data = {'some key': 'some data'} # Prepare custom directory and configuration custom_dir_name = 'some_dir' custom_dir = tmp_path / custom_dir_name custom_dir.mkdir() target_configuration = Configuration(storage_dir=str(custom_dir)) # Set expected values expected_exported_data = f'{json.dumps([{"some key": "some data"}])}' expected_kvs_dir = custom_dir / 'key_value_stores' / target_kvs_name # Populate dataset and export await dataset.push_data(data) await dataset.export_to( key=export_key, content_type='json', to_kvs_name=target_kvs_name, to_kvs_storage_client=target_storage_client, to_kvs_configuration=target_configuration, ) # Verify the directory was created assert expected_kvs_dir.is_dir() # Verify that kvs contains the exported data kvs = await KeyValueStore.open( name=target_kvs_name, storage_client=target_storage_client, configuration=target_configuration ) assert await kvs.get_value(key=export_key) == expected_exported_data async def test_large_dataset(dataset: Dataset) -> None: """Test handling a large dataset with many items.""" items = [{'id': i, 'value': f'value-{i}'} for i in range(100)] await dataset.push_data(items) # Test that all items are retrieved result = await dataset.get_data(limit=None) assert result.count == 100 assert result.total == 100 # Test pagination with large datasets result = await dataset.get_data(offset=50, limit=25) assert result.count == 25 assert result.offset == 50 assert result.items[0]['id'] == 50 assert result.items[-1]['id'] == 74 async def test_purge( storage_client: StorageClient, ) -> None: """Test purging a dataset removes all data but keeps the dataset itself.""" # First create a dataset dataset = await Dataset.open( name='purge-test-dataset', storage_client=storage_client, ) # Add some data initial_items = [ {'id': 1, 'name': 'Item 1'}, {'id': 2, 'name': 'Item 2'}, {'id': 3, 'name': 'Item 3'}, ] await dataset.push_data(initial_items) # Verify data was added data = await dataset.get_data() assert data.count == 3 assert data.total == 3 metadata = await dataset.get_metadata() assert metadata.item_count == 3 # Record the dataset ID dataset_id = dataset.id # Purge the dataset await dataset.purge() # Verify the dataset still exists but is empty assert dataset.id == dataset_id # Same ID preserved assert dataset.name == 'purge-test-dataset' # Same name preserved # Dataset should be empty now data = await dataset.get_data() assert data.count == 0 assert data.total == 0 metadata = await dataset.get_metadata() assert metadata.item_count == 0 # Verify we can add new data after purging new_item = {'id': 4, 'name': 'New Item After Purge'} await dataset.push_data(new_item) data = await dataset.get_data() assert data.count == 1 assert data.items[0]['name'] == 'New Item After Purge' # Clean up await dataset.drop() async def test_open_with_alias( storage_client: StorageClient, ) -> None: """Test opening datasets with alias parameter for NDU functionality.""" # Create datasets with different aliases dataset_1 = await Dataset.open( alias='test_alias_1', storage_client=storage_client, ) dataset_2 = await Dataset.open( alias='test_alias_2', storage_client=storage_client, ) # Verify they have different IDs but no names (unnamed) assert dataset_1.id != dataset_2.id assert dataset_1.name is None assert dataset_2.name is None # Add different data to each await dataset_1.push_data({'source': 'alias_1', 'value': 1}) await dataset_2.push_data({'source': 'alias_2', 'value': 2}) # Verify data isolation data_1 = await dataset_1.get_data() data_2 = await dataset_2.get_data() assert data_1.count == 1 assert data_2.count == 1 assert data_1.items[0]['source'] == 'alias_1' assert data_2.items[0]['source'] == 'alias_2' # Clean up await dataset_1.drop() await dataset_2.drop() async def test_alias_caching( storage_client: StorageClient, ) -> None: """Test that datasets with same alias return same instance (cached).""" # Open dataset with alias dataset_1 = await Dataset.open( alias='cache_test', storage_client=storage_client, ) # Open again with same alias dataset_2 = await Dataset.open( alias='cache_test', storage_client=storage_client, ) # Should be same instance assert dataset_1 is dataset_2 assert dataset_1.id == dataset_2.id # Clean up await dataset_1.drop() async def test_alias_with_id_error( storage_client: StorageClient, ) -> None: """Test that providing both alias and id raises error.""" with pytest.raises( ValueError, match=r'Only one of "id", "name", "alias" can be specified, but following arguments ' r'were specified: "id", "alias".', ): await Dataset.open( id='some-id', alias='some-alias', storage_client=storage_client, ) async def test_alias_with_name_error( storage_client: StorageClient, ) -> None: """Test that providing both alias and name raises error.""" with pytest.raises( ValueError, match=r'Only one of "id", "name", "alias" can be specified, but following arguments ' r'were specified: "name", "alias".', ): await Dataset.open( name='some-name', alias='some-alias', storage_client=storage_client, ) async def test_alias_with_all_parameters_error( storage_client: StorageClient, ) -> None: """Test that providing id, name, and alias raises error.""" with pytest.raises( ValueError, match=r'Only one of "id", "name", "alias" can be specified, but following arguments ' r'were specified: "id", "name", "alias".', ): await Dataset.open( id='some-id', name='some-name', alias='some-alias', storage_client=storage_client, ) async def test_alias_with_special_characters( storage_client: StorageClient, ) -> None: """Test alias functionality with special characters.""" special_aliases = [ 'alias-with-dashes', 'alias_with_underscores', 'alias.with.dots', 'alias123with456numbers', 'CamelCaseAlias', ] datasets = [] for alias in special_aliases: dataset = await Dataset.open( alias=alias, storage_client=storage_client, ) datasets.append(dataset) # Add data with the alias as identifier await dataset.push_data({'alias_used': alias, 'test': 'special_chars'}) # Verify all work correctly for i, dataset in enumerate(datasets): data = await dataset.get_data() assert data.count == 1 assert data.items[0]['alias_used'] == special_aliases[i] # Clean up for dataset in datasets: await dataset.drop() async def test_named_vs_alias_conflict_detection( storage_client: StorageClient, ) -> None: """Test that conflicts between named and alias storages are detected.""" # Test 1: Create named storage first, then try alias with same name named_dataset = await Dataset.open(name='conflict-test', storage_client=storage_client) assert named_dataset.name == 'conflict-test' # Try to create alias with same name - should raise error with pytest.raises(ValueError, match=r'Cannot create alias storage "conflict-test".*already exists'): await Dataset.open(alias='conflict-test', storage_client=storage_client) # Clean up await named_dataset.drop() # Test 2: Create alias first, then try named with same name alias_dataset = await Dataset.open(alias='conflict-test2', storage_client=storage_client) assert alias_dataset.name is None # Alias storages have no name # Try to create named with same name - should raise error with pytest.raises(ValueError, match=r'Cannot create named storage "conflict-test2".*already exists'): await Dataset.open(name='conflict-test2', storage_client=storage_client) # Clean up await alias_dataset.drop() async def test_alias_parameter( storage_client: StorageClient, ) -> None: """Test dataset creation and operations with alias parameter.""" # Create dataset with alias alias_dataset = await Dataset.open( alias='test_alias', storage_client=storage_client, ) # Verify alias dataset properties assert alias_dataset.id is not None assert alias_dataset.name is None # Alias storages should be unnamed # Test data operations await alias_dataset.push_data({'type': 'alias', 'value': 1}) data = await alias_dataset.get_data() assert data.count == 1 assert data.items[0]['type'] == 'alias' await alias_dataset.drop() async def test_alias_vs_named_isolation( storage_client: StorageClient, ) -> None: """Test that alias and named datasets with same identifier are isolated.""" # Create named dataset named_dataset = await Dataset.open( name='test-identifier', storage_client=storage_client, ) # Verify named dataset assert named_dataset.name == 'test-identifier' await named_dataset.push_data({'type': 'named'}) # Clean up named dataset first await named_dataset.drop() # Now create alias dataset with same identifier (should work after cleanup) alias_dataset = await Dataset.open( alias='test_identifier', storage_client=storage_client, ) # Should be different instance assert alias_dataset.name is None await alias_dataset.push_data({'type': 'alias'}) # Verify alias data alias_data = await alias_dataset.get_data() assert alias_data.items[0]['type'] == 'alias' await alias_dataset.drop() async def test_default_vs_alias_default_equivalence( storage_client: StorageClient, ) -> None: """Test that default dataset and alias='default' are equivalent.""" # Open default dataset default_dataset = await Dataset.open( storage_client=storage_client, ) alias_default_dataset = await Dataset.open( alias=StorageInstanceManager._DEFAULT_STORAGE_ALIAS, storage_client=storage_client, ) # Should be the same assert default_dataset.id == alias_default_dataset.id assert default_dataset.name is None assert alias_default_dataset.name is None # Data should be shared await default_dataset.push_data({'source': 'default'}) data = await alias_default_dataset.get_data() assert data.items[0]['source'] == 'default' await default_dataset.drop() async def test_multiple_alias_isolation( storage_client: StorageClient, ) -> None: """Test that different aliases create separate datasets.""" datasets = [] for i in range(3): dataset = await Dataset.open( alias=f'alias_{i}', storage_client=storage_client, ) await dataset.push_data({'alias': f'alias_{i}', 'index': i}) datasets.append(dataset) # All should be different for i in range(3): for j in range(i + 1, 3): assert datasets[i].id != datasets[j].id # Verify data isolation for i, dataset in enumerate(datasets): data = await dataset.get_data() assert data.items[0]['alias'] == f'alias_{i}' await dataset.drop() async def test_purge_on_start_enabled(storage_client: StorageClient) -> None: """Test purge behavior when purge_on_start=True: named storages retain data, unnamed storages are purged.""" # Skip this test for memory storage since it doesn't persist data between client instances. if isinstance(storage_client, MemoryStorageClient): pytest.skip('Memory storage does not persist data between client instances.') configuration = Configuration(purge_on_start=True) # First, create all storage types with purge enabled and add data. default_dataset = await Dataset.open( storage_client=storage_client, configuration=configuration, ) alias_dataset = await Dataset.open( alias='purge-test-alias', storage_client=storage_client, configuration=configuration, ) named_dataset = await Dataset.open( name='purge-test-named', storage_client=storage_client, configuration=configuration, ) await default_dataset.push_data({'type': 'default', 'data': 'should_be_purged'}) await alias_dataset.push_data({'type': 'alias', 'data': 'should_be_purged'}) await named_dataset.push_data({'type': 'named', 'data': 'should_persist'}) # Verify data was added default_data = await default_dataset.get_data() alias_data = await alias_dataset.get_data() named_data = await named_dataset.get_data() assert len(default_data.items) == 1 assert len(alias_data.items) == 1 assert len(named_data.items) == 1 # Verify that default and alias storages are unnamed default_metadata = await default_dataset.get_metadata() alias_metadata = await alias_dataset.get_metadata() named_metadata = await named_dataset.get_metadata() assert default_metadata.name is None assert alias_metadata.name is None assert named_metadata.name == 'purge-test-named' # Clear storage cache to simulate "reopening" storages service_locator.storage_instance_manager.clear_cache() # Now "reopen" all storages default_dataset_2 = await Dataset.open( storage_client=storage_client, configuration=configuration, ) alias_dataset_2 = await Dataset.open( alias='purge-test-alias', storage_client=storage_client, configuration=configuration, ) named_dataset_2 = await Dataset.open( name='purge-test-named', storage_client=storage_client, configuration=configuration, ) # Check the data after purge default_data_after = await default_dataset_2.get_data() alias_data_after = await alias_dataset_2.get_data() named_data_after = await named_dataset_2.get_data() # Unnamed storages (alias and default) should be purged (data removed) assert len(default_data_after.items) == 0 assert len(alias_data_after.items) == 0 # Named storage should retain data (not purged) assert len(named_data_after.items) == 1 # Clean up await named_dataset_2.drop() await alias_dataset_2.drop() await default_dataset_2.drop() async def test_purge_on_start_disabled(storage_client: StorageClient) -> None: """Test purge behavior when purge_on_start=False: all storages retain data regardless of type.""" # Skip this test for memory storage since it doesn't persist data between client instances. if isinstance(storage_client, MemoryStorageClient): pytest.skip('Memory storage does not persist data between client instances.') configuration = Configuration(purge_on_start=False) # First, create all storage types with purge disabled and add data. default_dataset = await Dataset.open( storage_client=storage_client, configuration=configuration, ) alias_dataset = await Dataset.open( alias='purge-test-alias', storage_client=storage_client, configuration=configuration, ) named_dataset = await Dataset.open( name='purge-test-named', storage_client=storage_client, configuration=configuration, ) await default_dataset.push_data({'type': 'default', 'data': 'should_persist'}) await alias_dataset.push_data({'type': 'alias', 'data': 'should_persist'}) await named_dataset.push_data({'type': 'named', 'data': 'should_persist'}) # Verify data was added default_data = await default_dataset.get_data() alias_data = await alias_dataset.get_data() named_data = await named_dataset.get_data() assert len(default_data.items) == 1 assert len(alias_data.items) == 1 assert len(named_data.items) == 1 # Verify that default and alias storages are unnamed default_metadata = await default_dataset.get_metadata() alias_metadata = await alias_dataset.get_metadata() named_metadata = await named_dataset.get_metadata() assert default_metadata.name is None assert alias_metadata.name is None assert named_metadata.name == 'purge-test-named' # Clear storage cache to simulate "reopening" storages service_locator.storage_instance_manager.clear_cache() # Now "reopen" all storages default_dataset_2 = await Dataset.open( storage_client=storage_client, configuration=configuration, ) alias_dataset_2 = await Dataset.open( alias='purge-test-alias', storage_client=storage_client, configuration=configuration, ) named_dataset_2 = await Dataset.open( name='purge-test-named', storage_client=storage_client, configuration=configuration, ) # Check the data after purge default_data_after = await default_dataset_2.get_data() alias_data_after = await alias_dataset_2.get_data() named_data_after = await named_dataset_2.get_data() # All storages should retain data (not purged) assert len(default_data_after.items) == 1 assert len(alias_data_after.items) == 1 assert len(named_data_after.items) == 1 assert default_data_after.items[0]['data'] == 'should_persist' assert alias_data_after.items[0]['data'] == 'should_persist' assert named_data_after.items[0]['data'] == 'should_persist' # Clean up await default_dataset_2.drop() await alias_dataset_2.drop() await named_dataset_2.drop() async def test_name_default_not_allowed(storage_client: StorageClient) -> None: """Test that storage can't have default alias as name, to prevent collisions with unnamed storage alias.""" with pytest.raises( ValueError, match=f'Storage name cannot be "{StorageInstanceManager._DEFAULT_STORAGE_ALIAS}" as ' f'it is reserved for default alias.', ): await Dataset.open(name=StorageInstanceManager._DEFAULT_STORAGE_ALIAS, storage_client=storage_client) @pytest.mark.parametrize( ('name', 'is_valid'), [ pytest.param('F', True, id='single-char'), pytest.param('7', True, id='single-digit'), pytest.param('FtghdfseySds', True, id='mixed-case'), pytest.param('125673450', True, id='all-digits'), pytest.param('Ft2134Sfe0O1hf', True, id='mixed-alphanumeric'), pytest.param('name-with-dashes', True, id='dashes'), pytest.param('1-value', True, id='number start'), pytest.param('value-1', True, id='number end'), pytest.param('test-1-value', True, id='number middle'), pytest.param('test-------value', True, id='multiple-dashes'), pytest.param('test-VALUES-test', True, id='multiple-cases'), pytest.param('name_with_underscores', False, id='underscores'), pytest.param('name with spaces', False, id='spaces'), pytest.param('-test', False, id='dashes start'), pytest.param('test-', False, id='dashes end'), ], ) async def test_validate_name(storage_client: StorageClient, name: str, *, is_valid: bool) -> None: """Test name validation logic.""" if is_valid: # Should not raise dataset = await Dataset.open(name=name, storage_client=storage_client) assert dataset.name == name await dataset.drop() else: with pytest.raises(ValueError, match=rf'Invalid storage name "{name}".*'): await Dataset.open(name=name, storage_client=storage_client) async def test_record_with_noascii_chars(dataset: Dataset) -> None: """Test handling record with non-ASCII characters.""" init_value = { 'record_1': 'Supermaxi El Jardín', 'record_2': 'záznam dva', 'record_3': '記録三', } # Save the record to the dataset await dataset.push_data(init_value) # Get the record and verify value = await dataset.get_data() assert value is not None assert value.items[0] == init_value