From e1a8626f2c4d8eec2ed6aca64d354d752b7ed48e Mon Sep 17 00:00:00 2001 From: Leon-Drq Date: Mon, 3 Aug 2026 21:19:16 -0700 Subject: [PATCH] feat: add optional Qveris data source --- README.md | 7 + .../app/data_sources/factory.py | 25 +- backend_api_python/app/data_sources/qveris.py | 576 ++++++++++++++++++ backend_api_python/env.example | 14 + .../tests/test_qveris_data_source.py | 305 ++++++++++ docs/README_CN.md | 6 + docs/integrations/QVERIS_DATA_SOURCE.md | 104 ++++ docs/screenshots/qveris-live-smoke-test.png | Bin 0 -> 66470 bytes 8 files changed, 1030 insertions(+), 7 deletions(-) create mode 100644 backend_api_python/app/data_sources/qveris.py create mode 100644 backend_api_python/tests/test_qveris_data_source.py create mode 100644 docs/integrations/QVERIS_DATA_SOURCE.md create mode 100644 docs/screenshots/qveris-live-smoke-test.png diff --git a/README.md b/README.md index 685d669aa..b961c148b 100644 --- a/README.md +++ b/README.md @@ -271,6 +271,13 @@ PostgreSQL migration guidance, see [Installation troubleshooting](docs/deployment/INSTALL_TROUBLESHOOTING.md) and the [cloud deployment guide](docs/deployment/CLOUD_DEPLOYMENT_EN.md). +### Optional Qveris data source + +QuantDinger can use Qveris as an opt-in unified market-data layer while keeping +the existing market providers as automatic fallbacks. See the +[Qveris data-source guide](docs/integrations/QVERIS_DATA_SOURCE.md) for setup, +supported markets, safety behavior, and verification. + ## Production deployment Validate secrets before starting a production stack: diff --git a/backend_api_python/app/data_sources/factory.py b/backend_api_python/app/data_sources/factory.py index 38903b613..206a85695 100644 --- a/backend_api_python/app/data_sources/factory.py +++ b/backend_api_python/app/data_sources/factory.py @@ -187,27 +187,38 @@ def _create_source(cls, market: str) -> BaseDataSource: """创建数据源实例""" if market == 'Crypto': from app.data_sources.crypto import CryptoDataSource - return CryptoDataSource() + source = CryptoDataSource() elif market == 'CNStock': from app.data_sources.cn_stock import CNStockDataSource - return CNStockDataSource() + source = CNStockDataSource() elif market == 'HKStock': from app.data_sources.hk_stock import HKStockDataSource - return HKStockDataSource() + source = HKStockDataSource() elif market == 'USStock': from app.data_sources.us_stock import USStockDataSource - return USStockDataSource() + source = USStockDataSource() elif market == 'Forex': from app.data_sources.forex import ForexDataSource - return ForexDataSource() + source = ForexDataSource() elif market == 'Futures': from app.data_sources.futures import FuturesDataSource - return FuturesDataSource() + source = FuturesDataSource() elif market == 'MOEX': from app.data_sources.moex import MOEXDataSource - return MOEXDataSource() + source = MOEXDataSource() else: raise UnsupportedMarketError(market) + return cls._wrap_optional_source(market, source) + + @staticmethod + def _wrap_optional_source(market: str, source: BaseDataSource) -> BaseDataSource: + """Wrap an existing source with Qveris only when explicitly enabled.""" + from app.data_sources.qveris import QverisDataSource + + if QverisDataSource.is_enabled_for(market): + logger.info("Qveris data source enabled for %s with %s fallback", market, source.name) + return QverisDataSource(market, source) + return source @classmethod def get_kline( diff --git a/backend_api_python/app/data_sources/qveris.py b/backend_api_python/app/data_sources/qveris.py new file mode 100644 index 000000000..f9270bb76 --- /dev/null +++ b/backend_api_python/app/data_sources/qveris.py @@ -0,0 +1,576 @@ +"""Optional Qveris market-data adapter with existing-source fallback. + +Qveris discovers a suitable upstream tool, executes it, and returns the +provider response. This adapter keeps provider-specific response shapes inside +the integration boundary and normalizes common OHLCV/quote layouts for +QuantDinger. +""" + +from __future__ import annotations + +import json +import os +import re +import time +from datetime import datetime, timezone +from typing import Any, Dict, List, Optional + +import requests + +from app.data_sources.base import BaseDataSource +from app.utils.logger import get_logger + +logger = get_logger(__name__) + + +class QverisError(RuntimeError): + """Raised when Qveris discovery, execution, or normalization fails.""" + + +_PARAM_ALIASES = { + "symbol": { + "symbol", + "symbols", + "symbolexchange", + "ticker", + "tickers", + "pair", + "instrument", + "instid", + "code", + "asset", + }, + "market": {"market", "assetclass", "assettype"}, + "timeframe": {"timeframe", "interval", "resolution", "period", "granularity", "frequency"}, + "limit": {"limit", "count", "rows", "size", "outputsize", "numresults", "numberofresults"}, + "start": {"start", "startdate", "starttime", "from", "fromdate", "fromtime", "since"}, + "end": {"end", "enddate", "endtime", "to", "todate", "totime", "until", "before"}, +} + +_ROW_ALIASES = { + "time": {"time", "times", "timestamp", "timestamps", "datetime", "datetimes", "date", "dates", "opentime", "t"}, + "open": {"open", "o", "1open"}, + "high": {"high", "h", "2high"}, + "low": {"low", "l", "3low"}, + "close": {"close", "c", "price", "4close", "adjustedclose", "adjclose"}, + "volume": {"volume", "v", "5volume", "6volume"}, +} + +_QUOTE_ALIASES = { + "last": {"last", "price", "close", "currentprice", "latestprice", "regularmarketprice", "c"}, + "change": {"change", "netchange", "d"}, + "changePercent": {"changepercent", "percentchange", "percentagechange", "changepct", "dp"}, + "high": {"high", "dayhigh", "regularmarketdayhigh", "h"}, + "low": {"low", "daylow", "regularmarketdaylow", "l"}, + "open": {"open", "regularmarketopen", "o"}, + "previousClose": {"previousclose", "prevclose", "regularmarketpreviousclose", "pc"}, +} + +_TIMEFRAME_OPTIONS = { + "1m": ("1m", "1min", "1minute"), + "3m": ("3m", "3min", "3minute"), + "5m": ("5m", "5min", "5minute"), + "15m": ("15m", "15min", "15minute"), + "30m": ("30m", "30min", "30minute"), + "1H": ("1H", "1h", "60m", "hour", "hourly"), + "4H": ("4H", "4h", "240m", "4hour"), + "1D": ("1D", "1d", "d", "day", "daily"), + "1W": ("1W", "1w", "w", "week", "weekly"), +} + +_MARKET_RANK_TERMS = { + "USStock": ({"stock", "equity", "nasdaq", "nyse"}, {"crypto", "cryptocurrency", "forex"}), + "CNStock": ({"stock", "equity", "china", "a-share"}, {"crypto", "cryptocurrency", "forex"}), + "HKStock": ({"stock", "equity", "hong kong", "hk"}, {"crypto", "cryptocurrency", "forex"}), + "Crypto": ({"crypto", "cryptocurrency", "digital asset"}, {"stock", "equity", "forex"}), + "Forex": ({"forex", "foreign exchange", "currency"}, {"stock", "equity", "crypto"}), + "Futures": ({"futures", "commodity", "derivative"}, {"stock", "equity", "crypto"}), + "MOEX": ({"moex", "russia", "russian"}, {"crypto", "cryptocurrency", "forex"}), +} + + +def _key(value: Any) -> str: + return re.sub(r"[^a-z0-9]", "", str(value or "").lower()) + + +def _float(value: Any) -> Optional[float]: + if value is None or isinstance(value, bool): + return None + try: + if isinstance(value, str): + value = value.strip().replace(",", "").replace("$", "").replace("%", "") + if not value or value.lower() in {"none", "null", "n/a", "nan", "-"}: + return None + return float(value) + except (TypeError, ValueError): + return None + + +def _timestamp(value: Any) -> Optional[int]: + if value is None or isinstance(value, bool): + return None + if isinstance(value, (int, float)): + number = float(value) + if number > 10_000_000_000: + number /= 1000.0 + return int(number) if number > 0 else None + text = str(value).strip() + if not text: + return None + if re.fullmatch(r"\d+(?:\.\d+)?", text): + return _timestamp(float(text)) + try: + parsed = datetime.fromisoformat(text.replace("Z", "+00:00")) + if parsed.tzinfo is None: + parsed = parsed.replace(tzinfo=timezone.utc) + return int(parsed.timestamp()) + except ValueError: + return None + + +def _field(row: Dict[str, Any], aliases: set[str]) -> Any: + normalized = {_key(name): value for name, value in row.items()} + for alias in aliases: + if alias in normalized: + return normalized[alias] + return None + + +class QverisDataSource(BaseDataSource): + """Use Qveris first and preserve the configured QuantDinger fallback.""" + + name = "Qveris" + DEFAULT_BASE_URL = "https://qveris.ai/api/v1" + + def __init__( + self, + market: str, + fallback: BaseDataSource, + *, + api_key: Optional[str] = None, + base_url: Optional[str] = None, + timeout: Optional[float] = None, + session: Optional[requests.Session] = None, + ): + self.market = str(market or "").strip() + self.fallback = fallback + self.api_key = str(api_key if api_key is not None else os.getenv("QVERIS_API_KEY", "")).strip() + self.base_url = str(base_url or os.getenv("QVERIS_BASE_URL") or self.DEFAULT_BASE_URL).rstrip("/") + self.timeout = float(timeout or os.getenv("QVERIS_TIMEOUT", "30")) + self.discovery_ttl = max(0, int(os.getenv("QVERIS_DISCOVERY_TTL_SECONDS", "3600"))) + self.session = session or requests.Session() + self.name = f"Qveris/{self.market}+{fallback.name}" + self._discovery_cache: Dict[str, tuple[float, Dict[str, Any], str]] = {} + + @classmethod + def is_enabled_for(cls, market: str) -> bool: + if not (os.getenv("QVERIS_API_KEY") or "").strip(): + return False + configured = { + value.strip().lower() + for value in (os.getenv("QVERIS_DATA_SOURCE_MARKETS") or "").split(",") + if value.strip() + } + return "*" in configured or str(market or "").strip().lower() in configured + + def get_kline( + self, + symbol: str, + timeframe: str, + limit: int, + before_time: Optional[int] = None, + after_time: Optional[int] = None, + ) -> List[Dict[str, Any]]: + try: + result = self._execute( + "kline", + symbol=symbol, + timeframe=timeframe, + limit=max(1, int(limit or 1)), + before_time=before_time, + after_time=after_time, + ) + rows = self._normalize_klines(result) + rows = self.filter_and_limit( + rows, + limit=max(1, int(limit or 1)), + before_time=before_time, + after_time=after_time, + truncate=(after_time is None), + ) + if rows: + self.log_result(symbol, rows, timeframe) + return rows + raise QverisError("Qveris returned no normalizable OHLCV rows") + except Exception as exc: + logger.warning( + "Qveris K-line request failed for %s:%s (%s); using %s", + self.market, + symbol, + str(exc)[:240], + self.fallback.name, + ) + return self.fallback.get_kline(symbol, timeframe, limit, before_time, after_time) + + def get_ticker(self, symbol: str) -> Dict[str, Any]: + try: + result = self._execute("ticker", symbol=symbol) + quote = self._normalize_ticker(result) + if quote and float(quote.get("last") or 0) > 0: + quote.setdefault("symbol", symbol) + return quote + raise QverisError("Qveris returned no normalizable quote") + except Exception as exc: + logger.warning( + "Qveris ticker request failed for %s:%s (%s); using %s", + self.market, + symbol, + str(exc)[:240], + self.fallback.name, + ) + return self.fallback.get_ticker(symbol) + + def _execute(self, operation: str, **context: Any) -> Any: + if not self.api_key: + raise QverisError("QVERIS_API_KEY is not configured") + tool, discovery_id = self._discover(operation, str(context.get("timeframe") or "")) + parameters = self._build_parameters(self._tool_parameters(tool), context) + payload = self._request_json( + "/tools/execute", + params={"tool_id": tool.get("tool_id")}, + json={ + "search_id": discovery_id, + "parameters": parameters, + "max_response_size": 262144, + }, + ) + if payload.get("success") is False: + raise QverisError(str(payload.get("error_message") or "Qveris tool execution failed")) + result = payload.get("result", payload) + if isinstance(result, str): + try: + return json.loads(result) + except ValueError: + return result + return result + + def _discover(self, operation: str, timeframe: str) -> tuple[Dict[str, Any], str]: + cache_key = f"{operation}:{self.market}:{timeframe}" + cached = self._discovery_cache.get(cache_key) + if cached and cached[0] > time.monotonic(): + return cached[1], cached[2] + + if operation == "kline": + query = ( + f"read-only historical OHLCV candlestick market data API for {self.market}; " + "accept symbol, interval or timeframe, limit, and optional start/end date" + ) + preferred = (os.getenv("QVERIS_KLINE_TOOL_ID") or "").strip() + else: + query = f"read-only latest market price quote API for {self.market} accepting a symbol" + preferred = (os.getenv("QVERIS_TICKER_TOOL_ID") or "").strip() + + payload = self._request_json("/search", json={"query": query, "limit": 10}) + discovery_id = str(payload.get("search_id") or payload.get("discovery_id") or "").strip() + candidates = payload.get("results") or [] + if not discovery_id or not isinstance(candidates, list): + raise QverisError("Qveris discovery response is missing search_id or results") + + tool = self._select_tool(candidates, preferred, operation, timeframe) + expires = time.monotonic() + self.discovery_ttl + self._discovery_cache[cache_key] = (expires, tool, discovery_id) + return tool, discovery_id + + def _select_tool( + self, + candidates: List[Dict[str, Any]], + preferred: str, + operation: str, + timeframe: str, + ) -> Dict[str, Any]: + if preferred: + for candidate in candidates: + if str(candidate.get("tool_id") or "") == preferred: + if self._supports_parameters(self._tool_parameters(candidate), operation, timeframe): + return candidate + raise QverisError(f"Configured Qveris tool {preferred} has unsupported required parameters") + raise QverisError(f"Configured Qveris tool {preferred} was not returned by discovery") + + compatible = [ + candidate + for candidate in candidates + if isinstance(candidate, dict) + and candidate.get("tool_id") + and self._supports_parameters(self._tool_parameters(candidate), operation, timeframe) + ] + if not compatible: + raise QverisError("Qveris discovery returned no compatible tool") + + terms = ( + ("ohlcv", "candlestick", "historical", "time series") + if operation == "kline" + else ("quote", "price", "ticker") + ) + + def rank(candidate: Dict[str, Any]) -> tuple[int, int, float, float]: + text = f"{candidate.get('name', '')} {candidate.get('description', '')}".lower() + relevance = sum(1 for term in terms if term in text) + positive, negative = _MARKET_RANK_TERMS.get(self.market, (set(), set())) + market_affinity = sum(1 for term in positive if term in text) + market_affinity -= 2 * sum(1 for term in negative if term in text) + stats = candidate.get("stats") if isinstance(candidate.get("stats"), dict) else {} + success_rate = _float(candidate.get("success_rate")) + if success_rate is None: + success_rate = _float(stats.get("success_rate")) or 0.0 + execution_time = _float(candidate.get("avg_execution_time_ms")) + if execution_time is None: + execution_time = _float(stats.get("avg_execution_time_ms")) + return relevance, market_affinity, success_rate, -(execution_time or float("inf")) + + compatible.sort(key=rank, reverse=True) + return compatible[0] + + @staticmethod + def _tool_parameters(tool: Dict[str, Any]) -> Any: + """Read current Qveris metadata while accepting older cached results.""" + return tool.get("parameters") or tool.get("params") or [] + + @staticmethod + def _supports_parameters(specs: Any, operation: str, timeframe: str = "") -> bool: + if not isinstance(specs, list) or not specs: + return False + known = set().union(*_PARAM_ALIASES.values()) + normalized = {_key(spec.get("name")) for spec in specs if isinstance(spec, dict)} + if not any(name in _PARAM_ALIASES["symbol"] for name in normalized): + return False + if operation == "kline" and not any(name in _PARAM_ALIASES["timeframe"] for name in normalized): + return False + if operation == "kline": + candidates = _TIMEFRAME_OPTIONS.get(timeframe, (timeframe,)) + for spec in specs: + if not isinstance(spec, dict): + continue + if _key(spec.get("name")) not in _PARAM_ALIASES["timeframe"]: + continue + options = spec.get("enum") or spec.get("options") or [] + option_values = [str(option.get("value") if isinstance(option, dict) else option) for option in options] + if option_values and not any( + _key(candidate) == _key(option) for candidate in candidates for option in option_values + ): + return False + for spec in specs: + if not isinstance(spec, dict) or not spec.get("required"): + continue + options = spec.get("enum") or spec.get("options") or [] + if _key(spec.get("name")) not in known and len(options) != 1: + return False + return True + + def _build_parameters(self, specs: Any, context: Dict[str, Any]) -> Dict[str, Any]: + parameters: Dict[str, Any] = {} + if not isinstance(specs, list): + raise QverisError("Qveris tool parameter metadata is missing") + for spec in specs: + if not isinstance(spec, dict): + continue + name = str(spec.get("name") or "").strip() + normalized = _key(name) + value = self._parameter_value(normalized, spec, context) + if value is None: + if spec.get("required"): + raise QverisError(f"Cannot map required Qveris tool parameter: {name}") + continue + parameters[name] = value + return parameters + + def _parameter_value(self, name: str, spec: Dict[str, Any], context: Dict[str, Any]) -> Any: + if name in _PARAM_ALIASES["symbol"]: + symbol = str(context.get("symbol") or "").strip() + param_type = str(spec.get("type") or "").lower() + description = str(spec.get("description") or "").lower() + if ( + self.market == "USStock" + and "." not in symbol + and (name == "symbolexchange" or "exchange suffix" in description or ".us" in description) + ): + symbol = f"{symbol}.US" + return [symbol] if "array" in param_type or name in {"symbols", "tickers"} else symbol + if name in _PARAM_ALIASES["market"]: + return self.market + if name in _PARAM_ALIASES["timeframe"]: + return self._timeframe_value(str(context.get("timeframe") or "1D"), spec) + if name in _PARAM_ALIASES["limit"]: + return int(context.get("limit") or 300) + if name in _PARAM_ALIASES["start"]: + start = context.get("after_time") + if start is None: + end = int(context.get("before_time") or time.time()) + start = end - self.calculate_time_range( + str(context.get("timeframe") or "1D"), + int(context.get("limit") or 300), + buffer_ratio=1.5, + ) + return self._time_parameter(int(start), name, spec) + if name in _PARAM_ALIASES["end"]: + end = int(context.get("before_time") or time.time()) + return self._time_parameter(end, name, spec) + options = spec.get("enum") or spec.get("options") or [] + option_values = [str(option.get("value") if isinstance(option, dict) else option) for option in options] + if name in {"fmt", "format", "datatype"}: + for option in option_values: + if option.lower() == "json": + return option + if len(option_values) == 1: + return option_values[0] + return None + + @staticmethod + def _time_parameter(timestamp: int, name: str, spec: Dict[str, Any]) -> Any: + description = str(spec.get("description") or "").lower() + if "date" in name or "yyyy-mm-dd" in description: + return datetime.fromtimestamp(timestamp, tz=timezone.utc).date().isoformat() + return timestamp + + @staticmethod + def _timeframe_value(timeframe: str, spec: Dict[str, Any]) -> str: + candidates = _TIMEFRAME_OPTIONS.get(timeframe, (timeframe,)) + options = spec.get("enum") or spec.get("options") or [] + option_values = [str(option.get("value") if isinstance(option, dict) else option) for option in options] + for candidate in candidates: + for option in option_values: + if _key(candidate) == _key(option): + return option + return candidates[0] + + def _request_json(self, path: str, *, params: Optional[dict] = None, json: Optional[dict] = None) -> Dict[str, Any]: + try: + response = self.session.post( + f"{self.base_url}{path}", + params=params or {}, + json=json or {}, + headers={ + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json", + }, + timeout=self.timeout, + ) + response.raise_for_status() + payload = response.json() + except requests.RequestException as exc: + raise QverisError(f"Qveris HTTP request failed: {exc}") from exc + except ValueError as exc: + raise QverisError("Qveris returned invalid JSON") from exc + if not isinstance(payload, dict): + raise QverisError("Qveris response must be a JSON object") + return payload + + def _normalize_klines(self, payload: Any) -> List[Dict[str, Any]]: + candidates: List[List[Dict[str, Any]]] = [] + self._collect_row_lists(payload, candidates, depth=0) + for dated_rows in self._dated_row_dicts(payload, depth=0): + candidates.append(dated_rows) + normalized = [self._normalize_row(row) for rows in candidates for row in rows] + valid = [row for row in normalized if row is not None] + deduped = {row["time"]: row for row in valid} + return [deduped[timestamp] for timestamp in sorted(deduped)] + + def _collect_row_lists(self, value: Any, output: List[List[Dict[str, Any]]], *, depth: int) -> None: + if depth > 7: + return + if isinstance(value, list): + dict_rows = [row for row in value if isinstance(row, dict)] + if dict_rows: + output.append(dict_rows) + for item in value: + self._collect_row_lists(item, output, depth=depth + 1) + elif isinstance(value, dict): + column_rows = self._column_rows(value) + if column_rows: + output.append(column_rows) + for item in value.values(): + self._collect_row_lists(item, output, depth=depth + 1) + + def _dated_row_dicts(self, value: Any, *, depth: int) -> List[List[Dict[str, Any]]]: + if depth > 7: + return [] + output: List[List[Dict[str, Any]]] = [] + if isinstance(value, dict): + rows: List[Dict[str, Any]] = [] + for key, item in value.items(): + if isinstance(item, dict) and _timestamp(key) is not None: + rows.append({"time": key, **item}) + if rows: + output.append(rows) + for item in value.values(): + output.extend(self._dated_row_dicts(item, depth=depth + 1)) + elif isinstance(value, list): + for item in value: + output.extend(self._dated_row_dicts(item, depth=depth + 1)) + return output + + @staticmethod + def _column_rows(value: Dict[str, Any]) -> List[Dict[str, Any]]: + normalized = {_key(name): item for name, item in value.items()} + + def column(field: str) -> Optional[List[Any]]: + for alias in _ROW_ALIASES[field]: + item = normalized.get(alias) + if isinstance(item, list): + return item + return None + + times = column("time") + opens = column("open") + highs = column("high") + lows = column("low") + closes = column("close") + volumes = column("volume") or [] + if not all((times, opens, highs, lows, closes)): + return [] + length = min(len(times), len(opens), len(highs), len(lows), len(closes)) + return [ + { + "time": times[index], + "open": opens[index], + "high": highs[index], + "low": lows[index], + "close": closes[index], + "volume": volumes[index] if index < len(volumes) else 0, + } + for index in range(length) + ] + + def _normalize_row(self, row: Dict[str, Any]) -> Optional[Dict[str, Any]]: + timestamp = _timestamp(_field(row, _ROW_ALIASES["time"])) + open_price = _float(_field(row, _ROW_ALIASES["open"])) + high = _float(_field(row, _ROW_ALIASES["high"])) + low = _float(_field(row, _ROW_ALIASES["low"])) + close = _float(_field(row, _ROW_ALIASES["close"])) + volume = _float(_field(row, _ROW_ALIASES["volume"])) or 0.0 + if timestamp is None or None in (open_price, high, low, close): + return None + return self.format_kline(timestamp, open_price, high, low, close, volume) + + def _normalize_ticker(self, payload: Any, *, depth: int = 0) -> Optional[Dict[str, Any]]: + if depth > 7: + return None + if isinstance(payload, dict): + last = _float(_field(payload, _QUOTE_ALIASES["last"])) + if last is not None and last > 0: + result: Dict[str, Any] = {"last": last} + for output_name, aliases in _QUOTE_ALIASES.items(): + if output_name == "last": + continue + value = _float(_field(payload, aliases)) + result[output_name] = value if value is not None else 0 + return result + for item in payload.values(): + quote = self._normalize_ticker(item, depth=depth + 1) + if quote: + return quote + elif isinstance(payload, list): + for item in payload: + quote = self._normalize_ticker(item, depth=depth + 1) + if quote: + return quote + return None diff --git a/backend_api_python/env.example b/backend_api_python/env.example index 18ea79594..ca9dd56af 100644 --- a/backend_api_python/env.example +++ b/backend_api_python/env.example @@ -501,6 +501,20 @@ MINIMAX_BASE_URL=https://api.minimax.io/v1 DATA_SOURCE_TIMEOUT=30 DATA_SOURCE_RETRY=3 DATA_SOURCE_RETRY_BACKOFF=0.5 + +# Optional Qveris unified data source. It is disabled unless both the API key +# and at least one market are configured. Existing providers remain the fallback. +# Markets: Crypto, Forex, Futures, USStock, CNStock, HKStock, MOEX, or *. +QVERIS_API_KEY= +QVERIS_DATA_SOURCE_MARKETS= +QVERIS_BASE_URL=https://qveris.ai/api/v1 +QVERIS_TIMEOUT=30 +QVERIS_DISCOVERY_TTL_SECONDS=3600 +# Optional: pin a discovered tool instead of accepting the highest-ranked +# compatible result. The tool still needs to appear in a fresh discovery. +QVERIS_KLINE_TOOL_ID= +QVERIS_TICKER_TOOL_ID= + FINNHUB_API_KEY= # Optional free-tier Finnhub helper for US stock quotes, company profile and news. # Keep FINNHUB_FREE_ONLY=true unless your plan includes paid endpoints such as Economic Calendar/Social Sentiment. diff --git a/backend_api_python/tests/test_qveris_data_source.py b/backend_api_python/tests/test_qveris_data_source.py new file mode 100644 index 000000000..4e2ac23ab --- /dev/null +++ b/backend_api_python/tests/test_qveris_data_source.py @@ -0,0 +1,305 @@ +"""Qveris market-data adapter tests.""" + +from __future__ import annotations + +import requests + +from app.data_sources.base import BaseDataSource +from app.data_sources.factory import DataSourceFactory +from app.data_sources.qveris import QverisDataSource + + +class FakeResponse: + def __init__(self, payload): + self.payload = payload + + def raise_for_status(self): + return None + + def json(self): + return self.payload + + +class FakeSession: + def __init__(self, payloads=None, error=None): + self.payloads = list(payloads or []) + self.error = error + self.calls = [] + + def post(self, url, **kwargs): + self.calls.append({"url": url, **kwargs}) + if self.error: + raise self.error + return FakeResponse(self.payloads.pop(0)) + + +class FallbackDataSource(BaseDataSource): + name = "fallback" + + def __init__(self): + self.kline_calls = 0 + self.ticker_calls = 0 + + def get_kline(self, symbol, timeframe, limit, before_time=None, after_time=None): + self.kline_calls += 1 + return [{"time": 1, "open": 1, "high": 1, "low": 1, "close": 1, "volume": 1}] + + def get_ticker(self, symbol): + self.ticker_calls += 1 + return {"last": 42.0, "symbol": symbol} + + +def kline_discovery(): + return { + "search_id": "search-kline", + "results": [ + { + "tool_id": "market.history.execute.v1", + "name": "Historical OHLCV", + "description": "Historical candlestick time series", + "parameters": [ + {"name": "symbol", "required": True, "type": "string"}, + {"name": "interval", "required": True, "type": "string", "enum": ["1h", "1d"]}, + {"name": "limit", "required": False, "type": "integer"}, + ], + } + ], + } + + +def ticker_discovery(): + return { + "search_id": "search-ticker", + "results": [ + { + "tool_id": "market.quote.execute.v1", + "name": "Latest quote", + "description": "Current market price quote", + "parameters": [{"name": "ticker", "required": True, "type": "string"}], + } + ], + } + + +def test_qveris_normalizes_row_oriented_klines(): + session = FakeSession( + [ + kline_discovery(), + { + "success": True, + "result": { + "data": [ + { + "datetime": "2026-08-01T00:00:00Z", + "open": "100", + "high": 105, + "low": 99, + "close": 104, + "volume": "1,200", + }, + { + "datetime": "2026-08-02T00:00:00Z", + "open": 104, + "high": 108, + "low": 103, + "close": 107, + "volume": 900, + }, + ] + }, + }, + ] + ) + fallback = FallbackDataSource() + source = QverisDataSource("USStock", fallback, api_key="test-key", session=session) + + rows = source.get_kline("AAPL", "1D", 2) + + assert [row["close"] for row in rows] == [104.0, 107.0] + assert rows[0]["volume"] == 1200.0 + assert fallback.kline_calls == 0 + assert session.calls[1]["params"] == {"tool_id": "market.history.execute.v1"} + assert session.calls[1]["json"]["parameters"] == { + "symbol": "AAPL", + "interval": "1d", + "limit": 2, + } + assert session.calls[1]["headers"]["Authorization"] == "Bearer test-key" + + +def test_qveris_normalizes_column_oriented_klines(): + session = FakeSession( + [ + kline_discovery(), + { + "success": True, + "result": { + "timestamps": [1785542400, 1785628800], + "open": [10, 11], + "high": [12, 13], + "low": [9, 10], + "close": [11, 12], + "volume": [100, 200], + }, + }, + ] + ) + source = QverisDataSource("USStock", FallbackDataSource(), api_key="test-key", session=session) + + rows = source.get_kline("MSFT", "1D", 2) + + assert [row["time"] for row in rows] == [1785542400, 1785628800] + assert [row["close"] for row in rows] == [11.0, 12.0] + + +def test_qveris_prefers_market_match_and_maps_eodhd_stock_parameters(): + discovery = { + "search_id": "search-eod", + "results": [ + { + "tool_id": "crypto-history", + "name": "Historical Data for Cryptocurrency", + "description": "Historical OHLCV data", + "params": [ + {"name": "symbol", "required": True, "type": "string"}, + {"name": "period", "required": False, "enum": ["d", "w", "m"]}, + ], + "stats": {"success_rate": 1.0}, + }, + { + "tool_id": "stock-history", + "name": "Historical Stock Market Data", + "description": "Historical end-of-day OHLCV for equities", + "params": [ + { + "name": "symbol_exchange", + "required": True, + "type": "string", + "description": "Ticker with exchange suffix, for example AAPL.US", + }, + {"name": "fmt", "required": False, "enum": ["json", "csv"]}, + {"name": "period", "required": False, "enum": ["d", "w", "m"]}, + {"name": "from", "required": False, "description": "Start date in YYYY-MM-DD format"}, + {"name": "to", "required": False, "description": "End date in YYYY-MM-DD format"}, + ], + "stats": {"success_rate": 0.8}, + }, + ], + } + session = FakeSession( + [ + discovery, + { + "success": True, + "result": [{"date": "2026-08-01", "open": 200, "high": 205, "low": 199, "close": 204, "volume": 1000}], + }, + ] + ) + source = QverisDataSource("USStock", FallbackDataSource(), api_key="test-key", session=session) + + rows = source.get_kline("AAPL", "1D", 1, before_time=1785715200) + + assert rows[0]["close"] == 204.0 + assert session.calls[1]["params"] == {"tool_id": "stock-history"} + assert session.calls[1]["json"]["parameters"] == { + "symbol_exchange": "AAPL.US", + "fmt": "json", + "period": "d", + "from": "2026-08-01", + "to": "2026-08-03", + } + + +def test_qveris_normalizes_dated_time_series(): + session = FakeSession( + [ + kline_discovery(), + { + "success": True, + "result": { + "Time Series (Daily)": { + "2026-08-01": { + "1. open": "20", + "2. high": "22", + "3. low": "19", + "4. close": "21", + "5. volume": "300", + } + } + }, + }, + ] + ) + source = QverisDataSource("USStock", FallbackDataSource(), api_key="test-key", session=session) + + rows = source.get_kline("NVDA", "1D", 1) + + assert rows[0]["close"] == 21.0 + assert rows[0]["volume"] == 300.0 + + +def test_qveris_normalizes_ticker(): + session = FakeSession( + [ + ticker_discovery(), + { + "success": True, + "result": { + "quote": { + "regularMarketPrice": 215.5, + "regularMarketPreviousClose": 210, + "regularMarketDayHigh": 217, + "regularMarketDayLow": 209, + } + }, + }, + ] + ) + fallback = FallbackDataSource() + source = QverisDataSource("USStock", fallback, api_key="test-key", session=session) + + quote = source.get_ticker("AAPL") + + assert quote["last"] == 215.5 + assert quote["previousClose"] == 210.0 + assert quote["symbol"] == "AAPL" + assert fallback.ticker_calls == 0 + + +def test_qveris_falls_back_without_exposing_failure(): + fallback = FallbackDataSource() + session = FakeSession(error=requests.ConnectionError("offline")) + source = QverisDataSource("USStock", fallback, api_key="secret-key", session=session) + + rows = source.get_kline("AAPL", "1D", 1) + quote = source.get_ticker("AAPL") + + assert rows[0]["close"] == 1 + assert quote["last"] == 42.0 + assert fallback.kline_calls == 1 + assert fallback.ticker_calls == 1 + + +def test_qveris_is_disabled_until_key_and_market_are_configured(monkeypatch): + monkeypatch.delenv("QVERIS_API_KEY", raising=False) + monkeypatch.setenv("QVERIS_DATA_SOURCE_MARKETS", "USStock") + assert not QverisDataSource.is_enabled_for("USStock") + + monkeypatch.setenv("QVERIS_API_KEY", "configured") + assert QverisDataSource.is_enabled_for("USStock") + assert not QverisDataSource.is_enabled_for("Crypto") + + monkeypatch.setenv("QVERIS_DATA_SOURCE_MARKETS", "*") + assert QverisDataSource.is_enabled_for("Crypto") + + +def test_factory_wraps_existing_source_only_when_enabled(monkeypatch): + fallback = FallbackDataSource() + monkeypatch.delenv("QVERIS_API_KEY", raising=False) + monkeypatch.setenv("QVERIS_DATA_SOURCE_MARKETS", "USStock") + assert DataSourceFactory._wrap_optional_source("USStock", fallback) is fallback + + monkeypatch.setenv("QVERIS_API_KEY", "configured") + wrapped = DataSourceFactory._wrap_optional_source("USStock", fallback) + assert isinstance(wrapped, QverisDataSource) + assert wrapped.fallback is fallback diff --git a/docs/README_CN.md b/docs/README_CN.md index 6db2326c7..22db9d05c 100644 --- a/docs/README_CN.md +++ b/docs/README_CN.md @@ -218,6 +218,12 @@ docker compose ps Windows、国内镜像、数据库迁移等问题见 [安装故障排查](deployment/INSTALL_TROUBLESHOOTING.md)和[云部署指南](deployment/CLOUD_DEPLOYMENT_CN.md)。 +### 可选 Qveris 数据源 + +QuantDinger 可以按需启用 Qveris 作为统一行情数据层,并在调用失败时自动回退到 +现有数据源。配置方式、支持市场、安全行为与验证步骤见 +[Qveris 数据源接入指南](integrations/QVERIS_DATA_SOURCE.md)。 + ## 生产部署 启动前校验全部生产密钥: diff --git a/docs/integrations/QVERIS_DATA_SOURCE.md b/docs/integrations/QVERIS_DATA_SOURCE.md new file mode 100644 index 000000000..92d306b36 --- /dev/null +++ b/docs/integrations/QVERIS_DATA_SOURCE.md @@ -0,0 +1,104 @@ +# Qveris Data Source + +Qveris can be enabled as an optional, unified market-data layer in front of QuantDinger's existing providers. The integration discovers a compatible read-only Qveris tool, executes it, normalizes common OHLCV or quote response shapes, and falls back to the existing market source when discovery, execution, or normalization fails. + +No existing provider is replaced by default. + +## Configuration + +Add the following values to `backend_api_python/.env`: + +```dotenv +QVERIS_API_KEY=your-qveris-api-key +QVERIS_DATA_SOURCE_MARKETS=USStock,CNStock,HKStock +``` + +Supported market names are `Crypto`, `Forex`, `Futures`, `USStock`, `CNStock`, `HKStock`, and `MOEX`. Use `*` to enable every market. + +Optional settings: + +```dotenv +QVERIS_BASE_URL=https://qveris.ai/api/v1 +QVERIS_TIMEOUT=30 +QVERIS_DISCOVERY_TTL_SECONDS=3600 +QVERIS_KLINE_TOOL_ID= +QVERIS_TICKER_TOOL_ID= +``` + +Leave the tool IDs empty to accept the highest-ranked compatible discovery result. Set them when you want to pin a specific tool. A pinned tool must still appear in a fresh Qveris discovery response so that the adapter receives a valid discovery ID. + +Restart the backend after changing the environment: + +```bash +docker compose up -d --build backend +``` + +## How Requests Flow + +1. `DataSourceFactory` creates the existing QuantDinger provider for the requested market. +2. When a Qveris API key and that market are explicitly configured, the factory wraps the existing provider with `QverisDataSource`. +3. The adapter searches Qveris for a compatible read-only OHLCV or quote tool. +4. It maps common parameters such as symbol, timeframe, limit, and start/end time. +5. It executes the selected tool and normalizes row-oriented, column-oriented, and dated time-series responses. +6. If any step fails, the original provider handles the request. + +Discovery results are cached in memory. API keys stay in the backend environment and are never returned to clients or included in logs. + +## Verify The Adapter + +Run the isolated unit tests, which cover tool discovery, market-aware tool selection, parameter mapping, three common OHLCV response layouts, quote normalization, opt-in behavior, and fallback: + +```bash +cd backend_api_python +python -m pytest tests/test_qveris_data_source.py -q +``` + +Expected result: + +```text +8 passed +``` + +For a live smoke test, start QuantDinger with a valid Qveris key and request data through the existing Agent Gateway: + +```bash +curl -sS -G http://localhost:5000/api/agent/v1/klines \ + -H "Authorization: Bearer ${QUANTDINGER_AGENT_TOKEN}" \ + --data-urlencode market=USStock \ + --data-urlencode symbol=AAPL \ + --data-urlencode timeframe=1D \ + --data-urlencode limit=5 +``` + +The public contract remains the existing normalized QuantDinger format: + +```json +{ + "code": 0, + "message": "ok", + "data": { + "market": "USStock", + "symbol": "AAPL", + "timeframe": "1D", + "count": 1, + "klines": [ + { + "time": 1785542400, + "open": 100.0, + "high": 105.0, + "low": 99.0, + "close": 104.0, + "volume": 1200.0 + } + ] + } +} +``` + +The exact upstream tool depends on Qveris discovery and the configured market. Pin a tool ID when reproducibility matters. + +## Live Result + +The adapter was smoke-tested with `USStock`, `AAPL`, `1D`, and a limit of five. Qveris discovered an end-of-day stock-data tool and returned five rows in QuantDinger's normalized OHLCV format without using the fallback. No API key is included in the image. + +![Qveris live smoke test](../screenshots/qveris-live-smoke-test.png) diff --git a/docs/screenshots/qveris-live-smoke-test.png b/docs/screenshots/qveris-live-smoke-test.png new file mode 100644 index 0000000000000000000000000000000000000000..faef05c76bac7dd10c8570d92ecba1cd01ca5507 GIT binary patch literal 66470 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 E ze(z*?Im}~sdstqpt+SJ{(-=_hNJvO3!V^V3X%b2}jORUbMZq1d`d#iYhRBNWw_@Y4 zhEG-<);~vb6nKBtMO7$2;oA_^8~zjwE63fu@CxeAFi2o2)sB%HzjF}hJ6YrPJ6Z7F|1KZvBsLH zAbq%WUM)-g(>^ki=s-Uu~+i`YP#oE-4V7`f?=lW4KfXU?Wu94kM&4BKFBy zA<5NAyw)fC`@zm_wUC)2gRrSXi*m7sL;MTNw~@QzmPe3*{3I|%k0Rt}2 zl_KJnmKODqx8ph96C86n-@dLBEoF#04xWc^H8nT$TJ=wUHExff7Ab57LacbWZY?1H z?Cq8R`mXrtQy`hDMzZg9^7&d810EO?)@(k=tR}mli8}CI|Ir4nqwnBK8rAsZ;@dzV?2ysl z>`CNa^TpA@!9n!u`VC2ZDoN*A?5zvH+Y^AU&ax#{CF|ExD2+;#!b>e9@O>_ch0reIZ=w0Kol}#E zQj;0<1?@dj7T=T0hKeO{y9x~_KbF(f)VzLwrIIQ(oTb;No~uYL;j7DHPRi;1D(NU8 zAmF>I)E&rtr9x2vn4Sp~$(mNLin4vz{eA#P$fg&T}4J2S^xk?FC5-wif>J@snqkEciz8kQFt8>H+mCn@8HnA< z{7~&ccvbvTPY^)v%^^-E>uuUmmP$F~h7?09HNfD0#fv19B%-Xe+_EZsrTxrA&Gubr z0-KAQnE$ zKd0=sdCcUh7SjJCbz^NG4!_+vO1Vkt^t~j3)8L|Eor*ZGd6IYYDpMlY>HKskXy#^l$XZ^lYlMA3rX;$dh z*=jT+c1KT(w2J2Io&5%;VHd|&FN*#_NpRM{!2(i%M>8lY&=pnNKn;(|E30FBcT_(H zg5|5|(}R=S8_Sh+kT`6fTc|yql#2kMnan%Kl*R}*?!gLd9|IrV`M2T~)IEsHdnv6S z59ewgeEe{;w6sK|8i3y44&TJ!DF$mAUSpu0-NQ(wWBXa!rEs~ZfH26`s#0C}MPvjU_714jjMDo;nCwU7#X!=yqtY)Q6c^De?&U3!MsnWV{zS%A^vmK@q z`z-26(Z&(Ivm@@(@m7aa2h?Px6pq>nJF5?hFO$62hSEh@S;w&A6~=1(Y<#0~8T_+N z?uTH4ZmP$zYiMIFI(p++>+I)Q(#;FUONR0K_m1X97i+qw`-HM?rCY@%`Mul?esHs27KtbQ^qzE%RIrf9rkNhic%ESem>5*V>zimwU-DFdL<7mDfC4)u}Bag#+9{8!56>zd4rWMBm4od=1okB_uBN5%c*1r|Ac3N$2f^pGd{_3%@kztvkmFMQ^VCZ;iH$Ymb`_t4HH+ z@OIShDSjy6Z`a6IUF}cOV2_IJx=63{;lEedMnSmib+-OU6=&C)KVUaB$TkaW-vRo??jOWvzeLi zV^M4B7&e!?1`!Ui@~m5c7)DJ^-4#Z?vW8wuee%)gZXlBn>aU%Pz09zKy-Y`hTLkFO zP!w!)>X9s(1y1(nFNkCy{^^=Z{Zr96a9_Az|CboTkY!`xk^;f$MUIn{snV~}f9x2AUvpV5E zXJ10G)7E%tj|L-SWsOmVTP~nobcT`VE+548)j2Mt*S#srYV^u^B`F4)BGg+>_w%{jy1Ox5$oWA#F_pQ+YWA!6{=xnxGpL}Ukzs!%@e;yxHJk@mbMKMFpIdz}3e6A~DZFtid`VNU<` zcjvGl%5xnJCIny;0Zms^gyyvc@x|)EchXbk*YiPeI2umpujV-L*k!eUs?wXa@P$H+ zw5=PR+!Y7^y&9dkUM&~}pKdAIsL`X;@ftlg@-)Za!N$gYdHpBJ0HD)pz>R_gLa>jo z!?y&EcSr2dEzJjRqv-5h9eZ;E%!ai)iBuq=ah*Tp<2ixc<@tr2{>%2JEK%rgNm2lM zwr1(>)eDkN5ArvUhnqE)T>$PW3B&eqfW{Ld8p>BN>PLrIHeVgnR)9`jV zvJAS1YrjtbIKN`~hIwWB{uX+rfLEkRtS*{U3%&5i4cUEsvKK@|3tAuTX3K2u!Yrm* zW&72{&tyroef7iG>zYq!McY2qb82$(4KzAI03xy!%1F_@bq_}3;A;`o+y9Lzte@#| ztEAsKekTO`^fx1J=vJ3o#P8TC+y^r=lxt09zJkAa#~TF(Q}iIQlajHYxEn(x8r?7A z4LAg2h-8Ah9(0~X54`%(M8=f3=q?oxV!M`JCGj9f*3A6bj_*C*REFU9`Cbe{?nVPb zV>b9TIJ6Wux~`{iiT(}1JG*HkzPLz1W%5i?5_wEH{(Zd8zxfW|{bH{_fBE^tzlB;} zN2x`DEFInvo%b}3e@LZoZ~m9DG9%0E4^|Vn!KwaBllVUzt~m66vj2YYL@fUP)j0pH z`pEwkv6AB4O76q?zr(Kk|C+if zaTj|d63C=8acGsnYdl&?(p&SLm)8NGkPy9eTf&;9bcdAWHNcm75ncmLDsaD&L2rwD zQvenoj)}m@K2~YGmsCNZV*#HmUd+4lF5R7C24rN0&sy2(-g`e(j`LaGb-9Iyf*cX$ zTe}o_p8*rZ`QmVlA96QZROjRgGBTmLof~=i*2a|E=`0bXM!i@#bsI?5^S`ycvDEsN zCQYA|JyA7nwGt+{J?%MZF&++p9_pE4BB8-_FbU{S)Tka_t|1~(iNDC`nKYi z8q?DVZjteo>11ns?5|a7Ta!0>C{k^!4v+&iIR+Uo0FY&b35(svxV968 z$KnQkQc03P*XAdP=wh23ev7e>J1n>=fZ4`9#S?I7s;tKD-IMSPY^DJ4xXzUepbV;V zPAL&Uv5P9PQ;adI<$NuXq+_~dH^?`+Kmo<~vfVT4-_&pCL7@_YL1~joQSCQrr9i1Tdq8#tI^&>$o$X>xns(z-lRwL-|d( zStG(;Z$;Iw$Rl4VMK+lEA4&hsl4+na;jk+*jbOnHyq23#exgiqyLHWM48i_DN?KZ) zq&Gv*D0x(U>I-dEej7qtqh0o~=b-(;h}ULGSL%0Gf)(`yE_;&gXhv@2&Nu6x*&w@7 zCSYRwHmavi6qRf{{1OvGuiy)Ce!v)!jDKP)cEzU$Y550ypQUdAsnKgCEx-u8hgFSM z-asH~TMl?nJ<&aSMTMt+zI4ZpssO|h+oh!A(7iMu>bb&Aq*v709QK*)dV)quD6J58_%5T%$5R~KN8T%v~bv2jqBfurJvT9NW_`wdiN)EDMz=RZ7-2SwVv=F zp|0(CEbQGo6Fmu$@+Rx|srH|k8i1d7Z?*5?3{Fk4&NOs=viU-y@>iK|qk{(8zNeJAA}gs8@lpi0?(5>f1eL7|+1p?ckKMG3XE(@oWC{d~Hz7A^dBUE>7c z$+}gi6BVxIwO$-t`0y7Ov=?mr4#1R>QxRKp?%^uKf0KGnDZu9J(72b|;I)F7XvS4H zC&##6O9$IqOxP~&4JW|ozl7LjNe4e#gluvs_jS4#Yd*_fKUE!foNDm7il;@QH=>2J z?Y}mmF93puf`p`OeZ0&LtaYUKj0WPce&zL7OMn$oe8(^oq9%pg9bpI19`D<`s6;#} zwPJigh?d`}KW4*uD5m%i-)4LjtiEyJHW}GU|3ooJo+?rYVQi*n7F~0enZ8LqvZ=q3 zb2U}2ga6hDtR73c4d<2Q?e80eo+u`207VHKk}$rR2)K8p6vb~HoG<$*%bCC7N7ns@J!ciquIW~Vx%Dn}$rf&|2S@D^GYbUo|U zMY&mtm17{J61+NPb9|Dm>{m(5alP@eZc-xAOjQ@qSw~w=e$tpUE`?1K0xHN{JE=@9 zmqbfsj?~5_@B2#aFO_4Q!%QK~7OFBEf#oeqw?W$pp) z!Im~Kn}2_sVm?_q@MT_2zn1uUl|@a0F4uaG#$A}@K*HEO3dLiRZU}IboSMET`@c8` zMY5C9+BtD=$n{%j7&OVh51;WE(k~Yqn_K;!6B4BZ?O#8bw z+F_JaV)n&E7;iambZ)?k-^miH4mQ&E7p+w8vMj}S-8^bZy*tv3LcJzFvC8PD6 zal&8Lw(3M@xMJ~gQ+f$$<~7nT+;c1X>EcAdffN-k zIXYmKHr6<15;D$EwL6q*VkpMT@^JJS8;wOzsKAoaYi)1(EF z7P1}W4msU?q@?hCFcn#_sFYjAzRCQH<*u$JBuYq1|8@+c0vk2JRXUzwMwQR|apM23H4nOD>Y z#mkd@GmC+%^9e%V~7EG4jU7b0D7I_1;y*dZ$I(EFh*{Tb#bU zv8UIatS}+uakWZ)oaj^`1w-T&E0MHweZRMYK2@%}|M5bZjC<0HGEUh1j(f@*b@*r3 znSh}%yYc=2yk)IxGpgs1k8{n}<(ir~ANG|`2{&^EQ;h{#rx+D25??Z|9o=hr^IN#t z`3yP+kRa+xCsaa3>w-cd{)f>646eYwJ!RgLA?V2?+6Y+Hh2i3WxWV#62;iGmz<#{6 zIgleI)!ssRWQTl$?DBBjus_dWDV!)&Jx;J5=2sTY_l$jH4+jc`?Ja1(XV$zhO=_~Rlnn%t_KD@-ZOLpx~b@65z=g2k}MID@<57?T$ zebX{XHZ%B>bOlfR17HZeoNmmCvOP=esa$FH!b}4#swJF5!OUQZ^&N;gMu61;W^<_X zQ(${VUAH!B73-e-q`L-msX>FvP0-H8{l%QRFo$x<-Byp~)OhN%z&hb@DQw;{&0c&= z3RNWDORA_**35hr3@{eqWY-Dyf9;CzMO{fTm%Oc}?p}&-XAiPFa?Twa{ehZC*!V4d zY#(q0yA?A4g93hWj-~s0cW~th-Iy9?-vw;<5|$xkoIc^41KTpJ4`!wn*-ds9H;9J|Z{s`SlPU=i-cBC;AU8_J?yn-TU5KH_NI*ZV@G^Q$z-A6r@a zB_kaGD=d)JfH^ik4&TJ$+ z)+EBFrKv%?WR4!=@>aLrJ|6I5*;fMQTkM-KBH2BIna=H#2}^zA9x}fy6lyJ)9EaZ1 zRWc{k7dyf^5Huu?1izOU=nP-UVecbA!uG!&f!gvf%9y+zs0H49nG7BCTYY4F37iye zsDs9Tgd0lM3wmZe(l5tjmGUn<-Eovc1>1W#-@nqkf@VCd!|}DH4}PYR_1U82fR_>J zTj?si-k7d_sClGjAC_IPf_nUBl5IVLDRQV5FC$@c{2xE0VA?_7!N{qJ(q~Rd4|USG zE`SL+*_4dl|xhR^vh?{epVvis1cBM&Rn3GQz(@XFeAP9eNORaTY zl+P>A8!4tpCzc?2jZd#=!TVK}Xo?}e^i%arkBSSZz@oJ_wq&hdo|@Aq%eci0f^$^r zQ%a(T6)_0b8(JJD2sI-k5=LR@MSOU!`9w>5isyFRLj7v^riJH#+2kr&Ui;m+GqHmF z+;x!<623>B4UyJOoTrx<*L*h{s3-by&C9#Y7LPC(hnXhR-8ytDwLu#9D%k#_fBO`( zER7|wK*4nR(x>tpM&dvzW%|XhySH($s##fCzBCewz`_fWyDf3Y=KcXN2JHx^Zhk)d z`)~Z{D=T*sDwA02p^OWRWITS~bT5lUdr)8?;E4+ee=#1uuEt<+rDbKot^f3g*+(Wt ze{C-*X!wA}dHTAGAdu!9*U|-b5+2@i^zILB7svO`jjjafe<+I2?Ei84_f-Es4t6Rb z-hr;u=KT5JyyKh%c8K~0i$AYEIL}`iHTanf1O3n+NzMO`8;IXlPw3(j6g0yiSlH8c z;B!1T z7pSK3=70=Sh1A0bI%P%~+}4>H88G0dM7Y_TLo6+41=-kqr+WE;v3u{3X#Ls<4k-Ug zCiwf?Q&q^kA>g2R`SSw?>W;Rsv!l!~#+mOj;zYYs)}@QMa===eKoq-2PxO?XH_8CtQcmJcxKyJ#R!_lE4f_TkrF($fvY&9Z(;sXO~*<8I| znOhIYcLF&Mlo{Rup8|I$YdjBYjX+NDXt72+yEUN{ytVbKS|Q@ko0szvcv0xmCp??+8WB5>1y{N==>~*E-aqN83;|_RKD}YcJsst zI+S|K#gU5i^3K41tg`BHJ2;x^EENe14E%0wY}Fa!%Dtzc&}vH;CD#+eZ@E??J^U>+ z0nYL|D{H$L{0&J$42YxSg3UJ?pDue^UuV+UdV^%bKEk;*PJKpqot_apetTw7g?kdF z2Wl$kc8G0(S=gcm+=g=M>a!{wM8=frH@=vYiE8~dOn2=y=rG;y?M_h_ud9rz20m7@ zUQWZN*{^zyAmU&d?q5}VpxMUM-;c;lZgiSDS2kb}5#)F^nQw1Z0)pN6?PeHcLY5CE za@?7w1hwB0v75&6@IKJK!DrIPOJ}MjZ0mi;WcU7W(r_JTnj&aHVw8+yjDZa>8hI1r zwoL$6zsR zqkTGG+luGV$Z1!mAM`ZVCv^IBLT%~RO&auMy`q)1SbWm=de7_kuUt>6dw>a-{mW3Z z(vXA{?r{t{4g;3Y2!a@|vH>A47X#iywMuHB==T&_tmNm4mg*;a^v8lUz@EB^CU-@6 z*o*q^;ntUt9(&g-q@&G%Vn?gO;Shs?W5wV*2Jx@#0wotZ9xbXi$?D5`M^vv ze-V69<9LZ^0W?z}{)1^!Xsq6yo=F?_R_Xgz-bhn<94f32643 z-6RPJ^~x;*N{sI9WiotKx03zF@^016yFx)%D?&fLg7ch$s?w!Pmt#0E+2I*NN~ErP z(9#wccj<;b*sJ1aU5Drt8Cqw?sjFg(&;_&U*FT2O`Br*@kY zbD~K(l&hpmi$Bc|bQ@mkwJ){Nb^Y4Dy(nHdrWwVj@W8ve(znVzj^NuD6Ip_b4cy}y zTc@?Mgf%0_LM_F*7;{8ruG01t?AahWcthwnTYMP%Zmg5tI$~!_M;`HH&IYyapYpQ( zS1JoX0NeS)0FXikGd8xvc$>z(85>hf~=q^AKj zHSWGX%;#Q5phCB{K4zH5rgQ6P2d5i}g1ir#tq31?TaZ?yXP^gannpUy*{38E!A@nY zf!TOdgKJV)T)FC0zpiS=9VUfKAE{XSV1S#u=}U#yXAT`{)W#VYJpi@o_gHeD_zgrE zy$8nX(Nd$i*H7)hLm#>5@j9<{+XL^|U0^ArI9IGq;P;FIq*;spblCoZ)_#)A>ML>v z{sb=rgldK`d|ee^1DYlZQ==Jz8gghfsDygV<+QO&g#NtPixSg9A`YI-fk34tFtArW zL^By9R$7H~^)BCm5jc(u9D4TQh!|_0CZEq<@%`}1%k46beT-tKu(o~Zd)1M~GJ%j5 z|16Jwlj>JApn^YKGvu*!E7L0lf!`75*5j8FSGXp7_4IM%H3`$-T#qIdWERF*BrPgc z^A=<3Jla>!2c}!Xyrzouvb|0yxXuzTzq8g8v&_|N$Om+PNaRIcn+QVEoipTz+MTOKn#$-ew2l?Ckt$!E^#f%KNV1>98*Fs>*gsM1#_Ka z3q<`nwTfbG;Q$0jJV&ew_pqN(ZxIGnoM+oDSuc!9HKT|f%CZq3Ed;&c1slo`KW*9~ zropla2?;Iko7ZqXTkqjSMpB<^!1~_eYIMKp-O7U+$)R#?ynpXJ$N=8(4Do#{=iO_# zCz~K^j@e8d*JPvd0~9@+(|Zfox-GVlDHOx(;i`7u;mUwfopucx7t1Cobzq*e?jN{K z?0c|00@P?!4<2k@{NR*mP8KLR9g3~6j@V8=KJBoZaK!G7>c=*oXM`*^dX>%%Ff1Kj*Cx1NA&EJKj3%U3==7onZVk?T)iM4A2v5@MM0{L=N^L zXaC(JX4H1=>2cdnp!>n!o@jjD(lljg9U9cB>{xh8BkItzlxt=nm1DJHcsL&*DxFVm?qQ=Dm3bUSIr9m3 zn%rkxvAEH3!6T} z-nDMxp1{DHD)dn`X`6R@rlQ5E?GhP5L7MTKc44avul2d<-Q`j0u{%#nL*w92vgt3s z60#zTPsz6)B*8O&G=(?%ZMi3!S(ftYzoUPSaNa){1Hq7zu>F^{0+pr>TPxyD-Edp@ zpKozqH1zIiz&%3=KY1+h4zFXCCI4T(2Awv_slx0S5$JA5PMR)^e$wU|UK9tkb++2Y zY;%mK(yupQSHwz6cnmZkz|4QHbmYr>KgWdzR@2(c&4Lv@9_!dHVBzmJx5b@PzA&bZ zvv=S&G^&(`hWgl;fC6RDq4&FBODQDr1YAxVAzYOGk_8~PHtY#=UCp?JJ*R^pvlbB4 z$BdJBMOYCdKa7o>EHTxrdl9?MsAh3%5IaTMeTb1fkwy_Hda1{W~v+VulH>DFZJ zN_mF=XNf{G9m>M??(9)V2n3Sbpior8q>{#eR8w0S?UDcUBd+c`5m~0P7VGlIzMc5P zZSTm>Z>mkC&PMjP4nKO zseqtxIaX{WmEK8SeFMFe#$x$$yyw0-V|T`WCvO{Z>!hu>)oR?o706`QNV9`l6<+Q% zw?oWO*5Ywx1@aF&b<(293~DDpr^83X;J9n3Uhjq31Yz;^)0I*P*R|fv6VNKVg6nm_ z^#H(^gVX*7b@duEj29Ty`KwH1_!OuU{e=(lSH_ApW6?v)g}ir;u4MHOb8j2LDoTzmx<*fTJFS$<(|*up?@m`;ukMHj_B}1gNiH(}(D=sx z)KIvpOBi%ktxV5MiwE{&tf$>EBGwW1TC8yko{M{f;uq8-xmqcKnqVL|jXHmKq6Es7 zDGXKGXtOtuiHR`{N#G|A?ieVoNVnxH&Y2b%RTY(yU-s5a9g@K2nyd8b_(%G>PJbf8 z4U`g|u{6@+Mp9i^uZm5{sivL{_;+vCP+Bq>3|m8X3}el6IajmFL#fkGa?G(WEHjP1I!lEy#NpX7SM+l)g$ z2^nex9(dS^d+I`_G9D&^QqWbLfN(GhJ$6h7+deJ*sqdiv|FHL#QB}6x-nX5Ef^-Vf zB`uB8-OU2&Ty(fdQR&V_cc*lNigZagh~y%rVNvgadR_az?|ttt@4NSS_cO-hN5&A= zIM;d1W6t0F|L0sc9n6pX@MQ3Oz5iytvz0aP1lUw}bZ%?P0WX~5EN0`cWT`6s0j?@V zjH<~X@IKT?C(l9*Ael$v)7qlZ}85=f%DD9A>|ZsX&4?>NSN)L6vrVPFTLc-IUvKqKIsDMhFvw` z=1e2rU^ianDk)JxOi#FJk32Cp_H54OYj&3I2(E>)+pLhX2JWxO)^51&5#C3=Jq|>% zZq71TN@-cxylPx77Z>N)k_t+dh!jaNaF41oBxhj9|N4%5f*cWDmEi_d5}5_7V=cOO z&^y9sRIc;qeo*OB_LfwM`6x&GwynV7PZr!OC1I+vB{Z^IJl&MiNmH3_q-*u*aZYL~ zRlt_4W@p9`Fagy#P7RGv1lrDK$GFb9lBT7eoe^xcnIvy;PuDnVP0nRPpS4{FQ=p#S z1ov3mqH{-&J~dL8#Vo_VGhTzG_4w$ z3lN9MM?=yjU_e=fgLtjW)T<%0CbzuREq7cY@uw>9K%T2MRyc6lb<2pcbf%v;h%-E9*}bG+CBpIwhi#pNG4!!|iD@7W^49&C)3 z=Wn{S)7k#;cHuJg_kVwTv=24~CPKl=6Y!7i&rkms=`DWgX9mjNrsn)M@V^m2qlu)` zG%hmL79Y3ggrofav3M?B^)^qYGF!0H?;!whm;X-~gKtvg_L=zjnD~X;{b4{%~+w5kZuHF8uBz+BZ4E0Q_igzC*{&!6JnWmy&@xj#V1hQaf=!+L%&I{fkD zHKi34gjThwe(aa|460G+^lnFgv(6?ydw~iVGFFxD!1yq*t$2H+F|w!#>UPCXoEsa)pwljEJ+)^iyBzttQxsCm$H1U~ zg4|1)fS2e*GW38VlPgmaYbEI2yX2nu=JAhtk58ueIz&nPVDp$yR#qJMMmKduLqN7+ z1X8jeb98lJB95nV??Y5nr4-F6bKD%p!DOv=*+j6hqzueOY6T5t zxe`P<4liu?>rXb3fG2!)6`m=uqM45xhjM@6tv;@$i^SwhL54w z`P$V*+XriWv-66>0^DQhXlTUzZ{FH6#eE0Y06H;$%xq_=djdAhens&udaqlS^apQm zCNMCn=j5Ts#r5;%b(LJfrWZM8*=UYn2`8RCHuB^IPNR}8)y^?0Iv4(7q@iaRHE*dE zVBlN6mTnKD^LagX=epI34tASNrlkghDd;Xh&n6$q>CsgIs)O7{HxhnYt*qj+T<~k$ zdVOR-YHiM>QQIziwhqqb8W@Qs<#WGSyS&M^Amn=J%`CK`%bfXOO{X^}NIDbacV&B; zSNzDeBZ+P$c1P(B$_YC^=99+Lb2jJC7zhf1r|w8}W)HLfl6);}VBz6SlnEW31ja}d z33`}bAu0h`+rHJrc9Dr;Tn>3fvx)NU*KXK1Q&L|DF!?{P+MaY>ZV=nI?cER9I@!Z% z4o?1Pv3>mjXRT-AVVYi=In$oS6aJR~hCv5)hgdV6>n=Zu9by_Dk@xA8aSDLRa2i^} za@tS-@hU(Xh?srAP0@~r-A1mFUyVlmWe3+#)=&`gIF_x)zkRQERBFGOpDdI`ia^7H6%gzJ? z{pK?w?By=)8*cT3)C}%tIQO}wmw;3Ll(M{BtI5WAAJ!Qn;>*F%7KBSX1AI(pRdaG< z#dFH&_nBEg(`&GhOw@}&(L0d>hg&xo1NK7~j!DO_J3S4+9d~rn^Lkx9zL{j}ayXIf zvNp$E|LY&Kyuv4J5s@cnm*YE!hkkmuw+;LHPb*8@d;3^RKlb%I6Rp;&a8CI$A@x=NYi*u9SavI13RYdUgX#KLKlerDw>)uwB+ zC1Xr1XOow`U$W?29oGpFb=(ye6HSigt=($MEDFA7!@t- zc$S;?;Af@CFg0d#Fz)HuNgyOaO=>}kIaHKDvaNe>a%oQ~M=}mKZ{eh8If5v2!ecPox}VlL$7kG5a~w-6}SR0TCDso<{l z&>qbPLd9IY5Ayvi$e;5MLdGy|iQh5ts7o`gN_(aewq+v0X;|~x zt6|q|dD>fiB|y0bDx?WB{xPl=xKXQCsG~O0JMlu)Tw0W8DrU332U4S)Y*Zti*Nk>+ zDfOvR$gA5@${%|poI3+X^l+$c4?smB06hLP496Eo9kTWPo9xPDF()pxhcHR+oitK`` zmwVXvnp>rnX$Dt&;ZmpfTTFCSV%V#dC~|GkUy{tMkFYe0Z;V&Up|wB6L;IRhTdZ^LBTm&|;Du2+qjk*x!|&DIriw{% z9-GO+lZ-ami1y()`>87PeBt+m?UGR?RbLG;9rlkU{I!?lRj)Ty`QaPkfTc?-pl=XO zubcI9s0x%rY$j&-Q0jV=szGK_FM0 z%1{CdJXq#Kkr#?r`NAvXFha0KstW8bw3ER<71G2#XZ6O+fC>V&U196 z-y55on}s98v6nidgx60D(nZ&CnJKf*izdaH* zpKjl0D{*TtcQ>7_vI-GufSBL(vi$+j=a=6a^kRK9O&UnPyxzky0!n8;_ zKd_LqjN?DD&e8SRhYKDstn+#%ek5Ct(0yxsc0#G$b-H0DWBc`O=2^HZftiKGGiHxn z2behH@|yNb|2q@r2kQsj`Hlp1W2W+%73FLXjpj>V3_a3K5Y6b>n{}sH^uge~qV7-q zv>2T2kuhwO@*r+YftB=xt(~RX)%;Ia#;9b>i4fZOzmNY{4w1lpBP<5(j{U((0I-r- z4EoOEB8g|~URbvYBb->1wM`QEA=_I?drO^5VG|y!pZK3*UY`P)gBC<^umyg%iU^KkhTN|zG`2B5rK;Pw)le(hVE}^7%9vtlfUJ*$6 z22$td2*Mx$b$m9f9pEUDF$|JEJx*=n>yDyNW`xEc7A0VvqG z&j7CD{a~@)>Clgtu-pVXmRFC#rwENnL(GXMg()5rcOx}1y~ZczN-GPxA~&b?y= zp{XiM+WA71?de8=!Q18)X%S%dr}b2wm%Hm?XOd8pFbid55+u{!?3*dUU>7Uqb+HdO zZr)TYo{%rkFFRBae8b(FUF9e|<0e5B_dbp3Lz9FL3)MJuati+1Wt;Org!RQVLOuBc9_XlfVGb3XSC=~$jz z_o{Y`_!^F#S5XjrXA|EWSDb^Ci?BSY`3=|VwgAf*^<<4vz1vrjz$Xvb&l9SxDOe)O z&HVeD93bCt_6mXt?@Pt3t!eX{DH6bx?d`+i+03e$Bb3^*SyCvgLq&!=WYv7zqs0m$ z*DoZ}@Qv^`Ete*^B|1tjBB`YEKR8@FgSzA`wulBt`XX?I>rssi3}(`x-4~7Zu-V2G zG8l`*{oXjbbVsuaRI{Zx#A#z-K#gnvI^+`%Ud{G_VA*?=Duj&Zn}*A83KKd!Y7Q(( z6lx(GEOwx=IZ{w#IZ-9$V{h1>k6koZ!7iKl0_-E+Ir3z69OcDR8bIuMujcf|3+Q$A zfUNpidFlNVkUxMRzE75 z`0Bye0cSWrVL8PIsfbXM&pK$tZY}@#{+ekf*3O~Ev~O5EP}%M+weE_)l1ZicsCXkz zL^zw$NP+!Ie*%wd6@9h{xEtqcO)e|$XY{%e_<6DMGa)eGzWOyUq^kMf6P6n>>!3 z!88cn!4W0Ch$u+g3sh!$X?wPnMhoZY?@k0fwXhh?^~hWYC*@7c@+JjvKDEpGh9<@G zTQm{+wFkD3XzAzXst|A$_Qs6pova-gbw^dm(?7dew5Hj)uY^`TN$pd<m_DFZG&64fbGrss%60>-*9FtPk{_5 zVAKy|3d2z25pAr498pNV$vT6hLYjr3-cIqA2hNb+7Y5SfCffb#0512YS5u&ZTx7Oe zeGBpde5&yZBVR(IY&rO-1Ec?ti{m=4UC#&NX0!;u;uWT(Aim;3GY zNVLG-U5P^t9rl(|MZxEgbk~^8+mC%L^yI6uLNUh>RS38-Nm`R$$(qE-)jC(F$lwrI zRX#Sjh~)-?562qqB~6yz;D_zQl!+S$V9F@P%|R(DW(&)L?YaW2%SXYY8ek#gN^k6V z*?E5f82$(c=OV1+Euw)3)m3PyF+Y96CjwD_Kunr+s|<)QY+@oM3Llwu(@p<5tDRpi zi<&&2BbH8k2KbY9(SPo_W__4uq_Z7QPfbnbNj_qkGC>((CR-rk-G|})?BmZ!a%o{; z6{!$xee{*rbB8Q4X$A)}%K!ZNx4kX*Q)wqX>!B=#Tcq5Xy|B4;J!FL$P?+`7L|}iZ zB;drP6n_nW>MpFN;d;bKI=$T#1UK@Tq!UAOJi2*~nW9!RvOJU{S4M`=sO)}C;53%4 zkn!lI`|;2DWoUmU$0(3%mmQzpAH^3TdH;@Bz=Ld|W#)S>=ji>Z@5Acf!4l9+xK3L^ zwYV?}PNjJUF28B3jTFKr2JdC1zu!vZDVRL~zZJvJah6d*V5B&2J+mVX+Q855WiK=m z^U`Zi)@sK^bYu6R)c4p-$(J2PiF9&2PbV@JW?7|zr0$A^EA zm|ym#;HS?}{a;$Bw_AUaqM5(OHP(s!nbzX)7Z6+h+gA1G4_r%iy+!y7nh75L?J=Z|{OCIZr^VWp(-e}T;r7mTA$Ewm&4#-8l z^K}s0d7O6!hfMlioi^1;b%`C%977XWR~TK8`x>B>0MN`5R6~8*;2lljHQ1_s2!F8L zKUE6puCZOfHyNP5@rdZCk=Sy);T#|`plQelHwAwNge{QmxY5NOa8;m8Tj@iLf7B_G z&!ST;nX9r7g(4jUDxpsa$Zj*MJFK;B@+1p+kPM|uwCu`ekwd~$<_abr#^4^wlgVu* zM$<*Zy}*G7B9&(MQ&}Qwtf{s3QHCGwp*f`4yPkW-y~tjG?n+Cmid7mV=U<%!in^?BYv<@~?z(ZE)r@mVS*vP0W>f2Oc7|ngBEGca z9>H5soJ1*T;S}-OyEU8GxPdVbf;+ngRMOFHZ^z#IW{W7|z>BqAmnKmUn4NyGg(g%F zr;CNAMCHq?G@(-j;%HQx1)_B0R*HY|8(5%9;dg!MuHE1^QX|NPQlwg4N=UZYkqjLk z_isdA8rRj`MQ?5{8Ff6lAL zT;cV1-!$^}%?VHO85j_RAU5l(ALxJNKli<0hWQW@yg5DY8t^9c$Xx&)TIFoTgZoOx zT?3nrp;~tLN&z=p&?aw}#(m+Syt~*5HVY|#E~>I+gqANe=rnpnYH~i~1kQL3HXJ`| ziga_#rF8LLtqQk=m`#kDgcFXpB_bI#IT5Kv&X-RUsZ zzA;`p<#0>^G$G`krxqlXy$SlVgLK<$mfuP+E?1aTt&m>_*b8cD{PNPZISoUHvc#G# zH3$QqFpi(Eq>w=dz^x<;_M?qdM(DpVBG*;OvCZ(|RX<_3rs;9q7y8$ziI~QV*OyF`}Uk>&~ z-y$F+Osp24ki<%C3&qDr)a4Ib4TBJny>;*^ICK{rGGS$Cx_0I!IM%IQs>MGv5nFsE zsB}2-dw;%79rcphi;B_f&8e{s~aAAVMi~u0N!cl3Jiix{pc8)^l*u{!(#IZTWM*R71jh_#&A`x_#=TPUahtO zEO2VVTSW(oZImTO|)ekd#^-UFGC~t*W3v6JalF&a`dFaF5+YPG?zIOpzE^s&SamXdC9UxInfW!*T9K2i_4H^HN=BA^5vMf}jtkb7EeXllDKnCw zATTU5YSZ?!XoCTc?S2>5vxZ#?;z()B@^_>YPeP497+9 zz^4e)WO!zs~-LL7|QwRO2T1@)AgWwtc;wvuu$Nmj;2MLt_z^r7x z{u8sx(B01(E^lX@)R|=YdtQJ$e?Og+v1`);v+k3%wz6mo3eFOptTGG=i&5)hjb4V~ z>l&~z%|RK%lydnLua=&(yeCaXAf~I9j4c{`6LXGyxFepSKomwC4xT(-$pE<+kv|5d za_HNoJwBJSOhScQckd>YrvsF=$e=h{D(FAC#o$K57jki92NfjO=}J$EEjW3XnKVZ$ zl}@P}P`}eXn2N$|xjYg(9qsLD*!PjFzm8j3{n*=a;Hp;l#lDZ0$y1?tQr5;S0kzDp zt4n<0n7y4kzf0c^XAx!SN~Bp}O7fU4wOtD5OCT67nrQ4N9h!bAE*4NNC;n>w7m7;d z9{fdX$k=|-niD{4j(lhN%@x2^2JS9M*(((=F8BZsx9l);G`#O%?O^@GeGFVR+7=ed zD;S7#%>Q%Nz2xoNBQTu=!&J0+RjLysN`U7~=nlrSHy68E?_xKlfCzK0;Ax8(s61Fz z3v)L<&ow?&2NY|^5)BM{o=%ypQm=h;2*Qxh{x(Bz1du?``RyzoFHV%>*qQFE?hMI` zfO;a5{4OZXRI~+18ekPKevc>KeZh%56Wl|BsKrSu_OyNJe*AS6HU_UzRLLZY%6~~ z+j>B8dQ~cHo(@TkZSmN==?dc7Gm&+9v;Lz24>QZU_l;~e$SNA^o%`Taa3SNfewlGNgzuZS8y)zekBq9+NKeB#V8*?Wr+x zu&&gp1-rA73DR^W+n#dX0@bx4a^tpfiOfX#WhyUEG>sV{x%p$1ELxx@Qhrjfmj!pIM2`V_FTOv#SYWnIjvadl_Y zsWQ)Pt7PY2E4mJEh4)2I@MnZ>lONdC@6xxR9paHQ-1u3ku0<7Iv$y6Lx{4z)D{$ty z09blSYSV&fBoYhxclI)0M3x2wBFnIr;Fzc;%|b`oIuDlg?=J`XKExeVGGVMy;yNh823w(1;aI3ZMgpvHd)dAMc|Junq->lO&-@Gn zGyf*?GbNz;b*k~t9aT%o1U4p|<>Lu~;$7vDl1P!2xR{Gd%Qjl((zl|1(soOYFK2Zx z&Omj%W%1Fz|H2GYG$DldzkpV^nWJ5lzpK{jr`H`_ekd-fw@ixl2-X9q^3iE?$QqIhYRiyn-GH-!>g zk2f?550ETgL;Nho1lw=m8Jobc5mzqwl^TD9(p)6prXUyu7OX&H8XvlL(5?OXDJ^5o ztYew}(Q&{RXxG8ktOoP+luAf=qPBUJ^$f3bF<~|)y-t(M=LoE_BG#ffd{jXSNrtZ6 zqCnN;xTd+cLYntl6Zv)1psO9WySQ8qf`OE%!I)S2yAZUY94QNK_*b*WI!9tI<5!`& zcO@dz0hrQGqRSQeXEcRd%vg+TZr<-9G^~{NHCOLu@QVI3iNrUvLH-O=mks7KqOge! zsk>$cxgvJBlSN0@xD+K4C=YhM%M+#Z^V+ma4cc{BtTls?y~BHPoQ0at49FNjRd;xZ zX9OS3bi8|DwAY488$*YgL>BF~o|U!sI;C^;(3-QQcr3GWhyJ6Cmh%<3y{7EWWhp<$ zy~SXEDX;0{N$^celjwnd{^~I5{az{6dlDre-6TJ~Hxzgxq741|3vQK^Se4~+4lX`- z`gAd#U5hAw#2mb_Wzbr-x-Ug?U^MW>8?-p{&~)ouWJ2%^zqhBj<{M;^&%7{z9$|wb zD#8nJS8*S#({1OHur0iUhU5lM;yP-dGyVAptoJ^_in=k*w0y>2AF<;2;fiET3Kw0G zP-gSF+WK4BEZ=tEvjy-Tb`!5pYEP_`WF5~I@wlQQysk>0it=cyrv?MEEg>w!6n5hni6*?dPraQos;yJ^&I#YiQFOFM9(%_9=pLYJIPlEmR zN1Ln@t^S~W4DRx$(7sb0^r0LX0|SF|m4GM#>*8X!`(}}EW*eypd~7&%{meje&jxa7 z!v=cR4gRGoNR_TuqB~{uF>7-59pSa2uc3cs#|u61ON z*57G3-tT!r-|MS@L7|GM9;#mZ(RBV*m%zrqprwJwj~>||H@p=o0%$KAJnbR6Ky5Yk z2P~1sU);QYx7zyjb?!Dbu~^pLp1n;E7N$+NN`c13_C%$Dmm6$&=ijp>f6d2lS3#5JHRv#a zZFn6CRBA>X}` z5L3N19y#*4NfQePTrj~RAWH*y3<(+mwf5W`g=$hSd$JXv6#|l*_*mhfLkFE`T3_8! z0=>f`g~~Ep=e^ymxlDxp)pf=P@pWQ|q>#xcy|edksFRG8Tq1w{8w4$$>13`)ILU)p zjm-H1rblza!}-1Ksi|NkB9wy=3-sG-)x%$baX;b3Qp^8|ND&0`yB(8iHGqN>d3uVz zH70dE{b_nOs3Mln`uI+PAY3U;JI=hgkAq{hjcnq+)p)}uIKLy)+O%r4Bmy41_M=z> z+W5Vq_LDyIgKzdgyxKWW{`^#H{6=ecXwq{PG)zF0*%%_s>l~IQK|Yup3R<4q%q zt9nS;J0C+aieATXCYynU-e5R=otP7ZSvQck9$a|>#GhzqNzS@7rVLhWz zB)fo4B7h%=O|LPKWzsB-#|l_pS zHjp|H73o2qw}|oRA21%hW~Eie1}9j$OH-7V?0$F5eEe)7<@$#^!L@Ah!QoY?Em=Fu zSemfq6OaDyendP~D*d9(V}~5Bwcj&a$*Z$zP0~#jqTHVA83Q46(|QBRuc4iK1r`LnXj9`;TvN$hhBnV7O7Y zBn)-H;*t~0X4)7jDnAS`JOO~fY^Gu7=Vx~cfnKf5jUcXvph(P*aWWk%b6+??sgu4Mgh=POI~%L?lh#I@~|w&(sB@9G&Ao>G z>C2Zd+!7dc1D+B*u`apTT1!QSa>Lek)L7+4A7ymG;G65s!Lj_VUW-ULqi%Gc=j}R>fexfXGRS_M0*qqPfAi)| zI+X&dP^^M6g1}+oNr8Um zt|DLlDD~ja5dg)v0`r6QU(Dhy1z3&` z#Jf!!AZ)s)@Z#EmbTgq77jPSJ5<4`xoOA2FyVd4>rMZ#?*Zo5`k~iB;YJ#6!Jv}`q zSo6q@)KAJi`3*^5zFT3^&2RDWDDgod;rnRa&&0QquGgqpW`@su<~b-GjdnI&Ye)U9 z2vlizg7LHFo5pUoerr+c9pQ4tzU$u>B=ELBl1>Z+k>-73QVEyN2T%1h1~WMx=<`n8 z<8?j!(VI|>p_69-b_^Ea=6^sc63L61KQTLAd-#>}NMuBm*h3_=u3OObGAT!a0vCvOUz{V}FJDrY_Ehcz&~u4+AasFN}#! zv^tDBg9H*^N{{}N@@kmbACp2+dd-JBHDvf7UhLyisjG<-5FS@y;*g2*s`cTS#L5cG zs-;4~a;q zefoJB%z4i4Rb#j5!#h^EjLm7KjUc4nRz?oT?LBZ{R0e@WAwzjyH34)A7P%xM`s<17 zQ4liQ2c|&Ll_d2`(Y0R|KO_+tE686%7&6hP+dyb|{L*WmdfB1J>2P|5_>27t(^6-W z(x&k+wPGXeR5d~sQ58Ns@`8o?p>U9qM`bv`gkbWu_f&jxyG0}Z7?DU@3-oxrXuxjvQ`-qS|8*iOm1yuUhFDqQ-d&4^K_N%Jsz zFc>rtFR8ij5uT1`&Xo-4iOjjW3rpUU%`5W z7eW~7c#PZ1F5=BP=I}DwbaV2AC~%TH-?H5Uja>`m6h)_oaUaCgjmEc$laY`WI&MY%2vSeSv?)1xw797=g>z>feye+43+64msox%J^{hHyvQJY33in17TPkC zY5E4}yVC1ZCH7v(@!q7!V3&&YmKg5&ZL(~CyGqzD8}R(nD18`O zzF$4rv4uE4J;ErDx7X76PLiH{FRrt%?~I%Rai3ayQUiG{h9Nf3&EGFR7{7=Y2)PE#LeJj`2` zp>oOm1)!+~DvaZu1-!u7W={cF+q69nBUVpJwLyK1?Lr&-)m;Y80q`RqfEmnp2tfBZdNV~Gb_Dsh&MwRh1JB zWZ=e3>iW~Be!QS=JAC<48nklHbQ<|RcK1PGwO?_tR4<7E9s&{k*#`G|i~fruo& zk0q+Dt;|}gLxX<#)cT~CoM3y;oZF9KJOYb#&5?p9j8StH`(F{|f$-u8uk7G8oRhS- zwr3Wemw=>58Fl}7t^0@vTlFs%08}XQSfpg@oX|$tddMI7R;4@vr^a_6>o3u+f4Trd zRRL*sfM(@DvXmAuc;r|$cU@lrDp>+C(IVm-y3foRn4n{PR(*-}tB4^Jpu~dCq?dvf zhNaZ$RcTJ)KH^grS7NVV``=S06!QA9)GixKE9R(!P8^TjsiqGs zsb$rwBmjl&)xe|}fZ{{gsK%?}A0RN$OrPWnC z1K7=?w$1yS-UH*|#XzRSP_646>>*roVEVRH-TsDko%A087X-r>R63Tkbs3sTZ6y32 zY}mjdkuKHm5yxWu0?a#cv!82n&-n-Hm8JL5ZG?L)9@PkL+*fBH^{M@+_Tg8?vx&V1Mc|K~*`Z~mAC z8PmuPhJQ?+vip~)IJ+9RnS~hTw-8tK*z#hHJ#3IsNbsSBiy|Va`lY3Ss9rbADWm?V z0W`FWbaQ;(;nF`C5p^y8OZ63zh~F+;`{!Fk9h{C_a{)Z!gb~P?^R>3GXNx?pL}d@C zFsmMW<^+pocRl@ox^gHsr9~q9-ub_-gbk1)~D?TP)?Z6)_ewi zH>2^szQdI*cRs6G)%Tr1vr)zq#|nKF-Gv4GF*C><#+#m-jB}!!ps`b-xAe-l=a5A{ z$9Uw2Y_aPRiCvz$EHh!hOcGbMOdusl-6Kc^JWK6(jy4K3dSwENwHrOaT$u@_%iJ~1 zJQdcX2&{ao>ADP^qP6g1i`ID5U5E~lEp4#Kw6Am|%Ru5$?%uw=M7O4#rVTiay6}i7 zeIlQR(<2N6?Qq z7rk~VE$5Ey3}9BAF4_xypx*#20l~EueC~(ZH;(Q&T1Z|Par1W_JbJDhrKR&Y{5>N~ zr{2wDO_^qIxo@JV|C3dcXN1mmk#cD6jAf8`=4NFc>v9a8lH?%*FlBHWOOVASpY@RC zXiB^8^352M9i4GgT`{!byc#S+_y5_U_{s`66oC~+*jmWI3-~QLw_JxKlb%&8=@Rq1 zyh`qdGZ?w?yKM+(0_rIN%?`M=v(Q~`RM@XIR)5sEHRGA;gSHE0_)XG=$*kwZb`$AL zoE)9*vt`+n4q{2z?KIm)cJV}`MO)ni#znPfz+OSu4$qQ$qV(uJ=yyf|HQ2Oz>6bY% zizgz&Gh(=M_A+75UnY*HIP2i6JYkN!B57h~V9R0BGymuj-6RzdeDXoe90bb<2g_Wd zyLJ_cn3Ra;njxCeYc_YL9IR&>*zFw$A6;o|fj}&+O)iN`u1Zs&(kx97JzXAJv2rB{ zQ|^S?FLhXPvv6NMW(Q53`G{{%MRpA%tU7Ey-+*DYld#^ruu`0){uz#!msfQDNMw7!$a+hXG;$>iGr1AcgJV^BvIf)= zCE9ssKwsIxm;vJ86|t!D)yz~7c^nCBJC!btlqB!@>n>qL*m+WAwjGHy89;Cmnbg4M z6ejMfNEEiP3*c{@=RwRr>A(z64G29~J9ZopgKW^DG{X~G@;2$`dpJ(VRlw*d%m%Oe z4{6v(5rtx3!6$COzCZ@*wk6l6zUIKbI4#m@^Z8+=c*d_6*w4uodVG0dI#(Bm$l`p2 zecd+(=;b7vV-U6W|DqP-st!^JG@Sb>l|$lesSPviUk0beX`5BtIMl%lzenSH*qqol zC{lN86xxW|3dTyV{0oVKK1R}4?=RC&_wS9TI@hmnpP!$DE<;*Kl-^SR#Yvdgt^3K% zZh2@cz{ptF3yh4Zz;{VO_o0F#lCsj@fdL4)seRry#~0Wi(xVNkv!8pHug%oI1^!}! z{$)XFZ1A+H#D!it3BLEelzUYQTDZ%7(LzKeS5LY#=VW8zA>tamM~P_f5Gro+=AD4^ z{@Ppa!Bx%igj=ex8n43*&Cu zU|%3$x!^~l-)7bs&X*altw1H_&X@TICEDv#m0hk~Vx2=zeUho>VC7!TS@ijQw z0)@ojX4PfC5)3+&vp&;k4sJN<|Xqmo9iJ-lt~T?1=iO$fa{d>^>3>LIwfWG zP1k!It~M2|Flk>H?4neP*9VZbtqqpWctHprF87~K`7y7>e)Y$op(M!y^h}xWpA};d zuc0y|)B=Q*eet$IMCoY86&=_=sC_&XU{{sic&e@^Sj-f#_*R(6KNeiIl+(3r+OHIbud+*WtBnc^6l97hiGBC{-V3s4jhJ*}mbn@uBMyr|PL4?- zy$U>ySy@I)l1oPFZ6A`FZ)bimHjFJhq#525^B}ny3C1Lem^`v;DP;yol zKBv`mPaKP6y)yT^VX{ryfLQ6jO^zKu-`@bUWD`RshKF)mYxm$lL1@PNd1!C;yo((_ z^J@H4147TU;c$6Vi%UOd!5ZYgJsOqD&m{OP3k9tGEQhk9QDx*0cI9y$&$ZpXhdf2E1U4p~3g|?$Gba#_C_R&-gr#un zWI%Dv%9*dcRAXf(y%DrV4^E6bg_siuEvE0a3?I4TVA&oekJY~oOPH59|5*d$c@K3N zk|`3_FKcuvUNz*FDkhvTT8zN5$XBzSD1*XLRyr~d|& z4#=tVhhHG6ZByUWzk_d_N;*!XSvJ^^gX5`yLL76B7(9d`04s(pXOLp?l=UBskC0a& z69E-U$C%CEur1413GF&gZ(Ke$g^anJMInJ-yb8epC_#Yl{rYHQaJP~ z-o?sfOj7%_#IG=AuC&Ra1zq*_4e%G(2iXFm?&SvyElck)u7RFVWTVS)o@?PsXRrY*H>5Qs+QxxpVHa zJbl~Da0}QXG-KOZB?WT+#&2ulb7G;Kma{tPtLS!yc3>(3qI|iV*`?u4N@x@iGEloS z9IOvV;n!F$O}3$~eckkBwq$och2a1c*3mU;3}us16r(l{x$#FrPnPmg8iX2j)lEFO6R0P3e|lA`CQd(3WjnevXqXtiyyB$TBx!naZcYhZ$J&cJ?dm_yMIhGRLfDUslDiX zW9=vKw3-iFXQNrGSLUvw-wuv61C0q5F3D5GO7hAj%5nmy6{)VF0eUPO##&8vRAV^G zB}b}ry=qGHdtjYhtfxQUWZ?y|heV4L-O(Z_i*D3mo%P#i{-M6!0%^}M#KCvH1yzC0 zSt%dT{pRvqezq{f{Go!)1Ht#b?ZjYo5y_n3R7ta-4LSEy0SbymKiWYuiXZ0Xu&Ltv zN+voiK;j6d2V5aKVx+LL$m5$5DTcnqjbK`sE;fUg;M^7jZPq!agot1 zXjxaI59_;cf5Es1Ioj_FqpVFgnX~@%m`0#kO|pZp6vLO{!Gi~N3oxc~_cr;7*c{Uc zK+o1NARCRQwKS^%d2!7C_nE&XY>G5fu>34N zv{Rzd;JiO*=#k8&*W33~UvK;}BL45OQvOY@ z$`$vw>vF1pf4(MzU(w*#XT8R^Kt=mMX65|*@Bi;HmmY48=?>I_j)FJ7fu7#uttgO6 zoAhYeoy3cIHPrHM!x$iyv)dyUcTa=BzLe<(cjxpEV9=#_v2ZlvOSESyO(gcZef}*NT+asp$Y!K)PlM{()u$ZO_=z9?p)oiKZNf` z$3CT|7DwCNWSyjxj3rebI7o1oO{zcMIOzsvV_W@pju6l9o30zfTxJl8@=^|{q6pjh zkO@;r694wWH?hgb|4)0@84c&!u9HX=C7XokgwdIe=+UB%QKO3z(IPt04QXVP2u6!C zL>M(9L}y5JL-bxF1~FQUI?Bv>>~Ed_=f^o`ed}BM@B8Cj?|RJg6#O~nkl1jAa>>Y;&$dszHlv5Ufl^B^9PDpW zlm<=L1uyJ@@;Y*ITy2<+%FMvvH;J1MImr{n2>s-Z0~SY~v$gwxb^;`aJEGY5NLlKG zOToL7bEj=fNa(iHFfhMc)aQ?r&Zko#jEUG(W@~+Gi0-uzI5aD8OERz@6;89u5>p)) zn7eeb*P`N<#l3V>U3`eb@8o$w(M6#oA;jTYo4U~GWFEYjjsB5@bq&EYi1ZWv@wUEb zN1f#KZQk_CM~)03^$(SBv5%h12VejT-tcsTzSFI<H_=F!`Z}8vl#ooXg zi`rTe@0xi|Pu7yX#`Z)KOM#x6`Rw>q_1WxpsAcVZPeiwW)e0DW^qefKHe0TGoMRoi z&w3~MR?wh(2#u3(=Ww=({rbv2yIiC95j)E&4~No4_wg#tyc7n_n$6aQs^#HYIOM|b z@NFqj6HyH&vr6l+GBvsHKVv(h>1L)lckX1a!DYop-gbj^UQ|Uq#d~N8Mnc{xjOj7? zjt-3wZ@Z!DA?}2o-rt<#(@!4W-%8N&U#YKf^$2VCCQecl=|kBSJglngym46SDV)V@ zcEzRxr)Te)2+K&kBetmxi!Jh7sLO*NEPdxMd`3{|etJy7*Jm@B=YqN3g%q^4$-$Mz znQIO-k?(%Z7PQGVn3l;JY6V3OfrA7$O`gqwFr@)2!lV)!gbm?0$wbDwY1UwoV4MObUo)`M^;1POXJ+LZ_n z^5J*;o!5qI8AWXqYPdeE09Gs4fbx1#uVXYL4GrnG%U-kJegD0+Vcs?77tfzF2;1_# zMjqe2_2L;f@5i~idKKO3uU~1A9$MV!hZA(-6TmHL%yWv(@T>RQjx0?2@PuFl#=dX} zkBQ1AgGMW4iB{)BM@QNBM^I4puJ!G6{n-<-&mQQEeXyZK?(F&kWSN?iA}fn5EyEgv zRo#S&(@X@Dgzx=s>4*h>PqrAB4sZs5!fHvY7Qv!`fW5!^CbBEO&6^u&Z=_OpK_-XlqeadEf5e*b+u z!I51uSD&}OZy7m_*&N9sd!>e%li3MAYdDJzy-cS5;@6=ur{Xbolha=qMY}917yE`V zM)k%5s$s%kyr%%8IpB2Wn=jvf0n9%#9I0p=VuB^Zuo*2K`eR0Znipu-zWa3%jgHz306zVPa!M#7|HYi?QsDyU&$07^{a~#h~?SFSV&G%xOzjN zw@{KuA#VvR@QY2kg$M_H|L>L8!-~6Rc~rSu$VzxAj(#YZ6a% zVz4(rftHUt(50uin^o2>@AgBq0(3hMmPAPF5NR?aEku%-H@NC@fzOkBP3TEp#o&x! z67^9eg;FTLSy91ZBuFgd1l0-oeYTyy(Z%dO$E8x#;<@_iZL=F|ZN-CnQI8INfjwg| zoKcAN`gP%Ovkp^T2do!aT`@MZA?QeNk}Z0^;xh3*m~|FB+wm%vYltO*69&oE%~exo zb`0`$N_nChxfnasxcoyhcB<_vV<(FH{`|QhKqTFAxra8O8lyJo;oih9?9G_Q;Crm= zG~6j7)09cC(9tKK5u0|Y!h=s2mQeNEDJ4lw@MP{5x}QMIf@i(b(nQeFx0A=PchQv{ z*ni&R%Tx?c4Na7s5lj=cBgfNUdYhR@D~*^O_A9C@Ag9J|0Jjk$)PRq|5Io!JX7Ga@9zyYLs_X5GunHr4Hbw~Z7&I8Yq5 zY3?DyPe)lR9mw+wN`9++R})J#(t9y_S@q^$9y`iUJxQFN)6>v?DjwK+zz79$HAPE7eZ{_|&rS(xTZnKLk#Dc4I`6ZIc|>~? z>4J`>9^WlhX-X#q}r#jNi{^3UVV)O88k!C)m3f7h;)!@O^;Cka3kJp-m=WCG8X_h0f)caiWd~*KV^- zd{im9r+&5TX)dkzLhHt!5gaB)!=Z#A`E+tsWX-v{bS(9ua#mwS?xA(Wz|)lg;W$2L zbUEZ8XWJOOOF9Yhv$4>L5QhMhP4)q zt*W9zCsVkv!Hy|8vaVHZFj&cNV=&j^wd43rDRumC?Q6OkTvw_>iv4H{UvH#*OoP5l zX++(IhB@-7PI~o6Z3y2L#!PVgw-BmuIn~v0s73oWV^}TQ7wusPnX=8xqYd0wKFbGF zm4E1EV)!{8*4}Hwwdg)ZsrgVB-i)4CRq`RYfxuCcKAfQMkIjG#>Z}gut$;S#B02Bo zLuQKySX|d4eM$;&^-K{KFpiUhzlYuaO9gY4vB#`Ba;omTJP)-Y5*4IuFCq8#9=F>?p7R`Uqc&DEJ4?(p)l@;gDtatTJ!tUDpu%9*UEG5$AO z$lfvf4j)jgsy1H#L`!wd`Q6^-vG$Q^8cJMa{Z-1dDtf}rx$4+K|T&fREFKSiPyI*6Dy+8H$>b+c6%;>$|sWNa8*0CeB2-< z`Lg5*z_vIjl6^?gdQvHb^pI3u3GVB}CattZ@YPIUqFAB$O%y#})e3S14a3C?s+R|x z-3;K65e$ma$%*6~WUUW6`s95G5D|njKJnWQz68t(8cdOksHgU{UMTU))R-n|6G~}z zQ`i-~d0`)3+UfVl{djLcnntc@6>A0uK?JnkaRN!7ynX;)MeN%MA?r}CzupWeB2*Ps zTufjA<#H1dLSv3LyF00Zy?PyeRyBBUxT2)X>#o6~+C-(0t>}-PTtjouZ&pT_ zdDs=)qX z;knP(dyd9Zbc>wG?5N5y$Y%Irq&uA#Rzs~E47bTX9J&IqLA}q(=pW%aDKbWMaYen0 z4b;w!$DV_3e&Syx8AG1{%Gh^-ta9q9|2-WqFeJM3d6ayg( z|MX1?(}=_$J&CwJqbtn{YAJDdFTN3o(y+P}^sUBa8`##4V*A%iZ%!J_^71vnL$BeN z$=P{k;Z;zk=!XaHS|%wzNb%0Bz@v>5lSq#@l#q-6T??=r`Q11}HFgaFRmV?oJ(mHUTyc=#`S!5Sruclof@yqQ$7 z7XuA>;wI8pePWWBEgxpwYD(D|?P_UO^KY@VWANnCY`I@TZvXz4?NoW1NrttMY*#QuI0NHlLAJ9OluNodR{r=~ zMTN^f1SM%ll#>&bhVaRT1jZ<1^e4EZrp=i0Hb! zn_IyZR`H3G$z9DZ=psvF*s4e2FTDOt*yfyw&2-J_g>{CaB}R7vV4#T|GeP*68BT*a7AA&z_ z4u45dyOGThxPy}o@-H#hKm_fRT23_eiq5{;x|I_hguMw2 zY@l-N+iYGTOa9IF^^i}qHINlpanoG1x>)FYeO<*;MrcdnO+$u_`-FylWh4z~J&e>} zRuYrtP#Yu3kNvo;E-nbcZOBNEc}#lf)o}sm?17N}zj`qjwLfy&J^-A#VfU9je>g*} z(q!y2y|F%ycl88W4vxlaUjy#^swa1czNBe~1I(SrgqW>78)8FCmbR-~A-E4kgrWf` zoUs4;`)o^xRh^GD8+2sPdAJxp<-TBd<W9uJ1 z+6T#%a(Q~{RWp!LO;*_3m~iUr5m!a6F7PgF+O|UNGpk!4-^xloa zg>~+_nt%!U8aGaHC#|_^P3J=T#qQ*B#E;+KxcMSuQ-#q$xynSMn;`}dTQslq*v6%9 zFFH;wZ6paLN|Lm7^O#1PoKbn}Qf?Ch#r}oAiG()f<{lCKB9{xef`3owF7x^pn%`^l zm!n<$aC>mi=dh+&)U9U`4-bf4?`N$Yw}*4XN~Aq!Kh1tNVY4lDO7a(B?)v&G`{a-$ zr9JzmI@UAYX#?crMt~+Zot_5`JP8Wgg21uC+jT8ie%L7%oMtSL-)nsj z<8vXk>``_ozQJcrB%#pPvh`3%>2rBG)&4b%V@};*PjZ7Qf`UtAuc`Xkr=V%1jPga& zJ26|f1UW~7Z9R#9=0IfH6>3?jk~YDW|7VSfv3|{8kAgu7tb}DOX=&?O4i};eq>|~9 z1xy=FTC-2OHT-b;EXPf8L*|sYuIx;Hu5M4O>O*|Y)X3{>URXu}{Aei~d6|19)K9?d zaob@D9T!CV-Dll|Lp;iedDQ3r=g^t)LM}!~qSI?Qad2t6Z#Q{)2B_JzQ8DrxIQBl` z%R*H!#h4kN+#)#1RlCSNY^Dl&wNY6tRcK~ElZ>JZff*KpRqY}Y$HdUl49Hd$Otdz? zBm6oTb3z|0L1N>SKD!Sc;%~roly=N{&myUc)@2rC3i%+fuO|lcv#qmPO46A8ofenc zukwQWK(ZiDR*iA3eSWXNAs~Q{PG@cWdVBl8(bqm$lG)cO{=IQg;!0;6N3TWuJ^fKi z$+A{!SV@Vl#B(hKXiPkmy&~CsF$2kr%@Z7+R2?P!MCXc_nuf!3W*e_g@G|Su8NoVl z4Lr!*)TC2s`_1;(qjSV2h|V>oVfjR1gLbuwfxinY_Xkxy99@|3{wyjgDn36@fb6&M zF55oux_Rm1s-z&ZW(HN)Ycfv?Ww58L%lK$2+OFOImD- zB&+}fOdQxp#W(6ha1Fa=K!e@r#TBE$ow(}9y?yhGWa4pzE%wi*nah-vVe6Zo4H;@w zTpVS`_jSk#_>-raRKM5_RdwV(&Rsaq^A`tEN#a&y{hnZZQW_xO&LQn^@)kMmCNw*X zO{{k)*Acl%Xwn(D5%<;IB>dy$aA(o~Ra^=V!@5e7#m$ z@plSUdF3yVdH$tT34|oc6ZHkBC;;HgOaB^;KGXmIA~X5#*PHm~i~imS|NQ=TYN nXUOD^SN(h7XkR?trW9ug$`uH;Xq%?f!wyr|Q~PxH!SnwDqQdhZ literal 0 HcmV?d00001