Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
2d2818d
refactor: lexicon updates to support ui
jirhiker Aug 16, 2025
dae3738
Formatting changes
jirhiker Aug 16, 2025
09c94c7
feat: added additional thing_types, PS, ES, M
jirhiker Aug 16, 2025
0405542
feat: added optional project_area to Group.
jirhiker Aug 16, 2025
bd3c858
Formatting changes
jirhiker Aug 16, 2025
beda58c
fix: reenabled User for use with continuum
jirhiker Aug 16, 2025
214e268
feat: added tests for getting project area as a geojson
jirhiker Aug 16, 2025
42056b5
Formatting changes
jirhiker Aug 16, 2025
f10fa7d
refactor: cleanup group schema
jirhiker Aug 17, 2025
b4a0afd
Formatting changes
jirhiker Aug 17, 2025
fe46ead
feat: updated transfer to handle owners/contacts
jirhiker Aug 18, 2025
0ac4d9c
Formatting changes
jirhiker Aug 18, 2025
d0d4df3
fix: fixed asset. added gcs_helper. added a demo transfer_assets func…
jirhiker Aug 18, 2025
32b1af1
Formatting changes
jirhiker Aug 18, 2025
099ddf8
fix: fixed asset test
jirhiker Aug 18, 2025
96afe57
Merge remote-tracking branch 'origin/jir-project-area' into jir-proje…
jirhiker Aug 18, 2025
8cea1f2
refactor: only add assets that dont already exist
jirhiker Aug 18, 2025
5185ed1
Formatting changes
jirhiker Aug 18, 2025
a323548
fix: fixed asset tests
jirhiker Aug 18, 2025
380c352
Formatting changes
jirhiker Aug 18, 2025
e0daa90
refactor: address pr feedback
jirhiker Aug 18, 2025
1addf92
Formatting changes
jirhiker Aug 18, 2025
7c43ffa
feat: improved transfer to start handling thing id links
jirhiker Aug 19, 2025
5f6fda7
refactor: comment out group association endpoint. fix gcs_helper for …
jirhiker Aug 20, 2025
3f9a92d
Formatting changes
jirhiker Aug 20, 2025
a587d62
refactor: store a uri in the Asset model. return a signed_url for whe…
jirhiker Aug 20, 2025
b5da904
Formatting changes
jirhiker Aug 20, 2025
7435f60
refactor: signed_url optional in AssetResponse
jirhiker Aug 20, 2025
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
9 changes: 8 additions & 1 deletion .gcloudignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@

tests
migration

gcs_credentials.json
launcher.sh
test.sh
docker/*
alembic/*
.gitattributes
.git/*

# User-specific stuff
.idea/**/workspace.xml
Expand Down Expand Up @@ -215,6 +221,7 @@ celerybeat.pid
.venv
env/
venv/
.venv/*
ENV/
env.bak/
venv.bak/
Expand Down
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ wheels/

# Virtual environments
.venv

requirements.txt

# JetBrains IDEs
.idea/
Expand All @@ -23,7 +23,7 @@ tests/uploads
migrate.sh
launcher.sh
gcs_credentials.json

transfers/data/assets*

# deployment files
app.yaml
150 changes: 75 additions & 75 deletions api/asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,43 +13,89 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# ===============================================================================
import os
from datetime import timedelta
from typing import List
from uuid import uuid4

from fastapi import APIRouter, Depends, UploadFile, File, HTTPException
from fastapi_pagination.ext.sqlalchemy import paginate
from sqlalchemy import select
from starlette.status import HTTP_201_CREATED

from api.pagination import CustomPage
from core.dependencies import session_dependency
from core.dependencies import (
session_dependency,
viewer_function,
admin_dependency,
admin_function,
editor_dependency,
)
from db import Thing
from db.asset import Asset, AssetThingAssociation
from schemas.asset import AssetResponse, CreateAsset, UpdateAsset
from services.audit_helper import audit_add
from services.crud_helper import model_patcher
from services.gcs_helper import (
get_storage_bucket,
gcs_upload,
check_asset_exists,
add_signed_url,
)

router = APIRouter(
prefix="/asset", tags=["asset"], dependencies=[Depends(viewer_function)]
)


# ======= Create =========
@router.post(
"/upload", status_code=HTTP_201_CREATED, dependencies=[Depends(admin_function)]
)
async def upload_asset(
bucket=Depends(get_storage_bucket), file: UploadFile = File(...)
):
uri, blob_name = gcs_upload(file, bucket)
return {
"uri": uri,
"storage_path": blob_name,
}

router = APIRouter(prefix="/asset", tags=["asset"])
GCS_BUCKET_NAME = os.environ.get("GCS_BUCKET_NAME")

@router.post("", status_code=HTTP_201_CREATED)
async def add_asset(
user: admin_dependency, session: session_dependency, asset_data: CreateAsset
) -> AssetResponse:

from google.cloud import storage
data = asset_data.model_dump()
thing_id = data.pop("thing_id", None)
storage_path = data["storage_path"]

# check to see if an asset entry already exists for
# this storage path and thing_id
existing_asset = check_asset_exists(session, storage_path, thing_id=thing_id)
if existing_asset:
# If an asset already exists, return it
return existing_asset

def get_storage_bucket() -> storage.Bucket:
client = storage.Client.from_service_account_json(
os.environ.get("GOOGLE_APPLICATION_CREDENTIALS")
)
bucket = client.bucket(GCS_BUCKET_NAME)
return bucket
data["storage_service"] = "gcs"
asset = Asset(**data)
audit_add(user, asset)

if thing_id:
assoc = AssetThingAssociation()
audit_add(user, assoc)
thing = session.get(Thing, thing_id)
assoc.thing = thing
assoc.asset = asset
session.add(assoc)

session.add(asset)
session.commit()
session.refresh(asset)
return asset


# ======= Read =========
@router.get("")
async def list_assets(
session: session_dependency,
# bucket=Depends(get_storage_bucket),
thing_id: int = None,
session: session_dependency, thing_id: int = None
) -> CustomPage[AssetResponse]:
"""
List all assets or assets associated with a specific thing.
Expand All @@ -60,17 +106,10 @@ async def list_assets(
AssetThingAssociation.thing_id == thing_id
)

# assets = session.scalars(sql).all()
# if not assets:
# return []

def transformer(assets: List[Asset]) -> AssetResponse:
# blob = bucket.blob(asset.storage_path)
# asset.url = blob.generate_signed_url(expiration=timedelta(minutes=10), method="GET")
# return [AssetResponse.model_validate(asset) for asset in assets]
for a in assets:
a.url = f"https://storage.googleapis.com/{GCS_BUCKET_NAME}/{a.storage_path}"
return assets
def transformer(a):
if thing_id is not None:
add_signed_url(a, get_storage_bucket())
return a

return paginate(query=sql, conn=session, transformer=transformer)

Expand All @@ -79,10 +118,8 @@ def transformer(assets: List[Asset]) -> AssetResponse:
async def get_asset(
asset_id: int,
session: session_dependency,
bucket=Depends(
get_storage_bucket
), # Assuming get_storage_bucket is defined elsewhere
thing_id: int = None,
bucket=Depends(get_storage_bucket),
) -> AssetResponse:
"""
Retrieve an asset by its ID.
Expand All @@ -99,59 +136,22 @@ async def get_asset(
if not asset:
raise HTTPException(status_code=404, detail="Asset not found")

blob = bucket.blob(asset.storage_path)
asset.url = blob.generate_signed_url(expiration=timedelta(minutes=10), method="GET")
return asset


@router.post("/upload", status_code=HTTP_201_CREATED)
async def upload_asset(
bucket=Depends(get_storage_bucket), file: UploadFile = File(...)
):
file_id = str(uuid4())
blob_name = f"uploads/{file_id}_{file.filename}"
blob = bucket.blob(blob_name)
blob.upload_from_file(file.file, content_type=file.content_type)
return {
"url": blob.generate_signed_url(expiration=timedelta(minutes=10), method="GET"),
"storage_path": blob_name,
}


@router.post("", status_code=HTTP_201_CREATED)
async def add_asset(
session: session_dependency, asset_data: CreateAsset
) -> AssetResponse:

data = asset_data.model_dump()
thing_id = data.pop("thing_id", None)
url = data.pop("url", "")

data["storage_service"] = "gcs"
asset = Asset(**data)

if thing_id:
assoc = AssetThingAssociation()
thing = session.get(Thing, thing_id)
assoc.thing = thing
assoc.asset = asset
session.add(assoc)

session.add(asset)
session.commit()
session.refresh(asset)
asset.url = url
add_signed_url(asset, bucket)
return asset


# ======= Update =========
@router.patch("/{asset_id}")
async def update_asset(
asset_id: int, session: session_dependency, asset_data: UpdateAsset
asset_id: int,
session: session_dependency,
asset_data: UpdateAsset,
user: editor_dependency,
):
"""
Update an existing asset.
"""
return model_patcher(session, Asset, asset_id, asset_data)
return model_patcher(session, Asset, asset_id, asset_data, user=user)


# ============= EOF =============================================
34 changes: 33 additions & 1 deletion api/geospatial.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,18 @@
import json
from typing import Annotated, List, Union

from fastapi import APIRouter, Query
from fastapi import APIRouter, Query, HTTPException
from fastapi.responses import FileResponse
from geoalchemy2.shape import to_shape
from shapely.io import to_geojson

# from starlette.responses import FileResponse

from core.dependencies import session_dependency
from db import Group
from schemas.thing import FeatureCollectionResponse
from services.geospatial_helper import create_shapefile, get_thing_features
from services.query_helper import simple_get_by_id

router = APIRouter(prefix="/geospatial", tags=["geospatial"])

Expand Down Expand Up @@ -55,6 +59,34 @@ async def get_geospatial(
return get_location_shapefile(session, thing_type, group)


@router.get("/project-area/{group_id}", summary="Get project area for group")
async def get_project_area(
session: session_dependency, group_id: int
) -> FeatureCollectionResponse:

group = simple_get_by_id(session, Group, group_id)

if not group.project_area:
raise HTTPException(status_code=404, detail="Group has no project area")

features = [
{
"type": "Feature",
"geometry": json.loads(to_geojson(to_shape(group.project_area))),
"properties": {
"group_id": group.id,
"group_name": group.name,
"group_description": group.description,
},
}
]

return {
"type": "FeatureCollection",
"features": features,
}


def get_feature_collection(
session: session_dependency,
thing_type: List[str] | None = None,
Expand Down
Loading
Loading