Initial Commit
This commit is contained in:
commit
3796b4711e
8 changed files with 226 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
.venv
|
||||
venv
|
||||
.idea
|
||||
__*
|
15
Dockerfile
Normal file
15
Dockerfile
Normal file
|
@ -0,0 +1,15 @@
|
|||
FROM python:3.12.9-alpine
|
||||
|
||||
ENV APP_HOME=/home/app
|
||||
|
||||
WORKDIR $APP_HOME
|
||||
|
||||
COPY requirements.txt .
|
||||
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["uvicorn", "main:app", "--port", "8000", "--host", "0.0.0.0"]
|
14
docker-compose.yml
Normal file
14
docker-compose.yml
Normal file
|
@ -0,0 +1,14 @@
|
|||
services:
|
||||
image_processor:
|
||||
container_name: image_processor
|
||||
ports:
|
||||
- "8000:8000"
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
restart: unless-stopped
|
||||
|
||||
|
||||
|
||||
|
||||
|
9
exceptions.py
Normal file
9
exceptions.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
from fastapi import HTTPException, status
|
||||
|
||||
|
||||
class InternalServerErrorException(HTTPException):
|
||||
status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
detail = "Internal Server Error"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(status_code=self.status_code, detail=self.detail)
|
44
image_service.py
Normal file
44
image_service.py
Normal file
|
@ -0,0 +1,44 @@
|
|||
import io
|
||||
from urllib.parse import urljoin
|
||||
|
||||
import aioboto3
|
||||
from PIL import Image
|
||||
|
||||
import pillow_avif
|
||||
from pydantic import HttpUrl
|
||||
|
||||
from schemas import SImageWithFormat
|
||||
|
||||
SUPPORTED_FORMATS = {"avif", "png", "webp"}
|
||||
|
||||
def process_image(img_bytes: bytes, formats: set[str]) -> list[SImageWithFormat]:
|
||||
images = []
|
||||
image = Image.open(io.BytesIO(img_bytes))
|
||||
for image_format in formats:
|
||||
buffer = io.BytesIO()
|
||||
image.save(buffer, format=image_format)
|
||||
images.append(SImageWithFormat.model_validate({"image": buffer.getvalue(), "format": image_format}))
|
||||
|
||||
return images
|
||||
|
||||
async def upload_images(imgs: list[SImageWithFormat], name: str, upload_url: str, bucket_name: str) -> list[HttpUrl]:
|
||||
urls = []
|
||||
s3_session = aioboto3.Session()
|
||||
async with s3_session.client(
|
||||
"s3",
|
||||
endpoint_url=upload_url,
|
||||
) as s3_client:
|
||||
for img in imgs:
|
||||
object_key = f"{name}.{img.format}"
|
||||
|
||||
# await s3_client.put_object(
|
||||
# Bucket=bucket_name,
|
||||
# Key=object_key,
|
||||
# Body=img.image,
|
||||
# ContentType=f"image/{img.format}"
|
||||
# )
|
||||
|
||||
urls.append(HttpUrl(urljoin(upload_url, f"{bucket_name}/{object_key}")))
|
||||
|
||||
return urls
|
||||
|
52
main.py
Normal file
52
main.py
Normal file
|
@ -0,0 +1,52 @@
|
|||
from base64 import b64decode
|
||||
|
||||
from fastapi import FastAPI, status
|
||||
|
||||
from exceptions import InternalServerErrorException
|
||||
from image_service import SUPPORTED_FORMATS, process_image, upload_images
|
||||
from schemas import SUploadedImages, SFormats, SAvailableFormats, SInternalServerError
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.post(
|
||||
"/process_images",
|
||||
status_code=status.HTTP_200_OK,
|
||||
response_model=SUploadedImages,
|
||||
responses={
|
||||
status.HTTP_500_INTERNAL_SERVER_ERROR: {
|
||||
"model": SInternalServerError,
|
||||
"description": "Internal Server Error",
|
||||
},
|
||||
},
|
||||
)
|
||||
async def process_images(upload_data: SFormats):
|
||||
try:
|
||||
all_urls = []
|
||||
for image in upload_data.images:
|
||||
imgs = process_image(b64decode(image.image), upload_data.formats)
|
||||
urls = await upload_images(imgs, image.name, str(upload_data.upload_url), upload_data.bucket_name)
|
||||
all_urls.append(urls)
|
||||
|
||||
return {"images_urls": all_urls}
|
||||
except Exception as e:
|
||||
print(e)
|
||||
raise InternalServerErrorException
|
||||
|
||||
|
||||
|
||||
@app.get(
|
||||
"/available_formats",
|
||||
status_code=status.HTTP_200_OK,
|
||||
response_model=SAvailableFormats,
|
||||
responses={
|
||||
status.HTTP_500_INTERNAL_SERVER_ERROR: {
|
||||
"model": SInternalServerError,
|
||||
"description": "Internal Server Error",
|
||||
},
|
||||
},
|
||||
)
|
||||
async def get_available_formats():
|
||||
return {"available_formats": SUPPORTED_FORMATS}
|
||||
|
||||
|
61
requirements.txt
Normal file
61
requirements.txt
Normal file
|
@ -0,0 +1,61 @@
|
|||
aioboto3==14.1.0
|
||||
aiobotocore==2.21.1
|
||||
aiofiles==24.1.0
|
||||
aiohappyeyeballs==2.5.0
|
||||
aiohttp==3.11.13
|
||||
aioitertools==0.12.0
|
||||
aiosignal==1.3.2
|
||||
annotated-types==0.7.0
|
||||
anyio==4.8.0
|
||||
attrs==25.1.0
|
||||
boto3==1.37.1
|
||||
botocore==1.37.1
|
||||
certifi==2025.1.31
|
||||
click==8.1.8
|
||||
dnspython==2.7.0
|
||||
email_validator==2.2.0
|
||||
fastapi==0.115.11
|
||||
fastapi-cli==0.0.7
|
||||
frozenlist==1.5.0
|
||||
h11==0.14.0
|
||||
httpcore==1.0.7
|
||||
httptools==0.6.4
|
||||
httpx==0.28.1
|
||||
idna==3.10
|
||||
itsdangerous==2.2.0
|
||||
Jinja2==3.1.6
|
||||
jmespath==1.0.1
|
||||
markdown-it-py==3.0.0
|
||||
MarkupSafe==3.0.2
|
||||
mdurl==0.1.2
|
||||
multidict==6.1.0
|
||||
orjson==3.10.15
|
||||
pillow==11.1.0
|
||||
pillow-avif-plugin==1.4.6
|
||||
propcache==0.3.0
|
||||
pydantic==2.10.6
|
||||
pydantic-extra-types==2.10.2
|
||||
pydantic-settings==2.8.1
|
||||
pydantic_core==2.27.2
|
||||
Pygments==2.19.1
|
||||
python-dateutil==2.9.0.post0
|
||||
python-dotenv==1.0.1
|
||||
python-multipart==0.0.20
|
||||
PyYAML==6.0.2
|
||||
rich==13.9.4
|
||||
rich-toolkit==0.13.2
|
||||
s3transfer==0.11.3
|
||||
shellingham==1.5.4
|
||||
six==1.17.0
|
||||
sniffio==1.3.1
|
||||
starlette==0.46.0
|
||||
typer==0.15.2
|
||||
typing_extensions==4.12.2
|
||||
ujson==5.10.0
|
||||
urllib3==2.3.0
|
||||
uvicorn==0.34.0
|
||||
uvloop==0.21.0
|
||||
watchfiles==1.0.4
|
||||
websockets==15.0.1
|
||||
wrapt==1.17.2
|
||||
yarl==1.18.3
|
27
schemas.py
Normal file
27
schemas.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, HttpUrl
|
||||
|
||||
|
||||
class UploadImage(BaseModel):
|
||||
image: bytes
|
||||
name: str
|
||||
|
||||
class SFormats(BaseModel):
|
||||
formats: set[Literal["avif", "png", "webp"]]
|
||||
images: list[UploadImage]
|
||||
upload_url: HttpUrl
|
||||
bucket_name: str
|
||||
|
||||
class SUploadedImages(BaseModel):
|
||||
images_urls: list[list[HttpUrl]]
|
||||
|
||||
class SAvailableFormats(BaseModel):
|
||||
available_formats: set[str]
|
||||
|
||||
class SImageWithFormat(BaseModel):
|
||||
image: bytes
|
||||
format: str
|
||||
|
||||
class SInternalServerError(BaseModel):
|
||||
detail: str = "Internal Server Error"
|
Loading…
Add table
Reference in a new issue