Skip to content

Commit

Permalink
Merge pull request #15 from ks6088ts-labs/feature/issue-14_add-blob-s…
Browse files Browse the repository at this point in the history
…ervice

add blob service
  • Loading branch information
ks6088ts authored Oct 18, 2024
2 parents 66eeafd + 32cdaf3 commit b994186
Show file tree
Hide file tree
Showing 14 changed files with 355 additions and 51 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ COPY . .
# Install dependencies
RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt

CMD ["python", "workshop_azure_iot/core.py"]
CMD ["uvicorn", "workshop_azure_iot.core:app", "--host", "0.0.0.0", "--port", "8000"]
13 changes: 10 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ ci-test: install-deps-dev format-check lint test ## run CI tests
# ---
DOCKER_REPO_NAME ?= ks6088ts
DOCKER_IMAGE_NAME ?= workshop-azure-iot
DOCKER_COMMAND ?= python main.py --help
DOCKER_COMMAND ?=

# Tools
TOOLS_DIR ?= $(HOME)/.local/bin
Expand All @@ -65,7 +65,14 @@ docker-build: ## build Docker image

.PHONY: docker-run
docker-run: ## run Docker container
docker run --rm $(DOCKER_REPO_NAME)/$(DOCKER_IMAGE_NAME):$(GIT_TAG) $(DOCKER_COMMAND)
docker run --rm \
-v $(PWD)/ai_services.env:/app/ai_services.env \
-v $(PWD)/blob_storage.env:/app/blob_storage.env \
-v $(PWD)/core.env:/app/core.env \
-v $(PWD)/iot_hub.env:/app/iot_hub.env \
-p 8000:8000 \
$(DOCKER_REPO_NAME)/$(DOCKER_IMAGE_NAME):$(GIT_TAG) \
$(DOCKER_COMMAND)

.PHONY: docker-lint
docker-lint: ## lint Dockerfile
Expand All @@ -78,7 +85,7 @@ docker-scan: ## scan Docker image
trivy image $(DOCKER_REPO_NAME)/$(DOCKER_IMAGE_NAME):$(GIT_TAG)

.PHONY: ci-test-docker
ci-test-docker: docker-lint docker-build docker-scan docker-run ## run CI test for Docker
ci-test-docker: docker-lint docker-build docker-scan ## run CI test for Docker

# ---
# Docs
Expand Down
3 changes: 3 additions & 0 deletions blob_storage.env.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
BLOB_STORAGE_ACCOUNT_NAME="account_name"
BLOB_STORAGE_CONTAINER_NAME="dev"
BLOB_STORAGE_SAS_TOKEN="sp=racwdli&st=2024-10-17T23:32:36Z&se=2024-10-18T07:32:36Z&spr=https&sv=2022-11-02&sr=c&sig=CHANGE_ME"
37 changes: 0 additions & 37 deletions main.py

This file was deleted.

191 changes: 190 additions & 1 deletion poetry.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ azure-functions = "^1.21.3"
pydantic-settings = "^2.5.2"
azure-iot-device = "^2.14.0"
openai = "^1.51.2"
azure-storage-blob = "^12.23.1"

[tool.poetry.group.dev.dependencies]
pre-commit = "^4.0.1"
Expand Down
6 changes: 3 additions & 3 deletions tests/test_ai_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@


@pytest.mark.skipif(SKIP_TEST, reason="need to launch the backend server first")
def test_ai_services():
path_format = "/ai_services/{0}"
def test_main():
path_format = "/ai_services{0}"
response = client.post(
url=path_format.format("chat/completions"),
url=path_format.format("/chat/completions"),
params={
"prompt": "Hello, how are you?",
},
Expand Down
19 changes: 19 additions & 0 deletions tests/test_blob_storage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from logging import getLogger

import pytest

from tests.utilities import SKIP_TEST, client

logger = getLogger(__name__)


@pytest.mark.skipif(SKIP_TEST, reason="need to launch the backend server first")
def test_main():
path_format = "/blob_storage{0}"

# list_images
response = client.get(
url=path_format.format("/images"),
)
assert response.status_code == 200
logger.info(f"response: {response.json()}")
6 changes: 3 additions & 3 deletions tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
logger = getLogger(__name__)


def test_core():
path_format = "/core/{0}"
def test_main():
path_format = "/core{0}"
response = client.get(
url=path_format.format("info"),
url=path_format.format("/info"),
)
assert response.status_code == 200
logger.info(f"response: {response.json()}")
6 changes: 3 additions & 3 deletions tests/test_iot.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@


@pytest.mark.skipif(SKIP_TEST, reason="need to launch the backend server first")
def test_iot():
path_format = "/iot_hub/{0}"
def test_main():
path_format = "/iot_hub{0}"
response = client.get(
url=path_format.format("device_twin"),
url=path_format.format("/device_twin"),
)
assert response.status_code == 200
logger.info(f"response: {response.json()}")
2 changes: 2 additions & 0 deletions workshop_azure_iot/core.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from fastapi import FastAPI

from workshop_azure_iot.routers.ai_services import router as ai_services_router
from workshop_azure_iot.routers.blob_storage import router as blob_storage_router
from workshop_azure_iot.routers.core import router as core_router
from workshop_azure_iot.routers.iot_hub import router as iot_hub_router

Expand All @@ -12,6 +13,7 @@
core_router,
iot_hub_router,
ai_services_router,
blob_storage_router,
# Add routers here
]:
app.include_router(router)
46 changes: 46 additions & 0 deletions workshop_azure_iot/internals/blob_storage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from logging import getLogger

from azure.storage.blob import BlobServiceClient

from workshop_azure_iot.settings.blob_storage import Settings

logger = getLogger(__name__)


class Client:
def __init__(self, settings: Settings) -> None:
self.settings = settings
self.blob_service_client = BlobServiceClient(
account_url=f"https://{self.settings.blob_storage_account_name}.blob.core.windows.net",
credential=self.settings.blob_storage_sas_token,
)

def download_blob_stream(
self,
blob_name: str,
) -> bytes:
blob_client = self.blob_service_client.get_blob_client(
container=self.settings.blob_storage_container_name,
blob=blob_name,
)
logger.info(f"Downloaded blob {blob_name} from container {self.settings.blob_storage_container_name}")
return blob_client.download_blob().readall()

def upload_blob_stream(
self,
blob_name: str,
stream: bytes,
) -> dict:
blob_client = self.blob_service_client.get_blob_client(
container=self.settings.blob_storage_container_name,
blob=blob_name,
)
logger.info(f"Uploaded blob {blob_name} to container {self.settings.blob_storage_container_name}")
return blob_client.upload_blob(stream, overwrite=True)

def list_blobs(
self,
) -> list:
container_client = self.blob_service_client.get_container_client(self.settings.blob_storage_container_name)
logger.info(f"Listed blobs in container {self.settings.blob_storage_container_name}")
return [blob.name for blob in container_client.list_blobs()]
65 changes: 65 additions & 0 deletions workshop_azure_iot/routers/blob_storage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from logging import getLogger

from fastapi import APIRouter, UploadFile, status
from fastapi.responses import JSONResponse, Response

from workshop_azure_iot.internals.blob_storage import Client
from workshop_azure_iot.settings.blob_storage import Settings

logger = getLogger(__name__)

client = Client(
settings=Settings(),
)

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


@router.get(
"/images/{blob_name}",
responses={200: {"content": {"image/jpeg": {}}}},
response_class=Response,
)
async def get_image(
blob_name: str,
):
image_bytes = client.download_blob_stream(
blob_name=blob_name,
)
return Response(
content=image_bytes,
media_type="image/jpeg",
)


@router.get(
"/images",
)
async def list_images():
return client.list_blobs()


@router.post(
"/images",
status_code=201,
)
async def upload_image(
file: UploadFile,
blob_name: str,
):
content = await file.read()
response = client.upload_blob_stream(
blob_name=blob_name,
stream=content,
)
logger.warning(f"Response: {response}, type: {type(response)}")
return JSONResponse(
status_code=status.HTTP_201_CREATED,
content={
"message": "Image uploaded successfully",
"etag": response.get("etag"),
},
)
9 changes: 9 additions & 0 deletions workshop_azure_iot/settings/blob_storage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
blob_storage_account_name: str
blob_storage_container_name: str
blob_storage_sas_token: str

model_config = SettingsConfigDict(env_file="blob_storage.env")

0 comments on commit b994186

Please sign in to comment.