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 0117b97
Show file tree
Hide file tree
Showing 26 changed files with 236 additions and 205 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/Publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ jobs:
on-master: latest
restrict-to: openzim/zimit-frontend
registries: ghcr.io
credentials: GHCRIO_USERNAME=${{ secrets.GHCR_USERNAME }}
# prettier-ignore
credentials:
GHCRIO_USERNAME=${{ secrets.GHCR_USERNAME }}
GHCRIO_TOKEN=${{ secrets.GHCR_TOKEN }}

- name: Deploy Zimit frontend changes to zimit.kiwix.org
Expand Down
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
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
20 changes: 11 additions & 9 deletions api/src/zimitfrontend/routes/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from zimitfrontend.constants import ApiConfiguration, logger
from zimitfrontend.routes.schemas import TaskCreateRequest, TaskCreateResponse, TaskInfo
from zimitfrontend.routes.utils import convert_zimfarm_task_to_info
from zimitfrontend.routes.utils import get_task_info
from zimitfrontend.zimfarm import query_api

Check warning on line 11 in api/src/zimitfrontend/routes/requests.py

View check run for this annotation

Codecov / codecov/patch

api/src/zimitfrontend/routes/requests.py#L8-L11

Added lines #L8 - L11 were not covered by tests

router = APIRouter(

Check warning on line 13 in api/src/zimitfrontend/routes/requests.py

View check run for this annotation

Codecov / codecov/patch

api/src/zimitfrontend/routes/requests.py#L13

Added line #L13 was not covered by tests
Expand All @@ -29,12 +29,10 @@ def task_info(
task_id: Annotated[str, Path()],
) -> TaskInfo:
# first try to find the task
success, status, task = query_api("GET", f"/tasks/{task_id}?hide_secrets=")
_, status, task = query_api("GET", f"/tasks/{task_id}?hide_secrets=")

Check warning on line 32 in api/src/zimitfrontend/routes/requests.py

View check run for this annotation

Codecov / codecov/patch

api/src/zimitfrontend/routes/requests.py#L32

Added line #L32 was not covered by tests
if status == HTTPStatus.NOT_FOUND:
# if it fails, try to find the requested task
success, status, task = query_api(
"GET", f"/requested-tasks/{task_id}?hide_secrets="
)
_, status, task = query_api("GET", f"/requested-tasks/{task_id}?hide_secrets=")

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

View check run for this annotation

Codecov / codecov/patch

api/src/zimitfrontend/routes/requests.py#L35

Added line #L35 was not covered by tests
if status != HTTPStatus.OK:
raise HTTPException(

Check warning on line 37 in api/src/zimitfrontend/routes/requests.py

View check run for this annotation

Codecov / codecov/patch

api/src/zimitfrontend/routes/requests.py#L37

Added line #L37 was not covered by tests
status_code=HTTPStatus.BAD_REQUEST,
Expand All @@ -43,7 +41,7 @@ def task_info(
"zimfarm_message": task,
},
)
return convert_zimfarm_task_to_info(task)
return get_task_info(task)

Check warning on line 44 in api/src/zimitfrontend/routes/requests.py

View check run for this annotation

Codecov / codecov/patch

api/src/zimitfrontend/routes/requests.py#L44

Added line #L44 was not covered by tests


@router.post(

Check warning on line 47 in api/src/zimitfrontend/routes/requests.py

View check run for this annotation

Codecov / codecov/patch

api/src/zimitfrontend/routes/requests.py#L47

Added line #L47 was not covered by tests
Expand Down Expand Up @@ -114,7 +112,7 @@ def _cap_limit(user_limit: int, zimit_limit: int) -> int:
}

# create schedule payload
payload = {
payload = { # pyright: ignore[reportUnknownVariableType]

Check warning on line 115 in api/src/zimitfrontend/routes/requests.py

View check run for this annotation

Codecov / codecov/patch

api/src/zimitfrontend/routes/requests.py#L115

Added line #L115 was not covered by tests
"name": schedule_name,
"language": {"code": "eng", "name_en": "English", "name_native": "English"},
"category": "other",
Expand All @@ -132,7 +130,7 @@ def _cap_limit(user_limit: int, zimit_limit: int) -> int:
f"&target={request.email}"
f"&lang={request.lang}"
)
payload.update(
payload.update( # pyright: ignore[reportUnknownMemberType]

Check warning on line 133 in api/src/zimitfrontend/routes/requests.py

View check run for this annotation

Codecov / codecov/patch

api/src/zimitfrontend/routes/requests.py#L133

Added line #L133 was not covered by tests
{
"notification": {
"requested": {"webhook": [webhook_url]},
Expand All @@ -142,7 +140,11 @@ def _cap_limit(user_limit: int, zimit_limit: int) -> int:
)

# create a unique schedule for that request on the zimfarm
success, status, resp = query_api("POST", "/schedules/", payload=payload)
success, status, resp = query_api(

Check warning on line 143 in api/src/zimitfrontend/routes/requests.py

View check run for this annotation

Codecov / codecov/patch

api/src/zimitfrontend/routes/requests.py#L143

Added line #L143 was not covered by tests
"POST",
"/schedules/",
payload=payload, # pyright: ignore[reportUnknownArgumentType]
)
if not success:
logger.error(f"Unable to create schedule via HTTP {status}: {resp}")
message = f"Unable to create schedule via HTTP {status}: {resp}"

Check warning on line 150 in api/src/zimitfrontend/routes/requests.py

View check run for this annotation

Codecov / codecov/patch

api/src/zimitfrontend/routes/requests.py#L149-L150

Added lines #L149 - L150 were not covered by tests
Expand Down
Loading

0 comments on commit 0117b97

Please sign in to comment.