cloudcostwise 0.1.2__tar.gz → 0.1.3__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.
Files changed (59) hide show
  1. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/PKG-INFO +3 -1
  2. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/pyproject.toml +3 -1
  3. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudcostwise/__init__.py +1 -1
  4. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudcostwise/mcp_server.py +10 -10
  5. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudcostwise.egg-info/PKG-INFO +3 -1
  6. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudwise_scan_core/advisory_types.py +12 -10
  7. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudwise_scan_core/cloudwatch_metrics_service.py +15 -9
  8. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudwise_scan_core/data_providers/base.py +31 -0
  9. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudwise_scan_core/data_providers/missing_data.py +7 -0
  10. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudwise_scan_core/data_providers/models.py +14 -2
  11. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudwise_scan_core/data_providers/offline.py +466 -87
  12. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudwise_scan_core/data_providers/online.py +291 -22
  13. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudwise_scan_core/detectors/open/database.py +254 -50
  14. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudwise_scan_core/detectors/open/network.py +9 -1
  15. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudwise_scan_core/detectors/open/optimizer.py +92 -23
  16. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudwise_scan_core/detectors/open/storage.py +29 -1
  17. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudwise_scan_core/metric_window.py +53 -0
  18. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudwise_scan_core/models.py +6 -0
  19. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudwise_scan_core/waste_detection_service.py +6 -0
  20. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/LICENSE.md +0 -0
  21. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/README.md +0 -0
  22. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/setup.cfg +0 -0
  23. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudcostwise/cli.py +0 -0
  24. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudcostwise/render.py +0 -0
  25. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudcostwise/runtime.py +0 -0
  26. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudcostwise/scan.py +0 -0
  27. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudcostwise.egg-info/SOURCES.txt +0 -0
  28. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudcostwise.egg-info/dependency_links.txt +0 -0
  29. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudcostwise.egg-info/entry_points.txt +0 -0
  30. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudcostwise.egg-info/requires.txt +0 -0
  31. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudcostwise.egg-info/top_level.txt +0 -0
  32. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudwise_scan_core/__init__.py +0 -0
  33. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudwise_scan_core/aws_ip_ranges.py +0 -0
  34. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudwise_scan_core/aws_ip_ranges_snapshot.py +0 -0
  35. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudwise_scan_core/aws_pricing_service.py +0 -0
  36. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudwise_scan_core/billed_cost.py +0 -0
  37. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudwise_scan_core/cache.py +0 -0
  38. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudwise_scan_core/circuit_breaker.py +0 -0
  39. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudwise_scan_core/config.py +0 -0
  40. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudwise_scan_core/cpu_sizing.py +0 -0
  41. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudwise_scan_core/credential_cache.py +0 -0
  42. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudwise_scan_core/data_providers/__init__.py +0 -0
  43. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudwise_scan_core/detectors/__init__.py +0 -0
  44. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudwise_scan_core/detectors/extended_support.py +0 -0
  45. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudwise_scan_core/detectors/open/__init__.py +0 -0
  46. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudwise_scan_core/detectors/open/commitment_cache.py +0 -0
  47. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudwise_scan_core/detectors/open/compute.py +0 -0
  48. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudwise_scan_core/detectors/open/management.py +0 -0
  49. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudwise_scan_core/detectors/open/savings.py +0 -0
  50. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudwise_scan_core/detectors/open/security.py +0 -0
  51. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudwise_scan_core/metrics.py +0 -0
  52. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudwise_scan_core/open_core_manifest.py +0 -0
  53. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudwise_scan_core/open_service.py +0 -0
  54. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudwise_scan_core/ports.py +0 -0
  55. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudwise_scan_core/py.typed +0 -0
  56. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudwise_scan_core/savings_cache_service.py +0 -0
  57. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/src/cloudwise_scan_core/tier_routing.py +0 -0
  58. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/tests/test_cli.py +0 -0
  59. {cloudcostwise-0.1.2 → cloudcostwise-0.1.3}/tests/test_mcp_server.py +0 -0
@@ -1,10 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudcostwise
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Find AWS waste from your terminal or your AI assistant. Runs locally with your own read-only credentials; nothing leaves your machine.
5
5
  Author: CloudWise
6
6
  License: FSL-1.1-ALv2
7
7
  Project-URL: Homepage, https://cloudcostwise.io/cli
8
+ Project-URL: Repository, https://github.com/cloudwise-app/cloudcostwise
9
+ Project-URL: Issues, https://github.com/cloudwise-app/cloudcostwise/issues
8
10
  Requires-Python: >=3.11
9
11
  Description-Content-Type: text/markdown
10
12
  License-File: LICENSE.md
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cloudcostwise"
7
- version = "0.1.2"
7
+ version = "0.1.3"
8
8
  description = "Find AWS waste from your terminal or your AI assistant. Runs locally with your own read-only credentials; nothing leaves your machine."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -20,6 +20,8 @@ cloudcostwise = "cloudcostwise.cli:main"
20
20
 
21
21
  [project.urls]
22
22
  Homepage = "https://cloudcostwise.io/cli"
23
+ Repository = "https://github.com/cloudwise-app/cloudcostwise"
24
+ Issues = "https://github.com/cloudwise-app/cloudcostwise/issues"
23
25
 
24
26
  [tool.setuptools.packages.find]
25
27
  where = ["src"]
@@ -1,3 +1,3 @@
1
1
  """cloudcostwise: find AWS waste locally (FSL-1.1-ALv2)."""
2
2
 
3
- __version__ = "0.1.2"
3
+ __version__ = "0.1.3"
@@ -62,16 +62,16 @@ def build_server() -> MCPServer:
62
62
  session = _Session()
63
63
  server = MCPServer(name="cloudcostwise", version=__version__, instructions=INSTRUCTIONS)
64
64
 
65
- @server.tool(
66
- annotations=READ_ONLY,
67
- description=(
68
- "Scan the user's AWS account for waste with their local credentials (read-only; any non-read "
69
- "AWS call is refused). Takes 30-60 s per region; up to 4 regions run at once. COST: the RI/Savings Plans checks call Cost "
70
- "Explorer, billed by AWS at $0.01 per request (about $0.13 a scan); tell the user before "
71
- "running, and pass include_cost_explorer=false to skip them. regions: comma-separated, or "
72
- "'all'; default us-east-1 plus the profile's region."
73
- ),
74
- )
65
+ # Same one-line decorator shape as the other three tools: directories that
66
+ # list tools by reading the source (mcprush) missed this one when its
67
+ # decorator opened on its own line.
68
+ @server.tool(annotations=READ_ONLY,
69
+ description="Scan the user's AWS account for waste with their local credentials (read-only; "
70
+ "any non-read AWS call is refused). Takes 30-60 s per region; up to 4 regions run "
71
+ "at once. COST: the RI/Savings Plans checks call Cost Explorer, billed by AWS at "
72
+ "$0.01 per request (about $0.13 a scan); tell the user before running, and pass "
73
+ "include_cost_explorer=false to skip them. regions: comma-separated, or 'all'; "
74
+ "default us-east-1 plus the profile's region.")
75
75
  async def scan(ctx: Context, profile: Optional[str] = None, regions: Optional[str] = None,
76
76
  include_cost_explorer: bool = True) -> str:
77
77
  try:
@@ -1,10 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudcostwise
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Find AWS waste from your terminal or your AI assistant. Runs locally with your own read-only credentials; nothing leaves your machine.
5
5
  Author: CloudWise
6
6
  License: FSL-1.1-ALv2
7
7
  Project-URL: Homepage, https://cloudcostwise.io/cli
8
+ Project-URL: Repository, https://github.com/cloudwise-app/cloudcostwise
9
+ Project-URL: Issues, https://github.com/cloudwise-app/cloudcostwise/issues
8
10
  Requires-Python: >=3.11
9
11
  Description-Content-Type: text/markdown
10
12
  License-File: LICENSE.md
@@ -25,6 +25,7 @@ UNVALIDATED = "unvalidated"
25
25
 
26
26
  # No path to L2 within budget.
27
27
  ADVISORY_WASTE_TYPES: frozenset[str] = frozenset({
28
+ "appsync_idle_cache",
28
29
  "aurora_io_optimization_opportunity",
29
30
  "aurora_serverless_opportunity",
30
31
  "convertible_ri_exchange_opportunity",
@@ -55,6 +56,7 @@ ADVISORY_WASTE_TYPES: frozenset[str] = frozenset({
55
56
  "ri_opportunity_opensearch",
56
57
  "ri_opportunity_rds",
57
58
  "ri_opportunity_redshift",
59
+ "s3_high_request_and_transfer_cost",
58
60
  "s3_no_default_encryption",
59
61
  "savings_plan_coverage_gap",
60
62
  "sp_opportunity_compute",
@@ -80,18 +82,22 @@ TRUSTED_WASTE_TYPES: frozenset[str] = frozenset({
80
82
  "dynamodb_no_deletion_protection",
81
83
  "ecr_no_lifecycle_policy",
82
84
  "ecs_container_insights_waste",
85
+ "ecs_no_autoscaling",
83
86
  "eip_on_stopped_instance",
84
87
  "elasticache_engine_migration",
85
88
  "elasticache_extended_support_cost",
89
+ "elasticache_replication_waste",
86
90
  "empty_log_group",
87
91
  "excessive_retention_log_group",
88
92
  "failed_glue_job_retry",
89
93
  "glue_job_missing_timeout",
90
94
  "gp2_migration",
95
+ "idle_beanstalk",
91
96
  "idle_documentdb",
92
97
  "idle_dynamodb",
93
98
  "idle_ecs_service",
94
99
  "idle_efs",
100
+ "idle_fsx",
95
101
  "idle_load_balancer",
96
102
  "idle_state_machine",
97
103
  "kinesis_firehose_idle",
@@ -99,6 +105,8 @@ TRUSTED_WASTE_TYPES: frozenset[str] = frozenset({
99
105
  "lambda_excessive_timeout",
100
106
  "lambda_old_runtime",
101
107
  "lambda_provisioned_concurrency_idle",
108
+ "lightsail_idle_database",
109
+ "lightsail_idle_load_balancer",
102
110
  "lightsail_unattached_disk",
103
111
  "lightsail_unattached_static_ip",
104
112
  "low_traffic_alb",
@@ -108,11 +116,13 @@ TRUSTED_WASTE_TYPES: frozenset[str] = frozenset({
108
116
  "no_retention_log_group",
109
117
  "old_ecr_images",
110
118
  "old_glue_job",
119
+ "opensearch_no_encryption_at_rest",
111
120
  "orphaned_dns_record",
112
121
  "orphaned_ebs_snapshot",
113
122
  "over_provisioned_dynamodb",
114
123
  "over_provisioned_lambda",
115
124
  "oversized_glue_job",
125
+ "oversized_sagemaker_endpoint",
116
126
  "previous_gen_sagemaker_instance",
117
127
  "rds_no_deletion_protection",
118
128
  "rds_publicly_accessible",
@@ -140,7 +150,6 @@ TRUSTED_WASTE_TYPES: frozenset[str] = frozenset({
140
150
 
141
151
  # L0/L1 with a path to L2.
142
152
  UNVALIDATED_WASTE_TYPES: frozenset[str] = frozenset({
143
- "appsync_idle_cache",
144
153
  "aurora_extended_support_cost",
145
154
  "aurora_to_rds_downgrade_opportunity",
146
155
  "backup_copy_policy_overreach",
@@ -151,20 +160,16 @@ UNVALIDATED_WASTE_TYPES: frozenset[str] = frozenset({
151
160
  "beanstalk_unnecessary_alb",
152
161
  "cloudtrail_s3_no_lifecycle",
153
162
  "disabled_global_accelerator",
154
- "ecs_no_autoscaling",
155
163
  "eks_extended_support_cost",
156
164
  "elasticache_data_tiering_opportunity",
157
- "elasticache_replication_waste",
158
165
  "elasticache_serverless_optimization",
159
166
  "emr_missing_auto_termination",
160
167
  "emr_previous_gen_instances",
161
168
  "emr_spot_opportunity",
162
169
  "fsx_throughput_overprovisioned",
163
- "idle_beanstalk",
164
170
  "idle_ec2",
165
171
  "idle_elasticache",
166
172
  "idle_emr_cluster",
167
- "idle_fsx",
168
173
  "idle_glue_crawler",
169
174
  "idle_kinesis_stream",
170
175
  "idle_lightsail",
@@ -179,8 +184,6 @@ UNVALIDATED_WASTE_TYPES: frozenset[str] = frozenset({
179
184
  "incomplete_multipart",
180
185
  "kinesis_enhanced_fan_out_waste",
181
186
  "kinesis_extended_retention_waste",
182
- "lightsail_idle_database",
183
- "lightsail_idle_load_balancer",
184
187
  "lightsail_old_snapshot",
185
188
  "neptune_old_snapshot",
186
189
  "neptune_serverless_opportunity",
@@ -192,7 +195,6 @@ UNVALIDATED_WASTE_TYPES: frozenset[str] = frozenset({
192
195
  "old_rds_snapshot",
193
196
  "opensearch_ebs_overprovisioned",
194
197
  "opensearch_extended_support_cost",
195
- "opensearch_no_encryption_at_rest",
196
198
  "over_provisioned_iops",
197
199
  "over_provisioned_kinesis",
198
200
  "overprovisioned_documentdb",
@@ -208,10 +210,8 @@ UNVALIDATED_WASTE_TYPES: frozenset[str] = frozenset({
208
210
  "oversized_neptune",
209
211
  "oversized_opensearch",
210
212
  "oversized_rds_optimizer",
211
- "oversized_sagemaker_endpoint",
212
213
  "oversized_workspace",
213
214
  "rds_extended_support_cost",
214
- "s3_high_request_and_transfer_cost",
215
215
  "s3_rapid_growth",
216
216
  "stopped_sagemaker_notebook_storage",
217
217
  "unused_api_gateway",
@@ -224,6 +224,7 @@ UNVALIDATED_WASTE_TYPES: frozenset[str] = frozenset({
224
224
 
225
225
  # Why each advisory type is advisory (ledger `advisory_reason`).
226
226
  ADVISORY_REASONS: Dict[str, str] = {
227
+ "appsync_idle_cache": "over_budget",
227
228
  "aurora_io_optimization_opportunity": "over_budget",
228
229
  "aurora_serverless_opportunity": "over_budget",
229
230
  "convertible_ri_exchange_opportunity": "commitment",
@@ -254,6 +255,7 @@ ADVISORY_REASONS: Dict[str, str] = {
254
255
  "ri_opportunity_opensearch": "commitment",
255
256
  "ri_opportunity_rds": "commitment",
256
257
  "ri_opportunity_redshift": "commitment",
258
+ "s3_high_request_and_transfer_cost": "over_budget",
257
259
  "s3_no_default_encryption": "retired_config",
258
260
  "savings_plan_coverage_gap": "commitment",
259
261
  "sp_opportunity_compute": "commitment",
@@ -164,22 +164,27 @@ class MSKMetrics:
164
164
  messages_in_per_sec: float
165
165
  bytes_in_per_sec: float
166
166
  bytes_out_per_sec: float
167
- # Online (CLO-549): the busiest broker's mean CpuUser + CpuSystem, and
168
- # traffic summed across brokers. Offline: the export's cluster figure.
167
+ # Both online (CLO-549) and offline (CLO-560): the busiest broker's mean
168
+ # CpuUser + CpuSystem, and traffic summed across brokers.
169
169
  cpu_user: float
170
170
  period_days: int
171
171
  is_idle: bool = False
172
- # CLO-457: hourly CpuUser datapoints behind ``cpu_user``. 0 means CPU was
173
- # not measured (``cpu_user`` is then a placeholder 0.0, not 0% CPU);
174
- # None means the source does not count them (the offline export).
172
+ # CLO-457/CLO-560: hourly CpuUser+CpuSystem datapoints behind
173
+ # ``cpu_user`` (the fewest of any broker). 0 means CPU was not measured
174
+ # on at least one broker (``cpu_user`` is then a placeholder 0.0, not 0%
175
+ # CPU). None is unused today — both providers count it.
175
176
  cpu_datapoints: Optional[int] = None
176
177
 
177
178
 
178
179
  @dataclass
179
180
  class NeptuneMetrics:
180
181
  """Neptune cluster metrics."""
181
- gremlin_requests: int
182
- sparql_requests: int
182
+ # CLO-584: float for parity with NeptuneMetricsData (data_providers/models.py) --
183
+ # this class and get_neptune_metrics() below are not on the live
184
+ # detector path (no caller in the repo), but kept consistent rather than
185
+ # left truncating.
186
+ gremlin_requests: float
187
+ sparql_requests: float
183
188
  loader_requests: int
184
189
  avg_cpu: float
185
190
  max_cpu: float
@@ -1779,8 +1784,9 @@ class CloudWatchMetricsService:
1779
1784
  )
1780
1785
  )
1781
1786
 
1782
- gremlin_requests = int(sum(dp.get('Sum', 0) for dp in gremlin_response.get('Datapoints', [])))
1783
- sparql_requests = int(sum(dp.get('Sum', 0) for dp in sparql_response.get('Datapoints', [])))
1787
+ # CLO-584: kept as floats (see NeptuneMetrics above).
1788
+ gremlin_requests = sum(dp.get('Sum', 0) for dp in gremlin_response.get('Datapoints', []))
1789
+ sparql_requests = sum(dp.get('Sum', 0) for dp in sparql_response.get('Datapoints', []))
1784
1790
 
1785
1791
  cpu_datapoints = cpu_response.get('Datapoints', [])
1786
1792
  avg_cpu = sum(dp.get('Average', 0) for dp in cpu_datapoints) / len(cpu_datapoints) if cpu_datapoints else 0
@@ -776,6 +776,26 @@ class WasteDataProvider(ABC):
776
776
  both names; datapoints from before the consumer existed are excluded."""
777
777
  pass
778
778
 
779
+ @abstractmethod
780
+ async def get_kinesis_consumer_subscribed(
781
+ self, stream_name: str, consumer_name: str,
782
+ ) -> Optional[bool]:
783
+ """Whether an enhanced fan-out consumer has subscribed in the past
784
+ two weeks (CLO-589 follow-up, PR #1654 review HIGH 1).
785
+
786
+ AWS publishes SubscribeToShard.Success (StreamName, ConsumerName) at
787
+ least once every 5 minutes while a subscription lives, and the
788
+ SubscribeToShardEvent.* series only while one is active; a consumer
789
+ that never subscribes publishes no series at all. CloudWatch
790
+ ListMetrics reports the metrics that had data in the past two weeks.
791
+ So: True when a successful ListMetrics answer holds any series for
792
+ the (StreamName, ConsumerName) pair; False only when a complete,
793
+ successful answer holds none; None (MISSING, noted) when it could not
794
+ be read. Never an empty GetMetricStatistics series read as idle.
795
+
796
+ Both providers implement it (repo rule: every get_* is abstract)."""
797
+ pass
798
+
779
799
  @abstractmethod
780
800
  async def get_firehose_delivery_streams(self) -> List['KinesisFirehoseData']:
781
801
  """Get all Kinesis Data Firehose delivery streams."""
@@ -1011,6 +1031,17 @@ class WasteDataProvider(ABC):
1011
1031
  """Get copy job summaries for the specified lookback period."""
1012
1032
  pass
1013
1033
 
1034
+ async def backup_vault_exists(self, vault_arn: str) -> Optional[bool]:
1035
+ """Whether the backup vault ``vault_arn`` exists now (CLO-589).
1036
+
1037
+ True / False only on a read that could see the vault; None when it
1038
+ could not be judged (a cross-account vault, a failed read, a
1039
+ provider that cannot look): MISSING, never "exists". ListCopyJobs
1040
+ keeps a deleted vault's COMPLETED jobs for its whole window, so
1041
+ backup_copy_policy_overreach needs this before naming a vault. The
1042
+ default cannot look."""
1043
+ return None
1044
+
1014
1045
  # =========================================================================
1015
1046
  # DocumentDB
1016
1047
  # =========================================================================
@@ -23,6 +23,13 @@ logger = logging.getLogger(__name__)
23
23
  # Example resource ids kept in a note; the count covers the rest.
24
24
  _MAX_EXAMPLES = 3
25
25
 
26
+ # CLO-589 follow-up: the one note kinesis_enhanced_fan_out_waste's withheld
27
+ # verdicts share (both providers, the base default and the detector), kept
28
+ # apart from the stream-level 'kinesis' note so their wording never mixes.
29
+ KINESIS_EFO_NOTE_SERVICE = 'kinesis enhanced fan-out'
30
+ KINESIS_EFO_NOTE_VERDICT = 'never-subscribed consumer'
31
+ KINESIS_EFO_NOTE_EVIDENCE = 'SubscribeToShard series (ListMetrics)'
32
+
26
33
 
27
34
  class MissingDataNotesMixin:
28
35
  """Provider mixin. Needs ``self.data_warnings`` (a list), ``self._region``
@@ -583,6 +583,15 @@ class ElastiCacheMetricsData:
583
583
  # applies to the CurrConnections coverage check — otherwise a shorter
584
584
  # export could never pass a coverage gate sized to the full window.
585
585
  cpu_window_days: Optional[int] = None
586
+ # CLO-572: DatabaseMemoryUsagePercentage datapoints behind
587
+ # database_memory_usage_pct, the same convention as cpu_datapoints: None
588
+ # means not reported, 0 means a read with no data (MISSING, never read
589
+ # as 0% used). oversized_elasticache's memory gate needs 75% coverage
590
+ # of memory_window_days (cpu_window_days's sibling: None means use
591
+ # period_days, the offline provider clamps it to the export's own
592
+ # shorter window).
593
+ memory_datapoints: Optional[int] = None
594
+ memory_window_days: Optional[int] = None
586
595
 
587
596
 
588
597
  @dataclass
@@ -1422,8 +1431,11 @@ class NeptuneSnapshotData:
1422
1431
  class NeptuneMetricsData:
1423
1432
  """Normalized Neptune cluster CloudWatch metrics."""
1424
1433
  cluster_id: str
1425
- gremlin_requests: int = 0
1426
- sparql_requests: int = 0
1434
+ # CLO-584: floats, not int -- a lightly used cluster's per-second-rate
1435
+ # Sum over the window can be well under 1, and an int() truncation made
1436
+ # that indistinguishable from true zero traffic.
1437
+ gremlin_requests: float = 0.0
1438
+ sparql_requests: float = 0.0
1427
1439
  avg_cpu: float = 0.0
1428
1440
  max_cpu: float = 0.0 # window maximum of hourly Maximums; reported, not gated by oversized_neptune
1429
1441
  # CLO-480: the shared CPU rule's inputs (cloudwise_scan_core.cpu_sizing)