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

Commit 3f5396a

Browse files
committed
Add tags to ingest process
1 parent 8db623b commit 3f5396a

File tree

4 files changed

+164
-15
lines changed

4 files changed

+164
-15
lines changed

api/models/ingest.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
from typing import Optional
33
from enum import Enum
44

5-
from pydantic import BaseModel, ConfigDict
5+
from pydantic import BaseModel, ConfigDict, field_validator
66

7-
from api.schemas import IngestState
7+
from api.schemas import IngestState, IngestProcessTag
88
from .source import Sources
99

1010

@@ -15,6 +15,7 @@ class Post(BaseModel):
1515
source_id: Optional[int] = None
1616
access_group_id: Optional[int] = None
1717
map_id: Optional[str] = None
18+
tags: Optional[list[str]] = None
1819

1920
class Config:
2021
orm_mode = True
@@ -29,7 +30,22 @@ class Get(Post):
2930
completed_on: Optional[datetime.datetime] = None
3031
source: Optional[Sources] = None
3132

33+
@field_validator("tags", mode="before")
34+
@classmethod
35+
def transform_tags(cls, v):
36+
if len(v) == 0:
37+
return []
38+
39+
if isinstance(v[0], IngestProcessTag):
40+
return [tag.tag for tag in v]
41+
42+
return v
43+
3244

3345
class Patch(Post):
3446
pass
3547

48+
49+
class Tag(BaseModel):
50+
tag: str
51+

api/routes/ingest.py

Lines changed: 76 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import os
22

33
from fastapi import APIRouter, Depends, HTTPException
4-
from sqlalchemy import insert, select, update, and_
4+
from sqlalchemy import insert, select, update, and_, delete
55
from sqlalchemy.orm import selectinload, joinedload, defer
66
import minio
77

@@ -13,7 +13,7 @@
1313
from api.routes.security import has_access
1414
import api.models.ingest as IngestProcessModel
1515
import api.models.object as Object
16-
from api.schemas import IngestProcess as IngestProcessSchema, ObjectGroup, Sources
16+
from api.schemas import IngestProcess as IngestProcessSchema, ObjectGroup, Sources, IngestProcessTag
1717
from api.query_parser import get_filter_query_params, QueryParser
1818

1919
router = APIRouter(
@@ -39,7 +39,8 @@ async def get_multiple_ingest_process(page: int = 0, page_size: int = 50, filter
3939
.limit(page_size)\
4040
.offset(page_size * page)\
4141
.where(and_(query_parser.where_expressions()))\
42-
.options(joinedload(IngestProcessSchema.source).defer(Sources.rgeom).defer(Sources.web_geom))
42+
.options(joinedload(IngestProcessSchema.source).defer(Sources.rgeom).defer(Sources.web_geom))\
43+
.options(selectinload(IngestProcessSchema.tags))
4344

4445
results = await session.execute(select_stmt)
4546

@@ -56,7 +57,8 @@ async def get_ingest_process(id: int):
5657
async with async_session() as session:
5758

5859
select_stmt = select(IngestProcessSchema).where(and_(IngestProcessSchema.id == id))\
59-
.options(joinedload(IngestProcessSchema.source).defer(Sources.rgeom).defer(Sources.web_geom))
60+
.options(joinedload(IngestProcessSchema.source).defer(Sources.rgeom).defer(Sources.web_geom))\
61+
.options(selectinload(IngestProcessSchema.tags))
6062

6163
result = await session.scalar(select_stmt)
6264

@@ -78,18 +80,25 @@ async def create_ingest_process(object: IngestProcessModel.Post, user_has_access
7880

7981
async with async_session() as session:
8082

81-
object_group_stmt = insert(ObjectGroup).values().returning(ObjectGroup)
82-
object_group = await session.scalar(object_group_stmt)
83-
84-
stmt = insert(IngestProcessSchema).values(**object.model_dump(), object_group_id=object_group.id).returning(IngestProcessSchema)
83+
object_group = ObjectGroup()
84+
session.add(object_group)
85+
await session.commit()
8586

86-
server_object = await session.scalar(stmt)
87+
tags = [IngestProcessTag(tag=tag) for tag in object.tags]
88+
del object.tags
8789

88-
server_object.source = await session.scalar(select(Sources).where(Sources.source_id == server_object.source_id))
90+
ingest_process = IngestProcessSchema(
91+
**object.model_dump(),
92+
object_group_id=object_group.id,
93+
tags=tags
94+
)
8995

96+
session.add(ingest_process)
9097
await session.commit()
9198

92-
return server_object
99+
ingest_process.source = await session.get(Sources, object.source_id)
100+
101+
return ingest_process
93102

94103

95104
@router.patch("/{id}", response_model=IngestProcessModel.Get)
@@ -119,6 +128,62 @@ async def patch_ingest_process(
119128
await session.commit()
120129
return response
121130

131+
@router.post("/{id}/tags", response_model=list[str])
132+
async def add_ingest_process_tag(
133+
id: int,
134+
tag: IngestProcessModel.Tag,
135+
user_has_access: bool = Depends(has_access)
136+
):
137+
"""Add a tag to an ingest process"""
138+
139+
if not user_has_access:
140+
raise HTTPException(status_code=403, detail="User does not have access to create an object")
141+
142+
engine = get_engine()
143+
async_session = get_async_session(engine)
144+
145+
async with async_session() as session:
146+
147+
ingest_process = await session.get(IngestProcessSchema, id)
148+
149+
if ingest_process is None:
150+
raise HTTPException(status_code=404, detail=f"IngestProcess with id ({id}) not found")
151+
152+
ingest_process.tags.append(IngestProcessTag(tag=tag.tag))
153+
await session.commit()
154+
155+
ingest_process = await session.get(IngestProcessSchema, id)
156+
return [tag.tag for tag in ingest_process.tags]
157+
158+
return None
159+
160+
@router.delete("/{id}/tags/{tag}", response_model=list[str])
161+
async def delete_ingest_process_tag(id: int, tag: str, user_has_access: bool = Depends(has_access)):
162+
"""Delete a tag from an ingest process"""
163+
164+
if not user_has_access:
165+
raise HTTPException(status_code=403, detail="User does not have access to create an object")
166+
167+
engine = get_engine()
168+
async_session = get_async_session(engine)
169+
170+
async with async_session() as session:
171+
172+
ingest_process = await session.get(IngestProcessSchema, id)
173+
174+
if ingest_process is None:
175+
raise HTTPException(status_code=404, detail=f"IngestProcess with id ({id}) not found")
176+
177+
tag_stmt = delete(IngestProcessTag).where(and_(IngestProcessTag.ingest_process_id == id, IngestProcessTag.tag == tag))
178+
await session.execute(tag_stmt)
179+
await session.commit()
180+
181+
ingest_process = await session.get(IngestProcessSchema, id)
182+
183+
return [tag.tag for tag in ingest_process.tags]
184+
185+
return ingest_process
186+
122187

123188
@router.get("/{id}/objects", response_model=list[Object.GetSecureURL])
124189
async def get_ingest_process_objects(id: int):

api/schemas.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import enum
22
from typing import List
33
import datetime
4-
from sqlalchemy import ForeignKey, func, DateTime, Enum, UniqueConstraint
4+
from sqlalchemy import ForeignKey, func, DateTime, Enum, PrimaryKeyConstraint, UniqueConstraint
55
from sqlalchemy.dialects.postgresql import VARCHAR, TEXT, INTEGER, ARRAY, BOOLEAN, JSON, JSONB
66
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
77
from geoalchemy2 import Geometry
@@ -171,3 +171,18 @@ class IngestProcess(Base):
171171
# Relationships
172172
object_group: Mapped[ObjectGroup] = relationship(back_populates="ingest_process", lazy="joined")
173173
source: Mapped[Sources] = relationship(back_populates="ingest_process")
174+
tags: Mapped[List["IngestProcessTag"]] = relationship(back_populates="ingest_process", lazy="joined")
175+
176+
177+
class IngestProcessTag(Base):
178+
__tablename__ = "ingest_process_tag"
179+
__table_args__ = (
180+
PrimaryKeyConstraint('ingest_process_id', 'tag', name='pk_tag'),
181+
{'schema': 'maps_metadata'}
182+
)
183+
184+
ingest_process_id: Mapped[int] = mapped_column(ForeignKey("maps_metadata.ingest_process.id"))
185+
tag: Mapped[str] = mapped_column(VARCHAR(255))
186+
187+
# Relationships
188+
ingest_process: Mapped[IngestProcess] = relationship(back_populates="tags")

api/tests/main.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,8 @@ def test_add_ingest_process(self, api_client):
433433

434434
ingest_process_data = {
435435
"comments": "This is a test comment",
436-
"state": "pending"
436+
"state": "pending",
437+
"tags" : ["test_tag", "cool_tag"]
437438
}
438439

439440
response = api_client.post(
@@ -443,6 +444,12 @@ def test_add_ingest_process(self, api_client):
443444

444445
assert response.status_code == 200
445446

447+
data = response.json()
448+
449+
assert data['comments'] == "This is a test comment"
450+
assert data['state'] == "pending"
451+
assert data['tags'] == ["test_tag", "cool_tag"]
452+
446453
def test_get_ingest_processes(self, api_client):
447454
response = api_client.get("/ingest-process")
448455
assert response.status_code == 200
@@ -483,6 +490,52 @@ def test_patch_ingest_process(self, api_client):
483490

484491
assert single_data['comments'] == "test"
485492

493+
def test_add_tag_to_ingest_process(self, api_client):
494+
"""Test adding a tag to an ingest process"""
495+
496+
test_tag = f"new_tag-{random.randint(0,10000000)}"
497+
498+
response = api_client.get("/ingest-process")
499+
assert response.status_code == 200
500+
501+
data = response.json()
502+
503+
assert len(data) > 0
504+
505+
response = api_client.post(f"/ingest-process/{data[0]['id']}/tags", json={"tag": test_tag})
506+
507+
assert response.status_code == 200
508+
509+
single_data = response.json()
510+
511+
assert test_tag in single_data
512+
513+
def test_delete_tag_from_ingest_process(self, api_client):
514+
"""Test deleting a tag from an ingest process"""
515+
516+
test_tag = f"new_tag-{random.randint(0,10000000)}"
517+
518+
response = api_client.get("/ingest-process")
519+
assert response.status_code == 200
520+
521+
data = response.json()
522+
523+
assert len(data) > 0
524+
525+
response = api_client.post(f"/ingest-process/{data[0]['id']}/tags", json={"tag": test_tag})
526+
post_data = response.json()
527+
528+
assert response.status_code == 200
529+
assert test_tag in post_data
530+
531+
response = api_client.delete(f"/ingest-process/{data[0]['id']}/tags/{test_tag}")
532+
533+
assert response.status_code == 200
534+
535+
single_data = response.json()
536+
537+
assert test_tag not in single_data
538+
486539
def test_pair_object_to_ingest(self, api_client):
487540
response = api_client.get("/ingest-process")
488541
assert response.status_code == 200

0 commit comments

Comments
 (0)