datasheet-lens 0.1.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.
- datasheet_lens/__init__.py +9 -0
- datasheet_lens/agents.py +129 -0
- datasheet_lens/cli.py +914 -0
- datasheet_lens/config.py +14 -0
- datasheet_lens/distributors.py +316 -0
- datasheet_lens/elements.py +330 -0
- datasheet_lens/errors.py +6 -0
- datasheet_lens/fetch.py +136 -0
- datasheet_lens/fileutil.py +38 -0
- datasheet_lens/identify.py +435 -0
- datasheet_lens/indexer.py +407 -0
- datasheet_lens/keys.py +188 -0
- datasheet_lens/knowledge.py +125 -0
- datasheet_lens/links.py +50 -0
- datasheet_lens/pages.py +173 -0
- datasheet_lens/pdflock.py +26 -0
- datasheet_lens/pdftext.py +198 -0
- datasheet_lens/project.py +1401 -0
- datasheet_lens/server.py +1005 -0
- datasheet_lens/setup.py +125 -0
- datasheet_lens/store.py +537 -0
- datasheet_lens/version_check.py +218 -0
- datasheet_lens-0.1.0.dist-info/METADATA +182 -0
- datasheet_lens-0.1.0.dist-info/RECORD +27 -0
- datasheet_lens-0.1.0.dist-info/WHEEL +4 -0
- datasheet_lens-0.1.0.dist-info/entry_points.txt +3 -0
- datasheet_lens-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""datasheet-lens: an index of component datasheets for LLM agents.
|
|
2
|
+
|
|
3
|
+
The tool does not convert the content of a datasheet. It finds the content (outline,
|
|
4
|
+
captions, element map, text search) and gives the original pages to the model.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
__version__ = "0.1.0"
|
|
8
|
+
# The same URL is in pyproject.toml ([project.urls]): change the two together.
|
|
9
|
+
PROJECT_URL = "https://github.com/turbyho/datasheet-lens"
|
datasheet_lens/agents.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""The datasheet-lens block in the instruction files of agents in a project.
|
|
2
|
+
|
|
3
|
+
An LLM agent reads AGENTS.md or CLAUDE.md of a project. project init puts a block
|
|
4
|
+
between the markers BEGIN and END into each of these files that exists. A second
|
|
5
|
+
run changes the block only when its text is different (for example from an older
|
|
6
|
+
version of datasheet-lens). The text outside the markers does not change. The
|
|
7
|
+
tool makes no new instruction file: the files belong to the user.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from .fileutil import write_atomic
|
|
16
|
+
|
|
17
|
+
BEGIN = "<!-- datasheet-lens -->"
|
|
18
|
+
END = "<!-- /datasheet-lens -->"
|
|
19
|
+
FILES = ("AGENTS.md", "CLAUDE.md")
|
|
20
|
+
|
|
21
|
+
TEXT = """\
|
|
22
|
+
## datasheet-lens — component datasheets
|
|
23
|
+
|
|
24
|
+
The parts of this project and their documents are in `datasheet-lens.toml`. The
|
|
25
|
+
copies of the documents are in `{folder}/`. datasheet-lens gives the content of
|
|
26
|
+
these datasheets to an agent: the MCP server `datasheet-lens`, or the CLI
|
|
27
|
+
`datasheet-lens`.
|
|
28
|
+
|
|
29
|
+
- Read each value (limits, pins, register bits, timing) from the original page:
|
|
30
|
+
get_page_image with an element ID, a caption ("Table 57", all pages of the
|
|
31
|
+
table) or a region. Do not use a value from the text layer alone or from
|
|
32
|
+
memory: the text layer can lose signs, units and table structure.
|
|
33
|
+
- Find the content step by step: list_documents, get_index, then search_text
|
|
34
|
+
(it gives the table or the region of each hit) or get_page_map, then
|
|
35
|
+
get_page_image.
|
|
36
|
+
- Before you read a value, look for it with find_facts. Save a value that the
|
|
37
|
+
work uses with save_fact, with its page and element.
|
|
38
|
+
- `datasheet-lens.toml` must hold the same parts as the CAD project (schematic
|
|
39
|
+
and BOM). This consistency is critical. Get the parts from the CAD tool (its
|
|
40
|
+
MCP server or its BOM export) and compare them: project_check (CLI
|
|
41
|
+
`datasheet-lens check BOM.csv`). It gives the missing, extra and different
|
|
42
|
+
parts with the next step. project_status (CLI `datasheet-lens ls`) gives the
|
|
43
|
+
parts and documents of the file. If you cannot get a datasheet, tell the
|
|
44
|
+
user, with the search links of part_links (CLI `datasheet-lens links MPN`;
|
|
45
|
+
also for symbols, footprints and 3D models). Tell the user about each different or extra part, and when the MPN of
|
|
46
|
+
the CAD project and the datasheet do not agree. Remove a part only when the
|
|
47
|
+
user agrees: project_remove_part (CLI `datasheet-lens rm MPN`). The MCP server
|
|
48
|
+
instructions give the full procedure.
|
|
49
|
+
- To add a datasheet: add_document with a file or a URL and the base part of the
|
|
50
|
+
datasheet (for example TLV755P). Then project_add_part with the concrete
|
|
51
|
+
orderable MPN of the BOM (for example TLV75533PDBVR; get_ordering_codes, CLI
|
|
52
|
+
`datasheet-lens codes DOC`, gives the codes) and base. CLI:
|
|
53
|
+
`datasheet-lens add PATH|URL -m MPN`.
|
|
54
|
+
- project_availability (CLI `datasheet-lens stock`) gives the lifecycle state
|
|
55
|
+
and the stock of each part at the distributors; tell the user about each
|
|
56
|
+
warning. search_distributors (CLI `datasheet-lens find MPN`) gives the
|
|
57
|
+
concrete orderable MPNs of a part. They ask the enabled distributors
|
|
58
|
+
(distributor_status; the user sets them with `datasheet-lens dist`, globally
|
|
59
|
+
or with `--local` for the project). Each needs a personal API key, which the
|
|
60
|
+
user saves with `datasheet-lens key mouser`: give the user each note about a
|
|
61
|
+
missing key.
|
|
62
|
+
- To change the MPN, the base part or the category of a part:
|
|
63
|
+
project_update_part (CLI `datasheet-lens set MPN -m NEW`), not remove and add.
|
|
64
|
+
- project_sync (CLI `datasheet-lens sync`) keeps `datasheet-lens.toml` and
|
|
65
|
+
`{folder}/` current. Do not change the copies by hand. Then project_check
|
|
66
|
+
again: it must give no missing, extra or different part.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def block(folder: str) -> str:
|
|
71
|
+
"""The block for a project with this document folder, with its markers."""
|
|
72
|
+
return f"{BEGIN}\n{TEXT.format(folder=folder).rstrip()}\n{END}"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# A marker is a line of its own: a mention of a marker in a sentence or in
|
|
76
|
+
# inline code is not a marker.
|
|
77
|
+
BEGIN_RE = re.compile(r"(?m)^" + re.escape(BEGIN) + r"[ \t]*$")
|
|
78
|
+
END_RE = re.compile(r"(?m)^" + re.escape(END) + r"[ \t]*$")
|
|
79
|
+
# CLAUDE.md that imports AGENTS.md ("@AGENTS.md", "@./AGENTS.md"), outside code blocks.
|
|
80
|
+
INCLUDE_RE = re.compile(r"(?m)(^|\s)@(\./)?AGENTS\.md\b")
|
|
81
|
+
FENCE_RE = re.compile(r"(?ms)^(```|~~~).*?^\1")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def update(root: Path, folder: str) -> list[tuple[str, str]]:
|
|
85
|
+
"""Put the current block into AGENTS.md and CLAUDE.md of the project root, if
|
|
86
|
+
the file exists. Give (file name, what happened) for each file: added,
|
|
87
|
+
updated, current, or a reason why the file did not change. A file that is a
|
|
88
|
+
symbolic link to the other one is done one time."""
|
|
89
|
+
results: list[tuple[str, str]] = []
|
|
90
|
+
done: dict[Path, str] = {} # real path -> the action
|
|
91
|
+
for name in FILES:
|
|
92
|
+
path = root / name
|
|
93
|
+
if not path.is_file():
|
|
94
|
+
continue
|
|
95
|
+
real = path.resolve()
|
|
96
|
+
if real in done:
|
|
97
|
+
results.append((name, f"{done[real]} (the same file as {FILES[0]})"))
|
|
98
|
+
continue
|
|
99
|
+
try:
|
|
100
|
+
action = _update_file(path, folder, root, results)
|
|
101
|
+
except (OSError, UnicodeDecodeError) as e:
|
|
102
|
+
action = f"not changed: {e}"
|
|
103
|
+
done[real] = action
|
|
104
|
+
results.append((name, action))
|
|
105
|
+
return results
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _update_file(path: Path, folder: str, root: Path, results: list[tuple[str, str]]) -> str:
|
|
109
|
+
text = path.read_bytes().decode("utf-8") # keep the line ends of the file (CRLF or LF)
|
|
110
|
+
# The line ends of most lines.
|
|
111
|
+
newline = "\r\n" if text.count("\r\n") * 2 > text.count("\n") else "\n"
|
|
112
|
+
wanted = block(folder).replace("\n", newline)
|
|
113
|
+
begin = BEGIN_RE.search(text)
|
|
114
|
+
agents_ok = any(n == "AGENTS.md" and a in ("added", "updated", "current") for n, a in results)
|
|
115
|
+
if path.name == "CLAUDE.md" and begin is None and agents_ok \
|
|
116
|
+
and INCLUDE_RE.search(FENCE_RE.sub("", text)):
|
|
117
|
+
return "not changed: it includes AGENTS.md, which has the block"
|
|
118
|
+
if begin is not None:
|
|
119
|
+
end = END_RE.search(text, begin.end())
|
|
120
|
+
if end is None:
|
|
121
|
+
return f"not changed: {BEGIN} has no {END} line"
|
|
122
|
+
if text[begin.start():end.end()] == wanted:
|
|
123
|
+
return "current"
|
|
124
|
+
new, action = text[:begin.start()] + wanted + text[end.end():], "updated"
|
|
125
|
+
else:
|
|
126
|
+
base = text.rstrip("\r\n")
|
|
127
|
+
new, action = (base + newline * 2 if base else "") + wanted + newline, "added"
|
|
128
|
+
write_atomic(path, new.encode("utf-8"))
|
|
129
|
+
return action
|