router-maestro 0.1.2__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 (57) hide show
  1. router_maestro/__init__.py +3 -0
  2. router_maestro/__main__.py +6 -0
  3. router_maestro/auth/__init__.py +18 -0
  4. router_maestro/auth/github_oauth.py +181 -0
  5. router_maestro/auth/manager.py +136 -0
  6. router_maestro/auth/storage.py +91 -0
  7. router_maestro/cli/__init__.py +1 -0
  8. router_maestro/cli/auth.py +167 -0
  9. router_maestro/cli/client.py +322 -0
  10. router_maestro/cli/config.py +132 -0
  11. router_maestro/cli/context.py +146 -0
  12. router_maestro/cli/main.py +42 -0
  13. router_maestro/cli/model.py +288 -0
  14. router_maestro/cli/server.py +117 -0
  15. router_maestro/cli/stats.py +76 -0
  16. router_maestro/config/__init__.py +72 -0
  17. router_maestro/config/contexts.py +29 -0
  18. router_maestro/config/paths.py +50 -0
  19. router_maestro/config/priorities.py +93 -0
  20. router_maestro/config/providers.py +34 -0
  21. router_maestro/config/server.py +115 -0
  22. router_maestro/config/settings.py +76 -0
  23. router_maestro/providers/__init__.py +31 -0
  24. router_maestro/providers/anthropic.py +203 -0
  25. router_maestro/providers/base.py +123 -0
  26. router_maestro/providers/copilot.py +346 -0
  27. router_maestro/providers/openai.py +188 -0
  28. router_maestro/providers/openai_compat.py +175 -0
  29. router_maestro/routing/__init__.py +5 -0
  30. router_maestro/routing/router.py +526 -0
  31. router_maestro/server/__init__.py +5 -0
  32. router_maestro/server/app.py +87 -0
  33. router_maestro/server/middleware/__init__.py +11 -0
  34. router_maestro/server/middleware/auth.py +66 -0
  35. router_maestro/server/oauth_sessions.py +159 -0
  36. router_maestro/server/routes/__init__.py +8 -0
  37. router_maestro/server/routes/admin.py +358 -0
  38. router_maestro/server/routes/anthropic.py +228 -0
  39. router_maestro/server/routes/chat.py +142 -0
  40. router_maestro/server/routes/models.py +34 -0
  41. router_maestro/server/schemas/__init__.py +57 -0
  42. router_maestro/server/schemas/admin.py +87 -0
  43. router_maestro/server/schemas/anthropic.py +246 -0
  44. router_maestro/server/schemas/openai.py +107 -0
  45. router_maestro/server/translation.py +636 -0
  46. router_maestro/stats/__init__.py +14 -0
  47. router_maestro/stats/heatmap.py +154 -0
  48. router_maestro/stats/storage.py +228 -0
  49. router_maestro/stats/tracker.py +73 -0
  50. router_maestro/utils/__init__.py +16 -0
  51. router_maestro/utils/logging.py +81 -0
  52. router_maestro/utils/tokens.py +51 -0
  53. router_maestro-0.1.2.dist-info/METADATA +383 -0
  54. router_maestro-0.1.2.dist-info/RECORD +57 -0
  55. router_maestro-0.1.2.dist-info/WHEEL +4 -0
  56. router_maestro-0.1.2.dist-info/entry_points.txt +2 -0
  57. router_maestro-0.1.2.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,288 @@
1
+ """Model management commands."""
2
+
3
+ import asyncio
4
+ from typing import Annotated
5
+
6
+ import typer
7
+ from rich.console import Console
8
+ from rich.table import Table
9
+
10
+ from router_maestro.cli.client import ServerNotRunningError, get_admin_client
11
+
12
+ app = typer.Typer(no_args_is_help=True)
13
+ console = Console()
14
+
15
+
16
+ def _handle_server_error(e: Exception) -> None:
17
+ """Handle server connection errors."""
18
+ if isinstance(e, ServerNotRunningError):
19
+ console.print(f"[red]{e}[/red]")
20
+ else:
21
+ console.print(f"[red]Error: {e}[/red]")
22
+ raise typer.Exit(1)
23
+
24
+
25
+ @app.command(name="list")
26
+ def list_models() -> None:
27
+ """List all available models with their priorities."""
28
+ client = get_admin_client()
29
+
30
+ # Get models and priorities
31
+ try:
32
+ models = asyncio.run(client.list_models())
33
+ priorities_data = asyncio.run(client.get_priorities())
34
+ priorities_list = priorities_data.get("priorities", [])
35
+ except Exception as e:
36
+ _handle_server_error(e)
37
+ return
38
+
39
+ if not models:
40
+ console.print("[dim]No models available.[/dim]")
41
+ console.print("[dim]Make sure you have authenticated with at least one provider.[/dim]")
42
+ return
43
+
44
+ table = Table(title="Available Models")
45
+ table.add_column("Priority", style="cyan", justify="right")
46
+ table.add_column("Model Key", style="green")
47
+ table.add_column("Display Name", style="white")
48
+ table.add_column("Provider", style="magenta")
49
+
50
+ for model in models:
51
+ model_key = f"{model['provider']}/{model['id']}"
52
+ # Check if this model is in the priority list
53
+ try:
54
+ priority_idx = priorities_list.index(model_key)
55
+ priority_str = str(priority_idx + 1)
56
+ except ValueError:
57
+ priority_str = "-"
58
+
59
+ table.add_row(
60
+ priority_str,
61
+ model_key,
62
+ model["name"],
63
+ model["provider"],
64
+ )
65
+
66
+ console.print(table)
67
+
68
+
69
+ @app.command(name="refresh")
70
+ def refresh_models() -> None:
71
+ """Refresh the models cache from all providers."""
72
+ client = get_admin_client()
73
+
74
+ console.print("[dim]Refreshing models cache...[/dim]")
75
+
76
+ try:
77
+ success = asyncio.run(client.refresh_models())
78
+ if success:
79
+ console.print("[green]Models cache refreshed successfully[/green]")
80
+ else:
81
+ console.print("[red]Failed to refresh models cache[/red]")
82
+ raise typer.Exit(1)
83
+ except Exception as e:
84
+ _handle_server_error(e)
85
+
86
+
87
+ # Priority subcommand group
88
+ priority_app = typer.Typer(no_args_is_help=True, help="Manage model priorities")
89
+ app.add_typer(priority_app, name="priority")
90
+
91
+
92
+ @priority_app.command(name="list")
93
+ def priority_list() -> None:
94
+ """List current model priorities."""
95
+ client = get_admin_client()
96
+
97
+ try:
98
+ data = asyncio.run(client.get_priorities())
99
+ priorities = data.get("priorities", [])
100
+ except Exception as e:
101
+ _handle_server_error(e)
102
+ return
103
+
104
+ if not priorities:
105
+ console.print("[dim]No priorities configured.[/dim]")
106
+ console.print(
107
+ "[dim]Use 'router-maestro model priority add <provider/model>' to add priorities.[/dim]"
108
+ )
109
+ return
110
+
111
+ table = Table(title="Model Priorities")
112
+ table.add_column("#", style="cyan", justify="right")
113
+ table.add_column("Model Key", style="green")
114
+
115
+ for idx, model_key in enumerate(priorities):
116
+ table.add_row(str(idx + 1), model_key)
117
+
118
+ console.print(table)
119
+
120
+
121
+ @priority_app.command(name="add")
122
+ def priority_add(
123
+ model_key: Annotated[str, typer.Argument(help="Model key in format 'provider/model'")],
124
+ position: Annotated[
125
+ int | None,
126
+ typer.Option("--position", "-p", help="Position in priority list (1-based)"),
127
+ ] = None,
128
+ ) -> None:
129
+ """Add or move a model in the priority list."""
130
+ if "/" not in model_key:
131
+ console.print("[red]Model key must be in format 'provider/model'[/red]")
132
+ raise typer.Exit(1)
133
+
134
+ client = get_admin_client()
135
+
136
+ try:
137
+ # Get current priorities
138
+ data = asyncio.run(client.get_priorities())
139
+ priorities = data.get("priorities", [])
140
+ fallback = data.get("fallback")
141
+
142
+ # Remove if already exists
143
+ if model_key in priorities:
144
+ priorities.remove(model_key)
145
+
146
+ # Insert at position
147
+ if position is None:
148
+ priorities.append(model_key)
149
+ else:
150
+ pos = position - 1 # Convert 1-based to 0-based
151
+ priorities.insert(pos, model_key)
152
+
153
+ # Save
154
+ asyncio.run(client.set_priorities(priorities, fallback))
155
+
156
+ if position:
157
+ console.print(f"[green]Added '{model_key}' at position {position}[/green]")
158
+ else:
159
+ console.print(f"[green]Added '{model_key}' to end of priority list[/green]")
160
+
161
+ except Exception as e:
162
+ _handle_server_error(e)
163
+
164
+
165
+ @priority_app.command(name="remove")
166
+ def priority_remove(
167
+ model_key: Annotated[str, typer.Argument(help="Model key in format 'provider/model'")],
168
+ ) -> None:
169
+ """Remove a model from the priority list."""
170
+ if "/" not in model_key:
171
+ console.print("[red]Model key must be in format 'provider/model'[/red]")
172
+ raise typer.Exit(1)
173
+
174
+ client = get_admin_client()
175
+
176
+ try:
177
+ # Get current priorities
178
+ data = asyncio.run(client.get_priorities())
179
+ priorities = data.get("priorities", [])
180
+ fallback = data.get("fallback")
181
+
182
+ if model_key in priorities:
183
+ priorities.remove(model_key)
184
+ asyncio.run(client.set_priorities(priorities, fallback))
185
+ console.print(f"[green]Removed '{model_key}' from priority list[/green]")
186
+ else:
187
+ console.print(f"[yellow]'{model_key}' was not in the priority list[/yellow]")
188
+
189
+ except Exception as e:
190
+ _handle_server_error(e)
191
+
192
+
193
+ @priority_app.command(name="clear")
194
+ def priority_clear() -> None:
195
+ """Clear all priorities."""
196
+ client = get_admin_client()
197
+
198
+ try:
199
+ data = asyncio.run(client.get_priorities())
200
+ fallback = data.get("fallback")
201
+ asyncio.run(client.set_priorities([], fallback))
202
+ console.print("[green]Cleared all priorities[/green]")
203
+ except Exception as e:
204
+ _handle_server_error(e)
205
+
206
+
207
+ # Fallback subcommand group
208
+ fallback_app = typer.Typer(no_args_is_help=True, help="Manage fallback configuration")
209
+ app.add_typer(fallback_app, name="fallback")
210
+
211
+ VALID_STRATEGIES = ["priority", "same-model", "none"]
212
+
213
+
214
+ @fallback_app.command(name="show")
215
+ def fallback_show() -> None:
216
+ """Show current fallback configuration."""
217
+ client = get_admin_client()
218
+
219
+ try:
220
+ data = asyncio.run(client.get_priorities())
221
+ fallback = data.get("fallback", {})
222
+ except Exception as e:
223
+ _handle_server_error(e)
224
+ return
225
+
226
+ strategy = fallback.get("strategy", "priority")
227
+ max_retries = fallback.get("maxRetries", 2)
228
+
229
+ console.print()
230
+ console.print("[bold]Fallback Configuration[/bold]")
231
+ console.print(f" Strategy: [cyan]{strategy}[/cyan]")
232
+ console.print(f" Max Retries: [cyan]{max_retries}[/cyan]")
233
+ console.print()
234
+
235
+
236
+ @fallback_app.command(name="set")
237
+ def fallback_set(
238
+ strategy: Annotated[
239
+ str | None,
240
+ typer.Option("--strategy", "-s", help="Fallback strategy (priority, same-model, none)"),
241
+ ] = None,
242
+ max_retries: Annotated[
243
+ int | None,
244
+ typer.Option("--max-retries", "-r", help="Maximum number of fallback retries (0-10)"),
245
+ ] = None,
246
+ ) -> None:
247
+ """Set fallback configuration."""
248
+ # Validate that at least one option is provided
249
+ if strategy is None and max_retries is None:
250
+ console.print("[red]At least one of --strategy or --max-retries must be provided[/red]")
251
+ raise typer.Exit(1)
252
+
253
+ # Validate strategy
254
+ if strategy is not None and strategy not in VALID_STRATEGIES:
255
+ console.print(f"[red]Invalid strategy '{strategy}'[/red]")
256
+ console.print(f"[dim]Valid strategies: {', '.join(VALID_STRATEGIES)}[/dim]")
257
+ raise typer.Exit(1)
258
+
259
+ # Validate max_retries
260
+ if max_retries is not None and (max_retries < 0 or max_retries > 10):
261
+ console.print("[red]max-retries must be between 0 and 10[/red]")
262
+ raise typer.Exit(1)
263
+
264
+ client = get_admin_client()
265
+
266
+ try:
267
+ # Get current config
268
+ data = asyncio.run(client.get_priorities())
269
+ priorities = data.get("priorities", [])
270
+ fallback = data.get("fallback", {})
271
+
272
+ # Update fallback config
273
+ if strategy is not None:
274
+ fallback["strategy"] = strategy
275
+ if max_retries is not None:
276
+ fallback["maxRetries"] = max_retries
277
+
278
+ # Save
279
+ asyncio.run(client.set_priorities(priorities, fallback))
280
+
281
+ console.print("[green]Fallback configuration updated[/green]")
282
+
283
+ # Show updated config
284
+ console.print(f" Strategy: [cyan]{fallback.get('strategy', 'priority')}[/cyan]")
285
+ console.print(f" Max Retries: [cyan]{fallback.get('maxRetries', 2)}[/cyan]")
286
+
287
+ except Exception as e:
288
+ _handle_server_error(e)
@@ -0,0 +1,117 @@
1
+ """Server management commands."""
2
+
3
+ import os
4
+ import socket
5
+
6
+ import typer
7
+ import uvicorn
8
+ from rich.console import Console
9
+ from rich.panel import Panel
10
+
11
+ from router_maestro.config.server import get_current_context_api_key, get_or_create_api_key
12
+
13
+ app = typer.Typer(no_args_is_help=True)
14
+ console = Console()
15
+
16
+
17
+ def is_port_in_use(port: int, host: str = "127.0.0.1") -> bool:
18
+ """Check if a port is in use."""
19
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
20
+ return s.connect_ex((host, port)) == 0
21
+
22
+
23
+ @app.command()
24
+ def start(
25
+ port: int = typer.Option(8080, "--port", "-p", help="Port to listen on"),
26
+ host: str = typer.Option("127.0.0.1", "--host", "-h", help="Host to bind to"),
27
+ api_key: str | None = typer.Option(None, "--api-key", "-k", help="API key for authentication"),
28
+ reload: bool = typer.Option(False, "--reload", help="Enable auto-reload (development)"),
29
+ log_level: str = typer.Option(
30
+ "INFO",
31
+ "--log-level",
32
+ "-l",
33
+ help="Log level (DEBUG, INFO, WARNING, ERROR)",
34
+ case_sensitive=False,
35
+ ),
36
+ ) -> None:
37
+ """Start router-maestro API server."""
38
+ if is_port_in_use(port, host):
39
+ console.print(f"[red]Error: Port {port} is already in use[/red]")
40
+ raise typer.Exit(1)
41
+
42
+ # Validate log level
43
+ valid_levels = ("DEBUG", "INFO", "WARNING", "ERROR")
44
+ log_level = log_level.upper()
45
+ if log_level not in valid_levels:
46
+ console.print(
47
+ f"[red]Error: Invalid log level '{log_level}'. Valid: {', '.join(valid_levels)}[/red]"
48
+ )
49
+ raise typer.Exit(1)
50
+
51
+ # Get or create API key
52
+ key, was_generated = get_or_create_api_key(api_key)
53
+
54
+ # Set environment variables for server process to read
55
+ os.environ["ROUTER_MAESTRO_API_KEY"] = key
56
+ os.environ["ROUTER_MAESTRO_LOG_LEVEL"] = log_level
57
+
58
+ console.print(f"[green]Starting Router-Maestro server on {host}:{port}...[/green]")
59
+ console.print(f"[dim]API endpoint: http://{host}:{port}/v1[/dim]")
60
+ console.print(f"[dim]Log level: {log_level}[/dim]")
61
+
62
+ if was_generated:
63
+ console.print(
64
+ Panel(
65
+ f"[yellow]API Key (auto-generated):[/yellow]\n[bold]{key}[/bold]",
66
+ title="Authentication",
67
+ border_style="yellow",
68
+ )
69
+ )
70
+ else:
71
+ console.print(f"[dim]API Key: {key[:12]}...{key[-4:]}[/dim]")
72
+
73
+ console.print("[dim]Press Ctrl+C to stop[/dim]\n")
74
+
75
+ # Map log level to uvicorn format
76
+ uvicorn_level = log_level.lower()
77
+
78
+ uvicorn.run(
79
+ "router_maestro.server:app",
80
+ host=host,
81
+ port=port,
82
+ reload=reload,
83
+ log_level=uvicorn_level,
84
+ )
85
+
86
+
87
+ @app.command()
88
+ def status() -> None:
89
+ """Show current server status (uses current context)."""
90
+ import asyncio
91
+
92
+ from router_maestro.cli.client import get_admin_client, get_current_endpoint
93
+
94
+ client = get_admin_client()
95
+ endpoint = get_current_endpoint()
96
+
97
+ console.print(f"[dim]Context endpoint: {endpoint}[/dim]")
98
+
99
+ try:
100
+ data = asyncio.run(client.test_connection())
101
+ console.print(f"[green]Server is running[/green]")
102
+ console.print(f" Version: {data.get('version', 'unknown')}")
103
+ console.print(f" Status: {data.get('status', 'unknown')}")
104
+ except Exception as e:
105
+ console.print(f"[yellow]Server not reachable: {e}[/yellow]")
106
+
107
+
108
+ @app.command()
109
+ def show_key() -> None:
110
+ """Show current server API key."""
111
+ api_key = get_current_context_api_key()
112
+ if api_key:
113
+ console.print(f"[green]API Key:[/green] {api_key}")
114
+ else:
115
+ console.print(
116
+ "[yellow]No API key configured. One will be generated on server start.[/yellow]"
117
+ )
@@ -0,0 +1,76 @@
1
+ """Token usage statistics command."""
2
+
3
+ import asyncio
4
+
5
+ import typer
6
+ from rich.console import Console
7
+ from rich.table import Table
8
+
9
+ from router_maestro.cli.client import AdminClientError, get_admin_client
10
+
11
+ console = Console()
12
+
13
+
14
+ def stats(
15
+ days: int = typer.Option(7, "--days", "-d", help="Number of days to show"),
16
+ provider: str = typer.Option(None, "--provider", "-p", help="Filter by provider"),
17
+ model: str = typer.Option(None, "--model", "-m", help="Filter by model"),
18
+ ) -> None:
19
+ """Show token usage statistics."""
20
+ client = get_admin_client()
21
+
22
+ try:
23
+ data = asyncio.run(client.get_stats(days=days, provider=provider, model=model))
24
+ except AdminClientError as e:
25
+ console.print(f"[red]{e}[/red]")
26
+ raise typer.Exit(1)
27
+ except Exception as e:
28
+ console.print(f"[red]Failed to get stats: {e}[/red]")
29
+ raise typer.Exit(1)
30
+
31
+ if data.get("total_requests", 0) == 0:
32
+ console.print("[dim]No usage data available.[/dim]")
33
+ return
34
+
35
+ # Summary table
36
+ console.print(f"\n[bold]Token Usage Summary (Last {days} Days)[/bold]\n")
37
+
38
+ summary_table = Table(show_header=False, box=None)
39
+ summary_table.add_column("Metric", style="cyan")
40
+ summary_table.add_column("Value", style="green", justify="right")
41
+
42
+ summary_table.add_row("Total Requests", f"{data.get('total_requests', 0):,}")
43
+ summary_table.add_row("Total Tokens", f"{data.get('total_tokens', 0):,}")
44
+ summary_table.add_row(" Prompt", f"{data.get('prompt_tokens', 0):,}")
45
+ summary_table.add_row(" Completion", f"{data.get('completion_tokens', 0):,}")
46
+
47
+ console.print(summary_table)
48
+
49
+ # By model table
50
+ by_model = data.get("by_model", {})
51
+ if by_model:
52
+ console.print("\n[bold]Usage by Model[/bold]\n")
53
+
54
+ model_table = Table()
55
+ model_table.add_column("Model", style="cyan")
56
+ model_table.add_column("Provider", style="magenta")
57
+ model_table.add_column("Requests", justify="right")
58
+ model_table.add_column("Total Tokens", justify="right", style="green")
59
+ model_table.add_column("Avg Latency", justify="right")
60
+
61
+ for model_key, record in by_model.items():
62
+ parts = model_key.split("/", 1)
63
+ provider_name = parts[0] if len(parts) > 1 else "-"
64
+ model_name = parts[1] if len(parts) > 1 else model_key
65
+
66
+ avg_latency = record.get("avg_latency_ms")
67
+ latency = f"{avg_latency:.0f} ms" if avg_latency else "-"
68
+ model_table.add_row(
69
+ model_name,
70
+ provider_name,
71
+ f"{record.get('request_count', 0):,}",
72
+ f"{record.get('total_tokens', 0):,}",
73
+ latency,
74
+ )
75
+
76
+ console.print(model_table)
@@ -0,0 +1,72 @@
1
+ """Config module for router-maestro."""
2
+
3
+ from router_maestro.config.contexts import ContextConfig, ContextsConfig
4
+ from router_maestro.config.paths import (
5
+ AUTH_FILE,
6
+ CONTEXTS_FILE,
7
+ LOG_FILE,
8
+ PRIORITIES_FILE,
9
+ PROVIDERS_FILE,
10
+ SERVER_CONFIG_FILE,
11
+ STATS_DB_FILE,
12
+ get_config_dir,
13
+ get_data_dir,
14
+ )
15
+ from router_maestro.config.priorities import (
16
+ FallbackConfig,
17
+ FallbackStrategy,
18
+ PrioritiesConfig,
19
+ )
20
+ from router_maestro.config.providers import (
21
+ CustomProviderConfig,
22
+ ModelConfig,
23
+ ProvidersConfig,
24
+ )
25
+ from router_maestro.config.server import (
26
+ get_current_context_api_key,
27
+ get_or_create_api_key,
28
+ set_local_api_key,
29
+ )
30
+ from router_maestro.config.settings import (
31
+ load_contexts_config,
32
+ load_priorities_config,
33
+ load_providers_config,
34
+ save_contexts_config,
35
+ save_priorities_config,
36
+ save_providers_config,
37
+ )
38
+
39
+ __all__ = [
40
+ # Paths
41
+ "get_data_dir",
42
+ "get_config_dir",
43
+ "AUTH_FILE",
44
+ "SERVER_CONFIG_FILE",
45
+ "PROVIDERS_FILE",
46
+ "PRIORITIES_FILE",
47
+ "CONTEXTS_FILE",
48
+ "STATS_DB_FILE",
49
+ "LOG_FILE",
50
+ # Provider models
51
+ "ModelConfig",
52
+ "CustomProviderConfig",
53
+ "ProvidersConfig",
54
+ # Priority models
55
+ "PrioritiesConfig",
56
+ "FallbackConfig",
57
+ "FallbackStrategy",
58
+ # Context models
59
+ "ContextConfig",
60
+ "ContextsConfig",
61
+ # Settings functions
62
+ "load_providers_config",
63
+ "save_providers_config",
64
+ "load_priorities_config",
65
+ "save_priorities_config",
66
+ "load_contexts_config",
67
+ "save_contexts_config",
68
+ # Server functions
69
+ "get_current_context_api_key",
70
+ "get_or_create_api_key",
71
+ "set_local_api_key",
72
+ ]
@@ -0,0 +1,29 @@
1
+ """Context configuration for remote deployments."""
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class ContextConfig(BaseModel):
7
+ """Configuration for a single deployment context."""
8
+
9
+ endpoint: str = Field(..., description="API endpoint URL")
10
+ api_key: str | None = Field(default=None, description="API key for authentication")
11
+
12
+
13
+ class ContextsConfig(BaseModel):
14
+ """Root configuration for deployment contexts."""
15
+
16
+ current: str = Field(default="local", description="Currently active context name")
17
+ contexts: dict[str, ContextConfig] = Field(
18
+ default_factory=dict, description="Available contexts"
19
+ )
20
+
21
+ @classmethod
22
+ def get_default(cls) -> "ContextsConfig":
23
+ """Get default configuration with local context."""
24
+ return cls(
25
+ current="local",
26
+ contexts={
27
+ "local": ContextConfig(endpoint="http://localhost:8080"),
28
+ },
29
+ )
@@ -0,0 +1,50 @@
1
+ """File path definitions for router-maestro."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+
6
+
7
+ def get_data_dir() -> Path:
8
+ """Get the data directory for router-maestro.
9
+
10
+ Returns ~/.local/share/router-maestro on Unix-like systems.
11
+ Returns %LOCALAPPDATA%/router-maestro on Windows.
12
+ """
13
+ if os.name == "nt":
14
+ # Windows
15
+ base = Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData" / "Local"))
16
+ else:
17
+ # Unix-like (Linux, macOS)
18
+ base = Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share"))
19
+
20
+ data_dir = base / "router-maestro"
21
+ data_dir.mkdir(parents=True, exist_ok=True)
22
+ return data_dir
23
+
24
+
25
+ def get_config_dir() -> Path:
26
+ """Get the config directory for router-maestro.
27
+
28
+ Returns ~/.config/router-maestro on Unix-like systems.
29
+ Returns %LOCALAPPDATA%/router-maestro on Windows.
30
+ """
31
+ if os.name == "nt":
32
+ # Windows - use same as data dir
33
+ base = Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData" / "Local"))
34
+ else:
35
+ # Unix-like (Linux, macOS)
36
+ base = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config"))
37
+
38
+ config_dir = base / "router-maestro"
39
+ config_dir.mkdir(parents=True, exist_ok=True)
40
+ return config_dir
41
+
42
+
43
+ # File paths
44
+ AUTH_FILE = get_data_dir() / "auth.json"
45
+ SERVER_CONFIG_FILE = get_data_dir() / "server.json"
46
+ PROVIDERS_FILE = get_config_dir() / "providers.json"
47
+ PRIORITIES_FILE = get_config_dir() / "priorities.json"
48
+ CONTEXTS_FILE = get_config_dir() / "contexts.json"
49
+ STATS_DB_FILE = get_data_dir() / "stats.db"
50
+ LOG_FILE = get_data_dir() / "router-maestro.log"