Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
danyiimp committed Sep 2, 2024
0 parents commit 08168d1
Show file tree
Hide file tree
Showing 66 changed files with 2,225 additions and 0 deletions.
33 changes: 33 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Continious Integration

on:
push:

jobs:
run-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up mongo
run: |
docker run --rm -d -p 27017:27017 mongo:latest
- name: Set up imgproxy
working-directory: imgproxy
run: |
docker run --rm -d --env-file env.list -v $(pwd)/data:/data:ro -v $(pwd)/filesystem:/sharedfs:ro -p 8080:8080 flagmansupport/imgproxy:latest
- name: Set up python
uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Install dependencies
run: |
python -m pip install -q --upgrade pip
python -m pip install -q -r requirements.txt -r requirements_dev.txt
- name: Run tests
run: |
pytest
158 changes: 158 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
.vscode
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
.python-version

# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock

# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# pytype static type analyzer
.pytype/

# Cython debug symbols
cython_debug/

# CMake
CMakeFiles/
CMakeCache.txt
CMakeScripts/
CTestTestfile.cmake
cmake_install.cmake
install_manifest.txt
Makefile
*.cmake
*.pot

# Poetry
# According to pypa/poetry#1915, it is recommended to include poetry.lock in version control.
# However, in case of collaboration, if having poetry.lock present causes conflicts, package
# managers like pipenv and pip-tools recommend to include it nevertheless.
#poetry.lock

# Pylance (https://github.com/microsoft/pylance-release)
# Pylance is a static type checker for Python. It is recommended to include its config file
# in version control when using Pylance. Following entries were added after running Pylance
# in a fresh project.
.pylance-config.json
local_storage/
project_context.txt

.DS_Store
10 changes: 10 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
FROM python:3.11-alpine

RUN mkdir /app
WORKDIR /app

COPY . .

RUN pip install -r requirements.txt

CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8080", "--workers", "1"]
14 changes: 14 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from fastapi import FastAPI

from app.api.processing import router as processing_router
from app.api.images import router as images_router


app = FastAPI(
title="MediaFlower API",
description="An API for processing and managing images, including variant creation and storage management.", # noqa
version="1.0.0",
)

app.include_router(processing_router, prefix="/images", tags=["Create"])
app.include_router(images_router, tags=["Images"])
Empty file added app/api/__init__.py
Empty file.
18 changes: 18 additions & 0 deletions app/api/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials

from app.core.settings import USERNAME, PASSWORD

security = HTTPBasic()


def authorize(
credentials: HTTPBasicCredentials = Depends(security),
):
if credentials.username != USERNAME or credentials.password != PASSWORD:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Basic"},
)
return credentials.username
70 changes: 70 additions & 0 deletions app/api/images.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
from fastapi import APIRouter, HTTPException, Query, Depends, Response, status
from typing import Optional

from app.core.db_init import db
from app.models.db.image_metadata import ImageMetadataDB
from app.api.auth import authorize
from app.models.py_object_id import PyObjectId
from app.schemas.image_metadata import ImagesResponse
from app.services.db_operations import (
mark_image_to_delete,
ImageNotFoundError,
ImageAlreadyDeletedError,
)

router = APIRouter()


@router.get(
"/images",
summary="Get Images",
description="Retrieve a list of images with optional filtering by project name and image name.", # noqa
response_description="A list of images with pagination details.",
)
async def get_images(
project_name: Optional[str] = None,
image_name: Optional[str] = None,
page: int = Query(1, ge=1),
limit: int = Query(10, ge=1, le=100),
_: str = Depends(authorize),
):
query = {"deleted": False}
if project_name:
query["project_name"] = project_name
if image_name:
query["image_name"] = image_name

skip = (page - 1) * limit
cursor = db.images.find(query).skip(skip).limit(limit)
total_count = await db.images.count_documents(query)
images = await cursor.to_list(length=limit)

images = [ImageMetadataDB(**image).model_dump() for image in images]
return ImagesResponse(
page=page, limit=limit, total_count=total_count, images=images
)


@router.delete(
"/image/{id}",
summary="Delete Image",
description="Mark image as deleted by its ID.",
response_description="The image has been marked as deleted.",
status_code=status.HTTP_204_NO_CONTENT,
)
async def delete_image(
id: PyObjectId,
_: str = Depends(authorize),
):
try:
await mark_image_to_delete(id)
except ImageNotFoundError as e:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=str(e)
)
except ImageAlreadyDeletedError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)
)

return Response(status_code=status.HTTP_204_NO_CONTENT)
86 changes: 86 additions & 0 deletions app/api/processing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from typing import Annotated
from fastapi import APIRouter, UploadFile, File, Query, Depends
from fastapi.responses import Response

from app.models.filetype_metadata import FileTypeMetadata
from app.services.image_processing.service import handle_image_processing
from app.helpers.image_downloader import download_image
from app.services.s3_operations import download_image_from_s3
from app.api.auth import authorize
from app.helpers.make_dependable import make_dependable

router = APIRouter()


@router.post(
"/file",
summary="Create from Image File",
description="Upload and process an image file, generating various image variants.", # noqa
response_description="The metadata of the processed image.",
)
async def process_image_file(
project_name: str,
image_type: str,
file_type_metadata: Annotated[
FileTypeMetadata, Depends(make_dependable(FileTypeMetadata))
],
file: UploadFile = File(...),
_: str = Depends(authorize),
):
raw_bytes = await file.read()
response = await handle_image_processing(
project_name, image_type, raw_bytes, file_type_metadata
)
return Response(
content=response.model_dump_json(), media_type="application/json"
)


@router.post(
"/url",
summary="Create from Image URL",
description="Download and process an image from a URL, generating various image variants.", # noqa
response_description="The metadata of the processed image.",
)
async def process_image_url(
project_name: str,
image_type: str,
file_type_metadata: Annotated[
FileTypeMetadata, Depends(make_dependable(FileTypeMetadata))
],
url: str = Query(...),
_: str = Depends(authorize),
):
raw_bytes = await download_image(url)
response = await handle_image_processing(
project_name, image_type, raw_bytes, file_type_metadata
)
return Response(
content=response.model_dump_json(), media_type="application/json"
)


@router.post(
"/s3",
summary="Create from S3 Image",
description="Download and process an image from S3, generating various image variants.", # noqa
response_description="The metadata of the processed image.",
)
async def process_image_s3(
project_name: str,
image_type: str,
file_type_metadata: Annotated[
FileTypeMetadata, Depends(make_dependable(FileTypeMetadata))
],
s3_bucket: str,
s3_key: str,
_: str = Depends(authorize),
):
raw_bytes = await download_image_from_s3(s3_bucket, s3_key)
response = await handle_image_processing(
project_name, image_type, raw_bytes, file_type_metadata
)

return Response(
content=response.model_dump_json(), media_type="application/json"
)
Empty file added app/core/__init__.py
Empty file.
Loading

0 comments on commit 08168d1

Please sign in to comment.