footprinter-cli 1.0.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.
- footprinter/__init__.py +8 -0
- footprinter/access.py +444 -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/cli/__init__.py +128 -0
- footprinter/cli/__main__.py +6 -0
- footprinter/cli/_common.py +332 -0
- footprinter/cli/_policy_helpers.py +646 -0
- footprinter/cli/_prompt.py +220 -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 +579 -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 +1836 -0
- footprinter/cli/status.py +729 -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 +610 -0
- footprinter/db/clients.py +307 -0
- footprinter/db/emails.py +279 -0
- footprinter/db/files.py +741 -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 +515 -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 +328 -0
- footprinter/ingest/db/schema.py +1043 -0
- footprinter/ingest/db/security.py +6 -0
- footprinter/ingest/file_indexer.py +261 -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 +125 -0
- footprinter/ingest/pipe_runner.py +217 -0
- footprinter/ingest/processing.py +165 -0
- footprinter/ingest/registry.py +201 -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 +57 -0
- footprinter/mcp/errors.py +102 -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 +15 -0
- footprinter/paths.py +91 -0
- footprinter/permissions.py +1160 -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 +1272 -0
- footprinter_cli-1.0.0.dist-info/LICENSE +21 -0
- footprinter_cli-1.0.0.dist-info/METADATA +229 -0
- footprinter_cli-1.0.0.dist-info/RECORD +134 -0
- footprinter_cli-1.0.0.dist-info/WHEEL +5 -0
- footprinter_cli-1.0.0.dist-info/entry_points.txt +2 -0
- footprinter_cli-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
"""fp connect — manage optional integrations.
|
|
2
|
+
|
|
3
|
+
Discover, install, and remove connectors
|
|
4
|
+
without needing to know about pip extras.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from footprinter.cli._common import FORMATTER, add_json_flag, console, output_json
|
|
12
|
+
from footprinter.cli._prompt import PromptCancelled
|
|
13
|
+
from footprinter.cli._prompt import SafeConfirm as Confirm
|
|
14
|
+
from footprinter.connectors import (
|
|
15
|
+
ConnectorSpec,
|
|
16
|
+
discover_connectors,
|
|
17
|
+
get_status,
|
|
18
|
+
is_configured,
|
|
19
|
+
is_installed,
|
|
20
|
+
resolve_check_auth,
|
|
21
|
+
resolve_hook,
|
|
22
|
+
)
|
|
23
|
+
from footprinter.connectors.config_utils import account_label
|
|
24
|
+
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
# argparse registration
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def register(subparsers) -> None:
|
|
31
|
+
"""Register the ``connect`` subcommand and its verbs."""
|
|
32
|
+
parser = subparsers.add_parser(
|
|
33
|
+
"connect",
|
|
34
|
+
help="Manage optional integrations",
|
|
35
|
+
description=(
|
|
36
|
+
"Discover, install, and remove data source connectors.\n\n"
|
|
37
|
+
"Connectors add support for external data sources.\n"
|
|
38
|
+
"Each connector installs its own dependencies and runs\n"
|
|
39
|
+
"a setup wizard."
|
|
40
|
+
),
|
|
41
|
+
epilog=(
|
|
42
|
+
"examples:\n"
|
|
43
|
+
" fp connect list Show all connectors with status\n"
|
|
44
|
+
" fp connect install <name> Install a connector\n"
|
|
45
|
+
" fp connect config <name> Reconfigure a connector\n"
|
|
46
|
+
" fp connect status <name> Detailed state and credentials\n"
|
|
47
|
+
" fp connect remove <name> Uninstall connector packages\n"
|
|
48
|
+
"\n"
|
|
49
|
+
"tip: use 'fp connect <command> --help' for details on any command."
|
|
50
|
+
),
|
|
51
|
+
formatter_class=FORMATTER,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def _connect_base(args, _parser=parser):
|
|
55
|
+
if not discover_connectors():
|
|
56
|
+
console.print(
|
|
57
|
+
"Connectors add support for external data sources.\n"
|
|
58
|
+
"Each connector is a separate package.\n\n"
|
|
59
|
+
"No connectors are installed.\n\n"
|
|
60
|
+
"Learn more: https://github.com/swellcitygroup/footprinter"
|
|
61
|
+
)
|
|
62
|
+
return
|
|
63
|
+
_parser.print_help()
|
|
64
|
+
|
|
65
|
+
parser.set_defaults(func=_connect_base)
|
|
66
|
+
|
|
67
|
+
subs = parser.add_subparsers(dest="verb", metavar="COMMAND", title="commands (one required)")
|
|
68
|
+
|
|
69
|
+
# list
|
|
70
|
+
p_list = subs.add_parser(
|
|
71
|
+
"list",
|
|
72
|
+
help="Show all connectors with status",
|
|
73
|
+
description="Show all available connectors with installed/configured status.",
|
|
74
|
+
formatter_class=FORMATTER,
|
|
75
|
+
)
|
|
76
|
+
add_json_flag(p_list)
|
|
77
|
+
p_list.set_defaults(func=_cmd_list)
|
|
78
|
+
|
|
79
|
+
# install
|
|
80
|
+
p_install = subs.add_parser(
|
|
81
|
+
"install",
|
|
82
|
+
help="Install a connector",
|
|
83
|
+
description=(
|
|
84
|
+
"Install a connector's dependencies and run its setup wizard.\n\n"
|
|
85
|
+
"Installs pip extras and configures OAuth credentials."
|
|
86
|
+
),
|
|
87
|
+
epilog=("examples:\n fp connect install <name> Install the <name> connector"),
|
|
88
|
+
formatter_class=FORMATTER,
|
|
89
|
+
)
|
|
90
|
+
p_install.add_argument("name", nargs="?", default=None, help="Connector name (from fp connect list)")
|
|
91
|
+
p_install.set_defaults(func=_cmd_install)
|
|
92
|
+
|
|
93
|
+
# remove
|
|
94
|
+
p_remove = subs.add_parser(
|
|
95
|
+
"remove",
|
|
96
|
+
help="Remove a connector",
|
|
97
|
+
description="Uninstall a connector's packages and disable in config.",
|
|
98
|
+
epilog=("examples:\n fp connect remove <name> Remove a connector and its packages"),
|
|
99
|
+
formatter_class=FORMATTER,
|
|
100
|
+
)
|
|
101
|
+
p_remove.add_argument("name", nargs="?", default=None, help="Connector name (from fp connect list)")
|
|
102
|
+
p_remove.set_defaults(func=_cmd_remove)
|
|
103
|
+
|
|
104
|
+
# status
|
|
105
|
+
p_status = subs.add_parser(
|
|
106
|
+
"status",
|
|
107
|
+
help="Show detailed connector state",
|
|
108
|
+
description=(
|
|
109
|
+
"Show detailed status for one or all connectors.\n\n"
|
|
110
|
+
"Includes install state, config, credentials, and account details."
|
|
111
|
+
),
|
|
112
|
+
epilog=(
|
|
113
|
+
"examples:\n fp connect status All connectors\n fp connect status <name> Single connector"
|
|
114
|
+
),
|
|
115
|
+
formatter_class=FORMATTER,
|
|
116
|
+
)
|
|
117
|
+
p_status.add_argument("name", nargs="?", default=None, help="Connector name (omit for all)")
|
|
118
|
+
add_json_flag(p_status)
|
|
119
|
+
p_status.set_defaults(func=_cmd_status)
|
|
120
|
+
|
|
121
|
+
# config
|
|
122
|
+
p_config = subs.add_parser(
|
|
123
|
+
"config",
|
|
124
|
+
help="Reconfigure an installed connector",
|
|
125
|
+
description=(
|
|
126
|
+
"Reconfigure an installed connector's settings.\n\n"
|
|
127
|
+
"Opens the setup wizard in reconfiguration mode —\n"
|
|
128
|
+
"skips dependency install, shows existing accounts,\n"
|
|
129
|
+
"and allows adding or modifying accounts."
|
|
130
|
+
),
|
|
131
|
+
epilog=("examples:\n fp connect config <name> Reconfigure a connector"),
|
|
132
|
+
formatter_class=FORMATTER,
|
|
133
|
+
)
|
|
134
|
+
p_config.add_argument("name", nargs="?", default=None, help="Connector name (from fp connect list)")
|
|
135
|
+
p_config.set_defaults(func=_cmd_config)
|
|
136
|
+
|
|
137
|
+
# label
|
|
138
|
+
p_label = subs.add_parser(
|
|
139
|
+
"label",
|
|
140
|
+
help="Set a display label for a connector account",
|
|
141
|
+
description=(
|
|
142
|
+
"Set a user-facing display label for a connector account.\n\n"
|
|
143
|
+
"The internal account name stays unchanged — only the\n"
|
|
144
|
+
"label shown in CLI output and status displays changes."
|
|
145
|
+
),
|
|
146
|
+
epilog=("examples:\n fp connect label <name> work Consulting\n fp connect label <name> personal Family"),
|
|
147
|
+
formatter_class=FORMATTER,
|
|
148
|
+
)
|
|
149
|
+
p_label.add_argument("name", nargs="?", default=None, help="Connector name (from fp connect list)")
|
|
150
|
+
p_label.add_argument("account", nargs="?", default=None, help="Account name (e.g. work, personal)")
|
|
151
|
+
p_label.add_argument("label", nargs="?", default=None, help="New display label")
|
|
152
|
+
|
|
153
|
+
def _label_handler(args, _parser=p_label) -> None:
|
|
154
|
+
if args.name is None or args.account is None or args.label is None:
|
|
155
|
+
connectors = discover_connectors()
|
|
156
|
+
if connectors:
|
|
157
|
+
console.print("Available connectors:\n")
|
|
158
|
+
for spec in connectors.values():
|
|
159
|
+
console.print(f" [bold]{spec.name}[/bold] — {spec.description}")
|
|
160
|
+
console.print("\nUsage: [bold]fp connect label <name> <account> <label>[/bold]")
|
|
161
|
+
else:
|
|
162
|
+
_parser.print_help()
|
|
163
|
+
return
|
|
164
|
+
_cmd_label(args)
|
|
165
|
+
|
|
166
|
+
p_label.set_defaults(func=_label_handler)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# ---------------------------------------------------------------------------
|
|
170
|
+
# Handlers
|
|
171
|
+
# ---------------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _cmd_list(args) -> None:
|
|
175
|
+
"""Show all connectors with installed/configured status."""
|
|
176
|
+
from footprinter.source_registry import ConfigError, get_config
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
config = get_config()
|
|
180
|
+
except ConfigError:
|
|
181
|
+
config = {}
|
|
182
|
+
|
|
183
|
+
connectors = discover_connectors()
|
|
184
|
+
|
|
185
|
+
if not connectors:
|
|
186
|
+
if getattr(args, "json", False):
|
|
187
|
+
output_json([])
|
|
188
|
+
return
|
|
189
|
+
console.print(
|
|
190
|
+
"No connectors installed.\n\n"
|
|
191
|
+
"Connectors are separate packages that plug into Footprinter's\n"
|
|
192
|
+
"ingest pipeline. To get started, visit:\n"
|
|
193
|
+
" https://github.com/swellcitygroup/footprinter"
|
|
194
|
+
)
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
if getattr(args, "json", False):
|
|
198
|
+
rows = []
|
|
199
|
+
for spec in connectors.values():
|
|
200
|
+
rows.append(
|
|
201
|
+
{
|
|
202
|
+
"name": spec.name,
|
|
203
|
+
"description": spec.description,
|
|
204
|
+
"status": get_status(spec, config),
|
|
205
|
+
}
|
|
206
|
+
)
|
|
207
|
+
output_json(rows)
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
from rich.table import Table
|
|
211
|
+
|
|
212
|
+
_STATUS_STYLE = {
|
|
213
|
+
"installed": "[green]installed[/green]",
|
|
214
|
+
"available": "[yellow]available[/yellow]",
|
|
215
|
+
"not available": "[dim]not available[/dim]",
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
table = Table(title="Connectors")
|
|
219
|
+
table.add_column("Name", style="bold")
|
|
220
|
+
table.add_column("Description")
|
|
221
|
+
table.add_column("Status")
|
|
222
|
+
|
|
223
|
+
for spec in connectors.values():
|
|
224
|
+
status = get_status(spec, config)
|
|
225
|
+
table.add_row(spec.name, spec.description, _STATUS_STYLE.get(status, status))
|
|
226
|
+
|
|
227
|
+
console.print(table)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _cmd_install(args) -> None:
|
|
231
|
+
"""Install a connector's dependencies and run its setup hook."""
|
|
232
|
+
import subprocess
|
|
233
|
+
|
|
234
|
+
name = args.name
|
|
235
|
+
connectors = discover_connectors()
|
|
236
|
+
if name is None:
|
|
237
|
+
if connectors:
|
|
238
|
+
console.print("Available connectors:\n")
|
|
239
|
+
for spec in connectors.values():
|
|
240
|
+
console.print(f" [bold]{spec.name}[/bold] — {spec.description}")
|
|
241
|
+
console.print("\nInstall one: [bold]fp connect install <name>[/bold]")
|
|
242
|
+
else:
|
|
243
|
+
console.print("No connectors available.\n\nLearn more: https://github.com/swellcitygroup/footprinter")
|
|
244
|
+
return
|
|
245
|
+
spec = connectors.get(name)
|
|
246
|
+
if spec is None:
|
|
247
|
+
console.print(f"[red]Unknown connector:[/red] {name}")
|
|
248
|
+
console.print(f"Available: {', '.join(connectors)}")
|
|
249
|
+
sys.exit(1)
|
|
250
|
+
|
|
251
|
+
already = is_installed(spec)
|
|
252
|
+
if already:
|
|
253
|
+
console.print(f"[green]{name}[/green] is already installed.")
|
|
254
|
+
|
|
255
|
+
# Check if already configured — prompt before reconfiguring
|
|
256
|
+
configured = is_configured(spec, _load_config())
|
|
257
|
+
if configured:
|
|
258
|
+
try:
|
|
259
|
+
if not Confirm.ask(
|
|
260
|
+
f" [bold]{name}[/bold] is already configured. Reconfigure?",
|
|
261
|
+
default=False,
|
|
262
|
+
):
|
|
263
|
+
console.print(" [dim]Keeping current configuration.[/dim]")
|
|
264
|
+
return
|
|
265
|
+
except PromptCancelled:
|
|
266
|
+
console.print("\n [dim]Keeping current configuration.[/dim]")
|
|
267
|
+
return
|
|
268
|
+
else:
|
|
269
|
+
console.print(f"Installing [bold]{name}[/bold] dependencies...")
|
|
270
|
+
try:
|
|
271
|
+
subprocess.check_call(
|
|
272
|
+
[sys.executable, "-m", "pip", "install", f"footprinter-cli[{spec.extra}]"],
|
|
273
|
+
)
|
|
274
|
+
except (subprocess.CalledProcessError, OSError) as e:
|
|
275
|
+
console.print(f"[red]Install failed:[/red] {e}")
|
|
276
|
+
sys.exit(1)
|
|
277
|
+
|
|
278
|
+
if not is_installed(spec):
|
|
279
|
+
console.print(f"[red]Install failed:[/red] {spec.probe_module} not importable after pip install")
|
|
280
|
+
sys.exit(1)
|
|
281
|
+
|
|
282
|
+
console.print(f"[green]{name} dependencies installed.[/green]")
|
|
283
|
+
|
|
284
|
+
# Run setup hook
|
|
285
|
+
try:
|
|
286
|
+
result = _resolve_setup_hook(spec, reconfigure=already)
|
|
287
|
+
if result:
|
|
288
|
+
_update_config_enabled(result, spec)
|
|
289
|
+
except (
|
|
290
|
+
Exception
|
|
291
|
+
) as e: # Intentional broad catch: setup hook loading is unpredictable (dynamic import + user config)
|
|
292
|
+
console.print(f"[red]Setup failed:[/red] {e}")
|
|
293
|
+
sys.exit(1)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _cmd_config(args) -> None:
|
|
297
|
+
"""Reconfigure an installed connector's settings."""
|
|
298
|
+
name = args.name
|
|
299
|
+
connectors = discover_connectors()
|
|
300
|
+
if name is None:
|
|
301
|
+
installed = {n: s for n, s in connectors.items() if is_installed(s)}
|
|
302
|
+
if installed:
|
|
303
|
+
console.print("Installed connectors:\n")
|
|
304
|
+
for spec in installed.values():
|
|
305
|
+
console.print(f" [bold]{spec.name}[/bold] — {spec.description}")
|
|
306
|
+
console.print("\nConfigure one: [bold]fp connect config <name>[/bold]")
|
|
307
|
+
else:
|
|
308
|
+
console.print("No connectors are installed.\n\nInstall one first: [bold]fp connect install <name>[/bold]")
|
|
309
|
+
return
|
|
310
|
+
spec = connectors.get(name)
|
|
311
|
+
if spec is None:
|
|
312
|
+
console.print(f"[red]Unknown connector:[/red] {name}")
|
|
313
|
+
console.print(f"Available: {', '.join(connectors)}")
|
|
314
|
+
sys.exit(1)
|
|
315
|
+
|
|
316
|
+
if not is_installed(spec):
|
|
317
|
+
console.print(f"[dim]{name} is not installed.[/dim]")
|
|
318
|
+
console.print(f" Install first: [bold]fp connect install {name}[/bold]")
|
|
319
|
+
sys.exit(1)
|
|
320
|
+
|
|
321
|
+
try:
|
|
322
|
+
result = _resolve_setup_hook(spec, reconfigure=True)
|
|
323
|
+
if result:
|
|
324
|
+
_update_config_enabled(result, spec)
|
|
325
|
+
except (
|
|
326
|
+
Exception
|
|
327
|
+
) as e: # Intentional broad catch: setup hook loading is unpredictable (dynamic import + user config)
|
|
328
|
+
console.print(f"[red]Setup failed:[/red] {e}")
|
|
329
|
+
sys.exit(1)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _resolve_setup_hook(spec, **kwargs):
|
|
333
|
+
"""Resolve and call the connector's setup hook. Returns hook result."""
|
|
334
|
+
hook_fn = resolve_hook(spec.setup_hook)
|
|
335
|
+
if hook_fn is None:
|
|
336
|
+
return None
|
|
337
|
+
return hook_fn(**kwargs)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _update_config_enabled(result: dict, spec: ConnectorSpec) -> None:
|
|
341
|
+
"""Update config enabled flags based on setup hook result.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
result: Dict like {"personal": {"services": ["drive", "gmail"], "root_folder_id": "..."}}.
|
|
345
|
+
spec: The ConnectorSpec whose config_apply hook to call.
|
|
346
|
+
"""
|
|
347
|
+
from footprinter.cli.setup import _require_config, write_config
|
|
348
|
+
|
|
349
|
+
config, config_path = _require_config()
|
|
350
|
+
if spec.config_apply:
|
|
351
|
+
fn = resolve_hook(spec.config_apply)
|
|
352
|
+
if fn:
|
|
353
|
+
fn(config, result)
|
|
354
|
+
write_config(config, config_path)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _cmd_remove(args) -> None:
|
|
358
|
+
"""Remove a connector's dependencies and disable in config."""
|
|
359
|
+
import subprocess
|
|
360
|
+
|
|
361
|
+
name = args.name
|
|
362
|
+
connectors = discover_connectors()
|
|
363
|
+
if name is None:
|
|
364
|
+
installed = {n: s for n, s in connectors.items() if is_installed(s)}
|
|
365
|
+
if installed:
|
|
366
|
+
console.print("Installed connectors:\n")
|
|
367
|
+
for spec in installed.values():
|
|
368
|
+
console.print(f" [bold]{spec.name}[/bold] — {spec.description}")
|
|
369
|
+
console.print("\nRemove one: [bold]fp connect remove <name>[/bold]")
|
|
370
|
+
else:
|
|
371
|
+
console.print("No connectors are installed.")
|
|
372
|
+
return
|
|
373
|
+
spec = connectors.get(name)
|
|
374
|
+
if spec is None:
|
|
375
|
+
console.print(f"[red]Unknown connector:[/red] {name}")
|
|
376
|
+
console.print(f"Available: {', '.join(connectors)}")
|
|
377
|
+
sys.exit(1)
|
|
378
|
+
|
|
379
|
+
if not is_installed(spec):
|
|
380
|
+
console.print(f"[dim]{name} is not installed.[/dim]")
|
|
381
|
+
return
|
|
382
|
+
|
|
383
|
+
console.print(f"Removing [bold]{name}[/bold] packages...")
|
|
384
|
+
try:
|
|
385
|
+
subprocess.check_call(
|
|
386
|
+
[sys.executable, "-m", "pip", "uninstall", "-y", *spec.remove_packages],
|
|
387
|
+
)
|
|
388
|
+
except (subprocess.CalledProcessError, OSError) as e:
|
|
389
|
+
console.print(f"[red]Uninstall failed:[/red] {e}")
|
|
390
|
+
sys.exit(1)
|
|
391
|
+
|
|
392
|
+
_disable_config_sections(spec)
|
|
393
|
+
console.print(f"[green]{name} removed.[/green]")
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _disable_config_sections(spec) -> None:
|
|
397
|
+
"""Disable config sections for a connector."""
|
|
398
|
+
from footprinter.cli.setup import _require_config, write_config
|
|
399
|
+
|
|
400
|
+
try:
|
|
401
|
+
config, config_path = _require_config()
|
|
402
|
+
except SystemExit:
|
|
403
|
+
return
|
|
404
|
+
|
|
405
|
+
for section in spec.config_sections:
|
|
406
|
+
if section in config:
|
|
407
|
+
config[section]["enabled"] = False
|
|
408
|
+
write_config(config, config_path)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _cmd_status(args) -> None:
|
|
412
|
+
"""Show detailed connector state."""
|
|
413
|
+
name = getattr(args, "name", None)
|
|
414
|
+
config = _load_config()
|
|
415
|
+
connectors = discover_connectors()
|
|
416
|
+
|
|
417
|
+
if name:
|
|
418
|
+
spec = connectors.get(name)
|
|
419
|
+
if spec is None:
|
|
420
|
+
console.print(f"[red]Unknown connector:[/red] {name}")
|
|
421
|
+
sys.exit(1)
|
|
422
|
+
specs = [spec]
|
|
423
|
+
else:
|
|
424
|
+
specs = list(connectors.values())
|
|
425
|
+
|
|
426
|
+
if getattr(args, "json", False):
|
|
427
|
+
if len(specs) == 1:
|
|
428
|
+
output_json(_status_dict(specs[0], config))
|
|
429
|
+
else:
|
|
430
|
+
output_json([_status_dict(s, config) for s in specs])
|
|
431
|
+
return
|
|
432
|
+
|
|
433
|
+
for spec in specs:
|
|
434
|
+
_print_status_panel(spec, config)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _load_config() -> dict:
|
|
438
|
+
"""Load config, returning empty dict on failure."""
|
|
439
|
+
from footprinter.source_registry import ConfigError, get_config
|
|
440
|
+
|
|
441
|
+
try:
|
|
442
|
+
return get_config()
|
|
443
|
+
except ConfigError:
|
|
444
|
+
return {}
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _resolve_auth_label(spec: ConnectorSpec, config: dict) -> str:
|
|
448
|
+
"""Resolve auth status: check_auth callable if available, else credential existence."""
|
|
449
|
+
from footprinter.connectors import has_credentials
|
|
450
|
+
|
|
451
|
+
auth_result = resolve_check_auth(spec, config)
|
|
452
|
+
if auth_result is not None:
|
|
453
|
+
return auth_result
|
|
454
|
+
return "credentials found" if has_credentials(spec, config) else "no credentials"
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def _status_dict(spec: ConnectorSpec, config: dict) -> dict:
|
|
458
|
+
"""Build a status dict for a connector."""
|
|
459
|
+
from footprinter.connectors import has_credentials as _has_creds
|
|
460
|
+
|
|
461
|
+
creds = _has_creds(spec, config)
|
|
462
|
+
auth = _resolve_auth_label(spec, config)
|
|
463
|
+
|
|
464
|
+
accounts = []
|
|
465
|
+
if is_configured(spec, config):
|
|
466
|
+
for section in spec.config_sections:
|
|
467
|
+
if section in config:
|
|
468
|
+
for acct in config[section].get("accounts", []):
|
|
469
|
+
token_path = acct.get("token_path", "")
|
|
470
|
+
expanded = Path(os.path.expanduser(token_path))
|
|
471
|
+
accounts.append(
|
|
472
|
+
{
|
|
473
|
+
"name": acct.get("name", ""),
|
|
474
|
+
"label": account_label(acct),
|
|
475
|
+
"section": section,
|
|
476
|
+
"token_path": str(expanded),
|
|
477
|
+
"token_exists": expanded.exists(),
|
|
478
|
+
}
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
return {
|
|
482
|
+
"name": spec.name,
|
|
483
|
+
"description": spec.description,
|
|
484
|
+
"status": get_status(spec, config),
|
|
485
|
+
"auth": auth,
|
|
486
|
+
"credentials": creds,
|
|
487
|
+
"pipes": list(spec.pipes),
|
|
488
|
+
"accounts": accounts,
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def _print_status_panel(spec: ConnectorSpec, config: dict) -> None:
|
|
493
|
+
"""Print a Rich panel with connector status."""
|
|
494
|
+
from rich.panel import Panel
|
|
495
|
+
|
|
496
|
+
status = get_status(spec, config)
|
|
497
|
+
status_style = {
|
|
498
|
+
"installed": "[green]installed[/green]",
|
|
499
|
+
"available": "[yellow]available[/yellow]",
|
|
500
|
+
"not available": "[red]not available[/red]",
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
auth_style = {
|
|
504
|
+
"authenticated": "[green]authenticated[/green]",
|
|
505
|
+
"expired": "[yellow]expired[/yellow]",
|
|
506
|
+
"error": "[red]error[/red]",
|
|
507
|
+
"no credentials": "[red]no credentials[/red]",
|
|
508
|
+
"credentials found": "[dim]credentials found[/dim]",
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
auth = _resolve_auth_label(spec, config)
|
|
512
|
+
|
|
513
|
+
lines = []
|
|
514
|
+
lines.append(f" Status: {status_style.get(status, status)}")
|
|
515
|
+
lines.append(f" Auth: {auth_style.get(auth, auth)}")
|
|
516
|
+
lines.append(f" Pipes: {', '.join(spec.pipes)}")
|
|
517
|
+
|
|
518
|
+
# Account details — only when connector is configured
|
|
519
|
+
if is_configured(spec, config):
|
|
520
|
+
for section in spec.config_sections:
|
|
521
|
+
if section in config:
|
|
522
|
+
for acct in config[section].get("accounts", []):
|
|
523
|
+
token_path = Path(os.path.expanduser(acct.get("token_path", "")))
|
|
524
|
+
token_icon = "[green]yes[/green]" if token_path.exists() else "[red]no[/red]"
|
|
525
|
+
lines.append(f" {account_label(acct)} ({section}): token {token_icon}")
|
|
526
|
+
|
|
527
|
+
if status == "not available":
|
|
528
|
+
lines.append(f"\n Install: [bold]fp connect install {spec.name}[/bold]")
|
|
529
|
+
elif status == "available":
|
|
530
|
+
lines.append(f"\n Configure: [bold]fp connect install {spec.name}[/bold]")
|
|
531
|
+
|
|
532
|
+
console.print(
|
|
533
|
+
Panel(
|
|
534
|
+
"\n".join(lines),
|
|
535
|
+
title=f"[bold]{spec.name}[/bold] — {spec.description}",
|
|
536
|
+
)
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def _require_config_for_label() -> tuple[dict, str]:
|
|
541
|
+
"""Load config for the label command. Separate function for testability."""
|
|
542
|
+
from footprinter.cli.setup import _require_config
|
|
543
|
+
|
|
544
|
+
return _require_config()
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def _cmd_label(args) -> None:
|
|
548
|
+
"""Set a display label for a connector account."""
|
|
549
|
+
from footprinter.cli.setup import write_config
|
|
550
|
+
|
|
551
|
+
name = args.name
|
|
552
|
+
connectors = discover_connectors()
|
|
553
|
+
spec = connectors.get(name)
|
|
554
|
+
if spec is None:
|
|
555
|
+
console.print(f"[red]Unknown connector:[/red] {name}")
|
|
556
|
+
console.print(f"Available: {', '.join(connectors)}")
|
|
557
|
+
sys.exit(1)
|
|
558
|
+
|
|
559
|
+
config, config_path = _require_config_for_label()
|
|
560
|
+
|
|
561
|
+
acct_name = args.account
|
|
562
|
+
new_label = args.label
|
|
563
|
+
found = False
|
|
564
|
+
|
|
565
|
+
# Update account entries across all config sections for this connector
|
|
566
|
+
for section in spec.config_sections:
|
|
567
|
+
if section not in config:
|
|
568
|
+
continue
|
|
569
|
+
for acct in config[section].get("accounts", []):
|
|
570
|
+
if acct.get("name") == acct_name:
|
|
571
|
+
acct["label"] = new_label
|
|
572
|
+
found = True
|
|
573
|
+
|
|
574
|
+
if not found:
|
|
575
|
+
console.print(f"[red]Account not found:[/red] {acct_name}")
|
|
576
|
+
console.print(f" Run [bold]fp connect status {name}[/bold] to see configured accounts.")
|
|
577
|
+
sys.exit(1)
|
|
578
|
+
|
|
579
|
+
# Update matching source seed labels (scoped to this connector's seeds)
|
|
580
|
+
if spec.seed_prefix:
|
|
581
|
+
seed_name = f"{spec.seed_prefix}_{acct_name}"
|
|
582
|
+
for seed in config.get("source_seeds", []):
|
|
583
|
+
if seed.get("name") == seed_name and seed.get("source_type") == "remote":
|
|
584
|
+
if spec.seed_label_fn:
|
|
585
|
+
fn = resolve_hook(spec.seed_label_fn)
|
|
586
|
+
seed["label"] = fn(new_label) if fn else new_label
|
|
587
|
+
else:
|
|
588
|
+
seed["label"] = new_label
|
|
589
|
+
|
|
590
|
+
write_config(config, config_path)
|
|
591
|
+
console.print(f"[green]Label updated:[/green] {acct_name} → {new_label}")
|