Skip to content

Commit 47243d5

Browse files
author
Radoslav Klic
committed
AI-1169 add link in run_job tool, update JobDetail to fit the specs
1 parent da209ea commit 47243d5

File tree

4 files changed

+27
-32
lines changed

4 files changed

+27
-32
lines changed

src/keboola_mcp_server/links.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88

99

1010
class Link(BaseModel):
11-
model_config = ConfigDict(frozen=True) # we make the model immutable to avoid unexpected changes
11+
model_config = ConfigDict(frozen=True)
12+
1213
type: URLType = Field(..., description='The type of the URL.')
1314
title: str = Field(..., description='The name of the URL.')
1415
url: str = Field(..., description='The URL.')
@@ -85,14 +86,14 @@ def get_used_components_link(
8586
)
8687

8788
def get_configuration_links(
88-
self, component_id: str, configuration_id: str, configuration_name: str
89-
) -> list[Link]:
89+
self, component_id: str, configuration_id: str, configuration_name: str
90+
) -> list[Link]:
9091
return [
91-
self.get_component_config_link(
92-
component_id=component_id, configuration_id=configuration_id, configuration_name=configuration_name
93-
),
94-
self.get_config_dashboard_link(component_id=component_id, component_name=component_id),
95-
]
92+
self.get_component_config_link(
93+
component_id=component_id, configuration_id=configuration_id, configuration_name=configuration_name
94+
),
95+
self.get_config_dashboard_link(component_id=component_id, component_name=component_id),
96+
]
9697

9798
# --- Transformations ---
9899
def get_transformations_dashboard_link(self) -> Link:

src/keboola_mcp_server/tools/jobs.py

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -94,22 +94,17 @@ class JobDetail(JobListItem):
9494
"""Represents a detailed job with all available information."""
9595

9696
url: str = Field(description='The URL of the job.')
97-
table_id: Optional[str] = Field(
98-
description='The ID of the table that the job is running on.',
99-
validation_alias=AliasChoices('tableId', 'table_id', 'table-id'),
100-
serialization_alias='tableId',
101-
default=None,
102-
)
97+
10398
config_data: Optional[dict[str, Any]] = Field(
10499
description='The data of the configuration.',
105100
validation_alias=AliasChoices('configData', 'config_data', 'config-data'),
106101
serialization_alias='configData',
107102
default=None,
108103
)
109-
config_row_ids: Optional[list[str]] = Field(
110-
description='The row IDs of the configuration.',
111-
validation_alias=AliasChoices('configRowIds', 'config_row_ids', 'config-row-ids'),
112-
serialization_alias='configRowIds',
104+
config_row: Optional[str] = Field(
105+
description='The configuration row ID.',
106+
validation_alias=AliasChoices('configRow', 'config_row', 'config-row'),
107+
serialization_alias='configRow',
113108
default=None,
114109
)
115110
run_id: Optional[str] = Field(
@@ -118,12 +113,6 @@ class JobDetail(JobListItem):
118113
serialization_alias='runId',
119114
default=None,
120115
)
121-
parent_run_id: Optional[str] = Field(
122-
description='The ID of the parent run that the job is running on.',
123-
validation_alias=AliasChoices('parentRunId', 'parent_run_id', 'parent-run-id'),
124-
serialization_alias='parentRunId',
125-
default=None,
126-
)
127116
result: Optional[dict[str, Any]] = Field(
128117
description='The results of the job.',
129118
validation_alias='result',
@@ -291,7 +280,9 @@ async def run_job(
291280
raw_job = await client.jobs_queue_client.create_job(
292281
component_id=component_id, configuration_id=configuration_id
293282
)
294-
job = JobDetail.model_validate(raw_job)
283+
links_manager = await ProjectLinksManager.from_client(client)
284+
links = links_manager.get_job_links(str(raw_job['id']))
285+
job = JobDetail.model_validate(raw_job | {'links': links})
295286
LOG.info(
296287
f'Started a new job with id: {job.id} for component {component_id} and configuration {configuration_id}.'
297288
)

tests/conftest.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ def keboola_client(mocker) -> KeboolaClient:
1919

2020
# Mock API clients
2121
client.storage_client = mocker.MagicMock(AsyncStorageClient)
22+
client.storage_client.base_api_url = 'test://api.keboola.com'
2223
client.storage_client.branch_id = 'default'
24+
client.storage_client.project_id.return_value = '123'
2325
client.jobs_queue_client = mocker.MagicMock(JobsQueueClient)
2426
client.ai_service_client = mocker.MagicMock(AIServiceClient)
2527

tests/tools/test_jobs.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from pytest_mock import MockerFixture
88

99
from keboola_mcp_server.client import KeboolaClient
10+
from keboola_mcp_server.links import Link
1011
from keboola_mcp_server.tools.jobs import (
1112
JobDetail,
1213
JobListItem,
@@ -60,13 +61,11 @@ def mock_job() -> dict[str, Any]:
6061
'endTime': '2024-01-01T00:00:02Z',
6162
'url': 'https://connection.keboola.com/jobs/123',
6263
'configData': {'source': 'file.csv'},
63-
'configRowIds': ['1', '2', '3'],
64+
'configRow': '1',
6465
'runId': '456',
65-
'parentRunId': '789',
6666
'durationSeconds': 100,
6767
'result': {'import': 'successful'},
6868
'metrics': {'rows': 1000},
69-
'links': []
7069
}
7170

7271

@@ -151,13 +150,10 @@ async def test_get_job(
151150
assert result.end_time.replace(tzinfo=None) == datetime.strptime(mock_job['endTime'], iso_format)
152151
assert result.url == mock_job['url']
153152
assert result.config_data == mock_job['configData']
154-
assert result.config_row_ids == mock_job['configRowIds']
153+
assert result.config_row == mock_job['configRow']
155154
assert result.run_id == mock_job['runId']
156-
assert result.parent_run_id == mock_job['parentRunId']
157155
assert result.duration_seconds == mock_job['durationSeconds']
158156
assert result.result == mock_job['result']
159-
# table_id is not present in the mock_job, should be None
160-
assert result.table_id is None
161157

162158
keboola_client.jobs_queue_client.get_job_detail.assert_called_once_with('123')
163159

@@ -247,6 +243,10 @@ async def test_run_job(
247243
assert job_detail.component_id == component_id
248244
assert job_detail.config_id == configuration_id
249245
assert job_detail.result == {}
246+
assert set(job_detail.links) == {
247+
Link(type='ui-detail', title='Job: 123', url='test://api.keboola.com/admin/projects/123/queue/123'),
248+
Link(type='ui-dashboard', title='Jobs in the project', url='test://api.keboola.com/admin/projects/123/queue'),
249+
}
250250

251251
keboola_client.jobs_queue_client.create_job.assert_called_once_with(
252252
component_id=component_id,
@@ -302,6 +302,7 @@ def test_job_detail_model_validate_dict_fields(
302302
:param mock_job: The mock job details - expecting api response.
303303
"""
304304
mock_job[field_name] = input_value
305+
mock_job['links'] = []
305306
if isinstance(expected_result, type) and issubclass(expected_result, Exception):
306307
with pytest.raises(expected_result):
307308
JobDetail.model_validate(mock_job)

0 commit comments

Comments
 (0)