crawlee-python/tests/unit/storages/test_dataset.py

1099 lines
34 KiB
Python

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