Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: tests

on:
pull_request:
push:
branches: [main]

jobs:
tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Install uv and set the Python version
uses: astral-sh/setup-uv@v7
with:
python-version: "3.14"
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Install dependencies
run: uv sync --locked
- name: Run tests
run: uv run pytest
# - name: Clone django-simple-deploy parent project and install dependencies
# run: |
# git clone https://github.com/django-simple-deploy/django-simple-deploy.git
# cd django-simple-deploy/
# uv venv
# uv pip install -e ".[dev]"
# uv add --editable "../[dev]"
# - name: Run integration tests
# run: |
# cd django-simple-deploy/
# uv run pytest
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.13.0
rev: v0.14.6
hooks:
# Run the linter.
- id: ruff-check
Expand Down
27 changes: 27 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# AGENTS.md

This file provides guidance when working with code in this repository.

## Project Overview

dsd-pythonahwyhere is a plugin for deploying Django projects to PythonAnywhere, using django-simple-deploy.

## Development Commands

### Environment Setup

- `uv` is used for Python dependency management.
- Install Python dependencies: `uv sync`
- Add Python dependencies: `uv add <library>` or `uv add --group dev <library>` for dev-only
- You can run generic Python commands using `uv run <command>`

### Testing

- Run tests with pytest: `uv run pytest`
- Tests are located in the `tests/` directory and follow standard pytest and pytest-mock conventions.

### Code Quality

- Run pre-commit hooks: `uv run pre-commit run --all-files`
- Format Python code with Ruff: `uv run ruff format .`
- Lint and auto-fix Python code: `uv run ruff check --fix .`
90 changes: 89 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,93 @@
# dsd-pythonanywhere

A plugin for deploying Django projects to PythonAnywhere, using django-simple-deploy.
A plugin for deploying Django projects to [PythonAnywhere](https://www.pythonanywhere.com/), using django-simple-deploy.

For full documentation, see the documentation for [django-simple-deploy](https://django-simple-deploy.readthedocs.io/en/latest/).

**Current status:** In active development. The plugin currently clones your
repository to PythonAnywhere, but it doesn't configure the web app just yet. Not
yet recommended for actual deployments yet.

## Motivation

This plugin is motivated by the desire to provide a deployment option for
`django-simple-deploy` that doesn't require a credit card to get started.
PythonAnywhere offers a free tier that allows users to deploy small Django apps
and may be a helpful way to get small Django apps online without financial
commitment.

## Quickstart

Deployment to [PythonAnywhere](https://www.pythonanywhere.com/) with this plugin
requires a few prerequisites:

- You must use Git to track your project and push your code to a remote
repository (e.g. GitHub, GitLab, Bitbucket).
- You must track dependencies with a `requirements.txt` file.
- Create a PythonAnywhere [Beginner account](https://www.pythonanywhere.com/registration/register/beginner/),
which is a limited account with one web app, but requires no credit card.
- Generate an [API token](https://help.pythonanywhere.com/pages/GettingYourAPIToken)
- Ideally, stay logged in to PythonAnywhere in your default browser to make the
first deployment smoother.

## Plugin Development

To set up a development environment for working on this plugin alongside
`django-simple-deploy`, follow these steps.

1. Create a parent directory to hold your development work:

```sh
mkdir dsd-dev
cd dsd-dev/
```

2. Clone `dsd-pythonanywhere` for development:

```sh
# Clone dsd-pythonanywhere for development (and switch to branch being worked on)
git clone [email protected]:caktus/dsd-pythonanywhere.git --branch add-api-client
```

3. Clone `django-simple-deploy` and create the blog sample project

```sh
git clone [email protected]:django-simple-deploy/django-simple-deploy.git
cd django-simple-deploy/
# Builds a copy of the sample project in parent dir for testing (../dsd-dev-project-[random_string]/)
uv run python tests/e2e_tests/utils/build_dev_env.py
```

4. Setup the development environment:

```sh
cd ../
cd dsd-dev-project-[random_string]/
source .venv/bin/activate
# Install dsd-pythonanywhere plugin in editable mode
pip install -e "../dsd-pythonanywhere/[dev]"
```

5. Create a [new public repository on GitHub](https://github.com/new).

6. Push the sample project to your new repository:

```sh
git remote add origin [email protected]:[your_github_username]/[your_new_repo_name].git
git branch -M main
git push -u origin main
```

7. Configure environment variables for PythonAnywhere API access:

```sh
export API_USER=[your_pythonanywhere_username]
export API_TOKEN=[your_pythonanywhere_api_token]
```

8. You can now make changes to `dsd-pythonanywhere` in the cloned directory
and test them by running deployments from the sample project:

```sh
python manage.py deploy
```
178 changes: 178 additions & 0 deletions developer_resources/api-exploration.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "561022fd",
"metadata": {},
"source": [
"## Explore interacting with the PythonAnywhere API"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "79210915",
"metadata": {},
"outputs": [],
"source": [
"%load_ext autoreload\n",
"%autoreload 2"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "225f3422-2872-4deb-9783-e249957aca30",
"metadata": {},
"outputs": [],
"source": [
"# Ensure package is in PYTHONPATH\n",
"import sys\n",
"\n",
"if \"..\" not in sys.path:\n",
" sys.path.insert(0, \"..\")"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "af8c7fe0",
"metadata": {},
"outputs": [],
"source": [
"import logging\n",
"import os\n",
"from pathlib import Path\n",
"\n",
"from dsd_pythonanywhere.client import APIClient\n",
"\n",
"logging.basicConfig(\n",
" level=logging.DEBUG, force=True, format=\"%(asctime)s - %(levelname)s - %(message)s\"\n",
")"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "1f0cff23",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Set API_TOKEN\n",
"Set API_USER\n",
"Set API_HOST\n"
]
}
],
"source": [
"# VS Code's Jupyter extension doesn't support loading .envrc, so do it manually here\n",
"\n",
"envrc = Path(\"../.envrc\")\n",
"env_vars = [\n",
" line.lstrip(\"export \").split(\"=\")\n",
" for line in envrc.read_text().splitlines()\n",
" if line.startswith(\"export\")\n",
"]\n",
"for k, v in env_vars:\n",
" os.environ[k] = v\n",
" print(f\"Set {k}\")"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "7adbd24d",
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"2025-09-12 13:46:45,962 - DEBUG - Converted retries value: 3 -> Retry(total=3, connect=None, read=None, redirect=None, status=None)\n"
]
}
],
"source": [
"username = os.getenv(\"API_USER\")\n",
"client = APIClient(username)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c8fbf886",
"metadata": {},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"2025-09-12 13:46:48,086 - DEBUG - Starting new HTTPS connection (1): www.pythonanywhere.com:443\n",
"2025-09-12 13:46:48,242 - DEBUG - https://www.pythonanywhere.com:443 \"GET /api/v0/user/copelco/consoles/ HTTP/1.1\" 200 2\n",
"2025-09-12 13:46:48,243 - DEBUG - API response: 200 []\n",
"2025-09-12 13:46:48,243 - DEBUG - No active bash console found, starting a new one...\n",
"2025-09-12 13:46:48,311 - DEBUG - https://www.pythonanywhere.com:443 \"POST /api/v0/user/copelco/consoles/ HTTP/1.1\" 201 233\n",
"2025-09-12 13:46:48,311 - DEBUG - API response: 201 {\"id\":42095523,\"user\":\"copelco\",\"executable\":\"bash\",\"arguments\":\"\",\"working_directory\":null,\"name\":\"Bash console 42095523\",\"console_url\":\"/user/copelco/consoles/42095523/\",\"console_frame_url\":\"/user/copelco/consoles/42095523/frame/\"}\n",
"2025-09-12 13:46:48,312 - DEBUG - Found bash console: {'id': 42095523, 'user': 'copelco', 'executable': 'bash', 'arguments': '', 'working_directory': None, 'name': 'Bash console 42095523', 'console_url': '/user/copelco/consoles/42095523/', 'console_frame_url': '/user/copelco/consoles/42095523/frame/'}\n",
"2025-09-12 13:46:48,312 - DEBUG - Attempt 0: checking if console is active\n",
"2025-09-12 13:46:48,380 - DEBUG - https://www.pythonanywhere.com:443 \"POST /api/v0/user/copelco/consoles/42095523/send_input/ HTTP/1.1\" 412 87\n",
"2025-09-12 13:46:48,381 - DEBUG - API error status_code=412 error_data={'error': 'Console not yet started. Please load it (or its iframe) in a browser first'}\n",
"2025-09-12 13:46:48,381 - DEBUG - API response: 412 {\"error\":\"Console not yet started. Please load it (or its iframe) in a browser first\"}\n",
"2025-09-12 13:46:48,382 - DEBUG - Console not yet started, opening browser...\n",
"2025-09-12 13:46:48,523 - DEBUG - Console not yet started, waiting...\n",
"2025-09-12 13:46:49,530 - DEBUG - Attempt 1: checking if console is active\n",
"2025-09-12 13:46:49,625 - DEBUG - https://www.pythonanywhere.com:443 \"POST /api/v0/user/copelco/consoles/42095523/send_input/ HTTP/1.1\" 200 15\n",
"2025-09-12 13:46:49,627 - DEBUG - API response: 200 {\"status\":\"OK\"}\n",
"2025-09-12 13:46:49,627 - DEBUG - Console is active.\n",
"2025-09-12 13:46:49,707 - DEBUG - https://www.pythonanywhere.com:443 \"POST /api/v0/user/copelco/consoles/42095523/send_input/ HTTP/1.1\" 200 15\n",
"2025-09-12 13:46:49,708 - DEBUG - API response: 200 {\"status\":\"OK\"}\n",
"2025-09-12 13:46:49,777 - DEBUG - https://www.pythonanywhere.com:443 \"GET /api/v0/user/copelco/consoles/42095523/get_latest_output/ HTTP/1.1\" 200 51\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"Preparing execution environment...\n"
]
}
],
"source": [
"print(client.run_command(\"ls -la\"))"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5e5f7022",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "dsd-pythonanywhere (3.12.0)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.0"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
Loading