Spaces:
Sleeping
Sleeping
Upload 4 files
Browse files
main.py
CHANGED
|
@@ -1,25 +1,16 @@
|
|
| 1 |
# main.py
|
| 2 |
-
import
|
| 3 |
import pandas as pd
|
| 4 |
import numpy as np
|
| 5 |
import io, tempfile, textwrap
|
| 6 |
import statsmodels.api as sm
|
| 7 |
|
| 8 |
-
from fastapi import FastAPI, HTTPException, Body,
|
| 9 |
from fastapi.responses import FileResponse, PlainTextResponse, ORJSONResponse
|
| 10 |
from fastapi.concurrency import run_in_threadpool
|
| 11 |
from datetime import datetime
|
| 12 |
from typing import List, Dict, Any, Optional
|
| 13 |
from dataclasses import dataclass
|
| 14 |
-
from starlette.middleware.base import BaseHTTPMiddleware
|
| 15 |
-
import time
|
| 16 |
-
|
| 17 |
-
# ---------- Server-side config ----------
|
| 18 |
-
GITHUB_OWNER = "datfid-valeriidashuk"
|
| 19 |
-
GITHUB_REPO = "datfid-hf"
|
| 20 |
-
USAGE_PATH = "hf_usage.json" # <— add this
|
| 21 |
-
BRANCH = "main"
|
| 22 |
-
GITHUB_PAT = os.environ.get("Github_key") # must have Contents: Read & Write
|
| 23 |
|
| 24 |
# Predefined globals
|
| 25 |
stored_model = None
|
|
@@ -244,164 +235,6 @@ class DATFIDModel:
|
|
| 244 |
out["forecast"] = out[f"{self.y}_forecast"]
|
| 245 |
return out
|
| 246 |
|
| 247 |
-
def _gh_get(path: str):
|
| 248 |
-
url = f"https://api.github.com/repos/{GITHUB_OWNER}/{GITHUB_REPO}/contents/{path}?ref={BRANCH}"
|
| 249 |
-
headers = {
|
| 250 |
-
"Authorization": f"Bearer {GITHUB_PAT}",
|
| 251 |
-
"Accept": "application/vnd.github+json",
|
| 252 |
-
"X-GitHub-Api-Version": "2022-11-28",
|
| 253 |
-
}
|
| 254 |
-
r = requests.get(url, headers=headers, timeout=15)
|
| 255 |
-
if r.status_code == 404:
|
| 256 |
-
return None, None # file not found
|
| 257 |
-
if r.status_code != 200:
|
| 258 |
-
raise HTTPException(status_code=502, detail=f"GitHub GET {path} failed")
|
| 259 |
-
data = r.json()
|
| 260 |
-
content = base64.b64decode(data["content"]).decode("utf-8")
|
| 261 |
-
sha = data["sha"]
|
| 262 |
-
return content, sha
|
| 263 |
-
|
| 264 |
-
def _gh_put(path: str, text: str, *, sha: Optional[str], message: str):
|
| 265 |
-
url = f"https://api.github.com/repos/{GITHUB_OWNER}/{GITHUB_REPO}/contents/{path}"
|
| 266 |
-
headers = {
|
| 267 |
-
"Authorization": f"Bearer {GITHUB_PAT}",
|
| 268 |
-
"Accept": "application/vnd.github+json",
|
| 269 |
-
"X-GitHub-Api-Version": "2022-11-28",
|
| 270 |
-
}
|
| 271 |
-
payload = {
|
| 272 |
-
"message": message,
|
| 273 |
-
"content": base64.b64encode(text.encode("utf-8")).decode("utf-8"),
|
| 274 |
-
"branch": BRANCH,
|
| 275 |
-
}
|
| 276 |
-
if sha:
|
| 277 |
-
payload["sha"] = sha
|
| 278 |
-
r = requests.put(url, headers=headers, json=payload, timeout=20)
|
| 279 |
-
if r.status_code in (200, 201):
|
| 280 |
-
return r.json()["content"]["sha"]
|
| 281 |
-
# surface conflict so we can retry
|
| 282 |
-
if r.status_code in (409, 422):
|
| 283 |
-
raise RuntimeError("GITHUB_CONFLICT")
|
| 284 |
-
raise HTTPException(status_code=502, detail=f"GitHub PUT {path} failed")
|
| 285 |
-
|
| 286 |
-
def _today() -> str:
|
| 287 |
-
"""Return current date as YYYY-MM-DD."""
|
| 288 |
-
return datetime.utcnow().strftime("%Y-%m-%d")
|
| 289 |
-
|
| 290 |
-
def _bucket(nbytes: Optional[int]) -> str:
|
| 291 |
-
"""Categorize byte size into buckets."""
|
| 292 |
-
if not isinstance(nbytes, int):
|
| 293 |
-
return "<1MB"
|
| 294 |
-
if nbytes < 1_000_000:
|
| 295 |
-
return "<1MB"
|
| 296 |
-
if nbytes < 10_000_000:
|
| 297 |
-
return "1-10MB"
|
| 298 |
-
return ">10MB"
|
| 299 |
-
|
| 300 |
-
def _new_day_bucket():
|
| 301 |
-
"""Create a new day bucket with all metrics initialized."""
|
| 302 |
-
return {
|
| 303 |
-
"calls": 0,
|
| 304 |
-
"ok_2xx": 0,
|
| 305 |
-
"client_4xx": 0,
|
| 306 |
-
"server_5xx": 0,
|
| 307 |
-
"total_duration_ms": 0,
|
| 308 |
-
"req_size_bucket": {"<1MB": 0, "1-10MB": 0, ">10MB": 0},
|
| 309 |
-
"resp_size_bucket": {"<1MB": 0, "1-10MB": 0, ">10MB": 0},
|
| 310 |
-
}
|
| 311 |
-
|
| 312 |
-
def bump_usage(user_id: str, endpoint: str, max_retries: int = 5,
|
| 313 |
-
status_code: Optional[int] = None, duration_ms: Optional[int] = None,
|
| 314 |
-
req_bytes: Optional[int] = None, resp_bytes: Optional[int] = None):
|
| 315 |
-
"""
|
| 316 |
-
Increment usage counters in hf_usage.json with optimistic concurrency.
|
| 317 |
-
Now supports extended metrics: status codes, duration, request/response sizes.
|
| 318 |
-
"""
|
| 319 |
-
yyyymm = datetime.utcnow().strftime("%Y-%m")
|
| 320 |
-
yyyymmdd = _today()
|
| 321 |
-
|
| 322 |
-
attempt = 0
|
| 323 |
-
while True:
|
| 324 |
-
attempt += 1
|
| 325 |
-
# read current usage (or create)
|
| 326 |
-
txt, sha = _gh_get(USAGE_PATH)
|
| 327 |
-
if txt is None:
|
| 328 |
-
usage = {}
|
| 329 |
-
sha = None
|
| 330 |
-
else:
|
| 331 |
-
try:
|
| 332 |
-
usage = json.loads(txt) if txt.strip() else {}
|
| 333 |
-
except Exception:
|
| 334 |
-
# corrupt file -> start fresh but do not lose the old SHA reference
|
| 335 |
-
usage = {}
|
| 336 |
-
|
| 337 |
-
# Get or create user record
|
| 338 |
-
rec = usage.get(user_id) or {
|
| 339 |
-
"total": 0,
|
| 340 |
-
"by_month": {},
|
| 341 |
-
"by_endpoint": {},
|
| 342 |
-
"by_endpoint_month": {},
|
| 343 |
-
"updated_at": None,
|
| 344 |
-
}
|
| 345 |
-
|
| 346 |
-
# Initialize endpoints structure if not present
|
| 347 |
-
if "endpoints" not in rec:
|
| 348 |
-
rec["endpoints"] = {}
|
| 349 |
-
|
| 350 |
-
# Get or create endpoint record
|
| 351 |
-
ep_rec = rec["endpoints"].get(endpoint) or {
|
| 352 |
-
"total": 0,
|
| 353 |
-
}
|
| 354 |
-
|
| 355 |
-
# Get or create day bucket for this endpoint
|
| 356 |
-
if yyyymmdd not in ep_rec:
|
| 357 |
-
ep_rec[yyyymmdd] = _new_day_bucket()
|
| 358 |
-
|
| 359 |
-
day_bucket = ep_rec[yyyymmdd]
|
| 360 |
-
|
| 361 |
-
# Increment basic counters
|
| 362 |
-
rec["total"] = int(rec.get("total", 0)) + 1
|
| 363 |
-
rec["by_month"][yyyymm] = int(rec["by_month"].get(yyyymm, 0)) + 1
|
| 364 |
-
rec["by_endpoint"][endpoint] = int(rec["by_endpoint"].get(endpoint, 0)) + 1
|
| 365 |
-
key_ep_month = f"{yyyymm}:{endpoint}"
|
| 366 |
-
rec["by_endpoint_month"][key_ep_month] = int(rec["by_endpoint_month"].get(key_ep_month, 0)) + 1
|
| 367 |
-
rec["updated_at"] = datetime.utcnow().isoformat() + "Z"
|
| 368 |
-
|
| 369 |
-
# Increment endpoint-level counters
|
| 370 |
-
ep_rec["total"] = int(ep_rec.get("total", 0)) + 1
|
| 371 |
-
|
| 372 |
-
# Increment extended metrics if provided
|
| 373 |
-
day_bucket["calls"] += 1
|
| 374 |
-
if status_code is not None:
|
| 375 |
-
if 200 <= status_code < 300:
|
| 376 |
-
day_bucket["ok_2xx"] += 1
|
| 377 |
-
elif 400 <= status_code < 500:
|
| 378 |
-
day_bucket["client_4xx"] += 1
|
| 379 |
-
elif 500 <= status_code < 600:
|
| 380 |
-
day_bucket["server_5xx"] += 1
|
| 381 |
-
if duration_ms is not None:
|
| 382 |
-
day_bucket["total_duration_ms"] += duration_ms
|
| 383 |
-
if req_bytes is not None:
|
| 384 |
-
day_bucket["req_size_bucket"][_bucket(req_bytes)] += 1
|
| 385 |
-
if resp_bytes is not None:
|
| 386 |
-
day_bucket["resp_size_bucket"][_bucket(resp_bytes)] += 1
|
| 387 |
-
|
| 388 |
-
# Update nested structures
|
| 389 |
-
ep_rec[yyyymmdd] = day_bucket
|
| 390 |
-
rec["endpoints"][endpoint] = ep_rec
|
| 391 |
-
usage[user_id] = rec
|
| 392 |
-
|
| 393 |
-
new_txt = json.dumps(usage, ensure_ascii = False, indent = 2, sort_keys = True) + "\n"
|
| 394 |
-
try:
|
| 395 |
-
_gh_put(USAGE_PATH, new_txt, sha=sha, message=f"usage: +1 {user_id} {endpoint} {yyyymmdd}")
|
| 396 |
-
return
|
| 397 |
-
except RuntimeError as e:
|
| 398 |
-
# conflict -> backoff & retry
|
| 399 |
-
if str(e) == "GITHUB_CONFLICT" and attempt < max_retries:
|
| 400 |
-
import time as _t
|
| 401 |
-
_t.sleep(0.25 * attempt)
|
| 402 |
-
continue
|
| 403 |
-
raise
|
| 404 |
-
|
| 405 |
def _maybe_json_list(s: Optional[str]):
|
| 406 |
if s is None or s == "":
|
| 407 |
return []
|
|
@@ -572,79 +405,12 @@ def _result_to_text(result_obj: Any) -> str:
|
|
| 572 |
# ---------- FastAPI app ----------
|
| 573 |
app = FastAPI(
|
| 574 |
title="DATFID API",
|
| 575 |
-
description="Public demo API
|
| 576 |
docs_url="/docs",
|
| 577 |
redoc_url=None,
|
| 578 |
default_response_class=ORJSONResponse,
|
| 579 |
)
|
| 580 |
|
| 581 |
-
# ---------- Metrics Middleware ----------
|
| 582 |
-
class UsageMetricsMiddleware(BaseHTTPMiddleware):
|
| 583 |
-
"""Middleware to capture extended usage metrics and call bump_usage with full context."""
|
| 584 |
-
|
| 585 |
-
async def dispatch(self, request: Request, call_next):
|
| 586 |
-
# Capture start time
|
| 587 |
-
t0 = time.time()
|
| 588 |
-
|
| 589 |
-
# Get request size
|
| 590 |
-
req_bytes = None
|
| 591 |
-
try:
|
| 592 |
-
cl = request.headers.get("content-length")
|
| 593 |
-
if cl:
|
| 594 |
-
req_bytes = int(cl)
|
| 595 |
-
except Exception:
|
| 596 |
-
pass
|
| 597 |
-
|
| 598 |
-
# Process request (this will execute dependencies and handlers)
|
| 599 |
-
response = await call_next(request)
|
| 600 |
-
|
| 601 |
-
# After handler execution, check if this request was metered
|
| 602 |
-
# (dependencies set request.state during handler execution)
|
| 603 |
-
has_metered = hasattr(request.state, "metered_user_id") and hasattr(request.state, "metered_endpoint")
|
| 604 |
-
|
| 605 |
-
if not has_metered:
|
| 606 |
-
# Not a metered endpoint, just return response
|
| 607 |
-
return response
|
| 608 |
-
|
| 609 |
-
# Calculate duration
|
| 610 |
-
duration_ms = int((time.time() - t0) * 1000)
|
| 611 |
-
|
| 612 |
-
# Get response size
|
| 613 |
-
resp_bytes = None
|
| 614 |
-
try:
|
| 615 |
-
rcl = response.headers.get("content-length")
|
| 616 |
-
if rcl:
|
| 617 |
-
resp_bytes = int(rcl)
|
| 618 |
-
except Exception:
|
| 619 |
-
pass
|
| 620 |
-
|
| 621 |
-
# Get status code
|
| 622 |
-
status_code = getattr(response, "status_code", None)
|
| 623 |
-
|
| 624 |
-
# Call bump_usage with extended metrics (non-blocking)
|
| 625 |
-
# In fully public mode without Github_key, skip persistence silently.
|
| 626 |
-
if not GITHUB_PAT:
|
| 627 |
-
return response
|
| 628 |
-
try:
|
| 629 |
-
user_id = request.state.metered_user_id
|
| 630 |
-
endpoint = request.state.metered_endpoint
|
| 631 |
-
bump_usage(
|
| 632 |
-
user_id=user_id,
|
| 633 |
-
endpoint=endpoint,
|
| 634 |
-
status_code=status_code,
|
| 635 |
-
duration_ms=duration_ms,
|
| 636 |
-
req_bytes=req_bytes,
|
| 637 |
-
resp_bytes=resp_bytes,
|
| 638 |
-
)
|
| 639 |
-
except Exception:
|
| 640 |
-
# Never break the request due to metrics failure
|
| 641 |
-
traceback.print_exc()
|
| 642 |
-
|
| 643 |
-
return response
|
| 644 |
-
|
| 645 |
-
# Register middleware
|
| 646 |
-
app.add_middleware(UsageMetricsMiddleware)
|
| 647 |
-
|
| 648 |
# In-memory model state
|
| 649 |
@dataclass
|
| 650 |
class ModelSlot:
|
|
@@ -665,24 +431,9 @@ def root():
|
|
| 665 |
def secure_ping():
|
| 666 |
return {"ok": True}
|
| 667 |
|
| 668 |
-
# Reusable dependency for protected routes
|
| 669 |
-
def require_valid_token(x_api_key: Optional[str] = Header(None)) -> str:
|
| 670 |
-
return x_api_key or ""
|
| 671 |
-
|
| 672 |
-
def metered(endpoint_name: str):
|
| 673 |
-
"""Public dependency: no auth, keeps endpoint metrics labels."""
|
| 674 |
-
async def _dep(request: Request, x_api_key: Optional[str] = Header(None)) -> str:
|
| 675 |
-
user_id = "public"
|
| 676 |
-
# Store in request state for middleware to use
|
| 677 |
-
request.state.metered_user_id = user_id
|
| 678 |
-
request.state.metered_endpoint = endpoint_name
|
| 679 |
-
return x_api_key or ""
|
| 680 |
-
return _dep
|
| 681 |
-
|
| 682 |
# ---------- Model endpoints (guarded) ----------
|
| 683 |
@app.post("/modelfit/")
|
| 684 |
async def modelfit(
|
| 685 |
-
_: str = Depends(metered("modelfit")),
|
| 686 |
df: List[Dict] = Body(...),
|
| 687 |
id_col: str = Body(...),
|
| 688 |
time_col: str = Body(...),
|
|
@@ -738,7 +489,6 @@ async def modelfit(
|
|
| 738 |
|
| 739 |
@app.post("/modelforecast/")
|
| 740 |
async def modelforecast(
|
| 741 |
-
_: str = Depends(metered("modelforecast")),
|
| 742 |
df_forecast: List[Dict] = Body(...),
|
| 743 |
):
|
| 744 |
global stored_model, stored_result_join
|
|
@@ -754,7 +504,6 @@ async def modelforecast(
|
|
| 754 |
|
| 755 |
@app.post("/modelfit-file/")
|
| 756 |
async def modelfit_file(
|
| 757 |
-
_: str = Depends(metered("modelfit-file")),
|
| 758 |
file: UploadFile = File(...),
|
| 759 |
id_col: str = Form(...),
|
| 760 |
time_col: str = Form(...),
|
|
@@ -823,7 +572,6 @@ async def modelfit_file(
|
|
| 823 |
|
| 824 |
@app.post("/modelforecast-file/")
|
| 825 |
async def modelforecast_file(
|
| 826 |
-
_: str = Depends(metered("modelforecast-file")),
|
| 827 |
df_forecast: UploadFile = File(...),
|
| 828 |
):
|
| 829 |
global stored_model, stored_result_join
|
|
@@ -870,7 +618,6 @@ async def modelforecast_file(
|
|
| 870 |
|
| 871 |
@app.post("/modelfit_ind/")
|
| 872 |
async def modelfit_ind(
|
| 873 |
-
_: str = Depends(metered("modelfit_ind")),
|
| 874 |
df: List[Dict] = Body(...),
|
| 875 |
id_col: str = Body(...),
|
| 876 |
time_col: str = Body(...),
|
|
@@ -936,7 +683,6 @@ async def modelfit_ind(
|
|
| 936 |
|
| 937 |
@app.post("/modelforecast_ind/")
|
| 938 |
async def modelforecast_ind(
|
| 939 |
-
_: str = Depends(metered("modelforecast_ind")),
|
| 940 |
df_forecast: List[Dict] = Body(...),
|
| 941 |
):
|
| 942 |
# Use per-ID models created by /modelfit_ind/
|
|
@@ -986,7 +732,6 @@ async def modelforecast_ind(
|
|
| 986 |
|
| 987 |
@app.post("/modelfit-file_ind/")
|
| 988 |
async def modelfit_file_ind(
|
| 989 |
-
_: str = Depends(metered("modelfit-file_ind")),
|
| 990 |
file: UploadFile = File(...),
|
| 991 |
id_col: str = Form(...),
|
| 992 |
time_col: str = Form(...),
|
|
@@ -1066,7 +811,6 @@ async def modelfit_file_ind(
|
|
| 1066 |
|
| 1067 |
@app.post("/modelforecast-file_ind/")
|
| 1068 |
async def modelforecast_file_ind(
|
| 1069 |
-
_: str = Depends(metered("modelforecast-file_ind")),
|
| 1070 |
df_forecast: UploadFile = File(...),
|
| 1071 |
):
|
| 1072 |
global model_store
|
|
|
|
| 1 |
# main.py
|
| 2 |
+
import json, traceback
|
| 3 |
import pandas as pd
|
| 4 |
import numpy as np
|
| 5 |
import io, tempfile, textwrap
|
| 6 |
import statsmodels.api as sm
|
| 7 |
|
| 8 |
+
from fastapi import FastAPI, HTTPException, Body, UploadFile, File, Form, Response, status, Request
|
| 9 |
from fastapi.responses import FileResponse, PlainTextResponse, ORJSONResponse
|
| 10 |
from fastapi.concurrency import run_in_threadpool
|
| 11 |
from datetime import datetime
|
| 12 |
from typing import List, Dict, Any, Optional
|
| 13 |
from dataclasses import dataclass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
# Predefined globals
|
| 16 |
stored_model = None
|
|
|
|
| 235 |
out["forecast"] = out[f"{self.y}_forecast"]
|
| 236 |
return out
|
| 237 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
def _maybe_json_list(s: Optional[str]):
|
| 239 |
if s is None or s == "":
|
| 240 |
return []
|
|
|
|
| 405 |
# ---------- FastAPI app ----------
|
| 406 |
app = FastAPI(
|
| 407 |
title="DATFID API",
|
| 408 |
+
description="Public demo API",
|
| 409 |
docs_url="/docs",
|
| 410 |
redoc_url=None,
|
| 411 |
default_response_class=ORJSONResponse,
|
| 412 |
)
|
| 413 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 414 |
# In-memory model state
|
| 415 |
@dataclass
|
| 416 |
class ModelSlot:
|
|
|
|
| 431 |
def secure_ping():
|
| 432 |
return {"ok": True}
|
| 433 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 434 |
# ---------- Model endpoints (guarded) ----------
|
| 435 |
@app.post("/modelfit/")
|
| 436 |
async def modelfit(
|
|
|
|
| 437 |
df: List[Dict] = Body(...),
|
| 438 |
id_col: str = Body(...),
|
| 439 |
time_col: str = Body(...),
|
|
|
|
| 489 |
|
| 490 |
@app.post("/modelforecast/")
|
| 491 |
async def modelforecast(
|
|
|
|
| 492 |
df_forecast: List[Dict] = Body(...),
|
| 493 |
):
|
| 494 |
global stored_model, stored_result_join
|
|
|
|
| 504 |
|
| 505 |
@app.post("/modelfit-file/")
|
| 506 |
async def modelfit_file(
|
|
|
|
| 507 |
file: UploadFile = File(...),
|
| 508 |
id_col: str = Form(...),
|
| 509 |
time_col: str = Form(...),
|
|
|
|
| 572 |
|
| 573 |
@app.post("/modelforecast-file/")
|
| 574 |
async def modelforecast_file(
|
|
|
|
| 575 |
df_forecast: UploadFile = File(...),
|
| 576 |
):
|
| 577 |
global stored_model, stored_result_join
|
|
|
|
| 618 |
|
| 619 |
@app.post("/modelfit_ind/")
|
| 620 |
async def modelfit_ind(
|
|
|
|
| 621 |
df: List[Dict] = Body(...),
|
| 622 |
id_col: str = Body(...),
|
| 623 |
time_col: str = Body(...),
|
|
|
|
| 683 |
|
| 684 |
@app.post("/modelforecast_ind/")
|
| 685 |
async def modelforecast_ind(
|
|
|
|
| 686 |
df_forecast: List[Dict] = Body(...),
|
| 687 |
):
|
| 688 |
# Use per-ID models created by /modelfit_ind/
|
|
|
|
| 732 |
|
| 733 |
@app.post("/modelfit-file_ind/")
|
| 734 |
async def modelfit_file_ind(
|
|
|
|
| 735 |
file: UploadFile = File(...),
|
| 736 |
id_col: str = Form(...),
|
| 737 |
time_col: str = Form(...),
|
|
|
|
| 811 |
|
| 812 |
@app.post("/modelforecast-file_ind/")
|
| 813 |
async def modelforecast_file_ind(
|
|
|
|
| 814 |
df_forecast: UploadFile = File(...),
|
| 815 |
):
|
| 816 |
global model_store
|