diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index c370489..7f06b78 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -1,35 +1,35 @@ name: Test on: push: pull_request: jobs: test: runs-on: ubuntu-latest strategy: matrix: python-version: - - '3.11' + - '3.13' steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - run: pip install --upgrade pip - run: pip install -r requirements.txt -r dev-requirements.txt - run: make check prod-requirements: runs-on: ubuntu-latest strategy: matrix: python-version: - '3.13' steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - run: pip install --upgrade pip # only install prod requirements - run: pip install -r requirements.txt # check that app.py runs without crashing on a missing import - run: python app.py diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..7252c4a --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: gunicorn --workers=4 app:app diff --git a/README.md b/README.md index 818322c..0c62bd0 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,102 @@ # SpeedPatrolling [This tool](https://speedpatrolling.toolforge.org/) helps Wikidata editors to patrol recent changes. For more information, please see the tool’s [on-wiki documentation page](https://www.wikidata.org/wiki/User:Lucas_Werkmeister/SpeedPatrolling). ## Toolforge setup -On Wikimedia Toolforge, this tool runs under the `speedpatrolling` tool name. -Source code resides in `~/www/python/src/`, -a virtual environment is set up in `~/www/python/venv/`, -logs end up in `~/uwsgi.log`. -The `uwsgi.ini` configuration file in the source code repository -is symlinked into `~/www/python/uwsgi.ini`. - -If the web service is not running for some reason, run the following command: +On Wikimedia Toolforge, this tool runs under the `speedpatrolling` tool name, +from a container built using the [Toolforge Build Service](https://wikitech.wikimedia.org/wiki/Help:Toolforge/Building_container_images). + +### Image build + +To build a new version of the image, +run the following command on Toolforge after becoming the tool account: + +```sh +toolforge build start --use-latest-versions https://gitlab.wikimedia.org/toolforge-repos/wd-image-positions +``` + +The image will contain all the dependencies listed in `requirements.txt`, +as well as the commands specified in the `Procfile`. + +### Webservice + +The web frontend of the tool runs as a webservice using the `buildpack` type. +The web service runs the first command in the `Procfile` (`web`), +which runs the Flask WSGI app using gunicorn. + ``` webservice start ``` + +Or, if the `~/service.template` file went missing: + +``` +webservice --mount=none buildservice start +``` + If it’s acting up, try the same command with `restart` instead of `start`. -Both should pull their config from the `service.template` file, -which is symlinked from the source code directory into the tool home directory. -To update the service, run the following commands after becoming the tool account: +### Configuration + +The tool reads configuration from both the `config.yaml` file (if it exists) +and from any environment variables starting with `TOOL_*`. +The config file is more convenient for local development; +the environment variables are used on Toolforge: +list them with `toolforge envvars list`. +Nested dicts are specified with envvar names where `__` separates the key components, +and the tool lowercases keys in nested dicts, +so that e.g. the following are equivalent: + +```sh +toolforge envvars create TOOL_OAUTH__CONSUMER_KEY 271b735e0cf895694f2ee7a3ae7a2dbc +``` + +```yaml +OAUTH: + CONSUMER_KEY: 271b735e0cf895694f2ee7a3ae7a2dbc ``` -webservice shell -source ~/www/python/venv/bin/activate -cd ~/www/python/src -git fetch -git diff @ @{u} # inspect changes -git merge --ff-only @{u} -pip-sync + +For the available configuration variables, see the `config.yaml.example` file. + +### Update + +To update the tool, build a new version of the image as described above, +then restart the webservice: + +```sh +toolforge build start --use-latest-versions https://gitlab.wikimedia.org/toolforge-repos/wd-image-positions webservice restart ``` ## Local development setup You can also run the tool locally, which is much more convenient for development (for example, Flask will automatically reload the application any time you save a file). ``` git clone https://gitlab.wikimedia.org/toolforge-repos/speedpatrolling.git cd tool-speedpatrolling pip3 install -r requirements.txt -r dev-requirements.txt flask --debug run ``` If you want, you can do this inside some virtualenv too. Note that your possibilities to work on this tool are rather limited unless you request your own OAuth consumer and configure it in a `config.yaml` file – without OAuth credentials, the tool cannot even load a list of unpatrolled changes. ## Contributing To send a patch, you can submit a [pull request on GitHub](https://github.com/lucaswerkmeister/tool-speedpatrolling) or a [merge request on GitLab](https://gitlab.wikimedia.org/toolforge-repos/speedpatrolling). (E-mail / patch-based workflows are also acceptable.) ## License The code in this repository is released under the AGPL v3, as provided in the `LICENSE` file. diff --git a/dev-requirements.txt b/dev-requirements.txt index c2a158f..f7c649c 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,50 +1,52 @@ # # This file is autogenerated by pip-compile with Python 3.13 # by the following command: # # pip-compile dev-requirements.in # flake8==7.3.0 # via -r dev-requirements.in iniconfig==2.1.0 # via pytest mccabe==0.7.0 # via flake8 mypy==1.17.0 # via -r dev-requirements.in mypy-extensions==1.1.0 # via mypy packaging==25.0 - # via pytest + # via + # -c /home/lucas/git/speedpatrolling/requirements.txt + # pytest pathspec==0.12.1 # via mypy pluggy==1.6.0 # via pytest pycodestyle==2.14.0 # via flake8 pyflakes==3.4.0 # via flake8 pygments==2.19.2 # via pytest pytest==8.4.1 # via -r dev-requirements.in types-beautifulsoup4==4.12.0.20250516 # via -r dev-requirements.in types-cachetools==6.0.0.20250525 # via -r dev-requirements.in types-decorator==5.2.0.20250324 # via -r dev-requirements.in types-html5lib==1.1.11.20250708 # via types-beautifulsoup4 types-pyyaml==6.0.12.20250516 # via -r dev-requirements.in types-requests==2.32.4.20250611 # via -r dev-requirements.in typing-extensions==4.14.1 # via # -c /home/lucas/git/speedpatrolling/requirements.txt # mypy urllib3==2.5.0 # via # -c /home/lucas/git/speedpatrolling/requirements.txt # types-requests diff --git a/requirements.in b/requirements.in index 1bd24f1..203e495 100644 --- a/requirements.in +++ b/requirements.in @@ -1,12 +1,14 @@ beautifulsoup4 cachetools decorator flask >= 2.0.0 +gunicorn intervaltree MarkupSafe mwapi mwoauth +pymysql @ git+https://github.com/PyMySQL/PyMySQL@main pyyaml requests requests_oauthlib toolforge >= 6.1 diff --git a/requirements.txt b/requirements.txt index b8c9318..2b0647a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,99 +1,105 @@ # # This file is autogenerated by pip-compile with Python 3.13 # by the following command: # # pip-compile # aiohappyeyeballs==2.6.1 # via aiohttp aiohttp==3.12.14 # via mwapi aiosignal==1.4.0 # via aiohttp attrs==25.3.0 # via aiohttp beautifulsoup4==4.13.4 # via -r requirements.in blinker==1.9.0 # via flask cachetools==6.1.0 # via -r requirements.in certifi==2025.7.14 # via requests charset-normalizer==3.4.2 # via requests click==8.2.1 # via flask decorator==5.2.1 # via # -r requirements.in # toolforge flask==3.1.1 # via -r requirements.in frozenlist==1.7.0 # via # aiohttp # aiosignal +gunicorn==23.0.0 + # via -r requirements.in idna==3.10 # via # requests # yarl intervaltree==3.1.0 # via -r requirements.in itsdangerous==2.2.0 # via flask jinja2==3.1.6 # via flask markupsafe==3.0.2 # via # -r requirements.in # flask # jinja2 # werkzeug multidict==6.6.3 # via # aiohttp # yarl mwapi==0.6.1 # via -r requirements.in mwoauth==0.4.0 # via -r requirements.in oauthlib==3.3.1 # via # mwoauth # requests-oauthlib +packaging==25.0 + # via gunicorn propcache==0.3.2 # via # aiohttp # yarl pyjwt==2.10.1 # via mwoauth -pymysql==1.1.1 - # via toolforge +pymysql @ git+https://github.com/PyMySQL/PyMySQL@main + # via + # -r requirements.in + # toolforge pyyaml==6.0.2 # via -r requirements.in requests==2.32.4 # via # -r requirements.in # mwapi # mwoauth # requests-oauthlib # toolforge requests-oauthlib==2.0.0 # via # -r requirements.in # mwoauth sortedcontainers==2.4.0 # via intervaltree soupsieve==2.7 # via beautifulsoup4 toolforge==6.1.0 # via -r requirements.in typing-extensions==4.14.1 # via beautifulsoup4 urllib3==2.5.0 # via requests werkzeug==3.1.3 # via flask yarl==1.20.1 # via aiohttp diff --git a/service.template b/service.template index 15c0ad7..c113691 100644 --- a/service.template +++ b/service.template @@ -1,4 +1,5 @@ # Toolforge webservice template # Provide default arguments for `webservice` commands for this tool. -type: python3.11 +type: buildservice health-check-path: /healthz +mount: none diff --git a/uwsgi.ini b/uwsgi.ini deleted file mode 100644 index dbdb050..0000000 --- a/uwsgi.ini +++ /dev/null @@ -1,5 +0,0 @@ -[uwsgi] -# default buffer size for all request headers is just 4096 bytes, -# while the browser limit for all cookies is some 4093 bytes – -# to fully utilize the cookie limit, we need to raise the request limit a bit -buffer-size = 8192