chartremotely 0.2.5__tar.gz → 0.2.7__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.
- {chartremotely-0.2.5 → chartremotely-0.2.7}/PKG-INFO +1 -1
- {chartremotely-0.2.5 → chartremotely-0.2.7}/chartremotely/assets/shortcut-template.plist +23 -0
- {chartremotely-0.2.5 → chartremotely-0.2.7}/chartremotely/forward.py +6 -2
- {chartremotely-0.2.5 → chartremotely-0.2.7}/chartremotely.egg-info/PKG-INFO +1 -1
- {chartremotely-0.2.5 → chartremotely-0.2.7}/chartremotely.egg-info/SOURCES.txt +1 -0
- {chartremotely-0.2.5 → chartremotely-0.2.7}/pyproject.toml +1 -1
- {chartremotely-0.2.5 → chartremotely-0.2.7}/tests/test_forward.py +28 -3
- {chartremotely-0.2.5 → chartremotely-0.2.7}/tests/test_setup.py +20 -0
- chartremotely-0.2.7/tests/test_stamp_changelog.py +75 -0
- {chartremotely-0.2.5 → chartremotely-0.2.7}/LICENSE +0 -0
- {chartremotely-0.2.5 → chartremotely-0.2.7}/README.md +0 -0
- {chartremotely-0.2.5 → chartremotely-0.2.7}/chartremotely/__init__.py +0 -0
- {chartremotely-0.2.5 → chartremotely-0.2.7}/chartremotely/answer.py +0 -0
- {chartremotely-0.2.5 → chartremotely-0.2.7}/chartremotely/ax.py +0 -0
- {chartremotely-0.2.5 → chartremotely-0.2.7}/chartremotely/cli.py +0 -0
- {chartremotely-0.2.5 → chartremotely-0.2.7}/chartremotely/config.py +0 -0
- {chartremotely-0.2.5 → chartremotely-0.2.7}/chartremotely/doctor.py +0 -0
- {chartremotely-0.2.5 → chartremotely-0.2.7}/chartremotely/guilock.py +0 -0
- {chartremotely-0.2.5 → chartremotely-0.2.7}/chartremotely/keystore.py +0 -0
- {chartremotely-0.2.5 → chartremotely-0.2.7}/chartremotely/layout.py +0 -0
- {chartremotely-0.2.5 → chartremotely-0.2.7}/chartremotely/mcpclient.py +0 -0
- {chartremotely-0.2.5 → chartremotely-0.2.7}/chartremotely/push.py +0 -0
- {chartremotely-0.2.5 → chartremotely-0.2.7}/chartremotely/registry.py +0 -0
- {chartremotely-0.2.5 → chartremotely-0.2.7}/chartremotely/relay.py +0 -0
- {chartremotely-0.2.5 → chartremotely-0.2.7}/chartremotely/requestlog.py +0 -0
- {chartremotely-0.2.5 → chartremotely-0.2.7}/chartremotely/resolve.py +0 -0
- {chartremotely-0.2.5 → chartremotely-0.2.7}/chartremotely/scales.py +0 -0
- {chartremotely-0.2.5 → chartremotely-0.2.7}/chartremotely/server.py +0 -0
- {chartremotely-0.2.5 → chartremotely-0.2.7}/chartremotely/services.py +0 -0
- {chartremotely-0.2.5 → chartremotely-0.2.7}/chartremotely/setup.py +0 -0
- {chartremotely-0.2.5 → chartremotely-0.2.7}/chartremotely/shortcut.py +0 -0
- {chartremotely-0.2.5 → chartremotely-0.2.7}/chartremotely/snapshot.py +0 -0
- {chartremotely-0.2.5 → chartremotely-0.2.7}/chartremotely/studies.py +0 -0
- {chartremotely-0.2.5 → chartremotely-0.2.7}/chartremotely/symbol.py +0 -0
- {chartremotely-0.2.5 → chartremotely-0.2.7}/chartremotely/tailnet.py +0 -0
- {chartremotely-0.2.5 → chartremotely-0.2.7}/chartremotely/timeframe.py +0 -0
- {chartremotely-0.2.5 → chartremotely-0.2.7}/chartremotely/vocab.py +0 -0
- {chartremotely-0.2.5 → chartremotely-0.2.7}/chartremotely/window.py +0 -0
- {chartremotely-0.2.5 → chartremotely-0.2.7}/chartremotely.egg-info/dependency_links.txt +0 -0
- {chartremotely-0.2.5 → chartremotely-0.2.7}/chartremotely.egg-info/entry_points.txt +0 -0
- {chartremotely-0.2.5 → chartremotely-0.2.7}/chartremotely.egg-info/requires.txt +0 -0
- {chartremotely-0.2.5 → chartremotely-0.2.7}/chartremotely.egg-info/top_level.txt +0 -0
- {chartremotely-0.2.5 → chartremotely-0.2.7}/setup.cfg +0 -0
- {chartremotely-0.2.5 → chartremotely-0.2.7}/tests/test_push.py +0 -0
- {chartremotely-0.2.5 → chartremotely-0.2.7}/tests/test_relay.py +0 -0
- {chartremotely-0.2.5 → chartremotely-0.2.7}/tests/test_requestlog.py +0 -0
- {chartremotely-0.2.5 → chartremotely-0.2.7}/tests/test_resolve.py +0 -0
- {chartremotely-0.2.5 → chartremotely-0.2.7}/tests/test_snapshot.py +0 -0
- {chartremotely-0.2.5 → chartremotely-0.2.7}/tests/test_timeframe.py +0 -0
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
<string>Which company?</string>
|
|
18
18
|
<key>WFInputType</key>
|
|
19
19
|
<string>Text</string>
|
|
20
|
+
<key>WFAskActionAllowsMultilineText</key>
|
|
21
|
+
<false/>
|
|
20
22
|
</dict>
|
|
21
23
|
</dict>
|
|
22
24
|
<dict>
|
|
@@ -231,6 +233,8 @@
|
|
|
231
233
|
<string>Which company?</string>
|
|
232
234
|
<key>WFInputType</key>
|
|
233
235
|
<string>Text</string>
|
|
236
|
+
<key>WFAskActionAllowsMultilineText</key>
|
|
237
|
+
<false/>
|
|
234
238
|
</dict>
|
|
235
239
|
</dict>
|
|
236
240
|
<dict>
|
|
@@ -425,6 +429,8 @@
|
|
|
425
429
|
<string>Spell the symbol</string>
|
|
426
430
|
<key>WFInputType</key>
|
|
427
431
|
<string>Text</string>
|
|
432
|
+
<key>WFAskActionAllowsMultilineText</key>
|
|
433
|
+
<false/>
|
|
428
434
|
</dict>
|
|
429
435
|
</dict>
|
|
430
436
|
<dict>
|
|
@@ -753,6 +759,8 @@
|
|
|
753
759
|
<string>What scale?</string>
|
|
754
760
|
<key>WFInputType</key>
|
|
755
761
|
<string>Text</string>
|
|
762
|
+
<key>WFAskActionAllowsMultilineText</key>
|
|
763
|
+
<false/>
|
|
756
764
|
</dict>
|
|
757
765
|
</dict>
|
|
758
766
|
<dict>
|
|
@@ -967,6 +975,8 @@
|
|
|
967
975
|
<string>Which timeframe?</string>
|
|
968
976
|
<key>WFInputType</key>
|
|
969
977
|
<string>Text</string>
|
|
978
|
+
<key>WFAskActionAllowsMultilineText</key>
|
|
979
|
+
<false/>
|
|
970
980
|
</dict>
|
|
971
981
|
</dict>
|
|
972
982
|
<dict>
|
|
@@ -1052,6 +1062,19 @@
|
|
|
1052
1062
|
<string>Where?</string>
|
|
1053
1063
|
<key>WFInputType</key>
|
|
1054
1064
|
<string>Text</string>
|
|
1065
|
+
<key>WFAskActionAllowsMultilineText</key>
|
|
1066
|
+
<false/>
|
|
1067
|
+
</dict>
|
|
1068
|
+
</dict>
|
|
1069
|
+
<dict>
|
|
1070
|
+
<key>WFWorkflowActionIdentifier</key>
|
|
1071
|
+
<string>is.workflow.actions.speaktext</string>
|
|
1072
|
+
<key>WFWorkflowActionParameters</key>
|
|
1073
|
+
<dict>
|
|
1074
|
+
<key>WFSpeakTextWait</key>
|
|
1075
|
+
<false/>
|
|
1076
|
+
<key>WFText</key>
|
|
1077
|
+
<string>On it, requesting your chart now. Look at your screen or visit the website for the screen capture.</string>
|
|
1055
1078
|
</dict>
|
|
1056
1079
|
</dict>
|
|
1057
1080
|
<dict>
|
|
@@ -57,11 +57,15 @@ def route(command: str, where: str, cfg: dict | None = None) -> str | None:
|
|
|
57
57
|
agent_id, secret = cfg.get("agent_id"), cfg.get("agent_secret")
|
|
58
58
|
if not (base and agent_id and secret):
|
|
59
59
|
return ERR + "This Mac is not paired, so it can only drive its own chart."
|
|
60
|
-
|
|
60
|
+
# The Shortcut builds the command from this Mac's own replies, and every
|
|
61
|
+
# reply ends in a newline ("PLTR\n"), so a forwarded command arrives as
|
|
62
|
+
# "set PLTR\n | half\n". Run here, vocab strips that; the operator rightly
|
|
63
|
+
# refuses anything unprintable. Collapse the whitespace before it leaves.
|
|
64
|
+
wanted = " ".join(where.split())
|
|
61
65
|
try:
|
|
62
66
|
status, body = _post(f"{base}/agent/forward",
|
|
63
67
|
{"agent_id": agent_id, "secret": secret,
|
|
64
|
-
"display": wanted, "cmd": command})
|
|
68
|
+
"display": wanted, "cmd": " ".join(command.split())})
|
|
65
69
|
except (urllib.error.URLError, TimeoutError, OSError):
|
|
66
70
|
return ERR + "I could not reach the operator."
|
|
67
71
|
if status == 200 and body.get("self"):
|
|
@@ -67,15 +67,29 @@ def listener(operator, monkeypatch):
|
|
|
67
67
|
from chartremotely import push
|
|
68
68
|
monkeypatch.setattr(push, "after_reply", lambda *a: pictures.append(a))
|
|
69
69
|
server.Handler.token = "T0KEN"
|
|
70
|
-
|
|
70
|
+
handled = threading.Event()
|
|
71
|
+
|
|
72
|
+
class Finishing(server.Handler):
|
|
73
|
+
# The reply goes out before the picture is scheduled, so a test that
|
|
74
|
+
# checks pictures must wait for the whole request, not just its reply.
|
|
75
|
+
def handle(self):
|
|
76
|
+
try:
|
|
77
|
+
super().handle()
|
|
78
|
+
finally:
|
|
79
|
+
handled.set()
|
|
80
|
+
|
|
81
|
+
http = HTTPServer(("127.0.0.1", 0), Finishing)
|
|
71
82
|
threading.Thread(target=http.serve_forever, daemon=True).start()
|
|
72
83
|
|
|
73
84
|
def post(body):
|
|
85
|
+
handled.clear()
|
|
74
86
|
request = urllib.request.Request(
|
|
75
87
|
f"http://127.0.0.1:{http.server_address[1]}/chart", data=json.dumps(body).encode(),
|
|
76
88
|
method="POST", headers={"Content-Type": "application/json", "X-Token": "T0KEN"})
|
|
77
89
|
with urllib.request.urlopen(request, timeout=10) as response:
|
|
78
|
-
|
|
90
|
+
reply = response.read().decode().strip()
|
|
91
|
+
assert handled.wait(5), "the listener never finished the request"
|
|
92
|
+
return reply
|
|
79
93
|
|
|
80
94
|
yield post, ran, pictures
|
|
81
95
|
http.shutdown()
|
|
@@ -170,9 +184,20 @@ def test_the_shortcut_asks_where_after_scale_and_sends_it_beside_cmd():
|
|
|
170
184
|
actions = plistlib.loads(shortcut.template())["WFWorkflowActions"]
|
|
171
185
|
prompts = [a["WFWorkflowActionParameters"].get("WFAskActionPrompt") for a in actions]
|
|
172
186
|
assert prompts.index("Where?") > prompts.index("What scale?")
|
|
173
|
-
final = actions[prompts.index("Where?")
|
|
187
|
+
final = next(a for a in actions[prompts.index("Where?"):]
|
|
188
|
+
if a["WFWorkflowActionIdentifier"].endswith(".downloadurl"))["WFWorkflowActionParameters"]
|
|
174
189
|
fields = {i["WFKey"]["Value"]["string"]: i["WFValue"]["Value"]
|
|
175
190
|
for i in final["WFJSONValues"]["Value"]["WFDictionaryFieldValueItems"]}
|
|
176
191
|
assert fields["cmd"]["string"] == "set  | ", "the vocab grammar is unchanged"
|
|
177
192
|
where = actions[prompts.index("Where?")]["WFWorkflowActionParameters"]["UUID"]
|
|
178
193
|
assert fields["where"]["attachmentsByRange"]["{0, 1}"]["OutputUUID"] == where
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def test_a_forwarded_command_carries_no_newlines_from_the_shortcut(listener):
|
|
197
|
+
post, ran, pictures = listener
|
|
198
|
+
# The Shortcut splices this Mac's own replies ("PLTR\n", "half\n") into the command.
|
|
199
|
+
post({"cmd": "set PLTR\n | half\n", "where": "Office\n"})
|
|
200
|
+
_, sent = StubOperator.seen[-1]
|
|
201
|
+
assert sent["cmd"] == "set PLTR | half"
|
|
202
|
+
assert sent["display"] == "Office"
|
|
203
|
+
assert ran == [] and pictures == []
|
|
@@ -43,6 +43,26 @@ def test_the_template_carries_placeholders_and_no_ones_address_or_token():
|
|
|
43
43
|
assert not re.search(r"<string>[A-Za-z0-9_-]{40,}</string>", raw)
|
|
44
44
|
|
|
45
45
|
|
|
46
|
+
def test_every_question_takes_one_line_so_return_answers_it():
|
|
47
|
+
actions = plistlib.loads(shortcut.template())["WFWorkflowActions"]
|
|
48
|
+
asks = [a["WFWorkflowActionParameters"] for a in actions
|
|
49
|
+
if a["WFWorkflowActionIdentifier"].endswith(".ask")]
|
|
50
|
+
assert asks, "the template asks nothing"
|
|
51
|
+
for ask in asks:
|
|
52
|
+
assert ask.get("WFAskActionAllowsMultilineText") is False, ask.get("WFAskActionPrompt")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_the_shortcut_says_it_is_on_it_while_the_request_goes_out():
|
|
56
|
+
actions = plistlib.loads(shortcut.template())["WFWorkflowActions"]
|
|
57
|
+
kinds = [a["WFWorkflowActionIdentifier"].rsplit(".", 1)[-1] for a in actions]
|
|
58
|
+
where = next(i for i, a in enumerate(actions)
|
|
59
|
+
if a["WFWorkflowActionParameters"].get("CustomOutputName") == "Where")
|
|
60
|
+
assert kinds[where + 1:where + 3] == ["speaktext", "downloadurl"]
|
|
61
|
+
ack = actions[where + 1]["WFWorkflowActionParameters"]
|
|
62
|
+
assert ack["WFSpeakTextWait"] is False, "waiting for the words would delay the chart"
|
|
63
|
+
assert ack["WFText"].startswith("On it, requesting your chart now.")
|
|
64
|
+
|
|
65
|
+
|
|
46
66
|
def test_filling_the_template_changes_the_placeholders_and_nothing_else():
|
|
47
67
|
raw = shortcut.template()
|
|
48
68
|
filled = shortcut.fill(raw, "https://mac.example.ts.net/chart", "T0KEN")
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""The CHANGELOG stamp the Release workflow runs after an automatic release."""
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
_SCRIPT = Path(__file__).resolve().parents[1] / ".github" / "scripts" / "stamp_changelog.py"
|
|
7
|
+
_spec = importlib.util.spec_from_file_location("stamp_changelog", _SCRIPT)
|
|
8
|
+
stamp_changelog = importlib.util.module_from_spec(_spec)
|
|
9
|
+
_spec.loader.exec_module(stamp_changelog)
|
|
10
|
+
|
|
11
|
+
HEAD = "# Changelog\n\nIntro.\n\n"
|
|
12
|
+
OLD = "## [0.1.0] - 2026-01-01\n\n### Added\n- First.\n"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _log(unreleased: str) -> str:
|
|
16
|
+
return f"{HEAD}## [Unreleased]\n{unreleased}\n{OLD}"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _release(version, text, previous=""):
|
|
20
|
+
items = stamp_changelog.added(
|
|
21
|
+
stamp_changelog.unreleased_entries(text), stamp_changelog.unreleased_entries(previous)
|
|
22
|
+
)
|
|
23
|
+
return (version, "2026-02-02", items)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_moves_shipped_lines_under_a_dated_section_keeping_subsections():
|
|
27
|
+
text = _log("\n### Fixed\n- A fix.\n\n### Added\n- A thing,\n wrapped.\n")
|
|
28
|
+
out = stamp_changelog.stamp(text, [_release("0.1.1", text)])
|
|
29
|
+
assert out == (
|
|
30
|
+
f"{HEAD}## [Unreleased]\n\n"
|
|
31
|
+
"## [0.1.1] - 2026-02-02\n\n### Fixed\n- A fix.\n\n### Added\n- A thing,\n wrapped.\n\n"
|
|
32
|
+
f"{OLD}"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_a_second_run_changes_nothing():
|
|
37
|
+
text = _log("\n### Fixed\n- A fix.\n")
|
|
38
|
+
once = stamp_changelog.stamp(text, [_release("0.1.1", text)])
|
|
39
|
+
assert stamp_changelog.stamp(once, [_release("0.1.1", text)]) == once
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_empty_unreleased_is_left_alone():
|
|
43
|
+
text = _log("")
|
|
44
|
+
assert stamp_changelog.stamp(text, [_release("0.1.1", text)]) == text
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_each_release_takes_only_what_it_added_and_later_lines_stay():
|
|
48
|
+
at_first = _log("\n### Changed\n- One.\n")
|
|
49
|
+
at_second = _log("\n### Changed\n- One.\n- Two.\n")
|
|
50
|
+
main = _log("\n### Fixed\n- Three.\n\n### Changed\n- One.\n- Two.\n")
|
|
51
|
+
out = stamp_changelog.stamp(
|
|
52
|
+
main, [_release("0.1.1", at_first), _release("0.1.2", at_second, at_first)]
|
|
53
|
+
)
|
|
54
|
+
assert out == (
|
|
55
|
+
f"{HEAD}## [Unreleased]\n\n### Fixed\n- Three.\n\n"
|
|
56
|
+
"## [0.1.2] - 2026-02-02\n\n### Changed\n- Two.\n\n"
|
|
57
|
+
"## [0.1.1] - 2026-02-02\n\n### Changed\n- One.\n\n"
|
|
58
|
+
f"{OLD}"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_repeated_subsections_merge_and_reworded_lines_stay_unreleased():
|
|
63
|
+
at_release = _log("\n### Changed\n- A.\n\n### Changed\n- B.\n- Old wording.\n")
|
|
64
|
+
main = _log("\n### Changed\n- A.\n\n### Changed\n- B.\n- New wording.\n")
|
|
65
|
+
out = stamp_changelog.stamp(main, [_release("0.1.1", at_release)])
|
|
66
|
+
assert out == (
|
|
67
|
+
f"{HEAD}## [Unreleased]\n\n### Changed\n- New wording.\n\n"
|
|
68
|
+
"## [0.1.1] - 2026-02-02\n\n### Changed\n- A.\n- B.\n\n"
|
|
69
|
+
f"{OLD}"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_a_version_that_already_has_a_section_is_skipped():
|
|
74
|
+
text = _log("\n### Fixed\n- A fix.\n")
|
|
75
|
+
assert stamp_changelog.stamp(text, [_release("0.1.0", text)]) == text
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|