Патч
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 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

View File

@ -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",
] ]

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 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)

View File

@ -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,
} }