cronipy 0.1.0__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.
- cronipy-0.1.0/PKG-INFO +69 -0
- cronipy-0.1.0/README.md +59 -0
- cronipy-0.1.0/cronipy/__init__.py +102 -0
- cronipy-0.1.0/cronipy/__main__.py +2 -0
- cronipy-0.1.0/cronipy/cli.py +18 -0
- cronipy-0.1.0/pyproject.toml +38 -0
- cronipy-0.1.0/pyproject.toml.orig +36 -0
cronipy-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: cronipy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Author: lantonic
|
|
6
|
+
Author-email: lantonic <luka.olegsson@gmail.com>
|
|
7
|
+
Requires-Dist: cyclopts>=5.3.2
|
|
8
|
+
Requires-Python: >=3.14
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# cronipy
|
|
12
|
+
|
|
13
|
+
`cronipy` turns Python functions into cron jobs. You decorate a function with a schedule, run one command, and cronipy writes the crontab entries for you. The package is in early development, and the interface can change.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install cronipy
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
Define your jobs in a module of your package, e.g. `mypackage.tasks`:
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from cronipy import Cron
|
|
27
|
+
|
|
28
|
+
cron = Cron()
|
|
29
|
+
|
|
30
|
+
@cron.task("*/5 * * * *")
|
|
31
|
+
def do_something_every_five_minutes():
|
|
32
|
+
...
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Then install the jobs into your crontab:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
cron.install()
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
or from a shell, install jobs from all `Cron` objects in the specified module:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
$ cronipy install mypackage.tasks
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`cronipy` adds one line per decorated function into the current user's `crontab`. Function output is logged in `logs/<function name>.log`.
|
|
48
|
+
|
|
49
|
+
## Options
|
|
50
|
+
|
|
51
|
+
You can pass these arguments to `Cron`:
|
|
52
|
+
|
|
53
|
+
- `dry_run`: when true, a job prints its name and does nothing.
|
|
54
|
+
- `env`: a dictionary of environment variables added to every crontab line.
|
|
55
|
+
- `log_dir`: where the log files go, `./logs` by default.
|
|
56
|
+
- `work_dir`: the directory each job runs in, `./` by default.
|
|
57
|
+
- `python`: the interpreter the jobs use, the one that runs cronipy by default.
|
|
58
|
+
|
|
59
|
+
## Development
|
|
60
|
+
|
|
61
|
+
This repository uses [uv](https://docs.astral.sh/uv/). Set up a development environment with `uv sync`, and run the checks with `uv run pytest`.
|
|
62
|
+
|
|
63
|
+
## Roadmap
|
|
64
|
+
|
|
65
|
+
- [ ] Enable autodiscovery of modules containing Cron objects.
|
|
66
|
+
- [ ] Enable compatibility with single-file modules.
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
[BSD 3-Clause](./LICENSE.txt)
|
cronipy-0.1.0/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# cronipy
|
|
2
|
+
|
|
3
|
+
`cronipy` turns Python functions into cron jobs. You decorate a function with a schedule, run one command, and cronipy writes the crontab entries for you. The package is in early development, and the interface can change.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install cronipy
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
Define your jobs in a module of your package, e.g. `mypackage.tasks`:
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from cronipy import Cron
|
|
17
|
+
|
|
18
|
+
cron = Cron()
|
|
19
|
+
|
|
20
|
+
@cron.task("*/5 * * * *")
|
|
21
|
+
def do_something_every_five_minutes():
|
|
22
|
+
...
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Then install the jobs into your crontab:
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
cron.install()
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
or from a shell, install jobs from all `Cron` objects in the specified module:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
$ cronipy install mypackage.tasks
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`cronipy` adds one line per decorated function into the current user's `crontab`. Function output is logged in `logs/<function name>.log`.
|
|
38
|
+
|
|
39
|
+
## Options
|
|
40
|
+
|
|
41
|
+
You can pass these arguments to `Cron`:
|
|
42
|
+
|
|
43
|
+
- `dry_run`: when true, a job prints its name and does nothing.
|
|
44
|
+
- `env`: a dictionary of environment variables added to every crontab line.
|
|
45
|
+
- `log_dir`: where the log files go, `./logs` by default.
|
|
46
|
+
- `work_dir`: the directory each job runs in, `./` by default.
|
|
47
|
+
- `python`: the interpreter the jobs use, the one that runs cronipy by default.
|
|
48
|
+
|
|
49
|
+
## Development
|
|
50
|
+
|
|
51
|
+
This repository uses [uv](https://docs.astral.sh/uv/). Set up a development environment with `uv sync`, and run the checks with `uv run pytest`.
|
|
52
|
+
|
|
53
|
+
## Roadmap
|
|
54
|
+
|
|
55
|
+
- [ ] Enable autodiscovery of modules containing Cron objects.
|
|
56
|
+
- [ ] Enable compatibility with single-file modules.
|
|
57
|
+
|
|
58
|
+
## License
|
|
59
|
+
[BSD 3-Clause](./LICENSE.txt)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import json
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from functools import wraps
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from types import FrameType
|
|
9
|
+
from typing import Any, Callable
|
|
10
|
+
|
|
11
|
+
from cyclopts import App
|
|
12
|
+
|
|
13
|
+
__version__ = "0.1.0"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Cron:
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
dry_run: bool = False,
|
|
20
|
+
env: dict | None = None,
|
|
21
|
+
log_dir: Path | None = None,
|
|
22
|
+
work_dir: Path | None = None,
|
|
23
|
+
python: str = sys.executable,
|
|
24
|
+
):
|
|
25
|
+
self.dry_run = dry_run
|
|
26
|
+
self.env = env or {}
|
|
27
|
+
self.log_dir = log_dir or Path("./logs")
|
|
28
|
+
self.work_dir = work_dir or Path("./")
|
|
29
|
+
self.tasks: list[tuple[Callable, str, dict]] = []
|
|
30
|
+
self.app = App()
|
|
31
|
+
self.app.command(self.install, name="install-crontab")
|
|
32
|
+
self.python = python
|
|
33
|
+
|
|
34
|
+
frame = inspect.currentframe().f_back
|
|
35
|
+
while frame and frame.f_globals["__name__"].startswith("importlib."):
|
|
36
|
+
frame = frame.f_back
|
|
37
|
+
# TODO: make this work for calling from single file scripts
|
|
38
|
+
# where module becomes __main__
|
|
39
|
+
self.module = frame.f_globals["__name__"]
|
|
40
|
+
if self.module == "__main__" and frame.f_globals["__spec__"]:
|
|
41
|
+
self.module = frame.f_globals["__spec__"].name.strip(".__main__")
|
|
42
|
+
self.frame = frame
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def name(self) -> str | None:
|
|
46
|
+
for k, v in [*self.frame.f_globals.items()]:
|
|
47
|
+
if isinstance(v, Cron):
|
|
48
|
+
return k
|
|
49
|
+
|
|
50
|
+
def __call__(self):
|
|
51
|
+
self.app()
|
|
52
|
+
|
|
53
|
+
def install(self, replace_existing: bool = False):
|
|
54
|
+
print(f"environment vars:\n\n{json.dumps(self.env, indent=2)}")
|
|
55
|
+
|
|
56
|
+
self.log_dir.mkdir(exist_ok=True, parents=True)
|
|
57
|
+
|
|
58
|
+
if replace_existing:
|
|
59
|
+
subprocess.run("crontab -r", shell=True, check=True)
|
|
60
|
+
|
|
61
|
+
for task, crontab, env in self.tasks:
|
|
62
|
+
self._cronify(task, crontab, env)
|
|
63
|
+
|
|
64
|
+
def _command(self, func: Callable) -> Callable:
|
|
65
|
+
def wrapped(*args, **kwargs) -> Any:
|
|
66
|
+
print(f"[{datetime.now().isoformat()}]: {func.__name__}", flush=True)
|
|
67
|
+
|
|
68
|
+
if self.dry_run:
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
func(*args, **kwargs)
|
|
72
|
+
|
|
73
|
+
return wrapped
|
|
74
|
+
|
|
75
|
+
def task(self, crontab: str, env: dict | None = None) -> Callable:
|
|
76
|
+
def _task(func):
|
|
77
|
+
self.tasks.append((func, crontab, env or {}))
|
|
78
|
+
self.app.command(self._command(func), name=func.__name__)
|
|
79
|
+
|
|
80
|
+
@wraps(func)
|
|
81
|
+
def wrapped(*args, **kwargs) -> Any:
|
|
82
|
+
return func(*args, **kwargs)
|
|
83
|
+
|
|
84
|
+
return wrapped
|
|
85
|
+
|
|
86
|
+
return _task
|
|
87
|
+
|
|
88
|
+
def _cronify(self, func: Callable, crontab: str, env: dict | None = None):
|
|
89
|
+
name = func.__name__
|
|
90
|
+
env = env or {}
|
|
91
|
+
_env = " ".join((f"{k}={v}" for k, v in (self.env | env).items()))
|
|
92
|
+
_call = (
|
|
93
|
+
f"{_env} {self.python} -c"
|
|
94
|
+
f' \\"from {self.module} import {self.name}; {self.name}()\\" {name} >>'
|
|
95
|
+
f" {self.log_dir.absolute()}/{name}.log 2>&1"
|
|
96
|
+
)
|
|
97
|
+
_cmd = (
|
|
98
|
+
f'(crontab -l 2>/dev/null; echo "{crontab}'
|
|
99
|
+
f" cd {self.work_dir.absolute()}"
|
|
100
|
+
f' && {_env} {_call}") | crontab -'
|
|
101
|
+
)
|
|
102
|
+
subprocess.run(_cmd, shell=True, check=True)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import cyclopts
|
|
2
|
+
|
|
3
|
+
from . import Cron
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main():
|
|
7
|
+
app = cyclopts.App()
|
|
8
|
+
|
|
9
|
+
@app.command
|
|
10
|
+
def install(module: str):
|
|
11
|
+
exec(f"""
|
|
12
|
+
import {module} as mod
|
|
13
|
+
for obj in mod.__dict__.values():
|
|
14
|
+
if isinstance(obj, Cron):
|
|
15
|
+
obj.install()
|
|
16
|
+
""")
|
|
17
|
+
|
|
18
|
+
app()
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "cronipy"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Add your description here"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.14"
|
|
7
|
+
dependencies = ["cyclopts>=5.3.2"]
|
|
8
|
+
|
|
9
|
+
[[project.authors]]
|
|
10
|
+
name = "lantonic"
|
|
11
|
+
email = "luka.olegsson@gmail.com"
|
|
12
|
+
|
|
13
|
+
[project.scripts]
|
|
14
|
+
cronipy = "cronipy.cli:main"
|
|
15
|
+
|
|
16
|
+
[dependency-groups]
|
|
17
|
+
dev = [
|
|
18
|
+
"pytest>=9.0.3",
|
|
19
|
+
"pytest-ruff>=0.5",
|
|
20
|
+
"ruff>=0.17.3",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[tool.ruff]
|
|
24
|
+
target-version = "py314"
|
|
25
|
+
|
|
26
|
+
[tool.ruff.lint]
|
|
27
|
+
ignore = [
|
|
28
|
+
"F401",
|
|
29
|
+
"S102",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[tool.uv.build-backend]
|
|
33
|
+
module-name = "cronipy"
|
|
34
|
+
module-root = ""
|
|
35
|
+
|
|
36
|
+
[build-system]
|
|
37
|
+
requires = ["uv_build>=0.13.5,<0.14.0"]
|
|
38
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "cronipy"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Add your description here"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "lantonic", email = "luka.olegsson@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.14"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"cyclopts>=5.3.2",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[project.scripts]
|
|
15
|
+
cronipy = "cronipy.cli:main"
|
|
16
|
+
|
|
17
|
+
[dependency-groups]
|
|
18
|
+
dev = [
|
|
19
|
+
"pytest>=9.0.3",
|
|
20
|
+
"pytest-ruff>=0.5",
|
|
21
|
+
"ruff>=0.17.3",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[tool.ruff]
|
|
25
|
+
target-version = "py314"
|
|
26
|
+
|
|
27
|
+
[tool.ruff.lint]
|
|
28
|
+
ignore = ["F401", "S102"]
|
|
29
|
+
|
|
30
|
+
[build-system]
|
|
31
|
+
requires = ["uv_build>=0.13.5,<0.14.0"]
|
|
32
|
+
build-backend = "uv_build"
|
|
33
|
+
|
|
34
|
+
[tool.uv.build-backend]
|
|
35
|
+
module-name = "cronipy"
|
|
36
|
+
module-root = ""
|