Skip to content

Commit d5c808b

Browse files
authored
Merge pull request #2038 from GSA/main
10/15/25 Production Release
2 parents 0cf5b66 + 9b85cb3 commit d5c808b

31 files changed

+1740
-1579
lines changed

app/__init__.py

Lines changed: 115 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
)
2020
from flask.ctx import has_app_context
2121
from flask_migrate import Migrate
22-
from flask_socketio import SocketIO
2322
from flask_sqlalchemy import SQLAlchemy as _SQLAlchemy
2423
from sqlalchemy import event
2524
from werkzeug.exceptions import HTTPException as WerkzeugHTTPException
@@ -31,7 +30,6 @@
3130
from app.clients.document_download import DocumentDownloadClient
3231
from app.clients.email.aws_ses import AwsSesClient
3332
from app.clients.email.aws_ses_stub import AwsSesStubClient
34-
from app.clients.pinpoint.aws_pinpoint import AwsPinpointClient
3533
from app.clients.sms.aws_sns import AwsSnsClient
3634
from notifications_utils import logging, request_helper
3735
from notifications_utils.clients.encryption.encryption_client import Encryption
@@ -79,9 +77,9 @@ def apply_driver_hacks(self, app, info, options):
7977
return (sa_url, options)
8078

8179

82-
# Set db engine settings here for now.
83-
# They were not being set previous (despite environmental variables with appropriate
84-
# sounding names) and were defaulting to low values
80+
# no monkey patching issue here. All the real work to set the db up
81+
# is done in db.init_app() which is called in create_app. But we need
82+
# to instantiate the db object here, because it's used in models.py
8583
db = SQLAlchemy(
8684
engine_options={
8785
"pool_size": config.Config.SQLALCHEMY_POOL_SIZE,
@@ -91,34 +89,105 @@ def apply_driver_hacks(self, app, info, options):
9189
"pool_pre_ping": True,
9290
}
9391
)
94-
migrate = Migrate()
92+
migrate = None
93+
94+
# safe to do this for monkeypatching because all real work happens in notify_celery.init_app()
95+
# called in create_app()
9596
notify_celery = NotifyCelery()
96-
aws_ses_client = AwsSesClient()
97-
aws_ses_stub_client = AwsSesStubClient()
98-
aws_sns_client = AwsSnsClient()
99-
aws_cloudwatch_client = AwsCloudwatchClient()
100-
aws_pinpoint_client = AwsPinpointClient()
101-
encryption = Encryption()
102-
zendesk_client = ZendeskClient()
97+
aws_ses_client = None
98+
aws_ses_stub_client = None
99+
aws_sns_client = None
100+
aws_cloudwatch_client = None
101+
encryption = None
102+
zendesk_client = None
103+
# safe to do this for monkeypatching because all real work happens in redis_store.init_app()
104+
# called in create_app()
103105
redis_store = RedisClient()
104-
document_download_client = DocumentDownloadClient()
105-
106-
socketio = SocketIO(
107-
cors_allowed_origins=[
108-
config.Config.ADMIN_BASE_URL,
109-
],
110-
message_queue=config.Config.REDIS_URL,
111-
logger=True,
112-
engineio_logger=True,
113-
)
106+
document_download_client = None
114107

108+
# safe for monkey patching, all work down in
109+
# notification_provider_clients.init_app() in create_app()
115110
notification_provider_clients = NotificationProviderClients()
116111

112+
# LocalProxy doesn't evaluate the target immediately, but defers
113+
# resolution to runtime. So there is no monkeypatching concern.
117114
api_user = LocalProxy(lambda: g.api_user)
118115
authenticated_service = LocalProxy(lambda: g.authenticated_service)
119116

120117

118+
def get_zendesk_client():
119+
global zendesk_client
120+
# Our unit tests mock anyway
121+
if os.environ.get("NOTIFY_ENVIRONMENT") == "test":
122+
return None
123+
if zendesk_client is None:
124+
zendesk_client = ZendeskClient()
125+
return zendesk_client
126+
127+
128+
def get_aws_ses_client():
129+
global aws_ses_client
130+
if os.environ.get("NOTIFY_ENVIRONMENT") == "test":
131+
return AwsSesClient()
132+
if aws_ses_client is None:
133+
raise RuntimeError(f"Celery not initialized aws_ses_client: {aws_ses_client}")
134+
return aws_ses_client
135+
136+
137+
def get_aws_sns_client():
138+
global aws_sns_client
139+
if os.environ.get("NOTIFY_ENVIRONMENT") == "test":
140+
return AwsSnsClient()
141+
if aws_ses_client is None:
142+
raise RuntimeError(f"Celery not initialized aws_sns_client: {aws_sns_client}")
143+
return aws_sns_client
144+
145+
146+
class FakeEncryptionApp:
147+
"""
148+
This class is just to support initialization of encryption
149+
during unit tests.
150+
"""
151+
152+
config = None
153+
154+
def init_fake_encryption_app(self, config):
155+
self.config = config
156+
157+
158+
def get_encryption():
159+
global encryption
160+
if os.environ.get("NOTIFY_ENVIRONMENT") == "test":
161+
encryption = Encryption()
162+
fake_app = FakeEncryptionApp()
163+
sekret = "SEKRET_KEY"
164+
sekret = sekret.replace("KR", "CR")
165+
fake_config = {
166+
"DANGEROUS_SALT": "SALTYSALTYSALTYSALTY",
167+
sekret: "FooFoo",
168+
} # noqa
169+
fake_app.init_fake_encryption_app(fake_config)
170+
encryption.init_app(fake_app)
171+
return encryption
172+
if encryption is None:
173+
raise RuntimeError(f"Celery not initialized encryption: {encryption}")
174+
return encryption
175+
176+
177+
def get_document_download_client():
178+
global document_download_client
179+
# Our unit tests mock anyway
180+
if os.environ.get("NOTIFY_ENVIRONMENT") == "test":
181+
return None
182+
if document_download_client is None:
183+
raise RuntimeError(
184+
f"Celery not initialized document_download_client: {document_download_client}"
185+
)
186+
return document_download_client
187+
188+
121189
def create_app(application):
190+
global zendesk_client, migrate, document_download_client, aws_ses_client, aws_ses_stub_client, aws_sns_client, encryption # noqa
122191
from app.config import configs
123192

124193
notify_environment = os.environ["NOTIFY_ENVIRONMENT"]
@@ -128,22 +197,35 @@ def create_app(application):
128197
application.config["NOTIFY_APP_NAME"] = application.name
129198
init_app(application)
130199

131-
socketio.init_app(application)
200+
request_helper.init_app(application)
201+
logging.init_app(application)
132202

133-
from app.socket_handlers import register_socket_handlers
203+
# start lazy initialization for gevent
204+
# NOTE: notify_celery and redis_store are safe to construct here
205+
# because all entry points (gunicorn_entry.py, run_celery.py) apply
206+
# monkey.patch_all() first.
207+
# Do NOT access or use them before create_app() is called and don't
208+
# call create_app() in multiple places.
134209

135-
register_socket_handlers(socketio)
136-
request_helper.init_app(application)
137210
db.init_app(application)
211+
212+
migrate = Migrate()
138213
migrate.init_app(application, db=db)
214+
if zendesk_client is None:
215+
zendesk_client = ZendeskClient()
139216
zendesk_client.init_app(application)
140-
logging.init_app(application)
141-
aws_sns_client.init_app(application)
142-
217+
document_download_client = DocumentDownloadClient()
218+
document_download_client.init_app(application)
219+
aws_cloudwatch_client = AwsCloudwatchClient()
220+
aws_cloudwatch_client.init_app(application)
221+
aws_ses_client = AwsSesClient()
143222
aws_ses_client.init_app()
223+
aws_ses_stub_client = AwsSesStubClient()
144224
aws_ses_stub_client.init_app(stub_url=application.config["SES_STUB_URL"])
145-
aws_cloudwatch_client.init_app(application)
146-
aws_pinpoint_client.init_app(application)
225+
aws_sns_client = AwsSnsClient()
226+
aws_sns_client.init_app(application)
227+
encryption = Encryption()
228+
encryption.init_app(application)
147229
# If a stub url is provided for SES, then use the stub client rather than the real SES boto client
148230
email_clients = (
149231
[aws_ses_stub_client]
@@ -153,11 +235,10 @@ def create_app(application):
153235
notification_provider_clients.init_app(
154236
sms_clients=[aws_sns_client], email_clients=email_clients
155237
)
238+
# end lazy initialization
156239

157240
notify_celery.init_app(application)
158-
encryption.init_app(application)
159241
redis_store.init_app(application)
160-
document_download_client.init_app(application)
161242

162243
register_blueprint(application)
163244

app/celery/scheduled_tasks.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from sqlalchemy import between, select, union
66
from sqlalchemy.exc import SQLAlchemyError
77

8-
from app import db, notify_celery, redis_store, zendesk_client
8+
from app import db, get_zendesk_client, notify_celery, redis_store
99
from app.celery.tasks import (
1010
get_recipient_csv_and_template_and_sender_id,
1111
process_incomplete_jobs,
@@ -44,6 +44,8 @@
4444

4545
MAX_NOTIFICATION_FAILS = 10000
4646

47+
zendesk_client = get_zendesk_client()
48+
4749

4850
@notify_celery.task(name="run-scheduled-jobs")
4951
def run_scheduled_jobs():

app/celery/service_callback_tasks.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
from flask import current_app
44
from requests import HTTPError, RequestException, request
55

6-
from app import encryption, notify_celery
6+
from app import get_encryption, notify_celery
77
from app.config import QueueNames
88
from app.utils import DATETIME_FORMAT
99

10+
encryption = get_encryption()
11+
1012

1113
@notify_celery.task(
1214
bind=True, name="send-delivery-status", max_retries=5, default_retry_delay=300

app/celery/tasks.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from requests import HTTPError, RequestException, request
1111
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
1212

13-
from app import create_uuid, encryption, notify_celery
13+
from app import create_uuid, get_encryption, notify_celery
1414
from app.aws import s3
1515
from app.celery import provider_tasks
1616
from app.config import Config, QueueNames
@@ -38,6 +38,8 @@
3838
from app.utils import DATETIME_FORMAT, hilite, utc_now
3939
from notifications_utils.recipients import RecipientCSV
4040

41+
encryption = get_encryption()
42+
4143

4244
@notify_celery.task(name="process-job")
4345
def process_job(job_id, sender_id=None):

app/dao/fact_notification_status_dao.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from datetime import timedelta
22

3-
from sqlalchemy import Date, case, cast, delete, func, select, union_all
3+
from sqlalchemy import Date, case, cast, delete, desc, func, select, union_all
44
from sqlalchemy.dialects.postgresql import insert
55
from sqlalchemy.orm import aliased
66
from sqlalchemy.sql.expression import extract, literal
@@ -108,6 +108,7 @@ def fetch_notification_status_for_service_by_month(start_date, end_date, service
108108
NotificationAllTimeView.notification_type,
109109
NotificationAllTimeView.status,
110110
)
111+
.order_by(desc(func.date_trunc("month", NotificationAllTimeView.created_at)))
111112
)
112113
return db.session.execute(stmt).all()
113114

app/dao/notifications_dao.py

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,9 @@
2626
from app import create_uuid, db
2727
from app.dao.dao_utils import autocommit
2828
from app.dao.inbound_sms_dao import Pagination
29-
from app.dao.jobs_dao import dao_get_job_by_id
3029
from app.enums import KeyType, NotificationStatus, NotificationType
3130
from app.models import FactNotificationStatus, Notification, NotificationHistory
3231
from app.utils import (
33-
emit_job_update_summary,
3432
escape_special_characters,
3533
get_midnight_in_utc,
3634
midnight_n_days_ago,
@@ -897,19 +895,6 @@ def dao_update_delivery_receipts(receipts, delivered):
897895
f"#loadtestperformance batch update query time: \
898896
updated {len(receipts)} notification in {elapsed_time} ms"
899897
)
900-
job_ids = (
901-
db.session.execute(
902-
select(Notification.job_id).where(
903-
Notification.message_id.in_(id_to_carrier.keys())
904-
)
905-
)
906-
.scalars()
907-
.all()
908-
)
909-
910-
for job_id in set(job_ids):
911-
job = dao_get_job_by_id(job_id)
912-
emit_job_update_summary(job)
913898

914899

915900
def dao_close_out_delivery_receipts():

app/job/rest.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@
4949

5050
@job_blueprint.route("/<job_id>", methods=["GET"])
5151
def get_job_by_service_and_job_id(service_id, job_id):
52-
current_app.logger.info(hilite("ENTER get_job_by_service_and_job_id"))
5352
check_suspicious_id(service_id, job_id)
5453
job = dao_get_job_by_service_id_and_job_id(service_id, job_id)
5554
statistics = dao_get_notification_outcomes_for_job(service_id, job_id)
@@ -150,7 +149,6 @@ def get_all_notifications_for_service_job(service_id, job_id):
150149
notifications = notification_with_template_schema.dump(
151150
paginated_notifications.items, many=True
152151
)
153-
current_app.logger.info(hilite("Got the dumped notifications and returning"))
154152

155153
return (
156154
jsonify(

app/models.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from sqlalchemy.orm import validates
1010
from sqlalchemy.orm.collections import attribute_mapped_collection
1111

12-
from app import db, encryption
12+
from app import db, get_encryption
1313
from app.enums import (
1414
AgreementStatus,
1515
AgreementType,
@@ -48,6 +48,8 @@
4848
)
4949
from notifications_utils.template import PlainTextEmailTemplate, SMSMessageTemplate
5050

51+
encryption = get_encryption()
52+
5153

5254
def filter_null_value_fields(obj):
5355
return dict(filter(lambda x: x[1] is not None, obj.items()))

app/socket_handlers.py

Lines changed: 0 additions & 27 deletions
This file was deleted.

app/utils.py

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -146,20 +146,6 @@ def debug_not_production(msg):
146146
current_app.logger.info(msg)
147147

148148

149-
def emit_job_update_summary(job):
150-
from app import socketio
151-
152-
socketio.emit(
153-
"job_updated",
154-
{
155-
"job_id": str(job.id),
156-
"job_status": job.job_status,
157-
"notification_count": job.notification_count,
158-
},
159-
room=f"job-{job.id}",
160-
)
161-
162-
163149
def is_suspicious_input(input_str):
164150
if not isinstance(input_str, str):
165151
return False

0 commit comments

Comments
 (0)