Commit 0f28062f by Jaime Collado

Initial commit

parents
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
# C extensions
*.so
# Distribution / packaging
bin/
build/
develop-eggs/
dist/
eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
.tox/
.coverage
.cache
nosetests.xml
coverage.xml
# Translations
*.mo
# Mr Developer
.mr.developer.cfg
.project
.pydevproject
# Rope
.ropeproject
# Django stuff:
*.log
*.pot
# Sphinx documentation
docs/_build/
# Database
*.db
# Environments
venv/
# Secrets
config.yaml
# PiRADS API
## Guía de instalación
1. Configuración:
1. Renombrar `config_example.yaml` a `config.yaml`
2. Generar una nueva `secret_key` con el comando `openssl rand -hex 32` en la terminal y copiarlo en el campo correspondiente en `config.yaml`
2. Crear un entorno virtual: `python3 -m venv /path/to/environment`
3. Activar el entorno virtual: `source /path/to/environment/bin/activate`
4. Instalar las bibliotecas necesarias: `pip install -r /path/to/requirements.txt`
5. Lanzar el servicio: `uvicorn main:app --host <HOST> --port <PORT>`
6. Probar que funciona en http://localhost:PORT/docs
\ No newline at end of file
File mode changed
No preview for this file type
account:
algorithm: HS256
secret_key: <your_secret_key>
access_token_expire_minutes: 10080
from sqlalchemy.orm import Session
import security, models, schemas
def get_user(db, username: str):
return db.query(models.User).filter(models.User.username == username).first()
def authenticate_user(db: Session, username: str, password: str):
user = get_user(db, username)
if not user:
return False
if not security.verify_password(password, user.hashed_password):
return False
return user
def create_user(db: Session, user: schemas.UserCreate):
hashed_password = security.get_password_hash(user.password)
# Remove unhashed password and insert the hashed one
user_dict = user.dict()
del user_dict["password"]
# Create the user
db_user = models.User(**user_dict, hashed_password=hashed_password)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
[
{
"id": 0,
"text": "EXPLORACIONES:\nMamografía tomosíntesis\nEcografía mamaria\n\n\nINFORMACION CLINICA\n\nRefiere dolor en CCEE de MI. Remitida para valoración. \n\nCOMPARADO CON:\n\nNo dispongo de estudios previos con los que comparar el actual. \n\nHALLAZGOS:\n\nSe realiza mamografía bilateral (proyecciones OML y CC) así como tomosíntesis en proyección CC y ecografía de ambas mamas. \n\nParénquima mamario muy denso, de distribución y características ecográficas normales (Patrón D ACR).\n\n\n.- En región paraareolar externa de MI se identifica un quiste de gran tamaño con una pequeña lesión focal dependiente de su pared, de 4 mm de diámetro, sugestivo de papiloma (BI-RADS 4a). \n\n\n2.- La zona dolorosa en CCEE de MI se corresponde con un quiste simple a tensión de 21 mm de diámetro (BI-RADS 2). \n\n\n3.- Calcificaciones benignas dispersas en ambas mamas (BI-RADS 2). Innumerables quistes simples de diferente tamaño dispersos en ambas mamas, algunos de gran tamaño y otros con contenido débilmente ecogénico en su interior (BI\n-RADS 2). No se observan otras alteraciones significativas en el momento actual.\n\nCONCLUSION:\n\nBI-RADS 4a. Se recomienda realizar PAAF/BAG de la posible lesión papilomatosa localizada en MI. También se recomienda dejar un marcador metálico en el lugar de la punción por la posibilidad de que al puncionar el quiste la lesión pase desapercibida en futuros estudios. Por otra parte, se recomienda realizar PAAF, con guía ecográfica e intención evacuadora, del quiste sintomático localizado en MI."
},
{
"id": 1,
"text": "EXPLORACIONES:\nMamografía tomosíntesis\nEcografía mamaria\n\n\nINFORMACION CLINICA\n\nRefiere dolor en CCEE de MI. Remitida para valoración. \n\nCOMPARADO CON:\n\nNo dispongo de estudios previos con los que comparar el actual. \n\nHALLAZGOS:\n\nSe realiza mamografía bilateral (proyecciones OML y CC) así como tomosíntesis en proyección CC y ecografía de ambas mamas. \n\nParénquima mamario muy denso, de distribución y características ecográficas normales (Patrón D ACR).\n\n\n.- En región paraareolar externa de MI se identifica un quiste de gran tamaño con una pequeña lesión focal dependiente de su pared, de 4 mm de diámetro, sugestivo de papiloma (BI-RADS 4a). \n\n\n2.- La zona dolorosa en CCEE de MI se corresponde con un quiste simple a tensión de 21 mm de diámetro (BI-RADS 2). \n\n\n3.- Calcificaciones benignas dispersas en ambas mamas (BI-RADS 2). Innumerables quistes simples de diferente tamaño dispersos en ambas mamas, algunos de gran tamaño y otros con contenido débilmente ecogénico en su interior (BI\n-RADS 2). No se observan otras alteraciones significativas en el momento actual.\n\nCONCLUSION:\n\nBI-RADS 4a. Se recomienda realizar PAAF/BAG de la posible lesión papilomatosa localizada en MI. También se recomienda dejar un marcador metálico en el lugar de la punción por la posibilidad de que al puncionar el quiste la lesión pase desapercibida en futuros estudios. Por otra parte, se recomienda realizar PAAF, con guía ecográfica e intención evacuadora, del quiste sintomático localizado en MI."
}
]
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
SQLALCHEMY_DATABASE_URL = "sqlite:///./sql_app.db"
# SQLALCHEMY_DATABASE_URL = "postgresql://user:password@postgresserver/db"
# connect_args={"check_same_thread": False is only for SQLite
engine = create_engine(
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
from fastapi.security import OAuth2PasswordBearer
from fastapi import Depends, HTTPException, status
from jose import JWTError, jwt
from sqlalchemy.orm import Session
import security, schemas, crud, database
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
def get_db():
db = database.SessionLocal()
try:
yield db
finally:
db.close()
async def get_current_user(
db: Session = Depends(get_db), token: str = Depends(oauth2_scheme)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, security.SECRET_KEY, algorithms=[security.ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
token_data = schemas.TokenData(username=username)
except JWTError:
raise credentials_exception
user = crud.get_user(db, username=token_data.username)
if user is None:
raise credentials_exception
return user
async def get_current_active_user(current_user: schemas.User = Depends(get_current_user)):
if not current_user.is_active:
raise HTTPException(status_code=400, detail="Inactive user")
return current_user
\ No newline at end of file
import joblib
from datetime import timedelta
from typing import List
from fastapi import FastAPI, Depends, HTTPException, status, Body
from fastapi.security import OAuth2PasswordRequestForm
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.orm import Session
import schemas, security, dependencies, crud, database, utils
database.Base.metadata.create_all(bind=database.engine)
# APP
app = FastAPI()
# CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Load the vectorizer
with open("./vectorizers/birads_tfidf.pkl", "rb") as pickled_file:
vectorizer = joblib.load(pickled_file)
# Load the model
with open("./classifiers/birads_model.joblib", "rb") as pickled_file:
clf = joblib.load(pickled_file)
def _predict(text, model, vectorizer):
# Preprocess input text
clean_text = utils.preprocessing_text(text)
clean_text = utils.clear_birads(clean_text)
# Vectorize text
X_test = vectorizer.transform([clean_text])
# Predict outputs
probs = model.predict_proba(X_test)
probs = [prob[0][1] for prob in probs]
return probs
# Methods
@app.post("/register", response_model=schemas.User)
def register(user: schemas.UserCreate, db: Session = Depends(dependencies.get_db)):
db_user = crud.get_user(db, username=user.username)
if db_user:
raise HTTPException(status_code=400, detail="Email already registered")
return crud.create_user(db=db, user=user)
@app.post("/token", response_model=schemas.Token)
async def login_for_access_token(
form_data: OAuth2PasswordRequestForm = Depends(),
db: Session = Depends(dependencies.get_db)
):
user = crud.authenticate_user(db, form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=security.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = security.create_access_token(
data={"sub": user.username}, expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}
@app.post("/predict", response_model=List[schemas.OutputData])
async def predict(
input_data: List[schemas.InputData] = Body(
example =
[
{
"id": 0,
"text": "EXPLORACIONES:\nMamografía tomosíntesis\nEcografía mamaria\n\n\nINFORMACION CLINICA\n\nRefiere dolor en CCEE de MI. Remitida para valoración. \n\nCOMPARADO CON:\n\nNo dispongo de estudios previos con los que comparar el actual. \n\nHALLAZGOS:\n\nSe realiza mamografía bilateral (proyecciones OML y CC) así como tomosíntesis en proyección CC y ecografía de ambas mamas. \n\nParénquima mamario muy denso, de distribución y características ecográficas normales (Patrón D ACR).\n\n\n.- En región paraareolar externa de MI se identifica un quiste de gran tamaño con una pequeña lesión focal dependiente de su pared, de 4 mm de diámetro, sugestivo de papiloma (BI-RADS 4a). \n\n\n2.- La zona dolorosa en CCEE de MI se corresponde con un quiste simple a tensión de 21 mm de diámetro (BI-RADS 2). \n\n\n3.- Calcificaciones benignas dispersas en ambas mamas (BI-RADS 2). Innumerables quistes simples de diferente tamaño dispersos en ambas mamas, algunos de gran tamaño y otros con contenido débilmente ecogénico en su interior (BI\n-RADS 2). No se observan otras alteraciones significativas en el momento actual.\n\nCONCLUSION:\n\nBI-RADS 4a. Se recomienda realizar PAAF/BAG de la posible lesión papilomatosa localizada en MI. También se recomienda dejar un marcador metálico en el lugar de la punción por la posibilidad de que al puncionar el quiste la lesión pase desapercibida en futuros estudios. Por otra parte, se recomienda realizar PAAF, con guía ecográfica e intención evacuadora, del quiste sintomático localizado en MI."
},
{
"id": 1,
"text": "EXPLORACIONES:\nMamografía tomosíntesis\nEcografía mamaria\n\n\nINFORMACION CLINICA\n\nRefiere dolor en CCEE de MI. Remitida para valoración. \n\nCOMPARADO CON:\n\nNo dispongo de estudios previos con los que comparar el actual. \n\nHALLAZGOS:\n\nSe realiza mamografía bilateral (proyecciones OML y CC) así como tomosíntesis en proyección CC y ecografía de ambas mamas. \n\nParénquima mamario muy denso, de distribución y características ecográficas normales (Patrón D ACR).\n\n\n.- En región paraareolar externa de MI se identifica un quiste de gran tamaño con una pequeña lesión focal dependiente de su pared, de 4 mm de diámetro, sugestivo de papiloma (BI-RADS 4a). \n\n\n2.- La zona dolorosa en CCEE de MI se corresponde con un quiste simple a tensión de 21 mm de diámetro (BI-RADS 2). \n\n\n3.- Calcificaciones benignas dispersas en ambas mamas (BI-RADS 2). Innumerables quistes simples de diferente tamaño dispersos en ambas mamas, algunos de gran tamaño y otros con contenido débilmente ecogénico en su interior (BI\n-RADS 2). No se observan otras alteraciones significativas en el momento actual.\n\nCONCLUSION:\n\nBI-RADS 4a. Se recomienda realizar PAAF/BAG de la posible lesión papilomatosa localizada en MI. También se recomienda dejar un marcador metálico en el lugar de la punción por la posibilidad de que al puncionar el quiste la lesión pase desapercibida en futuros estudios. Por otra parte, se recomienda realizar PAAF, con guía ecográfica e intención evacuadora, del quiste sintomático localizado en MI."
}
]
),
current_user: str = Depends(dependencies.get_current_active_user)
):
# Input data: [{"id": 1, "text": "a"}, {"id": 2, "text": "b"}]
predictions = []
for report in input_data:
prediction = _predict(text=report.text, model=clf, vectorizer=vectorizer)
predictions.append(
{
"id": report.id,
"prediction": prediction
}
)
# Return predictions
return predictions
\ No newline at end of file
from sqlalchemy import Boolean, Column, Integer, String
from database import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String)
hashed_password = Column(String)
is_active = Column(Boolean, default=True)
\ No newline at end of file
anyio==3.6.2
bcrypt==4.0.1
certifi==2022.12.7
cffi==1.15.1
click==8.1.3
cryptography==39.0.1
dnspython==2.3.0
ecdsa==0.18.0
email-validator==1.3.1
fastapi==0.89.1
greenlet==2.0.2
h11==0.14.0
httpcore==0.16.3
httptools==0.5.0
httpx==0.23.3
idna==3.4
itsdangerous==2.1.2
Jinja2==3.1.2
joblib==1.2.0
MarkupSafe==2.1.2
numpy==1.24.2
orjson==3.8.5
passlib==1.7.4
pyasn1==0.4.8
pycparser==2.21
pydantic==1.10.4
python-dotenv==0.21.1
python-jose==3.3.0
python-multipart==0.0.5
PyYAML==6.0
rfc3986==1.5.0
rsa==4.9
scikit-learn==1.2.1
scipy==1.10.0
six==1.16.0
sniffio==1.3.0
SQLAlchemy==2.0.2
starlette==0.22.0
threadpoolctl==3.1.0
typing_extensions==4.4.0
ujson==5.7.0
uvicorn==0.20.0
uvloop==0.17.0
watchfiles==0.18.1
websockets==10.4
xgboost==0.90
from typing import List, Union
from pydantic import BaseModel
# ---------- TOKEN SCHEMAS ----------
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
username: Union[str, None] = None
# ---------- USER SCHEMAS ----------
class UserBase(BaseModel):
username: str
class UserCreate(UserBase):
password: str
class User(UserBase):
is_active: Union[bool, None] = None
class Config:
orm_mode = True
class UserInDB(User):
hashed_password: str
# ---------- DATA SCHEMAS ----------
class InputData(BaseModel):
id: int
text: str
class OutputData(BaseModel):
id: int
prediction: List[float]
\ No newline at end of file
import yaml
import os
from typing import Union
from datetime import datetime, timedelta
from jose import jwt
from passlib.context import CryptContext
cwd = os.path.abspath(os.getcwd())
config_path = os.path.join(cwd, "config.yaml")
with open(config_path, "r") as ymlfile:
cfg = yaml.load(ymlfile, Loader=yaml.FullLoader)
# ------- ACCOUNT SECURITY -------
# to get a new secret key run:
# openssl rand -hex 32
SECRET_KEY = cfg["account"]["secret_key"]
ALGORITHM = cfg["account"]["algorithm"]
ACCESS_TOKEN_EXPIRE_MINUTES = cfg["account"]["access_token_expire_minutes"] # 7 days
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password):
return pwd_context.hash(password)
def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
\ No newline at end of file
import re
import unicodedata
def preprocessing_text(s):
# replace tab
s = re.sub('\t+', ' ', s)
# Unicode normalization
s = re.sub(r'BR', r'birads', s)
# string to lower
s = s.strip().lower()
s = ''.join(c for c in unicodedata.normalize('NFD', s) if unicodedata.category(c) != 'Mn')
# replace synonyms of birads
synon = ['bi rads', 'bi-rads', 'b-rads', 'birads-', 'birads -', 'bi_rads', 'birads/']
for sy in synon:
s = re.sub(sy, r'birads ', s)
s = re.sub(' +', ' ', s)
# roman numeral
dic_roman = {
'vi': '6',
'v': '5',
'iv': '4',
'iii': '3',
'ii': '2',
'i' : '1'
}
for key, value in dic_roman.items():
start = 'birads ' + key
end = 'birads ' + value
s = re.sub(start, end, s)
s = re.sub(' +', ' ', s)
s = re.sub(r'birads (\d)([a-z])', r'birads \1 \2', s)
s = re.sub(r'birads (\d) - (\d)', r'birads \1 birads \2', s)
s = re.sub(' +', ' ', s)
# include blank space before and after each punctuation mark
s = re.sub(r'([-¿?¡!()\'",.;:$€])', r' \1 ', s)
s = re.sub(' +', ' ', s)
s = s.replace("4a", ' 4 a')
s = s.replace("4b", ' 4 b')
s = s.replace("4c", ' 4 c')
s = re.sub(' +', ' ', s)
# s = re.sub(r' b'+str(i)+' ', r' birads '+str(i)+' ', s)
# s = re.sub(r' b '+str(i)+' ', r' birads '+str(i)+' ', s)
# replace separate numbers e.g.: 4 x 5 . 9 by 4x5.9
for n in [',', 'x', '.']:
s = re.sub(r"(\d+) "+ n + r" (\d+)", r"\1"+ n.replace(r"\\", "") +r"\2", s)
s = re.sub(' +', ' ', s)
return s.strip()
def clear_birads(text):
text = re.sub(r'birads.{1,3}\d{1}[a|b|c]?', '', text)
text = re.sub(r'birads categoria \d{1}', '', text)
text = text.replace("( )", "")
text = re.sub('\t+', ' ', text)
text = re.sub(' +', ' ', text)
return text.strip()
No preview for this file type
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment