Changes for sqlalchemy 2.0

This commit is contained in:
Aditi Anand
2024-04-24 16:27:20 -04:00
parent df2a590f1a
commit ad55eef5e9
14 changed files with 160 additions and 166 deletions
+7 -21
View File
@@ -779,45 +779,37 @@ def fetch_daily_volumes_for_platform(start_date, end_date):
FactBilling.local_date, FactBilling.local_date,
func.sum( func.sum(
case( case(
[
( (
FactBilling.notification_type == NotificationType.SMS, FactBilling.notification_type == NotificationType.SMS,
FactBilling.notifications_sent, FactBilling.notifications_sent,
) ),
],
else_=0, else_=0,
) )
).label("sms_totals"), ).label("sms_totals"),
func.sum( func.sum(
case( case(
[
( (
FactBilling.notification_type == NotificationType.SMS, FactBilling.notification_type == NotificationType.SMS,
FactBilling.billable_units, FactBilling.billable_units,
) ),
],
else_=0, else_=0,
) )
).label("sms_fragment_totals"), ).label("sms_fragment_totals"),
func.sum( func.sum(
case( case(
[
( (
FactBilling.notification_type == NotificationType.SMS, FactBilling.notification_type == NotificationType.SMS,
FactBilling.billable_units * FactBilling.rate_multiplier, FactBilling.billable_units * FactBilling.rate_multiplier,
) ),
],
else_=0, else_=0,
) )
).label("sms_fragments_times_multiplier"), ).label("sms_fragments_times_multiplier"),
func.sum( func.sum(
case( case(
[
( (
FactBilling.notification_type == NotificationType.EMAIL, FactBilling.notification_type == NotificationType.EMAIL,
FactBilling.notifications_sent, FactBilling.notifications_sent,
) ),
],
else_=0, else_=0,
) )
).label("email_totals"), ).label("email_totals"),
@@ -897,34 +889,28 @@ def fetch_volumes_by_service(start_date, end_date):
FactBilling.service_id, FactBilling.service_id,
func.sum( func.sum(
case( case(
[
( (
FactBilling.notification_type == NotificationType.SMS, FactBilling.notification_type == NotificationType.SMS,
FactBilling.notifications_sent, FactBilling.notifications_sent,
) ),
],
else_=0, else_=0,
) )
).label("sms_totals"), ).label("sms_totals"),
func.sum( func.sum(
case( case(
[
( (
FactBilling.notification_type == NotificationType.SMS, FactBilling.notification_type == NotificationType.SMS,
FactBilling.billable_units * FactBilling.rate_multiplier, FactBilling.billable_units * FactBilling.rate_multiplier,
) ),
],
else_=0, else_=0,
) )
).label("sms_fragments_times_multiplier"), ).label("sms_fragments_times_multiplier"),
func.sum( func.sum(
case( case(
[
( (
FactBilling.notification_type == NotificationType.EMAIL, FactBilling.notification_type == NotificationType.EMAIL,
FactBilling.notifications_sent, FactBilling.notifications_sent,
) ),
],
else_=0, else_=0,
) )
).label("email_totals"), ).label("email_totals"),
+8 -24
View File
@@ -459,25 +459,21 @@ def get_total_notifications_for_date_range(start_date, end_date):
FactNotificationStatus.local_date.label("local_date"), FactNotificationStatus.local_date.label("local_date"),
func.sum( func.sum(
case( case(
[
( (
FactNotificationStatus.notification_type FactNotificationStatus.notification_type
== NotificationType.EMAIL, == NotificationType.EMAIL,
FactNotificationStatus.notification_count, FactNotificationStatus.notification_count,
) ),
],
else_=0, else_=0,
) )
).label("emails"), ).label("emails"),
func.sum( func.sum(
case( case(
[
( (
FactNotificationStatus.notification_type FactNotificationStatus.notification_type
== NotificationType.SMS, == NotificationType.SMS,
FactNotificationStatus.notification_count, FactNotificationStatus.notification_count,
) ),
],
else_=0, else_=0,
) )
).label("sms"), ).label("sms"),
@@ -507,32 +503,27 @@ def fetch_monthly_notification_statuses_per_service(start_date, end_date):
FactNotificationStatus.notification_type, FactNotificationStatus.notification_type,
func.sum( func.sum(
case( case(
[
( (
FactNotificationStatus.notification_status.in_( FactNotificationStatus.notification_status.in_(
[NotificationStatus.SENDING, NotificationStatus.PENDING] [NotificationStatus.SENDING, NotificationStatus.PENDING]
), ),
FactNotificationStatus.notification_count, FactNotificationStatus.notification_count,
) ),
],
else_=0, else_=0,
) )
).label("count_sending"), ).label("count_sending"),
func.sum( func.sum(
case( case(
[
( (
FactNotificationStatus.notification_status FactNotificationStatus.notification_status
== NotificationStatus.DELIVERED, == NotificationStatus.DELIVERED,
FactNotificationStatus.notification_count, FactNotificationStatus.notification_count,
) ),
],
else_=0, else_=0,
) )
).label("count_delivered"), ).label("count_delivered"),
func.sum( func.sum(
case( case(
[
( (
FactNotificationStatus.notification_status.in_( FactNotificationStatus.notification_status.in_(
[ [
@@ -541,44 +532,37 @@ def fetch_monthly_notification_statuses_per_service(start_date, end_date):
] ]
), ),
FactNotificationStatus.notification_count, FactNotificationStatus.notification_count,
) ),
],
else_=0, else_=0,
) )
).label("count_technical_failure"), ).label("count_technical_failure"),
func.sum( func.sum(
case( case(
[
( (
FactNotificationStatus.notification_status FactNotificationStatus.notification_status
== NotificationStatus.TEMPORARY_FAILURE, == NotificationStatus.TEMPORARY_FAILURE,
FactNotificationStatus.notification_count, FactNotificationStatus.notification_count,
) ),
],
else_=0, else_=0,
) )
).label("count_temporary_failure"), ).label("count_temporary_failure"),
func.sum( func.sum(
case( case(
[
( (
FactNotificationStatus.notification_status FactNotificationStatus.notification_status
== NotificationStatus.PERMANENT_FAILURE, == NotificationStatus.PERMANENT_FAILURE,
FactNotificationStatus.notification_count, FactNotificationStatus.notification_count,
) ),
],
else_=0, else_=0,
) )
).label("count_permanent_failure"), ).label("count_permanent_failure"),
func.sum( func.sum(
case( case(
[
( (
FactNotificationStatus.notification_status FactNotificationStatus.notification_status
== NotificationStatus.SENT, == NotificationStatus.SENT,
FactNotificationStatus.notification_count, FactNotificationStatus.notification_count,
) ),
],
else_=0, else_=0,
) )
).label("count_sent"), ).label("count_sent"),
-2
View File
@@ -40,7 +40,6 @@ def get_processing_time_percentage_for_date_range(start_date, end_date):
FactProcessingTime.messages_total, FactProcessingTime.messages_total,
FactProcessingTime.messages_within_10_secs, FactProcessingTime.messages_within_10_secs,
case( case(
[
( (
FactProcessingTime.messages_total > 0, FactProcessingTime.messages_total > 0,
( (
@@ -52,7 +51,6 @@ def get_processing_time_percentage_for_date_range(start_date, end_date):
), ),
), ),
(FactProcessingTime.messages_total == 0, 100.0), (FactProcessingTime.messages_total == 0, 100.0),
]
).label("percentage"), ).label("percentage"),
) )
.filter( .filter(
+2 -4
View File
@@ -6,7 +6,7 @@ from sqlalchemy.orm import aliased
from app import db from app import db
from app.dao.dao_utils import autocommit from app.dao.dao_utils import autocommit
from app.enums import NotificationType from app.enums import NotificationType
from app.models import InboundSms, InboundSmsHistory, Service, ServiceDataRetention from app.models import InboundSms, InboundSmsHistory, ServiceDataRetention
from app.utils import midnight_n_days_ago from app.utils import midnight_n_days_ago
@@ -122,9 +122,7 @@ def delete_inbound_sms_older_than_retention():
) )
flexible_data_retention = ( flexible_data_retention = (
ServiceDataRetention.query.join( ServiceDataRetention.query.join(ServiceDataRetention.service)
ServiceDataRetention.service, Service.inbound_number
)
.filter(ServiceDataRetention.notification_type == NotificationType.SMS) .filter(ServiceDataRetention.notification_type == NotificationType.SMS)
.all() .all()
) )
+8 -8
View File
@@ -7,7 +7,7 @@ from notifications_utils.recipients import (
try_validate_and_format_phone_number, try_validate_and_format_phone_number,
validate_and_format_email_address, validate_and_format_email_address,
) )
from sqlalchemy import asc, desc, or_, select, union from sqlalchemy import asc, desc, or_, select, text, union
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
from sqlalchemy.orm.exc import NoResultFound from sqlalchemy.orm.exc import NoResultFound
from sqlalchemy.sql import functions from sqlalchemy.sql import functions
@@ -386,15 +386,15 @@ def insert_notification_history_delete_notifications(
"qry_limit": qry_limit, "qry_limit": qry_limit,
} }
db.session.execute(select_into_temp_table, input_params) db.session.execute(text(select_into_temp_table), input_params)
result = db.session.execute("select count(*) from NOTIFICATION_ARCHIVE").fetchone()[ result = db.session.execute(
0 text("select count(*) from NOTIFICATION_ARCHIVE")
] ).fetchone()[0]
db.session.execute(insert_query) db.session.execute(text(insert_query))
db.session.execute(delete_query) db.session.execute(text(delete_query))
return result return result
@@ -585,7 +585,7 @@ def dao_get_notifications_processing_time_stats(start_date, end_date):
) )
result = db.session.execute(stmt) result = db.session.execute(stmt)
return result.scalar_one() return result.one()
def dao_get_last_notification_added_for_job_id(job_id): def dao_get_last_notification_added_for_job_id(job_id):
+3 -1
View File
@@ -26,7 +26,9 @@ class PermissionDAO(DAOClass):
): ):
try: try:
if replace: if replace:
query = self.Meta.model.query.filter_by(user=user, service=service) query = self.Meta.model.query.filter(
self.Meta.model.user == user, self.Meta.model.service == service
)
query.delete() query.delete()
for p in permissions: for p in permissions:
p.user = user p.user = user
+6 -1
View File
@@ -4,7 +4,12 @@ from app.models import ServiceUser, User
def dao_get_service_user(user_id, service_id): def dao_get_service_user(user_id, service_id):
return ServiceUser.query.filter_by(user_id=user_id, service_id=service_id).one() # TODO: This has been changed to account for the test case failure
# that used this method but have any service user to return. Somehow, this
# started to throw an error with one() method in sqlalchemy 2.0 unlike 1.4
return ServiceUser.query.filter_by(
user_id=user_id, service_id=service_id
).one_or_none()
def dao_get_active_service_users(service_id): def dao_get_active_service_users(service_id):
+12 -14
View File
@@ -102,23 +102,17 @@ def dao_fetch_live_services_data():
Service.volume_sms.label("sms_volume_intent"), Service.volume_sms.label("sms_volume_intent"),
Service.volume_email.label("email_volume_intent"), Service.volume_email.label("email_volume_intent"),
case( case(
[
( (
this_year_ft_billing.c.notification_type this_year_ft_billing.c.notification_type == NotificationType.EMAIL,
== NotificationType.EMAIL,
func.sum(this_year_ft_billing.c.notifications_sent), func.sum(this_year_ft_billing.c.notifications_sent),
) ),
],
else_=0, else_=0,
).label("email_totals"), ).label("email_totals"),
case( case(
[
( (
this_year_ft_billing.c.notification_type this_year_ft_billing.c.notification_type == NotificationType.SMS,
== NotificationType.SMS,
func.sum(this_year_ft_billing.c.notifications_sent), func.sum(this_year_ft_billing.c.notifications_sent),
) ),
],
else_=0, else_=0,
).label("sms_totals"), ).label("sms_totals"),
AnnualBilling.free_sms_fragment_limit, AnnualBilling.free_sms_fragment_limit,
@@ -178,15 +172,15 @@ def dao_fetch_live_services_data():
def dao_fetch_service_by_id(service_id, only_active=False): def dao_fetch_service_by_id(service_id, only_active=False):
stmt = ( stmt = (
select(Service) select(Service)
.options(joinedload(Service.users))
.where(Service.id == service_id) .where(Service.id == service_id)
.options(joinedload(Service.users))
) )
if only_active: if only_active:
stmt = stmt.where(Service.active) stmt = stmt.where(Service.active)
result = db.session.execute(stmt) result = db.session.execute(stmt)
return result.unique().scalars().first() return result.unique().scalars().one()
def dao_fetch_service_by_inbound_number(number): def dao_fetch_service_by_inbound_number(number):
@@ -241,8 +235,7 @@ def dao_archive_service(service_id):
# to ensure that db.session still contains the models when it comes to creating history objects # to ensure that db.session still contains the models when it comes to creating history objects
service = ( service = (
Service.query.options( Service.query.options(
joinedload(Service.templates), joinedload(Service.templates).subqueryload(Template.template_redacted),
joinedload(Service.templates.template_redacted),
joinedload(Service.api_keys), joinedload(Service.api_keys),
) )
.filter(Service.id == service_id) .filter(Service.id == service_id)
@@ -329,6 +322,11 @@ def dao_add_user_to_service(service, user, permissions=None, folder_permissions=
try: try:
from app.dao.permissions_dao import permission_dao from app.dao.permissions_dao import permission_dao
# As per SQLAlchemy 2.0, we need to add the user to the service only if the user is not already added;
# otherwise it throws sqlalchemy.exc.IntegrityError:
# (psycopg2.errors.UniqueViolation) duplicate key value violates unique constraint "uix_user_to_service"
service_user = dao_get_service_user(user.id, service.id)
if service_user is None:
service.users.append(user) service.users.append(user)
permission_dao.set_user_service_permission( permission_dao.set_user_service_permission(
user, service, permissions, _commit=False user, service, permissions, _commit=False
+20 -8
View File
@@ -2,6 +2,8 @@ import uuid
from datetime import datetime, timedelta from datetime import datetime, timedelta
from secrets import randbelow from secrets import randbelow
import sqlalchemy
from flask import current_app
from sqlalchemy import func from sqlalchemy import func
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
@@ -11,7 +13,7 @@ from app.dao.permissions_dao import permission_dao
from app.dao.service_user_dao import dao_get_service_users_by_user_id from app.dao.service_user_dao import dao_get_service_users_by_user_id
from app.enums import AuthType, PermissionType from app.enums import AuthType, PermissionType
from app.errors import InvalidRequest from app.errors import InvalidRequest
from app.models import User, VerifyCode from app.models import Organization, Service, User, VerifyCode
from app.utils import escape_special_characters, get_archived_db_column_value from app.utils import escape_special_characters, get_archived_db_column_value
@@ -39,10 +41,21 @@ def get_login_gov_user(login_uuid, email_address):
user = User.query.filter_by(login_uuid=login_uuid).first() user = User.query.filter_by(login_uuid=login_uuid).first()
if user: if user:
if user.email_address != email_address: if user.email_address != email_address:
try:
save_user_attribute(user, {"email_address": email_address}) save_user_attribute(user, {"email_address": email_address})
except sqlalchemy.exc.IntegrityError as ie:
# We are trying to change the email address as a courtesy,
# based on the assumption that the user has somehow changed their
# address in login.gov.
# But if we cannot change the email address, at least we don't
# want to fail here, otherwise the user will be locked out.
current_app.logger.error(ie)
db.session.rollback()
return user return user
# Remove this 1 July 2025, all users should have login.gov uuids by now # Remove this 1 July 2025, all users should have login.gov uuids by now
user = User.query.filter_by(email_address=email_address).first() user = User.query.filter(User.email_address.ilike(email_address)).first()
if user: if user:
save_user_attribute(user, {"login_uuid": login_uuid}) save_user_attribute(user, {"login_uuid": login_uuid})
return user return user
@@ -172,15 +185,14 @@ def update_user_password(user, password):
def get_user_and_accounts(user_id): def get_user_and_accounts(user_id):
# TODO: With sqlalchemy 2.0 change as below because of the breaking change
# at User.organizations.services, we need to verify that the below subqueryload
# that we have put is functionally doing the same thing as before
return ( return (
User.query.filter(User.id == user_id) User.query.filter(User.id == user_id)
.options( .options(
# eagerly load the user's services and organizations, and also the service's org and vice versa joinedload(User.services).joinedload(Service.organization),
# (so we can see if the user knows about it) joinedload(User.organizations).subqueryload(Organization.services),
joinedload(User.services),
joinedload(User.organizations),
joinedload(User.organizations.services),
joinedload(User.services.organization),
) )
.one() .one()
) )
+2 -1
View File
@@ -1,4 +1,5 @@
from flask import Blueprint, jsonify, request from flask import Blueprint, jsonify, request
from sqlalchemy import text
from app import db, version from app import db, version
from app.dao.organization_dao import dao_count_organizations_with_live_services from app.dao.organization_dao import dao_count_organizations_with_live_services
@@ -37,5 +38,5 @@ def live_service_and_organization_counts():
def get_db_version(): def get_db_version():
query = "SELECT version_num FROM alembic_version" query = "SELECT version_num FROM alembic_version"
full_name = db.session.execute(query).fetchone()[0] full_name = db.session.execute(text(query)).fetchone()[0]
return full_name return full_name
+7
View File
@@ -6,6 +6,7 @@ from urllib.parse import urlencode
from flask import Blueprint, abort, current_app, jsonify, request from flask import Blueprint, abort, current_app, jsonify, request
from notifications_utils.recipients import is_us_phone_number, use_numeric_sender from notifications_utils.recipients import is_us_phone_number, use_numeric_sender
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm.exc import NoResultFound
from app import redis_store from app import redis_store
from app.config import QueueNames from app.config import QueueNames
@@ -533,6 +534,12 @@ def set_permissions(user_id, service_id):
# TODO fix security hole, how do we verify that the user # TODO fix security hole, how do we verify that the user
# who is making this request has permission to make the request. # who is making this request has permission to make the request.
service_user = dao_get_service_user(user_id, service_id) service_user = dao_get_service_user(user_id, service_id)
# TODO: Below exception is raised to account for the test case failure that got handled
# on its own in 1.4 when dao_get_service_user() returned an excpetion in case no result was found
if service_user is None:
raise NoResultFound(
"No ServiceUser found with the provided user_id and service_id"
)
user = get_user_by_id(user_id) user = get_user_by_id(user_id)
service = dao_fetch_service_by_id(service_id=service_id) service = dao_fetch_service_by_id(service_id=service_id)
+10 -7
View File
@@ -838,7 +838,7 @@ def test_fetch_sms_billing_for_all_services_with_remainder(notify_db_session):
}, },
] ]
assert [dict(result) for result in results] == expected_results assert [result._asdict() for result in results] == expected_results
def test_fetch_sms_billing_for_all_services_without_an_organization_appears( def test_fetch_sms_billing_for_all_services_without_an_organization_appears(
@@ -893,7 +893,7 @@ def test_fetch_sms_billing_for_all_services_without_an_organization_appears(
}, },
] ]
assert [dict(result) for result in results] == expected_results assert [result._asdict() for result in results] == expected_results
@freeze_time("2019-06-01 13:30") @freeze_time("2019-06-01 13:30")
@@ -1228,8 +1228,8 @@ def test_query_organization_sms_usage_for_year_handles_multiple_services(
result = query_organization_sms_usage_for_year(org.id, 2022).all() result = query_organization_sms_usage_for_year(org.id, 2022).all()
service_1_rows = [row for row in result if row.service_id == service_1.id] service_1_rows = [row._asdict() for row in result if row.service_id == service_1.id]
service_2_rows = [row for row in result if row.service_id == service_2.id] service_2_rows = [row._asdict() for row in result if row.service_id == service_2.id]
assert len(service_1_rows) == 2 assert len(service_1_rows) == 2
assert len(service_2_rows) == 2 assert len(service_2_rows) == 2
@@ -1256,10 +1256,10 @@ def test_query_organization_sms_usage_for_year_handles_multiple_services(
# assert total costs are accurate # assert total costs are accurate
assert ( assert (
float(sum(row.cost for row in service_1_rows)) == 1 float(sum(row["cost"] for row in service_1_rows)) == 1
) # rows with 2 and 4, allowance of 5 ) # rows with 2 and 4, allowance of 5
assert ( assert (
float(sum(row.cost for row in service_2_rows)) == 14 float(sum(row["cost"] for row in service_2_rows)) == 14
) # rows with 8 and 16, allowance of 10 ) # rows with 8 and 16, allowance of 10
@@ -1296,7 +1296,10 @@ def test_query_organization_sms_usage_for_year_handles_multiple_rates(
financial_year_start=current_year, financial_year_start=current_year,
) )
result = query_organization_sms_usage_for_year(org.id, 2022).all() result = [
row._asdict()
for row in query_organization_sms_usage_for_year(org.id, 2022).all()
]
# al lthe free allowance is used on the first day # al lthe free allowance is used on the first day
assert result[0]["local_date"] == date(2022, 4, 29) assert result[0]["local_date"] == date(2022, 4, 29)
@@ -237,7 +237,7 @@ def test_update_template_folder_users(admin_request, sample_service):
[ [
({}, "name is a required property"), ({}, "name is a required property"),
({"name": None}, "name None is not of type string"), ({"name": None}, "name None is not of type string"),
({"name": ""}, "name is too short"), ({"name": ""}, "name should be non-empty"),
], ],
) )
def test_update_template_folder_fails_if_missing_name( def test_update_template_folder_fails_if_missing_name(
+2 -2
View File
@@ -89,7 +89,7 @@ def test_create_webauthn_credential_returns_201(admin_request, sample_user):
"name None is not of type string", "name None is not of type string",
), ),
# name is empty # name is empty
({"name": "", "credential_data": "ABC123"}, "name is too short"), ({"name": "", "credential_data": "ABC123"}, "name should be non-empty"),
], ],
) )
def test_create_webauthn_credential_errors_if_schema_violation( def test_create_webauthn_credential_errors_if_schema_violation(
@@ -131,7 +131,7 @@ def test_update_webauthn_credential_returns_200(admin_request, sample_user):
# name is null # name is null
({"name": None}, "name None is not of type string"), ({"name": None}, "name None is not of type string"),
# name is empty # name is empty
({"name": ""}, "name is too short"), ({"name": ""}, "name should be non-empty"),
], ],
) )
def test_update_webauthn_credential_errors_if_schema_violation( def test_update_webauthn_credential_errors_if_schema_violation(