feat: Add profitability calculation schemas and service functions
- Introduced Pydantic schemas for profitability calculations in `schemas/calculations.py`. - Implemented service functions for profitability calculations in `services/calculations.py`. - Added new exception class `ProfitabilityValidationError` for handling validation errors. - Created repositories for managing project and scenario profitability snapshots. - Developed a utility script for verifying authenticated routes. - Added a new HTML template for the profitability calculator interface. - Implemented a script to fix user ID sequence in the database.
This commit is contained in:
112
scripts/_route_verification.py
Normal file
112
scripts/_route_verification.py
Normal file
@@ -0,0 +1,112 @@
|
||||
"""Utility script to verify key authenticated routes respond without errors."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.parse
|
||||
from http.client import HTTPConnection
|
||||
from http.cookies import SimpleCookie
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
HOST = "127.0.0.1"
|
||||
PORT = 8000
|
||||
|
||||
cookies: Dict[str, str] = {}
|
||||
|
||||
|
||||
def _update_cookies(headers: List[Tuple[str, str]]) -> None:
|
||||
for name, value in headers:
|
||||
if name.lower() != "set-cookie":
|
||||
continue
|
||||
cookie = SimpleCookie()
|
||||
cookie.load(value)
|
||||
for key, morsel in cookie.items():
|
||||
cookies[key] = morsel.value
|
||||
|
||||
|
||||
def _cookie_header() -> str | None:
|
||||
if not cookies:
|
||||
return None
|
||||
return "; ".join(f"{key}={value}" for key, value in cookies.items())
|
||||
|
||||
|
||||
def request(method: str, path: str, *, body: bytes | None = None, headers: Dict[str, str] | None = None) -> Tuple[int, Dict[str, str], bytes]:
|
||||
conn = HTTPConnection(HOST, PORT, timeout=10)
|
||||
prepared_headers = {"User-Agent": "route-checker"}
|
||||
if headers:
|
||||
prepared_headers.update(headers)
|
||||
cookie_header = _cookie_header()
|
||||
if cookie_header:
|
||||
prepared_headers["Cookie"] = cookie_header
|
||||
|
||||
conn.request(method, path, body=body, headers=prepared_headers)
|
||||
resp = conn.getresponse()
|
||||
payload = resp.read()
|
||||
status = resp.status
|
||||
reason = resp.reason
|
||||
response_headers = {name: value for name, value in resp.getheaders()}
|
||||
_update_cookies(list(resp.getheaders()))
|
||||
conn.close()
|
||||
print(f"{method} {path} -> {status} {reason}")
|
||||
return status, response_headers, payload
|
||||
|
||||
|
||||
def main() -> int:
|
||||
status, _, _ = request("GET", "/login")
|
||||
if status != 200:
|
||||
print("Unexpected status for GET /login", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
admin_username = os.getenv("CALMINER_SEED_ADMIN_USERNAME", "admin")
|
||||
admin_password = os.getenv("CALMINER_SEED_ADMIN_PASSWORD", "M11ffpgm.")
|
||||
login_payload = urllib.parse.urlencode(
|
||||
{"username": admin_username, "password": admin_password}
|
||||
).encode()
|
||||
status, headers, _ = request(
|
||||
"POST",
|
||||
"/login",
|
||||
body=login_payload,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
)
|
||||
if status not in {200, 303}:
|
||||
print("Login failed", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
location = headers.get("Location", "/")
|
||||
redirect_path = urllib.parse.urlsplit(location).path or "/"
|
||||
request("GET", redirect_path)
|
||||
|
||||
request("GET", "/")
|
||||
request("GET", "/projects/ui")
|
||||
|
||||
status, headers, body = request(
|
||||
"GET",
|
||||
"/projects",
|
||||
headers={"Accept": "application/json"},
|
||||
)
|
||||
projects: List[dict] = []
|
||||
if headers.get("Content-Type", "").startswith("application/json"):
|
||||
projects = json.loads(body.decode())
|
||||
|
||||
if projects:
|
||||
project_id = projects[0]["id"]
|
||||
request("GET", f"/projects/{project_id}/view")
|
||||
status, headers, body = request(
|
||||
"GET",
|
||||
f"/projects/{project_id}/scenarios",
|
||||
headers={"Accept": "application/json"},
|
||||
)
|
||||
scenarios: List[dict] = []
|
||||
if headers.get("Content-Type", "").startswith("application/json"):
|
||||
scenarios = json.loads(body.decode())
|
||||
if scenarios:
|
||||
scenario_id = scenarios[0]["id"]
|
||||
request("GET", f"/scenarios/{scenario_id}/view")
|
||||
|
||||
print("Cookies:", cookies)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
15
scripts/apply_users_sequence_fix.py
Normal file
15
scripts/apply_users_sequence_fix.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from sqlalchemy import create_engine, text
|
||||
from config.database import DATABASE_URL
|
||||
|
||||
engine = create_engine(DATABASE_URL, future=True)
|
||||
sqls = [
|
||||
"CREATE SEQUENCE IF NOT EXISTS users_id_seq;",
|
||||
"ALTER TABLE users ALTER COLUMN id SET DEFAULT nextval('users_id_seq');",
|
||||
"SELECT setval('users_id_seq', COALESCE((SELECT MAX(id) FROM users), 1));",
|
||||
"ALTER SEQUENCE users_id_seq OWNED BY users.id;",
|
||||
]
|
||||
with engine.begin() as conn:
|
||||
for s in sqls:
|
||||
print('EXECUTING:', s)
|
||||
conn.execute(text(s))
|
||||
print('SEQUENCE fix applied')
|
||||
Reference in New Issue
Block a user