Caching System

CountryFlag includes a robust caching system to improve performance when working with repeated country name lookups.

The caching system features automatic cache sharing, thread-safe operations, and intelligent key normalization for optimal performance.

Automatic Cache Sharing

Starting from version 1.0.1, CountryFlag automatically provides shared caching across instances when no explicit cache is provided. Enhanced in version 1.1.1 with improved performance.

from countryflag.core import CountryFlag

# These instances automatically share the same cache
cf1 = CountryFlag()
cf2 = CountryFlag()

# First call (cache miss)
flags1, _ = cf1.get_flag(["Germany"])

# Second call from different instance (cache hit!)
flags2, _ = cf2.get_flag(["Germany"])  # Much faster

# Verify cache sharing
assert cf1._cache is cf2._cache  # True

This behavior provides significant performance improvements as cache data accumulates across the application lifecycle, while maintaining full backward compatibility.

Global Cache Management

The shared cache can be easily managed for testing or reset scenarios:

# Clear the global cache
CountryFlag.clear_global_cache()

# Check cache statistics
hits = CountryFlag._global_cache.get_hits()
print(f"Cache hits: {hits}")

Thread Safety

All cache operations in CountryFlag are thread-safe, using threading.Lock to ensure data integrity in concurrent environments.

import threading
from countryflag.core import CountryFlag

def worker():
    """Worker function for concurrent access."""
    cf = CountryFlag()
    flags, _ = cf.get_flag(["Germany", "France"])
    return flags

# Create multiple threads
threads = []
for i in range(10):
    thread = threading.Thread(target=worker)
    threads.append(thread)
    thread.start()

# Wait for completion
for thread in threads:
    thread.join()

print("All threads completed safely")

Thread Safety Features:

  • All cache operations (get, set, delete, clear) are protected by locks

  • Concurrent access to global cache is safe across multiple instances

  • No data corruption or race conditions

  • Performance optimized with fine-grained locking

Key Normalization

CountryFlag uses intelligent key normalization to improve cache hit rates by creating deterministic cache keys regardless of input order.

cf = CountryFlag()

# These two calls will use the same cache entry
flags1, _ = cf.get_flag(["Germany", "France", "Italy"])
flags2, _ = cf.get_flag(["Italy", "Germany", "France"])  # Cache hit!

# The results are reordered to match input order
# but the underlying cache key is normalized

Key Normalization Benefits:

  • Order Independence: Same countries in different orders share cache entries

  • Higher Hit Rates: Reduces cache misses due to input variations

  • Memory Efficiency: Eliminates duplicate cache entries for equivalent requests

  • Intelligent Filtering: Only valid string entries are used for key generation

How It Works:

  1. Filter out invalid/empty entries from country list

  2. Sort valid country names alphabetically

  3. Create deterministic cache key with separator

  4. Store result with normalized key

  5. Reorder cached results to match current input order

Backward Compatibility and Migration

Important

Breaking Change in v1.0.1: The caching behavior has changed for CountryFlag instances created without an explicit cache parameter. Further optimized in v1.1.1.

Before v1.0.1:

# These instances had NO caching
cf1 = CountryFlag()  # cache = None
cf2 = CountryFlag()  # cache = None

# Each call was computed from scratch
flags1, _ = cf1.get_flag(["Germany"])  # No caching
flags2, _ = cf2.get_flag(["Germany"])  # No caching

From v1.0.1 onwards (enhanced in v1.1.1):

# These instances automatically share a global cache
cf1 = CountryFlag()  # cache = CountryFlag._global_cache
cf2 = CountryFlag()  # cache = CountryFlag._global_cache

# Cache data is shared between instances
flags1, _ = cf1.get_flag(["Germany"])  # Cache miss, stores result
flags2, _ = cf2.get_flag(["Germany"])  # Cache hit!

Migration Guide:

  1. No Code Changes Required: Existing code will continue to work

  2. Performance Improvement: Applications will automatically benefit from shared caching

  3. Custom Cache Users: No changes needed - explicit cache parameters work as before

  4. Testing Code: Use CountryFlag.clear_global_cache() to reset cache state between tests

Disabling Global Cache (if needed):

from countryflag.cache.base import NoOpCache

# Create instance with no caching (similar to old behavior)
cf = CountryFlag(cache=NoOpCache())

Impact on Performance:

  • Positive: Significantly faster repeated operations

  • Memory: Minimal increase due to singleton pattern

  • Thread Safety: Enhanced with proper locking mechanisms

Built-in Cache Types

Memory Cache

The simplest and fastest caching option, storing data in memory:

from countryflag.core import CountryFlag
from countryflag.cache import MemoryCache

# Create a memory cache
cache = MemoryCache()

# Create a CountryFlag instance with caching
cf = CountryFlag(cache=cache)

# First call (cache miss)
flags1 = cf.get_flag(["United States", "Canada"])

# Second call (cache hit - much faster)
flags2 = cf.get_flag(["United States", "Canada"])

Disk Cache

Persistent caching that survives program restarts:

from countryflag.cache import DiskCache

# Create a disk cache
cache = DiskCache("/path/to/cache/dir")

# Use it with CountryFlag
cf = CountryFlag(cache=cache)

Redis Cache

For distributed systems, use the Redis cache plugin:

from countryflag.plugins.redis_cache import RedisCache

# Create a Redis cache
cache = RedisCache(host="localhost", port=6379)

# Use it with CountryFlag
cf = CountryFlag(cache=cache)

Cache Configuration

Common configuration options:

# Memory cache with size limit
cache = MemoryCache(max_size=1000)

# Disk cache with TTL
cache = DiskCache(
    cache_dir="/path/to/cache",
    ttl=3600  # 1 hour
)

# Redis cache with custom configuration
cache = RedisCache(
    host="localhost",
    port=6379,
    db=0,
    password="optional_password",
    ttl=3600
)

Creating Custom Caches

You can create custom cache implementations by extending the base Cache class:

from countryflag.cache.base import Cache
from typing import Optional, Any

class CustomCache(Cache):
    def get(self, key: str) -> Optional[Any]:
        # Implementation
        pass

    def set(self, key: str, value: Any) -> None:
        # Implementation
        pass

    def delete(self, key: str) -> None:
        # Implementation
        pass

    def clear(self) -> None:
        # Implementation
        pass

    def contains(self, key: str) -> bool:
        # Implementation
        pass

Cache Performance

Benchmarking results for different cache types:

Operation

No Cache

Memory Cache

Disk Cache

First Call

100ms

100ms

100ms

Second Call

100ms

1ms

10ms

Best Practices

  1. Choose the Right Cache: - Use MemoryCache for single-process applications - Use DiskCache for persistence between runs - Use RedisCache for distributed systems

  2. Configure Cache Size: - Set appropriate size limits - Monitor cache usage - Implement cache eviction policies

  3. Handle Cache Failures: - Implement fallback mechanisms - Log cache errors - Monitor cache performance

  4. Cache Invalidation: - Implement clear cache policies - Use TTL for time-sensitive data - Provide cache clearing mechanisms

API Reference

Base cache interface for the countryflag package.

This module contains the base Cache interface that all cache implementations must implement.

class countryflag.cache.base.Cache[source]

Bases: ABC

Abstract base class for caching implementations.

This class defines the interface that all cache implementations must adhere to.

abstract get(key)[source]

Get a value from the cache.

Parameters:

key (str) – The cache key.

Returns:

The cached value, or None if the key is not in the cache.

Return type:

Any | None

abstract set(key, value)[source]

Set a value in the cache.

Parameters:
  • key (str) – The cache key.

  • value (Any) – The value to cache.

Return type:

None

abstract delete(key)[source]

Delete a value from the cache.

Parameters:

key (str) – The cache key to delete.

Return type:

None

abstract clear()[source]

Clear all values from the cache.

Return type:

None

abstract contains(key)[source]

Check if a key exists in the cache.

Parameters:

key (str) – The cache key to check.

Returns:

True if the key exists in the cache, False otherwise.

Return type:

bool

Memory-based cache implementation for the countryflag package.

This module contains the MemoryCache class, which implements in-memory caching.

class countryflag.cache.memory.MemoryCache[source]

Bases: Cache

In-memory cache implementation.

This class implements a simple in-memory cache using a dictionary.

_cache

Dictionary that stores the cached values.

Initialize the memory cache.

__init__()[source]

Initialize the memory cache.

Return type:

None

get(key)[source]

Get a value from the cache.

Parameters:

key (str) – The cache key.

Returns:

The cached value, or None if the key is not in the cache.

Return type:

Any | None

Example

>>> cache = MemoryCache()
>>> cache.set("key", "value")
>>> cache.get("key")
'value'
>>> cache.get("nonexistent")
None
set(key, value)[source]

Set a value in the cache.

Parameters:
  • key (str) – The cache key.

  • value (Any) – The value to cache.

Return type:

None

Example

>>> cache = MemoryCache()
>>> cache.set("key", "value")
>>> cache.get("key")
'value'
delete(key)[source]

Delete a value from the cache.

Parameters:

key (str) – The cache key to delete.

Return type:

None

Example

>>> cache = MemoryCache()
>>> cache.set("key", "value")
>>> cache.delete("key")
>>> cache.get("key")
None
clear()[source]

Clear all values from the cache.

Example

>>> cache = MemoryCache()
>>> cache.set("key1", "value1")
>>> cache.set("key2", "value2")
>>> cache.clear()
>>> cache.get("key1")
None
>>> cache.get("key2")
None
Return type:

None

contains(key)[source]

Check if a key exists in the cache.

Parameters:

key (str) – The cache key to check.

Returns:

True if the key exists in the cache, False otherwise.

Return type:

bool

Example

>>> cache = MemoryCache()
>>> cache.set("key", "value")
>>> cache.contains("key")
True
>>> cache.contains("nonexistent")
False
get_hits()[source]

Get the number of cache hits.

Returns:

The number of cache hits.

Return type:

int

property hits: int

Property to get the number of cache hits.

Returns:

The number of cache hits.

Return type:

int

reset_hits()[source]

Reset the cache hit counter.

Return type:

None

Disk-based cache implementation for the countryflag package.

This module contains the DiskCache class, which implements on-disk caching.

class countryflag.cache.disk.DiskCache(cache_dir)[source]

Bases: Cache

Disk-based cache implementation.

This class implements an on-disk cache using JSON files.

_cache_dir

Path to the cache directory.

_index

Dictionary that maps cache keys to filenames.

Initialize the disk cache.

Parameters:

cache_dir (str) – Path to the cache directory.

Raises:

CacheError – If the cache directory cannot be created.

__init__(cache_dir)[source]

Initialize the disk cache.

Parameters:

cache_dir (str) – Path to the cache directory.

Raises:

CacheError – If the cache directory cannot be created.

Return type:

None

get(key)[source]

Get a value from the cache.

Parameters:

key (str) – The cache key.

Returns:

The cached value, or None if the key is not in the cache.

Raises:

CacheError – If the cache file cannot be read.

Return type:

Any | None

Example

>>> cache = DiskCache("/tmp/countryflag_cache")
>>> cache.set("key", "value")
>>> cache.get("key")
'value'
>>> cache.get("nonexistent")
None
set(key, value)[source]

Set a value in the cache.

Parameters:
  • key (str) – The cache key.

  • value (Any) – The value to cache.

Raises:

CacheError – If the cache file cannot be written.

Return type:

None

Example

>>> cache = DiskCache("/tmp/countryflag_cache")
>>> cache.set("key", "value")
>>> cache.get("key")
'value'
delete(key)[source]

Delete a value from the cache.

Parameters:

key (str) – The cache key to delete.

Return type:

None

Example

>>> cache = DiskCache("/tmp/countryflag_cache")
>>> cache.set("key", "value")
>>> cache.delete("key")
>>> cache.get("key")
None
clear()[source]

Clear all values from the cache.

Example

>>> cache = DiskCache("/tmp/countryflag_cache")
>>> cache.set("key1", "value1")
>>> cache.set("key2", "value2")
>>> cache.clear()
>>> cache.get("key1")
None
>>> cache.get("key2")
None
Return type:

None

get_hits()[source]

Get the number of cache hits.

Returns:

The number of cache hits.

Return type:

int

reset_hits()[source]

Reset the cache hit counter.

Return type:

None

contains(key)[source]

Check if a key exists in the cache.

Parameters:

key (str) – The cache key to check.

Returns:

True if the key exists in the cache, False otherwise.

Return type:

bool

Example

>>> cache = DiskCache("/tmp/countryflag_cache")
>>> cache.set("key", "value")
>>> cache.contains("key")
True
>>> cache.contains("nonexistent")
False