Skip to content
This repository was archived by the owner on Jun 19, 2025. It is now read-only.

Commit 4435ec4

Browse files
authored
feat(tags): New mutation to add multiple tags to an experiment. (#98)
* add mutation for multiple tags with the change to the service and tests. * fix issue with duplicate tags with different casing. * fix minor issue in generate license report workflow. * update vscode setting to append a line at the end of the file.
1 parent 992fbde commit 4435ec4

File tree

11 files changed

+263
-36
lines changed

11 files changed

+263
-36
lines changed

.github/workflows/license_report.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,6 @@ jobs:
1414
with:
1515
python-version: "3.10"
1616

17-
- name: Install packages
18-
run: poetry install
19-
2017
- name: Create a markdown file with contents
2118
id: sets-licenses
2219
run: |

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,4 +133,5 @@ dmypy.json
133133

134134
# Other unneeded files
135135
.DS_Store
136-
frontend_build
136+
frontend_build
137+
license_report.md

.vscode/settings.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,7 @@
3434
"tag:yaml.org,2002:python/name:material.extensions.emoji.to_svg",
3535
"tag:yaml.org,2002:python/name:material.extensions.emoji.twemoji",
3636
"tag:yaml.org,2002:python/name:pymdownx.superfences.fence_code_format"
37-
]
38-
}
37+
],
38+
"files.insertFinalNewline": true,
39+
"notebook.insertFinalNewline": true
40+
}

aqueductcore/backend/routers/graphql/inputs.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@ class ExperimentTagInput:
3939
tag: str
4040

4141

42+
@strawberry.input
43+
class ExperimentTagsInput:
44+
"""Input type to add or remove tags from experiment"""
45+
46+
experiment_id: UUID
47+
tags: List[str]
48+
49+
4250
@strawberry.input
4351
class ExperimentCreateInput:
4452
"""Input type to create experiemnt"""

aqueductcore/backend/routers/graphql/mutations/experiment_mutations.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
ExperimentCreateInput,
88
ExperimentRemoveInput,
99
ExperimentTagInput,
10+
ExperimentTagsInput,
1011
ExperimentUpdateInput,
1112
)
1213
from aqueductcore.backend.routers.graphql.types import ExperimentData
@@ -49,11 +50,25 @@ async def add_tag_to_experiment(
4950
) -> ExperimentData:
5051
"""Add tag to experiment mutation"""
5152

52-
experiment = await experiment_service.add_tag_to_experiment(
53+
experiment = await experiment_service.add_tags_to_experiment(
5354
user_info=context.user_info,
5455
db_session=context.db_session,
5556
experiment_id=experiment_tag_input.experiment_id,
56-
tag=experiment_tag_input.tag,
57+
tags=[experiment_tag_input.tag],
58+
)
59+
return experiment_model_to_node(experiment)
60+
61+
62+
async def add_tags_to_experiment(
63+
context: ServerContext, experiment_tags_input: ExperimentTagsInput
64+
) -> ExperimentData:
65+
"""Add tag to experiment mutation"""
66+
67+
experiment = await experiment_service.add_tags_to_experiment(
68+
user_info=context.user_info,
69+
db_session=context.db_session,
70+
experiment_id=experiment_tags_input.experiment_id,
71+
tags=experiment_tags_input.tags,
5772
)
5873
return experiment_model_to_node(experiment)
5974

aqueductcore/backend/routers/graphql/mutations_schema.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111
ExperimentCreateInput,
1212
ExperimentRemoveInput,
1313
ExperimentTagInput,
14+
ExperimentTagsInput,
1415
ExperimentUpdateInput,
1516
)
1617
from aqueductcore.backend.routers.graphql.mutations.experiment_mutations import (
1718
add_tag_to_experiment,
19+
add_tags_to_experiment,
1820
create_experiment,
1921
remove_experiment,
2022
remove_tag_from_experiment,
@@ -65,6 +67,18 @@ async def add_tag_to_experiment(
6567
)
6668
return experiment
6769

70+
@strawberry.mutation
71+
async def add_tags_to_experiment(
72+
self, info: Info, experiment_tags_input: ExperimentTagsInput
73+
) -> ExperimentData:
74+
"""Mutation to add tag to experiment"""
75+
76+
context = cast(ServerContext, info.context)
77+
experiment = await add_tags_to_experiment(
78+
context=context, experiment_tags_input=experiment_tags_input
79+
)
80+
return experiment
81+
6882
@strawberry.mutation
6983
async def remove_tag_from_experiment(
7084
self, info: Info, experiment_tag_input: ExperimentTagInput

aqueductcore/backend/services/experiment.py

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -320,8 +320,7 @@ async def create_experiment(
320320
)
321321
db_session.add(db_experiment)
322322

323-
for db_tag in tags_in_db:
324-
db_experiment.tags.append(db_tag)
323+
db_experiment.tags.extend(tags_in_db)
325324

326325
await db_session.commit()
327326

@@ -369,8 +368,8 @@ async def update_experiment(
369368

370369

371370
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
372-
async def add_tag_to_experiment(
373-
user_info: UserInfo, db_session: AsyncSession, experiment_id: UUID, tag: ExperimentTag
371+
async def add_tags_to_experiment(
372+
user_info: UserInfo, db_session: AsyncSession, experiment_id: UUID, tags: List[ExperimentTag]
374373
) -> ExperimentRead:
375374
"""Add tag to experiment"""
376375

@@ -391,21 +390,25 @@ async def add_tag_to_experiment(
391390
"Non-existing experiment with the specified ID for the user."
392391
)
393392

394-
tag_key = tag.lower()
395-
experiment_tags = [tag.key for tag in db_experiment.tags]
396-
397-
if tag_key in experiment_tags:
398-
raise AQDDBError("DB query failed due to pre-existing tag with the Experiment.")
393+
new_tags = {tag.lower(): tag for tag in tags}
399394

400-
tag_statement = select(orm.Tag).filter(orm.Tag.key == tag_key)
395+
if len(set(new_tags.keys())) != len(tags):
396+
raise AQDValidationError("Duplicate tags are not allowed in the request.")
401397

398+
tag_statement = select(orm.Tag).filter(orm.Tag.key.in_(new_tags.keys()))
402399
result = await db_session.execute(tag_statement)
403-
db_tag = result.scalars().first()
404-
if db_tag is None:
405-
db_tag = orm.Tag(name=tag, key=tag_key)
406-
db_session.add(db_tag)
407400

408-
db_experiment.tags.append(db_tag)
401+
cur_db_tags = result.unique().scalars().all()
402+
for db_tag in cur_db_tags:
403+
if db_tag.key in new_tags:
404+
del new_tags[db_tag.key]
405+
if db_tag not in db_experiment.tags:
406+
db_experiment.tags.append(db_tag)
407+
408+
for key, value in new_tags.items():
409+
db_tag = orm.Tag(name=value, key=key)
410+
db_session.add(db_tag)
411+
db_experiment.tags.append(db_tag)
409412

410413
await db_session.commit()
411414

ci/generate_license_report.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ if [[ -z $(which poetry) ]]; then
2020
poetry config virtualenvs.in-project false
2121
fi
2222

23-
23+
echo "Installing dependencies"
24+
poetry install
2425
yarn install --cwd $FRONTEND_DIR
2526

2627
export BACKEND_LICENSES=$(poetry run pip-licenses --format=markdown --order=license)

ci/unit_tests.sh

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,3 @@ poetry install
2323

2424
echo "Build coverage report"
2525
poetry run $PROJECT_ROOT/scripts/run_unit_tests.sh
26-

tests/unittests/test_experiment_services.py

Lines changed: 93 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@
99
from sqlalchemy.ext.asyncio import AsyncSession
1010

1111
from aqueductcore.backend.context import UserInfo, UserScope
12-
from aqueductcore.backend.errors import AQDDBExperimentNonExisting
12+
from aqueductcore.backend.errors import AQDDBExperimentNonExisting, AQDValidationError
1313
from aqueductcore.backend.models import orm
1414
from aqueductcore.backend.models.experiment import ExperimentCreate, TagCreate
1515
from aqueductcore.backend.services.experiment import (
16-
add_tag_to_experiment,
16+
add_tags_to_experiment,
1717
build_experiment_dir_absolute_path,
1818
create_experiment,
1919
generate_experiment_id_and_alias,
@@ -384,20 +384,108 @@ async def test_add_db_tag_to_experiment(
384384

385385
await db_session.commit()
386386

387-
in_db_experiment = await add_tag_to_experiment(
387+
in_db_experiment = await add_tags_to_experiment(
388388
user_info=UserInfo(
389389
user_id=uuid4(), username=settings.default_username, scopes=set(UserScope)
390390
),
391391
db_session=db_session,
392392
experiment_id=experiments_data[0].id,
393-
tag="important",
393+
tags=["important"],
394394
)
395-
await db_session.commit()
396395

397396
in_db_experiment_tags = [tag.name for tag in in_db_experiment.tags]
398397
assert "important" in in_db_experiment_tags
399398

400399

400+
@pytest.mark.asyncio
401+
async def test_add_db_unique_tags_to_experiment(
402+
db_session: AsyncSession, experiments_data: List[ExperimentCreate]
403+
):
404+
"""Test update_db_experiment operation"""
405+
406+
db_user = orm.User(id=UUID(int=0), username=settings.default_username)
407+
db_session.add(db_user)
408+
409+
for experiment in experiments_data:
410+
db_experiment = experiment_model_to_orm(experiment)
411+
db_experiment.created_by_user = db_user
412+
db_session.add(db_experiment)
413+
414+
await db_session.commit()
415+
416+
expected_tags = ["test1", "test2", "test3"]
417+
in_db_experiment = await add_tags_to_experiment(
418+
user_info=UserInfo(
419+
user_id=uuid4(), username=settings.default_username, scopes=set(UserScope)
420+
),
421+
db_session=db_session,
422+
experiment_id=experiments_data[0].id,
423+
tags=expected_tags,
424+
)
425+
426+
in_db_experiment_tags = [tag.name for tag in in_db_experiment.tags]
427+
for item in expected_tags:
428+
assert item in in_db_experiment_tags
429+
430+
431+
@pytest.mark.asyncio
432+
async def test_add_db_unique_tags_to_experiment_pre_existing_tags(
433+
db_session: AsyncSession, experiments_data: List[ExperimentCreate]
434+
):
435+
"""Test update_db_experiment operation"""
436+
437+
db_user = orm.User(id=UUID(int=0), username=settings.default_username)
438+
db_session.add(db_user)
439+
440+
for experiment in experiments_data:
441+
db_experiment = experiment_model_to_orm(experiment)
442+
db_experiment.created_by_user = db_user
443+
db_session.add(db_experiment)
444+
445+
await db_session.commit()
446+
447+
expected_tags = {"test1", "test2", "test3", experiments_data[0].tags[0].name}
448+
in_db_experiment = await add_tags_to_experiment(
449+
user_info=UserInfo(
450+
user_id=uuid4(), username=settings.default_username, scopes=set(UserScope)
451+
),
452+
db_session=db_session,
453+
experiment_id=experiments_data[0].id,
454+
tags=list(expected_tags),
455+
)
456+
457+
in_db_experiment_tags = [tag.name for tag in in_db_experiment.tags]
458+
assert expected_tags.issubset(in_db_experiment_tags)
459+
460+
461+
@pytest.mark.asyncio
462+
async def test_add_db_duplicate_tags_in_request_to_experiment(
463+
db_session: AsyncSession, experiments_data: List[ExperimentCreate]
464+
):
465+
"""Test update_db_experiment operation"""
466+
467+
db_user = orm.User(id=UUID(int=0), username=settings.default_username)
468+
db_session.add(db_user)
469+
470+
for experiment in experiments_data:
471+
db_experiment = experiment_model_to_orm(experiment)
472+
db_experiment.created_by_user = db_user
473+
db_session.add(db_experiment)
474+
475+
await db_session.commit()
476+
477+
expected_tags = ["test1", "test1"]
478+
with pytest.raises(AQDValidationError):
479+
await add_tags_to_experiment(
480+
user_info=UserInfo(
481+
user_id=uuid4(), username=settings.default_username, scopes=set(UserScope)
482+
),
483+
db_session=db_session,
484+
experiment_id=experiments_data[0].id,
485+
tags=expected_tags,
486+
)
487+
488+
401489
@pytest.mark.asyncio
402490
async def test_remove_db_tag_from_experiment(
403491
db_session: AsyncSession, experiments_data: List[ExperimentCreate]

0 commit comments

Comments
 (0)