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.
- zettcode/files/__init__.py +11 -0
- zettcode/files/mentions.py +168 -0
- zettcode/files/plugin.py +23 -0
- zettcode/files/py.typed +0 -0
- zettcode_files-0.4.1.dist-info/METADATA +18 -0
- zettcode_files-0.4.1.dist-info/RECORD +8 -0
- zettcode_files-0.4.1.dist-info/WHEEL +4 -0
- zettcode_files-0.4.1.dist-info/entry_points.txt +2 -0
|
@@ -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
|
+
)
|
zettcode/files/plugin.py
ADDED
|
@@ -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))
|
zettcode/files/py.typed
ADDED
|
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,,
|