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,80 @@
1
+ import json
2
+ from functools import lru_cache
3
+ from importlib.util import find_spec
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+
8
+ MODEL_ALIASES = {
9
+ "opus": {"label": "Claude Opus 4.7", "litellm_model": "claude-opus-4-7"},
10
+ "sonnet": {"label": "Claude Sonnet 4.6", "litellm_model": "claude-sonnet-4-6"},
11
+ "haiku": {"label": "Claude Haiku 4.5", "litellm_model": "claude-haiku-4-5"},
12
+ "gpt4o": {"label": "GPT-4o", "litellm_model": "gpt-4o"},
13
+ "gpt4omini": {"label": "GPT-4o mini", "litellm_model": "gpt-4o-mini"},
14
+ "gpt5mini": {"label": "GPT-5 mini", "litellm_model": "gpt-5-mini"},
15
+ "gemini": {"label": "Gemini 2.5 Pro", "litellm_model": "gemini/gemini-2.5-pro"},
16
+ }
17
+
18
+ FALLBACK_MODEL_RATES = {
19
+ "opus": {"label": "Claude Opus 4.7", "input_per_1m": 5.00, "output_per_1m": 25.00},
20
+ "sonnet": {"label": "Claude Sonnet 4.6", "input_per_1m": 3.00, "output_per_1m": 15.00},
21
+ "haiku": {"label": "Claude Haiku 4.5", "input_per_1m": 1.00, "output_per_1m": 5.00},
22
+ "gpt4o": {"label": "GPT-4o", "input_per_1m": 2.50, "output_per_1m": 10.00},
23
+ "gpt4omini": {"label": "GPT-4o mini", "input_per_1m": 0.15, "output_per_1m": 0.60},
24
+ "gpt5mini": {"label": "GPT-5 mini", "input_per_1m": 0.25, "output_per_1m": 2.00},
25
+ "gemini": {"label": "Gemini 2.5 Pro", "input_per_1m": 1.25, "output_per_1m": 10.00},
26
+ }
27
+
28
+
29
+ def _litellm_pricing_path() -> Path | None:
30
+ spec = find_spec("litellm")
31
+ if not spec or not spec.submodule_search_locations:
32
+ return None
33
+ package_dir = Path(next(iter(spec.submodule_search_locations)))
34
+ for filename in (
35
+ "model_prices_and_context_window.json",
36
+ "model_prices_and_context_window_backup.json",
37
+ ):
38
+ candidate = package_dir / filename
39
+ if candidate.exists():
40
+ return candidate
41
+ return None
42
+
43
+
44
+ @lru_cache
45
+ def load_litellm_price_map() -> tuple[dict[str, Any], str | None]:
46
+ path = _litellm_pricing_path()
47
+ if not path:
48
+ return {}, None
49
+ try:
50
+ payload = json.loads(path.read_text(encoding="utf-8"))
51
+ except (OSError, json.JSONDecodeError):
52
+ return {}, None
53
+ return payload if isinstance(payload, dict) else {}, str(path)
54
+
55
+
56
+ def _per_million(value: Any) -> float | None:
57
+ if value is None:
58
+ return None
59
+ return round(float(value) * 1_000_000, 6)
60
+
61
+
62
+ def get_model_rates() -> tuple[dict[str, dict[str, Any]], str]:
63
+ price_map, source_path = load_litellm_price_map()
64
+ rates: dict[str, dict[str, Any]] = {}
65
+
66
+ for alias, meta in MODEL_ALIASES.items():
67
+ fallback = FALLBACK_MODEL_RATES[alias]
68
+ model_key = meta["litellm_model"]
69
+ local = price_map.get(model_key, {})
70
+ input_per_1m = _per_million(local.get("input_cost_per_token"))
71
+ output_per_1m = _per_million(local.get("output_cost_per_token"))
72
+ rates[alias] = {
73
+ "label": meta["label"],
74
+ "litellm_model": model_key,
75
+ "input_per_1m": input_per_1m if input_per_1m is not None else fallback["input_per_1m"],
76
+ "output_per_1m": output_per_1m if output_per_1m is not None else fallback["output_per_1m"],
77
+ }
78
+
79
+ source = "litellm_local" if source_path else "fallback"
80
+ return rates, source
@@ -0,0 +1,15 @@
1
+ import hashlib
2
+ import hmac
3
+
4
+
5
+ def verify_github_signature(payload_body: bytes, secret_token: str, signature_header: str | None) -> bool:
6
+ if not signature_header:
7
+ return False
8
+
9
+ expected_signature = "sha256=" + hmac.new(
10
+ secret_token.encode("utf-8"),
11
+ msg=payload_body,
12
+ digestmod=hashlib.sha256,
13
+ ).hexdigest()
14
+
15
+ return hmac.compare_digest(expected_signature, signature_header)
backend/app/storage.py ADDED
@@ -0,0 +1,31 @@
1
+ import json
2
+ from datetime import datetime, timezone
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+
7
+ def append_jsonl(path: str, record: dict[str, Any]) -> None:
8
+ target = Path(path)
9
+ target.parent.mkdir(parents=True, exist_ok=True)
10
+ payload = {
11
+ "recorded_at": datetime.now(timezone.utc).isoformat(),
12
+ **record,
13
+ }
14
+ with target.open("a", encoding="utf-8") as handle:
15
+ handle.write(json.dumps(payload, sort_keys=True, separators=(",", ":")) + "\n")
16
+
17
+
18
+ def read_jsonl(path: str, limit: int = 1000) -> list[dict[str, Any]]:
19
+ target = Path(path)
20
+ if not target.exists():
21
+ return []
22
+
23
+ rows: list[dict[str, Any]] = []
24
+ with target.open("r", encoding="utf-8") as handle:
25
+ for line in handle:
26
+ line = line.strip()
27
+ if not line:
28
+ continue
29
+ rows.append(json.loads(line))
30
+
31
+ return rows[-limit:]
backend/app/usage.py ADDED
@@ -0,0 +1,73 @@
1
+ from collections import defaultdict
2
+ from typing import Any
3
+
4
+ from pydantic import BaseModel, Field
5
+
6
+
7
+ FORBIDDEN_PAYLOAD_KEYS = {
8
+ "messages",
9
+ "prompt",
10
+ "input",
11
+ "output",
12
+ "response",
13
+ "completion",
14
+ "choices",
15
+ }
16
+
17
+
18
+ class UsageEvent(BaseModel):
19
+ event_type: str = "success"
20
+ model: str | None = None
21
+ user: str | None = None
22
+ key_alias: str | None = None
23
+ team_id: str | None = None
24
+ metadata: dict[str, Any] = Field(default_factory=dict)
25
+ usage: dict[str, Any] = Field(default_factory=dict)
26
+ cost: float | None = None
27
+ started_at: str | None = None
28
+ ended_at: str | None = None
29
+
30
+
31
+ def _sanitize_value(value: Any) -> Any:
32
+ if isinstance(value, dict):
33
+ return {
34
+ key: _sanitize_value(inner)
35
+ for key, inner in value.items()
36
+ if str(key).lower() not in FORBIDDEN_PAYLOAD_KEYS
37
+ }
38
+ if isinstance(value, list):
39
+ return [_sanitize_value(item) for item in value]
40
+ return value
41
+
42
+
43
+ def sanitize_usage_payload(payload: dict[str, Any]) -> dict[str, Any]:
44
+ return _sanitize_value(payload)
45
+
46
+
47
+ def summarize_usage(events: list[dict[str, Any]]) -> dict[str, Any]:
48
+ by_model: dict[str, dict[str, float]] = defaultdict(lambda: {"cost": 0.0, "tokens": 0.0})
49
+ by_user: dict[str, dict[str, float]] = defaultdict(lambda: {"cost": 0.0, "tokens": 0.0})
50
+ total_cost = 0.0
51
+ total_tokens = 0.0
52
+
53
+ for event in events:
54
+ cost = float(event.get("cost") or 0.0)
55
+ usage = event.get("usage") or {}
56
+ tokens = float(usage.get("total_tokens") or 0.0)
57
+ model = event.get("model") or "unknown"
58
+ user = event.get("user") or "unknown"
59
+
60
+ total_cost += cost
61
+ total_tokens += tokens
62
+ by_model[model]["cost"] += cost
63
+ by_model[model]["tokens"] += tokens
64
+ by_user[user]["cost"] += cost
65
+ by_user[user]["tokens"] += tokens
66
+
67
+ return {
68
+ "events": len(events),
69
+ "total_cost": round(total_cost, 8),
70
+ "total_tokens": int(total_tokens),
71
+ "by_model": dict(by_model),
72
+ "by_user": dict(by_user),
73
+ }
@@ -0,0 +1,340 @@
1
+ Metadata-Version: 2.4
2
+ Name: cloudcost-cli
3
+ Version: 0.1.0
4
+ Summary: CloudCost CLI for Terraform cost estimates and GitHub pull request cost automation.
5
+ Author: CloudCost AI
6
+ Project-URL: Homepage, https://cloudcost.live
7
+ Project-URL: Documentation, https://cloudcost.live/docs/cloudcost-cli.html
8
+ Project-URL: Source, https://github.com/zedxod/cloudcost
9
+ Keywords: finops,terraform,infracost,github,cloud-cost,llm-cost
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Software Development :: Build Tools
17
+ Classifier: Topic :: System :: Systems Administration
18
+ Requires-Python: >=3.11
19
+ Description-Content-Type: text/markdown
20
+ Requires-Dist: httpx>=0.27.0
21
+ Requires-Dist: pydantic-settings>=2.5.0
22
+ Provides-Extra: server
23
+ Requires-Dist: fastapi>=0.115.0; extra == "server"
24
+ Requires-Dist: litellm[proxy]>=1.85.0; extra == "server"
25
+ Requires-Dist: prisma>=0.15.0; extra == "server"
26
+ Requires-Dist: psycopg[binary]>=3.2.0; extra == "server"
27
+ Requires-Dist: PyJWT[crypto]>=2.9.0; extra == "server"
28
+ Requires-Dist: python-multipart>=0.0.12; extra == "server"
29
+ Requires-Dist: uvicorn[standard]>=0.30.0; extra == "server"
30
+ Provides-Extra: dev
31
+ Requires-Dist: build>=1.2.0; extra == "dev"
32
+ Requires-Dist: fastapi>=0.115.0; extra == "dev"
33
+ Requires-Dist: litellm[proxy]>=1.85.0; extra == "dev"
34
+ Requires-Dist: prisma>=0.15.0; extra == "dev"
35
+ Requires-Dist: psycopg[binary]>=3.2.0; extra == "dev"
36
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
37
+ Requires-Dist: PyJWT[crypto]>=2.9.0; extra == "dev"
38
+ Requires-Dist: python-multipart>=0.0.12; extra == "dev"
39
+ Requires-Dist: twine>=5.0.0; extra == "dev"
40
+ Requires-Dist: uvicorn[standard]>=0.30.0; extra == "dev"
41
+
42
+ # CloudCost AI
43
+
44
+ CloudCost AI is a developer-first FinOps MVP. It has two mechanisms:
45
+
46
+ 1. An AI spend proxy powered by LiteLLM.
47
+ 2. A GitHub App webhook that estimates Terraform cost deltas in pull requests with Infracost.
48
+
49
+ The landing page is served from `index.html`; the working backend is in `backend/app`.
50
+
51
+ ## Get Started
52
+
53
+ The first-run path is modeled around a local CLI estimate, then GitHub automation:
54
+
55
+ ```powershell
56
+ .\.venv\Scripts\python -m pip install -e .
57
+ cloudcost go
58
+ ```
59
+
60
+ Run `cloudcost go` from a Terraform project directory. It runs the local estimate, then opens the GitHub App manifest install flow with the required webhook, events, and permissions already filled in. If a team only wants the local estimate, `cloudcost` still works by itself. Power users can also run `cloudcost setup`, `cloudcost doctor`, `cloudcost connect-github`, or `cloudcost analyze --plan tfplan.json` directly.
61
+
62
+ When the package is published, users should install it with:
63
+
64
+ ```bash
65
+ pipx install cloudcost-cli
66
+ cloudcost go
67
+ ```
68
+
69
+ Publishing notes are in `docs/publishing-pypi.md`.
70
+
71
+ The signed-in dashboard sidebar links to `/get-started`, a docs-style first-run guide with macOS, Windows, and Linux command tabs that update the CLI install, setup, Terraform, CI, and GitHub steps. The left docs rail links to styled HTML docs for CloudCost CLI, VPS install, and pricing API reference.
72
+
73
+ ## Cloudflare Pages Landing Page
74
+
75
+ For a free public early-access page, deploy the static pages plus the Pages Functions:
76
+
77
+ - `functions/api/waitlist.js`
78
+ - `functions/api/auth/*`
79
+ - `functions/api/dashboard/summary.js`
80
+ - `package.json`
81
+
82
+ Use `npm run build` and set the Cloudflare Pages output directory to `dist`; the build script copies `index.html`, `assets/`, `docs/`, `/login`, `/signup`, `/dashboard`, `/get-started`, and `/tester-dashboard`.
83
+
84
+ Set `DATABASE_URL`, `AUTH_SECRET`, `RESEND_API_KEY`, and `FROM_EMAIL` in Cloudflare Pages environment variables. The Functions store early-access emails, users, OTPs, and browser sessions in Neon, and send Resend confirmations without running the Python backend.
85
+
86
+ See `docs/cloudflare-pages-waitlist.md`.
87
+
88
+ ## One-Click VPS Path
89
+
90
+ For a customer-facing install, use the VPS bundle instead of the local debug path.
91
+
92
+ Prerequisites:
93
+
94
+ - Ubuntu VPS with ports `80` and `443` open
95
+ - A domain pointed at the VPS
96
+ - Docker, or approval for the bootstrap script to install Docker
97
+ - A pricing mode:
98
+ - Recommended: CloudCost hosted pricing key
99
+ - Advanced: Infracost API key for syncing a self-hosted pricing database
100
+
101
+ Run:
102
+
103
+ ```bash
104
+ git clone YOUR_REPO_URL cloudcost-ai
105
+ cd cloudcost-ai
106
+ bash scripts/bootstrap-vps.sh
107
+ ```
108
+
109
+ The script generates local secrets, creates `.env`, starts Caddy HTTPS, backend, LiteLLM, and Postgres. If the customer chooses strict self-hosted pricing, it also initializes and runs the local Infracost pricing API. It then prints:
110
+
111
+ - Dashboard: `https://YOUR_DOMAIN/dashboard`
112
+ - Tester runbook: `https://YOUR_DOMAIN/tester-dashboard`
113
+ - GitHub install page: `https://YOUR_DOMAIN/install/github`
114
+ - Webhook URL: `https://YOUR_DOMAIN/api/github/webhook`
115
+
116
+ Open the GitHub install page and click **Install on GitHub**. GitHub creates a customer-owned GitHub App from CloudCost's manifest and redirects back with the App ID, webhook secret, and private key. CloudCost stores those on the VPS automatically.
117
+
118
+ After setup, run:
119
+
120
+ ```bash
121
+ bash scripts/doctor-vps.sh
122
+ ```
123
+
124
+ This path is the intended customer install path. The longer local setup below remains for development and debugging.
125
+
126
+ ## Architecture
127
+
128
+ ```text
129
+ Browser
130
+ -> FastAPI backend
131
+ -> static landing page
132
+ -> waitlist endpoint
133
+ -> GitHub webhook endpoint
134
+ -> LiteLLM admin wrapper
135
+ -> metadata-only usage ingest
136
+
137
+ LLM clients
138
+ -> LiteLLM proxy :4000
139
+ -> OpenAI / Anthropic / other providers
140
+ -> metadata-only callback
141
+ -> FastAPI /api/llm/usage
142
+
143
+ GitHub pull_request webhook
144
+ -> HMAC validation
145
+ -> installation access token
146
+ -> locate Terraform plan JSON
147
+ -> Infracost Plan JSON API
148
+ -> upsert PR comment
149
+ ```
150
+
151
+ ## Quick Start
152
+
153
+ 1. Create local environment config:
154
+
155
+ ```powershell
156
+ Copy-Item .env.example .env
157
+ ```
158
+
159
+ 2. Fill `.env` with:
160
+
161
+ - `DATABASE_URL`, preferably a pooled Neon Postgres URL with `sslmode=require`
162
+ - `GITHUB_APP_ID` or `GITHUB_CLIENT_ID`
163
+ - `GITHUB_PRIVATE_KEY_PATH`
164
+ - `GITHUB_WEBHOOK_SECRET`
165
+ - `INFRACOST_API_KEY`
166
+ - `LITELLM_MASTER_KEY`
167
+ - at least one provider key, such as `OPENAI_API_KEY`
168
+
169
+ Put the downloaded GitHub App private key at `secrets/github-app-private-key.pem`.
170
+ Docker Compose mounts `./secrets` into the backend container as `/app/secrets`.
171
+
172
+ `DATABASE_URL` is shared by LiteLLM and the backend. LiteLLM uses it for virtual keys and spend/auth state; the backend creates `cloudcost_waitlist`, `cloudcost_usage_events`, `cloudcost_users`, `cloudcost_email_otps`, and `cloudcost_sessions` tables for app data. Set `APP_DATABASE_URL` if you want those app tables in a separate database.
173
+
174
+ The waitlist endpoint accepts only an email address, stores one row per email, and uses `data/waitlist.jsonl` only when no Postgres URL is configured. Set `SMTP_HOST` and `SMTP_FROM_EMAIL` if you want waitlist signups to receive a confirmation email.
175
+
176
+ For app signups, set `AUTH_SECRET` to a long random value. New accounts verify a mail OTP before `/dashboard` creates a session.
177
+
178
+ 3. Start the stack:
179
+
180
+ ```powershell
181
+ docker compose up --build
182
+ ```
183
+
184
+ If Docker Desktop is not installed, run the backend and LiteLLM locally instead:
185
+
186
+ ```powershell
187
+ .\.venv\Scripts\python -m pip install -e ".[dev]"
188
+ .\.venv\Scripts\python -m pip install "litellm[proxy]" prisma
189
+ .\scripts\start-litellm-local.ps1
190
+ ```
191
+
192
+ In a second terminal:
193
+
194
+ ```powershell
195
+ .\.venv\Scripts\python -m uvicorn backend.app.main:app --reload --port 8000
196
+ ```
197
+
198
+ For the local no-Docker path, set `LITELLM_BASE_URL=http://127.0.0.1:4000` in `.env`.
199
+
200
+ 4. Open:
201
+
202
+ - Landing page: `http://localhost:8000`
203
+ - Sign in: `http://localhost:8000/login`
204
+ - Sign up: `http://localhost:8000/signup`
205
+ - Dashboard: `http://localhost:8000/dashboard`
206
+ - Get started: `http://localhost:8000/get-started`
207
+ - Tester runbook: `http://localhost:8000/tester-dashboard`
208
+ - Backend health: `http://localhost:8000/healthz`
209
+ - Mechanism map: `http://localhost:8000/api/mechanism`
210
+ - LiteLLM proxy: `http://localhost:4000`
211
+
212
+ ## GitHub App Setup
213
+
214
+ Register a GitHub App with:
215
+
216
+ - Webhook URL: `https://YOUR_DOMAIN/api/github/webhook`
217
+ - Webhook secret: same value as `GITHUB_WEBHOOK_SECRET`
218
+ - Subscribe to only `pull_request`
219
+ - Repository permissions:
220
+ - Contents: read
221
+ - Pull requests: read and write
222
+ - Issues: write
223
+ - Metadata: read
224
+
225
+ The backend handles `opened`, `reopened`, `synchronize`, and `ready_for_review` pull request actions.
226
+
227
+ ## Terraform Plan JSON
228
+
229
+ The MVP looks for one of these files in the pull request files first, then in the repository tree at the PR head SHA:
230
+
231
+ - `plan.json`
232
+ - `tfplan.json`
233
+ - `terraform-plan.json`
234
+ - `infracost-plan.json`
235
+ - files ending in `.tfplan.json` or `.plan.json`
236
+
237
+ Generate a compatible file with Terraform:
238
+
239
+ ```powershell
240
+ terraform init
241
+ terraform plan -out tfplan.binary
242
+ terraform show -json tfplan.binary > plan.json
243
+ ```
244
+
245
+ When found, the backend runs the local Infracost CLI against the plan JSON, parses `diffTotalMonthlyCost`, and creates or updates one CloudCost AI pull request comment. In the default self-hosted setup, the plan JSON stays inside your backend runtime; the CLI only talks to your Cloud Pricing API for price lookups.
246
+
247
+ ## Self-Hosted Infracost
248
+
249
+ The default project mode is:
250
+
251
+ ```env
252
+ INFRACOST_MODE=cli
253
+ INFRACOST_CLI_PATH=infracost
254
+ INFRACOST_PRICING_API_ENDPOINT=http://127.0.0.1:4001
255
+ ```
256
+
257
+ That means CloudCost AI does not call the hosted Infracost Plan JSON API. Instead:
258
+
259
+ 1. You self-host the Infracost Cloud Pricing API.
260
+ 2. The backend runs `infracost breakdown --path plan.json --format json`.
261
+ 3. The Infracost CLI requests prices from `INFRACOST_PRICING_API_ENDPOINT`.
262
+ 4. The backend comments the result on the pull request.
263
+
264
+ For local Windows development, install the Infracost CLI and make sure `infracost` is on PATH. For Docker builds, the backend image installs the CLI automatically.
265
+
266
+ See `/docs/pricing-api.html` for the full setup path.
267
+
268
+ ## LiteLLM Spend Proxy
269
+
270
+ LiteLLM requires `DATABASE_URL` for persistent virtual keys, budgets, spend logs, and auth state. With Neon, use the pooled connection string from the Neon dashboard:
271
+
272
+ ```env
273
+ DATABASE_URL=postgresql://USER:PASSWORD@HOST/neondb?sslmode=require&channel_binding=require
274
+ ```
275
+
276
+ Create a virtual key through CloudCost AI:
277
+
278
+ ```powershell
279
+ Invoke-RestMethod -Method Post http://localhost:8000/api/llm/keys `
280
+ -ContentType 'application/json' `
281
+ -Body '{
282
+ "user_id": "ada@example.com",
283
+ "team_id": "platform",
284
+ "models": ["gpt-5-mini"],
285
+ "max_budget": 10,
286
+ "budget_duration": "30d",
287
+ "metadata": {"service": "billing"}
288
+ }'
289
+ ```
290
+
291
+ Use the returned key against LiteLLM:
292
+
293
+ ```powershell
294
+ $env:OPENAI_API_KEY = "sk-returned-virtual-key"
295
+ ```
296
+
297
+ Point OpenAI-compatible clients at:
298
+
299
+ ```text
300
+ http://localhost:4000/v1
301
+ ```
302
+
303
+ On the VPS bundle, Caddy exposes LiteLLM through the same HTTPS origin:
304
+
305
+ ```text
306
+ https://YOUR_DOMAIN/llm/v1
307
+ ```
308
+
309
+ LiteLLM enforces the budgets and forwards metadata-only usage records to `/api/llm/usage`. The custom callback deliberately omits prompts, messages, responses, choices, and completions.
310
+
311
+ ## Local Development
312
+
313
+ Install dependencies:
314
+
315
+ ```powershell
316
+ python -m venv .venv
317
+ .\.venv\Scripts\Activate.ps1
318
+ python -m pip install -e ".[dev]"
319
+ ```
320
+
321
+ Run tests:
322
+
323
+ ```powershell
324
+ pytest
325
+ ```
326
+
327
+ Run the backend without Docker:
328
+
329
+ ```powershell
330
+ uvicorn backend.app.main:app --reload --port 8000
331
+ ```
332
+
333
+ ## Important Limits
334
+
335
+ This is an MVP scaffold, not a production billing system yet.
336
+
337
+ - Real GitHub, Infracost, and LLM provider credentials are required for end-to-end live calls.
338
+ - The JSONL stores in `data/` are for local validation; replace them with a database before production.
339
+ - For production GitHub traffic, put the backend behind HTTPS and keep the webhook secret/private key out of the repo.
340
+ - For production LiteLLM, pin image versions, rotate master keys, and use a managed Postgres database.
@@ -0,0 +1,21 @@
1
+ backend/__init__.py,sha256=N0SwZfj8doy0rALL59GNmGD2Mv_0oqS7C2Jb6MeZLdM,36
2
+ backend/app/__init__.py,sha256=WXhIwhrL14NlB8bjQG6RO3IM5HcX7JgOsxiGnwUnJ0E,44
3
+ backend/app/auth.py,sha256=Lb7tJwz40fKeWbfMS472nNhG-NOjjs82Ps_Va4I4veM,3074
4
+ backend/app/cli.py,sha256=uivTSbFj4q6rem5oUpvLDDuxBaoUNMsaP62oszaUCFQ,25830
5
+ backend/app/comments.py,sha256=sbynhhu4tGNgTIQpdRtWe8puh8sF4Ca5VnvPnO7_XQA,3004
6
+ backend/app/config.py,sha256=px2d0OQduvniWcnGUtTZ8LOb26KnkAVbCjQRsk2aIE4,7027
7
+ backend/app/database.py,sha256=qk-gCUye5rcng5qmNGH99Yx6b9Lak1kpBu3iR5MdczE,16083
8
+ backend/app/emailer.py,sha256=GmJ3aaHaH_oMdw_4LwPykopjaeEj3cXB_nK0ULEvILI,5713
9
+ backend/app/github_client.py,sha256=rf52xIorJYJuKToXbwZwem7zCW0rHCIi8rWFZHzKL0Q,7622
10
+ backend/app/infracost.py,sha256=87EFZEuAIJE6FWxoM_XqKotpuQymz_36m6IiQ6YDP5w,5143
11
+ backend/app/litellm_admin.py,sha256=w0NAi4q6IoNTRACSQUonYkwypvvymORaSWy4sJRxHV8,1327
12
+ backend/app/main.py,sha256=5YSlocT1aRG8mEcoPDfm-b-wd8YcJaIVmmrx_L51fOM,38538
13
+ backend/app/model_pricing.py,sha256=ENnaGg9VjYNaeWKmHE0QhOPEG88jznLBN-6bA408MXw,3187
14
+ backend/app/security.py,sha256=tXDGKvOpTiDVi-Lz6KLqiwFzx2W1KH_Yv4UKYEP0FxI,420
15
+ backend/app/storage.py,sha256=pMGrpuuDP0K8SfSEl5DorkXKmtDHFT5SS5mkgKg7le8,898
16
+ backend/app/usage.py,sha256=wCC5t73Jte_glefomRIHRf5w4jyVhe1vd4NB4XUGWMo,2090
17
+ cloudcost_cli-0.1.0.dist-info/METADATA,sha256=VSMcyIKoZq98-H5wvz-IOptjRP3VgUIowMXMMDij5Cs,12304
18
+ cloudcost_cli-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
19
+ cloudcost_cli-0.1.0.dist-info/entry_points.txt,sha256=lp30NDXoctR8I-f6Z-rorAEgyKR26j-aJVLfHNWLoa0,51
20
+ cloudcost_cli-0.1.0.dist-info/top_level.txt,sha256=o0r_qMzzBDDtK9APrv2QVcIw9oc00JRmCEsTE2-AFr0,8
21
+ cloudcost_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ cloudcost = backend.app.cli:main
@@ -0,0 +1 @@
1
+ backend