diff --git a/.dockerignore b/.dockerignore index a1cd5c7..ce18b20 100644 --- a/.dockerignore +++ b/.dockerignore @@ -20,3 +20,6 @@ uv.lock # Container container + +# Postgres +postgres diff --git a/.gitignore b/.gitignore index a1cd5c7..ce18b20 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,6 @@ uv.lock # Container container + +# Postgres +postgres diff --git a/docker-compose.yml b/docker-compose.yml index 7c548d4..5471dec 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,13 +8,14 @@ x-app-common: &app-common restart: unless-stopped stop_signal: SIGINT env_file: - - .env + - .test.env environment: DATABASE_URL: "postgresql://postgres:example@db:5432/postgres" REDIS_URL: "redis://valkey:6379/0" volumes: - - "./container/certt.cer:/var/opt/cprocsp/keys/cert.cer" - - "./container/cont:/app/cont" + # - "./container/certt.cer:/var/opt/cprocsp/keys/cert.cer" + - "./container/test.cer:/var/opt/cprocsp/keys/cert.cer" + - "./container/cont2:/app/cont" services: valkey: @@ -28,6 +29,21 @@ services: timeout: 10s 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: <<: *app-common ports: diff --git a/pyproject.toml b/pyproject.toml index 4b9fc3b..0eb998a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,9 +50,9 @@ _lint = "pre-commit run --all-files" lint = ["_git", "_lint"] check = "uv pip ls --outdated" -run = "uv run --directory ./src/ server.py" -manage = "uv run --directory ./src/ manage.py" -migrate = "uv run --directory ./src/ alembic revision --autogenerate" +run = "uv run --env-file ../.env --directory ./src/ server.py" +manage = "uv run --env-file ../.env --directory ./src/ manage.py" +migrate = "uv run --env-file ../.env --directory ./src/ alembic revision --autogenerate" [tool.uv] required-version = ">=0.7.0" diff --git a/src/apps/esia/scopes.py b/src/apps/esia/scopes.py index 0df0afa..f8408fc 100644 --- a/src/apps/esia/scopes.py +++ b/src/apps/esia/scopes.py @@ -1,10 +1,10 @@ SCOPES = [ 'openid', 'fullname', - 'email', - 'birthdate', - 'gender', - 'snils', - 'id_doc', - 'mobile', + # 'email', + # 'birthdate', + # 'gender', + # 'snils', + # 'id_doc', + # 'mobile', ] diff --git a/src/apps/esia/v1/router.py b/src/apps/esia/v1/router.py index eca32f1..9187455 100644 --- a/src/apps/esia/v1/router.py +++ b/src/apps/esia/v1/router.py @@ -2,9 +2,12 @@ import secrets from logging import getLogger from fastapi import APIRouter +from sqlmodel import select from apps.esia.sign import get_url +from apps.users.models import User from clients import clients as c +from database import AsyncSessionDep from shared import exceptions as e from shared.redis import client as cache @@ -26,7 +29,7 @@ async def login(): @router.post('/callback') -async def callback(code: str): +async def callback(session: AsyncSessionDep, code: str): token = None for i in range(3): try: @@ -42,9 +45,34 @@ async def callback(code: str): if token is None: 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) - cache.set(access_token, access_token) + cache.set(access_token, f'user:{user.id}') return s.Token(access_token=access_token) diff --git a/src/apps/users/auth.py b/src/apps/users/auth.py index 436b07e..fa8d4bc 100644 --- a/src/apps/users/auth.py +++ b/src/apps/users/auth.py @@ -2,7 +2,10 @@ from typing import Annotated from fastapi import Depends 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.redis import client as cache @@ -10,11 +13,20 @@ BEARER = HTTPBearer() async def login( + session: AsyncSessionDep, 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 - 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 diff --git a/src/apps/users/models.py b/src/apps/users/models.py new file mode 100644 index 0000000..6c2beac --- /dev/null +++ b/src/apps/users/models.py @@ -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) diff --git a/src/apps/users/v1/router.py b/src/apps/users/v1/router.py index b28087a..e1b5c4e 100644 --- a/src/apps/users/v1/router.py +++ b/src/apps/users/v1/router.py @@ -6,6 +6,7 @@ from typing import Annotated from fastapi import APIRouter, Body, Depends, status from apps.users.auth import login +from apps.users.models import User from clients import clients as c from clients.vitacore import schema as s from shared.redis import client as cache @@ -20,16 +21,16 @@ router = APIRouter( @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( - 'b62e9f22-a871-4c52-96d6-559c707a716d' - ) + return await c.vitacore_api.getProfile(user.vita_id) -@router.get('/getDepartments', response_model=list[s.DepartmentModel]) +@router.get('/getDepartments', + # response_model=list[s.DepartmentModel] + ) async def get_departments(): """ Get list of departments. @@ -38,7 +39,9 @@ async def get_departments(): @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. """ @@ -46,7 +49,7 @@ async def get_workers(departmentId: str): @router.get('/getSpecs', response_model=s.SpecsV021Model) -async def get_specs(): +async def get_specs(user: Annotated[User, Depends(login)]): """ Get list of specialties. """ @@ -54,27 +57,23 @@ async def get_specs(): @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. """ - return await c.vitacore_api.getEntries( - '6c7978f0-c573-4ccf-8c6e-f0cd9aceb1e1' - ) + return await c.vitacore_api.getEntries(user.vita_id) @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. """ - return await c.vitacore_api.getVaccsReport( - '6fe66cae-409a-4f56-8ae9-d55d3c38569b' - ) + return await c.vitacore_api.getVaccsReport(user.vita_id) @router.get('/getMedExamDict') -async def get_med_exam_dict(): +async def get_med_exam_dict(user: Annotated[User, Depends(login)]): """ Get medical examination dictionary. """ @@ -82,92 +81,80 @@ async def get_med_exam_dict(): @router.get('/getRoutesList') -async def get_routes_list(): +async def get_routes_list(user: Annotated[User, Depends(login)]): """ Get list of routes. """ - return await c.vitacore_api.getRoutesList( - '4e6de5f7-4dc9-451b-bf0d-7a64a9b8c279' - ) + return await c.vitacore_api.getRoutesList(user.vita_id) @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. """ return await c.vitacore_api.getHospExaminations( - '7bbdac30-9a33-4f13-9458-2c229c0c20f5', - 'f22be2c9-8e68-42d6-851e-fbf4a5e8f657', + user.vita_id, + examId, ) @router.get('/getCurrHosp') -async def get_curr_hosp(): +async def get_curr_hosp(user: Annotated[User, Depends(login)]): """ Get current hospitalization. """ - return await c.vitacore_api.getCurrHosp( - 'b708e782-4f83-4f3b-8639-512c0c9637bf' - ) + return await c.vitacore_api.getCurrHosp(user.vita_id) @router.get('/getHosps') -async def get_hosps(): +async def get_hosps(user: Annotated[User, Depends(login)]): """ Get list of hospitals. """ - return await c.vitacore_api.getHosps( - 'b708e782-4f83-4f3b-8639-512c0c9637bf' - ) + return await c.vitacore_api.getHosps(user.vita_id) @router.get('/getHospRecommendations') -async def get_hosp_recommendations(): +async def get_hosp_recommendations(user: Annotated[User, Depends(login)]): """ Get list of recommended hospitals. """ - return await c.vitacore_api.getHospRecommendations( - 'b708e782-4f83-4f3b-8639-512c0c9637bf' - ) + return await c.vitacore_api.getHospRecommendations(user.vita_id) @router.get('/getHospRoutes') -async def get_hosp_routes(): +async def get_hosp_routes(user: Annotated[User, Depends(login)]): """ Get list of recommended hospitals. """ - return await c.vitacore_api.getHospRoutes( - '3092e1c5-e08b-4654-a027-82be90fe8a49' - ) + return await c.vitacore_api.getHospRoutes(user.vita_id) @router.get('/getDiagnosticResults') -async def get_diagnostic_results(): +async def get_diagnostic_results(user: Annotated[User, Depends(login)]): """ Get list of diagnostic results. """ - return await c.vitacore_api.getDiagnosticResults( - '4867cc79-9805-4ae2-98d3-2f822848635e' - ) + return await c.vitacore_api.getDiagnosticResults(user.vita_id) @router.get('/getELNs') -async def get_eln(): +async def get_eln(user: Annotated[User, Depends(login)]): """ 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') -async def get_pat_flg(): +async def get_pat_flg(user: Annotated[User, Depends(login)]): """ Get list of ELNs. """ - return await c.vitacore_api.getPatFLG( - '0bf2e271-e565-42a8-924e-0017bcdedecd' - ) + return await c.vitacore_api.getPatFLG(user.vita_id) # @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) async def measurement( - user: Annotated[str, Depends(login)], + user: Annotated[User, Depends(login)], ad: Annotated[int, Body()], sd: Annotated[int, Body()], pulse: Annotated[int, Body()], @@ -208,7 +195,7 @@ async def measurement( 'comment': comment, 'status': status, } - cache_key = f'tdn:measurement:{user}:{created}' + cache_key = f'tdn:measurement:{user.id}:{created}' cache.set(cache_key, dumps(data)) return @@ -221,28 +208,35 @@ async def measurements( return data +@router.get('/test') +async def test_route(): + return await c.aemd_api.searchRegistryItem('16247900267') + + @router.get('/queue') async def queue(user: Annotated[bool, Depends(login)]): - 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': 'врач-терапевт', - } + 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': 'врач-терапевт', + } + ] diff --git a/src/clients/__init__.py b/src/clients/__init__.py index 653e46a..3e4bd0e 100644 --- a/src/clients/__init__.py +++ b/src/clients/__init__.py @@ -1,3 +1,4 @@ +from .aemd.api import AEMD_API from .esia.api import ESIA_API from .tdn.api import TDN_API from .vitacore.api import VITACORE_API @@ -7,6 +8,7 @@ class ClientsObject: _esia_api = None _vitacore_api = None _tdn_api = None + _aemd_api = None @property def esia_api(self): @@ -29,5 +31,12 @@ class ClientsObject: 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() diff --git a/src/clients/aemd/__init__.py b/src/clients/aemd/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/clients/aemd/api.py b/src/clients/aemd/api.py new file mode 100644 index 0000000..1603cdb --- /dev/null +++ b/src/clients/aemd/api.py @@ -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 ( + '' + '' + '' + f'{endpoint}' + '' + '' + f'{settings.AEMD_TOKEN}' + '' + '' + 'http://gist-sdw.ezdrav.ru:8708/EMDAService' + '' + '' + f'{body}' + '' + '' + ) + + async def searchRegistryItem(self, patient_snils: str): + envelope = self.get_envelope( + 'searchRegistryItem', + f'{patient_snils}', + ) + req = await self.post('/', content=envelope) + + return req.text diff --git a/src/clients/esia/schema.py b/src/clients/esia/schema.py index 904b7cd..8f3a680 100644 --- a/src/clients/esia/schema.py +++ b/src/clients/esia/schema.py @@ -33,7 +33,7 @@ class IDTokenModel(BaseModel): exp: int iat: int iss: str - acr: IDTokenACRModel + # acr: IDTokenACRModel urn_esia_amd: str = Field(alias='urn:esia:amd') urn_esia_sid: str = Field(alias='urn:esia:sid') urn_esia_sbj: IDTokenSBJModel = Field(alias='urn:esia:sbj') @@ -44,10 +44,10 @@ class UserInfoModel(BaseModel): firstName: str lastName: str middleName: str - birthDate: str - gender: str + # birthDate: str + # gender: str trusted: bool - citizenship: str + # citizenship: str snils: str inn: int updatedOn: int diff --git a/src/clients/vitacore/api.py b/src/clients/vitacore/api.py index 820b38a..e527d7c 100644 --- a/src/clients/vitacore/api.py +++ b/src/clients/vitacore/api.py @@ -1,10 +1,12 @@ +from datetime import UTC, datetime from logging import getLogger from fastapi import status as st -from httpx import AsyncClient +from httpx import AsyncClient, BasicAuth from core.config import settings from shared import exceptions as e +from shared.redis import client as cache from . import schema as s @@ -14,11 +16,56 @@ class VITACORE_API(AsyncClient): self.logger = getLogger(__name__) 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): - 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): - 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: case st.HTTP_200_OK: @@ -28,18 +75,24 @@ class VITACORE_API(AsyncClient): raise e.UnknownException 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: case st.HTTP_200_OK: return s.OrganizationsModel.model_validate(req.json()) case _: - self.logger.error(req.json()) + self.logger.error(req.text) raise e.UnknownException async def getWorkers(self, departmentId: str): + token = await self.get_token() req = await self.get( - '/getWorkers', params={'departmentId': departmentId} + '/getWorkers', + params={'departmentId': departmentId}, + headers={'Authorization': f'Bearer {token}'}, ) match req.status_code: @@ -50,7 +103,10 @@ class VITACORE_API(AsyncClient): raise e.UnknownException 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: case st.HTTP_200_OK: @@ -60,7 +116,13 @@ class VITACORE_API(AsyncClient): raise e.UnknownException 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: case st.HTTP_200_OK: @@ -76,17 +138,31 @@ class VITACORE_API(AsyncClient): raise e.UnknownException 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: 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': + return s.VaccsReportModel(content='') case _: self.logger.error(req.json()) raise e.UnknownException 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: case st.HTTP_200_OK: @@ -96,19 +172,39 @@ class VITACORE_API(AsyncClient): raise e.UnknownException 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: 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='fakeName', + Routes=[], + ) + case _: self.logger.error(req.json()) raise e.UnknownException async def getHospExaminations(self, patId: str, examId: str): + token = await self.get_token() req = await self.get( '/getHospExaminations', params={'patId': patId, 'examId': examId}, + headers={'Authorization': f'Bearer {token}'}, ) match req.status_code: @@ -119,49 +215,123 @@ class VITACORE_API(AsyncClient): raise e.UnknownException 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: case st.HTTP_200_OK: 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 _: self.logger.error(req.json()) raise e.UnknownException 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: 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() + patId = 'b66a85f1-4aaa-4db8-942a-2de44341824e' req = await self.get( - '/getHospRecommendations', params={'patId': patId} + '/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): - 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: 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): - 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: case st.HTTP_200_OK: @@ -171,7 +341,12 @@ class VITACORE_API(AsyncClient): raise e.UnknownException 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: case st.HTTP_200_OK: @@ -181,7 +356,12 @@ class VITACORE_API(AsyncClient): raise e.UnknownException 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: case st.HTTP_200_OK: diff --git a/src/clients/vitacore/schema.py b/src/clients/vitacore/schema.py index cd96139..177d8ac 100644 --- a/src/clients/vitacore/schema.py +++ b/src/clients/vitacore/schema.py @@ -7,6 +7,31 @@ class ErrorModel(BaseModel): 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): parentSnils: str = Field( title='СНИЛС представителя', examples=['156-125-394 57'] @@ -66,7 +91,7 @@ class ProfileModel(BaseModel): # examples=['99'], # ) trustedPersons: list[TrustedPersonModel] = Field( - title='Информация о представителе', + title='Информация о представителе', default=[] ) @@ -79,10 +104,10 @@ class DepartmentAddressModel(BaseModel): title='Адрес строкой', examples=['420097, г.Казань, ул.Заслонова, д.5'], ) - latitude: float | None = Field( + latitude: str | None = Field( title='Широта, при наличии', examples=[55.789], default=None ) - longitude: float | None = Field( + longitude: str | None = Field( title='Долгота, при наличии', examples=[37.789], default=None ) @@ -92,16 +117,19 @@ class DepartmentModel(BaseModel): title='Идентификатор МО/Филиала', examples=['a3677271-3385-4f27-a65d-c3430b7c61c2'], ) - OID: str = Field( - title='OID МО / Филиала', examples=['1.2.643.5.1.13.13.12.2.16.1084'] + 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 = Field( + fullname: str | None = Field( title='Полное наименование', examples=['ГБУЗС "Тестовая медицинская организация"'], + default=None, ) shortname: str = Field( title='Краткое наименование', @@ -112,14 +140,12 @@ class DepartmentModel(BaseModel): ', для филиалов: Стационар / Поликлиника / ФАП / Амбулатория)', examples=['Юридическое лицо'], ) - inn: str = Field(title='ИНН', examples=['0000000000']) - ogrn: str = Field(title='ОГРН', examples=['1149204047816']) + 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] - # code: str = Field( - # title='Региональный код или код ТФОМС', - # examples=['0000000000'], - # ) + address: list[DepartmentAddressModel] | None = None class OrganizationsModel(BaseModel): @@ -642,7 +668,7 @@ class PatientFLGModel(BaseModel): title='Дата следующего флюорографического осмотра', examples=['2021-09-24'], ) - PrgContingent: str = Field( + PrgContingent: str | None = Field( title='Контингент (флюорография)', examples=['Неорганизованное население'], ) diff --git a/src/core/config.py b/src/core/config.py index 1c5bb6b..02f4a7d 100644 --- a/src/core/config.py +++ b/src/core/config.py @@ -56,6 +56,14 @@ class Settings(BaseSettings): 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') diff --git a/src/core/main.py b/src/core/main.py index 2b0dd49..38fb9c5 100644 --- a/src/core/main.py +++ b/src/core/main.py @@ -3,7 +3,7 @@ from logging import getLogger from fastapi import FastAPI from fastapi.responses import ORJSONResponse -# from database import lifespan +from database import lifespan from middlewares import register_middlewares from .config import settings @@ -19,7 +19,7 @@ app = FastAPI( version=str(settings.VERSION), openapi_url=None, default_response_class=ORJSONResponse, - # lifespan=lifespan, + lifespan=lifespan, docs_url=None, redoc_url=None, ) diff --git a/src/migrations/env.py b/src/migrations/env.py index 00d508f..b79b300 100644 --- a/src/migrations/env.py +++ b/src/migrations/env.py @@ -4,6 +4,7 @@ from alembic import context from sqlalchemy import engine_from_config, pool from sqlmodel import SQLModel +from apps.users.models import * # noqa: F403 from core.log import config as log_config from database import db_manager diff --git a/src/migrations/script.py.mako b/src/migrations/script.py.mako index 480b130..045a768 100644 --- a/src/migrations/script.py.mako +++ b/src/migrations/script.py.mako @@ -1,28 +1,32 @@ -"""${message} +""" +${message} Revision ID: ${up_revision} Revises: ${down_revision | comma,n} Create Date: ${create_date} """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa -${imports if imports else ""} +import sqlmodel.sql.sqltypes +from alembic import op # revision identifiers, used by Alembic. revision: str = ${repr(up_revision)} -down_revision: Union[str, None] = ${repr(down_revision)} -branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} -depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} +down_revision: str | None = ${repr(down_revision)} +branch_labels: str | Sequence[str] | None = ${repr(branch_labels)} +depends_on: str | Sequence[str] | None = ${repr(depends_on)} + + +if not sqlmodel.sql: + msg = 'Something went wrong' + raise Exception(msg) def upgrade() -> None: - """Upgrade schema.""" ${upgrades if upgrades else "pass"} def downgrade() -> None: - """Downgrade schema.""" ${downgrades if downgrades else "pass"} diff --git a/src/migrations/versions/2025.10.09_17-42-25_a74bcd05c7b8.py b/src/migrations/versions/2025.10.09_17-42-25_a74bcd05c7b8.py new file mode 100644 index 0000000..cc3f7b0 --- /dev/null +++ b/src/migrations/versions/2025.10.09_17-42-25_a74bcd05c7b8.py @@ -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 ### diff --git a/src/migrations/versions/__init__.py b/src/migrations/versions/__init__.py new file mode 100644 index 0000000..e69de29