dron 0.1.20241008__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dron-0.1.20241008/.ci/release +66 -0
- dron-0.1.20241008/.ci/run +41 -0
- dron-0.1.20241008/.github/workflows/main.yml +90 -0
- dron-0.1.20241008/.gitignore +160 -0
- dron-0.1.20241008/LICENSE.txt +21 -0
- dron-0.1.20241008/PKG-INFO +46 -0
- dron-0.1.20241008/README.org +135 -0
- dron-0.1.20241008/conftest.py +47 -0
- dron-0.1.20241008/mypy.ini +14 -0
- dron-0.1.20241008/pyproject.toml +56 -0
- dron-0.1.20241008/pytest.ini +13 -0
- dron-0.1.20241008/ruff.toml +141 -0
- dron-0.1.20241008/setup.cfg +4 -0
- dron-0.1.20241008/src/dron/__init__.py +6 -0
- dron-0.1.20241008/src/dron/__main__.py +5 -0
- dron-0.1.20241008/src/dron/api.py +106 -0
- dron-0.1.20241008/src/dron/cli.py +382 -0
- dron-0.1.20241008/src/dron/common.py +174 -0
- dron-0.1.20241008/src/dron/conftest.py +18 -0
- dron-0.1.20241008/src/dron/dron.py +510 -0
- dron-0.1.20241008/src/dron/launchd.py +415 -0
- dron-0.1.20241008/src/dron/launchd_wrapper.py +90 -0
- dron-0.1.20241008/src/dron/monitor.py +157 -0
- dron-0.1.20241008/src/dron/notify/common.py +53 -0
- dron-0.1.20241008/src/dron/notify/email.py +43 -0
- dron-0.1.20241008/src/dron/notify/ntfy_common.py +24 -0
- dron-0.1.20241008/src/dron/notify/ntfy_desktop.py +15 -0
- dron-0.1.20241008/src/dron/notify/ntfy_telegram.py +12 -0
- dron-0.1.20241008/src/dron/notify/telegram.py +42 -0
- dron-0.1.20241008/src/dron/py.typed +0 -0
- dron-0.1.20241008/src/dron/systemd.py +542 -0
- dron-0.1.20241008/src/dron/tests/test_dron.py +119 -0
- dron-0.1.20241008/src/dron.egg-info/PKG-INFO +46 -0
- dron-0.1.20241008/src/dron.egg-info/SOURCES.txt +37 -0
- dron-0.1.20241008/src/dron.egg-info/dependency_links.txt +1 -0
- dron-0.1.20241008/src/dron.egg-info/entry_points.txt +2 -0
- dron-0.1.20241008/src/dron.egg-info/requires.txt +20 -0
- dron-0.1.20241008/src/dron.egg-info/top_level.txt +1 -0
- dron-0.1.20241008/tox.ini +54 -0
@@ -0,0 +1,66 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
'''
|
3
|
+
Run [[file:scripts/release][scripts/release]] to deploy Python package onto [[https://pypi.org][PyPi]] and [[https://test.pypi.org][test PyPi]].
|
4
|
+
|
5
|
+
The script expects =TWINE_PASSWORD= environment variable to contain the [[https://pypi.org/help/#apitoken][PyPi token]] (not the password!).
|
6
|
+
|
7
|
+
The script can be run manually.
|
8
|
+
It's also running as =pypi= job in [[file:.github/workflows/main.yml][Github Actions config]]. Packages are deployed on:
|
9
|
+
- every master commit, onto test pypi
|
10
|
+
- every new tag, onto production pypi
|
11
|
+
|
12
|
+
You'll need to set =TWINE_PASSWORD= and =TWINE_PASSWORD_TEST= in [[https://help.github.com/en/actions/configuring-and-managing-workflows/creating-and-storing-encrypted-secrets#creating-encrypted-secrets][secrets]]
|
13
|
+
for Github Actions deployment to work.
|
14
|
+
'''
|
15
|
+
|
16
|
+
import os
|
17
|
+
import sys
|
18
|
+
from pathlib import Path
|
19
|
+
from subprocess import check_call
|
20
|
+
import shutil
|
21
|
+
|
22
|
+
is_ci = os.environ.get('CI') is not None
|
23
|
+
|
24
|
+
def main() -> None:
|
25
|
+
import argparse
|
26
|
+
p = argparse.ArgumentParser()
|
27
|
+
p.add_argument('--test', action='store_true', help='use test pypi')
|
28
|
+
args = p.parse_args()
|
29
|
+
|
30
|
+
extra = []
|
31
|
+
if args.test:
|
32
|
+
extra.extend(['--repository', 'testpypi'])
|
33
|
+
|
34
|
+
root = Path(__file__).absolute().parent.parent
|
35
|
+
os.chdir(root) # just in case
|
36
|
+
|
37
|
+
if is_ci:
|
38
|
+
# see https://github.com/actions/checkout/issues/217
|
39
|
+
check_call('git fetch --prune --unshallow'.split())
|
40
|
+
|
41
|
+
dist = root / 'dist'
|
42
|
+
if dist.exists():
|
43
|
+
shutil.rmtree(dist)
|
44
|
+
|
45
|
+
check_call(['python3', '-m', 'build'])
|
46
|
+
|
47
|
+
TP = 'TWINE_PASSWORD'
|
48
|
+
password = os.environ.get(TP)
|
49
|
+
if password is None:
|
50
|
+
print(f"WARNING: no {TP} passed", file=sys.stderr)
|
51
|
+
import pip_secrets
|
52
|
+
password = pip_secrets.token_test if args.test else pip_secrets.token # meh
|
53
|
+
|
54
|
+
check_call([
|
55
|
+
'python3', '-m', 'twine',
|
56
|
+
'upload', *dist.iterdir(),
|
57
|
+
*extra,
|
58
|
+
], env={
|
59
|
+
'TWINE_USERNAME': '__token__',
|
60
|
+
TP: password,
|
61
|
+
**os.environ,
|
62
|
+
})
|
63
|
+
|
64
|
+
|
65
|
+
if __name__ == '__main__':
|
66
|
+
main()
|
@@ -0,0 +1,41 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
set -eu
|
3
|
+
|
4
|
+
cd "$(dirname "$0")"
|
5
|
+
cd .. # git root
|
6
|
+
|
7
|
+
if ! command -v sudo; then
|
8
|
+
# CI or Docker sometimes doesn't have it, so useful to have a dummy
|
9
|
+
function sudo {
|
10
|
+
"$@"
|
11
|
+
}
|
12
|
+
fi
|
13
|
+
|
14
|
+
if [ -n "${CI-}" ]; then
|
15
|
+
# install OS specific stuff here
|
16
|
+
case "$OSTYPE" in
|
17
|
+
darwin*)
|
18
|
+
# macos
|
19
|
+
:
|
20
|
+
;;
|
21
|
+
cygwin* | msys* | win*)
|
22
|
+
# windows
|
23
|
+
:
|
24
|
+
;;
|
25
|
+
*)
|
26
|
+
# must be linux?
|
27
|
+
# necessary for dbus-python
|
28
|
+
sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_SUSPEND=1 apt-get install --yes libdbus-1-dev libglib2.0-dev
|
29
|
+
;;
|
30
|
+
esac
|
31
|
+
fi
|
32
|
+
|
33
|
+
|
34
|
+
PY_BIN="python3"
|
35
|
+
# some systems might have python pointing to python3
|
36
|
+
if ! command -v python3 &> /dev/null; then
|
37
|
+
PY_BIN="python"
|
38
|
+
fi
|
39
|
+
|
40
|
+
"$PY_BIN" -m pip install --user tox
|
41
|
+
"$PY_BIN" -m tox --parallel --parallel-live "$@"
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# see https://github.com/karlicoss/pymplate for up-to-date reference
|
2
|
+
|
3
|
+
name: CI
|
4
|
+
on:
|
5
|
+
push:
|
6
|
+
branches: '*'
|
7
|
+
tags: 'v[0-9]+.*' # only trigger on 'release' tags for PyPi
|
8
|
+
# Ideally I would put this in the pypi job... but github syntax doesn't allow for regexes there :shrug:
|
9
|
+
# P.S. fuck made up yaml DSLs.
|
10
|
+
pull_request: # needed to trigger on others' PRs
|
11
|
+
# Note that people who fork it need to go to "Actions" tab on their fork and click "I understand my workflows, go ahead and enable them".
|
12
|
+
workflow_dispatch: # needed to trigger workflows manually
|
13
|
+
# todo cron?
|
14
|
+
inputs:
|
15
|
+
debug_enabled:
|
16
|
+
type: boolean
|
17
|
+
description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)'
|
18
|
+
required: false
|
19
|
+
default: false
|
20
|
+
|
21
|
+
|
22
|
+
jobs:
|
23
|
+
build:
|
24
|
+
strategy:
|
25
|
+
fail-fast: false
|
26
|
+
matrix:
|
27
|
+
platform: [ubuntu-latest, macos-latest]
|
28
|
+
python-version: ['3.9', '3.10', '3.11', '3.12']
|
29
|
+
# vvv just an example of excluding stuff from matrix
|
30
|
+
# exclude: [{platform: macos-latest, python-version: '3.6'}]
|
31
|
+
|
32
|
+
runs-on: ${{ matrix.platform }}
|
33
|
+
|
34
|
+
steps:
|
35
|
+
# ugh https://github.com/actions/toolkit/blob/main/docs/commands.md#path-manipulation
|
36
|
+
- run: echo "$HOME/.local/bin" >> $GITHUB_PATH
|
37
|
+
|
38
|
+
- uses: actions/setup-python@v5
|
39
|
+
with:
|
40
|
+
python-version: ${{ matrix.python-version }}
|
41
|
+
|
42
|
+
- uses: actions/checkout@v4
|
43
|
+
with:
|
44
|
+
submodules: recursive
|
45
|
+
fetch-depth: 0 # nicer to have all git history when debugging/for tests
|
46
|
+
|
47
|
+
- uses: mxschmitt/action-tmate@v3
|
48
|
+
if: ${{ github.event_name == 'workflow_dispatch' && inputs.debug_enabled }}
|
49
|
+
|
50
|
+
# explicit bash command is necessary for Windows CI runner, otherwise it thinks it's cmd...
|
51
|
+
- run: bash .ci/run
|
52
|
+
|
53
|
+
- if: matrix.platform == 'ubuntu-latest' # no need to compute coverage for other platforms
|
54
|
+
uses: actions/upload-artifact@v4
|
55
|
+
with:
|
56
|
+
include-hidden-files: true
|
57
|
+
name: .coverage.mypy_${{ matrix.platform }}_${{ matrix.python-version }}
|
58
|
+
path: .coverage.mypy/
|
59
|
+
|
60
|
+
|
61
|
+
pypi:
|
62
|
+
runs-on: ubuntu-latest
|
63
|
+
needs: [build] # add all other jobs here
|
64
|
+
|
65
|
+
steps:
|
66
|
+
# ugh https://github.com/actions/toolkit/blob/main/docs/commands.md#path-manipulation
|
67
|
+
- run: echo "$HOME/.local/bin" >> $GITHUB_PATH
|
68
|
+
|
69
|
+
- uses: actions/setup-python@v5
|
70
|
+
with:
|
71
|
+
python-version: '3.8'
|
72
|
+
|
73
|
+
- uses: actions/checkout@v4
|
74
|
+
with:
|
75
|
+
submodules: recursive
|
76
|
+
|
77
|
+
- name: 'release to test pypi'
|
78
|
+
# always deploy merged master to test pypi
|
79
|
+
if: github.event_name != 'pull_request' && github.event.ref == 'refs/heads/master'
|
80
|
+
env:
|
81
|
+
TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD_TEST }}
|
82
|
+
run: pip3 install --user --upgrade build twine && .ci/release --test
|
83
|
+
|
84
|
+
- name: 'release to pypi'
|
85
|
+
# always deploy tags to release pypi
|
86
|
+
# NOTE: release tags are guarded by on: push: tags on the top
|
87
|
+
if: github.event_name != 'pull_request' && startsWith(github.event.ref, 'refs/tags')
|
88
|
+
env:
|
89
|
+
TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }}
|
90
|
+
run: pip3 install --user --upgrade build twine && .ci/release
|
@@ -0,0 +1,160 @@
|
|
1
|
+
|
2
|
+
# Created by https://www.gitignore.io/api/python,emacs
|
3
|
+
# Edit at https://www.gitignore.io/?templates=python,emacs
|
4
|
+
|
5
|
+
### Emacs ###
|
6
|
+
# -*- mode: gitignore; -*-
|
7
|
+
*~
|
8
|
+
\#*\#
|
9
|
+
/.emacs.desktop
|
10
|
+
/.emacs.desktop.lock
|
11
|
+
*.elc
|
12
|
+
auto-save-list
|
13
|
+
tramp
|
14
|
+
.\#*
|
15
|
+
|
16
|
+
# Org-mode
|
17
|
+
.org-id-locations
|
18
|
+
*_archive
|
19
|
+
|
20
|
+
# flymake-mode
|
21
|
+
*_flymake.*
|
22
|
+
|
23
|
+
# eshell files
|
24
|
+
/eshell/history
|
25
|
+
/eshell/lastdir
|
26
|
+
|
27
|
+
# elpa packages
|
28
|
+
/elpa/
|
29
|
+
|
30
|
+
# reftex files
|
31
|
+
*.rel
|
32
|
+
|
33
|
+
# AUCTeX auto folder
|
34
|
+
/auto/
|
35
|
+
|
36
|
+
# cask packages
|
37
|
+
.cask/
|
38
|
+
dist/
|
39
|
+
|
40
|
+
# Flycheck
|
41
|
+
flycheck_*.el
|
42
|
+
|
43
|
+
# server auth directory
|
44
|
+
/server/
|
45
|
+
|
46
|
+
# projectiles files
|
47
|
+
.projectile
|
48
|
+
|
49
|
+
# directory configuration
|
50
|
+
.dir-locals.el
|
51
|
+
|
52
|
+
# network security
|
53
|
+
/network-security.data
|
54
|
+
|
55
|
+
|
56
|
+
### Python ###
|
57
|
+
# Byte-compiled / optimized / DLL files
|
58
|
+
__pycache__/
|
59
|
+
*.py[cod]
|
60
|
+
*$py.class
|
61
|
+
|
62
|
+
# C extensions
|
63
|
+
*.so
|
64
|
+
|
65
|
+
# Distribution / packaging
|
66
|
+
.Python
|
67
|
+
build/
|
68
|
+
develop-eggs/
|
69
|
+
downloads/
|
70
|
+
eggs/
|
71
|
+
.eggs/
|
72
|
+
lib/
|
73
|
+
lib64/
|
74
|
+
parts/
|
75
|
+
sdist/
|
76
|
+
var/
|
77
|
+
wheels/
|
78
|
+
pip-wheel-metadata/
|
79
|
+
share/python-wheels/
|
80
|
+
*.egg-info/
|
81
|
+
.installed.cfg
|
82
|
+
*.egg
|
83
|
+
MANIFEST
|
84
|
+
|
85
|
+
# PyInstaller
|
86
|
+
# Usually these files are written by a python script from a template
|
87
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
88
|
+
*.manifest
|
89
|
+
*.spec
|
90
|
+
|
91
|
+
# Installer logs
|
92
|
+
pip-log.txt
|
93
|
+
pip-delete-this-directory.txt
|
94
|
+
|
95
|
+
# Unit test / coverage reports
|
96
|
+
htmlcov/
|
97
|
+
.tox/
|
98
|
+
.nox/
|
99
|
+
.coverage
|
100
|
+
.coverage.*
|
101
|
+
.cache
|
102
|
+
nosetests.xml
|
103
|
+
coverage.xml
|
104
|
+
*.cover
|
105
|
+
.hypothesis/
|
106
|
+
.pytest_cache/
|
107
|
+
|
108
|
+
# Translations
|
109
|
+
*.mo
|
110
|
+
*.pot
|
111
|
+
|
112
|
+
# Scrapy stuff:
|
113
|
+
.scrapy
|
114
|
+
|
115
|
+
# Sphinx documentation
|
116
|
+
docs/_build/
|
117
|
+
|
118
|
+
# PyBuilder
|
119
|
+
target/
|
120
|
+
|
121
|
+
# pyenv
|
122
|
+
.python-version
|
123
|
+
|
124
|
+
# pipenv
|
125
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
126
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
127
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
128
|
+
# install all needed dependencies.
|
129
|
+
#Pipfile.lock
|
130
|
+
|
131
|
+
# celery beat schedule file
|
132
|
+
celerybeat-schedule
|
133
|
+
|
134
|
+
# SageMath parsed files
|
135
|
+
*.sage.py
|
136
|
+
|
137
|
+
# Spyder project settings
|
138
|
+
.spyderproject
|
139
|
+
.spyproject
|
140
|
+
|
141
|
+
# Rope project settings
|
142
|
+
.ropeproject
|
143
|
+
|
144
|
+
# Mr Developer
|
145
|
+
.mr.developer.cfg
|
146
|
+
.project
|
147
|
+
.pydevproject
|
148
|
+
|
149
|
+
# mkdocs documentation
|
150
|
+
/site
|
151
|
+
|
152
|
+
# mypy
|
153
|
+
.mypy_cache/
|
154
|
+
.dmypy.json
|
155
|
+
dmypy.json
|
156
|
+
|
157
|
+
# Pyre type checker
|
158
|
+
.pyre/
|
159
|
+
|
160
|
+
# End of https://www.gitignore.io/api/python,emacs
|
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2024 Dima Gerasimov
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
@@ -0,0 +1,46 @@
|
|
1
|
+
Metadata-Version: 2.1
|
2
|
+
Name: dron
|
3
|
+
Version: 0.1.20241008
|
4
|
+
Author-email: "Dima Gerasimov (@karlicoss)" <karlicoss@gmail.com>
|
5
|
+
Maintainer-email: "Dima Gerasimov (@karlicoss)" <karlicoss@gmail.com>
|
6
|
+
License: The MIT License (MIT)
|
7
|
+
|
8
|
+
Copyright (c) 2024 Dima Gerasimov
|
9
|
+
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
12
|
+
in the Software without restriction, including without limitation the rights
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
15
|
+
furnished to do so, subject to the following conditions:
|
16
|
+
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
18
|
+
copies or substantial portions of the Software.
|
19
|
+
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
26
|
+
SOFTWARE.
|
27
|
+
|
28
|
+
Project-URL: Homepage, https://github.com/karlicoss/dron
|
29
|
+
Requires-Python: >=3.9
|
30
|
+
License-File: LICENSE.txt
|
31
|
+
Requires-Dist: click
|
32
|
+
Requires-Dist: prompt_toolkit
|
33
|
+
Requires-Dist: tzlocal
|
34
|
+
Requires-Dist: textual
|
35
|
+
Requires-Dist: tabulate
|
36
|
+
Requires-Dist: termcolor
|
37
|
+
Requires-Dist: mypy
|
38
|
+
Requires-Dist: loguru
|
39
|
+
Requires-Dist: dbus-python; platform_system != "Darwin"
|
40
|
+
Provides-Extra: testing
|
41
|
+
Requires-Dist: pytest; extra == "testing"
|
42
|
+
Requires-Dist: ruff; extra == "testing"
|
43
|
+
Requires-Dist: mypy; extra == "testing"
|
44
|
+
Requires-Dist: lxml; extra == "testing"
|
45
|
+
Provides-Extra: notify-telegram
|
46
|
+
Requires-Dist: telegram-send>=0.37; extra == "notify-telegram"
|
@@ -0,0 +1,135 @@
|
|
1
|
+
#+begin_src python :results drawer :exports results
|
2
|
+
import dron; return dron.make_parser().description
|
3
|
+
#+end_src
|
4
|
+
|
5
|
+
#+RESULTS:
|
6
|
+
:results:
|
7
|
+
dron -- simple frontend for Systemd, inspired by cron.
|
8
|
+
|
9
|
+
- *d* stands for 'Systemd'
|
10
|
+
- *ron* stands for 'cron'
|
11
|
+
|
12
|
+
dron is my attempt to overcome things that make working with Systemd tedious
|
13
|
+
:end:
|
14
|
+
|
15
|
+
|
16
|
+
#+begin_src python :results drawer :exports results
|
17
|
+
import dron; return dron.make_parser().epilog
|
18
|
+
#+end_src
|
19
|
+
|
20
|
+
#+RESULTS:
|
21
|
+
:results:
|
22
|
+
|
23
|
+
* What does it do?
|
24
|
+
In short, you type ~dron edit~ and edit your config file, similarly to ~crontab -e~:
|
25
|
+
|
26
|
+
: from dron.api import job
|
27
|
+
:
|
28
|
+
: # at the moment you're expected to define jobs() function that yields jobs
|
29
|
+
: # in the future I might add more mechanisms
|
30
|
+
: def jobs():
|
31
|
+
: # simple job that doesn't do much
|
32
|
+
: yield job(
|
33
|
+
: 'daily',
|
34
|
+
: '/home/user/scripts/run-borg /home/user',
|
35
|
+
: unit_name='borg-backup-home',
|
36
|
+
: )
|
37
|
+
:
|
38
|
+
: yield job(
|
39
|
+
: 'daily',
|
40
|
+
: 'linkchecker https://beepb00p.xyz',
|
41
|
+
: unit_name='linkchecker-beepb00p',
|
42
|
+
: )
|
43
|
+
:
|
44
|
+
: # drontab is simply python code!
|
45
|
+
: # so if you're annoyed by having to rememver Systemd syntax, you can use a helper function
|
46
|
+
: def every(*, mins: int) -> str:
|
47
|
+
: return f'*:0/{mins}'
|
48
|
+
:
|
49
|
+
: # make sure my website is alive, it will send local email on failure
|
50
|
+
: yield job(
|
51
|
+
: every(mins=10),
|
52
|
+
: 'ping https://beepb00p.xyz',
|
53
|
+
: unit_name='ping-beepb00p',
|
54
|
+
: )
|
55
|
+
|
56
|
+
|
57
|
+
After you save your changes and exit the editor, your drontab is checked for syntax and applied
|
58
|
+
|
59
|
+
- if checks have passed, your jobs are mapped onto Systemd units and started up
|
60
|
+
- if there are potential errors, you are prompted to fix them before retrying
|
61
|
+
|
62
|
+
* Why?
|
63
|
+
In short, because I want to benefit from the heavy lifting that Systemd does: timeouts, resource management, restart policies, powerful scheduling specs and logging,
|
64
|
+
while not having to manually manipulate numerous unit files and restart the daemon all over.
|
65
|
+
|
66
|
+
I elaborate on what led me to implement it and motivation [[https://beepb00p.xyz/scheduler.html#what_do_i_want][here]]. Also:
|
67
|
+
|
68
|
+
- why not just use [[https://beepb00p.xyz/scheduler.html#cron][cron]]?
|
69
|
+
- why not just use [[https://beepb00p.xyz/scheduler.html#systemd][systemd]]?
|
70
|
+
|
71
|
+
:end:
|
72
|
+
|
73
|
+
|
74
|
+
* Setting up
|
75
|
+
|
76
|
+
1. install system dependencies (see =.ci/run= ) -- these are necessary for =dbus-python= library
|
77
|
+
2. install dron: =pip3 install --user git+https://github.com/karlicoss/dron=
|
78
|
+
3. install =sendmail= from your package manager if you want to recieve job failure emails
|
79
|
+
|
80
|
+
* Using
|
81
|
+
|
82
|
+
#+begin_src python :results value :exports results
|
83
|
+
import dron;
|
84
|
+
p = dron.make_parser()
|
85
|
+
p.prog = ''
|
86
|
+
p.epilog = ''
|
87
|
+
return p.format_help()
|
88
|
+
#+end_src
|
89
|
+
|
90
|
+
#+RESULTS:
|
91
|
+
#+begin_example
|
92
|
+
usage: [-h] [--marker MARKER] {monitor,past,edit,apply,lint,uninstall} ...
|
93
|
+
|
94
|
+
dron -- simple frontend for Systemd, inspired by cron.
|
95
|
+
|
96
|
+
- *d* stands for 'Systemd'
|
97
|
+
- *ron* stands for 'cron'
|
98
|
+
|
99
|
+
dron is my attempt to overcome things that make working with Systemd tedious
|
100
|
+
|
101
|
+
positional arguments:
|
102
|
+
{monitor,past,edit,apply,lint,uninstall}
|
103
|
+
monitor Monitor services/timers managed by dron
|
104
|
+
past List past job runs
|
105
|
+
edit Edit drontab (like 'crontab -e')
|
106
|
+
apply Apply drontab (like 'crontab' with no args)
|
107
|
+
lint Check drontab (no 'crontab' alternative, sadly!)
|
108
|
+
uninstall Uninstall all managed jobs
|
109
|
+
|
110
|
+
options:
|
111
|
+
-h, --help show this help message and exit
|
112
|
+
--marker MARKER Use custom marker instead of default `(MANAGED BY DRON)`. Possibly useful for developing/testing.
|
113
|
+
#+end_example
|
114
|
+
|
115
|
+
|
116
|
+
* Job syntax
|
117
|
+
|
118
|
+
The idea is that it's a simple python DSL that lets you define simple jobs with minimal friction.
|
119
|
+
|
120
|
+
However, if you wish you can pass arbitrary unit properties as keyword arguments as well.
|
121
|
+
|
122
|
+
* Caveats
|
123
|
+
- older systemd versions would only accept absolute path for =ExecStart=. That should be caught during =dron edit= though
|
124
|
+
|
125
|
+
* Potential improvements
|
126
|
+
- custom validation; at the moment it runs pylint, mypy and systemd verify
|
127
|
+
- make it more atomic?
|
128
|
+
|
129
|
+
E.g. roll back all the changes until daemon-reload
|
130
|
+
- more failure report mechanisms?
|
131
|
+
|
132
|
+
Ideally, benefit from [[https://github.com/dschep/ntfy][ntfy]]
|
133
|
+
|
134
|
+
** TODO add issues with various questions that I had in code?
|
135
|
+
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# this is a hack to monkey patch pytest so it handles tests inside namespace packages without __init__.py properly
|
2
|
+
# without it, pytest can't discover the package root for some reason
|
3
|
+
# also see https://github.com/karlicoss/pytest_namespace_pkgs for more
|
4
|
+
|
5
|
+
import os
|
6
|
+
import pathlib
|
7
|
+
from typing import Optional
|
8
|
+
|
9
|
+
import _pytest.main
|
10
|
+
import _pytest.pathlib
|
11
|
+
|
12
|
+
# we consider all dirs in repo/ to be namespace packages
|
13
|
+
root_dir = pathlib.Path(__file__).absolute().parent.resolve() / 'src'
|
14
|
+
assert root_dir.exists(), root_dir
|
15
|
+
|
16
|
+
# TODO assert it contains package name?? maybe get it via setuptools..
|
17
|
+
|
18
|
+
namespace_pkg_dirs = [str(d) for d in root_dir.iterdir() if d.is_dir()]
|
19
|
+
|
20
|
+
# resolve_package_path is called from _pytest.pathlib.import_path
|
21
|
+
# takes a full abs path to the test file and needs to return the path to the 'root' package on the filesystem
|
22
|
+
resolve_pkg_path_orig = _pytest.pathlib.resolve_package_path
|
23
|
+
def resolve_package_path(path: pathlib.Path) -> Optional[pathlib.Path]:
|
24
|
+
result = path # search from the test file upwards
|
25
|
+
for parent in result.parents:
|
26
|
+
if str(parent) in namespace_pkg_dirs:
|
27
|
+
return parent
|
28
|
+
if os.name == 'nt':
|
29
|
+
# ??? for some reason on windows it is trying to call this against conftest? but not on linux/osx
|
30
|
+
if path.name == 'conftest.py':
|
31
|
+
return resolve_pkg_path_orig(path)
|
32
|
+
raise RuntimeError("Couldn't determine path for ", path)
|
33
|
+
_pytest.pathlib.resolve_package_path = resolve_package_path
|
34
|
+
|
35
|
+
|
36
|
+
# without patching, the orig function returns just a package name for some reason
|
37
|
+
# (I think it's used as a sort of fallback)
|
38
|
+
# so we need to point it at the absolute path properly
|
39
|
+
# not sure what are the consequences.. maybe it wouldn't be able to run against installed packages? not sure..
|
40
|
+
search_pypath_orig = _pytest.main.search_pypath
|
41
|
+
def search_pypath(module_name: str) -> str:
|
42
|
+
mpath = root_dir / module_name.replace('.', os.sep)
|
43
|
+
if not mpath.is_dir():
|
44
|
+
mpath = mpath.with_suffix('.py')
|
45
|
+
assert mpath.exists(), mpath # just in case
|
46
|
+
return str(mpath)
|
47
|
+
_pytest.main.search_pypath = search_pypath
|
@@ -0,0 +1,14 @@
|
|
1
|
+
[mypy]
|
2
|
+
pretty = True
|
3
|
+
show_error_context = True
|
4
|
+
show_column_numbers = True
|
5
|
+
show_error_end = True
|
6
|
+
warn_redundant_casts = True
|
7
|
+
warn_unused_ignores = True
|
8
|
+
check_untyped_defs = True
|
9
|
+
strict_equality = True
|
10
|
+
enable_error_code = possibly-undefined
|
11
|
+
|
12
|
+
# an example of suppressing
|
13
|
+
# [mypy-my.config.repos.pdfannots.pdfannots]
|
14
|
+
# ignore_errors = True
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# see https://github.com/karlicoss/pymplate for up-to-date reference
|
2
|
+
[project]
|
3
|
+
dynamic = ["version"] # version is managed by setuptools_scm
|
4
|
+
name = "dron"
|
5
|
+
dependencies = [
|
6
|
+
"click" , # CLI
|
7
|
+
"prompt_toolkit", # CLI
|
8
|
+
"tzlocal" , # for monitor, to determine host timezone
|
9
|
+
"textual" , # for 'new' monitor
|
10
|
+
"tabulate" , # for 'old' monitor
|
11
|
+
"termcolor" , # for 'old' monitor
|
12
|
+
"mypy" , # for checking units
|
13
|
+
"loguru" , # nicer logging
|
14
|
+
"dbus-python; platform_system != 'Darwin'", # dbus interface to systemd
|
15
|
+
]
|
16
|
+
requires-python = ">=3.9"
|
17
|
+
# FIXME dbus
|
18
|
+
|
19
|
+
## these need to be set if you're planning to upload to pypi
|
20
|
+
license = {file = "LICENSE.txt"}
|
21
|
+
authors = [
|
22
|
+
{name = "Dima Gerasimov (@karlicoss)", email = "karlicoss@gmail.com"},
|
23
|
+
]
|
24
|
+
maintainers = [
|
25
|
+
{name = "Dima Gerasimov (@karlicoss)", email = "karlicoss@gmail.com"},
|
26
|
+
]
|
27
|
+
|
28
|
+
[project.urls]
|
29
|
+
Homepage = "https://github.com/karlicoss/dron"
|
30
|
+
##
|
31
|
+
|
32
|
+
[project.scripts]
|
33
|
+
dron = "dron.__main__:main"
|
34
|
+
|
35
|
+
[project.optional-dependencies]
|
36
|
+
testing = [
|
37
|
+
"pytest",
|
38
|
+
"ruff",
|
39
|
+
"mypy",
|
40
|
+
"lxml", # for mypy html coverage
|
41
|
+
]
|
42
|
+
notify-telegram = [
|
43
|
+
# version before that had a bug that prevented it from working
|
44
|
+
# see https://github.com/rahiel/telegram-send/issues/115#issuecomment-1368728425
|
45
|
+
"telegram-send>=0.37",
|
46
|
+
]
|
47
|
+
|
48
|
+
|
49
|
+
[build-system]
|
50
|
+
requires = ["setuptools", "setuptools-scm"]
|
51
|
+
build-backend = "setuptools.build_meta"
|
52
|
+
|
53
|
+
[tool.setuptools_scm]
|
54
|
+
version_scheme = "python-simplified-semver"
|
55
|
+
local_scheme = "dirty-tag"
|
56
|
+
|
@@ -0,0 +1,13 @@
|
|
1
|
+
[pytest]
|
2
|
+
# discover files that don't follow test_ naming. Useful to keep tests along with the source code
|
3
|
+
python_files = *.py
|
4
|
+
addopts =
|
5
|
+
# -rap to print tests summary even when they are successful
|
6
|
+
-rap
|
7
|
+
--verbose
|
8
|
+
|
9
|
+
# otherwise it won't discover doctests
|
10
|
+
--doctest-modules
|
11
|
+
|
12
|
+
# show all test durations (unless they are too short)
|
13
|
+
--durations=0
|