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.
- {chartremotely-0.3.9 → chartremotely-0.3.11}/PKG-INFO +2 -2
- {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/doctor.py +55 -10
- {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely.egg-info/PKG-INFO +2 -2
- {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely.egg-info/requires.txt +1 -1
- {chartremotely-0.3.9 → chartremotely-0.3.11}/pyproject.toml +2 -2
- {chartremotely-0.3.9 → chartremotely-0.3.11}/tests/test_doctor.py +48 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/LICENSE +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/README.md +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/__init__.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/answer.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/assets/shortcut.plist +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/ax.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/cli.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/config.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/displays.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/guilock.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/hearing.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/holder.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/keystore.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/layout.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/mcpclient.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/patron.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/popups.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/push.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/recent.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/registry.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/relay.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/requestlog.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/resolve.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/scales.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/screen.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/server.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/services.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/setup.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/shortcut.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/snapshot.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/studies.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/symbol.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/tailnet.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/terminal.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/timeframe.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/understand.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/vocab.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/voice.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely/window.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely.egg-info/SOURCES.txt +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely.egg-info/dependency_links.txt +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely.egg-info/entry_points.txt +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/chartremotely.egg-info/top_level.txt +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/setup.cfg +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/tests/test_changelog.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/tests/test_hearing.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/tests/test_patron.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/tests/test_popups.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/tests/test_push.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/tests/test_recent.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/tests/test_registry.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/tests/test_relay.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/tests/test_requestlog.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/tests/test_resolve.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/tests/test_scales.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/tests/test_screen.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/tests/test_setup.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/tests/test_snapshot.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/tests/test_stamp_changelog.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/tests/test_timeframe.py +0 -0
- {chartremotely-0.3.9 → chartremotely-0.3.11}/tests/test_understand.py +0 -0
- {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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "chartremotely"
|
|
3
|
-
version = "0.3.
|
|
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.
|
|
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
|
|
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
|
|
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
|