drydock-cli 3.0.61__tar.gz → 3.0.62__tar.gz
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.
- {drydock_cli-3.0.61/drydock_cli.egg-info → drydock_cli-3.0.62}/PKG-INFO +7 -4
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/README.md +4 -3
- drydock_cli-3.0.62/drydock/extract.py +94 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/drydock/graphrag.py +13 -5
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/drydock/tuning.py +1 -1
- {drydock_cli-3.0.61 → drydock_cli-3.0.62/drydock_cli.egg-info}/PKG-INFO +7 -4
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/drydock_cli.egg-info/SOURCES.txt +2 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/drydock_cli.egg-info/requires.txt +3 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/pyproject.toml +4 -1
- drydock_cli-3.0.62/tests/test_extract.py +71 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/LICENSE +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/NOTICE +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/drydock/__init__.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/drydock/__main__.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/drydock/agent.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/drydock/cli.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/drydock/compaction.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/drydock/config.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/drydock/detect.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/drydock/gittools.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/drydock/guards.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/drydock/mcp.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/drydock/providers.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/drydock/skills.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/drydock/tools/__init__.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/drydock/tui/app.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/drydock/tui/widgets.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/drydock/web.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/setup.cfg +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/tests/test_approval.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/tests/test_back_command.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/tests/test_bash_output_bounding.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/tests/test_compact_command.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/tests/test_compaction.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/tests/test_config.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/tests/test_config_migration.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/tests/test_detect.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/tests/test_dispatch.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/tests/test_failure_loop.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/tests/test_gittools.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/tests/test_mcp.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/tests/test_read_index.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/tests/test_skills.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/tests/test_stop.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/tests/test_subagent.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/tests/test_system_prompt_help.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/tests/test_todo.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/tests/test_tui.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/tests/test_tuning.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/tests/test_vision_input.py +0 -0
- {drydock_cli-3.0.61 → drydock_cli-3.0.62}/tests/test_web_tools.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: drydock-cli
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.62
|
|
4
4
|
Summary: Drydock — a local, provider-agnostic terminal coding agent for local LLMs
|
|
5
5
|
Author: Frank Bobe III
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -18,6 +18,8 @@ Requires-Dist: pytest; extra == "dev"
|
|
|
18
18
|
Requires-Dist: pytest-timeout; extra == "dev"
|
|
19
19
|
Requires-Dist: ruff; extra == "dev"
|
|
20
20
|
Requires-Dist: pyright; extra == "dev"
|
|
21
|
+
Provides-Extra: pdf
|
|
22
|
+
Requires-Dist: pypdf>=4; extra == "pdf"
|
|
21
23
|
Dynamic: license-file
|
|
22
24
|
|
|
23
25
|
# ⚓ Drydock
|
|
@@ -103,9 +105,10 @@ Typed into the prompt. The agent also knows these, so you can just **ask it**
|
|
|
103
105
|
|
|
104
106
|
Once built, the agent **automatically** retrieves from it (read-only `Knowledge`
|
|
105
107
|
tool) when a question touches your material. Ingests text formats
|
|
106
|
-
(`.md .txt .py .js .json .yaml .sql …`)
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
(`.md .txt .py .js .json .yaml .sql …`) **plus PDF and Word (`.docx`)**. `.docx`
|
|
109
|
+
needs nothing extra; PDF uses the `pdftotext` binary (poppler) if present, else
|
|
110
|
+
`pip install drydock-cli[pdf]` (pypdf). The index is a single JSON at
|
|
111
|
+
`<project>/.drydock/graphrag.json` — clean-room, no embeddings.
|
|
109
112
|
|
|
110
113
|
### Custom skills
|
|
111
114
|
|
|
@@ -81,9 +81,10 @@ Typed into the prompt. The agent also knows these, so you can just **ask it**
|
|
|
81
81
|
|
|
82
82
|
Once built, the agent **automatically** retrieves from it (read-only `Knowledge`
|
|
83
83
|
tool) when a question touches your material. Ingests text formats
|
|
84
|
-
(`.md .txt .py .js .json .yaml .sql …`)
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
(`.md .txt .py .js .json .yaml .sql …`) **plus PDF and Word (`.docx`)**. `.docx`
|
|
85
|
+
needs nothing extra; PDF uses the `pdftotext` binary (poppler) if present, else
|
|
86
|
+
`pip install drydock-cli[pdf]` (pypdf). The index is a single JSON at
|
|
87
|
+
`<project>/.drydock/graphrag.json` — clean-room, no embeddings.
|
|
87
88
|
|
|
88
89
|
### Custom skills
|
|
89
90
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Document text extraction for GraphRAG ingestion — PDF + Word (.docx).
|
|
2
|
+
|
|
3
|
+
Kept clean-room and dependency-light:
|
|
4
|
+
• .docx is a zip of XML, so we read it with STDLIB only (zipfile + ElementTree)
|
|
5
|
+
— no dependency.
|
|
6
|
+
• .pdf uses whatever the system already has: the `pdftotext` binary (poppler)
|
|
7
|
+
if present, else the optional `pypdf` package (`pip install drydock-cli[pdf]`).
|
|
8
|
+
|
|
9
|
+
Returns None when a document can't be read (unsupported, corrupt, or no PDF
|
|
10
|
+
backend available) so ingestion skips it cleanly instead of failing.
|
|
11
|
+
|
|
12
|
+
All logic original to Drydock.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import subprocess
|
|
17
|
+
import xml.etree.ElementTree as ET
|
|
18
|
+
import zipfile
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
# Extensions we can pull text out of (beyond the plain-text formats GraphRAG
|
|
22
|
+
# already reads directly).
|
|
23
|
+
EXTRACTABLE_EXT = {".pdf", ".docx"}
|
|
24
|
+
|
|
25
|
+
_W = "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _extract_docx(path: str | Path) -> str | None:
|
|
29
|
+
"""Pull paragraph text from a .docx (stdlib only). Each <w:p> is a paragraph;
|
|
30
|
+
its text lives in <w:t> runs."""
|
|
31
|
+
try:
|
|
32
|
+
with zipfile.ZipFile(path) as z:
|
|
33
|
+
xml = z.read("word/document.xml")
|
|
34
|
+
except (zipfile.BadZipFile, KeyError, OSError):
|
|
35
|
+
return None
|
|
36
|
+
try:
|
|
37
|
+
root = ET.fromstring(xml)
|
|
38
|
+
except ET.ParseError:
|
|
39
|
+
return None
|
|
40
|
+
paras: list[str] = []
|
|
41
|
+
for p in root.iter(f"{_W}p"):
|
|
42
|
+
runs = [t.text for t in p.iter(f"{_W}t") if t.text]
|
|
43
|
+
if runs:
|
|
44
|
+
paras.append("".join(runs))
|
|
45
|
+
text = "\n\n".join(paras).strip()
|
|
46
|
+
return text or None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _extract_pdf(path: str | Path) -> str | None:
|
|
50
|
+
"""Extract text from a PDF: try the `pdftotext` binary first, then `pypdf`.
|
|
51
|
+
Returns None if neither is available or the PDF has no extractable text."""
|
|
52
|
+
# 1) pdftotext (poppler) — fast and common on Linux/macOS.
|
|
53
|
+
try:
|
|
54
|
+
r = subprocess.run(
|
|
55
|
+
["pdftotext", "-q", str(path), "-"],
|
|
56
|
+
capture_output=True, text=True, timeout=120,
|
|
57
|
+
)
|
|
58
|
+
if r.returncode == 0 and r.stdout.strip():
|
|
59
|
+
return r.stdout
|
|
60
|
+
except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
|
|
61
|
+
pass
|
|
62
|
+
# 2) pypdf (optional dependency: drydock-cli[pdf]).
|
|
63
|
+
try:
|
|
64
|
+
import pypdf
|
|
65
|
+
|
|
66
|
+
reader = pypdf.PdfReader(str(path))
|
|
67
|
+
text = "\n\n".join((pg.extract_text() or "") for pg in reader.pages).strip()
|
|
68
|
+
return text or None
|
|
69
|
+
except Exception: # noqa: BLE001 — optional dep missing / unreadable PDF
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def extract_document(path: str | Path) -> str | None:
|
|
74
|
+
"""Extract text from a supported document (.pdf/.docx), or None."""
|
|
75
|
+
ext = Path(path).suffix.lower()
|
|
76
|
+
if ext == ".docx":
|
|
77
|
+
return _extract_docx(path)
|
|
78
|
+
if ext == ".pdf":
|
|
79
|
+
return _extract_pdf(path)
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def pdf_backend_available() -> bool:
|
|
84
|
+
"""Whether a PDF can be extracted at all (pdftotext binary or pypdf)."""
|
|
85
|
+
from shutil import which
|
|
86
|
+
|
|
87
|
+
if which("pdftotext"):
|
|
88
|
+
return True
|
|
89
|
+
try:
|
|
90
|
+
import pypdf # noqa: F401
|
|
91
|
+
|
|
92
|
+
return True
|
|
93
|
+
except ImportError:
|
|
94
|
+
return False
|
|
@@ -26,6 +26,8 @@ import json
|
|
|
26
26
|
import os
|
|
27
27
|
import re
|
|
28
28
|
from collections import Counter, defaultdict
|
|
29
|
+
|
|
30
|
+
from drydock import extract
|
|
29
31
|
from pathlib import Path
|
|
30
32
|
|
|
31
33
|
# Files we ingest as text. Everything else (binaries, images) is skipped.
|
|
@@ -106,7 +108,8 @@ def _iter_text_files(paths: list[str]):
|
|
|
106
108
|
dirs[:] = [d for d in dirs if d not in _SKIP_DIRS]
|
|
107
109
|
for f in files:
|
|
108
110
|
fp = Path(root) / f
|
|
109
|
-
|
|
111
|
+
ext = fp.suffix.lower()
|
|
112
|
+
if ext in _TEXT_EXT or ext in extract.EXTRACTABLE_EXT or fp.suffix == "":
|
|
110
113
|
yield fp
|
|
111
114
|
|
|
112
115
|
|
|
@@ -119,10 +122,15 @@ def _ingest_files(paths, cwd, chunks, entity_chunks, edges, skip_sources):
|
|
|
119
122
|
rel = os.path.relpath(str(fp), cwd)
|
|
120
123
|
if rel in skip_sources:
|
|
121
124
|
continue
|
|
122
|
-
|
|
123
|
-
text =
|
|
124
|
-
|
|
125
|
-
|
|
125
|
+
if fp.suffix.lower() in extract.EXTRACTABLE_EXT:
|
|
126
|
+
text = extract.extract_document(fp) # PDF/Word → text (or None)
|
|
127
|
+
if not text:
|
|
128
|
+
continue # unreadable / no PDF backend — skip cleanly
|
|
129
|
+
else:
|
|
130
|
+
try:
|
|
131
|
+
text = fp.read_text("utf-8", "ignore")
|
|
132
|
+
except OSError:
|
|
133
|
+
continue
|
|
126
134
|
if not text.strip():
|
|
127
135
|
continue
|
|
128
136
|
added += 1
|
|
@@ -217,7 +217,7 @@ _DRYDOCK_COMMANDS_HELP = (
|
|
|
217
217
|
"folder), `/graphrag add <path>` to add more, `/graphrag query <q>` to test "
|
|
218
218
|
"it, `/graphrag status`, `/graphrag clear`. Once built, you automatically use "
|
|
219
219
|
"the `Knowledge` tool to draw on it. It ingests text formats (md/txt/code/"
|
|
220
|
-
"json/yaml/…)
|
|
220
|
+
"json/yaml/…) plus PDF and Word (.docx) directly.\n"
|
|
221
221
|
"- Custom skills (reusable `/<name>` prompts): `/skills new <name> <prompt "
|
|
222
222
|
"text>` creates one (use $ARGS in the prompt for trailing input); `/skills` "
|
|
223
223
|
"lists them; then they run it as `/<name>`.\n"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: drydock-cli
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.62
|
|
4
4
|
Summary: Drydock — a local, provider-agnostic terminal coding agent for local LLMs
|
|
5
5
|
Author: Frank Bobe III
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -18,6 +18,8 @@ Requires-Dist: pytest; extra == "dev"
|
|
|
18
18
|
Requires-Dist: pytest-timeout; extra == "dev"
|
|
19
19
|
Requires-Dist: ruff; extra == "dev"
|
|
20
20
|
Requires-Dist: pyright; extra == "dev"
|
|
21
|
+
Provides-Extra: pdf
|
|
22
|
+
Requires-Dist: pypdf>=4; extra == "pdf"
|
|
21
23
|
Dynamic: license-file
|
|
22
24
|
|
|
23
25
|
# ⚓ Drydock
|
|
@@ -103,9 +105,10 @@ Typed into the prompt. The agent also knows these, so you can just **ask it**
|
|
|
103
105
|
|
|
104
106
|
Once built, the agent **automatically** retrieves from it (read-only `Knowledge`
|
|
105
107
|
tool) when a question touches your material. Ingests text formats
|
|
106
|
-
(`.md .txt .py .js .json .yaml .sql …`)
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
(`.md .txt .py .js .json .yaml .sql …`) **plus PDF and Word (`.docx`)**. `.docx`
|
|
109
|
+
needs nothing extra; PDF uses the `pdftotext` binary (poppler) if present, else
|
|
110
|
+
`pip install drydock-cli[pdf]` (pypdf). The index is a single JSON at
|
|
111
|
+
`<project>/.drydock/graphrag.json` — clean-room, no embeddings.
|
|
109
112
|
|
|
110
113
|
### Custom skills
|
|
111
114
|
|
|
@@ -10,6 +10,7 @@ drydock/cli.py
|
|
|
10
10
|
drydock/compaction.py
|
|
11
11
|
drydock/config.py
|
|
12
12
|
drydock/detect.py
|
|
13
|
+
drydock/extract.py
|
|
13
14
|
drydock/gittools.py
|
|
14
15
|
drydock/graphrag.py
|
|
15
16
|
drydock/guards.py
|
|
@@ -47,6 +48,7 @@ tests/test_context_limit_config.py
|
|
|
47
48
|
tests/test_detect.py
|
|
48
49
|
tests/test_dispatch.py
|
|
49
50
|
tests/test_empty_response.py
|
|
51
|
+
tests/test_extract.py
|
|
50
52
|
tests/test_failure_loop.py
|
|
51
53
|
tests/test_first_run_setup.py
|
|
52
54
|
tests/test_gittools.py
|
|
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
|
|
7
7
|
# PyPI distribution name is drydock-cli (the established install name, continued
|
|
8
8
|
# from the retired v2 fork); the import package + CLI command stay `drydock`.
|
|
9
9
|
name = "drydock-cli"
|
|
10
|
-
version = "3.0.
|
|
10
|
+
version = "3.0.62"
|
|
11
11
|
description = "Drydock — a local, provider-agnostic terminal coding agent for local LLMs"
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
requires-python = ">=3.11"
|
|
@@ -29,6 +29,9 @@ drydock = "drydock.cli:main"
|
|
|
29
29
|
|
|
30
30
|
[project.optional-dependencies]
|
|
31
31
|
dev = ["pytest", "pytest-timeout", "ruff", "pyright"]
|
|
32
|
+
# PDF ingestion fallback when the `pdftotext` binary (poppler) isn't installed.
|
|
33
|
+
# .docx needs nothing extra (stdlib zip/xml). `pip install drydock-cli[pdf]`.
|
|
34
|
+
pdf = ["pypdf>=4"]
|
|
32
35
|
|
|
33
36
|
[tool.ruff]
|
|
34
37
|
target-version = "py312"
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""PDF + Word (.docx) document extraction for GraphRAG ingestion."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import zipfile
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from drydock import extract, graphrag
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _make_docx(path, text):
|
|
12
|
+
doc = ('<?xml version="1.0"?><w:document xmlns:w='
|
|
13
|
+
'"http://schemas.openxmlformats.org/wordprocessingml/2006/main">'
|
|
14
|
+
f'<w:body><w:p><w:r><w:t>{text}</w:t></w:r></w:p></w:body></w:document>')
|
|
15
|
+
with zipfile.ZipFile(path, "w") as z:
|
|
16
|
+
z.writestr("word/document.xml", doc)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _make_pdf(path, text):
|
|
20
|
+
objs = [b"<< /Type /Catalog /Pages 2 0 R >>",
|
|
21
|
+
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
|
|
22
|
+
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R "
|
|
23
|
+
b"/Resources << /Font << /F1 5 0 R >> >> >>"]
|
|
24
|
+
stream = b"BT /F1 18 Tf 72 700 Td (" + text.encode() + b") Tj ET"
|
|
25
|
+
objs.append(b"<< /Length " + str(len(stream)).encode() + b" >>\nstream\n" + stream + b"\nendstream")
|
|
26
|
+
objs.append(b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>")
|
|
27
|
+
out = b"%PDF-1.4\n"; offs = []
|
|
28
|
+
for i, o in enumerate(objs, 1):
|
|
29
|
+
offs.append(len(out)); out += str(i).encode() + b" 0 obj\n" + o + b"\nendobj\n"
|
|
30
|
+
x = len(out); out += b"xref\n0 " + str(len(objs) + 1).encode() + b"\n0000000000 65535 f \n"
|
|
31
|
+
for o in offs:
|
|
32
|
+
out += ("%010d 00000 n \n" % o).encode()
|
|
33
|
+
out += (b"trailer\n<< /Size " + str(len(objs) + 1).encode() + b" /Root 1 0 R >>\n"
|
|
34
|
+
b"startxref\n" + str(x).encode() + b"\n%%EOF")
|
|
35
|
+
path.write_bytes(out)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_docx_extraction(tmp_path):
|
|
39
|
+
p = tmp_path / "d.docx"
|
|
40
|
+
_make_docx(p, "The Nimbus service uses the Falcon-Token header.")
|
|
41
|
+
assert extract.extract_document(p) == "The Nimbus service uses the Falcon-Token header."
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_pdf_extraction(tmp_path):
|
|
45
|
+
if not extract.pdf_backend_available():
|
|
46
|
+
pytest.skip("no PDF backend (pdftotext/pypdf) available")
|
|
47
|
+
p = tmp_path / "m.pdf"
|
|
48
|
+
_make_pdf(p, "Refunds via the Zephyr gateway.")
|
|
49
|
+
out = extract.extract_document(p)
|
|
50
|
+
assert out and "Zephyr gateway" in out
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_unsupported_and_corrupt_return_none(tmp_path):
|
|
54
|
+
assert extract.extract_document(tmp_path / "x.png") is None
|
|
55
|
+
bad = tmp_path / "bad.docx"; bad.write_text("not a zip")
|
|
56
|
+
assert extract.extract_document(bad) is None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_graphrag_ingests_docx_and_pdf(tmp_path):
|
|
60
|
+
_make_docx(tmp_path / "spec.docx", "The Nimbus API uses the Falcon-Token header for auth.")
|
|
61
|
+
if extract.pdf_backend_available():
|
|
62
|
+
_make_pdf(tmp_path / "manual.pdf", "Refunds are issued via the Zephyr gateway.")
|
|
63
|
+
store = tmp_path / ".drydock" / "graphrag.json"
|
|
64
|
+
graphrag.build_index(["."], store, cwd=str(tmp_path))
|
|
65
|
+
idx = graphrag.load_index(store)
|
|
66
|
+
srcs = graphrag.sources(idx)
|
|
67
|
+
assert "spec.docx" in srcs
|
|
68
|
+
assert graphrag.query_index(idx, "Falcon-Token header")["chunks"][0]["source"] == "spec.docx"
|
|
69
|
+
if extract.pdf_backend_available():
|
|
70
|
+
assert "manual.pdf" in srcs
|
|
71
|
+
assert graphrag.query_index(idx, "refunds Zephyr gateway")["chunks"][0]["source"] == "manual.pdf"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|