Skip to content

Api reference

components.health_programs.public.business_logic

actions

delete_health_program_data

delete_all_health_program_data_for_user
delete_all_health_program_data_for_user(
    app_id, app_user_id
)

Delete all health program progress and override records for a user.

Returns the total number of deleted records.

Source code in components/health_programs/public/business_logic/actions/delete_health_program_data.py
def delete_all_health_program_data_for_user(app_id: str, app_user_id: str) -> int:
    """Delete all health program progress and override records for a user.

    Returns the total number of deleted records.
    """
    nb_progress = current_session.scalar(
        select(func.count())
        .select_from(HealthProgramProgress)
        .where(
            HealthProgramProgress.app_id == app_id,
            HealthProgramProgress.app_user_id == app_user_id,
        )
    )

    current_session.execute(
        delete(HealthProgramProgress).where(
            HealthProgramProgress.app_id == app_id,
            HealthProgramProgress.app_user_id == app_user_id,
        )
    )

    nb_overrides = current_session.scalar(
        select(func.count())
        .select_from(HealthProgramOverride)
        .where(
            HealthProgramOverride.app_id == app_id,
            HealthProgramOverride.app_user_id == app_user_id,
        )
    )

    current_session.execute(
        delete(HealthProgramOverride).where(
            HealthProgramOverride.app_id == app_id,
            HealthProgramOverride.app_user_id == app_user_id,
        )
    )

    return (nb_progress or 0) + (nb_overrides or 0)

health_program_override

update_health_program_override_for_user
update_health_program_override_for_user(
    user,
    program_slug,
    overridden_segment_slug,
    custom_program_id,
    dedicated_time,
    commit=True,
)

Update or create the health program override for a user given some parameters.

Source code in components/health_programs/public/business_logic/actions/health_program_override.py
def update_health_program_override_for_user(
    user: FeatureUser,
    program_slug: str,
    overridden_segment_slug: Optional[str],
    custom_program_id: Optional[str],
    dedicated_time: Optional[str],
    commit: bool = True,
) -> None:
    """Update or create the health program override for a user given some parameters."""
    if overridden_segment_slug is not None and custom_program_id is not None:
        raise ValueError("You can't set both segment_slug and custom_program_id")

    program_override = (
        current_session.query(HealthProgramOverride)  # noqa: ALN085
        .filter(
            HealthProgramOverride.app_id == user.app_id,
            HealthProgramOverride.app_user_id == user.app_user_id,
            HealthProgramOverride.program_slug == program_slug,
        )
        .one_or_none()
    )

    if program_override is None:
        program_override = HealthProgramOverride(
            app_id=user.app_id,
            app_user_id=user.app_user_id,
            program_slug=program_slug,
        )
        current_session.add(program_override)

    update_health_program_override(
        program_override,
        overridden_segment_slug,
        custom_program_id,
        dedicated_time,
        commit,
    )

health_program_progress

mark_health_program_as_ready
mark_health_program_as_ready(
    user,
    program_slug,
    coach_name,
    coach_picture_url,
    has_booked_appointment,
    commit=True,
)
Source code in components/health_programs/public/business_logic/actions/health_program_progress.py
def mark_health_program_as_ready(  # noqa: D103
    user: FeatureUser,
    program_slug: str,
    coach_name: Optional[str],
    coach_picture_url: Optional[str],
    has_booked_appointment: bool,
    commit: bool = True,
) -> None:
    tracking_client.track(
        user.app_user_id,
        "health_program_ready",
        {
            "programSlug": program_slug,
            "coachName": coach_name,
            "coachPictureUrl": coach_picture_url,
            "hasBookedAppointment": has_booked_appointment,
        },
    )

    update_health_program_status(
        user=user,
        program_slug=program_slug,
        from_statuses=[
            HealthProgramStatus.program_in_preparation,
        ],
        to_status=HealthProgramStatus.program_ready,
        commit=commit,
    )

queries

health_program_override

get_all_health_program_overrides
get_all_health_program_overrides(user)
Source code in components/health_programs/public/business_logic/queries/health_program_override.py
def get_all_health_program_overrides(  # noqa: D103
    user: FeatureUser,
) -> list[HealthProgramOverrideData]:
    all_program_overrides = (
        current_session.query(HealthProgramOverride)  # noqa: ALN085
        .filter(
            HealthProgramOverride.app_id == user.app_id,
            HealthProgramOverride.app_user_id == user.app_user_id,
        )
        .all()
    )

    return [
        HealthProgramOverrideData.from_health_program_override_model(override)
        for override in all_program_overrides
    ]
get_health_program_override
get_health_program_override(user, program_slug)
Source code in components/health_programs/public/business_logic/queries/health_program_override.py
def get_health_program_override(  # noqa: D103
    user: FeatureUser, program_slug: str
) -> Optional[HealthProgramOverride]:
    return (
        current_session.query(HealthProgramOverride)  # noqa: ALN085
        .filter(
            HealthProgramOverride.app_id == user.app_id,
            HealthProgramOverride.app_user_id == user.app_user_id,
            HealthProgramOverride.program_slug == program_slug,
        )
        .one_or_none()
    )

health_program_progress

get_all_user_health_program_data
get_all_user_health_program_data(member_feature_user)

Get all health program progresses for a user.

Source code in components/health_programs/public/business_logic/queries/health_program_progress.py
def get_all_user_health_program_data(
    member_feature_user: FeatureUser,
) -> list[HealthProgramCompleteData]:
    """
    Get all health program progresses for a user.
    """
    # legacy code, we should use the queries that separates progress and override
    query = current_session.query(  # noqa: ALN085
        HealthProgramProgress, HealthProgramOverride
    ).outerjoin(
        HealthProgramOverride,
        (HealthProgramProgress.program_slug == HealthProgramOverride.program_slug)
        & (HealthProgramProgress.feature_user == HealthProgramOverride.feature_user),
    )

    results = query.filter(
        HealthProgramProgress.feature_user == member_feature_user,
    ).all()  # This will be a list of tuples (HealthProgramProgress, HealthProgramOverride or None)

    return [
        HealthProgramCompleteData.from_health_program_models(progress, override)
        for progress, override in results
    ]
get_all_user_health_program_progresses
get_all_user_health_program_progresses(member_feature_user)

Get all health program progresses for a user.

Source code in components/health_programs/public/business_logic/queries/health_program_progress.py
def get_all_user_health_program_progresses(
    member_feature_user: FeatureUser,
) -> list[HealthProgramProgressData]:
    """
    Get all health program progresses for a user.
    """
    all_program_progresses = (
        current_session.query(HealthProgramProgress)  # noqa: ALN085
        .filter(
            HealthProgramProgress.feature_user == member_feature_user,
        )
        .all()
    )
    return [
        HealthProgramProgressData.from_health_program_progress_model(program_progress)
        for program_progress in all_program_progresses
    ]
get_feature_users_with_health_program_interaction_after
get_feature_users_with_health_program_interaction_after(
    feature_users, cutoff
)

Return feature users whose most recent health program interaction is after cutoff.

Queries MAX(last_updated_at) across all HealthProgramProgress rows per feature user using batched tuple IN clauses to avoid overwhelming PostgreSQL with large OR chains.

Parameters:

Name Type Description Default
feature_users list[FeatureUser]

Feature users to check

required
cutoff datetime

Only return users with interactions strictly after this date

required
Source code in components/health_programs/public/business_logic/queries/health_program_progress.py
def get_feature_users_with_health_program_interaction_after(
    feature_users: list[FeatureUser],
    cutoff: datetime,
) -> set[FeatureUser]:
    """Return feature users whose most recent health program interaction is after cutoff.

    Queries MAX(last_updated_at) across all HealthProgramProgress rows per feature user
    using batched tuple IN clauses to avoid overwhelming PostgreSQL with large OR chains.

    Args:
        feature_users: Feature users to check
        cutoff: Only return users with interactions strictly after this date
    """
    if not feature_users:
        return set()

    total_batches = math.ceil(len(feature_users) / _BATCH_SIZE)
    current_logger.info(
        "checking recent health program interactions in batches",
        total=len(feature_users),
        total_batches=total_batches,
    )

    result: set[FeatureUser] = set()
    for batch_idx in range(total_batches):
        start = batch_idx * _BATCH_SIZE
        batch = feature_users[start : start + _BATCH_SIZE]
        batch_tuples = [(fu.app_id, fu.app_user_id) for fu in batch]

        rows = current_session.execute(
            select(
                HealthProgramProgress.app_id,
                HealthProgramProgress.app_user_id,
            )
            .where(
                tuple_(
                    HealthProgramProgress.app_id,
                    HealthProgramProgress.app_user_id,
                ).in_(batch_tuples)
            )
            .group_by(
                HealthProgramProgress.app_id,
                HealthProgramProgress.app_user_id,
            )
            .having(func.max(HealthProgramProgress.last_updated_at) > cutoff)
        ).all()

        batch_result = {
            FeatureUser(app_id=row.app_id, app_user_id=row.app_user_id) for row in rows
        }
        result.update(batch_result)
        current_logger.info(
            "checked health program batch",
            batch=batch_idx + 1,
            total_batches=total_batches,
            batch_results=len(batch_result),
        )

    return result
get_health_program_progress
get_health_program_progress(
    member_feature_user, program_slug
)

Get the health program progress for a user and a program.

Source code in components/health_programs/public/business_logic/queries/health_program_progress.py
def get_health_program_progress(
    member_feature_user: FeatureUser, program_slug: str
) -> HealthProgramProgressData | None:
    """
    Get the health program progress for a user and a program.
    """
    program_progress: HealthProgramProgress | None = (
        current_session.query(HealthProgramProgress)  # noqa: ALN085
        .filter(
            HealthProgramProgress.feature_user == member_feature_user,
            HealthProgramProgress.program_slug == program_slug,
        )
        .one_or_none()
    )

    if program_progress is None:
        return None

    return HealthProgramProgressData.from_health_program_progress_model(
        program_progress
    )

components.health_programs.public.controllers

health_program

HealthProgramController

Bases: BaseController

PostHealthProgramProgressPostJsonArgs dataclass

PostHealthProgramProgressPostJsonArgs(
    program_slug,
    program_status,
    tasks,
    survey_source_type,
    last_updated_at,
    program_name=None,
    program_id=None,
    program_available_at=None,
    preferred_notification_time_in_minutes=None,
    current_day=None,
    is_custom=None,
    assigned_coach_medical_admin_id=None,
)

Arguments for posting health program progress.

assigned_coach_medical_admin_id class-attribute instance-attribute
assigned_coach_medical_admin_id = None
current_day class-attribute instance-attribute
current_day = None
is_custom class-attribute instance-attribute
is_custom = None
last_updated_at instance-attribute
last_updated_at
preferred_notification_time_in_minutes class-attribute instance-attribute
preferred_notification_time_in_minutes = None
program_available_at class-attribute instance-attribute
program_available_at = None
program_id class-attribute instance-attribute
program_id = None
program_name class-attribute instance-attribute
program_name = None
program_slug instance-attribute
program_slug
program_status class-attribute instance-attribute
program_status = field(
    metadata={
        "marshmallow_field": Enum(
            HealthProgramStatus, by_value=True
        )
    }
)
survey_source_type instance-attribute
survey_source_type
tasks instance-attribute
tasks

PostHealthProgramProgressPostJsonSchema module-attribute

PostHealthProgramProgressPostJsonSchema = class_schema(
    PostHealthProgramProgressPostJsonArgs
)

get_health_programs_override

get_health_programs_override(user)
Source code in components/health_programs/public/controllers/health_program.py
@HealthProgramController.action_route(
    "/override",
    methods=["GET"],
    auth_strategy=GlobalAuthorizationStrategies().authenticated(),
)
@inject_feature_user
@obs.api_call()
def get_health_programs_override(user: FeatureUser) -> Response:  # noqa: D103
    from components.health_programs.public.business_logic.queries.health_program_override import (
        get_all_health_program_overrides,
    )

    return make_json_response(get_all_health_program_overrides(user=user))

get_health_programs_progress_for_sync

get_health_programs_progress_for_sync(user)
Source code in components/health_programs/public/controllers/health_program.py
@HealthProgramController.action_route(
    "/sync",
    methods=["GET"],
    auth_strategy=GlobalAuthorizationStrategies().authenticated(),
)
@inject_feature_user
@obs.api_call()
def get_health_programs_progress_for_sync(user: FeatureUser) -> Response:  # noqa: D103
    from components.health_programs.public.business_logic.queries.health_program_progress import (
        get_all_user_health_program_progresses,
    )

    return make_json_response(
        get_all_user_health_program_progresses(member_feature_user=user)
    )

health_program_endpoint module-attribute

health_program_endpoint = Endpoint('health_programs')

post_health_program_progress

post_health_program_progress(user, json_args)
Source code in components/health_programs/public/controllers/health_program.py
@HealthProgramController.action_route(
    "/progress",
    methods=["POST"],
    auth_strategy=GlobalAuthorizationStrategies().authenticated(),
)
@inject_feature_user
@use_args(
    PostHealthProgramProgressPostJsonSchema(),
    location="json",
    unknown=EXCLUDE,
    arg_name="json_args",
)
@obs.api_call()
def post_health_program_progress(  # noqa: D103
    user: FeatureUser, json_args: PostHealthProgramProgressPostJsonArgs
) -> Response:
    from components.health_programs.internal.business_logic.health_program_progress import (
        create_or_update_health_program_progress,
    )

    program_progress = create_or_update_health_program_progress(
        user=user,
        program_slug=json_args.program_slug,
        program_name=json_args.program_name,
        program_status=json_args.program_status,
        preferred_notification_time_in_minutes=json_args.preferred_notification_time_in_minutes,
        tasks=json_args.tasks,
        survey_source_type=json_args.survey_source_type,
        last_updated_at=json_args.last_updated_at,
        assigned_coach_medical_admin_id=json_args.assigned_coach_medical_admin_id,
        current_day=json_args.current_day,
    )

    return make_json_response(program_progress)

components.health_programs.public.entities

health_program_complete_data

HealthProgramCompleteData dataclass

HealthProgramCompleteData(
    program_slug,
    program_name,
    program_id,
    custom_program_id,
    overridden_segment_slug,
    program_status,
    preferred_notification_time_in_minutes,
    tasks,
    current_day,
    survey_source_type,
    last_updated_at,
    assigned_coach_medical_admin_id,
    dedicated_time,
)

Bases: DataClassJsonMixin

assigned_coach_medical_admin_id instance-attribute
assigned_coach_medical_admin_id
current_day instance-attribute
current_day
custom_program_id instance-attribute
custom_program_id
dedicated_time instance-attribute
dedicated_time
from_health_program_models classmethod
from_health_program_models(
    health_program_progress, health_program_override
)
Source code in components/health_programs/public/entities/health_program_complete_data.py
@classmethod
def from_health_program_models(  # noqa: D102
    cls,
    health_program_progress: "HealthProgramProgress",
    health_program_override: Optional["HealthProgramOverride"],
) -> "HealthProgramCompleteData":
    return cls(
        program_slug=health_program_progress.program_slug,
        program_name=health_program_progress.program_name,
        program_id="",  # mobile backward compatibility
        program_status=health_program_progress.program_status,
        preferred_notification_time_in_minutes=health_program_progress.preferred_notification_time_in_minutes,
        tasks=[
            HealthProgramTask(
                id=task["id"],
                status=task["status"],
                name=task["name"],
                day=task["day"],
                first_task_activity_date=task["first_task_activity_date"],
                last_task_activity_date=task["last_task_activity_date"],
                exercise_titles=task.get("exercise_titles"),
            )
            for task in health_program_progress.tasks
        ],
        survey_source_type=health_program_progress.survey_source_type,
        last_updated_at=health_program_progress.last_updated_at.replace(tzinfo=UTC),
        assigned_coach_medical_admin_id=health_program_progress.assigned_coach_medical_admin_id,
        current_day=health_program_progress.current_day,
        custom_program_id=(
            health_program_override.custom_program_id
            if health_program_override
            else None
        ),
        overridden_segment_slug=(
            health_program_override.segment_slug
            if health_program_override
            else None
        ),
        dedicated_time=(
            health_program_override.dedicated_time
            if health_program_override
            else None
        ),
    )
last_updated_at instance-attribute
last_updated_at
overridden_segment_slug instance-attribute
overridden_segment_slug
preferred_notification_time_in_minutes instance-attribute
preferred_notification_time_in_minutes
program_id instance-attribute
program_id
program_name instance-attribute
program_name
program_slug instance-attribute
program_slug
program_status instance-attribute
program_status
survey_source_type instance-attribute
survey_source_type
tasks instance-attribute
tasks

health_program_override

HealthProgramCustomTask dataclass

HealthProgramCustomTask(day, exercises_ids)

Bases: DataClassJsonMixin

day instance-attribute
day
exercises_ids instance-attribute
exercises_ids

HealthProgramOverrideData dataclass

HealthProgramOverrideData(
    program_slug,
    segment_slug,
    custom_program_id,
    custom_tasks,
    dedicated_time,
)

Bases: DataClassJsonMixin

custom_program_id instance-attribute
custom_program_id
custom_tasks instance-attribute
custom_tasks
dedicated_time instance-attribute
dedicated_time
from_health_program_override_model classmethod
from_health_program_override_model(health_program_override)
Source code in components/health_programs/public/entities/health_program_override.py
@classmethod
def from_health_program_override_model(  # noqa: D102
    cls, health_program_override: "HealthProgramOverride"
) -> "HealthProgramOverrideData":
    return cls(
        program_slug=health_program_override.program_slug,
        segment_slug=health_program_override.segment_slug,
        custom_program_id=health_program_override.custom_program_id,
        custom_tasks=[
            HealthProgramCustomTask(
                day=custom_task["day"], exercises_ids=custom_task["exercises_ids"]
            )
            for custom_task in health_program_override.custom_tasks
        ],
        dedicated_time=health_program_override.dedicated_time,
    )
program_slug instance-attribute
program_slug
segment_slug instance-attribute
segment_slug

health_program_progress

HealthProgramProgressData dataclass

HealthProgramProgressData(
    program_slug,
    program_name,
    program_status,
    preferred_notification_time_in_minutes,
    tasks,
    current_day,
    survey_source_type,
    last_updated_at,
    assigned_coach_medical_admin_id,
)

Bases: DataClassJsonMixin

assigned_coach_medical_admin_id instance-attribute
assigned_coach_medical_admin_id
current_day instance-attribute
current_day
from_health_program_progress_model classmethod
from_health_program_progress_model(health_program_progress)
Source code in components/health_programs/public/entities/health_program_progress.py
@classmethod
def from_health_program_progress_model(  # noqa: D102
    cls,
    health_program_progress: "HealthProgramProgress",
) -> "HealthProgramProgressData":
    return cls(
        program_slug=health_program_progress.program_slug,
        program_name=health_program_progress.program_name,
        program_status=health_program_progress.program_status,
        preferred_notification_time_in_minutes=health_program_progress.preferred_notification_time_in_minutes,
        tasks=[
            HealthProgramTask(
                id=task["id"],
                status=task["status"],
                name=task["name"],
                day=task["day"],
                first_task_activity_date=task["first_task_activity_date"],
                last_task_activity_date=task["last_task_activity_date"],
                exercise_titles=task.get("exercise_titles"),
            )
            for task in health_program_progress.tasks
        ],
        survey_source_type=health_program_progress.survey_source_type,
        last_updated_at=health_program_progress.last_updated_at.replace(tzinfo=UTC),
        assigned_coach_medical_admin_id=health_program_progress.assigned_coach_medical_admin_id,
        current_day=health_program_progress.current_day,
    )
last_updated_at instance-attribute
last_updated_at
preferred_notification_time_in_minutes instance-attribute
preferred_notification_time_in_minutes
program_name instance-attribute
program_name
program_slug instance-attribute
program_slug
program_status instance-attribute
program_status
survey_source_type instance-attribute
survey_source_type
tasks instance-attribute
tasks

HealthProgramTask dataclass

HealthProgramTask(
    id,
    status,
    name,
    day,
    exercise_titles,
    first_task_activity_date,
    last_task_activity_date,
)

Bases: DataClassJsonMixin

day instance-attribute
day
exercise_titles instance-attribute
exercise_titles
first_task_activity_date instance-attribute
first_task_activity_date
id instance-attribute
id
last_task_activity_date instance-attribute
last_task_activity_date
name instance-attribute
name
status instance-attribute
status

health_program_status

HealthProgramStatus

Bases: AlanBaseEnum

enrolled class-attribute instance-attribute
enrolled = 'enrolled'
not_started class-attribute instance-attribute
not_started = 'not-started'
program_in_preparation class-attribute instance-attribute
program_in_preparation = 'program-in-preparation'
program_ready class-attribute instance-attribute
program_ready = 'program-ready'

components.health_programs.public.subscription

subscribe_to_events

subscribe_to_events()

All event subscriptions for health_programs.

Source code in components/health_programs/public/subscription.py
def subscribe_to_events() -> None:
    """All event subscriptions for health_programs."""
    from components.clinic.public.events.events import ClinicUserDataDeleted
    from components.health_programs.internal.business_logic.events_subscribers import (
        delete_health_program_data_for_clinic_user,
    )

    message_broker = get_message_broker()
    message_broker.subscribe_async(
        ClinicUserDataDeleted,
        delete_health_program_data_for_clinic_user,
        queue_name=LOW_PRIORITY_QUEUE,
    )