Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""add_projects_extentions
Revision ID: a85557c02d71
Revises: 5756d9282a0a
Create Date: 2025-10-24 15:03:32.846288+00:00
"""

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = "a85557c02d71"
down_revision = "5756d9282a0a"
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"projects_extentions",
sa.Column("project_uuid", sa.String(), nullable=False),
sa.Column(
"allow_guests_to_push_states_and_output_ports",
sa.Boolean(),
server_default=sa.text("false"),
nullable=False,
),
sa.ForeignKeyConstraint(
["project_uuid"],
["projects.uuid"],
name="fk_projects_extentions_project_uuid_projects",
onupdate="CASCADE",
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("project_uuid"),
)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("projects_extentions")
# ### end Alembic commands ###
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import sqlalchemy as sa

from ._common import RefActions
from .base import metadata
from .projects import projects

projects_extentions = sa.Table(
"projects_extentions",
metadata,
sa.Column(
"project_uuid",
sa.String,
sa.ForeignKey(
projects.c.uuid,
name="fk_projects_extentions_project_uuid_projects",
ondelete=RefActions.CASCADE,
onupdate=RefActions.CASCADE,
),
primary_key=True,
doc="project reference and primary key for this table",
),
sa.Column(
"allow_guests_to_push_states_and_output_ports",
sa.Boolean(),
nullable=False,
server_default=sa.sql.expression.false(),
doc=(
"When True, guest will save the state of a service "
"and also push the data to the output ports"
),
),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import sqlalchemy as sa
from simcore_postgres_database.utils_repos import (
pass_or_acquire_connection,
transaction_context,
)
from sqlalchemy.ext.asyncio import AsyncEngine

from .models.projects_extentions import projects_extentions


class CouldNotCreateOrUpdateUserPreferenceError(Exception): ...


class ProjectsExtensionsRepo:
model: sa.Table = projects_extentions

@classmethod
async def allows_guests_to_push_states_and_output_ports(
cls, async_engine: AsyncEngine, *, project_uuid: str
) -> bool:
async with pass_or_acquire_connection(async_engine) as connection:
result: bool | None = await connection.scalar(
sa.select(
cls.model.c.allow_guests_to_push_states_and_output_ports
).where(cls.model.c.project_uuid == project_uuid)
)
return result if result is not None else False

@classmethod
async def set_allow_guests_to_push_states_and_output_ports(
cls, async_engine: AsyncEngine, *, project_uuid: str
) -> None:
async with transaction_context(async_engine) as connection:
await connection.execute(
sa.insert(projects_extentions).values(
project_uuid=project_uuid,
allow_guests_to_push_states_and_output_ports=True,
)
)
59 changes: 59 additions & 0 deletions packages/postgres-database/tests/test_utils_projects_extentions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# pylint: disable=redefined-outer-name

from collections.abc import Awaitable, Callable

import pytest
from aiopg.sa.connection import SAConnection
from aiopg.sa.result import RowProxy
from simcore_postgres_database.utils_projects_extentions import ProjectsExtensionsRepo
from sqlalchemy.ext.asyncio import AsyncEngine


@pytest.fixture
async def fake_user(
connection: SAConnection,
create_fake_user: Callable[..., Awaitable[RowProxy]],
) -> RowProxy:
user: RowProxy = await create_fake_user(connection, name=f"user.{__name__}")
return user


@pytest.fixture
async def fake_project(
connection: SAConnection,
fake_user: RowProxy,
create_fake_project: Callable[..., Awaitable[RowProxy]],
create_fake_nodes: Callable[..., Awaitable[RowProxy]],
) -> RowProxy:
project: RowProxy = await create_fake_project(connection, fake_user, hidden=True)
await create_fake_nodes(project)
return project


async def test_workflow(
asyncpg_engine: AsyncEngine,
connection: SAConnection,
create_fake_user: Callable[..., Awaitable[RowProxy]],
create_fake_project: Callable[..., Awaitable[RowProxy]],
):
user: RowProxy = await create_fake_user(connection)
project: RowProxy = await create_fake_project(connection, user, hidden=True)

assert (
await ProjectsExtensionsRepo.allows_guests_to_push_states_and_output_ports(
asyncpg_engine, project_uuid=project["uuid"]
)
is False
)

# add the entry in the table
await ProjectsExtensionsRepo.set_allow_guests_to_push_states_and_output_ports(
asyncpg_engine, project_uuid=project["uuid"]
)

assert (
await ProjectsExtensionsRepo.allows_guests_to_push_states_and_output_ports(
asyncpg_engine, project_uuid=project["uuid"]
)
is True
)
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
from servicelib.common_headers import UNDEFINED_DEFAULT_SIMCORE_USER_AGENT_VALUE
from servicelib.logging_utils import log_catch, log_context
from servicelib.utils import limited_as_completed, limited_gather
from simcore_postgres_database.utils_projects_extentions import ProjectsExtensionsRepo

from ..db.plugin import get_asyncpg_engine
from ..dynamic_scheduler import api as dynamic_scheduler_service
from ..projects._projects_service import (
is_node_id_present_in_any_project_workbench,
Expand Down Expand Up @@ -54,6 +56,14 @@ async def _remove_service(
)
)

if (
user_role == UserRole.GUEST
and await ProjectsExtensionsRepo.allows_guests_to_push_states_and_output_ports(
get_asyncpg_engine(app), project_uuid=f"{service.project_id}"
)
):
save_service_state = True

with (
log_catch(_logger, reraise=False),
log_context(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@
from servicelib.rest_constants import RESPONSE_MODEL_POLICY
from servicelib.utils import fire_and_forget_task, limited_gather, logged_gather
from simcore_postgres_database.models.users import UserRole
from simcore_postgres_database.utils_projects_extentions import ProjectsExtensionsRepo
from simcore_postgres_database.utils_projects_nodes import (
ProjectNodeCreate,
ProjectNodesNodeNotFoundError,
Expand All @@ -116,6 +117,7 @@
from ..application_settings import get_application_settings
from ..catalog import catalog_service
from ..constants import APP_FIRE_AND_FORGET_TASKS_KEY
from ..db.plugin import get_asyncpg_engine
from ..director_v2 import director_v2_service
from ..dynamic_scheduler import api as dynamic_scheduler_service
from ..models import ClientSessionID
Expand Down Expand Up @@ -817,6 +819,13 @@ async def _start_dynamic_service( # pylint: disable=too-many-statements # noqa
save_state = await has_user_project_access_rights(
request.app, project_id=project_uuid, user_id=user_id, permission="write"
)
if (
user_role == UserRole.GUEST
and await ProjectsExtensionsRepo.allows_guests_to_push_states_and_output_ports(
get_asyncpg_engine(request.app), project_uuid=f"{project_uuid}"
)
):
save_state = True

@exclusive(
get_redis_lock_manager_client_sdk(request.app),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@
from models_library.projects import ProjectID
from servicelib.aiohttp import status
from servicelib.aiohttp.typing_extension import Handler
from simcore_postgres_database.utils_projects_extentions import ProjectsExtensionsRepo

from ..constants import INDEX_RESOURCE_NAME
from ..db.plugin import get_asyncpg_engine
from ..director_v2 import director_v2_service
from ..dynamic_scheduler import api as dynamic_scheduler_service
from ..products import products_web
Expand Down Expand Up @@ -221,6 +223,14 @@ async def copy_study_to_account(
request.app, project_id=ProjectID(project["uuid"])
)

# set the same option in the new project from the template
if await ProjectsExtensionsRepo.allows_guests_to_push_states_and_output_ports(
get_asyncpg_engine(request.app), project_uuid=template_project["uuid"]
):
await ProjectsExtensionsRepo.set_allow_guests_to_push_states_and_output_ports(
get_asyncpg_engine(request.app), project_uuid=project["uuid"]
)

return project_uuid


Expand Down
Loading