This commit is contained in:
@ -24,6 +24,7 @@ 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",
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from sqlmodel import Field, SQLModel
|
from sqlmodel import Field, SQLModel
|
||||||
|
|
||||||
|
|
||||||
@ -6,3 +8,13 @@ class User(SQLModel, table=True):
|
|||||||
|
|
||||||
id: int = Field(default=None, primary_key=True)
|
id: int = Field(default=None, primary_key=True)
|
||||||
vita_id: str = Field(unique=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
|
||||||
|
|||||||
@ -1,15 +1,17 @@
|
|||||||
from datetime import datetime
|
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
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Body, Depends, status
|
from fastapi import APIRouter, Body, Depends, status
|
||||||
|
|
||||||
|
from apps.tdn.auth import token
|
||||||
from apps.users.auth import login
|
from apps.users.auth import login
|
||||||
from apps.users.models import User
|
from apps.users.models import User
|
||||||
from clients import clients as c
|
from clients import clients as c
|
||||||
from clients.tmk import schema as ts
|
from clients.tmk import schema as ts
|
||||||
from clients.vitacore import schema as vs
|
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
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
@ -164,6 +166,18 @@ async def queue(_: Annotated[User, Depends(login)]):
|
|||||||
return await c.tmk_api.getQueue()
|
return await c.tmk_api.getQueue()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('/aemd')
|
||||||
|
async def aemd(user: Annotated[User, Depends(login)]):
|
||||||
|
profile = await c.vitacore_api.getProfile(user.vita_id)
|
||||||
|
snils = profile.SNILS.replace('-', '').replace(' ', '')
|
||||||
|
docs = await c.aemd_api.searchRegistryItem(patient_snils=snils)
|
||||||
|
doc = docs['items'][0]
|
||||||
|
|
||||||
|
return await c.aemd_api.demandContent(
|
||||||
|
messageId='test123', emdrId=doc['emdrId']
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# @router.post('/measurement', status_code=status.HTTP_202_ACCEPTED)
|
# @router.post('/measurement', status_code=status.HTTP_202_ACCEPTED)
|
||||||
# async def measurement(tdn_access_token: Annotated[str, Depends(token)]):
|
# async def measurement(tdn_access_token: Annotated[str, Depends(token)]):
|
||||||
# patientId = '6debe050-b57e-442b-9b0e-8d304ca382b0'
|
# patientId = '6debe050-b57e-442b-9b0e-8d304ca382b0'
|
||||||
@ -182,18 +196,160 @@ async def queue(_: Annotated[User, Depends(login)]):
|
|||||||
|
|
||||||
# return observation_measurements
|
# return observation_measurements
|
||||||
|
|
||||||
|
# created = created_at.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
# data = {
|
||||||
|
# 'ad': ad,
|
||||||
|
# 'sd': sd,
|
||||||
|
# 'pulse': pulse,
|
||||||
|
# 'created_at': created,
|
||||||
|
# 'comment': comment,
|
||||||
|
# 'status': status,
|
||||||
|
# }
|
||||||
|
# cache_key = f'tdn:measurement:{user.id}:{created}'
|
||||||
|
# cache.set(cache_key, dumps(data))
|
||||||
|
|
||||||
|
|
||||||
@router.post('/measurement', status_code=status.HTTP_202_ACCEPTED)
|
@router.post('/measurement', status_code=status.HTTP_202_ACCEPTED)
|
||||||
async def measurement(
|
async def measurement(
|
||||||
|
tdn_access_token: Annotated[str, Depends(token)],
|
||||||
user: Annotated[User, Depends(login)],
|
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[str, Body()],
|
||||||
):
|
):
|
||||||
created = created_at.strftime('%Y-%m-%d %H:%M:%S')
|
observations = await c.tdn_api.observations_search(
|
||||||
|
tdn_access_token, user.vita_id
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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':
|
||||||
|
health_measurement = metric.uid
|
||||||
|
|
||||||
|
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
|
||||||
|
):
|
||||||
|
ad_obsrvMeasurementUid = None
|
||||||
|
sad_measurement = None
|
||||||
|
dad_measurement = None
|
||||||
|
pulse_measurement = None
|
||||||
|
ad_observationUid = None
|
||||||
|
health_obsrvMeasurementUid = None
|
||||||
|
health_observationUid = None
|
||||||
|
health_measurement = 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=str(sd)
|
||||||
|
)
|
||||||
|
|
||||||
|
# DAD
|
||||||
|
await c.tdn_api.create_series_values(
|
||||||
|
tdn_access_token, ad_series_uid, dad_measurement, nvalue=str(ad)
|
||||||
|
)
|
||||||
|
|
||||||
|
# PULSE
|
||||||
|
await c.tdn_api.create_series_values(
|
||||||
|
tdn_access_token, ad_series_uid, pulse_measurement, nvalue=str(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,
|
||||||
|
svalue=str(comment),
|
||||||
|
)
|
||||||
|
|
||||||
|
created = datetime.now(UTC).strftime('%Y-%m-%d %H:%M:%S')
|
||||||
data = {
|
data = {
|
||||||
'ad': ad,
|
'ad': ad,
|
||||||
'sd': sd,
|
'sd': sd,
|
||||||
@ -204,7 +360,6 @@ async def measurement(
|
|||||||
}
|
}
|
||||||
cache_key = f'tdn:measurement:{user.id}:{created}'
|
cache_key = f'tdn:measurement:{user.id}:{created}'
|
||||||
cache.set(cache_key, dumps(data))
|
cache.set(cache_key, dumps(data))
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
@router.get('/measurements')
|
@router.get('/measurements')
|
||||||
@ -213,8 +368,3 @@ async def measurements(
|
|||||||
):
|
):
|
||||||
data = [cache.get(key) for key in cache.keys(f'tdn:measurement:{user}:*')]
|
data = [cache.get(key) for key in cache.keys(f'tdn:measurement:{user}:*')]
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
@router.get('/aemd/test')
|
|
||||||
async def test_route():
|
|
||||||
return await c.aemd_api.searchRegistryItem('16247900267')
|
|
||||||
|
|||||||
146
src/clients/aemd/_schema.py
Normal file
146
src/clients/aemd/_schema.py
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from pydantic_xml import BaseXmlModel, attr, element, wrapped
|
||||||
|
|
||||||
|
# SOAP Envelope namespaces
|
||||||
|
NS_SOAP = 'http://www.w3.org/2003/05/soap-envelope'
|
||||||
|
NS_ADDR = 'http://www.w3.org/2005/08/addressing'
|
||||||
|
NS_WSSE = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd'
|
||||||
|
NS_WSU = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd'
|
||||||
|
NS_DS = 'http://www.w3.org/2000/09/xmldsig#'
|
||||||
|
NS_APP = 'http://egisz.rosminzdrav.ru/iehr/emdr/service/'
|
||||||
|
|
||||||
|
NSMAP = {
|
||||||
|
's': NS_SOAP,
|
||||||
|
'a': NS_ADDR,
|
||||||
|
'wsse': NS_WSSE,
|
||||||
|
'wsu': NS_WSU,
|
||||||
|
'ds': NS_DS,
|
||||||
|
'app': NS_APP,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Reference(BaseXmlModel, tag='Reference', ns='ds', nsmap=NSMAP):
|
||||||
|
uri: str = attr(name='URI')
|
||||||
|
digest_method: Literal['http://www.w3.org/2001/04/xmlenc#sha256'] = (
|
||||||
|
element(tag='DigestMethod', ns='ds', attr_name='Algorithm')
|
||||||
|
)
|
||||||
|
digest_value: str = element(tag='DigestValue', ns='ds')
|
||||||
|
|
||||||
|
|
||||||
|
class SignedInfo(BaseXmlModel, tag='SignedInfo', ns=NS_DS, nsmap=NSMAP):
|
||||||
|
canon_method: Literal['http://www.w3.org/2001/10/xml-exc-c14n#'] = element(
|
||||||
|
tag='CanonicalizationMethod', ns=NS_DS, attr_name='Algorithm'
|
||||||
|
)
|
||||||
|
sig_method: Literal[
|
||||||
|
'urn:ietf:params:xml:ns:cpxmlsec:algorithms:gostr34102012-gostr34112012-256'
|
||||||
|
] = element(tag='SignatureMethod', ns=NS_DS, attr_name='Algorithm')
|
||||||
|
references: list[Reference] = element(tag='Reference', ns=NS_DS)
|
||||||
|
|
||||||
|
|
||||||
|
class SecurityTokenReference(
|
||||||
|
BaseXmlModel,
|
||||||
|
tag='SecurityTokenReference',
|
||||||
|
ns=NS_WSSE,
|
||||||
|
nsmap={'wsse': NS_WSSE},
|
||||||
|
):
|
||||||
|
ref_uri: str = wrapped('Reference', attr(name='URI'))
|
||||||
|
ref_type: Literal[
|
||||||
|
'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3'
|
||||||
|
] = wrapped('Reference', attr(name='ValueType'))
|
||||||
|
|
||||||
|
|
||||||
|
class KeyInfo(BaseXmlModel, tag='KeyInfo', ns=NS_DS, nsmap=NSMAP):
|
||||||
|
token_ref: SecurityTokenReference = element(
|
||||||
|
tag='SecurityTokenReference', ns=NS_WSSE
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Signature(BaseXmlModel, tag='Signature', ns=NS_DS, nsmap=NSMAP):
|
||||||
|
signed_info: SignedInfo = element()
|
||||||
|
signature_value: str = element(tag='SignatureValue')
|
||||||
|
key_info: KeyInfo = element()
|
||||||
|
|
||||||
|
|
||||||
|
class BinarySecurityToken(
|
||||||
|
BaseXmlModel,
|
||||||
|
tag='BinarySecurityToken',
|
||||||
|
ns=NS_WSSE,
|
||||||
|
nsmap=NSMAP,
|
||||||
|
):
|
||||||
|
id: str = attr(name='Id', ns=NS_WSU)
|
||||||
|
value_type: Literal[
|
||||||
|
'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3'
|
||||||
|
] = attr(name='ValueType')
|
||||||
|
encoding_type: Literal[
|
||||||
|
'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary'
|
||||||
|
] = attr(name='EncodingType')
|
||||||
|
value: str
|
||||||
|
|
||||||
|
|
||||||
|
class Security(BaseXmlModel, tag='Security', ns=NS_WSSE, nsmap=NSMAP):
|
||||||
|
token: BinarySecurityToken = element()
|
||||||
|
signature: Signature = element()
|
||||||
|
|
||||||
|
|
||||||
|
class Action(BaseXmlModel, tag='Action', ns=NS_ADDR, nsmap=NSMAP):
|
||||||
|
id: str = attr(name='Id', ns=NS_WSU)
|
||||||
|
|
||||||
|
|
||||||
|
class MessageID(BaseXmlModel, tag='MessageID', ns=NS_ADDR, nsmap=NSMAP):
|
||||||
|
id: str = attr(name='Id', ns=NS_WSU)
|
||||||
|
value: str
|
||||||
|
|
||||||
|
|
||||||
|
class Header(BaseXmlModel, tag='Header', ns=NS_SOAP, nsmap=NSMAP):
|
||||||
|
action: Action | None = element(tag='Action', ns=NS_ADDR)
|
||||||
|
message_id: MessageID | None = element(tag='MessageID', ns=NS_ADDR)
|
||||||
|
security: Security = element(tag='Security', ns=NS_WSSE)
|
||||||
|
|
||||||
|
|
||||||
|
class Page(BaseXmlModel, tag='page', ns=NS_APP):
|
||||||
|
items_per_page: int = element(tag='itemsPerPage')
|
||||||
|
has_next: bool = element(tag='hasNext')
|
||||||
|
|
||||||
|
|
||||||
|
class Item(BaseXmlModel, tag='item', ns=NS_APP):
|
||||||
|
emdr_id: str = element(tag='emdrId')
|
||||||
|
local_uid: str = element(tag='localUid')
|
||||||
|
registration_date: datetime = element(tag='registrationDate')
|
||||||
|
registration_date_time: datetime = element(tag='registrationDateTime')
|
||||||
|
store_till_date: str = element(tag='storeTillDate')
|
||||||
|
doc_kind: str = element(tag='DocKind')
|
||||||
|
is_semd: bool = element(tag='IsSemd')
|
||||||
|
|
||||||
|
|
||||||
|
class Matches(BaseXmlModel, tag='matches', ns=NS_APP):
|
||||||
|
items: list[Item] = element(tag='item')
|
||||||
|
page: Page = element()
|
||||||
|
|
||||||
|
|
||||||
|
class SearchRegistryItemResponse(
|
||||||
|
BaseXmlModel,
|
||||||
|
tag='searchRegistryItemResponse',
|
||||||
|
ns=NS_APP,
|
||||||
|
nsmap={'app': NS_APP},
|
||||||
|
):
|
||||||
|
status: Literal['success'] = element()
|
||||||
|
matches: Matches = element()
|
||||||
|
|
||||||
|
|
||||||
|
class Body(BaseXmlModel, tag='Body', ns=NS_SOAP, nsmap=NSMAP):
|
||||||
|
id: str = attr(name='Id', ns=NS_WSU)
|
||||||
|
response: SearchRegistryItemResponse = element(
|
||||||
|
tag='searchRegistryItemResponse', ns=NS_APP
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Envelope(
|
||||||
|
BaseXmlModel,
|
||||||
|
tag='Envelope',
|
||||||
|
ns=NS_SOAP,
|
||||||
|
nsmap=NSMAP,
|
||||||
|
):
|
||||||
|
header: Header = element(tag='Header', ns=NS_SOAP)
|
||||||
|
body: Body = element(tag='Body', ns=NS_SOAP)
|
||||||
@ -1,8 +1,12 @@
|
|||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
|
||||||
|
from fastapi import status
|
||||||
from httpx import AsyncClient
|
from httpx import AsyncClient
|
||||||
|
|
||||||
from core.config import settings
|
from core.config import settings
|
||||||
|
from shared import exceptions as e
|
||||||
|
|
||||||
|
from . import schema as s
|
||||||
|
|
||||||
|
|
||||||
class AEMD_API(AsyncClient):
|
class AEMD_API(AsyncClient):
|
||||||
@ -43,4 +47,29 @@ class AEMD_API(AsyncClient):
|
|||||||
)
|
)
|
||||||
req = await self.post('/', content=envelope)
|
req = await self.post('/', content=envelope)
|
||||||
|
|
||||||
return req.text
|
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()
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
from datetime import UTC, datetime
|
||||||
from json import dumps
|
from json import dumps
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from urllib.parse import quote, urlencode
|
from urllib.parse import quote, urlencode
|
||||||
@ -65,18 +66,6 @@ class TDN_API(AsyncClient):
|
|||||||
async def observations_measurement_search(
|
async def observations_measurement_search(
|
||||||
self, access_token: str, observationUid: str
|
self, access_token: str, observationUid: str
|
||||||
):
|
):
|
||||||
# data = urlencode(
|
|
||||||
# dumps(
|
|
||||||
# {
|
|
||||||
# 'where': {'observationUid': observationUid},
|
|
||||||
# 'relations': [
|
|
||||||
# 'measurement',
|
|
||||||
# 'obsrvMtMetrics',
|
|
||||||
# 'obsrvMtMetrics.metric',
|
|
||||||
# ],
|
|
||||||
# }
|
|
||||||
# )
|
|
||||||
# )
|
|
||||||
encoded_query = urlencode(
|
encoded_query = urlencode(
|
||||||
{
|
{
|
||||||
'query': dumps(
|
'query': dumps(
|
||||||
@ -105,3 +94,69 @@ class TDN_API(AsyncClient):
|
|||||||
case _:
|
case _:
|
||||||
self.logger.error(res.json())
|
self.logger.error(res.json())
|
||||||
raise e.UnknownException
|
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: str | None = None,
|
||||||
|
fvalue: str | None = None,
|
||||||
|
svalue: str | None = None,
|
||||||
|
):
|
||||||
|
data = {
|
||||||
|
'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
|
||||||
|
|||||||
@ -68,7 +68,7 @@ class ObservationMeasurementModel(BaseModel):
|
|||||||
timeFrequency: int
|
timeFrequency: int
|
||||||
timePeriod: int
|
timePeriod: int
|
||||||
timePeriodMeasureUid: str
|
timePeriodMeasureUid: str
|
||||||
timeOfDay: list[str]
|
timeOfDay: list[str] | None
|
||||||
comment: str | None
|
comment: str | None
|
||||||
mobileId: str | None
|
mobileId: str | None
|
||||||
measurement: MeasurementModel
|
measurement: MeasurementModel
|
||||||
@ -78,3 +78,33 @@ class ObservationMeasurementModel(BaseModel):
|
|||||||
class ObservationMeasurementsModel(BaseModel):
|
class ObservationMeasurementsModel(BaseModel):
|
||||||
items: list[ObservationMeasurementModel]
|
items: list[ObservationMeasurementModel]
|
||||||
total: int
|
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: str | None
|
||||||
|
fvalue: str | None
|
||||||
|
svalue: str | None
|
||||||
|
filepath: str | None
|
||||||
|
mobileId: str | None
|
||||||
|
tisId: str | None
|
||||||
|
|||||||
@ -200,6 +200,7 @@ class VITACORE_API(AsyncClient):
|
|||||||
raise e.UnknownException
|
raise e.UnknownException
|
||||||
|
|
||||||
async def getHospExaminations(self, patId: str, examId: str):
|
async def getHospExaminations(self, patId: str, examId: str):
|
||||||
|
patId = 'b66a85f1-4aaa-4db8-942a-2de44341824e'
|
||||||
token = await self.get_token()
|
token = await self.get_token()
|
||||||
req = await self.get(
|
req = await self.get(
|
||||||
'/getHospExaminations',
|
'/getHospExaminations',
|
||||||
@ -210,6 +211,18 @@ class VITACORE_API(AsyncClient):
|
|||||||
match req.status_code:
|
match req.status_code:
|
||||||
case st.HTTP_200_OK:
|
case st.HTTP_200_OK:
|
||||||
return s.HospExaminationsModel.model_validate(req.json())
|
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='fakeName',
|
||||||
|
Examinations=[],
|
||||||
|
)
|
||||||
|
|
||||||
case _:
|
case _:
|
||||||
self.logger.error(req.json())
|
self.logger.error(req.json())
|
||||||
raise e.UnknownException
|
raise e.UnknownException
|
||||||
@ -351,6 +364,13 @@ class VITACORE_API(AsyncClient):
|
|||||||
match req.status_code:
|
match req.status_code:
|
||||||
case st.HTTP_200_OK:
|
case st.HTTP_200_OK:
|
||||||
return s.ELNsModel.model_validate(req.json())
|
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 _:
|
case _:
|
||||||
self.logger.error(req.json())
|
self.logger.error(req.json())
|
||||||
raise e.UnknownException
|
raise e.UnknownException
|
||||||
@ -369,3 +389,28 @@ class VITACORE_API(AsyncClient):
|
|||||||
case _:
|
case _:
|
||||||
self.logger.error(req.json())
|
self.logger.error(req.json())
|
||||||
raise e.UnknownException
|
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
|
||||||
|
|||||||
@ -672,3 +672,7 @@ class PatientFLGModel(BaseModel):
|
|||||||
title='Контингент (флюорография)',
|
title='Контингент (флюорография)',
|
||||||
examples=['Неорганизованное население'],
|
examples=['Неорганизованное население'],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DiagResultFileModel(BaseModel):
|
||||||
|
content: str
|
||||||
|
|||||||
Reference in New Issue
Block a user