cloudcost-cli 0.1.0__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.
@@ -0,0 +1,94 @@
1
+ from decimal import Decimal, InvalidOperation
2
+ from typing import Any
3
+
4
+
5
+ CLOUDCOST_COMMENT_MARKER = "<!-- cloudcost-ai:infracost -->"
6
+
7
+
8
+ def to_decimal(value: Any) -> Decimal | None:
9
+ if value is None or value == "":
10
+ return None
11
+ try:
12
+ return Decimal(str(value))
13
+ except (InvalidOperation, ValueError):
14
+ return None
15
+
16
+
17
+ def format_money(value: Any, signed: bool = False) -> str:
18
+ amount = to_decimal(value)
19
+ if amount is None:
20
+ return "unknown"
21
+
22
+ sign = ""
23
+ if signed and amount > 0:
24
+ sign = "+"
25
+ quantized = amount.quantize(Decimal("0.01"))
26
+ return f"{sign}${quantized:,.2f}"
27
+
28
+
29
+ def format_infracost_comment(
30
+ *,
31
+ plan_path: str,
32
+ diff_total_monthly_cost: Any,
33
+ total_monthly_cost: Any,
34
+ past_total_monthly_cost: Any,
35
+ pr_title: str,
36
+ ) -> str:
37
+ diff = to_decimal(diff_total_monthly_cost)
38
+ if diff is None:
39
+ verdict = "Infracost returned a result, but the monthly delta was not available."
40
+ elif diff > 0:
41
+ verdict = f"This pull request increases estimated monthly infrastructure cost by {format_money(diff, signed=True)}."
42
+ elif diff < 0:
43
+ verdict = f"This pull request reduces estimated monthly infrastructure cost by {format_money(diff, signed=True)}."
44
+ else:
45
+ verdict = "This pull request has no estimated monthly infrastructure cost change."
46
+
47
+ return "\n".join(
48
+ [
49
+ CLOUDCOST_COMMENT_MARKER,
50
+ "## CloudCost AI estimate",
51
+ "",
52
+ verdict,
53
+ "",
54
+ "| Metric | Estimate |",
55
+ "| --- | ---: |",
56
+ f"| Previous monthly cost | {format_money(past_total_monthly_cost)} |",
57
+ f"| Proposed monthly cost | {format_money(total_monthly_cost)} |",
58
+ f"| Diff total monthly cost | {format_money(diff_total_monthly_cost, signed=True)} |",
59
+ "",
60
+ f"Terraform plan JSON: `{plan_path}`",
61
+ f"Pull request: `{pr_title}`",
62
+ "",
63
+ "This comment was generated from Terraform plan JSON via the Infracost Plan JSON API.",
64
+ ]
65
+ )
66
+
67
+
68
+ def format_no_plan_comment(pr_title: str) -> str:
69
+ return "\n".join(
70
+ [
71
+ CLOUDCOST_COMMENT_MARKER,
72
+ "## CloudCost AI estimate",
73
+ "",
74
+ "No Terraform plan JSON file was found in this pull request.",
75
+ "",
76
+ "Add a generated plan file such as `plan.json`, `tfplan.json`, or `terraform-plan.json` to let CloudCost AI calculate the monthly infrastructure cost delta.",
77
+ "",
78
+ f"Pull request: `{pr_title}`",
79
+ ]
80
+ )
81
+
82
+
83
+ def format_error_comment(pr_title: str, message: str) -> str:
84
+ return "\n".join(
85
+ [
86
+ CLOUDCOST_COMMENT_MARKER,
87
+ "## CloudCost AI estimate",
88
+ "",
89
+ "CloudCost AI could not complete the cost estimate for this pull request.",
90
+ "",
91
+ f"Reason: `{message}`",
92
+ f"Pull request: `{pr_title}`",
93
+ ]
94
+ )
backend/app/config.py ADDED
@@ -0,0 +1,191 @@
1
+ from functools import lru_cache
2
+ import json
3
+ from pathlib import Path
4
+ from shutil import which
5
+ from typing import Any
6
+
7
+ from pydantic import Field
8
+ from pydantic_settings import BaseSettings, SettingsConfigDict
9
+
10
+
11
+ class Settings(BaseSettings):
12
+ model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")
13
+
14
+ app_name: str = "CloudCost AI"
15
+ environment: str = "local"
16
+ public_base_url: str = "http://localhost:8000"
17
+ database_url: str | None = None
18
+ app_database_url: str | None = None
19
+
20
+ auth_secret: str | None = None
21
+ auth_session_cookie: str = "cloudcost_session"
22
+ auth_session_days: int = 14
23
+ auth_cookie_secure: bool = False
24
+ auth_otp_ttl_minutes: int = 10
25
+ auth_otp_max_attempts: int = 5
26
+ auth_dev_otp_log_path: str = "logs/auth-otp.log"
27
+
28
+ smtp_host: str | None = None
29
+ smtp_port: int = 587
30
+ smtp_username: str | None = None
31
+ smtp_password: str | None = None
32
+ smtp_from_email: str | None = None
33
+ smtp_use_tls: bool = True
34
+ resend_api_key: str | None = None
35
+ from_email: str | None = None
36
+ waitlist_confirmation_enabled: bool = True
37
+
38
+ github_api_url: str = "https://api.github.com"
39
+ github_api_version: str = "2026-03-10"
40
+ github_app_id: str | None = None
41
+ github_client_id: str | None = None
42
+ github_private_key: str | None = None
43
+ github_private_key_path: str | None = None
44
+ github_webhook_secret: str | None = None
45
+ github_manifest_config_path: str = "data/github-app-manifest.json"
46
+ comment_when_no_plan: bool = True
47
+
48
+ infracost_api_key: str | None = None
49
+ infracost_api_url: str = "https://pricing.api.infracost.io/breakdown"
50
+ infracost_ci_platform: str = "cloudcost-ai"
51
+ infracost_mode: str = "cli"
52
+ infracost_pricing_mode: str = "self_hosted"
53
+ infracost_cli_path: str = "infracost"
54
+ infracost_pricing_api_endpoint: str | None = None
55
+ infracost_timeout_seconds: int = 120
56
+
57
+ litellm_base_url: str = "http://127.0.0.1:4000"
58
+ litellm_master_key: str | None = None
59
+
60
+ usage_ingest_token: str | None = None
61
+ waitlist_path: str = "data/waitlist.jsonl"
62
+ usage_events_path: str = "data/usage-events.jsonl"
63
+
64
+ terraform_plan_names: str = Field(
65
+ default="plan.json,tfplan.json,terraform-plan.json,infracost-plan.json"
66
+ )
67
+ terraform_plan_suffixes: str = Field(default=".tfplan.json,.plan.json")
68
+
69
+ @property
70
+ def github_jwt_issuer(self) -> str:
71
+ manifest = self.load_github_manifest_config()
72
+ issuer = (
73
+ self.github_client_id
74
+ or self.github_app_id
75
+ or manifest.get("client_id")
76
+ or manifest.get("id")
77
+ )
78
+ if not issuer:
79
+ raise RuntimeError("Set GITHUB_CLIENT_ID or GITHUB_APP_ID for GitHub App auth.")
80
+ return str(issuer)
81
+
82
+ def load_github_manifest_config(self) -> dict[str, Any]:
83
+ path = Path(self.github_manifest_config_path)
84
+ if not path.exists():
85
+ return {}
86
+ try:
87
+ payload = json.loads(path.read_text(encoding="utf-8"))
88
+ except (OSError, json.JSONDecodeError):
89
+ return {}
90
+ return payload if isinstance(payload, dict) else {}
91
+
92
+ @property
93
+ def github_app_configured(self) -> bool:
94
+ manifest = self.load_github_manifest_config()
95
+ has_id = bool(
96
+ self.github_app_id
97
+ or self.github_client_id
98
+ or manifest.get("id")
99
+ or manifest.get("client_id")
100
+ )
101
+ has_key = bool(
102
+ self.github_private_key
103
+ or self.github_private_key_path
104
+ or manifest.get("private_key_path")
105
+ or manifest.get("pem")
106
+ )
107
+ has_secret = bool(self.github_webhook_secret or manifest.get("webhook_secret"))
108
+ return has_id and has_key and has_secret
109
+
110
+ @property
111
+ def backend_database_url(self) -> str | None:
112
+ return self.app_database_url or self.database_url
113
+
114
+ @property
115
+ def plan_names(self) -> set[str]:
116
+ return {item.strip().lower() for item in self.terraform_plan_names.split(",") if item.strip()}
117
+
118
+ @property
119
+ def plan_suffixes(self) -> tuple[str, ...]:
120
+ return tuple(
121
+ item.strip().lower() for item in self.terraform_plan_suffixes.split(",") if item.strip()
122
+ )
123
+
124
+ def load_github_private_key(self) -> str:
125
+ manifest = self.load_github_manifest_config()
126
+ if self.github_private_key:
127
+ return self.github_private_key.replace("\\n", "\n")
128
+ if self.github_private_key_path:
129
+ return Path(self.github_private_key_path).read_text(encoding="utf-8")
130
+ if manifest.get("private_key_path"):
131
+ return Path(str(manifest["private_key_path"])).read_text(encoding="utf-8")
132
+ if manifest.get("pem"):
133
+ return str(manifest["pem"]).replace("\\n", "\n")
134
+ raise RuntimeError("Set GITHUB_PRIVATE_KEY or GITHUB_PRIVATE_KEY_PATH.")
135
+
136
+ def require_github_webhook_secret(self) -> str:
137
+ manifest = self.load_github_manifest_config()
138
+ if not self.github_webhook_secret:
139
+ if manifest.get("webhook_secret"):
140
+ return str(manifest["webhook_secret"])
141
+ raise RuntimeError("Set GITHUB_WEBHOOK_SECRET.")
142
+ return self.github_webhook_secret
143
+
144
+ def require_manifest_state_secret(self) -> str:
145
+ secret = (
146
+ self.auth_secret
147
+ or self.litellm_master_key
148
+ or self.usage_ingest_token
149
+ or self.github_webhook_secret
150
+ )
151
+ if not secret:
152
+ raise RuntimeError("Set LITELLM_MASTER_KEY, USAGE_INGEST_TOKEN, or AUTH_SECRET.")
153
+ return secret
154
+
155
+ def require_infracost_api_key(self) -> str:
156
+ if not self.infracost_api_key:
157
+ raise RuntimeError("Set INFRACOST_API_KEY.")
158
+ return self.infracost_api_key
159
+
160
+ @property
161
+ def infracost_configured(self) -> bool:
162
+ mode = self.infracost_mode.lower()
163
+ if mode == "api":
164
+ return bool(self.infracost_api_key)
165
+ if mode == "cli":
166
+ if not self.infracost_cli_path:
167
+ return False
168
+ cli_path = Path(self.infracost_cli_path)
169
+ has_cli = cli_path.exists() or which(self.infracost_cli_path) is not None
170
+ if not has_cli:
171
+ return False
172
+ if self.infracost_pricing_mode.lower() == "cloudcost":
173
+ return bool(self.infracost_api_key and self.infracost_pricing_api_endpoint)
174
+ return bool(self.infracost_pricing_api_endpoint)
175
+ return False
176
+
177
+ def require_litellm_master_key(self) -> str:
178
+ if not self.litellm_master_key:
179
+ raise RuntimeError("Set LITELLM_MASTER_KEY.")
180
+ return self.litellm_master_key
181
+
182
+ def require_auth_secret(self) -> str:
183
+ secret = self.auth_secret or self.litellm_master_key or self.github_webhook_secret
184
+ if not secret:
185
+ raise RuntimeError("Set AUTH_SECRET or LITELLM_MASTER_KEY.")
186
+ return secret
187
+
188
+
189
+ @lru_cache
190
+ def get_settings() -> Settings:
191
+ return Settings()