chartremotely 0.2.5__tar.gz → 0.2.6__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.
Files changed (49) hide show
  1. {chartremotely-0.2.5 → chartremotely-0.2.6}/PKG-INFO +1 -1
  2. {chartremotely-0.2.5 → chartremotely-0.2.6}/chartremotely/assets/shortcut-template.plist +12 -0
  3. {chartremotely-0.2.5 → chartremotely-0.2.6}/chartremotely/forward.py +6 -2
  4. {chartremotely-0.2.5 → chartremotely-0.2.6}/chartremotely.egg-info/PKG-INFO +1 -1
  5. {chartremotely-0.2.5 → chartremotely-0.2.6}/chartremotely.egg-info/SOURCES.txt +1 -0
  6. {chartremotely-0.2.5 → chartremotely-0.2.6}/pyproject.toml +1 -1
  7. {chartremotely-0.2.5 → chartremotely-0.2.6}/tests/test_forward.py +26 -2
  8. {chartremotely-0.2.5 → chartremotely-0.2.6}/tests/test_setup.py +9 -0
  9. chartremotely-0.2.6/tests/test_stamp_changelog.py +75 -0
  10. {chartremotely-0.2.5 → chartremotely-0.2.6}/LICENSE +0 -0
  11. {chartremotely-0.2.5 → chartremotely-0.2.6}/README.md +0 -0
  12. {chartremotely-0.2.5 → chartremotely-0.2.6}/chartremotely/__init__.py +0 -0
  13. {chartremotely-0.2.5 → chartremotely-0.2.6}/chartremotely/answer.py +0 -0
  14. {chartremotely-0.2.5 → chartremotely-0.2.6}/chartremotely/ax.py +0 -0
  15. {chartremotely-0.2.5 → chartremotely-0.2.6}/chartremotely/cli.py +0 -0
  16. {chartremotely-0.2.5 → chartremotely-0.2.6}/chartremotely/config.py +0 -0
  17. {chartremotely-0.2.5 → chartremotely-0.2.6}/chartremotely/doctor.py +0 -0
  18. {chartremotely-0.2.5 → chartremotely-0.2.6}/chartremotely/guilock.py +0 -0
  19. {chartremotely-0.2.5 → chartremotely-0.2.6}/chartremotely/keystore.py +0 -0
  20. {chartremotely-0.2.5 → chartremotely-0.2.6}/chartremotely/layout.py +0 -0
  21. {chartremotely-0.2.5 → chartremotely-0.2.6}/chartremotely/mcpclient.py +0 -0
  22. {chartremotely-0.2.5 → chartremotely-0.2.6}/chartremotely/push.py +0 -0
  23. {chartremotely-0.2.5 → chartremotely-0.2.6}/chartremotely/registry.py +0 -0
  24. {chartremotely-0.2.5 → chartremotely-0.2.6}/chartremotely/relay.py +0 -0
  25. {chartremotely-0.2.5 → chartremotely-0.2.6}/chartremotely/requestlog.py +0 -0
  26. {chartremotely-0.2.5 → chartremotely-0.2.6}/chartremotely/resolve.py +0 -0
  27. {chartremotely-0.2.5 → chartremotely-0.2.6}/chartremotely/scales.py +0 -0
  28. {chartremotely-0.2.5 → chartremotely-0.2.6}/chartremotely/server.py +0 -0
  29. {chartremotely-0.2.5 → chartremotely-0.2.6}/chartremotely/services.py +0 -0
  30. {chartremotely-0.2.5 → chartremotely-0.2.6}/chartremotely/setup.py +0 -0
  31. {chartremotely-0.2.5 → chartremotely-0.2.6}/chartremotely/shortcut.py +0 -0
  32. {chartremotely-0.2.5 → chartremotely-0.2.6}/chartremotely/snapshot.py +0 -0
  33. {chartremotely-0.2.5 → chartremotely-0.2.6}/chartremotely/studies.py +0 -0
  34. {chartremotely-0.2.5 → chartremotely-0.2.6}/chartremotely/symbol.py +0 -0
  35. {chartremotely-0.2.5 → chartremotely-0.2.6}/chartremotely/tailnet.py +0 -0
  36. {chartremotely-0.2.5 → chartremotely-0.2.6}/chartremotely/timeframe.py +0 -0
  37. {chartremotely-0.2.5 → chartremotely-0.2.6}/chartremotely/vocab.py +0 -0
  38. {chartremotely-0.2.5 → chartremotely-0.2.6}/chartremotely/window.py +0 -0
  39. {chartremotely-0.2.5 → chartremotely-0.2.6}/chartremotely.egg-info/dependency_links.txt +0 -0
  40. {chartremotely-0.2.5 → chartremotely-0.2.6}/chartremotely.egg-info/entry_points.txt +0 -0
  41. {chartremotely-0.2.5 → chartremotely-0.2.6}/chartremotely.egg-info/requires.txt +0 -0
  42. {chartremotely-0.2.5 → chartremotely-0.2.6}/chartremotely.egg-info/top_level.txt +0 -0
  43. {chartremotely-0.2.5 → chartremotely-0.2.6}/setup.cfg +0 -0
  44. {chartremotely-0.2.5 → chartremotely-0.2.6}/tests/test_push.py +0 -0
  45. {chartremotely-0.2.5 → chartremotely-0.2.6}/tests/test_relay.py +0 -0
  46. {chartremotely-0.2.5 → chartremotely-0.2.6}/tests/test_requestlog.py +0 -0
  47. {chartremotely-0.2.5 → chartremotely-0.2.6}/tests/test_resolve.py +0 -0
  48. {chartremotely-0.2.5 → chartremotely-0.2.6}/tests/test_snapshot.py +0 -0
  49. {chartremotely-0.2.5 → chartremotely-0.2.6}/tests/test_timeframe.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chartremotely
3
- Version: 0.2.5
3
+ Version: 0.2.6
4
4
  Summary: Remote control of desktop charting applications on macOS
5
5
  Author: Lonnie VanZandt
6
6
  License:
@@ -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,8 @@
1052
1062
  <string>Where?</string>
1053
1063
  <key>WFInputType</key>
1054
1064
  <string>Text</string>
1065
+ <key>WFAskActionAllowsMultilineText</key>
1066
+ <false/>
1055
1067
  </dict>
1056
1068
  </dict>
1057
1069
  <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
- wanted = where.strip()
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"):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chartremotely
3
- Version: 0.2.5
3
+ Version: 0.2.6
4
4
  Summary: Remote control of desktop charting applications on macOS
5
5
  Author: Lonnie VanZandt
6
6
  License:
@@ -43,4 +43,5 @@ tests/test_requestlog.py
43
43
  tests/test_resolve.py
44
44
  tests/test_setup.py
45
45
  tests/test_snapshot.py
46
+ tests/test_stamp_changelog.py
46
47
  tests/test_timeframe.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "chartremotely"
3
- version = "0.2.5"
3
+ version = "0.2.6"
4
4
  description = "Remote control of desktop charting applications on macOS"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -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
- http = HTTPServer(("127.0.0.1", 0), server.Handler)
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
- return response.read().decode().strip()
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()
@@ -176,3 +190,13 @@ def test_the_shortcut_asks_where_after_scale_and_sends_it_beside_cmd():
176
190
  assert fields["cmd"]["string"] == "set  | ", "the vocab grammar is unchanged"
177
191
  where = actions[prompts.index("Where?")]["WFWorkflowActionParameters"]["UUID"]
178
192
  assert fields["where"]["attachmentsByRange"]["{0, 1}"]["OutputUUID"] == where
193
+
194
+
195
+ def test_a_forwarded_command_carries_no_newlines_from_the_shortcut(listener):
196
+ post, ran, pictures = listener
197
+ # The Shortcut splices this Mac's own replies ("PLTR\n", "half\n") into the command.
198
+ post({"cmd": "set PLTR\n | half\n", "where": "Office\n"})
199
+ _, sent = StubOperator.seen[-1]
200
+ assert sent["cmd"] == "set PLTR | half"
201
+ assert sent["display"] == "Office"
202
+ assert ran == [] and pictures == []
@@ -43,6 +43,15 @@ 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
+
46
55
  def test_filling_the_template_changes_the_placeholders_and_nothing_else():
47
56
  raw = shortcut.template()
48
57
  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