Skip to content

Commit e9ac62e

Browse files
authored
Improvements after running through Django Girls tutorial (#12)
1 parent 0686f7d commit e9ac62e

File tree

14 files changed

+441
-276
lines changed

14 files changed

+441
-276
lines changed

.github/workflows/tests.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,7 @@ jobs:
1818
cache-dependency-glob: "uv.lock"
1919
- name: Install dependencies
2020
run: uv sync --locked
21+
- name: Run pre-commit
22+
run: uv run pre-commit run --all-files
2123
- name: Run tests
2224
run: uv run pytest

AGENTS.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ This file provides guidance when working with code in this repository.
44

55
## Project Overview
66

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

99
## Development Commands
1010

@@ -19,9 +19,12 @@ dsd-pythonahwyhere is a plugin for deploying Django projects to PythonAnywhere,
1919

2020
- Run tests with pytest: `uv run pytest`
2121
- Tests are located in the `tests/` directory and follow standard pytest and pytest-mock conventions.
22+
- Integration tests (in `dsd-pythonanywhere/tests/integration_tests`) must be run from the `django-simple-deploy` project root. See `.github/workflows/integration_tests.yaml` for test setup details.
23+
- Use `-k` to filter tests by name pattern, e.g., `uv run pytest -k "test_setup_script"` to run only setup script tests.
24+
- Add or update tests for the code you change, even if nobody asked.
25+
- New features and bug fixes should always include a concise test (not exhaustive).
26+
- Always run full test suite and ruff pre-commit hooks as the last tasks in your todo list
2227

2328
### Code Quality
2429

25-
- Run pre-commit hooks: `uv run pre-commit run --all-files`
26-
- Format Python code with Ruff: `uv run ruff format .`
27-
- Lint and auto-fix Python code: `uv run ruff check --fix .`
30+
- Run ruff pre-commit hooks: `uv run pre-commit run --all-files`.

README.md

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,53 @@ requires a few prerequisites:
4141
- Create a PythonAnywhere [Beginner account](https://www.pythonanywhere.com/registration/register/beginner/),
4242
which is a limited account with one web app, but requires no credit card.
4343
- Generate an [API token](https://help.pythonanywhere.com/pages/GettingYourAPIToken)
44-
- Ideally, stay logged in to PythonAnywhere in your default browser to make the
45-
deployment smoother.
44+
- Stay logged in to PythonAnywhere in your default browser.
4645

47-
With those prerequisites met, you can install the plugin and deploy your app:
46+
With those prerequisites met, and if you're coming from the [Django Girls tutorial Deploy section](https://tutorial.djangogirls.org/en/deploy/),
47+
you can deploy your project with the following steps:
48+
49+
1. Export your PythonAnywhere API credentials and install `dsd-pythonanywhere`:
50+
51+
```sh
52+
# Export your PythonAnywhere API credentials as environment variables
53+
export API_USER=[your_pythonanywhere_username]
54+
export API_TOKEN=[your_pythonanywhere_api_token]
55+
# Install dsd-pythonanywhere (which also installs django-simple-deploy)
56+
pip install git+https://github.com/caktus/dsd-pythonanywhere.git@main
57+
```
58+
59+
2. Add `django-simple-deploy` to your `INSTALLED_APPS` in `settings.py`:
60+
61+
```diff
62+
diff --git a/mysite/settings.py b/mysite/settings.py
63+
index 8bf8f39..b288aa1 100644
64+
--- a/mysite/settings.py
65+
+++ b/mysite/settings.py
66+
@@ -38,6 +38,7 @@ INSTALLED_APPS = [
67+
"django.contrib.messages",
68+
"django.contrib.staticfiles",
69+
"blog",
70+
+ "django_simple_deploy",
71+
]
72+
```
73+
74+
3. Run the deployment command:
4875

4976
```sh
50-
# TBD
77+
python manage.py deploy --automate-all
78+
```
79+
80+
This command can take several minutes as it creates the web app, installs
81+
dependencies, etc. You should see progress in your browser console on
82+
PythonAnywhere as well.
83+
84+
If you run into issues and need to re-run the deployment, you may need to
85+
reset your local and remote repositories to a clean state first:
86+
87+
```sh
88+
# Stash any local changes
89+
git stash --include-untracked
90+
# Go back to step 2 since settings.py was reverted
5191
```
5292

5393
## Approach
@@ -61,16 +101,21 @@ console, changes not being committed/pushed to version control, etc.
61101

62102
This plugin integrates with `django-simple-deploy` to provide a more familiar
63103
local workflow, though with some caveats due to free tier limitations (primarily
64-
lack of SSH access).
104+
lack of SSH access and required browser interaction).
65105

66106
```mermaid
67107
sequenceDiagram
68108
participant User as Local Machine
109+
participant Browser
69110
participant GitHub
70111
participant PA as PythonAnywhere
71112
72113
User->>GitHub: Commit & push changes
73114
115+
User->>PA: Bash Console API: create console
116+
User->>Browser: Open console URL
117+
Note over Browser,PA: Browser connection starts bash process
118+
74119
User->>PA: Bash Console API: clone repo
75120
PA->>GitHub: git clone
76121
PA->>PA: Install dependencies & create .env
@@ -82,7 +127,15 @@ sequenceDiagram
82127
```
83128

84129
**Note:** Users should stay logged into PythonAnywhere in their default browser
85-
during deployment, as the console API may need to start a new console session.
130+
during deployment.
131+
132+
Additionally:
133+
134+
* If a PythonAnywhere bash console isn't already running, the plugin will
135+
programmatically open your browser to the console URL. This is required because
136+
the PythonAnywhere API creates console objects but doesn't start the actual
137+
process. Only connecting to the console in a browser will do that (per the [API
138+
documentation](https://help.pythonanywhere.com/pages/API/#apiv0userusernameconsoles)).
86139

87140
## Plugin Development
88141

developer_resources/api-exploration.ipynb

Lines changed: 36 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
},
1111
{
1212
"cell_type": "code",
13-
"execution_count": 1,
13+
"execution_count": null,
1414
"id": "79210915",
1515
"metadata": {},
1616
"outputs": [],
@@ -21,7 +21,7 @@
2121
},
2222
{
2323
"cell_type": "code",
24-
"execution_count": 2,
24+
"execution_count": null,
2525
"id": "225f3422-2872-4deb-9783-e249957aca30",
2626
"metadata": {},
2727
"outputs": [],
@@ -35,7 +35,7 @@
3535
},
3636
{
3737
"cell_type": "code",
38-
"execution_count": 3,
38+
"execution_count": null,
3939
"id": "af8c7fe0",
4040
"metadata": {},
4141
"outputs": [],
@@ -44,7 +44,7 @@
4444
"import os\n",
4545
"from pathlib import Path\n",
4646
"\n",
47-
"from dsd_pythonanywhere.client import APIClient\n",
47+
"from dsd_pythonanywhere.client import PythonAnywhereClient\n",
4848
"\n",
4949
"logging.basicConfig(\n",
5050
" level=logging.DEBUG, force=True, format=\"%(asctime)s - %(levelname)s - %(message)s\"\n",
@@ -53,19 +53,10 @@
5353
},
5454
{
5555
"cell_type": "code",
56-
"execution_count": 6,
56+
"execution_count": null,
5757
"id": "1f0cff23",
5858
"metadata": {},
59-
"outputs": [
60-
{
61-
"name": "stdout",
62-
"output_type": "stream",
63-
"text": [
64-
"Set API_TOKEN\n",
65-
"Set API_USER\n"
66-
]
67-
}
68-
],
59+
"outputs": [],
6960
"source": [
7061
"# VS Code's Jupyter extension doesn't support loading .envrc, so do it manually here\n",
7162
"\n",
@@ -82,71 +73,38 @@
8273
},
8374
{
8475
"cell_type": "code",
85-
"execution_count": 7,
76+
"execution_count": null,
8677
"id": "7adbd24d",
8778
"metadata": {},
88-
"outputs": [
89-
{
90-
"name": "stderr",
91-
"output_type": "stream",
92-
"text": [
93-
"2025-12-29 12:32:51,046 - DEBUG - Converted retries value: 3 -> Retry(total=3, connect=None, read=None, redirect=None, status=None)\n"
94-
]
95-
}
96-
],
79+
"outputs": [],
9780
"source": [
9881
"username = os.getenv(\"API_USER\")\n",
99-
"client = APIClient(username)"
82+
"client = PythonAnywhereClient(username)"
10083
]
10184
},
10285
{
10386
"cell_type": "code",
10487
"execution_count": null,
10588
"id": "c8fbf886",
10689
"metadata": {},
107-
"outputs": [
108-
{
109-
"name": "stderr",
110-
"output_type": "stream",
111-
"text": [
112-
"2025-09-12 13:46:48,086 - DEBUG - Starting new HTTPS connection (1): www.pythonanywhere.com:443\n",
113-
"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",
114-
"2025-09-12 13:46:48,243 - DEBUG - API response: 200 []\n",
115-
"2025-09-12 13:46:48,243 - DEBUG - No active bash console found, starting a new one...\n",
116-
"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",
117-
"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",
118-
"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",
119-
"2025-09-12 13:46:48,312 - DEBUG - Attempt 0: checking if console is active\n",
120-
"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",
121-
"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",
122-
"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",
123-
"2025-09-12 13:46:48,382 - DEBUG - Console not yet started, opening browser...\n",
124-
"2025-09-12 13:46:48,523 - DEBUG - Console not yet started, waiting...\n",
125-
"2025-09-12 13:46:49,530 - DEBUG - Attempt 1: checking if console is active\n",
126-
"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",
127-
"2025-09-12 13:46:49,627 - DEBUG - API response: 200 {\"status\":\"OK\"}\n",
128-
"2025-09-12 13:46:49,627 - DEBUG - Console is active.\n",
129-
"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",
130-
"2025-09-12 13:46:49,708 - DEBUG - API response: 200 {\"status\":\"OK\"}\n",
131-
"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"
132-
]
133-
},
134-
{
135-
"name": "stdout",
136-
"output_type": "stream",
137-
"text": [
138-
"\n",
139-
"Preparing execution environment...\n"
140-
]
141-
}
142-
],
90+
"outputs": [],
14391
"source": [
144-
"print(client.run_command(\"ls -la\"))"
92+
"client.request(method=\"GET\", url=client._base_url(\"cpu\"))"
14593
]
14694
},
14795
{
14896
"cell_type": "code",
149-
"execution_count": 8,
97+
"execution_count": null,
98+
"id": "83e6f6f1",
99+
"metadata": {},
100+
"outputs": [],
101+
"source": [
102+
"client.webapp_exists()"
103+
]
104+
},
105+
{
106+
"cell_type": "code",
107+
"execution_count": null,
150108
"id": "5e5f7022",
151109
"metadata": {},
152110
"outputs": [],
@@ -156,81 +114,45 @@
156114
},
157115
{
158116
"cell_type": "code",
159-
"execution_count": 16,
117+
"execution_count": null,
160118
"id": "180bcefc",
161119
"metadata": {},
162120
"outputs": [],
163121
"source": [
164-
"webapp = Webapp(\"copelco.pythonanywhere.com\")"
122+
"webapp = Webapp.list_webapps()"
165123
]
166124
},
167125
{
168126
"cell_type": "code",
169-
"execution_count": 17,
127+
"execution_count": null,
170128
"id": "1fe24344",
171129
"metadata": {},
172-
"outputs": [
173-
{
174-
"name": "stderr",
175-
"output_type": "stream",
176-
"text": [
177-
"2025-12-29 13:12:08,848 - DEBUG - Starting new HTTPS connection (1): www.pythonanywhere.com:443\n"
178-
]
179-
},
180-
{
181-
"name": "stderr",
182-
"output_type": "stream",
183-
"text": [
184-
"2025-12-29 13:12:09,476 - DEBUG - https://www.pythonanywhere.com:443 \"GET /api/v0/user/copelco/webapps/copelco.pythonanywhere.com/ HTTP/1.1\" 403 63\n"
185-
]
186-
}
187-
],
130+
"outputs": [],
188131
"source": [
189132
"webapp.sanity_checks(nuke=False)"
190133
]
191134
},
192135
{
193136
"cell_type": "code",
194-
"execution_count": 18,
137+
"execution_count": null,
195138
"id": "a27334f9",
196139
"metadata": {},
197-
"outputs": [
198-
{
199-
"name": "stderr",
200-
"output_type": "stream",
201-
"text": [
202-
"2025-12-29 13:12:11,430 - DEBUG - Starting new HTTPS connection (1): www.pythonanywhere.com:443\n",
203-
"2025-12-29 13:12:20,857 - DEBUG - https://www.pythonanywhere.com:443 \"POST /api/v0/user/copelco/webapps/ HTTP/1.1\" 200 94\n",
204-
"2025-12-29 13:12:20,863 - DEBUG - Starting new HTTPS connection (1): www.pythonanywhere.com:443\n",
205-
"2025-12-29 13:12:21,097 - DEBUG - https://www.pythonanywhere.com:443 \"PATCH /api/v0/user/copelco/webapps/copelco.pythonanywhere.com/ HTTP/1.1\" 200 None\n"
206-
]
207-
}
208-
],
140+
"outputs": [],
209141
"source": [
210-
"webapp.create(python_version=\"3.13\", virtualenv_path=\"/home/copelco/venv\", project_path=\"/home/copelco/dsd-testproj\", nuke=False)"
142+
"webapp.create(\n",
143+
" python_version=\"3.13\",\n",
144+
" virtualenv_path=\"/home/copelco/venv\",\n",
145+
" project_path=\"/home/copelco/dsd-testproj\",\n",
146+
" nuke=False,\n",
147+
")"
211148
]
212149
},
213150
{
214151
"cell_type": "code",
215-
"execution_count": 25,
152+
"execution_count": null,
216153
"id": "64c54a59",
217154
"metadata": {},
218-
"outputs": [
219-
{
220-
"name": "stderr",
221-
"output_type": "stream",
222-
"text": [
223-
"2025-12-29 13:38:21,458 - DEBUG - Starting new HTTPS connection (1): www.pythonanywhere.com:443\n"
224-
]
225-
},
226-
{
227-
"name": "stderr",
228-
"output_type": "stream",
229-
"text": [
230-
"2025-12-29 13:38:32,034 - DEBUG - https://www.pythonanywhere.com:443 \"POST /api/v0/user/copelco/webapps/copelco.pythonanywhere.com/reload/ HTTP/1.1\" 200 15\n"
231-
]
232-
}
233-
],
155+
"outputs": [],
234156
"source": [
235157
"webapp.reload()"
236158
]

0 commit comments

Comments
 (0)