mirror of
https://github.com/GSA/notifications-api.git
synced 2026-08-27 09:43:41 -04:00
Compare commits
39 Commits
dynamic-sc
...
dac09809f5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dac09809f5 | ||
|
|
9f0b6b0ca1 | ||
|
|
7dd1751556 | ||
|
|
3ae7749d0b | ||
|
|
f6cc4afab7 | ||
|
|
6325849855 | ||
|
|
b564a8800e | ||
|
|
43247e81bb | ||
|
|
7f2325ea5e | ||
|
|
bcd18ac936 | ||
|
|
7fc65dd4f5 | ||
|
|
276446c57e | ||
|
|
bc26231164 | ||
|
|
8c80f2a462 | ||
|
|
4ccfc82c8c | ||
|
|
ebd0ed9365 | ||
|
|
02becbd1e5 | ||
|
|
419d6cee69 | ||
|
|
71e8f20b04 | ||
|
|
db90cecd65 | ||
|
|
e93e3f3690 | ||
|
|
d1fab496f4 | ||
|
|
1f4b408d1d | ||
|
|
f15d6128dc | ||
|
|
3ad7a3b2fd | ||
|
|
aea7249124 | ||
|
|
bf3897729c | ||
|
|
8181eda049 | ||
|
|
e3d2e75182 | ||
|
|
22085aa334 | ||
|
|
a236df97bf | ||
|
|
9e36418271 | ||
|
|
6f524cc907 | ||
|
|
b37c803edc | ||
|
|
1cc1d9a58d | ||
|
|
9174978e3e | ||
|
|
f8726ca6b7 | ||
|
|
0792ea123f | ||
|
|
16c68e06b9 |
18
Makefile
18
Makefile
@@ -5,7 +5,8 @@ DATE = $(shell date +%Y-%m-%d:%H:%M:%S)
|
||||
APP_VERSION_FILE = app/version.py
|
||||
|
||||
GIT_BRANCH ?= $(shell git symbolic-ref --short HEAD 2> /dev/null || echo "detached")
|
||||
GIT_COMMIT ?= $(shell git rev-parse HEAD)
|
||||
GIT_COMMIT ?= $(shell git rev-parse HEAD 2> /dev/null || echo "")
|
||||
GIT_HOOKS_PATH ?= $(shell git config --global core.hooksPath || echo "")
|
||||
|
||||
## DEVELOPMENT
|
||||
|
||||
@@ -23,6 +24,19 @@ bootstrap: ## Set up everything to run the app
|
||||
createdb test_notification_api || true
|
||||
(poetry run flask db upgrade) || true
|
||||
|
||||
.PHONY: bootstrap-with-git-hooks
|
||||
bootstrap-with-git-hooks: ## Sets everything up and accounts for pre-existing git hooks
|
||||
make generate-version-file
|
||||
poetry self add poetry-dotenv-plugin
|
||||
poetry lock --no-update
|
||||
poetry install --sync --no-root
|
||||
git config --global --unset-all core.hooksPath
|
||||
poetry run pre-commit install
|
||||
git config --global core.hookspath "${GIT_HOOKS_PATH}"
|
||||
createdb notification_api || true
|
||||
createdb test_notification_api || true
|
||||
(poetry run flask db upgrade) || true
|
||||
|
||||
.PHONY: bootstrap-with-docker
|
||||
bootstrap-with-docker: ## Build the image to run the app in Docker
|
||||
docker build -f docker/Dockerfile -t notifications-api .
|
||||
@@ -50,7 +64,7 @@ too-complex:
|
||||
poetry run radon cc ./app -a -nc
|
||||
|
||||
.PHONY: run-flask
|
||||
run-flask: ## Run flask
|
||||
run-flask:
|
||||
poetry run newrelic-admin run-program flask run -p 6011 --host=0.0.0.0
|
||||
|
||||
.PHONY: run-celery
|
||||
|
||||
@@ -13,6 +13,7 @@ from flask import current_app, g, has_request_context, jsonify, make_response, r
|
||||
from flask.ctx import has_app_context
|
||||
from flask_marshmallow import Marshmallow
|
||||
from flask_migrate import Migrate
|
||||
from flask_socketio import SocketIO
|
||||
from flask_sqlalchemy import SQLAlchemy as _SQLAlchemy
|
||||
from sqlalchemy import event
|
||||
from werkzeug.exceptions import HTTPException as WerkzeugHTTPException
|
||||
@@ -94,6 +95,14 @@ zendesk_client = ZendeskClient()
|
||||
redis_store = RedisClient()
|
||||
document_download_client = DocumentDownloadClient()
|
||||
|
||||
socketio = SocketIO(
|
||||
cors_allowed_origins=[
|
||||
config.Config.ADMIN_BASE_URL,
|
||||
],
|
||||
message_queue=config.Config.REDIS_URL,
|
||||
logger=True,
|
||||
engineio_logger=True,
|
||||
)
|
||||
|
||||
notification_provider_clients = NotificationProviderClients()
|
||||
|
||||
@@ -111,6 +120,11 @@ def create_app(application):
|
||||
application.config["NOTIFY_APP_NAME"] = application.name
|
||||
init_app(application)
|
||||
|
||||
socketio.init_app(application)
|
||||
|
||||
from app.socket_handlers import register_socket_handlers
|
||||
|
||||
register_socket_handlers(socketio)
|
||||
request_helper.init_app(application)
|
||||
db.init_app(application)
|
||||
migrate.init_app(application, db=db)
|
||||
|
||||
@@ -179,6 +179,7 @@ class Config(object):
|
||||
S3_RESOURCE = session.resource("s3", config=AWS_CLIENT_CONFIG)
|
||||
|
||||
CELERY = {
|
||||
"broker_connection_retry_on_startup": True,
|
||||
"worker_max_tasks_per_child": 500,
|
||||
"task_ignore_result": True,
|
||||
"result_persistent": False,
|
||||
|
||||
@@ -26,9 +26,11 @@ from werkzeug.datastructures import MultiDict
|
||||
from app import create_uuid, db
|
||||
from app.dao.dao_utils import autocommit
|
||||
from app.dao.inbound_sms_dao import Pagination
|
||||
from app.dao.jobs_dao import dao_get_job_by_id
|
||||
from app.enums import KeyType, NotificationStatus, NotificationType
|
||||
from app.models import FactNotificationStatus, Notification, NotificationHistory
|
||||
from app.utils import (
|
||||
emit_job_update_summary,
|
||||
escape_special_characters,
|
||||
get_midnight_in_utc,
|
||||
midnight_n_days_ago,
|
||||
@@ -895,6 +897,19 @@ def dao_update_delivery_receipts(receipts, delivered):
|
||||
f"#loadtestperformance batch update query time: \
|
||||
updated {len(receipts)} notification in {elapsed_time} ms"
|
||||
)
|
||||
job_ids = (
|
||||
db.session.execute(
|
||||
select(Notification.job_id).where(
|
||||
Notification.message_id.in_(id_to_carrier.keys())
|
||||
)
|
||||
)
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
|
||||
for job_id in set(job_ids):
|
||||
job = dao_get_job_by_id(job_id)
|
||||
emit_job_update_summary(job)
|
||||
|
||||
|
||||
def dao_close_out_delivery_receipts():
|
||||
|
||||
@@ -105,6 +105,17 @@ def send_sms_to_provider(notification):
|
||||
# The future home of the validation is TBD
|
||||
_experimentally_validate_phone_numbers(recipient)
|
||||
|
||||
# TODO current we allow US phone numbers to be uploaded without the country code (1)
|
||||
# This will break certain international phone numbers (Norway, Denmark, East Timor)
|
||||
# When we officially announce support for international numbers, US numbers must contain
|
||||
# their country code.
|
||||
recipient = str(recipient)
|
||||
if len(recipient) == 10:
|
||||
if os.getenv("NOTIFY_ENVIRONMENT") not in [
|
||||
"test"
|
||||
]: # we want to test intl support
|
||||
recipient = f"1{recipient}"
|
||||
|
||||
sender_numbers = get_sender_numbers(notification)
|
||||
if notification.reply_to_text not in sender_numbers:
|
||||
raise ValueError(
|
||||
|
||||
16
app/socket_handlers.py
Normal file
16
app/socket_handlers.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from flask import current_app, request
|
||||
from flask_socketio import join_room, leave_room
|
||||
|
||||
|
||||
def register_socket_handlers(socketio):
|
||||
@socketio.on("join")
|
||||
def on_join(data): # noqa: F401
|
||||
room = data.get("room")
|
||||
join_room(room)
|
||||
current_app.logger.info(f"Socket {request.sid} joined room {room}")
|
||||
|
||||
@socketio.on("leave")
|
||||
def on_leave(data): # noqa: F401
|
||||
room = data.get("room")
|
||||
leave_room(room)
|
||||
current_app.logger.info(f"Socket {request.sid} left room {room}")
|
||||
@@ -1,4 +1,4 @@
|
||||
from flask import Blueprint, jsonify, make_response, request
|
||||
from flask import Blueprint, jsonify, request
|
||||
from sqlalchemy import text
|
||||
|
||||
from app import db, version
|
||||
@@ -14,7 +14,7 @@ def show_status():
|
||||
if request.args.get("simple", None):
|
||||
return jsonify(status="ok"), 200
|
||||
else:
|
||||
response = make_response(
|
||||
return (
|
||||
jsonify(
|
||||
status="ok", # This should be considered part of the public API
|
||||
git_commit=version.__git_commit__,
|
||||
@@ -23,21 +23,17 @@ def show_status():
|
||||
),
|
||||
200,
|
||||
)
|
||||
response.headers["Content-Type"] = "application/json"
|
||||
return response
|
||||
|
||||
|
||||
@status.route("/_status/live-service-and-organization-counts")
|
||||
def live_service_and_organization_counts():
|
||||
response = make_response(
|
||||
return (
|
||||
jsonify(
|
||||
organizations=dao_count_organizations_with_live_services(),
|
||||
services=dao_count_live_services(),
|
||||
),
|
||||
200,
|
||||
)
|
||||
response.headers["Content-Type"] = "application/json"
|
||||
return response
|
||||
|
||||
|
||||
def get_db_version():
|
||||
|
||||
15
app/utils.py
15
app/utils.py
@@ -131,3 +131,18 @@ def utc_now():
|
||||
def debug_not_production(msg):
|
||||
if os.getenv("NOTIFY_ENVIRONMENT") not in ["production"]:
|
||||
current_app.logger.info(msg)
|
||||
|
||||
|
||||
def emit_job_update_summary(job):
|
||||
from app import socketio
|
||||
|
||||
current_app.logger.info(f"Emitting summary for job {job.id}")
|
||||
socketio.emit(
|
||||
"job_updated",
|
||||
{
|
||||
"job_id": str(job.id),
|
||||
"job_status": job.job_status,
|
||||
"notification_count": job.notification_count,
|
||||
},
|
||||
room=f"job-{job.id}",
|
||||
)
|
||||
|
||||
120
package-lock.json
generated
Normal file
120
package-lock.json
generated
Normal file
@@ -0,0 +1,120 @@
|
||||
{
|
||||
"name": "notifications-api",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"socket.io-client": "^4.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-client": {
|
||||
"version": "6.6.3",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
|
||||
"integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-parser": "~5.2.1",
|
||||
"ws": "~8.17.1",
|
||||
"xmlhttprequest-ssl": "~2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-parser": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
|
||||
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/socket.io-client": {
|
||||
"version": "4.8.1",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
|
||||
"integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.2",
|
||||
"engine.io-client": "~6.6.1",
|
||||
"socket.io-parser": "~4.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-parser": {
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
|
||||
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xmlhttprequest-ssl": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
|
||||
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
5
package.json
Normal file
5
package.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"socket.io-client": "^4.8.1"
|
||||
}
|
||||
}
|
||||
3663
poetry.lock
generated
3663
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -18,30 +18,30 @@ celery = {version = "==5.4.0", extras = ["redis"]}
|
||||
certifi = ">=2022.12.7"
|
||||
cffi = "==1.17.1"
|
||||
charset-normalizer = "^3.4.1"
|
||||
click = "==8.1.7"
|
||||
click = "==8.1.8"
|
||||
click-datetime = "==0.4.0"
|
||||
click-didyoumean = "==0.3.1"
|
||||
click-plugins = "==1.1.1"
|
||||
click-repl = "==0.3.0"
|
||||
deprecated = "==1.2.14"
|
||||
eventlet = "==0.36.1"
|
||||
eventlet = "==0.39.1"
|
||||
expiringdict = "==1.2.2"
|
||||
flask = "~=3.0"
|
||||
flask-bcrypt = "==1.0.1"
|
||||
flask-marshmallow = "==1.2.1"
|
||||
flask-migrate = "==4.0.7"
|
||||
flask-migrate = "==4.1.0"
|
||||
flask-redis = "==0.4.0"
|
||||
flask-sqlalchemy = "==3.1.1"
|
||||
gunicorn = {version = "==23.0.0", extras = ["eventlet"]}
|
||||
iso8601 = "==2.1.0"
|
||||
jsonschema = {version = "==4.23.0", extras = ["format"]}
|
||||
lxml = "==5.3.1"
|
||||
marshmallow = "==3.26.1"
|
||||
marshmallow = "==4.0.0"
|
||||
marshmallow-sqlalchemy = "==1.0.0"
|
||||
newrelic = "*"
|
||||
notifications-python-client = "==10.0.0"
|
||||
oscrypto = { git = "https://github.com/wbond/oscrypto.git", rev = "1547f53" }
|
||||
packaging = "==24.2"
|
||||
packaging = "==25.0"
|
||||
poetry-dotenv-plugin = "==0.2.0"
|
||||
psycopg2-binary = "==2.9.9"
|
||||
pyjwt = "==2.10.1"
|
||||
@@ -52,11 +52,11 @@ faker = "^26.0.0"
|
||||
async-timeout = "^5.0.1"
|
||||
bleach = "^6.1.0"
|
||||
geojson = "^3.2.0"
|
||||
numpy = "^2.2.3"
|
||||
numpy = "^2.2.5"
|
||||
ordered-set = "^4.1.0"
|
||||
phonenumbers = "^8.13.42"
|
||||
python-json-logger = "^2.0.7"
|
||||
regex = "^2024.7.24"
|
||||
python-json-logger = "^3.3.0"
|
||||
regex = "^2024.11.6"
|
||||
shapely = "^2.0.5"
|
||||
smartypants = "^2.0.1"
|
||||
mistune = "^3.1.3"
|
||||
@@ -76,6 +76,7 @@ itsdangerous = "^2.2.0"
|
||||
jinja2 = "^3.1.6"
|
||||
redis = "^5.0.8"
|
||||
requests = "^2.32.3"
|
||||
flask-socketio = "^5.5.1"
|
||||
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
@@ -90,7 +91,7 @@ freezegun = "^1.5.1"
|
||||
honcho = "*"
|
||||
isort = "^5.13.2"
|
||||
jinja2-cli = {version = "==0.8.2", extras = ["yaml"]}
|
||||
moto = "==5.1.0"
|
||||
moto = "==5.1.4"
|
||||
pip-audit = "*"
|
||||
pre-commit = "^4.2.0"
|
||||
pytest = "^8.3.2"
|
||||
|
||||
@@ -2029,12 +2029,23 @@ def test_update_delivery_receipts(mocker):
|
||||
mock_update.where.return_value = mock_where
|
||||
mock_where.values.return_value = mock_values
|
||||
|
||||
mock_session.execute.return_value = None
|
||||
FakeJob = type(
|
||||
"FakeJob",
|
||||
(object,),
|
||||
{"id": "job-123", "notification_count": 5, "job_status": "delivered"},
|
||||
)
|
||||
|
||||
fake_result = MagicMock()
|
||||
fake_result.scalars.return_value.all.return_value = ["job-1", "job-2"]
|
||||
fake_result.scalars.return_value.one.return_value = FakeJob()
|
||||
|
||||
mock_session.execute.side_effect = lambda *args, **kwargs: fake_result
|
||||
with patch("app.dao.notifications_dao.update", return_value=mock_update):
|
||||
dao_update_delivery_receipts(receipts, delivered)
|
||||
mock_update.where.assert_called_once()
|
||||
mock_where.values.assert_called_once()
|
||||
mock_session.execute.assert_called_once_with(mock_values)
|
||||
mock_session.execute.assert_any_call(mock_values)
|
||||
assert mock_session.execute.call_count == 4
|
||||
mock_session.commit.assert_called_once()
|
||||
|
||||
args, kwargs = mock_where.values.call_args
|
||||
|
||||
Reference in New Issue
Block a user