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 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)
@@ -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,2 @@
1
+ if __name__ == "__main__":
2
+ print("hi")
@@ -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 = ""