Патч
All checks were successful
Build And Push / publish (push) Successful in 4m45s

This commit is contained in:
2025-12-02 11:38:31 +03:00
parent f0d72e6af9
commit 1a45238dfc
12 changed files with 12825 additions and 7 deletions

View File

@ -67,6 +67,16 @@ FROM builder-base AS production
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
COPY --from=python-base /app/.python /app/.python

View File

@ -31,6 +31,8 @@ dependencies = [
"pyjwt==2.10.1",
"xmltodict==1.0.2",
"python-multipart==0.0.20",
"weasyprint==66.0",
"lxml==6.0.2; sys_platform != 'win32'",
# CLI
"typer-slim==0.16.1",
]

View 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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,5 @@
import base64
import io
from datetime import UTC, datetime
from json import dumps
from logging import getLogger
@ -5,8 +7,10 @@ from secrets import token_urlsafe
from typing import Annotated
from fastapi import APIRouter, Body, Depends, UploadFile, status
from fastapi.responses import StreamingResponse
from orjson import loads
from apps.remd.dependencies import convert_aemd_to_pdf, get_parsable_ids
from apps.tdn.auth import token
from apps.users.auth import login
from apps.users.models import User
@ -28,13 +32,13 @@ router = APIRouter(
)
@cache_response(ttl=600, namespace='main')
@router.get(
'/getProfile',
responses={
status.HTTP_200_OK: {'model': vs.ProfileModel},
},
)
@cache_response(ttl=600, namespace='main')
async def get_profile(user: Annotated[User, Depends(login)]):
"""
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)
@cache_response(ttl=3600, namespace='main')
@router.get(
'/getDepartments',
responses={
status.HTTP_200_OK: {'model': vs.OrganizationsModel},
},
)
@cache_response(ttl=3600, namespace='main')
async def get_departments():
"""
Get list of departments.
@ -56,10 +60,10 @@ async def get_departments():
return await c.vitacore_api.getDepartments()
@cache_response(ttl=3600, namespace='main')
@router.get(
'/getWorkers', responses={status.HTTP_200_OK: {'model': vs.WorkersModel}}
)
@cache_response(ttl=3600, namespace='main')
async def get_workers(
user: Annotated[User, Depends(login)], departmentId: str
):
@ -69,11 +73,11 @@ async def get_workers(
return await c.vitacore_api.getWorkers(departmentId)
@cache_response(ttl=3600, namespace='main')
@router.get(
'/getSpecs',
responses={status.HTTP_200_OK: {'model': vs.SpecsV021Model}},
)
@cache_response(ttl=3600, namespace='main')
async def get_specs(user: Annotated[User, Depends(login)]):
"""
Get list of specialties.
@ -198,7 +202,10 @@ async def queue(_: Annotated[User, Depends(login)]):
@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)
snils = profile.SNILS.replace('-', '').replace(' ', '')
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] = []
for item in items:
if item['DocKind'] not in parsable_ids:
continue
is_cached = await cache.get(f'aemd:{user.vita_id}:{item["emdrId"]}')
return_items.append(
@ -232,13 +242,25 @@ async def post_aemd(user: Annotated[User, Depends(login)], emdrId: str):
@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}')
if not data:
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)

View File

@ -26,9 +26,21 @@ class Config:
def __init__(self):
self.version = 1
self.disable_existing_loggers = False
self.formatters = self._get_formatters()
self.handlers = self._get_handlers()
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
def _get_handlers():
handlers: dict[str, Any] = {
@ -36,6 +48,7 @@ class Config:
'class': 'logging.StreamHandler',
'level': logging.INFO,
'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
def render(self):
return {
'version': self.version,
'disable_existing_loggers': self.disable_existing_loggers,
'formatters': self.formatters,
'handlers': self.handlers,
'loggers': self.loggers,
}