From 5d00ebdab23de0ffd80e767cf3b36579ce6f8ffa Mon Sep 17 00:00:00 2001 From: Spicer Bak Date: Thu, 16 Jul 2026 08:47:25 -0400 Subject: [PATCH 1/7] Cache server detection at module level with thread safety Previously, get_thredds_server() ran socket.gethostbyname() on every call to detect FRF vs external network. This adds module-level caching so the IP address and server selection are computed once at first use. Thread safety improvements: - Use threading.RLock (reentrant lock) to prevent deadlock when get_thredds_server() calls _get_cached_ip() while holding the lock - Double-checked locking pattern for safe concurrent initialization - Catch OSError (Python 3 standard) instead of socket.error Input validation: - Validate ip_address is a string to prevent type confusion attacks - Return safe defaults (empty string, False) for invalid inputs Changes: - Add _cache_lock (RLock), _cached_ip_address, _cached_server_result - Add _get_cached_ip() helper for lazy thread-safe IP detection - Modify get_thredds_server() to return cached result for default case - Modify is_frf_network() to use cached IP with type validation - Add clear_server_cache() for testing and network change scenarios - Add 9 tests for caching behavior including thread safety Addresses issue #50 task 2.1. Co-Authored-By: Claude Opus 4.5 --- murgtools/config.py | 108 +++++++++++++++++++++++++++++----- tests/test_config.py | 135 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 230 insertions(+), 13 deletions(-) diff --git a/murgtools/config.py b/murgtools/config.py index cd79afd..032a797 100644 --- a/murgtools/config.py +++ b/murgtools/config.py @@ -6,6 +6,7 @@ """ import socket +import threading # ============================================================================= # THREDDS Server URLs @@ -91,29 +92,97 @@ # Helper Functions # ============================================================================= +# ============================================================================= +# Server Detection Cache (thread-safe) +# ============================================================================= +# Cache is computed once at first use to avoid repeated socket operations. +# Both FRF and CHL servers are trusted government endpoints - server selection +# only affects performance (local vs remote), not security. + +# Use RLock (reentrant lock) because get_thredds_server calls _get_cached_ip +# while holding the lock +_cache_lock = threading.RLock() +_cached_ip_address = None +_cached_server_result = None + + +def _get_cached_ip(): + """Get the cached IP address, computing it once if needed (thread-safe). + + Returns: + str: The machine's IP address, or empty string if detection failed. + """ + global _cached_ip_address + + # Fast path: check without lock first (safe because we only write once) + if _cached_ip_address is not None: + return _cached_ip_address + + with _cache_lock: + # Double-check after acquiring lock + if _cached_ip_address is not None: + return _cached_ip_address + + try: + _cached_ip_address = socket.gethostbyname(socket.gethostname()) + except OSError: + # Covers socket.error, socket.gaierror, socket.herror in Python 3 + _cached_ip_address = '' + + return _cached_ip_address + def get_thredds_server(server=None, ip_address=None): - """Select appropriate THREDDS server based on network location. + """Select appropriate THREDDS server based on network location (thread-safe). + + Results are cached at module level for auto-detection (when server=None + and ip_address=None) to avoid repeated socket operations. Args: server (str, optional): Force server selection. 'FRF' for local, 'CHL' for public. If None, auto-detect based on IP. ip_address (str, optional): IP address to check. If None, - uses current machine's IP. + uses cached machine IP. Returns: tuple: (server_url, server_prefix) where server_prefix is 'FRF' or 'frf' for use in constructing data paths. """ + global _cached_server_result + + # Fast path: return cached result for default auto-detection case + if server is None and ip_address is None: + # Check without lock first + if _cached_server_result is not None: + return _cached_server_result + + with _cache_lock: + # Double-check after acquiring lock + if _cached_server_result is not None: + return _cached_server_result + + # Compute and cache the result + detected_ip = _get_cached_ip() + on_frf_network = detected_ip.startswith(FRF_IP_PREFIXES) + + if on_frf_network: + _cached_server_result = (THREDDS_FRF_LOCAL, 'FRF') + else: + _cached_server_result = (THREDDS_CHL_PUBLIC, 'frf') + + return _cached_server_result + + # Non-cached path: explicit server or ip_address provided if ip_address is None: - try: - ip_address = socket.gethostbyname(socket.gethostname()) - except socket.error: - ip_address = '' + ip_address = _get_cached_ip() + + # Validate ip_address is a string to prevent type confusion + if not isinstance(ip_address, str): + ip_address = '' - is_frf_network = ip_address.startswith(FRF_IP_PREFIXES) + on_frf_network = ip_address.startswith(FRF_IP_PREFIXES) - if server == 'FRF' or (server is None and is_frf_network): + if server == 'FRF' or (server is None and on_frf_network): return THREDDS_FRF_LOCAL, 'FRF' else: return THREDDS_CHL_PUBLIC, 'frf' @@ -124,15 +193,28 @@ def is_frf_network(ip_address=None): Args: ip_address (str, optional): IP address to check. If None, - uses current machine's IP. + uses cached machine IP. Returns: bool: True if on FRF network, False otherwise. """ if ip_address is None: - try: - ip_address = socket.gethostbyname(socket.gethostname()) - except socket.error: - return False + ip_address = _get_cached_ip() + + # Validate ip_address is a string to prevent type confusion + if not isinstance(ip_address, str): + return False return ip_address.startswith(FRF_IP_PREFIXES) + + +def clear_server_cache(): + """Clear the cached server detection results (thread-safe). + + Useful for testing or if network configuration changes during runtime. + """ + global _cached_ip_address, _cached_server_result + + with _cache_lock: + _cached_ip_address = None + _cached_server_result = None diff --git a/tests/test_config.py b/tests/test_config.py index a6ec5a1..05292b3 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -98,11 +98,146 @@ def test_force_chl_server(self): def test_socket_error_defaults_to_chl(self): """Verify socket errors default to CHL server.""" with patch('socket.gethostbyname', side_effect=OSError('Network error')): + config.clear_server_cache() # Clear cache to force re-detection url, prefix = config.get_thredds_server() assert url == config.THREDDS_CHL_PUBLIC assert prefix == 'frf' +class TestServerCaching: + """Tests for server detection caching functionality.""" + + def setup_method(self): + """Clear the cache before each test.""" + config.clear_server_cache() + + def teardown_method(self): + """Clear the cache after each test.""" + config.clear_server_cache() + + def test_cache_returns_same_result(self): + """Verify cached result is returned on subsequent calls.""" + # First call should compute and cache + result1 = config.get_thredds_server() + # Second call should return cached result + result2 = config.get_thredds_server() + + assert result1 == result2 + + def test_cache_avoids_repeated_socket_calls(self): + """Verify socket operations are only called once with caching.""" + with patch('murgtools.config.socket.gethostbyname', return_value='192.168.1.1') as mock_socket: + config.clear_server_cache() + + # Multiple calls should only trigger one socket operation + config.get_thredds_server() + config.get_thredds_server() + config.get_thredds_server() + + # Socket should only be called once (for IP detection) + assert mock_socket.call_count == 1 + + def test_clear_cache_resets_detection(self): + """Verify clear_server_cache resets the cached state.""" + with patch('murgtools.config.socket.gethostbyname', return_value='192.168.1.1') as mock_socket: + config.clear_server_cache() + + # First call + config.get_thredds_server() + assert mock_socket.call_count == 1 + + # Clear cache + config.clear_server_cache() + + # Second call should trigger socket again + config.get_thredds_server() + assert mock_socket.call_count == 2 + + def test_explicit_ip_bypasses_cache(self): + """Verify explicit ip_address parameter still works correctly.""" + config.clear_server_cache() + + # Cache with external IP + with patch('murgtools.config.socket.gethostbyname', return_value='192.168.1.1'): + url1, prefix1 = config.get_thredds_server() + assert prefix1 == 'frf' # External network + + # Explicit FRF IP should return FRF server regardless of cache + url2, prefix2 = config.get_thredds_server(ip_address='134.164.129.1') + assert prefix2 == 'FRF' + + def test_explicit_server_bypasses_cache(self): + """Verify explicit server parameter still works correctly.""" + config.clear_server_cache() + + # Cache with external IP + with patch('murgtools.config.socket.gethostbyname', return_value='192.168.1.1'): + url1, prefix1 = config.get_thredds_server() + assert prefix1 == 'frf' # External network + + # Explicit FRF server should return FRF regardless of cache + url2, prefix2 = config.get_thredds_server(server='FRF') + assert prefix2 == 'FRF' + + def test_is_frf_network_uses_cached_ip(self): + """Verify is_frf_network uses cached IP address.""" + with patch('murgtools.config.socket.gethostbyname', return_value='134.164.129.1') as mock_socket: + config.clear_server_cache() + + # Multiple calls should only trigger one socket operation + config.is_frf_network() + config.is_frf_network() + config.is_frf_network() + + assert mock_socket.call_count == 1 + + def test_thread_safety_concurrent_access(self): + """Verify cache is thread-safe under concurrent access.""" + import threading + import time + + config.clear_server_cache() + results = [] + errors = [] + + def worker(): + try: + for _ in range(10): + result = config.get_thredds_server() + results.append(result) + except Exception as e: + errors.append(e) + + # Run multiple threads concurrently + threads = [threading.Thread(target=worker) for _ in range(5)] + for t in threads: + t.start() + for t in threads: + t.join() + + # No errors should occur + assert len(errors) == 0, f"Thread errors: {errors}" + + # All results should be identical + assert len(set(results)) == 1, "Inconsistent results across threads" + + def test_invalid_ip_address_type_handled(self): + """Verify non-string ip_address is handled safely.""" + # Should not raise, should return CHL (external) server + url, prefix = config.get_thredds_server(ip_address=12345) + assert prefix == 'frf' + + url, prefix = config.get_thredds_server(ip_address=None) + # None triggers cache lookup, which is valid + assert prefix in ('frf', 'FRF') + + def test_is_frf_network_invalid_ip_returns_false(self): + """Verify is_frf_network returns False for invalid ip_address types.""" + assert config.is_frf_network(ip_address=12345) is False + assert config.is_frf_network(ip_address=['134.164.1.1']) is False + assert config.is_frf_network(ip_address={'ip': '134.164.1.1'}) is False + + class TestIsFrfNetwork: """Tests for is_frf_network helper function.""" From abaf54372d7a6b6f5b943d1cd33c5b42409bb70d Mon Sep 17 00:00:00 2001 From: Spicer Bak Date: Thu, 16 Jul 2026 11:33:04 -0400 Subject: [PATCH 2/7] Add disk-based data caching system (OFF by default) Implements a comprehensive data caching system for murgtools that stores retrieved data to disk to avoid repeated network requests. Features: - Caching is OFF by default, must be explicitly enabled - Enable via code: enable_cache() or DataCache(enabled=True) - Enable via environment: MURGTOOLS_CACHE_ENABLED=1 - Default cache directory: /data/getdata (configurable) - Default TTL: 180 days / 6 months (configurable) - Force refresh: cache.get(..., force_refresh=True) - Clear all or by age: clear_cache(older_than_days=30) - Thread-safe with RLock Configuration options: - cache_dir: Custom cache directory path - ttl_days: Time-to-live in days (0 = never expire) - Environment variables for all settings New files: - murgtools/cache.py: DataCache class and helper functions - tests/test_cache.py: 34 comprehensive tests Addresses issue #50 (partial - data caching infrastructure). Co-Authored-By: Claude Opus 4.5 --- murgtools/__init__.py | 7 + murgtools/cache.py | 571 ++++++++++++++++++++++++++++++++++++++++++ murgtools/config.py | 17 ++ tests/test_cache.py | 498 ++++++++++++++++++++++++++++++++++++ 4 files changed, 1093 insertions(+) create mode 100644 murgtools/cache.py create mode 100644 tests/test_cache.py diff --git a/murgtools/__init__.py b/murgtools/__init__.py index 53b56d0..d437578 100644 --- a/murgtools/__init__.py +++ b/murgtools/__init__.py @@ -4,6 +4,7 @@ from .getdata import forecastData, getSatelliteImagery, alt_PlotData from .getdata import getArgusImagery, threadGetArgusImagery from .plotting import conditions_plot, bin_data +from .cache import DataCache, get_cache, enable_cache, disable_cache, clear_cache __all__ = [ "getObs", @@ -18,4 +19,10 @@ "alt_PlotData", "conditions_plot", "bin_data", + # Cache functions + "DataCache", + "get_cache", + "enable_cache", + "disable_cache", + "clear_cache", ] diff --git a/murgtools/cache.py b/murgtools/cache.py new file mode 100644 index 0000000..141506f --- /dev/null +++ b/murgtools/cache.py @@ -0,0 +1,571 @@ +"""Data caching module for murgtools. + +This module provides disk-based caching for retrieved data to avoid repeated +network requests. Caching is OFF by default and must be explicitly enabled. + +Features: + - Disk-based persistence at configurable location (default: /data/getdata) + - Configurable TTL (default: 6 months / 180 days) + - Thread-safe operations + - Force refresh capability + - Automatic cache invalidation for stale data + +Usage: + from murgtools.cache import DataCache + + # Create cache instance (caching disabled by default) + cache = DataCache() + + # Enable caching + cache = DataCache(enabled=True) + + # Custom configuration + cache = DataCache( + enabled=True, + cache_dir='/custom/path', + ttl_days=90, # 3 months + ) + + # Force refresh even if cache is valid + data = cache.get(key, fetch_func, force_refresh=True) + +Environment Variables: + MURGTOOLS_CACHE_ENABLED: Set to '1' or 'true' to enable caching + MURGTOOLS_CACHE_DIR: Override default cache directory + MURGTOOLS_CACHE_TTL_DAYS: Override default TTL in days +""" + +import hashlib +import json +import logging +import os +import pickle +import threading +import time +from datetime import datetime, timedelta +from pathlib import Path +from typing import Any, Callable, Dict, Optional, Tuple, Union + +from . import config + +logger = logging.getLogger(__name__) + + +class CacheError(Exception): + """Base exception for cache-related errors.""" + pass + + +class CacheMetadata: + """Metadata for a cached entry.""" + + def __init__(self, created_at: float, data_source: str, time_range: Optional[Tuple] = None, + extra_info: Optional[Dict] = None): + """Initialize cache metadata. + + Args: + created_at: Unix timestamp when cache was created. + data_source: Identifier for the data source (e.g., URL, gauge name). + time_range: Optional tuple of (start_epoch, end_epoch) for time-series data. + extra_info: Optional dictionary with additional metadata. + """ + self.created_at = created_at + self.data_source = data_source + self.time_range = time_range + self.extra_info = extra_info or {} + + def to_dict(self) -> Dict: + """Convert metadata to dictionary for serialization.""" + return { + 'created_at': self.created_at, + 'data_source': self.data_source, + 'time_range': self.time_range, + 'extra_info': self.extra_info, + 'version': '1.0', + } + + @classmethod + def from_dict(cls, data: Dict) -> 'CacheMetadata': + """Create metadata from dictionary.""" + return cls( + created_at=data['created_at'], + data_source=data['data_source'], + time_range=data.get('time_range'), + extra_info=data.get('extra_info', {}), + ) + + def is_expired(self, ttl_days: int) -> bool: + """Check if the cache entry has expired. + + Args: + ttl_days: Time-to-live in days. + + Returns: + True if expired, False otherwise. + """ + if ttl_days <= 0: + return False # TTL of 0 means never expire + + expiry_time = self.created_at + (ttl_days * 24 * 60 * 60) + return time.time() > expiry_time + + def age_days(self) -> float: + """Get the age of this cache entry in days.""" + return (time.time() - self.created_at) / (24 * 60 * 60) + + +class DataCache: + """Thread-safe disk-based data cache. + + Caching is OFF by default. Enable it explicitly via the `enabled` parameter + or by setting the MURGTOOLS_CACHE_ENABLED environment variable. + """ + + def __init__(self, enabled: Optional[bool] = None, cache_dir: Optional[str] = None, + ttl_days: Optional[int] = None): + """Initialize the data cache. + + Args: + enabled: Whether caching is enabled. If None, checks environment + variable MURGTOOLS_CACHE_ENABLED, otherwise defaults to False. + cache_dir: Directory to store cache files. If None, checks environment + variable MURGTOOLS_CACHE_DIR, otherwise uses config.DEFAULT_CACHE_DIR. + ttl_days: Time-to-live in days. If None, checks environment variable + MURGTOOLS_CACHE_TTL_DAYS, otherwise uses config.DEFAULT_CACHE_TTL_DAYS. + Set to 0 for no expiration. + """ + self._lock = threading.RLock() + + # Determine if caching is enabled + if enabled is not None: + self._enabled = enabled + else: + env_enabled = os.environ.get(config.ENV_CACHE_ENABLED, '').lower() + self._enabled = env_enabled in ('1', 'true', 'yes', 'on') + + # Determine cache directory + if cache_dir is not None: + self._cache_dir = Path(cache_dir) + else: + env_dir = os.environ.get(config.ENV_CACHE_DIR) + self._cache_dir = Path(env_dir) if env_dir else Path(config.DEFAULT_CACHE_DIR) + + # Determine TTL + if ttl_days is not None: + self._ttl_days = ttl_days + else: + env_ttl = os.environ.get(config.ENV_CACHE_TTL_DAYS) + if env_ttl: + try: + self._ttl_days = int(env_ttl) + except ValueError: + logger.warning(f"Invalid TTL value '{env_ttl}', using default") + self._ttl_days = config.DEFAULT_CACHE_TTL_DAYS + else: + self._ttl_days = config.DEFAULT_CACHE_TTL_DAYS + + # Create cache directory if enabled and directory doesn't exist + if self._enabled: + self._ensure_cache_dir() + + @property + def enabled(self) -> bool: + """Whether caching is enabled.""" + return self._enabled + + @enabled.setter + def enabled(self, value: bool): + """Enable or disable caching.""" + with self._lock: + self._enabled = value + if value: + self._ensure_cache_dir() + + @property + def cache_dir(self) -> Path: + """The cache directory path.""" + return self._cache_dir + + @cache_dir.setter + def cache_dir(self, value: Union[str, Path]): + """Set the cache directory.""" + with self._lock: + self._cache_dir = Path(value) + if self._enabled: + self._ensure_cache_dir() + + @property + def ttl_days(self) -> int: + """Time-to-live in days.""" + return self._ttl_days + + @ttl_days.setter + def ttl_days(self, value: int): + """Set the TTL in days.""" + if value < 0: + raise ValueError("TTL must be non-negative") + self._ttl_days = value + + def _ensure_cache_dir(self): + """Create cache directory if it doesn't exist.""" + try: + self._cache_dir.mkdir(parents=True, exist_ok=True) + except PermissionError as e: + logger.warning(f"Cannot create cache directory {self._cache_dir}: {e}") + logger.warning("Caching will be disabled") + self._enabled = False + except OSError as e: + logger.warning(f"Error creating cache directory {self._cache_dir}: {e}") + self._enabled = False + + def _generate_cache_key(self, data_source: str, time_range: Optional[Tuple] = None, + extra_params: Optional[Dict] = None) -> str: + """Generate a unique cache key based on request parameters. + + Args: + data_source: Identifier for the data source. + time_range: Optional tuple of (start_epoch, end_epoch). + extra_params: Optional additional parameters that affect the data. + + Returns: + A hex string suitable for use as a filename. + """ + key_parts = [data_source] + + if time_range: + key_parts.append(f"t{time_range[0]}-{time_range[1]}") + + if extra_params: + # Sort keys for deterministic ordering + sorted_params = sorted(extra_params.items()) + key_parts.append(json.dumps(sorted_params, sort_keys=True)) + + key_string = '|'.join(str(p) for p in key_parts) + return hashlib.sha256(key_string.encode()).hexdigest()[:32] + + def _get_cache_paths(self, cache_key: str) -> Tuple[Path, Path]: + """Get paths for data and metadata files. + + Args: + cache_key: The cache key. + + Returns: + Tuple of (data_path, metadata_path). + """ + data_path = self._cache_dir / f"{cache_key}.pkl" + meta_path = self._cache_dir / f"{cache_key}.meta.json" + return data_path, meta_path + + def get(self, data_source: str, fetch_func: Callable[[], Any], + time_range: Optional[Tuple] = None, extra_params: Optional[Dict] = None, + force_refresh: bool = False) -> Any: + """Get data from cache or fetch it. + + Args: + data_source: Identifier for the data source (e.g., URL, gauge name). + fetch_func: Callable that fetches the data if not cached. + time_range: Optional tuple of (start_epoch, end_epoch) for time-series data. + extra_params: Optional additional parameters that affect the data. + force_refresh: If True, bypass cache and fetch fresh data. + + Returns: + The requested data (from cache or freshly fetched). + """ + # If caching is disabled, just fetch + if not self._enabled: + return fetch_func() + + cache_key = self._generate_cache_key(data_source, time_range, extra_params) + + with self._lock: + # Check cache unless force_refresh is requested + if not force_refresh: + cached_data = self._read_cache(cache_key) + if cached_data is not None: + return cached_data + + # Fetch fresh data + data = fetch_func() + + # Store in cache + if data is not None: + self._write_cache(cache_key, data, data_source, time_range, extra_params) + + return data + + def _read_cache(self, cache_key: str) -> Optional[Any]: + """Read data from cache if valid. + + Args: + cache_key: The cache key. + + Returns: + Cached data if valid, None otherwise. + """ + data_path, meta_path = self._get_cache_paths(cache_key) + + # Check if cache files exist + if not data_path.exists() or not meta_path.exists(): + return None + + try: + # Read and validate metadata + with open(meta_path, 'r') as f: + meta_dict = json.load(f) + metadata = CacheMetadata.from_dict(meta_dict) + + # Check if expired + if metadata.is_expired(self._ttl_days): + logger.debug(f"Cache expired for {cache_key} (age: {metadata.age_days():.1f} days)") + self._remove_cache(cache_key) + return None + + # Read data + with open(data_path, 'rb') as f: + data = pickle.load(f) + + logger.debug(f"Cache hit for {cache_key} (age: {metadata.age_days():.1f} days)") + return data + + except (json.JSONDecodeError, pickle.UnpicklingError, KeyError, OSError) as e: + logger.warning(f"Error reading cache {cache_key}: {e}") + self._remove_cache(cache_key) + return None + + def _write_cache(self, cache_key: str, data: Any, data_source: str, + time_range: Optional[Tuple], extra_params: Optional[Dict]): + """Write data to cache. + + Args: + cache_key: The cache key. + data: The data to cache. + data_source: Identifier for the data source. + time_range: Optional time range tuple. + extra_params: Optional extra parameters. + """ + data_path, meta_path = self._get_cache_paths(cache_key) + + try: + # Create metadata + metadata = CacheMetadata( + created_at=time.time(), + data_source=data_source, + time_range=time_range, + extra_info=extra_params, + ) + + # Write metadata first (smaller, validates we can write) + with open(meta_path, 'w') as f: + json.dump(metadata.to_dict(), f, indent=2) + + # Write data + with open(data_path, 'wb') as f: + pickle.dump(data, f, protocol=pickle.HIGHEST_PROTOCOL) + + logger.debug(f"Cached data for {cache_key}") + + except (OSError, pickle.PicklingError) as e: + logger.warning(f"Error writing cache {cache_key}: {e}") + # Clean up partial writes + self._remove_cache(cache_key) + + def _remove_cache(self, cache_key: str): + """Remove cache files for a key. + + Args: + cache_key: The cache key. + """ + data_path, meta_path = self._get_cache_paths(cache_key) + + for path in (data_path, meta_path): + try: + if path.exists(): + path.unlink() + except OSError as e: + logger.warning(f"Error removing cache file {path}: {e}") + + def clear(self, older_than_days: Optional[int] = None): + """Clear cache entries. + + Args: + older_than_days: If specified, only clear entries older than this. + If None, clear all entries. + """ + if not self._cache_dir.exists(): + return + + with self._lock: + cleared = 0 + for meta_path in self._cache_dir.glob("*.meta.json"): + try: + with open(meta_path, 'r') as f: + meta_dict = json.load(f) + metadata = CacheMetadata.from_dict(meta_dict) + + if older_than_days is None or metadata.age_days() > older_than_days: + cache_key = meta_path.stem.replace('.meta', '') + self._remove_cache(cache_key) + cleared += 1 + + except (json.JSONDecodeError, KeyError, OSError) as e: + logger.warning(f"Error reading metadata {meta_path}: {e}") + # Remove corrupted entry + cache_key = meta_path.stem.replace('.meta', '') + self._remove_cache(cache_key) + cleared += 1 + + logger.info(f"Cleared {cleared} cache entries") + + def get_stats(self) -> Dict: + """Get cache statistics. + + Returns: + Dictionary with cache statistics. + """ + if not self._cache_dir.exists(): + return { + 'enabled': self._enabled, + 'cache_dir': str(self._cache_dir), + 'ttl_days': self._ttl_days, + 'entry_count': 0, + 'total_size_mb': 0, + } + + entry_count = 0 + total_size = 0 + oldest_entry = None + newest_entry = None + + for data_path in self._cache_dir.glob("*.pkl"): + entry_count += 1 + total_size += data_path.stat().st_size + + meta_path = data_path.with_suffix('.meta.json') + if meta_path.exists(): + try: + with open(meta_path, 'r') as f: + meta_dict = json.load(f) + created = meta_dict.get('created_at', 0) + if oldest_entry is None or created < oldest_entry: + oldest_entry = created + if newest_entry is None or created > newest_entry: + newest_entry = created + except (json.JSONDecodeError, OSError): + pass + + return { + 'enabled': self._enabled, + 'cache_dir': str(self._cache_dir), + 'ttl_days': self._ttl_days, + 'entry_count': entry_count, + 'total_size_mb': round(total_size / (1024 * 1024), 2), + 'oldest_entry': datetime.fromtimestamp(oldest_entry).isoformat() if oldest_entry else None, + 'newest_entry': datetime.fromtimestamp(newest_entry).isoformat() if newest_entry else None, + } + + def is_cached(self, data_source: str, time_range: Optional[Tuple] = None, + extra_params: Optional[Dict] = None) -> bool: + """Check if data is cached and valid. + + Args: + data_source: Identifier for the data source. + time_range: Optional time range tuple. + extra_params: Optional extra parameters. + + Returns: + True if valid cache exists, False otherwise. + """ + if not self._enabled: + return False + + cache_key = self._generate_cache_key(data_source, time_range, extra_params) + data_path, meta_path = self._get_cache_paths(cache_key) + + if not data_path.exists() or not meta_path.exists(): + return False + + try: + with open(meta_path, 'r') as f: + meta_dict = json.load(f) + metadata = CacheMetadata.from_dict(meta_dict) + return not metadata.is_expired(self._ttl_days) + except (json.JSONDecodeError, KeyError, OSError): + return False + + def invalidate(self, data_source: str, time_range: Optional[Tuple] = None, + extra_params: Optional[Dict] = None): + """Invalidate (remove) a specific cache entry. + + Args: + data_source: Identifier for the data source. + time_range: Optional time range tuple. + extra_params: Optional extra parameters. + """ + cache_key = self._generate_cache_key(data_source, time_range, extra_params) + with self._lock: + self._remove_cache(cache_key) + + +# Global cache instance (disabled by default) +_global_cache: Optional[DataCache] = None +_global_cache_lock = threading.Lock() + + +def get_cache(enabled: Optional[bool] = None, cache_dir: Optional[str] = None, + ttl_days: Optional[int] = None) -> DataCache: + """Get or create the global cache instance. + + Args: + enabled: Whether to enable caching. If None on first call, defaults to False. + cache_dir: Cache directory. If None, uses default or environment variable. + ttl_days: TTL in days. If None, uses default or environment variable. + + Returns: + The global DataCache instance. + """ + global _global_cache + + with _global_cache_lock: + if _global_cache is None: + _global_cache = DataCache(enabled=enabled, cache_dir=cache_dir, ttl_days=ttl_days) + else: + # Update settings if provided + if enabled is not None: + _global_cache.enabled = enabled + if cache_dir is not None: + _global_cache.cache_dir = cache_dir + if ttl_days is not None: + _global_cache.ttl_days = ttl_days + + return _global_cache + + +def enable_cache(cache_dir: Optional[str] = None, ttl_days: Optional[int] = None) -> DataCache: + """Enable the global cache with optional configuration. + + Convenience function to enable caching with one call. + + Args: + cache_dir: Cache directory. If None, uses default. + ttl_days: TTL in days. If None, uses default (180 days). + + Returns: + The enabled DataCache instance. + """ + return get_cache(enabled=True, cache_dir=cache_dir, ttl_days=ttl_days) + + +def disable_cache(): + """Disable the global cache.""" + cache = get_cache() + cache.enabled = False + + +def clear_cache(older_than_days: Optional[int] = None): + """Clear the global cache. + + Args: + older_than_days: If specified, only clear entries older than this. + """ + cache = get_cache() + cache.clear(older_than_days=older_than_days) diff --git a/murgtools/config.py b/murgtools/config.py index 032a797..a842298 100644 --- a/murgtools/config.py +++ b/murgtools/config.py @@ -88,6 +88,23 @@ ARGUS_IMAGE_TYPES = ('timex', 'var', 'snap', 'bright', 'dark') +# ============================================================================= +# Data Cache Configuration +# ============================================================================= +# Caching is OFF by default. Users must explicitly enable it. +# Cache stores retrieved data to disk to avoid repeated network requests. + +# Default cache directory (user can override) +DEFAULT_CACHE_DIR = '/data/getdata' + +# Default cache TTL in days (6 months = ~180 days) +DEFAULT_CACHE_TTL_DAYS = 180 + +# Environment variable names for configuration overrides +ENV_CACHE_ENABLED = 'MURGTOOLS_CACHE_ENABLED' +ENV_CACHE_DIR = 'MURGTOOLS_CACHE_DIR' +ENV_CACHE_TTL_DAYS = 'MURGTOOLS_CACHE_TTL_DAYS' + # ============================================================================= # Helper Functions # ============================================================================= diff --git a/tests/test_cache.py b/tests/test_cache.py new file mode 100644 index 0000000..2d599a5 --- /dev/null +++ b/tests/test_cache.py @@ -0,0 +1,498 @@ +"""Tests for murgtools.cache module.""" + +import json +import os +import pickle +import tempfile +import threading +import time +from pathlib import Path +from unittest.mock import patch + +import pytest + +from murgtools import cache +from murgtools.cache import DataCache, CacheMetadata, get_cache, enable_cache, disable_cache, clear_cache + + +class TestCacheMetadata: + """Tests for CacheMetadata class.""" + + def test_to_dict_and_from_dict(self): + """Test serialization round-trip.""" + meta = CacheMetadata( + created_at=1000000.0, + data_source='test_source', + time_range=(100, 200), + extra_info={'param': 'value'}, + ) + + meta_dict = meta.to_dict() + restored = CacheMetadata.from_dict(meta_dict) + + assert restored.created_at == meta.created_at + assert restored.data_source == meta.data_source + assert restored.time_range == meta.time_range + assert restored.extra_info == meta.extra_info + + def test_is_expired_within_ttl(self): + """Test that recent entries are not expired.""" + meta = CacheMetadata(created_at=time.time(), data_source='test') + assert meta.is_expired(ttl_days=180) is False + + def test_is_expired_after_ttl(self): + """Test that old entries are expired.""" + # Created 200 days ago + created = time.time() - (200 * 24 * 60 * 60) + meta = CacheMetadata(created_at=created, data_source='test') + assert meta.is_expired(ttl_days=180) is True + + def test_is_expired_zero_ttl_never_expires(self): + """Test that TTL of 0 means never expire.""" + # Created 1000 days ago + created = time.time() - (1000 * 24 * 60 * 60) + meta = CacheMetadata(created_at=created, data_source='test') + assert meta.is_expired(ttl_days=0) is False + + def test_age_days(self): + """Test age calculation.""" + # Created 10 days ago + created = time.time() - (10 * 24 * 60 * 60) + meta = CacheMetadata(created_at=created, data_source='test') + assert 9.9 < meta.age_days() < 10.1 + + +class TestDataCache: + """Tests for DataCache class.""" + + def setup_method(self): + """Create a temporary cache directory for each test.""" + self.temp_dir = tempfile.mkdtemp() + # Reset global cache + cache._global_cache = None + + def teardown_method(self): + """Clean up temporary directory.""" + import shutil + shutil.rmtree(self.temp_dir, ignore_errors=True) + # Reset global cache + cache._global_cache = None + + def test_cache_disabled_by_default(self): + """Verify caching is disabled by default.""" + c = DataCache(cache_dir=self.temp_dir) + assert c.enabled is False + + def test_cache_can_be_enabled(self): + """Verify caching can be enabled.""" + c = DataCache(enabled=True, cache_dir=self.temp_dir) + assert c.enabled is True + + def test_cache_disabled_returns_fresh_data(self): + """Verify disabled cache always calls fetch function.""" + c = DataCache(enabled=False, cache_dir=self.temp_dir) + call_count = 0 + + def fetch(): + nonlocal call_count + call_count += 1 + return {'data': call_count} + + result1 = c.get('source', fetch) + result2 = c.get('source', fetch) + + assert call_count == 2 + assert result1['data'] == 1 + assert result2['data'] == 2 + + def test_cache_enabled_returns_cached_data(self): + """Verify enabled cache returns cached data.""" + c = DataCache(enabled=True, cache_dir=self.temp_dir) + call_count = 0 + + def fetch(): + nonlocal call_count + call_count += 1 + return {'data': call_count} + + result1 = c.get('source', fetch) + result2 = c.get('source', fetch) + + assert call_count == 1 + assert result1['data'] == 1 + assert result2['data'] == 1 + + def test_force_refresh_bypasses_cache(self): + """Verify force_refresh fetches fresh data.""" + c = DataCache(enabled=True, cache_dir=self.temp_dir) + call_count = 0 + + def fetch(): + nonlocal call_count + call_count += 1 + return {'data': call_count} + + result1 = c.get('source', fetch) + result2 = c.get('source', fetch, force_refresh=True) + + assert call_count == 2 + assert result1['data'] == 1 + assert result2['data'] == 2 + + def test_different_sources_cached_separately(self): + """Verify different data sources have separate cache entries.""" + c = DataCache(enabled=True, cache_dir=self.temp_dir) + + result1 = c.get('source1', lambda: 'data1') + result2 = c.get('source2', lambda: 'data2') + + assert result1 == 'data1' + assert result2 == 'data2' + + def test_different_time_ranges_cached_separately(self): + """Verify different time ranges have separate cache entries.""" + c = DataCache(enabled=True, cache_dir=self.temp_dir) + + result1 = c.get('source', lambda: 'data1', time_range=(100, 200)) + result2 = c.get('source', lambda: 'data2', time_range=(200, 300)) + + assert result1 == 'data1' + assert result2 == 'data2' + + def test_expired_cache_returns_fresh_data(self): + """Verify expired cache entries are refreshed.""" + c = DataCache(enabled=True, cache_dir=self.temp_dir, ttl_days=1) + + # First fetch + result1 = c.get('source', lambda: 'old_data') + + # Manually make the cache old by modifying metadata + cache_key = c._generate_cache_key('source') + _, meta_path = c._get_cache_paths(cache_key) + with open(meta_path, 'r') as f: + meta_dict = json.load(f) + # Set created_at to 10 days ago + meta_dict['created_at'] = time.time() - (10 * 24 * 60 * 60) + with open(meta_path, 'w') as f: + json.dump(meta_dict, f) + + # Second fetch should get fresh data + result2 = c.get('source', lambda: 'new_data') + + assert result1 == 'old_data' + assert result2 == 'new_data' + + def test_is_cached(self): + """Test is_cached method.""" + c = DataCache(enabled=True, cache_dir=self.temp_dir) + + assert c.is_cached('source') is False + + c.get('source', lambda: 'data') + + assert c.is_cached('source') is True + + def test_invalidate(self): + """Test cache invalidation.""" + c = DataCache(enabled=True, cache_dir=self.temp_dir) + + c.get('source', lambda: 'data1') + assert c.is_cached('source') is True + + c.invalidate('source') + assert c.is_cached('source') is False + + result = c.get('source', lambda: 'data2') + assert result == 'data2' + + def test_clear_all(self): + """Test clearing all cache entries.""" + c = DataCache(enabled=True, cache_dir=self.temp_dir) + + c.get('source1', lambda: 'data1') + c.get('source2', lambda: 'data2') + + assert c.is_cached('source1') is True + assert c.is_cached('source2') is True + + c.clear() + + assert c.is_cached('source1') is False + assert c.is_cached('source2') is False + + def test_clear_older_than(self): + """Test clearing entries older than a threshold.""" + c = DataCache(enabled=True, cache_dir=self.temp_dir) + + # Create two entries + c.get('source1', lambda: 'data1') + c.get('source2', lambda: 'data2') + + # Make source1 old + cache_key = c._generate_cache_key('source1') + _, meta_path = c._get_cache_paths(cache_key) + with open(meta_path, 'r') as f: + meta_dict = json.load(f) + meta_dict['created_at'] = time.time() - (100 * 24 * 60 * 60) # 100 days ago + with open(meta_path, 'w') as f: + json.dump(meta_dict, f) + + # Clear entries older than 50 days + c.clear(older_than_days=50) + + assert c.is_cached('source1') is False # Old, should be cleared + assert c.is_cached('source2') is True # New, should remain + + def test_get_stats(self): + """Test cache statistics.""" + c = DataCache(enabled=True, cache_dir=self.temp_dir) + + stats = c.get_stats() + assert stats['enabled'] is True + assert stats['entry_count'] == 0 + + c.get('source1', lambda: 'data1') + c.get('source2', lambda: 'data2') + + stats = c.get_stats() + assert stats['entry_count'] == 2 + assert stats['total_size_mb'] >= 0 # Small files may round to 0 + + def test_ttl_setting(self): + """Test TTL getter and setter.""" + c = DataCache(enabled=True, cache_dir=self.temp_dir, ttl_days=90) + assert c.ttl_days == 90 + + c.ttl_days = 30 + assert c.ttl_days == 30 + + def test_ttl_negative_raises_error(self): + """Test that negative TTL raises ValueError.""" + c = DataCache(enabled=True, cache_dir=self.temp_dir) + with pytest.raises(ValueError): + c.ttl_days = -1 + + def test_cache_dir_setting(self): + """Test cache_dir getter and setter.""" + c = DataCache(enabled=True, cache_dir=self.temp_dir) + assert c.cache_dir == Path(self.temp_dir) + + new_dir = tempfile.mkdtemp() + try: + c.cache_dir = new_dir + assert c.cache_dir == Path(new_dir) + finally: + import shutil + shutil.rmtree(new_dir, ignore_errors=True) + + def test_thread_safety(self): + """Test thread-safe concurrent access.""" + c = DataCache(enabled=True, cache_dir=self.temp_dir) + results = [] + errors = [] + fetch_count = 0 + fetch_lock = threading.Lock() + + def fetch(): + nonlocal fetch_count + with fetch_lock: + fetch_count += 1 + time.sleep(0.01) # Simulate network delay + return 'data' + + def worker(source): + try: + for _ in range(5): + result = c.get(source, fetch) + results.append(result) + except Exception as e: + errors.append(e) + + threads = [threading.Thread(target=worker, args=(f'source{i % 3}',)) for i in range(10)] + for t in threads: + t.start() + for t in threads: + t.join() + + assert len(errors) == 0, f"Thread errors: {errors}" + assert all(r == 'data' for r in results) + # With 3 unique sources and proper caching, should have at most 3 fetches + # (may have more due to race conditions before cache is populated) + assert fetch_count <= 10 + + +class TestEnvironmentVariables: + """Tests for environment variable configuration.""" + + def setup_method(self): + """Save original environment.""" + self.orig_env = { + k: os.environ.get(k) for k in [ + 'MURGTOOLS_CACHE_ENABLED', + 'MURGTOOLS_CACHE_DIR', + 'MURGTOOLS_CACHE_TTL_DAYS', + ] + } + # Clear environment + for k in self.orig_env: + if k in os.environ: + del os.environ[k] + # Reset global cache + cache._global_cache = None + self.temp_dir = tempfile.mkdtemp() + + def teardown_method(self): + """Restore original environment.""" + for k, v in self.orig_env.items(): + if v is None: + if k in os.environ: + del os.environ[k] + else: + os.environ[k] = v + # Reset global cache + cache._global_cache = None + import shutil + shutil.rmtree(self.temp_dir, ignore_errors=True) + + def test_env_cache_enabled(self): + """Test MURGTOOLS_CACHE_ENABLED environment variable.""" + os.environ['MURGTOOLS_CACHE_ENABLED'] = '1' + os.environ['MURGTOOLS_CACHE_DIR'] = self.temp_dir + + c = DataCache() + assert c.enabled is True + + def test_env_cache_enabled_true_string(self): + """Test MURGTOOLS_CACHE_ENABLED with 'true' string.""" + os.environ['MURGTOOLS_CACHE_ENABLED'] = 'true' + os.environ['MURGTOOLS_CACHE_DIR'] = self.temp_dir + + c = DataCache() + assert c.enabled is True + + def test_env_cache_dir(self): + """Test MURGTOOLS_CACHE_DIR environment variable.""" + custom_dir = tempfile.mkdtemp() + try: + os.environ['MURGTOOLS_CACHE_DIR'] = custom_dir + c = DataCache() + assert c.cache_dir == Path(custom_dir) + finally: + import shutil + shutil.rmtree(custom_dir, ignore_errors=True) + + def test_env_cache_ttl(self): + """Test MURGTOOLS_CACHE_TTL_DAYS environment variable.""" + os.environ['MURGTOOLS_CACHE_TTL_DAYS'] = '90' + os.environ['MURGTOOLS_CACHE_DIR'] = self.temp_dir + + c = DataCache() + assert c.ttl_days == 90 + + def test_env_cache_ttl_invalid(self): + """Test invalid TTL value falls back to default.""" + os.environ['MURGTOOLS_CACHE_TTL_DAYS'] = 'invalid' + os.environ['MURGTOOLS_CACHE_DIR'] = self.temp_dir + + c = DataCache() + assert c.ttl_days == 180 # default + + +class TestGlobalCacheFunctions: + """Tests for global cache functions.""" + + def setup_method(self): + """Reset global cache.""" + cache._global_cache = None + self.temp_dir = tempfile.mkdtemp() + + def teardown_method(self): + """Reset global cache and clean up.""" + cache._global_cache = None + import shutil + shutil.rmtree(self.temp_dir, ignore_errors=True) + + def test_get_cache_returns_same_instance(self): + """Test get_cache returns singleton.""" + c1 = get_cache(cache_dir=self.temp_dir) + c2 = get_cache() + assert c1 is c2 + + def test_enable_cache(self): + """Test enable_cache convenience function.""" + c = enable_cache(cache_dir=self.temp_dir) + assert c.enabled is True + + def test_disable_cache(self): + """Test disable_cache convenience function.""" + enable_cache(cache_dir=self.temp_dir) + disable_cache() + c = get_cache() + assert c.enabled is False + + def test_clear_cache(self): + """Test clear_cache convenience function.""" + c = enable_cache(cache_dir=self.temp_dir) + c.get('source', lambda: 'data') + assert c.is_cached('source') is True + + clear_cache() + assert c.is_cached('source') is False + + +class TestCacheWithComplexData: + """Tests for caching complex data types.""" + + def setup_method(self): + """Create temporary cache directory.""" + self.temp_dir = tempfile.mkdtemp() + cache._global_cache = None + + def teardown_method(self): + """Clean up.""" + import shutil + shutil.rmtree(self.temp_dir, ignore_errors=True) + cache._global_cache = None + + def test_cache_numpy_arrays(self): + """Test caching numpy arrays.""" + import numpy as np + + c = DataCache(enabled=True, cache_dir=self.temp_dir) + + data = {'array': np.array([1.0, 2.0, 3.0]), 'value': 42} + c.get('source', lambda: data) + + # Clear memory and reload from cache + result = c.get('source', lambda: None) + + assert np.array_equal(result['array'], data['array']) + assert result['value'] == 42 + + def test_cache_nested_dict(self): + """Test caching nested dictionaries.""" + c = DataCache(enabled=True, cache_dir=self.temp_dir) + + data = { + 'level1': { + 'level2': { + 'values': [1, 2, 3], + 'name': 'test', + } + } + } + c.get('source', lambda: data) + result = c.get('source', lambda: None) + + assert result == data + + def test_cache_with_extra_params(self): + """Test caching with extra parameters.""" + c = DataCache(enabled=True, cache_dir=self.temp_dir) + + # Different extra_params should create different cache entries + result1 = c.get('source', lambda: 'data1', extra_params={'gaugenumber': 'gauge1'}) + result2 = c.get('source', lambda: 'data2', extra_params={'gaugenumber': 'gauge2'}) + + assert result1 == 'data1' + assert result2 == 'data2' From 68a48764c26f49c5be037f6bb1d999f76fd0d37f Mon Sep 17 00:00:00 2001 From: Spicer Bak Date: Thu, 16 Jul 2026 11:38:42 -0400 Subject: [PATCH 3/7] Integrate data caching with getObs class Adds caching support to getObs for caching retrieved oceanographic data to disk. Caching is OFF by default and must be explicitly enabled. getObs class changes: - Add use_cache, cache_dir, cache_ttl_days parameters to __init__ - Add force_refresh parameter to control cache bypass - Add _get_with_cache() helper method for cache integration - Refactor getWaveData() to use caching via _getWaveData_impl() Cache module improvements: - Add NumpyJSONEncoder for handling numpy types in metadata - Fix JSON serialization of numpy int64/float64 values Usage example: obs = getObs(d1, d2, use_cache=True, cache_dir='/data/getdata') data = obs.getWaveData(gaugenumber='waverider-26m') # Second call returns cached data: data = obs.getWaveData(gaugenumber='waverider-26m') # Force refresh: data = obs.getWaveData(gaugenumber='waverider-26m', force_refresh=True) Tests: - Add 5 integration tests for getObs caching Co-Authored-By: Claude Opus 4.5 --- murgtools/cache.py | 17 ++++- murgtools/getdata/getDataFRF.py | 86 ++++++++++++++++++++++++- tests/test_cache.py | 109 ++++++++++++++++++++++++++++++++ 3 files changed, 208 insertions(+), 4 deletions(-) diff --git a/murgtools/cache.py b/murgtools/cache.py index 141506f..4bca675 100644 --- a/murgtools/cache.py +++ b/murgtools/cache.py @@ -46,8 +46,23 @@ from pathlib import Path from typing import Any, Callable, Dict, Optional, Tuple, Union +import numpy as np + from . import config + +class NumpyJSONEncoder(json.JSONEncoder): + """JSON encoder that handles numpy types.""" + + def default(self, obj): + if isinstance(obj, np.integer): + return int(obj) + if isinstance(obj, np.floating): + return float(obj) + if isinstance(obj, np.ndarray): + return obj.tolist() + return super().default(obj) + logger = logging.getLogger(__name__) @@ -356,7 +371,7 @@ def _write_cache(self, cache_key: str, data: Any, data_source: str, # Write metadata first (smaller, validates we can write) with open(meta_path, 'w') as f: - json.dump(metadata.to_dict(), f, indent=2) + json.dump(metadata.to_dict(), f, indent=2, cls=NumpyJSONEncoder) # Write data with open(data_path, 'wb') as f: diff --git a/murgtools/getdata/getDataFRF.py b/murgtools/getdata/getDataFRF.py index b24b004..0b494ca 100755 --- a/murgtools/getdata/getDataFRF.py +++ b/murgtools/getdata/getDataFRF.py @@ -23,6 +23,7 @@ from murgtools.utils import geoprocess as gp, sblib as sb from murgtools import config +from murgtools.cache import DataCache, get_cache from murgtools.exceptions import InvalidGaugeError def gettime(allEpoch, epochStart, epochEnd, indexRef=0): @@ -216,8 +217,27 @@ def removeDuplicatesFromDictionary(inputDict): class getObs: """Class focused on retrieving observational data.""" - def __init__(self, d1, d2, **kwargs): - """Data are returned in self.dataindex are inclusive at start, exclusive at end.""" + def __init__(self, d1, d2, use_cache=False, cache_dir=None, cache_ttl_days=None, **kwargs): + """Initialize observation data retriever. + + Data are returned in self.dataindex are inclusive at start, exclusive at end. + + Args: + d1 (datetime): Start date for data retrieval. + d2 (datetime): End date for data retrieval. + use_cache (bool): Enable disk caching of retrieved data. Default False. + When enabled, data is cached to disk and reused on subsequent calls + with the same parameters. Cache expires after cache_ttl_days. + cache_dir (str, optional): Directory for cache files. + Default: /data/getdata (or MURGTOOLS_CACHE_DIR env var). + cache_ttl_days (int, optional): Cache time-to-live in days. + Default: 180 days / 6 months (or MURGTOOLS_CACHE_TTL_DAYS env var). + + Keyword Args: + server (str): Force server selection ('FRF' or 'CHL'). + force_refresh (bool): If True, bypass cache and fetch fresh data. + Only applies when use_cache=True. + """ # this is active wave gauge list for looping through as needed self.waveGaugeList = ['waverider-26m', 'waverider-17m', 'waverider-17m-1d', 'waverider-20m-1d', 'awac-11m', 'awac-jpier-11m', '8m-array', 'awac-6m', 'awac-4.5m', 'adop-3.5m', @@ -243,6 +263,14 @@ def __init__(self, d1, d2, **kwargs): self.chlDataLoc = config.THREDDS_CHL_ALT self.server = kwargs.get('server', None) self._comp_time() + + # Cache configuration + self._use_cache = use_cache + self._force_refresh = kwargs.get('force_refresh', False) + if use_cache: + self._cache = DataCache(enabled=True, cache_dir=cache_dir, ttl_days=cache_ttl_days) + else: + self._cache = None # assert type(self.d2) == DT.datetime, 'd1 need to be in python "Datetime" data types' # assert type(self.d1) == DT.datetime, 'd2 need to be in python "Datetime" data types' @@ -270,7 +298,40 @@ def _roundtime(self, dt=None, roundto=60): rounding = (seconds + roundto / 2) // roundto * roundto return dt + DT.timedelta(0, rounding - seconds, -dt.microsecond) - def getWaveData(self, gaugenumber=0, roundto=30, removeBadDataFlag=4, **kwargs): + def _get_with_cache(self, data_type, fetch_func, extra_params=None, force_refresh=False): + """Retrieve data with optional caching. + + Args: + data_type (str): Type of data being retrieved (e.g., 'wave', 'wind', 'bathy'). + fetch_func (callable): Function that fetches the data from the server. + extra_params (dict, optional): Additional parameters that affect the data + (e.g., gauge number, variable names). Used in cache key generation. + force_refresh (bool): If True, bypass cache and fetch fresh data. + + Returns: + The requested data dictionary. + """ + if not self._use_cache or self._cache is None: + return fetch_func() + + # Build cache key components + data_source = f"getObs.{data_type}" + time_range = (self.epochd1, self.epochd2) + + # Include server in extra_params for cache key + cache_params = {'server': self.server} + if extra_params: + cache_params.update(extra_params) + + return self._cache.get( + data_source=data_source, + fetch_func=fetch_func, + time_range=time_range, + extra_params=cache_params, + force_refresh=force_refresh or self._force_refresh, + ) + + def getWaveData(self, gaugenumber=0, roundto=30, removeBadDataFlag=4, force_refresh=False, **kwargs): """This function pulls down the data from the thredds server and puts the data into a dictionary. TODO: Set optional date input from function arguments to change self.start self.end @@ -283,6 +344,8 @@ def getWaveData(self, gaugenumber=0, roundto=30, removeBadDataFlag=4, **kwargs): removeBadDataFlag (int): this will remove data with a directional flag of 3/4 signaling questionable or failed directional spectra (default = 4, remove failed (directional) spectral data time periods) valid values: [3, 4, False] False will not remove any data + force_refresh (bool): If True and caching is enabled, bypass cache and fetch fresh data. + Default False. Keyword Args: "returnAB" (bool): if this is True function will return a's and b's for time period (default=False) @@ -317,6 +380,23 @@ def getWaveData(self, gaugenumber=0, roundto=30, removeBadDataFlag=4, **kwargs): """ returnAB = kwargs.get('returnAB', False) spec = kwargs.get('spec', False) + + # Use caching if enabled + cache_params = { + 'gaugenumber': gaugenumber, + 'roundto': roundto, + 'removeBadDataFlag': removeBadDataFlag, + 'returnAB': returnAB, + 'spec': spec, + } + + def fetch_data(): + return self._getWaveData_impl(gaugenumber, roundto, removeBadDataFlag, returnAB, spec) + + return self._get_with_cache('wave', fetch_data, cache_params, force_refresh) + + def _getWaveData_impl(self, gaugenumber, roundto, removeBadDataFlag, returnAB, spec): + """Internal implementation of getWaveData (without caching).""" # Making gauges flexible self._waveGaugeURLlookup(gaugenumber) # parsing out data of interest in time diff --git a/tests/test_cache.py b/tests/test_cache.py index 2d599a5..066495f 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -496,3 +496,112 @@ def test_cache_with_extra_params(self): assert result1 == 'data1' assert result2 == 'data2' + + +class TestGetObsCacheIntegration: + """Tests for getObs class caching integration.""" + + def setup_method(self): + """Create temporary cache directory.""" + self.temp_dir = tempfile.mkdtemp() + cache._global_cache = None + + def teardown_method(self): + """Clean up.""" + import shutil + shutil.rmtree(self.temp_dir, ignore_errors=True) + cache._global_cache = None + + def test_getobs_cache_disabled_by_default(self): + """Test that getObs has caching disabled by default.""" + import datetime as DT + from murgtools.getdata.getDataFRF import getObs + + d1 = DT.datetime(2024, 1, 1) + d2 = DT.datetime(2024, 1, 2) + + obs = getObs(d1, d2) + assert obs._use_cache is False + assert obs._cache is None + + def test_getobs_cache_can_be_enabled(self): + """Test that getObs caching can be enabled.""" + import datetime as DT + from murgtools.getdata.getDataFRF import getObs + + d1 = DT.datetime(2024, 1, 1) + d2 = DT.datetime(2024, 1, 2) + + obs = getObs(d1, d2, use_cache=True, cache_dir=self.temp_dir) + assert obs._use_cache is True + assert obs._cache is not None + assert obs._cache.enabled is True + + def test_getobs_cache_custom_ttl(self): + """Test that getObs accepts custom TTL.""" + import datetime as DT + from murgtools.getdata.getDataFRF import getObs + + d1 = DT.datetime(2024, 1, 1) + d2 = DT.datetime(2024, 1, 2) + + obs = getObs(d1, d2, use_cache=True, cache_dir=self.temp_dir, cache_ttl_days=90) + assert obs._cache.ttl_days == 90 + + def test_getobs_get_with_cache_helper(self): + """Test the _get_with_cache helper method.""" + import datetime as DT + from murgtools.getdata.getDataFRF import getObs + + d1 = DT.datetime(2024, 1, 1) + d2 = DT.datetime(2024, 1, 2) + + # Test with caching disabled + obs_no_cache = getObs(d1, d2) + call_count = 0 + + def fetch(): + nonlocal call_count + call_count += 1 + return {'data': call_count} + + # Without cache, should call fetch each time + result1 = obs_no_cache._get_with_cache('test', fetch) + result2 = obs_no_cache._get_with_cache('test', fetch) + assert call_count == 2 + + # Test with caching enabled + call_count = 0 + obs_with_cache = getObs(d1, d2, use_cache=True, cache_dir=self.temp_dir) + + result3 = obs_with_cache._get_with_cache('test', fetch) + result4 = obs_with_cache._get_with_cache('test', fetch) + assert call_count == 1 # Only called once due to caching + assert result3 == result4 + + def test_getobs_force_refresh(self): + """Test force_refresh parameter.""" + import datetime as DT + from murgtools.getdata.getDataFRF import getObs + + d1 = DT.datetime(2024, 1, 1) + d2 = DT.datetime(2024, 1, 2) + + obs = getObs(d1, d2, use_cache=True, cache_dir=self.temp_dir) + call_count = 0 + + def fetch(): + nonlocal call_count + call_count += 1 + return {'data': call_count} + + # First call caches + result1 = obs._get_with_cache('test', fetch) + # Second call returns cached + result2 = obs._get_with_cache('test', fetch) + assert call_count == 1 + + # Force refresh should call again + result3 = obs._get_with_cache('test', fetch, force_refresh=True) + assert call_count == 2 + assert result3['data'] == 2 From 0445515ff59ad27da373f87ee9bbe274a8688aaa Mon Sep 17 00:00:00 2001 From: Spicer Bak Date: Thu, 16 Jul 2026 11:52:02 -0400 Subject: [PATCH 4/7] Add caching support to all major getObs data retrieval methods Extends caching integration to all commonly used data retrieval methods in the getObs class. Each method now accepts force_refresh parameter. Methods with caching support: - getWaveData (already done in previous commit) - getCurrents - ocean currents data - getWind - wind measurements - getWL - water level (NOAA tide) - getGaugeWL - gauge water level - getCTD - CTD measurements - getALT - altimeter data - getLidarRunup - lidar runup measurements - getBathyTransectFromNC - bathymetry transects Each method follows the same pattern: 1. Add force_refresh parameter 2. Create _impl method with actual logic 3. Wrap with _get_with_cache helper Usage: obs = getObs(d1, d2, use_cache=True) # All these calls are now cacheable: waves = obs.getWaveData(gaugenumber='waverider-26m') currents = obs.getCurrents(gaugenumber='awac-11m') wind = obs.getWind() wl = obs.getWL() bathy = obs.getBathyTransectFromNC() # Force refresh any cached data: waves = obs.getWaveData(gaugenumber='waverider-26m', force_refresh=True) Co-Authored-By: Claude Opus 4.5 --- murgtools/getdata/getDataFRF.py | 102 +++++++++++++++++++++++++++++--- 1 file changed, 93 insertions(+), 9 deletions(-) diff --git a/murgtools/getdata/getDataFRF.py b/murgtools/getdata/getDataFRF.py index 0b494ca..c7335e9 100755 --- a/murgtools/getdata/getDataFRF.py +++ b/murgtools/getdata/getDataFRF.py @@ -563,7 +563,7 @@ def getWaveSpec(self, gaugenumber=0, roundto=30, removeBadDataFlag=4, **kwargs): # else: return self.getWaveData(gaugenumber, roundto, removeBadDataFlag, returnAB=returnAB, spec=True) - def getCurrents(self, gaugenumber=5, roundto=1): + def getCurrents(self, gaugenumber=5, roundto=1, force_refresh=False): """This function pulls down the currents data from the Thredds Server. Args: @@ -587,6 +587,8 @@ def getCurrents(self, gaugenumber=5, roundto=1): increments data is rounded to the nearst [roundto] (default 1 min) + force_refresh (bool): If True and caching is enabled, bypass cache and fetch fresh data. + Returns: dict, None if error is encoutered 'name' (str): gauge name @@ -617,6 +619,15 @@ def getCurrents(self, gaugenumber=5, roundto=1): 'meanP' (array): mean pressure """ + cache_params = {'gaugenumber': gaugenumber, 'roundto': roundto} + + def fetch_data(): + return self._getCurrents_impl(gaugenumber, roundto) + + return self._get_with_cache('currents', fetch_data, cache_params, force_refresh) + + def _getCurrents_impl(self, gaugenumber, roundto): + """Internal implementation of getCurrents (without caching).""" valid_gauges = [2, 3, 4, 5, 6, 'awac-11m', 'awac-8m', 'awac-6m', 'awac-4.5m', 'awac-5m', 'adop-3.5m', 'awac-jpier-11m', 'awac-jpier', 'jpier-11m', 'sig769-300', '769-300', @@ -700,7 +711,7 @@ def getCurrents(self, gaugenumber=5, roundto=1): self.curpacket = None return self.curpacket - def getWind(self, gaugenumber=0, collectionlength=10): + def getWind(self, gaugenumber=0, collectionlength=10, force_refresh=False): """This function retrieves the wind data. Collection length is the time over which the wind record exists ie data is collected in 10 minute increments @@ -718,6 +729,8 @@ def getWind(self, gaugenumber=0, collectionlength=10): '732 wind gauge' in [3] + force_refresh (bool): If True and caching is enabled, bypass cache and fetch fresh data. + Returns: dict, will return None if an error is encountered 'name' (str): station name @@ -753,6 +766,15 @@ def getWind(self, gaugenumber=0, collectionlength=10): 'gaugeht' (float): gauge height for uncorrected wind measurements """ + cache_params = {'gaugenumber': gaugenumber, 'collectionlength': collectionlength} + + def fetch_data(): + return self._getWind_impl(gaugenumber, collectionlength) + + return self._get_with_cache('wind', fetch_data, cache_params, force_refresh) + + def _getWind_impl(self, gaugenumber, collectionlength): + """Internal implementation of getWind (without caching).""" # Making gauges flexible # different Gauges if gaugenumber in ['derived', 'Derived', 0]: @@ -842,7 +864,7 @@ def getWind(self, gaugenumber=0, collectionlength=10): print(' ---- ERROR: Problem finding wind !!!') return None - def getWL(self, collectionlength=6): + def getWL(self, collectionlength=6, force_refresh=False): """This function retrieves the water level data from the server. WL data on server is NAVD88 @@ -853,6 +875,7 @@ def getWL(self, collectionlength=6): Args: collectionlength (int): dictates what value to round time to (Default value = 6) + force_refresh (bool): If True and caching is enabled, bypass cache and fetch fresh data. Returns: dictionary with keys @@ -873,6 +896,15 @@ def getWL(self, collectionlength=6): 'predictedWL': predicted tide """ + cache_params = {'collectionlength': collectionlength} + + def fetch_data(): + return self._getWL_impl(collectionlength) + + return self._get_with_cache('waterlevel', fetch_data, cache_params, force_refresh) + + def _getWL_impl(self, collectionlength): + """Internal implementation of getWL (without caching).""" # this is the back end of the url for waterlevel self.dataloc = 'oceanography/waterlevel/eopNoaaTide/eopNoaaTide.ncml' @@ -904,7 +936,7 @@ def getWL(self, collectionlength=6): self.WLpacket = None return self.WLpacket - def getGaugeWL(self, gaugenumber=5, roundto=1): + def getGaugeWL(self, gaugenumber=5, roundto=1, force_refresh=False): """This function pulls down the water level data at a particular gauge from the Server. Args: @@ -912,6 +944,7 @@ def getGaugeWL(self, gaugenumber=5, roundto=1): roundto: the time over which the wind record exists ie data is collected in 10 minute increments data is rounded to the nearst [roundto] (default 1 min) + force_refresh (bool): If True and caching is enabled, bypass cache and fetch fresh data. Returns wlpacket (dict) with keys below @@ -932,6 +965,15 @@ def getGaugeWL(self, gaugenumber=5, roundto=1): 'yFRF': yFRF position of the gage """ + cache_params = {'gaugenumber': gaugenumber, 'roundto': roundto} + + def fetch_data(): + return self._getGaugeWL_impl(gaugenumber, roundto) + + return self._get_with_cache('gaugewl', fetch_data, cache_params, force_refresh) + + def _getGaugeWL_impl(self, gaugenumber, roundto): + """Internal implementation of getGaugeWL (without caching).""" # Making gauges flexible self._wlGageURLlookup(gaugenumber) # parsing out data of interest in time @@ -985,7 +1027,7 @@ def getGaugeWL(self, gaugenumber=5, roundto=1): 'name': str(self.ncfile.title), } return wlpacket - def getBathyTransectFromNC(self, profilenumbers=None, method=1, forceReturnAll=False): + def getBathyTransectFromNC(self, profilenumbers=None, method=1, forceReturnAll=False, force_refresh=False): """This function gets the bathymetric data from the server. Args: @@ -997,6 +1039,8 @@ def getBathyTransectFromNC(self, profilenumbers=None, method=1, forceReturnAll=F forceReturnAll (bool): (Default Value = False) This will force the survey to take and return all indices between start and end, not the single + force_refresh (bool): If True and caching is enabled, bypass cache and fetch fresh data. + Returns: dictionary with keys, will return None if call fails 'xFRF': x coordinate in frf @@ -1022,6 +1066,15 @@ def getBathyTransectFromNC(self, profilenumbers=None, method=1, forceReturnAll=F 'Ellipsoid': which ellipsoid is used """ + cache_params = {'profilenumbers': profilenumbers, 'method': method, 'forceReturnAll': forceReturnAll} + + def fetch_data(): + return self._getBathyTransectFromNC_impl(profilenumbers, method, forceReturnAll) + + return self._get_with_cache('bathytransect', fetch_data, cache_params, force_refresh) + + def _getBathyTransectFromNC_impl(self, profilenumbers, method, forceReturnAll): + """Internal implementation of getBathyTransectFromNC (without caching).""" # do check here on profile numbers # acceptableProfileNumbers = [None, ] self.dataloc = 'geomorphology/elevationTransects/survey/surveyTransects.ncml' # location @@ -1734,13 +1787,14 @@ def get_sensor_locations(self, datafile='frf_sensor_locations.pkl', window_days= return sensor_locations - def getLidarRunup(self, removeMasked=True): + def getLidarRunup(self, removeMasked=True, force_refresh=False): """This function will get the wave runup measurements from the lidar mounted in the dune. Args: removeMasked: if data come back as masked, remove from the arrays removeMasked will toggle the removing of data points from the tsTime series based on the flag status (Default value = True) + force_refresh (bool): If True and caching is enabled, bypass cache and fetch fresh data. Returns: dictionary with collected data. keys listed below (for more info see the netCDF file @@ -1773,6 +1827,15 @@ def getLidarRunup(self, removeMasked=True): measurement """ + cache_params = {'removeMasked': removeMasked} + + def fetch_data(): + return self._getLidarRunup_impl(removeMasked) + + return self._get_with_cache('lidarrunup', fetch_data, cache_params, force_refresh) + + def _getLidarRunup_impl(self, removeMasked): + """Internal implementation of getLidarRunup (without caching).""" self.dataloc = 'oceanography/waves/lidarWaveRunup/lidarWaveRunup.ncml' self.ncfile, self.allEpoch, _ = getnc(dataLoc=self.dataloc, callingClass=self.callingClass, dtRound=1 * 60) @@ -1833,14 +1896,15 @@ def getLidarRunup(self, removeMasked=True): out = None return out - def getCTD(self): + def getCTD(self, force_refresh=False): """THIS FUNCTION IS CURRENTLY BROKEN. THE PROBLEM IS THAT self.cshore_ncfile does not have any keys? TODO fix this function This function gets the CTD data from the thredds server - Args: None + Args: + force_refresh (bool): If True and caching is enabled, bypass cache and fetch fresh data. Returns: dict: output dictionary with keys listed below, will return None if error happens @@ -1861,6 +1925,15 @@ def getCTD(self): 'sigmaT': """ + cache_params = {} + + def fetch_data(): + return self._getCTD_impl() + + return self._get_with_cache('ctd', fetch_data, cache_params, force_refresh) + + def _getCTD_impl(self): + """Internal implementation of getCTD (without caching).""" # do check here on profile numbers # acceptableProfileNumbers = [None, ] self.dataloc = 'oceanography/ctd/eop-ctd/eop-ctd.ncml' # location of the gridded surveys @@ -1906,7 +1979,7 @@ def getCTD(self): return ctd_Dict - def getALT(self, gaugeName=None, removeMasked=True): + def getALT(self, gaugeName=None, removeMasked=True, force_refresh=False): """This function gets the Altimeter data from the thredds server. Args: @@ -1917,6 +1990,8 @@ def getALT(self, gaugeName=None, removeMasked=True): removeMasked (bool): remove the data that are masked (Default value = True) + force_refresh (bool): If True and caching is enabled, bypass cache and fetch fresh data. + Returns: a dictionary with below keys with selected data, for more info see netCDF files on server 'name': file title @@ -1944,6 +2019,15 @@ def getALT(self, gaugeName=None, removeMasked=True): 'bottomElev': Kalman filtered elevation """ + cache_params = {'gaugeName': gaugeName, 'removeMasked': removeMasked} + + def fetch_data(): + return self._getALT_impl(gaugeName, removeMasked) + + return self._get_with_cache('altimeter', fetch_data, cache_params, force_refresh) + + def _getALT_impl(self, gaugeName, removeMasked): + """Internal implementation of getALT (without caching).""" # location of the data gauge_list = ['Alt769-150', 'Alt769-200', 'Alt769-250', 'Alt769-300', 'Alt769-350', 'Alt861-150', 'Alt861-200', 'Alt861-250', 'Alt861-300', 'Alt861-350', From 50e367d82b37ebef7fa7ffaab2d4ff999395d50c Mon Sep 17 00:00:00 2001 From: Spicer Bak Date: Thu, 16 Jul 2026 14:17:26 -0400 Subject: [PATCH 5/7] Address review comments on PR #58 Fixes: 1. Remove unused `get_cache` import from getDataFRF.py 2. NumpyJSONEncoder: Add fallback to str() for unknown types (datetime, Path, etc.) 3. _generate_cache_key: Use NumpyJSONEncoder for consistent serialization 4. DataCache.get(): Release lock during fetch_func() to allow concurrent fetches Co-Authored-By: Claude Opus 4.5 --- .claude/settings.json | 10 +++++ .mcp-debug-tools/config.json | 7 ++++ .../combined_asv_wave_conditions_2023.png | Bin 0 -> 133945 bytes murgtools/cache.py | 35 +++++++++++++----- murgtools/getdata/getDataFRF.py | 2 +- 5 files changed, 43 insertions(+), 11 deletions(-) create mode 100644 .claude/settings.json create mode 100644 .mcp-debug-tools/config.json create mode 100644 examples/combined_asv_wave_conditions_2023.png diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..f91d88d --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,10 @@ +{ + "permissions": { + "allow": [ + "Bash(gh pr *)", + "Bash(pip show *)", + "Bash(find /home/slug/repos/murgTools -maxdepth 3 -name \"*.tif\" 2>&1 | head -20; ls /tmp/*.tif 2>/dev/null | head -5)", + "Read(//tmp/**)" + ] + } +} diff --git a/.mcp-debug-tools/config.json b/.mcp-debug-tools/config.json new file mode 100644 index 0000000..be67b51 --- /dev/null +++ b/.mcp-debug-tools/config.json @@ -0,0 +1,7 @@ +{ + "vscodeInstanceId": "vscode-2474-1774366165262", + "port": 8890, + "pid": 2474, + "workspacePath": "/home/slug/repos/murgTools", + "workspaceName": "murgTools" +} \ No newline at end of file diff --git a/examples/combined_asv_wave_conditions_2023.png b/examples/combined_asv_wave_conditions_2023.png new file mode 100644 index 0000000000000000000000000000000000000000..e844ba27e1685e36a6ee2278994b8a22ab595dde GIT binary patch literal 133945 zcmd?Rc{rBq`#$9 zlFWpRncC;wTA%g%e)ryg{PuC|WAFVt_Is?sT6y31d7k^auj{%usbZm4K z3WedsaanZ=WmOD?LTyaD8vn<$csT(761SDpv^{Nk$=1Qh#+agHWNT$+X=`S3p3C0Y z=9-D6#ePBIy@GoMxUSgRT3r(t5;FhyHv}zhE(-s|ax<;XFUQhm}y8Nt8 zokB&SoRB@L;TSg3;b6P*4EyTw%MSJI+qZ8&dR%4=mmc5E<7>~{`n;0)!7e#fW~NQM zZ|mKSr0))ehL!gaVMd@R6a|A<+aSIHIoQvH zON^bNy~*Wq``_2w(!dl!3iH3Opmp@eH~jlw4^HppQj-1mrCjPE^6#H9A6i9S_wOIc ze&6{2jW=HO3=G`JyOFNNKt|@K+0zq`cD<{tdh@2au=DZbEiL!oy^!~~nFQwUbeBph0IrWJXCsG90Gc&iWA31TS8Q=8Plv#((~l(bU`$ z;qvpuJ$A8mYu2n`T3Om4w}%B!Wv@xyUxSvF_gGT|)f2O>)N&VH3*Iuq-PZofLFH@} zd3>{6i=9Q+g$-X*G?KM96&DwePfi}QwH0|PX6bL;Q)x8Xnk~9Vdi!=N#aQ*T(l5@X z`j3p5MNhtevR@%pIorG~xL<1bOZ_6Y520dJKYMGe2kUlz@9QflIDXb?@`Kp(vq@AG zT%+UCbklTPzT*V9+x#F8J*W6aKXx&BTyE#_H?NzTHXBz59!=KH9&f#x+Kq=P-(P=48uZ}HNM&eUK%jor|fqgGaXB_$=9 zn3z0F8(uuO>?)p{{3NZ^*zLM7)1CQA>e#1QjxJj(lk;%Rf6OB5S zPXuZn3tzx{DHPSD2FKy#T!B`5Zf+au70k@}esgnLV^LEb_0=C|id7d`lI6c%*(;$~~!p0_}Qxu=9B>Dk&XxtMW9X%a<;hjipRCKg) zy-L`@T8lP~r+aksRL2|zb@M*|7;eci5AAe*_~_ASal{R3n(D^d7iV_AN-b9pb~$VI ze1Ub})a4)7jPIP&capQrw=Dh@S85_;P%N-=)v9-K479XqHR(Aq>o&#rSo-3DJ&{cL zb~)=z(%Fn|YMOPnCi$z5GUb@HoX<(&5Vw8%?P^#;0&n-%QcwA&kOvR8+YZ)MeBY^T%$?iqZ${g4(~Zhkd-m8|yOwS{sQzfupxA{~@0EQ8{=_%SO+`gzYPd;7)NJ-g z$J|)S?|UDwjddO_YP>QrF+sa#O}b+d<5tNdRsNh0!otk-ZoJ#Me!~W}5;xbE+S%3L z8r>5U6W0j_4g0a~U;XagySlCvy;o-Xefa0a8jk&!?v1Y68E?{>l{hlDlZPkze8t@q z!QeK<(L?;1Gits?IrF8qGr{BeD){WPHV@+ z#&$3Mn#?dd=Ce*tUH#!yixXC!ic%B2=aFk%oO1ZkUG5r}iHgk`v*q3l^`Y|7wVH*6 zCqo3E;|+P%J>Gayqph2U8%*<*NByT?m)y%lK=I7+3pTF4b@n82?_ut3b6wOXgSMWS@xmk35 z_S;BH=0W?BEsZa=JnwAai9(iA<=yC9vqjP|ZKQ~c7Vp1PYnP&#E;E*{eRcAXpmrAR zxm5k2&fP(Zg}?ejo6|4Qd<;9}*;5s8p{LSsZgKJx^XAPojEsyVkgrXAlari$zu%ef zG`~iIhfwj{it95Yr7fA4A3T11+iUISD2K7m@u?|WHIwk&2M=x$78b61c6dd=rrl=) z_v}A-uzBX@!+Mg8{S8i?qW@$dN_MRASW!`tu=}!0n7AF?_1Pa48OAl^)w@$8U4H!( zO|9wb+BVt$Da2LfMT03l>%NuPdUa_#PX-$<%}-}IwdI)mGVhRY*7dHFU1*g<{*{)T zABe8ItZ-_WiJ5u(!l|I$=RUSn1m#;ewy?*yyO=^3uWuq}hXR ziwDlx<%CNap!b*#Nq2{);;o`lhVHzCm&0@OJQ%_jEVV+ z-`=7a6b(gL3tY6RkfZf`h+MJs^Lj(6{93a4^ z*k%3g&6?;*F^hIh(b1R_k4~kGm3gf#>#Yg-5FD+YZ7Lsv;|nTX!Jrm}c8^ZZ#vdK8F55<6Bdpnf>HXe= zf@U@|GE;{Z6Ej~Y_}zf zO~;3empu*SM2#%Fzx~*)rw6Xm9X)#VW_)}+6AR0(EVCB%xw)PIFWf9!c#|q?MrNj$ zP|2eDmCunkzK)Fvtz5a1_QTTy8-fM2&f6Yx_|eWRwKR45IZ1wwj(J~udw0&w&(o=? zsmWl+co~&@uR&cupcxSU?qMM3uV25)9_~DO3srTEwY9YjvcH#2Z}pn3Ter#_IpU$3 zZ)5x%L0|T*!q=<4UDtCs{jUxgl!m?6`c4^rjyx)ZK)D47vwiPg)$#Fhrt085yCr9u zFO(_HPWUgjlqm5Pw5Owxr-mqbZPL+!E(qHT)PLr2f=1M(jYAP=q8?VvV z*UvDqAL}$fQ^fq_WQ);=iz^C1chPL;hOMqsYNb_G3cRP|?lEoN%wyJ^Rywep@utnP zWRFhMm&DTy$`0S(GIuweGuUL&QDAJfSRJ%`y1FCg#D*3Gw0weg_KQx3v>XYCAGAOw z87AKJtlXiON^h?>KOl}Gd3e;0op9o?fNkEQohmbM$x zXvx~J>r|SltE{ZXBEi((SFhV9_0nO&9ecAoy=vnP0t#`OrQt*Zz;3(fR$^zkzSPDttGyx4U(iNY4-KcvMxX zdI*JL2hf#vmfth?<@{2cgT6bd246XhX}i21YE0g(TjYGk@^J9V4JFI-SACoITeKha zY`ehAb;#-G#ppwwSHC0}r4`=fqIF-K@L}TMFv}4D8Vc(8ZY6)jxjs(Gxn|!+j@)=e z@N>l={tP>3XJ>ol}?@dvb*HD)B#(|;1Iskl>>gJ$JTKi zjLeNdoo;zCnvbCAoHfOM^HVNy|LxY2RcUdMnfbwjVf8w0Zm~40Zkd(^15_Os^qXz?!|ZOi^6U; z#tl=ZYL=?Dxkmz2Jl=nK9YXi+&6_KdLQGW7rRbuEaWS;g#WgUqvho#udiSoBY=yW5 zR5atUw(P4u0Cs~*Xi9E}=sT~`($d;h^Ds2j7Z9b|U%}DwV8rzwhgxir!Wu#ykgYNl zK3q6|9((Sxjccpzwv#k;lNVWftp~q4t*i+NXUtLpE zKHgnEEhQbV@~je7^W zoH@$w?(PDbX-9&~xTK}GAuEZvEsTn_WECWeBQBV?Z6o02v7U;$`ucC*zH!X3I-~53 zzvFrKxM&^IN-6{Qr33PQY&+Rg7WC4u^(#Lb6VlD&Y7549zOBr?Qd^cdc(u}>(-$|( z%E3dsLf?JK4qc&6Q9Clx*SU$Fy6cvfmVhlK$Bjj%oRW3(Ge(M@bMzrV8rjdHdudq! zoGQAOlWbfY{NTY2G{%4N@bn=0&tW}lbZlzBQ+0 zWh)p-16!f%=2ool;|0LRP5FxA?4ua_(7P+oy4-N9G=)_Dks|qM5+}lr&btmhFhnrG|?anqOo+b&N3b7*(2B1^Qc{IDpYW9cT;1afoAbW}Pv}w`D zJt0B{g4d;S!JbZU{0_OyrFEv|n73(5mgEoskm^$I!&rsNmu}f5RkZXVA|hoomoQR0 zdJIMqI7phLe`=XSjy;bIonbUqG`rQt#^z!*z0cARKqH5mu^({x+lJ2(T%yH0)g1ui}@_X6$ zncMt{u`qGt>W!H-@2jh23=Jb~PmiHw$fDF3*JFRt;MZPFN$uai0S_x=l>kBF`1Qqw zu@Z98pVKcWwCwZeIK&;KF!EYWZ0{NhFkDc>gmF#qlP+O#lzCro2KJ%epd$C*QsTA) z9%w;^KAeu0XFxBAze0be=9)Grj_r@^7^9*9at!o_MI5*qJaVmjaPST)(>L7AaYe;4 z;4SV~!cNmeX9|~Q+T;Y|h40+C^H|jUshMWQ-Axl%_HWoMb4#;b)8bZP5)Oi(IbvoD z`seH8RZjMgFD|-x+8IdDCbQpXp>A*d!mdMm(_zT(968n@buv&fwo|Hp22Udz zbrJ0$-PY?P!iD2yYY*BE+!rZEcG1kb@}M(2R#ZqxsKrTECs7EuOrLpB?MkjK1H@MOQaWU#q}=)YPs> zSmU?A6Zz2mK+b~1mEbyFhWOeK*2Ov-|6X1gGp---XGB>=x;L)>lA_o4{c>4T(>b=y z8#i7QNzLHe_`~5`#$U!hJF%;F=u~btaM{jod7@y|HTuI*?md+ZEh66PWmlf1u6n+A zw%Ee-nV??5@g8>&NJYj2aWwqAyi5dmhVGJ-)SY>f`JLlxGLqtRG>aL=Lyb$XUmr0- z$51C}nypPsPajw*X8PrA+`cV}9nY7*AaZ|!uXfHyuInL-x^L*UpS6N z*LV`$`rD{|8^8vg2pMdz`=2jBQ)A z6xUXE_PX?nBY+>VO^8^9jsiPj(=WX59}82ni<;ff<~`uxR<~1XoyCYsuGgy~G?#`y z+VfHQB)|nQZ`s1TU>0%ET1IpUbiv)Mh_#yqzoP9p=F75&iIsJYfq{VuCkkF&XlJ{# zjEv0ZWF4`Vw6|}M7d75#P1d%P4gqX;R&h_z;0Md8Avyw<`E2Y8-n+HY3O&FabqfS) zk#YvO6k|kaEb>EvD|jYJ$MGXnjBhnk9EiBj?oLxmjX|; zWEj^*9`)olJ5h^tajMa#rM~|3m?=n6}O%;;*y?CuX&zc%{SP8bN7I&^i z(P(SUz6uU0L9D&LegjyKM+`b8Zilf%o+PO$r1bRk_)Z-=6{DWG*KN@eoy8`0F^f01 z)@=A(V0RUDWlPvH@TO*wv)zNH%M9oXid^m$*m^!b&m=A(p_3X`P(LI!Z_d}D8-4it zjBKo8P|WP*PZ2>|^BX_dX$Ml0o-26xuJ_m#l3#LsAx&cR;r= z1H!(!{LIxG7^J(>kNpz$NMGXyPELkc_1`=m7KfFUl!E-DEy5Et5+CaCrRQz!uVV=j z)GNa?jGYCtGH{t#C3*Pp1gTpPPwMi}L)rdlXT^#?7Pot0cF0@(m{t;~U*{Wc6jEgM z(^3=q?T-9zx7gj$b3wq||3`X}(`4L;cX+x-miI`5K@Xp1repD(NxjEgaP zdw+Yz64^k6wlymrJLH=mRA0R|>)+3gq@$<*Qe%|V_Ch=4*|Tj0cEjFCvKIi^j;p9t0hQ3_DDG=yyE^{0 zG-DQ|p=PnmAyMij$Nl78}8d|Vod~^B9?(ZO&t}~=DkR0Nz_JtjTc5TPk%Z4D8&}U zgI0cdd3kKS0~+*+CIk29Xh!Ph-oHO(JM`s5@U~ZOOY^Hsj@>O&RsEzJ`AOUR=ADY8 zz>pWGd$C;z#}QantebC>pq05xJI8EIWLh!wjT*C)S1lny@K7R!rwc8OK*o8pcYn41cjNqmAo9ce%GUvXm)E(F;#7 z8aYif+cc;%T|dX{$vY`}+h@y}-xCjUbMqH(xxb5>zmi2rUpM9#_fvT<2BCK(~(Tw0Ihq!nC1P2uL|z8Ij~_DC^S2I)EN(g(I87V(1KMo!Cw9R*R6 z+7|Bu8a+HHz{3inBX}eYC!35CjBUK=HeDQR%e72xsttcGJ$lwN3dp-0l#=sr^wMAP zci^f*eaG%qNp6OB3v36Y#LPMioz5;%Xr^*Edvr#RR(i{U>&O#@G{-7D(+qr&wW(njKPxo98nHCjQ!eqm7>xVE{#E=j*Y#2gY| zBm1QR?Q!7sj9K)N&L-L(WmAgCiopkLAV$o6^A*~QR2k|y6L5IuF;OLUNi;Mn-H3YS ze&_bvB0{ZcoZ>uW^Cm9xp2hA#u}(*7v}mHg4i-NDn)y63?D(wD1k_6+u(qTDzP3$8 z_mXWa*5$I}>WP=Tk?@6HN9kB}C}@qf=SMdw4j>T<*&O;%t5lH=ZbeI+W!tuGo=!Oc zxO=XbT>iB6dw>6qUArp38lW&(0eyHL?ZxgTIKOTIr0l7uA}2(Z)F!u|OFGK{jo_u3 zrl~1Ek%E6V>p{ZWfB9ozr*Cq9))G`*7KZ&Ise1^_JLj-jKr>RT1B%gThSVrWoQL_I z?YqyuOqjjB1onP9B5!Uqw`LkV*Jepl}PvB%a6q@BLwcRq7Zh)vE@(c zu$4ZRvoFxX2=v!Ld;WgY#+P@Xh68bc*sn|(MsK}NYGGu*-Hd7aS(s{c4vy+V2V@a?zgV|edH%3@OqsU;Vg+`HZCMh^bN7dzk>6hd7nLXI5 z&i#hw`T6;d-Cmo=dqWJWy-Ox4*~#O%C>F@zv^Y@(CJO4n#WAv?vr~2GoRb_*_W>AQ zsg3d)i3ntEUO0j?pQv8I?rk>{+9R~+2ts-jPbn)0cW?|eB!!eN z1|YWv_h!6*|6Uf#AyK9-0D>Pxoj}4&2P=4Qi|`8MjyHHHz6-Ns>;gU({obw8{_F=# zFIq`!nWE8h4&b$H%VEeiZ72)mJDmn@lE4n~y$~L1QQqJKIAgzI2h0VZBq#5gu@Wvq z{@#`wAp3hz_aJhG=^rUTiMMCG2tj!}Ac3eEF2@mSXF9gfNXM(qK;QeD*@}2=w@DY< z4YRl**WhuT9g6(KR=}qX-MTi%TnBXX8Za8=FSRnK#GkKSw@%Q8CMFKmg#nqP5^UaW zC}ta=mRCSeBq8iFKN#=KNnxvDG~n3ce9EOR<^;)&D2Rj?jaCX36^%dp`42;P>HXsy zL}LR7f+sHs7OI1L&vhVxIjGiT0bc68Vu1?dEVes)VfR2wW_+hoF~}kYbi@@8b||D7 z*RT*71&qW)rTsSFk84Hl{_8JUp^92;GOMWpwM2iNM^_iX_17=7yDyC9lATDjKYZyT z)@5!WI)FZ6!|(ZFq3C`^h3maR=Pr(k;m^^eU4Sxm5D-fFnUvqHTPxp(9oj&EA~Mc6 z!o0PJqo99{pS&lq$a%EV_w>Pm-snjs_ z3txpC@gYp_N37$i?muYm>?{rMnHvn=RMkqijLjdFh( zrNhJ~f)J%~G+Zxra+TS-KsOal^ev+(Ue0+5+C#qI5(3hPh4XK2GoKB4A)W>*L-YIY5pU4RVkyKr&prD{rxCf$rY^|MTOGd?j3(O@s&fCD3vg_%i$D#i? z3H}JbQwg$7HTB2Sg{uuOg3MSq3+gf1>`-TQU1AX|Bgq||f zPGz=v%ZtP?SC|z`1*BAuF4jpPSJG^<&tz^*chk#yiey^tK;@m_=uL~k%b^Lx?fjz(K zV2W{#^6_QE@K$cN}QxnK< z_WH`uN?bb{!!}(Wl8S%U&Ov%1GRIk=T*UI~PgGIO&6+P^#t_m|%C+bihnlJN<%EH* zE(ad$#qqE+U(R+^idc4XxGznI2&iNUL%JMjPLDQg#%>h8^nS~$;CJgm%%qg7hynabyU=>}7^rcS`=2p6fhFk=dh!tvOUdkAg8kMYTRa9TTQj0$3 zVwV>@3D*YZ0~L2RrsyTj#yk+jr87#+9T74Q*}am6<^q(`U6avQb2-kzPNFi)1X0WA z2wBs*(f6M}efl)Qc!0$7vFpSiwIdyp-)`)rJC-F&EJB6jEAY*?LGd_VzkV!M{rRaU z`x10=1;M!8ge{H+RIKpR8WkP%u=G-8KYDwOLlk}kQ|>2H2Y~q-sP+UIadC66fQ?Hv zQ3K2#;jn9A_jn1037TW$N%rkS&FMSPSQB>u_g{alB*ZvKjRdvm`+$#>YqmU z@Hzk5<)UsBRZ;dr&V3hY)z#IS8%+yRlm2yY&wqJV0Z!iPR!g7;ZtqbtF43E-rrU?!E!a_&K6@%l=FB;A>ul^sMKl z|F~7co*o5mzw^|<4S){pT^iUH5>%hBK+ka#$om__Y$BWMRhyrJ7)UB0px?$-G@?lqCi9t%mW?2j7Byjh+b$GVr*T;%{fNUdylGp9_*_oo7 zA0;LL55P<@s>mEjb0XSlB&bq>UjBo3a7UF4Dmytw>Bl&4mSy@!ULb5Q^O>(hwkQ6$jIzrQm$<1T4exs zu&7kHL)bOA#l_iRfO#PPr9I8aQ=fAIl>6SRjRK)HM#+(pT%^tcNmU_sRN*kg20+-B z!iogP#4!Xx9MW5?r!~AYcOZAIBqArfW#J954pGnq;L|wrWS=p4XW>liRpKrJGBFyj zeDR0fE4QWo?EO!kQzBmcK(Y3HS3VCtS-b`A16xNxK4Fs8i$#X7&19g2!$)mU0Y<{cJ z#^>q#WPndUcJX42ucDmn4>Kf+jF0=foQL-m`LBJK2Tk?@%z$+Zs5OCa8>eJtV;TN< zieulSMX1G++zz3D&ys~*u7XeWw&Hp}`5kuerwNJ$Nt-c2LJejqT@7(={xxs>MQ@aS z*~Q1lHxl@1g^7s?Ht+*+3D>?Sm#03~%F2F^c%0#R`*y;{IYg3bvi1QItcRVaJZ6># zaJ%S7ZpW_)En!t+%D4%=LGp#nS(Z-jyv_>X9grg3If~K`c8W}9Idg2 zS&FgWem%aOu+nI)2{(6?x*;GY*;P~C#Joj>Ry|ejsBhh9ItlIEX_X$Gy!EhiUZln! z@AS@)iLHFd90{`gr{m}m=E6`@9&xT=4%&sK!#e~3z&(i?1wRijs#B6LbOg9azNLBn zH!t?J|Fvz~e;$Uc?EeZ5^Zy@SYLFu6U(hf3OYq4vgzN1Dk@KkFGKll!!WxVK!fuATea8@D- z(cnu~_cKqfR>32~&tT=*2x9&E)esyUAMe!@@wZ5oSu0eC$q!n1Xd{6h2uW@QYu=-_ zoz?L9fB^G^6Y4ptEBSO;*_8zjWBzEX*}_!>&dJEEz!UdE@qP~Y0>gjiiw$z4hFL`) zo=I<`r~(bYsi+wA+k9;wW-TZb`ycIh2#}JLb)Tj{B!xk|6zC((<(CbyS*)%Jd-(7sE{d9>?=m6RLL?9ZqcqWhhzRiV z(xM-0&cDV(?vm6{DD(YMytZSVoM5OZbl3O|O58XR zCY;N2J&c5%*wrr$GA6>Qj}^{@bw`ZuAHo;Fp!Wtn!5zY8>`?H314v_O%i90xnF2^* zYb+Yp@O6EC{R?xeK{1ht2xI_M)2Nx|Z9i7bCvn;S*+=Myn8)!sg z#}>B!CQHH(A{3zA+m9dR&!6Web1neE-$0$&SH`j0_Ev8qZ36)H3e`C+m?r6=#*;7Q zkapuBIwTYdy|eNF!`u#~L`s&G-FXDM0A=IqumfyS)r*so%N)Z2c$V5UYgx@fFvg?%tBd56S zYanZQ*nEIBxg;dmF=0@SEl!+d5DyJ!Mp_;px)xX4OXR!K($eM(`K^G zvtOL%xaZGTM9_Ntaa5aN+QbiY7OPT==lLx<3-6$-+`eZI1A=!Yh2V`jFp1+(O}{-- zks@(PLRa^dJwv#--3rympTrLz?x+nlMz(uoh1nSly%<6+e+$_hMV@84=_Y8sXX?Yw~DkBLdIgI z&?0wdn1-WAW(KxiklU<_YDq4Kcmd(Y}Q*_5DmjKWn2<3>g|aOVaJ;b+UB zg42Jz{_CePFs}iHi$lsqQ#1wSUtK}dORWo8iwK`barFnlkWqOsv9a;D7doA`Tm%+e z1-unK>AN5p5-T`7Y){X%xx}^)8?8F~!@)Q_b*77N$Wbh#N@TapOXI`#F*C7MzzS8ol zRjXJ##Nby?pd2AsEm@G7B=&ddf54U68r2*#Ei#woAt_f??7DD2JlqeG{K;gc?5h60 zzVmn*l&v`#IbU`JNZthVbc|^wj8O`rDQMkp`}wUh%nO>ruuV*GalxQC-pIq?4r!A< z(;NUdbd1I!YIg7J-`%F637fd<_`WnjXdwgL1cl8fq zI`7+!?W9O`SyOrb%Q7a;r)ldTzhd!xNF#{_tbn5Sy!Hwl>o1_VeOOz75Cxpyk6e0d{-kCE^y6R*y;L!`SeP_|Jmr#V7gn=Du z3Lyh)8@pOh+3n){X!%YAK%jABLrSO1q!Kg|f)T)y2}Zcho1h~l_{k9l5$l-|t&ku; zJpH#Fe~)@<$Y7>nJqX>mZ{LEur@0>#9c}-0F46u#v!2Ndj_IlxVs{7zd zsT(GSK=THE+7;kTE?N;IjPFb+ADvg(kW*GJ@i4HMd#3wSF_<3%0bEOQu(!`?=*_Mq zRUBxL5*$Cwy9Snpbd+z>;BIr9+YY-eOz%D$c$5v%KS45L0mkYZI*+kT=KNKwF;aH> zw&HCRXuk16yWz7VFhoM;ztWNsyRmtrj+$jh!6q!7dNsqbU(pq3-;{@N!~>pd@_S z&ZD=@ufPG(aKrpMbG;n`Qn8v@|0zi^+gzuZ$(#?Wff4dKu4U&J(*tDrh#9s)?l06^ z{uv^9}&%8mtkbjX1uhwtZG<+P;2rS1M>kMU!qa?-abE_nVZ6ti-w895BsXNHwp^Bi&Ar zy8747om9Mf$hm!iBjeL4AF`58s#-6tg)34Xb@@H!@6K2gYk=sBA{QW87$q#lurHT zo3a@vCm=Q)F^v_m)+tbAqkB-%VtJTG9_;casV?TipH3!WC9=*T&}8=^ZW6MWSR3Gt zG-DG!0*9&_#k<~)LabL53K`)7Oy{!;d>j^b4;*nfq{Cx?fFFVfK?bf=eJnBmYOI)= zB01ZB4QP7bL02%epQ9BRDPBj%pcPe%`_-Ucg8`lZ3AaOBT)T12^CF_$6LJzenoN1v zt>B|wvAn!sDY-ON56Hb={5S0R9)zBVjHCbzlwn_6FU**mxIniNwP?SM?TWUF#a^i7 z_{d-CizgxHS9TO#KPUQ8BmM?#Lh3$uV2|=lO-(JY*5!~>Q0NJF=q$xXyjVI23cLs2 zm?KDPAacd+M+VB&|Lhi;e|8HKyjY3RM*9~pEaaLeooKJ7NiiII^T|XgCo&?0N36Gfk!?O!o#U&QXK<}Xx@n7m{4XHFJ1cP zC)RlcqX-yw^2J`{!YCm!Mb^tlM--Ziu8U!6Ev5uT^kmOg_eJZLJivT*y^pv$(6LmM zE#kHb<<&a69feLCh!YCqUaQG{Z>_AXM9toW2AFhLWTNQY<{%P1&HqHtt=e^auf`8E z5m~z0nq%X}TOgm?zuI6Rm&`X358oz!4T@^>pdpZTno0dGvfL~|RR~&O*TN|&QBf9b z=L3fVu%Uwm_3olW=|(gWR2x})1;d&*#>cJDW_81xTLwrv{&q)zrpCIfhaox=(z6zB zMhMP3goGGTimLEicfsxv^YN~JSSj~GXq%{4k$#Sb=Qhfx`aO*lT~_d4uWzneOSZR9 zU{}N&TTa5;)rNiB*#b+cpF^h;%^vNnBTyQFeHbVSu&BUCc@G5mdH5q!_WVVPW~eF- z704@KA|Uhcm&VV(5fsS^c>TYBQ~)T3BPO_#8?ZOY%E{#y@c!pgWxwx&OL!%K0_ghk z2D$XLt7F3dLKlgOnIgD)Q&1$8`X`oR!E>Cd>+bZu2guG-X_%F;(8_cS!TGQ;ga0GsQC+~()krqUs^1+#_gS+n73G- zt$+Dtg2AO4%lHD*`We;1ul`+0%KVu>`e{6BKDl0ee5~Z8o@&?nE=yneXJ%A} zzelaVIP0n{$IL#v+4beO^z@Kmf9mj*MG%8?H$I>b`2j#~8bQOj7T(v))L4!t&!* z6^nF6yO~fLU4u(HGof}><#)v|$Sv*Dag%5`6Pn|rNaL`mZ>Ro*!Btt$^~a8Kj(NSI zoAZ4=-Q8(_U!ohMish_?`zC6C@!UhBVJ4+^e4iQ`C&#BMi(jNtQ?|}d*;|{mljqAk zh_NdkJ`<&30u}ze-cBfL>psZ+@y~y@x5JI`ASkGJ=scu>Cl!I5vcaDdZVW5X87PQS z0{PDc$9*|{b!AxI({+@D$>XdEffa@9S2s6oJH7C3&&>0WZl{m^8!ewT|EHe#RzLc( z#Nk=0X7}qH*;~&AmMCAOXov-{`14-sPbC=!`J)1bXLu-T!1_!62b2C&`S5i<;?H_pK8zT{JwcTb{ik8b8EdLpmv}7z29z^5Bg?$g1JZhEHdeYblamg z|JO^c@ye~KF{^RQ9CGAp`uTtQ%HrMv4xhhFgxRT~~&aDcx2D5(a&jNhadieLFPFQKUSxq*0 z!^+ab()-yMBBz|^w4<%fo!**)a-)+QbbEM{3fwz+MXi6m$MdSJa?IJJ2f@MKnAYGD z68h9o!N+!#TI8A5+^}#asI7VJA@ac5G@A`s zS_T4rlJEH)tC-IAAR12sA|5|^^4O(IyU^b#>Zv~cA7AT%AA9QR>Hr?7|28g`LyS?3 zWzP|8Ot=wRwbmdcz&PI9;&^l|VW>dx$f9+^8<@6k{V{a@(5AopEhMF#6#Q6u7>x!+ zwb;cekgw+>Coi4HV|UF&fyEuUQ5yWQ;*O)O2hfT{epwZxb@iVBRivO$({EULw3>Bv zY;5dX9n+@u!{@=LBucnvdWk&P85nUSX$_;nqmxd&L5@Y{FQ*Ecuc<8wXFqE{zintl zj4`kHea?v3u)ohHyUM%s#yN}jFOo#f8H3jSm@68J*~dN~RxsbL;C*Q7)t6P%A5gc9 z+IkYE^P(2FMs!xpFP_n*X8S*Et4_so9Heu&h_gU&^pm$v+~DUpRSFd|uA;3j#rG!H z5w*Mi@iM$AU79;jLHwnnU?kht`our$9&;xkKmjx0 z3yfP|`qWvg?%UnPl?1Bb6+|(!l*g$Nug$6Jz{GjOuQi_w0xl^SQ+w3ML7Rr{S{rkr|K%v%vLjf)L})xC~SW0y0)+WN9xhB=FQw= zoHpdBdHV9p$BkycMm5fvw{GR8f_o(TS{U`#Z5WB{ckS zSms~NVP|oz^-%qLl6T~y)si0A@H6wpuU{*zbCLBZ5f7+s+;`y{$i)Hr;4@wi9srF8mXr!NO1v2muOD(HnrV?rya_ z9SrXK5f1?L$#V3UqpI3}Z*uM`LIWlF9+9;;K62_)aHuxa2{LgA-^^{`-xiGsknO}s zj={T}LwyPrkJ{o!(n8Rceg&(1BR-y|`1kU4zAX$Chb+t~m-Y7x z{TWq!OiW$@Z>(}6<8e#O6hhS`#66fJW70#^W?wy*&|pf&$>nzb`$qoary?ddVG8Sp z!_2`>?JYx|YqkHc$$qkgAYRE#44NZifSg!_Rc#eT5?t`=Drx*vGtKaZ2aCiF7P+9d~yd7NI3$4%HY4*$G85;=~wpV zp6)OwrR8Ce^Pg8r1{yYU3oq7avX(IfyRIfc5ZF zi)Y1mVKRe;)j-d_Nj0{?+tb$R(3r6uQM6IM~l1 zeb5NXi6(0&%cF_g>pXRZxF3F~6?<)i;hDH?u#eM5by6W%o={8YE? zPy~YSv7}SRh${>U6YvT|sYT1fiL(Rdu-n8(-qh6e-R8|!S-%dd^cNYt3%=Srr7b_X4NN=en_x}JCy1kh~3Apb3Do=#bxOdYQ&Ug*puRV!2b6G7; zrnGxG0@P_&xGa_b{#|LE?9~7Mi6@rxB*OtJ`1xoCno_r6@A<$Org- z#Px&W#Czl8Wv;e36^U3nAj*}21|ZW7M9qg*CIcdxh!v;8#W&#y8MziE4OzO<*4s(! zL~%k7ln%ZJZ>{{EMREwY|J)GcRxkw}zqOif#2uf1=uI?^jE^`6He@;Nt(15Fu z;sHNGUfy4_6#*D}MqEPaI3Bdig5%98O-ozdP&OmYgL!wRmTMVBxh1-+PImL~`xySZ zeW`8%8=S^r-z2@VPU?Bqmrm1sgRYZ8Ip2ng%slHWy=lHavrfJjoX-13{%P$sj9;HAZ!O+=d=DC45hdJeomZza8_PaTh4va zooGkFbmqA&6||UCtYfkZo%emy-Dn$l{68D-`$sI952-#xm;xxmSs5F%q#S;PRn&mj zBRUA-rI2;Gcz6_Y=r7a^wdJOElG&Cl%dV|th6_R&8NAmmuw6somaOb8{k?1yVrGJN ziFI9_YuSY}zaCc@^HlLZ_;WtQ3Xjsh<$|H6R7MJEdP%-(0?D8pyCXNImNFOupq03C zTv#Ba?#jwRi-{ej#j5>wQPyjmV_c$TC&~3c2fVpGU96Vej67a<{sVae z0w=tGF8^lVpI`GUX^Yi=t9|imN!C3-ohV0SV@*|GXk6;E@{f7P>F0lXDCX_KsiP9U z@g37S6FhTv@g0&WL3aW!_Fa?bE(*V^yx@R`q_^F!2mTlvS2GD*GdmtVbyOd>hRzyu{=~1($2RG zO4tn%zY!SiZwME4e^v@6%PPQLKiA-&n5tKic=`VOYPyUYPkxv1P5e0Ux~6Mt>bvTp zRi;^UrtBVNzN@5D&%5#e7r2yX6NHr7+IkOJVJaSlwqce;B_j%MHt->&{5~QvY zD;zmOg49g-i67YP4Ap7G(J2h;N^A$$Qz9Y#XZ*$iW%40AKW<=UHL?En&bs|)P6vk9 zNZN#fAPPJjzWFt(D;Y+?tZF1wSK^N~i-7z|6iGwF*5e38a&1K`#N|wY zGbE=05Q7wTE@5^5yb>G}^oztPf9A7{d72^F!+Upr!ltaeTrKVtD>19%N`k$U8cYkP z8+8s|pLzN27p(bWm@_!xY@n*@fm0ON-rpl92~ttWQ5;y?ljS%C>;{1MX;Hu9W?HU* zD!#dQM#}f-9LM1{(Q}w$PkwqCF0jWqjii>Gc2Gf3I*!z@mk4-J4s@6fTXrEPSjf1gT+I|95Q!={8yRt^p$xPr%=(Za3;@W6n1w?zVqk8A@nfDX+VJ8}TILdaJNB1PYVkMYJeH zH-eqc@XT5hisNjD12Ol524=*Qu7m_i4&5UcM~(nFG=^lC4$C`EHMszHB+==Ko;d=a z29CtaFge4&i5y0UG8+X_xjD;Z1L4ETHijv)3{z@?y(uiyBP|=qQ7ZgrU*5*iC+C+J zf8oru;xlauay`N1^uT{!Z$-~n-7KK$#H8Uy;u<~}V9&;=hM;6vN8>?7z1r}#2bFU< zv7TUNaF9Xe`0@MtaI+9RS9w@|GVaT)BiQbYV^6yKbf+mx;#@eq6DQi$=C4V-^o(jO0W7__O!;< zh-QNn9!lg5pLpMsJ@OmrUZ=K~H;Lk88DcQ;DD&9&Ad`3Hg+ev05L)y8eV~5k2Ug}Mk3do5Gy*blKhT=J8}^uYh9ZZ~OgL5EPB^$>CD-oc@oa;H z4*?bT%~9vV-%C=C+%5@uDfadJJGtqqFK#xzt_hi4D_{SoI8$nd?`@i{`7cfO#Px;~ z#*IshloaXN2S)3b1nrKx?jG*H^GfR%Wu)Y=zjnax>TTcp*T~I;moZliEXIas9E+|! z604q{CNlqT@10oQs8hCvjp0X}J`E*s&*n99e3sc}!bd40tGrT!w#tb!R=jz$aX~{` zBLidU7w;?z7%DcDHI3<07cMHPsH_VLPn9oM5vBeQkt!YZU&u)v(Lz0a!cLy4qRtO) zV%q!qT z#eWsco&ybh{YdM7r%~-2sc*R$tfkyPu)Kb8MRi`DtcA>HikQmAV*dmh-n&ix;cwR$ zF8u@1zE?u0k7+WS24<_^{Fh;}z|@rJ;>`kPsY!&LdKl_|>zUMct>yNA3Ik(U4ociefe!ZUK z@wh+k168dq7m?ln{WVxQJwrcZm8vV4iwWanv(SG-TU(|1bA$8*?~=K=cIH0=KD_$d zoXk!dd&s{%L7=aHe&LUXoh5Enj&y|5ciZro2W4(i!AnadNk{PG=u}YSs9F7gy=q;1 zZvr{Da5L>1`M`+B;gp;uzP>$;%rXaY#QtMk(IHbej3~mVda*+8i(#;iT9@ox>h= z?Vm&9P&j6mgwjLtL-q8TP@)_H1A^Egt^(_qLOIv;lqxBF^@{4b=P6Nq!s*rj*$136 zaKQy>hT?ZwYHOATq~sX+Phz?k^B^6T6X>hm;Oh3=z~|7RH7sjrpk@)~j00~C!YUj$ z`ub+xIE+i@MqBMn>QJ_Pe5680nB$L*DL*>KOT@yKe6I&)Zm!pM;9sTeqt}1_sFEVv zXR|B3ymBSGkH)=tvLLuno+KF2j{ zoR~U2|3Q~~TUA!8?sM%AFmwLhXf2_`n=cc$>`23oDdF;a~63JY9h+E!!(fk-f6_G2hTY!mGSa#Y&3eJjH_X&s6z5a|*? zA%``=20S95Ut`Dii{Bu}>!1}?{E3}j%YRGLoFRdsteNJgbN&4PFR0dy|39df#4iha z%HqZU!mN|kSa0oA8y3wmV7&Wm_ih7NYOzPD#s9A*>)3O;#$kc~)T>?B9#UQ4+V!_e zeHxT~4^<-JbZrzfTWR9)jgMG0ZQjf@I)SfDx>gFgD=8Q;#sr&iapO0^moI~yGvKeh zHXWu1GA<)5@9&s)XG{0^{A-^@p)1=9vneBEhP)lQVYfm;gfsVT+nQAZzobW0O2jEDnV7IV>LIj*-n2WJ%^3iw^EniHf zVd1zLAd4T!M`RRB44dU#$1*hCaVGsmmhfSTG(lOJLI5+sTBEV81i{15_6&$sS^c0h z6$mk9=R%&m|Ix8Cs04IwjiM2zrYOQi&_+?rDm#;_5ql2a$fc;Gh)9z(wM0d+u%HfX zAOqskg0ax>oBwk}C3A=U2ChYl(+E`!VH4HlsSr( zuiXXGxWFZf+Vme^n3UeKjIwA21ubnT>GDx-xQ(Pd5fXF)H#1tw zE{N!8h`cByqyk{oM~E#@xW3KjKKA|}#tzQ!!OH9vjmoBiEL#O17asoX`%@PC&aNz5 zpFg=aYmvVM=DaW@rOLh7dkaG6tpWn8!I#^I4uFbut1$ACp& z*&w0e3>GxXaxPRLA=ve%05LFR-${9hzn1u4+1`^SwH$g62UOBasGiB{C1jfi54gzA zQSD(*qS`b5FH`#0VwuOw@LX3=e3s`<0;sF4NZRUn+>9>rM!74+O0I|xk; zBOx;!{*6CSM-Jd4x&8h*EC$4{y%25#8x?=_tso-=%}Aj)u!s%Py@8^hR@wB-UJ!{n z;KLFqa6Fk;QE_vx*Gx!LgBggeM2JKT(ZG$mjEw;5({0Gfh_Du<$rnPr-cV?v_)P#o zsIgYgP+_j=II+gHDC_b4}?GR zGhz4qZ*b+>=-)}q*YWYXi?AQW$zGY7J3Rcd)o0^)orY&^jy!GD9%xrF7xc`wd=0HIF z`~jrFqL@M1loxcCAq@=;W%LC}M^?e@;5x`j1dGQ1Sqev_M8E8NBW0Vjug-Vif(nZ~ znRVc+D?ZL9!P6K6;cIY%2R-vz8gEe=cN~Diurj*v!X1_k~Mh zN1kbQsw7>N^Lep1<--ldC+Uj+Dr~jAotplFC4#;V^1gurazVTPp#)_2<=+`TH~RC( zug_Cc=Z%dSyiI<8dE)RR+b6lolkQZpkQ_|u!Lo|?F^D{(auOYxok1vHibVDko5sY| ze*S6kz=m#N6O-X5F$|d2VA{Bw0x)|&gmhrbnIGSjCM>$C^4}v-*>3RvjE%2txGEqQ zP<^{RvJ@0m@qy2u$BUQNu&$YL1r?Vw_Df^Z+Xo#W(KKej5sWnPxNt~2OWdc4l8r(E zJxJn0*G5bk$QCD$9YUzx0Y-->IfMTxzaEH?X|3u`F+y`w$C1HJIfJfvt-g=0E(>O^ z_ON&(O0$bg=u!!P9H*BtkP*nQ*O>!x zD$xO5G4L0HuH!nHb3jrJsyaVmykk)t!F`0tLNL3?fE`K`=A{~=8|7Suh_nJ_FfR~b zh%L}gLWY4JpXpTlvy$nrqKOm=8&kbD1 zz|^FrfX^EKGa-0=L0*^%e4L20gcr_ra^@%YgMU~GQ;Wofe+?dk=gj}=>u{%TY`tg{ zWpcW}RcoXv=sBz?1tre6FyM)#i>$%p#gysb&qziA+>vCE1MDo?SPyL1mHR$&_P|)| zYT4ztk!=Lx0gNatvWJ4_-Tw6zB;fZST!NU7jM?5eRKDMfzzvwhy!$GCTu0|{dIZyfmzY{w5bZde-aRD;g0mp$v5=@up^61X=`P5z&qJV=B52)duy76;puS zIy~p_^;d!e!4YEjcN(evjm=1EBdR$H#lphk)hmg47i>Q8LHDJ(9`QG)96MH8HyiNO zdHu@EHZomz@Hb&p_70NVh{nwTH3wWUS{^>SeZc0H@Hi+9ZFk_n|Q z%efr2k%0R9mkq7f9Y6l%%a^_0-FQ|sl!VZ7-2JbkyPY%^fBxLEYnP^nFKlVZbO0+i zQi~nzPGC#qL*IIIj4G3RGeKblS*(w5JN%zvS?)v`_@_%)CAsX4EV0YU*G5ANgcob` zKbxxle;>s~n;rIf&*%zr=PFdTBxth}uR_d~NZdnrBdco(*W9?7m7pxz^)HbTy;EO1WMsZ>&pD z#jil30*MbZ5V*2{_1kvC9Soz}1JUV7C;?C@Bw|u%UEj=Zh)RW)@<86bv4U^Q)+}Ri z?{x)PvEheCfV=ABrD6a#NGM8o4G029MxKNv4?p~K?S>mj{0pWxRVZ55v$1{o=(75X z9}eZeV{*_e;UsVkyUP?cMwV{$fu#HYnt4&M6vE=nco$qpTZGBJM?6Ebs=$VS(4mfH zW*Lj>bzLy?!??U0rP3r6w_9QF3(;;wGE6v>@wp&fzVN-7dHwB(JK5^zQ=H!cHynHy zasOgOK=pO>2!-ei8q z)2>V!C71%kbCYTQdv7T4?T6sVlF=fj)=7WcPAEVCe70kmupLireg!gpYB1!>BD}8e zU&EA)Kmbo?byAxl)0v!_ayXc7sqT1I8Sza_$gw$8zr}U3g4~F#C-HZAqHM$|40=H& zIJ^#EH_oBYpbZ7H^D<@^<=ya;I9A40!BltSk^)6h@G`-TfS#D70dDo)^XAnxz>5sMlLCvO(j3s=6 z-ac8)9u=6+)~0ZH>h_=9S==RF0>Q zOL8#c<0a*9+_>@Uzy{g)TB>iblVW!_0aV1K_@lIC>pIqhHU*0Z26v({vs8}my5-go zjgYT_aO`puNSbukehasNqYng&+>}1hJIX*Jd0;||B$h8>{Eh25ID#;6LdV;OE>&cg ze_P=B+x6a8C@f1#ysGaNaXVjggBPYL2pRE@4sx)hK95YJLPtuNy91|j&OZrzq6Cz2Ri8;?|DtlmB1GylB#_>Q{mI5uhg`#NH zmA>#^abhw~>BB81+Ce=ne!16>z1U||3Da6d8YdMzenPuxc;#SzlD$H3&GpAkN;%&% z{3Iv>ymsfJA7km0;19yILdwLUGWf>dvWj!3^=IO##3*ADKG_C2U4%cC^8hr&3LpjR zKx2pgW^`y%Pnekd?*NO*jAN+n6+;k#Cn>is%0UHt&E8w(?T1Hywp|dIf79KlJ)9 zN^kqsX=LOtIy$Yl^0F3pT>fX5 zJ68j~`Mq1-xspyX9#&1nIS#P`rP`?E^-B+j6AB$LC`V4abQsw$NfT{Co0_Y}SN!u8zZ^G{j9?W?E>R)uQ2 z4}-@`E)LZJ5Je~W{`pPuBkKX^pFKeDfJ0Ac=-G%`hPB-2}kdeQRk;s6;Yn~onnSVmZTs3-MB@A?bigJCc41LW=ZmUcB}(z%x$0)<>b z-a*NC=arv1$`vxU!z8b#S=GUl2y0*^8i=KrpTBeMNl-iaCiLE;(5Iuoy5)d|M!<;~ z7;w{&4wUdWiFA-$<=cWZ4zT@#gCaQ+Ai=CcVi=Sv= zWo20O957nM_rM3^DD0(VmhioZ9bbf|N=1lTf`EhYS55l74_oX$5M&SUvka>8uLoMdX}XB!M#8{pBbZ&|jrvb7_zUT~)3}Y)*irn9FXml_4&mS9v)nP2Ov~)H z=y#^WQnAaCG#8_IE;%jM>MD0U`+V8xpIP9T!#^C82c@jiYsqLB1t!LOB*5>;Y2VDx z|GQA(g*te+zc+VMa~JolF8xh9_2nAtS?ZJyVdvm{BlSq%);9jh#ow84*VEn#aWy$L z&if9l%^zyPj`vId_415Dp zN^P+IFfh@2KRkoUk=(Pk!L4S>9fJBc*L^J_c~_y1NIs#olR;P|adFjFO4qv&WwD1x zc~qP?{jDKWb}k+RynSavK+;{S_a291!k=RK zhlpWH%a3DQT-87t=9u?ty1HKsHUwX`zEtpqt;R6g=lkC2ZwJ7hJ}pI;NV9L}bi9J1 zAGF=py~PTIyoXZ89FUxp=XEk(F*~(2*{pvN z-ku)pOrYGYJl5v$+?DQtu7;;C>$igpSq3FO%g^J~ZN>O=r$yh3Kd&(Qap1r&2A9g= ziNiD|q+!gMYe_wMbiSvY_m&ce?BaXvsh&1QCIhdF=|9CpGHadCBi*;E4+kpRnAG#+sU-WU9Yk&%&xf|6zL zGZ~LAv8pZWZzrTQGdXF{(a{y;?shd3oP69F)E?(-7>yvp)buBq#3ns-hSps|UQ&`3 zchJ1R_S>w7jWtPRB!XY0i2D}Mkfa3+LtGBE z2kn_MtHA+0ZgBpU+<6ga0@R{F*1T-<&^bp3mz7Aip#~f&bU7yKY__7nVxqXg-HO8E zdTvz1Yz1ex-_0E1L?9NsJhzCRd0BK$wQz@Vx!YO z37yfV!=m;u9-p1)=0xlyJaNuO9c2oen)pld3vbC0;Wxjfs4^><0 z-C5izgw$0aRMXw=xNimoM-(nRzCxT60l`$_N0zwo2Ct^M6AltkWMWAYc|*T*wf4bm zJ44g&BVD_HCb=c`O~<-8Oq=Y2G48GJ!RI92g>1CPH8Fn#+lZ z63!F}ct4F7`I9q*UK3>|yu1qXVJB-GV?)Qw%^G&(@Zs!NRI!qb-Gpy3&0fVxHG#1; z9;bo^d2l+v)5z!xU^VTN7|_gt-_bps^dAAQX|`B{D5c3H6}>buUxq+6460(>LhNYe ztec;)UKP^YV0a(aMO=_#+E?$z?lS+hgnBH@Z`|Z2TWp(ELmDzoNiXfO^$0w!x)^(7z!P;3g%e-`f_O zN&{tFbgT#gRY!2t@y3tjaDbQ|BiTzo@`G0FTlWzL{^YStKd~}$Hpvc*c>mwE_Kp_m z-Z4W8;akVO(Y0FIxm9-*DIUmiCK^el?*5NsF}|?sHZ8|4iYOF;LRlFU z?q(rtj4-gF2+!(Ah8t}Z@lLcUd}$zh6v;2WK8tC`TZYb=`BVX5eZQhnWUpvP`9aJ7 zVdKmNFpnDAI`)50IaBfm5~pr&zT&vurm;c`nK?&AI1JN^zHy)56{-c;&;d}?BHPHH zn@M9BgXf+2*2IvL+~RZS2iTfOQ1NV7v%2c9(iLN^Cgb{MOqzS{bYzQl$A4w;X-Pf`w$U?Kg{5}tT1z9ueg4R+`IaEb>a!|^)y$EBUleF zxy{0z0^(1jxW06I0kCKOV~q^YZ%8;UalM`AC z<+iN>8>?@ANdvS#0U$BHdMd%h>7h&5x)0`Eu5UBYVCTD(pLE$_eC+`$GK;79L1JJ)_;_0&p}CiN=#5t4nd>PjhZ)(S|S zU^~UTeS7)ig1a5;EcxvFJA_B@4QxvGhaVgmK2@mwL@SSd0f4hino&hJ>S2+B07pHO zfyN^RYdO|vJV{DAILxGyX6yHM?TOeVu%eC_My2XIYAGK`w;U_{%FrR3NY&SF6@S;H z;C$6*Ov8v{V#Cz@g?Br?bha3$=cK6>6&1lScNeGL9o|sBMUxaeY-9T+r zkewOkHh$*mjM8{-C^VMYi}}Hhd#V~^zGJGE^@OQh@y^q*9CF9ivNc`^qO!@76B8>* zxy1|of`Ot_#n-aosEq1(*fB35+Ki0h(L$lV(fsSa?l*WA*)q+qyuBqxEMh^4j1V4D z?O^|qf0`cyB;tiZ#qx!DrQevmWVCN4Yi&te(3rfmwTqSNa^?=`eY+G{L@CYZa!hy+ zE^L&2UUu-GlKize05+9*pQ(Wgw%Szbx_{u%g-3sn$1*p0X5g^Vu6ywD%ww$wL}q@}L52*H6{L{0fwM0{l(S@AKx7mmNX{j zONF-_*O6-Y!hMzKAZan)MQ9T^WBD;n#_gy&k*2>yo%K{@{|a)?wrsqyx1wFn+PdS!^LJeYyxvR;tLrc@9 znkSN{eHNQebmmq$g0N0PYZ!c(tpB+EgnB8dbwE~aai9bUEpf}Uj>R+$eIDgZKV$75 z`hOf*aW>x7arvwx@B5W!Tk8XMyU!2)M`b(j`ObY#Sbd1@DTR^9NXNaCisCxXh4e(6 zB)(xpih!KYHAfXJ_wIO+IkY!x>B>VfcUuORuiS7TLO)KG;Y)7@;PmR{3=Di=z%+%* z;;@Lyhb_-z)hK>Vdvn1IBQeG8Sph@9z*-6vo$)r^hY!!RCh-j!*|mQ+UtG76ajOF- zC5*3{GlrFg1zg09OA0pc7sEDf9MZW~5>h_gH_}+Bl9a8( z_-zUg9mZH1Qu3JC0HC6q%%E`rp#iwmv2-I@!tZa)MkmUaM=d_v_LQ%@WjDOfl13cP zP8q$0cE94ZN&1Y^lYH4kB+`5+E$gbD_=1=lTEmliDLOQM-+Wwy9zz%AM&cxH1oxhd zbk63FOK`cWW;Pac*&A+>$HedIwO< z0d;lFq1|eEdyZ)~Uj8yPbWqwC38lAyp|l;-SvX_YRn-x*HUFq$xFr3a>Q^8urJn zOiV*a#IB2LOj_z4I5>^>e|L=5H@(toXmRk%OQYmV#qe}w_ey{7$_@WDJj zM}&tn)RBJPwY?*j!vwkLy3Q8XHeKpnqLETP>pr>cHtCB4qoe?_RcuHfxHui0`tV_C zvR`%^hdVa3YV_%X6|7TrPtt_O%S+>MDxICJ`M42;#|-*>Fl0Uxr$9_BVLwWbNl2=} z@c^P9ZeIiqqBG}#Mcj|}rnMTuO0En;iY%THq9a!M+X<77at4i%HO!d6&NqRt;0+8u z%phj|0i<0hT7Ajk@uFMiyc?n!GLIy0-L{R)5rbPie<5fjN0K`&D0H)jr)ShNPU+-n zr48Cs;gOMFW?i01j%@h3{lP{Csq&s-6>|ZGva&L&Q*4J>o-{CyV6xE<{|6O_E5h#G z+i5(yF`;{X7r^~Q&e_Q9MyX-My%Kvi#h-)BF;SL*m~SDBqeoygD-Z78!3(c{6c{`g zr2x6MY?ny)$Rdx)n%+j8Vr9akzKkX zqp;!3zI{{@99j+2_7OFpm#;-wXUc+VvR0a2=bfyCrzv|6AhVf{EP61kJI4oyg8U@T zc)xf{NW#Pb%rzhPOsXe)%f!d=l0Uz%VhoLO7^x#8*fa9yl$W+{z*-*}U~*3Xcy~K|zkJC|m-2xd8xOTpvBX6o!1%#wUe3S}#Ru-@AT(|) zlw!WvW-x8IZ|R6+lc=_9!Xv{5|k*PG@tB-}EAEcj)~$zD@ab65D8Z6r9N6x7aY9qopw9mSDOU)3FKd_q@MEnYPP zhXim_5f(uh?3{&utefaH&YxxpPIJ;cUbFvXH|l5xw%x_g)yq*_AKHMQTzf06* zTQKO-J4E;8NV#509mB1S^cum^mKYz}xo!d+^YnB{al@^u!`*$$0Q{+{<2f1?lEHk) zP1)CglEQeWTC*4;EN|^nnc3LwnDl2%Okp6dT^*>|A%=d^`Ga zScGQFM?^Fd%4u9<#lH`L546R2_q2HfYkgc|F z-%do;YBl#@Nd^dMWLydNqL4wh#{8>RIj2- zbNIMEbo3D5Gnm*LG&&ww+6*8pMnVW4TDUqF4xR`6nM}7Ts%D&dUL8?yR<-d8K<|R=NPWBc$O)UM1^?Nq7 zgn`$1u|mh=f_ePUjg#8O=6@0?)%o%X`0kY`<7c;DnTAJtQ-Ob*1JZB#A5yQ&UqN z-n#$rr{pH)pQV1uwE~z$LhC|IoO|J?VSDZ-PGa`-n%0xW6Mrrp+nM_#<>ZUalux0n zgXX+Jyhi&H-0%&TLAvyZaLz=t=npu%0NiIxRa=n8KiP<^7D1`GeGv=6j>I+Hb|Op8c6oO5-U z&_5t`H1QmlO!zdKEWFNeQ{KDwkQbHSe2jTjQ;tot$Cntl8(W>SH$FG&ftXPuq$F;A zQo>an1B^>#no2G^Vq?I08dkF3)PlGD_(iw%OBpk5RbVU!^^H%9DD6+=x8Wk-3ws&8+<9B}|7Gf>JI^p0zpG=R=qOK=cF z4kXTGT5mp{Nh|mpaR*#)y8`#6>1pocRCyz;8BnCieit)b=l(f7fX*hH>py5@$w!98 zjMUv49kb1n=ZpO+g3GX<30B}L8ZmF~1ARFO11>48h+!~0^97!N2SPV1+&mH=73B|l zW5MKguzrngCsp&59BGR1yt#oea|x5m>0=C}$1(?;~@Q8&6l3_~tQ zd{NLzvjFM%n+j%D2R-P_x$}R4n1KMkHH7GA3gR*>4dNRt1BD;d8cT#Cr%c)O*5RoJ z3(d{UEQV2_?rTuldj4?}ocJNbr#-bjZXx?c|Jc;_$9yaI?T%LIXk=@w2zkvJg7-~# zro`!S+tbRnX(27{a15HndB@=?NV+5nktC5I8656vhC$~pQJ&YvPKyP{hk$4&>rwubzWcgV59+j-ie-_?b+AS4&(eLEpZ($lPI8k(OH}o6XNP&n`?2f+_Yctz`jWn-l!XUS z**LCpVmF7g@fus!Ifoq3QCMWF!rKnUq{VIvD(^`HRxvjDzyUt{6keAN)>@%oJJMK% z-tC|}b!lHhP2;j5`YSjolMrWg!%}i8)Y3WZr5nsusJ;&7&?7_ z27!vye(y?8JD`9*mH4xq=hTfWQyl^VuHSpcBb7471rXMDj^;79n+f`X*^cnwpO_WB zyYgpi<_!1wy^=`K$x{6k2+K%BdBd8+KKRy?`S}3hTp+_vsV(0o{spKZvWPW?$Kb&1 zafpvCG(OYszD`Q^SC1~E z_iHISng<(DaEl>M^kq#;ajxj0j~`F;Zc1O{3<2<`FP5if7LOIywHCFo%iR~jTY)xZ zJgoBY7h)@Os>G+Lhf#3qmzcaf7x-fkW#7HQ@Lw#03`$yCh z-z|I>Lh-9i)TBJ(wHwvB+Nc-vOgFLY1~B?LTXSAZ*nae*eqjYD_WvxUpo8 zg2|fYIq8m|H4AS%x|_+nxJl4mKUKSCtNdCCVu1Au&h3jSs+ zD`%!J(53v;XlA$|ghyDCl_<-IM}XW=q4lM|zJgC=9xPVx_TS98qdD*91NjGO9dz8q zJ(sDIr0aCQg%k%LZS(fbUTBWJDW4r*%*|~zc1^9{G($`s{XK;cG5HOfISkUb-SQth zWu^F0P72?o49(UD5p{;*&Q$H2Zp&MekL)QA|IXn0Qurh1#7v}6*mR84I9i(tm}DzhDIY^Kx`!!{9<`}QFm-umi+5a+;LE+S4h^K0@$=HhM6=CZ2pZ}&mI3@ZglM9)TP-|6~= zRE*4@@7x`^Ndcva~S_9)lTjz}+Uj{H(jpvFmht6IGFT|i(X@O|=;HY+d(rrnxb zT{`~hy6?vlO;OjZGvCbj=AyEJ+w2{Ywa;Jgx(8$Ps?DB1IU zsd4!5oyJ%4Myb5xJN8)>C09~iXLmyp+31rqsII4N$j-T=7c0<~33^3gqnEc^WUv%} z^EQoliJ-b5u}vs#?Y~W14*t5t*8ly(AurcZYQ~SdC*LsVE_)u!Wt=b$FmBeL%Ez5i_LYibg7h3zG9Gw`h+BB>v z$WgVKbGRw*$`bXUTU5ns!%Y;H>bceX-gg@nTnwErZdAVNlqYWg+m-*}v5gEG=NH86 zk# zxT7?uHne?s|6Xv+T4$JA>v`%x&V9ZTnxOsbBmx?Y)NX!p?6<0+bh9-MoS*Vg05YF0 zoCqiP_3Z3)eWq3u1HMzzuO9WTiL9h5f0C+-t&0_Q74~NkWS;MjSb6Kut#$RH19Q&j z&OK|cS!Z}PxMug=bPZbx>kw>>g2B%_T7g1w?Js;N){^?tmEOkbn*vlH7VhH*2!GY% zn&y~oIr#7}2e=)wYb0SO$SRFh&bC|TrM&P~DPIg3t!scRj#3yXNdI^t9jhQ=*P%QA z8z)=i`R+I;;dh#RJt?jHCi40l?m=mh)^w2OnA;Z9`p4d`vas48;z!!#u4wgs0%ZH`c z8AnBPs&C-3Q+C=G+2TBvmyUxEN}pRMvua;|=$=xqsuO=ROxl^gHEsum38l{`&f?Cu zYp(=UXJ$EVHko1MdfqD#m#|6Gcl7+;vAgFx z!-oP&%rE=f?9G<6X)J&2f@GD`t_h`j1H*zcGN(lU&ZjAaC*=2*>qqWhoj@>xH2OL6LD7sa7f&3yd3J+KkKaC@0H!V?tF2S z&~2<%6qAqeaA>O#fDF0vy=6PIgBav_NcX5hTZ9n1&^MMF&mA*k1i=CX08;a(ELWoY z<3;ix2wNT{Te$WW#KnvZc5{s@=U+(JX8tKSUPUGLoehuDajFU270kfbUnW20kA-oK zvZ=W^ki#}N<>wcH{IdPoEQO$LDp+KOJEEo zjJ|*WK0N)>Wq(HQ@*TRtn(E$|GgpCH1wGj@$b+_eug#vZT=CZ)5B$1~h3~c{k3rKM zTTvr=D4I!75m>6fxT`qpidQh2&fD4h3Ar1Dcu8S0Y-E$9?4UKT5Tyqn8xS0ujs8{$ zHZX_y$0^?5M24M(<*VHyz!#zDlMH=db1$?Zo?)!WHMBj_S($gk@rTw=gx5J023;N} z-eM@tpY&7|+&lUH4)%H9eXBSznIi>zHk&(N9c{_IBB?Gvp~|R%vx5m?%v_^{Nxie!SddKot-d_#^^4?Oj@A8WG{8 z?P0!?F>~5nhl6;2_mj>|GF{dGi%i(=Jv{7F&4}dP(KmDK(ubE zK`ag$YTN5uz&>@=;O)|D4IOaW$Y&`$SEG)Dyy@%tkk}eH@_eT}=dly=r@Mi>r+;K5 zs4A4_GdAukr{T3=gbBNk1IL)cyRZ_7h@~_9IaiPB_S!JQFej0E)d$f$(Za2JfGix@ zj$FH4g>_|RDjOaGIEz{qS{|kKSw>54yUwfr$iQJcSS6%V57Dy;?S3T{hotkd{I^$Y z5YiL2a!l)ypYwzom5tfS$|JdMH=Kd|s4}nn5G_fMHdPHFgcUm+F%-ULkj6G~)*#~N zc?PD88Ox469}~d&g`|v<-{*noMV9Xw?#c!wx(A(|vl~ojBD?HbtR*0RV<9uQezDi9 z8D{;j-bH6H#Yh_`W_}nCuEBBrQeWMuOHwzU(OWmLqwHu zKR1yk^&?Ew*1qFqwOT88C5^iA*FJM6OS$qS396qn8K4KUP;T}dJ}JH_C#k$u3TX5j zph;keDI;WZBeIq5Y+pIKp1SgVy1<*fZ6-VPy=vRRw!DK>(jBw!L*~Xi*&$|p+v|8) z`}JWPvMgfnV&=2^azZioe76PnS;J>=hy9C(=o8c2J7X*g$r$k?k{0Dc09K_ zC^$IyrHpdl+-c!z1oDmiZt1LoC7vmqwa$v(47oeD?&Yk^o04Foap%{}J;)I!G$njr zG1>Lzh@KnVnuMs=ET?USfv~f9rkipFb_SHN__19(Xz#CbH?***j=w4=*k>xTFA)px z?Vp9w)}!Kw_;wIN`5W^u;b+~%>p*GCTKaiNTmTkS#xJWhllbIb0am0>;u=&d0hN*k z{U6)!3m7{O%KShjR4sCn%Mbc0*1CA5i<8kXp90u>(!nszvUcTpeNZU1A1YrKs+WD_ zYivG|f8fvFT!cW-4yVi0pAz}hV^TEdQ>=q15%|%|re4@}Z1{azq%-cK(-KE%SM}Ousqij;>c;X57`#|~uiR64 zZ3BCHdb&M|9gir(3v_WOL`@1awyt|fy+l9y`^}_sf-f%yncrWwXfR`-#Ya~Q*4(?6 z;X4W2{Z@RvBJ_({gD8Y;-%&01UUqVIjg0*m616sSFrB|%=)>o6hE|5|L00Sldk2R% z#M!a*>xc5C?blyilwWsNtMA0EDZlJ*bN-)q@U%Rzm|^C1*tCIFcjvmkKx(SEW8b{f zQ-&$s6-^{r7mD2XN9@gRrkFm9jQD-wONTAJ7k5A36$ZoYy|m_3HpJxv+$1SWPq7DC$ftT`$p! zi#j{!^fTl;%DojCiXUrUsJQKJHHu96Jr@Iz`%QjnM97f`GNu_Kn+hg$4kbChJ$`LK zVE-eXqP0f@_cT?^@VI+=*3fQ28qsyKY<HsyHu6$$Zjs<%B+NKvX(KO;X|YYP|B{F!Tr;%B>C z4*vG`h3VEw(_>2|`muNKf3DdhGqvaCZ~G;YV#)gvOE2jdHuB1U`S#`J=WU8_r`Gel z`J~k$njE*n`BIv(v2g>&8||x)+^Ke~yw>ymYBWD1Knzi5$1@Egst|Mlb9>m~~nBXV;(VyF10_y^CcQ6mP}1 zSjtVg=%sftFlog6($Wg~(N{(<%zX|m(Fw|pO^rCK!47;2L$B?jzA&>1iu6RGad0(P z&jd8er<#hpH2&=02LTHVneHk0ig}z)5A2CLzyGsE=fUE!%AoNwj$i*;_^#ueyIS)| z-Xwma&XUJny^uPeR(r2<^;OdjW%$nhMTVnOf|bsJbei%Wv#T0HdCsWUPlwmLFLZFo zh<%dg8B!Yo6tZU1g8{|1On2eRlXV9s1KryS-z;@)jSQR3$rIhmBem~)jpXe&Lf zEI;=B>;gMUHO;S^W{67wCWic{oj_9Hl65Q}JP+SZJ4K#A#iPSO!Do1ZCQdS-g{@#HP}pM4UBlk}I36r+Ra zPFFa+K6QmI)ZHch>K*T5Yo zLqkF&H@a6&8MHhI7ms#q{fHuiiZX*+$9uRL%o#w&ZC(ugQp6~bQLc7KebVEgR?ZRO~; zT0r?!8(Yc-_L)yzz6kntR?WIz{ix2@!I~E#)-1C=o8t4(K!jHTDT=1DksEk+(=l>b zyu4}XjZzc;fypW1jzMv^y(w=5jJtMI&jG*e;9&X(v9XyRbLZZjM^GnqnjVQzzbd5a zL?yk$frlIRYdiK$M=^J)QR$5UJP>|qA#6W;fz4bZuz?7_Og4(TINbDaS zxV51&s|G*jI_~+H2ixYp_wrn&W!zsM_ntq8IYM(9SelZ>j+Z^){~oK+Q%Y!PnH?P~ zMh#4)oNOXSPu({C8a@ac5N&yZEEhoz-<>=0R`;P7n8NTa3zOG_4awDaQaGLY%D3NK zPnYJtd}+CyMe)j!K;oHz=_aBn-@^OV+#_f6n0lW z)Jr8{p{D!W(PmQl?Q^^#3-90cxhLWV%ZKj`)C{^Y+9*dEJ>H(Ye^cXYPMXBP?#W*# z{N{-{catT`Aw}tR5i4)<{?JteoKLzB{S*1_Q&_9 zHm}*S3+SENRek-t(a~%0CmwYLrlzJQn!J1+aq4wAH?sf2O^P1u!@aTy7VDsDoX!0->Dkiv0<{ zY3#kc3QsUQ9Dymy|>Ummau;en33J5cv(frdhRXMnxmiG~boX7dC-P+^3e0z*16%P81d1$u>YQ5!BYv|+80|wV(G{{*l+yS zw_jA>VElZL`p&OFMnDU9xcjR+Ek8h2b^Xuq@KeWx1z_G=mP(8&6;EcI*|+5k3Srx@ z*t${m(R!y1Y;61=UYIeI^t?RrvG*SO%f}G!z|MrWuM#*g~ntBl*@?Ac@stH%x2+v%e4o0}L~`-pP_a#cUXNFQyuRe4HyhyudQac?6(9)uOML}!lE-7Gr?Yq3vDuO8 zn?w$6KmDyOUT?$_QzV1pKaWP`usadaF=f@IrnKaRg8S!QrXfQP-moQZQ`81`_vEJ+ z*nYeLblDVcY-&-XgM-%?7*z&)tYp3#HGBE)G_t}q*pS=2^^rFFU*_jva17{mr zK0b3^E!nJ1Fw~qIS5?RSMo+6H z^{@J2FqD2oclJ{IP1U>UTZJ2(8QZy3`d#mz_@{VyO{KJ!pP&CRre6H&`Z7VriD3ptYJo>5WIyBUydCxu|9@!v z?|82J|9u#LX&4zP3L(iZWUquIWmTfGGP05xWt3G$$jVHVLWM{uBcfzxrLwYL85Lz^ zuj_c8pX>YkeXiT}_w~nl-rn!CjMww|829^eKgPo8hWvU~rJct)SeZLbM}0oKuIpMU zln&vKOAKF2x0pt!O0^orRGiXKovoAn{JWdOp|4~&WJ-w3u@bgFry*1d405wrid1JD zhJy|BBapvQbQKTpn_8AN@%+ke6kqu2)Wn5zF*#Jg6%%>c-oVIfrg(iRD|aD@FkD7? zItA*|$xX3N2k33$6`~BEwlw}%WY0M>JXH4U&u$A=)D1hWM*V)+>aphLFF+NT*Zq)r z7*li(jyLyvXNWY>txvz6jd7Xn$u3!PJeTpb+&9(t@m07+$5@3xbhgKP`&D|or`3Fs zi=|m6Q|G)ts{b1Pvq~AiSX}VT^l+WM0+9zlml8PeIXogFK77w>8)XknJ2aG`(eL=F zBQ>4*7ENjS4eRyQGcQ}D8;%I>)8SB6e63NL^AYN#@cyCmXTNHiTsr2L#&pWg@fQ$I zDq;{g#H3BDSo(OSj_#0uHPs9f5EOk^yF?3P+1XDCyi9WVUCk#geQusqZ{O{M`54YY zH5@S{)>GN$qWf}(1D|0qzuWfw?}NHC7c*{*9t}GoxQ#_F zNSE0@)|{9wSrpuB^sYj3sn?HXsaBoi{zqrV`|SH|6Hs1fH^WQ*wdXrc5R$t3kq9m( zAMM$P%4Xw`SXPzfn>!iwKwJI`mWY)Su`F5NPfsICF}iYWNC6i38!AsX>7U-9d}Q)I zLxah>u&gF^R<3z#)+4X?0xClS4~ca!H8dc&Y@#CTeQ)8-y6=gwvqsaHN+R5Z}&uaOlV~QFb zgaYE|$LsSD{lQ8l^oqZx^4OpnM;b@vYBT0aGli9X=OG=Gd zro`31bQ;mmWO8u}{!ucym=e))*h0v6_LN4SvCcdoaC7lqHb9*BquYX zh6qz%HgMI899CqG=4T+XFLR8G8T^MnJO#UkEwDF6j}9!sVh$lGObjxBb7J1V6w!3= zQpMbxZZBc!gaKIOcBMrUgr^DbBwGW@R!gCyA}()H*LagHtRJ_hb$l}!Msde0Cn>2{ zQW96DE8);3hQzLmnOvr6YBb3?9_7Io&tKuhFy_OPZhJ&Uy~%twjTy|<;rbl`bQdGk zc~V?pA*bcJ3hAb#k>6Etib>|zivo5+MFKdArGp7WB-FoqtS%!vrD3A_c#$jmvY|j% zkRjYxQXpezS9AxFysY=%X4CB&!-Z*(bR&^&;IU1b()29Zhfg{37niXo<4vf`31Obj zcc()g3EVNshNdlcwEX6h)}XKwTLW(^a&ZO_AHeA`hKmzaH8hQQ_Twqk`&BRM*Xuuphp{*VM zq0|OyPsbbU4JTBZlO*eYTNC6^2(zV@_Lv3-=lvKpb=JIeDJ|pQ9eGX342p~MZ&{TG zhPuBhvW>v|!thaVuK|ZSh<>QeB`&+6`ykbyK708Tq|QzKo@0*U=UyJ;nhb#R98FZx zi%xVRC(_+A;3`CzHlfNmSuoS9O*1{0vG^>UZ?>=;RRZi$D9q)k?%ww_6Ja2%!2}Pu z)#n*&{8b}H5#=_LQwoAPD4_)6ys7plwp%$!@cMWQ5Lbp+E!~v@49_(LdCWfnXUfUm z$T17%m6n!n^spP5dZ730KYiHM-1B$QFm=-@=cAe(k_MC=b5{!gv8hQLZU>Ez1Pv9k zp1c`8bqZ&DU~DZ?4^RHS71~2pF8XKY-!DB-SG7)=z%xthMbFdoFM?(S5;_upuy7G3 zZYFB8GG^~?v=2TO{yPp4#t=@Cn0?uAppWm~V>4<};_~4#&BK((xUj1-e93nur|Bh? zgqU0D#AJHpD?`mSRBqHUeEp!~170)2yKcKouK_Ax%MG<{(2-u#SQ5Spv8_ePG~u3v z*sCOA<4ZzU4Y}6t?jd%c&d)SIibQlFn1L0txF3ku>^c-ye9qX$Nv!t2NLnhRoKjtT z4aaYV8_gG^&%tA9^W(#m_|QaLglfaB;&u?E9c50dx_?!s+*7uMfaz3<&$-3G9@G;T zpmrcc*Ii{EhZL^;G%pLb;ZYjiKvG7SB(tMD#WX;2;->%x|2$Z_z7bn_^p$FyeCC$a z&kSNm#2pX|CSvq5efmeQ&z#Q zr;KW}ey}-VYsOsObFU?tQr+8QYIXBy)D1YkU3gI1jQ>UF|955c+a1Y-pGfyYys+Wf z`ycC=|HtsEu=9&+we?Xinp2iUcJ+e?2X4MhMCXUci$LZ58b|H8?BN4MoAjOhMNn}x zjhEt+$uDlxEBYyP{D0jrmj%iE{t@-3&!6k5D6pjaOXa^HwCWZ8AB>eRC&@aSl4eFI z;`WA^Zm0Uz{9Xx}IYWYF2B+r+r;hv*YZrV5-( zG$8uhG;Ra+jaHzV!2SXn;PM@$T~F8)!KorUlk? zfBx_1na5~M4=SxMT6TAaG;XSYm@@Ok>siMfVdjAzOV0h<)WVR1xO`v?lgiCAt~;Km za`HaPP%CFZh2}3Fj7dR*=<`0?KVnY3a{4(2J8*``dM%g@EN`fX3)UPG_N+{Pd(}oG zYnShb|I&1VJ@8$FZZ!J89B#rmT*N3?h`cY;x6-uIw=a#YRiviS0a~LbhFs>GkgwCv zCu`dG2$c@%zi9u@$-1NDh#^ySk<>9ebfPgg5=ZMVK4v~-%0VK?Zks>H@6nBE{t@zp zp?d_I%<$7hqXv2h5d|5@QIX_bHz5(xKHVJQ`QHq$>de*I$$OPuTw>;=(Uyk?SJ!^F z@qjX#zH;EI`8hwQ`$!YoHu8$}EAynYbK~v9#As||R^gND-+{6_=&K)iN4#;^Q9VYH zjVTrNiq<;&f!x!L;J7Avn{u2Xe^AIr*je{aL9v@ja(4T(&*$!6edqh}Mu0st3kys7 zSi85`zRpU+zOm``kEQ&R-Z!IQKC+Xd=z%R>y6H*Y;A0|Ss&5vD=+xxCVcMgHM<6#bQC{H<#w$C zigAq$qq0%(%eeP=usA$2vgnFmd~~!w9vP7V6LUR=>c@p#BWriI(H8rq2(Ibn+*EFQ z*1R{{%TFbwW8ixnr>qAi)%R9&E59yGPag#>(^@hkp0c*uq49}W|AX<0FIhTCM^bMsCUj@<=xS8Ys+S6}~k_#R{#xHSw}9e;Y3=K4Dd>O%Irp?Az&SJjp<4pCROt&ipgk4J>C zr%lgmGfU0Y%`p&{lb5%)qx7;DOlnfwP!C&dQQUs=Lbg7QztjDKtbtibiE4o@2R3R*z}0Jy*l#uuV_+NLZ@q--iz}hLmNx^jm*9#h1TlnJ3A&iMTD}*e@22Z@)c^? z_U*D1k+=Cs*-w-vZOZ+3qMQPJ#SyEtWA9!k7w`7VQa=YOE6=5;yDHeX@Htq6w<7cI z=+>VSADbe=qYtY$a^789<$*Ixl~+W**GX3<-~Ng5@fUKE{ax-CP(3i?%)@!GXReVRuMz9JlZL8JFsw1AesPayF{F$MuQAr6X)HBpgf-1-z3RdSXLeSl;WtbNj&ZM zzEYpMY#CxMiaA%6jKyzCKX27dP9}+5@!3&+#W0U~@GwKCH?dql^vjpnOYq0p^qBq4 zzP%zu_CP?Vk?R_QvV^$L2=;T$4qSnSJdsEheUY7$Q)HZwd^f%{wAssuu3x2X6qJ}e3q%L z^+XU!b|`Bl=d`!Tb(juxv#%)b%QYJf`q4QI2SBF1ObhY@wK}=Uo%-P(L#|?a58x)R>VI(+ZeKb2pUB9QVttCz@_F zEkZZ)DzH#3akQzY<{7=oZX2sU?2S7Jshn`ZQIWX$N}QZy;_EtwUln#KLfeF=n7UIK zqxUL#%i+MVoGJk*xal3xI>J%RN>%G4WU^{S1#0o$;RcZx-YgfL31EPYr6+xp&`wxzWd*|bJu zz36!GYw!hM8;Pigvp)GbhtkCG!1U1QXfU*OC)lRh zwANdEYnFM2`*@Oi2f0!MKAcEtYD@ZeiJ&+`*y&)@c+T(jExGa7A3e38CKPX9`=>vCjCctDKX z9I9$U`Fq}{1AzrkSDHlflxIun`P1T8bYP?{lw$k9{*bcm@SB_QgJaLh`$SXCJ0H0n z?b=+a>qjUaMPkjgX4XQoEJv$pn3xlVn79hjbzgD$sIKk3-duQdPOG-B+)Gb}V? zC(GtF?ASzjr_8>N?hUvRDMzelh^Y8EIR0(F0THt-E%A4tK8Pi0VO6T!>V$-HKv zubBBF!%3oU{Q2|~Q*u&f(gm^miK}9KYiH?+_J{SQCyZXW*{e88bN2g^4<(8;hE9sj zl)cB#s&eu3EjD3jrQT|qnsNh%%RD?Ehd#mEL%ctV z2Ydst+k&0i^US!El$2EcP|Pr+-e)}3KP*bFkt}0BeAH)YhH$Fo6W~;R)pF2aVNw}@ zvlT~0+TaQFx!W_jL;&_+NZaVZyyPdJT?a%= z|AcwIIjCgcdCEXwNy7*Zq``D-w?X?PhNxBc@_HkYwK!z zWp~}ex#a-?-SJ561yW(-2+DKOIAdfZih_U=1hz39j@uf5lDT^oUNGkgC}ZITtd$C3 z`bS0LKG>=s{JN~HEW_qm=@$y`0ZSc~8(3%Rg#k_ZqiV`fMe}JTNU`$qO%Kfb=;?=P z0+p#aGR1zzd*kWrhlq;Vv7VYZ5gHy|4H5=7$H5YG&svA{=jJu$(<;~QD~z&z>|}b% z5|1uhMo5;wf6Uf8&D_V^*a$f*Sc8J>DzLN*#@GXHim-jBMlj-gb0_+%wG-$Z&%n%h z$l0EMsNk71vt)jSV}E(~~c_R-oAC8fbzb3T zz>@RXi*Drhm+E=61-AuG+}Gq^0(Lh&(Je{v;dgJ%pxt#~3yG*_Fhu0-Ieh4#g>z9? zF~wUqpL~@=7oIqi-k*y_l;l#WGyEYiH4@?#IO(~SN9pUX?Xbrp`b`fAP#qG?z&2{~IQ3fc?_~|Wgai{hT3SiD(;WgA ze&^tNBhlrh<6zs-=*`&Z{Yp=&E-YVzUT27>6;%;GyBqw6%7zu9q9VWKnK@}eQF#Z3 zV#HK&+**kuT^!v)9WFoKP;q~!i!+E}#D>TpuH(v85)gO)dbU@#_P5F1*k_(q+L%`z z-R+4FLTIq(pR6zCuS;hd<<{a`MpT&D87juOVT>%NcIFHt9tYvL-_ViuCH06G6055y z&i$&nh3g9O7J;OFm1o{$tUqtNYbfjNo!CvA8yWvG-Z=Q*2ULq8~NTFx59rM&x82_fbJqb-1a7f`;K0O9A# z;cZ<Zd(3J(z^CjVsFsbfU2(&wUM3C=M zNnH$E`<&#nIF10%&+ZUj@tf5Z^YKYLqqhuf+Vkk*rC-%2IR;oBDthfi70T}%?&@uj zrJMH0_{YEHOR6uK8}aO-k+ld0A0g)d>F%T<@)KpR;>~$&R{5C!q42_D^mu07_Me}a zbB1GIHcT}BIqK*4c*DchwID5dry}DEziDR&sZW4Fgdas;h+Oo%^lR8*8EeFMW6eXX zHZb(z=1rT2EEnW$7-c+ zGZi|ig(y|zhAN-PwO~!|qRAHiON;4;b+j-U13Gv<@PK}eXIH>fI8!IAvv*K7967Wo z!l*267LNp0KfB*kY&gD{dgpV;65XeM1A2i38i9n+g_n7%>D6m0&kB6bU{AlQ)YQ?b zfe$rdAoJ_)dJjLM^kGId|y{X~ZT?qnl1O1+xO%%-V=z&0On;)`dby?r+SE}9Q|DsF)mE`Te zEZOEi5;|#AI!vOb2AttfQdPcr*-ay)IP3m>R%TI?u9tjV5Gqas*}H}d6APB$9G3mT zgm0TH2Q=zI>zJ?H!c9Tsn)S(7R=MrG+B^nYH-?cK^CmH=hbXD2Xs1a&PZ-8ufM?o7B-w;?lpS}D2RIOhjq9^%en^k^aO$)#_btBgXdS^agc|HXJ~ks_vX<4 zmrq})0VC4|rp+xGFrc9y)>%%*5R9_+zF7H{>X02($CVzJ8T4UVF#Nu1`yR!wzrlb| zl}TNEw_GhCux$8Nb6oiiihhZ!bR?5^H_j~{(H=@-N?FXPho}hPI}&tv9ypiHo1`*A zco^b9c2g^57`(1Bjk;BJN=`&X$6$d-VS?dbvV7|u%`oS{lp{BHgM!QWTXZ%&2Bi9i z4+va?+J%Bxj|IFGea9>0NQ49ClV?vCRS5FB*7yBR0qa|w$QnM?NohczSVSwH>m&y| z^}W>)b%$^zaPWJn+n7}+tua+be2U-{Qvv`M$jgx-ou!*j zNxI0N=$qh&rdJ%S0%oDJYOXx>Jw3+t2B6wxT3x%9TkmrL$lH*bIK;Jb+=%#9!EoE_&OTDJJgRKx2UDF z>-*mSj$UhOYKFnPOVZr%h{K7W?Jp9KcEoY`>K(un@Ni>(J1Gr*INcs8YD_CoQSEZt zB$%u0%&^@XV3=YfOBl2%RdB8&d^;POANu=~h~!{Q=&E!u`mlU8@PkjDFp|ZBO`i>& zTHC|YDc_Yh{A_?ASv zG3LIKMgbl$;gOMd4~yCiu5T@rDnxppMJk`H{%Q;7$Xt+4dxGrwz0Esu(G@i8$9Qo^ z%yyI*Kr$kM+R~1tmaErH5oU-u#=(&h>wmJ+?kTUHSyAnK_Xu$_ zs`!Z0#Oob^p5Kr8QL%Lci)A(2Z_I^nCnhF#)6~}(en8$<)zUXKD8x_@vJUOGlSkK+ zL0dZ4l{#%73e!N=l-B7T+~l*i%bo>Tdmg+#0*_uvprQqtPV(m7`V$IMvjZ4CXxP=@ z>e=K!aXoag-Xu;}k(oq4he)dC&foQumeRx-R-U1s=d@8}qV&{V%a|YAp7gqlbgFCNqWJX9$nnJoA0oH=Xh-C+ZKqvq?1Y; z0_baleP<=uL1PMujFe%I+nqXr&WXuilo~@9;>c{6@o;yq#qH$io7U3FUD_m|QW5EN z@+39zw`D>-_kCTuBh$wjc zb-4B~R2;7QyF1cRtQAt&VT=Oktm(0Oxze;!N3Zzt-nvz?ma?S2(AwOC>{MFS{F|8si{UgBm&+Lk0x1zM36d#BkE;2XYT(cng7FdvU%QW;p8EM>Q8@Vfs!@nGUS z0T-);@<)zPH-$=x>1I>2)Uk%x6Rdv#Dn4{GrYoSrN6U8IykX) z%e4jtvVAhfJx|O(-1b~wE$yCVnx3p%uq7}2w3V(yiLDm8lYgwAywVPMQfn{E>8g6I z8xIb*4)NXbB$+7hYaTaij29~y9^KziO0ZmNF(f7E3v`Ef+(ld|G(THZJY?PA1B=gG zaDX;)yE9qf;;4<(?%C<-4|vUioI<2by`5rDhX?MTopCP7x|YOG9^AL@$kibhZR~&@ z5*A+P1G0u5Lm{PhYtz=Lzh#r;bssxBMi{IL@`N3til*d>Pg%V1;(YTyrew2{aVm+h z-6Gi~M~V`jbv#~+@_T}RxOGHQm(u;(t|54W``80B;YRv}5tf(URXj>dcYMzX3khi~ z=E4sw44pD&EDcBFI>p`e?18EIh*P8uwP2%~H@SfvFWcPas_Z1u|iGFU+t)bkW!F>m#e%VT%DSaTSb>FO-pO=?a zd)1cS*Rr~oZb8wdYJ7<1*6!;EUXhtfa-HdA<0%7pDii*A^yjUTKO00RYH4c5cvs?C zk#V0?ZM~J1vz2fBQOv^myr&20-GCT%kC;}jMtxAKgEe_;s`3dykopJO$0w%-CIfe+ zl78zqXyo|1DCy*W5MRhYU$~_m+yAFT)$oeI#50|iNaG}HK!cYF4cq)rbKBCxb?=kz77~WzuUUc zDn3>2sw%Y-`^qCwbS0=+;Uh=eEdiVls5E1Ou;KpF{63r<0Z*DSDi`k>!!e^l{f(N% zS;DcX1BX`IiwlRk$Nnu)X)YXwdbJAW!e=;44^`6 zaU}b4Z@$~CA_BNOV^?z-DZ#852F#|L$9-+>dbHTcRD+Rh%cI?SI?aK z_bU4;YNhg3d|00AG)pJA<3^A8a{7vsc$5a(-)qbiV~JuX^S}iK+?#uG`M=<>W5uiU zL^UXIFw4RJ{53iki!0zp{`@yK7b=!xqH$?K)&? zQld_IEjI72mWK<(Y_(XTyt16t|Dvcc_a)rVP+U7FpH{p=hc9!EgZnSVckK*^#Z7&i z)>X~lkLUQ=OJ*!xzOY7fdTnhjsA&qLH=6X2VW&-9F@=SNZMt`Br-ZFYlp5L~Jh*sx zoOqlk&=oeSzJM9tZlr3(+^2k8*8gL`P|<8R$)W2NMX}oK4b~MUe+&20#oFzaN`mLI z;~K0(Tfxp{B3!u}1m!W@j1#?R(>h)Rd8c;2(7x z%_^HB#C+E+O7i(U6Wlj8*0wkFFWsAX$W`-?pR1eg4K4ofRe=UJU*C0V8X6*ULkt`M zMZfjlQKs5o+dr^CX-ZUBgzi%1LLm__L;AXDA!>+I@wVpH%=V;$wU*NcOU+w+nZU(r zU{GPXFXuJGiJ#6V4Mm-JXXUS|*Ei%oEmaQ{WLn+gDAl&# z8*TdbpRGmTSBYTMUc@K=8o=~+-Y63Q^wdIU?sI~lom6^{Y3jje%q6POni56yeV^^A z`$7_x?xmw>!vX+6@__#2EbV5c9o@hi&wy)xe3rG|NAz)7sWG_W=@h?k1ruzCaB(c1 znrk|?@8))UaxR*_y_O}$Y+RW`*{aWcnoRqXzXG}4`!y4Ug-Y#+F-JhMw&J1*z8@WU z-uPvzAQKZee@`MXTI!QiJ9n#&zS2(4tN-@R|=Jeai>95sKccVzO8ac~~KzI&ZgU-kS70-HVq@U$@F6}6p3FJX!1o4|q{ zmgGp3b)T}tJvG-Am%v&z_DDPB{o{7nT z!-MDC?|3Wyzqply9+1hxwoh%E?sMlJ8$w4 zcsSw=QAaH%;w0H1|KGXSDbjgkvnhu)!RQ`F0%6f6wKAS;r;K zp&8*s??U(zW1yKl(m)BCqw~K~)aJ<#9pw+NLZc38)QgPTy=DtW#ZHd@7KT|wS9$3@ zefGEE(r>U&mT?T*e?08ur|TgR9P&HSi4;yPM5WQ)!3ZRP*3uXpRT&fsm(FOJk?qFfz|oW3!BiOlUoQ2z;QI$U35 zEbxNvFUxFSR`(NmxD8(-#I%BfgSRC)q#tKf5ID6O_kF0v0DDI-I$b@+kw&E3#JjYoZIcKGw-2M^uNF<27ciYs=gB4)h})H}8K%@wbb zw}1jnTgwWcH4&P9V20pwfVaMydjUA;2{a!BP5T4#Xi-wtqTt};v5g}itNsBmYmAyf zI#hK%+UnekbV6zfLu>J?Q`C1}E((olbMVC;^KfNq{LXCvx za~i_*_wDVwKxRZSMzi~nslyowExUc~x{EbNkno^}2nTMK9vd^#oZLBh$q^%9;!E#v z`G!E8r|Be&GOXdUxoYltOR!Bv>R-JZE#5>l1-7y`k!>gSgARBtCICtnnx zEWAT7NPKMot$@|ac)J2poj`!8Gh?k$GL9pS&%3m6U3|{e%TSlBOky`^Bp-ngo&vBeNpQT^rTZj!o4VC0r z#pQTz8U{qXjVBB{(XF@KiwPAD;T%VQyV3REia}+LQP$a~E<;sYL208Ro=(w29F+m` z2`B#^qp%~3kvU^bR331x;v#b@norqD-cx3n?=qxH1j1AefB{;<2#8Zz#;G?1H0$?+ zheO9JrI8ws&mMH=4i$l|VdoPs9|X2i#tC9Ek@}|O_tHUmECdQt5G)A|Tbi%BySL1^ zltYzF9E1F3vF+^C^MvchFJ-=4ULGDELuoSb@*ylHr&8qH_I}>_0K(1ygkgR{U}sHr zHT4_^@j4SjyK%@jb;vlZ{ThD|^D+4Nwx>L#E~$MYE$?@n{>;tK-~eHwfv5LkNGF?4 zkWRP?J2zJYZ*=ZBR`O^V zemsjneeS@ZhD?Tt0FqM;tzIH{N%r}>n7XE&W3H0q655GmDS(? z5;ha8O~=ez;BYoYuAyB;V1nweZG&@f{|*gVL_f_r{~{l8FWfIK^mbyi#dAqp4;naLpTgOTp zFcI48p%i`duYuVW#(w;zh55+yRNpW#j`snK|H;HR$D=c}@)|u2O`X76+Xdho@!FRf zT8%5#m+et`@9}?GZU}IO?^Z4}Rczn9y{85#ASuhY_()0M6u2D{>3$*al?YXts$nss zX31LRxC|T;nam%c$Pe`C1|G)8wEWvoR*Csx@hlh?cfdUNMNVD75+w$l(L%_f2!Aw* zm5dvy&0+9~=QKH{;WthkL6cQ5%sF9s@xi%rjen(|;m_fdkzR=NUc~%~;3Eym+JtL3 zeIry`U{t~_jj5;+sRZe`A7gxf&W9hbujj`dArKNeH+vsnP7qmPleE}+hII1j$2ErnF~g!rb8r)uF|$MGkBb zMp~binLEvqs_1ijwr=A+YaQ?7VL=QeD38_Xbic9)5c@#e`gd%0J1I|KFL`du;p@Kx zAqXHGa5X)}YI&LV&8Yl^|3ur{>-NOp12Z*=w*xleLpc1alyo@!f3WD4J8a84?dUKg zt>uOAa<~Rp%|mW^Ng~7mYR>eL%{fTA3L+@7*X3|`hv4X|+I7YI*@6+=yC6t$pbZy? zs!CsCe_&_RmVKpm?{RnR-dLTD!gNCNsqE8gCs498m4D7m(PiK15hgDToU5M#_ZxmJQoWP+? z8P?*Sxvf~kG_pD)SB%3dUx;)Z1IA4pB)}wKce+Ya@z{KC;N;*N6JrclGLPKkn;c2s zOtPa)5?6fRj|!6e>jus-&(%fo-iK1}{Weoz|5=~e>xlM}TpOp}ap29iRdDI{!mGv=CGH0UFIN&7;;qCIm}-KlFPtJRN;~oS+I-1MWt?vQ^@i zU3w^BZ=<#%qoOD6W7E0f3M;a85u6&tI&9jwxZnHREAII5a(X){muJ8K%`dapA`>eaxsq+0AMkOUprqf!oBY!iDB0f%tAeJRj=f~2oO{7AO$V? z|C^xl`I!zoH+2clgMSmM; zNAt!*dg%4;09Da$qEV?Y`9ZO%bWgP_llu4A(xm4jD8dP6BlSYsnnjus%!Y}H9pZ1T zuEbGg-0GauUZN4<`r9mURNQ^mB_ea3oECJKaaPfGlFM})Vh}85atSqR^r=scTJBQ~ zQ-mu4wop16P>gTXkrj&IiqM}wFE>Wq*_hWMUKz9u#2}C~RD$oheQK zQz8(55Q(WNYq;1HvJ)^n37r3x$Y6+J2Aa~ikq6OH}?6huU1)o z0zGKwXoU;&qg`uIs(v`3Uyz1j_9svOxneGzoa_^^Xrj&0G+H-;Itj3*$xe?m zt`AXy@-?`UF4_rCXH?ay8yIQAtr^5+f{zAZ?vPhxRL1^N7C0#!o3`Nb2&WUzKX=~d zR41n3pgY9?m-AWBZ79I-l~x6AEcsP-u&&IL~BX$4!1NWrY( zCbIjaVllfyD(Gb-V%smThUrRaN+5Y6}{TSmxNpTq9}imG$eS_l=EA z_Q!9H?(HkHM7GC$Wj`fC$lstnJvX=Ob|aY^wyD84ZJ&eQmxe8WU&e8djI$T#&uug` z;z`qNtgM-r)o!l`{X=qwjV`^NxbLw+NSomJ%Y0>+-TqElnbiBC(S)+!VKxGfbl)Kw zlJu4?33sC^EXmNT!R8@T?>PhM*TheVAF42)Bg8`-#@QG07*vc>qs;s_y?wF@?r9fX zRfz)%Brbd@^{pgOxLLpHxnnK>Bq~U|9aq_qq^`~~0kPwhZ=I=*1$hHrV#kX*GDJ*D z#9-vf%|6&yXuG}0sV@xY-X;o{M!Nxbo!xU%J4N>(ONhgQ;o;ZLo&AGX zAH6#QER4Vuh&o?K{Xly*pYm17x(lEDvF~GN`3(Xl@8Eum1P6hP@{z?!b4pi{=R^dv zsq!RMUqmbD1X>b3#L>B>3pRD3_+ck+e0~&ep zm=S}5sKU0^NGAXtq`u`QX9YSo3EsLyF7A#WE3qgCyRWaabES56Kd*Iim}+yvMyq%A zI&MFe?9`#{?x3~(?*P+!CEC!c{ODA-onE)p$^7NYs63rrM4zR4OIm+o{s$ixSPY-Q zk0bnyYGmCp%oYt7q`1~uO>U#qJi%~K7vKQy3T|9ws4&%025>x3Q=7P>>CSkEoFrQ- zkpa;|K7LBsx4WMtF7#OSg%GV3ki;5bVuS^v#r{1`XoF-nv~RM3T4!ZY@XqV)Xi82z zwnCD!;CG8P-KuYsybCG0cPiZA3iBTn8#xF7!gFRg)a48Bqd!Sd{AUh(AcZzabLDOS z2<9k38-6DyFFka-etlOkpDTGn%-+hnkEv1?ab48^o>^IKYZWmnr#3xFWz!UWF<6*q zJh5a8v>%n}2Re4_{mUP$vR<6ioGkOC-8N71s~1fWK(pg-ijG`-7_|P*V?#-bd#{VT zc|=uj6T<32eL^gb3V;moclfLxctaAXZZCO4UHua3u&QXQ($K$<{zRZOqGj3<=7XfW znB^kH!rGdet@uEy=l=oa6@y@x5P^VXs)p}GY;Bw`#xigBC(qwaAfmFKv|axEL`Uw6 z)Z6QxBiZLpcJBF08_d8N2C_ps7AA7lC_o1N40|!~D$8%oZG#M}8a;p*w9hF@K6}9A zBw{vr-`t@%Ie(orn9&?(RWPHgSoG{{79%b)c=`-J^yt5Zs;D-KD+;$0VK%g})a?5Vh<=%&EKb8I?<%6JAkEr3Ubj<*|8GQfiPV|xva(5#ujLrhVK z)u|0I zlas`;C0a%ktSW(B??-oEs*wDxgOH@*_SFb3Wt@2?elC3pD8Igx3Be)js`RV;@YxGo z+3vHPY63_F6}&*S1sb2*2n{tY-+mJHT-VCKUjcx8?|UDz)Mouc?9y~uo=|;N)lEvk zKef)lqg}h?G_HfK}`lAe(&AY z_pBcQ6@}Z39`Rme#6Ya8!1}J0tMuMpZB0#8yufDuN7plu7C}A!8~(RF(M?=BVi}yU?g)u7IS8zoteUu?auM8#!+(*Lkggh<~YdfQd*MIB7V{8i`xI#hFhkzb%YCtn% zfw%WN;Y4cBuUMh97 zxB#{JcPrJ*-av6m$gc6{7O0MPJ2Poc3qMDKfb6pjwn{AL#@*aZ-y0fe2-=j9Z9JnY zHFq%XryyuTSTHrcFJZmy)I)_J0Ff5M_4m*k>$uBCb$v+QeW9tH;uokI1PF%dVSv~% zM}j?wwA#CynLzL)!fc6UjB*tFgB!I3QbArf9Dko&L}pYCk%o)lEwD`XqU&uwyZZ}# zRbk|@W^%4$)G_|iqev{}##6kQg6&Y_X`SSV6L>y=u z1K-_9{xzu=Zk=Gr77#g@+`*++srPb`e$Ka8s>J4Ia^Xyw;Ww@`2hD7S4Jw<82Gqx| zBvfqoIy_waBRk-GPURY@e%>ebox}InH0YWd-|z@d+z@UKo^y0sSrGU9IzV#R@WH{= z-L_uC`Kij%ShkO;)OsU+bC>0UQd(j zXOc&XzE1QnbZde6?{U~n*T^?J@v zB>kzR#%9O1`0rom1x<5ULC_1pQAdna(CByf@v1U6#8do&K@>N9(IKKcfo6*C5wed& zNJJ))P2wE%6G!1Vg>6VFnNU_%mX3kR=gU%yz^NU+dj6m9PW%Wjt&vau|A0(wCS@Y1 zW@2{r>bk-Sk-PuDUjct9~`r;bIVE z(AZn0F3is-l(*Y1aSyyVfTQUZBkA_ABhj);|#2diaLnGa#(J z6+S(e-#-|3(WWh5oP1V2n(cq!0)wchr5L56*URy!Y+WU>f~8T8?$_)0mmj*;IA<=D ziOX64pB?bZ{9(TNnnaH;d+rOvWu@Ui(dSy*6k3>1Ev)2ZMp%6aQ*s z?^}_IHvZ@3`?WtbJySDUuy%pLJkKZj<9w1$i`Jj{QfIkm?b~{odi0qN2z-CU+z>IQ z%i9Qml>Wkn&IzaB=HXBNuk-Q_k@kVtJHLdP5Xm1d_Qw*pl5ou?_$UesH~q9N7b3ob z95H&;V(`j0$+0q`qg~i1)r73`{khFu2gR30eIEF9z3vbCRw_~slocN%*0r5a zVdsMQf_%rkwfmb5ox{(pkDqVRa%?mEpLN~*XXW=q*YoTY(~XzRAB+9_m+$x09lLb+ z+47h6_H&u!DgJsaFBDLlNthAwt|*ZKs(-?Kh zUHgBnABtauYYcpA2tI?TNs0N?DhL?xQ)){K>b91KhZz`8ayR}75~6A^QCQ(%%5*z6 zmI)IKo8(%i|Fx{^jSpaL_4W3O;Tk8wB4so0ntY@^B9M zvjsUgxt?H4p~_Ri#c&UNdBp`V%85P@SAwd6!TpDoyZ+bK3@q<X7{qZLb zIID)3Oc-qc9jJ@~tj~>kGCp>6`8xI}%$yV+DfwJ^m4h;XJlqgV*j;KSibnk3_W%K} z>y~?7#o(<@*X;kX{rU|`C64VODJm+uz>+Z>pdw)Au6#=>C^d;|nqa+wUlK^R4hjq$ z#5Y*{{o+6M3o?FrFjU8gi4bP(K;TZD^Z$BC)ZETgyDY=4J$U~2krPpZ6U!QRW~YoG-31`sOjZ;sj44o8n^-kmL3+=-X@j zxI({z(+M;#KzSz+J_?06QGgOwrRZ=9-*OhI`DB$JA_O*Q5|51z|rM!!WzTEPTU#3?}J z%z)7NVNz31Pcn46=jP2|8DI0~mGp0|k)9#&3X%Yk!nS;Oqz`Bb=74~6V2!=InCC_2 zWYa<)F3AWdKdjD6r27y%Le2lWM1**w4u@BxT( z`SH#u3Ug2uCII$Zp(nA$2}(yAZ%e;Z>B`B9m6qWZ^!znYeBup& zuL(WcVIXSal>S)fcb`6eYWR)eCYD)8qIWXW#tMY0BFhfO=@pU#9^Gtr^VLrM)$%EN zTU#{h`7`rRG+Z!=0W0Ri2+?@aX7h z?dsZEHzt^TlXiHUL}s$q>C=gd6|jM%^p`(;xDbD)^#~^YmN=}f!H}KtgxY!q34E+1 z;@#e@n8e=*ijWxT87EPYNE&KK_03VxR{emTQ4T0Vco$efypwF;JAoIo2OR5oMPJx_ zjgF1gkzQe#+?M7pcziA0GTU#WjrgqJ#M={>AtC#SwK0zVG=U+(@VL)&%a@a+S76l` z{4abbv&asYzue)Dl} z=ArA_b0gSV&7@Z#ec`1^H@KnlZ4qBF{;kES=d%PQ0kx0U-(LBlX;gOkaPh^f1a|m3 zle1{5Xs!=@eFG3Uy3rWc!~MTiJ!)^RV2B~qIh6kN9CBwo5h7HGLrIe401fH?{LJ_{ zYoL2w$XDnB*4d5w!BtJ=!T;W&bKT@UJ>O^bPdMITj`rOQ%H3^>w`EJ@U5G^{99j5#o$1WX-K;y{^@XQXOFh@>m*uY0tOUwvuNd}<5(LDnFo_+IsXm_4? zdg0=;CUt=db6NMW!>#2y-#;!=zqO8`9ze&;N>xs!Fee7}@TOIX zX)O9Aga-}9cRlG9$TO_vM8GN?=>;JF<9}Wi%jQr1i{Mi3ci1-p#F~!O7%T99QTOKI zT<+`J@GXThr$K~h5)lz4nG+3417#jknI$B1=8_1dl9DEch>&>-NrfVrGL|xvDdTf~ zSZhD~cf9W(@AJp|$Fq-h?7h}r`MSUN{kgBlb}~dZgt}Wb`Y}1D%iaUf1qO7x{FoFO%me^KWSdTs92_zK&9G3>r}aeo3%e!-K;F@>%~(nQvX#FjkZShecuuTf~(u=Sjxh=6LSE3mPoaSL8~e`o;sA(X01 zGSq%8d!W(gK#lyEC3%$lMIfd|%UqCfD%H1T{j=D-@ckMZrO=NIk~W*T{p7l&m8;9z zFURZRVUHtj3Q@BuCbNY9LdVK`4V@JKo;}U6QacxoMw=r$EFq|~=9|C|&&i>XJV$}f zjHb6dCZy}b9UstaUfZ@9Xtv5^A2IC4E@*!6s1MAPu60n+h8etWXb`cklU&zrl6nWrnBEsug>BQHgSfJNn>f!b3FPLF6D zHfv$R3kKrK1wUWQVF@$pZ zJEk3Xu`irF9j7v!JdXL3mwr9I-tS|O7vOjfE~MoB7HThbGp^GZ@wp9Nt{xx0BM;|# zlVJ%saKvLa@Ft5J?Rxx7KhmsOMaQ*M`?6mXRxM4#wq#FS+5>eaP|K#!G zlQq1q5A!74^9Zes?$;T&%HMI@n5XQ4e}1T_IPm#8$cL}cog?w?dz9za9Er>+*`R7L zw}Og-iOb%;SCI%|or2Hxb-_KR^XhBKHyryrUsfdmdN^ehvge2)SL7E})s=jNee~KF$xe?gpuy-~XP%9*)#p^D} zJd4ST_&Z1kmtn&y~^^d1(2FKRr=yKvZCP)m$K}W=%cTP+v>*Z*!a*jSR zZu~n18xeD{91u@<(9Sp2ti{rvrNvxnS-zNO< z)0PK!e~xOJ4*?>3`0Q8A0|=jTdasq76nHlBB(3h-^*n>unF)`z!+Y5|IEv9CqkKZA zE0L&WId~UYO^n(K4-Ka^7-KO}sN%&YcY!5<9w9$pJ{$g;wSBO5*+pAG*le-&W6ie?{=KY_mrCdu|OC|gkgh>#LfLBx3(GDI5s9K&Y0$TMx?LL)->cA z+sK#l=hNO;bpHJxn_tTGwvc!g`TI&qy?gM}Grhh#O;iS@?`gH2C zZ>~O^#z=m4;!$l&$=A4ThioGePK`l)FhM@LPB9S`1D@Tv>sp8W-a_*1I*W`Dl&&&#AIoeaV~2 zx9_CiuyGD8{_Q6NrXt20HF^hoI%u zNn|LIx#dy{?$6wIqj_9K8)~|(A#Pjn_z!S@DyU@4&Falq)@*;rhX{dA24R+*a z7!-Y|tCMc*9?lnWe|A`X9jTp}ysA|gJUV`%(oUZ5qwl!VekM)0=x$T-Tkp)>&66yx zbl0+OS$Bxj^cotaSID>w!pbf#S><)omS3;Y7~uhlJKJF4Im%@@_pHJT_^QUFmHqfJ z7)Z>v1J<8Dd^n9zYf60sVPQqSEx-yK14x|;FOVlnoy0_l`F^(RxM&52Nwfvn79D!? zsqVGOePzezzGd-@DWMTDH~i_V&is79A)%gyv?NqA1XzL|A^3vboIMptv)gf+k)BT- zJsMUxwNMx%mg`RMNx3?yv9H{Tn2FJOU2<{xd7E3&q$X0PbTZ8F7Y?X~(;c_rSWK8s zj_~weo;+H1x1+;%DF<3FjD1Q5UOmY`IkaV|FJcGY1gxz$CK|J@*hi19y3{mDX6x$-c$JLYek3B^FpIB7+SauFoi1btw)_GL0bSg?i&g;B$-ul z0}c`W%BW<~f}o8MoISYj;e20-X73DjJ0ye^bi|0xOb9C_D4e~z*4?|gMxR{I>^mxI zgsoa!KfAl%gV}cyy6<+WZ|@tw;!UP2lACCyMW4qdsW#u|K`IB{akk?+$F0Ib97*qS^3-3J&BxgB5|V0 zB_9U@yH#VJdv@m@DF0Q!o@L|a4#MGn1S^7_un#5ldz755$nRiqBm*Tt512p*W!I38 zsH#IFPAK=F4nDBSzZq^C_|dbPx+&iO-Teobckf8wQKlSV9U#Seo+G>U;*$?Eh&e9{ zU9}TV7GkMk5hge!nG*QNY#>&Ali>>eIYSJVHV*6p`rd=yD;}t{(4e2#oPav%D`Ggg=wYh@H7AKtVL0IjmOCn^6738v_@ zx9;3|+b71TK0=IaSDdB?tAbh~{C)_WU@_n^E4oQ1aC3VVeTP_Ezwj0Bl383oxZ zR3s_*r4`*y-Z$GQcD1!Z}kf!SP&t!GWg@am*xo{eH>Ws#F>5H z6|{VW8E!^KMsf`apwL@E%uug2uFFS0s<$C6{zU$bJ(?|d2D0bNo6X}-#M9rDrjN{n zXWJhy{cntYr^?v%@{RuuvJZca@%kSyJX^s3J^zPYc|YC4urEv(GK2CKJW68HLZ;(j zs=v0XUykGL9mE)YW?^n4u&;#Ew7!{PVLo9`%NyF?kS0tifD()m26-WIfC@R4vF3=p z-&p3fr{Y1zKal|sz6^;fB}aDQ$5XhU*ev{ZlZR0BamhL%P2u&=y zXI0oXwD$L3M~n0bpwAn0%6syK7Org7qcEO@U;Zr=?<7WD{)mH15ayJShJCwS#(J;7 zcZQ5HKRiL?EvV|w<9mZIsCjgj*j5zH!Qo~l8E&bn z(vfARz#%%&?t$*U5%JOwd5!~}spGft%?sao1>9`rZ?K6eY2!Tp{#dOCPpaVwG$2&~ zXPjMK8AD*Z7YhvV8-@i4ashznDT;_HFu?}iv=)s{N;ntd?`kXsAP!t(11%wVE0g>}&|xr=p_fT}`#3Sp->@qMZ7h z%&WVI2`ZRIw^4s>;AE!oD-=~!78CsVq zM|oi!(I#HX3BcYTSI0ne&K;W6RqiYgUW;Lmx~A*Gb(Guu=Qx!vWG`SazGaXY_!5#F z97l-rCK0ArRUJVKi-wsNQAw4ywiJ{(yjBnUUyDmV=byib@-5J-&VOzoGqi@L1K)Eg zFH)q2*h0`lB|8t9ygpa0G<+YJ-pSEZa0_{hs&oj)vY@iE^8KoXC)>XB-_PThS^>!l zj@I#bEoP*<<-A=SYgY*!f7iMPZ_zFNTTxNm)H>|%82gKX5DkT)-Hn@@{`nNK|9mkz zlCyBAh!7II?@~~JHdM~L4|!oxqx#Xi9bj|8W5~aEJ01}$MV8+5{y{|omM(FxOeRJW zT_CDm0d~U=V)Kgum4$Da`26-0zwU)QdFcTBpeY69C>jdZ2ax)aY!B?4u9OpU@+*iP zMB*1V4if`fqVa_T2%%K~XJ`f4Yo}8$CvGlry&zs>x8v9-IAIeZJ5>B@@x%L2CbYsg z5A*OCyc(1jI{7vX)%IaJfqKFZ%`odVaaaxVK~<}W*NG6M;71WKt-A2?-d6hegsf<% zG5-W6B^I?1${2a?pr(X}PjSg!j~BOaxQR93PRH&QKs1iPDtn}-julJE`~li9sL>YS zWQhO6ki^#ntb-&-wvY_o#bYr+jss&)9;g-ZK_FlVfCCjwLP#y+lfLkH!g&5YDhqdy z|1E#q|D5>#-;`?p?@5aPzh3Dj%N$@8N1h)iY9%LWzuV^_X+ey@U*Mu09@_z#FWKo7 zy3J4yltqOnM+p<1o&rd8(%I9AtU?E4U(C2@ae(_7C(pC<@oeKi2PM0MX1V2TZsO%) zygYZAORo&o$p}qsw0SV`0NoAI{bCLn3n@D>H3n zM1NGSIqZe%*xpCL%?VF;AT;X%@Dp#xwK5J22&fUV128|yk(Y*%qC^%S3e1gwsTam4 z8>YC$uv6?pY1b<%Bcn4mgl*+{&^CZCq+!YHB+;AB3k0O%>sqLB9A96A46HIttXwR*)J9 zTLg7M^vA@sV8WY5rdNHCPl$Rk7`oFHo}-8zV)=NNB)GG%QkceR7=pDK>6>r`$<=0% z9tvu^&!38%=(yAL^Ev0$7W6ydy|-bI`4$Md7={g0R@h=px<35;`a=T`*-tBeFA!xr z>}|qhS7lJ8RRARtx`TPfD+=rSZ%518?zF?krM_wTLUplpq0ZQ#-MS;_(x(2|nW^f8 zQxYJ_6@B?aDv}bIcL!kv7Zj-vEk6!8zn#Sg+zXsv5~Rs;pZkqDmn2BB1SNJ6xON!G z2%bvW0P!RIE`@6!@0@^=IwHgnjvm`4mYG6c;Gmo>Q47YUrVzPKAwWR}} z3G0pBAc@kT-h_DJA-X|AfhIPz<(9#ty9ShJxj})^JbK`c{L9nc^1-S;cum_e2v@}$ zM_m0;Ke$Z~mBCeMJ*VlW7P7(pIBaF%8pN<{*}QK7iprAS0i$hC^HOJJi0>pR(E;&X zChVINFMM&^)xQJJ;FW|N0y97Y9iS>8Vy)CZtTkE$B<3>2dBy1G@hpigJmw8PkOy$O zw_s#a1&#%2)^S5{FjYaoA;7>vXg^7T-8+At)IPnPPe>`{P}i@ao_0nMxD4!(qmLPg zi{3of;-9{-$R1bq7LzP<&E4?!00@Kv^VXd^<|B^4l>o6}0xftc2V?c3E?Og85t+6O z&(DV}IOg*9w^SGs>t~-Xd!;GVhwF1ZP}rqZ2%){J9&gEYt^3t=rA3}Nv^1k{ISbG* z0mx^OCAK6NQORHdaJtbM&H@-dg^q-i`q|dT|2Gk@dOb>8vqoolqTB*Cr!5Wj+K0+Y z0HdT&6%yL-+>fEb8Bd8<7`}ljfcI-vZb)j$*oBT#W-S03ZD{~k|DuEc`Usks@;qq? ziEuLNAJ)OcFSqT1Vc?~ZS8`Y{Ayg41y^RP)ZiQpY(AI)FW{v4JEWLf#ISZ=Z-P$-! z<G$=DObE#)nMPx`cIo|Fd=S1Wj z&Y2nXv~Lt4F~Xj2q1IM=iCF1F28bw~Ad({@Q$Gl(x1LH_jC(YHg2sBytGA;(Wx|UI z!GkFZ@028U2UAB@>|+CJN@Tywp%GqF9}e@5tx-%7Ngd+V6=-n^_~p6^(P;$PIy8+4vnlr zBq-q3dc&j~UMEWvwS+H)=p?EFs2%Do=K!K*gug|npz7u(3+3ijP=;dg%e6c+K&zsq z2;UsA1=&LI;>XGC8RBsYDg^*)gyP>uB zzmN>#IXA^e$HeJ{mY7hEao39`cVWPE@PQl;xblT09)sW^fPV+LhJ6wgJmbKS5b2X+ z;I|Qu{=~#YD=_(Da7J$6_9Dd+6JkF15q5AQucH3l0CB^>0d;l4OIuRPNmL_ES&odr zm+?6STl{o?PY^bwaUmp~{}3|Y0!+Q{58O%4*mr}kr`0<0w<^M6b?`34aHpI<0$dZZ zV5~$}^6F9VAKb#T6oHX3V`t-i0Adhr!Cgsnlj^QJ>u2*2b;FpBWO`1gP1fJOe(u5N z-fm=G2`T|TSSc0nE&L^^vER=g+_mxvWBQFyI?r3{f(BeL=O;32SQo19^{1J!An|Q$ zE}m+F!|%^pXVna|or3>PPA(f@WnwywzPA@Q*(`r%jN29FMCIdW9jDevS#i=9kn`{q z!)kd`oT#EnA&-yfIPQQ6LtBvd_a}ErOa1W&5~Ux`tc!ZT$^c=IwtzTf)ZZ_`J*faA z2|BXy1XeQhPI*6?I+KZ^=-stxMq6~xsL>i>D8AiU4nbt4h|Y26k9bm;qQQ>qXI~%Y zYG@AB?gw4DVhl#!}+r7!jXke!9H5t zw6vhSd{FW!APX|6rHl|3R*C!p4S}LamyDK|t}+QyaSFR~XrLf!8$wG&WhG#|V_JPy zXcT4~Fl7JGDQ~*ATJ8zvLlgy9f!B>^LDSq`Q9-t3SZz9kiJK-?c2fl?*EUvKL%dHN z0s2#us9V>Xi*z!9{ppD|$Ka_lNGcYT7L*s}n#Mq72sf7$0j;e@ZWC<*cvb=RW~Z`d zusRQB2$TXO2zq*Yrz5#>eW>`zszMZt9K438Vo^+Ch$4QzT?G%|zQ^p}vlJl)Z){9G ziAms*bgNURPoHi@FfZ!B-atm6D?|+qLSK2_MG!ZoY*R&op(<__M=&GW5HA~d9$Dw) zLQ8-{N3cDL@^37_d^Z&Pm{2<}tEeVcGOFy;W3Ht?&JD! zd@K0W@>T|fU!o-Fy^xcj0k8xpoAvMx%%sC)k-x>M0t^~0hoz(8icbBu)d8QH7$Ja| zL>9a3zkhoCkR&NFztLk5g~$;Wk2qcc)52eWWEn=V7$)de&w|GTetBX~&Kp^&Ll)rlqc~*kbZSIz17hr-#vabtM0~JIe>2_JyC%&)>>kMRdG}6@STo|(EgsC;;3sFL$+CK|55MC=zKFH+`y>~caq6f8G~fjt$PNMoe|NSN$# zz(TS44Y@D%XttFV;29w#%~R3v^hwEr;g=u#dR5XoqyDZpI3|_dcyX-v)Wff79>15} zKYs_z?vWV`C!8#!M9{7*3O&Qo)6w%Z7nC+S$L<;$>S2wqoMk$n!K%exCuj=sQqXuQ zQroi!!#KPgTLR{!Pz!JWE8{wTQKPsnJYjJ#6bysNJnbORyanfn4J8zOG1dtRGLS_? zfV-?~^~ke2z5d^psT@jBi<{l1*V-WGqa4hbw-Ory6yI3?5&PFu82uq&X(0nGu;_-s zWq$~)mReU6^J&Z4W;wY#(XbUAc5TUl!_hKkbz^2Lll0xu^*l-vAHA1| zY)*Mqr>y;IC69=~VX;wH=9C~+^HQJN%Z5k7@k^m5Saq7%w?ln(=bLRreA04Sbeux% z^R(V;H0mdMeJZ9xW_4Y_r>i71Ltsp#`xFUNlD`R@IZXEtR$?CwPA9_-qY~h+erW6{ zSb^Ev)~5V@>_`0T0EYP;G@mJF+I6h{DdoF-S^WtOf$YX-)#um%UgCYCrNFNY#sS@L zP)s{>O&son%|>gGqlxv6e#2Q|RHI8MQjNiYRN7f?&mJC>RM_Mh# z_?Rl_?7V)jw74bP%79ErmE`ZcJCIORQZ%Ex8);~-KLu2hmc;_I9o zoOmw>1u>FB)AH-q`tiXlQ{OjXN$C9z7om~$_6!9p)_?(s^d!|pgrU>nBDB~<9$Tw6H4?)x;M1YX4*@asamb_M0x8oUea+nul`!z|LYZ%hIuI9URUP)p_%D27E2b~60h z|ANSZ$wUBIWaEGcIEaMGMHPU@TMa{FIIA6VmV-P8=%x}TafvgRwtj`!0_$vPy+lK0 zuvkL9c><*-p+P|dBNlhb0B)os11)@%7)-n7?bHT%@k#+0GQx5N^m!MnGRoJGC1HTZ zXercrK8Vj9=thg2ASFh%E#^9AMclsLsAE-Qtvn|5WSbMLimsF8eT7iL68amZKxVgW zY^`s$?Zu;H%l^+e?4$tLk;Hj{Q16Hu6K@(83NBCC2VQv~nryS*5}+P>BZapE9HG2` ztuJ-78wClJ*cQR>n*!#d~x*M7nrxi~YAC_Mqfpr?8 zfsr*H2Cwr#J$QtU3!al(hz%{UVL%$|p#=dQxCABAVEN_;C!1AWQA-vR*INt@NPmH< z6}G|zBx~>JXv2t#%$`6FTuh;=IQiNQXc$Q04CqOUf$k9JzOF8MEZ)XlS0`<`@kmCB z)6`O63`~|I{Gne19+dam}j?nv7R8V;#uY8)D9hV2$j}^m1KUDAc?%gBm zvV(UemwL{Ou0}DL`g$4#9^j}G6Bx3wC`9g>L;*+qjtPu}N)(2~z95mu0Kq$%=e7$Z z6ee5SfvsufI@iy&09BetG|>m5Eg~D3P=^w&IHC8F@&SP8~moDK(q$cT__S6K8eO5R!V2QU3M%EE`j9b}D;@fIEb=jQq!ZP*0~tk0ag%sK|6sOh^%ydx59(leP*=ojZ+^N zVT@wpgIxr)I1>MAqoCO!5NVj^W@5$$YpSe^1J{uwi=c5rE+;1fVtQiprw86Nvhb%; zTUGK_>uQR7^gNg^&gseCd-8KYiQmx$GL-)&V@>!Pp}6Eh9~2hFJEq#miXQ@oz2%=1 zo-6Gqr5OEl#%3I7~iRB{AX5VMfwX)S!XeHdcp@~-fI+ zM$V`s+EAw=T&&)H?3y~>ER3^XVS_i=b+kBx&~YgsgNXqRAjQ05i`V>R|MOqp58CP} z$Pfphi~SL@XGGV{pc+T2VIWfrz@Zr4EurAkw~Cl2fc-^8oKDu#3)eFLMSOF9e}5Ck zLx?#54K-mQLc~C6 z<}*A+ZT>tlvkstWZt5M(V)jI~zXc2|^VQS|8BygN@jhA=~ zXLWWqHVWt+AfPsc8R8TR5tAkgPU6vt3aY|iYhf_Aa2@ZWx)NE`9>9ly%nJMZL zz7?2<#PEZnWSn{f0aqNjZwUKEf)PPf+Ya5yDGU0_dtiIm-?~MrnSVd)l539n`H~D7 zx%0i_7~H5v-; z78j4F{?9+#8P?4FP6OXh)y_`L#ME>M`+R)5(sV@H+uJD$C@_l+52|GzQ{qRWm0Jg} zTIlBcQW1fFFYsMsZ{nv=Ixlp}3l2!_7DdU5#&|3 zA*z!h|HBdr|G5%+*B;u>B*%kZ29Sga6cI&dybN8OlIPeceRp$d>5N_r0e;_;u=H4h z6q7tTgX%LwCVM`S%wLMiCvNQ&0^h;=H>L9xwr-FWqZ6*cRVE-Oq9|Bu6~e?q%&t|T z=EVs{9D8r1%jPEj<;k zIw{1Ww)w2sgj-~stKre*p!@cHRsH9>UPZ#Y5mKM^;^OS63hSHKDiM$vk<1qyn0xm= zXdm~0xrD9W0v5G!=PzYLd_ku9IHO9ZpFDepv(H5`B_pG2&-SNa?*8WqDShWe7Euyw zKvG8m1}@!^h%5U8bIm2Wz=CeYqXIBXbjJt}DcPr3ut5DkcZ_Ygl++tBjH;073?^2{ zZQK=wY>)H~&G;39f{Kd|B{HggN#y_UpHVwl^YID?C#PEBxiC|pf{Us?&W)r$WHMI@ zY!)w#!OkvEeA5(9iBd3uSdflUU?F)#Yw-Si)qO;! z*q7NR4*Jc4f$f$LHUcPrvcWnd(rBf${%ZN&y=s*p}oE9 zUe|~4MRzj0T=PO2d4?F*NUczl9yN8J3e@4IHIHTZVR*UeNU~nGN#-k~dil_oJX8s~Cz;ds3 ztP92!Se{2~Y__nlXdQB1)ZjXiF*V2<_v8CP{hJ>-Cc~qju01e+Jv_$*`5gdlyv1IUiKmXrmuQaO|&}oE5xGn!Yg9qw|X5*-szuA}TCg3^psNC+=bC zCpI?|R|B(1eShE~)P${IpXbU8?qui>>7Y&SR>o05}pV=KIYS3Vd+CO~&*QJ{sIB+j-06xZr6+z1Wz0_DFQe@kHT;k&vlIpATR`er31 z3(s1+OudgUN=1c)icnF9l<~SU0pb{RmVG}9jGY$%#$I8!F0Qc;5|yn*jN9G<^AE(o z$ot5blxn)}P>t91Mj?M7{oRssi>RQHW|80pIb|DH`z#|tNYxbY#^qYw{6?) zxG%qD`og6Y*^(W5w0_v9J>7oiVDy;fW?+Z{S5GrN)UAZ`o=m5uwdRVUB+r@n6P1^s z^J;SVatIem_$D@Q+Wfs{Ky7&;@e|LB#pUJu_xr&arlahp`Z9zYE8f2!gdbhUd*P(F zaDt>$WE;dax7MG1P=d}z42#r%e2a}0AoLk$SmD9rYAz;G**k634f#mV46xM9;Y%Ys z(dvWWN6m7dG<2xmizrp%x7r|(aa#@>h9-->WXk*+)@RFso?m!V+u#ZcM^oH-WQwHm zlP2p9=^yM{)_J7ywB%QT;{3B0bhnP|6Iq>58?g51?zJq(wX}-5{`l2&7w0EA?%T|u zq#$;9{@F)!DE~_5&zBomcHgCY3;tL;PWMX=IGw_1bM0^@t)g4}IYe$!5N*9L2IFr|F$(Mt^M#7W1kmQ2 z$?o`S^pR%SZmyj-_IcAaFrP53cu<#-n>TZ@={!9_U5MPv88WWScc!$QYRBFRZQB;` z<+oL;-|V#T?HT2BvuD5TLJhak=+>oNgE!4KJEJvDCTc~XPGIx(GSXp5|M3d%tV?cQ zh3sXRa|sBV2oD zh>8$X;GtS=!yVbb4zMjI^j)^S>>15Yy5RJ3QN ze|p;ON6YsU+WJc#RR>}K#tm}}qbe`mPmx(N9`u3&mt=XTVO*>VNz>bHBl{Bf*^IWe zNZ0o{3E!+iH|p2e=db$c`IjIan^FFj5tlul7a`RP42(O4aHf3D;sj(V0Tn-#AYn(u z#UlyAFLcf|fEV-%fM60Jx32u0N#?=88sW;+eoNP8>9h$01A1bI1HqtkGP;_^{7ux0@Uv zzO#5HpCgJ>GTL1MX?U+E;?vMqUE8*wQMNWCdYB~pZ%}_T{9d?cWIyi?ixmWftut_C zM#xnFMH%ZsSrysre%#bvL=j990XYZnq{!sm1TEoZjEpxTA|Ai)IkyF88}wO)6?+M< z7y6giDYoRi{FqJ$&nCk58R7k4pK==`cnc1?akN$YF90<2u{kh}wPa1_L^Zf%hb#3{#AAT?r zGb#_;IPqZaFP|QQ#rrYB1b_w}^}cgQu(TLb6U67jQ#>sD!dvXNh>B_n<>*!bY9z#G zrnNiS2o?6*#o80vZKd3{Z^`}uw1TZS_VGTK@IdEAc@-5YQI1o`;*SeG(U05z#9Q!c zqSW{&-^-HhnvB2-Zy$W>B`q(@%Zj9nc#A~Q=9CSPg;vMCX7$lO<(uZyZKnytp|*A7 z3S%z0&ZD*3WcEan$7A5!wj|=#t$8a%FuShh&-B@j-aiXQv7o4^Dta_A0Jt$_O&(Ky z1pY}GBul8ye>o&gL-D%yier|)#kiknexKcUS%&^b0n*a`{5cQVnUOF2Cs=`yb5T{$ zfE~$a4sL+uE!9e12^|F~Pzu~|lHk&Kph`ifKhhoTM@-l1#WW~uW}78KHwjn(TaYp`rpv4i&{Nkxc8oTujxkXy{`FXB{PQ274d*0NYU$^{>|!H) z*;Swyw(9xygJ5^+>>OxwL&yTgz#5tF?|XUuzLd32!Oh|{(mTNFz^D<~!XK_FEZ}wc z86=fK5@ZMa!H%oKFAGplkbFgsEm9~wugkB8s`*b}ym;Z2l(^%a;xMpx?|weGkWj6J zK9VI5pqe_OuixC-F%i&glfg}f#t0yY4hT!%YRoUnFg?B!*r1rK#TP61)qIp+{b;m7 zP2r2U?8!w8^RFNekMg;0HLP%FrC{}_UA6Ai_ifrd^c{cxCPPC;6*f9bzHPH>4e^{_ zPaOB+lk&KzWXPLf9M{G-H$e}b-uY(7I%7B~JLA<;TJ{)w>1iZ+XQlk6bsBp{>6!8D$IDA?F(KReL7P$4nWY2L9L0HUS^cm@*tPmIjZR z>T*#(5O%!lVscK-T8eGu$`b>-2mwANWjkIGQ&1icxlF@$l^y76{?O+G+qN`f&l7k& zRbI=wf<&_z$nek6(ecqk8w}4V3IW^tvmV!HY_FmUP)_;uM`n!w{<8U(N2c~gXzbpF zJjXoj*k?1P#+^A(C-VUvfA@9)JwiW^xSD@9W>ZQTr(R+~Phb`mML(>UyyC zspq(6kAsX{^+uAwEKG4j=7YsKtQBLsD+7Zbc|KB=0(DNtI0U{5Qbu$0)Fq%!FUQx; z2Cb3$G5!~TGr9ePm>UR-Y&*>ou)Z~~*@oUL^piYB2O#zX+!O1}l>CV=yM!WjOy!<& zz3BLG`RPGrDo#379_Y5$TpkS&!2N(o7bDjK3D^{)lZ7vTXlA>N?5!{C?2blwc+bA@ zqX`C2{1*hMI5{C;eqhS^tno3`{VN3F)IMkQ;hH&$g2;KX@8{!`JMt6 zyO1ux0BYF@Pb=a=0tp;>;ge5pML|fYiQpDhVQU3iAOGqaepxI#d%tgP?>HHsU}R2F zDJd!5*V7vNCg9^kN#^R80XD3LmhwaNZfPS|WU{M4L9c9EJ^#xA+3q#3@U-CwY9UX3Cj<$nAZ#MaNVhd0Tet0G(V4M zPu5*bpd2E;=sBNP8SKc<{ z%%!K2l4`u}kje@gNiJsm_2Eov%Evw@yamNS>>nZ!@P^QH1#emxGd6HYgcL&$R5(a@ zz0B{RJxJ1H)3I~X2n%rQpn-hZiwGm&fV5|^VgW=Rb}jDXZP$1A$6f3NyMvNUlxg^3 zZw!UVoA#^1c0lRt>K~wB!z1LxnO{q85{6d9mwr^soc=If|Bf8KFm#=VBz%Dcow(ca$v z%`@d1ezma2$2BXYA5=cu8Y&p@Wq9A*ev3pc?*8BN_aZH*ROwu5{Rg%z+VBlaQB~k~ zphsYG*&A2cRSYh9ky!T6HP}OnZ8&e9RU6NYW#pIOp%9n0M})Z)RpwZyAAl+{A|hg) zoLpXY$6n31h`Y$)+}3>;KJKVPF|zOA%XiDn1!6^&*cPMgM$^3UUV6P%TwEOGg^GJN zXDs%Lo6>yZxZ!=XNZU`<&M9$6tRtxD`R|rJ`co#ixXB2_33`l?r=nV#fC( zUEAE1>1Bqn3I4{LErC8uMTL3!V#Vjz$^9^^Rsj$LAOrucxx&Z)3*H2-;HMgDiHPRa zgNMnvWl!V5hpF%|9-|QHwx{VZBVj$N=1gwx`cv33Bq)ti53|@J^z4^kdmceC9WiB} zWHI;NGEZ-D`p4uxmzzE1!+lSe>jN_SoyH1;C^D+GV(GYSh1`(Ay%{ zpi-`u{EYs!;x?~Zi@k`G%I5@PHM~*qvHJzDV^H+5paxxXX^hjp{Psn|B6xLwJZol( zOvD%;`MDKqIF&fq9c{=>k5h)*3d~>?r;bcK_9m% z{0cwH(9OjPcLM`;Zf;Pw+qw25S7X?VTmB866B)Ci@h|r9vTjgc#_+^a`y_pQ+}Cpx z&%(*CPQ-gzQ&S^`Uwzfc?F1fXqH5R%Qf5LuZJSyevPOcA&tt+kKEEK8_Gg3l=<_jW zS@NwPItcd3qxmxAB#(3lEx9)@{m8vZ;|eIQ?iJ%AvwYfsVw&*snyaZV4Bg#d?A1K3 zWRcOBc>FkN7D_mj&F7Hf>t}t)m8>F%LVq=00Ran+{7=E&r??v!8KU+nC7FGdy07^Q zXwHa>d#?M7A5rN1@H9|rFIJ4pc#)Mi#%GZxbI_0<7os^8&!g1X=kz1k)4!_byNV{$ z_zI=cMXRq$&Oe7kx6>AV$a?1@UL7GY`ztNf6-7!b_JYB% zlfYq+bn1#khysiv<){0U6MAe^+obC%}GkSjjt6Iyj@@!H1u4s+v zp%BlvSZA{csT`SbE#^=AKxXW@mh|EU$rVKQLpth@vsq-z3+XeV<;G@AibCG&{Q1)a z+;B1u#(;t4K_zh&=pda5#@E*nan8s;%_pxSX}~b(*X_G^;~Kg9nj&Ev2oIA?RgvS3 zGa8Mz-yw`gBpg)Oxu1fbPEH=5a?6up6hd*m${;D@lfc!&$>TkvE@RKJ5F}QEWS-v+ zMfl4i4eb~UurJfL`$~^6YO1p6nnW}qNu{vsk1^C3SXIYIs*cqL&NSE`p@4{nqSxno zZfDp5F)+Af8n?Y^^_FSGqRk-m(e$hJ>}U|3$PzL_0U{96g`2++hFS62W6~=%)zg6S zCc-{}&V-C5sRW2r0UvEL(YKqT?gA@OWCTGD~&P3oJY>EvG9ziHA&sjJ)w8wNY;P$ z#Te}93D3EKxi%ndjJG>L6iG(0H;oT211VwrjC^{pH=}65N^QN%C>L8F2Jk8Yh7PJ+ zQ%=GU`%J*}hJ!2iGRUm|@cfP#GdKOs?@ZNZJ~KvgR(mgzniPbgU~q%!Wgl8BBY5h} zzGS&tbn+-wC?$g$@X7 zr~Vsi%kgjlQbvrSWTB(_=#lgJ^U(yueBL^}3(V+T*^_NCYDG1q8ZD0T3+V;yz<}$( zu?uMIV`;ec>bXt+jFTI%%ZW1iS?ZiL`YL zp&OomFH5pDyioeO>uDRgxK#hTA1PqEzpA*S=F z8dXQJk=QyZDXmL+iYh!6cx9<|zbqqxB^iNTLuyNc6*d8W{OEBHX>$<;PQ**AaQVn_ zngUdOq+@mi(jcOv+y$0OfZ$aqwOztX$=ce2%w@BYlmliBbZOy?gEdckIurRnb+sb5$^AdVd?tfX}ZX zg;ii|AP80m@9ktKAOf}?d~f|T1+ya*xO9*Z6r)rWw`(ofyvv zrOmY-U5;MI5Ev!PI2LA2ULf3BjOo5>q$VDCa&${%>124LCt8>%GyANW-4jRqENx=8 zFep{M`gliKdFrlfp8xAHQ$H6u%@u73J4k>>CqpWxsKw0Be3sOu759`b>b>s#R|qHQ zUQI1^Shv#XsrDH<#B0hzFD7x?1i>p z9$4_3p;GQ4>{bJiyT4uzNWn;dGdDrpNLzuf7e1hT-s@Tc>My2kaj`%;D06ZSD5$+ozmV_vZyy zEJGNqckt;QppcLp!86G9FeZiqPDp+7%b6t>mqx59L^Lp{e`tUXmd(wy%prEQ z;Rm&@OLH=Ty0LV&%X5}~wZ6lR6nDg2tmaf!S6}+wxr3S9E08zdd!PdHDXe-tcM;jR zZPY~^Fv=$PZJi$V0MXXj-5QSuG>2B@E!YaQWBQJ@C+435I#QF5cXv2rIM991mEez` zPTV~HoYx>FlMbnUH_DxYg-{`@xMl0M12pgXtT-glOnj9pomBbiu|j% z19+HA0kfBZIn01*2L)v6AXYZ?qxXBhLc{?jTB32q?~%Z;V`gZ4L0Z ze`X|LakM{;vROpQj(sx)chfb%juPp7sjHX3XW+P1~l>jq+DM7j8FPOF~kohlNmX zrh3P5{|k{%>#fqY9&l*tLYvL6{h`ig^-;qgX@XY)MQEt$Pnjma?>Vx_YHZU+ffo;^ zJyX^yOl_0;g`*N*tAu!aPBIo@rOF~L!DCam3EmwtU?4QdDBHY3fY6V6$yORthXH2X z)oCXU%0&KD^S~g00|7AM)`842GBP5geZvE@EkX)OhRYmoHQ1%}aQK#f`KEmtAzb3O zlwg|Wg%myePn@2j*sNqfI;4U4pf~VkLh@ z8`0%10gWiGk* zwV$%FQ>_@gmSG^^3remD3_~!pz;P$ksG(=_GfOFua05fh4%RLj>izroTzkI$p97=5 zt7h--x9_pp8p?bH+Ms!;-D+WipkFyRkJowc(D_}h-xvaktm?etI*BXg4MkqXJ8l|lp7Psmp z{XP|`BXcXzj{vi>Da#Y?=S z;LQ}0**D<``g7Y9;2547I3a-cfcD5-`}N^5IrquM&4#B=Ep5#YQaftLK7P=R)W&#V zQF~^Am=)vD(JQj8Q*TTBwqBsy3h5kI^=PEo;ssHZos&!-PT-QpB!IYHrpL-UJlHAm z*pGQ{Bu{$mg~JU94(aLl1vKBXE#~h`Rtb9?W38;B>08{!5ch8EuiJjH@2jkpoy~W% zr)^BCi}$UZWn}&O(tFI%Sqz1RO!W0Rd3MbpYxv#(@gOVR#BB7=^>nL+`Qj!lqU;Kn ztd?}{g*oXUVs0y1C~X~scWYVp6}V?cn+VQjK+?5{E0_-a{)BN(jHgJ~1E0S_QoKUg zonIK=Et)%Wlqw*cYG6#YTj1tV%t}MSqKH+-z`*caOXEC5ERiC7Cf14IFm{~^AFt(BV+?-}Gl zM1X`pj|GkckKZa^3<3`sBSHPVcW?jxw}ukW3NPV1UTwM=+QdROwF1MnE~{Ve$YWn} zL9q6Ny)%zhTWJHM)TEmngE!k}@8GoJ-qJ-@S_T}NrleKBk5N_h`?DF1%$h460zUcj z8(I`vpTdH#Dk|0rWT?p5Z(dWqYHyhQ%m@d_{5OfrQ1m%YFms`o!!B zCS)KuF0;)UqT8f#kJB#a;~8ZwtyN@7M^wY0Tg3qYfNJ50fx&fXS@<_^epXoLN!n2Y zHDfp>qg@Pr>Lb#@zs%|uB1I>8!W#EwuVB)?r!(pI|3;|K5U7S`YIZm>U+_c|;UG(# z)u_6FB3v@2q!jw!AaM4HgDP22GQZ|;=XtX76DVK_So=4f_-6r>lLj6#%Gzu}f#~A1 zZ|8=vg{=d}`z@ZGIGT97!vTTQgxP~K2pt5QgX-;L62@FoEJC?!Vk_vT3f;b49VUJg zA28MdX}%NWS!?C#_^dP?YXt>;L!&-EE%H>F+~f?azH+D0o>OFt z_{x1I^$sKJazAx#MrN!}cXC4?D7ba2ikaC)zzlXhi5cxc`J-1{A<~8364k$okA9=EPGtN7UCiPyg?DxVA3ukjfdG@{qH?CuZ6cs*_)wx2ePY5b z+Z??IaUF<uD8TrcczZBSnt_oILy6K7Jl@TYrj>o$^}E08&9_p#t^T;DrJ)=9km6TSJ= zL!CtbLL;w2BQjfG&AXvHhI3f;`q}%JNQf+kdLbFsg~=a`5li2F{8$1tm(sFF*;%4| zY6GbI&|DXwU_D=N7MV8TX1~$F8b%OjgxO*@HNYqa!EKr1S(T>8r-J2fVEBGM{A{5? z3NzdSpuVNSl&jG^eTDjBMFkfZ8Bfo^xD$wYi^~e%p#e&Kh>PV8E!FWBOWw3B7%243 z)U^uqmbv`uVV_-yg-TV===4QZm6}h;5$Cb-!GvH(y|2m^HQ2t61SUqnGeR_KoO!kT zktmPal$%@=Dh4=Qaf6e+s{c2erU>XIbQE5CpY-1hs{f-MV_E@x`^z~CqdpuV2N~M* zT9!CVZ7u1q%YAE2YEG1|{DiyL;-bv*bPsRHXZvC+sS9n202+9Yu8C?hVZm+Bn{mCFjVF7CkwsH#<8Fc4T4bYzibNEt`f za?Iivzg}t{$D<}FqR4sj*oHwv=VYhRR8NM)qt7G0SK@ua(VCW4z$jo@mczOkl+c*v zOrsDh1L=-0IVm9lofYV5ILa`QW{5O!G2i)Ay~YgGBzU;n+I-%5lYbdylv=Ojt^s(9 z=jJ*FW-ZJ2cn(TI_C_MM@jOSSGH9OX?!oy@F{suP((vy%ME?je-Vv z#8%#!jCt>f&aKu8+W?u_lkYQVw&*GJ%1?8js;JBvZ;!V8!k=|_)z%;lVXDB&Mkf^D z{EG6qMofRcAdlLGbTpwNjUzB|bT#jqk{nLPEMI{lHBuzr0kVX_zC}|RoGz%3X zO&UldGL({%=1Fs;GDT^S1`UQJQ8Gl4r2pDaIp@6Jd*1K;{{8x$dp+dGXPxYCUM zq*NqNMpJoVRQWfCrFC_VlW-5n=z!m#(!fliz9onB)ER^_IjN`s@eG=kKgU+zT47$n z+b@IQH)&*aw|J&%+i~93I28fJeD+~JlVN!;J_7HM^&u#uBhIi?J0%@^(h0>IidZ(l zhi}#Q*sSKBc3g3(qi!UFL2NSf^{4UKDmhd#2X=EyPG|(+iZ-*4>yF~o9tY0brA0k} z&#Keh84B~1W#(zU-CKxQRrpC9X!nFEdwjlvQkV&YZ)S@Y=~=}vkzgQ>ar&4fU^E%j zk{!@A(G)Y;y_+R}MX2)L;t!YN6e^UHC=?@s!2OeAttYcVG3ag){2Yy7)NRr zI&UR& z=VPCk9qD0kQJkY<-H|7=kaMGC8@>Bl0^gd3P=DSB0~J^}9&YNrF7eoNm@UH!TbUDJ z-BYFnOiC%iitkD79>b}z;%aG`=U?A3$>9uZWdYlPj&HEx+N0H|&*L-Qw5R_^%+N5K zL*eYzo<459(G9snn};T}I!zU13z?9q@#J=^VRiG_@-u^hxAwYhJ!HOP=x2Tk=-921 z`wWCdaF=wWk%=FZ=_5B8@Fd1gBi5eh{h1#uw{{h1`i-YNjVJC{kmT91UY3Kh=u}6y zz3X^W(a4(47g#q*hySQruf`@>;|JkIXIhx|stJJOB%J>iIs61{_^CZ`X5U90b_e-P;NMQ17T2|D{&dMv?bCkYrey>wX;AqL7Q&|3V_dO?F_pm)*uLRlbNiE;}? zLm0L1Q>q!(IEsm|M=|*o-q`u^Vd4{jr8TCf_X@7ojhb-zG;W!=URWkr4%7=lmngHf zSLxzfSf!Ss{5~BWU2U%**`0GFYTCH$ML8pP9A|JApWD)U03$XK=uXf7{=t-hBi##> z7Jwy(tzaAFi6#-HGf#I%2C1oJn|N<2U)+GjC-W1x0bt$fsHhN7-q7v>nTaODKB%a; z4ajF!y2#tuU`5JcQIAaikYqUL9w0jz-?98~MbqSsyN;@=Oun|QBc?7=qR26G=A*zg znTa^e%7B1m!4(rv9$a%YoAA}`N{%bknplP0ILZ@!|J%gALq6IL%I#g$Ww({?s!zeW$rdOXh)F`Ba0KW5=8 z^!^IoU5orgJ)A=o{VfP!)cByn2uCJ}#^t+@e3^C=92?M--A=xMl{rdl?6+Iqz2V{E z2Qb8!$^JrV4_6UgkihnUaM6LEJGjmuCmwR5zCV=(>#Mj_jG=xO&0&D*OEYBW; z2!=uP?8w`6?d<06-M8;_WMpv3&(3)lRQK7`W@X&$<07vOWU?JC4jK2Ldpv#Sj34}M zXApc3?~{b-JzoYXcPFkiG?8UEr}-G{8m^sl0SWyG$2Fi{1 z=*?{+5#@y{o1Nz9Jb$<~B{^^u)_%hv;e%zV1$nrRzQmpLfAVfq^~aH{Jk&V3)p;P?{b6JCiePDa`uNNph+BZR3Fe># zykRO4dZ>4V?O%q*ICgaCsV;-wKme#l?07R66hCi!aN38!Wc^Vvd34`gohvWTUi))< zjMuL(m!0OP*??~3^|NECcohIY|m0U^&#fzbw6z13v0WcUz{?A*3Mzeomaz*kCL%PuQhTD?`d2K zxY4_ldIDID#HJ*BCE^bzr=+xlAYyDhj?q89A$K`c6DWZ15@LvWAA^i`lt~IiV&$3Y z*(o2t8~$+FpJVlmd2$6wYJd&MC_SQ7qSx_$cGt<7>W2@c)DXJ_XF=Tq?mm@#5g9eL zqK2;eNwgBZ_i;~qj>rkCC|Ct3FsP$0ak<&8+!dy*vk;7h=3hP@OxAbZoXzd zMsTt&X`0zG(t&N?v;6$BYiLMG)Ld>M#z!oN63|D6H%^Jz${BSck9P*9*-mD>M3=!K zyO8>#k*j>G#9mcB$&m%vFBuy^Q$;nt1p7cGiA-F<2;e6JRD`Wjo+KW5(3O;&41)IC zXEzpu@l~o0T$24o{~%+jFg_*{!6Vs$XDiiwSmX#XH}URWM($GF0JYM zOHVGkhY=Lm>4Va#uqe`e-z6aA3~%ZJymWg_;)%^x2WP+jf(bN}al>R}Wwnm{^19;z z!Uwn`lW-W?yZof%5ZH!E;m3ZD&vz%UFSW_uhANrGXOL~;G6v(ot7CIq&_3CDXv>zV z(TDpUdhhsS?@l+IrVqAG&J`p6gMbD<`!ZZU9tdz4-N7G1{pzZdEZxs$Y-RZ?`7F*2 zO1y*g1Rh>%v&)J4vobDAXcq+!%g1<%bWd!A*f6bx2TVxYgo}+eBmFP5CoX5tZa$I! z+=Ck=5D7vA$zu|&SEJkC3_-4?_7B?jG-y7(c)*N^4%*}63; z`WEa+fmg3$R&dIqp5XbqA9eI}6*T6axLr+doil(He5BA-kpqvb|2a`JSBd8JhOpedp@s(QjaQ$HvAg)m(LO`5(|ytb6XxN@42p zBb`h7cHwWiI>n3FQ5j>DK_E6tBd4@^CDeZ2lu5+Opc6h#Vf>Gtts}=@41d_~wBWoU zeARY9u+CUs^Xt=WgF2=D;sSqm3 zX{h8X^);DsGuGzEvyVptL1t~IgBbLwZD^D>7Wec_$51sTI_|I{^+jw3w}wfz6OWdW zsdVi2N#+qSiJ8sAt7PK0O%w=&f+XvxK>*!B4 zNWG^)UTvUT8*SQj6n@MkiN>5F&ep?03?>z6^enEDt!Tz$~!bMSQH}}IJ2AEVq+Rs#H5Rv zO}H`7i=A<~W{T_9%+Oz#D__gn4PJX}TMO3%6fbmYMbR@Tz3;<;{DVRmEBA@FGKOS=0U%rje7!HwhY0 zW;^<&CV^xUtRL)FR#Y7NDme}I{*P^SfKUR=Zgn=UJe`^~AeN?3U)oY)dEDzMNCCCA zs%M>e$34|a^Pzt6XkIf1(60-8)$sV`BytWv>FGz>O^ws=JfV8kTxG^YVZ$b8r@FeA zMOPS+i4s3w{Qd}uY?E~bu-GbI9Yvl&t1cbd_-*`CeKd<3@wl<;Fd5BF3U)q zsLh5vm?tjD3cW5JsHdUa80W=Ic_M^Ri9YmUnt~BrXK&A zu9PQ5SL3O1;*e1+Ov3`;C-B(zF0vcUmJ}8VSxFdb+K&T*)8&8V4< zI$O{8;U8KcVIR6Q`T)+)(wl@mR8d)J#N0#4@&+<`li(EsgCcgnGX4)q?pwm7%AE5T zJET7RgKLfPCt?l|R)7+NDPM$Axk1$i1rs%Xp{Vr&o|I92WQ6N&%j?D3FjwijF;{Ox zX^+PUnm$5JeX5all2;44l^g5pNerfmFg{~Af|MBBovyZ~Od8M113K&S( zJdcsB;)dLk=31y11|i!fH0-mtbl?dDgXNd}%!?qG`wMH9`9Z-pJ#<+g2fF&L-ZMH7 zUMaa@FhHISeE5g`z>dMdhkMN@B7i0MVmI&)9$Dk-FjAiG`5r~&wk%T@sBj57=Dbit ztRjd}B&4OUHE&(&kGO7hlw+0m`;scJ6Nk6n8vGU=Hg6o_ynU`&VaA3ealJ_-4!>M8 z!LKaFLVbs_D0;4Jtli*vDb|gy&!}^E+6_IlV8BIv(T0WwRQ5^8@20zop;!a2v+&S(t_X6RfEFfOpS&VxZz(=wn>aiD&>rqYYGNh9MBB3 zrUoZX+##Q2*uKb?Uj}7^SYp&@CZ_B{xd+sN=XwpbzW9VjTsw9)!<%v+ z@XX555D9lDN*UNMx6y3>jPjTCXBDa>86iCnqq;x9fT%<7sKg|iD~VU3)TJIyMFH@6 zPD;cWFDW4;{vsqCZHK>9f#j%WtHGWREJ zmJYP6w+TPiL0r`mv^Q*<%KAsHndKafbD>TK-fb**ymQaiBe=0o&FGZLDR9oCXJDuzHj;F84#usbs8VKom5N2ada^8fvR{_OCf|M4cf%YZe=ht2<8l&q%@+ z3*sEI#U*erU}2ODR_$iB`hq}**2BxT9*{w}8I-2d=Lbc_nr110bwiUKu>4;Un+j}@ zvplm+Z}6K~On{Vxw9*Hb%~%pueaiRM57wRIAGFWm@U1g9dqZ-uZc>G8&qA-b46|#Y zk8x6eWV260Lo8iaDo8D24BV{P>F|pbcBoRUYhyIw$ratlj{ZVWkXA>(2RcALcvd)L zWF^MVN;vTVq`z~oE$P`XNLKU62pe+l4k{~|Llcwx3{_Ux z?qC&AZd&SLVF0d;iG%}O;Z3b0G z?Vc`4{Av@VIE^S&aus*L-XWVta3*NMjP8rldxXZEN5(CTfZ?HG^wJo*^1?TW< zcSG>zJsV8%5pDwgCIp;x<%BZBVXNX7S&jqaCVNr{BZQRPfjtvY#&j2fG?T0Zvfq3{ z{9t3i4@;2W9&EH}&`-#ZDWEoRUWLhSC=Rl1_z5i|_@52~y0<3I_wC@V?Gy)cLS-5el3_>% zs%+?zfw|mc6moNz==P#XTwSZ75Xh+p&d4r*BNk8!jXn0H_M4b14BX0Dh4S9_uk?SYb!^z!Yl4quDTjWd5gAM^=G z7rz5#)#f_ixqTJbc=$%63ONmW9QmyIWFE-?IRpKQe2v@fHX)27(ixwjp#FCxDdRM2D!DH3wuB=Qh5;Vn)(5mo5!gl05T1>B}11GLYTm2d?J4b2{5etKc#P>i*%dpJnv^m?Rar9dCUm6lw< z;D?;Id$i`deqOOM-TmC1H#izkfJ;YDv1<1bHWcZ^x<>3nIn5l+uwjFla{0CD{JC9W z8Mn`8Jjyqf2k|3e{pI`QPl_%v!rKhkbOk?nPo;yK9Z~i#`s`b;3~E0D2pIV^ryAUQ zq}uVtffh!+FSkYY#QyJ7|jfS&c3g^6D_|`%)=ouAGDA6 z@MuxWp!gcClR!;Dn2LkdrjlsL&EqE|R{|AcSR=I{2Nm<-pGX|XGce@t=JOrkM*#zD zNI8Gq3GH42Qrtg2=LLrT>eY72#TnH?XDz`tq-RVN^;oPSG>LnaM-){=d8t-G@(LRp zvRlWtlaZ6t#?gQ*=gLhZ{<&0s4nm%f<~F{u>KN6<+VT_Xu?qEta;FBahkz&6+zYwtufuBigw3EXw!i8;+zAo!B+9*7E?n3-fzktnK7>^i`x6Hvyf zt$V6lF9--+2df%t+*)ZLS9lKAqNhT+72If(o~-(Tvr`icCm4y|R1<1J&VPpz12j`K z!=z{(SKOlJ3c~$;z$9mfqK=7k=%0fZ6;_Nt`N{6wx%2zCl94p|HzLd{Qvo*)iqmml z8~5IL2Js-iD`rlBi$HCln&~_^Ah2mgg+ejUNc{RnI%MP2YD>*OIS@KxJ`m&~`9%Ia zx%T9pb{BCx@==-d_A*Z0n`ko3pd^ikE)e{A;HA$57>$JwgHi-LO}h0*;qk59Gast; zPw%s9UL_?&Qx7niy@tk^FhR6a^s~ za+{N{J#*e)kkF7!&r8M6AOYXpX1Y$fX5{k$bPCsGsOzL8!pdr&-OQ6~CiW&HponX5 zozUZk9P2HvW-OP?Gf9sqvjK7pGAzJaL_pq1@B@h?Jqmm>6UqOGP z`De-^L1@teu8D%@GfCeO9Ej-$@ax5UjUPS}W|R`#i}VZDK_xV>4w)kOHP-lO=tjIm z4V$#kfbSxF!>U(|9LDrr%jW&aU9}UQ0I-M!ceX0pd=*e5yuJWDH6UI6@Gb{-E9iuZ z!`Z96!zeki1|5ntsk6{_zp{_EX@C;-WLKBVgvevw`ZTXAU~`|yWGcuVAPF3 zLG=k}*ekNMzP1h&5A8s{dt&?Bd@zA~yyq-iBxTe)Ks|4qz}oW1f_5m;G{ACFEPNdn zBsFO8a+PLCLT9pc_ zh^1JN0+{?ukvWgO{vf9>_%Y+5yQ0$Gxq)jx)QON;=D(c|+sLl)K(M)2B9#h2_D+-; zUfmqD*0dcR9q+zAzII7aP|Gp>_zhaSE_w;M{VvNwHt0GnxOg^iW^PL2tX{DV_J@#j z(BTg9*KyE;QR6ZN!hEQI%?#aW))v20uToP6%hNJl_y7D{sXaNpE zK80qT_$x)fguI<-k&~~tprEqDPy9``^yJyy%#GlGdQ|j{c;>xt{UvyzY|yndGGakn zWRth(SsXAa;|n@fE}~UvY6PABbNp zY%fIsbEF1w#rAeM7he2+DS|^lWHoT5X=FZ4TuhMkF2Q#dLDe83yE-dYW;@Q^@9#rLyYtqqE&Lpu|*Ku6QnY}>qt)NjH)2XPlI0%xPp zB^0LfPzKV~A??5oC(Q2p^+{n;fE31qeBYs_P;bw!XZ{_>G~A$Gae1#Jq|mfpRt=5FiG{Id&YvZA115gIcrD#l34p)=psCCP zu7OVigbb%LeT?SpIZdvy(kbcbfJk+4<8is5$ta7iw6353Kw%MH7cLvyvYT%&k9RNk z3ehAp9QbarFvt}V$p75H*Hv$QYb!nC@{DF zYtZIB!YmN7x$4kFOJ9z|qIo;;LpI@9&~3diDWbPhd%5vD3I%FRGOCAXc7s~!ej~gx zlp2HVyosRR*B*zV)Pp&}mXDCEP`QSfoKt-Z8yXBPUZPJT5(Q$M=Fx2dxPX#)78cV=N; zi)JobAox`&J$;p2FXPYbIE~;GJL3iKZ7VmALcz%*e ztK%Z1q~8zU@;+t8SLE<5abRMyU({u~C_I_7N-dWbP2F?GMl*i#xam9Iui$z%r&?!J z__7`H3lPa()|^0Idjis}XU-EjV&E)@r;MnHK!|^iLiMA4u1D@pnBnd1qs(|o=7?lo zfW8Z{tgxGR6(oT8T#FtyD1y9tVWO$8b4=xFcmmRQ(C!63*abdH6|!#3-5P&|gfa2UuZsl8xMY-j=4rg3PM)bzr@n`jS zYyDK&$6x>>A&w}VW=*%~VukTZ>G_iQ>@ZMM;0gMlT?_Q+;&Shgt11ObB7g}5i(zoQq`a`m!MigW9W|2whF5*`ETPdw0N8h^9~;n!DOW^+dW z+sA%A-uwba4{$tN!59b=XGJpM`!wOEt!H;q0<%qm$rey&f$4{i7xEH5k81SsslIbs zY$w@WE2XDpU~qDh%cA(Og;-kPO{Bhd9EYOZuBjbJK>P|g)7(X27}1|?cKr`d0(wk7 z=bZUdTK!xpF;suIuWJ8Z({Je{-(vPyb5Aw(@Nz_V=Bocpfd*{?Vq4>P@*F#{a!2hs zJM8f*K?NFs4W0lu;CL#Ho>CsrZ8io4*nk1j45Rhv7ySjj4Kk+nM*@GEl%8nLi z?q3Wz>F4&*V>VCsMDisV6KzcpkpEK zNe+T2N%-D9tyH{mLN0ZZQ5<~77|JDK3+K%X^>JNl_pYXU=(T$v5uRfSBSx_W02E8{6sS&W1CD#{;ZtOh;2Be!byPU8z%8MiF|SV&0c zmuX=3?*kEhvOsPg3VDX@k=eWOY|PgN``W&%Nd~@w;7ajx^f*#{EW*Q!%WN!?|o36<@+3#~<*NmW<7` zMhm~5WANdjeR6P}*#+vBufw~Viq5@cLx&($p{^H3(jNRxD}WUZ=zE-!nhT&}WS{DV zbW7u^DV|+4cNql(5p}ah5>J&kujyl90Egb4l0+$+?cNt%*1>!&z3$AnW;g>*g0^cqL;AIo zul29}gFJIe)j!u-PV1qMUwU}p00Q(bX?>G@@aiJfyyb3RFNZ_W^ze`5 z$(w>>6WJT!L`$oq&;kyFEX7Gg@L%ti8B>&fG>lxk!HA-B6Ktp35GK8;uiPf8#wV*S zsFNTB_4L!@i!}Exh4*|W*xgWJp<~&=>Fy9%sKDuG?qQ|6c3|Q;zmV|IZ!^2Rmbob2 zoWd?4?#k!B^|`dyrj&-XzJuC}q2md`1Eizra_6lYzCdlx%f(h18wT6b%we_=U@#(0 z_C~OXI2I%-!NH1AM?SFLTbvZOaYoaY+hyS1!K5hIt?_a=N^jQAr%#`bR2vC~+rx1x z1jH7!UdID)V!Dx0k_d0foMEIS68)!BGxYPXoHDufqRo0ST=fY>R}A(4{KT6@NT9#kV-BuF$D&?~11vRbe`()(7~aU}J{sAlIxV?7?c)V&m-p}dF3q`L~G5(>@&t5mJHTU+;|5ctJ z-m>lR4wk$TPk8hFT=p}ix@W1CdgI6;A^VpKTdkg(#^-xo5PRQMYxwqcAU3f~0WMA47{pUFn- zoQ>be(pso?uL!RO=jF~&QQn?1Y|w+0F=KrOJro+8aylrHlbDPcu0H#!yn+JA%txUy zBr7SNxzlEG_-;_0rf2pe+g_%Y9Qo0fTG|#lv9IR(K+Q&GxAefWXf>g-_T=vP>Xd;< z0jH!S7WZE9n{&$-*Z;VBg>l2(xcFvb685@DigDgU3Af(Jo9jRD)_CpL5kpTH3>R*W z7>9XF-nuNoF@eVdr14pMx;S`_-K&`>>S=Kfk@?%%SDp|NfZAhL#87q%%!05sQ8ckf z3B0Ladm$jiPk(p7+v{aVlxD&}0HQJ)Jw;8J2M=uHGGaT~HN6cII)Zm}yV`GV%Fn$v z{wW+BJy9wkUV$01cGD( z-VEkH!*C(vDC_|3S(&7|Bg~v`GzIQfJZ?Lh|85f2eN-_q{Zd)y(QNr!izS`FFFlF|c1m8nAQs-O0X*|%Cafng2R1e#1&}- zl#HgkHr^}TO{N`F9Tttuj9WeHnt_$j%&#*^ z@M9_u(e>SR5e>F7vp85G9`l8k8iHS#$~0H_2SvmuwdYPZXyVI2CkE9Ya<;srP029| zTE52CO;#7OuH0(k$w->jH+VQC@A;>XLyk%!R9Av^>HhX!1h`z1oMc-RJA(bfMncb? zoOgdq4p&)O&A`v?_x);k9P*h&^H294=$+|YiFY-{yZYr$vXi+vFfnWDS0jfrFo@l( z-r745<9Qi=X{eA;?o)IV@SlnoInIG&7il28-wDh?4T4MpxjD^p!I^c7|Jq9U6S4lQ z*b{ajt=r82gErQE0_LL4nc=7KROZD*VXLNmmsxil!p50v!9VV08!JYIL>X_q z<1ljEVdN8Z!41t#^`DB|9Zm%ZJ72waT57KN>om`;XtddWI+D3ZL3O=n`S+U5&#i9! z06~k$W+5xVt0}Wq`t;2go86BoLTmZ6ToQkK0v)j1%8t`Fc`Vd9&kMC>rch_u_AFBA zqi3nLa^=c`gmq$PtuiJsf)vfq+&@MFP-~@}AD003Now+I@f{t$<4{=Q*89oq`NuDV ztg^{|;~L9uJB(x&4crd-CE;#vFMon}_TB!H7nkN=S?2DkxjS=*{pFMa(~;p{t$tAn zv%AhiKzy`jbWtBf^DWjOB{S2IGp@?+4B1sh-72~NE3Ve_)!I$3(~kl&#cl^YUa4=# zC5s7$XdnG+t~i>C!8@>W#ZBk;Axm-YS{JzopL%n&=~r>4LtqoIATUrHl6{9u#>Y4> zX*INMH9Q~}YA65Gse5oZX_G9{VD=k*U3(+B2jgxQ5B&O(H{|6Q@&${==nqP9qr(2a z=Z3U&|E6w&yh2<-A!MFo;z>kzs;vKqiOISO$ZhS_!}Z67>o>&GhFm0SRk>J`za=Lk ze_-7&Q3nWjs~h>8$7y<-rHyCoe4cjj2*G&+#NJekM+XrhZA?S-_1@V(t-P==;m>{_ zG?o^-y!tITSj3TO2?vjZhDyL|-SBQ#MweF5s0Rp=G0EApG!!C-Tx_XHFZ}${FPh-=M@Da9AMrrp!%*ccTPXwE7v| zc25hWLl*LiO#r|Qsf<&wozVm%Q_~>CGgJ9gs_1 z6;ekxKu#{$^Whj@REzw*)Mn#zQ&qK2G+}`b3j>xtDK&a%Cos?Nz471AwPGIO^{4SM z3vXYF;Czy~2fxd2x#;P{H+M|VPC!o=nI9=Dzy)XNqJf8wj;WLE&A+7wx_$hi7HYgT z@i(uzjJde=GMd;JI_J~$I*_k2M0-`5Eda-1N+Ns{472Y~gj$VTSl(LZ*;>$KMm2>6 zdTy;wBBM-=aQA6o*FeL^Yc@CP+_`Hx!?;C%9n{56D+px7SK<+*r->W&4GdNy(EN+P z5S{3ek1BiJ7cYh23h+=FQUjl*{sv)ESidr)+UuXUI+5vUFrDFzdkPQCQ|HbJ=Olc^ z`g*z>yPx?M;b1Dq8q8g|a54t>9ff2DKo5`IHf^S8=FEayX~SxZxon(l8?qOL9z|B{ zv@Gs{2^3ht_gKd)222#8Kq`2cZ9T7f{AhoCJVi_Ta#}0&fs* z3mGHtRY$4=b@> z6&r)TjKraGi#2c<)uL*-yD!3PZNoKyMxcb13uS)n6E29>AuJa!6azT~>(fm*qsJCj z0p8$P1RfHNI$7Dt`n!uA%V3~_Z|EAmbR8WZ=6*N_=Ln$QcqpaYvr1t!fZlzRsv`FF zLYqf#KY`x~!<&mft_vS zeyH&Q=8o-F?fiG2_G6Yg<_!JEHDkUmWb97<@n}~MyB!c)$!Gppdgrpy8z+#fplH8Zr@rh#+MGl?QgL3m!11tGL-#|) z$|-?orQWseah`yuO`xj5LZ;O|lD{HK{@j$%>Eiw-s5TpKi@|yrF$HoC$%Y>9oajMv zm&3zcC<@1)K)y&{v3d8-iQ(jZ1UlTE<^KPlPb+GB)*Aa25dSZKxBhR@?X^MEnbGx> z7)O$Y`Z5SUmbGnsTMtl>A8lJ^loAGt#oYhpvqz&8PuyIMZQHK&-PmY8dh9=ZAvXFU zg_)?yO2_DcLn2N4<>ls?JC8oBjmZ{k)~o?o0(2eB>$|d>x$Hruq$nq)_s_mJ)>)>R z7`ph!&YcpoX2mshm2o*S-von{P^z}9hJP`h?;1#k0DP5VE8kuo&H#}Ik-#Cysx;*L8Yjws;Vy{=GA}6qoiE6nDcYfBNhf892HUkr_%ZszLODZ8Qlp^ z#|Y_=134N&kt5L?W(n|6A(kl7$~yaoh*}5Vd=d4-1(R&DSQlMKQ|&Jt-uZ3+50gtm zqp+XGnE&O-_;H#VZLhavo_7RORTBtUu-xaR`u>YXx3)PGv#22IoCx~iqF@^F)f`RK z{^8~<_^74!`QgliQhDBX+BgFl4AQvr7pUVZ{yG^IxUT;7YKIK{53%A}EKhHdD^2D8 zEWF)iUDdw7pL5IiAI4P&&nPJrYVoa3++<75G@wkvbYFuK*X+aGDbj#m-2;q+0cYC0 zx1Y^7TT?av~6GI+8m*5;2P}^w_QoZ?CbaT^x9EbjQ#qt7h8jIe+-;m zNHL3Fw|z#kq~P(>W%Jm3=FVYOF!k{}ZERQ>aLC}d95{79i0O~PdVlfe0=b;gv$JI~ zA}Kaz-QRbx{_3#$m!HF=qLxU^gIak|Ihn7ZPH4%k+fQjI9J6d7+xts~EDx;>M&OkxMdO`R zhsXh6cXnRM$q@unh7K#qD<3_gvoIyFbg#ere>;xS%&v^)B=ylPvrioXCj#E*S{Ka;Uc7!eu08L;VSO7I zZfHc$s%>KopUhwH5RZ32IR-9M@#7B6h837R`AKYf(Laz8#)sAbr~y^g7i*2gkJJS4(UIJcLpT?KgpUd}O6XjId&l zcBIqu3)ca&f%HRqM|9FpV*1@Y>k*QW$=&hK{f14vKI_fYJ+1Sc}b$ng`W05 zwmaupXQiGval&Q&(pM^9rF~B9p2R=)i7o%)Gxcte7>06sJ0?JZIxr{}4x}x^C{9#4TTWF+#SfdTj;Y}VJOXvS%waK5u>;FUA2aJ2np&XQE8U1JiZXUgpR+qpcwxSDcA)r-o$=@H< zaSMal)LL5>oKgp>faY}^3rvQ||BC1l=<@T6i9M74B5n_j0X+9sjkXy-&K~*q!_ZNn zcKr9h-cA>9x4R`LY59z6577l?l>V`x;n0a@$CYRhrkXgch8b-{TprpbJK!H#6)9uH zP09}l6~>Of_Ur$nUVL-gEbXsbZM3;T(dR4DrPhvZ5o?_o*9`5kn)MGy*yIlFY*`K09VNcQ3#1oX^Gpr&LRA z7YIeQ(^BH*TD)#lhA~Toa<%f;w$Ef!ubz;eK7041@1djCC^%mnTcl2@L#Tlf zoQ=WcVtB*Le*ClD+0BMqwj4uGJIhPi%?!vX0~Rll$_6`^e3L$bu+*>W=63ju_gKPO zlCkCtlI^&!lbB!AJXv7H7ajc1{3i}|B=DYvC=iV zC(9Yfsi%RZ*x#-+dCi16#379BB{wPc!Pk?Q^*xxyGPDAJVw7S9P77!@ascuhEUa)F zDx(OMBM1tnnbw*KJn?7FoG~&nA-6)5bzBE86-&)zAjhjW+1PXaZ;#v9sqT8*!hK$7KGuWi0+)+R+#y~sE@9p(K<(00@(2)x& zMVp1TDL!D@D*Wxm-JMJ%iebf1)RrpIp|4NYFCN;{1UWdARO20?4+X}(`NN9-`i&RV z++-QnC=TH0$VbX16fiUf4O|{N5+Efc)0BdO0-KsZwdk!~E6Y-O)9RX;+=1MXUG;fm zSpo{Y-{ivZ=YKZ5Xb7>Mo?UsfJ1^Z#gs{J?x%$?zG8YzRfwDUZuO7jNqzOq8%9sqo z;aeN~yZ4*8&uYXecZvn^`{u(^h-B&CA$%ApY+l1h+??z%4yk2Qj}ajU&g^5k2NP`ono@%LP=7K+44_HHsDff8#))C1*_Lu90aG|j(S@yquByMDQ zu}Xh?8{@>=%UQ;r2L9D8+HzG3?r(WZ6vl!foi5u`{}f^)9Jo^p>Cdm0aZ^;TDwY$8 z*YwZVu@_;p>)z|3kR5P{YwUuM%T6+PH$0D2LJ3_Zo6z6oMD?F9mDsb*U)hb*SDem9 zw2`YsGoHK?=g>mM@%wiY9Z;TG28eTji`5=I{`Y4zI?pT$f4^0|B9)fEzl?5-=F($7 z?PU(w`TfIO?!Rf~b^hmHtUbz1^XnIZ2{Bq_(*&+j{Q;ROVPB%_38G%x6Dj7!$12ji zMtHo)!Xgv|dwJnCQJ!OGI0|4I9giiofM?;@ucrqwfBk6l|4+k%DYT~w~iiV|Tn|64eB;9As1c7r>)vDl^(7 z0Di{gTogpvpp`&hmO}Uo_Q;{o>8&ctY6(m9{Ego9o2+cQM!rGC%dE$c10(Zo6YV6E z&r9i<=GY2gVrqTrXJlr0{@kKzbyBg7q!aol@Nn$;&84Yt{m>Lt@x`%F&l1ZfZOJT2 z`=I{}!@Ld^s?!k+`?rV&i-ULl9?jb`F#BKlDRK0TNAL#Dvw(6W}Mf z=*Ln8y}H7VHoh@@_iuOF+@0U(E@&b+jO+)rPK@0+aSq3IT2W`jIf(lOhZ8;kDhw)( z4d1PEJ1J!8J_@;~FF08;#C&8r^^F8f-K8z}*KMr0?V=O)n8FVBQJ7 zb*E`ll3^6tcMP#~7mYPB#|V2!Ek>1hB)0-Sh12NiH4;|ftA*Ae7VYf6hr!WH)nPD3 z-QC9iQ^%5xe)sp6{S?P$O&y6W;xL>WeKDxR_~=T)m;`{agd0I|FN$w9e(1(5EKc0K zx$F{JO2}7`hajC9E!?=+HEr_;$+Q;m@1Imi3~rL&M(BJ<0Qe8|*DX-g)Cj z`gktGe*WbZs@Bt)H`8cyIhS3mk{TuW|H26aWjOC%j+z1{np>B#GIPqNcdm{;S~~r& zj1@-Ll0vyggv^P5>Gm`tU zjYXcOnayL7N2cJMMFXq%=G@V$38FY7)7f+8urYxAM=Doa?fvi_Eg{gO1O&#Fr+ zSyI?#w_yZv0uzm9k|xx-xZmU_W-4L)*<9B6(Y91rs}FUeq>{=yW{)~oA&TMi&&b7l`l>GKi$r0!0D}rR z)Qw|}>(8~Ox~^bOt-yoj64NG6nbLHB;gxh*sMdj>qGgX-(gzem0L2;mEP$4QL{tV5 zg$&Q@l#Tx(6uIFzO>zra|eid(&{`5ma|@00v~+wG%D)nSs) z{%c;y#*0iZEW`fce`%ok{eCbFw>3o3oxBfMgutHk8+=5gTQjftDDWy2n=b(&l#(6g zVc1o|GASfl4(=Im2gb_MwB>Hcm5{k%gc`UUIt2-MApI|YUs_eU#*Y<%+A<-zAy>@r z(6fKWm>X6Gge2rB0J7(tN0b!Bp7j0wc~mB{y&L&X)8JssAijLL80-*9{{~@~KU~JC zyT+V-g4eXoZ5dy)5pE~0hdjfdU^~{U{u!38?GWds%mChy(r}yfhx=if<03Xj0MArk z@)Sce6+GVAG=GY`UA9I4&U}q3^}^UkXFWolgT_Rf2{%ZXZLn*=C^Xn$fFc5WSoG3$ zqG(Oy4(0$`*24SPkIe+h&kMQxpn*_xs9&P9Y6&UUj6C6QI*5B!tnG8l3 z)YermBNDl{i}S`n>CK3d|HHI3-SP&50b_XEQN)zuWNwg@UZz=EA`};+c0d*u(@7Bo zA{{UsHf*wqs)Fb4ytUqzJF-gkEY0vZ1qC}eQV=7u4Wu;K$14Pxr107byqamTFUvOe zL79eVDH#f41lp3BP5w|8X5L#u*ylV2CtD^Gn_}J zkp2IbxnfcRrddQBwn9UmsWN%-a*z7ujK{S_?%}y!=BHYQFm9zq_^SH>%wmWfQkWI% zeit7A`I6H94;FBqd_R)E|D(}{7vDTbw!SEQu`8ogpH)imSnI@1JLZ_}yKXH}XS%m% zqfvsm9AB2-skDxjA)%ogk3M_q#-B91P5R^+;WKIm!F*SfV;gxdeRVWEAzH@NNuIlO|yN9+s{djnn)tBgRt1t*B=hmC-en^y;gMJNDdo#x>p#-?N+-Z!ooNk-nRrqsW`a?eDwb;@W04y;%eKc`2E8lUbF8 zeZJNZXkd23J?&SA*U!)&aLmnspiNFR#2!7mmOQ6O^&gKz7kLCR-Kqoxy8w{`Gv>TPokv? z>~*6b<%waI=_bk3hF)GJb?v*?&xtXnH%Zs7~ zH)kZ3^t*&Nea*jbp()PGd=aIPs~hI>^6=Q&+O9rQvNg+eEhajg$7PwBn>Vzy$c6^4 z-X+^SoVF@iZDv}Dn_c9oI+vDPmn%EZKKx_l``wmuIoC>5Y}95HkA5W4N6rr)zj!fE z?|6Af5v;d`LRR8ck_QId$Fcg{=bNacODsPVuJv=;& zCxZLe{r0Vk+1{M|e5{B*k39j?)_e7w*6J}6YHD^gxn@Vdd(BQZvZ|ujz4GI?%C0XD zd-^Lrep}kEvQ6b08>^l5@hH`OE8(26dF$4#4Mh%3>F?kqfLE=>tcoiT4J@`kN*C$v zktM$-X_LO6pP%UT>6^s)F^}^M9$Z931U)$1eCe`CKLoTk=WmJO7!&oJGj<_XbCq({ zb;DNWb+gat&3P~*J-Mah^QZe0x&t&sFKEF&C<3B^IhEJ{`0mS<(H2u%m%DfHj%Xr)DVUR>tJ6LGGh^a66>&?*<0w@SzBv_L7sV)?2ko5lGD=GIXOAS znbfj|T{=>i`@?(+mw)mpSa~ijPVS9q;Y*n?VM2J7jj_CNikAJ2qKL(-l$trN`pn%6 zdyF|J_%d1&&jsdJcFrp?WuLMl=1sr3Til_Huw6HD0o>i|*drTHtJhjsMmShhM5GoQ zVeOMm%AyU>IJbZGcXn{ts4VLARcU@O*!})@w)x*_Pj++Xh%!is%7`~^h~F9*5OC=w zh-Kma_3>pY3K~iR>~*DrneuCJ@Auz++qf!v>OkB)MX%Lqq3W~r^rL$lLB`F6Oix{w zjV^oPshwN&rO)8K4YBx@L^kc)m;EFxuDIB;s;Y|pVB*rHI_ZZe-@Tq1(YxY(uIrIdSgI zK8e`|GwblbN27yvgq$Fk!Pd7HiV3Fj*T`sU(y=89s*xVFy+E?M(tM>q9^gU9dj+BOr22&ZS3l*_nrHe_T+? zHa!>r0;2<);Mt&hW%wg}B0b?9+~rI0XKmP8>9}9Nj_~sFRX~G3BQzy0P8%@@%h%q? z3Ga$^d3OdVyD}pNMjtF$;o-GJ}%FzmeD1D8X69} z8hdei(~)~?wZiNNoNl*;;XK}7n%lv!nHOUTV@os_U z>xN<(pWL^vYg&VL5Z>Ehbx9L(TA^N~F&`ye=#r zj}4G)A6kBAn!&~st2S73fg)`oCMJdr_$1^*vAyTlivlwr$ye6lKdxW5b8~gI1tZjL zx&qebqi^u`umc7Dy8N1ed5NDYc)1iT<<752jlH<3`{(@GhfXGDWUPmq?qj%XktfO9 zZEUug*g81=ar-K-%cB^u!-OR)aNja(>>z8AfNe4&JdMlNbNg!<_e{KTz3BR8H|aBb zWK)9gZO8u@g5qe}C-Z7`T2x9r+Uu8nJ_@_TxYy0jdK)V5_o~Rn2ixc`F8U^WFt4>a zbpI=?srw(GMNxq0OFERl>#@2SWwO!)>|9qh zxMZ?md&=^2K$)wD&kHfKJ!1TtHQWuMrPH?sK6q@K<5;+J%Yt>;vRCuuQswS+Bk#h! zw@{peshN#G;&S%)yVR&0zZc8p?~Cug!JGR^`VJlHyWi|fhaV0!tRaJWckaH^hWpH> zT(M&Gz8TWj^7A))dU`H6teLy)rf0oNvl36PV_P~d5sNe7`E#o@)2vv7xN(NNalo^& zvEe{CB}LB}=IeOQKaY6%t>UyU!n5QyopJ|lXuBjkZ0st6f}^; z$yds9M+M)D#hHy=55Xh;fPZd0=JevS({RtOrrm67wJ`yx5n#Zj=I8MEPDa?$5RXAr zUeLM}>yu{M21(7mx%bWm>9BLo#`=Mq)vw}m)G+GSM_H*-FjzNXQj79B-8iFX7*%Yr z=H$S@KyvbGhDZnk`I71QPB%MW*%^?QtjL?fdq;-*;+i|Qq2(P%YX_>3rgVI+=XtPe z0jpgMAJ3gy%vw5-g`-gEVY)@3q#m}y()3|Ga^qKDB`k|Q+++jB#Z)7_EVx)-TIxON z*iqlIn25;8sB_bMdwcDjov)S4dC%RJ-;>zDdN*2m{+WkM+Ox%!Tk{`3%5&FZue;EI zN2O?WQ|6A+|Lg6`!>L@`_LtHm(x4JWgJh17C}SmKWFAA3naZ$B85*#gD6>k)kRg;Y zDk_yNQ!GPBlFVc#wD_Gjdw<9K?(coS@BRMyIo8qP2x~ph{oMC8oacF6*Zq4H9cLw5 z+5L`9ynS0}mT%TGnG>eZVV~`3Kfny1_*7I~JLvm}zCQo0x9gvYzdptt962UJmlUnW z-L7*G?F|qvpmV;)Q-gBbQnXlQlUAd+gjleS%+T<{Q@_PgnJvHdmSO3S3KxvKBn~yq zO1Fhe(hR4t(es{_VdZ4#%q`dQnwf-|hNe6%D@~bt;m@;P@if(glhunxo^sqcy@f&% z3u>9kvEeSsF%t>C9d9_f#=u+%ru}dJGYNSITnp?a{58d#755s7=RaF5JADKUc@6pl z8j2neVv1EQ;{2hvNiEQN&ZxP`Z)HKkNX+Ni!n4osT>kMPTAPPO@mJ&y-@eeI;?tJ7 z>haeY$)8vo*D?) zms? zl}fzE1Y=eDKtGB`r)K7^8h}H6{^fBz9YKR9a_}i+WBlcCF*#ChE0dU#mDP}%9{-aE zO1|f1qV>1m`3M8FC-P=cX*{wGbV`T?4QqTnqhZCPE3d!a?^z--=X)s5AEzNZ2YNu* zZ(@^SAXCPxtrZON*dy`+=RVvoNtH`^4ECZ>0 z0UFtk0a`q2Gl%@cxsrZ@;*(z?%qWe%c1>BmrI%k<0)mU;au|2iafxG<7W<>sa&Ctq{fKsiNMoFbme#@&MPfjgBUGhyN= z{d~LP)Hpsf1vgB{jpih7>Oyz66C>&H8VaiIcIp0;-J; z?g}%QfBE3?VoGNS#=fefF=sGL=|y>87?6)N`woUfI^|p;1*g^U@T{z;dl(PG+G~>@ zto}^p=X+d@kpHl;u?1CiX&>Ow$4g*?Hv9;(w~k>=)n(>RGavnBxT32h?9U$NsG}=i zemp}TVKS^hn5+G%fs=a-*OTZNL%2}}r=DnA*)3D+XYt42qy$NL%a<=0=cXd>B1FU? z`#hBpL#E{R{bN=XLWwti$Bj*ej2odb1}25)d|$rw%KRC-zFKK<#2pya6Hg=XiZ`x% zo2>I8`60*V%^|jTcTB{t@>NjtSwwjykcpO|X+}9Jh{<6#e^38MD{J>>?iam%5gWD& za=AZjOVnkl&s4ZtPK9ec-@utE{r$!OhV2R#n78y z@6U$)@P{xXoQDF2(%Fn@`bbt~3M5c%NhWprv?iPdT%6If)3N`R(S>aq=T)S<8rT5@ z)E;qKPF`LrELBa_j3)D{RlS(lVnsI&Q;Q`!X&AQXlFvF#{fXE_2~qf3&3n?zkgdQn zR$~Kmpm!zufTdoPt7-7)oE)Gc3UheZ=}vC|N4DKkp-;Qst@5R#>_9Xu zZ0-Bf?@i&btRZ^O=fK&E8a{0CNA}r=DO_&Tn$4lxLOqk<;C*^x_FT;6k48JDbu#EN zoifz8DrQAA9c3K>C)!CutI5Nq%*Vn&vOPAo6ow@2H&4s%^E5xqmG^VFe z$Jl4G+UpY zIVd|j92j-&c=ti1BtE65V3lbPjxa>48au6)SxiY}LmvpW?Zv_@*AC%Dww$sScQ9{i z%u(*(qe|+Wc369FkM)heu!j~pANw;Wp|Ki?1s zH^}RYzXx{dwL>S=Jd=vsgz3a#aRv6)4@u&_yGBEOtJ+RSXndxlBnsjj*gP2?i#?j4 z@)pZ&1eq-AF7b{QhL2sR- zKR4fa$FiLM{!o7A{4a09FPBsB1knyYsPtroOf9ewOdff}Ebg-c$jD#E7m*27tYZ>h zxq7?frzi5T_+1r36xO(FepLz5c*+ruWM!5jgTTwmGihK0XU??dxu;vuMN*RK0a=X=GFvSB%Ot%9Vj--_g_5vh-$Shdy7nSXcZn1MTZy#>k98})!p0F zB8Mkpe+&`?DHz)_ng2tKad9!-o^7Ju3z{8JAkC4}BY*s;#Mc2xq9A zGo$&4YKxy)%xL;6Y=ioJ=^Qu3=?L;!Vgi3&wmW}rCMocYpGA^YyL*Dri}b=mnemAU zJD|mGY=RWE2Pd9(>+IL}tD4yLPNmrT!bABt))a~pGr83gVMbqt*lLOUmfSV|(~v2L zH4nI_`2F%Ka)}FKi`ayW88L6hXU)1q%Y8tJ+CrIm&rILOe=ERtHL(j`On-bay?;s# zhyT_0ZMTge?y6aRcW4vN<0ZL*H`t70gR?C~RI|Bpvp(!aAlL<64=#*h$Vnf+ z*_2A0|1t`bZliyJCeJ=c>Hc~SIO<5X_bXD=skIAunNWCeRabE9;BVZsPp|Yg)-WuI zuvMdmaOMAMJ2TM5aEPnLBaVT>mavEHw-2YXDqU_N;Ydv6?ka2#} zt#oTAZYTkr)?`u>DFS@UcenJ+fPOg_xeU8>{tY+qOLzr2dA@l&D+cw-S&kCM;;$l1 z5nF*-8rDB)%QBrZVUuZ=jwTj7;`s7-@6KJaQ`a~=$BsoNCWo7O83syE7&J~pNV(VM zBt>`>ToOXi!*SJ_9hfkca<|D##P)9D=pD2~d!a4Dmc{QZ>Jy3j4|9i!*h-*}gjft}Mia#3b?Pd&S<`*KH4)q?eU z&6AGq!lrDR`a-ED)|}5r3SX+HeVuXlS|vZ-uQe@UYZwOfxN)S$EypP?E%?J+{Xo0E z&~92|LbGl_a(z`6oJ;spVvH$jW-lJjMS9aR=5i1tj^|`}rt#eji#MB2FtdA=L@{?C zo;PNdZ?Dy)ZM=`W5{QBAJ-W}-gB@Vy-g}`Ith(FWwyutm6&I%aw{Xmy>xpH??(fd_ zq`96?du14eR1}5Ub^3YDf@oNHdY?(o7hy*9M#Hf?R{Wx7CeGI@G&WZ3Y`1j=ds)>c zq2-%-mBPI^bXt$zcH(?AcFt0=et>>row!lA)aN5sg$q!tNJ4(3@&(l|^#SUN zp7`DRZ^=M(&>6!p@e?rzw7#0Ktq%XvZ{FB7dh6L2VD)f`C>i9i&2Jvyn3&{Yir_!^ z)~L*GK=h8)y2c{c%?a%pzf9k=J*a0m;=D_VQ^VZM?%7t)?A9~S+I2WAAGX=`q@A6s ze$!erlY8e2J!{!BzQFy4%}jaeuJ?1iij10>eoKedi@YLdR09%Qh$@zIrd<&`6HN+L zr!lK36;KBF+#a)EH@ss}o+058fgZlBezE4lNqE5?TAzPvu-Sb(px&q_9-tWpUyKGI z{6}WFPP2KNxV-r38y2h{=rm6td|)i9K}LI>G7Nc5#k9$alxDS5dd2C+G+nZ^-8o`& zDbuqx{h2_TmJrj;0S!f+^t{1vzxNZg8o7KigWN%liFO6sKlV~DXV=6$6I0``Z9M7Q zW63mFvNoPs;=NXA+n|!5sv?8-t{q>u^hE4flGly~K9rba%j0*BkBun>byh`Zq^AqL zgz2#A`>p>|EJ*Wv(>Z1{ImZHX&USpbTh@wJjV)l=^JEPu5$!-kMnzh*z-ZZRe+NE! zgO>H4O$1XDQ|}m^x=SNPyt`{fy8Z@4eb0{@Ib>wD{%q!m`#ytJ0x6id?`mRD^Swm@B6=tuytszBEh$%wV*y?7UUF7J`iHcf@0YI4_>bY|GQg^Kt0@Pvt zZF#&~s+B(%?VZ^#U-*LA{;F9u(kC%giE9i8QQeHzt3TdXG}~uX;F^p^%Pn847g6kn z=h0Cl{8nWsY0?#U;|tZoxcfeBM~!%tG)IMT0^AiyT6*eATS`dyZnJbSi*rv4oWar7{N(rV;8 zYE)OFRUtAix(~s-p{JPscjT5QuqKVM+U$CeIMRI{Lb-8!dsknS1i(giSgytegJd+S0eI;OrAl{Af~vTgPGg!B)Ff1& zYCL8Byk=9E?eEYtN>@EfB>}upRE$(`X%Zl6Fs9K}`{~?zfI_Zad+U;E!U3(*CiX1` zsUZV=?V3C~IiSoXCsm8Twef}fH>cl0%N(A65uKp|S#E2Kd6-oXq%{qgV7DuK4xQP^ zn7EO1INv@6TW`-sw$fT!>ku_4(`WbLb%G#Bg(k*xL{=r2}kC%|d4ui_$}>tB_bC!qMN`>9E&WSKZWp`@26w zi!f?%Q+j+Z;;vJVbw4Bw?Dk95O4m(u(B}>Js|Y3kBWSmzWXF;+QOQ601_na8e8bYW zGp!J;HzMH;UFQFo^7G8ksT$Y7MGBGE?y~BXG{P_ffULNAN zYuy&*l%`8%@6x(i(r(opnP`YxX`Cn)QwNIxJYbXGvHBmyPINVYs5F1z>o9sftRx+p z@Kl$#fgw~iHRQ<30DIqZYOG5B9jp>pX$Nzly{!J%`dy2IO25Yku2?rotx{x}2pC`r zI$2rsYvWq&1KV*E$U4TVASB)Y6_bXnrcm$6fgJ+>k3k8?qXzhHM%dsuxrrr1Oz% zL#z3}NE+viUJLer&c^U~>+0+E8dPkPXLbGOSt39RDG3Sh41vEMiBb}|SHX8jsT*j9 zE<(D7J~zIUfuU!5IhkU!SpVjm8#XiNISJ|WO4Q&B?xI9YMYQ*O; z+rW9bR7rOcHs*k+gKugXr&eEFl~NF)+|hm8&CRVYXS;iW&XxibA;PAo)B;3w*?xF` z%wBcve3Qx*-u6pMi`73$V3E?sb$F`|+>>ywZ!7lyZ&V+bEvENrOG9Oh4+k@W4@dcD zx@x!(QsBCgp^0Nx)QW{`GtWHN+IYMHNS+(??QZVw^|sm6XfPFAYmafQ z7N*`xOXi|bg8MOIIw&4=o@L4(Aq%t#hKgdYVaP4Vy~Fcvk>GbE`hWhTKo#ZKno#h7 z%RsQtvdTYdwld-Z2$8HR8UFCH1f;nyFLX2OifN1I3Et%;--jM(7Q|0*jx^VUu%MWm zBq1RbI|QTgH(vb5=X~(u!n@!KkD~c;4YmRP_VMT!y?%0!WRK;d&P;6kWI&WCXsySxhW-Vl8fTuo=HCBF9>a$59two| zXIjJ1Vn;nDRze z_Ppa1wgBGcoa-eiHih5lrdSb`jK%4(kSo( z@)5=bLQg1MRwu`?T8-=!1jk4%Z7~^b?8y=zw$7s4He@Gbe6e5B~S)ND=@rUkFMPv{W8BOSev(J1~M5g0gc5ST0i{T<7n(Dcd#}!mMnZ zMqqtdC_&EF8{Pq(%N@onfN>BiYwr_)S zjKGFq zE;wkQ9rzarF7jl2Ltdljt@do{Dnx76O*+jIlqBCH%fQ@#7Do_Khi)(3vHOyw0It4r zxLtZu3_OMo-%bdzK!=>(_gIPRA&`EgqNC^SZCOp(beb<^zx+}N+Aqo~$d4%nIYCwM zB}vE*@aQz%mg5OV@*t29lmsowwYC~kp;){P-poK?uig5rEn6F=Yau>+jBCkLi2ZuC z4tIgrBT`MOkI(oLM4=d`WORw*LV?YqxScTEP*+cOq`vE#yn(fOVEDueB6ZSCIC~j0AyN>FK)(3yga|N;0=+Hler?&kNeP+jm25+E zwrGau(myr6gap7aOjpm;RI+V*RrsYIU}+TLR1oRyC4qe~b`ZAdA_d`LA-*98(qI_Z zz2BAXuwzcM3M!+({cH+4FHn4HRxlvYf7A9vRNi8q)d5^&KPGI?X3u}yXh4o{mr=Iy zrB9dK()6}YOgiN~rz;ZlQ6YQPupREr0~O=0Jv9j_ZpdkfC&)qL>VZ#B52BZc{j259 zQwsq9hU>dr!f?nUzOBK1nkPQl#e>;-4D^dC>3zZHv~ly?=Y3zpa5DZ$!Py#;-VP3Y z82Av=G|Lbpv#%L0mSDp5AX5h4TWs!74OOj&E^a_wDv4~*n- zd(%CXatoN_bCcPT`VFt&F(;j;>HSIV4Sdem2ItF^7J^;}6hCeI%Wv=?AF)Ys1cI|g z{6)RqJOGF>eQHpvks&_&LqVo>j`TK5zED2kj5*P@7!JNZ^}ZUdV%ePIAX*yvjZ%B_E4~b5RfGTb2R{4f zBL!kl(jAuUsX26m@_`t(Q`uyDAV~ZdS_ik`Z->zjP9Z8Pia@8>j*ib(FdU4%ed6h- z><2NY_p<)EBluGXuoq}2p6=^EbR&DwYm>t;ClS^EQ$SAm4nLl#7z_yG+i-&y0<*f0 zv6{cXI?KkIy#9A9!(zX>oJD;ApX#3%h{X#rrPwaH+(-_!a2(KFl^_Mxh5-8_(3HfV z!6)6jZb2N1xb9I+bb)gNq0b9W91FeS#+pNwTfr+rb=ft_yLpvv>K2X$ttN)}%n#%$(;lbSF}&>>l{B**xEx976X7^tg}f$LmWtseW%HsiJrTSS zB{+8BTtl3SeaFr7Hp2K)t=+N{{*4nOu-LHMC!QK?INChvzw+HB#jhD_@J52rEa6D$ zL3FfaqpWZz4K7DH2>r+R>K;TI^J=q>q`U>!T8qmHH;rv^Qv3C`g4GdI@A9&}aT`2O zHs$Fqr^k#b=MT~Y&n3@iuk)A3C{o~WbU6g6P=M`;;)ju1nO(ZdHF(#=c{OvyVi4{i ztQJhczRjmDE2yn_zW#UGwPVIY;i(MCs;Q}9WAIB$mo9~C>2QCZYUBlcbyiO#frJ#I zM%LDkND~Ru0$1g~82O7M;3^jwd3z{kJWw~$x39@KNYY*f5(1n}5&3P2>=#CgmAJkM zWC^4S+|OOy_rZN@=}9dK=Px=isg3tYC%GYsVh14axRr%SEcMX?4;37Vpm<0r?e$bP zR)mm<5J@zkB*Z$^w||@MD!QFCQ$@VX_at$73&eEb^477OFap0lFZC;O0Hy}OA-CD< zlRRX8b>L2^(FK_uXxZ7WzVRkI{DAp;5*3l00=WlNSjNbN`5~k9{b9zhzxCk>?$}yi zOO=TieEj#qkOP1?%tqyY!7Y)0$)zx0M_5`db16NU>2UcC92}gJjhM>4#pOH*CSIFA z>2*`YJu@Wpy{us?BK5sH^{ReTvrzgAQ{48|TrI45U^z4c{YLEB;eKB!^@Mi`BdK+r# zTrOF#9!)a&koj0dU%U0id2J~6dt6M}v_{`oex84>x$8VJ0>3e7w7_w>GUOh}kkFV`7Y-_61hx0JYXD6@bW1?jBUqDFg6fFLGkkKz&} zEpGw&E{N27F&|gOM#ijWl|_UENMU@%vHdrNn37eEy*~B%`T2#Eb$oY!ZojlHn-&3` zGO;jd3!w!=4izposqnl0iOt31kJ9^(nHs4(YCMdn>WZ4JP1q(*OSWDG5|n%i_bs@Z zWa9ZXSp!RuL3CTL6qB>{?zIu?71+JuhHf1=5h+K=bfxYn@r0L2(vtbF*aiCkbSY@V zIBoJ(ZWOwXWv{q6`P9YFOw!lPXzNcTLCS8WTZeOjyA6W+Ho9t8cv;==W-I@?*(^1o zaBbY|_B*#NSm6!eUWd<_RNI7<*7$6)i=VBHQ;&}*J^5i;Ti>sinl^9*zm6zDCOVur z{nOk`*zM(O;C|3;MC#m7jK_rw0NPm?e6u06as2Ya5YBVvbiU-46*cgw~e11f0TQ{0@@P@A|JgZ!72~ST{qeQ z`P{Gzn8BEXev9}S@UA!f58C=Q}s=^zo20s>i z8-khjj7y>Uf&L0CKQ^4T>5XmJJQ45Ej|=Tv+H~2GAiL-P>hR131*JJUo%Sbq-<0q2 z2+2Vb2{2w|i`I~b`y%C$0Oh8CN&B6W_T;<$*_i|WThzk+Y7wcD^U!e?>4mO56zO+t z??T;x>;sms?X&^9&S-C4eVU`}P2138`|4xY;J!}rA~gN%QYC5O+P9;Mapu_Jf7END z|4VOg@3**HxP6;vA678LRr9Q4S(L=TUq{Hwus$L+d0@5{^X3sClbir=o)S~sHgw;E zrVgW>Rr@zo@g1a6f2u71TjVk1g5B>|YytU8xb8tw`E5RmxN7KSAg**cAFG+fkgQJC ziPgM4ANvrAi!keb?QO2SWEm6ww?L#|k2*Ek4+<_s@M@aN%@t7`BtE0Pkzt8U#jq%m zB?^?Cy<6TaVV>UoZ!kFmzkOg~ShDM?(1sg0WLQvWf>toll`~wemOTQUAylyE4{*@N z9?6DU84^PCJ3ZCUg7HG@fV;k6M$Q)DC@7(ngUUELtjf-&%N+{3AL#{y1#iKeyqv!mfn{lT((BZQV;qC=&AIugHez3)F8yl@Tjn(w^>2@n3qiS?wP`~ zoMoB!PZ#XTPG5hVd85zs7N^UOYMW+OT&&(~_QV7vYqFi51up%oI}$UXdf1av>sWFj zWnp1}rNPn1-RD4r{h@F-Qo$~541elV=PVde>N3pmu|V^NGdhJ=Zmhu%l>x?F)DyM&Vs;4crfN6JRG zFYXD2O%Rz5)wk!r)VA5aZQ}I$<{iF`p>1xq&!iu?b6MJIDAY}iRW~*!W5WHBa3ND7 z4xd+wM-Lf?C9t<#fF=&JJ+5(}>{BXW0~S%#5Mo6fbKAS{I`fVP0mcE2qJf5ENJ4L= z$}vSxm0P=_WK5BHvCFHIzPZIYJ0g{ggP!K@KuL$C-H|i*ZpLXa^qgLol#15gEy4Ao(H!6Z?>ZXHh^%JZJdo376* zOn~;p9`(<+I93UB2iFB0N=a|x(1?%@a|i8}HDLbSK!7OA&gPpSA%nnxXl6ZL=Fu)x z=pu`wl5hqF5{L=ETbtXzkqhFF30{J>Sqh)-i)_jmQWzsuU9!0xvVZmOu&Y=g`U z&8us}a(ZwqQ?*=_nCljXVRhZ|Y8zpzAb<#~>Y}1A*+cYO^R6*+e+(umsFq4~X)alG z>hiU*dN@BQGRaz>+*4VRjvx{K4@wf}_mc1(gf}6RTb@^7Xmqw^$*MOtI8Bm&WIj&$ zDmh<^wLi4Vy}N(hBlWU%QESz%ev0|^@?4WU3_WYjrfb5->W!Uy24{Z^CjCM0L_O4e z2TE#y?c}A!3ziv-GtiU;(pmZJqF%YJa&pp3u|AMUhNDSZ?BZpsx;!m20P|B zwMdx)SgSs`sFMq@BBCB3Iz4&`ep@t~CHBWdszIufU&*RZHuRufd)+L|$Vv1`A01Ga#5i$iZ6 zvH9d{d~cVrJEO$PVGIl8QU;>!7KT~?arY++Kc)y@shEN@UHH=E^n<`8ZkfmyJWj*O z&n|&sLZ&HLY;li$fBk;@v26sDC0|J^`4+-XeD5ajJCEJt+ZdrgqtW^QhBsaKN?Q46 z%t@4qkiY)<-@TXRJW$qp(`#_jO9NHcMbL@wiL+NG9;+$(pv*~h9W6sppLD&=ZtTD`L^h*ltEI1U~dL_6j@H&CRz{SND- zNPYmFtVClMYlBEklYY^(u47ZSGCnOD#h@+&=m3=J;oN zn?Gm$Q_GUt>gre!q+~f!3a*2U?#sD$CL$xSl&S|inhFw+<}q3!L6E4IgPw!h8XAFk z!?Be`Lo21cevx|9M?WPNC5D$CC?UKrt^0^<4GfokdMmIMI6rQepRckft7aQzsdGO$ z=i(e9TL<;IgEvh+eHK7h$3424+%M{nqcP7>S-A%C?3@$t7*M*>i_tzr6a}b!ENbxL z5yV*R)M5Nl2lov5BOQB`v!GT2IxaUjCY2yumTTLaC4d}XVmP}8#-hJ>eZ1+ymMmc> zv!8D^3Mkjlw9a`Um-@r75Q+}fAlQWh5zamkVi7w_3091wbH8J5#B5eoHE5i@XqY`% z%H5nhqTIy+HAown-Q@CRe_oM&GFwqJB~hPOM04zenGWLuSgFx%U{c~^7kob8%$CI; zJo}7WGiAn;H4HZjneZ6T21oyT>rr*#i=}cVhxe~x5zat7T&K(`k#tMBoI$V2=Xz-S zXf(Uadt_G_S6{&XA5TW6enkX`YiC&MCB{7)X8cPgWwXLA0R<4(@MJujKA!h!_$7`$>2iR>~+M&bW)=a=JQ_(Z*Q zlWoDBHz*W_Tgr#zb&^XsE|BtxJP8By0PLn(X56Lg#@)w5xF8~eGW}-3U-upQO={`Q zAG>G2?RK60b-u-Wj)u^sG^x4jdmrniv3;ibXAcLt0yL{Lao!DudBanm#uF)(H>x~; zxLCE$5q-jJci^m2(YSGu_@!fuZsR7$-?}lfws6k2ajFE#{Cv#}<#XUaeV)#R*{QBN zDm4y(8A_%g0qVxK_;_P?Ay2sdSmFk<=4AnARJhg#0TkD9@!Yprs5$*OocoPZncXs2 zmp+Wqt`GDY+X0UoC4E7Ugij~$CPD?4NZN*Ky=N*vj0c`vf-AX)zV1C$(B^y}_ZaWk z?R{Qt<}_B_X?s@$xzJMLIJ%8nr^AkW^bk3c4N4>l&R!yB7ABUJVM$-pep2F_1f%8K z@o||A!p3S7=ZBAYj~($q6@E5^#K`A}l!X-fMgJ;|RtoZhRAq8vDPy_CUzcDCYR`+- z;Eg(>9yD^$zze=KSS=>9f=Fh(!z1Ea_aWPi%H07D^bG*&KJb@RL#KC|1YYcu zl4dFv_5i5>tw~$fVbtF-;XW>fC8+ItfmUS&aML@LA)(826EIK5J`!8iAR<@bMJw>? z(Dyp5pKelJB0GEL<0R-^^y|62&)ej7gp29)xEAYU=Dg(;Vj{8@#a0-eMiK z$=c!#&1L!-$4wh7>9P(>SKUBxuX6Xn;D$w_bht5CRtq6^n9d!62Oh#ZBF|QzelJld z(tjk(zZehn%gOteacv7GG??op8oI_g?Z*fBiAS+tS33xy1B@*dN+C2wNg#?Dt3J+i zU+GApJ-kFMISK*i9zz`50Ep3TVi#$EYiVxU-fx4pL==%rzpYby&(Ccz8LqPUqRg!tpd`vSk{qWoH`Ki15Xi0I%*r!BsXF z`PbDjIxNF$JXOl2^QPBSxOct)&rG}QtR>;*fQazy*E@2=AC~zH2sdfk7!?;+SwPM& z$#}-91HD|EXw?*L+dWlJi>B@sRTHO}5={i+w5AN&T?hfN#mTaWpGFRz-(R^+~-`@;#m}I`d}qfLXF?*sKg(VLt35p63SGPGHOZkMG@T z5YdQRCcKW6UF-k$TOKAiCZaS-1EoD^nBf(V8GWQMt`&3H%W{jbr7ELk&9Zjac>iRc zlgGI42JgASIa3G+s54(CWrzjAsA;kx#|P(VMHvklOVrqd-2#ZB5D|M}JoxQCOi|WF z41hxt4_Ct*U1d*tbf8bIw70mq(NE~~>^-N-%!G`?Do%lI?72ASyZfR%=k!N^F8ICo z_L-IQE=_i}UAEqgR%fORm0BlJaCExwelc`;^r`hfE&)jr+BxT*0fia+GFhk(CXgHr z3^b$_p}ZJ~%E#!rsc3sxn@X9iVJXF}(sY0=G76_tNP>cHPRBA{j9f>$mgCl^H-BL> zahuek-z%JaIoLCakZtS(e@1u4Y;OiBh4GxGM%RZIj_rhbS8H=#q^5!Ur}bvcrjV*7 z;zXx0sXGZpc1V6{BfIA^ADsKP!t+-0NW*qr1_C>J|qF-cfkl14W|wpvdI}diGlq;FNIqh)MAKFKH9a9s@ZI z0CkAQL6MoHgRBzsk-Pcb{EJhWz^jtXe!VlRMdMk3KKI>_56i;EvPqB>;p^Uo$X>f# z_~seMd1+!$JmRJ>-|8S1#--!vPUBtko~%Uey7>hbeurMvQvC&-zrHRbyTbs9Xr>jS z@aay8qGZ$_*b)A~(J`vmf}}Ls0rSa9#0l@&E^nFs0%B8X9WK|8pn`{@#-twyb^+_!ghp2B1TAH6{pm+EXWeMfGfkDH zX}?6~(9k6_nXkx9)+u83pLcKP>`X-7W8vOY;RWskcQGW7Q1v7R@}8UYZtZR(7<{|p ze8y8wf#~51=dT!Tca0rjB;vNklK(Ep~P7q=ECvIzEhxs$m9V_D{7MdrWKMd6w1ga7C4i>o9F j;D3 Optional[Any]: """Read data from cache if valid. diff --git a/murgtools/getdata/getDataFRF.py b/murgtools/getdata/getDataFRF.py index c7335e9..2da804f 100755 --- a/murgtools/getdata/getDataFRF.py +++ b/murgtools/getdata/getDataFRF.py @@ -23,7 +23,7 @@ from murgtools.utils import geoprocess as gp, sblib as sb from murgtools import config -from murgtools.cache import DataCache, get_cache +from murgtools.cache import DataCache from murgtools.exceptions import InvalidGaugeError def gettime(allEpoch, epochStart, epochEnd, indexRef=0): From 5979c16295940f7ba3602e01cdcda48fad94b810 Mon Sep 17 00:00:00 2001 From: Spicer Bak Date: Thu, 16 Jul 2026 15:37:54 -0400 Subject: [PATCH 6/7] Address PR #58 review comments - Remove unused timedelta import from cache.py - Fix thread safety in _get_cached_ip() by always using lock (clear_server_cache can reset the value, so lock-free fast path was unsafe) - Remove accidentally committed .claude/ and .mcp-debug-tools/ directories - Add .claude/ and .mcp-debug-tools/ to .gitignore Co-Authored-By: Claude Opus 4.5 --- .claude/settings.json | 10 ---------- .gitignore | 4 ++++ .mcp-debug-tools/config.json | 7 ------- murgtools/cache.py | 2 +- murgtools/config.py | 7 +------ 5 files changed, 6 insertions(+), 24 deletions(-) delete mode 100644 .claude/settings.json delete mode 100644 .mcp-debug-tools/config.json diff --git a/.claude/settings.json b/.claude/settings.json deleted file mode 100644 index f91d88d..0000000 --- a/.claude/settings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(gh pr *)", - "Bash(pip show *)", - "Bash(find /home/slug/repos/murgTools -maxdepth 3 -name \"*.tif\" 2>&1 | head -20; ls /tmp/*.tif 2>/dev/null | head -5)", - "Read(//tmp/**)" - ] - } -} diff --git a/.gitignore b/.gitignore index 44c81c6..9dcfc55 100755 --- a/.gitignore +++ b/.gitignore @@ -111,3 +111,7 @@ Thumbs.db *.nc *.pickle *.pkl + +# Local development configs +.claude/ +.mcp-debug-tools/ diff --git a/.mcp-debug-tools/config.json b/.mcp-debug-tools/config.json deleted file mode 100644 index be67b51..0000000 --- a/.mcp-debug-tools/config.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "vscodeInstanceId": "vscode-2474-1774366165262", - "port": 8890, - "pid": 2474, - "workspacePath": "/home/slug/repos/murgTools", - "workspaceName": "murgTools" -} \ No newline at end of file diff --git a/murgtools/cache.py b/murgtools/cache.py index 0c37a5b..b62967b 100644 --- a/murgtools/cache.py +++ b/murgtools/cache.py @@ -42,7 +42,7 @@ import pickle import threading import time -from datetime import datetime, timedelta +from datetime import datetime from pathlib import Path from typing import Any, Callable, Dict, Optional, Tuple, Union diff --git a/murgtools/config.py b/murgtools/config.py index a842298..c836444 100644 --- a/murgtools/config.py +++ b/murgtools/config.py @@ -131,12 +131,7 @@ def _get_cached_ip(): """ global _cached_ip_address - # Fast path: check without lock first (safe because we only write once) - if _cached_ip_address is not None: - return _cached_ip_address - with _cache_lock: - # Double-check after acquiring lock if _cached_ip_address is not None: return _cached_ip_address @@ -146,7 +141,7 @@ def _get_cached_ip(): # Covers socket.error, socket.gaierror, socket.herror in Python 3 _cached_ip_address = '' - return _cached_ip_address + return _cached_ip_address def get_thredds_server(server=None, ip_address=None): From a44ba9d03a9bcdde048992277316eb1883452dd1 Mon Sep 17 00:00:00 2001 From: Spicer Bak Date: Thu, 16 Jul 2026 15:50:22 -0400 Subject: [PATCH 7/7] Address additional PR #58 review comments - Remove unused pickle import from test_cache.py - Use TTL setter in __init__ for validation (prevents negative TTL) - Include resolved server URL in cache key (not just self.server which can be None for auto-detect, causing cache collisions across networks) Co-Authored-By: Claude Opus 4.5 --- murgtools/cache.py | 6 +++--- murgtools/getdata/getDataFRF.py | 6 ++++-- tests/test_cache.py | 1 - 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/murgtools/cache.py b/murgtools/cache.py index b62967b..ab4b7dc 100644 --- a/murgtools/cache.py +++ b/murgtools/cache.py @@ -179,14 +179,14 @@ def __init__(self, enabled: Optional[bool] = None, cache_dir: Optional[str] = No env_dir = os.environ.get(config.ENV_CACHE_DIR) self._cache_dir = Path(env_dir) if env_dir else Path(config.DEFAULT_CACHE_DIR) - # Determine TTL + # Determine TTL (use setter for validation) if ttl_days is not None: - self._ttl_days = ttl_days + self.ttl_days = ttl_days # Use setter for validation else: env_ttl = os.environ.get(config.ENV_CACHE_TTL_DAYS) if env_ttl: try: - self._ttl_days = int(env_ttl) + self.ttl_days = int(env_ttl) # Use setter for validation except ValueError: logger.warning(f"Invalid TTL value '{env_ttl}', using default") self._ttl_days = config.DEFAULT_CACHE_TTL_DAYS diff --git a/murgtools/getdata/getDataFRF.py b/murgtools/getdata/getDataFRF.py index 2da804f..002cbe8 100755 --- a/murgtools/getdata/getDataFRF.py +++ b/murgtools/getdata/getDataFRF.py @@ -318,8 +318,10 @@ def _get_with_cache(self, data_type, fetch_func, extra_params=None, force_refres data_source = f"getObs.{data_type}" time_range = (self.epochd1, self.epochd2) - # Include server in extra_params for cache key - cache_params = {'server': self.server} + # Include resolved server URL in extra_params for cache key + # Use FRFdataloc (the actual server being used) rather than self.server + # (which may be None for auto-detect) + cache_params = {'server_url': self.FRFdataloc} if extra_params: cache_params.update(extra_params) diff --git a/tests/test_cache.py b/tests/test_cache.py index 066495f..7fd6b95 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -2,7 +2,6 @@ import json import os -import pickle import tempfile import threading import time