zettcode-app 0.4.2__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.
- zettcode/app/__init__.py +63 -0
- zettcode/app/agent/__init__.py +47 -0
- zettcode/app/agent/agent/__init__.py +84 -0
- zettcode/app/agent/agent/commands.py +269 -0
- zettcode/app/agent/agent/settings.py +132 -0
- zettcode/app/agent/conversation/__init__.py +7 -0
- zettcode/app/agent/conversation/changes.py +64 -0
- zettcode/app/agent/conversation/effort_gauge.py +126 -0
- zettcode/app/agent/conversation/projection.py +265 -0
- zettcode/app/agent/conversation/rendering.py +837 -0
- zettcode/app/agent/conversation/replay.py +198 -0
- zettcode/app/agent/conversation/shimmer.py +256 -0
- zettcode/app/agent/conversation/stops.py +57 -0
- zettcode/app/agent/conversation/tips.py +86 -0
- zettcode/app/agent/conversation/transcript/__init__.py +80 -0
- zettcode/app/agent/conversation/transcript/fold.py +57 -0
- zettcode/app/agent/conversation/transcript/log.py +182 -0
- zettcode/app/agent/conversation/transcript/oneoff.py +55 -0
- zettcode/app/agent/conversation/transcript/stream.py +187 -0
- zettcode/app/agent/conversation/transcript/turn.py +323 -0
- zettcode/app/commandline/__init__.py +47 -0
- zettcode/app/commandline/arguments.py +169 -0
- zettcode/app/commandline/completion.py +262 -0
- zettcode/app/commandline/history.py +305 -0
- zettcode/app/commandline/runner.py +365 -0
- zettcode/app/errors.py +12 -0
- zettcode/app/plugins/__init__.py +110 -0
- zettcode/app/plugins/builtins.py +33 -0
- zettcode/app/plugins/groups.py +503 -0
- zettcode/app/plugins/loader.py +268 -0
- zettcode/app/py.typed +0 -0
- zettcode/app/ui/__init__.py +63 -0
- zettcode/app/ui/app.py +1507 -0
- zettcode/app/ui/bars.py +290 -0
- zettcode/app/ui/boot.py +131 -0
- zettcode/app/ui/clipboard.py +340 -0
- zettcode/app/ui/commands.py +323 -0
- zettcode/app/ui/config_form.py +343 -0
- zettcode/app/ui/demo.py +180 -0
- zettcode/app/ui/followups.py +430 -0
- zettcode/app/ui/gc_policy.py +147 -0
- zettcode/app/ui/keys.py +462 -0
- zettcode/app/ui/messaging.py +260 -0
- zettcode/app/ui/notices.py +195 -0
- zettcode/app/ui/providers.py +626 -0
- zettcode/app/ui/sessions.py +422 -0
- zettcode/app/ui/settings.py +161 -0
- zettcode/app/ui/shell.py +361 -0
- zettcode/app/ui/turns.py +115 -0
- zettcode/app/ui/updating.py +204 -0
- zettcode/app/ui/widgets/__init__.py +161 -0
- zettcode/app/ui/widgets/approval_page.py +284 -0
- zettcode/app/ui/widgets/ask_page.py +386 -0
- zettcode/app/ui/widgets/bash_page.py +90 -0
- zettcode/app/ui/widgets/completer.py +100 -0
- zettcode/app/ui/widgets/composer.py +529 -0
- zettcode/app/ui/widgets/document_page.py +159 -0
- zettcode/app/ui/widgets/effort_page.py +294 -0
- zettcode/app/ui/widgets/follow_up.py +168 -0
- zettcode/app/ui/widgets/form_page.py +380 -0
- zettcode/app/ui/widgets/input_page.py +175 -0
- zettcode/app/ui/widgets/model_page.py +63 -0
- zettcode/app/ui/widgets/model_settings_page.py +653 -0
- zettcode/app/ui/widgets/pet.py +247 -0
- zettcode/app/ui/widgets/pet_page.py +347 -0
- zettcode/app/ui/widgets/plugins_page.py +442 -0
- zettcode/app/ui/widgets/root.py +68 -0
- zettcode/app/ui/widgets/self_compact_page.py +363 -0
- zettcode/app/ui/widgets/send_kind_page.py +193 -0
- zettcode/app/ui/widgets/sessions_page.py +511 -0
- zettcode/app/ui/widgets/statusline_page.py +427 -0
- zettcode/app/ui/widgets/steering.py +126 -0
- zettcode/app/ui/widgets/transcript.py +287 -0
- zettcode/app/ui/widgets/update_page.py +65 -0
- zettcode/app/ui/widgets/welcome.py +24 -0
- zettcode/collector.py +54 -0
- zettcode/update.py +519 -0
- zettcode_app-0.4.2.dist-info/METADATA +26 -0
- zettcode_app-0.4.2.dist-info/RECORD +81 -0
- zettcode_app-0.4.2.dist-info/WHEEL +4 -0
- zettcode_app-0.4.2.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""Load the plugins for the terminal: the engine's loader, plus the commands, palettes, pets, presenters and rows.
|
|
2
|
+
|
|
3
|
+
Everything the engine's :class:`~zettcode.engine.plugins.loader.Loader` checks and merges, this one does too, and in
|
|
4
|
+
addition it keeps command names, theme names and pet names unique and merges the header and status rows, so a plugin
|
|
5
|
+
that collides on any of them is reported.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import inspect
|
|
11
|
+
from collections import Counter
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
from typing import TYPE_CHECKING, Any
|
|
14
|
+
|
|
15
|
+
from zettcode.engine.plugins.commands import PlainCommandContext, TextCommandResult, TurnCommandResult
|
|
16
|
+
from zettcode.engine.plugins.context import PluginContext
|
|
17
|
+
from zettcode.engine.plugins.loader import Loader, run
|
|
18
|
+
from zettcode.plugin_api.commands import Command, CommandResult
|
|
19
|
+
from zettcode.plugin_api.host import HOST_LINK, HostLink
|
|
20
|
+
from zettcode.plugin_api.plugin import TuiPlugin, TuiPlugins
|
|
21
|
+
from zettcode.plugin_api.state import TurnSegment
|
|
22
|
+
from zettcode.sdk.builtins import require_agent_parts
|
|
23
|
+
from zettcode.tui import RESERVED_THEMES
|
|
24
|
+
|
|
25
|
+
from .groups import TuiGroups, UiSlots
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING: # pragma: no cover - annotations only
|
|
28
|
+
from collections.abc import Iterable, Mapping, Sequence
|
|
29
|
+
from importlib.metadata import EntryPoint
|
|
30
|
+
|
|
31
|
+
from zettcode.config import ZettCodeConfig
|
|
32
|
+
from zettcode.engine.plugins.commands import PlainCommand
|
|
33
|
+
from zettcode.engine.plugins.loader import OfferedPlugin
|
|
34
|
+
from zettcode.engine.plugins.plugin import Plugin
|
|
35
|
+
from zettcode.plugin_api.commands import Command
|
|
36
|
+
from zettcode.plugin_api.pets import PetSource
|
|
37
|
+
from zettcode.plugin_api.presenters import Presenter
|
|
38
|
+
from zettcode.tui import ThemeSource
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def load_terminal_plugins(
|
|
42
|
+
config: ZettCodeConfig,
|
|
43
|
+
*,
|
|
44
|
+
entry_points: Iterable[EntryPoint] | None = None,
|
|
45
|
+
builtins: Sequence[OfferedPlugin] = (),
|
|
46
|
+
shared: Mapping[str, object] | None = None,
|
|
47
|
+
required: Sequence[str] = (),
|
|
48
|
+
agent: bool = True,
|
|
49
|
+
) -> TuiPlugins:
|
|
50
|
+
"""Load the builtin plugins and every enabled distribution into one bundle, for the terminal.
|
|
51
|
+
|
|
52
|
+
The arguments are those of :func:`zettcode.engine.plugins.loader.load_plugins`; the builtins default to the
|
|
53
|
+
terminal's own, and the agent's come from discovery like the other parts. ``agent=False`` leaves them out, for a
|
|
54
|
+
picture or a test that draws the shell with no runtime behind it.
|
|
55
|
+
"""
|
|
56
|
+
bundle = run(
|
|
57
|
+
TerminalLoader(config, shared),
|
|
58
|
+
config,
|
|
59
|
+
builtins=builtins,
|
|
60
|
+
entry_points=entry_points,
|
|
61
|
+
required=required,
|
|
62
|
+
terminal=True,
|
|
63
|
+
agent=agent,
|
|
64
|
+
)
|
|
65
|
+
assert isinstance(bundle, TuiPlugins)
|
|
66
|
+
return bundle
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
#: The plugins the terminal cannot run without: the rows it paints, the presenters that draw the conversation and the
|
|
70
|
+
#: commands that are the way back to a working setup (`/help`, `/plugins`, `/model`).
|
|
71
|
+
#: ``[plugins] disable`` does not reach them, and the terminal does not start when one is missing.
|
|
72
|
+
REQUIRED_PLUGINS: tuple[str, ...] = ("shell", "turn-line", "presenters", "commands")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def terminal_plugins(config: ZettCodeConfig, shared: Mapping[str, object]) -> TuiPlugins:
|
|
76
|
+
"""Load the plugins the terminal's way, in the shape :meth:`ZettCodeEngine.preview` takes as its loader."""
|
|
77
|
+
return require_agent_parts(load_terminal_plugins(config, shared=shared, required=REQUIRED_PLUGINS))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _tui(context: PluginContext) -> TuiGroups:
|
|
81
|
+
"""Return the terminal group of ``context``, which this loader always attaches."""
|
|
82
|
+
if context.tui is None or not isinstance(context.tui, TuiGroups):
|
|
83
|
+
raise TypeError(f"{type(context).__name__} has no terminal group")
|
|
84
|
+
return context.tui
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def terminal_context(
|
|
88
|
+
config: ZettCodeConfig, services: Mapping[str, object] | None = None, *, name: str = ""
|
|
89
|
+
) -> PluginContext:
|
|
90
|
+
"""Build one context with the terminal group attached, the way this loader does for a plugin."""
|
|
91
|
+
context = PluginContext(config, services, name=name)
|
|
92
|
+
context.attach_tui(TuiGroups(context))
|
|
93
|
+
return context
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def plain_commands(context: PluginContext) -> list[Command]:
|
|
97
|
+
"""Turn the plugin's plain commands into the shell's, keeping the handler's contract.
|
|
98
|
+
|
|
99
|
+
The handler returns a :class:`~zettcode.engine.plugins.commands.TextCommandResult` or a
|
|
100
|
+
:class:`~zettcode.engine.plugins.commands.TurnCommandResult` — words, a document, or a request to start — so there
|
|
101
|
+
is nothing a shell could show that a process without one could not; the adapter maps each onto what the shell does
|
|
102
|
+
with it.
|
|
103
|
+
"""
|
|
104
|
+
adapted: list[Command] = []
|
|
105
|
+
for plain in context.commands.commands:
|
|
106
|
+
adapted.append(
|
|
107
|
+
Command(
|
|
108
|
+
name=plain.name,
|
|
109
|
+
description=plain.summary,
|
|
110
|
+
type="plugin",
|
|
111
|
+
handler=_plain_handler(plain),
|
|
112
|
+
when_busy=plain.when_busy, # type: ignore[arg-type] # the shell validates the word
|
|
113
|
+
subcommands=plain.subcommands, # type: ignore[arg-type]
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
return adapted
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _plain_handler(plain: PlainCommand):
|
|
120
|
+
"""Wrap one plain handler as the shell's, one result type at a time."""
|
|
121
|
+
|
|
122
|
+
async def run(context):
|
|
123
|
+
host = context.host
|
|
124
|
+
level = host.client.permission if host is not None else "ask"
|
|
125
|
+
result = plain.handler(PlainCommandContext(argument=context.argument, level=level))
|
|
126
|
+
if inspect.isawaitable(result):
|
|
127
|
+
result = await result
|
|
128
|
+
if isinstance(result, TurnCommandResult):
|
|
129
|
+
# A preset for a turn: the shell starts it, and the conversation keeps the short form the command was
|
|
130
|
+
# typed as.
|
|
131
|
+
await context.require_host().prompt(result.prompt, shown=result.shown or None, cache=result.cache or None)
|
|
132
|
+
return CommandResult(notification=result.notification)
|
|
133
|
+
if isinstance(result, TextCommandResult):
|
|
134
|
+
# A document is a page of its own, so the shell opens one instead of stacking it into the conversation;
|
|
135
|
+
# the message beside it is unchanged, so a command that gives both gets both. The widget library is
|
|
136
|
+
# imported only where there is a document: a command that answers with words alone never pays for it.
|
|
137
|
+
widget = None
|
|
138
|
+
if result.markdown:
|
|
139
|
+
from zettcode.app.ui.widgets import DocumentPage
|
|
140
|
+
|
|
141
|
+
# Titled by the command as it was typed: the summary is already in the menu, and this page covers the
|
|
142
|
+
# conversation, so it has to say which command opened it.
|
|
143
|
+
widget = DocumentPage(result.markdown, title=plain.name)
|
|
144
|
+
return CommandResult(message=result.message, notification=result.notification, widget=widget)
|
|
145
|
+
raise TypeError(
|
|
146
|
+
f"a plain command returns a TextCommandResult or a TurnCommandResult, not {type(result).__name__}"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
return run
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class TerminalLoader(Loader):
|
|
153
|
+
"""The engine's loader with the terminal's registries."""
|
|
154
|
+
|
|
155
|
+
def __init__(self, config: ZettCodeConfig, shared: Mapping[str, object] | None = None) -> None:
|
|
156
|
+
super().__init__(config, shared)
|
|
157
|
+
self.services.setdefault(HOST_LINK, HostLink())
|
|
158
|
+
self.slots = UiSlots()
|
|
159
|
+
self.turn_segments: dict[str, TurnSegment] = {}
|
|
160
|
+
self.commands: list[Command] = []
|
|
161
|
+
#: The commands the program's own plugins (built in, or its parts) registered; the bundle puts them first.
|
|
162
|
+
self.program_commands: list[Command] = []
|
|
163
|
+
self.themes: list[ThemeSource] = []
|
|
164
|
+
self.pets: list[PetSource] = []
|
|
165
|
+
self.presenters: list[Presenter[Any]] = []
|
|
166
|
+
#: Told when the terminal window gains or loses focus, and when a turn of the conversation ends.
|
|
167
|
+
self.focus_handlers: list[Callable[[bool], None]] = []
|
|
168
|
+
self.turn_start_handlers: list[Callable[[], None]] = []
|
|
169
|
+
self.turn_end_handlers: list[Callable[[bool], None]] = []
|
|
170
|
+
self._claimed: set[str] = set()
|
|
171
|
+
self._theme_names: set[str] = set(RESERVED_THEMES)
|
|
172
|
+
self._pet_names: set[str] = set()
|
|
173
|
+
|
|
174
|
+
def new_context(self, name: str = "") -> PluginContext:
|
|
175
|
+
# Seed the registry with what is already loaded, so a segment that
|
|
176
|
+
# reuses a name — or asks to override a side — acts on the real rows
|
|
177
|
+
# instead of on an empty copy. A failure leaves it untouched.
|
|
178
|
+
context = terminal_context(self.config, self.services, name=name)
|
|
179
|
+
tui = _tui(context)
|
|
180
|
+
tui.slots.take_from(self.slots)
|
|
181
|
+
for segment in self.turn_segments.values():
|
|
182
|
+
tui.register_turn_segment(segment.name, segment.summary, segment.builder)
|
|
183
|
+
return context
|
|
184
|
+
|
|
185
|
+
def activated(self, plugin: Plugin[Any], context: PluginContext) -> None:
|
|
186
|
+
if isinstance(plugin, TuiPlugin):
|
|
187
|
+
terminal = _tui(context)
|
|
188
|
+
for region, side, slot_name, builder in plugin.ui_slots():
|
|
189
|
+
terminal.slots.add(region, side, slot_name, builder)
|
|
190
|
+
|
|
191
|
+
def clash(self, context: PluginContext) -> str | None:
|
|
192
|
+
terminal = _tui(context)
|
|
193
|
+
# Command lookup takes the first match, so a second plugin claiming the
|
|
194
|
+
# same name would be silently unreachable. Reject the plugin instead.
|
|
195
|
+
claimed = [command.name for command in terminal.registered_commands]
|
|
196
|
+
claimed += [plain.name for plain in context.commands.commands]
|
|
197
|
+
# Both registries of this plugin, and everything loaded before it, are one namespace: the shell keeps the
|
|
198
|
+
# first match, so a second spelling of a name would be unreachable rather than refused.
|
|
199
|
+
command_clash = next(
|
|
200
|
+
(name for name, count in Counter(claimed).items() if count > 1 or name in self._claimed), None
|
|
201
|
+
)
|
|
202
|
+
if command_clash is not None:
|
|
203
|
+
return f"command {command_clash} is already registered by another plugin"
|
|
204
|
+
found = super().clash(context)
|
|
205
|
+
if found is not None:
|
|
206
|
+
return found
|
|
207
|
+
# `/theme <name>` has to mean one palette, so a name an earlier plugin (the built-in one first) or the
|
|
208
|
+
# program itself already has is not shared out a second time.
|
|
209
|
+
theme_clash = next((source.name for source in terminal.themes if source.name in self._theme_names), None)
|
|
210
|
+
if theme_clash is not None:
|
|
211
|
+
return f"theme {theme_clash} is already registered"
|
|
212
|
+
# `/pet <name>` has to mean one pet, so a name an earlier plugin (the built-in one first) already has is not
|
|
213
|
+
# shared out a second time.
|
|
214
|
+
pet_clash = next((source.name for source in terminal.pets if source.name in self._pet_names), None)
|
|
215
|
+
if pet_clash is not None:
|
|
216
|
+
return f"pet {pet_clash} is already registered"
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
def merge(self, context: PluginContext) -> None:
|
|
220
|
+
terminal = _tui(context)
|
|
221
|
+
super().merge(context)
|
|
222
|
+
# A plain command is one the shell can run too: text and a toast need no page, so the plugin's
|
|
223
|
+
# ``ctx.commands`` become ordinary commands here, beside the ones registered on ``ctx.tui``.
|
|
224
|
+
plain = plain_commands(context)
|
|
225
|
+
self._claimed.update(command.name for command in plain)
|
|
226
|
+
before = len(self.commands)
|
|
227
|
+
self.commands.extend(plain)
|
|
228
|
+
self._theme_names.update(source.name for source in terminal.themes)
|
|
229
|
+
self.themes.extend(terminal.themes)
|
|
230
|
+
self._pet_names.update(source.name for source in terminal.pets)
|
|
231
|
+
self.pets.extend(terminal.pets)
|
|
232
|
+
self.presenters.extend(terminal.presenters)
|
|
233
|
+
self.focus_handlers.extend(terminal.focus_handlers)
|
|
234
|
+
self.turn_start_handlers.extend(terminal.turn_start_handlers)
|
|
235
|
+
self.turn_end_handlers.extend(terminal.turn_end_handlers)
|
|
236
|
+
self._claimed.update(command.name for command in terminal.registered_commands)
|
|
237
|
+
self.commands.extend(terminal.registered_commands)
|
|
238
|
+
if self.program:
|
|
239
|
+
self.program_commands.extend(self.commands[before:])
|
|
240
|
+
self.slots = terminal.slots
|
|
241
|
+
self.turn_segments = {segment.name: segment for segment in terminal.turn_segments}
|
|
242
|
+
|
|
243
|
+
def bundle(self) -> TuiPlugins:
|
|
244
|
+
plain = super().bundle()
|
|
245
|
+
program = tuple(self.program_commands)
|
|
246
|
+
own = {id(command) for command in program}
|
|
247
|
+
return TuiPlugins(
|
|
248
|
+
host=plain.host,
|
|
249
|
+
mentions=plain.mentions,
|
|
250
|
+
providers=plain.providers,
|
|
251
|
+
contributed=plain.contributed,
|
|
252
|
+
services=plain.services,
|
|
253
|
+
plain_commands=plain.plain_commands,
|
|
254
|
+
failed=plain.failed,
|
|
255
|
+
builtins=plain.builtins,
|
|
256
|
+
triggers=plain.triggers,
|
|
257
|
+
taken=plain.taken,
|
|
258
|
+
commands=(*program, *(command for command in self.commands if id(command) not in own)),
|
|
259
|
+
builtin_commands=tuple(program),
|
|
260
|
+
themes=tuple(self.themes),
|
|
261
|
+
pets=tuple(self.pets),
|
|
262
|
+
presenters=tuple(self.presenters),
|
|
263
|
+
rows=self.slots.rows(),
|
|
264
|
+
turn_segments=tuple(self.turn_segments.values()),
|
|
265
|
+
focus_handlers=tuple(self.focus_handlers),
|
|
266
|
+
turn_start_handlers=tuple(self.turn_start_handlers),
|
|
267
|
+
turn_end_handlers=tuple(self.turn_end_handlers),
|
|
268
|
+
)
|
zettcode/app/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""The ZettCode interface: the application shell and the widgets it composes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
#: Where each public name lives; read by :func:`__getattr__` on first use.
|
|
9
|
+
_EXPORTS = {
|
|
10
|
+
"CommandCompleter": ".widgets",
|
|
11
|
+
"ModelPage": ".widgets",
|
|
12
|
+
"TranscriptSource": ".widgets",
|
|
13
|
+
"TranscriptView": ".widgets",
|
|
14
|
+
"WELCOME": ".widgets",
|
|
15
|
+
"ZettCodeApp": ".app",
|
|
16
|
+
"ZettCodeRoot": ".widgets",
|
|
17
|
+
"help_text": ".widgets",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING: # pragma: no cover - for type checkers, not the runtime
|
|
22
|
+
from .app import ZettCodeApp
|
|
23
|
+
from .widgets import (
|
|
24
|
+
WELCOME,
|
|
25
|
+
CommandCompleter,
|
|
26
|
+
ModelPage,
|
|
27
|
+
TranscriptSource,
|
|
28
|
+
TranscriptView,
|
|
29
|
+
ZettCodeRoot,
|
|
30
|
+
help_text,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def __getattr__(name: str) -> object:
|
|
35
|
+
"""Import the module a public name lives in, the first time it is asked for.
|
|
36
|
+
|
|
37
|
+
This package is a facade over several subpackages, and a caller that wants
|
|
38
|
+
one name should not pay for the rest of them — the TUI alone holds the
|
|
39
|
+
widget library, the Markdown parser, the diff viewer, and the syntax
|
|
40
|
+
scanners. ``__all__`` is still the public surface.
|
|
41
|
+
"""
|
|
42
|
+
try:
|
|
43
|
+
module = _EXPORTS[name]
|
|
44
|
+
except KeyError:
|
|
45
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from None
|
|
46
|
+
return getattr(importlib.import_module(module, __name__), name)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def __dir__() -> list[str]:
|
|
50
|
+
"""Return the names this package publishes."""
|
|
51
|
+
return sorted(__all__)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
__all__ = [
|
|
55
|
+
"CommandCompleter",
|
|
56
|
+
"ModelPage",
|
|
57
|
+
"TranscriptSource",
|
|
58
|
+
"TranscriptView",
|
|
59
|
+
"WELCOME",
|
|
60
|
+
"ZettCodeApp",
|
|
61
|
+
"ZettCodeRoot",
|
|
62
|
+
"help_text",
|
|
63
|
+
]
|