Skip to content

Commit 260aae5

Browse files
adds nice explicit way to override username.
1 parent 7fd4072 commit 260aae5

File tree

9 files changed

+79
-51
lines changed

9 files changed

+79
-51
lines changed

docs/reference/environment-variables.rst

Lines changed: 44 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -32,24 +32,37 @@ API_TOKEN
3232
Optional Variables
3333
------------------
3434

35-
PYTHONANYWHERE_SITE
36-
~~~~~~~~~~~~~~~~~~~~
35+
PYTHONANYWHERE_CLIENT
36+
~~~~~~~~~~~~~~~~~~~~~~
3737

3838
**Required:** No
3939

40-
**Default:** ``www.pythonanywhere.com`` (or ``www.`` + ``PYTHONANYWHERE_DOMAIN`` if that is set)
40+
**Default:** Not set (library identifies itself without client information)
4141

42-
**Description:** Override the hostname used for API requests. Useful for testing against different PythonAnywhere environments or when using EU servers.
42+
**Description:** Identifies the client application using ``pythonanywhere-core`` in API requests. This information is included in the User-Agent header and helps PythonAnywhere understand API usage patterns and improve service analytics.
4343

44-
.. note::
45-
When running on PythonAnywhere, this variable is automatically set in the environment
46-
to match your system location (e.g., ``www.pythonanywhere.com`` or ``eu.pythonanywhere.com``).
44+
**Format:** ``client-name/version`` (e.g., ``pa/1.0.0``, ``mcp-server/0.5.0``)
45+
46+
**When to use:**
47+
- Building a CLI tool that uses this library
48+
- Creating an MCP server
49+
- Developing automation scripts or custom applications
50+
- Any downstream tool that wraps ``pythonanywhere-core``
51+
52+
**User-Agent format:**
53+
- Without ``PYTHONANYWHERE_CLIENT``: ``pythonanywhere-core/0.2.8 (Python/3.13.7)``
54+
- With ``PYTHONANYWHERE_CLIENT``: ``pythonanywhere-core/0.2.8 (pa/1.0.0; Python/3.13.7)``
4755

4856
**Usage:**
4957

50-
.. code-block:: bash
58+
.. code-block:: python
5159
52-
export PYTHONANYWHERE_SITE="eu.pythonanywhere.com"
60+
import os
61+
from importlib.metadata import version
62+
63+
# Set at application startup
64+
CLI_VERSION = version("my-cli-package")
65+
os.environ["PYTHONANYWHERE_CLIENT"] = f"my-cli/{CLI_VERSION}"
5366
5467
PYTHONANYWHERE_DOMAIN
5568
~~~~~~~~~~~~~~~~~~~~~~
@@ -70,37 +83,39 @@ PYTHONANYWHERE_DOMAIN
7083
7184
export PYTHONANYWHERE_DOMAIN="example.com"
7285
73-
PYTHONANYWHERE_CLIENT
74-
~~~~~~~~~~~~~~~~~~~~~~
86+
PYTHONANYWHERE_SITE
87+
~~~~~~~~~~~~~~~~~~~~
7588

7689
**Required:** No
7790

78-
**Default:** Not set (library identifies itself without client information)
91+
**Default:** ``www.pythonanywhere.com`` (or ``www.`` + ``PYTHONANYWHERE_DOMAIN`` if that is set)
7992

80-
**Description:** Identifies the client application using ``pythonanywhere-core`` in API requests. This information is included in the User-Agent header and helps PythonAnywhere understand API usage patterns and improve service analytics.
93+
**Description:** Override the hostname used for API requests. Useful for testing against different PythonAnywhere environments or when using EU servers.
8194

82-
**Format:** ``client-name/version`` (e.g., ``pa/1.0.0``, ``mcp-server/0.5.0``)
95+
.. note::
96+
When running on PythonAnywhere, this variable is automatically set in the environment
97+
to match your system location (e.g., ``www.pythonanywhere.com`` or ``eu.pythonanywhere.com``).
8398

84-
**When to use:**
85-
- Building a CLI tool that uses this library
86-
- Creating an MCP server
87-
- Developing automation scripts or custom applications
88-
- Any downstream tool that wraps ``pythonanywhere-core``
99+
**Usage:**
89100

90-
**User-Agent format:**
91-
- Without ``PYTHONANYWHERE_CLIENT``: ``pythonanywhere-core/0.2.8 (Python/3.13.7)``
92-
- With ``PYTHONANYWHERE_CLIENT``: ``pythonanywhere-core/0.2.8 (pa/1.0.0; Python/3.13.7)``
101+
.. code-block:: bash
93102
94-
**Usage:**
103+
export PYTHONANYWHERE_SITE="eu.pythonanywhere.com"
95104
96-
.. code-block:: python
105+
PYTHONANYWHERE_USERNAME
106+
~~~~~~~~~~~~~~~~~~~~~~~~
97107

98-
import os
99-
from importlib.metadata import version
108+
**Required:** No
100109

101-
# Set at application startup
102-
CLI_VERSION = version("my-cli-package")
103-
os.environ["PYTHONANYWHERE_CLIENT"] = f"my-cli/{CLI_VERSION}"
110+
**Default:** The current system username (via :func:`getpass.getuser`)
111+
112+
**Description:** Override the PythonAnywhere username used for constructing API endpoints. When running outside of PythonAnywhere (e.g., from a local machine), the system username often won't match your PythonAnywhere username, so this variable lets you specify the correct one.
113+
114+
**Usage:**
115+
116+
.. code-block:: bash
117+
118+
export PYTHONANYWHERE_USERNAME="your_pa_username"
104119
105120
See Also
106121
--------

pythonanywhere_core/base.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import getpass
12
import os
23
import platform
34
from typing import Dict
@@ -20,6 +21,13 @@
2021
}
2122

2223

24+
def get_username() -> str:
25+
"""Returns PythonAnywhere username from ``PYTHONANYWHERE_USERNAME``
26+
environment variable, falling back to :func:`getpass.getuser`."""
27+
28+
return os.environ.get("PYTHONANYWHERE_USERNAME", getpass.getuser())
29+
30+
2331
def get_api_endpoint(username: str, flavor: str) -> str:
2432
hostname = os.environ.get(
2533
"PYTHONANYWHERE_SITE",

pythonanywhere_core/files.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
import getpass
21
from pathlib import Path
32
from typing import Tuple, Union
43
from urllib.parse import urljoin
54

65
from requests.models import Response
76

8-
from pythonanywhere_core.base import call_api, get_api_endpoint
7+
from pythonanywhere_core.base import call_api, get_api_endpoint, get_username
98
from pythonanywhere_core.exceptions import PythonAnywhereApiException
109

1110

@@ -39,7 +38,7 @@ class Files:
3938
"""
4039

4140

42-
base_url = get_api_endpoint(username=getpass.getuser(), flavor="files")
41+
base_url = get_api_endpoint(username=get_username(), flavor="files")
4342
path_endpoint = urljoin(base_url, "path")
4443
sharing_endpoint = urljoin(base_url, "sharing/")
4544
tree_endpoint = urljoin(base_url, "tree/")

pythonanywhere_core/resources.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import getpass
2-
3-
from pythonanywhere_core.base import call_api, get_api_endpoint
1+
from pythonanywhere_core.base import call_api, get_api_endpoint, get_username
42
from pythonanywhere_core.exceptions import PythonAnywhereApiException
53

64

@@ -17,7 +15,7 @@ class CPU:
1715
"""
1816

1917
def __init__(self):
20-
self.base_url = get_api_endpoint(username=getpass.getuser(), flavor="cpu")
18+
self.base_url = get_api_endpoint(username=get_username(), flavor="cpu")
2119

2220
def get_cpu_usage(self):
2321
"""Get current CPU usage information.

pythonanywhere_core/schedule.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import getpass
21
from typing import List, Optional
32

43
from typing_extensions import Literal
54

6-
from pythonanywhere_core.base import call_api, get_api_endpoint
5+
from pythonanywhere_core.base import call_api, get_api_endpoint, get_username
76
from pythonanywhere_core.exceptions import PythonAnywhereApiException
87

98

@@ -28,7 +27,7 @@ class Schedule:
2827
- :meth:`Schedule.update`: Update an existing task.
2928
"""
3029

31-
base_url: str = get_api_endpoint(username=getpass.getuser(), flavor="schedule")
30+
base_url: str = get_api_endpoint(username=get_username(), flavor="schedule")
3231

3332
def create(self, params: dict) -> Optional[dict]:
3433
"""Creates new scheduled task using `params`.

pythonanywhere_core/students.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import getpass
21
from typing import Optional
32

4-
from pythonanywhere_core.base import call_api, get_api_endpoint
3+
from pythonanywhere_core.base import call_api, get_api_endpoint, get_username
54

65

76
class StudentsAPI:
@@ -22,7 +21,7 @@ class StudentsAPI:
2221
- :meth:`StudentsAPI.delete`: Remove a student.
2322
"""
2423

25-
base_url: str = get_api_endpoint(username=getpass.getuser(), flavor="students")
24+
base_url: str = get_api_endpoint(username=get_username(), flavor="students")
2625

2726
def get(self) -> Optional[dict]:
2827
"""Returns list of PythonAnywhere students related with user's account.

pythonanywhere_core/webapp.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
from __future__ import annotations
22

33
import os
4-
import getpass
54
from pathlib import Path
65
from textwrap import dedent
76
from typing import Any
87

98
from dateutil.parser import parse
109

11-
from pythonanywhere_core.base import call_api, get_api_endpoint, PYTHON_VERSIONS
10+
from pythonanywhere_core.base import call_api, get_api_endpoint, get_username, PYTHON_VERSIONS
1211
from pythonanywhere_core.exceptions import SanityException, PythonAnywhereApiException, MissingCNAMEException
1312

1413

@@ -35,7 +34,7 @@ class Webapp:
3534
Class Methods:
3635
- :meth:`Webapp.list_webapps`: List all webapps for the current user.
3736
"""
38-
username = getpass.getuser()
37+
username = get_username()
3938
files_url = get_api_endpoint(username=username, flavor="files")
4039
webapps_url = get_api_endpoint(username=username, flavor="webapps")
4140

pythonanywhere_core/website.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
import getpass
2-
3-
4-
from pythonanywhere_core.base import call_api, get_api_endpoint
1+
from pythonanywhere_core.base import call_api, get_api_endpoint, get_username
52
from pythonanywhere_core.exceptions import DomainAlreadyExistsException, PythonAnywhereApiException
63

74

@@ -24,8 +21,8 @@ class Website:
2421
"""
2522

2623
def __init__(self) -> None:
27-
self.websites_base_url = get_api_endpoint(username=getpass.getuser(), flavor="websites")
28-
self.domains_base_url = get_api_endpoint(username=getpass.getuser(), flavor="domains")
24+
self.websites_base_url = get_api_endpoint(username=get_username(), flavor="websites")
25+
self.domains_base_url = get_api_endpoint(username=get_username(), flavor="domains")
2926

3027

3128
def create(self, domain_name: str, command: str) -> dict:

tests/test_base.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import getpass
12
import platform
23
from pythonanywhere_core import __version__
34

@@ -7,11 +8,24 @@
78
from pythonanywhere_core.base import (
89
call_api,
910
get_api_endpoint,
11+
get_username,
1012
helpful_token_error_message,
1113
)
1214
from pythonanywhere_core.exceptions import AuthenticationError, NoTokenError
1315

1416

17+
def test_get_username_returns_env_var_when_set(monkeypatch):
18+
monkeypatch.setenv("PYTHONANYWHERE_USERNAME", "bill")
19+
20+
assert get_username() == "bill"
21+
22+
23+
def test_get_username_falls_back_to_getpass(monkeypatch):
24+
monkeypatch.delenv("PYTHONANYWHERE_USERNAME", raising=False)
25+
26+
assert get_username() == getpass.getuser()
27+
28+
1529
def test_get_api_endpoint_defaults_to_pythonanywhere_dot_com_if_no_environment_variables():
1630
result = get_api_endpoint(username="bill", flavor="webapp")
1731

0 commit comments

Comments
 (0)