From 26836052c8aee15257e875ae031209cdf9744081 Mon Sep 17 00:00:00 2001 From: Felipe Alvarado Date: Tue, 6 Aug 2024 12:18:33 +0200 Subject: [PATCH] Setup service --- .dockerignore | 12 ++ .env.sample | 1 + .github/CODEOWNERS | 3 + .github/ISSUE_TEMPLATE/bug_report.md | 30 +++++ .github/ISSUE_TEMPLATE/feature_request.md | 32 +++++ .github/PULL_REQUEST_TEMPLATE.md | 10 ++ .github/dependabot.yml | 25 ++++ .github/release.yml | 15 +++ .github/workflows/ci.yml | 139 ++++++++++++++++++++++ .github/workflows/cla.yml | 36 ++++++ .gitignore | 128 ++++++++++++++++++++ .pre-commit-config.yaml | 28 +++++ README.md | 43 +++++++ app/__init__.py | 1 + app/main.py | 20 ++++ app/models.py | 5 + app/routers/__init__.py | 0 app/routers/about.py | 14 +++ app/routers/default.py | 36 ++++++ app/tests/__init__.py | 1 + app/tests/routers/__init__.py | 0 app/tests/routers/test_about.py | 19 +++ app/tests/routers/test_default.py | 32 +++++ docker-compose.yml | 26 ++++ docker/nginx/nginx.conf | 73 ++++++++++++ docker/web/Dockerfile | 37 ++++++ docker/web/run_web.sh | 11 ++ requirements/dev.txt | 7 ++ requirements/prod.txt | 2 + run_tests.sh | 3 + scripts/autodeploy.sh | 10 ++ setup.cfg | 45 +++++++ static/favicon.ico | Bin 0 -> 15086 bytes 33 files changed, 844 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.sample create mode 100644 .github/CODEOWNERS create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/dependabot.yml create mode 100644 .github/release.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/cla.yml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 README.md create mode 100644 app/__init__.py create mode 100644 app/main.py create mode 100644 app/models.py create mode 100644 app/routers/__init__.py create mode 100644 app/routers/about.py create mode 100644 app/routers/default.py create mode 100644 app/tests/__init__.py create mode 100644 app/tests/routers/__init__.py create mode 100644 app/tests/routers/test_about.py create mode 100644 app/tests/routers/test_default.py create mode 100644 docker-compose.yml create mode 100644 docker/nginx/nginx.conf create mode 100644 docker/web/Dockerfile create mode 100755 docker/web/run_web.sh create mode 100644 requirements/dev.txt create mode 100644 requirements/prod.txt create mode 100755 run_tests.sh create mode 100644 scripts/autodeploy.sh create mode 100644 setup.cfg create mode 100644 static/favicon.ico diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0a407ba --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +.cache +.dockerignore +.gitignore +.git +.github +.env +.pylintrc +__pycache__ +*.pyc +*.egg-info +.idea/ +.vscode diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..7fff6fa --- /dev/null +++ b/.env.sample @@ -0,0 +1 @@ +SECRET_KEY=kamehameha diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..26d7482 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,3 @@ +# These owners will be the default owners for everything in +# the repo. Unless a later match takes precedence. +* @safe-global/core-api diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..5504f1c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,30 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Do POST on '...' + - Provide `json` you are submitting to the service (if it applies) +2. Then GET on '....' +3. Links to issues in other repos (if possible) + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Environment (please complete the following information):** + - Staging or production? + - Which chain? + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..570f69a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,32 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement + +--- + +# What is needed? +A clear and concise description of what you want to happen. + +# Background +More information about the feature needed + +# Related issues +Paste here the related links for the issues on the clients/safe project if applicable. Please provide at least one of the following: +- Links to epics in your repository +- Images taken from mocks +- Gitbook or any form of written documentation links, etc. Any of these alternatives will help us contextualise your request. + +# Endpoint +If applicable, description on the endpoint and the result you expect: + +## URL +`GET /api/v1/...` + +## Response +``` +{ + ... +} +``` diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..32d5f19 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,10 @@ +### Make sure these boxes are checked! 📦✅ + +- [ ] You ran `./run_tests.sh` +- [ ] You ran `pre-commit run -a` + +### What was wrong? 👾 + +Closes # + +### How was it fixed? 🎯 diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..75b5422 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,25 @@ +version: 2 +updates: + - package-ecosystem: pip + directory: "/" + schedule: + interval: weekly + day: monday + reviewers: + - "safe-global/core-api" + + - package-ecosystem: docker + directory: "/docker/web" + schedule: + interval: weekly + day: monday + reviewers: + - "safe-global/core-api" + + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + day: monday + reviewers: + - "safe-global/core-api" diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..1c0307d --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,15 @@ +changelog: + categories: + - title: 🏕 Features + labels: + - '*' + exclude: + labels: + - dependencies + - breaking_change + - title: 🛠 Breaking Changes + labels: + - breaking_change + - title: 👒 Dependencies + labels: + - dependencies diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..00c6a87 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,139 @@ +name: Python CI +on: + push: + branches: + - main + - develop + pull_request: + release: + types: [ released ] + +jobs: + linting: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.12"] + + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: pip install pre-commit + - name: Run pre-commit + run: pre-commit run --all-files + + test-app: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.12"] + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + cache-dependency-path: 'requirements/*.txt' + - name: Install dependencies + run: | + pip install wheel + pip install -r requirements/dev.txt coveralls + env: + PIP_USE_MIRRORS: true + - name: Run mypy + run: mypy . + - name: Run tests and coverage + run: | + coverage run --source=$SOURCE_FOLDER -m pytest -rxXs + env: + SOURCE_FOLDER: app + - name: Send results to coveralls + continue-on-error: true # Ignore coveralls problems + run: coveralls --service=github + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Required for coveralls + + docker-deploy: + runs-on: ubuntu-latest + needs: + - linting + - test-app + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' || (github.event_name == 'release' && github.event.action == 'released') + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-qemu-action@v3 + with: + platforms: arm64 + - uses: docker/setup-buildx-action@v3 + - name: Dockerhub login + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USER }} + password: ${{ secrets.DOCKER_PASSWORD }} + - name: Deploy Master + if: github.ref == 'refs/heads/main' + uses: docker/build-push-action@v6 + with: + context: . + file: docker/web/Dockerfile + push: true + tags: safeglobal/safe-queue-service:staging + platforms: | + linux/amd64 + linux/arm64 + cache-from: type=gha + cache-to: type=gha,mode=max + - name: Deploy Develop + if: github.ref == 'refs/heads/develop' + uses: docker/build-push-action@v6 + with: + context: . + file: docker/web/Dockerfile + push: true + tags: safeglobal/safe-queue-service:develop + platforms: | + linux/amd64 + linux/arm64 + cache-from: type=gha + cache-to: type=gha,mode=max + - name: Deploy Tag + if: (github.event_name == 'release' && github.event.action == 'released') + uses: docker/build-push-action@v6 + with: + context: . + file: docker/web/Dockerfile + push: true + tags: | + safeglobal/safe-queue-service:${{ github.event.release.tag_name }} + safeglobal/safe-queue-service:latest + platforms: | + linux/amd64 + linux/arm64 + cache-from: type=gha + cache-to: type=gha,mode=max + + autodeploy: + runs-on: ubuntu-latest + needs: [docker-deploy] + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' + steps: + - uses: actions/checkout@v4 + - name: Deploy Staging + if: github.ref == 'refs/heads/main' + run: bash scripts/autodeploy.sh + env: + AUTODEPLOY_URL: ${{ secrets.AUTODEPLOY_URL }} + AUTODEPLOY_TOKEN: ${{ secrets.AUTODEPLOY_TOKEN }} + TARGET_ENV: "staging" + - name: Deploy Develop + if: github.ref == 'refs/heads/develop' + run: bash scripts/autodeploy.sh + env: + AUTODEPLOY_URL: ${{ secrets.AUTODEPLOY_URL }} + AUTODEPLOY_TOKEN: ${{ secrets.AUTODEPLOY_TOKEN }} + TARGET_ENV: "develop" diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml new file mode 100644 index 0000000..8474ffc --- /dev/null +++ b/.github/workflows/cla.yml @@ -0,0 +1,36 @@ +name: "CLA Assistant" +on: + issue_comment: + types: [ created ] + pull_request_target: + types: [ opened,closed,synchronize ] + +jobs: + CLAssistant: + runs-on: ubuntu-latest + steps: + - name: "CLA Assistant" + if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' + # Beta Release + uses: cla-assistant/github-action@v2.4.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # the below token should have repo scope and must be manually added by you in the repository's secret + PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} + with: + path-to-signatures: 'signatures/version1/cla.json' + path-to-document: 'https://safe.global/cla/' + # branch should not be protected + branch: 'cla-signatures' + allowlist: hectorgomezv,moisses89,luarx,rmeissner,Uxio0,falvaradorodriguez,*bot # may need to update this expression if we add new bots + + #below are the optional inputs - If the optional inputs are not given, then default values will be taken + #remote-organization-name: enter the remote organization name where the signatures should be stored (Default is storing the signatures in the same repository) + #remote-repository-name: enter the remote repository name where the signatures should be stored (Default is storing the signatures in the same repository) + #create-file-commit-message: 'For example: Creating file for storing CLA Signatures' + #signed-commit-message: 'For example: $contributorName has signed the CLA in #$pullRequestNo' + #custom-notsigned-prcomment: 'pull request comment with Introductory message to ask new contributors to sign' + #custom-pr-sign-comment: 'The signature to be committed in order to sign the CLA' + #custom-allsigned-prcomment: 'pull request comment when all contributors has signed, defaults to **CLA Assistant Lite bot** All Contributors have signed the CLA.' + #lock-pullrequest-aftermerge: false - if you don't want this bot to automatically lock the pull request after merging (default - true) + #use-dco-flag: true - If you are using DCO instead of CLA diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..089761b --- /dev/null +++ b/.gitignore @@ -0,0 +1,128 @@ +# 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/ +*.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/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +# Django +staticfiles/ +safe_transaction_service/media + +### VisualStudioCode template +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# Provided default Pycharm Run/Debug Configurations should be tracked by git +# In case of local modifications made by Pycharm, use update-index command +# for each changed file, like this: +# git update-index --assume-unchanged .idea/safe_transaction_service.iml +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff: +.idea/ +.vscode/ +.DS_Store diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..2e99e23 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,28 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: + - repo: https://github.com/PyCQA/isort + rev: 5.13.2 + hooks: + - id: isort + - repo: https://github.com/psf/black + rev: 24.4.0 + hooks: + - id: black + - repo: https://github.com/PyCQA/flake8 + rev: 7.0.0 + hooks: + - id: flake8 + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: check-docstring-first + - id: check-merge-conflict + - id: debug-statements + - id: detect-private-key + - id: requirements-txt-fixer + - id: trailing-whitespace + - id: end-of-file-fixer + types: [python] + - id: check-yaml + - id: check-added-large-files diff --git a/README.md b/README.md new file mode 100644 index 0000000..e1ded58 --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +[![Python CI](https://github.com/safe-global/safe-queue-service/actions/workflows/ci.yml/badge.svg)](https://github.com/safe-global/safe-auth-service/actions/workflows/ci.yml) +[![Coverage Status](https://coveralls.io/repos/github/safe-global/safe-queue-service/badge.svg?branch=main)](https://coveralls.io/github/safe-global/safe-queue-service?branch=main) +[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit) +![Python 3.12](https://img.shields.io/badge/Python-3.12-blue.svg) +[![Docker Image Version (latest semver)](https://img.shields.io/docker/v/safeglobal/safe-queue-service?label=Docker&sort=semver)](https://hub.docker.com/r/safeglobal/safe-queue-service) + + +# Safe Queue Service +Safe Core{API} transaction queue service + +## Configuration +```bash +cp .env.sample .env +``` + +## Execution + +```bash +docker compose build +docker compose up +``` + +Then go to http://localhost:8000 to see the service documentation. + +## Setup for development +Use a virtualenv if possible: + +```bash +python -m venv venv +``` + +Then enter the virtualenv and install the dependencies: + +```bash +source venv/bin/activate +pip install -r requirements/dev.txt +pre-commit install -f +cp .env.sample .env +``` + + +## Contributors +[See contributors](https://github.com/safe-global/safe-queue-service/graphs/contributors) diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..7723ca4 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1 @@ +VERSION = "0.0.0" diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..14ae723 --- /dev/null +++ b/app/main.py @@ -0,0 +1,20 @@ +from fastapi import APIRouter, FastAPI + +from . import VERSION +from .routers import about, default + +app = FastAPI( + title="Safe Queue Service", + description="Safe Core{API} transaction queue service", + version=VERSION, + docs_url=None, + redoc_url=None, +) + +# Router configuration +api_v1_router = APIRouter( + prefix="/api/v1", +) +api_v1_router.include_router(about.router) +app.include_router(api_v1_router) +app.include_router(default.router) diff --git a/app/models.py b/app/models.py new file mode 100644 index 0000000..96d0d2a --- /dev/null +++ b/app/models.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + + +class About(BaseModel): + version: str diff --git a/app/routers/__init__.py b/app/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/routers/about.py b/app/routers/about.py new file mode 100644 index 0000000..dfcbd70 --- /dev/null +++ b/app/routers/about.py @@ -0,0 +1,14 @@ +from fastapi import APIRouter + +from .. import VERSION +from ..models import About + +router = APIRouter( + prefix="/about", + tags=["About"], +) + + +@router.get("", response_model=About) +async def about() -> "About": + return About(version=VERSION) diff --git a/app/routers/default.py b/app/routers/default.py new file mode 100644 index 0000000..d9a6d1e --- /dev/null +++ b/app/routers/default.py @@ -0,0 +1,36 @@ +from typing import Literal + +from fastapi import APIRouter +from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html +from fastapi.responses import RedirectResponse + +router = APIRouter() + + +@router.get("/", include_in_schema=False) +async def home() -> RedirectResponse: + return RedirectResponse(url="/docs") + + +@router.get("/docs", include_in_schema=False) +async def swagger_ui_html(): + return get_swagger_ui_html( + openapi_url="/openapi.json", + title="Safe Queue Service - Swagger UI", + swagger_favicon_url="/static/favicon.ico", + ) + + +@router.get("/redoc", include_in_schema=False) +async def redoc_html(): + return get_redoc_html( + openapi_url="/openapi.json", + title="Safe Queue Service - ReDoc", + redoc_js_url="https://unpkg.com/redoc@next/bundles/redoc.standalone.js", + redoc_favicon_url="/static/favicon.ico", + ) + + +@router.get("/health", include_in_schema=False) +async def health() -> Literal["OK"]: + return "OK" diff --git a/app/tests/__init__.py b/app/tests/__init__.py new file mode 100644 index 0000000..7723ca4 --- /dev/null +++ b/app/tests/__init__.py @@ -0,0 +1 @@ +VERSION = "0.0.0" diff --git a/app/tests/routers/__init__.py b/app/tests/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/tests/routers/test_about.py b/app/tests/routers/test_about.py new file mode 100644 index 0000000..abea23b --- /dev/null +++ b/app/tests/routers/test_about.py @@ -0,0 +1,19 @@ +import unittest + +from fastapi.testclient import TestClient + +from ... import VERSION +from ...main import app + + +class TestRouterAbout(unittest.TestCase): + client: TestClient + + @classmethod + def setUpClass(cls): + cls.client = TestClient(app) + + def test_view_about(self): + response = self.client.get("/api/v1/about") + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), {"version": VERSION}) diff --git a/app/tests/routers/test_default.py b/app/tests/routers/test_default.py new file mode 100644 index 0000000..567181c --- /dev/null +++ b/app/tests/routers/test_default.py @@ -0,0 +1,32 @@ +import unittest + +from fastapi.testclient import TestClient + +from ...main import app + + +class TestRouterDefault(unittest.TestCase): + client: TestClient + + @classmethod + def setUpClass(cls): + cls.client = TestClient(app) + + def test_view_home(self): + response = self.client.get("/", follow_redirects=False) + self.assertEqual(response.status_code, 307) + self.assertTrue(response.has_redirect_location) + self.assertEqual(response.headers["location"], "/docs") + + def test_view_redoc(self): + response = self.client.get("/redoc", follow_redirects=False) + self.assertEqual(response.status_code, 200) + + def test_view_swagger_ui(self): + response = self.client.get("/docs", follow_redirects=False) + self.assertEqual(response.status_code, 200) + + def test_view_health(self): + response = self.client.get("/health") + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), "OK") diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4716f78 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,26 @@ +volumes: + nginx-shared: + +services: + nginx: + image: nginx:alpine + hostname: nginx + ports: + - "8000:8000" + volumes: + - ./docker/nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - nginx-shared:/nginx + depends_on: + - web + web: + build: + context: . + dockerfile: docker/web/Dockerfile + env_file: + - .env + working_dir: /app + ports: + - "8888:8888" + volumes: + - nginx-shared:/nginx + command: docker/web/run_web.sh \ No newline at end of file diff --git a/docker/nginx/nginx.conf b/docker/nginx/nginx.conf new file mode 100644 index 0000000..b9a4ff8 --- /dev/null +++ b/docker/nginx/nginx.conf @@ -0,0 +1,73 @@ +# https://github.com/KyleAMathews/docker-nginx/blob/master/nginx.conf +# https://linode.com/docs/web-servers/nginx/configure-nginx-for-optimized-performance/ +# https://www.uvicorn.org/deployment/ + +worker_processes 1; + +events { + worker_connections 2000; # increase if you have lots of clients + accept_mutex off; # set to 'on' if nginx worker_processes > 1 + use epoll; # Enable epoll for Linux 2.6+ + # 'use kqueue;' to enable for FreeBSD, OSX +} + +http { + include mime.types; + # fallback in case we can't determine a type + default_type application/octet-stream; + sendfile on; + + map $http_upgrade $connection_upgrade { + default upgrade; + '' close; + } + + upstream app_server { + # ip_hash; # For load-balancing + # + # fail_timeout=0 means we always retry an upstream even if it failed + # to return a good HTTP response + server unix:/nginx/uvicorn.socket fail_timeout=0; + + # for a TCP configuration + # server web:8000 fail_timeout=0; + keepalive 32; + } + + server { + access_log off; + listen 8000 deferred; + charset utf-8; + keepalive_timeout 75s; + + # https://thoughts.t37.net/nginx-optimization-understanding-sendfile-tcp-nodelay-and-tcp-nopush-c55cdd276765 + # tcp_nopush on; + # tcp_nodelay on; + + gzip on; + gzip_min_length 1000; + gzip_comp_level 2; + # text/html is always included by default + gzip_types text/plain text/css application/json application/javascript application/x-javascript text/javascript text/xml application/xml application/rss+xml application/atom+xml application/rdf+xml; + gzip_disable "MSIE [1-6]\."; + + location /static { + alias /nginx/static; + expires 365d; + } + + location / { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host $host; + # we don't want nginx trying to do something clever with + # redirects, we set the Host: header above already. + proxy_redirect off; + proxy_pass http://app_server/; + + proxy_set_header X-Forwarded-Host $server_name; + proxy_set_header X-Real-IP $remote_addr; + add_header Front-End-Https on; + } + } +} diff --git a/docker/web/Dockerfile b/docker/web/Dockerfile new file mode 100644 index 0000000..06ab332 --- /dev/null +++ b/docker/web/Dockerfile @@ -0,0 +1,37 @@ +FROM python:3.12-slim + +EXPOSE 8888/tcp +ARG APP_HOME=/app +WORKDIR ${APP_HOME} +ENV PYTHONUNBUFFERED=1 + +COPY requirements/prod.txt ./requirements.txt +RUN set -ex \ + && buildDeps=" \ + build-essential \ + git \ + libssl-dev \ + " \ + && apt-get update \ + && apt-get upgrade -y \ + && apt-get install -y --no-install-recommends $buildDeps \ + && pip install -U --no-cache-dir wheel setuptools pip \ + && pip install --no-cache-dir -r requirements.txt \ + && apt-get purge -y --auto-remove $buildDeps \ + && rm -rf /var/lib/apt/lists/* \ + && find /usr/local \ + \( -type d -a -name test -o -name tests \) \ + -o \( -type f -a -name '*.pyc' -o -name '*.pyo' \) \ + -exec rm -rf '{}' + + + +# /nginx mount point must be created before so it doesn't have root permissions +# ${APP_HOME} root folder will not be updated by COPY --chown, so permissions need to be adjusted +RUN groupadd -g 999 python && \ + useradd -u 999 -r -g python python && \ + mkdir -p /nginx && \ + chown -R python:python /nginx ${APP_HOME} +COPY --chown=python:python . . + +# Use numeric ids so kubernetes identifies the user correctly +USER 999:999 diff --git a/docker/web/run_web.sh b/docker/web/run_web.sh new file mode 100755 index 0000000..e098785 --- /dev/null +++ b/docker/web/run_web.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +set -euo pipefail + +echo "==> $(date +%H:%M:%S) ==> Collecting statics... " +DOCKER_SHARED_DIR=/nginx +rm -rf $DOCKER_SHARED_DIR/* +cp -r static/ $DOCKER_SHARED_DIR/ + +echo "==> $(date +%H:%M:%S) ==> Running Uvicorn... " +exec uvicorn app.main:app --host 0.0.0.0 --port 8888 --proxy-headers --uds $DOCKER_SHARED_DIR/uvicorn.socket \ No newline at end of file diff --git a/requirements/dev.txt b/requirements/dev.txt new file mode 100644 index 0000000..5e86b67 --- /dev/null +++ b/requirements/dev.txt @@ -0,0 +1,7 @@ +-r prod.txt +coverage +freezegun +httpx +mypy +pre-commit +pytest diff --git a/requirements/prod.txt b/requirements/prod.txt new file mode 100644 index 0000000..9e7ad42 --- /dev/null +++ b/requirements/prod.txt @@ -0,0 +1,2 @@ +fastapi[all]==0.111.1 +pydantic-settings==2.4.0 diff --git a/run_tests.sh b/run_tests.sh new file mode 100755 index 0000000..08eedd8 --- /dev/null +++ b/run_tests.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +pytest -rxXs \ No newline at end of file diff --git a/scripts/autodeploy.sh b/scripts/autodeploy.sh new file mode 100644 index 0000000..c27052f --- /dev/null +++ b/scripts/autodeploy.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +set -ev + +curl -s --output /dev/null --write-out "%{http_code}" \ + -H "Content-Type: application/json" \ + -X POST \ + -u "$AUTODEPLOY_TOKEN" \ + -d '{"push_data": {"tag": "'$TARGET_ENV'" }}' \ + $AUTODEPLOY_URL \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..e7851d0 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,45 @@ +[flake8] +max-line-length = 88 +select = C,E,F,W,B,B950 +extend-ignore = E203,E501,F841,W503 +exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv + +[pycodestyle] +max-line-length = 120 +exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv + +[isort] +profile = black +default_section = THIRDPARTY +known_first_party = safe_transaction_service +known_gnosis = py_eth_sig_utils,gnosis +known_fastapi = fastapi,pydantic +sections = FUTURE,STDLIB,FASTAPI,THIRDPARTY,GNOSIS,FIRSTPARTY,LOCALFOLDER + +[mypy] +python_version = 3.12 +check_untyped_defs = True +ignore_missing_imports = True +warn_unused_ignores = True +warn_redundant_casts = True +warn_unused_configs = True + +[coverage:report] +exclude_lines = +# Have to re-enable the standard pragma + pragma: no cover + + # Don't complain if tests don't hit defensive assertion code: + raise NotImplementedError + + # Don't complain if non-runnable code isn't run: + if __name__ == .__main__.: + if settings.DEBUG + + # Ignore pass lines + pass + +[coverage:run] +include = app/* +omit = + test_* diff --git a/static/favicon.ico b/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..aa3ddd177aca986c24ef28fb254979aa92b86878 GIT binary patch literal 15086 zcmdU$X^>o16~|w9&rHuuX42hZ$z*{nBqT(ZK*+uf1qoyUmskS&L5MqBLiRlwQ!HBG z2SkDz%c7)2N+VE~ey~IxwkkxwvM54^BoNjR_Gm(`|KFXy&3muk>+aXn(Xy-lbKkqm zIp>~t-nr+V`^NK9UdEd|*;6^%yMMgrP4hf&^yuP!-t%5mU0x;a-)i!_Thw8q`e=xE zwLrc7jq5KO`M1Xm$q93Wb;54pY2j_*W5E=`^&#OI;Xz@eFi$8nQkGh2iEl>ulz<$M z3U3L=1SfB`lD^FG31O>nwjj+P-=BD-g>!|S!Ye{GIcsfyRkU^qQ-$>Ln1$$e2|I*F z${n8HYoh-hVMHV6AbQ=x<3bV}3Cot`{x9M+Uq}sP9va^yd__2(I**&z>*9N@&{EGF z7$@LU-V-8ud|UN91j>5h#>epZK=s>%)+BRKfAo&*9}2OuFMmE~=HJ?8X0L0l8;ic* zZq~k38>jyh z&q&*M*!d#p1II5^vQ`rUmVUZV33r70#f`VLSMAT66DFh^V*i<}XXbym&8#_)H|yUj z;$YrvJ5q?72eC%RO+eQZg}t@P@A@shcaRw|Eo0E%Nl7s&ZSteiCf(HUznH_2@o6*k z?2I}2tc)$Ed^2m-MElkQlHnYm{HvTyiOOUBest~Yd6O7#i1qg%K^INB(glF&hZ?5*_8OhtQ*dBSh zx3)&wG4?%UMYhrUCnpdaBfDpTTw%C2f4S{w!A$&Yd`vp_q9(KDa4^o?_-?_RGP@~u z&KKX_7Uw_F1$hk+(2KiA=FJ}z>~qIgJFI(Cv7+qR^>Zb5KH)R*@{hSt@_!VRA0Aiy zy2Er$&X~ccq)qPBPjJT4W^-d-uI#;W{EB6y{Lp3Wq|Z0obuy>5(n9p%b@}d&GP?E1 zZ)K1!&4dM&xH5n z8FKQE>;aaC$D;owceTg*&s5qoGgr4-e~lli2R3ICu?~i1iMij1?EKU|UfARB$9qU@ zJN3G(A-^&7oQz$!PB=YnGA;eFI{PNpOMGc!Xzx6HYR0e!_4BRq8lQh=zx4fob$@$! zEOXedn1I~OdhMszt7MOTK~Gz0->T8E�C*=Dqsm;5rhPBjSFi>H_Bky5~HlFQRW? zUBs?dEBC-&yB7JX^&y8xE-%5lfbP!|-LjwRHwgHp*k=kKavbCdL}0+EL;f9Qf)^_! z_ld$E{;TpU2bnkj>%$i1fO8#xF3_oKqxC|T;Q6TK4z)`#QyEcT23HsR!Y<*YPL zbW_44fmj0{TXC&Zdo9le8v6v!AI=ua&Q>bPR8O0D;Gf0`yx+v$>>tYZK&p$ApT4gP z*d)382|^&AsYkAQ#)vQb2lhbhFV~zo{7tx5AZJY8yQ3bS{xQb% zWsO#%5)OObDDL|_j}^-FdLApS>CtyYlnmCNW0c!H?7YL>AwDP*C<0pG)63!G>3cnHd2pXja-uvGQs|DWIn!gK( z6G=2!r)CPA>+Kb4@$>ZAFZ@)PBh7AnyYGwm?qZKaSV zJd*c0SGAbV;VIKOq6*Z9J>V4&l;@E}`$ZvA531*RKPHsvobPGiEm>;@dBvC*?n&_o z*n{p_cWw*w<{ldCBiK(Q;ZIIBhClPeCjIB+*cA3=6<)YtxN{aI;JV#!gD3hQVH~F>GVwVAkT14!8brtug#jvb(XRHSYSVc zf55MuA?&HdpEl;gnLT-uicp`WKObae@av0jB0~8PVjKMA^+HWChSOuEl6?oc?jhrX zc@UmoXitQn#8>YY%5+|>F1)`%CdN>rAD)qa{t^7?&wBCqYP`bj?9(s%al6e|k+Zt= z?sl_Y=R8g?3HbBe-wo5}-qCXNa($Thr++fYkf$uoFXb8cJ%pXVY=Hb%sa<}9GqfA^ zO+ttsYiQY;w)w+?;hc(dJp2-E zwyQm;f9ZG89p&Gn(BHxF2$ksnIafzt*0{kVgZ%L+tMyJfTI5WZ>})slwzOLC?APhL z8QLAK8cWurU%I}v@TV{R(1Ra!@J3c*xodR2;6hAH={n2E7|TE1)I>cGBO_gzA-uTM!AJc}g!nLja041eP4k^M6d z{#E_?!n1xEpg}B&4;nl&WqkwwArA1KysX7+dOz~{6Wh)Rat+?8y;~^xBxE6$#^!DO zO$=TE_|MJpyypkGMc;`hc>|usG57=YP%GRRU-`JMr$=*(u2p~R0h@eHh_j<=_1HOa z_3&sem1{MQ*c6+%OTbrKKdG`3U;G+-;U5aqg;JbdnOl4-Gz9!4zMAy{`yg&%?FL*E zM-j^rPZHDC#Md0k8Pk(8$~SCR?xCA>0;z>QWhr`t{7yhI!0vbmJqFJ1M=&eH#X;72d}Q7NY5`5vJ^aX)SRj|k`d@Xij_c&*Q0ez?=p zgf=m!FK%Z1UdFrc5DQ}z(`oMXx%$tY=Bht+TEFR^o13pnpIbclpP`Vmk9R>*cG;c-`p?UFDw)SXIqCyd0wC1=w5lX9`db1+|RYsmobd} E4}oZ4vH$=8 literal 0 HcmV?d00001