Skip to content

Commit

Permalink
Rework following review
Browse files Browse the repository at this point in the history
  • Loading branch information
benoit74 committed Sep 17, 2024
1 parent ee6ce35 commit 5d66daf
Show file tree
Hide file tree
Showing 27 changed files with 276 additions and 213 deletions.
33 changes: 28 additions & 5 deletions .github/workflows/Publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,42 @@ jobs:
- name: Retrieve source code
uses: actions/checkout@v4

- name: Build and publish Docker Image
- name: Build and publish Docker Image for UI
uses: openzim/docker-publish-action@v10
with:
image-name: openzim/zimit-ui
image-name: openzim/zimit-frontend-ui
on-master: latest
restrict-to: openzim/zimit-frontend
registries: ghcr.io
credentials: GHCRIO_USERNAME=${{ secrets.GHCR_USERNAME }}
dockerfile: Dockerfile-ui
# prettier-ignore
credentials:
GHCRIO_USERNAME=${{ secrets.GHCR_USERNAME }}
GHCRIO_TOKEN=${{ secrets.GHCR_TOKEN }}

- name: Deploy Zimit frontend changes to zimit.kiwix.org
- name: Build and publish Docker Image for API
uses: openzim/docker-publish-action@v10
with:
image-name: openzim/zimit-frontend-api
on-master: latest
restrict-to: openzim/zimit-frontend
registries: ghcr.io
dockerfile: Dockerfile-api
# prettier-ignore
credentials:
GHCRIO_USERNAME=${{ secrets.GHCR_USERNAME }}
GHCRIO_TOKEN=${{ secrets.GHCR_TOKEN }}

- name: Deploy Zimit frontend UI changes to zimit.kiwix.org
uses: actions-hub/kubectl@master
env:
KUBE_CONFIG: ${{ secrets.ZIMIT_KUBE_CONFIG }}
with:
args: rollout restart deployments frontend-ui-deployment -n zimit

- name: Deploy Zimit frontend API changes to zimit.kiwix.org
uses: actions-hub/kubectl@master
env:
KUBE_CONFIG: ${{ secrets.ZIMIT_KUBE_CONFIG }}
with:
args: rollout restart deployments ui-deployment -n zimit
args: rollout restart deployments frontend-api-deployment -n zimit
5 changes: 5 additions & 0 deletions .github/workflows/QA.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,8 @@ jobs:
working-directory: ui
run: |
yarn lint
- name: Check Typescript typing
working-directory: ui
run: |
yarn type-check
19 changes: 15 additions & 4 deletions .github/workflows/Tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,24 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Ensure we can build the Docker image
- name: Ensure we can build the Docker image for UI
run: |
docker build -t zimitfrontend .
docker build -t zimitfrontend-ui . -f Dockerfile-ui
- name: Ensure we can start the Docker image
- name: Ensure we can start the Docker image for UI
run: |
docker run -d --rm --name test_container zimitfrontend
docker run -d --rm --name test_container zimitfrontend-ui
sleep 5
docker ps | grep test_container
docker stop test_container
- name: Ensure we can build the Docker image for API
run: |
docker build -t zimitfrontend-api . -f Dockerfile-api
- name: Ensure we can start the Docker image for API
run: |
docker run -d --rm --name test_container zimitfrontend-api
sleep 5
docker ps | grep test_container
docker stop test_container
24 changes: 4 additions & 20 deletions Dockerfile → Dockerfile-api
Original file line number Diff line number Diff line change
@@ -1,21 +1,6 @@
FROM node:20-alpine as ui_builder

RUN apk --no-cache add yarn
WORKDIR /src/ui
COPY ui/package.json ui/yarn.lock /src/ui/
RUN yarn install
COPY ui/index.html /src/ui/
COPY ui/*.json /src/ui/
COPY ui/*.ts /src/ui/
COPY ui/*.js /src/ui/
COPY ui/public /src/ui/public
COPY ui/src /src/ui/src
COPY locales /src/locales
RUN NODE_ENV=production yarn build


FROM python:3.12-alpine
LABEL org.opencontainers.image.source https://github.com/offspot/metrics
LABEL org.opencontainers.image.source https://github.com/openzim/zimit-frontend

# Specifying a workdir which is not "/"" is mandatory for proper uvicorn watchfiles
# operation (used mostly only in dev, but changing the workdir does not harm production)
Expand All @@ -25,11 +10,10 @@ WORKDIR "/home"
RUN python -m pip install --no-cache-dir -U \
pip

# to set to your clients' origins
# Set to your client origin(s)
ENV ALLOWED_ORIGINS http://localhost:8001|http://127.0.0.1:8001
ENV DATABASE_URL sqlite+pysqlite:////data/database.db
ENV LOGWATCHER_DATA_FOLDER /data/logwatcher

# Copy minimal files for installation of project dependencies
COPY api/pyproject.toml api/README.md /src/
COPY api/src/zimitfrontend/__about__.py /src/src/zimitfrontend/__about__.py

Expand All @@ -44,8 +28,8 @@ COPY api/*.md /src/
RUN pip install --no-cache-dir /src \
&& rm -rf /src

ENV LOCALES_LOCATION /locales
COPY locales /locales
COPY --from=ui_builder /src/ui/dist /src/ui

EXPOSE 80

Expand Down
22 changes: 22 additions & 0 deletions Dockerfile-ui
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
FROM node:20-alpine as ui_builder

RUN apk --no-cache add yarn
WORKDIR /src/ui
COPY ui/package.json ui/yarn.lock /src/ui/
RUN yarn install
COPY ui/index.html /src/ui/
COPY ui/*.json /src/ui/
COPY ui/*.ts /src/ui/
COPY ui/*.js /src/ui/
COPY ui/public /src/ui/public
COPY ui/src /src/ui/src
COPY locales /src/locales
RUN NODE_ENV=production yarn build


FROM caddy:2.8-alpine
LABEL org.opencontainers.image.source https://github.com/openzim/zimit-frontend

COPY --from=ui_builder /src/ui/dist /usr/share/caddy

COPY locales /locales
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[![Docker](https://ghcr-badge.deta.dev/openzim/zimit-ui/latest_tag?label=docker)](https://ghcr.io/openzim/zimit-ui)
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)

This project is a UI (and it API / backend-for-frontend) allowing any user to submit
This project is a UI (and its API / backend-for-frontend) allowing any user to submit
Zimit requests to a Zimfarm instance. It is NOT a standalone tool allowing to run zimit
scraper. A Zimfarm instance and associated worker(s) is required for the system to be
functional.
Expand Down
2 changes: 1 addition & 1 deletion api/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -204,5 +204,5 @@ include = ["src", "tests", "tasks.py"]
exclude = [".env/**", ".venv/**", "src/zimitfrontend/templates", ".hatch"]
extraPaths = ["src"]
pythonVersion = "3.12"
typeCheckingMode = "basic"
typeCheckingMode = "strict"
disableBytesTypePromotions = true
28 changes: 23 additions & 5 deletions api/src/zimitfrontend/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
def _get_int_setting(environment_variable_name: str, default_value: int) -> int:
"""Get environment variable as integer or fallback to default value"""
try:
return int(os.getenv(environment_variable_name, default_value))
return int(os.getenv(environment_variable_name) or default_value)
except Exception as exc:
logger.error(

Check warning on line 25 in api/src/zimitfrontend/constants.py

View check run for this annotation

Codecov / codecov/patch

api/src/zimitfrontend/constants.py#L24-L25

Added lines #L24 - L25 were not covered by tests
f"Unable to parse {environment_variable_name}: "
Expand All @@ -34,7 +34,7 @@ def _get_size_setting(environment_variable_name: str, default_value: str) -> int
"""Get environment variable as size (parsed with unit) or fallback to default"""
try:
return humanfriendly.parse_size(
os.getenv(environment_variable_name, default_value)
os.getenv(environment_variable_name) or default_value
)
except Exception as exc:
logger.error(

Check warning on line 40 in api/src/zimitfrontend/constants.py

View check run for this annotation

Codecov / codecov/patch

api/src/zimitfrontend/constants.py#L39-L40

Added lines #L39 - L40 were not covered by tests
Expand All @@ -45,6 +45,24 @@ def _get_size_setting(environment_variable_name: str, default_value: str) -> int
return humanfriendly.parse_size(default_value)

Check warning on line 45 in api/src/zimitfrontend/constants.py

View check run for this annotation

Codecov / codecov/patch

api/src/zimitfrontend/constants.py#L45

Added line #L45 was not covered by tests


def _get_time_setting(environment_variable_name: str, default_value: str) -> float:
"""Get environment variable as time (parsed with unit) or fallback to default
Returned value is in seconds, not matter the unit passed in environement variable
"""
try:
return humanfriendly.parse_timespan(
os.getenv(environment_variable_name) or default_value
)
except Exception as exc:
logger.error(

Check warning on line 58 in api/src/zimitfrontend/constants.py

View check run for this annotation

Codecov / codecov/patch

api/src/zimitfrontend/constants.py#L57-L58

Added lines #L57 - L58 were not covered by tests
f"Unable to apply custom {environment_variable_name}: "
f"{os.getenv(environment_variable_name)}. "
f"Using {default_value}. Error: {exc}"
)
return humanfriendly.parse_timespan(default_value)

Check warning on line 63 in api/src/zimitfrontend/constants.py

View check run for this annotation

Codecov / codecov/patch

api/src/zimitfrontend/constants.py#L63

Added line #L63 was not covered by tests


class ApiConfiguration:
"""Shared backend configuration"""

Expand All @@ -56,7 +74,7 @@ class ApiConfiguration:
zimfarm_api_url = os.getenv(
"INTERNAL_ZIMFARM_WEBAPI", "https://api.farm.zimit.kiwix.org/v1"
)
zimfarm_requests_timeout = int(os.getenv("ZIMFARM_REQUESTS_TIMEOUT", "10"))
zimfarm_requests_timeout = _get_time_setting("ZIMFARM_REQUESTS_TIMEOUT", "10s")
mailgun_requests_timeout = int(os.getenv("MAILGUN_REQUESTS_TIMEOUT", "10"))
zimfarm_username = os.getenv("_ZIMFARM_USERNAME", "-")
zimfarm_password = os.getenv("_ZIMFARM_PASSWORD", "-")
Expand All @@ -73,7 +91,7 @@ class ApiConfiguration:

# mailgun
mailgun_from = os.getenv("MAILGUN_FROM", "Zimit <info@zimit.kiwix.org>")
mailgun_api_key = os.getenv("MAILGUN_API_KEY", "")
mailgun_api_key = os.getenv("MAILGUN_API_KEY")
mailgun_api_url = os.getenv(
"MAILGUN_API_URL", "https://api.mailgun.net/v3/mg.zimit.kiwix.org"
)
Expand All @@ -91,7 +109,7 @@ class ApiConfiguration:
)
hook_token = os.getenv("HOOK_TOKEN", uuid.uuid4().hex)

ui_location = pathlib.Path(os.getenv("UI_LOCATION", "/src/ui"))
locales_location = pathlib.Path(os.getenv("LOCALES_LOCATION", "../locales"))

# list of rtl language codes
rtl_language_codes = ("fa",)
51 changes: 27 additions & 24 deletions api/src/zimitfrontend/i18n.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,37 @@
from pathlib import Path
from typing import Any

import i18n
import i18n # pyright: ignore

from zimitfrontend.constants import logger
from zimitfrontend.constants import ApiConfiguration, logger


def setup_i18n():
def setup_i18n() -> None:
"""Configure python-i18n"""
i18n.set("locale", "en")
i18n.set("fallback", "en")
i18n.set("file_format", "json")
i18n.set("filename_format", "{locale}.{format}")
i18n.set("skip_locale_root_data", True)

prod_folder = Path("/locales")
if prod_folder.exists():
logger.info(f"Loading locales from {prod_folder}")
i18n.load_path.append(prod_folder)

dev_folder = Path(Path(__file__).parent.parent.parent.parent / "locales")
if dev_folder.exists():
logger.info(f"Loading locales from {dev_folder}")
i18n.load_path.append(dev_folder)


def set_locale(lang):
i18n.set("locale", "en") # pyright: ignore[reportUnknownMemberType]
i18n.set("fallback", "en") # pyright: ignore[reportUnknownMemberType]
i18n.set("file_format", "json") # pyright: ignore[reportUnknownMemberType]
i18n.set( # pyright: ignore[reportUnknownMemberType]
"filename_format", "{locale}.{format}"
)
i18n.set("skip_locale_root_data", True) # pyright: ignore[reportUnknownMemberType]

locales_location = Path(ApiConfiguration.locales_location)
if not locales_location.exists():
raise Exception(f"Missing locales folder '{locales_location}'")

Check warning on line 21 in api/src/zimitfrontend/i18n.py

View check run for this annotation

Codecov / codecov/patch

api/src/zimitfrontend/i18n.py#L21

Added line #L21 was not covered by tests
logger.info(f"Loading locales from {locales_location}")
i18n.load_path.append(locales_location) # pyright: ignore


def change_locale(lang: str) -> None:
"""Change locale"""
i18n.set("locale", lang)
i18n.set("locale", lang) # pyright: ignore[reportUnknownMemberType]


def t(key, **kwargs):
def t(key: str, **kwargs: Any) -> str:
"""Get translated string"""
return i18n.t(key, **kwargs)
return (
i18n.t( # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
key, **kwargs
)
)
57 changes: 5 additions & 52 deletions api/src/zimitfrontend/main.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
from http import HTTPStatus

Check warning on line 1 in api/src/zimitfrontend/main.py

View check run for this annotation

Codecov / codecov/patch

api/src/zimitfrontend/main.py#L1

Added line #L1 was not covered by tests
from pathlib import Path

from fastapi import FastAPI
from fastapi.encoders import jsonable_encoder
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, JSONResponse, RedirectResponse
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
from fastapi.responses import JSONResponse, RedirectResponse
from starlette.requests import Request

Check warning on line 7 in api/src/zimitfrontend/main.py

View check run for this annotation

Codecov / codecov/patch

api/src/zimitfrontend/main.py#L3-L7

Added lines #L3 - L7 were not covered by tests

from zimitfrontend import __about__
Expand All @@ -23,6 +21,7 @@ def create_app(self) -> FastAPI:
)

@self.app.get("/api")
@self.app.get("/")
async def landing() -> RedirectResponse: # pyright: ignore
"""Redirect to root of latest version of the API"""
return RedirectResponse(

Check warning on line 27 in api/src/zimitfrontend/main.py

View check run for this annotation

Codecov / codecov/patch

api/src/zimitfrontend/main.py#L27

Added line #L27 was not covered by tests
Expand Down Expand Up @@ -53,7 +52,9 @@ async def landing() -> RedirectResponse: # pyright: ignore
)

@api.exception_handler(500)

Check warning on line 54 in api/src/zimitfrontend/main.py

View check run for this annotation

Codecov / codecov/patch

api/src/zimitfrontend/main.py#L54

Added line #L54 was not covered by tests
async def internal_exception_handler(_: Request, exc: Exception):
async def internal_exception_handler( # pyright: ignore[reportUnusedFunction]
_: Request, exc: Exception
):
logger.exception(

Check warning on line 58 in api/src/zimitfrontend/main.py

View check run for this annotation

Codecov / codecov/patch

api/src/zimitfrontend/main.py#L58

Added line #L58 was not covered by tests
exc
) # log the exception which occured so that we can debug
Expand All @@ -75,52 +76,4 @@ async def internal_exception_handler(_: Request, exc: Exception):

self.app.mount(f"/api/{__about__.__api_version__}", api)

Check warning on line 77 in api/src/zimitfrontend/main.py

View check run for this annotation

Codecov / codecov/patch

api/src/zimitfrontend/main.py#L77

Added line #L77 was not covered by tests

class ServeVueUiFromRoot(BaseHTTPMiddleware):
"""Custom middleware to serve the Vue.JS application
We need a bit of black magic to:
- serve the Vue.JS UI from "/"
- but still keep the API on "/api"
- and support Vue.JS routes like "/home"
- and still return 404 when the UI is requesting a file which does not exits
"""

ui_location = Path()

async def dispatch(
self, request: Request, call_next: RequestResponseEndpoint
):
path = request.url.path

# API is served normally
if path.startswith("/api"):
response = await call_next(request)
return response

# Serve index.html on root
if path == "/":
return FileResponse(
ApiConfiguration.ui_location.joinpath("index.html")
)

local_path = ApiConfiguration.ui_location.joinpath(path[1:])

# If there is no dot, then we are probably serving a Vue.JS internal
# route, so let's serve Vue.JS app
if "." not in local_path.name:
return FileResponse(
ApiConfiguration.ui_location.joinpath("index.html")
)

# If the path exists and is a file, serve it
if local_path.exists() and local_path.is_file():
return FileResponse(local_path)

# Otherwise continue to next handler (which is probably a 404)
response = await call_next(request)
return response

# Apply the custom middleware
self.app.add_middleware(ServeVueUiFromRoot)

return self.app

Check warning on line 79 in api/src/zimitfrontend/main.py

View check run for this annotation

Codecov / codecov/patch

api/src/zimitfrontend/main.py#L79

Added line #L79 was not covered by tests
3 changes: 2 additions & 1 deletion api/src/zimitfrontend/routes/hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ def webhook(
if result.status == SUCCESS and result.target and result.subject and result.body:
try:
resp = send_email_via_mailgun(result.target, result.subject, result.body)

Check warning on line 35 in api/src/zimitfrontend/routes/hook.py

View check run for this annotation

Codecov / codecov/patch

api/src/zimitfrontend/routes/hook.py#L34-L35

Added lines #L34 - L35 were not covered by tests
logger.info(f"Mailgun notif sent: {resp}")
if resp:
logger.info(f"Mailgun notif sent: {resp}")
except Exception as exc:
logger.error(f"Failed to send mailgun notif: {exc}", exc_info=exc)
return result.status

Check warning on line 40 in api/src/zimitfrontend/routes/hook.py

View check run for this annotation

Codecov / codecov/patch

api/src/zimitfrontend/routes/hook.py#L37-L40

Added lines #L37 - L40 were not covered by tests
Loading

0 comments on commit 5d66daf

Please sign in to comment.