This commit is contained in:
2025-10-16 16:43:18 +03:00
parent cb3b138241
commit a4239a0c52
21 changed files with 524 additions and 140 deletions

View File

@ -20,3 +20,6 @@ uv.lock
# Container # Container
container container
# Postgres
postgres

3
.gitignore vendored
View File

@ -20,3 +20,6 @@ uv.lock
# Container # Container
container container
# Postgres
postgres

View File

@ -8,13 +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:
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:
@ -28,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:

View File

@ -50,9 +50,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"

View File

@ -1,10 +1,10 @@
SCOPES = [ SCOPES = [
'openid', 'openid',
'fullname', 'fullname',
'email', # 'email',
'birthdate', # 'birthdate',
'gender', # 'gender',
'snils', # 'snils',
'id_doc', # 'id_doc',
'mobile', # 'mobile',
] ]

View File

@ -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,34 @@ 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
)
vita_user = await c.vitacore_api.findBySnils(esia_user.snils)
if len(vita_user.patients) == 0:
raise e.BadRequestException(detail='Patient not found')
vita_user = vita_user.patients[0]
existing_user_stmt = (
select(User).where(User.vita_id == vita_user.id).limit(1)
)
existing_user = (
await session.execute(existing_user_stmt)
).scalar_one_or_none()
if existing_user is None:
user = User(vita_id=vita_user.id)
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) cache.set(access_token, f'user:{user.id}')
return s.Token(access_token=access_token) return s.Token(access_token=access_token)

View File

@ -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,20 @@ 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 = cache.get(credentials.credentials)
if is_exist is None: if user is None:
raise e.UnauthorizedException raise e.UnauthorizedException
return is_exist.decode() _, user_id = user.decode().split(':')
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

8
src/apps/users/models.py Normal file
View File

@ -0,0 +1,8 @@
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)

View File

@ -6,6 +6,7 @@ from typing import Annotated
from fastapi import APIRouter, Body, Depends, status from fastapi import APIRouter, Body, Depends, status
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 import clients as c
from clients.vitacore import schema as s from clients.vitacore import schema as s
from shared.redis import client as cache from shared.redis import client as cache
@ -20,16 +21,16 @@ router = APIRouter(
@router.get('/getProfile', response_model=s.ProfileModel) @router.get('/getProfile', response_model=s.ProfileModel)
async def get_profile(): async def get_profile(user: Annotated[User, Depends(login)]):
""" """
Get profile of user by id. Get profile of user.
""" """
return await c.vitacore_api.getProfile( return await c.vitacore_api.getProfile(user.vita_id)
'b62e9f22-a871-4c52-96d6-559c707a716d'
)
@router.get('/getDepartments', response_model=list[s.DepartmentModel]) @router.get('/getDepartments',
# response_model=list[s.DepartmentModel]
)
async def get_departments(): async def get_departments():
""" """
Get list of departments. Get list of departments.
@ -38,7 +39,9 @@ async def get_departments():
@router.get('/getWorkers', response_model=s.WorkersModel) @router.get('/getWorkers', response_model=s.WorkersModel)
async def get_workers(departmentId: str): async def get_workers(
user: Annotated[User, Depends(login)], departmentId: str
):
""" """
Get list of workers by department id. Get list of workers by department id.
""" """
@ -46,7 +49,7 @@ async def get_workers(departmentId: str):
@router.get('/getSpecs', response_model=s.SpecsV021Model) @router.get('/getSpecs', response_model=s.SpecsV021Model)
async def get_specs(): async def get_specs(user: Annotated[User, Depends(login)]):
""" """
Get list of specialties. Get list of specialties.
""" """
@ -54,27 +57,23 @@ async def get_specs():
@router.get('/getEntries', response_model=s.EntriesModel) @router.get('/getEntries', response_model=s.EntriesModel)
async def get_entries(): async def get_entries(user: Annotated[User, Depends(login)]):
""" """
Get list of entries for user by id. Get list of entries for user by id.
""" """
return await c.vitacore_api.getEntries( return await c.vitacore_api.getEntries(user.vita_id)
'6c7978f0-c573-4ccf-8c6e-f0cd9aceb1e1'
)
@router.get('/getVaccsReport') @router.get('/getVaccsReport')
async def get_vaccs_report(): async def get_vaccs_report(user: Annotated[User, Depends(login)]):
""" """
Get report of vaccinations for user by id. Get report of vaccinations for user by id.
""" """
return await c.vitacore_api.getVaccsReport( return await c.vitacore_api.getVaccsReport(user.vita_id)
'6fe66cae-409a-4f56-8ae9-d55d3c38569b'
)
@router.get('/getMedExamDict') @router.get('/getMedExamDict')
async def get_med_exam_dict(): async def get_med_exam_dict(user: Annotated[User, Depends(login)]):
""" """
Get medical examination dictionary. Get medical examination dictionary.
""" """
@ -82,92 +81,80 @@ async def get_med_exam_dict():
@router.get('/getRoutesList') @router.get('/getRoutesList')
async def get_routes_list(): async def get_routes_list(user: Annotated[User, Depends(login)]):
""" """
Get list of routes. Get list of routes.
""" """
return await c.vitacore_api.getRoutesList( return await c.vitacore_api.getRoutesList(user.vita_id)
'4e6de5f7-4dc9-451b-bf0d-7a64a9b8c279'
)
@router.get('/getHospExaminations') @router.get('/getHospExaminations')
async def get_hosp_examinations(): async def get_hosp_examinations(
user: Annotated[User, Depends(login)], examId: str
):
""" """
Get list of hospital examinations. Get list of hospital examinations.
""" """
return await c.vitacore_api.getHospExaminations( return await c.vitacore_api.getHospExaminations(
'7bbdac30-9a33-4f13-9458-2c229c0c20f5', user.vita_id,
'f22be2c9-8e68-42d6-851e-fbf4a5e8f657', examId,
) )
@router.get('/getCurrHosp') @router.get('/getCurrHosp')
async def get_curr_hosp(): async def get_curr_hosp(user: Annotated[User, Depends(login)]):
""" """
Get current hospitalization. Get current hospitalization.
""" """
return await c.vitacore_api.getCurrHosp( return await c.vitacore_api.getCurrHosp(user.vita_id)
'b708e782-4f83-4f3b-8639-512c0c9637bf'
)
@router.get('/getHosps') @router.get('/getHosps')
async def get_hosps(): async def get_hosps(user: Annotated[User, Depends(login)]):
""" """
Get list of hospitals. Get list of hospitals.
""" """
return await c.vitacore_api.getHosps( return await c.vitacore_api.getHosps(user.vita_id)
'b708e782-4f83-4f3b-8639-512c0c9637bf'
)
@router.get('/getHospRecommendations') @router.get('/getHospRecommendations')
async def get_hosp_recommendations(): async def get_hosp_recommendations(user: Annotated[User, Depends(login)]):
""" """
Get list of recommended hospitals. Get list of recommended hospitals.
""" """
return await c.vitacore_api.getHospRecommendations( return await c.vitacore_api.getHospRecommendations(user.vita_id)
'b708e782-4f83-4f3b-8639-512c0c9637bf'
)
@router.get('/getHospRoutes') @router.get('/getHospRoutes')
async def get_hosp_routes(): async def get_hosp_routes(user: Annotated[User, Depends(login)]):
""" """
Get list of recommended hospitals. Get list of recommended hospitals.
""" """
return await c.vitacore_api.getHospRoutes( return await c.vitacore_api.getHospRoutes(user.vita_id)
'3092e1c5-e08b-4654-a027-82be90fe8a49'
)
@router.get('/getDiagnosticResults') @router.get('/getDiagnosticResults')
async def get_diagnostic_results(): async def get_diagnostic_results(user: Annotated[User, Depends(login)]):
""" """
Get list of diagnostic results. Get list of diagnostic results.
""" """
return await c.vitacore_api.getDiagnosticResults( return await c.vitacore_api.getDiagnosticResults(user.vita_id)
'4867cc79-9805-4ae2-98d3-2f822848635e'
)
@router.get('/getELNs') @router.get('/getELNs')
async def get_eln(): async def get_eln(user: Annotated[User, Depends(login)]):
""" """
Get list of ELNs. Get list of ELNs.
""" """
return await c.vitacore_api.getELNs('d4493f1c-fcbb-4242-99e6-32328bed53b9') return await c.vitacore_api.getELNs(user.vita_id)
@router.get('/getPatFLG') @router.get('/getPatFLG')
async def get_pat_flg(): async def get_pat_flg(user: Annotated[User, Depends(login)]):
""" """
Get list of ELNs. Get list of ELNs.
""" """
return await c.vitacore_api.getPatFLG( return await c.vitacore_api.getPatFLG(user.vita_id)
'0bf2e271-e565-42a8-924e-0017bcdedecd'
)
# @router.post('/measurement', status_code=status.HTTP_202_ACCEPTED) # @router.post('/measurement', status_code=status.HTTP_202_ACCEPTED)
@ -191,7 +178,7 @@ async def get_pat_flg():
@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)], 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()],
@ -208,7 +195,7 @@ 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)) cache.set(cache_key, dumps(data))
return return
@ -221,28 +208,35 @@ async def measurements(
return data return data
@router.get('/test')
async def test_route():
return await c.aemd_api.searchRegistryItem('16247900267')
@router.get('/queue') @router.get('/queue')
async def queue(user: Annotated[bool, Depends(login)]): async def queue(user: Annotated[bool, Depends(login)]):
return { return [
'id': 60, {
'guid': '92b3343d-1cb2-47b2-8497-a37e38b6ba24', 'id': 60,
'tmk_date': None, 'guid': '92b3343d-1cb2-47b2-8497-a37e38b6ba24',
'created_at': '2025-04-02 15:21:19.890343', 'tmk_date': None,
'code_mo': '166502', 'created_at': '2025-04-02 15:21:19.890343',
'mo_name': 'ГАУЗ "ГКБ№7 ИМ. М.Н.САДЫКОВА"', 'code_mo': '166502',
'doctor_spec': '109', 'mo_name': 'ГАУЗ "ГКБ№7 ИМ. М.Н.САДЫКОВА"',
'doctor_snils': None, 'doctor_spec': '109',
'patient_name': 'Иванов Петр Федорович', 'doctor_snils': None,
'patient_birthday': '1997-03-01', 'patient_name': 'Иванов Петр Федорович',
'patient_snils': '099-678-666 12', 'patient_birthday': '1997-03-01',
'patient_policy': None, 'patient_snils': '099-678-666 12',
'patient_phone': '+79123456789', 'patient_policy': None,
'patient_email': None, 'patient_phone': '+79123456789',
'tmk_status': 1, 'patient_email': None,
'tmk_status_name': 'Создана', 'tmk_status': 1,
'tmk_cancel_reason': None, 'tmk_status_name': 'Создана',
'tmk_cancel_reason_name': None, 'tmk_cancel_reason': None,
'vks_doctor_link': None, 'tmk_cancel_reason_name': None,
'vks_patient_link': None, 'vks_doctor_link': None,
'doctor_spec_name': 'врач-терапевт', 'vks_patient_link': None,
} 'doctor_spec_name': 'врач-терапевт',
}
]

View File

@ -1,3 +1,4 @@
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 .tdn.api import TDN_API
from .vitacore.api import VITACORE_API from .vitacore.api import VITACORE_API
@ -7,6 +8,7 @@ class ClientsObject:
_esia_api = None _esia_api = None
_vitacore_api = None _vitacore_api = None
_tdn_api = None _tdn_api = None
_aemd_api = None
@property @property
def esia_api(self): def esia_api(self):
@ -29,5 +31,12 @@ class ClientsObject:
return self._tdn_api return self._tdn_api
@property
def aemd_api(self):
if not self._aemd_api:
self._aemd_api = AEMD_API()
return self._aemd_api
clients = ClientsObject() clients = ClientsObject()

View File

46
src/clients/aemd/api.py Normal file
View File

@ -0,0 +1,46 @@
from logging import getLogger
from httpx import AsyncClient
from core.config import settings
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)
return req.text

View File

@ -33,7 +33,7 @@ class IDTokenModel(BaseModel):
exp: int exp: int
iat: int iat: int
iss: str iss: str
acr: IDTokenACRModel # acr: IDTokenACRModel
urn_esia_amd: str = Field(alias='urn:esia:amd') urn_esia_amd: str = Field(alias='urn:esia:amd')
urn_esia_sid: str = Field(alias='urn:esia:sid') urn_esia_sid: str = Field(alias='urn:esia:sid')
urn_esia_sbj: IDTokenSBJModel = Field(alias='urn:esia:sbj') urn_esia_sbj: IDTokenSBJModel = Field(alias='urn:esia:sbj')
@ -44,10 +44,10 @@ class UserInfoModel(BaseModel):
firstName: str firstName: str
lastName: str lastName: str
middleName: str middleName: str
birthDate: str # birthDate: str
gender: str # gender: str
trusted: bool trusted: bool
citizenship: str # citizenship: str
snils: str snils: str
inn: int inn: int
updatedOn: int updatedOn: int

View File

@ -1,10 +1,12 @@
from datetime import UTC, datetime
from logging import getLogger from logging import getLogger
from fastapi import status as st from fastapi import status as st
from httpx import AsyncClient from httpx import AsyncClient, BasicAuth
from core.config import settings from core.config import settings
from shared import exceptions as e from shared import exceptions as e
from shared.redis import client as cache
from . import schema as s from . import schema as s
@ -14,11 +16,56 @@ class VITACORE_API(AsyncClient):
self.logger = getLogger(__name__) self.logger = getLogger(__name__)
super().__init__(base_url=settings.VITACORE_BASE_URL) super().__init__(base_url=settings.VITACORE_BASE_URL)
async def get_token(self):
token = cache.get('vitacore_token')
if token is None:
token = await self.login()
cache.set('vitacore_token', token, 10800)
else:
token = token.decode()
return token
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): async def findBySnils(self, snils: str):
return 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:
return s.PatientsModel.model_validate(req.json())
case _:
self.logger.error(req.json())
raise e.UnknownException
async def getProfile(self, patId: str): async def getProfile(self, patId: str):
req = await self.get('/getProfile', params={'patId': patId}) token = await self.get_token()
req = await self.get(
'/getProfile',
params={'patId': patId},
headers={'Authorization': f'Bearer {token}'},
)
match req.status_code: match req.status_code:
case st.HTTP_200_OK: case st.HTTP_200_OK:
@ -28,18 +75,24 @@ class VITACORE_API(AsyncClient):
raise e.UnknownException raise e.UnknownException
async def getDepartments(self): async def getDepartments(self):
req = await self.get('/getDepartments') token = await self.get_token()
req = await self.get(
'/getDepartments', headers={'Authorization': f'Bearer {token}'}
)
match req.status_code: match req.status_code:
case st.HTTP_200_OK: case st.HTTP_200_OK:
return s.OrganizationsModel.model_validate(req.json()) return s.OrganizationsModel.model_validate(req.json())
case _: case _:
self.logger.error(req.json()) self.logger.error(req.text)
raise e.UnknownException raise e.UnknownException
async def getWorkers(self, departmentId: str): async def getWorkers(self, departmentId: str):
token = await self.get_token()
req = await self.get( req = await self.get(
'/getWorkers', params={'departmentId': departmentId} '/getWorkers',
params={'departmentId': departmentId},
headers={'Authorization': f'Bearer {token}'},
) )
match req.status_code: match req.status_code:
@ -50,7 +103,10 @@ class VITACORE_API(AsyncClient):
raise e.UnknownException raise e.UnknownException
async def getSpecsV021(self): async def getSpecsV021(self):
req = await self.get('/getSpecsV021') token = await self.get_token()
req = await self.get(
'/getSpecsV021', headers={'Authorization': f'Bearer {token}'}
)
match req.status_code: match req.status_code:
case st.HTTP_200_OK: case st.HTTP_200_OK:
@ -60,7 +116,13 @@ class VITACORE_API(AsyncClient):
raise e.UnknownException raise e.UnknownException
async def getEntries(self, patId: str): async def getEntries(self, patId: str):
req = await self.get('/getEntries', params={'patId': patId}) token = await self.get_token()
patId = 'b66a85f1-4aaa-4db8-942a-2de44341824e'
req = await self.get(
'/getEntries',
params={'patId': patId},
headers={'Authorization': f'Bearer {token}'},
)
match req.status_code: match req.status_code:
case st.HTTP_200_OK: case st.HTTP_200_OK:
@ -76,17 +138,31 @@ class VITACORE_API(AsyncClient):
raise e.UnknownException raise e.UnknownException
async def getVaccsReport(self, patId: str): async def getVaccsReport(self, patId: str):
req = await self.get('/getVaccsReport', params={'patId': patId}) token = await self.get_token()
patId = 'b66a85f1-4aaa-4db8-942a-2de44341824e'
req = await self.get(
'/getVaccsReport',
params={'patId': patId},
headers={'Authorization': f'Bearer {token}'},
)
match req.status_code: match req.status_code:
case st.HTTP_200_OK: case st.HTTP_200_OK:
return s.VaccsReportModel.model_validate(req.json()) return s.VaccsReportModel.model_validate(req.json())
case st.HTTP_206_PARTIAL_CONTENT:
error = s.ErrorModel.model_validate(req.json())
if error.error == 'Не найдены записи по указанному patId':
return s.VaccsReportModel(content='')
case _: case _:
self.logger.error(req.json()) self.logger.error(req.json())
raise e.UnknownException raise e.UnknownException
async def getMedExamDict(self): async def getMedExamDict(self):
req = await self.get('/getMedExamDict') token = await self.get_token()
req = await self.get(
'/getMedExamDict', headers={'Authorization': f'Bearer {token}'}
)
match req.status_code: match req.status_code:
case st.HTTP_200_OK: case st.HTTP_200_OK:
@ -96,19 +172,39 @@ class VITACORE_API(AsyncClient):
raise e.UnknownException raise e.UnknownException
async def getRoutesList(self, patId: str): async def getRoutesList(self, patId: str):
req = await self.get('/getRoutesList', params={'patId': patId}) token = await self.get_token()
patId = 'b66a85f1-4aaa-4db8-942a-2de44341824e'
req = await self.get(
'/getRoutesList',
params={'patId': patId},
headers={'Authorization': f'Bearer {token}'},
)
match req.status_code: match req.status_code:
case st.HTTP_200_OK: case st.HTTP_200_OK:
return s.RoutesListModel.model_validate(req.json()) 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='fakeName',
Routes=[],
)
case _: case _:
self.logger.error(req.json()) self.logger.error(req.json())
raise e.UnknownException raise e.UnknownException
async def getHospExaminations(self, patId: str, examId: str): async def getHospExaminations(self, patId: str, examId: str):
token = await self.get_token()
req = await self.get( req = await self.get(
'/getHospExaminations', '/getHospExaminations',
params={'patId': patId, 'examId': examId}, params={'patId': patId, 'examId': examId},
headers={'Authorization': f'Bearer {token}'},
) )
match req.status_code: match req.status_code:
@ -119,49 +215,123 @@ class VITACORE_API(AsyncClient):
raise e.UnknownException raise e.UnknownException
async def getCurrHosp(self, patId: str): async def getCurrHosp(self, patId: str):
req = await self.get('/getCurrHosp', params={'patId': patId}) token = await self.get_token()
patId = 'b66a85f1-4aaa-4db8-942a-2de44341824e'
req = await self.get(
'/getCurrHosp',
params={'patId': patId},
headers={'Authorization': f'Bearer {token}'},
)
match req.status_code: match req.status_code:
case st.HTTP_200_OK: case st.HTTP_200_OK:
return s.CurHospitalizationsModel.model_validate(req.json()) return s.CurHospitalizationsModel.model_validate(req.json())
case st.HTTP_206_PARTIAL_CONTENT:
error = s.ErrorModel.model_validate(req.json())
if error.error == 'Пациент не госпитализирован!':
return s.CurHospitalizationsModel(
Hospitalizations=[],
)
case _: case _:
self.logger.error(req.json()) self.logger.error(req.json())
raise e.UnknownException raise e.UnknownException
async def getHosps(self, patId: str): async def getHosps(self, patId: str):
req = await self.get('/getHosps', params={'patId': patId}) token = await self.get_token()
patId = 'b66a85f1-4aaa-4db8-942a-2de44341824e'
req = await self.get(
'/getHosps',
params={'patId': patId},
headers={'Authorization': f'Bearer {token}'},
)
match req.status_code: match req.status_code:
case st.HTTP_200_OK: case st.HTTP_200_OK:
return s.HospitalizationsModel.model_validate(req.json()) 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 _: case _:
self.logger.error(req.json()) self.logger.error(req.json())
raise e.UnknownException raise e.UnknownException
async def getHospRecommendations(self, patId: str): async def getHospRecommendations(self, patId: str):
token = await self.get_token()
patId = 'b66a85f1-4aaa-4db8-942a-2de44341824e'
req = await self.get( req = await self.get(
'/getHospRecommendations', params={'patId': patId} '/getHospRecommendations',
params={'patId': patId},
headers={'Authorization': f'Bearer {token}'},
) )
match req.status_code: match req.status_code:
case st.HTTP_200_OK: case st.HTTP_200_OK:
return s.HospRecommendationsModel.model_validate(req.json()) 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 _: case _:
self.logger.error(req.json()) self.logger.error(req.json())
raise e.UnknownException raise e.UnknownException
async def getHospRoutes(self, patId: str): async def getHospRoutes(self, patId: str):
req = await self.get('/getHospRoutes', params={'patId': patId}) token = await self.get_token()
patId = 'b66a85f1-4aaa-4db8-942a-2de44341824e'
req = await self.get(
'/getHospRoutes',
params={'patId': patId},
headers={'Authorization': f'Bearer {token}'},
)
match req.status_code: match req.status_code:
case st.HTTP_200_OK: case st.HTTP_200_OK:
return s.HospRoutesModel.model_validate(req.json()) 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 _: case _:
self.logger.error(req.json()) self.logger.error(req.json())
raise e.UnknownException raise e.UnknownException
async def getDiagnosticResults(self, patId: str): async def getDiagnosticResults(self, patId: str):
req = await self.get('/getDiagnosticResults', params={'patId': patId}) token = await self.get_token()
req = await self.get(
'/getDiagnosticResults',
params={'patId': patId},
headers={'Authorization': f'Bearer {token}'},
)
match req.status_code: match req.status_code:
case st.HTTP_200_OK: case st.HTTP_200_OK:
@ -171,7 +341,12 @@ class VITACORE_API(AsyncClient):
raise e.UnknownException raise e.UnknownException
async def getELNs(self, patId: str): async def getELNs(self, patId: str):
req = await self.get('/getELNs', params={'patId': patId}) token = await self.get_token()
req = await self.get(
'/getELNs',
params={'patId': patId},
headers={'Authorization': f'Bearer {token}'},
)
match req.status_code: match req.status_code:
case st.HTTP_200_OK: case st.HTTP_200_OK:
@ -181,7 +356,12 @@ class VITACORE_API(AsyncClient):
raise e.UnknownException raise e.UnknownException
async def getPatFLG(self, patId: str): async def getPatFLG(self, patId: str):
req = await self.get('/getPatFLG', params={'patId': patId}) token = await self.get_token()
req = await self.get(
'/getPatFLG',
params={'patId': patId},
headers={'Authorization': f'Bearer {token}'},
)
match req.status_code: match req.status_code:
case st.HTTP_200_OK: case st.HTTP_200_OK:

View File

@ -7,6 +7,31 @@ class ErrorModel(BaseModel):
error: str = Field(title='Текст ошибки') error: str = Field(title='Текст ошибки')
class PatientModel(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: datetime = Field(title='Дата рождения', examples=['2024-10-16'])
gender: str = Field(title='Пол', examples=['М'])
docType: str = Field(title='Тип документа', examples=['Паспорт РФ'])
docSer: str = Field(title='Серия документа', examples=['12 34'])
docNum: str = Field(title='Номер документа', examples=['999999'])
polNum: str = Field(title='Номер полиса', examples=['999999'])
address1: str = Field(
title='Адрес проживания',
examples=['г. Москва, ул. Пушкина, д. 1'],
)
class PatientsModel(BaseModel):
patients: list[PatientModel]
class TrustedPersonModel(BaseModel): class TrustedPersonModel(BaseModel):
parentSnils: str = Field( parentSnils: str = Field(
title='СНИЛС представителя', examples=['156-125-394 57'] title='СНИЛС представителя', examples=['156-125-394 57']
@ -66,7 +91,7 @@ class ProfileModel(BaseModel):
# examples=['99'], # examples=['99'],
# ) # )
trustedPersons: list[TrustedPersonModel] = Field( trustedPersons: list[TrustedPersonModel] = Field(
title='Информация о представителе', title='Информация о представителе', default=[]
) )
@ -79,10 +104,10 @@ class DepartmentAddressModel(BaseModel):
title='Адрес строкой', title='Адрес строкой',
examples=['420097, г.Казань, ул.Заслонова, д.5'], examples=['420097, г.Казань, ул.Заслонова, д.5'],
) )
latitude: float | None = Field( latitude: str | None = Field(
title='Широта, при наличии', examples=[55.789], default=None title='Широта, при наличии', examples=[55.789], default=None
) )
longitude: float | None = Field( longitude: str | None = Field(
title='Долгота, при наличии', examples=[37.789], default=None title='Долгота, при наличии', examples=[37.789], default=None
) )
@ -92,16 +117,19 @@ class DepartmentModel(BaseModel):
title='Идентификатор МО/Филиала', title='Идентификатор МО/Филиала',
examples=['a3677271-3385-4f27-a65d-c3430b7c61c2'], examples=['a3677271-3385-4f27-a65d-c3430b7c61c2'],
) )
OID: str = Field( OID: str | None = Field(
title='OID МО / Филиала', examples=['1.2.643.5.1.13.13.12.2.16.1084'] title='OID МО / Филиала',
examples=['1.2.643.5.1.13.13.12.2.16.1084'],
default=None,
) )
parentId: str | None = Field( parentId: str | None = Field(
title='Идентификатор вышестоящего подразделения', title='Идентификатор вышестоящего подразделения',
examples=['a3677271-3385-4f27-a65d-c3430b7c61c2'], examples=['a3677271-3385-4f27-a65d-c3430b7c61c2'],
) )
fullname: str = Field( fullname: str | None = Field(
title='Полное наименование', title='Полное наименование',
examples=['ГБУЗС "Тестовая медицинская организация"'], examples=['ГБУЗС "Тестовая медицинская организация"'],
default=None,
) )
shortname: str = Field( shortname: str = Field(
title='Краткое наименование', title='Краткое наименование',
@ -112,14 +140,12 @@ class DepartmentModel(BaseModel):
', для филиалов: Стационар / Поликлиника / ФАП / Амбулатория)', ', для филиалов: Стационар / Поликлиника / ФАП / Амбулатория)',
examples=['Юридическое лицо'], examples=['Юридическое лицо'],
) )
inn: str = Field(title='ИНН', examples=['0000000000']) inn: str | None = Field(title='ИНН', examples=['0000000000'], default=None)
ogrn: str = Field(title='ОГРН', examples=['1149204047816']) ogrn: str | None = Field(
title='ОГРН', examples=['1149204047816'], default=None
)
kpp: str | None = Field(title='КПП', examples=['0000000000'], default=None) kpp: str | None = Field(title='КПП', examples=['0000000000'], default=None)
address: list[DepartmentAddressModel] address: list[DepartmentAddressModel] | None = None
# code: str = Field(
# title='Региональный код или код ТФОМС',
# examples=['0000000000'],
# )
class OrganizationsModel(BaseModel): class OrganizationsModel(BaseModel):
@ -642,7 +668,7 @@ class PatientFLGModel(BaseModel):
title='Дата следующего флюорографического осмотра', title='Дата следующего флюорографического осмотра',
examples=['2021-09-24'], examples=['2021-09-24'],
) )
PrgContingent: str = Field( PrgContingent: str | None = Field(
title='Контингент (флюорография)', title='Контингент (флюорография)',
examples=['Неорганизованное население'], examples=['Неорганизованное население'],
) )

View File

@ -56,6 +56,14 @@ class Settings(BaseSettings):
VITACORE_BASE_URL: str = Field( VITACORE_BASE_URL: str = Field(
default='https://gist-cws.ezdrav.ru:8899/MP_API' 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
TDN_BASE_URL: str = Field(default='https://tdn.tatar.ru/api') TDN_BASE_URL: str = Field(default='https://tdn.tatar.ru/api')

View File

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

View File

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

View File

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

View 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 ###

View File