Repository navigation
Expand file tree
/
Copy pathpyproject.toml
More file actions
387 lines (369 loc) · 16.2 KB
/
Copy pathpyproject.toml
File metadata and controls
387 lines (369 loc) · 16.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
[build-system]
# setuptools 83.0.0 fixes CVE-2026-59890, which lets an sdist built on macOS
# include files that MANIFEST.in excludes. setuptools_scm 8.0.0 adds the
# version_file option.
requires = ["setuptools>=83.0.0", "setuptools_scm>=8.0.0"]
build-backend = "setuptools.build_meta"
[project]
name = "cronstable"
description = "A job scheduler with retries, alerts, saved run history, workflows, and dashboards for single machines and clusters."
readme = "README.md"
requires-python = ">=3.11"
license = "MIT"
license-files = ["LICENSE", "TRADEMARKS.md", "LICENSING.md"]
authors = [{ name = "Parker Loflin", email = "parker@cronstable.dev" }]
maintainers = [{ name = "Parker Loflin", email = "parker@cronstable.dev" }]
keywords = [
"cron",
"crontab",
"scheduler",
"job-scheduler",
"task-scheduler",
"scheduling",
"cron-replacement",
"cronjob",
"container",
"docker",
"kubernetes",
"k8s",
"daemon",
"yaml",
"devops",
"sre",
"sysadmin",
"sentry",
"statsd",
"prometheus",
"metrics",
"monitoring",
"retry",
"timezone",
"asyncio",
]
dynamic = ["version"]
# Each floor here and in the extras is the oldest release cronstable
# supports, and its comment names the reason, such as a security fix or an
# API that cronstable calls. pip installs the newest release anyway, so a
# higher floor only blocks users who hold a package back for another
# dependency. See "Dependency floors" in CONTRIBUTING.md.
dependencies = [
# Require an importable release when testing minimum dependencies.
# PyPI withdrew 1.7.0 because it causes an import error.
"strictyaml>=1.7.3,<2",
# The web dashboard is an aiohttp server. 3.14.3 fixes the newest
# published advisories, and 3.14.4 hardens request parsing and closes
# idle and stalled connections.
"aiohttp>=3.14.4,<4",
# 2.8.0 fixes CVE-2024-40647, which can expose environment variables to
# subprocesses.
"sentry-sdk>=2.8.0,<3",
# 5.1.3 carries the SMTP command-injection and STARTTLS
# response-injection fixes (CVE-2026-53533, CVE-2026-55558).
"aiosmtplib>=5.1.3,<6",
# 3.1.6 carries the fixes for the published Jinja2 advisories, through
# CVE-2025-27516.
"jinja2>=3.1.6,<4",
# zoneinfo's tz database, so timezones resolve on minimal/slim images
# that don't ship the system tzdata. The package has no API, so any
# release works; the floor keeps zone rules from 2024 or later.
"tzdata>=2024.1",
# Per-run CPU/memory accounting (opt-in `monitorResources`; see
# cronstable.resources). The feature is off by default, but the import is
# unconditional so `cronstable --help` and mypy resolve it everywhere.
# 5.9.4 is the first release with Python 3.11 wheels.
"psutil>=5.9.4",
]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"Intended Audience :: System Administrators",
"Natural Language :: English",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: 3.15",
"Environment :: Console",
"Framework :: AsyncIO",
"Operating System :: POSIX :: Linux",
"Operating System :: MacOS",
"Operating System :: Microsoft :: Windows",
"Topic :: System :: Systems Administration",
"Topic :: System :: Monitoring",
"Topic :: Office/Business :: Scheduling",
"Topic :: Utilities",
]
[project.urls]
Homepage = "https://github.com/ptweezy/cronstable"
Documentation = "https://github.com/ptweezy/cronstable/wiki"
Demo = "https://demo.cronstable.com"
Source = "https://github.com/ptweezy/cronstable"
Changelog = "https://github.com/ptweezy/cronstable/releases"
Issues = "https://github.com/ptweezy/cronstable/issues"
Container = "https://github.com/ptweezy/cronstable/pkgs/container/cronstable"
[project.scripts]
cronstable = "cronstable.__main__:main"
[project.optional-dependencies]
# Official Kubernetes client for the `kubernetes` leadership backend. Without
# it (or where its grpc/protobuf wheels are missing) the backend falls back to
# its own apiserver REST transport over aiohttp. The etcd backend needs none.
# 27.2.0 is the oldest release tested with the backend's Lease calls.
kubernetes = ["kubernetes>=27.2.0"]
# uvloop's faster event loop; cronstable falls back to stock asyncio without
# it. Windows has no uvloop build (and needs the Proactor loop for
# subprocesses), so the marker skips it there. cronstable calls only
# uvloop.new_event_loop(); 0.19.0 is the oldest release tested with it.
speedups = [
"uvloop>=0.19.0; sys_platform != 'win32'",
# orjson speeds the state-persistence and cluster-gossip JSON paths
# (cronstable._json falls back to stdlib json). Optional because some
# targets have no wheel: riscv64, armv6, ppc64le, s390x, musl armv7,
# musl i686, and 32-bit Windows. The line has no marker because 64-bit
# Windows has a wheel. The cp315 glibc wheels require glibc 2.39 or
# later, which a marker cannot see, so pip builds from source on an
# older glibc. 3.11.6 fixes CVE-2025-67221, unbounded recursion on
# deeply nested documents.
"orjson>=3.11.6",
# isal (ISA-L) gzips web and cluster responses with SIMD deflate
# (cronstable._gzip falls back to stdlib zlib). The marker lists only
# targets with a wheel, so pip never tries the sdist, which builds ISA-L
# and needs nasm on x86. Linux stops at x86_64 and aarch64, with the
# same 32-bit userland caveat as the push extra's cryptography line.
# 1.9.0 is the first release with a wheel for every target the marker
# lists, including Python 3.15.
"isal>=1.9.0; (sys_platform == 'linux' and (platform_machine == 'x86_64' or platform_machine == 'aarch64')) or sys_platform == 'darwin' or (sys_platform == 'win32' and platform_machine == 'AMD64')",
]
# End-to-end encrypted push alerts (the `push` reporter; see cronstable.push).
# PyNaCl seals `x25519` and fails closed: a config with a `push:` section
# refuses to start without it (config._validate_push_config).
# cryptography seals the post-quantum `xwing` suite. Its marker lists only
# targets with a wheel, so pip never tries the sdist (which needs Rust and
# OpenSSL headers); elsewhere the daemon seals `x25519`, refuses `xwing`
# pairings and says so at startup. Linux stops at x86_64 and aarch64, the
# only arches with wheels under both libcs (a marker cannot see the libc).
# The release binaries (`pq`/`pq_src` in release.yml) and container images
# (docker/extract_deps.py) build xwing for more targets.
# The floor is the patched release (GHSA-537c-gmf6-5ccf, GHSA-g6cj-pr64-35w5,
# GHSA-m2h6-j472-rp4c, GHSA-jwv3-5hgf-82ww). 49.0.0 dropped Intel macOS and
# 32-bit Windows; they keep x25519 only. Do not cap at a vulnerable 48.x.
# `platform_machine` reports the kernel: on a 32-bit userland with a 64-bit
# kernel, install with `linux32 pip install "cronstable[push]"`. A 32-bit
# Python on 64-bit Windows reports `AMD64`; install `pynacl` by hand there or
# use a 64-bit Python.
push = [
"pynacl>=1.6.2",
"cryptography>=50.0.2,<51; (sys_platform == 'linux' and (platform_machine == 'x86_64' or platform_machine == 'aarch64')) or (sys_platform == 'darwin' and platform_machine == 'arm64') or (sys_platform == 'win32' and platform_machine == 'AMD64')",
]
# Zero-config LAN discovery (the opt-in `web.bonjour` mDNS advert; see
# cronstable.discovery). Same fail-closed contract: web.bonjour without this
# installed is a ConfigError, never a silent no-advert. 0.149.16 fixes the
# LAN-side denial-of-service and cache-corruption advisories through
# CVE-2026-48487.
discovery = ["zeroconf>=0.149.16"]
dev = [
# Canonical dev list: tox and pip install this extra, and
# scripts/generate_build_files.py derives requirements/min.txt and
# requirements/dev-freethreaded.txt from it. Edit dependencies here.
# cryptography (mTLS and X-Wing tests) reuses the push marker and its
# linux32 caveat; those tests skip where it is absent.
"cryptography>=50.0.2,<51; (sys_platform == 'linux' and (platform_machine == 'x86_64' or platform_machine == 'aarch64')) or (sys_platform == 'darwin' and platform_machine == 'arm64') or (sys_platform == 'win32' and platform_machine == 'AMD64')",
# orjson is wheel-only here: the marker skips targets that would need a
# Rust source build. The exception is Python 3.15 on a glibc older than
# 2.39, which a marker cannot see (see the speedups extra). py314t and
# py315t use a generated list without it, because no marker can detect
# the free-threaded ABI that orjson rejects.
"orjson>=3.11.6; platform_machine != 'ARM64' or sys_platform != 'win32'",
# isal reuses the speedups marker; tests/test_gzip.py covers both
# backends wherever it installs.
"isal>=1.9.0; (sys_platform == 'linux' and (platform_machine == 'x86_64' or platform_machine == 'aarch64')) or sys_platform == 'darwin' or (sys_platform == 'win32' and platform_machine == 'AMD64')",
"pynacl>=1.6.2",
"zeroconf>=0.149.16",
# Browser-backed tests require a separate `playwright install chromium`
# step after pip installs the library. No wheel/sdist exists on win-arm64.
"playwright; platform_machine != 'ARM64' or sys_platform != 'win32'",
# security linter; bandit[toml] reads [tool.bandit] from pyproject.toml.
"bandit[toml]",
"mypy",
# validates docs/openapi.yaml (.github/scripts/check_openapi.py, tox -e openapi).
"openapi-spec-validator",
# validates MCP tool results against their declared outputSchema
# (tests/test_mcp_tools.py).
"jsonschema",
# Compare the saved cron test cases with the original library in
# tests/test_cronexpr.py on each CI runner.
"crontab>=1.0.5,<2",
# Property tests generate schedules, configuration, Task XML, and
# protocol payloads to check invariants (tests/test_properties_*.py).
"hypothesis>=6.168.5",
"pytest",
"pytest-asyncio",
"pytest-cov",
# Randomize test order to find dependencies between tests; reproduce an
# order with the header seed: `-p randomly --randomly-seed=N`.
"pytest-randomly",
# Report timed-out tests and their thread stacks before the
# faulthandler_timeout limit terminates the session.
"pytest-timeout",
"ruff",
"tox",
# tox-uv routes tox's env creation + installs through uv (see tox.ini).
"tox-uv",
]
[tool.setuptools]
packages = ["cronstable", "cronstable.web", "cronstable.backends"]
[tool.setuptools.package-data]
"cronstable.web" = ["*.html"]
# License notices ship in every artifact (wheel, Docker, frozen binaries)
# so the LGPL notice for bundled python-zeroconf stays with its code.
# `cronstable --third-party-licenses` prints them.
"cronstable" = ["licenses/*.txt"]
[tool.setuptools_scm]
version_file = "cronstable/version.py"
[tool.ruff]
target-version = "py311"
line-length = 79
[tool.ruff.lint]
select = [
# Bugbear warnings
"B",
"B9",
# Blind except: each broad `except Exception` carries a
# `# noqa: BLE001 - <reason>` marker
"BLE001",
# mccabe complexity warnings
"C",
# Pycodestyle errors https://pycodestyle.pycqa.org/en/latest/intro.html#error-codes
"E",
# Pyflakes violations https://flake8.pycqa.org/en/latest/user/error-codes.html
"F",
# Pycodestyle warnings
"W",
# pyupgrade: typing modernization (builtin generics, PEP 604 unions and
# optionals, deprecated typing imports) and the Python 3.11 spellings
# datetime.UTC and builtin TimeoutError
"UP006",
"UP007",
"UP017",
"UP035",
"UP041",
"UP045",
"I", # <= import sorting
]
[tool.ruff.lint.mccabe]
max-complexity = 20
[tool.mypy]
python_version = "3.11"
# Check the POSIX API surface (grp, pwd, os.setuid) on every OS, as Linux CI
# does; Windows branches are runtime-guarded and the Windows test rows cover
# them. No `--platform win32` pass: cronstable.platform.apply_priority
# reaches os.setpriority behind a plain bool, so that pass would need a
# `type: ignore` that the Linux pass reports as unused.
platform = "linux"
no_implicit_optional = true
warn_no_return = true
warn_return_any = true
strict_optional = true
[[tool.mypy.overrides]]
# Optional-extra deps, imported behind try/except guards, so a checkout
# without the extras still type-checks. Where an extra is installed and ships
# py.typed (cryptography in the dev env), mypy still checks it fully.
module = [
"orjson",
"uvloop",
"isal",
"isal.*",
"nacl",
"nacl.*",
"cryptography",
"cryptography.*",
"zeroconf",
"zeroconf.*",
"kubernetes",
"kubernetes.*",
]
ignore_missing_imports = true
[[tool.mypy.overrides]]
# Core runtime deps with no py.typed (stub packages deliberately omitted).
# This table is the ONLY missing-import allowance, so a new unresolvable
# import fails tox -e mypy.
module = [
"strictyaml",
"strictyaml.*",
"psutil",
]
ignore_missing_imports = true
[tool.pytest.ini_options]
asyncio_mode = "auto"
# Every async fixture is function-scoped. Leaving this unset raises a
# pytest-asyncio deprecation warning, which the "error" filter makes fatal.
asyncio_default_fixture_loop_scope = "function"
testpaths = ["tests", ".github/tests"]
# A wedged await dumps every thread's stack and fails the run before the
# 30-minute CI job timeout cancels it. Armed per test; none comes near 300s.
faulthandler_timeout = 300
faulthandler_exit_on_timeout = true
# Report a test timeout and thread stacks after the asyncio task dump
# (tests/conftest.py, at 80% of faulthandler_timeout), but before
# faulthandler terminates the session. Recovery depends on the timeout method.
timeout = 270
# Treat warnings as errors, including unawaited coroutines, unclosed
# resources, and deprecated API use. Document a reason for each exception.
filterwarnings = [
"error",
# cron.py and the demo gateway store request values under string keys.
# Using web.RequestKey would import aiohttp when the daemon module loads,
# violating the startup constraint in tests/test_perf_invariants.py.
"ignore::aiohttp.web_exceptions.NotAppKeyWarning",
]
[tool.coverage.run]
source = ["cronstable"]
branch = true
omit = ["cronstable/version.py"]
[tool.coverage.report]
show_missing = true
# Three pragma tokens, two profiles. A bare `# pragma: no cover` is hidden on
# every OS; `# pragma: no cover (windows)` is hidden on POSIX and MEASURED on
# Windows; `(posix)` is the mirror. The token may follow other text.
# Rule 2 is the only per-profile rule: tox.ini sets CRONSTABLE_COVERAGE_SKIP
# to the platform whose branches CANNOT run in that env, and the `windows`
# default gives a bare `pytest --cov` or an IDE run the POSIX profile.
#
# exclude_lines REPLACES coverage's defaults (with `exclude_also`, the default
# pragma rule would swallow the tagged pragmas), so rule 1 is
# DEFAULT_EXCLUDE[0] plus a lookahead and the last two copy DEFAULT_EXCLUDE[1]
# and [2]. tests/test_coverage_profiles.py pins rule 1 verbatim and the last
# two by behavior, because coverage rewords those in patch releases.
exclude_lines = [
'#\s*(pragma|PRAGMA)[:\s]?\s*(no|NO)\s*(cover|COVER)(?!.*\((windows|posix)\))',
'#\s*(pragma|PRAGMA)[:\s]?\s*(no|NO)\s*(cover|COVER).*\(${CRONSTABLE_COVERAGE_SKIP-windows}\)',
'^\s*(((async )?def .*?)?[\])]+(\s*->.*?)?:\s*)?\.\.\.\s*(#|$)',
'if (typing\.)?TYPE_CHECKING:',
]
[tool.mutmut]
# Nightly mutation tests, one module per job; a surviving mutant points to a
# missing or weak assertion. List only input-driven modules, which avoids
# timing-related failures. To run locally:
# pip install mutmut && mutmut run "cronstable.redact*" && mutmut results
source_paths = [
"cronstable/cronexpr.py",
"cronstable/crontabs.py",
"cronstable/redact.py",
"cronstable/ical.py",
"cronstable/_json.py",
"cronstable/fingerprint.py",
]
pytest_add_cli_args_test_selection = [
"tests/test_cronexpr.py",
"tests/test_properties_cronexpr.py",
"tests/test_crontabs.py",
"tests/test_redact.py",
"tests/test_ical.py",
"tests/test_json_portability.py",
"tests/test_fingerprint.py",
"tests/test_properties_parsers.py",
]
also_copy = ["tests", "cronstable"]
pytest_add_cli_args = ["-p", "no:randomly", "-p", "no:cacheprovider"]
[tool.bandit]
exclude_dirs = ["tests"]