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.
Files changed (138) hide show
  1. footprinter/__init__.py +8 -0
  2. footprinter/access.py +431 -0
  3. footprinter/api/__init__.py +1 -0
  4. footprinter/api/db.py +61 -0
  5. footprinter/api/entities.py +250 -0
  6. footprinter/api/search.py +47 -0
  7. footprinter/api/semantic.py +33 -0
  8. footprinter/api/server.py +66 -0
  9. footprinter/api/status.py +15 -0
  10. footprinter/bundled/__init__.py +0 -0
  11. footprinter/bundled/config.example.yaml +161 -0
  12. footprinter/bundled/patterns/context_patterns.yaml +18 -0
  13. footprinter/bundled/patterns/extensions.yaml +283 -0
  14. footprinter/bundled/patterns/filename_patterns.yaml +61 -0
  15. footprinter/bundled/patterns/mime_mappings.yaml +68 -0
  16. footprinter/bundled/patterns/salesforce_rules.yaml +84 -0
  17. footprinter/bundled/patterns/security_patterns.yaml +27 -0
  18. footprinter/bundled/samples/hidden-client-file-sample.txt +2 -0
  19. footprinter/bundled/samples/opaque-project-file-sample.txt +2 -0
  20. footprinter/bundled/samples/visible-file-sample.txt +2 -0
  21. footprinter/cli/__init__.py +135 -0
  22. footprinter/cli/__main__.py +6 -0
  23. footprinter/cli/_common.py +327 -0
  24. footprinter/cli/_policy_helpers.py +646 -0
  25. footprinter/cli/_prompt.py +220 -0
  26. footprinter/cli/_sample_seed.py +204 -0
  27. footprinter/cli/api_cmd.py +32 -0
  28. footprinter/cli/connect.py +591 -0
  29. footprinter/cli/data.py +879 -0
  30. footprinter/cli/delete.py +128 -0
  31. footprinter/cli/ingest.py +543 -0
  32. footprinter/cli/mcp_cmd.py +750 -0
  33. footprinter/cli/mcp_setup.py +306 -0
  34. footprinter/cli/search.py +393 -0
  35. footprinter/cli/search_cmd.py +69 -0
  36. footprinter/cli/setup.py +2001 -0
  37. footprinter/cli/status.py +747 -0
  38. footprinter/cli/status_cmd.py +104 -0
  39. footprinter/cli/upsert.py +794 -0
  40. footprinter/cli/vectorize_cmd.py +215 -0
  41. footprinter/cli/view.py +322 -0
  42. footprinter/connectors/__init__.py +171 -0
  43. footprinter/connectors/config_utils.py +141 -0
  44. footprinter/db/__init__.py +37 -0
  45. footprinter/db/browser.py +198 -0
  46. footprinter/db/chats.py +602 -0
  47. footprinter/db/clients.py +307 -0
  48. footprinter/db/emails.py +279 -0
  49. footprinter/db/files.py +724 -0
  50. footprinter/db/folders.py +659 -0
  51. footprinter/db/messages.py +192 -0
  52. footprinter/db/policies.py +151 -0
  53. footprinter/db/projects.py +673 -0
  54. footprinter/db/search.py +573 -0
  55. footprinter/db/sql_utils.py +168 -0
  56. footprinter/db/status.py +320 -0
  57. footprinter/db/uploads.py +70 -0
  58. footprinter/ingest/__init__.py +0 -0
  59. footprinter/ingest/adapters/__init__.py +33 -0
  60. footprinter/ingest/adapters/browser.py +54 -0
  61. footprinter/ingest/adapters/chat.py +57 -0
  62. footprinter/ingest/adapters/ingest.py +146 -0
  63. footprinter/ingest/adapters/local_files.py +68 -0
  64. footprinter/ingest/adapters/local_folders.py +52 -0
  65. footprinter/ingest/adapters/protocol.py +174 -0
  66. footprinter/ingest/browser_indexer.py +216 -0
  67. footprinter/ingest/chat_dedup.py +156 -0
  68. footprinter/ingest/chat_indexer.py +487 -0
  69. footprinter/ingest/chat_parsers/__init__.py +8 -0
  70. footprinter/ingest/chat_parsers/chatgpt_parser.py +229 -0
  71. footprinter/ingest/chat_parsers/claude_parser.py +161 -0
  72. footprinter/ingest/cli.py +827 -0
  73. footprinter/ingest/content_extractors.py +117 -0
  74. footprinter/ingest/database.py +36 -0
  75. footprinter/ingest/db/__init__.py +1 -0
  76. footprinter/ingest/db/connector_schema.py +47 -0
  77. footprinter/ingest/db/migration.py +315 -0
  78. footprinter/ingest/db/schema.py +1043 -0
  79. footprinter/ingest/db/security.py +6 -0
  80. footprinter/ingest/file_indexer.py +223 -0
  81. footprinter/ingest/file_scanner.py +277 -0
  82. footprinter/ingest/folder_indexer.py +226 -0
  83. footprinter/ingest/full_content_extractor.py +321 -0
  84. footprinter/ingest/orchestrator.py +112 -0
  85. footprinter/ingest/pipe_runner.py +200 -0
  86. footprinter/ingest/processing.py +165 -0
  87. footprinter/ingest/registry.py +186 -0
  88. footprinter/ingest/run_record.py +91 -0
  89. footprinter/ingest/status.py +346 -0
  90. footprinter/mcp/__init__.py +0 -0
  91. footprinter/mcp/__main__.py +5 -0
  92. footprinter/mcp/db.py +67 -0
  93. footprinter/mcp/errors.py +105 -0
  94. footprinter/mcp/extraction.py +226 -0
  95. footprinter/mcp/server.py +39 -0
  96. footprinter/mcp/tools/__init__.py +0 -0
  97. footprinter/mcp/tools/navigation.py +70 -0
  98. footprinter/mcp/tools/read.py +75 -0
  99. footprinter/mcp/tools/search.py +158 -0
  100. footprinter/mcp/tools/semantic.py +79 -0
  101. footprinter/mcp/tools/status.py +19 -0
  102. footprinter/paths.py +117 -0
  103. footprinter/permissions.py +1152 -0
  104. footprinter/semantic/__init__.py +13 -0
  105. footprinter/semantic/chunking.py +52 -0
  106. footprinter/semantic/embeddings.py +23 -0
  107. footprinter/semantic/hybrid_search.py +273 -0
  108. footprinter/semantic/vector_store.py +471 -0
  109. footprinter/services/__init__.py +49 -0
  110. footprinter/services/access_service.py +342 -0
  111. footprinter/services/chat_service.py +85 -0
  112. footprinter/services/client_service.py +267 -0
  113. footprinter/services/content_service.py +181 -0
  114. footprinter/services/email_service.py +89 -0
  115. footprinter/services/file_service.py +83 -0
  116. footprinter/services/folder_service.py +122 -0
  117. footprinter/services/includes.py +19 -0
  118. footprinter/services/ingest_service.py +231 -0
  119. footprinter/services/project_service.py +262 -0
  120. footprinter/services/roles.py +25 -0
  121. footprinter/services/search_service.py +177 -0
  122. footprinter/services/semantic_service.py +360 -0
  123. footprinter/services/status_service.py +18 -0
  124. footprinter/services/visit_service.py +65 -0
  125. footprinter/source_registry.py +194 -0
  126. footprinter/utils/__init__.py +7 -0
  127. footprinter/utils/hash_utils.py +59 -0
  128. footprinter/utils/logging_config.py +68 -0
  129. footprinter/utils/mime.py +30 -0
  130. footprinter/utils/text.py +6 -0
  131. footprinter/utils/time.py +11 -0
  132. footprinter/visibility.py +1264 -0
  133. footprinter_cli-1.0.0rc1.dist-info/LICENSE +21 -0
  134. footprinter_cli-1.0.0rc1.dist-info/METADATA +223 -0
  135. footprinter_cli-1.0.0rc1.dist-info/RECORD +138 -0
  136. footprinter_cli-1.0.0rc1.dist-info/WHEEL +5 -0
  137. footprinter_cli-1.0.0rc1.dist-info/entry_points.txt +2 -0
  138. 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)