zettcode-plugin-api 0.4.1__py3-none-any.whl → 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.
@@ -1,6 +1,6 @@
1
1
  """What a plugin imports: the contract, and the one dependency it needs.
2
2
 
3
- A ZettCode plugin is a class published under the ``zettcode.plugins`` entry point, and everything it may touch is
3
+ A ZettCode plugin is a class published under the ``zettcode.plugin`` entry point, and everything it may touch is
4
4
  re-exported here: the base classes and hooks of the agent side and of the terminal's rows, the command a handler returns,
5
5
  the row model and the presenter it implements, the pet format, the link to the shell, and the few widgets a panel is
6
6
  drawn with.
@@ -29,6 +29,20 @@ _EXPORTS = {
29
29
  "AgentBuild": "zettcode.engine.plugins",
30
30
  "BusyPolicy": ".commands",
31
31
  "CONTENT_INDENT": ".presenters",
32
+ "ContextReport": "zettcode.engine.extensions.context",
33
+ "ContextShare": "zettcode.engine.extensions.context",
34
+ "Ledger": "zettcode.engine.storage.ledger",
35
+ "SessionInfo": "zettcode.engine.storage.metadata",
36
+ "ContextPageService": ".services",
37
+ "ContextPageSnapshot": ".services",
38
+ "ContextPageWriter": ".services",
39
+ "ContextReportService": ".services",
40
+ "SessionStoreService": ".services",
41
+ "UsageLedgerService": ".services",
42
+ "SESSION_STORE": ".services",
43
+ "USAGE_LEDGER": ".services",
44
+ "CONTEXT_PAGE": ".services",
45
+ "CONTEXT_REPORT": ".services",
32
46
  "Canvas": "zettcode.tui",
33
47
  "ChangesEntry": ".entries",
34
48
  "Command": ".commands",
@@ -54,6 +68,8 @@ _EXPORTS = {
54
68
  "HOST_LINK": ".host",
55
69
  "Host": "zettcode.tui",
56
70
  "HostLink": ".host",
71
+ "INBOX": "zettcode.engine.inbox",
72
+ "Inbox": "zettcode.engine.inbox",
57
73
  "MarkdownEntry": ".entries",
58
74
  "Mention": "zettcode.mention",
59
75
  "MentionProvider": "zettcode.mention",
@@ -62,10 +78,12 @@ _EXPORTS = {
62
78
  "ModelState": ".state",
63
79
  "MouseAction": "zettcode.tui",
64
80
  "MouseEvent": "zettcode.tui",
81
+ "NextPresenter": ".presenters",
65
82
  "NoticeLevel": ".entries",
66
83
  "Pet": ".pets",
67
84
  "PetSource": ".pets",
68
85
  "PlainCommandContext": "zettcode.engine.plugins.commands",
86
+ "PlainCommandHost": "zettcode.engine.plugins.commands",
69
87
  "Plugin": "zettcode.engine.plugins",
70
88
  "PluginAgentMixin": "zettcode.engine.plugins",
71
89
  "PluginContext": "zettcode.engine.plugins",
@@ -81,6 +99,14 @@ _EXPORTS = {
81
99
  "PluginTurnMixin": "zettcode.engine.plugins",
82
100
  "PluginUiMixin": ".mixins",
83
101
  "Plugins": "zettcode.engine.plugins",
102
+ "LLM": "zettcode.engine.plugins",
103
+ "ModelAccess": "zettcode.engine.plugins",
104
+ "PLUGIN_AGENT": "zettcode.engine.plugins",
105
+ "PLUGIN_COMMANDS": "zettcode.engine.plugins",
106
+ "PLUGIN_CONFIG": "zettcode.engine.plugins",
107
+ "PLUGIN_SETTINGS": "zettcode.engine.plugins",
108
+ "PLUGIN_TUI": "zettcode.engine.plugins",
109
+ "PLUGIN_WORKSPACE": "zettcode.engine.plugins",
84
110
  "Presenter": ".presenters",
85
111
  "Presenters": ".presenters",
86
112
  "Provider": "zettcode.providers",
@@ -103,6 +129,9 @@ _EXPORTS = {
103
129
  "TuiGroup": "zettcode.plugin_tui",
104
130
  "TuiPlugin": ".plugin",
105
131
  "TuiPlugins": ".plugin",
132
+ "Trigger": "zettcode.engine.plugins",
133
+ "TriggerEvent": "zettcode.engine.plugins",
134
+ "TriggerScope": "zettcode.engine.plugins",
106
135
  "TurnCommandResult": "zettcode.engine.plugins.commands",
107
136
  "TurnSegment": ".state",
108
137
  "UiBuilder": ".state",
@@ -111,6 +140,7 @@ _EXPORTS = {
111
140
  "UiSegment": ".state",
112
141
  "UiSide": ".state",
113
142
  "WaitEntry": ".entries",
143
+ "Waiting": "zettcode.engine.inbox",
114
144
  "Widget": "zettcode.tui",
115
145
  "WriteEntry": ".entries",
116
146
  "ZettCodeConfig": "zettcode.config",
@@ -125,10 +155,20 @@ _EXPORTS = {
125
155
 
126
156
  if TYPE_CHECKING: # pragma: no cover - for type checkers, not the runtime
127
157
  from zettcode.config import ModelConfig, ZettCodeConfig
158
+ from zettcode.engine.extensions.context import ContextReport, ContextShare
159
+ from zettcode.engine.inbox import INBOX, Inbox, Waiting
128
160
  from zettcode.engine.plugins import (
161
+ LLM,
162
+ PLUGIN_AGENT,
163
+ PLUGIN_COMMANDS,
164
+ PLUGIN_CONFIG,
165
+ PLUGIN_SETTINGS,
166
+ PLUGIN_TUI,
167
+ PLUGIN_WORKSPACE,
129
168
  AgentBuild,
130
169
  Contribution,
131
170
  ExtensionFactory,
171
+ ModelAccess,
132
172
  Plugin,
133
173
  PluginAgentMixin,
134
174
  PluginContext,
@@ -143,8 +183,30 @@ if TYPE_CHECKING: # pragma: no cover - for type checkers, not the runtime
143
183
  PluginSetupMixin,
144
184
  PluginToolMixin,
145
185
  PluginTurnMixin,
186
+ Trigger,
187
+ TriggerEvent,
188
+ TriggerScope,
146
189
  )
147
- from zettcode.engine.plugins.commands import PlainCommandContext, TextCommandResult, TurnCommandResult
190
+ from zettcode.engine.plugins.commands import (
191
+ PlainCommandContext,
192
+ PlainCommandHost,
193
+ TextCommandResult,
194
+ TurnCommandResult,
195
+ )
196
+ from zettcode.engine.service_contracts import (
197
+ CONTEXT_PAGE,
198
+ CONTEXT_REPORT,
199
+ SESSION_STORE,
200
+ USAGE_LEDGER,
201
+ ContextPageService,
202
+ ContextPageSnapshot,
203
+ ContextPageWriter,
204
+ ContextReportService,
205
+ SessionStoreService,
206
+ UsageLedgerService,
207
+ )
208
+ from zettcode.engine.storage.ledger import Ledger
209
+ from zettcode.engine.storage.metadata import SessionInfo
148
210
  from zettcode.mention import Mention, MentionProvider
149
211
  from zettcode.plugin_tui import TuiGroup
150
212
  from zettcode.providers import Provider
@@ -191,7 +253,7 @@ if TYPE_CHECKING: # pragma: no cover - for type checkers, not the runtime
191
253
  from .panels import bottom_panel, pressed
192
254
  from .pets import Pet, PetSource
193
255
  from .plugin import TuiPlugin, TuiPlugins
194
- from .presenters import CONTENT_INDENT, Presenter, Presenters
256
+ from .presenters import CONTENT_INDENT, NextPresenter, Presenter, Presenters
195
257
  from .rows import Row
196
258
  from .state import (
197
259
  Activity,
@@ -236,6 +298,20 @@ __all__ = [
236
298
  "AgentBuild",
237
299
  "BusyPolicy",
238
300
  "CONTENT_INDENT",
301
+ "CONTEXT_PAGE",
302
+ "CONTEXT_REPORT",
303
+ "SESSION_STORE",
304
+ "USAGE_LEDGER",
305
+ "ContextReport",
306
+ "ContextShare",
307
+ "Ledger",
308
+ "SessionInfo",
309
+ "ContextPageService",
310
+ "ContextPageSnapshot",
311
+ "ContextPageWriter",
312
+ "ContextReportService",
313
+ "SessionStoreService",
314
+ "UsageLedgerService",
239
315
  "Canvas",
240
316
  "ChangesEntry",
241
317
  "Command",
@@ -261,6 +337,8 @@ __all__ = [
261
337
  "HOST_LINK",
262
338
  "Host",
263
339
  "HostLink",
340
+ "INBOX",
341
+ "Inbox",
264
342
  "MarkdownEntry",
265
343
  "Mention",
266
344
  "MentionProvider",
@@ -269,10 +347,12 @@ __all__ = [
269
347
  "ModelState",
270
348
  "MouseAction",
271
349
  "MouseEvent",
350
+ "NextPresenter",
272
351
  "NoticeLevel",
273
352
  "Pet",
274
353
  "PetSource",
275
354
  "PlainCommandContext",
355
+ "PlainCommandHost",
276
356
  "Plugin",
277
357
  "PluginAgentMixin",
278
358
  "PluginContext",
@@ -288,6 +368,14 @@ __all__ = [
288
368
  "PluginTurnMixin",
289
369
  "PluginUiMixin",
290
370
  "Plugins",
371
+ "LLM",
372
+ "ModelAccess",
373
+ "PLUGIN_AGENT",
374
+ "PLUGIN_COMMANDS",
375
+ "PLUGIN_CONFIG",
376
+ "PLUGIN_SETTINGS",
377
+ "PLUGIN_TUI",
378
+ "PLUGIN_WORKSPACE",
291
379
  "Presenter",
292
380
  "Presenters",
293
381
  "Provider",
@@ -310,6 +398,9 @@ __all__ = [
310
398
  "TuiGroup",
311
399
  "TuiPlugin",
312
400
  "TuiPlugins",
401
+ "Trigger",
402
+ "TriggerEvent",
403
+ "TriggerScope",
313
404
  "TurnCommandResult",
314
405
  "TurnSegment",
315
406
  "UiBuilder",
@@ -318,6 +409,7 @@ __all__ = [
318
409
  "UiSegment",
319
410
  "UiSide",
320
411
  "WaitEntry",
412
+ "Waiting",
321
413
  "Widget",
322
414
  "WriteEntry",
323
415
  "ZettCodeConfig",
@@ -6,7 +6,7 @@ from abc import ABC, abstractmethod
6
6
  from collections.abc import Awaitable, Callable, Sequence
7
7
  from dataclasses import dataclass
8
8
  from pathlib import Path
9
- from typing import TYPE_CHECKING, Literal, Protocol, TypeAlias
9
+ from typing import TYPE_CHECKING, Literal, TypeAlias
10
10
 
11
11
  from zettcode.plugin_api.entries import ThinkingEntry
12
12
  from zettcode.registry import Provider
@@ -45,7 +45,7 @@ class CommandUi(ABC):
45
45
  """Show a one-off toast, replacing any that is still on screen."""
46
46
 
47
47
 
48
- class CommandAgent(Protocol):
48
+ class CommandAgent(ABC):
49
49
  """What a command may read of the application agent, without importing the terminal that implements it.
50
50
 
51
51
  A command needs to know which session it is in and where that session works; the rest of the agent is the
@@ -53,17 +53,21 @@ class CommandAgent(Protocol):
53
53
  """
54
54
 
55
55
  @property
56
+ @abstractmethod
56
57
  def session_id(self) -> str:
57
58
  """Return the session this shell is working in."""
58
59
 
59
60
  @property
61
+ @abstractmethod
60
62
  def workspace(self) -> Path:
61
63
  """Return the directory the session works in."""
62
64
 
63
- @property
64
- def runtime(self) -> ZettCodeEngine:
65
- """Return the runtime: the agent, its configuration and the session store."""
65
+ #: Set once when the agent is built. It stays a plain attribute because the engine's `AgentBase` declares it
66
+ #: that way; a property here would need the agent to expose a backer, which is not this contract's to arrange.
67
+ #: A command reads it and does not assign it.
68
+ runtime: ZettCodeEngine
66
69
 
70
+ @abstractmethod
67
71
  def require_session(self) -> None:
68
72
  """Refuse to go on when the active session is another process's.
69
73
 
@@ -72,29 +76,35 @@ class CommandAgent(Protocol):
72
76
  """
73
77
 
74
78
 
75
- class CommandTranscript(Protocol):
79
+ class CommandTranscript(ABC):
76
80
  """What a command may add to the conversation: a note, an error, or a row of Markdown."""
77
81
 
82
+ @abstractmethod
78
83
  def notice(self, text: str) -> object:
79
84
  """Add a line that says something happened."""
80
85
 
86
+ @abstractmethod
81
87
  def error(self, text: str) -> object:
82
88
  """Add a line that says something failed."""
83
89
 
90
+ @abstractmethod
84
91
  def markdown(self, text: str) -> object:
85
92
  """Add a row of Markdown, which a command's own answer is."""
86
93
 
94
+ @abstractmethod
87
95
  def start_compaction(self) -> ThinkingEntry:
88
96
  """Open the row that shows a compaction working."""
89
97
 
98
+ @abstractmethod
90
99
  def complete_compaction(self, text: str = "") -> None:
91
100
  """Close the running compaction row and stamp its elapsed time."""
92
101
 
102
+ @abstractmethod
93
103
  def clear(self) -> None:
94
104
  """Forget every row: the conversation starts over on screen, as a new session's does."""
95
105
 
96
106
 
97
- class CommandHost(Protocol):
107
+ class CommandHost(ABC):
98
108
  """What a command that acts on the application, and not only on the agent, may ask of the shell.
99
109
 
100
110
  A command that opens a panel, switches the session or changes the model needs more than a place to write:
@@ -110,23 +120,35 @@ class CommandHost(Protocol):
110
120
  commands: Every command the shell knows, in the order ``/help`` lists them.
111
121
  """
112
122
 
113
- client: Client
114
- commands: tuple[Command, ...]
123
+ @property
124
+ @abstractmethod
125
+ def client(self) -> Client:
126
+ """Return the SDK client over the application agent."""
115
127
 
116
128
  @property
129
+ @abstractmethod
130
+ def commands(self) -> tuple[Command, ...]:
131
+ """Return every command the shell knows, in the order ``/help`` lists them."""
132
+
133
+ @property
134
+ @abstractmethod
117
135
  def agent(self) -> CommandAgent:
118
- """Return the application agent; read-only, so the terminal's own class satisfies it."""
136
+ """Return the application agent, read-only."""
119
137
 
120
138
  @property
139
+ @abstractmethod
121
140
  def transcript(self) -> CommandTranscript:
122
- """Return the conversation as drawn; read-only, for the same reason."""
141
+ """Return the conversation as drawn, read-only."""
123
142
 
143
+ @abstractmethod
124
144
  def close_page(self) -> None:
125
145
  """Close the page a command presented and return to the composer."""
126
146
 
147
+ @abstractmethod
127
148
  def request_layout(self) -> None:
128
149
  """Ask for the screen to be laid out again, after a change in what it shows."""
129
150
 
151
+ @abstractmethod
130
152
  async def wait_for_frame(self) -> None:
131
153
  """Wait until the next frame has reached the terminal.
132
154
 
@@ -135,30 +157,39 @@ class CommandHost(Protocol):
135
157
  work begins instead of after it.
136
158
  """
137
159
 
160
+ @abstractmethod
138
161
  def offer(self, command: str) -> None:
139
162
  """Put ``command`` and a space in the draft, which opens the menu of the words it takes."""
140
163
 
164
+ @abstractmethod
141
165
  def quit(self) -> None:
142
166
  """Leave the application."""
143
167
 
168
+ @abstractmethod
144
169
  def select_session(self, session_id: str) -> None:
145
170
  """Switch to a session picked in a panel: close the panel and resume behind the interface."""
146
171
 
172
+ @abstractmethod
147
173
  async def resume_session(self, session_id: str) -> None:
148
174
  """Restore a stored session without holding the event loop for the work."""
149
175
 
176
+ @abstractmethod
150
177
  async def fork_session(self) -> str:
151
178
  """Carry this conversation into a new session and continue there; return the new id."""
152
179
 
180
+ @abstractmethod
153
181
  async def rename_session(self, title: str) -> str:
154
182
  """Name the active session and return the name as stored."""
155
183
 
184
+ @abstractmethod
156
185
  async def run_continue(self) -> None:
157
186
  """Carry on the request that was interrupted."""
158
187
 
188
+ @abstractmethod
159
189
  async def run_side_question(self, question: str) -> None:
160
190
  """Answer a question that is kept out of the conversation."""
161
191
 
192
+ @abstractmethod
162
193
  async def prompt(self, text: str, *, shown: str | None = None, cache: str | None = None) -> bool:
163
194
  """Run ``text`` as one turn of this conversation, exactly as if the reader had typed it.
164
195
 
@@ -189,15 +220,19 @@ class CommandHost(Protocol):
189
220
  mid-request says ``when_busy="queue"``, and this then runs when the request in flight has ended.
190
221
  """
191
222
 
223
+ @abstractmethod
192
224
  def change_model(self, name: ModelInfo | str) -> ModelInfo:
193
225
  """Switch models and announce the change in the transcript."""
194
226
 
227
+ @abstractmethod
195
228
  def select_model(self, name: ModelInfo) -> None:
196
229
  """Apply a model picked in a panel and close the panel."""
197
230
 
231
+ @abstractmethod
198
232
  def change_effort(self, name: str) -> str:
199
233
  """Switch reasoning effort and announce the change."""
200
234
 
235
+ @abstractmethod
201
236
  def change_permission(self, name: str) -> str:
202
237
  """Set how much of the shell may run unasked and announce the change."""
203
238
 
@@ -208,6 +243,8 @@ class CommandContext:
208
243
 
209
244
  Attributes:
210
245
  argument: Trimmed text after the command name; ``""`` when none.
246
+ arguments: The same words split on whitespace, so a command taking several values — daemon names, short
247
+ values without spaces — can read them without parsing ``argument`` itself.
211
248
  ui: Surfaces the command may write to while it runs.
212
249
  host: The shell, for a command that acts on the application itself; ``None`` where there is no shell,
213
250
  such as a handler called directly in a test. Use :meth:`require_host` in a command that needs it.
@@ -216,6 +253,12 @@ class CommandContext:
216
253
  argument: str
217
254
  ui: CommandUi
218
255
  host: CommandHost | None = None
256
+ arguments: tuple[str, ...] = ()
257
+
258
+ def __post_init__(self) -> None:
259
+ """Derive the list form when the caller did not give one."""
260
+ if not self.arguments:
261
+ object.__setattr__(self, "arguments", tuple(self.argument.split()))
219
262
 
220
263
  def require_host(self) -> CommandHost:
221
264
  """Return the shell, or raise ``RuntimeError`` when the command was run without one."""
@@ -1,9 +1,9 @@
1
1
  """The conversation's entries: plain data, one class for each way a row can look.
2
2
 
3
3
  An entry says *what happened* — a user asked, a tool ran, the model is thinking —
4
- and nothing about how that is drawn. The drawing is ``presenters.py``, which looks
5
- an entry up by its :class:`EntryKind`; the list of entries, and what the agent
6
- does to it, is ``transcript.py``.
4
+ and nothing about how that is drawn. The drawing is ``presenters.py``, whose chain each presenter takes part in
5
+ through ``handle``, claiming a row by its :class:`EntryKind` or, for a tool, by its name; the list of entries, and
6
+ what the agent does to it, is ``transcript.py``.
7
7
 
8
8
  Two base classes carry what the concrete ones share. :class:`Entry` is identity
9
9
  and a place for the presenter to leave its rendered rows; :class:`TimedEntry`
@@ -222,7 +222,7 @@ class ToolEntry(TimedEntry):
222
222
 
223
223
  A tool whose row shows something of its own has an entry of its own that adds it
224
224
  (:class:`ReadEntry`, :class:`WriteEntry`, :class:`EditEntry`); they are all ``tool`` rows,
225
- folded and timed alike, and the presenter registry finds the one that draws each by its class.
225
+ folded and timed alike, and the presenter for each claims it by the tool's own name.
226
226
  Which class a call gets is decided by the render handler that describes it
227
227
  (:class:`~.rendering.ToolRow`), so nothing here, and nothing in the transcript, names a tool.
228
228
  """
@@ -1,8 +1,10 @@
1
1
  """What a plugin implements to draw a row: the base class, and how a drawn block is cached.
2
2
 
3
- A presenter turns one kind of entry into lines. The built-in ones live in `zettcode.presenters`; a plugin registers its
3
+ A presenter turns one kind of row into lines. The built-in ones live in `zettcode.presenters`; a plugin registers its
4
4
  own through `ctx.tui.register_presenter`, and the row it draws is the same kind of object, so this — the base class and
5
- the rule for when a row is drawn again — is part of the plugin contract, not part of the terminal.
5
+ the rule for when a row is drawn again — is part of the plugin contract, not part of the terminal. A presenter takes
6
+ part in the registry's chain through its own `handle(entry, next)`: it returns itself to claim the row, or calls
7
+ `next(entry)` to leave it to the presenters after it.
6
8
 
7
9
  The registry (`Presenters`) is here and not with the built-in rows because `Presenter.block` takes it as its `owner`:
8
10
  the base class names the registry in its own signature, so moving the registry out would make the base class import its
@@ -15,14 +17,14 @@ the width, the palette, the registry and what the row looks like, so a frame tha
15
17
  from __future__ import annotations
16
18
 
17
19
  from abc import ABC, abstractmethod
18
- from collections.abc import Iterable
20
+ from collections.abc import Callable, Iterable
19
21
  from dataclasses import fields as dataclass_fields
20
22
  from typing import Any, ClassVar, Generic, TypeVar
21
23
 
22
24
  from zettcode.tui import LineSource, Span, StaticLines, Style, TextLine, Theme
23
25
  from zettcode.tui.render import inset_line, layout_rich_lines, wrap_words
24
26
 
25
- from .entries import Entry, EntryKind
27
+ from .entries import Entry
26
28
 
27
29
  #: Columns a row's content is inset by, past the gutter marker.
28
30
  CONTENT_INDENT = 2
@@ -30,6 +32,9 @@ CONTENT_INDENT = 2
30
32
  #: The entry a presenter accepts; each subclass pins it to one entry type.
31
33
  E = TypeVar("E", bound=Entry)
32
34
 
35
+ #: The rest of the chain: asks the presenters after this one who draws the entry.
36
+ NextPresenter = Callable[[Entry], "Presenter[Any]"]
37
+
33
38
 
34
39
  #: The entry fields that are not part of how a row looks: its identity, and the
35
40
  #: slots the cache itself lives in.
@@ -55,15 +60,9 @@ def _look_fields(entry_type: type) -> tuple[str, ...]:
55
60
  class Presenter(Generic[E], ABC):
56
61
  """Turns one kind of entry into rows.
57
62
 
58
- The type parameter is the entry the presenter handles, and :attr:`kind` is the
59
- kind that routes it there, so :meth:`lines` receives exactly that entry.
63
+ The type parameter is the entry the presenter handles: :meth:`handle` claims it, then :meth:`lines` draws it.
60
64
 
61
65
  Attributes:
62
- kind: The kind of entry this presenter presents; one presenter per kind.
63
- entry_type: The entry class this presenter is for, when it is for one family of a kind
64
- rather than the kind as a whole: the presenter of an edit row is the one for
65
- ``EditEntry``, though an edit row is a ``tool`` row like any other. The registry looks
66
- for the entry's own class first (and its parents') and the kind second.
67
66
  gutter: Whether the rows open with a marker the presenter draws in the
68
67
  gutter's own column — the user's arrow, the wait row's sparkle, a
69
68
  tool's outcome. Every other row is inset past the gutter, so every
@@ -75,11 +74,18 @@ class Presenter(Generic[E], ABC):
75
74
  kind, or to beat another plugin, raises this.
76
75
  """
77
76
 
78
- kind: ClassVar[EntryKind]
79
- entry_type: ClassVar[type[Entry] | None] = None
80
77
  gutter: ClassVar[bool] = False
81
78
  priority: ClassVar[int] = 0
82
79
 
80
+ @abstractmethod
81
+ def handle(self, entry: Entry, next: NextPresenter) -> Presenter[Any]:
82
+ """Return the presenter that draws ``entry``, handing it to ``next`` when this one does not claim it.
83
+
84
+ This is the chain's one decision: every presenter is asked in turn, and each answers for itself. A
85
+ presenter returns ``self`` to claim the entry, or calls ``next(entry)`` to pass it on. The registry orders
86
+ the chain and starts it, but never matches an entry itself.
87
+ """
88
+
83
89
  @abstractmethod
84
90
  def lines(self, entry: E, width: int, theme: Theme, frame: int) -> list[TextLine]:
85
91
  """Return the rows this entry paints at the available width."""
@@ -202,71 +208,49 @@ class Indented(LineSource):
202
208
 
203
209
 
204
210
  class Presenters:
205
- """The presenters of one transcript: one for each kind of entry, and for the families inside a kind.
206
-
207
- A presenter is found by the entry's class first, walking up its parents, and by its kind
208
- second. Most kinds have one presenter and no family (a notice is a notice), and ``tool`` has
209
- one for tools in general and one for each tool whose entry carries more than the rest.
210
-
211
- When more than one presenter could draw the same rows, the one with the higher
212
- :attr:`Presenter.priority` wins, and an equal priority goes to the later registration. That is
213
- what makes a transcript's own presenters the default: they are registered first, at priority 0,
214
- so anything a plugin registers for the same rows wins without having to outrank them.
215
- :meth:`replacing` appends, which is the same thing for a caller holding a registry rather than a
216
- plugin container.
217
-
218
- A presenter for a whole kind is a claim on every row of that kind, so it beats the families
219
- registered before it at its priority: a plugin that draws every tool row draws every tool row.
220
- A family registered after it, or at a higher priority, is a refinement and keeps its own.
211
+ """The chain of presenters one transcript asks, strongest claim first, with a fallback last.
212
+
213
+ Every presenter in the chain is asked through :meth:`Presenter.handle`: it decides whether it draws the entry
214
+ and hands it to the next presenter when it does not. The chain is ordered by :attr:`Presenter.priority` and then
215
+ by registration order, later first. The registry only starts the chain; it never matches an entry itself.
221
216
  """
222
217
 
223
218
  def __init__(self, presenters: Iterable[Presenter[Any]]) -> None:
224
- """Register presenters in order; for one kind or class, the strongest claim wins."""
225
- self._by_kind: dict[EntryKind, tuple[int, int, Presenter[Any]]] = {}
226
- self._by_type: dict[type[Entry], tuple[int, int, Presenter[Any]]] = {}
227
- for order, presenter in enumerate(presenters):
228
- rank = (presenter.priority, order)
229
- if presenter.entry_type is not None:
230
- current = self._by_type.get(presenter.entry_type)
231
- if current is None or rank >= current[:2]:
232
- self._by_type[presenter.entry_type] = (*rank, presenter)
233
- else:
234
- current = self._by_kind.get(presenter.kind)
235
- if current is None or rank >= current[:2]:
236
- self._by_kind[presenter.kind] = (*rank, presenter)
219
+ """Build the chain; a later registration wins at equal priority."""
220
+ self._registered = tuple(presenters)
221
+ ordered = sorted(enumerate(self._registered), key=lambda pair: (pair[1].priority, pair[0]), reverse=True)
222
+ self._chain: tuple[Presenter[Any], ...] = tuple(presenter for _, presenter in ordered)
237
223
 
238
224
  @property
239
225
  def presenters(self) -> tuple[Presenter[Any], ...]:
240
- """Return the presenters that draw something: the kinds' first, then the families'.
226
+ """Return the presenters in registration order, which is what a new registry is built from."""
227
+ return self._registered
241
228
 
242
- A family a kind's presenter has outranked is left out: it would never be asked for a row.
243
- """
244
- live = [presenter for _, _, presenter in self._by_kind.values()]
245
- live.extend(
246
- presenter
247
- for priority, order, presenter in self._by_type.values()
248
- if self._by_kind.get(presenter.kind) is None or (priority, order) > self._by_kind[presenter.kind][:2]
249
- )
250
- return tuple(live)
229
+ @property
230
+ def chain(self) -> tuple[Presenter[Any], ...]:
231
+ """Return the presenters in the order the chain asks them."""
232
+ return self._chain
251
233
 
252
234
  def replacing(self, *presenters: Presenter[Any]) -> Presenters:
253
- """Return a registry that draws the given kinds, and families, with the given presenters."""
254
- return Presenters((*self.presenters, *presenters))
235
+ """Return a registry that appends the given presenters to this chain."""
236
+ return Presenters((*self._registered, *presenters))
255
237
 
256
238
  def for_entry(self, entry: Entry) -> Presenter[Any]:
257
- """Return the presenter for an entry: the strongest claim on its class or kind, else say so."""
258
- family: tuple[int, int, Presenter[Any]] | None = None
259
- for cls in type(entry).__mro__:
260
- found = self._by_type.get(cls)
261
- if found is not None:
262
- family = found
263
- break
264
- kind = self._by_kind.get(entry.kind)
265
- if family is not None and (kind is None or family[:2] > kind[:2]):
266
- return family[2]
267
- if kind is None:
239
+ """Ask the chain, presenter by presenter, which one draws the entry, else raise.
240
+
241
+ The registry only starts the chain: each presenter's :meth:`Presenter.handle` decides whether it claims
242
+ the entry and hands it to the next when it does not.
243
+ """
244
+ chain = self._chain
245
+ if not chain:
268
246
  raise ValueError(f"No presenter for a {entry.kind.value} row")
269
- return kind[2]
247
+
248
+ def ask(current: Entry, index: int) -> Presenter[Any]:
249
+ if index == len(chain):
250
+ raise ValueError(f"No presenter for a {current.kind.value} row")
251
+ return chain[index].handle(current, lambda entry: ask(entry, index + 1))
252
+
253
+ return ask(entry, 0)
270
254
 
271
255
  def lines(self, entry: Entry, width: int, theme: Theme, frame: int) -> list[TextLine]:
272
256
  """Return the rows an entry paints, rendered now and not cached."""
@@ -0,0 +1,33 @@
1
+ """Re-exports the page-facing service contracts the engine defines.
2
+
3
+ The engine owns the names and the ABCs (they are implemented by engine objects, and the engine cannot import this
4
+ package); this module is the one a plugin imports, so a page depends on the contract rather than on the engine path.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from zettcode.engine.service_contracts import (
10
+ CONTEXT_PAGE,
11
+ CONTEXT_REPORT,
12
+ SESSION_STORE,
13
+ USAGE_LEDGER,
14
+ ContextPageService,
15
+ ContextPageSnapshot,
16
+ ContextPageWriter,
17
+ ContextReportService,
18
+ SessionStoreService,
19
+ UsageLedgerService,
20
+ )
21
+
22
+ __all__ = [
23
+ "CONTEXT_PAGE",
24
+ "CONTEXT_REPORT",
25
+ "SESSION_STORE",
26
+ "USAGE_LEDGER",
27
+ "ContextPageService",
28
+ "ContextPageSnapshot",
29
+ "ContextPageWriter",
30
+ "ContextReportService",
31
+ "SessionStoreService",
32
+ "UsageLedgerService",
33
+ ]
@@ -33,7 +33,7 @@ UiRegion: TypeAlias = Literal["header", "status"]
33
33
  #: registration method it calls, or the builtin segment it replaces, does.
34
34
  UiSide: TypeAlias = Literal["left", "right"]
35
35
 
36
- #: What one segment paints: a styled line, a plain string, a :class:`~zettcode.statusline.Row` of
36
+ #: What one segment paints: a styled line, a plain string, a :class:`~zettcode.plugin_api.rows.Row` of
37
37
  #: named parts the reader may switch off, or nothing. The alias quotes its members because
38
38
  #: :class:`TextLine` is imported only for the checker; an alias evaluates eagerly, so the names must stay strings.
39
39
  UiSegmentValue: TypeAlias = "TextLine | Row | str | None"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: zettcode-plugin-api
3
- Version: 0.4.1
3
+ Version: 0.4.2
4
4
  Summary: The one package a ZettCode plugin depends on: the plugin surface, re-exported
5
5
  Project-URL: Repository, https://github.com/Chang-LeHung/zettcode
6
6
  Author: Chang-LeHung
@@ -0,0 +1,17 @@
1
+ zettcode/plugin_api/__init__.py,sha256=ZCa4G0yuS9lm8Wf5EylpNUvgdnRmD4MEi2Uak7zf9dM,12458
2
+ zettcode/plugin_api/commands.py,sha256=jjsMojsNqEfvBmICgYWiuptsSSh53DpC3JV1ecy5xQM,19276
3
+ zettcode/plugin_api/entries.py,sha256=6wZGE-haprSsvIN-zM8xBkJrwHh2tAI6bGBABnqLKCw,14831
4
+ zettcode/plugin_api/host.py,sha256=LT38RB4ipgn05SiupkoBol9B9J57RCSXW1vqK5sahQE,1476
5
+ zettcode/plugin_api/mixins.py,sha256=v1tdhi9I8rLwQJrj--3DUDWVQmInS87EA9QQ13yrf-8,3083
6
+ zettcode/plugin_api/panels.py,sha256=rtcu27JhfLSKlw5PDik-Gm1FIVRca25ecxqHEsvE41o,1729
7
+ zettcode/plugin_api/pets.py,sha256=q2opVI1psQqphjU0pSdAxhPmGwIHpDw_zgSZGFZVH9A,9858
8
+ zettcode/plugin_api/plugin.py,sha256=rF6BLQkEszdK6sD72a2qfDF7g4986nreIgoLMfJF_RU,4150
9
+ zettcode/plugin_api/presenters.py,sha256=45fKS_wd5at8uJuEL8rf554gdZRU6ab2fDNmI-4Cvw8,12550
10
+ zettcode/plugin_api/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ zettcode/plugin_api/rows.py,sha256=d4EgXCNX17fS3UoUOC3n2rv_hDdel36o71iS_Xstr-o,3574
12
+ zettcode/plugin_api/services.py,sha256=sdDSS5_9dQdFIxwcn8cfDrEB_ZPwu_b92dvhc9xZ8b8,870
13
+ zettcode/plugin_api/state.py,sha256=vDBs5wSDins4lHPNc6DGsxMxtdcD7aF4M6XGufod_S0,9188
14
+ zettcode/plugin_api/switch.py,sha256=r2ayyIY2E6a8lns3Dy6uwR7HL-6P9jmRIPEBpsrv_p4,3819
15
+ zettcode_plugin_api-0.4.2.dist-info/METADATA,sha256=NEiNboyjkVPGwnv16Z2hAyrkGA8WiLqPN8vYg5MjlHw,832
16
+ zettcode_plugin_api-0.4.2.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
17
+ zettcode_plugin_api-0.4.2.dist-info/RECORD,,
@@ -1,16 +0,0 @@
1
- zettcode/plugin_api/__init__.py,sha256=2kP0ceBF5fzi0AB5aXwHlbo_i4uXCVHNEj2ZuUc_brw,9644
2
- zettcode/plugin_api/commands.py,sha256=Ku0aHPOVQjp79W8p8FMFhKjbmMUmgB9TMCQIOIyyTlE,17956
3
- zettcode/plugin_api/entries.py,sha256=taGQFPzwj-UBchOBLrDGpRjUHGIj8qSma1UH6A_FV24,14761
4
- zettcode/plugin_api/host.py,sha256=LT38RB4ipgn05SiupkoBol9B9J57RCSXW1vqK5sahQE,1476
5
- zettcode/plugin_api/mixins.py,sha256=v1tdhi9I8rLwQJrj--3DUDWVQmInS87EA9QQ13yrf-8,3083
6
- zettcode/plugin_api/panels.py,sha256=rtcu27JhfLSKlw5PDik-Gm1FIVRca25ecxqHEsvE41o,1729
7
- zettcode/plugin_api/pets.py,sha256=q2opVI1psQqphjU0pSdAxhPmGwIHpDw_zgSZGFZVH9A,9858
8
- zettcode/plugin_api/plugin.py,sha256=rF6BLQkEszdK6sD72a2qfDF7g4986nreIgoLMfJF_RU,4150
9
- zettcode/plugin_api/presenters.py,sha256=EeNlDVjqUaQW4VtwEy1FVA8sWTwDwKoddYr5yK7iIJ4,13686
10
- zettcode/plugin_api/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
- zettcode/plugin_api/rows.py,sha256=d4EgXCNX17fS3UoUOC3n2rv_hDdel36o71iS_Xstr-o,3574
12
- zettcode/plugin_api/state.py,sha256=FvhkdTvI_Kza0tqkFycSe8teHCD_-fpOT6HopXnlHZM,9183
13
- zettcode/plugin_api/switch.py,sha256=r2ayyIY2E6a8lns3Dy6uwR7HL-6P9jmRIPEBpsrv_p4,3819
14
- zettcode_plugin_api-0.4.1.dist-info/METADATA,sha256=uzgElZqENd_TNy_rmjNn8718_A8CKW9Xbfj6T6Z27DQ,832
15
- zettcode_plugin_api-0.4.1.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
16
- zettcode_plugin_api-0.4.1.dist-info/RECORD,,