-
Notifications
You must be signed in to change notification settings - Fork 3
feat: multi-profile support and project discovery #17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
46c7d86
c2f08ff
cb49fe0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| # | ||
| # Copyright (C) 2017-2026 Dremio Corporation | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
| # | ||
| """dremio context — switch between named profiles.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from pathlib import Path | ||
|
|
||
| import typer | ||
| from rich.console import Console | ||
| from rich.table import Table | ||
|
|
||
| from drs.auth import ( | ||
| DEFAULT_CONFIG_PATH, | ||
| DEFAULT_URI, | ||
| get_default_profile_name, | ||
| list_profiles, | ||
| set_default_profile, | ||
| ) | ||
|
|
||
| app = typer.Typer( | ||
| help="Manage named configuration profiles.", | ||
| context_settings={"help_option_names": ["-h", "--help"]}, | ||
| ) | ||
|
|
||
| console = Console() | ||
| err_console = Console(stderr=True) | ||
|
|
||
|
|
||
| def _get_config_path(ctx: typer.Context) -> Path: | ||
| """Resolve config path from the global --config flag.""" | ||
| if ctx.obj and ctx.obj.get("config_path"): | ||
| return ctx.obj["config_path"] | ||
| return DEFAULT_CONFIG_PATH | ||
|
|
||
|
|
||
| @app.command("list") | ||
| def cli_list(ctx: typer.Context) -> None: | ||
| """List all configured profiles.""" | ||
| config_path = _get_config_path(ctx) | ||
| profiles = list_profiles(config_path) | ||
|
|
||
| if not profiles: | ||
| err_console.print( | ||
| "[yellow]No profiles configured.[/yellow]\nRun [bold cyan]dremio setup[/bold cyan] to create one." | ||
| ) | ||
| raise typer.Exit(1) | ||
|
|
||
| default_name = get_default_profile_name(config_path) | ||
|
|
||
| table = Table(show_header=True, header_style="bold") | ||
| table.add_column("") | ||
| table.add_column("Profile") | ||
| table.add_column("Region") | ||
| table.add_column("Project ID") | ||
|
|
||
| for name, values in profiles.items(): | ||
| is_active = name == default_name | ||
| marker = "*" if is_active else " " | ||
| uri = values.get("uri", DEFAULT_URI) | ||
| region = "EU" if "eu.dremio" in uri else "US" | ||
| project_id = values.get("project_id", "—") | ||
| style = "bold" if is_active else "" | ||
| table.add_row(marker, name, region, project_id, style=style) | ||
|
|
||
| console.print(table) | ||
|
|
||
|
|
||
| @app.command("use") | ||
| def cli_use( | ||
| ctx: typer.Context, | ||
| name: str = typer.Argument(help="Profile name to set as default"), | ||
| ) -> None: | ||
| """Switch the default profile.""" | ||
| config_path = _get_config_path(ctx) | ||
| try: | ||
| set_default_profile(name, config_path) | ||
| except ValueError as exc: | ||
| err_console.print(f"[red]{exc}[/red]") | ||
| profiles = list_profiles(config_path) | ||
| if profiles: | ||
| err_console.print(f"Available profiles: {', '.join(profiles)}") | ||
| raise typer.Exit(1) | ||
|
|
||
| console.print(f"Switched to profile [bold]{name}[/bold].") | ||
|
|
||
|
|
||
| @app.command("current") | ||
| def cli_current(ctx: typer.Context) -> None: | ||
| """Show the active (default) profile name.""" | ||
| config_path = _get_config_path(ctx) | ||
| name = get_default_profile_name(config_path) | ||
|
|
||
| if not name: | ||
| err_console.print( | ||
| "[yellow]No profiles configured.[/yellow]\nRun [bold cyan]dremio setup[/bold cyan] to create one." | ||
| ) | ||
| raise typer.Exit(1) | ||
|
|
||
| console.print(name) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -60,15 +60,42 @@ async def create(client: DremioClient, path: str, rtype: str, display_fields: li | |
| raise handle_api_error(exc) from exc | ||
|
|
||
|
|
||
| async def list_reflections(client: DremioClient, path: str) -> dict: | ||
| """List reflections on a dataset via sys.project.reflections.""" | ||
| parts = parse_path(path) | ||
| try: | ||
| entity = await client.get_catalog_by_path(parts) | ||
| except httpx.HTTPStatusError as exc: | ||
| raise handle_api_error(exc) from exc | ||
| dataset_id = entity["id"] | ||
| sql = f"SELECT * FROM sys.project.reflections WHERE dataset_id = '{dataset_id}'" | ||
| async def list_reflections( | ||
| client: DremioClient, | ||
| path: str | None = None, | ||
| *, | ||
| rtype: str | None = None, | ||
| status: str | None = None, | ||
| dataset_name: str | None = None, | ||
| limit: int | None = None, | ||
| ) -> dict: | ||
| """List reflections via sys.project.reflections. | ||
|
|
||
| When *path* is given, only reflections for that dataset are returned. | ||
| When omitted, all reflections in the project are returned. | ||
| Optional filters narrow results by *rtype*, *status*, or *dataset_name*. | ||
| An optional *limit* caps the number of rows returned. | ||
| """ | ||
| sql = "SELECT * FROM sys.project.reflections" | ||
| conditions: list[str] = [] | ||
| if path is not None: | ||
| parts = parse_path(path) | ||
| try: | ||
| entity = await client.get_catalog_by_path(parts) | ||
| except httpx.HTTPStatusError as exc: | ||
| raise handle_api_error(exc) from exc | ||
| dataset_id = entity["id"] | ||
| conditions.append(f"dataset_id = '{dataset_id}'") | ||
| if rtype is not None: | ||
| conditions.append(f"type = '{rtype.upper()}'") | ||
| if status is not None: | ||
| conditions.append(f"status = '{status.upper()}'") | ||
| if dataset_name is not None: | ||
| conditions.append(f"dataset_name ILIKE '%{dataset_name}%'") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The Useful? React with 👍 / 👎. |
||
| if conditions: | ||
| sql += " WHERE " + " AND ".join(conditions) | ||
| if limit is not None: | ||
| sql += f" LIMIT {limit}" | ||
| return await run_query(client, sql) | ||
|
|
||
|
|
||
|
|
@@ -142,12 +169,20 @@ def cli_create( | |
|
|
||
| @app.command("list") | ||
| def cli_list( | ||
| path: str = typer.Argument(help="Dot-separated dataset path"), | ||
| path: str = typer.Argument(None, help="Dot-separated dataset path (omit to list all reflections)"), | ||
| rtype: str = typer.Option(None, "--type", "-t", help="Filter by reflection type: raw or aggregation"), | ||
| status: str = typer.Option(None, "--status", "-s", help="Filter by status (e.g. CAN_ACCELERATE, FAILED, EXPIRED)"), | ||
| dataset_name: str = typer.Option(None, "--dataset-name", "-d", help="Filter by dataset name (substring match)"), | ||
| limit: int = typer.Option(None, "--limit", "-l", help="Maximum number of reflections to return"), | ||
| fmt: OutputFormat = typer.Option(OutputFormat.json, "--output", "-o", help="Output format"), | ||
| ) -> None: | ||
| """List all reflections defined on a dataset.""" | ||
| """List reflections. Shows all project reflections, or those for a specific dataset.""" | ||
| client = _get_client() | ||
| _run_command(list_reflections(client, path), client, fmt) | ||
| _run_command( | ||
| list_reflections(client, path, rtype=rtype, status=status, dataset_name=dataset_name, limit=limit), | ||
| client, | ||
| fmt, | ||
| ) | ||
|
|
||
|
|
||
| @app.command("get") | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
set_default_profileonly checks the explicitprofilesmap, so legacy flat configs (which the same module treats as an implicitdefaultprofile) always raise “Profile not found”. This makesdremio context use defaultfail for valid legacy configs even thoughcontext list/context currentexposedefault, which breaks the advertised backward-compatibility path for profile management.Useful? React with 👍 / 👎.