-
Notifications
You must be signed in to change notification settings - Fork 18
Ai 1343 Add Clients for DataScience, Encryption Services And Refactor #235
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 28 commits
Commits
Show all changes
38 commits
Select commit
Hold shift + click to select a range
6a03cbd
AI-1343 feat: add data app component id
mariankrotil a7b25c7
AI-1343 feat: create client for data science service, create client s…
mariankrotil 1c03526
AI-1343 refactor: move client file to client subpackage and create ba…
mariankrotil d18d013
AI-1343 refactor: update imports wrt client submodule changes
mariankrotil 971cd97
AI-1343 feat: add encryption client to encrpyt values
mariankrotil cea8f3f
AI-1343 style: update code wrt flake8 using black and isort
mariankrotil a3e98de
AI-1343 fix: update endpoints, docs, refactor
mariankrotil 6dfc84d
AI-1343 feat: update data science client, add password, deploy, and s…
mariankrotil 4146e06
AI-1343 feat: add patch method to the base client
mariankrotil f9a9e22
AI-1343 feat: add method getting versions of component from versions …
mariankrotil 4ecb6d1
AI-1343 style: apply flake8 changes
mariankrotil 4bb1f9f
Merge branch 'main' into AI-1343-mcp-add-support-for-data-apps
mariankrotil a95af68
Merge branch 'main' into AI-1343-mcp-add-support-for-data-apps
mariankrotil 92042e3
AI-1343 feat(client): add handlers for listing, getting logs and dele…
mariankrotil 2eab15a
AI-1343 feat: add get method returning text value instead of json
mariankrotil 9b08f89
AI-1343 test: Move tests for client to the corresponding dir, add tes…
mariankrotil ad226b5
AI-1343 refactor: move client tests to the cor. dir
mariankrotil 4c9a436
AI-1343 style: apply black, isort and flake8 changes
mariankrotil 9a31c75
AI-1343 test: add client ecryption integ tests
mariankrotil 86489a9
AI-1343 refactor: move clients to separate submodules
mariankrotil 892b024
AI-1343 test: skip global serarch test for client method as it is uns…
mariankrotil 51f2bfb
AI-1343 style: apply isort
mariankrotil ad951c7
AI-1343 refactor: validate parameters in encryption client for encryp…
mariankrotil 7ddeea2
AI-1343 fix: make clients token parameter optional and handle headers…
mariankrotil 0fb290f
AI-1343 style: apply black
mariankrotil a7b124e
AI-1343 refactor: update description, rename classes wrt reviews
mariankrotil 5efd70b
Merge branch 'main' into AI-1343-mcp-add-support-for-data-apps
mariankrotil 7cda4d8
AI-1343 style: apply tox
mariankrotil 008f229
AI-1343 docs: improve client method get_text description to follow re…
mariankrotil 5e89017
AI-1343 refactor: update parameters for client methods - structured, …
mariankrotil 8247f12
AI-1343 test: add unit tests for data science client tail_app_log method
mariankrotil b503441
AI-1343 test: improve tests with asynccontext for initial data app tests
mariankrotil b48ff37
AI-1343 fix: increase waiting time for global search
mariankrotil 4457d0c
AI-1343 refactor: update parameter types for encrypt method wrt reviews
mariankrotil 812a341
AI-1343 test: unskip the global search test
mariankrotil a9359bf
AI-1343 refactor: remove async annot for non-async test, improve test…
mariankrotil a16b0de
AI-1343 refactor: add shortcut imports
mariankrotil 558a387
AI-1343 refactor: change Optional[s] to s | None in clients, ref code
mariankrotil 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 |
---|---|---|
@@ -0,0 +1,105 @@ | ||
import logging | ||
|
||
import pytest | ||
|
||
from keboola_mcp_server.clients.client import DATA_APP_COMPONENT_ID, KeboolaClient | ||
from keboola_mcp_server.clients.data_science import DataAppResponse, DataScienceClient | ||
|
||
LOG = logging.getLogger(__name__) | ||
|
||
|
||
def _minimal_parameters(slug: str) -> dict[str, object]: | ||
"""Build minimal valid parameters for a code-based Streamlit data app.""" | ||
return { | ||
'size': 'tiny', | ||
'autoSuspendAfterSeconds': 600, | ||
'dataApp': { | ||
'slug': slug, | ||
'streamlit': { | ||
'config.toml': '[theme]\nbase = "light"', | ||
}, | ||
}, | ||
'script': [ | ||
'import streamlit as st', | ||
"st.write('Hello from integration test')", | ||
], | ||
} | ||
|
||
|
||
def _public_access_authorization() -> dict[str, object]: | ||
"""Allow public access to all paths; no providers required.""" | ||
return { | ||
'app_proxy': { | ||
'auth_providers': [], | ||
'auth_rules': [ | ||
{'type': 'pathPrefix', 'value': '/', 'auth_required': False}, | ||
], | ||
} | ||
} | ||
|
||
|
||
@pytest.fixture | ||
def ds_client(keboola_client: KeboolaClient) -> DataScienceClient: | ||
return keboola_client.data_science_client | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_create_and_fetch_data_app( | ||
ds_client: DataScienceClient, unique_id: str, keboola_client: KeboolaClient | ||
) -> None: | ||
"""Test creating a data app and fetching it from detail and list endpoints""" | ||
slug = f'test-app-{unique_id}' | ||
created: DataAppResponse = await ds_client.create_data_app( | ||
name=f'IntegTest {slug}', | ||
description='Created by integration tests', | ||
parameters=_minimal_parameters(slug), | ||
authorization=_public_access_authorization(), | ||
) | ||
mariankrotil marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
try: | ||
# Check if the created data app is valid | ||
assert isinstance(created, DataAppResponse) | ||
assert created.id | ||
assert created.type == 'streamlit' | ||
assert created.component_id == DATA_APP_COMPONENT_ID | ||
|
||
# Deploy the data app | ||
response = await ds_client.deploy_data_app(created.id, created.config_version) | ||
assert response.id == created.id | ||
|
||
# Fetch the data app from data science | ||
fethced_ds = await ds_client.get_data_app(created.id) | ||
assert fethced_ds.id == created.id | ||
assert fethced_ds.type == created.type | ||
assert fethced_ds.component_id == created.component_id | ||
assert fethced_ds.project_id == created.project_id | ||
assert fethced_ds.config_id == created.config_id | ||
assert fethced_ds.config_version == created.config_version | ||
|
||
# Fetch the data app config from storage | ||
fetched_s = await keboola_client.storage_client.configuration_detail( | ||
component_id=DATA_APP_COMPONENT_ID, | ||
configuration_id=created.config_id, | ||
) | ||
|
||
# check if the data app ids are the same (data app from data science and config from storage) | ||
assert 'configuration' in fetched_s | ||
assert isinstance(fetched_s['configuration'], dict) | ||
assert 'parameters' in fetched_s['configuration'] | ||
assert isinstance(fetched_s['configuration']['parameters'], dict) | ||
assert 'id' in fetched_s['configuration']['parameters'] | ||
assert fethced_ds.id == fetched_s['configuration']['parameters']['id'] | ||
|
||
# Fetch the all data apps and check if the created data app is in the list | ||
data_apps = await ds_client.list_data_apps() | ||
assert isinstance(data_apps, list) | ||
assert len(data_apps) > 0 | ||
assert any(app.id == created.id for app in data_apps) | ||
|
||
finally: | ||
for _ in range(2): # Delete configuration 2 times (from storage and then from temporal bin) | ||
try: | ||
await ds_client.delete_data_app(created.id) | ||
except Exception as e: | ||
LOG.exception(f'Failed to delete data app: {e}') | ||
pass |
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,44 @@ | ||
from typing import Any | ||
|
||
import pytest | ||
|
||
from keboola_mcp_server.clients.client import DATA_APP_COMPONENT_ID, KeboolaClient | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_encrypt_without_project_id(keboola_client: KeboolaClient) -> None: | ||
mariankrotil marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"""Check that the encryption client does not send any authorization headers.""" | ||
assert 'Authorization' not in keboola_client.encryption_client.raw_client.headers | ||
assert 'X-StorageAPI-Token' not in keboola_client.encryption_client.raw_client.headers | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_encrypt_string_not_equal(keboola_client: KeboolaClient) -> None: | ||
project_id = await keboola_client.storage_client.project_id() | ||
plaintext = 'my-plain-text' | ||
encrypted = await keboola_client.encryption_client.encrypt( | ||
value=plaintext, | ||
project_id=str(project_id), | ||
component_id=DATA_APP_COMPONENT_ID, | ||
) | ||
assert isinstance(encrypted, str) | ||
assert encrypted != plaintext | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_encrypt_dict_hash_keys_only(keboola_client: KeboolaClient) -> None: | ||
project_id = await keboola_client.storage_client.project_id() | ||
payload: dict[str, Any] = { | ||
'#secret': 'sensitive-value', | ||
'public': 'visible-value', | ||
} | ||
result = await keboola_client.encryption_client.encrypt( | ||
value=payload, | ||
project_id=str(project_id), | ||
component_id=DATA_APP_COMPONENT_ID, | ||
) | ||
assert isinstance(result, dict) | ||
# Values under keys beginning with '#' should be encrypted (changed) | ||
assert result['#secret'] != payload['#secret'] | ||
# Non-secret values should remain the same | ||
assert result['public'] == payload['public'] |
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
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
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
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
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
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
mariankrotil marked this conversation as resolved.
Show resolved
Hide resolved
|
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,85 @@ | ||
from typing import Any, Optional, cast | ||
|
||
from pydantic import BaseModel, Field | ||
|
||
from keboola_mcp_server.clients.base import JsonDict, KeboolaServiceClient, RawKeboolaClient | ||
|
||
|
||
class DocsQuestionResponse(BaseModel): | ||
""" | ||
The AI service response to a request to `/docs/question` endpoint. | ||
""" | ||
|
||
text: str = Field(description='Text of the answer to a documentation query.') | ||
source_urls: list[str] = Field( | ||
description='List of URLs to the sources of the answer.', | ||
default_factory=list, | ||
alias='sourceUrls', | ||
) | ||
|
||
|
||
class SuggestedComponent(BaseModel): | ||
"""The AI service response to a /docs/suggest-component request.""" | ||
|
||
component_id: str = Field(description='The component ID.', alias='componentId') | ||
score: float = Field(description='Score of the component suggestion.') | ||
source: str = Field(description='Source of the component suggestion.') | ||
|
||
|
||
class ComponentSuggestionResponse(BaseModel): | ||
"""The AI service response to a /suggest/component request.""" | ||
|
||
components: list[SuggestedComponent] = Field(description='List of suggested components.', default_factory=list) | ||
|
||
|
||
class AIServiceClient(KeboolaServiceClient): | ||
"""Async client for Keboola AI Service.""" | ||
|
||
@classmethod | ||
def create(cls, root_url: str, token: Optional[str], headers: dict[str, Any] | None = None) -> 'AIServiceClient': | ||
""" | ||
Creates an AIServiceClient from a Keboola Storage API token. | ||
|
||
:param root_url: The root URL of the AI service API. | ||
:param token: The Keboola Storage API token. If None, the client will not send any authorization header. | ||
:param headers: Additional headers for the requests. | ||
:return: A new instance of AIServiceClient. | ||
""" | ||
return cls(raw_client=RawKeboolaClient(base_api_url=root_url, api_token=token, headers=headers)) | ||
|
||
async def get_component_detail(self, component_id: str) -> JsonDict: | ||
""" | ||
Retrieves information about a given component. | ||
|
||
:param component_id: The id of the component. | ||
:return: Component details as dictionary. | ||
""" | ||
return cast(JsonDict, await self.get(endpoint=f'docs/components/{component_id}')) | ||
|
||
async def docs_question(self, query: str) -> DocsQuestionResponse: | ||
""" | ||
Answers a question using the Keboola documentation as a source. | ||
:param query: The query to answer. | ||
:return: Response containing the answer and source URLs. | ||
""" | ||
response = await self.raw_client.post( | ||
endpoint='docs/question', | ||
data={'query': query}, | ||
headers={'Accept': 'application/json'}, | ||
) | ||
|
||
return DocsQuestionResponse.model_validate(response) | ||
|
||
async def suggest_component(self, query: str) -> ComponentSuggestionResponse: | ||
""" | ||
Provides list of component suggestions based on natural language query. | ||
:param query: The query to answer. | ||
:return: Response containing the list of suggested component IDs, their score and source. | ||
""" | ||
response = await self.raw_client.post( | ||
endpoint='suggest/component', | ||
data={'prompt': query}, | ||
headers={'Accept': 'application/json'}, | ||
) | ||
|
||
return ComponentSuggestionResponse.model_validate(response) |
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.