TNM / ajcc_api.py
MohamedTry's picture
Update ajcc_api.py
9cec003 verified
"""
AJCC Anatomic Staging API (Simplified)
--------------------------------------
Single-file FastAPI app that exposes TNM -> Stage mapping for:
1) Breast Cancer – AJCC 8 (Anatomic)
2) Lung Cancer (NSCLC) – AJCC 8
3) Colon / Rectal Cancer – AJCC 8
4) Stomach (Gastric Cancer) – AJCC 8
5) Pancreatic Cancer – AJCC 8
6) Liver Cancer (Hepatocellular Carcinoma) – AJCC 8
7) Melanoma – AJCC 8
8) Prostate Cancer – (Anatomic logic only)
9) Kidney (Renal Cell Carcinoma)
10) Bladder Cancer – AJCC 8
Plus a general fallback AJCC rule for any other / unknown cancer types.
This is a simplified logic based on the mapping you provided,
NOT a full official AJCC implementation.
"""
from typing import Optional, Any, Dict, List
from fastapi import FastAPI
from pydantic import BaseModel
# ---------------------------------------------------------------------
# Core TNM staging logic (previously in ajcc_staging.py)
# ---------------------------------------------------------------------
def normalize_tnm(value: Optional[str]) -> Optional[str]:
if value is None:
return None
value = value.strip()
if not value:
return None
return value.upper()
def stage_cancer(cancer_type: str, T: str, N: str, M: str) -> str:
"""
Main entry point.
cancer_type: string key identifying the cancer
T, N, M: TNM strings like 'T1', 'T2B', 'N1', 'N1MI', 'M1A', etc.
Returns: stage string like 'Stage IIA', 'Stage IIIC', 'Stage IVB', etc.
"""
cancer_type = cancer_type.strip().lower()
T = normalize_tnm(T)
N = normalize_tnm(N)
M = normalize_tnm(M)
if cancer_type == "breast":
return stage_breast(T, N, M)
elif cancer_type in ("lung", "nsclc", "non-small cell lung", "non small cell lung"):
return stage_nsclc(T, N, M)
elif cancer_type in ("colon", "rectal", "colorectal"):
return stage_colorectal(T, N, M)
elif cancer_type in ("stomach", "gastric"):
return stage_stomach(T, N, M)
elif cancer_type in ("pancreas", "pancreatic"):
return stage_pancreas(T, N, M)
elif cancer_type in ("liver", "hcc", "hepatocellular"):
return stage_liver(T, N, M)
elif cancer_type == "melanoma":
return stage_melanoma(T, N, M)
elif cancer_type == "prostate":
return stage_prostate(T, N, M)
elif cancer_type in ("kidney", "renal"):
return stage_kidney(T, N, M)
elif cancer_type == "bladder":
return stage_bladder(T, N, M)
else:
return stage_general(T, N, M)
# ---------------------------------------------------------------------
# General fallback AJCC rule (for unknown cancer types)
# ---------------------------------------------------------------------
def stage_general(T: str, N: str, M: str) -> str:
"""
General AJCC mapping:
IF T = Tis AND N = N0 AND M = M0 -> Stage 0
IF (T = T1 OR T = T2) AND N = N0 AND M = M0 -> Stage I
IF (T = T3 AND N = N0 AND M = M0)
OR ((T = T1 OR T = T2) AND N = N1 AND M = M0) -> Stage II
IF ((T = T3 OR T = T4) AND (N = N1 OR N = N2 OR N = N3) AND M = M0)
OR ((T = T1 OR T = T2 OR T = T3 OR T = T4)
AND (N = N2 OR N = N3) AND M = M0) -> Stage III
IF M = M1 -> Stage IV
"""
# Stage IV
if M == "M1":
return "Stage IV"
# Stage 0
if T == "TIS" and N == "N0" and M == "M0":
return "Stage 0"
# Stage I
if T in ("T1", "T2") and N == "N0" and M == "M0":
return "Stage I"
# Stage II
if (T == "T3" and N == "N0" and M == "M0") or (
T in ("T1", "T2") and N == "N1" and M == "M0"
):
return "Stage II"
# Stage III
if (
(T in ("T3", "T4") and N in ("N1", "N2", "N3") and M == "M0")
or (T in ("T1", "T2", "T3", "T4") and N in ("N2", "N3") and M == "M0")
):
return "Stage III"
return "Stage not mapped (general)"
# ---------------------------------------------------------------------
# 1) Breast Cancer – AJCC 8 (Anatomic)
# ---------------------------------------------------------------------
def stage_breast(T: str, N: str, M: str) -> str:
# Stage IV first
if M == "M1":
return "Stage IV"
# Stage 0
if T == "TIS" and N == "N0" and M == "M0":
return "Stage 0"
# Stage IA
if T == "T1" and N == "N0" and M == "M0":
return "Stage IA"
# Stage IB
if (T in ("T0", "T1")) and N == "N1MI" and M == "M0":
return "Stage IB"
# Stage IIA
if (
((T in ("T0", "T1")) and N == "N1" and M == "M0")
or (T == "T2" and N == "N0" and M == "M0")
):
return "Stage IIA"
# Stage IIB
if (
(T == "T2" and N == "N1" and M == "M0")
or (T == "T3" and N == "N0" and M == "M0")
):
return "Stage IIB"
# Stage IIIA
if (
(T in ("T0", "T1", "T2") and N == "N2" and M == "M0")
or (T == "T3" and N in ("N1", "N2") and M == "M0")
):
return "Stage IIIA"
# Stage IIIB
if T == "T4" and M == "M0":
return "Stage IIIB"
# Stage IIIC
if N == "N3" and M == "M0":
return "Stage IIIC"
return "Stage not mapped (breast)"
# ---------------------------------------------------------------------
# 2) Lung Cancer (NSCLC) – AJCC 8
# ---------------------------------------------------------------------
def stage_nsclc(T: str, N: str, M: str) -> str:
# Stage IV
if M in ("M1A", "M1B"):
return "Stage IVA"
if M == "M1C":
return "Stage IVB"
if M and M != "M0":
# any other M1 pattern
return "Stage IV"
# Stage 0
if T == "TIS" and N == "N0" and M == "M0":
return "Stage 0"
# Stage IA1
if T in ("T1A", "T1A(MI)", "T1AMI") and N == "N0" and M == "M0":
return "Stage IA1"
# Stage IA2
if T == "T1B" and N == "N0" and M == "M0":
return "Stage IA2"
# Stage IA3
if T == "T1C" and N == "N0" and M == "M0":
return "Stage IA3"
# Stage IB
if T == "T2A" and N == "N0" and M == "M0":
return "Stage IB"
# Stage IIA
if T == "T2B" and N == "N0" and M == "M0":
return "Stage IIA"
# Stage IIB
if (T == "T3" and N == "N0" and M == "M0") or (
T in ("T1", "T2") and N == "N1" and M == "M0"
):
return "Stage IIB"
# Stage IIIA
if (
T in ("T1", "T2") and N == "N2" and M == "M0"
) or (T == "T3" and N in ("N1", "N2") and M == "M0"):
return "Stage IIIA"
# Stage IIIB
if (
T in ("T3", "T4") and N == "N2" and M == "M0"
) or (T in ("T1", "T2") and N == "N3" and M == "M0"):
return "Stage IIIB"
# Stage IIIC
if T in ("T3", "T4") and N == "N3" and M == "M0":
return "Stage IIIC"
return "Stage not mapped (NSCLC)"
# ---------------------------------------------------------------------
# 3) Colon / Rectal Cancer – AJCC 8
# ---------------------------------------------------------------------
def stage_colorectal(T: str, N: str, M: str) -> str:
# Stage IV
if M == "M1A":
return "Stage IVA"
if M == "M1B":
return "Stage IVB"
if M == "M1C":
return "Stage IVC"
if M and M != "M0":
return "Stage IV"
# Stage 0
if T == "TIS" and N == "N0" and M == "M0":
return "Stage 0"
# Stage I
if T in ("T1", "T2") and N == "N0" and M == "M0":
return "Stage I"
# Stage II
if T == "T3" and N == "N0" and M == "M0":
return "Stage IIA"
if T == "T4A" and N == "N0" and M == "M0":
return "Stage IIB"
if T == "T4B" and N == "N0" and M == "M0":
return "Stage IIC"
# Stage IIIA
if (
T in ("T1", "T2") and N in ("N1", "N1C") and M == "M0"
) or (T == "T1" and N == "N2A" and M == "M0"):
return "Stage IIIA"
# Stage IIIB
if (
(T in ("T3", "T4A") and N in ("N1", "N1C") and M == "M0")
or (T in ("T2", "T3") and N == "N2A" and M == "M0")
or (T in ("T1", "T2") and N == "N2B" and M == "M0")
):
return "Stage IIIB"
# Stage IIIC
if (T in ("T4A", "T4B") and N in ("N2", "N2A", "N2B") and M == "M0"):
return "Stage IIIC"
return "Stage not mapped (colorectal)"
# ---------------------------------------------------------------------
# 4) Stomach (Gastric Cancer) – AJCC 8
# ---------------------------------------------------------------------
def stage_stomach(T: str, N: str, M: str) -> str:
# Stage IV
if M and M != "M0":
return "Stage IV"
# Stage I
if (T == "T1" and N in ("N0", "N1")) or (T == "T2" and N == "N0"):
return "Stage I"
# Stage II
if (T == "T1" and N == "N2") or (T == "T2" and N == "N1") or (
T == "T3" and N == "N0"
):
return "Stage II"
# Stage III
if (T == "T2" and N == "N2") or (T == "T3" and N in ("N1", "N2")) or (
T == "T4A" and N in ("N0", "N1", "N2")
) or (T == "T4B" and N in ("N0", "N1")):
return "Stage III"
return "Stage not mapped (stomach)"
# ---------------------------------------------------------------------
# 5) Pancreatic Cancer – AJCC 8
# ---------------------------------------------------------------------
def stage_pancreas(T: str, N: str, M: str) -> str:
# Stage IV
if M and M != "M0":
return "Stage IV"
# Stage I
if T in ("T1", "T2") and N == "N0" and M == "M0":
return "Stage I"
# Stage II
if (T == "T3" and N == "N0" and M == "M0") or (
T in ("T1", "T2", "T3") and N == "N1" and M == "M0"
):
return "Stage II"
# Stage III
# mapping given: T4 (unresectable arterial invasion) OR N2 (β‰₯4 nodes), with M0
if (T == "T4" and M == "M0") or (N == "N2" and M == "M0"):
return "Stage III"
return "Stage not mapped (pancreas)"
# ---------------------------------------------------------------------
# 6) Liver Cancer (Hepatocellular Carcinoma) – AJCC 8
# ---------------------------------------------------------------------
def stage_liver(T: str, N: str, M: str) -> str:
# Stage IV subdivided:
if M and M != "M0":
return "Stage IVB"
if N == "N1" and M == "M0":
return "Stage IVA"
# Stage I
if T == "T1" and N == "N0" and M == "M0":
return "Stage I"
# Stage II
if T == "T2" and N == "N0" and M == "M0":
return "Stage II"
# Stage III
if (T == "T3" and N == "N0" and M == "M0") or (
T == "T4" and N == "N0" and M == "M0"
):
return "Stage III"
return "Stage not mapped (liver)"
# ---------------------------------------------------------------------
# 7) Melanoma – AJCC 8
# ---------------------------------------------------------------------
def stage_melanoma(T: str, N: str, M: str) -> str:
# Stage 0
if T == "TIS" and N == "N0" and M == "M0":
return "Stage 0"
# Stage IV
if M in ("M1A", "M1B", "M1C", "M1D") or (M and M != "M0"):
if M == "M1A":
return "Stage IV (M1a)"
if M == "M1B":
return "Stage IV (M1b)"
if M == "M1C":
return "Stage IV (M1c)"
if M == "M1D":
return "Stage IV (M1d)"
return "Stage IV"
# Stage I
if T == "T1A" and N == "N0" and M == "M0":
return "Stage IA"
if T in ("T1B", "T2A") and N == "N0" and M == "M0":
return "Stage IB"
# Stage II
if T in ("T2B", "T3A") and N == "N0" and M == "M0":
return "Stage IIA"
if T in ("T3B", "T4A") and N == "N0" and M == "M0":
return "Stage IIB"
if T == "T4B" and N == "N0" and M == "M0":
return "Stage IIC"
# Stage III
if N in ("N1", "N2", "N3") and M == "M0":
return "Stage III"
return "Stage not mapped (melanoma)"
# ---------------------------------------------------------------------
# 8) Prostate Cancer – Anatomic Logic
# ---------------------------------------------------------------------
def stage_prostate(T: str, N: str, M: str) -> str:
# Stage IV
if M and M != "M0":
return "Stage IVB"
if N == "N1" and M == "M0":
return "Stage IVA"
# Stage I
if T in ("T1", "T2A") and N == "N0" and M == "M0":
return "Stage I"
# Stage II (no A/B/C separation here – depends on grade/PSA in real AJCC)
if T in ("T2B", "T2C") and N == "N0" and M == "M0":
return "Stage II"
# Stage IIIA
if T == "T3A" and N == "N0" and M == "M0":
return "Stage IIIA"
# Stage IIIB
if T in ("T3B", "T4") and N == "N0" and M == "M0":
return "Stage IIIB"
return "Stage not mapped (prostate)"
# ---------------------------------------------------------------------
# 9) Kidney (Renal Cell Carcinoma)
# ---------------------------------------------------------------------
def stage_kidney(T: str, N: str, M: str) -> str:
# Stage IV
if T == "T4" and M == "M0":
return "Stage IV"
if M and M != "M0":
return "Stage IV"
# Stage I
if T == "T1" and N == "N0" and M == "M0":
return "Stage I"
# Stage II
if T == "T2" and N == "N0" and M == "M0":
return "Stage II"
# Stage III
if (T == "T3" and N == "N0" and M == "M0") or (N == "N1" and M == "M0"):
return "Stage III"
return "Stage not mapped (kidney)"
# ---------------------------------------------------------------------
# 10) Bladder Cancer – AJCC 8
# ---------------------------------------------------------------------
def stage_bladder(T: str, N: str, M: str) -> str:
# Stage IV
if T == "T4B" and M == "M0":
return "Stage IV"
if N in ("N1", "N2", "N3") and M == "M0":
return "Stage IV"
if M and M != "M0":
return "Stage IV"
# Stage 0
if T in ("TA", "TIS") and N == "N0" and M == "M0":
return "Stage 0"
# Stage I
if T == "T1" and N == "N0" and M == "M0":
return "Stage I"
# Stage II
if T == "T2" and N == "N0" and M == "M0":
return "Stage II"
# Stage III
if T in ("T3", "T4A") and N == "N0" and M == "M0":
return "Stage III"
return "Stage not mapped (bladder)"
# ---------------------------------------------------------------------
# FastAPI Layer
# ---------------------------------------------------------------------
app = FastAPI(
title="AJCC TNM Staging API",
description=(
"Simplified AJCC anatomic staging API for multiple cancer types.\n"
"Not a full official AJCC implementation; uses custom mapping logic."
),
version="1.0.0",
)
class StagingRequest(BaseModel):
cancer_type: str
T: str
N: str
M: str
class StagingResponse(BaseModel):
cancer_type: str
T: str
N: str
M: str
stage: str
@app.get("/health", tags=["system"])
def health_check():
return {"status": "ok"}
@app.post("/stage", response_model=StagingResponse, tags=["staging"])
def get_stage(body: StagingRequest):
"""
Return AJCC stage group based on cancer type and TNM.
"""
stage = stage_cancer(
cancer_type=body.cancer_type,
T=body.T,
N=body.N,
M=body.M,
)
return StagingResponse(
cancer_type=body.cancer_type,
T=body.T,
N=body.N,
M=body.M,
stage=stage,
)
if __name__ == "__main__":
import uvicorn
uvicorn.run("ajcc_api:app", host="0.0.0.0", port=8000, reload=True)