diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 0fef0aed..0bc79cc5 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -6,7 +6,7 @@ on: [push] jobs: test: runs-on: ubuntu-latest - container: python:3.7 + container: python:3.11 # Service containers to run with `container-job` services: # Label used to access the service container diff --git a/Dockerfile b/Dockerfile index 17672847..5c84d1c2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.7 +FROM python:3.11 WORKDIR /opt/app diff --git a/isacc_messaging/api/isacc_record_creator.py b/isacc_messaging/api/isacc_record_creator.py index 346af7c9..e72f5ea1 100644 --- a/isacc_messaging/api/isacc_record_creator.py +++ b/isacc_messaging/api/isacc_record_creator.py @@ -1,14 +1,15 @@ from datetime import datetime, timedelta +from flask import current_app import re +import requests from typing import List, Tuple from fhirclient.models.communication import Communication -from flask import current_app from twilio.base.exceptions import TwilioRestException from isacc_messaging.api.email_notifications import send_message_received_notification -from isacc_messaging.api.ml_utils import predict_score from isacc_messaging.audit import audit_entry +from isacc_messaging.exceptions import IsaccTwilioSIDnotFound from isacc_messaging.models.fhir import ( HAPI_request, first_in_bundle, @@ -47,11 +48,6 @@ def case_insensitive_replace(text, old, new): return c -class IsaccTwilioError(Exception): - """Raised when Twilio SMS are not functioning as required for ISACC""" - pass - - class IsaccRecordCreator: def __init__(self): pass @@ -210,7 +206,7 @@ def on_twilio_message_status_update(self, values): extra={"message_sid": message_sid}, level='error' ) - raise IsaccTwilioError(f"ERROR! {error}: {message_sid}") + raise IsaccTwilioSIDnotFound(f"ERROR! {error}: {message_sid}") cr = CommunicationRequest(cr) patient = resolve_reference(cr.recipient[0].reference) @@ -303,12 +299,20 @@ def on_twilio_message_received(self, values): ) def score_message(self, message): - model_path = current_app.config.get('TORCH_MODEL_PATH') - if not model_path: + ml_service_address = current_app.config.get('ML_SERVICE_ADDRESS') + if not ml_service_address: return "routine" try: - score = predict_score(message, model_path) + url = f'{ml_service_address}/predict_score' + response = requests.post(url, json={"message": message}) + response.raise_for_status() + audit_entry( + f"predict_score call response: {response.json()}", + level='info' + ) + + score = response.json().get('score') if score == 1: return "stat" except Exception as e: @@ -321,11 +325,11 @@ def score_message(self, message): def execute_requests(self) -> Tuple[List[dict], List[dict]]: """ - For all due CommunicationRequests, generate SMS, create Communication resource, and update CommunicationRequest + For all due CommunicationRequests (up to throttle limit), generate SMS, create Communication resource, and update CommunicationRequest """ successes = [] errors = [] - + throttle_limit = 30 # conservative value based on heuristics from logs now = datetime.now().astimezone() cutoff = now - timedelta(days=2) @@ -335,6 +339,7 @@ def execute_requests(self) -> Tuple[List[dict], List[dict]]: "occurrence": f"le{now.isoformat()}", }) + sent = 0 for cr_json in next_in_bundle(result): cr = CommunicationRequest(cr_json) # as that message was likely the next-outgoing for the patient, @@ -385,7 +390,7 @@ def execute_requests(self) -> Tuple[List[dict], List[dict]]: try: cr.status = "completed" cr.persist() - comm_status, comm_statusReason = self.process_cr(cr, successes) + comm_status, comm_statusReason = self.process_cr(cr) dispatched_comm = comm.change_status(status=comm_status) audit_entry( f"Updated status of Communication/{comm.id} to {comm_status}", @@ -415,8 +420,15 @@ def execute_requests(self) -> Tuple[List[dict], List[dict]]: level='exception' ) + # Flooding system on occasions such as a holiday message to all, + # leads to an overwhelmed system. Restrict the flood by processing + # only throttle_limit per run. + sent += 1 + if sent > throttle_limit: + break + return successes, errors - def process_cr(self, cr: CommunicationRequest, successes: list): + def process_cr(self, cr: CommunicationRequest): status, statusReason = self.dispatch_cr(cr=cr) return status, statusReason diff --git a/isacc_messaging/api/ml_utils.py b/isacc_messaging/api/ml_utils.py deleted file mode 100644 index e4151fc1..00000000 --- a/isacc_messaging/api/ml_utils.py +++ /dev/null @@ -1,84 +0,0 @@ -import torch -from transformers import AutoTokenizer -import pandas as pd -from torch.utils.data import TensorDataset -from torch.utils.data import SequentialSampler, DataLoader -from scipy.special import expit as sigmoid -import numpy as np - - -def predict_score(message, model_path): - if torch.cuda.is_available(): - model = torch.load(model_path) - else: - model = torch.load(model_path, map_location=torch.device('cpu')) - - dl = get_dataloader([message]) - - result = predict(model, dl) - return np.argmax(result[0]) - -def encode_sentences(sentences, labels, tokenizer): - input_ids = [] - input_labels = [] - attention_masks = [] - - for i, sentence in enumerate(sentences): - encoded_dict = tokenizer.encode_plus( - sentence, - add_special_tokens=True, - max_length=512, - padding='max_length', - return_tensors='pt', - return_attention_mask=True, - truncation=True - ) - - input_ids.append(encoded_dict['input_ids']) - attention_masks.append(encoded_dict['attention_mask']) - input_labels.append(labels[i]) - - input_ids = torch.cat(input_ids, dim=0) - attention_masks = torch.cat(attention_masks, dim=0) - labels = torch.tensor(labels) - - # Combine the training inputs into a TensorDataset. - return TensorDataset(input_ids, attention_masks, labels.float()) - -def encode_labels(labels: pd.Series): - return pd.get_dummies(labels).values - -def get_dataloader(test_sentences): - tokenizer = AutoTokenizer.from_pretrained("publichealthsurveillance/PHS-BERT", do_lower_case=True) - test_labels = [0] * len(test_sentences) - labels = encode_labels(test_labels) - test_dataset = encode_sentences(test_sentences, labels, tokenizer) - sampler = SequentialSampler(test_dataset) - test_dataloader = DataLoader(test_dataset, sampler=sampler, batch_size=8) - return test_dataloader - -def predict(model, dataloader): - device = get_device() - print(f'Predicting labels for {len(dataloader.dataset)} documents...') - model.eval() - predictions = [] - for batch in dataloader: - batch = tuple(t.to(device) for t in batch) - batch_input_ids, batch_input_mask, batch_labels = batch - with torch.no_grad(): - outputs = model(batch_input_ids, token_type_ids=None, - attention_mask=batch_input_mask) - logits = outputs[0] - logits = logits.detach().cpu().numpy() - predictions = predictions + list(logits) - print('DONE.') - return sigmoid(predictions) - -def get_device(index=0): - if torch.cuda.is_available(): - print('There are %d GPU(s) available.' % torch.cuda.device_count()) - print('We will use the GPU:', torch.cuda.get_device_name(index), "at index:", index) - return torch.device(index) - else: - print('No GPU available, using the CPU instead.') - return torch.device("cpu") diff --git a/isacc_messaging/api/views.py b/isacc_messaging/api/views.py index 5ed3d8c1..3f63b9b8 100644 --- a/isacc_messaging/api/views.py +++ b/isacc_messaging/api/views.py @@ -2,12 +2,16 @@ import click import logging from datetime import datetime +from time import sleep from flask import Blueprint, jsonify, request from flask import current_app +from flask.cli import with_appcontext +from twilio.request_validator import RequestValidator from isacc_messaging.api.isacc_record_creator import IsaccRecordCreator from isacc_messaging.audit import audit_entry -from twilio.request_validator import RequestValidator +from isacc_messaging.exceptions import IsaccTwilioSIDnotFound +from isacc_messaging.robust_request import serialize_request, queue_request, pop_request base_blueprint = Blueprint('base', __name__, cli_group=None) @@ -88,22 +92,43 @@ def auditlog_addevent(): @base_blueprint.route("/MessageStatus", methods=['POST']) -def message_status_update(): +def message_status_update(callback_req=None, attempt_count=0): + """Registered callback for Twilio to transmit updates + + As Twilio occasionally hits this callback prior to local data being + available, it is also called subsequently from a job queue. The + parameters are only defined in the retry state. + + :param req: request from a job queue + :param attempt_count: the number of failed attemts thus far, only + defined from job queue + """ + use_request = request + if callback_req: + use_request = callback_req + audit_entry( f"Call to /MessageStatus webhook", - extra={'request.values': dict(request.values)}, + extra={'use_request.values': dict(use_request.values)}, level='debug' ) record_creator = IsaccRecordCreator() try: - record_creator.on_twilio_message_status_update(request.values) + record_creator.on_twilio_message_status_update(use_request.values) except Exception as ex: audit_entry( f"on_twilio_message_status_update generated error {ex}", level='error' ) - return ex, 200 + # Couldn't locate the message, most likely means twilio was quicker + # to call back, than HAPI could persist and find. Push to REDIS + # for another attempt later + if isinstance(ex, IsaccTwilioSIDnotFound): + req = serialize_request(use_request, attempt_count=attempt_count) + queue_request(req) + + return str(ex), 200 return '', 204 @@ -155,8 +180,8 @@ def incoming_sms(): level="error") return stackstr, 200 if result is not None: - # Occurs when message is incoming from unknown phone - # or request is coming from a subscribed phone number, but + # Occurs when message is incoming from unknown phone + # or request is coming from a subscribed phone number, but # internal logic renders it invalid audit_entry( f"on_twilio_message_received generated error {result}", @@ -213,6 +238,25 @@ def execute_requests(): ]) +@base_blueprint.cli.command("retry_requests") +@with_appcontext +def retry_requests(): + """Look for any failed requests and retry now""" + while True: + failed_request = pop_request() + if not failed_request: + break + + # Only expecting one route at this time + if ( + failed_request.url.endswith("/MessageStatus") and + failed_request.method.upper() == 'POST'): + with current_app.test_request_context(): + response, response_code = message_status_update( + failed_request, failed_request.attempt_count + 1) + if response_code != 204: + sleep(1) # give system a moment to catch up before retry + @base_blueprint.cli.command("send-system-emails") @click.argument("category", required=True) @click.option("--dry-run", is_flag=True, default=False, help="Simulate execution; generate but don't send email") @@ -301,4 +345,3 @@ def deactivate_patient(patient_id): f"Patient {patient_id} active set to false", level='info' ) - diff --git a/isacc_messaging/config.py b/isacc_messaging/config.py index 34fe9e14..b2b7a9d7 100644 --- a/isacc_messaging/config.py +++ b/isacc_messaging/config.py @@ -29,7 +29,7 @@ TWILIO_ACCOUNT_SID = os.getenv("TWILIO_ACCOUNT_SID") TWILIO_AUTH_TOKEN = os.getenv("TWILIO_AUTH_TOKEN") TWILIO_PHONE_NUMBER = os.getenv("TWILIO_PHONE_NUMBER") -TORCH_MODEL_PATH = os.getenv("TORCH_MODEL_PATH") +ML_SERVICE_ADDRESS = os.getenv("ML_SERVICE_ADDRESS") ISACC_NOTIFICATION_EMAIL_SENDER_ADDRESS = os.getenv("ISACC_NOTIFICATION_EMAIL_SENDER_ADDRESS") ISACC_NOTIFICATION_EMAIL_PASSWORD = os.getenv("ISACC_NOTIFICATION_EMAIL_PASSWORD") @@ -40,4 +40,3 @@ ISACC_APP_URL = os.getenv("ISACC_APP_URL") EMAIL_PORT = os.getenv("EMAIL_PORT", 465) EMAIL_SERVER = os.getenv("EMAIL_SERVER", "smtp.gmail.com") - diff --git a/isacc_messaging/exceptions.py b/isacc_messaging/exceptions.py new file mode 100644 index 00000000..e6da6300 --- /dev/null +++ b/isacc_messaging/exceptions.py @@ -0,0 +1,10 @@ +"""Module to define exceptions used by isacc_messaging.""" + +class IsaccTwilioSIDnotFound(Exception): + """Raised when Twilio calls with SID that can't be found""" + pass + + +class IsaccRequestRetriesExhausted(Exception): + """Raised when max retries have been tried w/o success""" + pass diff --git a/isacc_messaging/models/isacc_communication.py b/isacc_messaging/models/isacc_communication.py index 13a99c07..b08e27ab 100644 --- a/isacc_messaging/models/isacc_communication.py +++ b/isacc_messaging/models/isacc_communication.py @@ -39,12 +39,15 @@ def change_status(self, status): def about_patient(patient): """Query for "outside" Communications about the patient + This includes the dummy Communications added when staff resolve + a message without a response (category:isacc-message-resolved-no-send) + NB: only `sent` or `received` will have a valueDateTime depending on direction of outside communication. `sent` implies communication from practitioner, `received` implies communication from patient. """ return HAPI_request("GET", "Communication", params={ - "category": "isacc-non-sms-message", + "category": "isacc-non-sms-message,isacc-message-resolved-no-send", "subject": f"Patient/{patient.id}", "_sort": "-sent", }) diff --git a/isacc_messaging/models/isacc_fhirdate.py b/isacc_messaging/models/isacc_fhirdate.py index a115adaf..c5a4371b 100644 --- a/isacc_messaging/models/isacc_fhirdate.py +++ b/isacc_messaging/models/isacc_fhirdate.py @@ -36,4 +36,4 @@ def __repr__(self): DEEP_PAST = IsaccFHIRDate("1975-01-01T00:00:00Z") -DEEP_FUTURE = IsaccFHIRDate("2025-01-01T00:00:00Z") +DEEP_FUTURE = IsaccFHIRDate("2025-01-01T00:00:00Z") \ No newline at end of file diff --git a/isacc_messaging/models/isacc_patient.py b/isacc_messaging/models/isacc_patient.py index 88c3bc59..02f06795 100644 --- a/isacc_messaging/models/isacc_patient.py +++ b/isacc_messaging/models/isacc_patient.py @@ -235,7 +235,7 @@ def mark_followup_extension(self, persist_on_change=True): for c in next_in_bundle(Communication.for_patient(self, category="isacc-manually-sent-message")): most_recent_followup = FHIRDate(c["sent"]) break - # also possible a followup was recorded as `outside communication` + # also possible a followup was recorded as `outside communication` or resolved for c in next_in_bundle(Communication.about_patient(self)): # only consider outside communications reported to have been `sent` if "sent" in c: diff --git a/isacc_messaging/robust_request.py b/isacc_messaging/robust_request.py new file mode 100644 index 00000000..4acdf8d6 --- /dev/null +++ b/isacc_messaging/robust_request.py @@ -0,0 +1,44 @@ +"""Functions used for robust handling of failed requests + +Basic model: a request fails. Rather than give up, push the request +to a job queue and try again from a consumer. +""" +import json +import redis +from flask import current_app + +from isacc_messaging.exceptions import IsaccRequestRetriesExhausted + + +def serialize_request(req, attempt_count=1, max_retries=3): + """Given a request object, returns a serialized form + + :param req: The request object + :param attempt_count: Increment from previous failure on each call + :param max_retries: Maximum number of retries before giving up + + Need a serialized form of the request to push into a job queue. + This also maintains and enforces the number of attempts doesn't + exceed the maximum. + """ + serialized_form = json.dumps({ + "method": req.method, + "url": req.url, + "headers": dict(req.headers), + "body": req.get_data(as_text=True), + "attempt_count": attempt_count, + "max_retries": max_retries + }) + if attempt_count > max_retries: + raise IsaccRequestRetriesExhausted(serialized_form) + return serialized_form + +def queue_request(serialized_request): + redis_client = redis.StrictRedis.from_url(current_app.config.get("REQUEST_CACHE_URL")) + redis_client.lpush("http_request_queue", serialized_request) + + +def pop_request(): + redis_client = redis.StrictRedis.from_url(current_app.config.get("REQUEST_CACHE_URL")) + return redis_client.rpop("http_request_queue") + diff --git a/requirements.dev.txt b/requirements.dev.txt index f34a8aaf..77f24b07 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -1,25 +1,167 @@ # -# This file is autogenerated by pip-compile -# To update, run: +# This file is autogenerated by pip-compile with Python 3.9 +# by the following command: # -# pip-compile +# pip-compile --extra=dev --output-file=requirements.dev.txt setup.cfg # ---requirement requirements.txt -attrs==21.2.0 # via pytest -importlib-metadata==4.8.1 # via pluggy, pytest -iniconfig==1.1.1 # via pytest -mirakuru==2.4.1 # via pytest-redis -packaging==21.0 # via pytest -pluggy==1.0.0 # via pytest -port-for==0.6.1 # via pytest-redis -psutil==5.8.0 # via mirakuru -py==1.10.0 # via pytest -pyparsing==2.4.7 # via packaging -pytest-datadir==1.3.1 # via pytest-datadir -pytest-mock==3.6.1 # via isacc_messaging (setup.py) -pytest-redis==2.1.1 # via isacc_messaging (setup.py) -pytest==6.2.5 # via pytest-mock, pytest-redis, isacc_messaging (setup.py) -requests-mock==1.9.3 # via isacc_messaging (setup.py) -toml==0.10.2 # via pytest -typing-extensions==3.10.0.2 # via importlib-metadata -zipp==3.5.0 # via importlib-metadata +aiohappyeyeballs==2.4.0 + # via aiohttp +aiohttp==3.10.5 + # via + # aiohttp-retry + # twilio +aiohttp-retry==2.8.3 + # via twilio +aiosignal==1.3.1 + # via aiohttp +async-timeout==4.0.3 + # via + # aiohttp + # redis +attrs==24.2.0 + # via + # aiohttp + # cattrs + # requests-cache +authlib==1.3.1 + # via isacc_messaging (setup.cfg) +blinker==1.8.2 + # via flask +cachelib==0.13.0 + # via flask-session +cattrs==23.2.3 + # via requests-cache +certifi==2024.7.4 + # via requests +cffi==1.17.0 + # via cryptography +charset-normalizer==3.3.2 + # via requests +click==8.1.7 + # via flask +cryptography==43.0.0 + # via + # authlib + # python-jose +ecdsa==0.19.0 + # via python-jose +exceptiongroup==1.2.2 + # via + # cattrs + # pytest +fhirclient==4.1.0 + # via isacc_messaging (setup.cfg) +flask==3.0.3 + # via + # flask-cors + # flask-session + # isacc_messaging (setup.cfg) +flask-cors==4.0.1 + # via isacc_messaging (setup.cfg) +flask-session==0.8.0 + # via isacc_messaging (setup.cfg) +frozenlist==1.4.1 + # via + # aiohttp + # aiosignal +gunicorn==23.0.0 + # via isacc_messaging (setup.cfg) +idna==3.7 + # via + # requests + # yarl +importlib-metadata==8.4.0 + # via flask +iniconfig==2.0.0 + # via pytest +itsdangerous==2.2.0 + # via flask +jinja2==3.1.4 + # via flask +markupsafe==2.1.5 + # via + # jinja2 + # werkzeug +mirakuru==2.5.2 + # via pytest-redis +msgspec==0.18.6 + # via flask-session +multidict==6.0.5 + # via + # aiohttp + # yarl +packaging==24.1 + # via + # gunicorn + # pytest +platformdirs==4.2.2 + # via requests-cache +pluggy==1.5.0 + # via pytest +port-for==0.7.2 + # via pytest-redis +psutil==6.0.0 + # via mirakuru +pyasn1==0.6.0 + # via + # python-jose + # rsa +pycparser==2.22 + # via cffi +pyjwt==2.9.0 + # via twilio +pytest==8.3.2 + # via + # isacc_messaging (setup.cfg) + # pytest-mock + # pytest-redis +pytest-mock==3.14.0 + # via isacc_messaging (setup.cfg) +pytest-redis==3.1.2 + # via isacc_messaging (setup.cfg) +pytest-datadir==1.3.1 + # via pytest-datadir +python-jose[cryptography]==3.3.0 + # via isacc_messaging (setup.cfg) +python-json-logger==2.0.7 + # via isacc_messaging (setup.cfg) +redis==5.0.8 + # via + # isacc_messaging (setup.cfg) + # pytest-redis +requests==2.32.3 + # via + # fhirclient + # isacc_messaging (setup.cfg) + # requests-cache + # requests-mock + # twilio +requests-cache==1.2.1 + # via isacc_messaging (setup.cfg) +requests-mock==1.12.1 + # via isacc_messaging (setup.cfg) +rsa==4.9 + # via python-jose +six==1.16.0 + # via + # ecdsa + # url-normalize +tomli==2.0.1 + # via pytest +twilio==9.2.3 + # via isacc_messaging (setup.cfg) +typing-extensions==4.12.2 + # via cattrs +url-normalize==1.4.3 + # via requests-cache +urllib3==2.2.2 + # via + # requests + # requests-cache +werkzeug==3.0.3 + # via flask +yarl==1.9.4 + # via aiohttp +zipp==3.20.0 + # via importlib-metadata +git+https://github.com/uwcirg/fhir-migrations \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index cbee8aa1..d3efd059 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,43 +1,135 @@ # -# This file is autogenerated by pip-compile -# To update, run: +# This file is autogenerated by pip-compile with Python 3.9 +# by the following command: # -# pip-compile --output-file=requirements.txt +# pip-compile --output-file=requirements.txt setup.cfg # -authlib==0.14.3 # via isacc_messaging (setup.py) -certifi==2021.5.30 # via requests -cffi==1.14.1 # via cryptography -charset-normalizer==2.0.6 # via requests -click==8.0.1 # via flask -cryptography==3.3.2 # via authlib, python-jose -ecdsa==0.14.1 # via python-jose -fhirclient==4.1.0 # via isacc_messaging (setup.py) -flask==1.1.2 # via isacc_messaging (setup.py) -gunicorn==20.1.0 # via isacc_messaging (setup.py) -idna==3.2 # via requests -importlib-metadata==4.8.1 # via click -itsdangerous==2.0.1 # via flask -jinja2==3.0.1 # via flask -markupsafe==2.0.1 # via jinja2 -pyasn1==0.4.8 # via python-jose, rsa -pycparser==2.20 # via cffi -python-jose[cryptography]==3.2.0 # via isacc_messaging (setup.py) -python-json-logger==0.1.11 # via isacc_messaging (setup.py) -redis==3.5.3 # via isacc_messaging (setup.py) -requests-cache==0.6.4 # via isacc_messaging (setup.py) -requests==2.26.0 # via requests-cache, isacc_messaging (setup.py) -rsa==4.7 # via python-jose -six==1.16.0 # via ecdsa, flask-cors, url-normalize -typing-extensions==3.10.0.2 # via importlib-metadata -url-normalize==1.4.3 # via requests-cache -urllib3==1.26.7 # via requests, requests-cache -werkzeug==2.0.1 # via flask -zipp==3.5.0 # via importlib-metadata -twilio==7.14.1 +aiohappyeyeballs==2.4.0 + # via aiohttp +aiohttp==3.10.5 + # via + # aiohttp-retry + # twilio +aiohttp-retry==2.8.3 + # via twilio +aiosignal==1.3.1 + # via aiohttp +async-timeout==4.0.3 + # via + # aiohttp + # redis +attrs==24.2.0 + # via + # aiohttp + # cattrs + # requests-cache +authlib==1.3.1 + # via isacc_messaging (setup.cfg) +blinker==1.8.2 + # via flask +cachelib==0.13.0 + # via flask-session +cattrs==23.2.3 + # via requests-cache +certifi==2024.7.4 + # via requests +cffi==1.17.0 + # via cryptography +charset-normalizer==3.3.2 + # via requests +click==8.1.7 + # via flask +cryptography==43.0.0 + # via + # authlib + # python-jose +ecdsa==0.19.0 + # via python-jose +exceptiongroup==1.2.2 + # via cattrs fhirclient==4.1.0 -torch==1.13.0 -transformers==4.24.0 -scipy==1.7.3 -numpy==1.21.5 -pandas==1.3.5 + # via isacc_messaging (setup.cfg) +flask==3.0.3 + # via + # flask-cors + # flask-session + # isacc_messaging (setup.cfg) +flask-cors==4.0.1 + # via isacc_messaging (setup.cfg) +flask-session==0.8.0 + # via isacc_messaging (setup.cfg) +frozenlist==1.4.1 + # via + # aiohttp + # aiosignal +gunicorn==23.0.0 + # via isacc_messaging (setup.cfg) +idna==3.7 + # via + # requests + # yarl +importlib-metadata==8.4.0 + # via flask +itsdangerous==2.2.0 + # via flask +jinja2==3.1.4 + # via flask +markupsafe==2.1.5 + # via + # jinja2 + # werkzeug +msgspec==0.18.6 + # via flask-session +multidict==6.0.5 + # via + # aiohttp + # yarl +packaging==24.1 + # via gunicorn +platformdirs==4.2.2 + # via requests-cache +pyasn1==0.6.0 + # via + # python-jose + # rsa +pycparser==2.22 + # via cffi +pyjwt==2.9.0 + # via twilio +python-jose[cryptography]==3.3.0 + # via isacc_messaging (setup.cfg) +python-json-logger==2.0.7 + # via isacc_messaging (setup.cfg) +redis==5.0.8 + # via isacc_messaging (setup.cfg) +requests==2.32.3 + # via + # fhirclient + # isacc_messaging (setup.cfg) + # requests-cache + # twilio +requests-cache==1.2.1 + # via isacc_messaging (setup.cfg) +rsa==4.9 + # via python-jose +six==1.16.0 + # via + # ecdsa + # url-normalize +twilio==9.2.3 + # via isacc_messaging (setup.cfg) +typing-extensions==4.12.2 + # via cattrs +url-normalize==1.4.3 + # via requests-cache +urllib3==2.2.2 + # via + # requests + # requests-cache +werkzeug==3.0.3 + # via flask +yarl==1.9.4 + # via aiohttp +zipp==3.20.0 + # via importlib-metadata git+https://github.com/uwcirg/fhir-migrations \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index b09dddf4..6db5c951 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,9 +12,11 @@ include_package_data = True # abstract requirements; # concrete requirements belong in requirements.txt # https://caremad.io/posts/2013/07/setup-vs-requirement/ +# pinned fhirclient to older version to mitigate +# changes in date requirement introduced in FHIRDate install_requires = authlib - fhirclient + fhirclient==4.1.0 flask flask-cors flask-session @@ -25,7 +27,6 @@ install_requires = requests requests-cache twilio - fhirclient [options.extras_require] dev =