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.
@@ -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"
@@ -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