chartremotely 0.3.9__tar.gz → 0.3.11__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 (68) hide show
  1. {chartremotely-0.3.9 → chartremotely-0.3.11}/PKG-INFO +2 -2
  2. {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/doctor.py +55 -10
  3. {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely.egg-info/PKG-INFO +2 -2
  4. {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely.egg-info/requires.txt +1 -1
  5. {chartremotely-0.3.9 → chartremotely-0.3.11}/pyproject.toml +2 -2
  6. {chartremotely-0.3.9 → chartremotely-0.3.11}/tests/test_doctor.py +48 -0
  7. {chartremotely-0.3.9 → chartremotely-0.3.11}/LICENSE +0 -0
  8. {chartremotely-0.3.9 → chartremotely-0.3.11}/README.md +0 -0
  9. {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/__init__.py +0 -0
  10. {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/answer.py +0 -0
  11. {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/assets/shortcut.plist +0 -0
  12. {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/ax.py +0 -0
  13. {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/cli.py +0 -0
  14. {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/config.py +0 -0
  15. {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/displays.py +0 -0
  16. {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/guilock.py +0 -0
  17. {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/hearing.py +0 -0
  18. {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/holder.py +0 -0
  19. {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/keystore.py +0 -0
  20. {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/layout.py +0 -0
  21. {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/mcpclient.py +0 -0
  22. {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/patron.py +0 -0
  23. {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/popups.py +0 -0
  24. {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/push.py +0 -0
  25. {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/recent.py +0 -0
  26. {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/registry.py +0 -0
  27. {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/relay.py +0 -0
  28. {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/requestlog.py +0 -0
  29. {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/resolve.py +0 -0
  30. {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/scales.py +0 -0
  31. {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/screen.py +0 -0
  32. {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/server.py +0 -0
  33. {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/services.py +0 -0
  34. {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/setup.py +0 -0
  35. {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/shortcut.py +0 -0
  36. {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/snapshot.py +0 -0
  37. {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/studies.py +0 -0
  38. {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/symbol.py +0 -0
  39. {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/tailnet.py +0 -0
  40. {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/terminal.py +0 -0
  41. {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/timeframe.py +0 -0
  42. {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/understand.py +0 -0
  43. {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/vocab.py +0 -0
  44. {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/voice.py +0 -0
  45. {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/window.py +0 -0
  46. {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely.egg-info/SOURCES.txt +0 -0
  47. {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely.egg-info/dependency_links.txt +0 -0
  48. {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely.egg-info/entry_points.txt +0 -0
  49. {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely.egg-info/top_level.txt +0 -0
  50. {chartremotely-0.3.9 → chartremotely-0.3.11}/setup.cfg +0 -0
  51. {chartremotely-0.3.9 → chartremotely-0.3.11}/tests/test_changelog.py +0 -0
  52. {chartremotely-0.3.9 → chartremotely-0.3.11}/tests/test_hearing.py +0 -0
  53. {chartremotely-0.3.9 → chartremotely-0.3.11}/tests/test_patron.py +0 -0
  54. {chartremotely-0.3.9 → chartremotely-0.3.11}/tests/test_popups.py +0 -0
  55. {chartremotely-0.3.9 → chartremotely-0.3.11}/tests/test_push.py +0 -0
  56. {chartremotely-0.3.9 → chartremotely-0.3.11}/tests/test_recent.py +0 -0
  57. {chartremotely-0.3.9 → chartremotely-0.3.11}/tests/test_registry.py +0 -0
  58. {chartremotely-0.3.9 → chartremotely-0.3.11}/tests/test_relay.py +0 -0
  59. {chartremotely-0.3.9 → chartremotely-0.3.11}/tests/test_requestlog.py +0 -0
  60. {chartremotely-0.3.9 → chartremotely-0.3.11}/tests/test_resolve.py +0 -0
  61. {chartremotely-0.3.9 → chartremotely-0.3.11}/tests/test_scales.py +0 -0
  62. {chartremotely-0.3.9 → chartremotely-0.3.11}/tests/test_screen.py +0 -0
  63. {chartremotely-0.3.9 → chartremotely-0.3.11}/tests/test_setup.py +0 -0
  64. {chartremotely-0.3.9 → chartremotely-0.3.11}/tests/test_snapshot.py +0 -0
  65. {chartremotely-0.3.9 → chartremotely-0.3.11}/tests/test_stamp_changelog.py +0 -0
  66. {chartremotely-0.3.9 → chartremotely-0.3.11}/tests/test_timeframe.py +0 -0
  67. {chartremotely-0.3.9 → chartremotely-0.3.11}/tests/test_understand.py +0 -0
  68. {chartremotely-0.3.9 → chartremotely-0.3.11}/tests/test_voice.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chartremotely
3
- Version: 0.3.9
3
+ Version: 0.3.11
4
4
  Summary: Remote control of desktop charting applications on macOS
5
5
  Author: Lonnie VanZandt
6
6
  License:
@@ -219,7 +219,7 @@ Requires-Dist: pyobjc-framework-Quartz>=10; extra == "macos"
219
219
  Requires-Dist: pyobjc-framework-Security>=10; extra == "macos"
220
220
  Requires-Dist: mlx-whisper>=0.4.3; (sys_platform == "darwin" and platform_machine == "arm64") and extra == "macos"
221
221
  Provides-Extra: setup
222
- Requires-Dist: tollbooth-dpyc[nostr]==0.106.0; extra == "setup"
222
+ Requires-Dist: tollbooth-dpyc[nostr]==0.109.3; extra == "setup"
223
223
  Provides-Extra: dev
224
224
  Requires-Dist: pytest>=8; extra == "dev"
225
225
  Requires-Dist: ruff>=0.6; extra == "dev"
@@ -75,7 +75,25 @@ def _check_services() -> tuple[str, str]:
75
75
  return service_verdict({name: services.running_python(name) for name in services.LABELS})
76
76
 
77
77
 
78
- def _check_controls() -> tuple[str, str]:
78
+ def screen_locked() -> bool:
79
+ """True while the login session's screen is locked.
80
+
81
+ A locked screen hides every window from the accessibility layer, so the
82
+ chart's controls look missing when they are only out of reach.
83
+ """
84
+ import Quartz
85
+
86
+ session = Quartz.CGSessionCopyCurrentDictionary() or {}
87
+ return bool(session.get("CGSSessionScreenIsLocked"))
88
+
89
+
90
+ LOCKED = (BAD, ("the screen is locked, so the chart controls cannot be read - "
91
+ "unlock this Mac; until then it cannot change the chart"))
92
+
93
+
94
+ def _check_controls(locked=screen_locked) -> tuple[str, str]:
95
+ if locked():
96
+ return LOCKED
79
97
  from . import ax, symbol, timeframe
80
98
  cfg = config.load()
81
99
  try:
@@ -106,20 +124,47 @@ def _check_registry() -> tuple[str, str]:
106
124
  return OK, f"{len(rows)} securities cached"
107
125
 
108
126
 
127
+ #: A free lookup the listener answers from a table: no operator trip, no
128
+ #: fare, and the chart stays where it is. "read" was all three.
129
+ LISTENER_PROBE = b"scale half"
130
+ LISTENER_SECONDS = 5
131
+
132
+
133
+ def _post_probe(port: int, token: str) -> str:
134
+ request = urllib.request.Request(
135
+ f"http://127.0.0.1:{port}/chart", data=LISTENER_PROBE,
136
+ headers={"X-Token": token}, method="POST")
137
+ with urllib.request.urlopen(request, timeout=LISTENER_SECONDS) as response:
138
+ return response.read().decode().strip()
139
+
140
+
141
+ def listener_verdict(port: int, token: str, post=_post_probe) -> tuple[str, str]:
142
+ """Ask the listener a free table lookup and judge the answer."""
143
+ try:
144
+ reply = post(port, token)
145
+ except urllib.error.HTTPError as exc:
146
+ if exc.code == 403:
147
+ return BAD, f"listener on :{port} refused the token in config.json - restart it: chartremotely setup"
148
+ return BAD, f"listener on :{port} answered HTTP {exc.code}"
149
+ except urllib.error.URLError as exc:
150
+ if isinstance(exc.reason, TimeoutError):
151
+ return BAD, (f"listener on :{port} took over {LISTENER_SECONDS} s to answer a table lookup - "
152
+ "restart it: chartremotely setup")
153
+ return WARN, f"nothing listening on :{port} - run: chartremotely serve"
154
+ except TimeoutError:
155
+ return BAD, (f"listener on :{port} took over {LISTENER_SECONDS} s to answer a table lookup - "
156
+ "restart it: chartremotely setup")
157
+ if reply != "half":
158
+ return BAD, f"listener on :{port} answered {reply[:60]!r} to a table lookup"
159
+ return OK, f"listener answering on :{port}"
160
+
161
+
109
162
  def _check_listener() -> tuple[str, str]:
110
163
  cfg = config.load()
111
- port = cfg["port"]
112
164
  token = cfg.get("token")
113
165
  if not token:
114
166
  return WARN, "no token yet - it is generated when the listener first starts"
115
- request = urllib.request.Request(
116
- f"http://127.0.0.1:{port}/chart", data=b"read",
117
- headers={"X-Token": token}, method="POST")
118
- try:
119
- with urllib.request.urlopen(request, timeout=5) as response:
120
- return OK, f"listener answering on :{port} - {response.read().decode().strip()}"
121
- except urllib.error.URLError:
122
- return WARN, f"nothing listening on :{port} - run: chartremotely serve"
167
+ return listener_verdict(cfg["port"], token)
123
168
 
124
169
 
125
170
  CHECKS = (
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chartremotely
3
- Version: 0.3.9
3
+ Version: 0.3.11
4
4
  Summary: Remote control of desktop charting applications on macOS
5
5
  Author: Lonnie VanZandt
6
6
  License:
@@ -219,7 +219,7 @@ Requires-Dist: pyobjc-framework-Quartz>=10; extra == "macos"
219
219
  Requires-Dist: pyobjc-framework-Security>=10; extra == "macos"
220
220
  Requires-Dist: mlx-whisper>=0.4.3; (sys_platform == "darwin" and platform_machine == "arm64") and extra == "macos"
221
221
  Provides-Extra: setup
222
- Requires-Dist: tollbooth-dpyc[nostr]==0.106.0; extra == "setup"
222
+ Requires-Dist: tollbooth-dpyc[nostr]==0.109.3; extra == "setup"
223
223
  Provides-Extra: dev
224
224
  Requires-Dist: pytest>=8; extra == "dev"
225
225
  Requires-Dist: ruff>=0.6; extra == "dev"
@@ -13,4 +13,4 @@ pyobjc-framework-Security>=10
13
13
  mlx-whisper>=0.4.3
14
14
 
15
15
  [setup]
16
- tollbooth-dpyc[nostr]==0.106.0
16
+ tollbooth-dpyc[nostr]==0.109.3
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "chartremotely"
3
- version = "0.3.9"
3
+ version = "0.3.11"
4
4
  description = "Remote control of desktop charting applications on macOS"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -25,7 +25,7 @@ macos = [
25
25
  ]
26
26
  # `chartremotely setup` makes and proves Nostr identities. The key and proof
27
27
  # code is the tollbooth-dpyc SDK's own, never rewritten here.
28
- setup = ["tollbooth-dpyc[nostr]==0.106.0"]
28
+ setup = ["tollbooth-dpyc[nostr]==0.109.3"]
29
29
  dev = ["pytest>=8", "ruff>=0.6"]
30
30
 
31
31
  [project.urls]
@@ -40,3 +40,51 @@ def test_both_running_on_a_live_python_is_ok():
40
40
  def test_launchctl_print_yields_the_pid_only_while_running():
41
41
  assert services.pid_in("com.chartremotely.relay = {\n\tstate = running\n\tpid = 12804\n}") == 12804
42
42
  assert services.pid_in("com.chartremotely.relay = {\n\tstate = not running\n}") is None
43
+
44
+
45
+ # -- the listener: a free table lookup, never a paid chart read ----------------------------
46
+
47
+ import urllib.error
48
+
49
+
50
+ def test_the_listener_probe_is_a_free_lookup_never_read():
51
+ # server.LOOKUPS needs Quartz to import; the verb is pinned here instead.
52
+ assert doctor.LISTENER_PROBE.split()[0] == b"scale"
53
+
54
+
55
+ def test_a_listener_that_answers_the_lookup_is_ok():
56
+ assert doctor.listener_verdict(8899, "t", post=lambda port, token: "half")[0] == doctor.OK
57
+
58
+
59
+ def _raises(exc):
60
+ def post(port, token):
61
+ raise exc
62
+ return post
63
+
64
+
65
+ def test_a_slow_listener_fails_and_says_so():
66
+ for exc in (TimeoutError("timed out"), urllib.error.URLError(TimeoutError("timed out"))):
67
+ status, detail = doctor.listener_verdict(8899, "t", post=_raises(exc))
68
+ assert status == doctor.BAD and "took over" in detail
69
+
70
+
71
+ def test_nothing_listening_points_at_serve():
72
+ status, detail = doctor.listener_verdict(8899, "t", post=_raises(urllib.error.URLError(ConnectionRefusedError())))
73
+ assert status == doctor.WARN and "chartremotely serve" in detail
74
+
75
+
76
+ def test_a_refused_token_is_named():
77
+ refused = urllib.error.HTTPError("http://127.0.0.1:8899/chart", 403, "Forbidden", {}, None)
78
+ status, detail = doctor.listener_verdict(8899, "t", post=_raises(refused))
79
+ assert status == doctor.BAD and "token" in detail
80
+
81
+
82
+ def test_a_wrong_answer_fails():
83
+ assert doctor.listener_verdict(8899, "t", post=lambda port, token: "ERR nope")[0] == doctor.BAD
84
+
85
+
86
+ # -- chart controls: a locked screen is named, not mistaken for a missing chart -----------
87
+
88
+ def test_a_locked_screen_is_named_before_looking_for_controls():
89
+ status, detail = doctor._check_controls(locked=lambda: True)
90
+ assert status == doctor.BAD and "locked" in detail and "unlock" in detail
File without changes
File without changes
File without changes