mcli-framework 7.10.0__py3-none-any.whl → 7.10.2__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/lib/custom_commands.py +10 -0
- mcli/lib/optional_deps.py +240 -0
- mcli/ml/backtesting/run.py +5 -3
- mcli/ml/models/ensemble_models.py +1 -0
- mcli/ml/models/recommendation_models.py +1 -0
- mcli/ml/optimization/optimize.py +6 -4
- mcli/ml/serving/serve.py +2 -2
- mcli/ml/training/train.py +14 -7
- mcli/self/completion_cmd.py +2 -2
- mcli/workflow/doc_convert.py +82 -112
- mcli/workflow/git_commit/ai_service.py +13 -2
- mcli/workflow/notebook/converter.py +375 -0
- mcli/workflow/notebook/notebook_cmd.py +441 -0
- mcli/workflow/notebook/schema.py +402 -0
- mcli/workflow/notebook/validator.py +313 -0
- mcli/workflow/workflow.py +14 -0
- {mcli_framework-7.10.0.dist-info → mcli_framework-7.10.2.dist-info}/METADATA +37 -3
- {mcli_framework-7.10.0.dist-info → mcli_framework-7.10.2.dist-info}/RECORD +22 -37
- mcli/ml/features/political_features.py +0 -677
- mcli/ml/preprocessing/politician_trading_preprocessor.py +0 -570
- mcli/workflow/politician_trading/config.py +0 -134
- mcli/workflow/politician_trading/connectivity.py +0 -492
- mcli/workflow/politician_trading/data_sources.py +0 -654
- mcli/workflow/politician_trading/database.py +0 -412
- mcli/workflow/politician_trading/demo.py +0 -249
- mcli/workflow/politician_trading/models.py +0 -327
- mcli/workflow/politician_trading/monitoring.py +0 -413
- mcli/workflow/politician_trading/scrapers.py +0 -1074
- mcli/workflow/politician_trading/scrapers_california.py +0 -434
- mcli/workflow/politician_trading/scrapers_corporate_registry.py +0 -797
- mcli/workflow/politician_trading/scrapers_eu.py +0 -376
- mcli/workflow/politician_trading/scrapers_free_sources.py +0 -509
- mcli/workflow/politician_trading/scrapers_third_party.py +0 -373
- mcli/workflow/politician_trading/scrapers_uk.py +0 -378
- mcli/workflow/politician_trading/scrapers_us_states.py +0 -471
- mcli/workflow/politician_trading/seed_database.py +0 -520
- mcli/workflow/politician_trading/supabase_functions.py +0 -354
- mcli/workflow/politician_trading/workflow.py +0 -879
- {mcli_framework-7.10.0.dist-info → mcli_framework-7.10.2.dist-info}/WHEEL +0 -0
- {mcli_framework-7.10.0.dist-info → mcli_framework-7.10.2.dist-info}/entry_points.txt +0 -0
- {mcli_framework-7.10.0.dist-info → mcli_framework-7.10.2.dist-info}/licenses/LICENSE +0 -0
- {mcli_framework-7.10.0.dist-info → mcli_framework-7.10.2.dist-info}/top_level.txt +0 -0
|
@@ -1,413 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Monitoring and status reporting for politician trading data collection
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import asyncio
|
|
6
|
-
import logging
|
|
7
|
-
from datetime import datetime, timedelta, timezone
|
|
8
|
-
from typing import Any, Dict, List, Optional
|
|
9
|
-
|
|
10
|
-
from rich.console import Console
|
|
11
|
-
from rich.panel import Panel
|
|
12
|
-
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
13
|
-
from rich.table import Table
|
|
14
|
-
|
|
15
|
-
from .config import WorkflowConfig
|
|
16
|
-
from .database import PoliticianTradingDB
|
|
17
|
-
|
|
18
|
-
logger = logging.getLogger(__name__)
|
|
19
|
-
console = Console()
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class PoliticianTradingMonitor:
|
|
23
|
-
"""Monitor and report on politician trading data collection"""
|
|
24
|
-
|
|
25
|
-
def __init__(self, config: WorkflowConfig = None):
|
|
26
|
-
self.config = config or WorkflowConfig.default()
|
|
27
|
-
self.db = PoliticianTradingDB(self.config)
|
|
28
|
-
|
|
29
|
-
async def get_system_health(self) -> Dict[str, Any]:
|
|
30
|
-
"""Get overall system health status"""
|
|
31
|
-
health = {
|
|
32
|
-
"timestamp": datetime.utcnow().isoformat(),
|
|
33
|
-
"status": "unknown",
|
|
34
|
-
"database": {"status": "unknown"},
|
|
35
|
-
"data_freshness": {"status": "unknown"},
|
|
36
|
-
"recent_jobs": {"status": "unknown"},
|
|
37
|
-
"errors": [],
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
try:
|
|
41
|
-
# Test database connection
|
|
42
|
-
db_health = await self._check_database_health()
|
|
43
|
-
health["database"] = db_health
|
|
44
|
-
|
|
45
|
-
# Check data freshness
|
|
46
|
-
freshness = await self._check_data_freshness()
|
|
47
|
-
health["data_freshness"] = freshness
|
|
48
|
-
|
|
49
|
-
# Check recent job status
|
|
50
|
-
job_health = await self._check_recent_jobs()
|
|
51
|
-
health["recent_jobs"] = job_health
|
|
52
|
-
|
|
53
|
-
# Determine overall status
|
|
54
|
-
if all(h["status"] == "healthy" for h in [db_health, freshness, job_health]):
|
|
55
|
-
health["status"] = "healthy"
|
|
56
|
-
elif any(h["status"] == "critical" for h in [db_health, freshness, job_health]):
|
|
57
|
-
health["status"] = "critical"
|
|
58
|
-
else:
|
|
59
|
-
health["status"] = "degraded"
|
|
60
|
-
|
|
61
|
-
except Exception as e:
|
|
62
|
-
logger.error(f"Health check failed: {e}")
|
|
63
|
-
health["status"] = "critical"
|
|
64
|
-
health["errors"].append(str(e))
|
|
65
|
-
|
|
66
|
-
return health
|
|
67
|
-
|
|
68
|
-
async def _check_database_health(self) -> Dict[str, Any]:
|
|
69
|
-
"""Check database connectivity and basic operations"""
|
|
70
|
-
try:
|
|
71
|
-
# Test connection with a simple query
|
|
72
|
-
result = self.db.client.table("data_pull_jobs").select("count").execute()
|
|
73
|
-
|
|
74
|
-
return {
|
|
75
|
-
"status": "healthy",
|
|
76
|
-
"connection": "ok",
|
|
77
|
-
"last_check": datetime.utcnow().isoformat(),
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
except Exception as e:
|
|
81
|
-
return {
|
|
82
|
-
"status": "critical",
|
|
83
|
-
"connection": "failed",
|
|
84
|
-
"error": str(e),
|
|
85
|
-
"last_check": datetime.utcnow().isoformat(),
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async def _check_data_freshness(self) -> Dict[str, Any]:
|
|
89
|
-
"""Check how fresh our data is"""
|
|
90
|
-
try:
|
|
91
|
-
# Get most recent disclosure
|
|
92
|
-
recent_disclosures = (
|
|
93
|
-
self.db.client.table("trading_disclosures")
|
|
94
|
-
.select("created_at")
|
|
95
|
-
.order("created_at", desc=True)
|
|
96
|
-
.limit(1)
|
|
97
|
-
.execute()
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
# Get most recent successful job
|
|
101
|
-
recent_jobs = (
|
|
102
|
-
self.db.client.table("data_pull_jobs")
|
|
103
|
-
.select("completed_at")
|
|
104
|
-
.eq("status", "completed")
|
|
105
|
-
.order("completed_at", desc=True)
|
|
106
|
-
.limit(1)
|
|
107
|
-
.execute()
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
now = datetime.now(timezone.utc)
|
|
111
|
-
status = "healthy"
|
|
112
|
-
|
|
113
|
-
# Check if we have recent data
|
|
114
|
-
if recent_disclosures.data:
|
|
115
|
-
last_disclosure = datetime.fromisoformat(
|
|
116
|
-
recent_disclosures.data[0]["created_at"].replace("Z", "+00:00")
|
|
117
|
-
)
|
|
118
|
-
hours_since_disclosure = (now - last_disclosure).total_seconds() / 3600
|
|
119
|
-
|
|
120
|
-
if hours_since_disclosure > 168: # 1 week
|
|
121
|
-
status = "critical"
|
|
122
|
-
elif hours_since_disclosure > 48: # 2 days
|
|
123
|
-
status = "degraded"
|
|
124
|
-
else:
|
|
125
|
-
status = "critical" # No disclosures at all
|
|
126
|
-
|
|
127
|
-
# Check recent job success
|
|
128
|
-
if recent_jobs.data:
|
|
129
|
-
last_job = datetime.fromisoformat(
|
|
130
|
-
recent_jobs.data[0]["completed_at"].replace("Z", "+00:00")
|
|
131
|
-
)
|
|
132
|
-
hours_since_job = (now - last_job).total_seconds() / 3600
|
|
133
|
-
|
|
134
|
-
if hours_since_job > 24: # Should run at least daily
|
|
135
|
-
status = "degraded" if status == "healthy" else status
|
|
136
|
-
else:
|
|
137
|
-
status = "critical" # No successful jobs
|
|
138
|
-
|
|
139
|
-
return {
|
|
140
|
-
"status": status,
|
|
141
|
-
"last_disclosure": (
|
|
142
|
-
recent_disclosures.data[0]["created_at"] if recent_disclosures.data else None
|
|
143
|
-
),
|
|
144
|
-
"last_successful_job": (
|
|
145
|
-
recent_jobs.data[0]["completed_at"] if recent_jobs.data else None
|
|
146
|
-
),
|
|
147
|
-
"hours_since_disclosure": (
|
|
148
|
-
hours_since_disclosure if recent_disclosures.data else None
|
|
149
|
-
),
|
|
150
|
-
"hours_since_job": hours_since_job if recent_jobs.data else None,
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
except Exception as e:
|
|
154
|
-
return {"status": "critical", "error": str(e)}
|
|
155
|
-
|
|
156
|
-
async def _check_recent_jobs(self) -> Dict[str, Any]:
|
|
157
|
-
"""Check recent job execution status"""
|
|
158
|
-
try:
|
|
159
|
-
# Get jobs from last 24 hours
|
|
160
|
-
yesterday = datetime.now(timezone.utc) - timedelta(hours=24)
|
|
161
|
-
|
|
162
|
-
recent_jobs = (
|
|
163
|
-
self.db.client.table("data_pull_jobs")
|
|
164
|
-
.select("*")
|
|
165
|
-
.gte("started_at", yesterday.isoformat())
|
|
166
|
-
.order("started_at", desc=True)
|
|
167
|
-
.execute()
|
|
168
|
-
)
|
|
169
|
-
|
|
170
|
-
if not recent_jobs.data:
|
|
171
|
-
return {
|
|
172
|
-
"status": "degraded",
|
|
173
|
-
"message": "No jobs executed in last 24 hours",
|
|
174
|
-
"job_count": 0,
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
jobs = recent_jobs.data
|
|
178
|
-
total_jobs = len(jobs)
|
|
179
|
-
failed_jobs = len([j for j in jobs if j["status"] == "failed"])
|
|
180
|
-
success_rate = (total_jobs - failed_jobs) / total_jobs if total_jobs > 0 else 0
|
|
181
|
-
|
|
182
|
-
if success_rate >= 0.8:
|
|
183
|
-
status = "healthy"
|
|
184
|
-
elif success_rate >= 0.5:
|
|
185
|
-
status = "degraded"
|
|
186
|
-
else:
|
|
187
|
-
status = "critical"
|
|
188
|
-
|
|
189
|
-
return {
|
|
190
|
-
"status": status,
|
|
191
|
-
"job_count": total_jobs,
|
|
192
|
-
"failed_jobs": failed_jobs,
|
|
193
|
-
"success_rate": round(success_rate * 100, 1),
|
|
194
|
-
"recent_jobs": jobs[:5], # Last 5 jobs
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
except Exception as e:
|
|
198
|
-
return {"status": "critical", "error": str(e)}
|
|
199
|
-
|
|
200
|
-
async def get_detailed_stats(self) -> Dict[str, Any]:
|
|
201
|
-
"""Get detailed statistics about the data collection"""
|
|
202
|
-
try:
|
|
203
|
-
# Get total counts
|
|
204
|
-
politicians_result = self.db.client.table("politicians").select("id").execute()
|
|
205
|
-
|
|
206
|
-
disclosures_result = self.db.client.table("trading_disclosures").select("id").execute()
|
|
207
|
-
|
|
208
|
-
jobs_result = self.db.client.table("data_pull_jobs").select("id").execute()
|
|
209
|
-
|
|
210
|
-
# Get breakdown by region/role
|
|
211
|
-
us_politicians = (
|
|
212
|
-
self.db.client.table("politicians")
|
|
213
|
-
.select("id")
|
|
214
|
-
.in_("role", ["us_house_rep", "us_senator"])
|
|
215
|
-
.execute()
|
|
216
|
-
)
|
|
217
|
-
|
|
218
|
-
eu_politicians = (
|
|
219
|
-
self.db.client.table("politicians").select("id").eq("role", "eu_mep").execute()
|
|
220
|
-
)
|
|
221
|
-
|
|
222
|
-
# Get recent activity (last 30 days)
|
|
223
|
-
thirty_days_ago = datetime.utcnow() - timedelta(days=30)
|
|
224
|
-
recent_disclosures = (
|
|
225
|
-
self.db.client.table("trading_disclosures")
|
|
226
|
-
.select("id")
|
|
227
|
-
.gte("created_at", thirty_days_ago.isoformat())
|
|
228
|
-
.execute()
|
|
229
|
-
)
|
|
230
|
-
|
|
231
|
-
# Get top assets by volume
|
|
232
|
-
top_assets = (
|
|
233
|
-
self.db.client.table("trading_disclosures")
|
|
234
|
-
.select("asset_ticker")
|
|
235
|
-
.not_.is_("asset_ticker", "null")
|
|
236
|
-
.limit(100)
|
|
237
|
-
.execute()
|
|
238
|
-
)
|
|
239
|
-
|
|
240
|
-
return {
|
|
241
|
-
"total_counts": {
|
|
242
|
-
"politicians": len(politicians_result.data) if politicians_result.data else 0,
|
|
243
|
-
"disclosures": len(disclosures_result.data) if disclosures_result.data else 0,
|
|
244
|
-
"jobs": len(jobs_result.data) if jobs_result.data else 0,
|
|
245
|
-
},
|
|
246
|
-
"politician_breakdown": {
|
|
247
|
-
"us_total": len(us_politicians.data) if us_politicians.data else 0,
|
|
248
|
-
"eu_total": len(eu_politicians.data) if eu_politicians.data else 0,
|
|
249
|
-
},
|
|
250
|
-
"recent_activity": {
|
|
251
|
-
"disclosures_last_30_days": (
|
|
252
|
-
len(recent_disclosures.data) if recent_disclosures.data else 0
|
|
253
|
-
)
|
|
254
|
-
},
|
|
255
|
-
"top_assets": top_assets.data if top_assets.data else [],
|
|
256
|
-
"generated_at": datetime.utcnow().isoformat(),
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
except Exception as e:
|
|
260
|
-
logger.error(f"Failed to get detailed stats: {e}")
|
|
261
|
-
return {"error": str(e)}
|
|
262
|
-
|
|
263
|
-
def display_health_report(self, health: Dict[str, Any]):
|
|
264
|
-
"""Display a formatted health report"""
|
|
265
|
-
console.print("\n🏛️ Politician Trading Monitor - System Health", style="bold cyan")
|
|
266
|
-
|
|
267
|
-
# Overall status
|
|
268
|
-
status_color = {
|
|
269
|
-
"healthy": "green",
|
|
270
|
-
"degraded": "yellow",
|
|
271
|
-
"critical": "red",
|
|
272
|
-
"unknown": "white",
|
|
273
|
-
}.get(health["status"], "white")
|
|
274
|
-
|
|
275
|
-
status_panel = Panel(
|
|
276
|
-
f"Overall Status: [{status_color}]{health['status'].upper()}[/{status_color}]\n"
|
|
277
|
-
f"Last Check: {health['timestamp']}",
|
|
278
|
-
title="🎯 System Status",
|
|
279
|
-
border_style=status_color,
|
|
280
|
-
)
|
|
281
|
-
console.print(status_panel)
|
|
282
|
-
|
|
283
|
-
# Database health
|
|
284
|
-
db_status = health.get("database", {})
|
|
285
|
-
db_color = "green" if db_status.get("status") == "healthy" else "red"
|
|
286
|
-
|
|
287
|
-
db_panel = Panel(
|
|
288
|
-
f"Connection: [{db_color}]{db_status.get('connection', 'unknown')}[/{db_color}]\n"
|
|
289
|
-
f"Last Check: {db_status.get('last_check', 'unknown')}",
|
|
290
|
-
title="💾 Database",
|
|
291
|
-
border_style=db_color,
|
|
292
|
-
)
|
|
293
|
-
console.print(db_panel)
|
|
294
|
-
|
|
295
|
-
# Data freshness
|
|
296
|
-
freshness = health.get("data_freshness", {})
|
|
297
|
-
fresh_color = {"healthy": "green", "degraded": "yellow", "critical": "red"}.get(
|
|
298
|
-
freshness.get("status"), "white"
|
|
299
|
-
)
|
|
300
|
-
|
|
301
|
-
fresh_text = (
|
|
302
|
-
f"Status: [{fresh_color}]{freshness.get('status', 'unknown')}[/{fresh_color}]\n"
|
|
303
|
-
)
|
|
304
|
-
|
|
305
|
-
if freshness.get("hours_since_disclosure"):
|
|
306
|
-
fresh_text += (
|
|
307
|
-
f"Hours since last disclosure: {freshness['hours_since_disclosure']:.1f}\n"
|
|
308
|
-
)
|
|
309
|
-
if freshness.get("hours_since_job"):
|
|
310
|
-
fresh_text += f"Hours since last job: {freshness['hours_since_job']:.1f}"
|
|
311
|
-
|
|
312
|
-
fresh_panel = Panel(fresh_text, title="🕒 Data Freshness", border_style=fresh_color)
|
|
313
|
-
console.print(fresh_panel)
|
|
314
|
-
|
|
315
|
-
# Recent jobs
|
|
316
|
-
jobs = health.get("recent_jobs", {})
|
|
317
|
-
job_color = {"healthy": "green", "degraded": "yellow", "critical": "red"}.get(
|
|
318
|
-
jobs.get("status"), "white"
|
|
319
|
-
)
|
|
320
|
-
|
|
321
|
-
job_text = f"Status: [{job_color}]{jobs.get('status', 'unknown')}[/{job_color}]\n"
|
|
322
|
-
if jobs.get("job_count"):
|
|
323
|
-
job_text += f"Jobs (24h): {jobs['job_count']}\n"
|
|
324
|
-
job_text += f"Success Rate: {jobs.get('success_rate', 0)}%"
|
|
325
|
-
|
|
326
|
-
job_panel = Panel(job_text, title="🔄 Recent Jobs", border_style=job_color)
|
|
327
|
-
console.print(job_panel)
|
|
328
|
-
|
|
329
|
-
# Errors
|
|
330
|
-
if health.get("errors"):
|
|
331
|
-
error_panel = Panel("\n".join(health["errors"]), title="⚠️ Errors", border_style="red")
|
|
332
|
-
console.print(error_panel)
|
|
333
|
-
|
|
334
|
-
def display_stats_report(self, stats: Dict[str, Any]):
|
|
335
|
-
"""Display detailed statistics"""
|
|
336
|
-
if "error" in stats:
|
|
337
|
-
console.print(f"❌ Failed to generate stats: {stats['error']}", style="red")
|
|
338
|
-
return
|
|
339
|
-
|
|
340
|
-
console.print("\n📊 Detailed Statistics", style="bold blue")
|
|
341
|
-
|
|
342
|
-
# Summary table
|
|
343
|
-
summary_table = Table(title="Summary Counts")
|
|
344
|
-
summary_table.add_column("Metric", style="cyan")
|
|
345
|
-
summary_table.add_column("Count", justify="right", style="green")
|
|
346
|
-
|
|
347
|
-
total = stats.get("total_counts", {})
|
|
348
|
-
breakdown = stats.get("politician_breakdown", {})
|
|
349
|
-
|
|
350
|
-
summary_table.add_row("Total Politicians", f"{total.get('politicians', 0):,}")
|
|
351
|
-
summary_table.add_row("- US Politicians", f"{breakdown.get('us_total', 0):,}")
|
|
352
|
-
summary_table.add_row("- EU Politicians", f"{breakdown.get('eu_total', 0):,}")
|
|
353
|
-
summary_table.add_row("Total Disclosures", f"{total.get('disclosures', 0):,}")
|
|
354
|
-
summary_table.add_row(
|
|
355
|
-
"Recent Disclosures (30d)",
|
|
356
|
-
f"{stats.get('recent_activity', {}).get('disclosures_last_30_days', 0):,}",
|
|
357
|
-
)
|
|
358
|
-
summary_table.add_row("Total Jobs", f"{total.get('jobs', 0):,}")
|
|
359
|
-
|
|
360
|
-
console.print(summary_table)
|
|
361
|
-
|
|
362
|
-
# Top assets
|
|
363
|
-
if stats.get("top_assets"):
|
|
364
|
-
assets_table = Table(title="Recent Assets")
|
|
365
|
-
assets_table.add_column("Asset Ticker", style="cyan")
|
|
366
|
-
|
|
367
|
-
# Group by asset ticker to count occurrences
|
|
368
|
-
from collections import Counter
|
|
369
|
-
|
|
370
|
-
asset_counts = Counter(
|
|
371
|
-
asset.get("asset_ticker", "Unknown") for asset in stats["top_assets"]
|
|
372
|
-
)
|
|
373
|
-
|
|
374
|
-
for asset_ticker, count in asset_counts.most_common(5): # Top 5
|
|
375
|
-
assets_table.add_row(asset_ticker)
|
|
376
|
-
|
|
377
|
-
console.print(assets_table)
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
async def run_health_check() -> Dict[str, Any]:
|
|
381
|
-
"""Standalone function to run health check"""
|
|
382
|
-
monitor = PoliticianTradingMonitor()
|
|
383
|
-
return await monitor.get_system_health()
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
async def run_stats_report() -> Dict[str, Any]:
|
|
387
|
-
"""Standalone function to generate stats report"""
|
|
388
|
-
monitor = PoliticianTradingMonitor()
|
|
389
|
-
return await monitor.get_detailed_stats()
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
if __name__ == "__main__":
|
|
393
|
-
# Allow running this file directly for testing
|
|
394
|
-
async def main():
|
|
395
|
-
monitor = PoliticianTradingMonitor()
|
|
396
|
-
|
|
397
|
-
with Progress(
|
|
398
|
-
SpinnerColumn(),
|
|
399
|
-
TextColumn("[progress.description]{task.description}"),
|
|
400
|
-
console=console,
|
|
401
|
-
) as progress:
|
|
402
|
-
task = progress.add_task("Checking system health...", total=None)
|
|
403
|
-
|
|
404
|
-
health = await monitor.get_system_health()
|
|
405
|
-
progress.update(task, description="Generating detailed stats...")
|
|
406
|
-
|
|
407
|
-
stats = await monitor.get_detailed_stats()
|
|
408
|
-
progress.remove_task(task)
|
|
409
|
-
|
|
410
|
-
monitor.display_health_report(health)
|
|
411
|
-
monitor.display_stats_report(stats)
|
|
412
|
-
|
|
413
|
-
asyncio.run(main())
|