telefi 0.3.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.
telefi-0.3.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Michael Huang
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.
telefi-0.3.0/PKG-INFO ADDED
@@ -0,0 +1,199 @@
1
+ Metadata-Version: 2.4
2
+ Name: telefi
3
+ Version: 0.3.0
4
+ Summary: Telegram as a two-way control surface for scripts, CI and agents: drop a JSON envelope in a folder, get a readable message, and receive the owner's replies back as events.
5
+ Author: Michael Huang
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/sibulus13/telefi
8
+ Project-URL: Issues, https://github.com/sibulus13/telefi/issues
9
+ Keywords: telegram,two-way,chatops,human-in-the-loop,notifications,agents
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: Microsoft :: Windows :: Windows 11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Topic :: Communications :: Chat
14
+ Requires-Python: >=3.12
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest==8.4.2; extra == "dev"
19
+ Dynamic: license-file
20
+
21
+ # TeleFi
22
+
23
+ > **Developed and verified on Windows 11** (build 26200, Python 3.12.4). Pure standard-library
24
+ > Python, so other platforms should work, but only Windows 11 is tested today.
25
+
26
+ **Telegram as a two-way control surface for your scripts, CI and autonomous agents.** Programs
27
+ drop a small JSON file in a folder; TeleFi sends it to your Telegram in a readable format and
28
+ records the delivery. You answer from your phone (a button, a typed note, a `/command`) and TeleFi
29
+ hands your answer back to the program as a validated JSON event. Pure Python standard library:
30
+ no runtime dependencies.
31
+
32
+ ```mermaid
33
+ flowchart TD
34
+ P["your programs<br/>(any language)"] -->|"envelope JSON<br/>into spool/out/"| H["TeleFi hub"]
35
+ H --> T["your Telegram<br/>(glance-first message)"]
36
+ H --> L["local records<br/>outbox / inbox / conversation"]
37
+ T -->|"tap · reply · /command"| H
38
+ H -->|"inbound JSON<br/>to a queue or a command"| P
39
+ ```
40
+
41
+ ## Why TeleFi
42
+
43
+ - **Steer autonomous code from your phone.** An agent or CI job hits a failure; you tap *Retry with
44
+ note*, type "use the 2016-2025 window", and the job receives that note as an event. No laptop, no
45
+ remote session.
46
+ - **One way to notify, from any language.** Producers write a JSON file: no SDK, no network code, no
47
+ bot token in every program. A producer works even while the hub is down (the file waits).
48
+ - **Readable at a glance.** A one-line headline, a one-line "so what", details folded away, links
49
+ as buttons. Every program's messages look the same.
50
+ - **Everything is on record.** Every message, every delivery result, every reply and the whole chat
51
+ transcript, as JSON lines on your disk, rotated before they fill up.
52
+ - **Contracts, not conventions.** Messages, replies, config and command lists are JSON Schemas; a
53
+ new program adopts TeleFi by reading `telefi schema envelope.v1`.
54
+
55
+ ## Features
56
+
57
+ | feature | how |
58
+ |---|---|
59
+ | Routing | config rules by source, kind and severity -> Telegram, a local file, or a mock |
60
+ | Dedup, holds, ack | one delivery per `dedup_key` per window; `not_before` holds a message (a grace period); Ack silences re-alerts for N days |
61
+ | Glance-first format | headline + insight, collapsed details, URL buttons, action buttons |
62
+ | Two-way | buttons, ForceReply prompts for notes, replies to any message, `/commands` |
63
+ | Command registry | each program ships a manifest; `/help`, `/list` and Telegram's `/` menu are built from them |
64
+ | Sinks | `queue` (a folder your program drains) or `command` (a program fed the event on stdin; its last output line is the reply) |
65
+ | Log rotation | each log rotates at `rotate_at` x `max_bytes` (the safety margin), keeping `keep` old files |
66
+ | Safe tests | a mock adapter by default; live sends only with `TELEFI_LIVE=1` |
67
+
68
+ ## Requirements
69
+
70
+ | | |
71
+ |---|---|
72
+ | OS | Windows 11 (verified). Linux/macOS untested. |
73
+ | Python | 3.12 (verified with 3.12.4) |
74
+ | Runtime dependencies | none ([requirements.txt](requirements.txt) says so explicitly) |
75
+ | Test dependencies | pinned in [requirements-dev.txt](requirements-dev.txt) (pytest 8.4.2 and its deps) |
76
+ | Telegram | a bot token from @BotFather and your chat id |
77
+
78
+ ## Try it locally, step by step
79
+
80
+ Each step is self-contained and was run end to end on 2026-10-25. Steps 1-5 need no Telegram.
81
+
82
+ **1. Install and run the tests** (expect all green, one live test skipped)
83
+
84
+ ```bash
85
+ git clone <this repo> telefi && cd telefi
86
+ pip install -e . -r requirements-dev.txt
87
+ python -m pytest
88
+ ```
89
+
90
+ **2. Use a throwaway home** so nothing touches a real install
91
+
92
+ ```bash
93
+ export TELEFI_HOME="$PWD/.try" # PowerShell: $env:TELEFI_HOME = "$PWD\.try"
94
+ ```
95
+
96
+ **3. See every message type exactly as Telegram shows it** (sends nothing)
97
+
98
+ ```bash
99
+ python -m telefi preview
100
+ ```
101
+
102
+ **4. Route the samples through the full pipeline with the mock channel**
103
+
104
+ ```bash
105
+ python -m telefi samples
106
+ # ci.failure [error] -> telegram=ok, file=ok ... mode: mock (nothing sent)
107
+ ```
108
+
109
+ **5. Publish your own message, deliver it, and read the record**
110
+
111
+ ```bash
112
+ python -c "from telefi import envelope, publish; publish(envelope(source='me', kind='info', severity='info', title='hello from TeleFi'))"
113
+ python -m telefi drain # info routes to the local file only
114
+ tail -1 .try/outbox.jsonl # the envelope and each channel's result
115
+ ```
116
+
117
+ **6. Connect Telegram.** Copy `examples/.env.example` to `.env` (git-ignored) and fill it in. Create a bot with [@BotFather](https://t.me/BotFather), send it any message,
118
+ and read your chat id from `https://api.telegram.org/bot<TOKEN>/getUpdates`.
119
+
120
+ | variable | required | meaning |
121
+ |---|---|---|
122
+ | `TELEGRAM_BOT_TOKEN` | yes | the bot token from BotFather |
123
+ | `TELEGRAM_CHAT_ID` | yes | the only chat TeleFi talks to; every other chat is ignored |
124
+ | `TELEFI_HOME` | no | config and records (default `~/.telefi`) |
125
+ | `TELEFI_SPOOL` | no | the drop folder (default `$TELEFI_HOME/spool`) |
126
+
127
+ ```bash
128
+ python -m telefi samples --send # one real message of each type
129
+ ```
130
+
131
+ **7. Turn on replies.** Copy the full example config (every option, documented in
132
+ [docs/CONFIGURATION.md](docs/CONFIGURATION.md)); it enables replies, routes `/ci` commands to
133
+ `examples/echo_sink.py` and lists `examples/commands.example.json`:
134
+
135
+ ```bash
136
+ cp examples/hub.example.json "$TELEFI_HOME/hub.json"
137
+ python -m telefi serve # leave running; one hub per bot
138
+ ```
139
+
140
+ In Telegram: send `/help`, then `/list`, then `/ci retry 7 try again`; the echo sink answers
141
+ `retry received`. Tap *Retry with note* on a sample from step 6, type a note, and see it echoed.
142
+
143
+ **8. Check the records**
144
+
145
+ ```bash
146
+ tail -3 "$TELEFI_HOME/conversation.jsonl" # both directions, in order
147
+ tail -1 "$TELEFI_HOME/inbox.jsonl" # your reply and what the sink returned
148
+ ```
149
+
150
+ ## Run it in the background (optional)
151
+
152
+ The hub is one long-running command, `python -m telefi serve`: run it under whatever you already
153
+ use to keep processes alive. Two ready-made examples:
154
+
155
+ - Windows 11 (verified): `powershell -NoProfile -File examples/windows-service.ps1` registers a
156
+ scheduled task running `pythonw.exe -m telefi serve` (no console window), restarted every 5
157
+ minutes if it stops.
158
+ - Linux (untested): `examples/telefi.service` (systemd user unit).
159
+
160
+ Command sinks run in the hub's working directory, so give them absolute paths (or `cwd`).
161
+
162
+ ## Send from your program
163
+
164
+ ```python
165
+ from telefi import envelope, publish
166
+
167
+ publish(envelope(source="ci", kind="failure", severity="error",
168
+ title="Build #43 failed", insight="Tests passed; the deploy step timed out.",
169
+ body="step deploy: timeout after 600s", dedup_key="ci:build:43",
170
+ actions=[{"id": "retry_note", "label": "Retry with note", "namespace": "ci",
171
+ "input": {"prompt": "What should change?"}}]))
172
+ ```
173
+
174
+ From any other language, write the same JSON to `$TELEFI_HOME/spool/out/<id>.json` atomically
175
+ (write a temp file, then rename): see `examples/publish.mjs` for Node. Replies reach your program
176
+ through a command sink (`examples/echo_sink.py`) or a queue folder (`examples/queue_drain.py`).
177
+
178
+ ## Examples (one per configurable part, all validated by the tests)
179
+
180
+ | file | what it configures |
181
+ |---|---|
182
+ | `examples/.env.example` | secrets and paths |
183
+ | `examples/hub.minimal.json` | send-only hub |
184
+ | `examples/hub.example.json` | every hub option: routes, channels, dedup, logs, replies, sinks |
185
+ | `examples/commands.example.json` | an app's `/commands` |
186
+ | `examples/envelope.example.json` | every message field |
187
+ | `examples/inbound.example.json` | what your program receives |
188
+ | `examples/echo_sink.py`, `examples/queue_drain.py` | the two kinds of reply handler |
189
+ | `examples/publish.mjs` | publishing from Node |
190
+ | `examples/windows-service.ps1`, `examples/telefi.service` | running in the background (optional) |
191
+
192
+ ## Reference
193
+
194
+ - Contracts: `python -m telefi schema envelope.v1 | inbound.v1 | hub_config.v1 | commands.v1`
195
+ - [Configuration reference](docs/CONFIGURATION.md) · [Design](docs/DESIGN.md) · [Similar projects](docs/LANDSCAPE.md)
196
+
197
+ ## License
198
+
199
+ MIT, see [LICENSE](LICENSE).
telefi-0.3.0/README.md ADDED
@@ -0,0 +1,179 @@
1
+ # TeleFi
2
+
3
+ > **Developed and verified on Windows 11** (build 26200, Python 3.12.4). Pure standard-library
4
+ > Python, so other platforms should work, but only Windows 11 is tested today.
5
+
6
+ **Telegram as a two-way control surface for your scripts, CI and autonomous agents.** Programs
7
+ drop a small JSON file in a folder; TeleFi sends it to your Telegram in a readable format and
8
+ records the delivery. You answer from your phone (a button, a typed note, a `/command`) and TeleFi
9
+ hands your answer back to the program as a validated JSON event. Pure Python standard library:
10
+ no runtime dependencies.
11
+
12
+ ```mermaid
13
+ flowchart TD
14
+ P["your programs<br/>(any language)"] -->|"envelope JSON<br/>into spool/out/"| H["TeleFi hub"]
15
+ H --> T["your Telegram<br/>(glance-first message)"]
16
+ H --> L["local records<br/>outbox / inbox / conversation"]
17
+ T -->|"tap · reply · /command"| H
18
+ H -->|"inbound JSON<br/>to a queue or a command"| P
19
+ ```
20
+
21
+ ## Why TeleFi
22
+
23
+ - **Steer autonomous code from your phone.** An agent or CI job hits a failure; you tap *Retry with
24
+ note*, type "use the 2016-2025 window", and the job receives that note as an event. No laptop, no
25
+ remote session.
26
+ - **One way to notify, from any language.** Producers write a JSON file: no SDK, no network code, no
27
+ bot token in every program. A producer works even while the hub is down (the file waits).
28
+ - **Readable at a glance.** A one-line headline, a one-line "so what", details folded away, links
29
+ as buttons. Every program's messages look the same.
30
+ - **Everything is on record.** Every message, every delivery result, every reply and the whole chat
31
+ transcript, as JSON lines on your disk, rotated before they fill up.
32
+ - **Contracts, not conventions.** Messages, replies, config and command lists are JSON Schemas; a
33
+ new program adopts TeleFi by reading `telefi schema envelope.v1`.
34
+
35
+ ## Features
36
+
37
+ | feature | how |
38
+ |---|---|
39
+ | Routing | config rules by source, kind and severity -> Telegram, a local file, or a mock |
40
+ | Dedup, holds, ack | one delivery per `dedup_key` per window; `not_before` holds a message (a grace period); Ack silences re-alerts for N days |
41
+ | Glance-first format | headline + insight, collapsed details, URL buttons, action buttons |
42
+ | Two-way | buttons, ForceReply prompts for notes, replies to any message, `/commands` |
43
+ | Command registry | each program ships a manifest; `/help`, `/list` and Telegram's `/` menu are built from them |
44
+ | Sinks | `queue` (a folder your program drains) or `command` (a program fed the event on stdin; its last output line is the reply) |
45
+ | Log rotation | each log rotates at `rotate_at` x `max_bytes` (the safety margin), keeping `keep` old files |
46
+ | Safe tests | a mock adapter by default; live sends only with `TELEFI_LIVE=1` |
47
+
48
+ ## Requirements
49
+
50
+ | | |
51
+ |---|---|
52
+ | OS | Windows 11 (verified). Linux/macOS untested. |
53
+ | Python | 3.12 (verified with 3.12.4) |
54
+ | Runtime dependencies | none ([requirements.txt](requirements.txt) says so explicitly) |
55
+ | Test dependencies | pinned in [requirements-dev.txt](requirements-dev.txt) (pytest 8.4.2 and its deps) |
56
+ | Telegram | a bot token from @BotFather and your chat id |
57
+
58
+ ## Try it locally, step by step
59
+
60
+ Each step is self-contained and was run end to end on 2026-10-25. Steps 1-5 need no Telegram.
61
+
62
+ **1. Install and run the tests** (expect all green, one live test skipped)
63
+
64
+ ```bash
65
+ git clone <this repo> telefi && cd telefi
66
+ pip install -e . -r requirements-dev.txt
67
+ python -m pytest
68
+ ```
69
+
70
+ **2. Use a throwaway home** so nothing touches a real install
71
+
72
+ ```bash
73
+ export TELEFI_HOME="$PWD/.try" # PowerShell: $env:TELEFI_HOME = "$PWD\.try"
74
+ ```
75
+
76
+ **3. See every message type exactly as Telegram shows it** (sends nothing)
77
+
78
+ ```bash
79
+ python -m telefi preview
80
+ ```
81
+
82
+ **4. Route the samples through the full pipeline with the mock channel**
83
+
84
+ ```bash
85
+ python -m telefi samples
86
+ # ci.failure [error] -> telegram=ok, file=ok ... mode: mock (nothing sent)
87
+ ```
88
+
89
+ **5. Publish your own message, deliver it, and read the record**
90
+
91
+ ```bash
92
+ python -c "from telefi import envelope, publish; publish(envelope(source='me', kind='info', severity='info', title='hello from TeleFi'))"
93
+ python -m telefi drain # info routes to the local file only
94
+ tail -1 .try/outbox.jsonl # the envelope and each channel's result
95
+ ```
96
+
97
+ **6. Connect Telegram.** Copy `examples/.env.example` to `.env` (git-ignored) and fill it in. Create a bot with [@BotFather](https://t.me/BotFather), send it any message,
98
+ and read your chat id from `https://api.telegram.org/bot<TOKEN>/getUpdates`.
99
+
100
+ | variable | required | meaning |
101
+ |---|---|---|
102
+ | `TELEGRAM_BOT_TOKEN` | yes | the bot token from BotFather |
103
+ | `TELEGRAM_CHAT_ID` | yes | the only chat TeleFi talks to; every other chat is ignored |
104
+ | `TELEFI_HOME` | no | config and records (default `~/.telefi`) |
105
+ | `TELEFI_SPOOL` | no | the drop folder (default `$TELEFI_HOME/spool`) |
106
+
107
+ ```bash
108
+ python -m telefi samples --send # one real message of each type
109
+ ```
110
+
111
+ **7. Turn on replies.** Copy the full example config (every option, documented in
112
+ [docs/CONFIGURATION.md](docs/CONFIGURATION.md)); it enables replies, routes `/ci` commands to
113
+ `examples/echo_sink.py` and lists `examples/commands.example.json`:
114
+
115
+ ```bash
116
+ cp examples/hub.example.json "$TELEFI_HOME/hub.json"
117
+ python -m telefi serve # leave running; one hub per bot
118
+ ```
119
+
120
+ In Telegram: send `/help`, then `/list`, then `/ci retry 7 try again`; the echo sink answers
121
+ `retry received`. Tap *Retry with note* on a sample from step 6, type a note, and see it echoed.
122
+
123
+ **8. Check the records**
124
+
125
+ ```bash
126
+ tail -3 "$TELEFI_HOME/conversation.jsonl" # both directions, in order
127
+ tail -1 "$TELEFI_HOME/inbox.jsonl" # your reply and what the sink returned
128
+ ```
129
+
130
+ ## Run it in the background (optional)
131
+
132
+ The hub is one long-running command, `python -m telefi serve`: run it under whatever you already
133
+ use to keep processes alive. Two ready-made examples:
134
+
135
+ - Windows 11 (verified): `powershell -NoProfile -File examples/windows-service.ps1` registers a
136
+ scheduled task running `pythonw.exe -m telefi serve` (no console window), restarted every 5
137
+ minutes if it stops.
138
+ - Linux (untested): `examples/telefi.service` (systemd user unit).
139
+
140
+ Command sinks run in the hub's working directory, so give them absolute paths (or `cwd`).
141
+
142
+ ## Send from your program
143
+
144
+ ```python
145
+ from telefi import envelope, publish
146
+
147
+ publish(envelope(source="ci", kind="failure", severity="error",
148
+ title="Build #43 failed", insight="Tests passed; the deploy step timed out.",
149
+ body="step deploy: timeout after 600s", dedup_key="ci:build:43",
150
+ actions=[{"id": "retry_note", "label": "Retry with note", "namespace": "ci",
151
+ "input": {"prompt": "What should change?"}}]))
152
+ ```
153
+
154
+ From any other language, write the same JSON to `$TELEFI_HOME/spool/out/<id>.json` atomically
155
+ (write a temp file, then rename): see `examples/publish.mjs` for Node. Replies reach your program
156
+ through a command sink (`examples/echo_sink.py`) or a queue folder (`examples/queue_drain.py`).
157
+
158
+ ## Examples (one per configurable part, all validated by the tests)
159
+
160
+ | file | what it configures |
161
+ |---|---|
162
+ | `examples/.env.example` | secrets and paths |
163
+ | `examples/hub.minimal.json` | send-only hub |
164
+ | `examples/hub.example.json` | every hub option: routes, channels, dedup, logs, replies, sinks |
165
+ | `examples/commands.example.json` | an app's `/commands` |
166
+ | `examples/envelope.example.json` | every message field |
167
+ | `examples/inbound.example.json` | what your program receives |
168
+ | `examples/echo_sink.py`, `examples/queue_drain.py` | the two kinds of reply handler |
169
+ | `examples/publish.mjs` | publishing from Node |
170
+ | `examples/windows-service.ps1`, `examples/telefi.service` | running in the background (optional) |
171
+
172
+ ## Reference
173
+
174
+ - Contracts: `python -m telefi schema envelope.v1 | inbound.v1 | hub_config.v1 | commands.v1`
175
+ - [Configuration reference](docs/CONFIGURATION.md) · [Design](docs/DESIGN.md) · [Similar projects](docs/LANDSCAPE.md)
176
+
177
+ ## License
178
+
179
+ MIT, see [LICENSE](LICENSE).
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "telefi"
7
+ version = "0.3.0"
8
+ description = "Telegram as a two-way control surface for scripts, CI and agents: drop a JSON envelope in a folder, get a readable message, and receive the owner's replies back as events."
9
+ readme = "README.md"
10
+ requires-python = ">=3.12"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Michael Huang" }]
13
+ keywords = ["telegram", "two-way", "chatops", "human-in-the-loop", "notifications", "agents"]
14
+ classifiers = [
15
+ "License :: OSI Approved :: MIT License",
16
+ "Operating System :: Microsoft :: Windows :: Windows 11",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Topic :: Communications :: Chat",
19
+ ]
20
+ # Runtime is pure standard library: no third-party dependencies.
21
+ dependencies = []
22
+
23
+ [project.urls]
24
+ Homepage = "https://github.com/sibulus13/telefi"
25
+ Issues = "https://github.com/sibulus13/telefi/issues"
26
+
27
+ [project.scripts]
28
+ telefi = "telefi.__main__:main"
29
+
30
+ [project.optional-dependencies]
31
+ dev = ["pytest==8.4.2"]
32
+
33
+ [tool.setuptools.packages.find]
34
+ include = ["telefi*"]
35
+
36
+ [tool.setuptools.package-data]
37
+ "telefi" = ["schemas/*.json"]
38
+
39
+ [tool.pytest.ini_options]
40
+ addopts = "-q"
41
+ testpaths = ["tests"]
telefi-0.3.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,9 @@
1
+ """TeleFi: a Telegram adapter layer for two-way messaging; channels and reply sinks are adapters.
2
+
3
+ Producers: `from telefi import envelope, publish` (Python) or write an envelope.v1 JSON file
4
+ into the spool's out/ directory (any language). Design: docs/HUB_DESIGN.md. Format: render.py.
5
+ """
6
+ from telefi.core import drain, envelope, publish, spool_root
7
+ from telefi.render import render_telegram
8
+
9
+ __all__ = ["drain", "envelope", "publish", "render_telegram", "spool_root"]
@@ -0,0 +1,127 @@
1
+ """TeleFi CLI.
2
+
3
+ python -m telefi drain deliver everything in the spool once (the scheduled task)
4
+ python -m telefi serve [--interval 30] drain forever; also long-polls Telegram if inbound.enabled
5
+ python -m telefi preview [type ...] print each sample exactly as the owner would see it (no send)
6
+ python -m telefi samples [--send] route the samples; mock unless --send (then really delivers)
7
+ python -m telefi validate <file.json> check an envelope or inbound event against its schema
8
+ python -m telefi schema <name> print a published schema (envelope.v1 | inbound.v1 | hub_config.v1)
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import json
14
+ import sys
15
+ import tempfile
16
+ import time
17
+ import traceback
18
+ from pathlib import Path
19
+
20
+ from telefi import core, inbound, schema
21
+ from telefi.render import render_plain
22
+ from telefi.samples import samples
23
+
24
+ SERVE_RETRY_S = 5
25
+ SERVE_RETRY_MAX_S = 300
26
+ PRUNE_EVERY_S = 3600
27
+
28
+
29
+ def _preview(names: list[str], buttons: bool) -> int:
30
+ for name, env in samples().items():
31
+ if names and name not in names:
32
+ continue
33
+ print(f"===== {name} (routes to: {', '.join(core.route(env, core.DEFAULT_CONFIG)) or 'nothing'})")
34
+ print(render_plain(env, buttons=buttons))
35
+ print()
36
+ return 0
37
+
38
+
39
+ def _samples(send: bool) -> int:
40
+ cfg = core.load_config()
41
+ if not send:
42
+ cfg = {**cfg, "mode": "mock"}
43
+ with tempfile.TemporaryDirectory() as tmp:
44
+ spool = Path(tmp) / "spool"
45
+ for env in samples().values():
46
+ core.publish(env, spool=spool)
47
+ for rec in core.drain(cfg, spool=spool):
48
+ env = rec["envelope"]
49
+ res = ", ".join(f"{d['channel']}={'ok' if d['ok'] else 'FAILED ' + str(d['error'])}" for d in rec["deliveries"])
50
+ print(f"{env['source']}.{env['kind']} [{env['severity']}] -> {res or 'no route'}")
51
+ print("mode:", "LIVE (really sent)" if send else "mock (nothing sent)")
52
+ return 0
53
+
54
+
55
+ def _serve(interval: float) -> int:
56
+ """Drain + long-poll forever. A failing round is logged to ~/.telefi/hub.log and retried; it
57
+ never ends the process (the scheduled task would only restart it 5 minutes later)."""
58
+ cfg = core.load_config()
59
+ log = core.HOME / "hub.log"
60
+ core.append_jsonl(log, {"at": core.now_iso(), "event": "serve started", "inbound": cfg.get("inbound", {}).get("enabled")})
61
+ if cfg.get("inbound", {}).get("enabled"):
62
+ menu = inbound.register_menu(cfg)
63
+ core.append_jsonl(log, {"at": core.now_iso(), "event": "command menu", "ok": menu["ok"], "error": menu.get("error")})
64
+ failures, pruned_at = 0, 0.0
65
+ while True:
66
+ try:
67
+ core.drain(cfg)
68
+ if time.monotonic() - pruned_at > PRUNE_EVERY_S:
69
+ core.prune_done(core.spool_root(cfg))
70
+ pruned_at = time.monotonic()
71
+ if cfg.get("inbound", {}).get("enabled"):
72
+ inbound.poll_once(cfg, timeout_s=int(interval))
73
+ else:
74
+ time.sleep(interval)
75
+ failures = 0
76
+ except Exception:
77
+ failures += 1
78
+ wait = min(SERVE_RETRY_S * 2 ** (failures - 1), SERVE_RETRY_MAX_S) # an outage never spins
79
+ core.append_jsonl(log, {"at": core.now_iso(), "event": "round failed", "retry_in_s": wait,
80
+ "error": traceback.format_exc()[-2000:]})
81
+ time.sleep(wait)
82
+
83
+
84
+ def main(argv=None) -> int:
85
+ if sys.stdout is not None: # None under pythonw.exe (no console)
86
+ sys.stdout.reconfigure(encoding="utf-8") # Windows consoles default to cp1252
87
+ ap = argparse.ArgumentParser(prog="python -m telefi")
88
+ sub = ap.add_subparsers(dest="cmd", required=True)
89
+ sub.add_parser("drain")
90
+ s = sub.add_parser("serve")
91
+ s.add_argument("--interval", type=float, default=30.0)
92
+ p = sub.add_parser("preview")
93
+ p.add_argument("types", nargs="*")
94
+ p.add_argument("--no-buttons", action="store_true", help="hide action buttons (as sent while inbound is disabled)")
95
+ sm = sub.add_parser("samples")
96
+ sm.add_argument("--send", action="store_true")
97
+ v = sub.add_parser("validate")
98
+ v.add_argument("file")
99
+ sc = sub.add_parser("schema")
100
+ sc.add_argument("name", choices=schema.SCHEMAS)
101
+ a = ap.parse_args(argv)
102
+
103
+ if a.cmd == "drain":
104
+ recs = core.drain(core.load_config())
105
+ failed = [r for r in recs if r.get("rejected") or any(not d["ok"] for d in r["deliveries"])]
106
+ print(f"[hub] delivered {len(recs) - len(failed)}/{len(recs)}", flush=True)
107
+ return 1 if failed else 0
108
+ if a.cmd == "serve":
109
+ return _serve(a.interval)
110
+ if a.cmd == "preview":
111
+ return _preview(a.types, buttons=not a.no_buttons)
112
+ if a.cmd == "samples":
113
+ return _samples(a.send)
114
+ if a.cmd == "validate":
115
+ obj = json.loads(Path(a.file).read_text(encoding="utf-8"))
116
+ name = "inbound.v1" if str(obj.get("schema", "")).endswith(".inbound.v1") else "envelope.v1"
117
+ bad = schema.validate(obj, name)
118
+ print("\n".join(bad) or f"valid {name}")
119
+ return 1 if bad else 0
120
+ if a.cmd == "schema":
121
+ print(json.dumps(schema.load(a.name), indent=2))
122
+ return 0
123
+ return 2
124
+
125
+
126
+ if __name__ == "__main__":
127
+ sys.exit(main())