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.
- backend/__init__.py +1 -0
- backend/app/__init__.py +1 -0
- backend/app/auth.py +104 -0
- backend/app/cli.py +726 -0
- backend/app/comments.py +94 -0
- backend/app/config.py +191 -0
- backend/app/database.py +470 -0
- backend/app/emailer.py +157 -0
- backend/app/github_client.py +197 -0
- backend/app/infracost.py +129 -0
- backend/app/litellm_admin.py +41 -0
- backend/app/main.py +833 -0
- backend/app/model_pricing.py +80 -0
- backend/app/security.py +15 -0
- backend/app/storage.py +31 -0
- backend/app/usage.py +73 -0
- cloudcost_cli-0.1.0.dist-info/METADATA +340 -0
- cloudcost_cli-0.1.0.dist-info/RECORD +21 -0
- cloudcost_cli-0.1.0.dist-info/WHEEL +5 -0
- cloudcost_cli-0.1.0.dist-info/entry_points.txt +2 -0
- cloudcost_cli-0.1.0.dist-info/top_level.txt +1 -0
backend/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CloudCost AI backend package."""
|
backend/app/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Application modules for CloudCost AI."""
|
backend/app/auth.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import hashlib
|
|
3
|
+
import hmac
|
|
4
|
+
import re
|
|
5
|
+
import secrets
|
|
6
|
+
from datetime import UTC, datetime, timedelta
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
from backend.app.config import Settings
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
EMAIL_PATTERN = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
|
|
15
|
+
PASSWORD_ITERATIONS = 260_000
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SignupRequest(BaseModel):
|
|
19
|
+
email: str
|
|
20
|
+
password: str = Field(min_length=8, max_length=256)
|
|
21
|
+
full_name: str | None = Field(default=None, max_length=160)
|
|
22
|
+
company: str | None = Field(default=None, max_length=160)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class VerifySignupRequest(BaseModel):
|
|
26
|
+
email: str
|
|
27
|
+
code: str = Field(min_length=4, max_length=12)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ResendSignupOtpRequest(BaseModel):
|
|
31
|
+
email: str
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class LoginRequest(BaseModel):
|
|
35
|
+
email: str
|
|
36
|
+
password: str = Field(min_length=1, max_length=256)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def normalize_email(email: str) -> str:
|
|
40
|
+
normalized = email.strip().lower()
|
|
41
|
+
if not EMAIL_PATTERN.match(normalized):
|
|
42
|
+
raise ValueError("Enter a valid email address.")
|
|
43
|
+
return normalized
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def utc_now() -> datetime:
|
|
47
|
+
return datetime.now(tz=UTC)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def hash_password(password: str) -> str:
|
|
51
|
+
salt = secrets.token_bytes(16)
|
|
52
|
+
digest = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, PASSWORD_ITERATIONS)
|
|
53
|
+
return "pbkdf2_sha256${}${}${}".format(
|
|
54
|
+
PASSWORD_ITERATIONS,
|
|
55
|
+
base64.b64encode(salt).decode("ascii"),
|
|
56
|
+
base64.b64encode(digest).decode("ascii"),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def verify_password(password: str, password_hash: str | None) -> bool:
|
|
61
|
+
if not password_hash:
|
|
62
|
+
return False
|
|
63
|
+
try:
|
|
64
|
+
scheme, iterations, salt_b64, digest_b64 = password_hash.split("$", 3)
|
|
65
|
+
if scheme != "pbkdf2_sha256":
|
|
66
|
+
return False
|
|
67
|
+
expected = base64.b64decode(digest_b64)
|
|
68
|
+
salt = base64.b64decode(salt_b64)
|
|
69
|
+
actual = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, int(iterations))
|
|
70
|
+
return hmac.compare_digest(actual, expected)
|
|
71
|
+
except (ValueError, TypeError):
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def generate_otp_code() -> str:
|
|
76
|
+
return f"{secrets.randbelow(1_000_000):06d}"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def hash_otp_code(settings: Settings, email: str, code: str, purpose: str) -> str:
|
|
80
|
+
payload = f"{purpose}:{email}:{code}".encode("utf-8")
|
|
81
|
+
return hmac.new(settings.require_auth_secret().encode("utf-8"), payload, hashlib.sha256).hexdigest()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def new_session_token() -> str:
|
|
85
|
+
return secrets.token_urlsafe(48)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def hash_session_token(settings: Settings, token: str) -> str:
|
|
89
|
+
return hmac.new(settings.require_auth_secret().encode("utf-8"), token.encode("utf-8"), hashlib.sha256).hexdigest()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def session_expires_at(settings: Settings) -> datetime:
|
|
93
|
+
return utc_now() + timedelta(days=settings.auth_session_days)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def public_user(user: dict[str, Any]) -> dict[str, Any]:
|
|
97
|
+
return {
|
|
98
|
+
"id": user["id"],
|
|
99
|
+
"email": user["email"],
|
|
100
|
+
"full_name": user.get("full_name"),
|
|
101
|
+
"company": user.get("company"),
|
|
102
|
+
"email_verified": bool(user.get("email_verified_at")),
|
|
103
|
+
"created_at": user.get("created_at").isoformat() if user.get("created_at") else None,
|
|
104
|
+
}
|