zettcode-files 0.4.1__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.
@@ -0,0 +1,11 @@
1
+ """The files of the workspace as ``@`` references: the provider and the plugin that registers it.
2
+
3
+ A part of the program that lives in a distribution of its own, found through the ``zettcode.plugin`` entry point.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from .mentions import FileMentions
9
+ from .plugin import FileResources
10
+
11
+ __all__ = ["FileMentions", "FileResources"]
@@ -0,0 +1,168 @@
1
+ """The ``@`` provider for the files of the workspace."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from collections.abc import Callable, Sequence
7
+ from heapq import nsmallest
8
+ from pathlib import Path
9
+ from time import monotonic
10
+ from typing import TypeAlias
11
+
12
+ from zettcode.mention import Mention, MentionProvider
13
+
14
+ __all__ = ["LISTING_SECONDS", "MAX_CANDIDATES", "MAX_FILES", "SKIP_DIRECTORIES", "FileMentions"]
15
+
16
+ #: Directories ``@`` never offers files from: version control, the dependency
17
+ #: trees, and the caches. They are where a repository keeps things nobody wants
18
+ #: to reference in a message, and walking them is most of what a listing costs.
19
+ SKIP_DIRECTORIES = frozenset(
20
+ {
21
+ ".git",
22
+ ".hg",
23
+ ".svn",
24
+ ".venv",
25
+ "venv",
26
+ "node_modules",
27
+ "__pycache__",
28
+ ".mypy_cache",
29
+ ".pytest_cache",
30
+ ".ruff_cache",
31
+ ".tox",
32
+ "site-packages",
33
+ "dist",
34
+ "build",
35
+ "target",
36
+ }
37
+ )
38
+
39
+ #: Files offered in one listing. A repository larger than this is listed up to
40
+ #: here rather than not at all, and the walk stops as soon as the cap is reached.
41
+ MAX_FILES = 4000
42
+
43
+ #: Candidates returned for one query. The menu shows a handful; the rest would be
44
+ #: scrolled past, and building them on every keystroke is the cost this bounds.
45
+ MAX_CANDIDATES = 40
46
+
47
+ #: How long a listing is reused. Long enough that typing a path walks the tree
48
+ #: once instead of once a keystroke, short enough that a file made in the
49
+ #: meantime shows up in the menu.
50
+ LISTING_SECONDS = 15.0
51
+
52
+ #: One listed file: its path, the path lowercased, and its name lowercased. Built
53
+ #: once per walk, because the menu is filtered again on every keystroke and a
54
+ #: listing of thousands would otherwise be lowercased and split every time.
55
+ ListedFile: TypeAlias = tuple[str, str, str]
56
+
57
+
58
+ class FileMentions(MentionProvider):
59
+ """Files in the workspace, so a draft can point at one by path.
60
+
61
+ The listing is not the completeness of ``ls``: it is what a reader is likely to
62
+ type after an ``@``, so the version control and dependency directories are
63
+ skipped and there is a cap. What a token expands to is a sentence naming the
64
+ path, not the file's contents: the model has ``read_file``, and inlining a
65
+ file nobody needs would spend context on it.
66
+ """
67
+
68
+ kind = "file"
69
+
70
+ def __init__(self, workspace: Path, *, clock: Callable[[], float] = monotonic) -> None:
71
+ """List ``workspace`` when something first asks, and remember it for a while.
72
+
73
+ Args:
74
+ workspace: Directory to walk; the paths offered are relative to it.
75
+ clock: Time source, injectable so a test can age the listing.
76
+ """
77
+ self.workspace = workspace
78
+ self._clock = clock
79
+ self._files: tuple[ListedFile, ...] = ()
80
+ self._listed_at: float | None = None
81
+
82
+ def _listing(self) -> tuple[ListedFile, ...]:
83
+ """Return the workspace's files, walking it again when the listing is stale.
84
+
85
+ The walk happens on the first keystroke after an ``@``, so it is bounded three
86
+ ways: the directories nobody references are never entered, the walk stops at
87
+ :data:`MAX_FILES`, and the result is reused for :data:`LISTING_SECONDS`.
88
+ """
89
+ now = self._clock()
90
+ if self._listed_at is not None and now - self._listed_at < LISTING_SECONDS:
91
+ return self._files
92
+ found: list[ListedFile] = []
93
+ for root, directories, names in os.walk(self.workspace):
94
+ directories[:] = sorted(
95
+ name for name in directories if name not in SKIP_DIRECTORIES and not name.startswith(".")
96
+ )
97
+ base = Path(root)
98
+ for name in sorted(names):
99
+ path = (base / name).relative_to(self.workspace).as_posix()
100
+ found.append((path, path.lower(), name.lower()))
101
+ if len(found) >= MAX_FILES:
102
+ break
103
+ if len(found) >= MAX_FILES:
104
+ break
105
+ self._files = tuple(found)
106
+ self._listed_at = now
107
+ return self._files
108
+
109
+ def candidates(self, query: str) -> Sequence[Mention]:
110
+ """Offer the files whose path matches ``query``, the likeliest ones first.
111
+
112
+ A path that starts with what was typed, or whose file name does, comes
113
+ before one that merely contains it: ``@cli`` should offer
114
+ ``cli.py`` before ``src/zettcode/tui/render/cli_helper.py``.
115
+ """
116
+ lowered = query.lower()
117
+ ranked: list[tuple[int, int, str]] = []
118
+ for path, path_lower, name_lower in self._listing():
119
+ if path_lower.startswith(lowered) or name_lower.startswith(lowered):
120
+ rank = 0
121
+ elif lowered in path_lower:
122
+ rank = 1
123
+ else:
124
+ continue
125
+ ranked.append((rank, len(path), path))
126
+ # Only the rows that will be shown are ordered: a one-letter query can match a
127
+ # listing of thousands in hundreds of places, and sorting all of those is work
128
+ # the menu never shows.
129
+ return tuple(
130
+ Mention(token=f"@{path}", kind=self.kind, name=path, description=described)
131
+ for _, _, path in nsmallest(MAX_CANDIDATES, ranked)
132
+ if (described := self._describe(path)) is not None
133
+ )
134
+
135
+ def _describe(self, path: str) -> str | None:
136
+ """Return the menu's one line for a file: how big it is, or ``None`` if it is gone.
137
+
138
+ Asking the file also drops one that was deleted since the listing, which is
139
+ the one way a cached listing goes wrong while it is being typed into.
140
+ """
141
+ try:
142
+ size = (self.workspace / path).stat().st_size
143
+ except OSError:
144
+ return None
145
+ if size < 1024:
146
+ return f"{size} B"
147
+ if size < 1024 * 1024:
148
+ return f"{size / 1024:.1f} kB"
149
+ return f"{size / (1024 * 1024):.1f} MB"
150
+
151
+ def expand(self, name: str) -> str | None:
152
+ """Name the path for the model, which reads it with a tool if it matters.
153
+
154
+ A directory is named as one, because the tool to reach for is not
155
+ ``read_file`` and saying so is cheaper than the model finding out.
156
+ """
157
+ target = self.workspace / name
158
+ if target.is_dir():
159
+ return (
160
+ f'The reader pointed at the directory "{name}" in the workspace; '
161
+ "list or search it with the file tools when it matters."
162
+ )
163
+ if not target.is_file():
164
+ return None
165
+ return (
166
+ f'The reader pointed at the file "{name}" in the workspace; '
167
+ "read it before answering when its contents matter."
168
+ )
@@ -0,0 +1,23 @@
1
+ """The plugin that offers the workspace's files as ``@`` references."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from zettcode.engine.plugins.context import PluginContext
6
+ from zettcode.engine.plugins.plugin import Plugin
7
+
8
+ from .mentions import FileMentions
9
+
10
+
11
+ class FileResources(Plugin[PluginContext]):
12
+ """The ``@`` resource provider for the workspace's own files.
13
+
14
+ A name a skill (``zettcode-skills``) also offers is the skill: a repository holds a file called ``test`` and a
15
+ skill is the more specific thing to mean, which the skills provider says with its ``precedence``. Nothing turns
16
+ this off, because a workspace always has files.
17
+ """
18
+
19
+ name = "files"
20
+
21
+ def activate(self, ctx: PluginContext) -> None:
22
+ """Offer the workspace's files as ``@`` resources."""
23
+ ctx.agent.register_mention(FileMentions(ctx.config.workspace))
File without changes
@@ -0,0 +1,18 @@
1
+ Metadata-Version: 2.5
2
+ Name: zettcode-files
3
+ Version: 0.4.1
4
+ Summary: The files of a ZettCode workspace as @ references, as a part of the agent that lives apart from it
5
+ Project-URL: Repository, https://github.com/Chang-LeHung/zettcode
6
+ Author: Chang-LeHung
7
+ License-Expression: MIT
8
+ Keywords: agent,files,zettcode
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Programming Language :: Python :: 3.15
16
+ Classifier: Typing :: Typed
17
+ Requires-Python: >=3.10
18
+ Requires-Dist: zettcode-engine
@@ -0,0 +1,8 @@
1
+ zettcode/files/__init__.py,sha256=v4o80d7HN_s7R7m9L-FsV6uTwSNxq5E9bctNnqy0M-c,368
2
+ zettcode/files/mentions.py,sha256=tOb_JFMk4BXTKUZGluzLC5PRB3d8fZyllN1O0CrmoUM,6719
3
+ zettcode/files/plugin.py,sha256=ge9samd4WVZgFnAKIcDPx_6lWpnfTuervSFGr_-NFWU,863
4
+ zettcode/files/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ zettcode_files-0.4.1.dist-info/METADATA,sha256=dodX_L2zkebq5l4PyhtUOMpR8BnsqSKFxvCnMbPHTds,753
6
+ zettcode_files-0.4.1.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
7
+ zettcode_files-0.4.1.dist-info/entry_points.txt,sha256=boJP8VNrmcQNs5zY792dBICLkhATFo9_6midgR6Q2lo,62
8
+ zettcode_files-0.4.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [zettcode.plugin]
2
+ files = zettcode.files.plugin:FileResources