mcli-framework 7.1.1__py3-none-any.whl → 7.1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mcli-framework might be problematic. Click here for more details.
- mcli/app/completion_cmd.py +59 -49
- mcli/app/completion_helpers.py +60 -138
- mcli/app/logs_cmd.py +6 -2
- mcli/app/main.py +17 -14
- mcli/app/model_cmd.py +19 -4
- mcli/chat/chat.py +3 -2
- mcli/lib/search/cached_vectorizer.py +1 -0
- mcli/lib/services/data_pipeline.py +12 -5
- mcli/lib/services/lsh_client.py +68 -57
- mcli/ml/api/app.py +28 -36
- mcli/ml/api/middleware.py +8 -16
- mcli/ml/api/routers/admin_router.py +3 -1
- mcli/ml/api/routers/auth_router.py +32 -56
- mcli/ml/api/routers/backtest_router.py +3 -1
- mcli/ml/api/routers/data_router.py +3 -1
- mcli/ml/api/routers/model_router.py +35 -74
- mcli/ml/api/routers/monitoring_router.py +3 -1
- mcli/ml/api/routers/portfolio_router.py +3 -1
- mcli/ml/api/routers/prediction_router.py +60 -65
- mcli/ml/api/routers/trade_router.py +6 -2
- mcli/ml/api/routers/websocket_router.py +12 -9
- mcli/ml/api/schemas.py +10 -2
- mcli/ml/auth/auth_manager.py +49 -114
- mcli/ml/auth/models.py +30 -15
- mcli/ml/auth/permissions.py +12 -19
- mcli/ml/backtesting/backtest_engine.py +134 -108
- mcli/ml/backtesting/performance_metrics.py +142 -108
- mcli/ml/cache.py +12 -18
- mcli/ml/cli/main.py +37 -23
- mcli/ml/config/settings.py +29 -12
- mcli/ml/dashboard/app.py +122 -130
- mcli/ml/dashboard/app_integrated.py +955 -154
- mcli/ml/dashboard/app_supabase.py +176 -108
- mcli/ml/dashboard/app_training.py +212 -206
- mcli/ml/dashboard/cli.py +14 -5
- mcli/ml/data_ingestion/api_connectors.py +51 -81
- mcli/ml/data_ingestion/data_pipeline.py +127 -125
- mcli/ml/data_ingestion/stream_processor.py +72 -80
- mcli/ml/database/migrations/env.py +3 -2
- mcli/ml/database/models.py +112 -79
- mcli/ml/database/session.py +6 -5
- mcli/ml/experimentation/ab_testing.py +149 -99
- mcli/ml/features/ensemble_features.py +9 -8
- mcli/ml/features/political_features.py +6 -5
- mcli/ml/features/recommendation_engine.py +15 -14
- mcli/ml/features/stock_features.py +7 -6
- mcli/ml/features/test_feature_engineering.py +8 -7
- mcli/ml/logging.py +10 -15
- mcli/ml/mlops/data_versioning.py +57 -64
- mcli/ml/mlops/experiment_tracker.py +49 -41
- mcli/ml/mlops/model_serving.py +59 -62
- mcli/ml/mlops/pipeline_orchestrator.py +203 -149
- mcli/ml/models/base_models.py +8 -7
- mcli/ml/models/ensemble_models.py +6 -5
- mcli/ml/models/recommendation_models.py +7 -6
- mcli/ml/models/test_models.py +18 -14
- mcli/ml/monitoring/drift_detection.py +95 -74
- mcli/ml/monitoring/metrics.py +10 -22
- mcli/ml/optimization/portfolio_optimizer.py +172 -132
- mcli/ml/predictions/prediction_engine.py +62 -50
- mcli/ml/preprocessing/data_cleaners.py +6 -5
- mcli/ml/preprocessing/feature_extractors.py +7 -6
- mcli/ml/preprocessing/ml_pipeline.py +3 -2
- mcli/ml/preprocessing/politician_trading_preprocessor.py +11 -10
- mcli/ml/preprocessing/test_preprocessing.py +4 -4
- mcli/ml/scripts/populate_sample_data.py +36 -16
- mcli/ml/tasks.py +82 -83
- mcli/ml/tests/test_integration.py +86 -76
- mcli/ml/tests/test_training_dashboard.py +169 -142
- mcli/mygroup/test_cmd.py +2 -1
- mcli/self/self_cmd.py +31 -16
- mcli/self/test_cmd.py +2 -1
- mcli/workflow/dashboard/dashboard_cmd.py +13 -6
- mcli/workflow/lsh_integration.py +46 -58
- mcli/workflow/politician_trading/commands.py +576 -427
- mcli/workflow/politician_trading/config.py +7 -7
- mcli/workflow/politician_trading/connectivity.py +35 -33
- mcli/workflow/politician_trading/data_sources.py +72 -71
- mcli/workflow/politician_trading/database.py +18 -16
- mcli/workflow/politician_trading/demo.py +4 -3
- mcli/workflow/politician_trading/models.py +5 -5
- mcli/workflow/politician_trading/monitoring.py +13 -13
- mcli/workflow/politician_trading/scrapers.py +332 -224
- mcli/workflow/politician_trading/scrapers_california.py +116 -94
- mcli/workflow/politician_trading/scrapers_eu.py +70 -71
- mcli/workflow/politician_trading/scrapers_uk.py +118 -90
- mcli/workflow/politician_trading/scrapers_us_states.py +125 -92
- mcli/workflow/politician_trading/workflow.py +98 -71
- {mcli_framework-7.1.1.dist-info → mcli_framework-7.1.3.dist-info}/METADATA +1 -1
- {mcli_framework-7.1.1.dist-info → mcli_framework-7.1.3.dist-info}/RECORD +94 -94
- {mcli_framework-7.1.1.dist-info → mcli_framework-7.1.3.dist-info}/WHEEL +0 -0
- {mcli_framework-7.1.1.dist-info → mcli_framework-7.1.3.dist-info}/entry_points.txt +0 -0
- {mcli_framework-7.1.1.dist-info → mcli_framework-7.1.3.dist-info}/licenses/LICENSE +0 -0
- {mcli_framework-7.1.1.dist-info → mcli_framework-7.1.3.dist-info}/top_level.txt +0 -0
mcli/ml/dashboard/cli.py
CHANGED
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
import subprocess
|
|
4
4
|
import sys
|
|
5
5
|
from pathlib import Path
|
|
6
|
+
|
|
6
7
|
import typer
|
|
7
8
|
from rich.console import Console
|
|
8
9
|
|
|
9
10
|
app = typer.Typer()
|
|
10
11
|
console = Console()
|
|
11
12
|
|
|
13
|
+
|
|
12
14
|
@app.command()
|
|
13
15
|
def launch(
|
|
14
16
|
port: int = typer.Option(8501, "--port", "-p", help="Port to run dashboard on"),
|
|
@@ -26,11 +28,17 @@ def launch(
|
|
|
26
28
|
|
|
27
29
|
# Build streamlit command
|
|
28
30
|
cmd = [
|
|
29
|
-
sys.executable,
|
|
31
|
+
sys.executable,
|
|
32
|
+
"-m",
|
|
33
|
+
"streamlit",
|
|
34
|
+
"run",
|
|
30
35
|
str(dashboard_path),
|
|
31
|
-
"--server.port",
|
|
32
|
-
|
|
33
|
-
"--
|
|
36
|
+
"--server.port",
|
|
37
|
+
str(port),
|
|
38
|
+
"--server.address",
|
|
39
|
+
host,
|
|
40
|
+
"--browser.gatherUsageStats",
|
|
41
|
+
"false",
|
|
34
42
|
]
|
|
35
43
|
|
|
36
44
|
if debug:
|
|
@@ -47,5 +55,6 @@ def launch(
|
|
|
47
55
|
console.print(f"[red]Failed to start dashboard: {e}[/red]")
|
|
48
56
|
raise typer.Exit(1)
|
|
49
57
|
|
|
58
|
+
|
|
50
59
|
if __name__ == "__main__":
|
|
51
|
-
app()
|
|
60
|
+
app()
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
"""API connectors for real-time data ingestion"""
|
|
2
2
|
|
|
3
|
-
import requests
|
|
4
3
|
import asyncio
|
|
5
|
-
import aiohttp
|
|
6
|
-
import websockets
|
|
7
4
|
import json
|
|
8
|
-
import pandas as pd
|
|
9
|
-
from typing import Dict, Any, Optional, List, Callable, AsyncIterator
|
|
10
|
-
from datetime import datetime, timedelta
|
|
11
|
-
from dataclasses import dataclass
|
|
12
5
|
import logging
|
|
13
|
-
from abc import ABC, abstractmethod
|
|
14
6
|
import time
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from datetime import datetime, timedelta
|
|
10
|
+
from typing import Any, AsyncIterator, Callable, Dict, List, Optional
|
|
15
11
|
from urllib.parse import urljoin
|
|
12
|
+
|
|
13
|
+
import aiohttp
|
|
14
|
+
import pandas as pd
|
|
15
|
+
import requests
|
|
16
|
+
import websockets
|
|
16
17
|
import yfinance as yf
|
|
17
18
|
|
|
18
19
|
logger = logging.getLogger(__name__)
|
|
@@ -21,6 +22,7 @@ logger = logging.getLogger(__name__)
|
|
|
21
22
|
@dataclass
|
|
22
23
|
class APIConfig:
|
|
23
24
|
"""API configuration"""
|
|
25
|
+
|
|
24
26
|
api_key: Optional[str] = None
|
|
25
27
|
base_url: str = ""
|
|
26
28
|
rate_limit: int = 100 # requests per minute
|
|
@@ -59,10 +61,7 @@ class BaseAPIConnector(ABC):
|
|
|
59
61
|
self.session = aiohttp.ClientSession()
|
|
60
62
|
|
|
61
63
|
async with self.session.get(
|
|
62
|
-
url,
|
|
63
|
-
params=params,
|
|
64
|
-
headers=headers,
|
|
65
|
-
timeout=self.config.timeout
|
|
64
|
+
url, params=params, headers=headers, timeout=self.config.timeout
|
|
66
65
|
) as response:
|
|
67
66
|
response.raise_for_status()
|
|
68
67
|
return await response.json()
|
|
@@ -110,10 +109,7 @@ class CongressionalDataAPI(BaseAPIConnector):
|
|
|
110
109
|
|
|
111
110
|
def __init__(self, config: Optional[APIConfig] = None):
|
|
112
111
|
if not config:
|
|
113
|
-
config = APIConfig(
|
|
114
|
-
base_url="https://api.capitoltrades.com/v1/",
|
|
115
|
-
rate_limit=60
|
|
116
|
-
)
|
|
112
|
+
config = APIConfig(base_url="https://api.capitoltrades.com/v1/", rate_limit=60)
|
|
117
113
|
super().__init__(config)
|
|
118
114
|
|
|
119
115
|
async def fetch_recent_trades(self, days: int = 30) -> List[Dict[str, Any]]:
|
|
@@ -121,7 +117,7 @@ class CongressionalDataAPI(BaseAPIConnector):
|
|
|
121
117
|
params = {
|
|
122
118
|
"from_date": (datetime.now() - timedelta(days=days)).isoformat(),
|
|
123
119
|
"to_date": datetime.now().isoformat(),
|
|
124
|
-
"limit": 1000
|
|
120
|
+
"limit": 1000,
|
|
125
121
|
}
|
|
126
122
|
|
|
127
123
|
try:
|
|
@@ -142,19 +138,24 @@ class CongressionalDataAPI(BaseAPIConnector):
|
|
|
142
138
|
def _generate_mock_trades(self) -> List[Dict[str, Any]]:
|
|
143
139
|
"""Generate mock trades for testing"""
|
|
144
140
|
import random
|
|
141
|
+
|
|
145
142
|
trades = []
|
|
146
143
|
politicians = ["Nancy Pelosi", "Mitch McConnell", "Chuck Schumer", "Kevin McCarthy"]
|
|
147
144
|
tickers = ["AAPL", "MSFT", "GOOGL", "AMZN", "TSLA", "META", "NVDA"]
|
|
148
145
|
|
|
149
146
|
for _ in range(50):
|
|
150
|
-
trades.append(
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
147
|
+
trades.append(
|
|
148
|
+
{
|
|
149
|
+
"politician": random.choice(politicians),
|
|
150
|
+
"ticker": random.choice(tickers),
|
|
151
|
+
"transaction_type": random.choice(["buy", "sell"]),
|
|
152
|
+
"amount": random.randint(1000, 1000000),
|
|
153
|
+
"transaction_date": (
|
|
154
|
+
datetime.now() - timedelta(days=random.randint(1, 30))
|
|
155
|
+
).isoformat(),
|
|
156
|
+
"disclosure_date": datetime.now().isoformat(),
|
|
157
|
+
}
|
|
158
|
+
)
|
|
158
159
|
|
|
159
160
|
return trades
|
|
160
161
|
|
|
@@ -166,7 +167,7 @@ class CongressionalDataAPI(BaseAPIConnector):
|
|
|
166
167
|
"party": "Independent",
|
|
167
168
|
"state": "CA",
|
|
168
169
|
"chamber": "House",
|
|
169
|
-
"committees": ["Finance", "Technology"]
|
|
170
|
+
"committees": ["Finance", "Technology"],
|
|
170
171
|
}
|
|
171
172
|
|
|
172
173
|
|
|
@@ -193,17 +194,13 @@ class AlphaVantageConnector(StockMarketAPI):
|
|
|
193
194
|
config = APIConfig(
|
|
194
195
|
api_key=api_key,
|
|
195
196
|
base_url="https://www.alphavantage.co/query",
|
|
196
|
-
rate_limit=5 # Free tier: 5 requests per minute
|
|
197
|
+
rate_limit=5, # Free tier: 5 requests per minute
|
|
197
198
|
)
|
|
198
199
|
super().__init__(config)
|
|
199
200
|
|
|
200
201
|
async def fetch_quote(self, symbol: str) -> Dict[str, Any]:
|
|
201
202
|
"""Fetch current quote from Alpha Vantage"""
|
|
202
|
-
params = {
|
|
203
|
-
"function": "GLOBAL_QUOTE",
|
|
204
|
-
"symbol": symbol,
|
|
205
|
-
"apikey": self.config.api_key
|
|
206
|
-
}
|
|
203
|
+
params = {"function": "GLOBAL_QUOTE", "symbol": symbol, "apikey": self.config.api_key}
|
|
207
204
|
|
|
208
205
|
data = await self._make_request("", params)
|
|
209
206
|
return self._parse_quote(data.get("Global Quote", {}))
|
|
@@ -214,16 +211,16 @@ class AlphaVantageConnector(StockMarketAPI):
|
|
|
214
211
|
"function": "TIME_SERIES_DAILY",
|
|
215
212
|
"symbol": symbol,
|
|
216
213
|
"outputsize": "full" if period == "max" else "compact",
|
|
217
|
-
"apikey": self.config.api_key
|
|
214
|
+
"apikey": self.config.api_key,
|
|
218
215
|
}
|
|
219
216
|
|
|
220
217
|
data = await self._make_request("", params)
|
|
221
218
|
time_series = data.get("Time Series (Daily)", {})
|
|
222
219
|
|
|
223
220
|
# Convert to DataFrame
|
|
224
|
-
df = pd.DataFrame.from_dict(time_series, orient=
|
|
221
|
+
df = pd.DataFrame.from_dict(time_series, orient="index")
|
|
225
222
|
df.index = pd.to_datetime(df.index)
|
|
226
|
-
df.columns = [
|
|
223
|
+
df.columns = ["open", "high", "low", "close", "volume"]
|
|
227
224
|
df = df.astype(float)
|
|
228
225
|
|
|
229
226
|
return df.sort_index()
|
|
@@ -236,7 +233,7 @@ class AlphaVantageConnector(StockMarketAPI):
|
|
|
236
233
|
"volume": int(quote_data.get("06. volume", 0)),
|
|
237
234
|
"timestamp": quote_data.get("07. latest trading day", ""),
|
|
238
235
|
"change": float(quote_data.get("09. change", 0)),
|
|
239
|
-
"change_percent": quote_data.get("10. change percent", "0%")
|
|
236
|
+
"change_percent": quote_data.get("10. change percent", "0%"),
|
|
240
237
|
}
|
|
241
238
|
|
|
242
239
|
|
|
@@ -259,7 +256,7 @@ class YahooFinanceConnector(StockMarketAPI):
|
|
|
259
256
|
"volume": info.get("volume", 0),
|
|
260
257
|
"market_cap": info.get("marketCap", 0),
|
|
261
258
|
"pe_ratio": info.get("trailingPE", 0),
|
|
262
|
-
"dividend_yield": info.get("dividendYield", 0)
|
|
259
|
+
"dividend_yield": info.get("dividendYield", 0),
|
|
263
260
|
}
|
|
264
261
|
except Exception as e:
|
|
265
262
|
logger.error(f"Failed to fetch Yahoo Finance quote: {e}")
|
|
@@ -280,11 +277,7 @@ class PolygonIOConnector(StockMarketAPI):
|
|
|
280
277
|
"""Polygon.io API connector"""
|
|
281
278
|
|
|
282
279
|
def __init__(self, api_key: str):
|
|
283
|
-
config = APIConfig(
|
|
284
|
-
api_key=api_key,
|
|
285
|
-
base_url="https://api.polygon.io/",
|
|
286
|
-
rate_limit=100
|
|
287
|
-
)
|
|
280
|
+
config = APIConfig(api_key=api_key, base_url="https://api.polygon.io/", rate_limit=100)
|
|
288
281
|
super().__init__(config)
|
|
289
282
|
|
|
290
283
|
async def fetch_quote(self, symbol: str) -> Dict[str, Any]:
|
|
@@ -295,16 +288,12 @@ class PolygonIOConnector(StockMarketAPI):
|
|
|
295
288
|
data = await self._make_request(endpoint, params)
|
|
296
289
|
return self._parse_polygon_quote(data)
|
|
297
290
|
|
|
298
|
-
async def fetch_aggregates(
|
|
299
|
-
|
|
300
|
-
|
|
291
|
+
async def fetch_aggregates(
|
|
292
|
+
self, symbol: str, from_date: str, to_date: str, timespan: str = "day"
|
|
293
|
+
) -> pd.DataFrame:
|
|
301
294
|
"""Fetch aggregate bars from Polygon.io"""
|
|
302
295
|
endpoint = f"v2/aggs/ticker/{symbol}/range/1/{timespan}/{from_date}/{to_date}"
|
|
303
|
-
params = {
|
|
304
|
-
"apiKey": self.config.api_key,
|
|
305
|
-
"adjusted": "true",
|
|
306
|
-
"sort": "asc"
|
|
307
|
-
}
|
|
296
|
+
params = {"apiKey": self.config.api_key, "adjusted": "true", "sort": "asc"}
|
|
308
297
|
|
|
309
298
|
data = await self._make_request(endpoint, params)
|
|
310
299
|
results = data.get("results", [])
|
|
@@ -313,16 +302,10 @@ class PolygonIOConnector(StockMarketAPI):
|
|
|
313
302
|
return pd.DataFrame()
|
|
314
303
|
|
|
315
304
|
df = pd.DataFrame(results)
|
|
316
|
-
df[
|
|
317
|
-
df = df.rename(columns={
|
|
318
|
-
'o': 'open',
|
|
319
|
-
'h': 'high',
|
|
320
|
-
'l': 'low',
|
|
321
|
-
'c': 'close',
|
|
322
|
-
'v': 'volume'
|
|
323
|
-
})
|
|
305
|
+
df["timestamp"] = pd.to_datetime(df["t"], unit="ms")
|
|
306
|
+
df = df.rename(columns={"o": "open", "h": "high", "l": "low", "c": "close", "v": "volume"})
|
|
324
307
|
|
|
325
|
-
return df.set_index(
|
|
308
|
+
return df.set_index("timestamp")
|
|
326
309
|
|
|
327
310
|
def _parse_polygon_quote(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
328
311
|
"""Parse Polygon.io quote"""
|
|
@@ -331,7 +314,7 @@ class PolygonIOConnector(StockMarketAPI):
|
|
|
331
314
|
"symbol": results.get("T", ""),
|
|
332
315
|
"price": results.get("P", 0),
|
|
333
316
|
"size": results.get("S", 0),
|
|
334
|
-
"timestamp": results.get("t", 0)
|
|
317
|
+
"timestamp": results.get("t", 0),
|
|
335
318
|
}
|
|
336
319
|
|
|
337
320
|
|
|
@@ -340,26 +323,20 @@ class QuiverQuantConnector(BaseAPIConnector):
|
|
|
340
323
|
|
|
341
324
|
def __init__(self, api_key: str):
|
|
342
325
|
config = APIConfig(
|
|
343
|
-
api_key=api_key,
|
|
344
|
-
base_url="https://api.quiverquant.com/beta/",
|
|
345
|
-
rate_limit=100
|
|
326
|
+
api_key=api_key, base_url="https://api.quiverquant.com/beta/", rate_limit=100
|
|
346
327
|
)
|
|
347
328
|
super().__init__(config)
|
|
348
329
|
|
|
349
330
|
async def fetch_congress_trades(self) -> List[Dict[str, Any]]:
|
|
350
331
|
"""Fetch congressional trading data"""
|
|
351
|
-
headers = {
|
|
352
|
-
"Authorization": f"Bearer {self.config.api_key}",
|
|
353
|
-
"Accept": "application/json"
|
|
354
|
-
}
|
|
332
|
+
headers = {"Authorization": f"Bearer {self.config.api_key}", "Accept": "application/json"}
|
|
355
333
|
|
|
356
334
|
try:
|
|
357
335
|
if not self.session:
|
|
358
336
|
self.session = aiohttp.ClientSession()
|
|
359
337
|
|
|
360
338
|
async with self.session.get(
|
|
361
|
-
f"{self.config.base_url}historical/congresstrading",
|
|
362
|
-
headers=headers
|
|
339
|
+
f"{self.config.base_url}historical/congresstrading", headers=headers
|
|
363
340
|
) as response:
|
|
364
341
|
response.raise_for_status()
|
|
365
342
|
data = await response.json()
|
|
@@ -370,18 +347,14 @@ class QuiverQuantConnector(BaseAPIConnector):
|
|
|
370
347
|
|
|
371
348
|
async def fetch_lobbying(self, ticker: str) -> List[Dict[str, Any]]:
|
|
372
349
|
"""Fetch lobbying data for a ticker"""
|
|
373
|
-
headers = {
|
|
374
|
-
"Authorization": f"Bearer {self.config.api_key}",
|
|
375
|
-
"Accept": "application/json"
|
|
376
|
-
}
|
|
350
|
+
headers = {"Authorization": f"Bearer {self.config.api_key}", "Accept": "application/json"}
|
|
377
351
|
|
|
378
352
|
try:
|
|
379
353
|
if not self.session:
|
|
380
354
|
self.session = aiohttp.ClientSession()
|
|
381
355
|
|
|
382
356
|
async with self.session.get(
|
|
383
|
-
f"{self.config.base_url}historical/lobbying/{ticker}",
|
|
384
|
-
headers=headers
|
|
357
|
+
f"{self.config.base_url}historical/lobbying/{ticker}", headers=headers
|
|
385
358
|
) as response:
|
|
386
359
|
response.raise_for_status()
|
|
387
360
|
data = await response.json()
|
|
@@ -418,10 +391,7 @@ class WebSocketDataStream:
|
|
|
418
391
|
if not self.websocket:
|
|
419
392
|
await self.connect()
|
|
420
393
|
|
|
421
|
-
message = {
|
|
422
|
-
"action": "subscribe",
|
|
423
|
-
"symbols": symbols
|
|
424
|
-
}
|
|
394
|
+
message = {"action": "subscribe", "symbols": symbols}
|
|
425
395
|
await self.websocket.send(json.dumps(message))
|
|
426
396
|
|
|
427
397
|
async def stream(self):
|
|
@@ -473,7 +443,7 @@ class DataAggregator:
|
|
|
473
443
|
# Fetch from all sources concurrently
|
|
474
444
|
tasks = []
|
|
475
445
|
for name, connector in self.sources.items():
|
|
476
|
-
if hasattr(connector,
|
|
446
|
+
if hasattr(connector, "fetch_quote"):
|
|
477
447
|
tasks.append(self._fetch_with_name(name, connector.fetch_quote(symbol)))
|
|
478
448
|
|
|
479
449
|
responses = await asyncio.gather(*tasks, return_exceptions=True)
|
|
@@ -498,4 +468,4 @@ class DataAggregator:
|
|
|
498
468
|
async def _fetch_with_name(self, name: str, coro):
|
|
499
469
|
"""Helper to fetch with source name"""
|
|
500
470
|
result = await coro
|
|
501
|
-
return name, result
|
|
471
|
+
return name, result
|