This commit is contained in:
10
Dockerfile
10
Dockerfile
@ -67,6 +67,16 @@ FROM builder-base AS production
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
# ────────────────────── WEASYPRINT SYSTEM DEPENDENCIES ──────────────────────
|
||||||
|
# These are the exact packages required for WeasyPrint to work on Debian Bookworm
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y gcc libpq-dev \
|
||||||
|
libcairo2 libcairo2-dev libpangocairo-1.0-0 weasyprint && \
|
||||||
|
apt clean && \
|
||||||
|
rm -rf /var/cache/apt/*
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
RUN chown -R appuser:appuser /app
|
RUN chown -R appuser:appuser /app
|
||||||
|
|
||||||
COPY --from=python-base /app/.python /app/.python
|
COPY --from=python-base /app/.python /app/.python
|
||||||
|
|||||||
@ -31,6 +31,8 @@ dependencies = [
|
|||||||
"pyjwt==2.10.1",
|
"pyjwt==2.10.1",
|
||||||
"xmltodict==1.0.2",
|
"xmltodict==1.0.2",
|
||||||
"python-multipart==0.0.20",
|
"python-multipart==0.0.20",
|
||||||
|
"weasyprint==66.0",
|
||||||
|
"lxml==6.0.2; sys_platform != 'win32'",
|
||||||
# CLI
|
# CLI
|
||||||
"typer-slim==0.16.1",
|
"typer-slim==0.16.1",
|
||||||
]
|
]
|
||||||
|
|||||||
33
src/apps/remd/dependencies.py
Normal file
33
src/apps/remd/dependencies.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
from anyio import Path
|
||||||
|
from fastapi import HTTPException
|
||||||
|
from lxml import etree # type: ignore
|
||||||
|
from weasyprint import HTML # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
async def get_parsable_ids():
|
||||||
|
parsable_files_dir = await Path('/app/apps/remd/xls').resolve()
|
||||||
|
parsable_ids: list[str] = [
|
||||||
|
file.name.split('.')[0]
|
||||||
|
async for file in parsable_files_dir.iterdir()
|
||||||
|
if await file.is_file()
|
||||||
|
]
|
||||||
|
|
||||||
|
return parsable_ids
|
||||||
|
|
||||||
|
|
||||||
|
async def convert_aemd_to_pdf(xml_str: bytes, docKind: str):
|
||||||
|
xml = etree.fromstring(xml_str) # type: ignore
|
||||||
|
xsl = etree.parse(f'/app/apps/remd/xls/{docKind}.xsl') # type: ignore
|
||||||
|
transform = etree.XSLT(xsl) # type: ignore
|
||||||
|
|
||||||
|
html = transform(xml) # type: ignore
|
||||||
|
html_str = etree.tostring( # type: ignore
|
||||||
|
html, pretty_print=True, encoding='unicode', method='html'
|
||||||
|
)
|
||||||
|
|
||||||
|
pdf = HTML(string=html_str).write_pdf() # type: ignore
|
||||||
|
|
||||||
|
if not pdf:
|
||||||
|
raise HTTPException(status_code=500, detail='PDF not generated')
|
||||||
|
|
||||||
|
return pdf
|
||||||
1221
src/apps/remd/xls/110.xsl
Normal file
1221
src/apps/remd/xls/110.xsl
Normal file
File diff suppressed because it is too large
Load Diff
1928
src/apps/remd/xls/111.xsl
Normal file
1928
src/apps/remd/xls/111.xsl
Normal file
File diff suppressed because it is too large
Load Diff
1374
src/apps/remd/xls/119.xsl
Normal file
1374
src/apps/remd/xls/119.xsl
Normal file
File diff suppressed because it is too large
Load Diff
1161
src/apps/remd/xls/122.xsl
Normal file
1161
src/apps/remd/xls/122.xsl
Normal file
File diff suppressed because it is too large
Load Diff
2019
src/apps/remd/xls/148.xsl
Normal file
2019
src/apps/remd/xls/148.xsl
Normal file
File diff suppressed because it is too large
Load Diff
1329
src/apps/remd/xls/75.xsl
Normal file
1329
src/apps/remd/xls/75.xsl
Normal file
File diff suppressed because it is too large
Load Diff
3702
src/apps/remd/xls/92.xsl
Normal file
3702
src/apps/remd/xls/92.xsl
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,3 +1,5 @@
|
|||||||
|
import base64
|
||||||
|
import io
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
from json import dumps
|
from json import dumps
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
@ -5,8 +7,10 @@ from secrets import token_urlsafe
|
|||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Body, Depends, UploadFile, status
|
from fastapi import APIRouter, Body, Depends, UploadFile, status
|
||||||
|
from fastapi.responses import StreamingResponse
|
||||||
from orjson import loads
|
from orjson import loads
|
||||||
|
|
||||||
|
from apps.remd.dependencies import convert_aemd_to_pdf, get_parsable_ids
|
||||||
from apps.tdn.auth import token
|
from apps.tdn.auth import token
|
||||||
from apps.users.auth import login
|
from apps.users.auth import login
|
||||||
from apps.users.models import User
|
from apps.users.models import User
|
||||||
@ -28,13 +32,13 @@ router = APIRouter(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@cache_response(ttl=600, namespace='main')
|
|
||||||
@router.get(
|
@router.get(
|
||||||
'/getProfile',
|
'/getProfile',
|
||||||
responses={
|
responses={
|
||||||
status.HTTP_200_OK: {'model': vs.ProfileModel},
|
status.HTTP_200_OK: {'model': vs.ProfileModel},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@cache_response(ttl=600, namespace='main')
|
||||||
async def get_profile(user: Annotated[User, Depends(login)]):
|
async def get_profile(user: Annotated[User, Depends(login)]):
|
||||||
"""
|
"""
|
||||||
Get profile of user.
|
Get profile of user.
|
||||||
@ -42,13 +46,13 @@ async def get_profile(user: Annotated[User, Depends(login)]):
|
|||||||
return await c.vitacore_api.getProfile(user.vita_id)
|
return await c.vitacore_api.getProfile(user.vita_id)
|
||||||
|
|
||||||
|
|
||||||
@cache_response(ttl=3600, namespace='main')
|
|
||||||
@router.get(
|
@router.get(
|
||||||
'/getDepartments',
|
'/getDepartments',
|
||||||
responses={
|
responses={
|
||||||
status.HTTP_200_OK: {'model': vs.OrganizationsModel},
|
status.HTTP_200_OK: {'model': vs.OrganizationsModel},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@cache_response(ttl=3600, namespace='main')
|
||||||
async def get_departments():
|
async def get_departments():
|
||||||
"""
|
"""
|
||||||
Get list of departments.
|
Get list of departments.
|
||||||
@ -56,10 +60,10 @@ async def get_departments():
|
|||||||
return await c.vitacore_api.getDepartments()
|
return await c.vitacore_api.getDepartments()
|
||||||
|
|
||||||
|
|
||||||
@cache_response(ttl=3600, namespace='main')
|
|
||||||
@router.get(
|
@router.get(
|
||||||
'/getWorkers', responses={status.HTTP_200_OK: {'model': vs.WorkersModel}}
|
'/getWorkers', responses={status.HTTP_200_OK: {'model': vs.WorkersModel}}
|
||||||
)
|
)
|
||||||
|
@cache_response(ttl=3600, namespace='main')
|
||||||
async def get_workers(
|
async def get_workers(
|
||||||
user: Annotated[User, Depends(login)], departmentId: str
|
user: Annotated[User, Depends(login)], departmentId: str
|
||||||
):
|
):
|
||||||
@ -69,11 +73,11 @@ async def get_workers(
|
|||||||
return await c.vitacore_api.getWorkers(departmentId)
|
return await c.vitacore_api.getWorkers(departmentId)
|
||||||
|
|
||||||
|
|
||||||
@cache_response(ttl=3600, namespace='main')
|
|
||||||
@router.get(
|
@router.get(
|
||||||
'/getSpecs',
|
'/getSpecs',
|
||||||
responses={status.HTTP_200_OK: {'model': vs.SpecsV021Model}},
|
responses={status.HTTP_200_OK: {'model': vs.SpecsV021Model}},
|
||||||
)
|
)
|
||||||
|
@cache_response(ttl=3600, namespace='main')
|
||||||
async def get_specs(user: Annotated[User, Depends(login)]):
|
async def get_specs(user: Annotated[User, Depends(login)]):
|
||||||
"""
|
"""
|
||||||
Get list of specialties.
|
Get list of specialties.
|
||||||
@ -198,7 +202,10 @@ async def queue(_: Annotated[User, Depends(login)]):
|
|||||||
|
|
||||||
|
|
||||||
@router.get('/aemd')
|
@router.get('/aemd')
|
||||||
async def get_aemd(user: Annotated[User, Depends(login)]):
|
async def get_aemd(
|
||||||
|
user: Annotated[User, Depends(login)],
|
||||||
|
parsable_ids: Annotated[list[str], Depends(get_parsable_ids)],
|
||||||
|
):
|
||||||
profile = await c.vitacore_api.getProfile(user.vita_id)
|
profile = await c.vitacore_api.getProfile(user.vita_id)
|
||||||
snils = profile.SNILS.replace('-', '').replace(' ', '')
|
snils = profile.SNILS.replace('-', '').replace(' ', '')
|
||||||
docs = await c.aemd_api.searchRegistryItem(patient_snils=snils)
|
docs = await c.aemd_api.searchRegistryItem(patient_snils=snils)
|
||||||
@ -206,6 +213,9 @@ async def get_aemd(user: Annotated[User, Depends(login)]):
|
|||||||
return_items: list[s.AEMDReturnFile] = []
|
return_items: list[s.AEMDReturnFile] = []
|
||||||
|
|
||||||
for item in items:
|
for item in items:
|
||||||
|
if item['DocKind'] not in parsable_ids:
|
||||||
|
continue
|
||||||
|
|
||||||
is_cached = await cache.get(f'aemd:{user.vita_id}:{item["emdrId"]}')
|
is_cached = await cache.get(f'aemd:{user.vita_id}:{item["emdrId"]}')
|
||||||
|
|
||||||
return_items.append(
|
return_items.append(
|
||||||
@ -232,13 +242,25 @@ async def post_aemd(user: Annotated[User, Depends(login)], emdrId: str):
|
|||||||
|
|
||||||
|
|
||||||
@router.get('/aemd/{emdrId}')
|
@router.get('/aemd/{emdrId}')
|
||||||
async def get_aemd_file(user: Annotated[User, Depends(login)], emdrId: str):
|
async def get_aemd_file(
|
||||||
|
user: Annotated[User, Depends(login)], emdrId: str, docKind: str
|
||||||
|
):
|
||||||
data = await cache.get(f'aemd:{user.vita_id}:{emdrId}')
|
data = await cache.get(f'aemd:{user.vita_id}:{emdrId}')
|
||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
raise e.NotFoundException(status_code=404, detail='File not found')
|
raise e.NotFoundException(status_code=404, detail='File not found')
|
||||||
|
|
||||||
return loads(data)
|
b64 = loads(data)['data']
|
||||||
|
decoded = base64.b64decode(b64)
|
||||||
|
pdf = await convert_aemd_to_pdf(decoded, docKind)
|
||||||
|
|
||||||
|
return StreamingResponse(
|
||||||
|
io.BytesIO(pdf),
|
||||||
|
media_type='application/pdf',
|
||||||
|
headers={
|
||||||
|
'Content-Disposition': f'attachment; filename="{emdrId}.pdf"'
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post('/measurement', status_code=status.HTTP_202_ACCEPTED)
|
@router.post('/measurement', status_code=status.HTTP_202_ACCEPTED)
|
||||||
|
|||||||
@ -26,9 +26,21 @@ class Config:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.version = 1
|
self.version = 1
|
||||||
self.disable_existing_loggers = False
|
self.disable_existing_loggers = False
|
||||||
|
self.formatters = self._get_formatters()
|
||||||
self.handlers = self._get_handlers()
|
self.handlers = self._get_handlers()
|
||||||
self.loggers = self._get_loggers()
|
self.loggers = self._get_loggers()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_formatters() -> dict[str, Any]:
|
||||||
|
# Common formatter that includes logger name
|
||||||
|
fmt = '%(asctime)s | %(levelname)-8s | %(message)s'
|
||||||
|
return {
|
||||||
|
'default': {
|
||||||
|
'format': fmt,
|
||||||
|
'datefmt': '%Y-%m-%d %H:%M:%S',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_handlers():
|
def _get_handlers():
|
||||||
handlers: dict[str, Any] = {
|
handlers: dict[str, Any] = {
|
||||||
@ -36,6 +48,7 @@ class Config:
|
|||||||
'class': 'logging.StreamHandler',
|
'class': 'logging.StreamHandler',
|
||||||
'level': logging.INFO,
|
'level': logging.INFO,
|
||||||
'stream': 'ext://sys.stderr',
|
'stream': 'ext://sys.stderr',
|
||||||
|
'formatter': 'default',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,12 +66,16 @@ class Config:
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loggers['fontTools'] = {'level': logging.CRITICAL, 'propagate': False}
|
||||||
|
loggers['weasyprint'] = {'level': logging.CRITICAL, 'propagate': False}
|
||||||
|
|
||||||
return loggers
|
return loggers
|
||||||
|
|
||||||
def render(self):
|
def render(self):
|
||||||
return {
|
return {
|
||||||
'version': self.version,
|
'version': self.version,
|
||||||
'disable_existing_loggers': self.disable_existing_loggers,
|
'disable_existing_loggers': self.disable_existing_loggers,
|
||||||
|
'formatters': self.formatters,
|
||||||
'handlers': self.handlers,
|
'handlers': self.handlers,
|
||||||
'loggers': self.loggers,
|
'loggers': self.loggers,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user