diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 0000000..4259262 --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,40 @@ +name: Tests + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [ '3.6', '3.7', '3.8', '3.9', '3.10' ] + + name: Python ${{ matrix.python-version }} + steps: + + - uses: actions/checkout@v2 + + - name: Setup timezone + uses: zcong1993/setup-timezone@master + with: + timezone: UTC + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + + - name: Install Python dependencies + run: | + python3 -m pip install --upgrade pip + pip3 install -r requirements.txt + pip3 install -e . + + - name: Test with pytest + run: | + pytest + + - name: Check coverage + run: | + pytest --cov=cli --cov=pythonanywhere --cov=scripts --cov-fail-under=65 \ No newline at end of file diff --git a/README.md b/README.md index 4deda82..eba75c8 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,60 @@ -[![Build Status](https://travis-ci.org/pythonanywhere/helper_scripts.svg?branch=master)](https://travis-ci.org/pythonanywhere/helper_scripts) +![Build Status](https://github.com/pythonanywhere/helper_scripts/actions/workflows/tests.yaml/badge.svg) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![PyPI](https://img.shields.io/pypi/v/pythonanywhere)](https://pypi.org/project/pythonanywhere/) [![Downloads](https://pepy.tech/badge/pythonanywhere)](https://pepy.tech/project/pythonanywhere) -# PythonAnywhere helper scripts +# PythonAnywhere cli tool -These scripts are designed to be run from PythonAnywhere consoles +`pa` is a single command to manage PythonAnywhere services. + +It is designed to be run from PythonAnywhere consoles, but many subcommands can be executed directly +from your own machine (see [usage](#Usage) below). ## Installing +### On PythonAnywhere +In a PythonAnywhere Bash console, run: + + pip3.9 install --user pythonanywhere - pip3.6 install --user pythonanywhere +If there is no `python3.9` on your PythonAnywhere account, +you should upgrade your account to the newest system image. +See [here](https://help.pythonanywhere.com/pages/ChangingSystemImage) how to do that. +`pa` works with python 3.6, 3.7 and 3.8, but we recommend using the latest system image. + +### On your own machine +Install the `pythonanywhere` package from [PyPI](https://pypi.org/project/pythonanywhere/). +We recommend using `pipx` if you want to use it only as a cli tool, or a virtual environment +if you want to use a programmatic interface in your own code. -If there is no `python3.6` on your PythonAnywhere account, -you should contact [support@pythonanywhere.com](mailto:support@pythonanywhere.com) and ask for an upgrade. - ## Usage -There are two ways to use that package. You can just run the scripts or use underlying api wrappers directly in your scripts. +There are two ways to use the package. You can just run the scripts or use the underlying api wrappers directly in your scripts. + +### Command line interface + +### Running `pa` on your local machine + +`pa` expects the presence of some environment variables that are provided when you run your code in a PythonAnywere console. +You need to provide them if you run `pa` on your local machine. + +`API_TOKEN` -- you need to set this to allow `pa` to connect to the [PythonAnywere API](https://help.pythonanywhere.com/pages/API). +To get an API token, log into PythonAnywhere and go to the "Account" page using the link at the top right. +Click on the "API token" tab, and click the "Create a new API token" button to get your token. -There are scripts provided for dealing with web apps: +`PYTHONANYWHERE_SITE` is used to connect to PythonAnywhere API and defaults to `www.pythonanywhere.com`, +but you may need to set it to `eu.pythonanywhere.com` if you use our EU site. -* [pa_autoconfigure_django.py](https://github.com/pythonanywhere/helper_scripts/blob/master/scripts/pa_autoconfigure_django.py) -* [pa_create_webapp_with_virtualenv.py](https://github.com/pythonanywhere/helper_scripts/blob/master/scripts/pa_create_webapp_with_virtualenv.py) -* [pa_delete_webapp_logs.py](https://github.com/pythonanywhere/helper_scripts/blob/master/scripts/pa_delete_webapp_logs.py) -* [pa_install_webapp_letsencrypt_ssl.py](https://github.com/pythonanywhere/helper_scripts/blob/master/scripts/pa_install_webapp_letsencrypt_ssl.py) -* [pa_install_webapp_ssl.py](https://github.com/pythonanywhere/helper_scripts/blob/master/scripts/pa_install_webapp_ssl.py) -* [pa_reload_webapp.py](https://github.com/pythonanywhere/helper_scripts/blob/master/scripts/pa_reload_webapp.py) -* [pa_start_django_webapp_with_virtualenv.py](https://github.com/pythonanywhere/helper_scripts/blob/master/scripts/pa_start_django_webapp_with_virtualenv.py) +If your username on PythonAnywhere is different from the username on your local machine, +you may need to set `USER` for the environment you run `pa` in. -and scheduled tasks: +### Programmatic usage in your code -* [pa_create_scheduled_task.py](https://github.com/pythonanywhere/helper_scripts/blob/master/scripts/pa_create_scheduled_task.py) -* [pa_delete_scheduled_task.py](https://github.com/pythonanywhere/helper_scripts/blob/master/scripts/pa_delete_scheduled_task.py) -* [pa_get_scheduled_tasks_list.py](https://github.com/pythonanywhere/helper_scripts/blob/master/scripts/pa_get_scheduled_tasks_list.py) -* [pa_get_scheduled_task_specs.py](https://github.com/pythonanywhere/helper_scripts/blob/master/scripts/pa_get_scheduled_task_specs.py) -* [pa_update_scheduled_task.py](https://github.com/pythonanywhere/helper_scripts/blob/master/scripts/pa_update_scheduled_task.py) +Take a look at the [`pythonanywhere.task`](https://github.com/pythonanywhere/helper_scripts/blob/master/pythonanywhere/task.py) +module and docstrings of `pythonanywhere.task.Task` class and its methods. -Run any of them with `--help` flag to get information about usage. +### Legacy scripts -See the [blog post](https://blog.pythonanywhere.com/155/) +Some legacy [scripts](https://github.com/pythonanywhere/helper_scripts/blob/master/legacy.md) (separate for each action) are still available. ## Contributing diff --git a/legacy.md b/legacy.md new file mode 100644 index 0000000..432f34e --- /dev/null +++ b/legacy.md @@ -0,0 +1,27 @@ +# Legacy scripts + +We still provide separate scripts for specific actions that are now all integrated +into unified `pa` cli tool. We will keep them available for people who rely on them in +their workflow, but we plan to drop them when we release 1.0. + +There are scripts provided for dealing with web apps: + +* [pa_autoconfigure_django.py](https://github.com/pythonanywhere/helper_scripts/blob/master/scripts/pa_autoconfigure_django.py) +* [pa_create_webapp_with_virtualenv.py](https://github.com/pythonanywhere/helper_scripts/blob/master/scripts/pa_create_webapp_with_virtualenv.py) +* [pa_delete_webapp_logs.py](https://github.com/pythonanywhere/helper_scripts/blob/master/scripts/pa_delete_webapp_logs.py) +* [pa_install_webapp_letsencrypt_ssl.py](https://github.com/pythonanywhere/helper_scripts/blob/master/scripts/pa_install_webapp_letsencrypt_ssl.py) +* [pa_install_webapp_ssl.py](https://github.com/pythonanywhere/helper_scripts/blob/master/scripts/pa_install_webapp_ssl.py) +* [pa_reload_webapp.py](https://github.com/pythonanywhere/helper_scripts/blob/master/scripts/pa_reload_webapp.py) +* [pa_start_django_webapp_with_virtualenv.py](https://github.com/pythonanywhere/helper_scripts/blob/master/scripts/pa_start_django_webapp_with_virtualenv.py) + +and scheduled tasks: + +* [pa_create_scheduled_task.py](https://github.com/pythonanywhere/helper_scripts/blob/master/scripts/pa_create_scheduled_task.py) +* [pa_delete_scheduled_task.py](https://github.com/pythonanywhere/helper_scripts/blob/master/scripts/pa_delete_scheduled_task.py) +* [pa_get_scheduled_tasks_list.py](https://github.com/pythonanywhere/helper_scripts/blob/master/scripts/pa_get_scheduled_tasks_list.py) +* [pa_get_scheduled_task_specs.py](https://github.com/pythonanywhere/helper_scripts/blob/master/scripts/pa_get_scheduled_task_specs.py) +* [pa_update_scheduled_task.py](https://github.com/pythonanywhere/helper_scripts/blob/master/scripts/pa_update_scheduled_task.py) + +Run any of them with `--help` flag to get information about usage. + +See the [blog post](https://blog.pythonanywhere.com/155/) about how it all started. diff --git a/pythonanywhere/api/base.py b/pythonanywhere/api/base.py index b14d546..dd0505c 100644 --- a/pythonanywhere/api/base.py +++ b/pythonanywhere/api/base.py @@ -3,7 +3,11 @@ import requests PYTHON_VERSIONS = { - "3.6": "python36", "3.7": "python37", "3.8": "python38", "3.9": "python39", + "3.6": "python36", + "3.7": "python37", + "3.8": "python38", + "3.9": "python39", + "3.10": "python310", } @@ -49,5 +53,3 @@ def call_api(url, method, **kwargs): f"Authentication error {response.status_code} calling API: {response.text}" ) return response - - diff --git a/requirements.txt b/requirements.txt index d62baf6..cea5aec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,14 +1,16 @@ python-dateutil==2.8.1 +click==8.0.3 docopt==0.6.2 packaging psutil==5.7.0 -pytest==5.4.2 -pytest-cov==2.8.1 -pytest-mock==3.1.0 +pytest==6.2.5 +pytest-cov==3.0.0 +pytest-mock==3.6.1 pytest-mypy==0.6.2 -requests==2.23.0 -responses==0.10.14 +requests==2.26.0 +responses==0.16.0 schema==0.7.2 -tabulate==0.8.7 -typer==0.3.2 +tabulate==0.8.9 +typer==0.4.0 +urllib3==1.26.7 virtualenvwrapper==4.8.4 diff --git a/setup.py b/setup.py index 37614e4..11b974a 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( name="pythonanywhere", - version="0.9.8", + version="0.9.11", description="PythonAnywhere helper tools for users", long_description=long_description, long_description_content_type="text/markdown", @@ -22,6 +22,8 @@ "Intended Audience :: Developers", "Topic :: Software Development :: Libraries", "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.6", diff --git a/tests/test_cli_schedule.py b/tests/test_cli_schedule.py index bccf031..eabc484 100644 --- a/tests/test_cli_schedule.py +++ b/tests/test_cli_schedule.py @@ -70,12 +70,12 @@ def test_validates_minutes(self): result = runner.invoke(app, ["set", "-c", "echo foo", "-h", "8", "-m", "66"]) assert "Invalid value" in result.stdout - assert "66 is not in the valid range of 0 to 59" in result.stdout + assert "66 is not in the range 0<=x<=59" in result.stdout def test_validates_hours(self): result = runner.invoke(app, ["set", "-c", "echo foo", "-h", "66", "-m", "1"]) assert "Invalid value" in result.stdout - assert "66 is not in the valid range of 0 to 23" in result.stdout + assert "66 is not in the range 0<=x<=23" in result.stdout def test_logs_warning_when_create_schedule_raises(self, mocker): mock_logger = mocker.patch("cli.schedule.get_logger").return_value @@ -347,11 +347,11 @@ def test_ensures_proper_hourly_params(self, mocker): def test_validates_minute(self): result = runner.invoke(app, ["update", "42", "--minute", "88"]) - assert "88 is not in the valid range of 0 to 59" in result.stdout + assert "88 is not in the range 0<=x<=59" in result.stdout def test_validates_hour(self): result = runner.invoke(app, ["update", "42", "--daily", "--hour", "33"]) - assert "33 is not in the valid range of 0 to 23" in result.stdout + assert "33 is not in the range 0<=x<=23" in result.stdout def test_complains_when_no_id_provided(self): result = runner.invoke(app, ["update"]) diff --git a/tests/test_django_project.py b/tests/test_django_project.py index d3102a9..258504b 100644 --- a/tests/test_django_project.py +++ b/tests/test_django_project.py @@ -476,7 +476,7 @@ def test_actually_produces_wsgi_file_that_can_import_project_non_nested( running_python_version = ".".join(python_version().split(".")[:2]) project = DjangoProject("mydomain.com", running_python_version) shutil.copytree(str(non_nested_submodule), str(project.project_path)) - if running_python_version in ["3.7", "3.8"]: + if running_python_version in ["3.7", "3.8", "3.9", "3.10"]: project.create_virtualenv(django_version="latest") else: project.create_virtualenv() @@ -496,7 +496,7 @@ def test_actually_produces_wsgi_file_that_can_import_nested_project( running_python_version = ".".join(python_version().split(".")[:2]) project = DjangoProject("mydomain.com", running_python_version) shutil.copytree(str(more_nested_submodule), str(project.project_path)) - if running_python_version in ["3.7", "3.8"]: + if running_python_version in ["3.7", "3.8", "3.9", "3.10"]: project.create_virtualenv(django_version="latest") else: project.create_virtualenv()