smartpipe-cli 1.3.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.
Files changed (126) hide show
  1. smartpipe/__init__.py +6 -0
  2. smartpipe/__main__.py +8 -0
  3. smartpipe/assets/probe.png +0 -0
  4. smartpipe/assets/probe.txt +1 -0
  5. smartpipe/assets/probe.wav +0 -0
  6. smartpipe/cli/__init__.py +5 -0
  7. smartpipe/cli/auth_cmd.py +78 -0
  8. smartpipe/cli/cache_cmd.py +60 -0
  9. smartpipe/cli/chart_cmd.py +60 -0
  10. smartpipe/cli/cite_cmd.py +26 -0
  11. smartpipe/cli/cluster_cmd.py +102 -0
  12. smartpipe/cli/completions.py +91 -0
  13. smartpipe/cli/config_cmd.py +234 -0
  14. smartpipe/cli/diff_cmd.py +100 -0
  15. smartpipe/cli/distinct_cmd.py +94 -0
  16. smartpipe/cli/doctor_cmd.py +207 -0
  17. smartpipe/cli/echo_cmd.py +44 -0
  18. smartpipe/cli/embed_cmd.py +80 -0
  19. smartpipe/cli/extend_cmd.py +138 -0
  20. smartpipe/cli/filter_cmd.py +87 -0
  21. smartpipe/cli/getschema_cmd.py +31 -0
  22. smartpipe/cli/input_options.py +113 -0
  23. smartpipe/cli/interrupts.py +92 -0
  24. smartpipe/cli/join_cmd.py +162 -0
  25. smartpipe/cli/map_cmd.py +150 -0
  26. smartpipe/cli/outliers_cmd.py +82 -0
  27. smartpipe/cli/probe_cmd.py +223 -0
  28. smartpipe/cli/reduce_cmd.py +129 -0
  29. smartpipe/cli/root.py +281 -0
  30. smartpipe/cli/run_cmd.py +136 -0
  31. smartpipe/cli/sample_cmd.py +35 -0
  32. smartpipe/cli/schema_cmd.py +75 -0
  33. smartpipe/cli/screens.py +231 -0
  34. smartpipe/cli/sem_file.py +453 -0
  35. smartpipe/cli/sort_cmd.py +33 -0
  36. smartpipe/cli/split_cmd.py +76 -0
  37. smartpipe/cli/summarize_cmd.py +37 -0
  38. smartpipe/cli/top_k_cmd.py +97 -0
  39. smartpipe/cli/usage_cmd.py +66 -0
  40. smartpipe/cli/where_cmd.py +36 -0
  41. smartpipe/config/__init__.py +5 -0
  42. smartpipe/config/credentials.py +118 -0
  43. smartpipe/config/display.py +70 -0
  44. smartpipe/config/doctor.py +58 -0
  45. smartpipe/config/paths.py +38 -0
  46. smartpipe/config/store.py +252 -0
  47. smartpipe/container.py +439 -0
  48. smartpipe/core/__init__.py +5 -0
  49. smartpipe/core/errors.py +57 -0
  50. smartpipe/core/jsontools.py +56 -0
  51. smartpipe/engine/__init__.py +9 -0
  52. smartpipe/engine/aggregate.py +234 -0
  53. smartpipe/engine/blocking.py +44 -0
  54. smartpipe/engine/chart.py +143 -0
  55. smartpipe/engine/chunking.py +161 -0
  56. smartpipe/engine/clustering.py +94 -0
  57. smartpipe/engine/predicate.py +330 -0
  58. smartpipe/engine/prompts.py +601 -0
  59. smartpipe/engine/ranking.py +62 -0
  60. smartpipe/engine/runner.py +175 -0
  61. smartpipe/engine/schema.py +208 -0
  62. smartpipe/engine/schema_dsl.py +144 -0
  63. smartpipe/engine/tally.py +53 -0
  64. smartpipe/engine/timebin.py +67 -0
  65. smartpipe/engine/units.py +43 -0
  66. smartpipe/engine/windows.py +78 -0
  67. smartpipe/io/__init__.py +9 -0
  68. smartpipe/io/diagnostics.py +148 -0
  69. smartpipe/io/inputs.py +44 -0
  70. smartpipe/io/items.py +149 -0
  71. smartpipe/io/leaderboard.py +52 -0
  72. smartpipe/io/metering.py +180 -0
  73. smartpipe/io/progress.py +140 -0
  74. smartpipe/io/readers.py +455 -0
  75. smartpipe/io/text.py +40 -0
  76. smartpipe/io/tty.py +88 -0
  77. smartpipe/io/usage.py +214 -0
  78. smartpipe/io/writers.py +340 -0
  79. smartpipe/models/__init__.py +5 -0
  80. smartpipe/models/anthropic_adapter.py +149 -0
  81. smartpipe/models/base.py +170 -0
  82. smartpipe/models/budget.py +94 -0
  83. smartpipe/models/cache.py +132 -0
  84. smartpipe/models/gemini_native.py +196 -0
  85. smartpipe/models/http_support.py +77 -0
  86. smartpipe/models/jina.py +98 -0
  87. smartpipe/models/local_embed.py +76 -0
  88. smartpipe/models/ollama.py +204 -0
  89. smartpipe/models/openai_codex.py +237 -0
  90. smartpipe/models/openai_compat.py +328 -0
  91. smartpipe/models/openai_oauth.py +366 -0
  92. smartpipe/models/resolve.py +78 -0
  93. smartpipe/models/retry.py +69 -0
  94. smartpipe/models/stt.py +80 -0
  95. smartpipe/models/windows.py +116 -0
  96. smartpipe/parsing/__init__.py +10 -0
  97. smartpipe/parsing/detect.py +178 -0
  98. smartpipe/parsing/extract.py +582 -0
  99. smartpipe/py.typed +0 -0
  100. smartpipe/verbs/__init__.py +5 -0
  101. smartpipe/verbs/chart.py +153 -0
  102. smartpipe/verbs/cluster.py +220 -0
  103. smartpipe/verbs/common.py +468 -0
  104. smartpipe/verbs/convert.py +251 -0
  105. smartpipe/verbs/diff.py +206 -0
  106. smartpipe/verbs/distinct.py +164 -0
  107. smartpipe/verbs/embed.py +166 -0
  108. smartpipe/verbs/extend.py +180 -0
  109. smartpipe/verbs/filter.py +191 -0
  110. smartpipe/verbs/getschema.py +135 -0
  111. smartpipe/verbs/join.py +413 -0
  112. smartpipe/verbs/map.py +315 -0
  113. smartpipe/verbs/outliers.py +119 -0
  114. smartpipe/verbs/reduce.py +428 -0
  115. smartpipe/verbs/sample.py +52 -0
  116. smartpipe/verbs/sortverb.py +63 -0
  117. smartpipe/verbs/split.py +333 -0
  118. smartpipe/verbs/summarize.py +60 -0
  119. smartpipe/verbs/top_k.py +318 -0
  120. smartpipe/verbs/where.py +47 -0
  121. smartpipe_cli-1.3.0.dist-info/METADATA +192 -0
  122. smartpipe_cli-1.3.0.dist-info/RECORD +126 -0
  123. smartpipe_cli-1.3.0.dist-info/WHEEL +4 -0
  124. smartpipe_cli-1.3.0.dist-info/entry_points.txt +3 -0
  125. smartpipe_cli-1.3.0.dist-info/licenses/LICENSE +201 -0
  126. smartpipe_cli-1.3.0.dist-info/licenses/NOTICE +5 -0
@@ -0,0 +1,116 @@
1
+ """Dynamic context-window discovery (D26 layer 1).
2
+
3
+ Four of the six wires publish their window; asking costs one cheap metadata GET,
4
+ so the probe runs at most once per run, lazily, and only when the static table's
5
+ budget already looks too small. A failed probe is never fatal: the conservative
6
+ table stays the floor, and reduce's bisection is the backstop when everything lies.
7
+ OpenAI and Anthropic don't expose window size via API; ``SMARTPIPE_CONTEXT_TOKENS``
8
+ covers them (and overrides everything else).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import TYPE_CHECKING
14
+
15
+ from smartpipe.core.jsontools import as_items, as_record
16
+
17
+ if TYPE_CHECKING:
18
+ from collections.abc import Mapping
19
+
20
+ import httpx
21
+
22
+ from smartpipe.models.base import ModelRef
23
+
24
+ __all__ = ["probe_context_window"]
25
+
26
+ _PROBE_TIMEOUT_S = 5.0
27
+
28
+
29
+ async def probe_context_window(
30
+ ref: ModelRef, *, client: httpx.AsyncClient, env: Mapping[str, str]
31
+ ) -> int | None:
32
+ """The provider's own answer to "how big is this model's window?", or None."""
33
+ try:
34
+ match ref.provider:
35
+ case "ollama":
36
+ return await _ollama(ref.name, client, env)
37
+ case "mistral":
38
+ return await _mistral(ref.name, client, env)
39
+ case "openrouter":
40
+ return await _openrouter(ref.name, client, env)
41
+ case "gemini":
42
+ return await _gemini(ref.name, client, env)
43
+ case _: # openai/anthropic publish no window via API
44
+ return None
45
+ except Exception:
46
+ return None
47
+
48
+
49
+ async def _ollama(name: str, client: httpx.AsyncClient, env: Mapping[str, str]) -> int | None:
50
+ from smartpipe.models.ollama import resolve_host
51
+
52
+ response = await client.post(
53
+ f"{resolve_host(env)}/api/show", json={"model": name}, timeout=_PROBE_TIMEOUT_S
54
+ )
55
+ response.raise_for_status()
56
+ record = as_record(response.json())
57
+ info = as_record(record.get("model_info")) if record is not None else None
58
+ if info is None:
59
+ return None
60
+ # the key is architecture-prefixed: "llama.context_length", "qwen3.context_length", …
61
+ lengths = (value for key, value in info.items() if key.endswith(".context_length"))
62
+ first = next(lengths, None)
63
+ return first if isinstance(first, int) else None
64
+
65
+
66
+ async def _mistral(name: str, client: httpx.AsyncClient, env: Mapping[str, str]) -> int | None:
67
+ from smartpipe.models.openai_compat import MISTRAL_WIRE, resolve_base_url
68
+
69
+ key = env.get(MISTRAL_WIRE.key_env, "").strip()
70
+ if not key:
71
+ return None
72
+ response = await client.get(
73
+ f"{resolve_base_url(env, MISTRAL_WIRE)}/v1/models/{name}",
74
+ headers={"Authorization": f"Bearer {key}"},
75
+ timeout=_PROBE_TIMEOUT_S,
76
+ )
77
+ response.raise_for_status()
78
+ record = as_record(response.json())
79
+ value = record.get("max_context_length") if record is not None else None
80
+ return value if isinstance(value, int) else None
81
+
82
+
83
+ async def _openrouter(name: str, client: httpx.AsyncClient, env: Mapping[str, str]) -> int | None:
84
+ from smartpipe.models.openai_compat import OPENROUTER_WIRE, resolve_base_url
85
+
86
+ response = await client.get(
87
+ f"{resolve_base_url(env, OPENROUTER_WIRE)}/v1/models", timeout=_PROBE_TIMEOUT_S
88
+ )
89
+ response.raise_for_status()
90
+ record = as_record(response.json())
91
+ rows = as_items(record.get("data")) if record is not None else None
92
+ if rows is None:
93
+ return None
94
+ for row in rows:
95
+ entry = as_record(row)
96
+ if entry is not None and entry.get("id") == name:
97
+ value = entry.get("context_length")
98
+ return value if isinstance(value, int) else None
99
+ return None
100
+
101
+
102
+ async def _gemini(name: str, client: httpx.AsyncClient, env: Mapping[str, str]) -> int | None:
103
+ from smartpipe.models.openai_compat import GEMINI_WIRE, resolve_base_url
104
+
105
+ key = env.get(GEMINI_WIRE.key_env, "").strip()
106
+ if not key:
107
+ return None
108
+ # the native endpoint lives one path segment above the OpenAI-compat root
109
+ native = resolve_base_url(env, GEMINI_WIRE).removesuffix("/openai")
110
+ response = await client.get(
111
+ f"{native}/models/{name}", headers={"x-goog-api-key": key}, timeout=_PROBE_TIMEOUT_S
112
+ )
113
+ response.raise_for_status()
114
+ record = as_record(response.json())
115
+ value = record.get("inputTokenLimit") if record is not None else None
116
+ return value if isinstance(value, int) else None
@@ -0,0 +1,10 @@
1
+ """File parsing: detect a file's kind, extract its text (spec §3.1, D08).
2
+
3
+ The user never names a parser. ``detect`` sniffs the kind (pure, never raises);
4
+ ``extract`` turns it into text, lazy-importing the optional markitdown bridge only
5
+ when a document actually needs it.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ __all__: list[str] = []
@@ -0,0 +1,178 @@
1
+ """Detect a file's kind (spec §3.1, stage-07 contract) — pure, never raises.
2
+
3
+ Extension first, magic bytes as a backstop. Detection only *classifies*; extraction
4
+ (and any missing-dependency handling) lives in ``extract``.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from enum import Enum
10
+ from typing import TYPE_CHECKING, Literal
11
+
12
+ if TYPE_CHECKING:
13
+ from pathlib import Path
14
+
15
+ __all__ = ["FileKind", "Route", "audio_mime", "detect_kind", "route", "video_mime"]
16
+
17
+ Route = Literal["text", "doc", "audio", "video", "image", "skip"]
18
+
19
+
20
+ class FileKind(Enum):
21
+ TEXT = "text"
22
+ MARKDOWN = "markdown"
23
+ CSV = "csv"
24
+ JSON = "json"
25
+ PDF = "pdf"
26
+ DOCX = "docx"
27
+ XLSX = "xlsx"
28
+ PPTX = "pptx"
29
+ HTML = "html"
30
+ EPUB = "epub"
31
+ AUDIO = "audio"
32
+ VIDEO = "video"
33
+ IMAGE = "image"
34
+ UNKNOWN_BINARY = "unknown-binary"
35
+
36
+
37
+ _BY_EXTENSION: dict[str, FileKind] = {
38
+ ".txt": FileKind.TEXT,
39
+ ".text": FileKind.TEXT,
40
+ ".log": FileKind.TEXT,
41
+ ".md": FileKind.MARKDOWN,
42
+ ".markdown": FileKind.MARKDOWN,
43
+ ".csv": FileKind.CSV,
44
+ ".tsv": FileKind.CSV,
45
+ ".json": FileKind.JSON,
46
+ ".jsonl": FileKind.JSON,
47
+ ".ndjson": FileKind.JSON,
48
+ ".pdf": FileKind.PDF,
49
+ ".docx": FileKind.DOCX,
50
+ ".xlsx": FileKind.XLSX,
51
+ ".pptx": FileKind.PPTX,
52
+ ".html": FileKind.HTML,
53
+ ".htm": FileKind.HTML,
54
+ ".epub": FileKind.EPUB,
55
+ ".mp3": FileKind.AUDIO,
56
+ ".wav": FileKind.AUDIO,
57
+ ".flac": FileKind.AUDIO,
58
+ ".m4a": FileKind.AUDIO,
59
+ ".ogg": FileKind.AUDIO,
60
+ ".mp4": FileKind.VIDEO,
61
+ ".mov": FileKind.VIDEO,
62
+ ".mkv": FileKind.VIDEO,
63
+ ".webm": FileKind.VIDEO,
64
+ ".png": FileKind.IMAGE,
65
+ ".jpg": FileKind.IMAGE,
66
+ ".jpeg": FileKind.IMAGE,
67
+ ".gif": FileKind.IMAGE,
68
+ ".webp": FileKind.IMAGE,
69
+ }
70
+
71
+ _ROUTES: dict[FileKind, Route] = {
72
+ FileKind.TEXT: "text",
73
+ FileKind.MARKDOWN: "text",
74
+ FileKind.CSV: "text",
75
+ FileKind.JSON: "text",
76
+ FileKind.PDF: "doc",
77
+ FileKind.DOCX: "doc",
78
+ FileKind.XLSX: "doc",
79
+ FileKind.PPTX: "doc",
80
+ FileKind.HTML: "doc",
81
+ FileKind.EPUB: "doc",
82
+ FileKind.AUDIO: "audio",
83
+ FileKind.VIDEO: "video",
84
+ FileKind.IMAGE: "image",
85
+ FileKind.UNKNOWN_BINARY: "skip",
86
+ }
87
+
88
+
89
+ _AUDIO_MIME_BY_SUFFIX = {
90
+ ".mp3": "audio/mpeg",
91
+ ".wav": "audio/wav",
92
+ ".flac": "audio/flac",
93
+ ".m4a": "audio/mp4",
94
+ ".ogg": "audio/ogg",
95
+ }
96
+
97
+
98
+ def audio_mime(path: Path) -> str:
99
+ """The wire mime for an audio file — suffix-driven, mp3 as the safe default."""
100
+ return _AUDIO_MIME_BY_SUFFIX.get(path.suffix.lower(), "audio/mpeg")
101
+
102
+
103
+ def route(kind: FileKind) -> Route:
104
+ return _ROUTES[kind]
105
+
106
+
107
+ def detect_kind(path: Path, head: bytes) -> FileKind:
108
+ by_ext = _BY_EXTENSION.get(path.suffix.lower())
109
+ if by_ext is not None:
110
+ return by_ext
111
+ return _sniff(head)
112
+
113
+
114
+ def _sniff(head: bytes) -> FileKind:
115
+ if head.startswith(b"%PDF"):
116
+ return FileKind.PDF
117
+ if head.startswith(b"PK\x03\x04"):
118
+ return _sniff_zip(head)
119
+ if _is_audio(head):
120
+ return FileKind.AUDIO
121
+ if len(head) >= 12 and head[4:8] == b"ftyp":
122
+ return FileKind.VIDEO # mp4/mov family
123
+ if _is_image(head):
124
+ return FileKind.IMAGE
125
+ if _is_utf8(head):
126
+ return FileKind.TEXT
127
+ return FileKind.UNKNOWN_BINARY
128
+
129
+
130
+ def _sniff_zip(head: bytes) -> FileKind:
131
+ # OOXML/EPUB are zips; the inner directory names appear near the front
132
+ if b"word/" in head:
133
+ return FileKind.DOCX
134
+ if b"xl/" in head:
135
+ return FileKind.XLSX
136
+ if b"ppt/" in head:
137
+ return FileKind.PPTX
138
+ if b"epub" in head or b"mimetype" in head:
139
+ return FileKind.EPUB
140
+ return FileKind.UNKNOWN_BINARY
141
+
142
+
143
+ def _is_audio(head: bytes) -> bool:
144
+ if head.startswith((b"ID3", b"fLaC", b"OggS")):
145
+ return True
146
+ if head.startswith(b"\xff\xfb") or head.startswith(b"\xff\xf3"): # MP3 frame sync
147
+ return True
148
+ return head.startswith(b"RIFF") and head[8:12] == b"WAVE"
149
+
150
+
151
+ def _is_image(head: bytes) -> bool:
152
+ if head.startswith((b"\x89PNG\r\n\x1a\n", b"\xff\xd8\xff", b"GIF87a", b"GIF89a")):
153
+ return True
154
+ return head.startswith(b"RIFF") and head[8:12] == b"WEBP"
155
+
156
+
157
+ def _is_utf8(head: bytes) -> bool:
158
+ if b"\x00" in head:
159
+ return False # NUL bytes never appear in text; a strong binary signal
160
+ try:
161
+ head.decode("utf-8")
162
+ except UnicodeDecodeError as exc:
163
+ # tolerate an invalid byte only in the last 3 positions — a multibyte char
164
+ # truncated at the 8 KiB sniff boundary, not mid-stream binary
165
+ return exc.start >= len(head) - 3
166
+ return True
167
+
168
+
169
+ _VIDEO_MIME_BY_SUFFIX = {
170
+ ".mp4": "video/mp4",
171
+ ".mov": "video/quicktime",
172
+ ".mkv": "video/x-matroska",
173
+ ".webm": "video/webm",
174
+ }
175
+
176
+
177
+ def video_mime(path: Path) -> str:
178
+ return _VIDEO_MIME_BY_SUFFIX.get(path.suffix.lower(), "video/mp4")