Compare commits
75 Commits
d64320426b
...
latest
| Author | SHA1 | Date | |
|---|---|---|---|
| 344c37c202 | |||
| 5bc260cbfa | |||
| defebaaa4f | |||
| 4016c4fa5d | |||
| d090932cce | |||
| 2333301225 | |||
| 187396c003 | |||
| 9e735c2cc6 | |||
| 0b907afec3 | |||
| 1a45238dfc | |||
| f0d72e6af9 | |||
| 633125cf7b | |||
| 3749f9b0bb | |||
| bf39b8e574 | |||
| 9612a8be67 | |||
| eceefd1dff | |||
| 780cc15c44 | |||
| 84e4801085 | |||
| 50300627ed | |||
| aebaac40f0 | |||
| 18e70ceb7d | |||
| d440ecfc7d | |||
| 8eb149ee0d | |||
| f3c9cb42d6 | |||
| 45a4123708 | |||
| f25e20a8bf | |||
| 0158acb9b9 | |||
| c862c4dd35 | |||
| 439cb147d8 | |||
| 7c36a938d8 | |||
| 98155a0c77 | |||
| 179d2310a3 | |||
| c56468b521 | |||
| 5b0fb8f536 | |||
| d1e327e55c | |||
| a71bed1e18 | |||
| 043de7e034 | |||
| cf1324633d | |||
| b602b75234 | |||
| 44ed7796ab | |||
| e0d7ab5442 | |||
| cb8b48fcca | |||
| 6523a65dd5 | |||
| 265d4b31aa | |||
| db48b14ac5 | |||
| a4239a0c52 | |||
| cb3b138241 | |||
| ebd4d14508 | |||
| 848d314718 | |||
| b16e077c1c | |||
| c78da4c979 | |||
| a34deaedb3 | |||
| 1a7bd7120a | |||
| 87eb228210 | |||
| c3fcd2021d | |||
| c7f439e48f | |||
| 422adfd503 | |||
| ce36e07266 | |||
| 6bc0cf98a3 | |||
| 355ea5c921 | |||
| 5d84ea567d | |||
| 4be216375c | |||
| d553b93825 | |||
| da772336ee | |||
| 92b889faf5 | |||
| eff590df2b | |||
| 365d6b1696 | |||
| 02a35d0441 | |||
| fa83caf49d | |||
| aed85d5201 | |||
| d15666d0e8 | |||
| eae517af1c | |||
| caf0fa3be9 | |||
| bd7162b803 | |||
| e218d0b130 |
@ -20,3 +20,6 @@ uv.lock
|
|||||||
|
|
||||||
# Container
|
# Container
|
||||||
container
|
container
|
||||||
|
|
||||||
|
# Postgres
|
||||||
|
postgres
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -20,3 +20,6 @@ uv.lock
|
|||||||
|
|
||||||
# Container
|
# Container
|
||||||
container
|
container
|
||||||
|
|
||||||
|
# Postgres
|
||||||
|
postgres
|
||||||
|
|||||||
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
|
||||||
|
|||||||
@ -8,14 +8,14 @@ x-app-common: &app-common
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
stop_signal: SIGINT
|
stop_signal: SIGINT
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .test.env
|
||||||
environment:
|
environment:
|
||||||
DEBUG: false
|
|
||||||
DATABASE_URL: "postgresql://postgres:example@db:5432/postgres"
|
DATABASE_URL: "postgresql://postgres:example@db:5432/postgres"
|
||||||
REDIS_URL: "redis://valkey:6379/0"
|
REDIS_URL: "redis://valkey:6379/0"
|
||||||
volumes:
|
volumes:
|
||||||
- "./container/certt.cer:/var/opt/cprocsp/keys/cert.cer"
|
# - "./container/certt.cer:/var/opt/cprocsp/keys/cert.cer"
|
||||||
- "./container/cont:/app/cont"
|
- "./container/test.cer:/var/opt/cprocsp/keys/cert.cer"
|
||||||
|
- "./container/cont2:/app/cont"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
valkey:
|
valkey:
|
||||||
@ -29,6 +29,21 @@ services:
|
|||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:17.2-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- ${POSTGRES_PORT:-5432}:5432
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: example
|
||||||
|
volumes:
|
||||||
|
- "${POSTGRES_DATA:-./postgres}:/var/lib/postgresql/data/"
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD-SHELL", "pg_isready -U postgres" ]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
web:
|
web:
|
||||||
<<: *app-common
|
<<: *app-common
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@ -24,10 +24,15 @@ dependencies = [
|
|||||||
"sqlmodel==0.0.24",
|
"sqlmodel==0.0.24",
|
||||||
# Types
|
# Types
|
||||||
"pydantic==2.11.7",
|
"pydantic==2.11.7",
|
||||||
|
"pydantic-xml==2.18.0",
|
||||||
"pydantic-settings==2.10.1",
|
"pydantic-settings==2.10.1",
|
||||||
"pydantic-extra-types==2.10.5",
|
"pydantic-extra-types==2.10.5",
|
||||||
"semver==3.0.4",
|
"semver==3.0.4",
|
||||||
"pyjwt==2.10.1",
|
"pyjwt==2.10.1",
|
||||||
|
"xmltodict==1.0.2",
|
||||||
|
"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",
|
||||||
]
|
]
|
||||||
@ -50,9 +55,9 @@ _lint = "pre-commit run --all-files"
|
|||||||
|
|
||||||
lint = ["_git", "_lint"]
|
lint = ["_git", "_lint"]
|
||||||
check = "uv pip ls --outdated"
|
check = "uv pip ls --outdated"
|
||||||
run = "uv run --directory ./src/ server.py"
|
run = "uv run --env-file ../.env --directory ./src/ server.py"
|
||||||
manage = "uv run --directory ./src/ manage.py"
|
manage = "uv run --env-file ../.env --directory ./src/ manage.py"
|
||||||
migrate = "uv run --directory ./src/ alembic revision --autogenerate"
|
migrate = "uv run --env-file ../.env --directory ./src/ alembic revision --autogenerate"
|
||||||
|
|
||||||
[tool.uv]
|
[tool.uv]
|
||||||
required-version = ">=0.7.0"
|
required-version = ">=0.7.0"
|
||||||
|
|||||||
@ -24,8 +24,6 @@ def csp_sign(data: str):
|
|||||||
with open(source_path, 'w', encoding='utf-8') as f:
|
with open(source_path, 'w', encoding='utf-8') as f:
|
||||||
f.write(data)
|
f.write(data)
|
||||||
|
|
||||||
print(data)
|
|
||||||
|
|
||||||
cmd = [
|
cmd = [
|
||||||
'cryptcp',
|
'cryptcp',
|
||||||
'-signf',
|
'-signf',
|
||||||
|
|||||||
@ -2,9 +2,12 @@ import secrets
|
|||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
from sqlmodel import select
|
||||||
|
|
||||||
from apps.esia.sign import get_url
|
from apps.esia.sign import get_url
|
||||||
|
from apps.users.models import User
|
||||||
from clients import clients as c
|
from clients import clients as c
|
||||||
|
from database import AsyncSessionDep
|
||||||
from shared import exceptions as e
|
from shared import exceptions as e
|
||||||
from shared.redis import client as cache
|
from shared.redis import client as cache
|
||||||
|
|
||||||
@ -26,7 +29,7 @@ async def login():
|
|||||||
|
|
||||||
|
|
||||||
@router.post('/callback')
|
@router.post('/callback')
|
||||||
async def callback(code: str):
|
async def callback(session: AsyncSessionDep, code: str):
|
||||||
token = None
|
token = None
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
try:
|
try:
|
||||||
@ -42,9 +45,32 @@ async def callback(code: str):
|
|||||||
if token is None:
|
if token is None:
|
||||||
raise e.BadRequestException
|
raise e.BadRequestException
|
||||||
|
|
||||||
await c.esia_api.get_user_info(token.access_token, token.id_token)
|
esia_user = await c.esia_api.get_user_info(
|
||||||
|
token.access_token, token.id_token
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
vita_user = await c.vitacore_api.findBySnils(esia_user.snils)
|
||||||
|
patId = vita_user.patId
|
||||||
|
except e.UnknownException:
|
||||||
|
patId = 'b66a85f1-4aaa-4db8-942a-2de44341824e'
|
||||||
|
# raise e.BadRequestException(detail='Patient not found') from None
|
||||||
|
|
||||||
|
existing_user_stmt = select(User).where(User.vita_id == patId).limit(1)
|
||||||
|
existing_user = (
|
||||||
|
await session.execute(existing_user_stmt)
|
||||||
|
).scalar_one_or_none()
|
||||||
|
|
||||||
|
if existing_user is None:
|
||||||
|
user = User(vita_id=patId)
|
||||||
|
session.add(user)
|
||||||
|
await session.commit()
|
||||||
|
await session.refresh(user)
|
||||||
|
|
||||||
|
else:
|
||||||
|
user = existing_user
|
||||||
|
|
||||||
access_token = secrets.token_urlsafe(32)
|
access_token = secrets.token_urlsafe(32)
|
||||||
cache.set(access_token, access_token)
|
await cache.set(access_token, f'user:{user.id}')
|
||||||
|
|
||||||
return s.Token(access_token=access_token)
|
return s.Token(access_token=access_token)
|
||||||
|
|||||||
0
src/apps/remd/__init__.py
Normal file
0
src/apps/remd/__init__.py
Normal file
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
|
||||||
0
src/apps/remd/v1/__init__.py
Normal file
0
src/apps/remd/v1/__init__.py
Normal file
45
src/apps/remd/v1/router.py
Normal file
45
src/apps/remd/v1/router.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
from logging import getLogger
|
||||||
|
|
||||||
|
import xmltodict
|
||||||
|
from fastapi import APIRouter, HTTPException, Request
|
||||||
|
from orjson import dumps, loads
|
||||||
|
|
||||||
|
from apps.users.v1.schema import AEMDDemandContent
|
||||||
|
from shared.redis import client
|
||||||
|
|
||||||
|
from . import schema as s
|
||||||
|
|
||||||
|
logger = getLogger(__name__)
|
||||||
|
router = APIRouter(
|
||||||
|
prefix='/remd',
|
||||||
|
tags=[
|
||||||
|
'REMD',
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post('/callback')
|
||||||
|
async def callback(request: Request):
|
||||||
|
body_bytes = await request.body()
|
||||||
|
if not body_bytes:
|
||||||
|
logger.warning('Empty body')
|
||||||
|
raise HTTPException(status_code=400, detail='Empty body')
|
||||||
|
|
||||||
|
body = body_bytes.decode('utf-8').replace('\r', '').replace('\n', '')
|
||||||
|
body_dict = xmltodict.parse(body)
|
||||||
|
body = s.Remd.model_validate(body_dict)
|
||||||
|
file_request = body.Envelope.body.sendDocumentFileRequest
|
||||||
|
|
||||||
|
data = await client.get(f'aemd_messages:{file_request.relatesToMessage}')
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
raise HTTPException(status_code=400, detail='Message not found')
|
||||||
|
|
||||||
|
messageData = AEMDDemandContent(**loads(data))
|
||||||
|
await client.delete(f'aemd_messages:{file_request.relatesToMessage}')
|
||||||
|
await client.set(
|
||||||
|
f'aemd:{messageData["vitaId"]}:{messageData["emdrId"]}',
|
||||||
|
dumps(s.AEMDFileData(data=file_request.file.data)),
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
28
src/apps/remd/v1/schema.py
Normal file
28
src/apps/remd/v1/schema.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
from typing import TypedDict
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
class FileModel(BaseModel):
|
||||||
|
data: str
|
||||||
|
|
||||||
|
|
||||||
|
class RequestModel(BaseModel):
|
||||||
|
relatesToMessage: str
|
||||||
|
file: FileModel
|
||||||
|
|
||||||
|
|
||||||
|
class BodyModel(BaseModel):
|
||||||
|
sendDocumentFileRequest: RequestModel
|
||||||
|
|
||||||
|
|
||||||
|
class EnvelopeModel(BaseModel):
|
||||||
|
body: BodyModel = Field(..., alias='s:Body')
|
||||||
|
|
||||||
|
|
||||||
|
class Remd(BaseModel):
|
||||||
|
Envelope: EnvelopeModel = Field(..., alias='s:Envelope')
|
||||||
|
|
||||||
|
|
||||||
|
class AEMDFileData(TypedDict):
|
||||||
|
data: str
|
||||||
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
1834
src/apps/remd/xls/141.xsl
Normal file
1834
src/apps/remd/xls/141.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
0
src/apps/tdn/__init__.py
Normal file
0
src/apps/tdn/__init__.py
Normal file
15
src/apps/tdn/auth.py
Normal file
15
src/apps/tdn/auth.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
from clients import clients as c
|
||||||
|
from shared.redis import client as cache
|
||||||
|
|
||||||
|
|
||||||
|
async def token():
|
||||||
|
access_token = await cache.get('tdn_token')
|
||||||
|
|
||||||
|
if access_token is None:
|
||||||
|
tokens = await c.tdn_api.signin()
|
||||||
|
await cache.set('tdn_token', tokens.accessToken, 60)
|
||||||
|
|
||||||
|
return tokens.accessToken
|
||||||
|
|
||||||
|
else:
|
||||||
|
return access_token.decode()
|
||||||
0
src/apps/tmk/__init__.py
Normal file
0
src/apps/tmk/__init__.py
Normal file
0
src/apps/tmk/v1/__init__.py
Normal file
0
src/apps/tmk/v1/__init__.py
Normal file
43
src/apps/tmk/v1/router.py
Normal file
43
src/apps/tmk/v1/router.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
from json import dumps
|
||||||
|
from logging import getLogger
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from sqlmodel import select
|
||||||
|
|
||||||
|
from apps.users.models import User
|
||||||
|
from clients import clients as c
|
||||||
|
from database import AsyncSessionDep
|
||||||
|
from shared.exceptions import UnknownException
|
||||||
|
from shared.redis import client as cache
|
||||||
|
|
||||||
|
from . import schema as s
|
||||||
|
|
||||||
|
logger = getLogger(__name__)
|
||||||
|
router = APIRouter(
|
||||||
|
prefix='/tmk',
|
||||||
|
tags=[
|
||||||
|
'TMK',
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post('/update')
|
||||||
|
async def update(session: AsyncSessionDep, update: s.TMKUpdate):
|
||||||
|
guid = update.tmk_guid
|
||||||
|
info = await c.tmk_api.getInfo(guid)
|
||||||
|
snils = info.patient_snils
|
||||||
|
|
||||||
|
try:
|
||||||
|
patient = await c.vitacore_api.findBySnils(snils)
|
||||||
|
except UnknownException:
|
||||||
|
return
|
||||||
|
|
||||||
|
user_stmt = select(User).where(User.vita_id == patient.patId).limit(1)
|
||||||
|
user = await session.scalar(user_stmt)
|
||||||
|
|
||||||
|
if user is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
key = f'tmk:{user.id}:{guid}'
|
||||||
|
value = {'id': info.id, 'status': info.tmk_status_name, 'is_read': False}
|
||||||
|
await cache.set(key, dumps(value))
|
||||||
5
src/apps/tmk/v1/schema.py
Normal file
5
src/apps/tmk/v1/schema.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class TMKUpdate(BaseModel):
|
||||||
|
tmk_guid: str
|
||||||
@ -2,7 +2,10 @@ from typing import Annotated
|
|||||||
|
|
||||||
from fastapi import Depends
|
from fastapi import Depends
|
||||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||||
|
from sqlmodel import select
|
||||||
|
|
||||||
|
from apps.users.models import User
|
||||||
|
from database import AsyncSessionDep
|
||||||
from shared import exceptions as e
|
from shared import exceptions as e
|
||||||
from shared.redis import client as cache
|
from shared.redis import client as cache
|
||||||
|
|
||||||
@ -10,11 +13,23 @@ BEARER = HTTPBearer()
|
|||||||
|
|
||||||
|
|
||||||
async def login(
|
async def login(
|
||||||
|
session: AsyncSessionDep,
|
||||||
credentials: Annotated[HTTPAuthorizationCredentials, Depends(BEARER)],
|
credentials: Annotated[HTTPAuthorizationCredentials, Depends(BEARER)],
|
||||||
):
|
):
|
||||||
is_exist = cache.get(credentials.credentials)
|
user = await cache.get(credentials.credentials)
|
||||||
|
|
||||||
if is_exist is None:
|
if user is None:
|
||||||
raise e.UnauthorizedException
|
raise e.UnauthorizedException
|
||||||
|
|
||||||
return True
|
try:
|
||||||
|
_, user_id = user.decode().split(':')
|
||||||
|
except ValueError:
|
||||||
|
raise e.UnauthorizedException from None
|
||||||
|
|
||||||
|
user_model_stmt = select(User).where(User.id == int(user_id))
|
||||||
|
user_model = (await session.execute(user_model_stmt)).scalar_one_or_none()
|
||||||
|
|
||||||
|
if user_model is None:
|
||||||
|
raise e.UnauthorizedException
|
||||||
|
|
||||||
|
return user_model
|
||||||
|
|||||||
20
src/apps/users/models.py
Normal file
20
src/apps/users/models.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlmodel import Field, SQLModel
|
||||||
|
|
||||||
|
|
||||||
|
class User(SQLModel, table=True):
|
||||||
|
__tablename__: str = 'users' # type: ignore
|
||||||
|
|
||||||
|
id: int = Field(default=None, primary_key=True)
|
||||||
|
vita_id: str = Field(unique=True)
|
||||||
|
|
||||||
|
|
||||||
|
class UserDocument(SQLModel, table=True):
|
||||||
|
__tablename__: str = 'user_documents' # type: ignore
|
||||||
|
|
||||||
|
id: int = Field(default=None, primary_key=True)
|
||||||
|
user_id: str = Field()
|
||||||
|
token: str
|
||||||
|
base64: str | None = Field(default=None)
|
||||||
|
created_at: datetime
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,14 +1,24 @@
|
|||||||
from datetime import datetime
|
import base64
|
||||||
|
from datetime import UTC, datetime
|
||||||
from json import dumps
|
from json import dumps
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from typing import Annotated, Any
|
from secrets import token_urlsafe
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Body, Depends, status
|
from fastapi import APIRouter, Body, Depends, UploadFile, status
|
||||||
|
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.auth import login
|
||||||
|
from apps.users.models import User
|
||||||
|
from clients import clients as c
|
||||||
|
from clients.tmk import schema as ts
|
||||||
|
from clients.vitacore import schema as vs
|
||||||
|
from shared import exceptions as e
|
||||||
from shared.redis import client as cache
|
from shared.redis import client as cache
|
||||||
|
|
||||||
from . import mock
|
from . import schema as s
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
router = APIRouter(
|
router = APIRouter(
|
||||||
@ -19,17 +29,394 @@ router = APIRouter(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
'/getProfile',
|
||||||
|
responses={
|
||||||
|
status.HTTP_200_OK: {'model': vs.ProfileModel},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
async def get_profile(user: Annotated[User, Depends(login)]):
|
||||||
|
"""
|
||||||
|
Get profile of user.
|
||||||
|
"""
|
||||||
|
return await c.vitacore_api.getProfile(user.vita_id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
'/getDepartments',
|
||||||
|
responses={
|
||||||
|
status.HTTP_200_OK: {'model': vs.OrganizationsModel},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
async def get_departments():
|
||||||
|
"""
|
||||||
|
Get list of departments.
|
||||||
|
"""
|
||||||
|
return await c.vitacore_api.getDepartments()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
'/getWorkers', responses={status.HTTP_200_OK: {'model': vs.WorkersModel}}
|
||||||
|
)
|
||||||
|
async def get_workers(
|
||||||
|
user: Annotated[User, Depends(login)], departmentId: str
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Get list of workers by department id.
|
||||||
|
"""
|
||||||
|
return await c.vitacore_api.getWorkers(departmentId)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
'/getSpecs',
|
||||||
|
responses={status.HTTP_200_OK: {'model': vs.SpecsV021Model}},
|
||||||
|
)
|
||||||
|
async def get_specs(user: Annotated[User, Depends(login)]):
|
||||||
|
"""
|
||||||
|
Get list of specialties.
|
||||||
|
"""
|
||||||
|
return await c.vitacore_api.getSpecsV021()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
'/getEntries', responses={status.HTTP_200_OK: {'model': vs.EntriesModel}}
|
||||||
|
)
|
||||||
|
async def get_entries(user: Annotated[User, Depends(login)]):
|
||||||
|
"""
|
||||||
|
Get list of entries for user by id.
|
||||||
|
"""
|
||||||
|
return await c.vitacore_api.getEntries(user.vita_id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('/getVaccsReport')
|
||||||
|
async def get_vaccs_report(
|
||||||
|
user: Annotated[User, Depends(login)], resultId: str | None = None
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Get report of vaccinations for user by id.
|
||||||
|
"""
|
||||||
|
if resultId is not None:
|
||||||
|
return await c.vitacore_api.getDiagResultFile(resultId)
|
||||||
|
|
||||||
|
return await c.vitacore_api.getVaccsReport(user.vita_id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('/getMedExamDict')
|
||||||
|
async def get_med_exam_dict(user: Annotated[User, Depends(login)]):
|
||||||
|
"""
|
||||||
|
Get medical examination dictionary.
|
||||||
|
"""
|
||||||
|
return await c.vitacore_api.getMedExamDict()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('/getRoutesList')
|
||||||
|
async def get_routes_list(user: Annotated[User, Depends(login)]):
|
||||||
|
"""
|
||||||
|
Get list of routes.
|
||||||
|
"""
|
||||||
|
return await c.vitacore_api.getRoutesList(user.vita_id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('/getHospExaminations')
|
||||||
|
async def get_hosp_examinations(
|
||||||
|
user: Annotated[User, Depends(login)], examId: str | None = None
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Get list of hospital examinations.
|
||||||
|
"""
|
||||||
|
return await c.vitacore_api.getHospExaminations(
|
||||||
|
user.vita_id,
|
||||||
|
examId,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('/getCurrHosp')
|
||||||
|
async def get_curr_hosp(user: Annotated[User, Depends(login)]):
|
||||||
|
"""
|
||||||
|
Get current hospitalization.
|
||||||
|
"""
|
||||||
|
return await c.vitacore_api.getCurrHosp(user.vita_id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('/getHosps')
|
||||||
|
async def get_hosps(user: Annotated[User, Depends(login)]):
|
||||||
|
"""
|
||||||
|
Get list of hospitals.
|
||||||
|
"""
|
||||||
|
return await c.vitacore_api.getHosps(user.vita_id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('/getHospRecommendations')
|
||||||
|
async def get_hosp_recommendations(user: Annotated[User, Depends(login)]):
|
||||||
|
"""
|
||||||
|
Get list of recommended hospitals.
|
||||||
|
"""
|
||||||
|
return await c.vitacore_api.getHospRecommendations(user.vita_id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('/getHospRoutes')
|
||||||
|
async def get_hosp_routes(user: Annotated[User, Depends(login)]):
|
||||||
|
"""
|
||||||
|
Get list of recommended hospitals.
|
||||||
|
"""
|
||||||
|
return await c.vitacore_api.getHospRoutes(user.vita_id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('/getDiagnosticResults')
|
||||||
|
async def get_diagnostic_results(user: Annotated[User, Depends(login)]):
|
||||||
|
"""
|
||||||
|
Get list of diagnostic results.
|
||||||
|
"""
|
||||||
|
return await c.vitacore_api.getDiagnosticResults(user.vita_id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('/getELNs')
|
||||||
|
async def get_eln(user: Annotated[User, Depends(login)]):
|
||||||
|
"""
|
||||||
|
Get list of ELNs.
|
||||||
|
"""
|
||||||
|
return await c.vitacore_api.getELNs(user.vita_id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('/getPatFLG')
|
||||||
|
async def get_pat_flg(user: Annotated[User, Depends(login)]):
|
||||||
|
"""
|
||||||
|
Get list of ELNs.
|
||||||
|
"""
|
||||||
|
return await c.vitacore_api.getPatFLG(user.vita_id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('/queue', response_model=list[ts.QueueModel])
|
||||||
|
async def queue(_: Annotated[User, Depends(login)]):
|
||||||
|
"""
|
||||||
|
Get list of VKS queues.
|
||||||
|
"""
|
||||||
|
return await c.tmk_api.getQueue()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('/aemd')
|
||||||
|
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)
|
||||||
|
items: list[s.AEMDFile] = docs['items']
|
||||||
|
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(
|
||||||
|
s.AEMDReturnFile(
|
||||||
|
emdrId=item['emdrId'],
|
||||||
|
registrationDate=item['registrationDate'],
|
||||||
|
DocKind=item['DocKind'],
|
||||||
|
IsSemd=item['IsSemd'],
|
||||||
|
isCached=bool(is_cached),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return return_items
|
||||||
|
|
||||||
|
|
||||||
|
@router.post('/aemd', status_code=status.HTTP_202_ACCEPTED)
|
||||||
|
async def post_aemd(user: Annotated[User, Depends(login)], emdrId: str):
|
||||||
|
messageId = token_urlsafe(32)
|
||||||
|
data = s.AEMDDemandContent(
|
||||||
|
messageId=messageId, emdrId=emdrId, vitaId=user.vita_id
|
||||||
|
)
|
||||||
|
await cache.set(f'aemd_messages:{messageId}', dumps(data))
|
||||||
|
await c.aemd_api.demandContent(messageId=messageId, emdrId=emdrId)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('/aemd/{emdrId}')
|
||||||
|
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')
|
||||||
|
|
||||||
|
b64 = loads(data)['data']
|
||||||
|
decoded = base64.b64decode(b64)
|
||||||
|
pdf = await convert_aemd_to_pdf(decoded, docKind)
|
||||||
|
b64_pdf = base64.b64encode(pdf).decode('utf-8')
|
||||||
|
|
||||||
|
return {
|
||||||
|
'filename': f'{emdrId}.pdf',
|
||||||
|
'content_type': 'application/pdf',
|
||||||
|
'data': b64_pdf,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.post('/measurement', status_code=status.HTTP_202_ACCEPTED)
|
@router.post('/measurement', status_code=status.HTTP_202_ACCEPTED)
|
||||||
async def measurement(
|
async def measurement(
|
||||||
user: Annotated[str, Depends(login)],
|
tdn_access_token: Annotated[str, Depends(token)],
|
||||||
|
user: Annotated[User, Depends(login)],
|
||||||
ad: Annotated[int, Body()],
|
ad: Annotated[int, Body()],
|
||||||
sd: Annotated[int, Body()],
|
sd: Annotated[int, Body()],
|
||||||
pulse: Annotated[int, Body()],
|
pulse: Annotated[int, Body()],
|
||||||
created_at: Annotated[datetime, Body()],
|
|
||||||
comment: Annotated[str, Body()],
|
comment: Annotated[str, Body()],
|
||||||
status: Annotated[str, Body()],
|
status: Annotated[int, Body(ge=1, le=3)],
|
||||||
|
serial_number: Annotated[str, Body()],
|
||||||
|
ekg: UploadFile,
|
||||||
):
|
):
|
||||||
created = created_at.strftime('%Y-%m-%d %H:%M:%S')
|
vitaId = '109df850-2268-49fb-bb78-2e0f3c57314d'
|
||||||
|
patient = await c.tdn_api.patient_search(tdn_access_token, vitaId)
|
||||||
|
patientUid = patient['items'][0]['uid']
|
||||||
|
observations = await c.tdn_api.observations_search(
|
||||||
|
tdn_access_token, patientUid
|
||||||
|
)
|
||||||
|
|
||||||
|
if observations.total == 0:
|
||||||
|
raise e.NotFoundException(detail='No observations found')
|
||||||
|
|
||||||
|
ad_obsrvMeasurementUid = None
|
||||||
|
ad_observationUid = None
|
||||||
|
health_obsrvMeasurementUid = None
|
||||||
|
health_observationUid = None
|
||||||
|
|
||||||
|
sad_measurement = None
|
||||||
|
dad_measurement = None
|
||||||
|
pulse_measurement = None
|
||||||
|
health_measurement = None
|
||||||
|
health_measurement_created_at = None
|
||||||
|
|
||||||
|
observations = observations.items[::-1]
|
||||||
|
for observation in observations:
|
||||||
|
observation_measurements = (
|
||||||
|
await c.tdn_api.observations_measurement_search(
|
||||||
|
tdn_access_token, observation.uid
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for measurement in observation_measurements.items:
|
||||||
|
if measurement.measurement.code == 'ADPULSE':
|
||||||
|
ad_obsrvMeasurementUid = measurement.uid
|
||||||
|
ad_observationUid = measurement.observationUid
|
||||||
|
|
||||||
|
for metric in measurement.obsrvMtMetrics:
|
||||||
|
if metric.metric.code == 'SAD':
|
||||||
|
sad_measurement = metric.uid
|
||||||
|
|
||||||
|
if metric.metric.code == 'DAD':
|
||||||
|
dad_measurement = metric.uid
|
||||||
|
|
||||||
|
if metric.metric.code == 'PULSE':
|
||||||
|
pulse_measurement = metric.uid
|
||||||
|
|
||||||
|
if measurement.measurement.code == 'HEALTH':
|
||||||
|
health_obsrvMeasurementUid = measurement.uid
|
||||||
|
health_observationUid = measurement.observationUid
|
||||||
|
|
||||||
|
for metric in measurement.obsrvMtMetrics:
|
||||||
|
if metric.metric.code == 'HEALTH' and (
|
||||||
|
health_measurement_created_at is None
|
||||||
|
or metric.createdAt > health_measurement_created_at
|
||||||
|
):
|
||||||
|
health_measurement = metric.uid
|
||||||
|
health_measurement_created_at = metric.createdAt
|
||||||
|
|
||||||
|
if (
|
||||||
|
not ad_obsrvMeasurementUid
|
||||||
|
or not sad_measurement
|
||||||
|
or not dad_measurement
|
||||||
|
or not pulse_measurement
|
||||||
|
or not ad_observationUid
|
||||||
|
or not health_obsrvMeasurementUid
|
||||||
|
or not health_observationUid
|
||||||
|
or not health_measurement
|
||||||
|
or not health_measurement_created_at
|
||||||
|
):
|
||||||
|
ad_obsrvMeasurementUid = None
|
||||||
|
sad_measurement = None
|
||||||
|
dad_measurement = None
|
||||||
|
pulse_measurement = None
|
||||||
|
ad_observationUid = None
|
||||||
|
health_obsrvMeasurementUid = None
|
||||||
|
health_observationUid = None
|
||||||
|
health_measurement = None
|
||||||
|
health_measurement_created_at = None
|
||||||
|
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
if not ad_obsrvMeasurementUid or not ad_observationUid:
|
||||||
|
raise e.NotFoundException(detail='No ADPULSE measurement found')
|
||||||
|
|
||||||
|
if not sad_measurement:
|
||||||
|
raise e.NotFoundException(detail='No SAD measurement found')
|
||||||
|
|
||||||
|
if not dad_measurement:
|
||||||
|
raise e.NotFoundException(detail='No DAD measurement found')
|
||||||
|
|
||||||
|
if not pulse_measurement:
|
||||||
|
raise e.NotFoundException(detail='No PULSE measurement found')
|
||||||
|
|
||||||
|
if not health_obsrvMeasurementUid or not health_observationUid:
|
||||||
|
raise e.NotFoundException(detail='No HEALTH measurement found')
|
||||||
|
|
||||||
|
if not health_measurement:
|
||||||
|
raise e.NotFoundException(detail='No HEALTH measurement found')
|
||||||
|
|
||||||
|
if not health_obsrvMeasurementUid or not health_observationUid:
|
||||||
|
raise e.NotFoundException(detail='No HEALTH measurement found')
|
||||||
|
|
||||||
|
ad_series = await c.tdn_api.create_series(
|
||||||
|
tdn_access_token,
|
||||||
|
ad_observationUid,
|
||||||
|
ad_obsrvMeasurementUid,
|
||||||
|
)
|
||||||
|
|
||||||
|
ad_series_uid = ad_series.uid
|
||||||
|
|
||||||
|
# SAD
|
||||||
|
await c.tdn_api.create_series_values(
|
||||||
|
tdn_access_token, ad_series_uid, sad_measurement, nvalue=sd
|
||||||
|
)
|
||||||
|
|
||||||
|
# DAD
|
||||||
|
await c.tdn_api.create_series_values(
|
||||||
|
tdn_access_token, ad_series_uid, dad_measurement, nvalue=ad
|
||||||
|
)
|
||||||
|
|
||||||
|
# PULSE
|
||||||
|
await c.tdn_api.create_series_values(
|
||||||
|
tdn_access_token, ad_series_uid, pulse_measurement, nvalue=pulse
|
||||||
|
)
|
||||||
|
|
||||||
|
health_series = await c.tdn_api.create_series(
|
||||||
|
tdn_access_token,
|
||||||
|
health_observationUid,
|
||||||
|
health_obsrvMeasurementUid,
|
||||||
|
)
|
||||||
|
|
||||||
|
health_series_uid = health_series.uid
|
||||||
|
|
||||||
|
# HEALTH
|
||||||
|
await c.tdn_api.create_series_values(
|
||||||
|
tdn_access_token,
|
||||||
|
health_series_uid,
|
||||||
|
health_measurement,
|
||||||
|
nvalue=status,
|
||||||
|
svalue=comment,
|
||||||
|
)
|
||||||
|
|
||||||
|
# EKG
|
||||||
|
await c.tdn_api.ekg(
|
||||||
|
tdn_access_token,
|
||||||
|
patientUid,
|
||||||
|
serial_number,
|
||||||
|
ekg,
|
||||||
|
)
|
||||||
|
|
||||||
|
created = datetime.now(UTC).strftime('%Y-%m-%d %H:%M:%S')
|
||||||
data = {
|
data = {
|
||||||
'ad': ad,
|
'ad': ad,
|
||||||
'sd': sd,
|
'sd': sd,
|
||||||
@ -38,117 +425,53 @@ async def measurement(
|
|||||||
'comment': comment,
|
'comment': comment,
|
||||||
'status': status,
|
'status': status,
|
||||||
}
|
}
|
||||||
cache_key = f'tdn:measurement:{user}:{created}'
|
cache_key = f'tdn:measurement:{user.id}:{created}'
|
||||||
cache.set(cache_key, dumps(data))
|
await cache.set(cache_key, dumps(data))
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
@router.get('/measurements')
|
@router.get('/measurements')
|
||||||
async def measurements(
|
async def measurements(
|
||||||
user: Annotated[str, Depends(login)],
|
user: Annotated[User, Depends(login)],
|
||||||
):
|
):
|
||||||
data = [cache.get(key) for key in cache.keys(f'tdn:measurement:{user}:*')]
|
data = [
|
||||||
|
await cache.get(key)
|
||||||
|
for key in await cache.keys(f'tdn:measurement:{user.id}:*')
|
||||||
|
]
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
@router.get('/queue')
|
@router.delete('/account', status_code=status.HTTP_204_NO_CONTENT)
|
||||||
async def queue(user: Annotated[bool, Depends(login)]):
|
async def delete_account(user: Annotated[User, Depends(login)]):
|
||||||
return {
|
return
|
||||||
'id': 60,
|
|
||||||
'guid': '92b3343d-1cb2-47b2-8497-a37e38b6ba24',
|
|
||||||
'tmk_date': None,
|
|
||||||
'created_at': '2025-04-02 15:21:19.890343',
|
|
||||||
'code_mo': '166502',
|
|
||||||
'mo_name': 'ГАУЗ "ГКБ№7 ИМ. М.Н.САДЫКОВА"',
|
|
||||||
'doctor_spec': '109',
|
|
||||||
'doctor_snils': None,
|
|
||||||
'patient_name': 'Иванов Петр Федорович',
|
|
||||||
'patient_birthday': '1997-03-01',
|
|
||||||
'patient_snils': '099-678-666 12',
|
|
||||||
'patient_policy': None,
|
|
||||||
'patient_phone': '+79123456789',
|
|
||||||
'patient_email': None,
|
|
||||||
'tmk_status': 1,
|
|
||||||
'tmk_status_name': 'Создана',
|
|
||||||
'tmk_cancel_reason': None,
|
|
||||||
'tmk_cancel_reason_name': None,
|
|
||||||
'vks_doctor_link': None,
|
|
||||||
'vks_patient_link': None,
|
|
||||||
'doctor_spec_name': 'врач-терапевт',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@router.get('/getDepartments')
|
@router.get('/notifications')
|
||||||
async def get_departments():
|
async def notifications(user: Annotated[User, Depends(login)]):
|
||||||
data: dict[Any, Any] = {}
|
keys = await cache.keys(f'tmk:{user.id}:*')
|
||||||
return data
|
notif: list[dict[str, str | bool]] = []
|
||||||
|
|
||||||
|
for key in keys:
|
||||||
|
val = await cache.get(key)
|
||||||
|
|
||||||
|
if val is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
value = loads(val)
|
||||||
|
notif_val = value.copy()
|
||||||
|
notif.append(notif_val)
|
||||||
|
|
||||||
|
if value['is_read'] is False:
|
||||||
|
value['is_read'] = True
|
||||||
|
await cache.set(key, dumps(value))
|
||||||
|
|
||||||
|
return s.Notifications(
|
||||||
|
notifications=notif,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get('/getSpecs')
|
@router.post('/complaint', status_code=status.HTTP_204_NO_CONTENT)
|
||||||
async def get_specs():
|
async def complaint(
|
||||||
return mock.specs
|
user: Annotated[User, Depends(login)], complaints: s.Complaints
|
||||||
|
):
|
||||||
|
cache_key = f'complaint:{user.vita_id}'
|
||||||
@router.get('/findPat')
|
await cache.set(cache_key, dumps(complaints.complaints))
|
||||||
async def find_pat(user: Annotated[str, Depends(login)]):
|
|
||||||
return mock.findpat[0]
|
|
||||||
|
|
||||||
|
|
||||||
@router.get('/getProfile')
|
|
||||||
async def get_profile(user: Annotated[str, Depends(login)]):
|
|
||||||
return mock.profile[0]
|
|
||||||
|
|
||||||
|
|
||||||
@router.get('/getHosps')
|
|
||||||
async def get_hosps():
|
|
||||||
return mock.hosps
|
|
||||||
|
|
||||||
|
|
||||||
@router.get('/getELNS')
|
|
||||||
async def get_elns(user: Annotated[str, Depends(login)]):
|
|
||||||
return mock.elns[0]
|
|
||||||
|
|
||||||
|
|
||||||
@router.get('/getVaccsReport')
|
|
||||||
async def get_vaccs_report(user: Annotated[str, Depends(login)]):
|
|
||||||
return mock.vacs[0]
|
|
||||||
|
|
||||||
|
|
||||||
@router.get('/getDiagnosticResults')
|
|
||||||
async def get_diagnostic_results(user: Annotated[str, Depends(login)]):
|
|
||||||
return mock.diagnosticResults
|
|
||||||
|
|
||||||
|
|
||||||
@router.get('/getCurrHosp')
|
|
||||||
async def get_curr_hosp(user: Annotated[str, Depends(login)]):
|
|
||||||
return mock.currHosp[0]
|
|
||||||
|
|
||||||
|
|
||||||
@router.get('/getPatFLG')
|
|
||||||
async def get_pat_flg(user: Annotated[str, Depends(login)]):
|
|
||||||
return mock.patFLG[0]
|
|
||||||
|
|
||||||
|
|
||||||
@router.get('/getEntries')
|
|
||||||
async def get_entries(user: Annotated[str, Depends(login)]):
|
|
||||||
return mock.entries[0]
|
|
||||||
|
|
||||||
|
|
||||||
@router.get('/getRoutesList')
|
|
||||||
async def get_routes_list(user: Annotated[str, Depends(login)]):
|
|
||||||
return mock.routesList[0]
|
|
||||||
|
|
||||||
|
|
||||||
@router.get('/getMedExamDict')
|
|
||||||
async def get_med_exam_dict(user: Annotated[str, Depends(login)]):
|
|
||||||
return mock.medexamDict
|
|
||||||
|
|
||||||
|
|
||||||
@router.get('/getHospRecommendations')
|
|
||||||
async def get_hosp_recommendations(user: Annotated[str, Depends(login)]):
|
|
||||||
return mock.hospRecommendations
|
|
||||||
|
|
||||||
|
|
||||||
@router.get('/getHospRoutes')
|
|
||||||
async def get_hosp_routes(user: Annotated[str, Depends(login)]):
|
|
||||||
return mock.hospRoutes
|
|
||||||
|
|||||||
35
src/apps/users/v1/schema.py
Normal file
35
src/apps/users/v1/schema.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
from typing import TypedDict
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class AEMDFile(TypedDict):
|
||||||
|
emdrId: str
|
||||||
|
localUid: str
|
||||||
|
registrationDate: str
|
||||||
|
registrationDateTime: str
|
||||||
|
storeTillDate: str
|
||||||
|
DocKind: str
|
||||||
|
IsSemd: bool
|
||||||
|
|
||||||
|
|
||||||
|
class AEMDReturnFile(TypedDict):
|
||||||
|
emdrId: str
|
||||||
|
registrationDate: str
|
||||||
|
DocKind: str
|
||||||
|
IsSemd: bool
|
||||||
|
isCached: bool
|
||||||
|
|
||||||
|
|
||||||
|
class AEMDDemandContent(TypedDict):
|
||||||
|
messageId: str
|
||||||
|
emdrId: str
|
||||||
|
vitaId: str
|
||||||
|
|
||||||
|
|
||||||
|
class Notifications(TypedDict):
|
||||||
|
notifications: list[dict[str, str | bool]]
|
||||||
|
|
||||||
|
|
||||||
|
class Complaints(BaseModel):
|
||||||
|
complaints: str
|
||||||
0
src/apps/vitacore/__init__.py
Normal file
0
src/apps/vitacore/__init__.py
Normal file
0
src/apps/vitacore/v1/__init__.py
Normal file
0
src/apps/vitacore/v1/__init__.py
Normal file
29
src/apps/vitacore/v1/router.py
Normal file
29
src/apps/vitacore/v1/router.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
from logging import getLogger
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from shared.redis import client as cache
|
||||||
|
|
||||||
|
from . import schema as s
|
||||||
|
|
||||||
|
logger = getLogger(__name__)
|
||||||
|
router = APIRouter(
|
||||||
|
prefix='/vitacore',
|
||||||
|
tags=[
|
||||||
|
'Витакор',
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post('/hospComplaint')
|
||||||
|
async def callback(complaint: s.HospComplaint):
|
||||||
|
value = await cache.get(f'complaint:{complaint.patID}')
|
||||||
|
value = value.decode() if value else ''
|
||||||
|
|
||||||
|
return s.ComplaintData(
|
||||||
|
patId=complaint.patID,
|
||||||
|
complaints=(
|
||||||
|
f'{value} | {complaint.eventId} | {complaint.datetime} | '
|
||||||
|
f'{complaint.MO_id}'
|
||||||
|
),
|
||||||
|
)
|
||||||
16
src/apps/vitacore/v1/schema.py
Normal file
16
src/apps/vitacore/v1/schema.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import TypedDict
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class HospComplaint(BaseModel):
|
||||||
|
patID: str
|
||||||
|
eventId: str
|
||||||
|
datetime: datetime
|
||||||
|
MO_id: str
|
||||||
|
|
||||||
|
|
||||||
|
class ComplaintData(TypedDict):
|
||||||
|
patId: str
|
||||||
|
complaints: str
|
||||||
@ -1,8 +1,16 @@
|
|||||||
|
from .aemd.api import AEMD_API
|
||||||
from .esia.api import ESIA_API
|
from .esia.api import ESIA_API
|
||||||
|
from .tdn.api import TDN_API
|
||||||
|
from .tmk.api import TMK_API
|
||||||
|
from .vitacore.api import VITACORE_API
|
||||||
|
|
||||||
|
|
||||||
class ClientsObject:
|
class ClientsObject:
|
||||||
_esia_api = None
|
_esia_api = None
|
||||||
|
_vitacore_api = None
|
||||||
|
_tdn_api = None
|
||||||
|
_tmk_api = None
|
||||||
|
_aemd_api = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def esia_api(self):
|
def esia_api(self):
|
||||||
@ -11,5 +19,33 @@ class ClientsObject:
|
|||||||
|
|
||||||
return self._esia_api
|
return self._esia_api
|
||||||
|
|
||||||
|
@property
|
||||||
|
def vitacore_api(self):
|
||||||
|
if not self._vitacore_api:
|
||||||
|
self._vitacore_api = VITACORE_API()
|
||||||
|
|
||||||
|
return self._vitacore_api
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tdn_api(self):
|
||||||
|
if not self._tdn_api:
|
||||||
|
self._tdn_api = TDN_API()
|
||||||
|
|
||||||
|
return self._tdn_api
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tmk_api(self):
|
||||||
|
if not self._tmk_api:
|
||||||
|
self._tmk_api = TMK_API()
|
||||||
|
|
||||||
|
return self._tmk_api
|
||||||
|
|
||||||
|
@property
|
||||||
|
def aemd_api(self):
|
||||||
|
if not self._aemd_api:
|
||||||
|
self._aemd_api = AEMD_API()
|
||||||
|
|
||||||
|
return self._aemd_api
|
||||||
|
|
||||||
|
|
||||||
clients = ClientsObject()
|
clients = ClientsObject()
|
||||||
|
|||||||
0
src/clients/aemd/__init__.py
Normal file
0
src/clients/aemd/__init__.py
Normal file
75
src/clients/aemd/api.py
Normal file
75
src/clients/aemd/api.py
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
from logging import getLogger
|
||||||
|
|
||||||
|
from fastapi import status
|
||||||
|
from httpx import AsyncClient
|
||||||
|
|
||||||
|
from core.config import settings
|
||||||
|
from shared import exceptions as e
|
||||||
|
|
||||||
|
from . import schema as s
|
||||||
|
|
||||||
|
|
||||||
|
class AEMD_API(AsyncClient):
|
||||||
|
def __init__(self):
|
||||||
|
self.logger = getLogger(__name__)
|
||||||
|
super().__init__(
|
||||||
|
base_url=settings.AEMD_BASE_URL,
|
||||||
|
headers={'Content-Type': 'application/soap+xml'},
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_envelope(self, endpoint: str, body: str):
|
||||||
|
return (
|
||||||
|
'<?xml version="1.0" encoding="utf-8"?>'
|
||||||
|
'<s:Envelope xmlns:s= "http://www.w3.org/2003/05/soap-envelope" '
|
||||||
|
'xmlns:a= "http://www.w3.org/2005/08/addressing">'
|
||||||
|
'<s:Header>'
|
||||||
|
f'<a:Action s:mustUnderstand="1">{endpoint}</a:Action>'
|
||||||
|
'<h:transportHeader xmlns:h="http://egisz.rosminzdrav.ru" '
|
||||||
|
'xmlns="http://egisz.rosminzdrav.ru" '
|
||||||
|
'xmlns:xsd="http://www.w3.org/2001/XMLSchema" '
|
||||||
|
'xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance">'
|
||||||
|
'<authInfo>'
|
||||||
|
f'<clientEntityId>{settings.AEMD_TOKEN}</clientEntityId>'
|
||||||
|
'</authInfo>'
|
||||||
|
'</h:transportHeader>'
|
||||||
|
'<a:To s:mustUnderstand= "1">http://gist-sdw.ezdrav.ru:8708/EMDAService</a:To>'
|
||||||
|
'</s:Header>'
|
||||||
|
'<s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">'
|
||||||
|
f'{body}'
|
||||||
|
'</s:Body>'
|
||||||
|
'</s:Envelope>'
|
||||||
|
)
|
||||||
|
|
||||||
|
async def searchRegistryItem(self, patient_snils: str):
|
||||||
|
envelope = self.get_envelope(
|
||||||
|
'searchRegistryItem',
|
||||||
|
f'<searchRegistryItemRequest xmlns="http://egisz.rosminzdrav.ru/iehr/emdr/service/"><patientSnils>{patient_snils}</patientSnils></searchRegistryItemRequest>',
|
||||||
|
)
|
||||||
|
req = await self.post('/', content=envelope)
|
||||||
|
|
||||||
|
match req.status_code:
|
||||||
|
case status.HTTP_200_OK:
|
||||||
|
envelope = s.Envelope.from_xml(req.text)
|
||||||
|
|
||||||
|
case _:
|
||||||
|
self.logger.error(req.text)
|
||||||
|
raise e.UnknownException
|
||||||
|
|
||||||
|
return envelope.model_dump()['body']['response']['matches']
|
||||||
|
|
||||||
|
async def demandContent(self, messageId: str, emdrId: str):
|
||||||
|
envelope = self.get_envelope(
|
||||||
|
'demandContent',
|
||||||
|
f'<demandContentRequest xmlns="http://egisz.rosminzdrav.ru/iehr/emdr/service/"><messageId>{messageId}</messageId><emdrId>{emdrId}</emdrId></demandContentRequest>',
|
||||||
|
)
|
||||||
|
req = await self.post('/', content=envelope)
|
||||||
|
|
||||||
|
match req.status_code:
|
||||||
|
case status.HTTP_200_OK:
|
||||||
|
envelope = s.DemandContentEnvelope.from_xml(req.text)
|
||||||
|
|
||||||
|
case _:
|
||||||
|
self.logger.error(req.text)
|
||||||
|
raise e.UnknownException
|
||||||
|
|
||||||
|
return envelope.model_dump()['body']
|
||||||
168
src/clients/aemd/schema.py
Normal file
168
src/clients/aemd/schema.py
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
from pydantic import constr
|
||||||
|
from pydantic_xml import BaseXmlModel, RootXmlModel, attr, element
|
||||||
|
|
||||||
|
NSMAP = {
|
||||||
|
'a': 'http://www.w3.org/2005/08/addressing',
|
||||||
|
'b': 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd',
|
||||||
|
's': 'http://www.w3.org/2003/05/soap-envelope',
|
||||||
|
'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
|
||||||
|
'xsd': 'http://www.w3.org/2001/XMLSchema',
|
||||||
|
'd2p1': 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd',
|
||||||
|
'd3p1': 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd',
|
||||||
|
}
|
||||||
|
|
||||||
|
SECURITY_NSMAP = NSMAP | {
|
||||||
|
'': 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd'
|
||||||
|
}
|
||||||
|
SIGNATURE_NSMAP = NSMAP | {'': 'http://www.w3.org/2000/09/xmldsig#'}
|
||||||
|
RESPONSE_NSMAP = NSMAP | {'': 'http://egisz.rosminzdrav.ru/iehr/emdr/service/'}
|
||||||
|
|
||||||
|
|
||||||
|
class Action(BaseXmlModel, tag='Action', ns='a', nsmap=NSMAP):
|
||||||
|
id: str = attr(name='Id', ns='d3p1')
|
||||||
|
|
||||||
|
|
||||||
|
class MessageID(BaseXmlModel, tag='MessageID', ns='a', nsmap=NSMAP):
|
||||||
|
id: str = attr(name='Id', ns='d3p1')
|
||||||
|
description: constr(strip_whitespace=True) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
class BinarySecurityToken(
|
||||||
|
BaseXmlModel, tag='BinarySecurityToken', nsmap=NSMAP
|
||||||
|
):
|
||||||
|
id: str = attr(name='Id', ns='b')
|
||||||
|
ValueType: str = attr(name='ValueType')
|
||||||
|
EncodingType: str = attr(name='EncodingType')
|
||||||
|
description: constr(strip_whitespace=True) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
class CanonicalizationMethod(
|
||||||
|
BaseXmlModel, tag='CanonicalizationMethod', nsmap=SIGNATURE_NSMAP
|
||||||
|
):
|
||||||
|
algorithm: str = attr(name='Algorithm')
|
||||||
|
|
||||||
|
|
||||||
|
class SignatureMethod(
|
||||||
|
BaseXmlModel, tag='SignatureMethod', nsmap=SIGNATURE_NSMAP
|
||||||
|
):
|
||||||
|
algorithm: str = attr(name='Algorithm')
|
||||||
|
|
||||||
|
|
||||||
|
class Transform(BaseXmlModel, tag='Transform', nsmap=SIGNATURE_NSMAP):
|
||||||
|
algorithm: str = attr(name='Algorithm')
|
||||||
|
|
||||||
|
|
||||||
|
class Transforms(RootXmlModel, tag='Transforms', nsmap=SIGNATURE_NSMAP): # type: ignore
|
||||||
|
root: list[Transform] = element()
|
||||||
|
|
||||||
|
|
||||||
|
class DigestMethod(BaseXmlModel, tag='DigestMethod', nsmap=SIGNATURE_NSMAP):
|
||||||
|
algorithm: str = attr(name='Algorithm')
|
||||||
|
|
||||||
|
|
||||||
|
class DigestValue(BaseXmlModel, tag='DigestValue', nsmap=SIGNATURE_NSMAP):
|
||||||
|
value: str
|
||||||
|
|
||||||
|
|
||||||
|
class Reference(BaseXmlModel, tag='Reference', nsmap=SIGNATURE_NSMAP):
|
||||||
|
uri: str = attr(name='URI')
|
||||||
|
transforms: Transforms = element()
|
||||||
|
digest_method: DigestMethod = element()
|
||||||
|
digest_value: DigestValue = element()
|
||||||
|
|
||||||
|
|
||||||
|
class SignedInfo(BaseXmlModel, tag='SignedInfo', nsmap=SIGNATURE_NSMAP):
|
||||||
|
canonicalization_method: CanonicalizationMethod = element()
|
||||||
|
signature_method: SignatureMethod = element()
|
||||||
|
references: list[Reference] = element()
|
||||||
|
|
||||||
|
|
||||||
|
class SignatureValue(
|
||||||
|
BaseXmlModel, tag='SignatureValue', nsmap=SIGNATURE_NSMAP
|
||||||
|
):
|
||||||
|
description: constr(strip_whitespace=True) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
class SecurityTokenReference(
|
||||||
|
BaseXmlModel, tag='Reference', nsmap=SECURITY_NSMAP
|
||||||
|
):
|
||||||
|
ValueType: str = attr(name='ValueType')
|
||||||
|
URI: str = attr(name='URI')
|
||||||
|
|
||||||
|
|
||||||
|
class SecurityToken(
|
||||||
|
BaseXmlModel, tag='SecurityTokenReference', ns='', nsmap=SECURITY_NSMAP
|
||||||
|
):
|
||||||
|
reference: SecurityTokenReference = element()
|
||||||
|
|
||||||
|
|
||||||
|
class KeyInfo(BaseXmlModel, tag='KeyInfo', nsmap=SIGNATURE_NSMAP):
|
||||||
|
security_token_reference: SecurityToken = element()
|
||||||
|
|
||||||
|
|
||||||
|
class Signature(BaseXmlModel, tag='Signature', ns='', nsmap=SIGNATURE_NSMAP):
|
||||||
|
signed_info: SignedInfo = element()
|
||||||
|
signature_value: SignatureValue = element()
|
||||||
|
key_info: KeyInfo = element()
|
||||||
|
|
||||||
|
|
||||||
|
class Security(BaseXmlModel, tag='Security', ns='', nsmap=SECURITY_NSMAP):
|
||||||
|
binary_security_token: BinarySecurityToken = element()
|
||||||
|
signature: Signature = element()
|
||||||
|
|
||||||
|
|
||||||
|
class Header(BaseXmlModel, tag='Header', ns='s', nsmap=NSMAP):
|
||||||
|
action: Action = element()
|
||||||
|
message_id: MessageID = element()
|
||||||
|
security: Security = element()
|
||||||
|
|
||||||
|
|
||||||
|
class searchRegistryItemResponseItem(
|
||||||
|
BaseXmlModel, tag='item', nsmap=RESPONSE_NSMAP
|
||||||
|
):
|
||||||
|
emdrId: str = element()
|
||||||
|
localUid: str = element()
|
||||||
|
registrationDate: str = element()
|
||||||
|
registrationDateTime: str = element()
|
||||||
|
storeTillDate: str = element()
|
||||||
|
DocKind: str = element()
|
||||||
|
IsSemd: bool = element()
|
||||||
|
|
||||||
|
|
||||||
|
class searchRegistryItemResponseMatches(
|
||||||
|
BaseXmlModel, tag='matches', nsmap=RESPONSE_NSMAP
|
||||||
|
):
|
||||||
|
items: list[searchRegistryItemResponseItem] = element()
|
||||||
|
|
||||||
|
|
||||||
|
class searchRegistryItemResponse(
|
||||||
|
BaseXmlModel, tag='searchRegistryItemResponse', ns='', nsmap=RESPONSE_NSMAP
|
||||||
|
):
|
||||||
|
status: str = element()
|
||||||
|
matches: searchRegistryItemResponseMatches
|
||||||
|
|
||||||
|
|
||||||
|
class Body(BaseXmlModel, tag='Body', ns='s', nsmap=NSMAP):
|
||||||
|
id: str = attr(name='Id', ns='d2p1')
|
||||||
|
response: searchRegistryItemResponse = element()
|
||||||
|
|
||||||
|
|
||||||
|
class Envelope(BaseXmlModel, tag='Envelope', ns='s', nsmap=NSMAP):
|
||||||
|
header: Header = element()
|
||||||
|
body: Body = element()
|
||||||
|
|
||||||
|
|
||||||
|
class Acknowledgment(
|
||||||
|
BaseXmlModel, tag='acknowledgment', ns='', nsmap=RESPONSE_NSMAP
|
||||||
|
):
|
||||||
|
status: str = element()
|
||||||
|
|
||||||
|
|
||||||
|
class DemandContentBody(BaseXmlModel, tag='Body', ns='s', nsmap=NSMAP):
|
||||||
|
id: str = attr(name='Id', ns='d2p1')
|
||||||
|
acknowledgment: Acknowledgment
|
||||||
|
|
||||||
|
|
||||||
|
class DemandContentEnvelope(BaseXmlModel, tag='Envelope', ns='s', nsmap=NSMAP):
|
||||||
|
header: Header = element()
|
||||||
|
body: DemandContentBody = element()
|
||||||
@ -18,7 +18,9 @@ from . import schema as s
|
|||||||
class ESIA_API(AsyncClient):
|
class ESIA_API(AsyncClient):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.logger = getLogger(__name__)
|
self.logger = getLogger(__name__)
|
||||||
super().__init__(base_url=settings.ESIA_BASE_URL)
|
super().__init__(
|
||||||
|
base_url=settings.ESIA_BASE_URL, proxy=settings.ESIA_PROXY
|
||||||
|
)
|
||||||
|
|
||||||
async def sign_request(self, data: dict[str, Any]):
|
async def sign_request(self, data: dict[str, Any]):
|
||||||
timestamp = datetime.now(UTC).strftime('%Y.%m.%d %H:%M:%S %z').strip()
|
timestamp = datetime.now(UTC).strftime('%Y.%m.%d %H:%M:%S %z').strip()
|
||||||
|
|||||||
@ -21,7 +21,7 @@ class IDTokenSBJModel(BaseModel):
|
|||||||
typ: str = Field(alias='urn:esia:sbj:typ')
|
typ: str = Field(alias='urn:esia:sbj:typ')
|
||||||
is_tru: bool = Field(alias='urn:esia:sbj:is_tru')
|
is_tru: bool = Field(alias='urn:esia:sbj:is_tru')
|
||||||
oid: int = Field(alias='urn:esia:sbj:oid')
|
oid: int = Field(alias='urn:esia:sbj:oid')
|
||||||
name: str = Field(alias='urn:esia:sbj:name')
|
name: str = Field(alias='urn:esia:sbj:nam')
|
||||||
|
|
||||||
|
|
||||||
class IDTokenModel(BaseModel):
|
class IDTokenModel(BaseModel):
|
||||||
@ -43,7 +43,7 @@ class UserInfoModel(BaseModel):
|
|||||||
stateFacts: list[str]
|
stateFacts: list[str]
|
||||||
firstName: str
|
firstName: str
|
||||||
lastName: str
|
lastName: str
|
||||||
# middleName: str
|
middleName: str
|
||||||
# birthDate: str
|
# birthDate: str
|
||||||
# gender: str
|
# gender: str
|
||||||
trusted: bool
|
trusted: bool
|
||||||
|
|||||||
0
src/clients/tdn/__init__.py
Normal file
0
src/clients/tdn/__init__.py
Normal file
204
src/clients/tdn/api.py
Normal file
204
src/clients/tdn/api.py
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
from datetime import UTC, datetime
|
||||||
|
from json import dumps
|
||||||
|
from logging import getLogger
|
||||||
|
from urllib.parse import quote, urlencode
|
||||||
|
|
||||||
|
from fastapi import UploadFile
|
||||||
|
from fastapi import status as st
|
||||||
|
from httpx import AsyncClient
|
||||||
|
|
||||||
|
from core.config import settings
|
||||||
|
from shared import exceptions as e
|
||||||
|
|
||||||
|
from . import schema as s
|
||||||
|
|
||||||
|
|
||||||
|
class TDN_API(AsyncClient):
|
||||||
|
def __init__(self):
|
||||||
|
self.logger = getLogger(__name__)
|
||||||
|
super().__init__(
|
||||||
|
base_url=settings.TDN_BASE_URL,
|
||||||
|
verify=False,
|
||||||
|
headers={
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def signin(self):
|
||||||
|
data = {
|
||||||
|
'username': settings.TDN_LOGIN,
|
||||||
|
'password': settings.TDN_PASSWORD,
|
||||||
|
}
|
||||||
|
|
||||||
|
res = await self.post('/core/auth/signin', json=data)
|
||||||
|
|
||||||
|
match res.status_code:
|
||||||
|
case st.HTTP_200_OK:
|
||||||
|
return s.SignInModel.model_validate(res.json())
|
||||||
|
case _:
|
||||||
|
self.logger.error(res.json())
|
||||||
|
raise e.UnknownException
|
||||||
|
|
||||||
|
async def patient_search(self, access_token: str, vitaId: str):
|
||||||
|
query = (
|
||||||
|
quote(dumps({'where': {'vitaId': vitaId}}))
|
||||||
|
.replace('%7B', '{')
|
||||||
|
.replace('%7D', '}')
|
||||||
|
)
|
||||||
|
|
||||||
|
req = await self.get(
|
||||||
|
f'/ddn/patients/search?query={query}',
|
||||||
|
headers={'Authorization': f'Bearer {access_token}'},
|
||||||
|
)
|
||||||
|
|
||||||
|
return req.json()
|
||||||
|
|
||||||
|
async def observations_search(self, access_token: str, patientUid: str):
|
||||||
|
query = (
|
||||||
|
quote(dumps({'where': {'patientUid': patientUid}}))
|
||||||
|
.replace('%7B', '{')
|
||||||
|
.replace('%7D', '}')
|
||||||
|
)
|
||||||
|
|
||||||
|
res = await self.get(
|
||||||
|
f'/ddn/observations/search?query={query}',
|
||||||
|
headers={'Authorization': f'Bearer {access_token}'},
|
||||||
|
)
|
||||||
|
|
||||||
|
match res.status_code:
|
||||||
|
case st.HTTP_200_OK:
|
||||||
|
return s.ObservationsModel.model_validate(res.json())
|
||||||
|
case _:
|
||||||
|
self.logger.error(res.json())
|
||||||
|
raise e.UnknownException
|
||||||
|
|
||||||
|
async def observations_measurement_search(
|
||||||
|
self, access_token: str, observationUid: str
|
||||||
|
):
|
||||||
|
encoded_query = urlencode(
|
||||||
|
{
|
||||||
|
'query': dumps(
|
||||||
|
{
|
||||||
|
'where': {'observationUid': observationUid},
|
||||||
|
'relations': [
|
||||||
|
'measurement',
|
||||||
|
'obsrvMtMetrics',
|
||||||
|
'obsrvMtMetrics.metric',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
res = await self.get(
|
||||||
|
f'/ddn/observation/obsrv-measurements/search?{encoded_query}',
|
||||||
|
headers={'Authorization': f'Bearer {access_token}'},
|
||||||
|
)
|
||||||
|
|
||||||
|
match res.status_code:
|
||||||
|
case st.HTTP_200_OK:
|
||||||
|
return s.ObservationMeasurementsModel.model_validate(
|
||||||
|
res.json()
|
||||||
|
)
|
||||||
|
case _:
|
||||||
|
self.logger.error(res.json())
|
||||||
|
raise e.UnknownException
|
||||||
|
|
||||||
|
async def create_series(
|
||||||
|
self, access_token: str, observationUid: str, obsrvMeasurementUid: str
|
||||||
|
):
|
||||||
|
now = datetime.now(UTC)
|
||||||
|
tz_offset = now.strftime('%z')
|
||||||
|
formatted_offset = (
|
||||||
|
f'{tz_offset[:3]}:{tz_offset[3:]}' if tz_offset else '+0000'
|
||||||
|
)
|
||||||
|
date_str = (
|
||||||
|
f'{now.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]} {formatted_offset}'
|
||||||
|
)
|
||||||
|
|
||||||
|
res = await self.patch(
|
||||||
|
'/ddn/observation/series',
|
||||||
|
headers={'Authorization': f'Bearer {access_token}'},
|
||||||
|
json={
|
||||||
|
'observationUid': observationUid,
|
||||||
|
'obsrvMeasurementUid': obsrvMeasurementUid,
|
||||||
|
'date': date_str,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
match res.status_code:
|
||||||
|
case st.HTTP_200_OK:
|
||||||
|
return s.SeriesModel.model_validate(res.json())
|
||||||
|
case _:
|
||||||
|
self.logger.error(res.json())
|
||||||
|
raise e.UnknownException
|
||||||
|
|
||||||
|
async def create_series_values(
|
||||||
|
self,
|
||||||
|
access_token: str,
|
||||||
|
seriesUid: str,
|
||||||
|
obsrvMtMetricUid: str,
|
||||||
|
*,
|
||||||
|
nvalue: int | None = None,
|
||||||
|
fvalue: float | None = None,
|
||||||
|
svalue: str | None = None,
|
||||||
|
):
|
||||||
|
data: dict[str, str | int | float] = {
|
||||||
|
'seriesUid': seriesUid,
|
||||||
|
'obsrvMtMetricUid': obsrvMtMetricUid,
|
||||||
|
}
|
||||||
|
|
||||||
|
if nvalue is not None:
|
||||||
|
data['nvalue'] = nvalue
|
||||||
|
|
||||||
|
if fvalue is not None:
|
||||||
|
data['fvalue'] = fvalue
|
||||||
|
|
||||||
|
if svalue is not None:
|
||||||
|
data['svalue'] = svalue
|
||||||
|
|
||||||
|
res = await self.patch(
|
||||||
|
'/ddn/observation/series-values',
|
||||||
|
headers={'Authorization': f'Bearer {access_token}'},
|
||||||
|
json=data,
|
||||||
|
)
|
||||||
|
|
||||||
|
match res.status_code:
|
||||||
|
case st.HTTP_200_OK:
|
||||||
|
return s.SeriesValueModel.model_validate(res.json())
|
||||||
|
case _:
|
||||||
|
self.logger.error(res.json())
|
||||||
|
raise e.UnknownException
|
||||||
|
|
||||||
|
async def ekg(
|
||||||
|
self,
|
||||||
|
access_token: str,
|
||||||
|
patientUid: str,
|
||||||
|
serial_number: str,
|
||||||
|
ecg_file: UploadFile,
|
||||||
|
):
|
||||||
|
req = await self.post(
|
||||||
|
'/ddn/observation/series-values/ecg/krb02',
|
||||||
|
headers={
|
||||||
|
'Authorization': f'Bearer {access_token}',
|
||||||
|
'Content-Type': 'multipart/form-data; boundary=+++',
|
||||||
|
},
|
||||||
|
data={
|
||||||
|
'patientUid': patientUid,
|
||||||
|
'serialNumber': serial_number,
|
||||||
|
},
|
||||||
|
files={
|
||||||
|
'ecg': (
|
||||||
|
ecg_file.filename,
|
||||||
|
ecg_file.file,
|
||||||
|
ecg_file.content_type or 'application/octet-stream',
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
match req.status_code:
|
||||||
|
case st.HTTP_201_CREATED:
|
||||||
|
return s.EkgModel.model_validate(req.json())
|
||||||
|
case _:
|
||||||
|
self.logger.error(req.json())
|
||||||
|
raise e.UnknownException
|
||||||
115
src/clients/tdn/schema.py
Normal file
115
src/clients/tdn/schema.py
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class SignInModel(BaseModel):
|
||||||
|
accessToken: str
|
||||||
|
refreshToken: str
|
||||||
|
|
||||||
|
|
||||||
|
class ObservationModel(BaseModel):
|
||||||
|
uid: str
|
||||||
|
createdAt: datetime
|
||||||
|
updatedAt: datetime
|
||||||
|
realmUid: str
|
||||||
|
patientUid: str
|
||||||
|
nosologyUid: str
|
||||||
|
exclusionReasonUid: str | None
|
||||||
|
exclusionComment: str | None
|
||||||
|
exclusionDate: datetime | None
|
||||||
|
employeeUid: str
|
||||||
|
mobileId: str | None
|
||||||
|
|
||||||
|
|
||||||
|
class ObservationsModel(BaseModel):
|
||||||
|
items: list[ObservationModel]
|
||||||
|
total: int
|
||||||
|
|
||||||
|
|
||||||
|
class MeasurementModel(BaseModel):
|
||||||
|
uid: str
|
||||||
|
createdAt: datetime
|
||||||
|
updatedAt: datetime
|
||||||
|
code: str
|
||||||
|
title: str
|
||||||
|
order: int
|
||||||
|
isSelfControl: bool
|
||||||
|
|
||||||
|
|
||||||
|
class MetricModel(BaseModel):
|
||||||
|
uid: str
|
||||||
|
createdAt: datetime
|
||||||
|
updatedAt: datetime
|
||||||
|
code: str
|
||||||
|
title: str
|
||||||
|
order: int
|
||||||
|
shortName: str
|
||||||
|
measureUid: str | None
|
||||||
|
format: str
|
||||||
|
|
||||||
|
|
||||||
|
class ObservationMtMetricModel(BaseModel):
|
||||||
|
uid: str
|
||||||
|
createdAt: datetime
|
||||||
|
updatedAt: datetime
|
||||||
|
obsrvMeasurementUid: str
|
||||||
|
metricUid: str
|
||||||
|
mobileId: str | None
|
||||||
|
metric: MetricModel
|
||||||
|
|
||||||
|
|
||||||
|
class ObservationMeasurementModel(BaseModel):
|
||||||
|
uid: str
|
||||||
|
createdAt: datetime
|
||||||
|
updatedAt: datetime
|
||||||
|
observationUid: str
|
||||||
|
measurementUid: str
|
||||||
|
timeFrequency: int
|
||||||
|
timePeriod: int
|
||||||
|
timePeriodMeasureUid: str
|
||||||
|
timeOfDay: list[str] | None
|
||||||
|
comment: str | None
|
||||||
|
mobileId: str | None
|
||||||
|
measurement: MeasurementModel
|
||||||
|
obsrvMtMetrics: list[ObservationMtMetricModel]
|
||||||
|
|
||||||
|
|
||||||
|
class ObservationMeasurementsModel(BaseModel):
|
||||||
|
items: list[ObservationMeasurementModel]
|
||||||
|
total: int
|
||||||
|
|
||||||
|
|
||||||
|
class SeriesRealmModel(BaseModel):
|
||||||
|
uid: str
|
||||||
|
|
||||||
|
|
||||||
|
class SeriesModel(BaseModel):
|
||||||
|
observationUid: str
|
||||||
|
obsrvMeasurementUid: str
|
||||||
|
date: str
|
||||||
|
realm: SeriesRealmModel
|
||||||
|
mobileId: str | None
|
||||||
|
uid: str
|
||||||
|
createdAt: datetime
|
||||||
|
updatedAt: datetime
|
||||||
|
|
||||||
|
|
||||||
|
class SeriesValueModel(BaseModel):
|
||||||
|
uid: str
|
||||||
|
seriesUid: str
|
||||||
|
obsrvMtMetricUid: str
|
||||||
|
realm: SeriesRealmModel
|
||||||
|
createdAt: datetime
|
||||||
|
updatedAt: datetime
|
||||||
|
nvalue: int | None
|
||||||
|
fvalue: float | None
|
||||||
|
svalue: str | None
|
||||||
|
filepath: str | None
|
||||||
|
mobileId: str | None
|
||||||
|
tisId: str | None
|
||||||
|
|
||||||
|
|
||||||
|
class EkgModel(BaseModel):
|
||||||
|
success: bool
|
||||||
|
# result: ...
|
||||||
0
src/clients/tmk/__init__.py
Normal file
0
src/clients/tmk/__init__.py
Normal file
111
src/clients/tmk/api.py
Normal file
111
src/clients/tmk/api.py
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
from datetime import date
|
||||||
|
from logging import getLogger
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from fastapi import status as st
|
||||||
|
from httpx import AsyncClient
|
||||||
|
|
||||||
|
from core.config import settings
|
||||||
|
from shared import exceptions as e
|
||||||
|
from shared.functions import clean_params
|
||||||
|
from shared.redis import client as cache
|
||||||
|
|
||||||
|
from . import schema as s
|
||||||
|
|
||||||
|
|
||||||
|
class TMK_API(AsyncClient):
|
||||||
|
def __init__(self):
|
||||||
|
self.logger = getLogger(__name__)
|
||||||
|
super().__init__(base_url=settings.TMK_BASE_URL, verify=False)
|
||||||
|
|
||||||
|
async def get_token(self):
|
||||||
|
token = await cache.get('tmk_token')
|
||||||
|
|
||||||
|
if token is None:
|
||||||
|
token = await self.login()
|
||||||
|
await cache.set('tmk_token', token, 10800)
|
||||||
|
|
||||||
|
else:
|
||||||
|
token = token.decode()
|
||||||
|
|
||||||
|
return token
|
||||||
|
|
||||||
|
async def login(self):
|
||||||
|
req = await self.post(
|
||||||
|
'/auth',
|
||||||
|
json={
|
||||||
|
'login': settings.TMK_LOGIN,
|
||||||
|
'password': settings.TMK_PASSWORD,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
match req.status_code:
|
||||||
|
case st.HTTP_200_OK:
|
||||||
|
return s.AccessTokenModel.model_validate(
|
||||||
|
req.json()
|
||||||
|
).access_token
|
||||||
|
|
||||||
|
case _:
|
||||||
|
self.logger.error(req.json())
|
||||||
|
raise e.UnknownException
|
||||||
|
|
||||||
|
async def getQueue(
|
||||||
|
self,
|
||||||
|
code_mo: str | None = None,
|
||||||
|
doctor_spec: str | None = None,
|
||||||
|
doctor_snils: str | None = None,
|
||||||
|
doctor_snils_strict: Literal['y', 'n'] = 'n',
|
||||||
|
date_begin: date | None = None,
|
||||||
|
date_end: date | None = None,
|
||||||
|
patient_snils: str | None = None,
|
||||||
|
patient_fio: str | None = None,
|
||||||
|
patient_policy: str | None = None,
|
||||||
|
patient_phone: str | None = None,
|
||||||
|
patient_birthdate: date | None = None,
|
||||||
|
tk_status: str | None = None,
|
||||||
|
):
|
||||||
|
token = await self.get_token()
|
||||||
|
req = await self.get(
|
||||||
|
'/getQueue',
|
||||||
|
headers={'Authorization': f'Bearer {token}'},
|
||||||
|
params=clean_params(
|
||||||
|
{
|
||||||
|
'code_mo': code_mo,
|
||||||
|
'doctor_spec': doctor_spec,
|
||||||
|
'doctor_snils': doctor_snils,
|
||||||
|
'doctor_snils_strict': doctor_snils_strict,
|
||||||
|
'date_begin': date_begin,
|
||||||
|
'date_end': date_end,
|
||||||
|
'patient_snils': patient_snils,
|
||||||
|
'patient_fio': patient_fio,
|
||||||
|
'patient_policy': patient_policy,
|
||||||
|
'patient_phone': patient_phone,
|
||||||
|
'patient_birthdate': patient_birthdate,
|
||||||
|
'tk_status': tk_status,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
match req.status_code:
|
||||||
|
case st.HTTP_200_OK:
|
||||||
|
return [s.QueueModel.model_validate(i) for i in req.json()]
|
||||||
|
|
||||||
|
case _:
|
||||||
|
self.logger.error(req.json())
|
||||||
|
raise e.UnknownException
|
||||||
|
|
||||||
|
async def getInfo(self, guid: str):
|
||||||
|
token = await self.get_token()
|
||||||
|
req = await self.get(
|
||||||
|
'/getTMKInfo',
|
||||||
|
headers={'Authorization': f'Bearer {token}'},
|
||||||
|
params={'guid': guid},
|
||||||
|
)
|
||||||
|
|
||||||
|
match req.status_code:
|
||||||
|
case st.HTTP_200_OK:
|
||||||
|
return s.QueueModel.model_validate(req.json())
|
||||||
|
|
||||||
|
case _:
|
||||||
|
self.logger.error(req.json())
|
||||||
|
raise e.UnknownException
|
||||||
31
src/clients/tmk/schema.py
Normal file
31
src/clients/tmk/schema.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class AccessTokenModel(BaseModel):
|
||||||
|
access_token: str
|
||||||
|
|
||||||
|
|
||||||
|
class QueueModel(BaseModel):
|
||||||
|
id: int
|
||||||
|
guid: str
|
||||||
|
created_at: datetime
|
||||||
|
code_mo: int | None
|
||||||
|
mo_name: str | None
|
||||||
|
doctor_spec: str
|
||||||
|
doctor_snils: str | None
|
||||||
|
doctor_fio: str | None
|
||||||
|
patient_name: str | None
|
||||||
|
patient_birthday: datetime | None
|
||||||
|
patient_snils: str
|
||||||
|
patient_policy: str | None
|
||||||
|
patient_phone: str | None
|
||||||
|
patient_email: str | None
|
||||||
|
tmk_status: int
|
||||||
|
tmk_status_name: str
|
||||||
|
tmk_cancel_reason: int | None
|
||||||
|
tmk_cancel_reason_name: str | None
|
||||||
|
vks_doctor_link: str | None
|
||||||
|
vks_patient_link: str | None
|
||||||
|
doctor_spec_name: str | None
|
||||||
0
src/clients/vitacore/__init__.py
Normal file
0
src/clients/vitacore/__init__.py
Normal file
517
src/clients/vitacore/api.py
Normal file
517
src/clients/vitacore/api.py
Normal file
@ -0,0 +1,517 @@
|
|||||||
|
from datetime import UTC, datetime
|
||||||
|
from logging import getLogger
|
||||||
|
|
||||||
|
from fastapi import status as st
|
||||||
|
from httpx import AsyncClient, BasicAuth
|
||||||
|
from orjson import dumps, loads
|
||||||
|
|
||||||
|
from core.config import settings
|
||||||
|
from shared import exceptions as e
|
||||||
|
from shared.redis import client as cache
|
||||||
|
|
||||||
|
from . import schema as s
|
||||||
|
|
||||||
|
|
||||||
|
class VITACORE_API(AsyncClient):
|
||||||
|
def __init__(self):
|
||||||
|
self.logger = getLogger(__name__)
|
||||||
|
super().__init__(base_url=settings.VITACORE_BASE_URL)
|
||||||
|
|
||||||
|
async def get_token(self):
|
||||||
|
token = await self.get_cache('vitacore_token')
|
||||||
|
|
||||||
|
if token is None:
|
||||||
|
token = await self.login()
|
||||||
|
await self.set_cache(
|
||||||
|
'vitacore_token', dumps({'token': token}).decode(), 10800
|
||||||
|
)
|
||||||
|
|
||||||
|
return token
|
||||||
|
|
||||||
|
return token['token']
|
||||||
|
|
||||||
|
async def get_cache(self, key: str):
|
||||||
|
data = await cache.get(key)
|
||||||
|
|
||||||
|
if data:
|
||||||
|
return loads(data.decode())
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def set_cache(self, key: str, value: str, ttl: int = 600):
|
||||||
|
await cache.set(key, value, ttl)
|
||||||
|
|
||||||
|
async def login(self):
|
||||||
|
req = await self.post(
|
||||||
|
'/auth',
|
||||||
|
auth=BasicAuth(
|
||||||
|
settings.VITACORE_USERNAME, settings.VITACORE_PASSWORD
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
match req.status_code:
|
||||||
|
case st.HTTP_200_OK:
|
||||||
|
return req.text
|
||||||
|
|
||||||
|
case _:
|
||||||
|
self.logger.error(req.text)
|
||||||
|
raise e.UnknownException
|
||||||
|
|
||||||
|
async def findBySnils(self, snils: str):
|
||||||
|
data = await self.get_cache(f'vitacore_findBySnils2:{snils}')
|
||||||
|
|
||||||
|
if data:
|
||||||
|
return s.PatientModel.model_validate(data)
|
||||||
|
|
||||||
|
token = await self.get_token()
|
||||||
|
req = await self.get(
|
||||||
|
'/findBySnils',
|
||||||
|
params={'snils': snils},
|
||||||
|
headers={'Authorization': f'Bearer {token}'},
|
||||||
|
)
|
||||||
|
|
||||||
|
match req.status_code:
|
||||||
|
case st.HTTP_200_OK:
|
||||||
|
model = s.PatientModel.model_validate(req.json())
|
||||||
|
await self.set_cache(
|
||||||
|
f'vitacore_findBySnils2:{snils}',
|
||||||
|
model.model_dump_json(),
|
||||||
|
14400,
|
||||||
|
)
|
||||||
|
return model
|
||||||
|
|
||||||
|
case _:
|
||||||
|
self.logger.error(req.json())
|
||||||
|
raise e.UnknownException
|
||||||
|
|
||||||
|
async def getProfile(self, patId: str):
|
||||||
|
data = await self.get_cache(f'vitacore_getProfile:{patId}')
|
||||||
|
|
||||||
|
if data:
|
||||||
|
return s.ProfileModel.model_validate(data)
|
||||||
|
|
||||||
|
token = await self.get_token()
|
||||||
|
req = await self.get(
|
||||||
|
'/getProfile',
|
||||||
|
params={'patId': patId},
|
||||||
|
headers={'Authorization': f'Bearer {token}'},
|
||||||
|
)
|
||||||
|
|
||||||
|
match req.status_code:
|
||||||
|
case st.HTTP_200_OK:
|
||||||
|
model = s.ProfileModel.model_validate(req.json())
|
||||||
|
await self.set_cache(
|
||||||
|
f'vitacore_getProfile:{patId}',
|
||||||
|
model.model_dump_json(),
|
||||||
|
14400,
|
||||||
|
)
|
||||||
|
return model
|
||||||
|
case _:
|
||||||
|
self.logger.error(req.json())
|
||||||
|
raise e.UnknownException
|
||||||
|
|
||||||
|
async def getDepartments(self):
|
||||||
|
data = await self.get_cache('vitacore_getDepartments')
|
||||||
|
|
||||||
|
if data:
|
||||||
|
return s.OrganizationsModel.model_validate(data)
|
||||||
|
|
||||||
|
token = await self.get_token()
|
||||||
|
req = await self.get(
|
||||||
|
'/getDepartments', headers={'Authorization': f'Bearer {token}'}
|
||||||
|
)
|
||||||
|
|
||||||
|
match req.status_code:
|
||||||
|
case st.HTTP_200_OK:
|
||||||
|
model = s.OrganizationsModel.model_validate(req.json())
|
||||||
|
await self.set_cache(
|
||||||
|
'vitacore_getDepartments',
|
||||||
|
model.model_dump_json(),
|
||||||
|
14400,
|
||||||
|
)
|
||||||
|
return model
|
||||||
|
case _:
|
||||||
|
self.logger.error(req.text)
|
||||||
|
raise e.UnknownException
|
||||||
|
|
||||||
|
async def getWorkers(self, departmentId: str):
|
||||||
|
data = await self.get_cache(f'vitacore_getWorkers:{departmentId}')
|
||||||
|
|
||||||
|
if data:
|
||||||
|
return s.WorkersModel.model_validate(data)
|
||||||
|
|
||||||
|
token = await self.get_token()
|
||||||
|
req = await self.get(
|
||||||
|
'/getWorkers',
|
||||||
|
params={'departmentId': departmentId},
|
||||||
|
headers={'Authorization': f'Bearer {token}'},
|
||||||
|
)
|
||||||
|
|
||||||
|
match req.status_code:
|
||||||
|
case st.HTTP_200_OK:
|
||||||
|
data = s.WorkersModel.model_validate(req.json())
|
||||||
|
await self.set_cache(
|
||||||
|
f'vitacore_getWorkers:{departmentId}',
|
||||||
|
data.model_dump_json(),
|
||||||
|
14400,
|
||||||
|
)
|
||||||
|
return data
|
||||||
|
|
||||||
|
case st.HTTP_206_PARTIAL_CONTENT:
|
||||||
|
return s.WorkersModel(Workers=[])
|
||||||
|
|
||||||
|
case _:
|
||||||
|
self.logger.error(req.json())
|
||||||
|
raise e.UnknownException
|
||||||
|
|
||||||
|
async def getSpecsV021(self):
|
||||||
|
data = await self.get_cache('vitacore_getSpecsV021')
|
||||||
|
|
||||||
|
if data:
|
||||||
|
return s.SpecsV021Model.model_validate(data)
|
||||||
|
|
||||||
|
token = await self.get_token()
|
||||||
|
req = await self.get(
|
||||||
|
'/getSpecsV021', headers={'Authorization': f'Bearer {token}'}
|
||||||
|
)
|
||||||
|
|
||||||
|
match req.status_code:
|
||||||
|
case st.HTTP_200_OK:
|
||||||
|
data = s.SpecsV021Model.model_validate(req.json())
|
||||||
|
await self.set_cache(
|
||||||
|
'vitacore_getSpecsV021',
|
||||||
|
data.model_dump_json(),
|
||||||
|
14400,
|
||||||
|
)
|
||||||
|
return data
|
||||||
|
case _:
|
||||||
|
self.logger.error(req.json())
|
||||||
|
raise e.UnknownException
|
||||||
|
|
||||||
|
async def getEntries(self, patId: str):
|
||||||
|
token = await self.get_token()
|
||||||
|
if patId == '9a4d4b06-5928-4101-b95e-e5ba03a1abfd':
|
||||||
|
patId = 'b66a85f1-4aaa-4db8-942a-2de44341824e'
|
||||||
|
|
||||||
|
req = await self.get(
|
||||||
|
'/getEntries',
|
||||||
|
params={'patId': patId},
|
||||||
|
headers={'Authorization': f'Bearer {token}'},
|
||||||
|
)
|
||||||
|
|
||||||
|
match req.status_code:
|
||||||
|
case st.HTTP_200_OK:
|
||||||
|
return s.EntriesModel.model_validate(req.json())
|
||||||
|
case st.HTTP_206_PARTIAL_CONTENT:
|
||||||
|
error = s.ErrorModel.model_validate(req.json())
|
||||||
|
|
||||||
|
if error.error == 'Не найдены записи по указанному patId':
|
||||||
|
return s.EntriesModel(Entries=[])
|
||||||
|
|
||||||
|
case _:
|
||||||
|
self.logger.error(req.json())
|
||||||
|
raise e.UnknownException
|
||||||
|
|
||||||
|
async def getVaccsReport(self, patId: str):
|
||||||
|
token = await self.get_token()
|
||||||
|
if patId == '9a4d4b06-5928-4101-b95e-e5ba03a1abfd':
|
||||||
|
patId = 'b66a85f1-4aaa-4db8-942a-2de44341824e'
|
||||||
|
|
||||||
|
req = await self.get(
|
||||||
|
'/getVaccsReport',
|
||||||
|
params={'patId': patId},
|
||||||
|
headers={'Authorization': f'Bearer {token}'},
|
||||||
|
)
|
||||||
|
|
||||||
|
match req.status_code:
|
||||||
|
case st.HTTP_200_OK:
|
||||||
|
return s.VaccsReportModel.model_validate(req.json())
|
||||||
|
case st.HTTP_206_PARTIAL_CONTENT:
|
||||||
|
error = s.ErrorModel.model_validate(req.json())
|
||||||
|
|
||||||
|
if (
|
||||||
|
error.error == 'Не найдены записи по указанному patId'
|
||||||
|
or error.error
|
||||||
|
== 'Не найдены вакцинации по данному пациенту'
|
||||||
|
):
|
||||||
|
return s.VaccsReportModel(content='')
|
||||||
|
case _:
|
||||||
|
self.logger.error(req.json())
|
||||||
|
raise e.UnknownException
|
||||||
|
|
||||||
|
async def getMedExamDict(self):
|
||||||
|
data = await self.get_cache('vitacore_getMedExamDict')
|
||||||
|
|
||||||
|
if data:
|
||||||
|
return s.MedExamDictModel.model_validate(data)
|
||||||
|
|
||||||
|
token = await self.get_token()
|
||||||
|
req = await self.get(
|
||||||
|
'/getMedExamDict', headers={'Authorization': f'Bearer {token}'}
|
||||||
|
)
|
||||||
|
|
||||||
|
match req.status_code:
|
||||||
|
case st.HTTP_200_OK:
|
||||||
|
data = s.MedExamDictModel.model_validate(req.json())
|
||||||
|
await self.set_cache(
|
||||||
|
'vitacore_getMedExamDict',
|
||||||
|
data.model_dump_json(),
|
||||||
|
14400,
|
||||||
|
)
|
||||||
|
return data
|
||||||
|
case _:
|
||||||
|
self.logger.error(req.json())
|
||||||
|
raise e.UnknownException
|
||||||
|
|
||||||
|
async def getRoutesList(self, patId: str):
|
||||||
|
token = await self.get_token()
|
||||||
|
if patId == '9a4d4b06-5928-4101-b95e-e5ba03a1abfd':
|
||||||
|
patId = 'b66a85f1-4aaa-4db8-942a-2de44341824e'
|
||||||
|
|
||||||
|
req = await self.get(
|
||||||
|
'/getRoutesList',
|
||||||
|
params={'patId': patId},
|
||||||
|
headers={'Authorization': f'Bearer {token}'},
|
||||||
|
)
|
||||||
|
|
||||||
|
match req.status_code:
|
||||||
|
case st.HTTP_200_OK:
|
||||||
|
return s.RoutesListModel.model_validate(req.json())
|
||||||
|
|
||||||
|
case st.HTTP_206_PARTIAL_CONTENT:
|
||||||
|
error = s.ErrorModel.model_validate(req.json())
|
||||||
|
|
||||||
|
if error.error == 'Не найдены случаи по указанному patId':
|
||||||
|
return s.RoutesListModel(
|
||||||
|
EventID='none',
|
||||||
|
EventDate=datetime.now(UTC),
|
||||||
|
LpuName='',
|
||||||
|
Routes=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
case _:
|
||||||
|
self.logger.error(req.json())
|
||||||
|
raise e.UnknownException
|
||||||
|
|
||||||
|
async def getHospExaminations(self, patId: str, examId: str | None = None):
|
||||||
|
if patId == '9a4d4b06-5928-4101-b95e-e5ba03a1abfd':
|
||||||
|
patId = 'b66a85f1-4aaa-4db8-942a-2de44341824e'
|
||||||
|
|
||||||
|
token = await self.get_token()
|
||||||
|
req = await self.get(
|
||||||
|
'/getHospExaminations',
|
||||||
|
params={'patId': patId, 'examId': examId},
|
||||||
|
headers={'Authorization': f'Bearer {token}'},
|
||||||
|
)
|
||||||
|
|
||||||
|
match req.status_code:
|
||||||
|
case st.HTTP_200_OK:
|
||||||
|
return s.HospExaminationsModel.model_validate(req.json())
|
||||||
|
|
||||||
|
case st.HTTP_206_PARTIAL_CONTENT:
|
||||||
|
error = s.ErrorModel.model_validate(req.json())
|
||||||
|
|
||||||
|
if error.error == 'Пациент не госпитализирован!':
|
||||||
|
return s.HospExaminationsModel(
|
||||||
|
EventID='none',
|
||||||
|
EventDate=datetime.now(UTC),
|
||||||
|
LpuName='',
|
||||||
|
Examinations=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
case _:
|
||||||
|
self.logger.error(req.json())
|
||||||
|
raise e.UnknownException
|
||||||
|
|
||||||
|
async def getCurrHosp(self, patId: str):
|
||||||
|
token = await self.get_token()
|
||||||
|
if patId == '9a4d4b06-5928-4101-b95e-e5ba03a1abfd':
|
||||||
|
patId = 'b66a85f1-4aaa-4db8-942a-2de44341824e'
|
||||||
|
|
||||||
|
req = await self.get(
|
||||||
|
'/getCurrHosp',
|
||||||
|
params={'patId': patId},
|
||||||
|
headers={'Authorization': f'Bearer {token}'},
|
||||||
|
)
|
||||||
|
|
||||||
|
match req.status_code:
|
||||||
|
case st.HTTP_200_OK:
|
||||||
|
return s.CurHospitalizationsModel.model_validate(req.json())
|
||||||
|
|
||||||
|
case st.HTTP_206_PARTIAL_CONTENT:
|
||||||
|
return s.CurHospitalizationsModel(
|
||||||
|
Hospitalizations=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
case _:
|
||||||
|
self.logger.error(req.json())
|
||||||
|
raise e.UnknownException
|
||||||
|
|
||||||
|
async def getHosps(self, patId: str):
|
||||||
|
token = await self.get_token()
|
||||||
|
if patId == '9a4d4b06-5928-4101-b95e-e5ba03a1abfd':
|
||||||
|
patId = 'b66a85f1-4aaa-4db8-942a-2de44341824e'
|
||||||
|
|
||||||
|
req = await self.get(
|
||||||
|
'/getHosps',
|
||||||
|
params={'patId': patId},
|
||||||
|
headers={'Authorization': f'Bearer {token}'},
|
||||||
|
)
|
||||||
|
|
||||||
|
match req.status_code:
|
||||||
|
case st.HTTP_200_OK:
|
||||||
|
return s.HospitalizationsModel.model_validate(req.json())
|
||||||
|
|
||||||
|
case st.HTTP_206_PARTIAL_CONTENT:
|
||||||
|
error = s.ErrorModel.model_validate(req.json())
|
||||||
|
|
||||||
|
if (
|
||||||
|
error.error
|
||||||
|
== 'Не найдены госпитализации по указанному patId'
|
||||||
|
):
|
||||||
|
return s.HospitalizationsModel(
|
||||||
|
Hospitalizations=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
case _:
|
||||||
|
self.logger.error(req.json())
|
||||||
|
raise e.UnknownException
|
||||||
|
|
||||||
|
async def getHospRecommendations(self, patId: str):
|
||||||
|
token = await self.get_token()
|
||||||
|
if patId == '9a4d4b06-5928-4101-b95e-e5ba03a1abfd':
|
||||||
|
patId = 'b66a85f1-4aaa-4db8-942a-2de44341824e'
|
||||||
|
|
||||||
|
req = await self.get(
|
||||||
|
'/getHospRecommendations',
|
||||||
|
params={'patId': patId},
|
||||||
|
headers={'Authorization': f'Bearer {token}'},
|
||||||
|
)
|
||||||
|
|
||||||
|
match req.status_code:
|
||||||
|
case st.HTTP_200_OK:
|
||||||
|
return s.HospRecommendationsModel.model_validate(req.json())
|
||||||
|
|
||||||
|
case st.HTTP_206_PARTIAL_CONTENT:
|
||||||
|
error = s.ErrorModel.model_validate(req.json())
|
||||||
|
|
||||||
|
if (
|
||||||
|
error.error
|
||||||
|
== 'Не найдены госпитализации по указанному patId'
|
||||||
|
):
|
||||||
|
return s.HospRecommendationsModel(
|
||||||
|
EventID='none',
|
||||||
|
EventDate=datetime.now(UTC),
|
||||||
|
Recommendations=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
case _:
|
||||||
|
self.logger.error(req.json())
|
||||||
|
raise e.UnknownException
|
||||||
|
|
||||||
|
async def getHospRoutes(self, patId: str):
|
||||||
|
token = await self.get_token()
|
||||||
|
if patId == '9a4d4b06-5928-4101-b95e-e5ba03a1abfd':
|
||||||
|
patId = 'b66a85f1-4aaa-4db8-942a-2de44341824e'
|
||||||
|
|
||||||
|
req = await self.get(
|
||||||
|
'/getHospRoutes',
|
||||||
|
params={'patId': patId},
|
||||||
|
headers={'Authorization': f'Bearer {token}'},
|
||||||
|
)
|
||||||
|
|
||||||
|
match req.status_code:
|
||||||
|
case st.HTTP_200_OK:
|
||||||
|
return s.HospRoutesModel.model_validate(req.json())
|
||||||
|
|
||||||
|
case st.HTTP_206_PARTIAL_CONTENT:
|
||||||
|
error = s.ErrorModel.model_validate(req.json())
|
||||||
|
|
||||||
|
if error.error == 'Пациент не госпитализирован!':
|
||||||
|
return s.HospRoutesModel(
|
||||||
|
EventID='none',
|
||||||
|
EventDate=datetime.now(UTC),
|
||||||
|
RoutesToDoctor=[],
|
||||||
|
RoutesToDiagnostic=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
case _:
|
||||||
|
self.logger.error(req.json())
|
||||||
|
raise e.UnknownException
|
||||||
|
|
||||||
|
async def getDiagnosticResults(self, patId: str):
|
||||||
|
token = await self.get_token()
|
||||||
|
req = await self.get(
|
||||||
|
'/getDiagnosticResults',
|
||||||
|
params={'patId': patId},
|
||||||
|
headers={'Authorization': f'Bearer {token}'},
|
||||||
|
)
|
||||||
|
|
||||||
|
match req.status_code:
|
||||||
|
case st.HTTP_200_OK:
|
||||||
|
return s.DiagnosticResultsModel.model_validate(req.json())
|
||||||
|
case _:
|
||||||
|
self.logger.error(req.json())
|
||||||
|
raise e.UnknownException
|
||||||
|
|
||||||
|
async def getELNs(self, patId: str):
|
||||||
|
token = await self.get_token()
|
||||||
|
req = await self.get(
|
||||||
|
'/getELNs',
|
||||||
|
params={'patId': patId},
|
||||||
|
headers={'Authorization': f'Bearer {token}'},
|
||||||
|
)
|
||||||
|
|
||||||
|
match req.status_code:
|
||||||
|
case st.HTTP_200_OK:
|
||||||
|
return s.ELNsModel.model_validate(req.json())
|
||||||
|
|
||||||
|
case st.HTTP_206_PARTIAL_CONTENT:
|
||||||
|
error = s.ErrorModel.model_validate(req.json())
|
||||||
|
|
||||||
|
if error.error == 'Пациент не госпитализирован!':
|
||||||
|
return s.ELNsModel(PatientELNs=[])
|
||||||
|
|
||||||
|
case _:
|
||||||
|
self.logger.error(req.json())
|
||||||
|
raise e.UnknownException
|
||||||
|
|
||||||
|
async def getPatFLG(self, patId: str):
|
||||||
|
token = await self.get_token()
|
||||||
|
req = await self.get(
|
||||||
|
'/getPatFLG',
|
||||||
|
params={'patId': patId},
|
||||||
|
headers={'Authorization': f'Bearer {token}'},
|
||||||
|
)
|
||||||
|
|
||||||
|
match req.status_code:
|
||||||
|
case st.HTTP_200_OK:
|
||||||
|
return s.PatientFLGModel.model_validate(req.json())
|
||||||
|
case _:
|
||||||
|
self.logger.error(req.json())
|
||||||
|
raise e.UnknownException
|
||||||
|
|
||||||
|
async def getDiagResultFile(self, resultId: str):
|
||||||
|
token = await self.get_token()
|
||||||
|
req = await self.get(
|
||||||
|
'/getDiagResultFile',
|
||||||
|
params={'resultId': resultId},
|
||||||
|
headers={'Authorization': f'Bearer {token}'},
|
||||||
|
)
|
||||||
|
|
||||||
|
match req.status_code:
|
||||||
|
case st.HTTP_200_OK:
|
||||||
|
return s.DiagResultFileModel.model_validate(req.json())
|
||||||
|
|
||||||
|
case st.HTTP_206_PARTIAL_CONTENT:
|
||||||
|
error = s.ErrorModel.model_validate(req.json())
|
||||||
|
|
||||||
|
if (
|
||||||
|
error.error == 'Не найдены проведенные исследования по '
|
||||||
|
'данному идентификатору'
|
||||||
|
):
|
||||||
|
return s.DiagResultFileModel(content='')
|
||||||
|
|
||||||
|
case _:
|
||||||
|
self.logger.error(req.json())
|
||||||
|
raise e.UnknownException
|
||||||
717
src/clients/vitacore/schema.py
Normal file
717
src/clients/vitacore/schema.py
Normal file
@ -0,0 +1,717 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorModel(BaseModel):
|
||||||
|
model_config = ConfigDict(
|
||||||
|
validate_by_alias=True,
|
||||||
|
validate_by_name=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
error: str = Field(title='Текст ошибки', alias='message')
|
||||||
|
|
||||||
|
|
||||||
|
class PatientModel(BaseModel):
|
||||||
|
patId: str = Field(
|
||||||
|
title='Идентификатор пациента',
|
||||||
|
examples=['b62e9f22-a871-4c52-96d6-559c707a716d'],
|
||||||
|
)
|
||||||
|
SNILS: str = Field(title='СНИЛС', examples=['000-000-600 18'])
|
||||||
|
lastName: str = Field(title='Фамилия', examples=['Тестовый'])
|
||||||
|
firstName: str = Field(title='Имя', examples=['Пациент'])
|
||||||
|
middleName: str = Field(title='Отчество', examples=['Пациентович'])
|
||||||
|
birthDate: datetime = Field(title='Дата рождения', examples=['2024-10-16'])
|
||||||
|
gender: str = Field(title='Пол', examples=['М'])
|
||||||
|
|
||||||
|
|
||||||
|
class TrustedPersonModel(BaseModel):
|
||||||
|
parentSnils: str = Field(
|
||||||
|
title='СНИЛС представителя', examples=['156-125-394 57']
|
||||||
|
)
|
||||||
|
represType: str = Field(
|
||||||
|
title='Вид родства (Мать/Отец/Иной родственник/Не родственник)',
|
||||||
|
examples=['Мать'],
|
||||||
|
)
|
||||||
|
fullName: str = Field(
|
||||||
|
title='ФИО представителя', examples=['Тестова Тест Тестовна']
|
||||||
|
)
|
||||||
|
phone: str = Field(
|
||||||
|
title='Мобильный телефон представителя',
|
||||||
|
examples=['+7 (999) 112-33-21'],
|
||||||
|
)
|
||||||
|
IsGuardian: bool = Field(title='Флаг Опекун', examples=['true'])
|
||||||
|
IsTrustee: bool = Field(title='Флаг Попечитель', examples=['true'])
|
||||||
|
status: bool = Field(
|
||||||
|
title='Флаг Текущий представитель',
|
||||||
|
examples=['false'],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileModel(BaseModel):
|
||||||
|
id: str = Field(
|
||||||
|
title='Идентификатор пациента',
|
||||||
|
examples=['b62e9f22-a871-4c52-96d6-559c707a716d'],
|
||||||
|
)
|
||||||
|
SNILS: str = Field(title='СНИЛС', examples=['000-000-600 18'])
|
||||||
|
lastName: str = Field(title='Фамилия', examples=['Тестовый'])
|
||||||
|
firstName: str = Field(title='Имя', examples=['Пациент'])
|
||||||
|
middleName: str = Field(title='Отчество', examples=['Ребенок'])
|
||||||
|
birthDate: str = Field(title='Дата рождения', examples=['2024-10-16'])
|
||||||
|
gender: str = Field(title='Пол', examples=['М'])
|
||||||
|
docType: str = Field(
|
||||||
|
title='Тип документа',
|
||||||
|
examples=['Свидетельство о рождении, выданное в РФ'],
|
||||||
|
)
|
||||||
|
docSer: str = Field(title='Серия документа', examples=['III-КБ'])
|
||||||
|
docNum: str = Field(title='Номер документа', examples=['999999'])
|
||||||
|
ENP: str | None = Field(
|
||||||
|
title='Единый номер полиса ОМС (16-ти значный)',
|
||||||
|
examples=['?'],
|
||||||
|
default=None,
|
||||||
|
)
|
||||||
|
phones: str | None = Field(
|
||||||
|
title='Номера телефонов',
|
||||||
|
examples=['+7 (999) 112-33-21 (моб.)'],
|
||||||
|
default=None,
|
||||||
|
)
|
||||||
|
addressReal: str = Field(
|
||||||
|
title='Адрес проживания',
|
||||||
|
examples=[
|
||||||
|
'420000, Татарстан Респ, г.Казань, ул.Магистральная (Большие Клыки), д.1, кв.1' # noqa: E501
|
||||||
|
],
|
||||||
|
)
|
||||||
|
attachBranchId: str = Field(
|
||||||
|
title='Идентификатор СТП прикреплния',
|
||||||
|
examples=['string'],
|
||||||
|
)
|
||||||
|
attachState: str = Field(
|
||||||
|
title='Номер участка',
|
||||||
|
examples=['99'],
|
||||||
|
)
|
||||||
|
trustedPersons: list[TrustedPersonModel] = Field(
|
||||||
|
title='Информация о представителе', default=[]
|
||||||
|
)
|
||||||
|
dispFlag: bool | None = Field(
|
||||||
|
title='Флаг диспансеризации', examples=['false'], default=None
|
||||||
|
)
|
||||||
|
dispDate: datetime | None = Field(
|
||||||
|
title='Дата диспансеризации', default=None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DepartmentAddressModel(BaseModel):
|
||||||
|
type: str = Field(
|
||||||
|
title='для МО: «Юридический», для филиалов «Фактический»',
|
||||||
|
examples=['«Юридический»'],
|
||||||
|
)
|
||||||
|
display: str = Field(
|
||||||
|
title='Адрес строкой',
|
||||||
|
examples=['420097, г.Казань, ул.Заслонова, д.5'],
|
||||||
|
)
|
||||||
|
latitude: str | None = Field(
|
||||||
|
title='Широта, при наличии', examples=[55.789], default=None
|
||||||
|
)
|
||||||
|
longitude: str | None = Field(
|
||||||
|
title='Долгота, при наличии', examples=[37.789], default=None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DepartmentModel(BaseModel):
|
||||||
|
id: str = Field(
|
||||||
|
title='Идентификатор МО/Филиала',
|
||||||
|
examples=['a3677271-3385-4f27-a65d-c3430b7c61c2'],
|
||||||
|
)
|
||||||
|
OID: str | None = Field(
|
||||||
|
title='OID МО / Филиала',
|
||||||
|
examples=['1.2.643.5.1.13.13.12.2.16.1084'],
|
||||||
|
default=None,
|
||||||
|
)
|
||||||
|
parentId: str | None = Field(
|
||||||
|
title='Идентификатор вышестоящего подразделения',
|
||||||
|
examples=['a3677271-3385-4f27-a65d-c3430b7c61c2'],
|
||||||
|
)
|
||||||
|
fullname: str | None = Field(
|
||||||
|
title='Полное наименование',
|
||||||
|
examples=['ГБУЗС "Тестовая медицинская организация"'],
|
||||||
|
default=None,
|
||||||
|
)
|
||||||
|
shortname: str = Field(
|
||||||
|
title='Краткое наименование',
|
||||||
|
examples=['ГБУЗС "Тестовая медицинская организация"'],
|
||||||
|
)
|
||||||
|
type: str = Field(
|
||||||
|
title='Тип подразделения (для МО: «Юридический»'
|
||||||
|
', для филиалов: Стационар / Поликлиника / ФАП / Амбулатория)',
|
||||||
|
examples=['Юридическое лицо'],
|
||||||
|
)
|
||||||
|
phone: str | None = Field(
|
||||||
|
title='Номер телефона',
|
||||||
|
default=None,
|
||||||
|
)
|
||||||
|
inn: str | None = Field(title='ИНН', examples=['0000000000'], default=None)
|
||||||
|
ogrn: str | None = Field(
|
||||||
|
title='ОГРН', examples=['1149204047816'], default=None
|
||||||
|
)
|
||||||
|
kpp: str | None = Field(title='КПП', examples=['0000000000'], default=None)
|
||||||
|
address: list[DepartmentAddressModel] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class OrganizationsModel(BaseModel):
|
||||||
|
Organizations: list[DepartmentModel]
|
||||||
|
|
||||||
|
|
||||||
|
class WorkersPositionModel(BaseModel):
|
||||||
|
id: str = Field(
|
||||||
|
title='Идентификатор сотрудника в связке с должностью',
|
||||||
|
examples=['41019'],
|
||||||
|
)
|
||||||
|
dateBegin: datetime = Field(
|
||||||
|
title='Дата приёма на работу', examples=['01.08.2022']
|
||||||
|
)
|
||||||
|
departmentId: str = Field(
|
||||||
|
title='ID ФИЛИАЛА',
|
||||||
|
examples=['10ea04ca-339a-4867-aab4-d9a83b7e9098'],
|
||||||
|
)
|
||||||
|
departmentName: str = Field(
|
||||||
|
title='Краткое наименование филиала',
|
||||||
|
examples=['Поликлиника'],
|
||||||
|
)
|
||||||
|
positionName: str = Field(
|
||||||
|
title='Тип должности.Наименование (POST_TYPE)',
|
||||||
|
examples=['Врач-онколог'],
|
||||||
|
)
|
||||||
|
positionFedCode: str = Field(
|
||||||
|
title='Тип должности.Код (POST_TYPE)',
|
||||||
|
examples=['3037'],
|
||||||
|
)
|
||||||
|
positionRegName: str | None = Field(
|
||||||
|
title='ФРМР. Должности медицинского персонала',
|
||||||
|
examples=['врач-онколог'],
|
||||||
|
)
|
||||||
|
positionSpecialityCode: str | None = Field(
|
||||||
|
title='Код специальности по V021',
|
||||||
|
examples=['41'],
|
||||||
|
default=None,
|
||||||
|
)
|
||||||
|
positionSpecialityName: str | None = Field(
|
||||||
|
title='Наименование специальности по V021',
|
||||||
|
examples=['Онкология'],
|
||||||
|
default=None,
|
||||||
|
)
|
||||||
|
rate: float = Field(
|
||||||
|
title='Ставка',
|
||||||
|
examples=[0.5],
|
||||||
|
)
|
||||||
|
|
||||||
|
@field_validator('rate', mode='before')
|
||||||
|
def convert_comma(cls, v: str | float):
|
||||||
|
if isinstance(v, str):
|
||||||
|
return v.replace(',', '.')
|
||||||
|
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
class WorkerModel(BaseModel):
|
||||||
|
id: str = Field(
|
||||||
|
title='Идентификатор сотрудника',
|
||||||
|
examples=['dc911302-5044-46f4-b935-c6ffd85eb68f'],
|
||||||
|
)
|
||||||
|
SNILS: str = Field(
|
||||||
|
title='СНИЛС',
|
||||||
|
examples=['059-486-659 26'],
|
||||||
|
)
|
||||||
|
firstName: str = Field(title='Имя', examples=['Владимир'])
|
||||||
|
lastName: str = Field(title='Фамилия', examples=['Камашев'])
|
||||||
|
middleName: str | None = Field(
|
||||||
|
title='Отчество', examples=['Михайлович'], default=None
|
||||||
|
)
|
||||||
|
birthDate: datetime | None = Field(
|
||||||
|
title='Дата рождения', examples=['30.05.1961'], default=None
|
||||||
|
)
|
||||||
|
positions: list[WorkersPositionModel] = []
|
||||||
|
|
||||||
|
|
||||||
|
class WorkersModel(BaseModel):
|
||||||
|
Workers: list[WorkerModel]
|
||||||
|
|
||||||
|
|
||||||
|
class SpecialityV021Model(BaseModel):
|
||||||
|
SpecialityID: str = Field(
|
||||||
|
title='Код спциальности по справочнику V021', examples=['1']
|
||||||
|
)
|
||||||
|
SpecialityName: str = Field(
|
||||||
|
title='Наимнование спциальности по справочнику V021',
|
||||||
|
examples=['Авиационная и космическая медицина'],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SpecsV021Model(BaseModel):
|
||||||
|
SpecialitiesV021: list[SpecialityV021Model]
|
||||||
|
|
||||||
|
|
||||||
|
class EntryModel(BaseModel):
|
||||||
|
DateTime: datetime = Field(
|
||||||
|
title='Дата и время записи',
|
||||||
|
examples=['2022-08-01T00:00:00.000Z'],
|
||||||
|
)
|
||||||
|
TicketType: str = Field(title='Тип талона', examples=['Первичный'])
|
||||||
|
EntryPlace: str = Field(
|
||||||
|
title='Место записи',
|
||||||
|
examples=['Регистратура'],
|
||||||
|
)
|
||||||
|
EntryType: str = Field(
|
||||||
|
title='Тип записи', examples=['Запись на прием к врачу']
|
||||||
|
)
|
||||||
|
Place: str = Field(
|
||||||
|
title='Кабинет приёма',
|
||||||
|
examples=['6'],
|
||||||
|
)
|
||||||
|
ResourceName: str = Field(
|
||||||
|
title='ФИО врача или наимнование кабинета полностью',
|
||||||
|
examples=['Сиразетдинов Рамзис Халафутдинович'],
|
||||||
|
)
|
||||||
|
Speciality: str = Field(
|
||||||
|
title='Специальность',
|
||||||
|
examples=['Врач общей практики (семейный врач)'],
|
||||||
|
)
|
||||||
|
Branch: str = Field(
|
||||||
|
title='Подразделение',
|
||||||
|
examples=['Главное здание'],
|
||||||
|
)
|
||||||
|
LpuName: str = Field(
|
||||||
|
title='Наименование МО',
|
||||||
|
examples=['ГАУЗ Азнакаевская ЦРБ'],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class EntriesModel(BaseModel):
|
||||||
|
Entries: list[EntryModel]
|
||||||
|
|
||||||
|
|
||||||
|
class VaccsReportModel(BaseModel):
|
||||||
|
content: str = Field(
|
||||||
|
title='Содержимое документа в формате .doc '
|
||||||
|
'(сжатое методом кодирования Base64)'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MedExamItemModel(BaseModel):
|
||||||
|
AgeGroupName: str = Field(
|
||||||
|
title='Наименование возрастной группы',
|
||||||
|
examples=['ДВН 1 этап. приказ 404н'],
|
||||||
|
)
|
||||||
|
# AgeGroupCriteria
|
||||||
|
Required: str = Field(
|
||||||
|
title='Обязательность',
|
||||||
|
examples=['Обязательный/Дополнительный'],
|
||||||
|
)
|
||||||
|
Type: str = Field(
|
||||||
|
title='Тип',
|
||||||
|
examples=['Специальность/Услуга'],
|
||||||
|
)
|
||||||
|
MedicalServiceCode: str = Field(
|
||||||
|
title='Код услуги',
|
||||||
|
examples=['B04.010.002'],
|
||||||
|
)
|
||||||
|
MedicalServiceName: str = Field(
|
||||||
|
title='Наименрование услуги',
|
||||||
|
examples=['Осмотр фельдшером (акушеркой) или врачом акушером'],
|
||||||
|
)
|
||||||
|
SpecialityName: str | None = Field(
|
||||||
|
title='Специальность',
|
||||||
|
examples=['Акушер-гинеколог'],
|
||||||
|
default=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MedExamTypeModel(BaseModel):
|
||||||
|
Name: str = Field(
|
||||||
|
title='Наименование медосмотра',
|
||||||
|
examples=['ДВН 1 этап. приказ 404н'],
|
||||||
|
)
|
||||||
|
Code: str = Field(
|
||||||
|
title='Код медосмотра',
|
||||||
|
examples=['024'],
|
||||||
|
)
|
||||||
|
MedExamItems: list[MedExamItemModel]
|
||||||
|
|
||||||
|
|
||||||
|
class MedExamDictModel(BaseModel):
|
||||||
|
MedExamTypes: list[MedExamTypeModel]
|
||||||
|
|
||||||
|
|
||||||
|
class ResultModel(BaseModel):
|
||||||
|
Resource: str = Field(title='Врач', examples=['Изотова Г.М.'])
|
||||||
|
ExaminationId: str | None = Field(
|
||||||
|
title='Идентификатор осмотра',
|
||||||
|
examples=['4d8550e6-7ad8-46f7-b874-9b20c673d168'],
|
||||||
|
default=None,
|
||||||
|
)
|
||||||
|
DiagResultId: str | None = Field(
|
||||||
|
title='Идентификатор результата исследования',
|
||||||
|
examples=['77312a5f-0498-4656-8215-9cf151ec2a9b'],
|
||||||
|
default=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RouteModel(BaseModel):
|
||||||
|
Type: str = Field(
|
||||||
|
title='Тип',
|
||||||
|
examples=['ROUTE_TO_DOCTOR_INSPECTION', 'ROUTE_TO_DIAGNOSTICS'],
|
||||||
|
)
|
||||||
|
CreationDateTime: datetime = Field(
|
||||||
|
title='Дата и время создания', examples=['2025-03-24 14:49']
|
||||||
|
)
|
||||||
|
Name: str = Field(
|
||||||
|
title='Наименование услуги / специальности',
|
||||||
|
examples=['Кардиолог [1259]', 'Общий анализ крови [B03.016.002]'],
|
||||||
|
)
|
||||||
|
EntryPlace: str | None = Field(
|
||||||
|
title='Кабинет приёма',
|
||||||
|
examples=['Каб.№211 Шайдуллина Г.И. (Кардиолог)', None],
|
||||||
|
default=None,
|
||||||
|
)
|
||||||
|
EntryResName: str | None = Field(
|
||||||
|
title='ФИО врача или кабинета полностью',
|
||||||
|
examples=['Каб.№211 Шайдуллина Г.И. (Кардиолог)', None],
|
||||||
|
default=None,
|
||||||
|
)
|
||||||
|
ResultExits: str = Field(title='Исполнение', examples=['0', '1'])
|
||||||
|
Result: ResultModel | None = Field(
|
||||||
|
title='Связка осмотра/услуги с выполнением направления', default=None
|
||||||
|
)
|
||||||
|
LpuName: str | None = Field(
|
||||||
|
title='Наименование МО', examples=['ГАУЗ "ГКБ №7"'], default=None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RoutesListModel(BaseModel):
|
||||||
|
EventID: str = Field(
|
||||||
|
title='Идентификатор случая госпитализации',
|
||||||
|
examples=['a268e6d7-618c-4b83-97ca-c9dc8b79b55b'],
|
||||||
|
)
|
||||||
|
EventDate: datetime = Field(
|
||||||
|
title='Дата обращения', examples=['2025-03-17']
|
||||||
|
)
|
||||||
|
LpuName: str = Field(title='Наименование МО', examples=['ГАУЗ "ГКБ №7"'])
|
||||||
|
Routes: list[RouteModel] = Field(title='Список направлений')
|
||||||
|
|
||||||
|
|
||||||
|
class SEMDModel(BaseModel):
|
||||||
|
SEMDContent: str = Field(
|
||||||
|
title='СЭМД (XML сжатый методом шифрования Base64)',
|
||||||
|
examples=['77u/PD94bWwgdmVyc2lvbj0iMS...'],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ExaminationModel(BaseModel):
|
||||||
|
ExaminationId: str = Field(
|
||||||
|
title='Идентификатор осмотра',
|
||||||
|
examples=['f22be2c9-8e68-42d6-851e-fbf4a5e8f657'],
|
||||||
|
)
|
||||||
|
DateTime: str = Field(
|
||||||
|
title='Дата и время создания', examples=['01.08.2025 15:47:15']
|
||||||
|
)
|
||||||
|
Post: str = Field(title='Врач', examples=['Абдуллина Ирина Владимировна'])
|
||||||
|
Speciality: str = Field(
|
||||||
|
title='Специальность врача', examples=['Акушер-гинеколог']
|
||||||
|
)
|
||||||
|
ExaminationText: str = Field(
|
||||||
|
title='Текст осмотра',
|
||||||
|
examples=[
|
||||||
|
'<TABLE id=e0f61ef0-8f5e-42c6-95b6-4074715902e6 class=Complaint '
|
||||||
|
'style="BORDER-TOP: #ffffff 1px..... </TABLE>'
|
||||||
|
],
|
||||||
|
)
|
||||||
|
Recommendation: str | None = Field(
|
||||||
|
title='Идентификатор результата исследования',
|
||||||
|
examples=['рекомендации 1 тест'],
|
||||||
|
default=None,
|
||||||
|
)
|
||||||
|
SEMDs: list[SEMDModel] | None = Field(title='Список СЭМД', default=None)
|
||||||
|
|
||||||
|
|
||||||
|
class HospExaminationsModel(BaseModel):
|
||||||
|
EventID: str = Field(
|
||||||
|
title='Идентификатор случая госпитализации',
|
||||||
|
examples=['2f7d395d-f5fc-4a0e-af2d-855324f6e7f1'],
|
||||||
|
)
|
||||||
|
EventDate: datetime = Field(
|
||||||
|
title='Дата обращения', examples=['2025-08-01']
|
||||||
|
)
|
||||||
|
LpuName: str = Field(title='Наименование МО', examples=['ГАУЗ "ГКБ №7"'])
|
||||||
|
Examinations: list[ExaminationModel] | None = Field(
|
||||||
|
title='Список осмотров', default=None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RouteToDoctorModel(BaseModel):
|
||||||
|
RouteDate: str = Field(title='Дата направления', examples=['2025-07-17'])
|
||||||
|
SpecialityCode: str = Field(title='Код специальности', examples=['013'])
|
||||||
|
SpecialityName: str = Field(
|
||||||
|
title='Наименование специальности', examples=['Кардиолог']
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RouteToDiagnosticModel(BaseModel):
|
||||||
|
RouteDate: str = Field(title='Дата направления', examples=['2025-08-06'])
|
||||||
|
ResearchCode: str = Field(title='Код услуги', examples=['B03.016.003'])
|
||||||
|
ResearchName: str = Field(
|
||||||
|
title='Наименование услуги',
|
||||||
|
examples=['Общий (клинический) анализ крови'],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class HospDestinationModel(BaseModel):
|
||||||
|
Signa: str = Field(
|
||||||
|
title='Сигнатура назначения',
|
||||||
|
examples=[
|
||||||
|
'Diphenhydraminum+Oleum foliorum Menthae piperitae+'
|
||||||
|
'Tinctura Leonuri+Ethylbromisovalerinatum капли для приема внутрь'
|
||||||
|
' 25 мл x 1 доза. \nВнутрь (перорально) (утром) 3 раза через день'
|
||||||
|
'(08:00, 13:00, 18:00). Повторять 2 раза. Назначил:Изотова Г.М.'
|
||||||
|
' (Сердечно-сосудистый хирург)'
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ExamModel(BaseModel):
|
||||||
|
ExaminationDate: str = Field(title='Дата осмотра', examples=['2025-08-04'])
|
||||||
|
ExaminationTime: str = Field(title='Время осмотра', examples=['09:21'])
|
||||||
|
Status: str = Field(title='Состояние при осмотре', examples=[''])
|
||||||
|
Post: str = Field(title='Врач', examples=['Шайдуллина Г.И. (Кардиолог)'])
|
||||||
|
MedicalExaminationType: str = Field(
|
||||||
|
title='Тип медицинского осмотра',
|
||||||
|
examples=['Осмотр врача-консультанта'],
|
||||||
|
)
|
||||||
|
RoutesToDoctor: list[RouteToDoctorModel] | None = Field(
|
||||||
|
title='Направления ко врачу', default=None
|
||||||
|
)
|
||||||
|
RoutesToDiagnostic: list[RouteToDiagnosticModel] | None = Field(
|
||||||
|
title='Направления на услугу', default=None
|
||||||
|
)
|
||||||
|
HospDestinations: list[HospDestinationModel] | None = Field(
|
||||||
|
title='Лекарственные назначения', default=None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseHospitalizationModel(BaseModel):
|
||||||
|
EventID: str = Field(
|
||||||
|
title='Идентификатор случая госпитализации',
|
||||||
|
examples=['ddfa23ea-b0de-4d88-8abe-7d6a7a241df1'],
|
||||||
|
)
|
||||||
|
CreationDateTime: datetime = Field(
|
||||||
|
title='Дата и время регистрации', examples=['2025-07-10 17:29']
|
||||||
|
)
|
||||||
|
ReceptionDiagnosis: str | None = Field(
|
||||||
|
title='Диагноз при поступлении',
|
||||||
|
examples=[
|
||||||
|
'I11.9 | Гипертензивная [гипертоническая] болезнь с '
|
||||||
|
'преимущественным поражением сердца без (застойной) '
|
||||||
|
'сердечной недостаточности'
|
||||||
|
],
|
||||||
|
default=None,
|
||||||
|
)
|
||||||
|
HospitalizationType: str | None = Field(
|
||||||
|
title='Тип госпитализации', examples=['экстренная'], default=None
|
||||||
|
)
|
||||||
|
HospitalizationReason: str | None = Field(
|
||||||
|
title='Причина госпитализации', examples=['заболевание'], default=None
|
||||||
|
)
|
||||||
|
Division: str | None = Field(
|
||||||
|
title='Отделение', examples=['Кардиология №1'], default=None
|
||||||
|
)
|
||||||
|
LpuName: str = Field(title='Наименование МО', examples=['ГАУЗ "ГКБ №7"'])
|
||||||
|
|
||||||
|
|
||||||
|
class HospitalizationModel(BaseHospitalizationModel):
|
||||||
|
Diagnosis: str = Field(
|
||||||
|
title='Текущий диагноз',
|
||||||
|
examples=[
|
||||||
|
'I11.9 | Гипертензивная [гипертоническая] болезнь с '
|
||||||
|
'преимущественным поражением сердца без (застойной) '
|
||||||
|
'сердечной недостаточности'
|
||||||
|
],
|
||||||
|
)
|
||||||
|
Exams: list[ExamModel] = Field(title='Список осмотров')
|
||||||
|
|
||||||
|
|
||||||
|
class CurHospitalizationsModel(BaseModel):
|
||||||
|
Hospitalizations: list[HospitalizationModel] = Field(
|
||||||
|
title='Список госпитализаций'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class HospitalizationsModel(BaseModel):
|
||||||
|
Hospitalizations: list[BaseHospitalizationModel] = Field(
|
||||||
|
title='Список госпитализаций'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RecommendationModel(BaseModel):
|
||||||
|
Type: str = Field(title='Тип осмотра', examples=['Осмотр', 'Эпикриз'])
|
||||||
|
DateTime: str = Field(
|
||||||
|
title='Дата и время создания',
|
||||||
|
examples=['18.07.2025 8:30:43'],
|
||||||
|
)
|
||||||
|
Recommendation: str = Field(
|
||||||
|
title='Текст рекомендации', examples=['рекомендации тест']
|
||||||
|
)
|
||||||
|
Post: str = Field(title='Врач', examples=['Тестовый В.С.'])
|
||||||
|
|
||||||
|
|
||||||
|
class HospRecommendationsModel(BaseModel):
|
||||||
|
EventID: str = Field(
|
||||||
|
title='Идентификатор случая госпитализации',
|
||||||
|
examples=['ddfa23ea-b0de-4d88-8abe-7d6a7a241df1'],
|
||||||
|
)
|
||||||
|
EventDate: datetime = Field(
|
||||||
|
title='Дата обращения', examples=['2025-07-10']
|
||||||
|
)
|
||||||
|
Recommendations: list[RecommendationModel] = Field(
|
||||||
|
title='Список рекомендаций'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class HospRoutesModel(BaseModel):
|
||||||
|
EventID: str = Field(
|
||||||
|
title='Идентификатор случая госпитализации',
|
||||||
|
examples=['b8227793-0f40-40f0-b8aa-9fc00cc13b96'],
|
||||||
|
)
|
||||||
|
EventDate: datetime = Field(
|
||||||
|
title='Дата обращения', examples=['2025-07-21']
|
||||||
|
)
|
||||||
|
RoutesToDoctor: list[RouteToDoctorModel] = Field(
|
||||||
|
title='Направления ко врачу'
|
||||||
|
)
|
||||||
|
RoutesToDiagnostic: list[RouteToDiagnosticModel] = Field(
|
||||||
|
title='Направления на услугу'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DiagnosticResultModel(BaseModel):
|
||||||
|
DiagResultID: str = Field(
|
||||||
|
title='Идентификатор результата исследования',
|
||||||
|
examples=['1407910a-1901-4b21-be2d-0ef89041f4fe'],
|
||||||
|
)
|
||||||
|
ContainsFile: int = Field(
|
||||||
|
title='Признак наличия файла исследования (из ЛИС)', examples=['0']
|
||||||
|
)
|
||||||
|
PostingDate: datetime = Field(
|
||||||
|
title='Дата направления', examples=['2025-05-06']
|
||||||
|
)
|
||||||
|
MedServiceCode: str = Field(
|
||||||
|
title='Код исследования', examples=['A12.05.004.002']
|
||||||
|
)
|
||||||
|
MedServiceName: str = Field(
|
||||||
|
title='Наименование исследования',
|
||||||
|
examples=[
|
||||||
|
'Проба на совместимость перед переливанием эритроцитов по '
|
||||||
|
'неполным антителам (IgG)'
|
||||||
|
],
|
||||||
|
)
|
||||||
|
PostName: str = Field(title='Врач', examples=['Сиразиева Г.Р.'])
|
||||||
|
PostSpec: str = Field(title='Специальность', examples=['Терапевт'])
|
||||||
|
LpuName: str = Field(title='Наименование МО', examples=['ГАУЗ "ГКБ №7"'])
|
||||||
|
EventID: str = Field(
|
||||||
|
title='Идентификатор случая',
|
||||||
|
examples=['36cf2c90-fdad-4961-899c-652c5e0817a9'],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DiagnosticResultsModel(BaseModel):
|
||||||
|
DainosticsResults: list[DiagnosticResultModel] = Field(
|
||||||
|
title='Список результатов исследований'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ProlongationModel(BaseModel):
|
||||||
|
StartDate: datetime = Field(
|
||||||
|
title='Дата продления с', examples=['2020-10-21']
|
||||||
|
)
|
||||||
|
ProlongationDate: str = Field(
|
||||||
|
title='Дата продления до', examples=['2020-10-25']
|
||||||
|
)
|
||||||
|
Post: str = Field(
|
||||||
|
title='Врач',
|
||||||
|
examples=['Ахсанова Р.М. (Врач общей практики (семейный врач))'],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PatientELNModel(BaseModel):
|
||||||
|
PatientSNILS: str | None = Field(
|
||||||
|
title='СНИЛС болеющего', examples=['000-000-600 01']
|
||||||
|
)
|
||||||
|
Number: str | None = Field(
|
||||||
|
title='Номер листа нетрудоспособности', examples=['910040906829']
|
||||||
|
)
|
||||||
|
OpenDate: datetime | None = Field(
|
||||||
|
title='Дата открытия', examples=['2020-10-21'], default=None
|
||||||
|
)
|
||||||
|
DateClose: datetime | None = Field(
|
||||||
|
title='Дата закрытия', examples=['2020-10-25'], default=None
|
||||||
|
)
|
||||||
|
WorkDate: datetime | None = Field(
|
||||||
|
title='Дата выхода на работу', examples=['2020-10-26'], default=None
|
||||||
|
)
|
||||||
|
DaysCount: int = Field(
|
||||||
|
title='Количество дней нетрудоспособности', examples=[5]
|
||||||
|
)
|
||||||
|
Cause: str = Field(
|
||||||
|
title='Причина нетрудоспособности',
|
||||||
|
examples=[
|
||||||
|
'Заболевание (в том числе профессиональное заболевание и его '
|
||||||
|
'обострение)'
|
||||||
|
],
|
||||||
|
)
|
||||||
|
Prolongations: list[ProlongationModel] | None = Field(
|
||||||
|
title='Продления', default=None
|
||||||
|
)
|
||||||
|
LpuName: str | None = Field(
|
||||||
|
title='Наименование МО', examples=['ГАУЗ Новошешминская ЦРБ']
|
||||||
|
)
|
||||||
|
BranchAddress: str | None = Field(
|
||||||
|
title='Адрес подразделения выдачи',
|
||||||
|
examples=['Республика Татарстан,с.Новошешминск, ул.Майская, д.8'],
|
||||||
|
)
|
||||||
|
FssLnStatus: str = Field(title='Статус ЭЛН в ФСС', examples=['30'])
|
||||||
|
SentSNILS: str = Field(
|
||||||
|
title='СНИЛС получателя ЭЛН (отправленный в ФСС)',
|
||||||
|
examples=['00000060001'],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ELNsModel(BaseModel):
|
||||||
|
PatientELNs: list[PatientELNModel] = Field(
|
||||||
|
title='Список электронных листов нетрудоспособности'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PatientFLGModel(BaseModel):
|
||||||
|
id: str = Field(
|
||||||
|
title='Идентификатор пациента',
|
||||||
|
examples=['0bf2e271-e565-42a8-924e-0017bcdedecd'],
|
||||||
|
)
|
||||||
|
SNILS: str = Field(title='СНИЛС', examples=['127-192-834 66'])
|
||||||
|
LastFgDate: str = Field(
|
||||||
|
title='Дата последнего флюрографического осмотра',
|
||||||
|
examples=['2020-09-24'],
|
||||||
|
)
|
||||||
|
NextPrgDate: str = Field(
|
||||||
|
title='Дата следующего флюорографического осмотра',
|
||||||
|
examples=['2021-09-24'],
|
||||||
|
)
|
||||||
|
PrgContingent: str | None = Field(
|
||||||
|
title='Контингент (флюорография)',
|
||||||
|
examples=['Неорганизованное население'],
|
||||||
|
)
|
||||||
|
PrgDecision: str | None = Field(
|
||||||
|
title='Решение (флюорография)',
|
||||||
|
examples=['Требует дообследования'],
|
||||||
|
default=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DiagResultFileModel(BaseModel):
|
||||||
|
content: str
|
||||||
@ -41,15 +41,36 @@ class Settings(BaseSettings):
|
|||||||
# Loki Logging
|
# Loki Logging
|
||||||
LOKI_URL: str | None = Field(default=None)
|
LOKI_URL: str | None = Field(default=None)
|
||||||
|
|
||||||
# Environment
|
|
||||||
TMK_BASE_URL: str = Field(default='https://tmk-api.tatar.ru/api')
|
|
||||||
|
|
||||||
# ESIA
|
# ESIA
|
||||||
ESIA_BASE_URL: str = Field(default='https://esia.gosuslugi.ru')
|
ESIA_BASE_URL: str = Field(default='https://esia.gosuslugi.ru')
|
||||||
ESIA_CLIENT_ID: str = Field(default='')
|
ESIA_CLIENT_ID: str = Field(default='')
|
||||||
ESIA_REDIRECT_URI: str = Field(default='')
|
ESIA_REDIRECT_URI: str = Field(default='')
|
||||||
ESIA_CONTAINER_PASSWORD: str = Field(default='')
|
ESIA_CONTAINER_PASSWORD: str = Field(default='')
|
||||||
ESIA_CONTAINER_THUMBPRINT: str = Field(default='')
|
ESIA_CONTAINER_THUMBPRINT: str = Field(default='')
|
||||||
|
ESIA_PROXY: str | None = Field(default=None)
|
||||||
|
|
||||||
|
# Vitacore
|
||||||
|
VITACORE_BASE_URL: str = Field(
|
||||||
|
default='https://gist-cws.ezdrav.ru:8899/MP_API'
|
||||||
|
)
|
||||||
|
VITACORE_USERNAME: str = Field(default='')
|
||||||
|
VITACORE_PASSWORD: str = Field(default='')
|
||||||
|
|
||||||
|
# AEMD
|
||||||
|
AEMD_BASE_URL: str = Field(
|
||||||
|
default='http://gist-sdw.ezdrav.ru:8708/EMDAService'
|
||||||
|
)
|
||||||
|
AEMD_TOKEN: str = Field(default='')
|
||||||
|
|
||||||
|
# TDN
|
||||||
|
TDN_BASE_URL: str = Field(default='https://tdn.tatar.ru/api')
|
||||||
|
TDN_LOGIN: str = Field(default='')
|
||||||
|
TDN_PASSWORD: str = Field(default='')
|
||||||
|
|
||||||
|
# TMK
|
||||||
|
TMK_BASE_URL: str = Field(default='https://tmk-api.tatar.ru/api')
|
||||||
|
TMK_LOGIN: str = Field(default='admin')
|
||||||
|
TMK_PASSWORD: str = Field(default='12345')
|
||||||
|
|
||||||
@model_validator(mode='after')
|
@model_validator(mode='after')
|
||||||
def celery_env(self):
|
def celery_env(self):
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ from logging import getLogger
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.responses import ORJSONResponse
|
from fastapi.responses import ORJSONResponse
|
||||||
|
|
||||||
# from database import lifespan
|
from database import lifespan
|
||||||
from middlewares import register_middlewares
|
from middlewares import register_middlewares
|
||||||
|
|
||||||
from .config import settings
|
from .config import settings
|
||||||
@ -19,7 +19,7 @@ app = FastAPI(
|
|||||||
version=str(settings.VERSION),
|
version=str(settings.VERSION),
|
||||||
openapi_url=None,
|
openapi_url=None,
|
||||||
default_response_class=ORJSONResponse,
|
default_response_class=ORJSONResponse,
|
||||||
# lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
docs_url=None,
|
docs_url=None,
|
||||||
redoc_url=None,
|
redoc_url=None,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -22,7 +22,7 @@ def get_openapi_schema(router: APIRouter):
|
|||||||
'description': 'Development environment',
|
'description': 'Development environment',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'url': 'https://med-assistant-api.tatar.ru/',
|
'url': 'https://med-assistant.tatar.ru/api/',
|
||||||
'description': 'Production environment',
|
'description': 'Production environment',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException
|
||||||
|
|
||||||
from apps.esia.v1.router import router as esia_router
|
from apps.esia.v1.router import router as esia_router
|
||||||
|
from apps.remd.v1.router import router as remd_router
|
||||||
|
from apps.tmk.v1.router import router as tmk_router
|
||||||
from apps.users.v1.router import router as users_router
|
from apps.users.v1.router import router as users_router
|
||||||
|
from apps.vitacore.v1.router import router as vitacore_router
|
||||||
|
|
||||||
from . import get_openapi_schema, get_swagger_html
|
from . import get_openapi_schema, get_swagger_html
|
||||||
|
|
||||||
@ -9,6 +12,9 @@ router = APIRouter(prefix='/v1')
|
|||||||
|
|
||||||
router.include_router(esia_router)
|
router.include_router(esia_router)
|
||||||
router.include_router(users_router)
|
router.include_router(users_router)
|
||||||
|
router.include_router(remd_router)
|
||||||
|
router.include_router(vitacore_router)
|
||||||
|
router.include_router(tmk_router)
|
||||||
|
|
||||||
openapi_schema = get_openapi_schema(router)
|
openapi_schema = get_openapi_schema(router)
|
||||||
swagger_ui_html = get_swagger_html(router)
|
swagger_ui_html = get_swagger_html(router)
|
||||||
|
|||||||
@ -4,6 +4,7 @@ from alembic import context
|
|||||||
from sqlalchemy import engine_from_config, pool
|
from sqlalchemy import engine_from_config, pool
|
||||||
from sqlmodel import SQLModel
|
from sqlmodel import SQLModel
|
||||||
|
|
||||||
|
from apps.users.models import * # noqa: F403
|
||||||
from core.log import config as log_config
|
from core.log import config as log_config
|
||||||
from database import db_manager
|
from database import db_manager
|
||||||
|
|
||||||
|
|||||||
@ -1,28 +1,32 @@
|
|||||||
"""${message}
|
"""
|
||||||
|
${message}
|
||||||
|
|
||||||
Revision ID: ${up_revision}
|
Revision ID: ${up_revision}
|
||||||
Revises: ${down_revision | comma,n}
|
Revises: ${down_revision | comma,n}
|
||||||
Create Date: ${create_date}
|
Create Date: ${create_date}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from typing import Sequence, Union
|
from collections.abc import Sequence
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
${imports if imports else ""}
|
import sqlmodel.sql.sqltypes
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision: str = ${repr(up_revision)}
|
revision: str = ${repr(up_revision)}
|
||||||
down_revision: Union[str, None] = ${repr(down_revision)}
|
down_revision: str | None = ${repr(down_revision)}
|
||||||
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
branch_labels: str | Sequence[str] | None = ${repr(branch_labels)}
|
||||||
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
depends_on: str | Sequence[str] | None = ${repr(depends_on)}
|
||||||
|
|
||||||
|
|
||||||
|
if not sqlmodel.sql:
|
||||||
|
msg = 'Something went wrong'
|
||||||
|
raise Exception(msg)
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
"""Upgrade schema."""
|
|
||||||
${upgrades if upgrades else "pass"}
|
${upgrades if upgrades else "pass"}
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
"""Downgrade schema."""
|
|
||||||
${downgrades if downgrades else "pass"}
|
${downgrades if downgrades else "pass"}
|
||||||
|
|||||||
46
src/migrations/versions/2025.10.09_17-42-25_a74bcd05c7b8.py
Normal file
46
src/migrations/versions/2025.10.09_17-42-25_a74bcd05c7b8.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
"""
|
||||||
|
empty message
|
||||||
|
|
||||||
|
Revision ID: a74bcd05c7b8
|
||||||
|
Revises:
|
||||||
|
Create Date: 2025-10-09 17:42:25.347412
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
import sqlmodel.sql.sqltypes
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'a74bcd05c7b8'
|
||||||
|
down_revision: str | None = None
|
||||||
|
branch_labels: str | Sequence[str] | None = None
|
||||||
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
if not sqlmodel.sql:
|
||||||
|
msg = 'Something went wrong'
|
||||||
|
raise Exception(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table(
|
||||||
|
'users',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column(
|
||||||
|
'vita_id', sqlmodel.sql.sqltypes.AutoString(), nullable=False
|
||||||
|
),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('id'),
|
||||||
|
sa.UniqueConstraint('vita_id'),
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('users')
|
||||||
|
# ### end Alembic commands ###
|
||||||
0
src/migrations/versions/__init__.py
Normal file
0
src/migrations/versions/__init__.py
Normal file
5
src/shared/functions.py
Normal file
5
src/shared/functions.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def clean_params(data: dict[Any, Any]):
|
||||||
|
return {k: v for k, v in data.items() if v is not None}
|
||||||
@ -1,5 +1,45 @@
|
|||||||
from redis import Redis
|
from collections.abc import Callable, Coroutine
|
||||||
|
from functools import wraps
|
||||||
|
from typing import Any, TypeVar
|
||||||
|
|
||||||
|
from fastapi.responses import ORJSONResponse
|
||||||
|
from orjson import dumps, loads
|
||||||
|
from redis.asyncio import Redis
|
||||||
|
|
||||||
|
from apps.users.models import User
|
||||||
from core.config import settings
|
from core.config import settings
|
||||||
|
|
||||||
client = Redis.from_url(settings.REDIS_URL)
|
client = Redis.from_url(settings.REDIS_URL)
|
||||||
|
T = TypeVar('T')
|
||||||
|
|
||||||
|
|
||||||
|
def cache_response(ttl: int = 60, namespace: str = 'main'):
|
||||||
|
def decorator(
|
||||||
|
func: Callable[..., Coroutine[Any, Any, T]],
|
||||||
|
) -> Callable[..., Coroutine[Any, Any, T | ORJSONResponse]]:
|
||||||
|
@wraps(func)
|
||||||
|
async def wrapper(*args: Any, **kwargs: Any):
|
||||||
|
cache_key = f'endpoints:{namespace}_{func.__name__}'
|
||||||
|
user = kwargs.get('user')
|
||||||
|
user_id = kwargs.get('user_id')
|
||||||
|
|
||||||
|
if user_id:
|
||||||
|
cache_key += f':{user_id}'
|
||||||
|
|
||||||
|
if isinstance(user, User):
|
||||||
|
cache_key += f':{user.id}'
|
||||||
|
|
||||||
|
cached_value = await client.get(cache_key)
|
||||||
|
if cached_value:
|
||||||
|
return ORJSONResponse(
|
||||||
|
content=loads(cached_value), headers={'X-Cache': 'hit'}
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await func(*args, **kwargs)
|
||||||
|
await client.set(cache_key, dumps(response), ex=ttl)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|||||||
Reference in New Issue
Block a user