Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
113 changes: 113 additions & 0 deletions .github/workflows/build-flask.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
name: build-flask

on:
push:
branches:
- feature/add_flask
pathes:
- flask/*

env:
REGISTRY_IMAGE: a901002666/flask-debug

jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
platform:
- linux/amd64
- linux/arm64
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV

- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY_IMAGE }}
tags: |
type=semver,pattern={{major}}.{{minor}},value=1.1.0
type=sha,format=short,prefix=sha-

- name: Set up QEMU
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Build and push by digest
id: build
uses: docker/build-push-action@v6
with:
context: flask/
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true

- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"

- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1

merge:
runs-on: ubuntu-latest
needs:
- build
steps:
- name: Download digests
uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY_IMAGE }}
tags: |
type=semver,pattern={{major}}.{{minor}},value=1.1.0
type=sha,format=short,prefix=sha-

- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Create manifest list and push
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)

- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }}
2 changes: 2 additions & 0 deletions flask/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
__pycache__/*

59 changes: 59 additions & 0 deletions flask/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
ARG base=python:3.11.3-alpine3.17

###

FROM ${base} AS base

FROM base AS poetry

ARG MAKEFLAGS
ARG POETRY_VERSION=1.8.2

ENV MAKEFLAGS=${MAKEFLAGS}
ENV POETRY_VERSION=${POETRY_VERSION}

RUN apk add --no-cache --virtual .build-deps \
curl \
build-base \
libffi-dev && \
curl -sSL https://install.python-poetry.org | python && \
apk del .build-deps
###

FROM base AS builder
WORKDIR /usr/src/app

ENV PATH=/root/.local/bin:$PATH
ENV POETRY_VIRTUALENVS_CREATE=false
ENV PIP_DISABLE_PIP_VERSION_CHECK=on

COPY --from=poetry /root/.local /root/.local

COPY pyproject.toml .
COPY poetry.lock .

RUN apk add --no-cache --virtual .build-deps && \
poetry install -vv -n --only=main --no-root && \
# Whitelist removal
find /usr/local -type f -name "*.pyc" -delete && \
find /usr/local -type f -name "*.pyo" -delete && \
find /usr/local -type d -name "__pycache__" -delete && \
find /usr/local -type d -name "tests" -exec rm -rf '{}' + && \
apk del .build-deps

###

FROM base

WORKDIR /usr/src/app

ENV PYTHONUNBUFFERED=1

EXPOSE 8000/tcp
ENTRYPOINT ["flask"]
CMD ["run", "--host", "0.0.0.0", "-p", "8000"]


COPY --from=poetry /root/.local /root/.local
COPY --from=builder /usr/local /usr/local
COPY . /usr/src/app
151 changes: 151 additions & 0 deletions flask/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@

import time
import werkzeug
import datetime
import json

from flask import Flask, request, current_app, wrappers

def to_int(value, default=None):
if isinstance(value, datetime.timedelta):
value = value.total_seconds()

try:
return int(float(value))
except (ValueError, TypeError):
pass
return default


class CacheControlResponseMixin:
cache_control_class = werkzeug.datastructures.ResponseCacheControl

@property
def cache_control(self) -> werkzeug.datastructures.ResponseCacheControl:
# REF: https://github.com/pallets/werkzeug/blob/2.1.2/src/werkzeug/sansio/response.py#L484
def on_update(cache_control: werkzeug.datastructures.ResponseCacheControl) -> None:
if not cache_control and "cache-control" in self.headers:
del self.headers["cache-control"]
elif cache_control:
# NOTE: Private takes precedence over public
if cache_control.public and cache_control.private:
cache_control.public = False

self.headers["Cache-Control"] = cache_control.to_header()

return werkzeug.http.parse_cache_control_header(
self.headers.get("cache-control"), on_update, self.cache_control_class,
)


class ResponseCacheControl(werkzeug.datastructures.ResponseCacheControl):
private = werkzeug.datastructures.cache_control.cache_control_property(
'private', None, bool,
)
stale_while_revalidate = werkzeug.datastructures.cache_control.cache_control_property(
'stale-while-revalidate', None, to_int,
)
max_age = werkzeug.datastructures.cache_control.cache_control_property('max-age', -1, to_int)
s_maxage = werkzeug.datastructures.cache_control.cache_control_property('s-maxage', None, to_int)


class Response(
CacheControlResponseMixin,
wrappers.Response,
):
cache_control_class = ResponseCacheControl


class Flask(Flask):
response_class = Response

def make_response(self, *args, status=None, content_type: str=None, mimetype: str=None):
"""
Custom response serialization for specific types

flask.make_response only supports positional args
"""
if not args:
return self.response_class(
status=status, content_type=content_type, mimetype=mimetype,
)

response, *_ = args

response_type = type(response)

if response_type in {dict, list}:
response = self.response_class(
json.dumps(response),
content_type=content_type,
mimetype='application/json',
status=status,
)

return response

response = super().make_response(*args)
if status:
response.status = status
if content_type:
response.content_type = content_type
if mimetype:
response.mimetype = mimetype
return response


app = Flask(__name__)

@app.route('/')
def hello_world():
return '<p>Hello, World!</p>'


@app.route('/now')
def now():
now = time.time()
response = current_app.make_response({'ts': now})
response.cache_control.public = True
response.cache_control.max_age = 60
response.cache_control.s_maxage = 10
response.cache_control.stale_while_revalidate = 30
return response


@app.route('/now-private')
def now_private():
now = time.time()
response = current_app.make_response({'ts': now})
response.cache_control.private = True
response.cache_control.stale_while_revalidate = 30
return response


@app.route('/now-v2')
def now_v2():
now = time.time()
response = current_app.make_response({'ts': now})

public = bool(request.args.get('public'))
private = bool(request.args.get('private'))

swr = (swr := request.args.get('swr')) and int(swr)
max_age = (max_age := request.args.get('max_age')) and int(max_age)
s_maxage = (s_maxage := request.args.get('s_maxage')) and int(s_maxage)

if public:
response.cache_control.public = True

if private:
response.cache_control.private = True

if swr:
response.cache_control.stale_while_revalidate = swr

if max_age:
response.cache_control.max_age = max_age

if s_maxage:
response.cache_control.s_maxage = s_maxage

return response
12 changes: 12 additions & 0 deletions flask/compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
version: '3.6'

services:
api:
build: .
command: ['flask', 'run', '--host', '0.0.0.0', '--port', '8000', '--reload']
# env_file:
# - .env
volumes:
- ./:/usr/src/app
ports:
- 8000:8000
Loading