From 8ee769d3a877f959a2a4f63e2fb4ba744319f892 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Bournhonesque?= Date: Tue, 3 Oct 2023 17:18:40 +0200 Subject: [PATCH] feat: add basic project structure --- .env | 17 +++ .flake8 | 5 + .github/dependabot.yml | 22 +++ .github/workflows/auto-assign-pr.yml | 15 ++ .github/workflows/codeql.yml | 74 ++++++++++ .github/workflows/container-build.yml | 57 ++++++++ .github/workflows/container-deploy.yml | 194 +++++++++++++++++++++++++ .github/workflows/pre-commit.yml | 14 ++ .github/workflows/release-please.yml | 13 ++ .github/workflows/semantic-pr.yml | 17 +++ .gitignore | 136 +++++++++++++++++ .pre-commit-config.yaml | 39 +++++ Dockerfile | 22 +++ Makefile | 54 +++++++ README.md | 9 ++ app/__init__.py | 0 app/api.py | 40 +++++ app/config.py | 34 +++++ app/templates/index.html | 6 + app/utils.py | 20 +++ docker-compose.yml | 19 +++ docker/dev.yml | 18 +++ docker/prod.yml | 3 + requirements.txt | 7 + tests/__init__.py | 0 tests/unit/__init__.py | 0 26 files changed, 835 insertions(+) create mode 100644 .env create mode 100644 .flake8 create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/auto-assign-pr.yml create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/container-build.yml create mode 100644 .github/workflows/container-deploy.yml create mode 100644 .github/workflows/pre-commit.yml create mode 100644 .github/workflows/release-please.yml create mode 100644 .github/workflows/semantic-pr.yml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 README.md create mode 100644 app/__init__.py create mode 100644 app/api.py create mode 100644 app/config.py create mode 100644 app/templates/index.html create mode 100644 app/utils.py create mode 100644 docker-compose.yml create mode 100644 docker/dev.yml create mode 100644 docker/prod.yml create mode 100644 requirements.txt create mode 100644 tests/__init__.py create mode 100644 tests/unit/__init__.py diff --git a/.env b/.env new file mode 100644 index 0000000..8eed936 --- /dev/null +++ b/.env @@ -0,0 +1,17 @@ +# set project name to have a short one +COMPOSE_PROJECT_NAME=nutripatrol +# unify separator with windows style +COMPOSE_PATH_SEPARATOR=; +# dev is default target +COMPOSE_FILE=docker-compose.yml;docker/dev.yml + +API_PORT=127.0.0.1:8000 + +# by default on dev desktop, no restart +RESTART_POLICY=no + +# Sentry DNS for bug tracking, used only in staging and production +SENTRY_DNS= + +# Log level to use, DEBUG by default in dev +LOG_LEVEL=DEBUG diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..b9c59d0 --- /dev/null +++ b/.flake8 @@ -0,0 +1,5 @@ +[flake8] +ignore = E203, E501, W503 +max-line-length = 88 +exclude = .git,__pycache__,build,dist,*_pb2.py,.venv +max-doc-length = 79 \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..a348f3b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,22 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: docker + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 30 + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 30 + - package-ecosystem: pip + directory: "/app" + schedule: + interval: daily + open-pull-requests-limit: 30 diff --git a/.github/workflows/auto-assign-pr.yml b/.github/workflows/auto-assign-pr.yml new file mode 100644 index 0000000..2538480 --- /dev/null +++ b/.github/workflows/auto-assign-pr.yml @@ -0,0 +1,15 @@ +# .github/workflows/auto-author-assign.yml +name: 'Auto Author Assign' + +on: + pull_request_target: + types: [opened, reopened] + +permissions: + pull-requests: write + +jobs: + assign-author: + runs-on: ubuntu-latest + steps: + - uses: toshimaru/auto-author-assign@v2.0.0 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..4a0f30c --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,74 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ "main", deploy-* ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "main" ] + schedule: + - cron: '25 5 * * 5' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'python' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/container-build.yml b/.github/workflows/container-build.yml new file mode 100644 index 0000000..ecc3d96 --- /dev/null +++ b/.github/workflows/container-build.yml @@ -0,0 +1,57 @@ +name: Container Image Build CI + +on: + push: + branches: + - main + - deploy-* + tags: + - v*.*.* + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + image_name: + - api + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 1 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + with: + version: v0.6.0 + buildkitd-flags: --debug + + - name: Login to DockerHub + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker meta + id: meta + uses: docker/metadata-action@v4 + with: + images: | + ghcr.io/${{ github.repository }}/${{ matrix.image_name }} + tags: | + type=semver,pattern={{version}} + type=ref,event=pr + type=ref,event=branch + type=sha,format=long + + - name: Build and push + uses: docker/build-push-action@v3 + with: + context: . + push: true + file: Dockerfile + cache-from: type=registry,ref=ghcr.io/${{ github.repository }}/${{ matrix.image_name }}:buildcache + cache-to: type=registry,ref=ghcr.io/${{ github.repository }}/${{ matrix.image_name }}:buildcache,mode=max + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/container-deploy.yml b/.github/workflows/container-deploy.yml new file mode 100644 index 0000000..c335525 --- /dev/null +++ b/.github/workflows/container-deploy.yml @@ -0,0 +1,194 @@ +name: Container Image Deployment CI + +on: + push: + branches: + - main + - deploy-* +# only staging for now, not prod +# tags: +# - v*.*.* + + +# Note on secrets used for connection +# They are configured as environment secrets +# HOST is the internal ip of VM containing docker +# PROXY_HOST is the host of VMs +# USERNAME is the user used for operations +# SSH_PRIVATE_KEY is the private key (shared between VM and host) + +jobs: + deploy: + runs-on: ubuntu-latest + strategy: + matrix: + env: + # only stagging for now + # Note: env is also the name of the directory on the server + - nutripatrol-net + environment: ${{ matrix.env }} + concurrency: ${{ matrix.env }} + steps: + - name: Set various variable for staging (net) deployment + if: matrix.env == 'nutripatrol-net' + run: | + # direct container access + echo "OPENFOODFACTS_API_URL=https://off:off@world.openfoodfacts.net" >> $GITHUB_ENV + # deploy target + echo "SSH_HOST=10.1.0.200" >> $GITHUB_ENV + echo "SSH_PROXY_HOST=ovh1.openfoodfacts.org" >> $GITHUB_ENV + echo "SSH_USERNAME=off" >> $GITHUB_ENV + - name: Wait for docker image container build workflow + uses: tomchv/wait-my-workflow@v1.1.0 + id: wait-build + with: + token: ${{ secrets.GITHUB_TOKEN }} + checkName: build (api) + ref: ${{ github.event.pull_request.head.sha || github.sha }} + intervalSeconds: 10 + timeoutSeconds: 600 # 10m + + - name: Do something if build isn't launch + if: steps.wait-build.outputs.conclusion == 'does not exist' + run: echo job does not exist && true + + - name: Do something if build fail + if: steps.wait-build.outputs.conclusion == 'failure' + run: echo fail && false # fail if build fail + + - name: Do something if build timeout + if: steps.wait-build.outputs.conclusion == 'timed_out' + run: echo Timeout && false # fail if build time out + + - name: Checkout git repository + uses: appleboy/ssh-action@master + with: + host: ${{ env.SSH_HOST }} + username: ${{ env.SSH_USERNAME }} + key: ${{ secrets.SSH_PRIVATE_KEY }} + proxy_host: ${{ env.SSH_PROXY_HOST }} + proxy_username: ${{ env.SSH_USERNAME }} + proxy_key: ${{ secrets.SSH_PRIVATE_KEY }} + script_stop: false + script: | + # Clone Git repository if not already there + [ ! -d '${{ matrix.env }}' ] && git clone --depth 1 https://github.com/${{ github.repository }} ${{ matrix.env }} --no-single-branch 2>&1 + + # Go to repository directory + cd ${{ matrix.env }} + + # Fetch newest commits (in case it wasn't freshly cloned) + git fetch --depth 1 + + # Checkout current commit SHA + git checkout -qf ${{ github.sha }} + + - name: Set environment variables + uses: appleboy/ssh-action@master + with: + host: ${{ env.SSH_HOST }} + username: ${{ env.SSH_USERNAME }} + key: ${{ secrets.SSH_PRIVATE_KEY }} + proxy_host: ${{ env.SSH_PROXY_HOST }} + proxy_username: ${{ env.SSH_USERNAME }} + proxy_key: ${{ secrets.SSH_PRIVATE_KEY }} + script_stop: false + script: | + # Go to repository directory + cd ${{ matrix.env }} + + mv .env .env-dev + # init .env + echo "# Env file generated by container-deploy action"> .env + # Set Docker Compose variables + echo "DOCKER_CLIENT_TIMEOUT=180" >> .env + echo "COMPOSE_HTTP_TIMEOUT=180" >> .env + echo "COMPOSE_PROJECT_NAME=nutripatrol" >> .env + echo "COMPOSE_PATH_SEPARATOR=;" >> .env + echo "COMPOSE_FILE=docker-compose.yml;docker/prod.yml" >> .env + # Copy variables that are same as dev + grep '\(STACK_VERSION\|ES_PORT\)' .env-dev >> .env + # Set docker variables + echo "TAG=sha-${{ github.sha }}" >> .env + echo "RESTART_POLICY=always" >> .env + # Set App variables + echo "CLUSTER_NAME=${{ matrix.env }}-es-cluster" >> .env + echo "SEARCH_PORT=8180" >> .env + echo "ES_VUE_PORT=8181" >> .env + echo "REDIS_PORT=8182" >> .env + echo "MEM_LIMIT=4294967296" >> .env + # this is the network shared with productopener + echo "COMMON_NET_NAME=po_webnet">> .env + echo "OPENFOODFACTS_API_URL=${{ env.OPENFOODFACTS_API_URL }}" >> .env + # This secret is to be generated using htpasswd, see .env file + # use simple quotes to avoid interpolation of $apr1$ ! + echo 'NGINX_BASIC_AUTH_USER_PASSWD=${{ secrets.NGINX_BASIC_AUTH_USER_PASSWD }}' >> .env + echo "SENTRY_DNS=${{ secrets.SENTRY_DSN }}" >> .env + echo "CONFIG_PATH=data/config/openfoodfacts.yml" >> .env + + - name: Create Docker volumes + uses: appleboy/ssh-action@master + with: + host: ${{ env.SSH_HOST }} + username: ${{ env.SSH_USERNAME }} + key: ${{ secrets.SSH_PRIVATE_KEY }} + proxy_host: ${{ env.SSH_PROXY_HOST }} + proxy_username: ${{ env.SSH_USERNAME }} + proxy_key: ${{ secrets.SSH_PRIVATE_KEY }} + script_stop: false + script: | + cd ${{ matrix.env }} + make create_external_volumes + + - name: Start services + uses: appleboy/ssh-action@master + with: + host: ${{ env.SSH_HOST }} + username: ${{ env.SSH_USERNAME }} + key: ${{ secrets.SSH_PRIVATE_KEY }} + proxy_host: ${{ env.SSH_PROXY_HOST }} + proxy_username: ${{ env.SSH_USERNAME }} + proxy_key: ${{ secrets.SSH_PRIVATE_KEY }} + script_stop: false + script: | + cd ${{ matrix.env }} + docker-compose down + docker-compose up -d --remove-orphans 2>&1 + + - name: Check services are up + uses: appleboy/ssh-action@master + if: ${{ always() }} + with: + host: ${{ env.SSH_HOST }} + username: ${{ env.SSH_USERNAME }} + key: ${{ secrets.SSH_PRIVATE_KEY }} + proxy_host: ${{ env.SSH_PROXY_HOST }} + proxy_username: ${{ env.SSH_USERNAME }} + proxy_key: ${{ secrets.SSH_PRIVATE_KEY }} + script_stop: false + script: | + cd ${{ matrix.env }} + make livecheck + + - name: Cleanup obsolete Docker objects + uses: appleboy/ssh-action@master + if: ${{ always() }} + with: + host: ${{ env.SSH_HOST }} + username: ${{ env.SSH_USERNAME }} + key: ${{ secrets.SSH_PRIVATE_KEY }} + proxy_host: ${{ env.SSH_PROXY_HOST }} + proxy_username: ${{ env.SSH_USERNAME }} + proxy_key: ${{ secrets.SSH_PRIVATE_KEY }} + script_stop: false + script: | + cd ${{ matrix.env }} + docker system prune -af + + - uses: frankie567/grafana-annotation-action@v1.0.3 + if: ${{ always() }} + with: + apiHost: https://grafana.openfoodfacts.org + apiToken: ${{ secrets.GRAFANA_API_TOKEN }} + text: Deployment ${{ steps.livecheck.outcome }} on ${{ matrix.env }} + tags: type:deployment,origin:github,status:${{ steps.livecheck.outcome }},repo:${{ github.repository }},sha:${{ github.sha }},app:robotoff,env:${{ matrix.env }} diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..ed4198d --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,14 @@ +# Enforce pre-commit hook server side +name: Run pre-commit hooks + +on: + pull_request: + branches: [ main ] + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v3 + - uses: pre-commit/action@v3.0.0 diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 0000000..392a143 --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,13 @@ +name: Run release-please +on: + push: + branches: + - main +jobs: + release-please: + runs-on: ubuntu-latest + steps: + - uses: GoogleCloudPlatform/release-please-action@v3.3.0 + with: + token: ${{ secrets.GITHUB_TOKEN }} + release-type: simple diff --git a/.github/workflows/semantic-pr.yml b/.github/workflows/semantic-pr.yml new file mode 100644 index 0000000..3513db2 --- /dev/null +++ b/.github/workflows/semantic-pr.yml @@ -0,0 +1,17 @@ +name: "Semantic PRs" + +on: + pull_request_target: + types: + - opened + - edited + - synchronize + +jobs: + main: + name: Validate PR title + runs-on: ubuntu-latest + steps: + - uses: amannn/action-semantic-pull-request@v4 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..81075bb --- /dev/null +++ b/.gitignore @@ -0,0 +1,136 @@ +# 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/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# 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/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,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 +.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/ + +# Pycharm +.idea/ +.DS_Store + +# vscode +.vscode +*.code-workspace diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..736e6a8 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,39 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.3.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: debug-statements + - id: double-quote-string-fixer + - id: name-tests-test + - id: requirements-txt-fixer +- repo: https://github.com/asottile/setup-cfg-fmt + rev: v2.0.0 + hooks: + - id: setup-cfg-fmt +- repo: https://github.com/asottile/reorder_python_imports + rev: v3.8.2 + hooks: + - id: reorder-python-imports + args: [--py37-plus, --add-import, 'from __future__ import annotations'] +- repo: https://github.com/asottile/add-trailing-comma + rev: v2.2.3 + hooks: + - id: add-trailing-comma + args: [--py36-plus] +- repo: https://github.com/pre-commit/mirrors-autopep8 + rev: v1.6.0 + hooks: + - id: autopep8 +- repo: https://github.com/PyCQA/flake8 + rev: 5.0.2 + hooks: + - id: flake8 + args: [--ignore=E501] +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v0.971 + hooks: + - id: mypy + additional_dependencies: [types-all] diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0fe6597 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +# syntax = docker/dockerfile:1.2 +# Base user uid / gid keep 1000 on prod, align with your user on dev +ARG USER_UID=1000 +ARG USER_GID=1000 + + +FROM python:3.11 +# Instructions from https://fastapi.tiangolo.com/deployment/docker/ +ARG USER_UID +ARG USER_GID +RUN groupadd -g $USER_GID off && \ + useradd -u $USER_UID -g off -m off && \ + mkdir -p /home/off && \ + mkdir -p /opt/nutripatrol && \ + chown off:off -R /opt/nutripatrol /home/off +WORKDIR /opt/nutripatrol +COPY --chown=off:off requirements.txt requirements.txt +RUN pip install --no-cache-dir --upgrade -r requirements.txt + +USER off:off +COPY --chown=off:off app app +CMD ["uvicorn", "app.api:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..bfc8c8c --- /dev/null +++ b/Makefile @@ -0,0 +1,54 @@ +MOUNT_POINT ?= /mnt +DOCKER_LOCAL_DATA ?= /srv/off/docker_data +ENV_FILE ?= .env +# for dev we need to align user uid with the one in the container +# this is handled through build args +UID ?= $(shell id -u) +export USER_UID:=${UID} +# prefer to use docker buildkit +export DOCKER_BUILDKIT=1 +export COMPOSE_DOCKER_CLI_BUILD=1 +# we need COMPOSE_PROJECT_NAME for some commands +# take it form env, or from env file +COMPOSE_PROJECT_NAME ?= $(shell grep COMPOSE_PROJECT_NAME ${ENV_FILE} | cut -d '=' -f 2) +DOCKER_COMPOSE=docker-compose --env-file=${ENV_FILE} + +#------------# +# Production # +#------------# + +test: + echo ${ENV_FILE} ${COMPOSE_PROJECT_NAME} + +livecheck: + @echo "🥫 livecheck services…" ; \ + exit_code=0; \ + services=`${DOCKER_COMPOSE} config --service | tr '\n' ' '`; \ + for service in $$services; do \ + if [ -z `docker-compose ps -q $$service` ] || [ -z `docker ps -q --no-trunc | grep $$(${DOCKER_COMPOSE} ps -q $$service)` ]; then \ + echo "$$service: DOWN"; \ + exit_code=1; \ + else \ + echo "$$service: UP"; \ + fi \ + done; \ + [ $$exit_code -eq 0 ] && echo "Success !"; \ + exit $$exit_code; + + +build: + @echo "🥫 building docker (for dev)" + docker-compose build + + +up: +ifdef service + ${DOCKER_COMPOSE} up -d ${service} 2>&1 +else + ${DOCKER_COMPOSE} up -d 2>&1 +endif + + +down: + @echo "🥫 Bringing down containers …" + ${DOCKER_COMPOSE} down diff --git a/README.md b/README.md new file mode 100644 index 0000000..c77ab98 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# Nutripatrol + +Open Food Facts moderation tool (WIP). + +### Pre-Commit +This repo uses [pre-commit](https://pre-commit.com/) to enforce code styling, etc. To use it: +```console +pre-commit run +``` diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/api.py b/app/api.py new file mode 100644 index 0000000..e149bff --- /dev/null +++ b/app/api.py @@ -0,0 +1,40 @@ +from pathlib import Path + +from fastapi import FastAPI, Request +from fastapi.responses import HTMLResponse, PlainTextResponse +from fastapi.templating import Jinja2Templates +from openfoodfacts.utils import get_logger + +from app.config import settings +from app.utils import init_sentry + +logger = get_logger(level=settings.log_level.to_int()) + + +app = FastAPI( + title="nutripatrol", + contact={ + "name": "The Open Food Facts team", + "url": "https://world.openfoodfacts.org", + "email": "contact@openfoodfacts.org", + }, + license_info={ + "name": " AGPL-3.0", + "url": "https://www.gnu.org/licenses/agpl-3.0.en.html", + }, +) +templates = Jinja2Templates(directory=Path(__file__).parent / "templates") +init_sentry(settings.sentry_dns) + + +@app.get("/", response_class=HTMLResponse) +def main_page(request: Request): + return templates.TemplateResponse( + "index.html", + {"request": request}, + ) + + +@app.get("/robots.txt", response_class=PlainTextResponse) +def robots_txt(): + return """User-agent: *\nDisallow: /""" diff --git a/app/config.py b/app/config.py new file mode 100644 index 0000000..7051e60 --- /dev/null +++ b/app/config.py @@ -0,0 +1,34 @@ +from enum import StrEnum + +from pydantic_settings import BaseSettings + + +class LoggingLevel(StrEnum): + NOTSET = "NOTSET" + DEBUG = "DEBUG" + INFO = "INFO" + WARNING = "WARNING" + ERROR = "ERROR" + CRITICAL = "CRITICAL" + + def to_int(self): + if self is LoggingLevel.NOTSET: + return 0 + elif self is LoggingLevel.DEBUG: + return 10 + elif self is LoggingLevel.INFO: + return 20 + elif self is LoggingLevel.WARNING: + return 30 + elif self is LoggingLevel.ERROR: + return 40 + elif self is LoggingLevel.CRITICAL: + return 50 + + +class Settings(BaseSettings): + sentry_dns: str | None = None + log_level: LoggingLevel = LoggingLevel.INFO + + +settings = Settings() diff --git a/app/templates/index.html b/app/templates/index.html new file mode 100644 index 0000000..fa8057c --- /dev/null +++ b/app/templates/index.html @@ -0,0 +1,6 @@ + + + +

Hello world!

+ + \ No newline at end of file diff --git a/app/utils.py b/app/utils.py new file mode 100644 index 0000000..3062a7a --- /dev/null +++ b/app/utils.py @@ -0,0 +1,20 @@ +import logging + +import sentry_sdk +from sentry_sdk.integrations import Integration +from sentry_sdk.integrations.logging import LoggingIntegration + + +def init_sentry(sentry_dsn: str | None, integrations: list[Integration] | None = None): + if sentry_dsn: + integrations = integrations or [] + integrations.append( + LoggingIntegration( + level=logging.INFO, # Capture info and above as breadcrumbs + event_level=logging.WARNING, # Send warning and errors as events + ) + ) + sentry_sdk.init( # type:ignore # mypy say it's abstract + sentry_dsn, + integrations=integrations, + ) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..8393e86 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +version: "3.7" + +x-api-common: &api-common + image: ghcr.io/openfoodfacts/nutripatrol/api:${TAG:-dev} + restart: ${RESTART_POLICY} + environment: + - SENTRY_DNS + - LOG_LEVEL + networks: + - default + +services: + api: + <<: *api-common + ports: + - "${API_PORT}:8000" + +volumes: + rediscache: diff --git a/docker/dev.yml b/docker/dev.yml new file mode 100644 index 0000000..2caec6f --- /dev/null +++ b/docker/dev.yml @@ -0,0 +1,18 @@ +version: "3.7" +# this file setup various thinks that are for dev environment + +x-api-base: &api-base + build: + context: . + args: + USER_UID: ${USER_UID:-1000} + USER_GID: ${USER_GID:-1000} + volumes: + # mount code dynamically + - "./app:/opt/nutripatrol/app" + +services: + api: + <<: *api-base + # uvicorn in reload mode + command: ["uvicorn", "app.api:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "8000", "--reload"] diff --git a/docker/prod.yml b/docker/prod.yml new file mode 100644 index 0000000..c404b8b --- /dev/null +++ b/docker/prod.yml @@ -0,0 +1,3 @@ +version: "3.7" + +# modifications to docker-compose for production settings \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..49ba13e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +fastapi==0.103.1 +uvicorn==0.23.2 +openfoodfacts==0.1.10 +requests==2.31.0 +pydantic-settings==2.0.3 +sentry-sdk[fastapi]==1.31.0 +jinja2==3.1.2 \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29