pltr-cli 0.1.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.
@@ -0,0 +1,98 @@
1
+ """
2
+ Simplified dataset commands that work with foundry-platform-sdk v1.27.0.
3
+ """
4
+
5
+ import typer
6
+ from typing import Optional
7
+ from rich.console import Console
8
+
9
+ from ..services.dataset import DatasetService
10
+ from ..utils.formatting import OutputFormatter
11
+ from ..utils.progress import SpinnerProgressTracker
12
+ from ..auth.base import ProfileNotFoundError, MissingCredentialsError
13
+
14
+ app = typer.Typer()
15
+ console = Console()
16
+ formatter = OutputFormatter(console)
17
+
18
+
19
+ @app.command("get")
20
+ def get_dataset(
21
+ dataset_rid: str = typer.Argument(..., help="Dataset Resource Identifier"),
22
+ profile: Optional[str] = typer.Option(None, "--profile", "-p", help="Profile name"),
23
+ format: str = typer.Option(
24
+ "table", "--format", "-f", help="Output format (table, json, csv)"
25
+ ),
26
+ output: Optional[str] = typer.Option(
27
+ None, "--output", "-o", help="Output file path"
28
+ ),
29
+ ):
30
+ """Get detailed information about a specific dataset."""
31
+ try:
32
+ service = DatasetService(profile=profile)
33
+
34
+ with SpinnerProgressTracker().track_spinner(
35
+ f"Fetching dataset {dataset_rid}..."
36
+ ):
37
+ dataset = service.get_dataset(dataset_rid)
38
+
39
+ formatter.format_dataset_detail(dataset, format, output)
40
+
41
+ if output:
42
+ formatter.print_success(f"Dataset information saved to {output}")
43
+
44
+ except (ProfileNotFoundError, MissingCredentialsError) as e:
45
+ formatter.print_error(f"Authentication error: {e}")
46
+ raise typer.Exit(1)
47
+ except Exception as e:
48
+ formatter.print_error(f"Failed to get dataset: {e}")
49
+ raise typer.Exit(1)
50
+
51
+
52
+ # schema command removed - uses preview-only API that returns INVALID_ARGUMENT
53
+
54
+
55
+ @app.command("create")
56
+ def create_dataset(
57
+ name: str = typer.Argument(..., help="Dataset name"),
58
+ profile: Optional[str] = typer.Option(None, "--profile", "-p", help="Profile name"),
59
+ parent_folder: Optional[str] = typer.Option(
60
+ None, "--parent-folder", help="Parent folder RID"
61
+ ),
62
+ format: str = typer.Option(
63
+ "table", "--format", "-f", help="Output format (table, json, csv)"
64
+ ),
65
+ ):
66
+ """Create a new dataset."""
67
+ try:
68
+ service = DatasetService(profile=profile)
69
+
70
+ with SpinnerProgressTracker().track_spinner(f"Creating dataset '{name}'..."):
71
+ dataset = service.create_dataset(name=name, parent_folder_rid=parent_folder)
72
+
73
+ formatter.print_success(f"Successfully created dataset '{name}'")
74
+ formatter.print_info(f"Dataset RID: {dataset.get('rid', 'unknown')}")
75
+
76
+ # Show dataset details
77
+ formatter.format_dataset_detail(dataset, format)
78
+
79
+ except (ProfileNotFoundError, MissingCredentialsError) as e:
80
+ formatter.print_error(f"Authentication error: {e}")
81
+ raise typer.Exit(1)
82
+ except Exception as e:
83
+ formatter.print_error(f"Failed to create dataset: {e}")
84
+ raise typer.Exit(1)
85
+
86
+
87
+ @app.callback()
88
+ def main():
89
+ """
90
+ Dataset operations using foundry-platform-sdk.
91
+
92
+ Note: This SDK version requires knowing dataset RIDs in advance.
93
+ Find dataset RIDs in the Foundry web interface.
94
+
95
+ Available commands work with Resource Identifiers (RIDs) like:
96
+ ri.foundry.main.dataset.12345678-1234-1234-1234-123456789abc
97
+ """
98
+ pass
@@ -0,0 +1,185 @@
1
+ """
2
+ Verification command to test authentication.
3
+ """
4
+
5
+ import typer
6
+ import requests
7
+ from typing import Optional
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+
11
+ from ..auth.manager import AuthManager
12
+ from ..auth.base import (
13
+ ProfileNotFoundError,
14
+ MissingCredentialsError,
15
+ InvalidCredentialsError,
16
+ )
17
+
18
+ app = typer.Typer()
19
+ console = Console()
20
+
21
+
22
+ @app.callback(invoke_without_command=True)
23
+ def verify(
24
+ ctx: typer.Context,
25
+ profile: Optional[str] = typer.Option(
26
+ None, "--profile", "-p", help="Profile to verify"
27
+ ),
28
+ ):
29
+ """Verify authentication by connecting to Palantir Foundry."""
30
+ if ctx.invoked_subcommand is not None:
31
+ return
32
+
33
+ auth_manager = AuthManager()
34
+
35
+ # Show which profile we're using
36
+ active_profile = profile or auth_manager.get_current_profile()
37
+ if not active_profile:
38
+ console.print("[red]Error:[/red] No profile configured")
39
+ console.print("Run 'pltr configure configure' to set up authentication")
40
+ raise typer.Exit(1)
41
+
42
+ console.print(f"Verifying profile: [bold]{active_profile}[/bold]")
43
+
44
+ try:
45
+ # Get credentials directly without creating SDK client
46
+ from ..auth.storage import CredentialStorage
47
+
48
+ storage = CredentialStorage()
49
+
50
+ # Get credentials for the profile
51
+ try:
52
+ credentials = storage.get_profile(active_profile)
53
+ except ProfileNotFoundError:
54
+ console.print(f"[red]Error:[/red] Profile '{active_profile}' not found")
55
+ raise typer.Exit(1)
56
+
57
+ # Extract authentication details
58
+ host = credentials.get("host")
59
+ auth_type = credentials.get("auth_type")
60
+
61
+ if not host:
62
+ console.print(
63
+ f"[red]Error:[/red] No host URL configured for profile '{active_profile}'"
64
+ )
65
+ raise typer.Exit(1)
66
+
67
+ # Prepare the request
68
+ url = f"{host}/multipass/api/me"
69
+ headers = {}
70
+
71
+ # Set up authentication headers based on type
72
+ if auth_type == "token":
73
+ token = credentials.get("token")
74
+ if not token:
75
+ console.print(
76
+ f"[red]Error:[/red] No token configured for profile '{active_profile}'"
77
+ )
78
+ raise typer.Exit(1)
79
+ headers["Authorization"] = f"Bearer {token}"
80
+
81
+ elif auth_type == "oauth":
82
+ # OAuth - need to get access token first
83
+ client_id = credentials.get("client_id")
84
+ client_secret = credentials.get("client_secret")
85
+
86
+ if not client_id or not client_secret:
87
+ console.print(
88
+ f"[red]Error:[/red] OAuth credentials incomplete for profile '{active_profile}'"
89
+ )
90
+ raise typer.Exit(1)
91
+
92
+ token_url = f"{host}/multipass/api/oauth2/token"
93
+ token_response = requests.post(
94
+ token_url,
95
+ data={
96
+ "grant_type": "client_credentials",
97
+ "client_id": client_id,
98
+ "client_secret": client_secret,
99
+ },
100
+ )
101
+ if token_response.status_code == 200:
102
+ access_token = token_response.json().get("access_token")
103
+ headers["Authorization"] = f"Bearer {access_token}"
104
+ else:
105
+ console.print(
106
+ f"[red]Error:[/red] Failed to get OAuth token: {token_response.text}"
107
+ )
108
+ raise typer.Exit(1)
109
+
110
+ # Make the request to /multipass/api/me
111
+ response = requests.get(url, headers=headers)
112
+
113
+ if response.status_code == 200:
114
+ user_info = response.json()
115
+
116
+ # Display success with user information
117
+ console.print("[green]✓[/green] Authentication successful!")
118
+ console.print()
119
+
120
+ # Create a table with user information
121
+ table = Table(title="User Information", show_header=False)
122
+ table.add_column("Field", style="cyan")
123
+ table.add_column("Value")
124
+
125
+ # Add user details
126
+ table.add_row("Username", user_info.get("username", "N/A"))
127
+ table.add_row("Email", user_info.get("email", "N/A"))
128
+ table.add_row("User ID", user_info.get("id", "N/A"))
129
+
130
+ # Add organization info if available
131
+ if "organization" in user_info:
132
+ table.add_row(
133
+ "Organization", user_info["organization"].get("rid", "N/A")
134
+ )
135
+
136
+ # Add groups if available
137
+ if "groups" in user_info and user_info["groups"]:
138
+ groups_list = ", ".join(
139
+ g.get("name", "N/A") for g in user_info["groups"][:3]
140
+ )
141
+ if len(user_info["groups"]) > 3:
142
+ groups_list += f" (+{len(user_info['groups']) - 3} more)"
143
+ table.add_row("Groups", groups_list)
144
+
145
+ console.print(table)
146
+ console.print()
147
+ console.print(
148
+ f"[green]Profile '{active_profile}' is properly configured![/green]"
149
+ )
150
+
151
+ elif response.status_code == 401:
152
+ console.print("[red]✗[/red] Authentication failed: Invalid credentials")
153
+ console.print(
154
+ f"Please check your token/credentials for profile '{active_profile}'"
155
+ )
156
+ raise typer.Exit(1)
157
+ elif response.status_code == 403:
158
+ console.print("[red]✗[/red] Authentication failed: Access forbidden")
159
+ console.print(
160
+ "Your credentials are valid but you don't have access to this endpoint"
161
+ )
162
+ raise typer.Exit(1)
163
+ else:
164
+ console.print(f"[red]✗[/red] Request failed: {response.status_code}")
165
+ console.print(f"Response: {response.text}")
166
+ raise typer.Exit(1)
167
+
168
+ except ProfileNotFoundError as e:
169
+ console.print(f"[red]Error:[/red] {e}")
170
+ raise typer.Exit(1)
171
+ except MissingCredentialsError as e:
172
+ console.print(f"[red]Error:[/red] {e}")
173
+ raise typer.Exit(1)
174
+ except InvalidCredentialsError as e:
175
+ console.print(f"[red]Error:[/red] {e}")
176
+ raise typer.Exit(1)
177
+ except requests.exceptions.ConnectionError:
178
+ console.print(
179
+ "[red]✗[/red] Connection failed: Unable to reach the Foundry host"
180
+ )
181
+ console.print("Please check your network connection and host URL")
182
+ raise typer.Exit(1)
183
+ except Exception as e:
184
+ console.print(f"[red]Error:[/red] Unexpected error: {e}")
185
+ raise typer.Exit(1)
@@ -0,0 +1 @@
1
+ """Configuration management for pltr."""
@@ -0,0 +1,124 @@
1
+ """
2
+ Profile management for pltr CLI.
3
+ """
4
+
5
+ import json
6
+ from typing import List, Optional
7
+
8
+ from .settings import Settings
9
+
10
+
11
+ class ProfileManager:
12
+ """Manages authentication profiles."""
13
+
14
+ def __init__(self):
15
+ """Initialize profile manager."""
16
+ self.settings = Settings()
17
+ self.profiles_file = self.settings.config_dir / "profiles.json"
18
+ self._profiles = self._load_profiles()
19
+
20
+ def _load_profiles(self) -> dict:
21
+ """Load profiles from file."""
22
+ if not self.profiles_file.exists():
23
+ return {"profiles": [], "default": None}
24
+
25
+ try:
26
+ with open(self.profiles_file, "r") as f:
27
+ return json.load(f)
28
+ except (json.JSONDecodeError, IOError):
29
+ return {"profiles": [], "default": None}
30
+
31
+ def _save_profiles(self) -> None:
32
+ """Save profiles to file."""
33
+ with open(self.profiles_file, "w") as f:
34
+ json.dump(self._profiles, f, indent=2)
35
+
36
+ def add_profile(self, profile: str) -> None:
37
+ """
38
+ Add a new profile.
39
+
40
+ Args:
41
+ profile: Profile name
42
+ """
43
+ if profile not in self._profiles["profiles"]:
44
+ self._profiles["profiles"].append(profile)
45
+ self._save_profiles()
46
+
47
+ def remove_profile(self, profile: str) -> None:
48
+ """
49
+ Remove a profile.
50
+
51
+ Args:
52
+ profile: Profile name
53
+ """
54
+ if profile in self._profiles["profiles"]:
55
+ self._profiles["profiles"].remove(profile)
56
+
57
+ # If this was the default profile, clear the default
58
+ if self._profiles["default"] == profile:
59
+ self._profiles["default"] = None
60
+ # Set a new default if there are other profiles
61
+ if self._profiles["profiles"]:
62
+ self._profiles["default"] = self._profiles["profiles"][0]
63
+
64
+ self._save_profiles()
65
+
66
+ def list_profiles(self) -> List[str]:
67
+ """
68
+ List all profiles.
69
+
70
+ Returns:
71
+ List of profile names
72
+ """
73
+ return self._profiles["profiles"].copy()
74
+
75
+ def get_default(self) -> Optional[str]:
76
+ """
77
+ Get the default profile.
78
+
79
+ Returns:
80
+ Default profile name or None
81
+ """
82
+ return self._profiles["default"]
83
+
84
+ def set_default(self, profile: str) -> None:
85
+ """
86
+ Set the default profile.
87
+
88
+ Args:
89
+ profile: Profile name
90
+ """
91
+ if profile in self._profiles["profiles"]:
92
+ self._profiles["default"] = profile
93
+ self._save_profiles()
94
+ # Also update in settings
95
+ self.settings.set("default_profile", profile)
96
+
97
+ def profile_exists(self, profile: str) -> bool:
98
+ """
99
+ Check if a profile exists.
100
+
101
+ Args:
102
+ profile: Profile name
103
+
104
+ Returns:
105
+ True if profile exists, False otherwise
106
+ """
107
+ return profile in self._profiles["profiles"]
108
+
109
+ def get_active_profile(self) -> Optional[str]:
110
+ """
111
+ Get the active profile (from environment or default).
112
+
113
+ Returns:
114
+ Active profile name
115
+ """
116
+ import os
117
+
118
+ # Check environment variable first
119
+ env_profile = os.environ.get("PLTR_PROFILE")
120
+ if env_profile and self.profile_exists(env_profile):
121
+ return env_profile
122
+
123
+ # Fall back to default
124
+ return self.get_default()
@@ -0,0 +1,103 @@
1
+ """
2
+ Configuration settings management for pltr CLI.
3
+ """
4
+
5
+ import os
6
+ import json
7
+ from pathlib import Path
8
+ from typing import Dict, Any
9
+
10
+
11
+ class Settings:
12
+ """Manages application settings and configuration."""
13
+
14
+ def __init__(self):
15
+ """Initialize settings manager."""
16
+ self.config_dir = self._get_config_dir()
17
+ self.settings_file = self.config_dir / "settings.json"
18
+ self._ensure_config_dir()
19
+ self._settings = self._load_settings()
20
+
21
+ def _get_config_dir(self) -> Path:
22
+ """Get the configuration directory path."""
23
+ # Follow XDG Base Directory specification
24
+ xdg_config_home = os.environ.get("XDG_CONFIG_HOME")
25
+ if xdg_config_home:
26
+ return Path(xdg_config_home) / "pltr"
27
+
28
+ # Default to ~/.config/pltr
29
+ return Path.home() / ".config" / "pltr"
30
+
31
+ def _ensure_config_dir(self) -> None:
32
+ """Ensure configuration directory exists."""
33
+ self.config_dir.mkdir(parents=True, exist_ok=True)
34
+
35
+ def _load_settings(self) -> Dict[str, Any]:
36
+ """Load settings from file."""
37
+ if not self.settings_file.exists():
38
+ return self._get_default_settings()
39
+
40
+ try:
41
+ with open(self.settings_file, "r") as f:
42
+ return json.load(f)
43
+ except (json.JSONDecodeError, IOError):
44
+ return self._get_default_settings()
45
+
46
+ def _get_default_settings(self) -> Dict[str, Any]:
47
+ """Get default settings."""
48
+ return {
49
+ "default_profile": "default",
50
+ "output_format": "table",
51
+ "color_output": True,
52
+ "page_size": 20,
53
+ "timeout": 30,
54
+ "verify_ssl": True,
55
+ }
56
+
57
+ def save(self) -> None:
58
+ """Save settings to file."""
59
+ with open(self.settings_file, "w") as f:
60
+ json.dump(self._settings, f, indent=2)
61
+
62
+ def get(self, key: str, default: Any = None) -> Any:
63
+ """
64
+ Get a setting value.
65
+
66
+ Args:
67
+ key: Setting key
68
+ default: Default value if key doesn't exist
69
+
70
+ Returns:
71
+ Setting value or default
72
+ """
73
+ return self._settings.get(key, default)
74
+
75
+ def set(self, key: str, value: Any) -> None:
76
+ """
77
+ Set a setting value.
78
+
79
+ Args:
80
+ key: Setting key
81
+ value: Setting value
82
+ """
83
+ self._settings[key] = value
84
+ self.save()
85
+
86
+ def update(self, settings: Dict[str, Any]) -> None:
87
+ """
88
+ Update multiple settings.
89
+
90
+ Args:
91
+ settings: Dictionary of settings to update
92
+ """
93
+ self._settings.update(settings)
94
+ self.save()
95
+
96
+ def reset(self) -> None:
97
+ """Reset settings to defaults."""
98
+ self._settings = self._get_default_settings()
99
+ self.save()
100
+
101
+ def get_all(self) -> Dict[str, Any]:
102
+ """Get all settings."""
103
+ return self._settings.copy()
@@ -0,0 +1 @@
1
+ """Service wrappers for Foundry SDK."""
pltr/services/base.py ADDED
@@ -0,0 +1,62 @@
1
+ """
2
+ Base service class for Foundry API wrappers.
3
+ """
4
+
5
+ from typing import Any, Optional
6
+ from abc import ABC, abstractmethod
7
+
8
+ from ..auth.manager import AuthManager
9
+
10
+
11
+ class BaseService(ABC):
12
+ """Base class for Foundry service wrappers."""
13
+
14
+ def __init__(self, profile: Optional[str] = None):
15
+ """
16
+ Initialize base service.
17
+
18
+ Args:
19
+ profile: Authentication profile name (uses default if not specified)
20
+ """
21
+ self.profile = profile
22
+ self.auth_manager = AuthManager()
23
+ self._client: Optional[Any] = None
24
+
25
+ @property
26
+ def client(self) -> Any:
27
+ """
28
+ Get authenticated Foundry client.
29
+
30
+ Returns:
31
+ Configured FoundryClient instance
32
+
33
+ Raises:
34
+ ProfileNotFoundError: If profile doesn't exist
35
+ MissingCredentialsError: If credentials are incomplete
36
+ """
37
+ if self._client is None:
38
+ self._client = self.auth_manager.get_client(self.profile)
39
+ return self._client
40
+
41
+ @abstractmethod
42
+ def _get_service(self) -> Any:
43
+ """
44
+ Get the specific Foundry SDK service instance.
45
+
46
+ Returns:
47
+ Configured service instance from foundry-platform-sdk
48
+
49
+ This method should be implemented by subclasses to return the
50
+ appropriate service (e.g., client.datasets, client.ontology)
51
+ """
52
+ pass
53
+
54
+ @property
55
+ def service(self) -> Any:
56
+ """
57
+ Get the Foundry SDK service instance.
58
+
59
+ Returns:
60
+ Configured service instance
61
+ """
62
+ return self._get_service()
@@ -0,0 +1,90 @@
1
+ """
2
+ Dataset service wrapper for Foundry SDK v2 API.
3
+ Only includes operations that are actually supported by foundry-platform-sdk v1.27.0.
4
+ """
5
+
6
+ from typing import Any, Optional, Dict
7
+
8
+ from .base import BaseService
9
+
10
+
11
+ class DatasetService(BaseService):
12
+ """Service wrapper for Foundry dataset operations using v2 API."""
13
+
14
+ def _get_service(self) -> Any:
15
+ """Get the Foundry datasets service."""
16
+ return self.client.datasets
17
+
18
+ def get_dataset(self, dataset_rid: str) -> Dict[str, Any]:
19
+ """
20
+ Get information about a specific dataset.
21
+
22
+ Args:
23
+ dataset_rid: Dataset Resource Identifier
24
+
25
+ Returns:
26
+ Dataset information dictionary
27
+ """
28
+ try:
29
+ # Use the v2 API's Dataset.get method
30
+ dataset = self.service.Dataset.get(dataset_rid)
31
+ return self._format_dataset_info(dataset)
32
+ except Exception as e:
33
+ raise RuntimeError(f"Failed to get dataset {dataset_rid}: {e}")
34
+
35
+ # get_schema method removed - uses preview-only API
36
+
37
+ def create_dataset(
38
+ self, name: str, parent_folder_rid: Optional[str] = None
39
+ ) -> Dict[str, Any]:
40
+ """
41
+ Create a new dataset.
42
+
43
+ Args:
44
+ name: Dataset name
45
+ parent_folder_rid: Parent folder RID (optional)
46
+
47
+ Returns:
48
+ Created dataset information
49
+ """
50
+ try:
51
+ # The create method parameters depend on the SDK version
52
+ dataset = self.service.Dataset.create(
53
+ name=name, parent_folder_rid=parent_folder_rid
54
+ )
55
+ return self._format_dataset_info(dataset)
56
+ except Exception as e:
57
+ raise RuntimeError(f"Failed to create dataset '{name}': {e}")
58
+
59
+ def read_table(self, dataset_rid: str, format: str = "arrow") -> Any:
60
+ """
61
+ Read dataset as a table.
62
+
63
+ Args:
64
+ dataset_rid: Dataset Resource Identifier
65
+ format: Output format (arrow, pandas, etc.)
66
+
67
+ Returns:
68
+ Table data in specified format
69
+ """
70
+ try:
71
+ return self.service.Dataset.read_table(dataset_rid, format=format)
72
+ except Exception as e:
73
+ raise RuntimeError(f"Failed to read dataset {dataset_rid}: {e}")
74
+
75
+ def _format_dataset_info(self, dataset: Any) -> Dict[str, Any]:
76
+ """
77
+ Format dataset information for consistent output.
78
+
79
+ Args:
80
+ dataset: Dataset object from Foundry SDK
81
+
82
+ Returns:
83
+ Formatted dataset information dictionary
84
+ """
85
+ # The v2 Dataset object only has rid, name, and parent_folder_rid
86
+ return {
87
+ "rid": dataset.rid,
88
+ "name": dataset.name,
89
+ "parent_folder_rid": dataset.parent_folder_rid,
90
+ }