-
Notifications
You must be signed in to change notification settings - Fork 1
feat: add knowledge CLI command group #55
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
Merged
Merged
Changes from 13 commits
Commits
Show all changes
27 commits
Select commit
Hold shift + click to select a range
35c4998
refactor: move token and instance-name to top-level command
mdesmet 98e1232
feat: add config file support with env var substitution
mdesmet e9d3f71
build: add python-dotenv dependency
mdesmet ff3dca6
refactor: standardize string quotes and improve code consistency
mdesmet 34288ef
feat: hide token input in CLI for security
mdesmet c4059c7
chore: update config file loading to use Path.open
mdesmet e5eaab4
fix: add 'dbt' prefix to project-health command in tests
mdesmet a23748f
refactor: simplify config file opening syntax
mdesmet 2aeda41
feat: add knowledge CLI command group
mdesmet 9de835b
feat: add knowledge serve command with HTTP server
mdesmet 6236ac1
fix: correct knowledge cli import path in main.py
mdesmet 521b381
feat: add knowledge CLI group and serve command
mdesmet 2f3858c
feat: change default server port from 3000 to 4000
mdesmet f963721
style: reformat imports and strings in knowledge CLI
mdesmet c9f4229
fix: validate URL scheme to allow only HTTP/HTTPS in knowledge base h…
mdesmet 54e3495
feat: Add timeout to urlopen() calls
mdesmet 13b3d1b
fix: store HTTPError.read() result to avoid double read
mdesmet 1ceafce
style: standardize string quotes in URL scheme check
mdesmet 973bb0f
fix: suppress S310 warning for validated URL scheme
mdesmet 9f439e9
fix: Shorten noqa comment for urlopen security warning
mdesmet 8494366
fix: add security comment for urlopen usage
mdesmet 5222d4a
fix: add noqa comment for bandit security check
mdesmet a02e6c2
feat: add --version option to CLI
mdesmet 788c259
Merge branch 'main' into feat/kb-proxy
mdesmet 7954b55
refactor: extract KnowledgeBaseHandler to separate file
mdesmet 79efc66
style: reorder imports and clean up whitespace
mdesmet 5be69f7
refactor: update knowledge base endpoint paths
mdesmet File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,94 @@ | ||
| import json | ||
| import os | ||
| import re | ||
| from pathlib import Path | ||
|
|
||
| import click | ||
| from dotenv import load_dotenv | ||
|
|
||
| from datapilot.core.mcp_utils.mcp import mcp | ||
| from datapilot.core.platforms.dbt.cli.cli import dbt | ||
| from datapilot.core.knowledge.cli import cli as knowledge | ||
|
|
||
|
|
||
| def load_config_from_file(): | ||
| """Load configuration from ~/.altimate/altimate.json if it exists.""" | ||
| config_path = Path.home() / ".altimate" / "altimate.json" | ||
|
|
||
| if not config_path.exists(): | ||
| return {} | ||
|
|
||
| try: | ||
| with config_path.open() as f: | ||
| config = json.load(f) | ||
| return config | ||
| except (OSError, json.JSONDecodeError) as e: | ||
| click.echo(f"Warning: Failed to load config from {config_path}: {e}", err=True) | ||
| return {} | ||
|
|
||
|
|
||
| def substitute_env_vars(value): | ||
| """Replace ${env:ENV_VARIABLE} patterns with actual environment variable values.""" | ||
| if not isinstance(value, str): | ||
| return value | ||
|
|
||
| # Pattern to match ${env:VARIABLE_NAME} | ||
| pattern = r"\$\{env:([^}]+)\}" | ||
|
|
||
| def replacer(match): | ||
| env_var = match.group(1) | ||
| return os.environ.get(env_var, match.group(0)) | ||
|
|
||
| return re.sub(pattern, replacer, value) | ||
|
|
||
|
|
||
| def process_config(config): | ||
| """Process configuration dictionary to substitute environment variables.""" | ||
| processed = {} | ||
| for key, value in config.items(): | ||
| processed[key] = substitute_env_vars(value) | ||
| return processed | ||
|
|
||
|
|
||
| @click.group() | ||
| def datapilot(): | ||
| @click.option("--token", required=False, help="Your API token for authentication.", hide_input=True) | ||
| @click.option("--instance-name", required=False, help="Your tenant ID.") | ||
| @click.option("--backend-url", required=False, help="Altimate's Backend URL", default="https://api.myaltimate.com") | ||
| @click.pass_context | ||
| def datapilot(ctx, token, instance_name, backend_url): | ||
| """Altimate CLI for DBT project management.""" | ||
| # Load .env file from current directory if it exists | ||
| load_dotenv() | ||
|
|
||
| # Load configuration from file | ||
| file_config = load_config_from_file() | ||
| file_config = process_config(file_config) | ||
|
|
||
| # Map config file keys to CLI option names | ||
| config_mapping = {"altimateApiKey": "token", "altimateInstanceName": "instance_name", "altimateUrl": "backend_url"} | ||
|
|
||
| # Store common options in context, with CLI args taking precedence | ||
| ctx.ensure_object(dict) | ||
|
|
||
| # Apply file config first | ||
| for file_key, cli_key in config_mapping.items(): | ||
| if file_key in file_config: | ||
| ctx.obj[cli_key] = file_config[file_key] | ||
|
|
||
| # Override with CLI arguments if provided | ||
| if token is not None: | ||
| ctx.obj["token"] = token | ||
| if instance_name is not None: | ||
| ctx.obj["instance_name"] = instance_name | ||
| if backend_url != "https://api.myaltimate.com": # Only override if not default | ||
| ctx.obj["backend_url"] = backend_url | ||
|
|
||
| # Set defaults if nothing was provided | ||
| ctx.obj.setdefault("token", None) | ||
| ctx.obj.setdefault("instance_name", None) | ||
| ctx.obj.setdefault("backend_url", "https://api.myaltimate.com") | ||
|
|
||
|
|
||
| datapilot.add_command(dbt) | ||
| datapilot.add_command(mcp) | ||
| datapilot.add_command(knowledge) |
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| import click | ||
| import json | ||
| from http.server import HTTPServer, BaseHTTPRequestHandler | ||
| from urllib.parse import urlparse | ||
| from urllib.request import Request, urlopen | ||
| from urllib.error import URLError, HTTPError | ||
| import re | ||
|
|
||
|
|
||
| @click.group(name="knowledge") | ||
| def cli(): | ||
| """knowledge specific commands.""" | ||
|
|
||
|
|
||
| @cli.command() | ||
| @click.option('--port', default=4000, help='Port to run the server on') | ||
| @click.pass_context | ||
| def serve(ctx, port): | ||
| """Serve knowledge bases via HTTP server.""" | ||
| # Get configuration from parent context | ||
| token = ctx.parent.obj.get('token') | ||
| instance_name = ctx.parent.obj.get('instance_name') | ||
| backend_url = ctx.parent.obj.get('backend_url') | ||
|
|
||
| if not token or not instance_name: | ||
| click.echo("Error: API token and instance name are required. Use --token and --instance-name options or set them in config.", err=True) | ||
| ctx.exit(1) | ||
|
|
||
| class KnowledgeBaseHandler(BaseHTTPRequestHandler): | ||
| def do_GET(self): | ||
| """Handle GET requests.""" | ||
| path = urlparse(self.path).path | ||
|
|
||
| # Match /knowledge_bases/{uuid} pattern | ||
| match = re.match(r'^/knowledge_bases/([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})$', path) | ||
|
|
||
| if match: | ||
| public_id = match.group(1) | ||
| self.handle_knowledge_base(public_id) | ||
| elif path == '/health': | ||
| self.handle_health() | ||
| else: | ||
| self.send_error(404, "Not Found") | ||
|
|
||
| def handle_knowledge_base(self, public_id): | ||
| """Fetch and return knowledge base data.""" | ||
| url = f"{backend_url}/knowledge_bases/public/{public_id}" | ||
|
|
||
| headers = { | ||
| 'Authorization': f'Bearer {token}', | ||
| 'X-Tenant': instance_name, | ||
| 'Content-Type': 'application/json' | ||
| } | ||
|
|
||
| req = Request(url, headers=headers) | ||
|
|
||
| try: | ||
| with urlopen(req) as response: | ||
| data = response.read() | ||
| self.send_response(200) | ||
| self.send_header('Content-Type', 'application/json') | ||
| self.end_headers() | ||
| self.wfile.write(data) | ||
| except HTTPError as e: | ||
| error_data = e.read().decode('utf-8') if e.read() else '{"error": "HTTP Error"}' | ||
mdesmet marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| self.send_response(e.code) | ||
| self.send_header('Content-Type', 'application/json') | ||
| self.end_headers() | ||
| self.wfile.write(error_data.encode('utf-8')) | ||
| except URLError as e: | ||
| self.send_response(500) | ||
| self.send_header('Content-Type', 'application/json') | ||
| self.end_headers() | ||
| error_msg = json.dumps({'error': str(e)}) | ||
| self.wfile.write(error_msg.encode('utf-8')) | ||
|
|
||
| def handle_health(self): | ||
| """Handle health check endpoint.""" | ||
| self.send_response(200) | ||
| self.send_header('Content-Type', 'application/json') | ||
| self.end_headers() | ||
| self.wfile.write(json.dumps({'status': 'ok'}).encode('utf-8')) | ||
|
|
||
| def log_message(self, format, *args): | ||
| """Override to use click.echo for logging.""" | ||
| click.echo(f"{self.address_string()} - {format % args}") | ||
|
|
||
| server_address = ('', port) | ||
| httpd = HTTPServer(server_address, KnowledgeBaseHandler) | ||
|
|
||
| click.echo(f"Starting knowledge base server on port {port}...") | ||
| click.echo(f"Backend URL: {backend_url}") | ||
| click.echo(f"Instance: {instance_name}") | ||
| click.echo(f"Server running at http://localhost:{port}") | ||
|
|
||
| try: | ||
| httpd.serve_forever() | ||
| except KeyboardInterrupt: | ||
| click.echo("\nShutting down server...") | ||
| httpd.shutdown() | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.