diff options
author | Danglewood <85772166+deeleeramone@users.noreply.github.com> | 2024-03-18 14:38:34 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-03-18 21:38:34 +0000 |
commit | 9dcad4472b1df380af6e4b78976e5603ddcb8790 (patch) | |
tree | 9ba4e613c2775ddba3b22d7fb040317530fd0726 | |
parent | c5c7adeef19e357911f21dea0601219dcb2f9e24 (diff) |
[Feature] Add Intrinio to `obb.equity.market_snapshots()` (#6232)
* add intrinio to equity market snapshots
* field definitions
* map date
* static files
* black
* date check
* test cassette
* pylint
---------
Co-authored-by: Igor Radovanovic <74266147+IgorWounds@users.noreply.github.com>
9 files changed, 4775 insertions, 17 deletions
diff --git a/openbb_platform/extensions/equity/integration/test_equity_api.py b/openbb_platform/extensions/equity/integration/test_equity_api.py index afe8fdeb22d..9b60930f4b5 100644 --- a/openbb_platform/extensions/equity/integration/test_equity_api.py +++ b/openbb_platform/extensions/equity/integration/test_equity_api.py @@ -1663,7 +1663,8 @@ def test_equity_darkpool_otc(params, headers): "params", [ ({"provider": "fmp", "market": "euronext"}), - # ({"provider": "polygon"}), # premium endpoint + ({"provider": "polygon"}), + ({"provider": "intrinio", "date": "2022-06-30"}), ], ) @pytest.mark.integration diff --git a/openbb_platform/extensions/equity/integration/test_equity_python.py b/openbb_platform/extensions/equity/integration/test_equity_python.py index 71400e8409a..68f8daffef2 100644 --- a/openbb_platform/extensions/equity/integration/test_equity_python.py +++ b/openbb_platform/extensions/equity/integration/test_equity_python.py @@ -1559,7 +1559,8 @@ def test_equity_darkpool_otc(params, obb): "params", [ ({"provider": "fmp", "market": "euronext"}), - # ({"provider": "polygon"}), # premium endpoint + ({"provider": "polygon"}), + ({"provider": "intrinio", "date": "2022-06-30"}), ], ) @pytest.mark.integration diff --git a/openbb_platform/openbb/assets/reference.json b/openbb_platform/openbb/assets/reference.json index fd9de6d9d5d..96162f453bf 100644 --- a/openbb_platform/openbb/assets/reference.json +++ b/openbb_platform/openbb/assets/reference.json @@ -18912,7 +18912,7 @@ "standard": [ { "name": "provider", - "type": "Literal['fmp', 'polygon']", + "type": "Literal['fmp', 'intrinio', 'polygon']", "description": "The provider to use for the query, by default None. If None, the provider specified in defaults is selected or 'fmp' if there is no default.", "default": "fmp", "optional": true @@ -18927,6 +18927,15 @@ "optional": true } ], + "intrinio": [ + { + "name": "date", + "type": "Union[Union[date, datetime, str], str]", + "description": "The date of the data. Can be a datetime or an ISO datetime string. Historical data appears to go back to mid-June 2022. Example: '2024-03-08T12:15:00+0400'", + "default": null, + "optional": true + } + ], "polygon": [] }, "returns": { @@ -18938,7 +18947,7 @@ }, { "name": "provider", - "type": "Optional[Literal['fmp', 'polygon']]", + "type": "Optional[Literal['fmp', 'intrinio', 'polygon']]", "description": "Provider name." }, { @@ -19124,6 +19133,78 @@ "optional": true } ], + "intrinio": [ + { + "name": "last_price", + "type": "float", + "description": "The last trade price.", + "default": null, + "optional": true + }, + { + "name": "last_size", + "type": "int", + "description": "The last trade size.", + "default": null, + "optional": true + }, + { + "name": "last_volume", + "type": "int", + "description": "The last trade volume.", + "default": null, + "optional": true + }, + { + "name": "last_trade_timestamp", + "type": "datetime", + "description": "The timestamp of the last trade.", + "default": null, + "optional": true + }, + { + "name": "bid_size", + "type": "int", + "description": "The size of the last bid price. Bid price and size is not always available.", + "default": null, + "optional": true + }, + { + "name": "bid_price", + "type": "float", + "description": "The last bid price. Bid price and size is not always available.", + "default": null, + "optional": true + }, + { + "name": "ask_price", + "type": "float", + "description": "The last ask price. Ask price and size is not always available.", + "default": null, + "optional": true + }, + { + "name": "ask_size", + "type": "int", + "description": "The size of the last ask price. Ask price and size is not always available.", + "default": null, + "optional": true + }, + { + "name": "last_bid_timestamp", + "type": "datetime", + "description": "The timestamp of the last bid price. Bid price and size is not always available.", + "default": null, + "optional": true + }, + { + "name": "last_ask_timestamp", + "type": "datetime", + "description": "The timestamp of the last ask price. Ask price and size is not always available.", + "default": null, + "optional": true + } + ], "polygon": [ { "name": "vwap", diff --git a/openbb_platform/openbb/package/equity.py b/openbb_platform/openbb/package/equity.py index f36be502f66..b1700d93dd4 100644 --- a/openbb_platform/openbb/package/equity.py +++ b/openbb_platform/openbb/package/equity.py @@ -77,7 +77,7 @@ class ROUTER_equity(Container): def market_snapshots( self, provider: Annotated[ - Optional[Literal["fmp", "polygon"]], + Optional[Literal["fmp", "intrinio", "polygon"]], OpenBBCustomParameter( description="The provider to use for the query, by default None.\n If None, the provider specified in defaults is selected or 'fmp' if there is\n no default." ), @@ -88,19 +88,21 @@ class ROUTER_equity(Container): Parameters ---------- - provider : Optional[Literal['fmp', 'polygon']] + provider : Optional[Literal['fmp', 'intrinio', 'polygon']] The provider to use for the query, by default None. If None, the provider specified in defaults is selected or 'fmp' if there is no default. market : Literal['amex', 'ams', 'ase', 'asx', 'ath', 'bme', 'bru', 'bud', 'bue', 'cai', 'cnq', 'cph', 'dfm', 'doh', 'etf', 'euronext', 'hel', 'hkse', 'ice', 'iob', 'ist', 'jkt', 'jnb', 'jpx', 'kls', 'koe', 'ksc', 'kuw', 'lse', 'mex', 'mutual_fund', 'nasdaq', 'neo', 'nse', 'nyse', 'nze', 'osl', 'otc', 'pnk', 'pra', 'ris', 'sao', 'sau', 'set', 'sgo', 'shh', 'shz', 'six', 'sto', 'tai', 'tlv', 'tsx', 'two', 'vie', 'wse', 'xetra'] The market to fetch data for. (provider: fmp) + date : Optional[Union[datetime.date, datetime.datetime, str]] + The date of the data. Can be a datetime or an ISO datetime string. Historical data appears to go back to mid-June 2022. Example: '2024-03-08T12:15:00+0400' (provider: intrinio) Returns ------- OBBject results : List[MarketSnapshots] Serializable results. - provider : Optional[Literal['fmp', 'polygon']] + provider : Optional[Literal['fmp', 'intrinio', 'polygon']] Provider name. warnings : Optional[List[Warning_]] List of warnings. @@ -130,7 +132,8 @@ class ROUTER_equity(Container): change_percent : Optional[float] The change in price from the previous close, as a normalized percent. last_price : Optional[float] - The last price of the stock. (provider: fmp) + The last price of the stock. (provider: fmp); + The last trade price. (provider: intrinio) last_price_timestamp : Optional[Union[date, datetime]] The timestamp of the last price. (provider: fmp) ma50 : Optional[float] @@ -157,6 +160,27 @@ class ROUTER_equity(Container): The exchange of the stock. (provider: fmp) earnings_date : Optional[Union[date, datetime]] The upcoming earnings announcement date. (provider: fmp) + last_size : Optional[int] + The last trade size. (provider: intrinio) + last_volume : Optional[int] + The last trade volume. (provider: intrinio) + last_trade_timestamp : Optional[datetime] + The timestamp of the last trade. (provider: intrinio); + The last trade timestamp. (provider: polygon) + bid_size : Optional[int] + The size of the last bid price. Bid price and size is not always available. (provider: intrinio); + The current bid size. (provider: polygon) + bid_price : Optional[float] + The last bid price. Bid price and size is not always available. (provider: intrinio) + ask_price : Optional[float] + The last ask price. Ask price and size is not always available. (provider: intrinio) + ask_size : Optional[int] + The size of the last ask price. Ask price and size is not always available. (provider: intrinio); + The current ask size. (provider: polygon) + last_bid_timestamp : Optional[datetime] + The timestamp of the last bid price. Bid price and size is not always available. (provider: intrinio) + last_ask_timestamp : Optional[datetime] + The timestamp of the last ask price. Ask price and size is not always available. (provider: intrinio) vwap : Optional[float] The volume weighted average price of the stock on the current trading day. (provider: polygon) prev_open : Optional[float] @@ -173,10 +197,6 @@ class ROUTER_equity(Container): The last time the data was updated. (provider: polygon) bid : Optional[float] The current bid price. (provider: polygon) - bid_size : Optional[int] - The current bid size. (provider: polygon) - ask_size : Optional[int] - The current ask size. (provider: polygon) ask : Optional[float] The current ask price. (provider: polygon) quote_timestamp : Optional[datetime] @@ -189,8 +209,6 @@ class ROUTER_equity(Container): The last trade condition codes. (provider: polygon) last_trade_exchange : Optional[int] The last trade exchange ID code. (provider: polygon) - last_trade_timestamp : Optional[datetime] - The last trade timestamp. (provider: polygon) Examples -------- @@ -205,7 +223,7 @@ class ROUTER_equity(Container): "provider": self._get_provider( provider, "/equity/market_snapshots", - ("fmp", "polygon"), + ("fmp", "intrinio", "polygon"), ) }, standard_params={}, diff --git a/openbb_platform/openbb/package/equity_price.py b/openbb_platform/openbb/package/equity_price.py index 52e106d9081..e0baa4ecd7a 100644 --- a/openbb_platform/openbb/package/equity_price.py +++ b/openbb_platform/openbb/package/equity_price.py @@ -354,6 +354,8 @@ class ROUTER_equity_price(Container): PricePerformance ---------------- + symbol : Optional[str] + Symbol representing the entity requested in the data. one_day : Optional[float] One-day return. wtd : Optional[float] @@ -374,16 +376,18 @@ class ROUTER_equity_price(Container): Year to date return. one_year : Optional[float] One-year return. + two_year : Optional[float] + Two-year return. three_year : Optional[float] Three-year return. + four_year : Optional[float] + Four-year five_year : Optional[float] Five-year return. ten_year : Optional[float] Ten-year return. max : Optional[float] Return from the beginning of the time series. - symbol : Optional[str] - The ticker symbol. (provider: fmp) Examples -------- diff --git a/openbb_platform/providers/intrinio/openbb_intrinio/__init__.py b/openbb_platform/providers/intrinio/openbb_intrinio/__init__.py index 554cc5b8687..49af00c352b 100644 --- a/openbb_platform/providers/intrinio/openbb_intrinio/__init__.py +++ b/openbb_platform/providers/intrinio/openbb_intrinio/__init__.py @@ -34,6 +34,7 @@ from openbb_intrinio.models.insider_trading import IntrinioInsiderTradingFetcher # ) from openbb_intrinio.models.key_metrics import IntrinioKeyMetricsFetcher from openbb_intrinio.models.latest_attributes import IntrinioLatestAttributesFetcher +from openbb_intrinio.models.market_snapshots import IntrinioMarketSnapshotsFetcher from openbb_intrinio.models.options_chains import IntrinioOptionsChainsFetcher from openbb_intrinio.models.options_unusual import IntrinioOptionsUnusualFetcher from openbb_intrinio.models.reported_financials import IntrinioReportedFinancialsFetcher @@ -76,6 +77,7 @@ intrinio_provider = Provider( "KeyMetrics": IntrinioKeyMetricsFetcher, "LatestAttributes": IntrinioLatestAttributesFetcher, "MarketIndices": IntrinioIndexHistoricalFetcher, + "MarketSnapshots": IntrinioMarketSnapshotsFetcher, "OptionsChains": IntrinioOptionsChainsFetcher, "OptionsUnusual": IntrinioOptionsUnusualFetcher, "ReportedFinancials": IntrinioReportedFinancialsFetcher, diff --git a/openbb_platform/providers/intrinio/openbb_intrinio/models/market_snapshots.py b/openbb_platform/providers/intrinio/openbb_intrinio/models/market_snapshots.py new file mode 100644 index 00000000000..2cb54996683 --- /dev/null +++ b/openbb_platform/providers/intrinio/openbb_intrinio/models/market_snapshots.py @@ -0,0 +1,255 @@ +"""Intrinio Market Snapshots Model.""" + +# pylint: disable=unused-argument + +import asyncio +import gzip +from datetime import ( + date as dateType, + datetime, +) +from io import BytesIO +from typing import Any, Dict, List, Optional, Union + +from openbb_core.provider.abstract.fetcher import Fetcher +from openbb_core.provider.standard_models.market_snapshots import ( + MarketSnapshotsData, + MarketSnapshotsQueryParams, +) +from openbb_core.provider.utils.helpers import amake_request +from pandas import DataFrame, notna, read_csv, to_datetime +from pydantic import Field +from pytz import timezone + + +class IntrinioMarketSnapshotsQueryParams(MarketSnapshotsQueryParams): + """Intrinio Market Snapshots Query. + + Source: https://docs.intrinio.com/documentation/web_api/get_security_snapshots_v2 + """ + + date: Optional[Union[dateType, datetime, str]] = Field( + default=None, + description="The date of the data. Can be a datetime or an ISO datetime string." + + " Historical data appears to go back to mid-June 2022." + + " Example: '2024-03-08T12:15:00+0400'", + ) + + +class IntrinioMarketSnapshotsData(MarketSnapshotsData): + """Intrinio Market Snapshots Data.""" + + __alias_dict__ = { + "last_price": "trade_price", + "last_size": "trade_size", + "last_volume": "total_trade_volume", + } + + last_price: Optional[float] = Field( + default=None, + description="The last trade price.", + ) + last_size: Optional[int] = Field( + default=None, + description="The last trade size.", + ) + last_volume: Optional[int] = Field( + default=None, + description="The last trade volume.", + ) + last_trade_timestamp: Optional[datetime] = Field( + default=None, + description="The timestamp of the last trade.", + ) + bid_size: Optional[int] = Field( + default=None, + description="The size of the last bid price. Bid price and size is not always available.", + ) + bid_price: Optional[float] = Field( + default=None, + description="The last bid price. Bid price and size is not always available.", + ) + ask_price: Optional[float] = Field( + default=None, + description="The last ask price. Ask price and size is not always available.", + ) + ask_size: Optional[int] = Field( + default=None, + description="The size of the last ask price. Ask price and size is not always available.", + ) + last_bid_timestamp: Optional[datetime] = Field( + default=None, + description="The timestamp of the last bid price. Bid price and size is not always available.", + ) + last_ask_timestamp: Optional[datetime] = Field( + default=None, + description="The timestamp of the last ask price. Ask price and size is not always available.", + ) + + +class IntrinioMarketSnapshotsFetcher( + Fetcher[ + IntrinioMarketSnapshotsQueryParams, + List[IntrinioMarketSnapshotsData], + ] +): + """Transform the query, extract and transform the data from the Intrinio endpoints.""" + + @staticmethod + def transform_query(params: Dict[str, Any]) -> IntrinioMarketSnapshotsQueryParams: + """Transform the query params.""" + transformed_params = params + + if "date" in transformed_params: + if isinstance(transformed_params["date"], datetime): + dt = transformed_params["date"] + dt = dt.astimezone(tz=timezone("America/New_York")) + if isinstance(transformed_params["date"], dateType): + dt = transformed_params["date"] + if isinstance(dt, dateType): + dt = datetime( + dt.year, + dt.month, + dt.day, + 20, + 0, + 0, + 0, + tzinfo=timezone("America/New_York"), + ) + if isinstance(transformed_params["date"], str): + dt = datetime.fromisoformat(transformed_params["date"]) + else: + try: + dt = datetime.fromisoformat(str(transformed_params["date"])) # type: ignore + except ValueError as exc: + raise ValueError( + "Invalid date format. Please use '2024-03-08T12:15-0400'." + ) from exc + + transformed_params["date"] = ( + dt.strftime("%Y-%m-%dT%H:%M:%S.%f%z") + .replace("+", "-") + .replace("T00:", "T20:") + if isinstance(dt, datetime) + else dt + ) + return IntrinioMarketSnapshotsQueryParams(**transformed_params) + + @staticmethod + async def aextract_data( + query: IntrinioMarketSnapshotsQueryParams, + credentials: Optional[Dict[str, str]], + **kwargs: Any, + ) -> List[Dict]: + """Return the raw data from the Intrinio endpoint.""" + + api_key = credentials.get("intrinio_api_key") if credentials else "" + + # This gets the URL to the actual file. + url = f"https://api-v2.intrinio.com/securities/snapshots?api_key={api_key}" + if query.date: + url += f"&at_datetime={query.date}" + + response = await amake_request(url, **kwargs) + + if isinstance(response, dict) and "error" in response: + raise RuntimeError( + f"Error: {response.get('error')}. Message: {response.get('message')}" + ) + urls = [] + # Get the URL to the CSV file. + if response.get("snapshots"): # type: ignore + for d in response["snapshots"]: # type: ignore + if d.get("files"): + for f in d["files"]: + if f.get("url"): + urls.append(f.get("url")) + if not urls: + raise RuntimeError("No snapshots found.") + + results = [] + + async def response_callback(response, _): + """Response Callback.""" + return await response.read() + + async def get_csv(url): + """Return the CSV data.""" + response = await amake_request( + url, response_callback=response_callback, **kwargs + ) + df = DataFrame() + if isinstance(response, bytes): + file = gzip.decompress(response) + df = read_csv(BytesIO(file)) + if df.empty: + raise RuntimeError("Empty CSV file") + df.columns = df.columns.str.lower().str.replace(" ", "_") + + df = ( + df.dropna(how="all", axis=1) + .dropna(subset=["trade_price", "last_trade_timestamp", "symbol"]) + .sort_values("last_trade_timestamp", ascending=False) + )[ + [ + "symbol", + "trade_price", + "trade_size", + "total_trade_volume", + "bid_size", + "bid_price", + "ask_price", + "ask_size", + "last_trade_timestamp", + "last_bid_timestamp", + "last_ask_timestamp", + ] + ] + + for col in [ + "last_trade_timestamp", + "last_bid_timestamp", + "last_ask_timestamp", + ]: + df[col] = ( + to_datetime( + df[col].apply( + lambda x: ( + datetime.fromtimestamp(x, tz=timezone("UTC")) + if notna(x) + else x + ) + ) + ) + .dt.tz_convert("America/New_York") + .dt.floor("S") + ) + + for c in ["trade_size", "total_trade_volume"]: + df[c] = df[c].astype("int64") + + # Clear out NaN and non-numeric values with None. + df = ( + df.replace("Max", None) + .replace("Min", None) + .replace(0, None) + .fillna("N/A") + .replace("N/A", None) + ) + + if len(df) > 0: + results.extend(df.reset_index(drop=True).to_dict(orient="records")) + + await asyncio.gather(*[get_csv(url) for url in urls]) + + return results + + @staticmethod + def transform_data( + query: IntrinioMarketSnapshotsQueryParams, + data: List[Dict], + **kwargs: Any, + ) -> List[IntrinioMarketSnapshotsData]: + """Return the transformed data.""" + return [IntrinioMarketSnapshotsData.model_validate(d) for d in data] diff --git a/openbb_platform/providers/intrinio/tests/record/http/test_intrinio_fetchers/test_intrinio_market_snapshots_fetcher.yaml b/openbb_platform/providers/intrinio/tests/record/http/test_intrinio_fetchers/test_intrinio_market_snapshots_fetcher.yaml new file mode 100644 index 00000000000..655c9db02c7 --- /dev/null +++ b/openbb_platform/providers/intrinio/tests/record/http/test_intrinio_fetchers/test_intrinio_market_snapshots_fetcher.yaml @@ -0,0 +1,4380 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + method: GET + uri: https://api-v2.intrinio.com/securities/snapshots?api_key=MOCK_API_KEY&at_datetime=2022-06-30T20%3A00%3A00.000000 + response: + body: + string: !!binary | + H4sIAIYs9mUAA1RR/UsjQQz9V2TA35w2zexMdWE5Ru/OqvgB27PeF0fsZtuB7oczsygt/u83Ih49 + EkIgLy8vvJ0ILfVh3cUg8p87EV3DIhcIiBKMVHCAkMNbiiNRuw2/wwa/Sah1jH3Ix2PXRu9a10l+ + Glx0HOQ/0lFQI2po27X0HEbLrhmXN/aunN3O5ccFiSABRqut6z89SNtspd2sOu/iuinsoszk7Nqe + yXJmUZtfAwCad9SZ54rb6GhT2KsLe3G5uPl+X5rs/PTb3encHuLX9EUGamJSOwTJFKKcpD6oVJKc + 7I9PgjnEfdbPFLn4WJyjUpmCH/uALy+98xyKY5MB7A9Kt2q5mjFV7EOx7v7nfZtSHDwXSzBMqqrU + sVbTCdHJ8lFPOaMpmhNCDVRDxUoZhajqWpMhPSGc4iNrTOR1MqInH0UORyK4bbILM62Ufv2d4i8A + AAD//wMAN6nLcdABAAA= + headers: + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Sat, 16 Mar 2024 23:34:30 GMT + Transfer-Encoding: + - chunked + Vary: + - Origin,Accept-Encoding + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + method: GET + uri: https://intrinio-equities-snapshots.s3.amazonaws.com/SNAPSHOT-22-06-30-20-00.gzip?X-Amz-Algorithm=MOCK_X-Amz-Algorithm&X-Amz-Credential=MOCK_X-Amz-Credential&X-Amz-Date=MOCK_X-Amz-Date&X-Amz-Expires=MOCK_X-Amz-Expires&X-Amz-Signature=MOCK_X-Amz-Signature&X-Amz-SignedHeaders=MOCK_X-Amz-SignedHeaders + response: + body: + string: !!binary | + H4sIAMoAvmIA/6T9yXrjyM4Fis7zKeoB+PmLvhlSVMe0OpN0l7M7vJMzPo9/sBCUxJCd+6+Ca9dm + ZplhUSQjEAvAwsL4eVydD800tOvNP5eh7zbz38f+D/31PLWHf8oP3s6H1+OmObTjNP9k6o+bcWqP + l/lX9v1uX33G4fw+/3c7Pi/+xp/NH4T/un/Mql/Po/C3+yj8123Ur0t7OjT+SaUm+yYmkxodfAg6 + 5ZSessrOGkPndcAg1xz/f/9vo+Yh2Sj1pJRWyerm+P//f7498ev3cbNpnH/yqgn0Px6hVPb5KWaT + ncrzyXIUfH43DU3557//8nF4Hhr1ZF2jjWsC/X8epKN5ytFpEy3Oexys4AL9uZu++3Y626cYbNJ0 + /03zazced+KbGC9vr+Jf/r2ZRvr5k9dNVDTCJWfi/VvSHPBZBaN5DCbHkzJfLxM1fZrxXy8T6Qaz + d7+m6f1Mz9g8edvgw0zM9DmLyygbTUz8Hp4CfQGjMSf/+/089+uxcfbJ0HXofrIL4X4ZmnNOqah9 + 49wTZp7BqxdMm8OlbWx4SvQRrrHe3y9B3z54Q3c0n6djlEzr82l7fm8W//wa31fP/7yP1c+649A2 + 5inYxjdZ2+vapbukh2hiTJrORochWvAlVrvDutHpydEbw+RQ11FJe/OUtA6W1uw8gI///RrTrm2b + /JQdf3zUuYzyOZqAlUeWiCYkD6CD5ArH/rSm9UsfbJrrnHPZxacYlTc5lnN8kLynzQvmG+YAfX9/ + e0Sa1tFTJoOqTeABPN2S6DVM3WejnyIuERsTlyuHTLOPIdCD0U+JJsBTkKya08d6os2ibXR+Cu7h + ZTtjcX+JNgN1HcDHMgdX76vVpZqVh65f13P3+bhtImwMfyoZhetHp6ieUsgZayhiHvOo/34Dbb+X + bwLrz8ZpfkM0m3NSd4uR3JPR0WtaQk5HLCKnaJ5nwVU2++6f9178Jfeb9e8m+CcbyzN0Js/DIq2Q + J1ryMdLmTSM8bWSO5rZkoq3Hvnpvu1V/bgImN63tHHI18UzOZMc1neeDNZL98e3QffNMUlD6iR51 + hCGl77Gd2EikRztEb8g+JUV2yIVyPsuW2G5oV+/il9Puepq4tCyxERgT7jAiR4JOtHDoCcUnsmPh + KUsmz2Hf/V9fjp5SO350ACqZtz6ybD7dvoin96W8CzRtFAw1D/vvX+Rlvelg6qMp5nr5JsKTNtZb + 2hFpQIo8TPIuLqv2gz4dzxPXMOq+IyTayIz3mb77dcC3eOT/vMb7wmZb7ZRdbOGJZ1TyZLbp7SmM + tJL7eF9NjbFslZPBbRhsk/erBKusIiyCMeWo+Ei2Ri0+kNayciHFhJMxfz1pbdY0B9dvRzL/OvM9 + Oa8WizXSYnLWabqAfiI8T8MkYHbszquywHA790+He2BUyj9bgu3wNtxNz9+faj9s6BbpOaWGgI3W |