Skip to content

Commit

Permalink
Merge pull request #2 from alneberg/github_actions
Browse files Browse the repository at this point in the history
GitHub actions, config singleton and pre-commit
  • Loading branch information
aanil authored Feb 23, 2024
2 parents 590385c + cbe0dbd commit 24db174
Show file tree
Hide file tree
Showing 10 changed files with 521 additions and 61 deletions.
57 changes: 27 additions & 30 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -1,33 +1,30 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-dockerfile
{
"name": "spectacles",
"build": {
// Sets the run context to one level up instead of the .devcontainer folder.
"context": "..",
// Update the 'dockerFile' property if you aren't using the standard 'Dockerfile' filename.
"dockerfile": "../Dockerfile",
"target": "base"
},
"features": {},
"customizations": {
"vscode": {
"extensions": [
"ms-python.python",
"charliermarsh.ruff"
]
}
},
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
//"postCreateCommand": "cd ../flowcell_parser/ && pip3 install -e . && cd ../TACA && pip3 install -e .",
// Configure tool-specific properties.
// "customizations": {},
// Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "devcontainer"
//"mounts": [
// "source=${localEnv:HOME}/repos/flowcell_parser,target=/workspaces/flowcell_parser,type=bind,consistency=cached"
//]
}
"name": "spectacles",
"build": {
// Sets the run context to one level up instead of the .devcontainer folder.
"context": "..",
// Update the 'dockerFile' property if you aren't using the standard 'Dockerfile' filename.
"dockerfile": "../Dockerfile",
"target": "development"
},
"features": {},
"customizations": {
"vscode": {
"extensions": ["ms-python.python", "charliermarsh.ruff"]
}
}
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
//"postCreateCommand": "cd ../flowcell_parser/ && pip3 install -e . && cd ../TACA && pip3 install -e .",
// Configure tool-specific properties.
// "customizations": {},
// Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "devcontainer"
//"mounts": [
// "source=${localEnv:HOME}/repos/flowcell_parser,target=/workspaces/flowcell_parser,type=bind,consistency=cached"
//]
}
83 changes: 83 additions & 0 deletions .github/workflows/lint-code.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
name: Lint code
on: [push, pull_request]

jobs:
# Use ruff to check for code style violations
ruff-check:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.12"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install ruff
- name: ruff --> Check for style violations
# Configured in pyproject.toml
run: ruff check .

# Use ruff to check code formatting
ruff-format:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.12"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install ruff
- name: ruff --> Check code formatting
run: ruff format --check .

# Use mypy for static type checking
mypy-check:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.12"
- name: Run image
uses: abatilo/actions-poetry@v2
with:
poetry-version: 1.7.1
- name: Setup a local virtual environment (if no poetry.toml file)
run: |
poetry config virtualenvs.create true --local
poetry config virtualenvs.in-project true --local
- uses: actions/cache@v3
name: Define a cache for the virtual environment based on the dependencies lock file
with:
path: ./.venv
key: venv-${{ hashFiles('poetry.lock') }}
- name: Install the project dependencies
run: poetry install
- name: Run mypy
run: poetry run mypy **/*.py

# Use Prettier to check various file formats
prettier:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup node
uses: actions/setup-node@v4
with:
node-version: "20"

- name: Install Prettier
run: npm install -g prettier

- name: Run Prettier --check
run: prettier --check .
21 changes: 21 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# .pre-commit-config.yaml
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.6
hooks:
- id: ruff
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: "v1.8.0"
hooks:
- id: mypy
additional_dependencies: [types-PyYAML]
- repo: https://github.com/pre-commit/mirrors-prettier
rev: "v4.0.0-alpha.8"
hooks:
- id: prettier
- repo: https://github.com/floatingpurr/sync_with_poetry
rev: "" # the revision or tag to clone at
hooks:
- id: sync_with_poetry
args: [] # optional args
10 changes: 8 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,18 @@ RUN pip install --no-cache-dir poetry

# Use Poetry to install dependencies
RUN poetry config virtualenvs.create false \
&& poetry install --no-interaction --no-ansi
&& poetry install --no-interaction --no-ansi --without=dev

# Make port 8000 available to the world outside this container
EXPOSE 8000

FROM base AS development

# Use Poetry to install dependencies
RUN poetry config virtualenvs.create false \
&& poetry install --with=dev --no-interaction --no-ansi

FROM base AS main
# Not meant for production.
# Run app.py when the container launches
CMD ["poetry", "run", "uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
CMD ["poetry", "run", "uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# spectacles
API for a clearer look at clarity

API for a clearer look at clarity using FastAPI.
29 changes: 29 additions & 0 deletions app/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import os

import dotenv

dotenv.load_dotenv()


class Config:
__instance = None

# Ensure only one instance of Config is created
def __new__(cls, *args, **kwargs):
if not cls.__instance:
cls.__instance = super().__new__(cls, *args, **kwargs)
return cls.__instance

def __init__(self):
# Load or set your configuration data here
self.SECRET_KEY = os.getenv("SPECTACLES_SECRET_KEY")
self.ALGORITHM = os.getenv("SPECTACLES_ALGORITHM")
self.ACCESS_TOKEN_EXPIRE_MINUTES = 90


config_values = Config()

if not config_values.SECRET_KEY or not config_values.ALGORITHM:
raise ValueError(
"SPECTACLES_SECRET_KEY and SPECTACLES_ALGORITHM must be set as environmental variables"
)
4 changes: 1 addition & 3 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import dotenv
from fastapi import FastAPI

dotenv.load_dotenv()

# from config import config_values
from .routers import auth


Expand Down
54 changes: 30 additions & 24 deletions app/routers/auth.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from datetime import datetime, timedelta, timezone
import os
from typing import Annotated


Expand All @@ -9,23 +8,18 @@
from passlib.context import CryptContext
from pydantic import BaseModel

# to get a string like this run:
# openssl rand -hex 32

# Fetch environmental variables
SECRET_KEY = os.getenv("SPECTACLES_SECRET_KEY")
ALGORITHM = os.getenv("SPECTACLES_ALGORITHM")

if not SECRET_KEY or not ALGORITHM:
raise ValueError("SPECTACLES_SECRET_KEY and SPECTACLES_ALGORITHM must be set as environmental variables")

ACCESS_TOKEN_EXPIRE_MINUTES = 90

from ..config import config_values

# TODO, this is not a database
clients_db = {
"first_client": {"disabled": False, "client_id": "first_client", "client_secret_hashed": "$2b$12$Yqwzj50q0.5brgJAYwOIEO1l10tdgStMZEB41HwRMFzU/h5wuDsh."}
"first_client": {
"disabled": False,
"client_id": "first_client",
"client_secret_hashed": "$2b$12$Yqwzj50q0.5brgJAYwOIEO1l10tdgStMZEB41HwRMFzU/h5wuDsh.",
}
}


class Token(BaseModel):
access_token: str
token_type: str
Expand All @@ -34,14 +28,17 @@ class Token(BaseModel):
class TokenData(BaseModel):
client_id: str | None = None


class Client(BaseModel):
"""Client without the hashed secret, more suitable to view"""

client_id: str
disabled: bool | None = None


class ClientInDB(Client):
"""Client with hashed secret"""

client_secret_hashed: str


Expand All @@ -59,11 +56,13 @@ def verify_client_secret(plain_secret, hashed_secret):
def get_secret_hash(secret):
return secret_context.hash(secret)


def get_client(db, client_id: str):
if client_id in db:
client_dict = db[client_id]
return ClientInDB(**client_dict)


def authenticate_client(clients_db, client_id: str, client_secret: str):
client: ClientInDB = get_client(clients_db, client_id)
if not client:
Expand All @@ -80,7 +79,9 @@ def create_access_token(data: dict, expires_delta: timedelta | None = None):
else:
expire = datetime.now(timezone.utc) + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
encoded_jwt = jwt.encode(
to_encode, config_values.SECRET_KEY, algorithm=config_values.ALGORITHM
)
return encoded_jwt


Expand All @@ -91,23 +92,28 @@ async def get_current_client(token: Annotated[str, Depends(oauth2_scheme)]):
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
client_id: str = payload.get("sub")
if client_id is None:
payload = jwt.decode(
token, config_values.SECRET_KEY, algorithms=[config_values.ALGORITHM]
)
client_id = payload.get("sub")
if client_id is None or not isinstance(client_id, str):
raise credentials_exception

token_data = TokenData(client_id=client_id)
except JWTError:
raise credentials_exception

# If we get this far, the token is valid
if token_data.client_id is None:
raise credentials_exception
client = get_client(clients_db, client_id=token_data.client_id)
if client is None:
raise credentials_exception
return client


async def get_current_active_client(
current_client: Annotated[Client, Depends(get_current_client)]
current_client: Annotated[Client, Depends(get_current_client)],
):
if current_client.disabled:
raise HTTPException(status_code=400, detail="Inactive user")
Expand All @@ -116,7 +122,7 @@ async def get_current_active_client(

@router.post("/token")
async def login_for_access_token(
form_data: Annotated[OAuth2PasswordRequestForm, Depends()]
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
) -> Token:
client = authenticate_client(clients_db, form_data.username, form_data.password)
if not client:
Expand All @@ -125,7 +131,7 @@ async def login_for_access_token(
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token_expires = timedelta(minutes=config_values.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": client.client_id}, expires_delta=access_token_expires
)
Expand All @@ -134,13 +140,13 @@ async def login_for_access_token(

@router.get("/users/me/", response_model=Client)
async def read_users_me(
current_client: Annotated[Client, Depends(get_current_active_client)]
current_client: Annotated[Client, Depends(get_current_active_client)],
):
return current_client


@router.get("/users/me/items/")
async def read_own_items(
current_client: Annotated[Client, Depends(get_current_active_client)]
current_client: Annotated[Client, Depends(get_current_active_client)],
):
return [{"item_id": "Foo", "owner": current_client.client_id}]
return [{"item_id": "Foo", "owner": current_client.client_id}]
Loading

0 comments on commit 24db174

Please sign in to comment.