onesearch-cli 0.12.1__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.
onesearch/config.py ADDED
@@ -0,0 +1,185 @@
1
+ # Copyright (C) 2025 demigodmode
2
+ # SPDX-License-Identifier: AGPL-3.0-only
3
+
4
+ """Configuration management for OneSearch CLI."""
5
+
6
+ import os
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ import yaml
11
+
12
+
13
+ def get_config_dir() -> Path:
14
+ """Get the configuration directory path."""
15
+ # Use XDG_CONFIG_HOME on Linux, or appropriate dir on Windows/Mac
16
+ if os.name == "nt": # Windows
17
+ config_base = Path(os.environ.get("APPDATA", Path.home() / "AppData" / "Roaming"))
18
+ else: # Linux/Mac
19
+ config_base = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config"))
20
+ return config_base / "onesearch"
21
+
22
+
23
+ def get_config_path() -> Path:
24
+ """Get the configuration file path."""
25
+ return get_config_dir() / "config.yml"
26
+
27
+
28
+ def load_config() -> dict:
29
+ """Load configuration from file.
30
+
31
+ Returns:
32
+ Configuration dictionary, or empty dict if file doesn't exist.
33
+ """
34
+ config_path = get_config_path()
35
+ if not config_path.exists():
36
+ return {}
37
+
38
+ try:
39
+ with open(config_path) as f:
40
+ config = yaml.safe_load(f) or {}
41
+ return config
42
+ except Exception:
43
+ return {}
44
+
45
+
46
+ def save_config(config: dict) -> None:
47
+ """Save configuration to file.
48
+
49
+ Args:
50
+ config: Configuration dictionary to save.
51
+ """
52
+ config_dir = get_config_dir()
53
+ config_dir.mkdir(parents=True, exist_ok=True)
54
+
55
+ config_path = get_config_path()
56
+ with open(config_path, "w") as f:
57
+ yaml.safe_dump(config, f, default_flow_style=False, sort_keys=False)
58
+
59
+
60
+ def get_config_value(key: str, default: Any = None) -> Any:
61
+ """Get a configuration value.
62
+
63
+ Args:
64
+ key: Dot-separated key path (e.g., "output.colors").
65
+ default: Default value if key not found.
66
+
67
+ Returns:
68
+ Configuration value or default.
69
+ """
70
+ config = load_config()
71
+ keys = key.split(".")
72
+ value = config
73
+
74
+ for k in keys:
75
+ if isinstance(value, dict) and k in value:
76
+ value = value[k]
77
+ else:
78
+ return default
79
+
80
+ return value
81
+
82
+
83
+ def set_config_value(key: str, value: Any) -> None:
84
+ """Set a configuration value.
85
+
86
+ Args:
87
+ key: Dot-separated key path (e.g., "output.colors").
88
+ value: Value to set.
89
+ """
90
+ config = load_config()
91
+ keys = key.split(".")
92
+
93
+ # Navigate to the parent dict
94
+ current = config
95
+ for k in keys[:-1]:
96
+ if k not in current or not isinstance(current[k], dict):
97
+ current[k] = {}
98
+ current = current[k]
99
+
100
+ # Set the value
101
+ current[keys[-1]] = value
102
+ save_config(config)
103
+
104
+
105
+ def delete_config_value(key: str) -> bool:
106
+ """Delete a configuration value.
107
+
108
+ Args:
109
+ key: Dot-separated key path.
110
+
111
+ Returns:
112
+ True if value was deleted, False if not found.
113
+ """
114
+ config = load_config()
115
+ keys = key.split(".")
116
+
117
+ # Navigate to the parent dict
118
+ current = config
119
+ for k in keys[:-1]:
120
+ if k not in current or not isinstance(current[k], dict):
121
+ return False
122
+ current = current[k]
123
+
124
+ # Delete the key
125
+ if keys[-1] in current:
126
+ del current[keys[-1]]
127
+ save_config(config)
128
+ return True
129
+ return False
130
+
131
+
132
+ def get_backend_url() -> str:
133
+ """Get the backend URL from config/env/default.
134
+
135
+ Priority:
136
+ 1. Environment variable ONESEARCH_URL
137
+ 2. Config file backend_url
138
+ 3. Default http://localhost:8000
139
+ """
140
+ env_url = os.environ.get("ONESEARCH_URL")
141
+ if env_url:
142
+ return env_url
143
+
144
+ config_url = get_config_value("backend_url")
145
+ if config_url:
146
+ return config_url
147
+
148
+ return "http://localhost:8000"
149
+
150
+
151
+ def get_auth_token() -> str | None:
152
+ """Get auth token from env/config.
153
+
154
+ Priority:
155
+ 1. Environment variable ONESEARCH_TOKEN
156
+ 2. Config file auth.token
157
+ """
158
+ env_token = os.environ.get("ONESEARCH_TOKEN")
159
+ if env_token:
160
+ return env_token
161
+
162
+ return get_config_value("auth.token")
163
+
164
+
165
+ # Default configuration template
166
+ DEFAULT_CONFIG = """\
167
+ # OneSearch CLI Configuration
168
+ # Location: {config_path}
169
+
170
+ # Backend API URL
171
+ backend_url: http://localhost:8000
172
+
173
+ # Authentication
174
+ auth:
175
+ token: null
176
+
177
+ # Output settings
178
+ output:
179
+ colors: true
180
+ format: table # table or json
181
+
182
+ # Default values for commands
183
+ defaults:
184
+ search_limit: 20
185
+ """
onesearch/context.py ADDED
@@ -0,0 +1,51 @@
1
+ # Copyright (C) 2025 demigodmode
2
+ # SPDX-License-Identifier: AGPL-3.0-only
3
+
4
+ """Shared CLI context and utilities."""
5
+
6
+ import io
7
+
8
+ import click
9
+ from rich.console import Console
10
+
11
+ from onesearch.api import OneSearchAPI
12
+ from onesearch.config import get_auth_token
13
+
14
+ # Rich console for output
15
+ console = Console()
16
+ err_console = Console(stderr=True)
17
+
18
+ # Quiet console that discards output
19
+ _quiet_console = Console(file=io.StringIO(), force_terminal=False)
20
+
21
+
22
+ class Context:
23
+ """CLI context object passed to commands."""
24
+
25
+ def __init__(self):
26
+ self.api: OneSearchAPI | None = None
27
+ self.verbose: bool = False
28
+ self.quiet: bool = False
29
+ self.url: str = "http://localhost:8000"
30
+ self.token: str | None = None
31
+
32
+ def get_api(self) -> OneSearchAPI:
33
+ """Get or create the API client."""
34
+ if self.api is None:
35
+ self.token = get_auth_token()
36
+ self.api = OneSearchAPI(base_url=self.url, token=self.token)
37
+ return self.api
38
+
39
+ def reset_api(self) -> None:
40
+ """Clear cached API client after config/auth changes."""
41
+ self.api = None
42
+ self.token = None
43
+
44
+ def get_console(self) -> Console:
45
+ """Get the appropriate console based on quiet mode."""
46
+ if self.quiet:
47
+ return _quiet_console
48
+ return console
49
+
50
+
51
+ pass_context = click.make_pass_decorator(Context, ensure=True)
onesearch/main.py ADDED
@@ -0,0 +1,153 @@
1
+ # Copyright (C) 2025 demigodmode
2
+ # SPDX-License-Identifier: AGPL-3.0-only
3
+
4
+ """OneSearch CLI entry point."""
5
+
6
+ import os
7
+ import sys
8
+
9
+ import click
10
+
11
+ from onesearch import __version__
12
+ from onesearch.api import APIError
13
+ from onesearch.banner import build_startup_panel
14
+ from onesearch.config import load_config
15
+ from onesearch.context import Context, console, err_console
16
+
17
+ # Context settings for all commands
18
+ CONTEXT_SETTINGS = {
19
+ "help_option_names": ["-h", "--help"],
20
+ }
21
+
22
+
23
+ def get_default_url() -> str:
24
+ """Get the default backend URL from config/env/default."""
25
+ from onesearch.config import get_backend_url
26
+ return get_backend_url()
27
+
28
+
29
+ def _has_configured_backend(resolved_url: str | None = None) -> bool:
30
+ config_data = load_config()
31
+ return bool(resolved_url or os.environ.get("ONESEARCH_URL") or config_data.get("backend_url"))
32
+
33
+
34
+ def _render_startup_panel(ctx: Context) -> None:
35
+ configured = _has_configured_backend(ctx.url)
36
+ if not configured:
37
+ console.print(
38
+ build_startup_panel(
39
+ configured=False,
40
+ backend_url=None,
41
+ cli_version=__version__,
42
+ )
43
+ )
44
+ return
45
+
46
+ api = ctx.get_api()
47
+ try:
48
+ health = api.health(allow_degraded=True)
49
+ server_version = health.get("version")
50
+ server_status = health.get("status", "unknown")
51
+ auth_state = "not logged in"
52
+
53
+ try:
54
+ user = api.whoami()
55
+ auth_state = f"logged in as {user.get('username', 'unknown')}"
56
+ except APIError as auth_error:
57
+ if auth_error.status_code not in (401, 403):
58
+ console.print(
59
+ build_startup_panel(
60
+ configured=True,
61
+ backend_url=ctx.url,
62
+ cli_version=__version__,
63
+ server_version=server_version,
64
+ server_status=server_status,
65
+ error_message=auth_error.message,
66
+ )
67
+ )
68
+ return
69
+
70
+ console.print(
71
+ build_startup_panel(
72
+ configured=True,
73
+ backend_url=ctx.url,
74
+ cli_version=__version__,
75
+ server_version=server_version,
76
+ server_status=server_status,
77
+ auth_state=auth_state,
78
+ )
79
+ )
80
+ except APIError as e:
81
+ console.print(
82
+ build_startup_panel(
83
+ configured=True,
84
+ backend_url=ctx.url,
85
+ cli_version=__version__,
86
+ error_message=e.message,
87
+ )
88
+ )
89
+
90
+
91
+ @click.group(context_settings=CONTEXT_SETTINGS, invoke_without_command=True)
92
+ @click.version_option(version=__version__, prog_name="onesearch")
93
+ @click.option(
94
+ "--url",
95
+ envvar="ONESEARCH_URL",
96
+ default=get_default_url,
97
+ help="Backend API URL.",
98
+ show_default=True,
99
+ )
100
+ @click.option(
101
+ "-v", "--verbose",
102
+ is_flag=True,
103
+ help="Enable verbose output.",
104
+ )
105
+ @click.option(
106
+ "-q", "--quiet",
107
+ is_flag=True,
108
+ help="Suppress non-essential output. Only show results/errors.",
109
+ )
110
+ @click.pass_context
111
+ def cli(click_ctx: click.Context, url: str, verbose: bool, quiet: bool):
112
+ """OneSearch - Self-hosted, privacy-focused search for your homelab.
113
+
114
+ Search across all your files, documents, and notes from a single,
115
+ unified command-line interface.
116
+
117
+ \b
118
+ Examples:
119
+ onesearch search "kubernetes deployment"
120
+ onesearch source list
121
+ onesearch status
122
+ onesearch health
123
+
124
+ Use 'onesearch COMMAND --help' for more information on a command.
125
+ """
126
+ ctx = click_ctx.ensure_object(Context)
127
+ ctx.url = url
128
+ ctx.verbose = verbose
129
+ ctx.quiet = quiet
130
+
131
+ if click_ctx.invoked_subcommand is None:
132
+ _render_startup_panel(ctx)
133
+
134
+
135
+ # Import and register command groups after cli is defined
136
+ from onesearch.commands import auth, config, search, source, status # noqa: E402, F401
137
+
138
+
139
+ def main():
140
+ """Main entry point."""
141
+ try:
142
+ cli()
143
+ except KeyboardInterrupt:
144
+ err_console.print("\n[yellow]Aborted.[/yellow]")
145
+ sys.exit(130)
146
+ except Exception as e:
147
+ # Show concise error; users can use -v for more details
148
+ err_console.print(f"[red]Error:[/red] {type(e).__name__}: {e}")
149
+ sys.exit(1)
150
+
151
+
152
+ if __name__ == "__main__":
153
+ main()
@@ -0,0 +1,222 @@
1
+ Metadata-Version: 2.4
2
+ Name: onesearch-cli
3
+ Version: 0.12.1
4
+ Summary: Standalone CLI client for a running OneSearch server
5
+ License: AGPL-3.0-only
6
+ Requires-Python: >=3.10
7
+ Requires-Dist: click>=8.1.0
8
+ Requires-Dist: python-dotenv>=1.0.0
9
+ Requires-Dist: pyyaml>=6.0.0
10
+ Requires-Dist: requests>=2.31.0
11
+ Requires-Dist: rich>=13.0.0
12
+ Description-Content-Type: text/markdown
13
+
14
+ # OneSearch CLI
15
+
16
+ Command-line interface for OneSearch - Self-hosted, privacy-focused search for your homelab.
17
+
18
+ ## Installation
19
+
20
+ The CLI is a standalone client for a running OneSearch server. It does not include the backend, indexer, or search data. Tagged OneSearch releases publish the Docker image and the `onesearch-cli` package together on the same shared version.
21
+
22
+ ### Recommended: pipx
23
+
24
+ ```bash
25
+ pipx install onesearch-cli
26
+ onesearch config set backend_url http://localhost:8000
27
+ onesearch login
28
+ ```
29
+
30
+ ### From Source (Development)
31
+
32
+ ```bash
33
+ cd cli
34
+ python -m venv .venv
35
+
36
+ # Windows
37
+ .venv\Scripts\activate
38
+
39
+ # Linux/Mac
40
+ source .venv/bin/activate
41
+
42
+ pip install -e .
43
+ ```
44
+
45
+ ### Verify Installation
46
+
47
+ ```bash
48
+ onesearch --version
49
+ onesearch --help
50
+ ```
51
+
52
+ ## Quick Start
53
+
54
+ ```bash
55
+ # Check system health
56
+ onesearch health
57
+
58
+ # List configured sources
59
+ onesearch source list
60
+
61
+ # Add a source (ID is auto-generated from name as slug, e.g., "documents")
62
+ onesearch source add "Documents" /data/docs --include "**/*.pdf,**/*.md"
63
+
64
+ # Trigger indexing (use source ID from 'source list')
65
+ onesearch source reindex documents
66
+
67
+ # Search
68
+ onesearch search "kubernetes deployment"
69
+
70
+ # Check indexing status
71
+ onesearch status
72
+ ```
73
+
74
+ > **Note:** Source IDs are slugified from the name (e.g., "My Documents" → "my-documents").
75
+
76
+ ## Commands
77
+
78
+ ### Source Management
79
+
80
+ ```bash
81
+ # List all sources
82
+ onesearch source list
83
+
84
+ # Add a new source
85
+ onesearch source add <name> <path> [--include PATTERNS] [--exclude PATTERNS]
86
+
87
+ # Show source details
88
+ onesearch source show <source_id>
89
+
90
+ # Reindex a source
91
+ onesearch source reindex <source_id>
92
+
93
+ # Delete a source
94
+ onesearch source delete <source_id> [--yes]
95
+ ```
96
+
97
+ ### Search
98
+
99
+ ```bash
100
+ # Basic search
101
+ onesearch search "query"
102
+
103
+ # With filters (use source ID from 'source list')
104
+ onesearch search "python" --source documents --type pdf --limit 10
105
+
106
+ # JSON output for scripting
107
+ onesearch search "error" --json | jq '.results[].path'
108
+
109
+ # Pagination
110
+ onesearch search "docker" --offset 20 --limit 10
111
+ ```
112
+
113
+ ### Status & Health
114
+
115
+ ```bash
116
+ # Overall status
117
+ onesearch status
118
+
119
+ # Specific source status
120
+ onesearch status <source_id>
121
+
122
+ # System health check
123
+ onesearch health
124
+
125
+ # JSON output for monitoring
126
+ onesearch health --json
127
+ ```
128
+
129
+ ## Configuration
130
+
131
+ ### Environment Variables
132
+
133
+ - `ONESEARCH_URL` - Backend API URL (default: `http://localhost:8000`)
134
+ - `ONESEARCH_TOKEN` - Bearer token for non-interactive auth
135
+
136
+ ```bash
137
+ # Use a custom backend URL
138
+ export ONESEARCH_URL=http://onesearch.local:8000
139
+ onesearch search "test"
140
+
141
+ # Non-interactive auth for scripts
142
+ export ONESEARCH_TOKEN=xxxxx
143
+ onesearch search "test" --json
144
+
145
+ # Or pass the URL directly
146
+ onesearch --url http://onesearch.local:8000 search "test"
147
+ ```
148
+
149
+ ### Global Options
150
+
151
+ - `--url URL` - Override backend API URL
152
+ - `-v, --verbose` - Enable verbose output
153
+ - `-q, --quiet` - Suppress non-essential output (headers, hints, decorations)
154
+ - `-h, --help` - Show help message
155
+ - `--version` - Show version
156
+
157
+ ## Output Formats
158
+
159
+ Most commands support `--json` for machine-readable output:
160
+
161
+ ```bash
162
+ # JSON for scripting
163
+ onesearch health --json
164
+ onesearch status --json
165
+ onesearch search "test" --json
166
+ ```
167
+
168
+ ## Examples
169
+
170
+ ### Add and Index a Source
171
+
172
+ ```bash
173
+ # Add a documents source (creates ID "my-documents")
174
+ onesearch source add "My Documents" /mnt/nas/documents \
175
+ --include "**/*.pdf,**/*.md,**/*.txt" \
176
+ --exclude "**/archive/**"
177
+
178
+ # Start indexing (use the source ID)
179
+ onesearch source reindex my-documents
180
+
181
+ # Monitor progress
182
+ onesearch status my-documents
183
+ ```
184
+
185
+ > **Docker users:** If the path only exists inside the container, use `--no-validate` to skip local path validation:
186
+ > ```bash
187
+ > onesearch source add "NAS Docs" /data/nas --no-validate
188
+ > ```
189
+
190
+ ### Search with Filters
191
+
192
+ ```bash
193
+ # Search PDFs only
194
+ onesearch search "quarterly report" --type pdf
195
+
196
+ # Search specific source (use source ID)
197
+ onesearch search "meeting notes" --source my-documents
198
+
199
+ # Get more results
200
+ onesearch search "python" --limit 50
201
+ ```
202
+
203
+ ### Health Monitoring
204
+
205
+ ```bash
206
+ # Quick health check (returns non-zero on failure)
207
+ onesearch health || echo "OneSearch is down!"
208
+
209
+ # JSON for monitoring systems
210
+ onesearch health --json | jq '.status'
211
+ ```
212
+
213
+ ## Development
214
+
215
+ ```bash
216
+ # Install CLI in development mode
217
+ pip install -e .
218
+
219
+ # Run tests (install pytest first)
220
+ pip install pytest
221
+ pytest
222
+ ```
@@ -0,0 +1,16 @@
1
+ onesearch/__init__.py,sha256=CNzQ2BVpOiKjVj9EVq-K42qipFQTcmqJyouwfP5QR2A,159
2
+ onesearch/api.py,sha256=rLqWMcBU8fcI4wrCu-BSdk_I9zMqRu5EHz5YKh03jkE,7819
3
+ onesearch/banner.py,sha256=bBUaJ57kJuLtVYHogc628A8dvA_YdygvpFzFdCzr3XE,2244
4
+ onesearch/config.py,sha256=_0lMbXk3Hy6chW1pmPMVkK8kSBPfmqTsl6PTsbSh98s,4252
5
+ onesearch/context.py,sha256=kMCRiR7_fTbISktPevZDmhQqQ8J6tdBMc1pKRyYOxcc,1382
6
+ onesearch/main.py,sha256=ZmRglroIaAjrgTFhSY1p6EMuYVSJx3ZLzmLIT5RhzSM,4360
7
+ onesearch/commands/__init__.py,sha256=VvkpGc4aV5zn_mUTyW06pKm_wlcCkSIvevmyM5O_v3M,102
8
+ onesearch/commands/auth.py,sha256=Jh_VKaSJtHcqboh-jHNA9yzkTVVW8ysE6c4nTaFwuho,2040
9
+ onesearch/commands/config.py,sha256=kixtrN5aFi8_JfXb9nhcjDgjk7v7RCZmoQn29wqQcTM,4761
10
+ onesearch/commands/search.py,sha256=Oxn6y8tQnXY_Msf22B08PfB5taijZkwGtXKaRtMMlFI,4833
11
+ onesearch/commands/source.py,sha256=snNnCtoAuQOw1Bh9J6SYCrhE8Uw-_fYyMupnE-043tc,8256
12
+ onesearch/commands/status.py,sha256=2kqJAPMlD467QRn0z480T1MFOQmG1cnir62rE4s1Xb4,8191
13
+ onesearch_cli-0.12.1.dist-info/METADATA,sha256=zNg24frjkscREjp7dpcKO-lXDRzc-JXdpobCQj_OV3s,4623
14
+ onesearch_cli-0.12.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
15
+ onesearch_cli-0.12.1.dist-info/entry_points.txt,sha256=WdfuKdCaH4rLJIwtZZBmC3zZ_l0PyGNuDvvg5m2UJiE,49
16
+ onesearch_cli-0.12.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ onesearch = onesearch.main:cli