footprinter-cli 1.0.0rc1__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.
- footprinter/__init__.py +8 -0
- footprinter/access.py +431 -0
- footprinter/api/__init__.py +1 -0
- footprinter/api/db.py +61 -0
- footprinter/api/entities.py +250 -0
- footprinter/api/search.py +47 -0
- footprinter/api/semantic.py +33 -0
- footprinter/api/server.py +66 -0
- footprinter/api/status.py +15 -0
- footprinter/bundled/__init__.py +0 -0
- footprinter/bundled/config.example.yaml +161 -0
- footprinter/bundled/patterns/context_patterns.yaml +18 -0
- footprinter/bundled/patterns/extensions.yaml +283 -0
- footprinter/bundled/patterns/filename_patterns.yaml +61 -0
- footprinter/bundled/patterns/mime_mappings.yaml +68 -0
- footprinter/bundled/patterns/salesforce_rules.yaml +84 -0
- footprinter/bundled/patterns/security_patterns.yaml +27 -0
- footprinter/bundled/samples/hidden-client-file-sample.txt +2 -0
- footprinter/bundled/samples/opaque-project-file-sample.txt +2 -0
- footprinter/bundled/samples/visible-file-sample.txt +2 -0
- footprinter/cli/__init__.py +135 -0
- footprinter/cli/__main__.py +6 -0
- footprinter/cli/_common.py +327 -0
- footprinter/cli/_policy_helpers.py +646 -0
- footprinter/cli/_prompt.py +220 -0
- footprinter/cli/_sample_seed.py +204 -0
- footprinter/cli/api_cmd.py +32 -0
- footprinter/cli/connect.py +591 -0
- footprinter/cli/data.py +879 -0
- footprinter/cli/delete.py +128 -0
- footprinter/cli/ingest.py +543 -0
- footprinter/cli/mcp_cmd.py +750 -0
- footprinter/cli/mcp_setup.py +306 -0
- footprinter/cli/search.py +393 -0
- footprinter/cli/search_cmd.py +69 -0
- footprinter/cli/setup.py +2001 -0
- footprinter/cli/status.py +747 -0
- footprinter/cli/status_cmd.py +104 -0
- footprinter/cli/upsert.py +794 -0
- footprinter/cli/vectorize_cmd.py +215 -0
- footprinter/cli/view.py +322 -0
- footprinter/connectors/__init__.py +171 -0
- footprinter/connectors/config_utils.py +141 -0
- footprinter/db/__init__.py +37 -0
- footprinter/db/browser.py +198 -0
- footprinter/db/chats.py +602 -0
- footprinter/db/clients.py +307 -0
- footprinter/db/emails.py +279 -0
- footprinter/db/files.py +724 -0
- footprinter/db/folders.py +659 -0
- footprinter/db/messages.py +192 -0
- footprinter/db/policies.py +151 -0
- footprinter/db/projects.py +673 -0
- footprinter/db/search.py +573 -0
- footprinter/db/sql_utils.py +168 -0
- footprinter/db/status.py +320 -0
- footprinter/db/uploads.py +70 -0
- footprinter/ingest/__init__.py +0 -0
- footprinter/ingest/adapters/__init__.py +33 -0
- footprinter/ingest/adapters/browser.py +54 -0
- footprinter/ingest/adapters/chat.py +57 -0
- footprinter/ingest/adapters/ingest.py +146 -0
- footprinter/ingest/adapters/local_files.py +68 -0
- footprinter/ingest/adapters/local_folders.py +52 -0
- footprinter/ingest/adapters/protocol.py +174 -0
- footprinter/ingest/browser_indexer.py +216 -0
- footprinter/ingest/chat_dedup.py +156 -0
- footprinter/ingest/chat_indexer.py +487 -0
- footprinter/ingest/chat_parsers/__init__.py +8 -0
- footprinter/ingest/chat_parsers/chatgpt_parser.py +229 -0
- footprinter/ingest/chat_parsers/claude_parser.py +161 -0
- footprinter/ingest/cli.py +827 -0
- footprinter/ingest/content_extractors.py +117 -0
- footprinter/ingest/database.py +36 -0
- footprinter/ingest/db/__init__.py +1 -0
- footprinter/ingest/db/connector_schema.py +47 -0
- footprinter/ingest/db/migration.py +315 -0
- footprinter/ingest/db/schema.py +1043 -0
- footprinter/ingest/db/security.py +6 -0
- footprinter/ingest/file_indexer.py +223 -0
- footprinter/ingest/file_scanner.py +277 -0
- footprinter/ingest/folder_indexer.py +226 -0
- footprinter/ingest/full_content_extractor.py +321 -0
- footprinter/ingest/orchestrator.py +112 -0
- footprinter/ingest/pipe_runner.py +200 -0
- footprinter/ingest/processing.py +165 -0
- footprinter/ingest/registry.py +186 -0
- footprinter/ingest/run_record.py +91 -0
- footprinter/ingest/status.py +346 -0
- footprinter/mcp/__init__.py +0 -0
- footprinter/mcp/__main__.py +5 -0
- footprinter/mcp/db.py +67 -0
- footprinter/mcp/errors.py +105 -0
- footprinter/mcp/extraction.py +226 -0
- footprinter/mcp/server.py +39 -0
- footprinter/mcp/tools/__init__.py +0 -0
- footprinter/mcp/tools/navigation.py +70 -0
- footprinter/mcp/tools/read.py +75 -0
- footprinter/mcp/tools/search.py +158 -0
- footprinter/mcp/tools/semantic.py +79 -0
- footprinter/mcp/tools/status.py +19 -0
- footprinter/paths.py +117 -0
- footprinter/permissions.py +1152 -0
- footprinter/semantic/__init__.py +13 -0
- footprinter/semantic/chunking.py +52 -0
- footprinter/semantic/embeddings.py +23 -0
- footprinter/semantic/hybrid_search.py +273 -0
- footprinter/semantic/vector_store.py +471 -0
- footprinter/services/__init__.py +49 -0
- footprinter/services/access_service.py +342 -0
- footprinter/services/chat_service.py +85 -0
- footprinter/services/client_service.py +267 -0
- footprinter/services/content_service.py +181 -0
- footprinter/services/email_service.py +89 -0
- footprinter/services/file_service.py +83 -0
- footprinter/services/folder_service.py +122 -0
- footprinter/services/includes.py +19 -0
- footprinter/services/ingest_service.py +231 -0
- footprinter/services/project_service.py +262 -0
- footprinter/services/roles.py +25 -0
- footprinter/services/search_service.py +177 -0
- footprinter/services/semantic_service.py +360 -0
- footprinter/services/status_service.py +18 -0
- footprinter/services/visit_service.py +65 -0
- footprinter/source_registry.py +194 -0
- footprinter/utils/__init__.py +7 -0
- footprinter/utils/hash_utils.py +59 -0
- footprinter/utils/logging_config.py +68 -0
- footprinter/utils/mime.py +30 -0
- footprinter/utils/text.py +6 -0
- footprinter/utils/time.py +11 -0
- footprinter/visibility.py +1264 -0
- footprinter_cli-1.0.0rc1.dist-info/LICENSE +21 -0
- footprinter_cli-1.0.0rc1.dist-info/METADATA +223 -0
- footprinter_cli-1.0.0rc1.dist-info/RECORD +138 -0
- footprinter_cli-1.0.0rc1.dist-info/WHEEL +5 -0
- footprinter_cli-1.0.0rc1.dist-info/entry_points.txt +2 -0
- footprinter_cli-1.0.0rc1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""Safe prompt wrappers with Escape key and Ctrl+C/Ctrl+D support.
|
|
2
|
+
|
|
3
|
+
Wraps Rich's ``Prompt`` and ``Confirm`` to detect Escape (via raw terminal)
|
|
4
|
+
and convert ``KeyboardInterrupt``/``EOFError`` into ``PromptCancelled``.
|
|
5
|
+
|
|
6
|
+
``PromptCancelled`` inherits from ``BaseException`` so it passes through the
|
|
7
|
+
~9 ``except Exception`` broad catches in the setup wizard without being
|
|
8
|
+
swallowed.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
from rich.prompt import Confirm, Prompt
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
import select
|
|
18
|
+
import termios
|
|
19
|
+
import tty
|
|
20
|
+
|
|
21
|
+
_HAS_TERMIOS = True
|
|
22
|
+
except ImportError:
|
|
23
|
+
# Windows or other non-POSIX — fall back to input()
|
|
24
|
+
select = None # type: ignore[assignment]
|
|
25
|
+
termios = None # type: ignore[assignment]
|
|
26
|
+
tty = None # type: ignore[assignment]
|
|
27
|
+
_HAS_TERMIOS = False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PromptCancelled(BaseException):
|
|
31
|
+
"""Raised when the user presses Escape, Ctrl+C, or Ctrl+D at a prompt.
|
|
32
|
+
|
|
33
|
+
Inherits from ``BaseException`` (not ``Exception``) so it propagates
|
|
34
|
+
through ``except Exception`` blocks in wizard steps.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
_ESCAPE_TIMEOUT = 0.05 # 50ms to distinguish bare Escape from escape sequences
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _read_utf8_char(fd: int, lead: int) -> tuple[str, bytes]:
|
|
42
|
+
"""Read a complete UTF-8 character given its lead byte.
|
|
43
|
+
|
|
44
|
+
Determines the expected continuation byte count from the lead byte
|
|
45
|
+
pattern, reads that many bytes via ``_read_with_timeout``, and decodes.
|
|
46
|
+
|
|
47
|
+
Returns ``(char, leftover)`` where *char* is the decoded character
|
|
48
|
+
(empty string on failure) and *leftover* is a non-continuation byte
|
|
49
|
+
that was consumed but could not be used — the caller must re-process it.
|
|
50
|
+
"""
|
|
51
|
+
if 0xC0 <= lead <= 0xDF:
|
|
52
|
+
n = 1
|
|
53
|
+
elif 0xE0 <= lead <= 0xEF:
|
|
54
|
+
n = 2
|
|
55
|
+
elif 0xF0 <= lead <= 0xF7:
|
|
56
|
+
n = 3
|
|
57
|
+
else:
|
|
58
|
+
return ("", b"")
|
|
59
|
+
|
|
60
|
+
raw = bytes([lead])
|
|
61
|
+
for _ in range(n):
|
|
62
|
+
b = _read_with_timeout(fd, _ESCAPE_TIMEOUT)
|
|
63
|
+
if not b:
|
|
64
|
+
return ("", b"")
|
|
65
|
+
if not (0x80 <= b[0] <= 0xBF):
|
|
66
|
+
return ("", b) # Return consumed byte for re-processing
|
|
67
|
+
raw += b
|
|
68
|
+
|
|
69
|
+
decoded = raw.decode("utf-8", errors="replace")
|
|
70
|
+
if "\ufffd" in decoded:
|
|
71
|
+
return ("", b"")
|
|
72
|
+
return (decoded, b"")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _read_with_timeout(fd: int, timeout: float) -> bytes:
|
|
76
|
+
"""Read a single byte from *fd* with a timeout via ``select``."""
|
|
77
|
+
ready, _, _ = select.select([fd], [], [], timeout)
|
|
78
|
+
if ready:
|
|
79
|
+
return os.read(fd, 1)
|
|
80
|
+
return b""
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _safe_readline(password: bool = False) -> str:
|
|
84
|
+
"""Read a line from stdin with Escape/Ctrl+C/Ctrl+D detection.
|
|
85
|
+
|
|
86
|
+
Uses raw terminal mode on POSIX systems. Falls back to ``input()``
|
|
87
|
+
when termios is unavailable (Windows, non-TTY, piped input).
|
|
88
|
+
"""
|
|
89
|
+
if not _HAS_TERMIOS or not sys.stdin.isatty():
|
|
90
|
+
try:
|
|
91
|
+
return input()
|
|
92
|
+
except (KeyboardInterrupt, EOFError) as exc:
|
|
93
|
+
raise PromptCancelled(str(exc)) from exc
|
|
94
|
+
|
|
95
|
+
fd = sys.stdin.fileno()
|
|
96
|
+
old_settings = termios.tcgetattr(fd)
|
|
97
|
+
buf: list[str] = []
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
tty.setraw(fd)
|
|
101
|
+
pending = b""
|
|
102
|
+
|
|
103
|
+
while True:
|
|
104
|
+
if pending:
|
|
105
|
+
ch = pending
|
|
106
|
+
pending = b""
|
|
107
|
+
else:
|
|
108
|
+
ready, _, _ = select.select([fd], [], [])
|
|
109
|
+
if not ready:
|
|
110
|
+
continue
|
|
111
|
+
ch = os.read(fd, 1)
|
|
112
|
+
|
|
113
|
+
if not ch:
|
|
114
|
+
raise PromptCancelled("EOF")
|
|
115
|
+
|
|
116
|
+
byte = ch[0]
|
|
117
|
+
|
|
118
|
+
if byte == 0x1B: # Escape
|
|
119
|
+
# Check if more bytes follow (escape sequence vs bare Escape)
|
|
120
|
+
follow = _read_with_timeout(fd, _ESCAPE_TIMEOUT)
|
|
121
|
+
if not follow:
|
|
122
|
+
# Bare Escape — user pressed Esc
|
|
123
|
+
raise PromptCancelled("Escape")
|
|
124
|
+
# Escape sequence — consume and ignore
|
|
125
|
+
if follow == b"[":
|
|
126
|
+
# CSI sequence (\x1b[...) — consume until alpha terminator
|
|
127
|
+
while True:
|
|
128
|
+
seq_byte = _read_with_timeout(fd, _ESCAPE_TIMEOUT)
|
|
129
|
+
if not seq_byte or (0x40 <= seq_byte[0] <= 0x7E):
|
|
130
|
+
break
|
|
131
|
+
elif follow == b"O":
|
|
132
|
+
# SS3 sequence (\x1bO..., e.g., F1–F4) — consume terminator
|
|
133
|
+
_read_with_timeout(fd, _ESCAPE_TIMEOUT)
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
if byte == 0x03: # Ctrl+C
|
|
137
|
+
raise PromptCancelled("Ctrl+C")
|
|
138
|
+
|
|
139
|
+
if byte == 0x04: # Ctrl+D
|
|
140
|
+
raise PromptCancelled("Ctrl+D")
|
|
141
|
+
|
|
142
|
+
if byte in (0x0D, 0x0A): # Enter
|
|
143
|
+
sys.stdout.write("\n")
|
|
144
|
+
sys.stdout.flush()
|
|
145
|
+
return "".join(buf)
|
|
146
|
+
|
|
147
|
+
if byte in (0x7F, 0x08): # Backspace / Delete
|
|
148
|
+
if buf:
|
|
149
|
+
buf.pop()
|
|
150
|
+
if not password:
|
|
151
|
+
sys.stdout.write("\b \b")
|
|
152
|
+
sys.stdout.flush()
|
|
153
|
+
continue
|
|
154
|
+
|
|
155
|
+
if byte >= 0x80: # High byte (UTF-8 lead or stray continuation)
|
|
156
|
+
char, leftover = _read_utf8_char(fd, byte)
|
|
157
|
+
if char:
|
|
158
|
+
buf.append(char)
|
|
159
|
+
if not password:
|
|
160
|
+
sys.stdout.write(char)
|
|
161
|
+
sys.stdout.flush()
|
|
162
|
+
if leftover:
|
|
163
|
+
pending = leftover
|
|
164
|
+
elif byte >= 0x20: # Printable ASCII character
|
|
165
|
+
buf.append(chr(byte))
|
|
166
|
+
if not password:
|
|
167
|
+
sys.stdout.write(chr(byte))
|
|
168
|
+
sys.stdout.flush()
|
|
169
|
+
finally:
|
|
170
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class SafePrompt(Prompt):
|
|
174
|
+
"""Rich Prompt subclass with Escape key and interrupt handling."""
|
|
175
|
+
|
|
176
|
+
@classmethod
|
|
177
|
+
def get_input(
|
|
178
|
+
cls,
|
|
179
|
+
console, # noqa: ANN001
|
|
180
|
+
prompt, # noqa: ANN001
|
|
181
|
+
password: bool = False,
|
|
182
|
+
stream=None, # noqa: ANN001
|
|
183
|
+
) -> str:
|
|
184
|
+
if stream is not None:
|
|
185
|
+
try:
|
|
186
|
+
return super().get_input(console, prompt, password=password, stream=stream)
|
|
187
|
+
except (KeyboardInterrupt, EOFError) as exc:
|
|
188
|
+
raise PromptCancelled(str(exc)) from exc
|
|
189
|
+
|
|
190
|
+
console.print(prompt, end="")
|
|
191
|
+
try:
|
|
192
|
+
return _safe_readline(password=password)
|
|
193
|
+
except PromptCancelled:
|
|
194
|
+
console.print() # Newline after the prompt
|
|
195
|
+
raise
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class SafeConfirm(Confirm):
|
|
199
|
+
"""Rich Confirm subclass with Escape key and interrupt handling."""
|
|
200
|
+
|
|
201
|
+
@classmethod
|
|
202
|
+
def get_input(
|
|
203
|
+
cls,
|
|
204
|
+
console, # noqa: ANN001
|
|
205
|
+
prompt, # noqa: ANN001
|
|
206
|
+
password: bool = False,
|
|
207
|
+
stream=None, # noqa: ANN001
|
|
208
|
+
) -> str:
|
|
209
|
+
if stream is not None:
|
|
210
|
+
try:
|
|
211
|
+
return super().get_input(console, prompt, password=password, stream=stream)
|
|
212
|
+
except (KeyboardInterrupt, EOFError) as exc:
|
|
213
|
+
raise PromptCancelled(str(exc)) from exc
|
|
214
|
+
|
|
215
|
+
console.print(prompt, end="")
|
|
216
|
+
try:
|
|
217
|
+
return _safe_readline(password=password)
|
|
218
|
+
except PromptCancelled:
|
|
219
|
+
console.print() # Newline after the prompt
|
|
220
|
+
raise
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Seed and clear sample data for new-user onboarding.
|
|
2
|
+
|
|
3
|
+
``fp setup --seed-samples`` copies bundled sample files to FOOTPRINTER_HOME/samples/,
|
|
4
|
+
inserts tagged records (``source='sample'``), and seeds demonstration access policies.
|
|
5
|
+
|
|
6
|
+
``fp setup --clear-samples`` soft-deletes all sample records and removes sample policies.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import shutil
|
|
11
|
+
import sqlite3
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from footprinter.access import recalculate_access
|
|
15
|
+
from footprinter.db.policies import set_permission_policy, set_visibility_policy
|
|
16
|
+
from footprinter.paths import get_bundled_path, get_home
|
|
17
|
+
|
|
18
|
+
SAMPLE_SOURCE = "sample"
|
|
19
|
+
"""Value used in the ``source`` column to identify sample records."""
|
|
20
|
+
|
|
21
|
+
_SAMPLE_FILES = [
|
|
22
|
+
"visible-file-sample.txt",
|
|
23
|
+
"opaque-project-file-sample.txt",
|
|
24
|
+
"hidden-client-file-sample.txt",
|
|
25
|
+
]
|
|
26
|
+
"""Bundled sample filenames (must match footprinter/bundled/samples/)."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _get_sample_dir() -> Path:
|
|
30
|
+
"""Return the sample-data directory inside FOOTPRINTER_HOME."""
|
|
31
|
+
return get_home() / "samples"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _copy_bundled_samples(sample_dir: Path) -> list[Path]:
|
|
35
|
+
"""Copy bundled sample .txt files into *sample_dir*.
|
|
36
|
+
|
|
37
|
+
Returns list of destination paths.
|
|
38
|
+
"""
|
|
39
|
+
sample_dir.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
bundled = Path(get_bundled_path("samples"))
|
|
41
|
+
copied: list[Path] = []
|
|
42
|
+
for name in _SAMPLE_FILES:
|
|
43
|
+
src = bundled / name
|
|
44
|
+
dst = sample_dir / name
|
|
45
|
+
if src.exists():
|
|
46
|
+
shutil.copy2(src, dst)
|
|
47
|
+
copied.append(dst)
|
|
48
|
+
return copied
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def seed_samples(conn: sqlite3.Connection) -> dict:
|
|
52
|
+
"""Create sample records and demonstration access policies.
|
|
53
|
+
|
|
54
|
+
Copies bundled sample files to ``~/.footprinter/samples/``, inserts
|
|
55
|
+
folder + file records tagged with ``source='sample'``, seeds policies
|
|
56
|
+
that demonstrate the access-control hierarchy, and stamps
|
|
57
|
+
``mcp_view``/``mcp_read`` columns via ``recalculate_access()``.
|
|
58
|
+
|
|
59
|
+
Idempotent — re-running skips existing records via INSERT OR IGNORE.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Dict with ``files_created``, ``folder_created``, ``policies_seeded``.
|
|
63
|
+
"""
|
|
64
|
+
sample_dir = _get_sample_dir()
|
|
65
|
+
copied = _copy_bundled_samples(sample_dir)
|
|
66
|
+
|
|
67
|
+
cursor = conn.cursor()
|
|
68
|
+
|
|
69
|
+
# --- Source row (required FK target on some queries) ---
|
|
70
|
+
cursor.execute(
|
|
71
|
+
"INSERT OR IGNORE INTO sources (name, source_type, adapter, account, label, icon, enabled) "
|
|
72
|
+
"VALUES (?, 'file', 'sample', NULL, 'Sample Data', 'beaker', 1)",
|
|
73
|
+
(SAMPLE_SOURCE,),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# --- Folder ---
|
|
77
|
+
sample_path = str(sample_dir)
|
|
78
|
+
relative = sample_path.replace(os.path.expanduser("~"), "").lstrip(os.sep)
|
|
79
|
+
cursor.execute(
|
|
80
|
+
"INSERT OR IGNORE INTO folders (path, relative_path, name, source) "
|
|
81
|
+
"VALUES (?, ?, 'samples', ?)",
|
|
82
|
+
(sample_path, relative, SAMPLE_SOURCE),
|
|
83
|
+
)
|
|
84
|
+
conn.commit()
|
|
85
|
+
|
|
86
|
+
folder_id = cursor.execute(
|
|
87
|
+
"SELECT id FROM folders WHERE path = ?", (sample_path,)
|
|
88
|
+
).fetchone()[0]
|
|
89
|
+
|
|
90
|
+
# --- Files ---
|
|
91
|
+
files_created = 0
|
|
92
|
+
file_ids: dict[str, int] = {}
|
|
93
|
+
for file_path in copied:
|
|
94
|
+
name = file_path.name
|
|
95
|
+
full_path = str(file_path)
|
|
96
|
+
size = file_path.stat().st_size if file_path.exists() else 0
|
|
97
|
+
# Guard: skip if this sample file already exists (idempotency)
|
|
98
|
+
existing = cursor.execute(
|
|
99
|
+
"SELECT id FROM files WHERE path = ? AND source = ?",
|
|
100
|
+
(full_path, SAMPLE_SOURCE),
|
|
101
|
+
).fetchone()
|
|
102
|
+
if existing is None:
|
|
103
|
+
cursor.execute(
|
|
104
|
+
"INSERT INTO files (name, path, source, status, content_type, size_bytes, folder_id) "
|
|
105
|
+
"VALUES (?, ?, ?, 'active', 'text', ?, ?)",
|
|
106
|
+
(name, full_path, SAMPLE_SOURCE, size, folder_id),
|
|
107
|
+
)
|
|
108
|
+
files_created += cursor.rowcount
|
|
109
|
+
|
|
110
|
+
conn.commit()
|
|
111
|
+
|
|
112
|
+
# Collect IDs for policy seeding
|
|
113
|
+
rows = cursor.execute(
|
|
114
|
+
"SELECT id, name FROM files WHERE source = ? AND status != 'removed'",
|
|
115
|
+
(SAMPLE_SOURCE,),
|
|
116
|
+
).fetchall()
|
|
117
|
+
for row in rows:
|
|
118
|
+
file_ids[row[1]] = row[0]
|
|
119
|
+
|
|
120
|
+
# --- Demonstration policies ---
|
|
121
|
+
policies_seeded = 0
|
|
122
|
+
|
|
123
|
+
# visible-file-sample.txt → visible + allow (default, no override needed)
|
|
124
|
+
# opaque-project-file-sample.txt → opaque visibility
|
|
125
|
+
opaque_id = file_ids.get("opaque-project-file-sample.txt")
|
|
126
|
+
if opaque_id:
|
|
127
|
+
set_visibility_policy(conn, f"file:{opaque_id}", "opaque")
|
|
128
|
+
policies_seeded += 1
|
|
129
|
+
|
|
130
|
+
# hidden-client-file-sample.txt → hidden visibility + deny read
|
|
131
|
+
hidden_id = file_ids.get("hidden-client-file-sample.txt")
|
|
132
|
+
if hidden_id:
|
|
133
|
+
set_visibility_policy(conn, f"file:{hidden_id}", "hidden")
|
|
134
|
+
set_permission_policy(conn, f"file:{hidden_id}", "deny")
|
|
135
|
+
policies_seeded += 2
|
|
136
|
+
|
|
137
|
+
# Stamp mcp_view/mcp_read columns
|
|
138
|
+
recalculate_access(conn, "global")
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
"files_created": files_created,
|
|
142
|
+
"folder_created": 1,
|
|
143
|
+
"policies_seeded": policies_seeded,
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def clear_samples(conn: sqlite3.Connection) -> dict:
|
|
148
|
+
"""Remove all sample records and policies.
|
|
149
|
+
|
|
150
|
+
Soft-deletes entity records (``status='removed'``) per the project's
|
|
151
|
+
never-delete-rows convention. Hard-deletes policy rows since the
|
|
152
|
+
policy tables have no status column.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Dict with ``files_removed``, ``folders_removed``, ``policies_removed``.
|
|
156
|
+
"""
|
|
157
|
+
cursor = conn.cursor()
|
|
158
|
+
|
|
159
|
+
# Find sample file IDs for policy cleanup
|
|
160
|
+
sample_file_ids = [
|
|
161
|
+
row[0]
|
|
162
|
+
for row in cursor.execute(
|
|
163
|
+
"SELECT id FROM files WHERE source = ? AND status != 'removed'",
|
|
164
|
+
(SAMPLE_SOURCE,),
|
|
165
|
+
).fetchall()
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
# Soft-delete files
|
|
169
|
+
cursor.execute(
|
|
170
|
+
"UPDATE files SET status = 'removed', status_reason = 'sample:cleared' "
|
|
171
|
+
"WHERE source = ? AND status != 'removed'",
|
|
172
|
+
(SAMPLE_SOURCE,),
|
|
173
|
+
)
|
|
174
|
+
files_removed = cursor.rowcount
|
|
175
|
+
|
|
176
|
+
# Soft-delete folders
|
|
177
|
+
cursor.execute(
|
|
178
|
+
"UPDATE folders SET status = 'removed' "
|
|
179
|
+
"WHERE source = ? AND status != 'removed'",
|
|
180
|
+
(SAMPLE_SOURCE,),
|
|
181
|
+
)
|
|
182
|
+
folders_removed = cursor.rowcount
|
|
183
|
+
|
|
184
|
+
# Remove item-level policies for sample files
|
|
185
|
+
policies_removed = 0
|
|
186
|
+
for fid in sample_file_ids:
|
|
187
|
+
cursor.execute(
|
|
188
|
+
"DELETE FROM visibility_policies WHERE scope = ?",
|
|
189
|
+
(f"file:{fid}",),
|
|
190
|
+
)
|
|
191
|
+
policies_removed += cursor.rowcount
|
|
192
|
+
cursor.execute(
|
|
193
|
+
"DELETE FROM permission_policies WHERE scope = ?",
|
|
194
|
+
(f"file:{fid}",),
|
|
195
|
+
)
|
|
196
|
+
policies_removed += cursor.rowcount
|
|
197
|
+
|
|
198
|
+
conn.commit()
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
"files_removed": files_removed,
|
|
202
|
+
"folders_removed": folders_removed,
|
|
203
|
+
"policies_removed": policies_removed,
|
|
204
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""fp api — start the HTTP API server."""
|
|
2
|
+
|
|
3
|
+
from footprinter.cli._common import FORMATTER
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _start_api(args) -> None:
|
|
7
|
+
from footprinter.api.server import main
|
|
8
|
+
|
|
9
|
+
main(host=args.host, port=args.port)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def register(subparsers) -> None:
|
|
13
|
+
"""Register the ``api`` subcommand."""
|
|
14
|
+
parser = subparsers.add_parser(
|
|
15
|
+
"api",
|
|
16
|
+
help="Start the HTTP API server",
|
|
17
|
+
description=(
|
|
18
|
+
"Start the Footprinter HTTP API server.\n\n"
|
|
19
|
+
"Provides REST endpoints for programmatic access to indexed data.\n"
|
|
20
|
+
"Auto-generated docs available at /docs (Swagger UI)."
|
|
21
|
+
),
|
|
22
|
+
epilog=(
|
|
23
|
+
"examples:\n"
|
|
24
|
+
" fp api Start on localhost:8000\n"
|
|
25
|
+
" fp api --port 9000 Start on custom port\n"
|
|
26
|
+
" fp api --host 0.0.0.0 Listen on all interfaces"
|
|
27
|
+
),
|
|
28
|
+
formatter_class=FORMATTER,
|
|
29
|
+
)
|
|
30
|
+
parser.add_argument("--host", default="127.0.0.1", help="Host to bind (default: 127.0.0.1)")
|
|
31
|
+
parser.add_argument("--port", type=int, default=8000, help="Port to bind (default: 8000)")
|
|
32
|
+
parser.set_defaults(func=_start_api)
|