Skip to content

Commit 878e5cf

Browse files
aweilandrcmerlofarmerpaulmbantingdependabot[bot]
authored
Release/2025.07.1 [main] (#1917)
* feat: Enhances OneUp Health integration for document retrieval (M2-8883) (#1879) * Enhances OneUp Health integration for document retrieval Improves the OneUp Health integration by adding functionality to download and store patient documents associated with EHR data. This change introduces the ability to fetch documents referenced in DocumentReference resources, store them in a zip file, and upload the zip to the EHR storage. It also introduces generic file uploading and listing capabilities in the EHR storage, along with helper methods. * Adds metadata to EHR answers Adds metadata to the EHR answers table to store information about the uploaded zip files, including their names and sizes. This allows for better tracking and management of the EHR data stored in the system. * Adds assertion to task ingest user data Adds an assertion to check that the result of the `ingest_user_data` function is not None. This ensures that the task completes successfully and returns a valid result. * Update src/apps/integrations/oneup_health/service/oneup_health.py Co-authored-by: Farmer Paul <paul.hh@metalab.com> * Refactors EHR metadata handling Updates EHR metadata to use a dedicated data model. This change introduces `EHRFileMetadata` and `EHRFileTypeEnum` to provide more structured and type-safe handling of EHR file metadata, replacing the previous use of dictionaries. This improves code clarity and maintainability. * Adds provider name to EHR document filename Ensures that the EHR document filename includes the healthcare provider's name (or ID if name unavailable) to improve identification and organization. * Adds buckets for answers and operations Extends MinIO bucket creation to include buckets for answers and operations, in addition to the existing media bucket. This allows for a more organized storage structure within MinIO and improves separation of concerns. * Adds a TODO item to optimize zip file creation. Adds a TODO item to address potential memory issues when creating zip files, especially when dealing with large documents or numerous files. * Refactors EHR file upload process Streamlines the EHR file upload process by moving the base path generation logic into the `EHRStorage.upload_file` method. This change improves code maintainability and reduces redundancy. --------- Co-authored-by: Farmer Paul <paul.hh@metalab.com> * fix: Refactors ehr zip download to use storage path (M2-9443) (#1893) Simplifies the ehr zip download process by directly using the storage path. This eliminates redundant path construction and improves code readability. * chore: Added worker configuration to uvicorn startup (#1896) * fix: Optimizes schedule event retrieval for performance (#1897) * Optimizes event retrieval for schedule app Improves performance by fetching events and associated notifications/reminders in bulk using applet IDs. This reduces database queries and speeds up schedule loading. * Optimizes event data retrieval Reduces the number of database queries required to fetch event notifications and reminders by retrieving them in bulk. This change improves performance when fetching schedule events, especially when retrieving events for a large number of users. * fix: Temp fix for workers (#1902) * Fix: Enhances EHR exports to filter only allowed subjects answers (M2-9434) (#1894) * Adjusts retry delay for task processing errors Modifies the retry mechanism to use a linear delay after the first failed attempt. This avoids excessive wait times when processing tasks that encounter errors. * Filters EHR answers by allowed subjects Ensures that exported EHR answers are filtered based on the subjects the user is authorized to access. If a list of allowed subjects is available, it's added as a filter when querying EHR answers. * Refines subject ID filtering for EHR answers Ensures that the target subject IDs for EHR answer exports are filtered to only include IDs that are allowed for the user. This prevents unauthorized access to data associated with subjects outside the user's permitted scope. * Adds reviewer role to EHR data endpoint test Extends the EHR data endpoint test to include scenarios for users with the reviewer role. This ensures that reviewers can properly access and download EHR data relevant to their responsibilities. * Disabling APM for feature environments * fix: Adds index to activity_items.activity_id (#1901) Improves query performance by adding an index to the activity_id column in the activity_items table. Optimizes database lookups and enhances the efficiency of related queries. * [pip] Dependabot: Bump ddtrace from 2.21.8 to 3.9.4 (#1908) Bumps [ddtrace](https://github.com/DataDog/dd-trace-py) from 2.21.8 to 3.9.4. - [Release notes](https://github.com/DataDog/dd-trace-py/releases) - [Changelog](https://github.com/DataDog/dd-trace-py/blob/main/CHANGELOG.md) - [Commits](DataDog/dd-trace-py@v2.21.8...v3.9.4) --- updated-dependencies: - dependency-name: ddtrace dependency-version: 3.9.4 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * [pip] Dependabot: Bump botocore from 1.38.28 to 1.38.46 (#1907) --- updated-dependencies: - dependency-name: botocore dependency-version: 1.38.46 dependency-type: indirect update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * [pip] Dependabot: Bump google-api-python-client from 2.170.0 to 2.174.0 (#1906) --- updated-dependencies: - dependency-name: google-api-python-client dependency-version: 2.174.0 dependency-type: indirect update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Use the real exception message (#1904) * chore: Removing workers (#1909) * chore: Enable Database connection pooling (M2-9464) (#1898) * chore: Enabled connection pooling * fixed timeout and docs * cq * feat: Add logging around /answers 422 errors (M2-9475) (#1905) * feat: Add logging around /answers 422 errors (M2-9475) * Remove unused methods * M2 9399 transfer ownership (#1888) * Update errors.py Adding the expected message error for the transfer ownvership test * Update tests.py Adding the new migrated test from TAF: - Adding the fixture applet_one_bob_coordinator_reviewer - Adding the tests test_manager_can_not_transfer_ownership test_respondent_can_not_transfer_ownershipto test_reviewer_can_not_transfer_ownership test_editor_can_not_transfer_ownership test_coordinator_can_not_transfer_ownership * Update errors.py deleting the error exception to use TransferOwnershipAccessDenied * Update tests.py deleting in all the test key = uuid.uuid4() mocker.patch("uuid.uuid4", return_value=key) updating the exception: AccessDenied.message to TransferOwnershipAccessDenied.message * M2 9368 api update applets in workspace (#1882) * Update test_workspaces.py adding the test test_manager_can_update_applet, pending fix * Update test_workspaces.py moving the test to the correct place, test_applet * Update test_applet.py adding the test test_manager_can_update_applet to the test_applet file, adding the missing values * Update test_applet.py adding 2 new tests: 1. test_coordinator_can_not_update_applet 2. test_editor_can_update_applet Updating the response for a http status * Update PR update the pr after run make cqf command * fix: Disabling connection pooling (#1916) --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Rodrigo Colao Merlo <rodrigo.merlo@metalab.com> Co-authored-by: Farmer Paul <paul.hh@metalab.com> Co-authored-by: Marty <mbanting@yahoo.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: SebasYanik <96058252+SebasYanik@users.noreply.github.com> Co-authored-by: egodoy-metalab <elder.gordy@metalab.com>
1 parent 9f6ca3a commit 878e5cf

13 files changed

Lines changed: 318 additions & 127 deletions

File tree

Pipfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ azure-storage-blob = "==12.25.*"
1414
bcrypt = "==4.3.0"
1515
boto3 = "==1.38.28"
1616
bytecode = "==0.16.*"
17-
ddtrace = "==2.21.*"
17+
ddtrace = "==3.9.*"
1818
fastapi = "==0.115.*"
1919
fastapi-mail = "==1.2.9"
2020
firebase-admin = "==6.8.*"

Pipfile.lock

Lines changed: 101 additions & 106 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ pipenv --python /opt/homebrew/bin/python3.13
8484
| DATABASE\_\_USER | postgres | User name for Postgresql Database user |
8585
| DATABASE\_\_PASSWORD | postgres | Password for Postgresql Database user |
8686
| DATABASE\_\_DB | mindlogger_backend | Database name |
87+
| DATABASE\_\_POOL\_SIZE | 5 | Database connection pool size |
88+
| DATABASE\_\_POOL\_OVERFLOW\_SIZE | 10 | Allowed overflow size of the connection pool |
89+
| DATABASE\_\_POOL\_TIMEOUT | 30 | The number of seconds to wait for a connection from the pool to become available |
8790
| CORS\_\_ALLOW\_ORIGINS | `*` | Represents the list of allowed origins. Set the `Access-Control-Allow-Origin` header. Example: `https://dev.com,http://localohst:8000` |
8891
| CORS\_\_ALLOW\_ORIGINS\_REGEX | - | Regex pattern of allowed origins. |
8992
| CORS\_\_ALLOW\_CREDENTIALS | true | Set the `Access-Control-Allow-Credentials` header |

compose/fastapi/start-backend-datadog

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,11 @@ set -o pipefail
1313
set -o nounset
1414

1515

16-
#NUM_WORKERS=1
17-
#if command -v nproc >/dev/null 2>&1; then
18-
# NUM_WORKERS=$(nproc)
19-
# echo "Starting app with $NUM_WORKERS workers..."
20-
#else
21-
# echo "No nproc, defaulting to 1 worker..."
22-
#fi
23-
24-
NUM_WORKERS=2
25-
2616
# https://www.uvicorn.org/settings/
2717
export UVICORN_HOST="0.0.0.0"
2818
export UVICORN_PORT=80
2919

3020
uvicorn main:app \
3121
--host ${UVICORN_HOST} --port ${UVICORN_PORT} \
3222
--reload --proxy-headers \
33-
--log-config uvicorn_disable_logging.json --workers ${NUM_WORKERS}
23+
--log-config uvicorn_disable_logging.json

copilot/mindlogger-backend/manifest.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ sidecars:
181181
DD_LOGS_ENABLED: true
182182
DD_LOGS_CONFIG_CONTAINER_COLLECT_ALL: false
183183
DD_CONTAINER_EXCLUDE: "name:datadog name:datadog-agent name:fluent-bit name:firelens_log_router name:ecs-agent name:aws-fargate-supervisor"
184-
DD_APM_ENABLED: true
184+
DD_APM_ENABLED: false
185185
secrets:
186186
DD_API_KEY:
187187
secretsmanager: "cmiml-feature-${COPILOT_ENVIRONMENT_NAME}:DD_API_KEY::"

src/apps/activities/db/schemas/activity_item.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class _BaseActivityItemSchema:
2222
class ActivityItemSchema(_BaseActivityItemSchema, Base):
2323
__tablename__ = "activity_items"
2424

25-
activity_id = Column(ForeignKey("activities.id", ondelete="CASCADE"), nullable=False)
25+
activity_id = Column(ForeignKey("activities.id", ondelete="CASCADE"), nullable=False, index=True)
2626

2727

2828
class ActivityItemHistorySchema(_BaseActivityItemSchema, Base):

src/apps/answers/api.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,17 @@ async def create_answer(
122122
service = AnswerService(session, user.id, answer_session)
123123
if tz_offset is not None and schema.answer.tz_offset is None:
124124
schema.answer.tz_offset = tz_offset // 60 # value in minutes
125-
async with atomic(answer_session):
126-
answer = await service.create_answer(schema, device.device_id if device else None)
125+
126+
try:
127+
async with atomic(answer_session):
128+
answer = await service.create_answer(schema, device.device_id if device else None)
129+
except Exception as e:
130+
logger.error(
131+
f"Answer creation failed: applet_id={schema.applet_id}, user_id={user.id}, \
132+
activity_id={schema.activity_id}, error={type(e).__name__}: {e}"
133+
)
134+
raise
135+
127136
await service.create_report_from_answer(answer)
128137
if schema.allowed_ehr_ingest:
129138
await service.trigger_ehr_ingestion(

src/apps/applets/tests/test_applet.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ class TestApplet:
5757
history_changes_url = f"{applet_detail_url}/versions/{{version}}/changes"
5858
applet_base_info_url = f"{applet_detail_url}/base_info"
5959
access_link_url = f"{applet_detail_url}/access_link"
60+
applets_updates_url = f"{applet_list_url}/{{applet_id}}"
6061

6162
public_applet_detail_url = "/public/applets/{key}"
6263
public_applet_base_info_url = f"{public_applet_detail_url}/base_info"
@@ -1298,3 +1299,70 @@ async def test_applet_retrieve_meta(
12981299
response = await client.get(self.applet_detail_url.format(pk=applet_with_reviewable_activity.id))
12991300
assert response.status_code == http.HTTPStatus.OK
13001301
assert response.json()["appletMeta"]["hasAssessment"]
1302+
1303+
async def test_manager_can_update_applet(
1304+
self, client: TestClient, lucy: User, applet_one_lucy_manager: AppletFull, encryption: Encryption
1305+
):
1306+
client.login(lucy)
1307+
edit_data = dict(
1308+
{
1309+
"display_name": "Updated Name",
1310+
"encryption": encryption.dict(),
1311+
"activities": [],
1312+
"activityFlows": [],
1313+
}
1314+
)
1315+
1316+
response = await client.put(
1317+
self.applets_updates_url.format(
1318+
applet_id=applet_one_lucy_manager.id,
1319+
),
1320+
data=edit_data,
1321+
)
1322+
assert response.status_code == http.HTTPStatus.OK, response.json()
1323+
data = response.json()["result"]
1324+
assert data["displayName"] == "Updated Name"
1325+
1326+
async def test_coordinator_can_not_update_applet(
1327+
self, client: TestClient, lucy: User, applet_one_lucy_coordinator: AppletFull, encryption: Encryption
1328+
):
1329+
client.login(lucy)
1330+
edit_data = dict(
1331+
{
1332+
"display_name": "Updated Name",
1333+
"encryption": encryption.dict(),
1334+
"activities": [],
1335+
"activityFlows": [],
1336+
}
1337+
)
1338+
1339+
response = await client.put(
1340+
self.applets_updates_url.format(
1341+
applet_id=applet_one_lucy_coordinator.id,
1342+
),
1343+
data=edit_data,
1344+
)
1345+
assert response.status_code == http.HTTPStatus.FORBIDDEN, response.json()
1346+
1347+
async def test_editor_can_update_applet(
1348+
self, client: TestClient, lucy: User, applet_one_lucy_editor: AppletFull, encryption: Encryption
1349+
):
1350+
client.login(lucy)
1351+
edit_data = dict(
1352+
{
1353+
"display_name": "Updated Name",
1354+
"encryption": encryption.dict(),
1355+
"activities": [],
1356+
"activityFlows": [],
1357+
}
1358+
)
1359+
1360+
response = await client.put(
1361+
self.applets_updates_url.format(
1362+
applet_id=applet_one_lucy_editor.id,
1363+
),
1364+
data=edit_data,
1365+
)
1366+
assert response.status_code == http.HTTPStatus.OK, response.json()
1367+
data = response.json()["result"]
1368+
assert data["displayName"] == "Updated Name"

src/apps/transfer_ownership/tests.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from apps.transfer_ownership.crud import TransferCRUD
2222
from apps.transfer_ownership.errors import TransferEmailError
2323
from apps.users.domain import User
24+
from apps.workspaces.errors import TransferOwnershipAccessDenied
2425
from apps.workspaces.service.user_applet_access import UserAppletAccessService
2526

2627

@@ -55,6 +56,13 @@ async def applet_one_lucy_respondent(session: AsyncSession, applet_one: AppletFu
5556
return applet_one
5657

5758

59+
@pytest.fixture
60+
async def applet_one_bob_coordinator_reviewer(session: AsyncSession, applet_one: AppletFull, tom, bob) -> AppletFull:
61+
await UserAppletAccessService(session, tom.id, applet_one.id).add_role(bob.id, Role.COORDINATOR)
62+
await UserAppletAccessService(session, tom.id, applet_one.id).add_role(bob.id, Role.REVIEWER)
63+
return applet_one
64+
65+
5866
class TestTransfer(BaseTest):
5967
fixtures = [
6068
"transfer_ownership/fixtures/transfers.json",
@@ -536,3 +544,68 @@ async def test_decline_transfer__applet_not_in_transfer(
536544
result = resp.json()["result"]
537545
assert len(result) == 1
538546
assert result[0]["message"] == PermissionsError.message
547+
548+
async def test_manager_can_not_transfer_ownership(
549+
self,
550+
client: TestClient,
551+
applet_one: AppletFull,
552+
lucy: User,
553+
tom: User,
554+
):
555+
client.login(lucy)
556+
data = {"email": tom.email_encrypted}
557+
resp = await client.post(self.transfer_url.format(applet_id=applet_one.id), data=data)
558+
assert resp.status_code == http.HTTPStatus.FORBIDDEN
559+
assert resp.json()["result"][0]["message"] == TransferOwnershipAccessDenied.message
560+
561+
async def test_respondent_can_not_transfer_ownershipto(
562+
self,
563+
client: TestClient,
564+
applet_one_lucy_respondent: AppletFull,
565+
lucy: User,
566+
tom: User,
567+
):
568+
client.login(lucy)
569+
data = {"email": tom.email_encrypted}
570+
resp = await client.post(self.transfer_url.format(applet_id=applet_one_lucy_respondent.id), data=data)
571+
assert resp.status_code == http.HTTPStatus.FORBIDDEN
572+
assert resp.json()["result"][0]["message"] == TransferOwnershipAccessDenied.message
573+
574+
async def test_reviewer_can_not_transfer_ownership(
575+
self,
576+
client: TestClient,
577+
applet_one_bob_coordinator_reviewer: AppletFull,
578+
lucy: User,
579+
tom: User,
580+
):
581+
client.login(lucy)
582+
data = {"email": tom.email_encrypted}
583+
resp = await client.post(self.transfer_url.format(applet_id=applet_one_bob_coordinator_reviewer.id), data=data)
584+
assert resp.status_code == http.HTTPStatus.FORBIDDEN
585+
assert resp.json()["result"][0]["message"] == TransferOwnershipAccessDenied.message
586+
587+
async def test_editor_can_not_transfer_ownership(
588+
self,
589+
client: TestClient,
590+
applet_one_lucy_editor: AppletFull,
591+
lucy: User,
592+
tom: User,
593+
):
594+
client.login(lucy)
595+
data = {"email": tom.email_encrypted}
596+
resp = await client.post(self.transfer_url.format(applet_id=applet_one_lucy_editor.id), data=data)
597+
assert resp.status_code == http.HTTPStatus.FORBIDDEN
598+
assert resp.json()["result"][0]["message"] == TransferOwnershipAccessDenied.message
599+
600+
async def test_coordinator_can_not_transfer_ownership(
601+
self,
602+
client: TestClient,
603+
applet_one_bob_coordinator_reviewer: AppletFull,
604+
lucy: User,
605+
tom: User,
606+
):
607+
client.login(lucy)
608+
data = {"email": tom.email_encrypted}
609+
resp = await client.post(self.transfer_url.format(applet_id=applet_one_bob_coordinator_reviewer.id), data=data)
610+
assert resp.status_code == http.HTTPStatus.FORBIDDEN
611+
assert resp.json()["result"][0]["message"] == TransferOwnershipAccessDenied.message

src/config/database.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ class DatabaseSettings(BaseModel):
77
password: str = "postgres"
88
user: str = "postgres"
99
db: str = "mindlogger_backend"
10-
pool_size: int = 2
10+
pool_size: int = 5
11+
pool_overflow_size: int = 10
12+
pool_timeout: int = 30
1113

1214
@property
1315
def url(self) -> str:

0 commit comments

Comments
 (0)