steadycron 0.1.0__py3-none-any.whl

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.
steadycron/__init__.py ADDED
@@ -0,0 +1,61 @@
1
+ """SteadyCron code-monitoring SDK for Python.
2
+
3
+ Quickstart::
4
+
5
+ import steadycron
6
+
7
+ steadycron.api_key = "sc_ro_..." # read-only key, or set STEADYCRON_API_KEY env var
8
+
9
+ @steadycron.job("nightly-db-backup")
10
+ def backup():
11
+ ... # start on entry, success on return, fail (+re-raise) on exception
12
+
13
+ """
14
+ from __future__ import annotations
15
+
16
+ from typing import Optional
17
+
18
+ # ── Module-level configuration (set directly or via configure()) ──────────────
19
+ api_key: Optional[str] = None
20
+ api_url: str = "https://api.steadycron.com"
21
+ ping_url: str = "https://ping.steadycron.com"
22
+ environment: Optional[str] = None
23
+ capture_errors: bool = False
24
+ ping_timeout: float = 5.0
25
+ resolve_cache_ttl: float = 3600.0
26
+ monitors: dict[str, str] = {} # key → direct ping token (hardened path)
27
+
28
+ # ── Public API ────────────────────────────────────────────────────────────────
29
+ from ._config import configure
30
+ from ._exceptions import (
31
+ AmbiguousJobKeyError,
32
+ ConfigurationError,
33
+ InvalidMonitorKindError,
34
+ MonitorNotFoundError,
35
+ )
36
+ from ._monitor import Monitor, job, monitor
37
+ from ._resolve import clear_cache
38
+
39
+ __all__ = [
40
+ # Config
41
+ "api_key",
42
+ "api_url",
43
+ "ping_url",
44
+ "environment",
45
+ "capture_errors",
46
+ "ping_timeout",
47
+ "resolve_cache_ttl",
48
+ "monitors",
49
+ "configure",
50
+ # Core API
51
+ "Monitor",
52
+ "job",
53
+ "monitor",
54
+ # Exceptions
55
+ "MonitorNotFoundError",
56
+ "AmbiguousJobKeyError",
57
+ "InvalidMonitorKindError",
58
+ "ConfigurationError",
59
+ # Utils
60
+ "clear_cache",
61
+ ]
steadycron/_config.py ADDED
@@ -0,0 +1,70 @@
1
+ """Module-level configuration and the configure() helper."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ from typing import Optional
6
+
7
+ # ── Module-level settings ────────────────────────────────────────────────────
8
+
9
+ api_key: Optional[str] = None
10
+ api_url: str = "https://api.steadycron.com"
11
+ ping_url: str = "https://ping.steadycron.com"
12
+ environment: Optional[str] = None
13
+ capture_errors: bool = False
14
+ ping_timeout: float = 5.0
15
+ resolve_cache_ttl: float = 3600.0
16
+ monitors: dict[str, str] = {} # key → direct ping token (hardened path)
17
+
18
+
19
+ def configure(
20
+ *,
21
+ api_key: Optional[str] = None,
22
+ api_url: Optional[str] = None,
23
+ ping_url: Optional[str] = None,
24
+ environment: Optional[str] = None,
25
+ capture_errors: Optional[bool] = None,
26
+ ping_timeout: Optional[float] = None,
27
+ resolve_cache_ttl: Optional[float] = None,
28
+ ) -> None:
29
+ """Set one or more configuration values at once."""
30
+ import steadycron as _mod
31
+
32
+ if api_key is not None:
33
+ _mod.api_key = api_key
34
+ if api_url is not None:
35
+ _mod.api_url = api_url
36
+ if ping_url is not None:
37
+ _mod.ping_url = ping_url
38
+ if environment is not None:
39
+ _mod.environment = environment
40
+ if capture_errors is not None:
41
+ _mod.capture_errors = capture_errors
42
+ if ping_timeout is not None:
43
+ _mod.ping_timeout = ping_timeout
44
+ if resolve_cache_ttl is not None:
45
+ _mod.resolve_cache_ttl = resolve_cache_ttl
46
+
47
+
48
+ def effective_api_key() -> Optional[str]:
49
+ """Return the configured API key, falling back to STEADYCRON_API_KEY env var."""
50
+ import steadycron as _mod
51
+
52
+ return _mod.api_key or os.environ.get("STEADYCRON_API_KEY")
53
+
54
+
55
+ def effective_api_url() -> str:
56
+ import steadycron as _mod
57
+
58
+ return _mod.api_url or os.environ.get("STEADYCRON_API_URL") or "https://api.steadycron.com"
59
+
60
+
61
+ def effective_ping_url() -> str:
62
+ import steadycron as _mod
63
+
64
+ return _mod.ping_url or os.environ.get("STEADYCRON_PING_URL") or "https://ping.steadycron.com"
65
+
66
+
67
+ def effective_environment() -> Optional[str]:
68
+ import steadycron as _mod
69
+
70
+ return _mod.environment or os.environ.get("STEADYCRON_ENVIRONMENT")
@@ -0,0 +1,57 @@
1
+ """Exceptions raised by the SteadyCron SDK."""
2
+ from __future__ import annotations
3
+
4
+
5
+ class MonitorNotFoundError(Exception):
6
+ """Raised when a job key is not found (resolve returned 404).
7
+
8
+ This is a developer/configuration error — verify the key matches
9
+ a job in the SteadyCron Dashboard.
10
+ """
11
+
12
+ def __init__(self, key: str) -> None:
13
+ self.key = key
14
+ super().__init__(
15
+ f"Job key '{key}' was not found. "
16
+ "Verify the key matches a job in the SteadyCron Dashboard."
17
+ )
18
+
19
+
20
+ class AmbiguousJobKeyError(Exception):
21
+ """Raised when a key matches more than one job in the account (resolve returned 409).
22
+
23
+ Keys must be unique within the account for code monitoring to work.
24
+ """
25
+
26
+ def __init__(self, key: str) -> None:
27
+ self.key = key
28
+ super().__init__(
29
+ f"Job key '{key}' is ambiguous — it matches more than one job "
30
+ "in the account. Make the key unique in the SteadyCron Dashboard."
31
+ )
32
+
33
+
34
+ class InvalidMonitorKindError(Exception):
35
+ """Raised when the resolved job has kind 'http' rather than 'heartbeat'.
36
+
37
+ HTTP jobs are executed server-side and cannot be pinged from user code.
38
+ """
39
+
40
+ def __init__(self, key: str, kind: str) -> None:
41
+ self.key = key
42
+ self.kind = kind
43
+ super().__init__(
44
+ f"Job '{key}' has kind '{kind}'. "
45
+ "Only 'heartbeat' jobs can be pinged from code."
46
+ )
47
+
48
+
49
+ class ConfigurationError(Exception):
50
+ """Raised when neither a direct token nor an API key is available for a key."""
51
+
52
+ def __init__(self, key: str) -> None:
53
+ self.key = key
54
+ super().__init__(
55
+ f"No ping token or API key is configured for job '{key}'. "
56
+ "Either set steadycron.api_key or add an entry to steadycron.monitors."
57
+ )
steadycron/_monitor.py ADDED
@@ -0,0 +1,122 @@
1
+ """Monitor class, @job decorator, and monitor() context manager."""
2
+ from __future__ import annotations
3
+
4
+ import functools
5
+ import uuid
6
+ from contextlib import contextmanager
7
+ from typing import Any, Callable, Generator, Optional, TypeVar
8
+
9
+ from ._ping import send_ping
10
+ from ._resolve import resolve_token
11
+
12
+ F = TypeVar("F", bound=Callable[..., Any])
13
+
14
+
15
+ class Monitor:
16
+ """Represents a named SteadyCron monitor for manual ping control."""
17
+
18
+ def __init__(self, key: str) -> None:
19
+ self.key = key
20
+
21
+ def ping(
22
+ self,
23
+ state: str = "success",
24
+ message: Optional[str] = None,
25
+ ) -> None:
26
+ """Send a ping.
27
+
28
+ Args:
29
+ state: One of ``"start"``, ``"success"`` (default), or ``"fail"``.
30
+ message: Optional error message; sent only when ``state="fail"``
31
+ (or always if explicitly provided).
32
+ """
33
+ import steadycron as _mod
34
+
35
+ token = resolve_token(self.key)
36
+ suffix = state if state != "success" else None
37
+ ping_url = (_mod.ping_url or __import__("os").environ.get("STEADYCRON_PING_URL") or "https://ping.steadycron.com")
38
+ env = _mod.environment or __import__("os").environ.get("STEADYCRON_ENVIRONMENT")
39
+ send_ping(
40
+ token,
41
+ suffix,
42
+ env=env,
43
+ message=message if (state == "fail" or message is not None) else None,
44
+ timeout=_mod.ping_timeout,
45
+ ping_url_base=ping_url,
46
+ )
47
+
48
+
49
+ def _run_tracked(key: str, fn: Callable[..., Any], args: Any, kwargs: Any) -> Any:
50
+ """Core tracking logic shared by the decorator and context manager."""
51
+ import steadycron as _mod
52
+
53
+ token = resolve_token(key)
54
+ run_id = uuid.uuid4().hex
55
+ ping_url = (_mod.ping_url or __import__("os").environ.get("STEADYCRON_PING_URL") or "https://ping.steadycron.com")
56
+ env = _mod.environment or __import__("os").environ.get("STEADYCRON_ENVIRONMENT")
57
+ timeout = _mod.ping_timeout
58
+
59
+ send_ping(token, "start", env=env, run_id=run_id, timeout=timeout, ping_url_base=ping_url)
60
+ try:
61
+ result = fn(*args, **kwargs)
62
+ send_ping(token, None, env=env, run_id=run_id, timeout=timeout, ping_url_base=ping_url)
63
+ return result
64
+ except Exception:
65
+ msg: Optional[str] = None
66
+ if _mod.capture_errors:
67
+ import sys
68
+ exc = sys.exc_info()[1]
69
+ msg = str(exc) if exc is not None else None
70
+ send_ping(token, "fail", env=env, run_id=run_id, message=msg, timeout=timeout, ping_url_base=ping_url)
71
+ raise
72
+
73
+
74
+ def job(key: str) -> Callable[[F], F]:
75
+ """Decorator: wrap a function with start/success/fail pings.
76
+
77
+ Usage::
78
+
79
+ @steadycron.job("nightly-db-backup")
80
+ def backup():
81
+ ...
82
+ """
83
+
84
+ def decorator(fn: F) -> F:
85
+ @functools.wraps(fn)
86
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
87
+ return _run_tracked(key, fn, args, kwargs)
88
+
89
+ return wrapper # type: ignore[return-value]
90
+
91
+ return decorator
92
+
93
+
94
+ @contextmanager
95
+ def monitor(key: str) -> Generator[Monitor, None, None]:
96
+ """Context manager: wrap a block with start/success/fail pings.
97
+
98
+ Usage::
99
+
100
+ with steadycron.monitor("nightly-db-backup"):
101
+ run_backup()
102
+ """
103
+ import steadycron as _mod
104
+
105
+ token = resolve_token(key)
106
+ run_id = uuid.uuid4().hex
107
+ ping_url = (_mod.ping_url or __import__("os").environ.get("STEADYCRON_PING_URL") or "https://ping.steadycron.com")
108
+ env = _mod.environment or __import__("os").environ.get("STEADYCRON_ENVIRONMENT")
109
+ timeout = _mod.ping_timeout
110
+
111
+ send_ping(token, "start", env=env, run_id=run_id, timeout=timeout, ping_url_base=ping_url)
112
+ try:
113
+ yield Monitor(key)
114
+ send_ping(token, None, env=env, run_id=run_id, timeout=timeout, ping_url_base=ping_url)
115
+ except Exception:
116
+ msg2: Optional[str] = None
117
+ if _mod.capture_errors:
118
+ import sys
119
+ exc2 = sys.exc_info()[1]
120
+ msg2 = str(exc2) if exc2 is not None else None
121
+ send_ping(token, "fail", env=env, run_id=run_id, message=msg2, timeout=timeout, ping_url_base=ping_url)
122
+ raise
steadycron/_ping.py ADDED
@@ -0,0 +1,68 @@
1
+ """Ping transport: fire-and-forget HTTP pings, never raise."""
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ import socket
6
+ import urllib.error
7
+ import urllib.request
8
+ from typing import Optional
9
+ from urllib.parse import urlencode
10
+
11
+ _logger = logging.getLogger("steadycron")
12
+
13
+ _ERROR_MESSAGE_MAX_BYTES = 1024
14
+
15
+
16
+ def send_ping(
17
+ token: str,
18
+ suffix: Optional[str] = None,
19
+ *,
20
+ env: Optional[str] = None,
21
+ run_id: Optional[str] = None,
22
+ message: Optional[str] = None,
23
+ timeout: float = 5.0,
24
+ ping_url_base: str = "https://ping.steadycron.com",
25
+ ) -> None:
26
+ """Send a single ping. All errors are swallowed — never raises."""
27
+ base = ping_url_base.rstrip("/")
28
+ path = f"/{token}/{suffix}" if suffix else f"/{token}"
29
+ url = base + path
30
+
31
+ qs_parts: dict[str, str] = {}
32
+ if env:
33
+ qs_parts["env"] = env
34
+ if run_id:
35
+ qs_parts["run_id"] = run_id
36
+ if qs_parts:
37
+ url += "?" + urlencode(qs_parts)
38
+
39
+ body: Optional[bytes] = None
40
+ if message:
41
+ truncated = _truncate_utf8(message, _ERROR_MESSAGE_MAX_BYTES)
42
+ body = truncated.encode("utf-8")
43
+
44
+ req = urllib.request.Request(url, data=body or b"", method="POST")
45
+ if body:
46
+ req.add_header("Content-Type", "text/plain; charset=utf-8")
47
+
48
+ try:
49
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
50
+ status = resp.status
51
+ if status >= 300:
52
+ _logger.debug("SteadyCron ping to %s returned HTTP %d.", url, status)
53
+ except TimeoutError:
54
+ _logger.warning("SteadyCron ping to %s timed out after %.1fs.", url, timeout)
55
+ except socket.timeout:
56
+ _logger.warning("SteadyCron ping to %s timed out after %.1fs.", url, timeout)
57
+ except urllib.error.URLError as exc:
58
+ _logger.warning("SteadyCron ping to %s failed: %s", url, exc.reason)
59
+ except Exception as exc: # noqa: BLE001
60
+ _logger.warning("SteadyCron ping to %s failed unexpectedly: %s", url, exc)
61
+
62
+
63
+ def _truncate_utf8(text: str, max_bytes: int) -> str:
64
+ encoded = text.encode("utf-8")
65
+ if len(encoded) <= max_bytes:
66
+ return text
67
+ truncated = encoded[: max_bytes - 3].decode("utf-8", errors="ignore")
68
+ return truncated + "..."
steadycron/_resolve.py ADDED
@@ -0,0 +1,97 @@
1
+ """Token resolution: resolve a monitor key to its ping token via the API."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import logging
6
+ import threading
7
+ import time
8
+ import urllib.error
9
+ import urllib.request
10
+ from typing import Optional
11
+ from urllib.parse import urlencode
12
+
13
+ from ._exceptions import (
14
+ AmbiguousJobKeyError,
15
+ ConfigurationError,
16
+ InvalidMonitorKindError,
17
+ MonitorNotFoundError,
18
+ )
19
+
20
+ _logger = logging.getLogger("steadycron")
21
+
22
+ # Cache: key → (token, expires_at_unix)
23
+ _cache: dict[str, tuple[str, float]] = {}
24
+ _cache_lock = threading.Lock()
25
+
26
+
27
+ def _cache_get(key: str) -> Optional[str]:
28
+ with _cache_lock:
29
+ entry = _cache.get(key)
30
+ if entry is not None and entry[1] > time.monotonic():
31
+ return entry[0]
32
+ return None
33
+
34
+
35
+ def _cache_set(key: str, token: str, ttl: float) -> None:
36
+ with _cache_lock:
37
+ _cache[key] = (token, time.monotonic() + ttl)
38
+
39
+
40
+ def clear_cache() -> None:
41
+ """Clear the entire resolution cache (useful in tests)."""
42
+ with _cache_lock:
43
+ _cache.clear()
44
+
45
+
46
+ def resolve_token(key: str) -> str:
47
+ """Return the ping token for *key*, using cache or the resolve endpoint.
48
+
49
+ Raises:
50
+ ConfigurationError: if neither a direct token nor an API key is available.
51
+ MonitorNotFoundError: if the key is not found (404).
52
+ AmbiguousJobKeyError: if the key matches multiple monitors (409).
53
+ InvalidMonitorKindError: if the resolved monitor is not a heartbeat.
54
+ """
55
+ import steadycron as _mod
56
+
57
+ # 1. Direct token map (hardened path).
58
+ direct = _mod.monitors.get(key)
59
+ if direct:
60
+ return direct
61
+
62
+ # 2. Cache.
63
+ cached = _cache_get(key)
64
+ if cached is not None:
65
+ return cached
66
+
67
+ # 3. Resolve via API key.
68
+ api_key = _mod.api_key or __import__("os").environ.get("STEADYCRON_API_KEY")
69
+ if not api_key:
70
+ raise ConfigurationError(key)
71
+
72
+ api_url = (_mod.api_url or __import__("os").environ.get("STEADYCRON_API_URL") or "https://api.steadycron.com").rstrip("/")
73
+ params = urlencode({"key": key})
74
+ url = f"{api_url}/api/monitors/resolve?{params}"
75
+
76
+ req = urllib.request.Request(url, headers={"Authorization": f"Bearer {api_key}"})
77
+ try:
78
+ with urllib.request.urlopen(req) as resp:
79
+ body = json.loads(resp.read().decode())
80
+ except urllib.error.HTTPError as exc:
81
+ if exc.code == 404:
82
+ raise MonitorNotFoundError(key) from exc
83
+ if exc.code == 409:
84
+ raise AmbiguousJobKeyError(key) from exc
85
+ raise RuntimeError(f"Resolve returned HTTP {exc.code} for key '{key}'.") from exc
86
+
87
+ kind = body.get("kind", "")
88
+ if kind != "heartbeat":
89
+ raise InvalidMonitorKindError(key, kind)
90
+
91
+ ping_token: Optional[str] = body.get("ping_token")
92
+ if not ping_token:
93
+ raise RuntimeError(f"Resolve response missing ping_token for key '{key}'.")
94
+
95
+ ttl = _mod.resolve_cache_ttl
96
+ _cache_set(key, ping_token, ttl)
97
+ return ping_token
steadycron/py.typed ADDED
File without changes
@@ -0,0 +1,152 @@
1
+ Metadata-Version: 2.4
2
+ Name: steadycron
3
+ Version: 0.1.0
4
+ Summary: Code-monitoring SDK for SteadyCron — wrap your scheduled jobs with @steadycron.job to send start/success/fail pings automatically.
5
+ Project-URL: Homepage, https://steadycron.com
6
+ Project-URL: Repository, https://github.com/steadycron/steadycron-python
7
+ Project-URL: Bug Tracker, https://github.com/steadycron/steadycron-python/issues
8
+ Author: SteadyCron
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: Libraries
21
+ Classifier: Topic :: System :: Monitoring
22
+ Requires-Python: >=3.9
23
+ Description-Content-Type: text/markdown
24
+
25
+ # SteadyCron Python SDK
26
+
27
+ `steadycron` is the official Python code-monitoring SDK for [SteadyCron](https://steadycron.com).
28
+
29
+ Wrap your scheduled jobs with `@steadycron.job` and SteadyCron will:
30
+ - Know when a job **started**, **succeeded**, or **failed**
31
+ - Alert you when a job **doesn't run** on time (missed heartbeat)
32
+ - Detect **stuck runs** that start but never finish
33
+
34
+ The monitor must already exist — create it in the Dashboard, via YAML manifest, or Terraform.
35
+ The SDK never creates monitors. For a fully code-driven workflow, declare the monitor in your
36
+ Terraform configuration or YAML manifest and reference it from your application by key:
37
+ the cron schedule, alert rules, and SDK instrumentation all live in the same repository.
38
+
39
+ ---
40
+
41
+ ## Installation
42
+
43
+ ```bash
44
+ pip install steadycron
45
+ ```
46
+
47
+ No runtime dependencies — uses Python's standard library only.
48
+
49
+ ---
50
+
51
+ ## Quick start
52
+
53
+ ```python
54
+ import steadycron
55
+
56
+ steadycron.api_key = "sc_ro_..." # read-only key; or set STEADYCRON_API_KEY env var
57
+
58
+ @steadycron.job("nightly-db-backup")
59
+ def backup():
60
+ run_backup() # start on entry, success on return, fail (+re-raise) on exception
61
+ ```
62
+
63
+ ### Context manager
64
+
65
+ ```python
66
+ with steadycron.monitor("nightly-db-backup"):
67
+ run_backup()
68
+ ```
69
+
70
+ ### Manual pings
71
+
72
+ ```python
73
+ m = steadycron.Monitor("nightly-db-backup")
74
+ m.ping() # bare success heartbeat
75
+ m.ping(state="start")
76
+ m.ping(state="fail", message="disk full")
77
+ ```
78
+
79
+ ---
80
+
81
+ ## Authentication
82
+
83
+ The SDK uses a **read-only** API key to resolve monitor keys to ping tokens at startup. Create one in:
84
+
85
+ **SteadyCron Dashboard → Settings → API keys → New key → Scope: Read-only**
86
+
87
+ Set it via:
88
+ - Environment variable: `STEADYCRON_API_KEY=sc_ro_...`
89
+ - Module attribute: `steadycron.api_key = "sc_ro_..."`
90
+ - `steadycron.configure(api_key="sc_ro_...")`
91
+
92
+ ---
93
+
94
+ ## Configuration
95
+
96
+ ```python
97
+ import steadycron
98
+
99
+ steadycron.configure(
100
+ api_key="sc_ro_...",
101
+ environment="production", # optional — sent on every ping
102
+ capture_errors=True, # include exception message on fail pings (default False)
103
+ ping_timeout=5.0, # seconds (default 5)
104
+ resolve_cache_ttl=3600.0, # seconds (default 3600 = 1 hour)
105
+ )
106
+ ```
107
+
108
+ | Setting | Default | Env var fallback |
109
+ |---|---|---|
110
+ | `api_key` | `None` | `STEADYCRON_API_KEY` |
111
+ | `api_url` | `https://api.steadycron.com` | `STEADYCRON_API_URL` |
112
+ | `ping_url` | `https://ping.steadycron.com` | `STEADYCRON_PING_URL` |
113
+ | `environment` | `None` | `STEADYCRON_ENVIRONMENT` |
114
+ | `capture_errors` | `False` | — |
115
+ | `ping_timeout` | `5.0` s | — |
116
+ | `resolve_cache_ttl` | `3600.0` s | — |
117
+
118
+ ---
119
+
120
+ ## How it works
121
+
122
+ 1. On first use, the SDK calls `GET /api/monitors/resolve?key=<your-key>` with the read-only API key to retrieve the ping token. The token is cached for 1 hour (configurable).
123
+ 2. All pings are fire-and-forget: a bounded `ping_timeout` is applied; any error is logged via `logging.getLogger("steadycron")` at WARNING and discarded. They never raise.
124
+ 3. Resolution errors (404 unknown key, 409 ambiguous key, wrong kind) raise immediately — they indicate misconfiguration.
125
+ 4. On exception, a `fail` ping is sent and the **original exception is re-raised unchanged**.
126
+
127
+ ---
128
+
129
+ ## Direct / token mode (hardened path)
130
+
131
+ If you cannot use API key resolution (e.g. air-gapped environments), set the ping token directly:
132
+
133
+ ```python
134
+ steadycron.monitors = {"nightly-db-backup": "hRkmWz8oZtlMFzvTAUdnRE"}
135
+ ```
136
+
137
+ The token is visible via **Job detail → Code monitoring → Reveal ping token** in the Dashboard.
138
+
139
+ ---
140
+
141
+ ## Reliability contract
142
+
143
+ - **Ping failures are never raised.** A transport error, timeout, or non-2xx response is logged and discarded.
144
+ - **Resolution errors always raise.** A 404 or 409 from the resolve endpoint raises on first use; fix the key or remove the decorator.
145
+ - **Original exceptions pass through unchanged.** The decorator and context manager do not wrap exceptions.
146
+ - **No runtime dependencies.** The SDK uses `urllib.request` from the standard library.
147
+
148
+ ---
149
+
150
+ ## License
151
+
152
+ MIT
@@ -0,0 +1,11 @@
1
+ steadycron/__init__.py,sha256=YOD0touQBH4TBWjHZFjSIE8e5j96ftR4KvqucYxK4rg,1682
2
+ steadycron/_config.py,sha256=szP8GE_rKQVYr4EfIES8G7SFDaV8U_P_ex9_QJNI2L4,2237
3
+ steadycron/_exceptions.py,sha256=DG4NBjNijcncslmnku90mVnG1EY2BB5pDvWxli0jqX0,1833
4
+ steadycron/_monitor.py,sha256=BP5AG8xLd90WTfcty2-33X-Gpsgv99gHzkGgCDkgkjo,4136
5
+ steadycron/_ping.py,sha256=cLPG8bS3zKWB5FAjkwSbY8E7aJ43wi4zpLgqV0r6VY4,2211
6
+ steadycron/_resolve.py,sha256=wBQ3pJ9ht2UuZDde5_bE_KXtEQXPNGOKt2fPD-nKRQE,2965
7
+ steadycron/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ steadycron-0.1.0.dist-info/METADATA,sha256=boODK6T9ZLqUXg90zY0UR985W3wVA44MdbiIeNemtQA,5081
9
+ steadycron-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
10
+ steadycron-0.1.0.dist-info/licenses/LICENSE,sha256=sAzhUjSXWkDJKX4EeGmAHvyyMa6A6fFbVhOYUsfu5yo,1067
11
+ steadycron-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SteadyCron
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.