Skip to content

Commit

Permalink
Merge branch 'development' into feat/2fa
Browse files Browse the repository at this point in the history
  • Loading branch information
TanookiVerde committed Aug 30, 2024
2 parents 095bcfa + 39b3883 commit d745a88
Show file tree
Hide file tree
Showing 7 changed files with 94 additions and 184 deletions.
2 changes: 1 addition & 1 deletion app/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ def inject_environment_variables(environment: str):
f"Injecting {len(secrets)} environment variables from Infisical:")
for secret in secrets:
logger.info(
f" - {secret.secret_name}: {'*' * len(secret.secret_value)}")
f" - {secret.secret_name}: {len(secret.secret_value)} chars")


environment = getenv_or_action("ENVIRONMENT", action="warn", default="dev")
Expand Down
11 changes: 10 additions & 1 deletion app/config/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,17 @@
# Logging
LOG_LEVEL = getenv_or_action("LOG_LEVEL", default="INFO")

# BigQuery Project
# BigQuery Integration
BIGQUERY_PROJECT = getenv_or_action("BIGQUERY_PROJECT", action="raise")
BIGQUERY_PATIENT_HEADER_TABLE_ID = getenv_or_action(
"BIGQUERY_PATIENT_HEADER_TABLE_ID", action="raise"
)
BIGQUERY_PATIENT_SUMMARY_TABLE_ID = getenv_or_action(
"BIGQUERY_PATIENT_SUMMARY_TABLE_ID", action="raise"
)
BIGQUERY_PATIENT_ENCOUNTERS_TABLE_ID = getenv_or_action(
"BIGQUERY_PATIENT_ENCOUNTERS_TABLE_ID", action="raise"
)

# JWT configuration
JWT_SECRET_KEY = getenv_or_action("JWT_SECRET_KEY", default=token_bytes(32).hex())
Expand Down
203 changes: 42 additions & 161 deletions app/routers/frontend.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
# -*- coding: utf-8 -*-
import json

from typing import Annotated, List
from fastapi import APIRouter, Depends, HTTPException
from basedosdados import read_sql
from tortoise.exceptions import ValidationError

from app.dependencies import (
get_current_frontend_user
Expand All @@ -15,8 +13,14 @@
Encounter,
UserInfo,
)
from app.config import BIGQUERY_PROJECT
from app.utils import read_timestamp, normalize_case
from app.utils import read_bq
from app.validators import CPFValidator
from app.config import (
BIGQUERY_PROJECT,
BIGQUERY_PATIENT_HEADER_TABLE_ID,
BIGQUERY_PATIENT_SUMMARY_TABLE_ID,
BIGQUERY_PATIENT_ENCOUNTERS_TABLE_ID
)

router = APIRouter(prefix="/frontend", tags=["Frontend Application"])

Expand Down Expand Up @@ -45,80 +49,32 @@ async def get_patient_header(
_: Annotated[User, Depends(get_current_frontend_user)],
cpf: str,
) -> PatientHeader:
results_json = read_sql(
validator = CPFValidator()
try:
validator(cpf)
except ValidationError:
raise HTTPException(status_code=400, detail="Invalid CPF")

results = await read_bq(
f"""
SELECT *
FROM `{BIGQUERY_PROJECT}`.`saude_dados_mestres`.`paciente`
FROM `{BIGQUERY_PROJECT}`.{BIGQUERY_PATIENT_HEADER_TABLE_ID}
WHERE cpf = '{cpf}'
""",
from_file="/tmp/credentials.json",
).to_json(orient="records")
)

results = json.loads(results_json)

if len(results) > 0:
patient_record = results[0]
else:
if len(results) == 0:
raise HTTPException(status_code=404, detail="Patient not found")

data = patient_record["dados"]

cns_principal = None
if len(patient_record["cns"]) > 0:
cns_principal = patient_record["cns"][0]

telefone_principal = None
if len(patient_record["contato"]["telefone"]) > 0:
telefone_principal = patient_record["contato"]["telefone"][0]["valor"]

clinica_principal, equipe_principal = {}, {}
medicos, enfermeiros = [], []
if len(patient_record["equipe_saude_familia"]) > 0:
equipe_principal = patient_record["equipe_saude_familia"][0]

# Pega Clínica da Família
if equipe_principal["clinica_familia"]:
clinica_principal = equipe_principal["clinica_familia"]

for equipe in patient_record["equipe_saude_familia"]:
medicos.extend(equipe["medicos"])
enfermeiros.extend(equipe["enfermeiros"])

for medico in medicos:
medico['registry'] = medico.pop('id_profissional_sus')
medico['name'] = medico.pop('nome')

for enfermeiro in enfermeiros:
enfermeiro['registry'] = enfermeiro.pop('id_profissional_sus')
enfermeiro['name'] = enfermeiro.pop('nome')
dados = results[0]
configuracao_exibicao = dados.get('exibicao', {})

data_nascimento = None
if data.get("data_nascimento") is not None:
data_nascimento = read_timestamp(data.get("data_nascimento"), output_format='date')
if configuracao_exibicao.get('indicador', False) is False:
message = ",".join(configuracao_exibicao.get('motivos', []))
raise HTTPException(status_code=403, detail=message)

return {
"registration_name": data.get("nome"),
"social_name": data.get("nome_social"),
"cpf": f"{cpf[:3]}.{cpf[3:6]}.{cpf[6:9]}-{cpf[9:]}",
"cns": cns_principal,
"birth_date": data_nascimento,
"gender": data.get("genero"),
"race": data.get("raca"),
"phone": telefone_principal,
"family_clinic": {
"cnes": clinica_principal.get("id_cnes"),
"name": clinica_principal.get("nome"),
"phone": clinica_principal.get("telefone"),
},
"family_health_team": {
"ine_code": equipe_principal.get("id_ine"),
"name": equipe_principal.get("nome"),
"phone": equipe_principal.get("telefone"),
},
"medical_responsible": medicos,
"nursing_responsible": enfermeiros,
"validated": data.get("identidade_validada_indicador"),
}
return dados



Expand All @@ -128,51 +84,18 @@ async def get_patient_summary(
cpf: str,
) -> PatientSummary:

query = f"""
with
base as (select '{cpf}' as cpf),
alergias_grouped as (
select
cpf,
alergias as allergies
from `saude_historico_clinico.alergia`
where cpf = '{cpf}'
),
medicamentos_cronicos_single as (
select
cpf,
med.nome as nome_medicamento
from `saude_historico_clinico.medicamentos_cronicos`,
unnest(medicamentos) as med
where cpf = '{cpf}'
),
medicamentos_cronicos_grouped as (
select
cpf,
array_agg(nome_medicamento) as continuous_use_medications
from medicamentos_cronicos_single
group by cpf
)
select
alergias_grouped.allergies,
medicamentos_cronicos_grouped.continuous_use_medications
from base
left join alergias_grouped on alergias_grouped.cpf = base.cpf
left join medicamentos_cronicos_grouped on medicamentos_cronicos_grouped.cpf = base.cpf
"""
results_json = read_sql(
query,
from_file="/tmp/credentials.json"
).to_json(orient="records")

result = json.loads(results_json)
if len(result) > 0:
return result[0]

return {
"allergies": [],
"continuous_use_medications": []
}
results = await read_bq(
f"""
SELECT *
FROM `{BIGQUERY_PROJECT}`.{BIGQUERY_PATIENT_SUMMARY_TABLE_ID}
WHERE cpf = '{cpf}'
""",
from_file="/tmp/credentials.json",
)
if len(results) == 0:
raise HTTPException(status_code=404, detail="Patient not found")
else:
return results[0]

@router.get("/patient/filter_tags")
async def get_filter_tags(
Expand All @@ -196,54 +119,12 @@ async def get_patient_encounters(
cpf: str,
) -> List[Encounter]:

results_json = read_sql(
results = await read_bq(
f"""
SELECT *
FROM `{BIGQUERY_PROJECT}`.`saude_historico_clinico`.`episodio_assistencial`
WHERE paciente.cpf = '{cpf}'
FROM `{BIGQUERY_PROJECT}`.{BIGQUERY_PATIENT_ENCOUNTERS_TABLE_ID}
WHERE cpf = '{cpf}' and exibicao.indicador = true
""",
from_file="/tmp/credentials.json",
).to_json(orient="records")

encounters = []
for result in json.loads(results_json):
# Responsible professional
professional = result.get('profissional_saude_responsavel')
if professional:
if isinstance(professional, list):
professional = professional[0] if len(professional) > 0 else {}

if not professional['nome'] and not professional['especialidade']:
professional = None
else:
professional = {
"name": professional.get('nome'),
"role": professional.get('especialidade')
}

# Filter Tags
unit_type = result['estabelecimento']['estabelecimento_tipo']
if unit_type in [
'CLINICA DA FAMILIA',
'CENTRO MUNICIPAL DE SAUDE'
]:
unit_type = 'CF/CMS'

encounter = {
"entry_datetime": read_timestamp(result['entrada_datahora'], output_format='datetime'),
"exit_datetime": read_timestamp(result['saida_datahora'], output_format='datetime'),
"location": result['estabelecimento']['nome'],
"type": result['tipo'],
"subtype": result['subtipo'],
"active_cids": [cid['descricao'] for cid in result['condicoes'] if cid['descricao']],
"responsible": professional,
"clinical_motivation": normalize_case(result['motivo_atendimento']),
"clinical_outcome": normalize_case(result['desfecho_atendimento']),
"filter_tags": [unit_type],
}
encounters.append(encounter)

# Sort Encounters by entry_datetime
encounters = sorted(encounters, key=lambda x: x['entry_datetime'], reverse=True)

return encounters
)
return results
8 changes: 7 additions & 1 deletion app/types/frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ class FamilyHealthTeam(BaseModel):
name: Optional[str]
phone: Optional[str]

# Clinical Exam Model
class ClinicalExam(BaseModel):
type: str
description: Optional[str]

# Medical Conditions model
class PatientSummary(BaseModel):
Expand All @@ -32,14 +36,16 @@ class Responsible(BaseModel):
# Medical Visit model
class Encounter(BaseModel):
entry_datetime: str
exit_datetime: str
exit_datetime: Optional[str]
location: str
type: str
subtype: Optional[str]
exhibition_type: str = 'default'
active_cids: List[str]
responsible: Optional[Responsible]
clinical_motivation: Optional[str]
clinical_outcome: Optional[str]
clinical_exams: List[ClinicalExam]
filter_tags: List[str]


Expand Down
37 changes: 18 additions & 19 deletions app/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
import jwt
import hashlib
import json
from typing import Literal
import os

from google.cloud import bigquery
from google.oauth2 import service_account
from asyncer import asyncify
from loguru import logger
from passlib.context import CryptContext

Expand Down Expand Up @@ -124,24 +128,19 @@ async def get_instance(Model, table, slug=None, code=None):
return table[slug]


def read_timestamp(timestamp: int, output_format=Literal['date','datetime']) -> str:
if output_format == 'date':
denominator = 1000
str_format = "%Y-%m-%d"
elif output_format == 'datetime':
denominator = 1
str_format = "%Y-%m-%d %H:%M:%S"
else:
raise ValueError("Invalid format")
async def read_bq(query, from_file="/tmp/credentials.json"):
logger.debug(f"""Reading BigQuery with query (QUERY_PREVIEW_ENABLED={
os.environ['QUERY_PREVIEW_ENABLED']
}): {query}""")

try:
value = datetime(1970, 1, 1) + timedelta(seconds=timestamp/denominator)
except Exception as exc:
logger.error(f"Invalid timestamp: {timestamp} from {exc}")
return None
def execute_job():
credentials = service_account.Credentials.from_service_account_file(
from_file,
)
client = bigquery.Client(credentials=credentials)
row_iterator = client.query_and_wait(query)
return [dict(row) for row in row_iterator]

return value.strftime(str_format)
rows = await asyncify(execute_job)()

def normalize_case(text):
# TODO
return text
return rows
16 changes: 15 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ urllib3 = "2.0.7"
idna = "3.7"
basedosdados = "^2.0.0b16"
nltk = "^3.9.1"
asyncer = "^0.0.8"


[tool.poetry.group.dev.dependencies]
Expand Down

0 comments on commit d745a88

Please sign in to comment.