From 085c34a4c725d01894c516d0543dadaa88f71c03 Mon Sep 17 00:00:00 2001 From: Conrad Date: Fri, 25 Oct 2024 13:40:27 +0200 Subject: [PATCH] major: added initial version --- .github/workflows/ci.yml | 121 +++++++++++++++++++++++++++++++++++++++ .gitignore | 28 ++++----- .vscode/launch.json | 35 +++++++++++ .vscode/settings.json | 42 ++++++++++++++ Dockerfile | 25 ++++++++ README.md | 1 + app/__init__.py | 0 app/main.py | 55 ++++++++++++++++++ app/test_main.py | 42 ++++++++++++++ pytest.ini | 5 ++ requirements.txt | 15 +++++ 11 files changed, 351 insertions(+), 18 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 Dockerfile create mode 100644 app/__init__.py create mode 100644 app/main.py create mode 100644 app/test_main.py create mode 100644 pytest.ini create mode 100644 requirements.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ef3fdb3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,121 @@ +name: Lint, Test, Tag, Build and Deploy DEV + +on: + push: + branches: + - dev + - master + paths-ignore: + - "**/.github/**" + - "**/.gitignore" + - "**/.vscode/**" + - "**/README.md" + - "**/CHANGELOG.md" + - "**/docs/**" + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: psf/black@stable + with: + options: "-l 100 --exclude '/.venv/|alembic/|/__init__.py'" + - uses: creyD/autoflake_action@master + with: + no_commit: True + options: --in-place --remove-all-unused-imports -r --exclude **/__init__.py,**/db/models.py, + - uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: Adjusted files for isort & autopep + + test: + runs-on: ubuntu-latest + needs: lint + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.12 + cache: 'pip' # caching pip dependencies + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Run tests + run: pytest + + tag: + needs: test + runs-on: ubuntu-latest + permissions: + contents: write # for the tags + outputs: + version: ${{ steps.git_version.outputs.version }} + + steps: + - uses: actions/checkout@v4 + with: + fetch-tags: true + ref: ${{ github.ref }} + fetch-depth: 0 + + - name: setup git + run: | + git config --local user.email "15138480+creyD@users.noreply.github.com" + git config --local user.name "creyD" + + - name: Git Version + uses: PaulHatch/semantic-version@v5.4.0 + id: git_version + with: + tag_prefix: "" + major_pattern: "breaking:" + minor_pattern: "feat:" + enable_prerelease_mode: false + version_format: "${major}.${minor}.${patch}-rc${increment}" + + - name: Create Tag + run: git tag ${{ steps.git_version.outputs.version }} + + - name: Push tag + run: git push origin ${{ steps.git_version.outputs.version }} + + build_and_push: + runs-on: ubuntu-latest + permissions: write-all + needs: tag + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-${{ github.ref_name }} + tags: latest + + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile + build-args: | + VERSION=${{ needs.tag.outputs.version }}-${{ github.ref_name }} + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.gitignore b/.gitignore index 82f9275..b98379a 100644 --- a/.gitignore +++ b/.gitignore @@ -55,16 +55,6 @@ cover/ *.mo *.pot -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - # Scrapy stuff: .scrapy @@ -77,6 +67,7 @@ target/ # Jupyter Notebook .ipynb_checkpoints +.virtual_documents # IPython profile_default/ @@ -106,10 +97,8 @@ ipython_config.py #pdm.lock # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it # in version control. -# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +# https://pdm.fming.dev/#use-with-ide .pdm.toml -.pdm-python -.pdm-build/ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ @@ -155,8 +144,11 @@ dmypy.json cython_debug/ # PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +.idea/ + +# MacOS +.DS_Store + +*.log +files/ +DOCKER/ diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..882e252 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,35 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run", + "type": "debugpy", + "request": "launch", + "module": "uvicorn", + "args": ["app.main:app", "--reload", "--port", "8000"], + "jinja": true, + "justMyCode": true, + "env": { + "PYDEVD_DISABLE_FILE_VALIDATION": "1" + } + }, + { + "name": "Migrate", + "type": "debugpy", + "request": "launch", + "module": "alembic", + "args": ["upgrade", "head"], + "jinja": true, + "justMyCode": true + }, + { + "name": "Make Migrations", + "type": "debugpy", + "request": "launch", + "module": "alembic", + "args": ["revision", "--autogenerate"], + "jinja": true, + "justMyCode": true + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..de8d7cb --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,42 @@ +{ + "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python3", + "python.globalModuleInstallation": false, + "python.terminal.activateEnvironment": true, + "python.analysis.typeCheckingMode": "basic", + "[python]": { + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit" + }, + "editor.defaultFormatter": "ms-python.black-formatter" + }, + "black-formatter.args": ["--line-length", "100"], + "isort.args": ["--profile", "black"], + // Editor General + "files.insertFinalNewline": true, + "editor.fontSize": 15, + "editor.formatOnSave": true, + "editor.rulers": [100], + "editor.minimap.enabled": true, + "files.exclude": { + "**/.git": true, + "**/.pytest_cache": true, + "**/.venv": true, + "**/.svn": true, + "**/.hg": true, + "**/db.sqlite3": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__/": true + }, + "search.exclude": { + "**/.git": true, + "**/.venv": true, + "**/tmp": true, + "htmlcov/*": true, + "docs/*": true, + ".venv/*": true + }, + "python.testing.pytestArgs": ["app"], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..588fb16 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +FROM python:3.12-slim +ARG VERSION=unkown + +WORKDIR /app +COPY . . + +# Python setup +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 +ENV VERSION=${VERSION} + +# Install dependencies +RUN pip install --no-cache-dir --upgrade -r requirements.txt +RUN pip install 'uvicorn[standard]' + +ENV ENV=DEV + +EXPOSE 8000 +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] + +# Install curl +RUN apt-get update && apt-get install -y curl && apt-get clean + +HEALTHCHECK --interval=30s --timeout=10s --retries=5 \ + CMD curl --fail http://localhost:8000/openapi.json || exit 1 diff --git a/README.md b/README.md index 955e923..2193842 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ # pong + A really simple FastAPI service to return the request with code and content specified in the parameters. diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..47f0559 --- /dev/null +++ b/app/main.py @@ -0,0 +1,55 @@ +import os +from fastapi import APIRouter +from fastapi import Response +from creyPY.fastapi.app import generate_unique_id +from dotenv import load_dotenv +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +load_dotenv() + +ENV = os.getenv("ENV", "local").lower() +VERSION = os.getenv("VERSION", "Alpha") + +# App Setup +app = FastAPI( + title="ServerCrow Pong API", + description="A really simple FastAPI service to return the request with code and content specified in the parameters. No logging, no nothing.", + version=VERSION, + docs_url="/", + redoc_url=None, + debug=ENV != "prod", + swagger_ui_parameters={ + "docExpansion": "list", + "displayOperationId": True, + "defaultModelsExpandDepth": 5, + "defaultModelExpandDepth": 5, + "filter": True, + "displayRequestDuration": True, + "defaultModelRendering": "model", + "persistAuthorization": True, + }, + generate_unique_id_function=generate_unique_id, +) + +# CORS Setup +origins = ["http://localhost:5173", "https://pong.servercrow.com"] +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# App Routers +router = APIRouter(prefix="/pong", tags=["public"]) + + +@router.get("/", operation_id="get_pong") +async def get_status(code: int, response_text: str = "OK") -> Response: + """Get the ping to your pong. Returns the code that is specified and a response if provided.""" + return Response(status_code=code, content=response_text) + + +app.include_router(router) diff --git a/app/test_main.py b/app/test_main.py new file mode 100644 index 0000000..88d1638 --- /dev/null +++ b/app/test_main.py @@ -0,0 +1,42 @@ +from creyPY.fastapi.testing import GenericClient +from .main import app + + +class TestAPI: + def setup_class(self): + self.c = GenericClient(app) + + def test_swagger_gen(self): + re = self.c.get("/openapi.json") + assert re["info"]["title"] == "ServerCrow Pong API" + + def test_health_check(self): + self.c.get("/", parse_json=False) + + def test_get_pong(self): + re = self.c.get("/pong/?code=200&response_text=OK", parse_json=False) + assert re == b"OK" + + def test_get_pong_404(self): + re = self.c.get("/pong/?code=404&response_text=Not Found", parse_json=False, r_code=404) + assert re == b"Not Found" + + def test_get_pong_503(self): + re = self.c.get( + "/pong/?code=503&response_text=Service Unavailable", parse_json=False, r_code=503 + ) + assert re == b"Service Unavailable" + + def test_get_pong_500(self): + re = self.c.get( + "/pong/?code=500&response_text=Internal Server Error", parse_json=False, r_code=500 + ) + assert re == b"Internal Server Error" + + def test_get_pong_400(self): + re = self.c.get("/pong/?code=400&response_text=Bad Request", parse_json=False, r_code=400) + assert re == b"Bad Request" + + def test_get_pong_401(self): + re = self.c.get("/pong/?code=401&response_text=Unauthorized", parse_json=False, r_code=401) + assert re == b"Unauthorized" diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..ad4b7bd --- /dev/null +++ b/pytest.ini @@ -0,0 +1,5 @@ +[pytest] +filterwarnings = + default:::app.* + ignore +addopts = -sl diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d5a1506 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,15 @@ +certifi==2023.11.17 # Testing +iniconfig==2.0.0 # PyTest Testing +packaging==23.2 # PyTest Testing +pluggy==1.3.0 # PyTest Testing +pytest==7.4.3 # PyTest Testing + +click==8.1.7 # Uvicorn +httptools==0.6.1 # Uvicorn +pyyaml==6.0.1 # Uvicorn +uvicorn==0.27.0.post1 # Uvicorn +uvloop==0.19.0 # Uvicorn +watchfiles==0.21.0 # Uvicorn +websockets==12.0 # Uvicorn + +creyPY==1.3.0 # My own package