argus-cloud-optimizer 0.4.0__tar.gz → 0.4.1__tar.gz
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.
- {argus_cloud_optimizer-0.4.0/argus_cloud_optimizer.egg-info → argus_cloud_optimizer-0.4.1}/PKG-INFO +22 -8
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/README.md +20 -6
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/adapters/aws/adapter.py +3 -1
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/adapters/gcp/adapter.py +5 -1
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/adapters/gcp/asset_inventory.py +66 -27
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/adapters/gcp/cloud_logging.py +1 -2
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/adapters/gcp/cloud_monitoring.py +2 -2
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/ai/azure_openai.py +16 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/ai/vertexai.py +6 -3
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1/argus_cloud_optimizer.egg-info}/PKG-INFO +22 -8
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/argus_cloud_optimizer.egg-info/SOURCES.txt +1 -0
- argus_cloud_optimizer-0.4.1/core/__version__.py +1 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/core/agent/loop.py +6 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/core/agent/prompts.py +1 -1
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/core/config.py +1 -1
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/core/reports/delivery.py +16 -2
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/core/reports/generator.py +21 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/deploy/aws/multi-account/spoke-role.yaml +1 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/deploy/aws/single-account/template.yaml +1 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/deploy/gcp/deploy.sh +34 -2
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/docs/concepts/ai-providers.md +25 -3
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/docs/getting-started/configuration.md +2 -2
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/docs/getting-started/quickstart.md +54 -2
- argus_cloud_optimizer-0.4.1/docs/index.md +441 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/docs/reference/env-vars.md +3 -3
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/docs/reference/troubleshooting.md +25 -4
- argus_cloud_optimizer-0.4.1/docs/roadmap.md +80 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/entrypoints/aws_lambda.py +16 -2
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/entrypoints/azure_function.py +17 -2
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/entrypoints/cli.py +3 -1
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/entrypoints/cli_chat.py +2 -4
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/entrypoints/gcp_cloudrun.py +93 -16
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/pyproject.toml +1 -1
- argus_cloud_optimizer-0.4.0/core/__version__.py +0 -1
- argus_cloud_optimizer-0.4.0/docs/index.md +0 -278
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/.env.example +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/LICENSE +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/MANIFEST.in +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/accounts.yaml.example +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/adapters/__init__.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/adapters/aws/__init__.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/adapters/aws/auth.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/adapters/aws/cloudtrail.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/adapters/aws/cloudwatch.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/adapters/aws/config.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/adapters/aws/cost_explorer.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/adapters/aws/resource_explorer.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/adapters/aws/retry.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/adapters/azure/__init__.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/adapters/azure/activity_log.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/adapters/azure/adapter.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/adapters/azure/cost_management.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/adapters/azure/monitor.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/adapters/azure/resource_graph.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/adapters/azure/retry.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/adapters/base.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/adapters/gcp/__init__.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/adapters/gcp/billing.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/adapters/gcp/retry.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/ai/__init__.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/ai/anthropic.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/ai/base.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/ai/bedrock.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/argus_cloud_optimizer.egg-info/dependency_links.txt +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/argus_cloud_optimizer.egg-info/entry_points.txt +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/argus_cloud_optimizer.egg-info/requires.txt +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/argus_cloud_optimizer.egg-info/top_level.txt +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/core/__init__.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/core/agent/__init__.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/core/agent/chat.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/core/log.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/core/models/__init__.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/core/models/finding.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/core/py.typed +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/core/reports/__init__.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/core/reports/comparison.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/core/reports/export.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/core/reports/html.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/core/reports/multi_cloud.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/core/secrets.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/core/token_tracker.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/core/validation.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/deploy/aws/multi-account/hub/template.yaml +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/deploy/azure/function-app.bicep +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/docs/concepts/adapters.md +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/docs/concepts/ai-reasoning.md +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/docs/concepts/how-it-works.md +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/docs/concepts/index.md +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/docs/contributing/development.md +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/docs/contributing/index.md +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/docs/contributing/new-adapter.md +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/docs/contributing/new-ai-provider.md +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/docs/deployment/aws.md +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/docs/deployment/azure.md +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/docs/deployment/gcp.md +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/docs/deployment/index.md +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/docs/deployment/multi-account.md +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/docs/getting-started/first-scan.md +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/docs/getting-started/index.md +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/docs/reference/iam-permissions.md +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/docs/reference/index.md +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/docs/reference/report-schema.md +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/docs/reference/resource-types.md +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/docs/reference/security.md +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/entrypoints/__init__.py +0 -0
- {argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/setup.cfg +0 -0
{argus_cloud_optimizer-0.4.0/argus_cloud_optimizer.egg-info → argus_cloud_optimizer-0.4.1}/PKG-INFO
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: argus-cloud-optimizer
|
|
3
|
-
Version: 0.4.
|
|
4
|
-
Summary: AI
|
|
3
|
+
Version: 0.4.1
|
|
4
|
+
Summary: Argus — AI Cloud Detective for AWS, GCP, and Azure
|
|
5
5
|
Author-email: Vamshi Siddarth Gaddam <vamshisiddarth02@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/vamshisiddarth/argus
|
|
@@ -62,12 +62,12 @@ Requires-Dist: mkdocs-minify-plugin>=0.8.0; extra == "dev"
|
|
|
62
62
|
Dynamic: license-file
|
|
63
63
|
|
|
64
64
|
<p align="center">
|
|
65
|
-
<img src="docs/assets/images/logo
|
|
65
|
+
<img src="docs/assets/images/logo.svg" alt="Argus" height="72">
|
|
66
66
|
</p>
|
|
67
67
|
|
|
68
|
-
<p align="center"><strong>
|
|
68
|
+
<p align="center"><strong>Argus — AI Cloud Detective for AWS, GCP, and Azure.</strong></p>
|
|
69
69
|
|
|
70
|
-
Argus
|
|
70
|
+
Argus is an open-source AI Cloud Detective — it hunts idle and wasted cloud resources across AWS, GCP, and Azure (stopped EC2 instances, unattached EBS volumes, orphaned Elastic IPs, underutilized RDS databases) and delivers a prioritized, AI-reasoned report to Slack every week. No rules. No thresholds. Just reasoning.
|
|
71
71
|
|
|
72
72
|
[](https://github.com/vamshisiddarth/argus/actions/workflows/ci.yml)
|
|
73
73
|
[](https://github.com/vamshisiddarth/argus/actions/workflows/publish.yml)
|
|
@@ -100,6 +100,20 @@ Top findings
|
|
|
100
100
|
|
|
101
101
|
---
|
|
102
102
|
|
|
103
|
+
## 🆕 What's new in v0.4.1
|
|
104
|
+
|
|
105
|
+
> **Biggest release yet.** Argus jumps from scheduled reporter to a conversational FinOps companion.
|
|
106
|
+
|
|
107
|
+
- **Interactive chat mode** — `argus chat` lets you ask natural-language questions about your cloud costs and get intelligent, context-aware answers backed by real metrics and cost data.
|
|
108
|
+
- **True multi-cloud, multi-account** — GCP multi-project and Azure multi-subscription support with unified reporting across all accounts in a single run.
|
|
109
|
+
- **Expanded resource coverage** — 31 GCP resource types (up from 22) and 40 Azure resource types (up from 26), all with curated metric mappings.
|
|
110
|
+
- **Chat UX polish** — live spinner updates per tool call, AI-generated context summaries, turn-safe history trimming, and rich terminal formatting via `pip install argus-cloud-optimizer[chat]`.
|
|
111
|
+
- **Docs overhaul** — new architecture diagram, corrected IAM permissions for all three clouds, and accurate env-var reference.
|
|
112
|
+
|
|
113
|
+
[Full changelog →](CHANGELOG.md)
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
103
117
|
## What it does
|
|
104
118
|
|
|
105
119
|
Every week (or on demand), Argus:
|
|
@@ -166,13 +180,13 @@ docker run --rm \
|
|
|
166
180
|
|
|
167
181
|
```bash
|
|
168
182
|
pip install argus-cloud-optimizer
|
|
169
|
-
argus --version # argus 0.
|
|
183
|
+
argus --version # argus 0.4.1
|
|
170
184
|
argus --help
|
|
171
185
|
```
|
|
172
186
|
|
|
173
187
|
One package — all three clouds included. No extras needed. `--cloud` auto-detects from your environment (GCP_PROJECT_ID / AZURE_SUBSCRIPTION_IDS / AWS credentials), or specify explicitly.
|
|
174
188
|
|
|
175
|
-
> **Verified 2026-06-
|
|
189
|
+
> **Verified 2026-06-24:** `pip install argus-cloud-optimizer && argus --version` works on a clean Python 3.11/3.12/3.13 venv with no project files.
|
|
176
190
|
|
|
177
191
|
**AWS:**
|
|
178
192
|
|
|
@@ -231,7 +245,7 @@ argus chat # auto-detects cloud
|
|
|
231
245
|
```
|
|
232
246
|
|
|
233
247
|
```
|
|
234
|
-
Argus v0.
|
|
248
|
+
Argus v0.4.1 — AI Cloud Detective
|
|
235
249
|
Cloud: AWS | Accounts: prod (123456789012) | Budget: $1.00/session
|
|
236
250
|
Tip: end a line with \ to continue on the next line.
|
|
237
251
|
Type your question, or /help for commands.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="docs/assets/images/logo
|
|
2
|
+
<img src="docs/assets/images/logo.svg" alt="Argus" height="72">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
-
<p align="center"><strong>
|
|
5
|
+
<p align="center"><strong>Argus — AI Cloud Detective for AWS, GCP, and Azure.</strong></p>
|
|
6
6
|
|
|
7
|
-
Argus
|
|
7
|
+
Argus is an open-source AI Cloud Detective — it hunts idle and wasted cloud resources across AWS, GCP, and Azure (stopped EC2 instances, unattached EBS volumes, orphaned Elastic IPs, underutilized RDS databases) and delivers a prioritized, AI-reasoned report to Slack every week. No rules. No thresholds. Just reasoning.
|
|
8
8
|
|
|
9
9
|
[](https://github.com/vamshisiddarth/argus/actions/workflows/ci.yml)
|
|
10
10
|
[](https://github.com/vamshisiddarth/argus/actions/workflows/publish.yml)
|
|
@@ -37,6 +37,20 @@ Top findings
|
|
|
37
37
|
|
|
38
38
|
---
|
|
39
39
|
|
|
40
|
+
## 🆕 What's new in v0.4.1
|
|
41
|
+
|
|
42
|
+
> **Biggest release yet.** Argus jumps from scheduled reporter to a conversational FinOps companion.
|
|
43
|
+
|
|
44
|
+
- **Interactive chat mode** — `argus chat` lets you ask natural-language questions about your cloud costs and get intelligent, context-aware answers backed by real metrics and cost data.
|
|
45
|
+
- **True multi-cloud, multi-account** — GCP multi-project and Azure multi-subscription support with unified reporting across all accounts in a single run.
|
|
46
|
+
- **Expanded resource coverage** — 31 GCP resource types (up from 22) and 40 Azure resource types (up from 26), all with curated metric mappings.
|
|
47
|
+
- **Chat UX polish** — live spinner updates per tool call, AI-generated context summaries, turn-safe history trimming, and rich terminal formatting via `pip install argus-cloud-optimizer[chat]`.
|
|
48
|
+
- **Docs overhaul** — new architecture diagram, corrected IAM permissions for all three clouds, and accurate env-var reference.
|
|
49
|
+
|
|
50
|
+
[Full changelog →](CHANGELOG.md)
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
40
54
|
## What it does
|
|
41
55
|
|
|
42
56
|
Every week (or on demand), Argus:
|
|
@@ -103,13 +117,13 @@ docker run --rm \
|
|
|
103
117
|
|
|
104
118
|
```bash
|
|
105
119
|
pip install argus-cloud-optimizer
|
|
106
|
-
argus --version # argus 0.
|
|
120
|
+
argus --version # argus 0.4.1
|
|
107
121
|
argus --help
|
|
108
122
|
```
|
|
109
123
|
|
|
110
124
|
One package — all three clouds included. No extras needed. `--cloud` auto-detects from your environment (GCP_PROJECT_ID / AZURE_SUBSCRIPTION_IDS / AWS credentials), or specify explicitly.
|
|
111
125
|
|
|
112
|
-
> **Verified 2026-06-
|
|
126
|
+
> **Verified 2026-06-24:** `pip install argus-cloud-optimizer && argus --version` works on a clean Python 3.11/3.12/3.13 venv with no project files.
|
|
113
127
|
|
|
114
128
|
**AWS:**
|
|
115
129
|
|
|
@@ -168,7 +182,7 @@ argus chat # auto-detects cloud
|
|
|
168
182
|
```
|
|
169
183
|
|
|
170
184
|
```
|
|
171
|
-
Argus v0.
|
|
185
|
+
Argus v0.4.1 — AI Cloud Detective
|
|
172
186
|
Cloud: AWS | Accounts: prod (123456789012) | Budget: $1.00/session
|
|
173
187
|
Tip: end a line with \ to continue on the next line.
|
|
174
188
|
Type your question, or /help for commands.
|
|
@@ -82,4 +82,6 @@ class AWSAdapter(CloudAdapter):
|
|
|
82
82
|
) -> "AWSAdapter":
|
|
83
83
|
"""Convenience constructor — resolves auth and returns a ready adapter."""
|
|
84
84
|
session = auth.get_session(account=account, region=region)
|
|
85
|
-
|
|
85
|
+
# Pass aggregator_region=None so __init__ honours RESOURCE_EXPLORER_REGION
|
|
86
|
+
# env var before falling back to the default "us-east-1".
|
|
87
|
+
return cls(session=session, aggregator_region=None)
|
|
@@ -33,12 +33,16 @@ class GCPAdapter(CloudAdapter):
|
|
|
33
33
|
)
|
|
34
34
|
self._project_id: str = resolved
|
|
35
35
|
self._bq_billing_table = bq_billing_table or os.environ.get("BILLING_BQ_TABLE")
|
|
36
|
+
# Populated after list_resources() — asset types skipped due to disabled APIs
|
|
37
|
+
self.skipped_asset_types: list[str] = []
|
|
36
38
|
|
|
37
39
|
def list_resources(self, ignore_regions: list[str] | None = None) -> list[Resource]:
|
|
38
|
-
|
|
40
|
+
resources, skipped = asset_inventory.list_resources(
|
|
39
41
|
project_id=self._project_id,
|
|
40
42
|
ignore_regions=ignore_regions,
|
|
41
43
|
)
|
|
44
|
+
self.skipped_asset_types = skipped
|
|
45
|
+
return resources
|
|
42
46
|
|
|
43
47
|
def get_metrics(
|
|
44
48
|
self,
|
|
@@ -3,7 +3,12 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
5
|
import structlog
|
|
6
|
-
from google.api_core.exceptions import
|
|
6
|
+
from google.api_core.exceptions import (
|
|
7
|
+
GoogleAPICallError,
|
|
8
|
+
InvalidArgument,
|
|
9
|
+
NotFound,
|
|
10
|
+
PermissionDenied,
|
|
11
|
+
)
|
|
7
12
|
from google.cloud import asset_v1
|
|
8
13
|
|
|
9
14
|
from adapters.base import Resource
|
|
@@ -32,7 +37,7 @@ SCANNED_ASSET_TYPES: list[str] = [
|
|
|
32
37
|
"compute.googleapis.com/VpnTunnel",
|
|
33
38
|
"vpcaccess.googleapis.com/Connector", # Serverless VPC
|
|
34
39
|
# --- Databases ---
|
|
35
|
-
"
|
|
40
|
+
"sqladmin.googleapis.com/Instance", # Cloud SQL
|
|
36
41
|
"spanner.googleapis.com/Instance",
|
|
37
42
|
"bigtable.googleapis.com/Instance",
|
|
38
43
|
"alloydb.googleapis.com/Cluster", # AlloyDB (managed Postgres)
|
|
@@ -55,42 +60,76 @@ SCANNED_ASSET_TYPES: list[str] = [
|
|
|
55
60
|
def list_resources(
|
|
56
61
|
project_id: str,
|
|
57
62
|
ignore_regions: list[str] | None = None,
|
|
58
|
-
) -> list[Resource]:
|
|
63
|
+
) -> tuple[list[Resource], list[str]]:
|
|
59
64
|
"""
|
|
60
|
-
Return
|
|
61
|
-
|
|
65
|
+
Return (resources, skipped_asset_types) for a GCP project.
|
|
66
|
+
|
|
67
|
+
Uses a single paginated Cloud Asset Inventory call. If any asset type's API
|
|
68
|
+
is not enabled in the project, INVALID_ARGUMENT is returned for that type.
|
|
69
|
+
We strip it and retry so a project without Bigtable/Spanner/etc. enabled
|
|
70
|
+
doesn't block the whole scan. Callers receive the list of skipped types so
|
|
71
|
+
they can surface a warning in the Slack digest.
|
|
62
72
|
"""
|
|
63
73
|
client = asset_v1.AssetServiceClient()
|
|
64
74
|
parent = f"projects/{project_id}"
|
|
65
75
|
ignore_set = set(ignore_regions or [])
|
|
66
76
|
resources: list[Resource] = []
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
77
|
+
asset_types = list(SCANNED_ASSET_TYPES) # mutable — bad types get stripped
|
|
78
|
+
skipped_types: list[str] = []
|
|
79
|
+
|
|
80
|
+
while True:
|
|
81
|
+
request = asset_v1.ListAssetsRequest(
|
|
82
|
+
parent=parent,
|
|
83
|
+
asset_types=asset_types,
|
|
84
|
+
content_type=asset_v1.ContentType.RESOURCE,
|
|
85
|
+
)
|
|
86
|
+
try:
|
|
87
|
+
for asset in retry_on_transient(
|
|
88
|
+
client.list_assets, request=request, timeout=60
|
|
89
|
+
):
|
|
90
|
+
parsed = _parse_asset(asset, ignore_set)
|
|
91
|
+
if parsed:
|
|
92
|
+
resources.append(parsed)
|
|
93
|
+
break # success — exit retry loop
|
|
94
|
+
except InvalidArgument as exc:
|
|
95
|
+
# An asset type whose API is not enabled in this project causes
|
|
96
|
+
# INVALID_ARGUMENT. Strip it and retry with the remaining types.
|
|
97
|
+
bad_type = _extract_bad_asset_type(str(exc), asset_types)
|
|
98
|
+
if bad_type:
|
|
99
|
+
logger.warning(
|
|
100
|
+
"asset_type_api_not_enabled_skipping",
|
|
101
|
+
asset_type=bad_type,
|
|
102
|
+
project_id=project_id,
|
|
103
|
+
)
|
|
104
|
+
skipped_types.append(bad_type)
|
|
105
|
+
asset_types.remove(bad_type)
|
|
106
|
+
resources = [] # reset — avoid duplicates from partial page
|
|
107
|
+
else:
|
|
108
|
+
raise RuntimeError(
|
|
109
|
+
f"Cloud Asset Inventory INVALID_ARGUMENT (unknown type): {exc}"
|
|
110
|
+
) from exc
|
|
111
|
+
except (PermissionDenied, NotFound) as exc:
|
|
112
|
+
raise PermissionError(
|
|
113
|
+
f"Project '{project_id}' not found or Argus lacks permission. "
|
|
114
|
+
f"Check the project ID and ensure cloudasset.assets.listAssets "
|
|
115
|
+
f"is granted to your credentials."
|
|
116
|
+
) from exc
|
|
117
|
+
except GoogleAPICallError as exc:
|
|
118
|
+
raise RuntimeError(f"Cloud Asset Inventory API error: {exc}") from exc
|
|
88
119
|
|
|
89
120
|
logger.info(
|
|
90
121
|
"asset_inventory_complete",
|
|
91
122
|
extra={"project_id": project_id, "total": len(resources)},
|
|
92
123
|
)
|
|
93
|
-
return resources
|
|
124
|
+
return resources, skipped_types
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _extract_bad_asset_type(error_msg: str, asset_types: list[str]) -> str | None:
|
|
128
|
+
"""Return the first asset type string found in an INVALID_ARGUMENT error."""
|
|
129
|
+
for asset_type in asset_types:
|
|
130
|
+
if asset_type in error_msg:
|
|
131
|
+
return asset_type
|
|
132
|
+
return None
|
|
94
133
|
|
|
95
134
|
|
|
96
135
|
def _parse_asset(asset: Any, ignore_set: set[str]) -> Resource | None:
|
|
@@ -52,7 +52,6 @@ def get_last_activity(
|
|
|
52
52
|
filter_=log_filter,
|
|
53
53
|
order_by=gcp_logging.DESCENDING,
|
|
54
54
|
page_size=1,
|
|
55
|
-
timeout=60,
|
|
56
55
|
)
|
|
57
56
|
)
|
|
58
57
|
except GoogleAPICallError as exc:
|
|
@@ -76,7 +75,7 @@ def _service_from_resource_type(resource_type: str) -> str | None:
|
|
|
76
75
|
mapping: dict[str, str] = {
|
|
77
76
|
"compute.googleapis.com/Instance": "compute.googleapis.com",
|
|
78
77
|
"compute.googleapis.com/Disk": "compute.googleapis.com",
|
|
79
|
-
"
|
|
78
|
+
"sqladmin.googleapis.com/Instance": "cloudsql.googleapis.com",
|
|
80
79
|
"container.googleapis.com/Cluster": "container.googleapis.com",
|
|
81
80
|
"storage.googleapis.com/Bucket": "storage.googleapis.com",
|
|
82
81
|
"bigquery.googleapis.com/Dataset": "bigquery.googleapis.com",
|
{argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1}/adapters/gcp/cloud_monitoring.py
RENAMED
|
@@ -26,7 +26,7 @@ _METRICS: dict[str, list[tuple[str, str]]] = {
|
|
|
26
26
|
("compute.googleapis.com/instance/disk/read_ops_count", "sum"),
|
|
27
27
|
("compute.googleapis.com/instance/disk/write_ops_count", "sum"),
|
|
28
28
|
],
|
|
29
|
-
"
|
|
29
|
+
"sqladmin.googleapis.com/Instance": [
|
|
30
30
|
("cloudsql.googleapis.com/database/cpu/utilization", "mean"),
|
|
31
31
|
("cloudsql.googleapis.com/database/network/connections", "mean"),
|
|
32
32
|
("cloudsql.googleapis.com/database/network/received_bytes_count", "sum"),
|
|
@@ -394,7 +394,7 @@ def _resource_filter(resource_id: str, resource_type: str) -> str:
|
|
|
394
394
|
match resource_type:
|
|
395
395
|
case "compute.googleapis.com/Instance":
|
|
396
396
|
return f'resource.labels.instance_id="{name}"'
|
|
397
|
-
case "
|
|
397
|
+
case "sqladmin.googleapis.com/Instance":
|
|
398
398
|
return f'resource.labels.database_id="{name}"'
|
|
399
399
|
case "container.googleapis.com/Cluster":
|
|
400
400
|
return f'resource.labels.cluster_name="{name}"'
|
|
@@ -128,6 +128,22 @@ class AzureOpenAIProvider(AIProvider):
|
|
|
128
128
|
for attempt in range(MAX_RETRIES):
|
|
129
129
|
try:
|
|
130
130
|
return self._client.chat.completions.create(**kwargs)
|
|
131
|
+
except openai.BadRequestError as exc:
|
|
132
|
+
# Reasoning models (o1/o3/o4) have two restrictions vs standard models:
|
|
133
|
+
# 1. Require max_completion_tokens instead of max_tokens.
|
|
134
|
+
# 2. Do not accept temperature (fixed at 1).
|
|
135
|
+
# Catch both in one retry so we don't need two round-trips.
|
|
136
|
+
err_msg = str(exc)
|
|
137
|
+
changed = False
|
|
138
|
+
if "max_completion_tokens" in err_msg and "max_tokens" in kwargs:
|
|
139
|
+
kwargs["max_completion_tokens"] = kwargs.pop("max_tokens")
|
|
140
|
+
changed = True
|
|
141
|
+
if "temperature" in err_msg and "temperature" in kwargs:
|
|
142
|
+
kwargs.pop("temperature")
|
|
143
|
+
changed = True
|
|
144
|
+
if changed:
|
|
145
|
+
continue
|
|
146
|
+
raise
|
|
131
147
|
except openai.RateLimitError:
|
|
132
148
|
if attempt < MAX_RETRIES - 1:
|
|
133
149
|
logger.warning(
|
|
@@ -9,7 +9,9 @@ No API key needed when running on Cloud Run / GCE with the right service account
|
|
|
9
9
|
Environment variables:
|
|
10
10
|
VERTEXAI_PROJECT GCP project ID (required)
|
|
11
11
|
VERTEXAI_LOCATION GCP region (default: us-central1)
|
|
12
|
-
VERTEXAI_MODEL Model name (default: gemini-1.5-pro-002)
|
|
12
|
+
VERTEXAI_MODEL Model name (default: google/gemini-1.5-pro-002)
|
|
13
|
+
Must include publisher prefix for the OpenAI-compat endpoint
|
|
14
|
+
(e.g. google/gemini-1.5-pro-002, google/gemini-2.0-flash-001)
|
|
13
15
|
"""
|
|
14
16
|
|
|
15
17
|
from __future__ import annotations
|
|
@@ -38,10 +40,11 @@ class VertexAIProvider(AIProvider):
|
|
|
38
40
|
or directly via vertexai package. Falls back to the OpenAI-compatible
|
|
39
41
|
Vertex AI endpoint so we can reuse the openai SDK already in requirements.txt.
|
|
40
42
|
|
|
41
|
-
Model: gemini-1.5-pro-002 (default) —
|
|
43
|
+
Model: google/gemini-1.5-pro-002 (default) — the publisher prefix is required
|
|
44
|
+
by the OpenAI-compatible Vertex AI endpoint (/endpoints/openapi).
|
|
42
45
|
"""
|
|
43
46
|
|
|
44
|
-
DEFAULT_MODEL = "gemini-1.5-pro-002"
|
|
47
|
+
DEFAULT_MODEL = "google/gemini-1.5-pro-002"
|
|
45
48
|
DEFAULT_LOCATION = "us-central1"
|
|
46
49
|
DEFAULT_MAX_TOKENS = 4096
|
|
47
50
|
DEFAULT_TEMPERATURE = 0.0
|
{argus_cloud_optimizer-0.4.0 → argus_cloud_optimizer-0.4.1/argus_cloud_optimizer.egg-info}/PKG-INFO
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: argus-cloud-optimizer
|
|
3
|
-
Version: 0.4.
|
|
4
|
-
Summary: AI
|
|
3
|
+
Version: 0.4.1
|
|
4
|
+
Summary: Argus — AI Cloud Detective for AWS, GCP, and Azure
|
|
5
5
|
Author-email: Vamshi Siddarth Gaddam <vamshisiddarth02@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/vamshisiddarth/argus
|
|
@@ -62,12 +62,12 @@ Requires-Dist: mkdocs-minify-plugin>=0.8.0; extra == "dev"
|
|
|
62
62
|
Dynamic: license-file
|
|
63
63
|
|
|
64
64
|
<p align="center">
|
|
65
|
-
<img src="docs/assets/images/logo
|
|
65
|
+
<img src="docs/assets/images/logo.svg" alt="Argus" height="72">
|
|
66
66
|
</p>
|
|
67
67
|
|
|
68
|
-
<p align="center"><strong>
|
|
68
|
+
<p align="center"><strong>Argus — AI Cloud Detective for AWS, GCP, and Azure.</strong></p>
|
|
69
69
|
|
|
70
|
-
Argus
|
|
70
|
+
Argus is an open-source AI Cloud Detective — it hunts idle and wasted cloud resources across AWS, GCP, and Azure (stopped EC2 instances, unattached EBS volumes, orphaned Elastic IPs, underutilized RDS databases) and delivers a prioritized, AI-reasoned report to Slack every week. No rules. No thresholds. Just reasoning.
|
|
71
71
|
|
|
72
72
|
[](https://github.com/vamshisiddarth/argus/actions/workflows/ci.yml)
|
|
73
73
|
[](https://github.com/vamshisiddarth/argus/actions/workflows/publish.yml)
|
|
@@ -100,6 +100,20 @@ Top findings
|
|
|
100
100
|
|
|
101
101
|
---
|
|
102
102
|
|
|
103
|
+
## 🆕 What's new in v0.4.1
|
|
104
|
+
|
|
105
|
+
> **Biggest release yet.** Argus jumps from scheduled reporter to a conversational FinOps companion.
|
|
106
|
+
|
|
107
|
+
- **Interactive chat mode** — `argus chat` lets you ask natural-language questions about your cloud costs and get intelligent, context-aware answers backed by real metrics and cost data.
|
|
108
|
+
- **True multi-cloud, multi-account** — GCP multi-project and Azure multi-subscription support with unified reporting across all accounts in a single run.
|
|
109
|
+
- **Expanded resource coverage** — 31 GCP resource types (up from 22) and 40 Azure resource types (up from 26), all with curated metric mappings.
|
|
110
|
+
- **Chat UX polish** — live spinner updates per tool call, AI-generated context summaries, turn-safe history trimming, and rich terminal formatting via `pip install argus-cloud-optimizer[chat]`.
|
|
111
|
+
- **Docs overhaul** — new architecture diagram, corrected IAM permissions for all three clouds, and accurate env-var reference.
|
|
112
|
+
|
|
113
|
+
[Full changelog →](CHANGELOG.md)
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
103
117
|
## What it does
|
|
104
118
|
|
|
105
119
|
Every week (or on demand), Argus:
|
|
@@ -166,13 +180,13 @@ docker run --rm \
|
|
|
166
180
|
|
|
167
181
|
```bash
|
|
168
182
|
pip install argus-cloud-optimizer
|
|
169
|
-
argus --version # argus 0.
|
|
183
|
+
argus --version # argus 0.4.1
|
|
170
184
|
argus --help
|
|
171
185
|
```
|
|
172
186
|
|
|
173
187
|
One package — all three clouds included. No extras needed. `--cloud` auto-detects from your environment (GCP_PROJECT_ID / AZURE_SUBSCRIPTION_IDS / AWS credentials), or specify explicitly.
|
|
174
188
|
|
|
175
|
-
> **Verified 2026-06-
|
|
189
|
+
> **Verified 2026-06-24:** `pip install argus-cloud-optimizer && argus --version` works on a clean Python 3.11/3.12/3.13 venv with no project files.
|
|
176
190
|
|
|
177
191
|
**AWS:**
|
|
178
192
|
|
|
@@ -231,7 +245,7 @@ argus chat # auto-detects cloud
|
|
|
231
245
|
```
|
|
232
246
|
|
|
233
247
|
```
|
|
234
|
-
Argus v0.
|
|
248
|
+
Argus v0.4.1 — AI Cloud Detective
|
|
235
249
|
Cloud: AWS | Accounts: prod (123456789012) | Budget: $1.00/session
|
|
236
250
|
Tip: end a line with \ to continue on the next line.
|
|
237
251
|
Type your question, or /help for commands.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.1"
|
|
@@ -195,6 +195,7 @@ class AgentLoop:
|
|
|
195
195
|
resources = self._adapter.list_resources(ignore_regions=ignore_regions or None)
|
|
196
196
|
resources = _apply_exclusion_filters(resources)
|
|
197
197
|
total_discovered = len(resources)
|
|
198
|
+
logger.info("resources_discovered", count=total_discovered)
|
|
198
199
|
|
|
199
200
|
if not resources:
|
|
200
201
|
self._prefiltered_payload = []
|
|
@@ -242,6 +243,11 @@ class AgentLoop:
|
|
|
242
243
|
discovered=total_discovered,
|
|
243
244
|
sent_to_ai=len(payload),
|
|
244
245
|
)
|
|
246
|
+
logger.info(
|
|
247
|
+
"ai_analysis_starting",
|
|
248
|
+
resources=len(payload),
|
|
249
|
+
msg=f"Analyzing {len(payload)} resources with AI (this takes 2-5 minutes)…",
|
|
250
|
+
)
|
|
245
251
|
return payload
|
|
246
252
|
|
|
247
253
|
def _execute(self, tc: ToolCall) -> tuple[str, bool]:
|
|
@@ -48,7 +48,7 @@ class AISettings(BaseSettings):
|
|
|
48
48
|
# Vertex AI
|
|
49
49
|
vertexai_project: str | None = Field(None, alias="VERTEXAI_PROJECT")
|
|
50
50
|
vertexai_location: str = Field("us-central1", alias="VERTEXAI_LOCATION")
|
|
51
|
-
vertexai_model: str = Field("gemini-1.5-pro-002", alias="VERTEXAI_MODEL")
|
|
51
|
+
vertexai_model: str = Field("google/gemini-1.5-pro-002", alias="VERTEXAI_MODEL")
|
|
52
52
|
|
|
53
53
|
# Azure OpenAI
|
|
54
54
|
azure_openai_endpoint: str | None = Field(None, alias="AZURE_OPENAI_ENDPOINT")
|
|
@@ -219,7 +219,14 @@ def build_notification_providers() -> list[NotificationProvider]:
|
|
|
219
219
|
return providers
|
|
220
220
|
|
|
221
221
|
|
|
222
|
-
def notify_all(payload: dict[str, Any], dry_run: bool | None = None) ->
|
|
222
|
+
def notify_all(payload: dict[str, Any], dry_run: bool | None = None) -> bool:
|
|
223
|
+
"""
|
|
224
|
+
Send payload to all configured notification providers.
|
|
225
|
+
|
|
226
|
+
Returns True if at least one provider delivered successfully, False if
|
|
227
|
+
every provider failed. Callers should treat False as a delivery failure
|
|
228
|
+
and exit non-zero so the scheduler / CloudWatch alarm fires.
|
|
229
|
+
"""
|
|
223
230
|
from core.config import get_settings
|
|
224
231
|
|
|
225
232
|
resolved_dry_run = dry_run if dry_run is not None else get_settings().report.dry_run
|
|
@@ -229,12 +236,14 @@ def notify_all(payload: dict[str, Any], dry_run: bool | None = None) -> None:
|
|
|
229
236
|
"dry_run_notification_skipped",
|
|
230
237
|
payload_preview=json.dumps(payload, indent=2)[:500],
|
|
231
238
|
)
|
|
232
|
-
return
|
|
239
|
+
return True
|
|
233
240
|
|
|
234
241
|
providers = build_notification_providers()
|
|
242
|
+
any_succeeded = False
|
|
235
243
|
for provider in providers:
|
|
236
244
|
try:
|
|
237
245
|
provider.notify(payload)
|
|
246
|
+
any_succeeded = True
|
|
238
247
|
except (NotificationDeliveryError, OSError) as exc:
|
|
239
248
|
logger.error(
|
|
240
249
|
"notification_delivery_failed",
|
|
@@ -242,6 +251,11 @@ def notify_all(payload: dict[str, Any], dry_run: bool | None = None) -> None:
|
|
|
242
251
|
error=str(exc),
|
|
243
252
|
)
|
|
244
253
|
|
|
254
|
+
if not any_succeeded and providers:
|
|
255
|
+
logger.error("all_notification_providers_failed", provider_count=len(providers))
|
|
256
|
+
|
|
257
|
+
return any_succeeded or not providers
|
|
258
|
+
|
|
245
259
|
|
|
246
260
|
# ---------------------------------------------------------------------------
|
|
247
261
|
# Legacy function — delegates to notify_all for backward compatibility
|
|
@@ -23,6 +23,7 @@ def build_report(
|
|
|
23
23
|
agent_output_tokens: int = 0,
|
|
24
24
|
scan_diff: dict[str, Any] | None = None,
|
|
25
25
|
scan_errors: list[dict[str, str]] | None = None,
|
|
26
|
+
skipped_resource_types: list[str] | None = None,
|
|
26
27
|
) -> dict[str, Any]:
|
|
27
28
|
"""
|
|
28
29
|
Convert a list of ResourceFinding objects into the canonical JSON report.
|
|
@@ -53,6 +54,7 @@ def build_report(
|
|
|
53
54
|
),
|
|
54
55
|
"scan_diff": scan_diff,
|
|
55
56
|
"scan_errors": scan_errors or [],
|
|
57
|
+
"skipped_resource_types": skipped_resource_types or [],
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
|
|
@@ -207,6 +209,25 @@ def build_slack_payload(
|
|
|
207
209
|
{"type": "divider"},
|
|
208
210
|
]
|
|
209
211
|
|
|
212
|
+
skipped_types = report.get("skipped_resource_types") or []
|
|
213
|
+
if skipped_types:
|
|
214
|
+
short_names = [t.split("/")[-1] for t in skipped_types]
|
|
215
|
+
blocks.append(
|
|
216
|
+
{
|
|
217
|
+
"type": "section",
|
|
218
|
+
"text": {
|
|
219
|
+
"type": "mrkdwn",
|
|
220
|
+
"text": (
|
|
221
|
+
f":information_source: *Skipped {len(skipped_types)} resource "
|
|
222
|
+
f"type{'s' if len(skipped_types) != 1 else ''} "
|
|
223
|
+
f"(API not enabled in project):* "
|
|
224
|
+
+ ", ".join(f"`{n}`" for n in short_names)
|
|
225
|
+
),
|
|
226
|
+
},
|
|
227
|
+
}
|
|
228
|
+
)
|
|
229
|
+
blocks.append({"type": "divider"})
|
|
230
|
+
|
|
210
231
|
scan_errors = report.get("scan_errors") or []
|
|
211
232
|
if scan_errors:
|
|
212
233
|
total_attempted = accounts + len(scan_errors)
|