Skip to content

Commit

Permalink
Subdirect (#15)
Browse files Browse the repository at this point in the history
### Background and Links
* Django imports at the top level, like `allowedflare.LoginView`, are
problematic because they depend on Django setup

### Changes and Testing
* Rename `allowedflare.allowedflare` to `allowedflare.core` for brevity
* Drop Django-specific imports from the top level. This is a breaking
change. All users will need to update their imports as demonstrated in
`demodj/settings.py`
* Fix warning for backslash dot in plain string `'\.'`
* Improve the example JupyterHub configuration
* Upgrade dependencies and move from `requirements.in` to
`pyproject.toml` because of issues with the
`djangorestframework-stubs[compatible-mypy]` extra seemingly forcing the
oldest version to be chosen; the UV docs say "Note extras are not
supported with the requirements.in format."
* Use uv in the container
  • Loading branch information
covracer authored Nov 8, 2024
1 parent ec0912b commit 314e6d6
Show file tree
Hide file tree
Showing 18 changed files with 336 additions and 237 deletions.
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.venv
26 changes: 17 additions & 9 deletions .github/workflows/check.yaml
Original file line number Diff line number Diff line change
@@ -1,33 +1,41 @@
jobs:
check:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: source includes.sh && ghas
- run: source includes.sh dcb
# Ideally this would come from default.settings_module()
- run: source includes.sh dcr web python -m pytest --ds=demodj.settings

check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
cache: npm
node-version-file: .nvmrc
- run: npm install --frozen-lockfile

- uses: actions/setup-python@v5
with:
python-version-file: .python-version

- run: pip install --upgrade pip uv
env:
PIP_PROGRESS_BAR: 'off'
- run: |
pip install --disable-pip-version-check --progress-bar off --upgrade \
"$(grep ^uv requirements.txt)"
- run: uv venv
- run: source includes.sh && ups && a && pcam --color always --show-diff-on-failure
- run: .venv/bin/python -m build
- run: .venv/bin/twine check dist/*
- run: .venv/bin/python -m manage check
- run: .venv/bin/python -m manage makemigrations --check
- run: source includes.sh dcb
# Ideally this would come from default.settings_module()
- run: source includes.sh dcr web pytest --ds=demodj.settings

summarize:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: source includes.sh summarize

on: # yamllint disable-line rule:truthy
- pull_request
9 changes: 8 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,23 @@ repos:
- --write-changes
- id: dot-yaml
- id: gitignore-node-python
- id: hadolint
- id: includes-sh
- id: mailmap
- id: mypy
- id: prettier-write
exclude: ^package-lock\.json$
- id: ruff-check-fix
- id: ruff-format
- id: shellcheck
- id: uv-pip-compile
args:
- --all-extras
- --quiet
- pyproject.toml
files: pyproject.toml
- id: yamllint
exclude: |
(?x)^(
package-lock.json
package-lock\.json
)$
39 changes: 29 additions & 10 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,20 +1,39 @@
FROM python:3.12
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim

# TODO assert C.UTF8 and PYTHONUNBUFFERED are set correctly
# TODO assert C.UTF8 locale and PYTHONUNBUFFERED are set correctly
ENV PYTHONUNBUFFERED 1

WORKDIR /srv
RUN --mount=type=cache,target=/root/.cache pip install --upgrade pip-tools wheel
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache pip-sync

# TODO multi-stage for faster development builds?
RUN mkdir -p static
# hadolint ignore=DL3008,SC2046
RUN --mount=type=cache,target=/var/cache/apt \
--mount=type=bind,source=includes.sh,target=includes.sh \
rm /etc/apt/apt.conf.d/docker-clean \
&& echo 'Binary::apt::APT::Keep-Downloaded-Packages "1";' > /etc/apt/apt.conf.d/99cache \
&& apt-get update \
&& apt-get install -qq --no-install-recommends --yes nodejs npm \
$(sed -En 's/"$//; s/^PACKAGES="//p' includes.sh) \
&& rm -rf /var/lib/apt/lists/*

# Bind mount caused "EROFS: read-only file system, open '/srv/package-lock.json'"
COPY package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
--mount=type=bind,source=package.json,target=package.json \
npm install --frozen-lockfile

COPY . .
# Not using /srv/.venv because it would make volume-mounting /srv harder
ENV UV_COMPILE_BYTECODE=1
ENV UV_LINK_MODE=copy
ENV UV_SYSTEM_PYTHON=1
# hadolint ignore=DL3013,DL3042
RUN --mount=type=cache,target=/root/.cache \
--mount=type=bind,source=requirements.txt,target=requirements.txt \
uv pip sync --quiet requirements.txt

COPY . ./

ARG STATIC_URL
ENV STATIC_URL ${STATIC_URL:-/static/}
RUN STATIC_URL=${STATIC_URL} python -m manage collectstatic --no-input
RUN mkdir -p static && STATIC_URL=${STATIC_URL} python -m manage collectstatic --no-input

EXPOSE 8001
ENV PYTHONUNBUFFERED 1
25 changes: 4 additions & 21 deletions allowedflare/__init__.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,6 @@
# TODO Avoid "AppRegistryNotReady: Apps aren't loaded yet."
# To avoid "AppRegistryNotReady: Apps aren't loaded yet." and similar problems, this file must
# only import from the likes of allowedflare.core, not the likes of allowedflare.django.

from allowedflare.allowedflare import clean_username
from allowedflare.core import authenticate, clean_username

# Django REST Framework authentication class
from allowedflare.django import Authentication

# Django Admin authentication backend
from allowedflare.django import Backend

# Django Admin login view
from allowedflare.django import LoginView

# SQL Explorer login view wrapper
from allowedflare.django import login_view_wrapper

__all__ = (
Authentication.__name__,
Backend.__name__,
LoginView.__name__,
clean_username.__name__,
login_view_wrapper.__name__,
)
__all__ = (authenticate.__name__, clean_username.__name__)
File renamed without changes.
2 changes: 1 addition & 1 deletion allowedflare/django.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from django.http.response import HttpResponseBase
from rest_framework.authentication import BaseAuthentication

from allowedflare.allowedflare import authenticate
from allowedflare import authenticate

logger = logging.getLogger(__name__)

Expand Down
2 changes: 1 addition & 1 deletion allowedflare/juptyerhub.py → allowedflare/jupyter.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from jupyterhub.auth import Authenticator # type: ignore
from tornado.web import RequestHandler

from allowedflare.allowedflare import authenticate
from allowedflare import authenticate


class JupyterHub(Authenticator):
Expand Down
11 changes: 5 additions & 6 deletions allowedflare/test_allowedflare.py → allowedflare/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
from cryptography.hazmat.backends import default_backend
from datetime import UTC, datetime

from allowedflare import clean_username
from allowedflare.allowedflare import authenticate
from allowedflare import authenticate, clean_username


def test_clean_username_unmodified(monkeypatch):
Expand All @@ -18,7 +17,7 @@ def test_clean_username_unmodified(monkeypatch):


def test_clean_username_email_domain_removed(monkeypatch):
monkeypatch.setenv('ALLOWEDFLARE_EMAIL_REGEX', 'domain\.com')
monkeypatch.setenv('ALLOWEDFLARE_EMAIL_REGEX', r'domain\.com')
assert clean_username('user@domain.com') == 'user'


Expand Down Expand Up @@ -57,7 +56,7 @@ def test_authenticate_invalid_token(monkeypatch):


def test_authenticate_jwk_client_error(monkeypatch):
private_key = generate_private_key(65537, 512, default_backend())
private_key = generate_private_key(65537, 1024, default_backend())
monkeypatch.setenv('ALLOWEDFLARE_ACCESS_URL', 'https://demo.cloudflareaccess.com')
user, message, token = authenticate({'CF_Authorization': encode({}, private_key, 'RS256')})
assert user == ''
Expand All @@ -66,7 +65,7 @@ def test_authenticate_jwk_client_error(monkeypatch):


def test_authenticate_valid_token(mocker, monkeypatch):
private_key = generate_private_key(65537, 512, default_backend())
private_key = generate_private_key(65537, 1024, default_backend())
token = {
'aud': 'audience',
'email': 'firstname.lastname@domain.com',
Expand All @@ -76,7 +75,7 @@ def test_authenticate_valid_token(mocker, monkeypatch):
monkeypatch.setenv('ALLOWEDFLARE_ACCESS_URL', 'https://demo.cloudflareaccess.com')
monkeypatch.setenv('ALLOWEDFLARE_AUDIENCE', 'audience')
mock_get_signing_key_from_jwt = mocker.patch(
'allowedflare.allowedflare.PyJWKClient.get_signing_key_from_jwt', autospec=True
'allowedflare.core.PyJWKClient.get_signing_key_from_jwt', autospec=True
)
mock_get_signing_key_from_jwt.return_value.key = private_key.public_key()

Expand Down
2 changes: 1 addition & 1 deletion allowedflare/test_django.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.test import RequestFactory

from allowedflare import LoginView
from allowedflare.django import LoginView


def test_allowedflare_login_view(monkeypatch, rf: RequestFactory):
Expand Down
11 changes: 7 additions & 4 deletions demodj/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,13 @@
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

AUTHENTICATION_BACKENDS = ['django.contrib.auth.backends.ModelBackend', 'allowedflare.Backend']
AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend',
'allowedflare.django.Backend',
]

REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': ('allowedflare.Authentication',),
'DEFAULT_AUTHENTICATION_CLASSES': ('allowedflare.django.Authentication',),
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'DEFAULT_PERMISSION_CLASSES': ('rest_framework.permissions.IsAuthenticated',),
'PAGE_SIZE': 10,
Expand All @@ -73,7 +76,7 @@
TEMPLATES = [
{ # To avoid admin.E403, configure Django templates
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': ['django_allowedflare'],
'DIRS': ['allowedflare'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
Expand All @@ -95,7 +98,7 @@
DATABASES = {'default': {'ENGINE': 'django.db.backends.sqlite3', 'NAME': BASE_DIR / 'db.sqlite3'}}
EXPLORER_CONNECTIONS = {'Demo DJ': 'default'}
EXPLORER_DEFAULT_CONNECTION = 'default'
EXPLORER_NO_PERMISSION_VIEW = 'allowedflare.login_view_wrapper'
EXPLORER_NO_PERMISSION_VIEW = 'allowedflare.django.login_view_wrapper'


# Password validation
Expand Down
2 changes: 1 addition & 1 deletion demodj/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from django.urls import include, path
from rest_framework import routers

from allowedflare import LoginView
from allowedflare.django import LoginView
from . import views

router = routers.DefaultRouter()
Expand Down
7 changes: 3 additions & 4 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ services:
proxy:
image: cloudflare/cloudflared:2024.2.1

lab:
build: .
command: python -m manage shell_plus --allow-root
hub:
command: PATH="node_modules/.bin:$PATH" .venv/bin/python -m jupyterhub
depends_on:
- postgres
environment:
Expand All @@ -29,7 +28,7 @@ services:

web:
build: .
command: python -m manage runserver
command: .venv/bin/python -m manage runserver
depends_on:
- postgres
environment:
Expand Down
4 changes: 2 additions & 2 deletions jupyterhub_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@
# - null: jupyterhub.auth.NullAuthenticator
# - pam: jupyterhub.auth.PAMAuthenticator
# Default: 'jupyterhub.auth.PAMAuthenticator'
c.JupyterHub.authenticator_class = 'jupyterhub_allowedflare.JupyterHub'
c.JupyterHub.authenticator_class = 'allowedflare.jupyter.JupyterHub'

## The base URL of the entire application.
#
Expand Down Expand Up @@ -1150,7 +1150,7 @@
# Note that this does *not* prevent users from accessing files outside of this
# path! They can do so with many other means.
# Default: ''
# c.Spawner.notebook_dir = ''
c.Spawner.notebook_dir = '~/code/allowedflare'

## Allowed scopes for oauth tokens issued by this server's oauth client.
#
Expand Down
5 changes: 5 additions & 0 deletions overrides.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"@jupyterlab/apputils-extension:themes": {
"theme": "JupyterLab Dark"
}
}
27 changes: 26 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,33 @@ dynamic = ['version']
name = 'allowedflare'
readme = 'README.md'

[project.optional-dependencies]
precommit = [
'codespell',
'django-stubs',
'djangorestframework-stubs[compatible-mypy]',
'hadolint-py @ git+https://github.com/AleksaC/hadolint-py.git',
'mypy',
'pre-commit',
'ruff',
'shellcheck-py',
'uv',
'yamllint',
]
demo = [
'dj-notebook',
'django',
'django-extensions',
'django-health-check',
'django-sql-explorer',
'djangorestframework',
'jupyterlab',
]
build = ['build', 'twine']
test = ['pytest', 'pytest-django', 'pytest-mock']

[project.urls]
Homepage = 'https://github.com/covracer/allowedflare'
Homepage = 'https://github.com/biobuddies/allowedflare'

[tool.codespell]
ignore-words-list = 'afterall'
Expand Down
29 changes: 0 additions & 29 deletions requirements.in

This file was deleted.

Loading

0 comments on commit 314e6d6

Please sign in to comment.