tinyjukebox 0.47.0__py3-none-any.whl
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.
- tinyjukebox/__init__.py +7 -0
- tinyjukebox/albumplayer/__init__.py +1 -0
- tinyjukebox/albumplayer/aalbumplayer.py +402 -0
- tinyjukebox/albumplayer/clock.py +39 -0
- tinyjukebox/albumplayer/dummyplayer.py +49 -0
- tinyjukebox/albumplayer/player_interface.py +97 -0
- tinyjukebox/albumplayer/radioplayer.py +391 -0
- tinyjukebox/albumplayer/simpleplayer.py +419 -0
- tinyjukebox/albumplayer/spotifyplayer.py +671 -0
- tinyjukebox/backup.py +176 -0
- tinyjukebox/backups.py +340 -0
- tinyjukebox/cli.py +1076 -0
- tinyjukebox/control.py +1386 -0
- tinyjukebox/core/__init__.py +1 -0
- tinyjukebox/core/config.py +100 -0
- tinyjukebox/core/logging/__init__.py +113 -0
- tinyjukebox/core/microservice.py +303 -0
- tinyjukebox/core/pubsub/__init__.py +15 -0
- tinyjukebox/core/pubsub/base.py +196 -0
- tinyjukebox/core/pubsub/localqueue.py +102 -0
- tinyjukebox/core/pubsub/statistics.py +57 -0
- tinyjukebox/core/py.typed +0 -0
- tinyjukebox/core/schema/__init__.py +5 -0
- tinyjukebox/core/schema/logger.py +40 -0
- tinyjukebox/core/schema/microservice.py +48 -0
- tinyjukebox/core/threads.py +39 -0
- tinyjukebox/core/ui/__init__.py +5 -0
- tinyjukebox/core/ui/cli.py +120 -0
- tinyjukebox/core/ui/tools.py +73 -0
- tinyjukebox/core/unexpected.py +46 -0
- tinyjukebox/data/__init__.py +0 -0
- tinyjukebox/data/activities.py +175 -0
- tinyjukebox/data/activity.py +138 -0
- tinyjukebox/data/common.py +91 -0
- tinyjukebox/data/degraded.py +39 -0
- tinyjukebox/data/jsondata.py +218 -0
- tinyjukebox/data/sanitycheck.py +71 -0
- tinyjukebox/data/tag.py +98 -0
- tinyjukebox/data/tags.py +160 -0
- tinyjukebox/data/target.py +568 -0
- tinyjukebox/data/targetalbumtools.py +88 -0
- tinyjukebox/data/targets.py +256 -0
- tinyjukebox/data/volumelimits.py +68 -0
- tinyjukebox/data_folder.py +180 -0
- tinyjukebox/devices/__init__.py +1 -0
- tinyjukebox/devices/baseclasses/__init__.py +1 -0
- tinyjukebox/devices/baseclasses/adriver.py +115 -0
- tinyjukebox/devices/baseclasses/aevents.py +47 -0
- tinyjukebox/devices/baseclasses/gpiolevel.py +16 -0
- tinyjukebox/devices/bluetooth/__init__.py +1 -0
- tinyjukebox/devices/bluetooth/avrcp_input.py +118 -0
- tinyjukebox/devices/bluetooth/bluez.py +446 -0
- tinyjukebox/devices/bluetooth/link.py +396 -0
- tinyjukebox/devices/bluetooth/virtual.py +155 -0
- tinyjukebox/devices/drivermanager/__init__.py +1 -0
- tinyjukebox/devices/drivermanager/driverfactory.py +75 -0
- tinyjukebox/devices/drivermanager/drivermanager.py +151 -0
- tinyjukebox/devices/drivers/__init__.py +31 -0
- tinyjukebox/devices/drivers/bluetooth.py +257 -0
- tinyjukebox/devices/drivers/input.py +165 -0
- tinyjukebox/devices/drivers/rfid.py +297 -0
- tinyjukebox/devices/drivers/rgbled.py +215 -0
- tinyjukebox/devices/py.typed +0 -0
- tinyjukebox/devices/schema/__init__.py +1 -0
- tinyjukebox/devices/schema/adriver.py +33 -0
- tinyjukebox/devices/schema/bluetooth.py +31 -0
- tinyjukebox/devices/schema/drivermanager.py +41 -0
- tinyjukebox/devices/schema/input.py +23 -0
- tinyjukebox/devices/schema/rfid.py +44 -0
- tinyjukebox/devices/schema/rgbled.py +26 -0
- tinyjukebox/diagnostics.py +179 -0
- tinyjukebox/errors.py +91 -0
- tinyjukebox/headset.py +296 -0
- tinyjukebox/images.py +133 -0
- tinyjukebox/latency.py +43 -0
- tinyjukebox/library/__init__.py +2 -0
- tinyjukebox/library/diagnostics.py +183 -0
- tinyjukebox/library/journal.py +94 -0
- tinyjukebox/library/releases.py +348 -0
- tinyjukebox/library/schema.py +98 -0
- tinyjukebox/library/service.py +1115 -0
- tinyjukebox/library/statistics.py +288 -0
- tinyjukebox/library/store.py +1386 -0
- tinyjukebox/messages.py +202 -0
- tinyjukebox/metadata.py +133 -0
- tinyjukebox/py.typed +0 -0
- tinyjukebox/restore.py +391 -0
- tinyjukebox/rgbled.py +158 -0
- tinyjukebox/schema/__init__.py +1 -0
- tinyjukebox/schema/activities.py +27 -0
- tinyjukebox/schema/tags.py +54 -0
- tinyjukebox/schema/targets.py +83 -0
- tinyjukebox/schema/tinyjukebox.py +217 -0
- tinyjukebox/schema/volume.py +28 -0
- tinyjukebox/slots.py +234 -0
- tinyjukebox/spotify/__init__.py +1 -0
- tinyjukebox/spotify/account.py +647 -0
- tinyjukebox/spotify/engine.py +568 -0
- tinyjukebox/spotify/oembed.py +65 -0
- tinyjukebox/spotify/soloist.py +198 -0
- tinyjukebox/spotify/sound.py +316 -0
- tinyjukebox/stm/__init__.py +0 -0
- tinyjukebox/stm/base.py +174 -0
- tinyjukebox/stm/playradio.py +237 -0
- tinyjukebox/stm/playspotify.py +418 -0
- tinyjukebox/stm/stmoperation.py +769 -0
- tinyjukebox/stm/stmplayalbum.py +655 -0
- tinyjukebox/stm/stmvolume.py +221 -0
- tinyjukebox/sync/__init__.py +42 -0
- tinyjukebox/sync/box.py +486 -0
- tinyjukebox/sync/client.py +189 -0
- tinyjukebox/sync/data.py +489 -0
- tinyjukebox/sync/files.py +447 -0
- tinyjukebox/sync/handshake.py +42 -0
- tinyjukebox/sync/link.py +379 -0
- tinyjukebox/sync/manifest.py +162 -0
- tinyjukebox/sync/util.py +137 -0
- tinyjukebox/tinyjukebox.py +680 -0
- tinyjukebox/tools/__init__.py +1 -0
- tinyjukebox/tools/backup.py +173 -0
- tinyjukebox/tools/fill_library.py +120 -0
- tinyjukebox/tools/import_cards.py +274 -0
- tinyjukebox/tools/import_covers.py +138 -0
- tinyjukebox/tools/migrate_box01.py +151 -0
- tinyjukebox/volumemanager.py +355 -0
- tinyjukebox/web/__init__.py +1 -0
- tinyjukebox/web/auth.py +373 -0
- tinyjukebox/web/certificates.py +111 -0
- tinyjukebox/web/logbuffer.py +62 -0
- tinyjukebox/web/server.py +1237 -0
- tinyjukebox/web/uploads.py +262 -0
- tinyjukebox/web/users.py +106 -0
- tinyjukebox/web/web/app.js +3050 -0
- tinyjukebox/web/web/favicon.svg +6 -0
- tinyjukebox/web/web/help/de/admin.html +714 -0
- tinyjukebox/web/web/help/de/manual.html +1147 -0
- tinyjukebox/web/web/help/de/quick-guide.html +79 -0
- tinyjukebox/web/web/help/en/admin.html +701 -0
- tinyjukebox/web/web/help/en/manual.html +1126 -0
- tinyjukebox/web/web/help/en/quick-guide.html +79 -0
- tinyjukebox/web/web/help/help.css +25 -0
- tinyjukebox/web/web/help/images/box-2026.jpg +0 -0
- tinyjukebox/web/web/help/images/box-back.jpg +0 -0
- tinyjukebox/web/web/help/images/lid-top.jpg +0 -0
- tinyjukebox/web/web/help/images/logo-mark.svg +38 -0
- tinyjukebox/web/web/index.html +867 -0
- tinyjukebox/web/web/style.css +412 -0
- tinyjukebox/web/web/texts/de.json +892 -0
- tinyjukebox/web/web/texts/en.json +892 -0
- tinyjukebox-0.47.0.dist-info/METADATA +236 -0
- tinyjukebox-0.47.0.dist-info/RECORD +154 -0
- tinyjukebox-0.47.0.dist-info/WHEEL +4 -0
- tinyjukebox-0.47.0.dist-info/entry_points.txt +9 -0
- tinyjukebox-0.47.0.dist-info/licenses/LICENSE.txt +21 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""STMVolume - the state machine of the volume buttons."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Protocol
|
|
7
|
+
|
|
8
|
+
from tinyjukebox.core import unexpected
|
|
9
|
+
from tinyjukebox.core.logging import get_child
|
|
10
|
+
from tinyjukebox.core.pubsub import PubSubClient
|
|
11
|
+
from tinyjukebox.latency import ReactionTimer
|
|
12
|
+
from tinyjukebox.messages import BUTTON_HELD, BUTTON_PRESSED, Button
|
|
13
|
+
from tinyjukebox.stm.base import State, create_machine, write_dot_file
|
|
14
|
+
|
|
15
|
+
__all__ = ["STMVolume", "States", "Transitions", "VolumeButtons"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class VolumeButtons(Protocol):
|
|
19
|
+
"""What the buttons change: a :class:`~tinyjukebox.volumemanager.VolumeManager` or - speaker and headset - the
|
|
20
|
+
:class:`~tinyjukebox.volumemanager.Volumes`."""
|
|
21
|
+
|
|
22
|
+
def update_volume(self) -> None: ...
|
|
23
|
+
|
|
24
|
+
def volume_up(self, held: bool = False) -> None: ...
|
|
25
|
+
|
|
26
|
+
def volume_down(self, held: bool = False) -> None: ...
|
|
27
|
+
|
|
28
|
+
def volume_off(self) -> None: ...
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class States(Enum):
|
|
32
|
+
IDLE = "idle"
|
|
33
|
+
INIT = "init"
|
|
34
|
+
VOLUME_UP = "up"
|
|
35
|
+
VOLUME_DOWN = "down"
|
|
36
|
+
VOLUME_UP_HELD = "up_held" # the small step of a button that is kept pressed
|
|
37
|
+
VOLUME_DOWN_HELD = "down_held"
|
|
38
|
+
END = "end"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Transitions(Enum):
|
|
42
|
+
START = "start"
|
|
43
|
+
TO_IDLE = "to_idle"
|
|
44
|
+
TO_VOLUME_UP = "to_volume_up"
|
|
45
|
+
TO_VOLUME_DOWN = "to_volume_down"
|
|
46
|
+
TO_VOLUME_UP_HELD = "to_volume_up_held"
|
|
47
|
+
TO_VOLUME_DOWN_HELD = "to_volume_down_held"
|
|
48
|
+
END = "to_end"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
_TRANSITIONS: dict[str, dict[object, Transitions]] = {
|
|
52
|
+
BUTTON_PRESSED: {
|
|
53
|
+
Button.VOLUME_UP: Transitions.TO_VOLUME_UP,
|
|
54
|
+
Button.VOLUME_DOWN: Transitions.TO_VOLUME_DOWN,
|
|
55
|
+
},
|
|
56
|
+
BUTTON_HELD: {
|
|
57
|
+
Button.VOLUME_UP: Transitions.TO_VOLUME_UP_HELD,
|
|
58
|
+
Button.VOLUME_DOWN: Transitions.TO_VOLUME_DOWN_HELD,
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Idle(State):
|
|
64
|
+
def __init__(self, logger: logging.Logger) -> None:
|
|
65
|
+
super().__init__(States.IDLE, logger)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class End(State):
|
|
69
|
+
def __init__(self, logger: logging.Logger, volumemanager: VolumeButtons) -> None:
|
|
70
|
+
super().__init__(States.END, logger)
|
|
71
|
+
self._volumemanager = volumemanager
|
|
72
|
+
|
|
73
|
+
def _on_entry(self) -> Enum | None:
|
|
74
|
+
self._volumemanager.volume_off() # mute on shutdown
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class Init(State):
|
|
79
|
+
def __init__(self, logger: logging.Logger, volumemanager: VolumeButtons) -> None:
|
|
80
|
+
super().__init__(States.INIT, logger)
|
|
81
|
+
self._volumemanager = volumemanager
|
|
82
|
+
|
|
83
|
+
def _on_exit(self) -> None:
|
|
84
|
+
self._volumemanager.update_volume()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class _VolumeStep(State):
|
|
88
|
+
"""One step of the volume - ``held``: the small step of a button that is kept pressed.
|
|
89
|
+
|
|
90
|
+
The machine goes back to ``IDLE`` in any case. Only there it takes the next press: a step that failed (an
|
|
91
|
+
exception while the mixer is set) would otherwise leave the volume buttons without effect until the next start.
|
|
92
|
+
The exception is logged and reported as any unexpected one (:mod:`tinyjukebox.core.unexpected`)."""
|
|
93
|
+
|
|
94
|
+
def __init__(
|
|
95
|
+
self,
|
|
96
|
+
state_id: States,
|
|
97
|
+
logger: logging.Logger,
|
|
98
|
+
volumemanager: VolumeButtons,
|
|
99
|
+
latency: ReactionTimer,
|
|
100
|
+
held: bool,
|
|
101
|
+
) -> None:
|
|
102
|
+
super().__init__(state_id, logger)
|
|
103
|
+
self._volumemanager = volumemanager
|
|
104
|
+
self._latency = latency
|
|
105
|
+
self._held = held
|
|
106
|
+
|
|
107
|
+
def _step(self) -> None:
|
|
108
|
+
raise NotImplementedError
|
|
109
|
+
|
|
110
|
+
def _on_entry(self) -> Enum | None:
|
|
111
|
+
try:
|
|
112
|
+
self._step()
|
|
113
|
+
except Exception as error:
|
|
114
|
+
self._logger.exception("_on_entry - the volume could not be set")
|
|
115
|
+
unexpected.report(error, f"volume button ({self.id.value})")
|
|
116
|
+
else:
|
|
117
|
+
self._latency.end("volume set")
|
|
118
|
+
return Transitions.TO_IDLE
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class VolumeUp(_VolumeStep):
|
|
122
|
+
def __init__(
|
|
123
|
+
self, logger: logging.Logger, volumemanager: VolumeButtons, latency: ReactionTimer, held: bool = False
|
|
124
|
+
) -> None:
|
|
125
|
+
super().__init__(States.VOLUME_UP_HELD if held else States.VOLUME_UP, logger, volumemanager, latency, held)
|
|
126
|
+
|
|
127
|
+
def _step(self) -> None:
|
|
128
|
+
self._volumemanager.volume_up(self._held)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class VolumeDown(_VolumeStep):
|
|
132
|
+
def __init__(
|
|
133
|
+
self, logger: logging.Logger, volumemanager: VolumeButtons, latency: ReactionTimer, held: bool = False
|
|
134
|
+
) -> None:
|
|
135
|
+
super().__init__(States.VOLUME_DOWN_HELD if held else States.VOLUME_DOWN, logger, volumemanager, latency, held)
|
|
136
|
+
|
|
137
|
+
def _step(self) -> None:
|
|
138
|
+
self._volumemanager.volume_down(self._held)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class STMVolume:
|
|
142
|
+
"""Changes the volume on the volume buttons: one step per press (:data:`~tinyjukebox.messages.BUTTON_PRESSED`), a
|
|
143
|
+
small step per hold event (:data:`~tinyjukebox.messages.BUTTON_HELD` - the input drivers of the volume buttons
|
|
144
|
+
repeat it while the button is kept pressed). The handlers only queue the event - setting the mixer runs in the
|
|
145
|
+
machine's thread, not in the pubsub dispatch thread."""
|
|
146
|
+
|
|
147
|
+
def __init__(
|
|
148
|
+
self,
|
|
149
|
+
pubsub_client: PubSubClient,
|
|
150
|
+
volumemanager: VolumeButtons,
|
|
151
|
+
logger: logging.Logger,
|
|
152
|
+
latency: ReactionTimer | None = None,
|
|
153
|
+
) -> None:
|
|
154
|
+
""":param latency: measures button pressed → volume set (default: a timer of its own)."""
|
|
155
|
+
self._logger = get_child(logger, type(self).__name__)
|
|
156
|
+
self._pubsub_client = pubsub_client
|
|
157
|
+
self._latency = latency or ReactionTimer("volume button", self._logger)
|
|
158
|
+
self._machine = create_machine(self._logger)
|
|
159
|
+
for state in [
|
|
160
|
+
Init(self._logger, volumemanager),
|
|
161
|
+
Idle(self._logger),
|
|
162
|
+
VolumeUp(self._logger, volumemanager, self._latency),
|
|
163
|
+
VolumeDown(self._logger, volumemanager, self._latency),
|
|
164
|
+
VolumeUp(self._logger, volumemanager, self._latency, held=True),
|
|
165
|
+
VolumeDown(self._logger, volumemanager, self._latency, held=True),
|
|
166
|
+
End(self._logger, volumemanager),
|
|
167
|
+
]:
|
|
168
|
+
self._machine.addstate(state)
|
|
169
|
+
self._machine.setstartstate(States.INIT)
|
|
170
|
+
self._machine.addtransition(States.INIT, Transitions.START, States.IDLE)
|
|
171
|
+
self._machine.addtransition(States.IDLE, Transitions.TO_VOLUME_UP, States.VOLUME_UP)
|
|
172
|
+
self._machine.addtransition(States.IDLE, Transitions.TO_VOLUME_DOWN, States.VOLUME_DOWN)
|
|
173
|
+
self._machine.addtransition(States.IDLE, Transitions.TO_VOLUME_UP_HELD, States.VOLUME_UP_HELD)
|
|
174
|
+
self._machine.addtransition(States.IDLE, Transitions.TO_VOLUME_DOWN_HELD, States.VOLUME_DOWN_HELD)
|
|
175
|
+
changes = (States.VOLUME_UP, States.VOLUME_DOWN, States.VOLUME_UP_HELD, States.VOLUME_DOWN_HELD)
|
|
176
|
+
for state_id in changes:
|
|
177
|
+
self._machine.addtransition(state_id, Transitions.TO_IDLE, States.IDLE)
|
|
178
|
+
for state_id in (States.IDLE, *changes):
|
|
179
|
+
self._machine.addtransition(state_id, Transitions.END, States.END)
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
def machine(self) -> object:
|
|
183
|
+
"""The tantamount machine (diagram export, tests)."""
|
|
184
|
+
return self._machine
|
|
185
|
+
|
|
186
|
+
def dump_to_dot(self, filename: str | Path) -> None:
|
|
187
|
+
write_dot_file(self._machine, filename)
|
|
188
|
+
|
|
189
|
+
def wait_until_idle(self) -> None:
|
|
190
|
+
"""Block until all queued button events have been processed (tests)."""
|
|
191
|
+
self._machine.wait_until_idle()
|
|
192
|
+
|
|
193
|
+
def _handle(self, topic: str, message: object) -> None:
|
|
194
|
+
transition = _TRANSITIONS[topic].get(message)
|
|
195
|
+
if transition is None:
|
|
196
|
+
return
|
|
197
|
+
self._logger.info("_handle - received: %s (%s)", message, topic)
|
|
198
|
+
self._latency.begin(self._pubsub_client.published_at())
|
|
199
|
+
self._latency.step("handler")
|
|
200
|
+
self._machine.asyncoperate(transition)
|
|
201
|
+
|
|
202
|
+
def _button_handler(self, message: object) -> None:
|
|
203
|
+
"""Handler for :data:`~tinyjukebox.messages.BUTTON_PRESSED` - reacts on the volume buttons only."""
|
|
204
|
+
self._handle(BUTTON_PRESSED, message)
|
|
205
|
+
|
|
206
|
+
def _held_handler(self, message: object) -> None:
|
|
207
|
+
"""Handler for :data:`~tinyjukebox.messages.BUTTON_HELD` - reacts on the volume buttons only."""
|
|
208
|
+
self._handle(BUTTON_HELD, message)
|
|
209
|
+
|
|
210
|
+
def start(self) -> None:
|
|
211
|
+
self._machine.start()
|
|
212
|
+
self._machine.operate(Transitions.START)
|
|
213
|
+
self._pubsub_client.subscribe(BUTTON_PRESSED, self._button_handler)
|
|
214
|
+
self._pubsub_client.subscribe(BUTTON_HELD, self._held_handler)
|
|
215
|
+
|
|
216
|
+
def stop(self) -> None:
|
|
217
|
+
self._pubsub_client.unsubscribe(BUTTON_PRESSED, self._button_handler, ignore_not_found=True)
|
|
218
|
+
self._pubsub_client.unsubscribe(BUTTON_HELD, self._held_handler, ignore_not_found=True)
|
|
219
|
+
if self._machine._startdone: # not started if the start of the box failed before
|
|
220
|
+
self._machine.operate(Transitions.END)
|
|
221
|
+
self._machine.stop()
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""What a box and the library share: the interface between them, files with checksums, the handshake.
|
|
2
|
+
|
|
3
|
+
The design is in docs/developer/library.md. ``data``, ``link``, ``files`` and ``box`` are the side of a box,
|
|
4
|
+
``tinyjukebox.library`` is the library; ``client``, ``handshake``, ``manifest`` and ``util`` are used by both.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import contextlib
|
|
8
|
+
import contextvars
|
|
9
|
+
from collections.abc import Iterator
|
|
10
|
+
|
|
11
|
+
__all__ = ["INTERFACE", "KINDS", "LIBRARY", "SOFTWARE", "acting_as", "actor"]
|
|
12
|
+
|
|
13
|
+
INTERFACE = 2
|
|
14
|
+
"""Version of the interface between box and library that this code speaks (a box) or serves (the library). Raised
|
|
15
|
+
only for a change that an older counterpart cannot cope with."""
|
|
16
|
+
|
|
17
|
+
KINDS = ("tag", "target", "position", "setting")
|
|
18
|
+
"""What is synchronized entry by entry. Tags and targets are the catalog, the same for all boxes; positions and
|
|
19
|
+
settings belong to one box."""
|
|
20
|
+
|
|
21
|
+
LIBRARY = "library"
|
|
22
|
+
"""The side that changed an entry last, if it was not a box: something entered on the page of the library."""
|
|
23
|
+
|
|
24
|
+
SOFTWARE = "software"
|
|
25
|
+
"""Who made a change that no person asked for: a position that moved on, a tag created for an unknown uid."""
|
|
26
|
+
|
|
27
|
+
_actor: contextvars.ContextVar[str] = contextvars.ContextVar("tinyjukebox_actor", default=SOFTWARE)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def actor() -> str:
|
|
31
|
+
"""Who is making the changes of this thread right now: a user of the web page, the console, or the software."""
|
|
32
|
+
return _actor.get()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@contextlib.contextmanager
|
|
36
|
+
def acting_as(who: str) -> Iterator[None]:
|
|
37
|
+
"""Changes made inside are noted as made by ``who``, e.g. ``web:anna`` or ``console``."""
|
|
38
|
+
token = _actor.set(who)
|
|
39
|
+
try:
|
|
40
|
+
yield
|
|
41
|
+
finally:
|
|
42
|
+
_actor.reset(token)
|