Skip to content

Commit d0d4df3

Browse files
committed
fix: fixed asset. added gcs_helper. added a demo transfer_assets function
1 parent 0ac4d9c commit d0d4df3

5 files changed

Lines changed: 167 additions & 91 deletions

File tree

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ tests/uploads
2323
migrate.sh
2424
launcher.sh
2525
gcs_credentials.json
26-
26+
transfers/data/assets*
2727

2828
# deployment files
2929
app.yaml

api/asset.py

Lines changed: 53 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -24,32 +24,67 @@
2424
from starlette.status import HTTP_201_CREATED
2525

2626
from api.pagination import CustomPage
27-
from core.dependencies import session_dependency
27+
from core.dependencies import session_dependency, viewer_function, admin_dependency, admin_function, editor_dependency
2828
from db import Thing
2929
from db.asset import Asset, AssetThingAssociation
3030
from schemas.asset import AssetResponse, CreateAsset, UpdateAsset
31+
from services.audit_helper import audit_add
3132
from services.crud_helper import model_patcher
33+
from services.gcs_helper import GCS_BUCKET_NAME, get_storage_bucket, gcs_upload, set_asset_url
3234

33-
router = APIRouter(prefix="/asset", tags=["asset"])
34-
GCS_BUCKET_NAME = os.environ.get("GCS_BUCKET_NAME")
35+
router = APIRouter(prefix="/asset", tags=["asset"],
36+
dependencies=[Depends(viewer_function)])
3537

3638

37-
from google.cloud import storage
39+
# ======= Create =========
40+
@router.post("/upload", status_code=HTTP_201_CREATED,
41+
dependencies=[Depends(admin_function)])
42+
async def upload_asset(
43+
bucket=Depends(get_storage_bucket), file: UploadFile = File(...)
44+
):
45+
signed_url, blob_name = gcs_upload(bucket, file)
46+
return {
47+
"url": signed_url,
48+
"storage_path": blob_name,
49+
50+
}
51+
52+
53+
@router.post("", status_code=HTTP_201_CREATED)
54+
async def add_asset(
55+
user: admin_dependency,
56+
session: session_dependency, asset_data: CreateAsset
57+
) -> AssetResponse:
58+
59+
data = asset_data.model_dump()
60+
thing_id = data.pop("thing_id", None)
61+
url = data.pop("url", "")
3862

63+
data["storage_service"] = "gcs"
64+
asset = Asset(**data)
65+
audit_add(user, asset)
3966

40-
def get_storage_bucket() -> storage.Bucket:
41-
client = storage.Client.from_service_account_json(
42-
os.environ.get("GOOGLE_APPLICATION_CREDENTIALS")
43-
)
44-
bucket = client.bucket(GCS_BUCKET_NAME)
45-
return bucket
67+
if thing_id:
68+
assoc = AssetThingAssociation()
69+
audit_add(user, assoc)
70+
thing = session.get(Thing, thing_id)
71+
assoc.thing = thing
72+
assoc.asset = asset
73+
session.add(assoc)
74+
75+
session.add(asset)
76+
session.commit()
77+
session.refresh(asset)
78+
asset.url = url
79+
return asset
4680

4781

82+
# ======= Read =========
4883
@router.get("")
4984
async def list_assets(
5085
session: session_dependency,
51-
# bucket=Depends(get_storage_bucket),
5286
thing_id: int = None,
87+
bucket=Depends(get_storage_bucket) # Assuming get_storage_bucket is defined elsewhere
5388
) -> CustomPage[AssetResponse]:
5489
"""
5590
List all assets or assets associated with a specific thing.
@@ -60,16 +95,10 @@ async def list_assets(
6095
AssetThingAssociation.thing_id == thing_id
6196
)
6297

63-
# assets = session.scalars(sql).all()
64-
# if not assets:
65-
# return []
6698

6799
def transformer(assets: List[Asset]) -> AssetResponse:
68-
# blob = bucket.blob(asset.storage_path)
69-
# asset.url = blob.generate_signed_url(expiration=timedelta(minutes=10), method="GET")
70-
# return [AssetResponse.model_validate(asset) for asset in assets]
71100
for a in assets:
72-
a.url = f"https://storage.googleapis.com/{GCS_BUCKET_NAME}/{a.storage_path}"
101+
set_asset_url(a, bucket)
73102
return assets
74103

75104
return paginate(query=sql, conn=session, transformer=transformer)
@@ -79,9 +108,7 @@ def transformer(assets: List[Asset]) -> AssetResponse:
79108
async def get_asset(
80109
asset_id: int,
81110
session: session_dependency,
82-
bucket=Depends(
83-
get_storage_bucket
84-
), # Assuming get_storage_bucket is defined elsewhere
111+
bucket=Depends(get_storage_bucket), # Assuming get_storage_bucket is defined elsewhere
85112
thing_id: int = None,
86113
) -> AssetResponse:
87114
"""
@@ -99,59 +126,21 @@ async def get_asset(
99126
if not asset:
100127
raise HTTPException(status_code=404, detail="Asset not found")
101128

102-
blob = bucket.blob(asset.storage_path)
103-
asset.url = blob.generate_signed_url(expiration=timedelta(minutes=10), method="GET")
104-
return asset
105-
106-
107-
@router.post("/upload", status_code=HTTP_201_CREATED)
108-
async def upload_asset(
109-
bucket=Depends(get_storage_bucket), file: UploadFile = File(...)
110-
):
111-
file_id = str(uuid4())
112-
blob_name = f"uploads/{file_id}_{file.filename}"
113-
blob = bucket.blob(blob_name)
114-
blob.upload_from_file(file.file, content_type=file.content_type)
115-
return {
116-
"url": blob.generate_signed_url(expiration=timedelta(minutes=10), method="GET"),
117-
"storage_path": blob_name,
118-
}
119-
120-
121-
@router.post("", status_code=HTTP_201_CREATED)
122-
async def add_asset(
123-
session: session_dependency, asset_data: CreateAsset
124-
) -> AssetResponse:
125-
126-
data = asset_data.model_dump()
127-
thing_id = data.pop("thing_id", None)
128-
url = data.pop("url", "")
129-
130-
data["storage_service"] = "gcs"
131-
asset = Asset(**data)
129+
set_asset_url(asset, bucket)
132130

133-
if thing_id:
134-
assoc = AssetThingAssociation()
135-
thing = session.get(Thing, thing_id)
136-
assoc.thing = thing
137-
assoc.asset = asset
138-
session.add(assoc)
139131

140-
session.add(asset)
141-
session.commit()
142-
session.refresh(asset)
143-
asset.url = url
144132
return asset
145133

146-
134+
# ======= Update =========
147135
@router.patch("/{asset_id}")
148136
async def update_asset(
149-
asset_id: int, session: session_dependency, asset_data: UpdateAsset
137+
asset_id: int, session: session_dependency, asset_data: UpdateAsset,
138+
user: editor_dependency
150139
):
151140
"""
152141
Update an existing asset.
153142
"""
154-
return model_patcher(session, Asset, asset_id, asset_data)
143+
return model_patcher(session, Asset, asset_id, asset_data, user=user)
155144

156145

157146
# ============= EOF =============================================

db/asset.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,11 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515
# ===============================================================================
16+
from typing import Optional
17+
1618
from sqlalchemy import Column, String, Integer, ForeignKey
17-
from sqlalchemy.orm import relationship
19+
from sqlalchemy.orm import relationship, Mapped
20+
from sqlalchemy.testing.schema import mapped_column
1821
from sqlalchemy_utils import TSVectorType
1922

2023
from db.base import Base, AutoBaseMixin
@@ -27,14 +30,14 @@ class Asset(Base, AutoBaseMixin):
2730
# content = Column(UploadedFileField)
2831
# photo = Column(UploadedFileField(upload_type=UploadedImageWithThumb))
2932

30-
name = Column(String, nullable=False)
31-
label = Column(String, nullable=True)
32-
storage_service = Column(String, nullable=False)
33-
storage_path = Column(String, nullable=False)
34-
mime_type = Column(String, nullable=False)
35-
size = Column(Integer, nullable=False)
33+
name: Mapped[str]=mapped_column(String, nullable=False)
34+
label: Mapped[Optional[str]]=mapped_column(String, nullable=True)
35+
storage_service: Mapped[str]=mapped_column(String, nullable=False)
36+
storage_path: Mapped[str]=mapped_column(String, nullable=False)
37+
mime_type: Mapped[str]=mapped_column(String, nullable=False)
38+
size: Mapped[int]=mapped_column(Integer, nullable=False)
3639

37-
search_vector = Column(
40+
search_vector: Mapped[TSVectorType]=mapped_column(
3841
TSVectorType("name", "mime_type", "storage_service", "storage_path")
3942
)
4043

services/gcs_helper.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# ===============================================================================
2+
# Copyright 2025 ross
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
# ===============================================================================
16+
import os
17+
from datetime import timedelta
18+
from uuid import uuid4
19+
20+
from fastapi import File, UploadFile
21+
22+
GCS_BUCKET_NAME = os.environ.get("GCS_BUCKET_NAME")
23+
24+
25+
from google.cloud import storage
26+
27+
28+
def get_storage_bucket() -> storage.Bucket:
29+
client = storage.Client.from_service_account_json(
30+
os.environ.get("GOOGLE_APPLICATION_CREDENTIALS")
31+
)
32+
bucket = client.bucket(GCS_BUCKET_NAME)
33+
return bucket
34+
35+
36+
def gcs_upload(file: UploadFile, bucket: storage.Bucket=None):
37+
if bucket is None:
38+
bucket = get_storage_bucket()
39+
40+
file_id = str(uuid4())
41+
head, extension = os.path.splitext(file.filename)
42+
43+
blob_name = f"uploads/{head}_{file_id}{extension}"
44+
blob = bucket.blob(blob_name)
45+
blob.upload_from_file(file.file, content_type=file.content_type)
46+
signed_url = blob.generate_signed_url(expiration=timedelta(minutes=10), method="GET")
47+
48+
return signed_url, blob_name
49+
50+
def set_asset_url(asset, bucket=None):
51+
if bucket is None:
52+
bucket = get_storage_bucket()
53+
blob = bucket.blob(asset.storage_path)
54+
asset.url = blob.generate_signed_url(expiration=timedelta(minutes=10), method="GET")
55+
# ============= EOF =============================================

transfers/transfer2.py

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515
# ===============================================================================
16-
16+
import os
1717
import time
1818
import uuid
1919
from datetime import datetime
@@ -25,6 +25,7 @@
2525
from shapely import Point
2626
from shapely.ops import transform
2727
from sqlalchemy import select
28+
from starlette.datastructures import UploadFile
2829

2930
from core.app import init_lexicon
3031
from db import (
@@ -41,10 +42,12 @@
4142
ThingContactAssociation,
4243
Base,
4344
Sensor,
44-
Address,
45+
Address, Asset, AssetThingAssociation,
4546
)
4647
from db.engine import session_ctx
4748
from schemas.thing import CreateWellScreen
49+
from services.audit_helper import audit_add
50+
from services.gcs_helper import gcs_upload
4851

4952
# from db.observation.groundwaterlevel import GroundwaterLevelObservation
5053

@@ -457,6 +460,32 @@ def transfer_wellscreens(session, limit=None):
457460
continue
458461
# session.add(screen)
459462

463+
def transfer_assets(session):
464+
for p in ('asset1.png', 'asset2.png', 'asset3.png'):
465+
with open(f"./data/assets/{p}", "rb") as f:
466+
uf = UploadFile(file=f,
467+
filename=p,
468+
size=10)
469+
url, blob_name = gcs_upload(uf)
470+
thing_id = 151
471+
asset = Asset(
472+
name=p,
473+
label=p,
474+
storage_path=blob_name,
475+
storage_service='gcs',
476+
mime_type='image/png',
477+
size=uf.size
478+
)
479+
assoc = AssetThingAssociation()
480+
audit_add({'sub': 'foobar',
481+
'name': 'Mr. Foobar'}, assoc)
482+
thing = session.get(Thing, thing_id)
483+
assoc.thing = thing
484+
assoc.asset = asset
485+
session.add(assoc)
486+
session.add(asset)
487+
session.commit()
488+
460489

461490
def init_sensor(session):
462491
sensor = Sensor()
@@ -471,20 +500,20 @@ def init_sensor(session):
471500
if __name__ == "__main__":
472501

473502
with session_ctx() as sess:
474-
Base.metadata.drop_all(sess.bind)
475-
Base.metadata.create_all(sess.bind)
476-
477-
init_lexicon("../core/lexicon.json")
478-
479-
init_sensor(sess)
480-
transfer_wells(sess, 1000)
481-
transfer_springs(sess, limit=1000)
482-
transfer_perennial_stream(sess)
483-
transfer_ephemeral_stream(sess)
484-
transfer_met(sess)
485-
486-
transfer_owners(sess)
487-
transfer_wellscreens(sess)
488-
transfer_water_levels(sess)
489-
503+
# Base.metadata.drop_all(sess.bind)
504+
# Base.metadata.create_all(sess.bind)
505+
#
506+
# init_lexicon("../core/lexicon.json")
507+
#
508+
# init_sensor(sess)
509+
# transfer_wells(sess, 1000)
510+
# transfer_springs(sess, limit=1000)
511+
# transfer_perennial_stream(sess)
512+
# transfer_ephemeral_stream(sess)
513+
# transfer_met(sess)
514+
#
515+
# transfer_owners(sess)
516+
# transfer_wellscreens(sess)
517+
# transfer_water_levels(sess)
518+
transfer_assets(sess)
490519
# ============= EOF =============================================

0 commit comments

Comments
 (0)