borgee-plugin-sdk 0.1.2__tar.gz → 0.2.0__tar.gz
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.
- {borgee_plugin_sdk-0.1.2/src/borgee_plugin_sdk.egg-info → borgee_plugin_sdk-0.2.0}/PKG-INFO +1 -1
- {borgee_plugin_sdk-0.1.2 → borgee_plugin_sdk-0.2.0}/pyproject.toml +1 -1
- {borgee_plugin_sdk-0.1.2 → borgee_plugin_sdk-0.2.0}/src/borgee_plugin_sdk/__init__.py +7 -1
- {borgee_plugin_sdk-0.1.2 → borgee_plugin_sdk-0.2.0}/src/borgee_plugin_sdk/client.py +27 -0
- {borgee_plugin_sdk-0.1.2 → borgee_plugin_sdk-0.2.0}/src/borgee_plugin_sdk/models.py +56 -0
- {borgee_plugin_sdk-0.1.2 → borgee_plugin_sdk-0.2.0/src/borgee_plugin_sdk.egg-info}/PKG-INFO +1 -1
- {borgee_plugin_sdk-0.1.2 → borgee_plugin_sdk-0.2.0}/src/borgee_plugin_sdk.egg-info/SOURCES.txt +1 -0
- borgee_plugin_sdk-0.2.0/tests/test_task_properties.py +149 -0
- {borgee_plugin_sdk-0.1.2 → borgee_plugin_sdk-0.2.0}/tests/test_verify_installed_sdk.py +2 -2
- {borgee_plugin_sdk-0.1.2 → borgee_plugin_sdk-0.2.0}/LICENSE +0 -0
- {borgee_plugin_sdk-0.1.2 → borgee_plugin_sdk-0.2.0}/README.md +0 -0
- {borgee_plugin_sdk-0.1.2 → borgee_plugin_sdk-0.2.0}/setup.cfg +0 -0
- {borgee_plugin_sdk-0.1.2 → borgee_plugin_sdk-0.2.0}/src/borgee_plugin_sdk/_connection.py +0 -0
- {borgee_plugin_sdk-0.1.2 → borgee_plugin_sdk-0.2.0}/src/borgee_plugin_sdk/_wire/__init__.py +0 -0
- {borgee_plugin_sdk-0.1.2 → borgee_plugin_sdk-0.2.0}/src/borgee_plugin_sdk/_wire/frames_generated.py +0 -0
- {borgee_plugin_sdk-0.1.2 → borgee_plugin_sdk-0.2.0}/src/borgee_plugin_sdk/cursor.py +0 -0
- {borgee_plugin_sdk-0.1.2 → borgee_plugin_sdk-0.2.0}/src/borgee_plugin_sdk/errors.py +0 -0
- {borgee_plugin_sdk-0.1.2 → borgee_plugin_sdk-0.2.0}/src/borgee_plugin_sdk/py.typed +0 -0
- {borgee_plugin_sdk-0.1.2 → borgee_plugin_sdk-0.2.0}/src/borgee_plugin_sdk/testing/__init__.py +0 -0
- {borgee_plugin_sdk-0.1.2 → borgee_plugin_sdk-0.2.0}/src/borgee_plugin_sdk/testing/fake_transport.py +0 -0
- {borgee_plugin_sdk-0.1.2 → borgee_plugin_sdk-0.2.0}/src/borgee_plugin_sdk.egg-info/dependency_links.txt +0 -0
- {borgee_plugin_sdk-0.1.2 → borgee_plugin_sdk-0.2.0}/src/borgee_plugin_sdk.egg-info/requires.txt +0 -0
- {borgee_plugin_sdk-0.1.2 → borgee_plugin_sdk-0.2.0}/src/borgee_plugin_sdk.egg-info/top_level.txt +0 -0
- {borgee_plugin_sdk-0.1.2 → borgee_plugin_sdk-0.2.0}/tests/test_connection_unit.py +0 -0
- {borgee_plugin_sdk-0.1.2 → borgee_plugin_sdk-0.2.0}/tests/test_cursor_store.py +0 -0
- {borgee_plugin_sdk-0.1.2 → borgee_plugin_sdk-0.2.0}/tests/test_models_and_client.py +0 -0
- {borgee_plugin_sdk-0.1.2 → borgee_plugin_sdk-0.2.0}/tests/test_task_crud.py +0 -0
- {borgee_plugin_sdk-0.1.2 → borgee_plugin_sdk-0.2.0}/tests/test_websocket_integration.py +0 -0
- {borgee_plugin_sdk-0.1.2 → borgee_plugin_sdk-0.2.0}/tests/test_wire_contract.py +0 -0
|
@@ -34,12 +34,15 @@ from .models import (
|
|
|
34
34
|
SentMessage,
|
|
35
35
|
Task,
|
|
36
36
|
TaskOutcome,
|
|
37
|
+
TaskPropertyDefinition,
|
|
38
|
+
TaskPropertyRender,
|
|
37
39
|
User,
|
|
38
40
|
UserRole,
|
|
39
41
|
task_from_wire,
|
|
42
|
+
task_property_definition_from_wire,
|
|
40
43
|
)
|
|
41
44
|
|
|
42
|
-
__version__ = "0.
|
|
45
|
+
__version__ = "0.2.0"
|
|
43
46
|
|
|
44
47
|
__all__ = [
|
|
45
48
|
"MISSING",
|
|
@@ -76,8 +79,11 @@ __all__ = [
|
|
|
76
79
|
"StaleDeliveryError",
|
|
77
80
|
"Task",
|
|
78
81
|
"TaskOutcome",
|
|
82
|
+
"TaskPropertyDefinition",
|
|
83
|
+
"TaskPropertyRender",
|
|
79
84
|
"User",
|
|
80
85
|
"UserRole",
|
|
81
86
|
"create_borgee_plugin",
|
|
82
87
|
"task_from_wire",
|
|
88
|
+
"task_property_definition_from_wire",
|
|
83
89
|
]
|
|
@@ -27,8 +27,10 @@ from .models import (
|
|
|
27
27
|
SentMessage,
|
|
28
28
|
Task,
|
|
29
29
|
TaskOutcome,
|
|
30
|
+
TaskPropertyDefinition,
|
|
30
31
|
User,
|
|
31
32
|
task_from_wire,
|
|
33
|
+
task_property_definition_from_wire,
|
|
32
34
|
user_from_wire,
|
|
33
35
|
)
|
|
34
36
|
|
|
@@ -283,6 +285,31 @@ class BorgeePluginClient:
|
|
|
283
285
|
result = await self._transport.perform("update_task", payload)
|
|
284
286
|
return task_from_wire(result)
|
|
285
287
|
|
|
288
|
+
# ─── Task properties (BPP semantic actions) ────────────
|
|
289
|
+
# One key per call. The server has no whole-object property write, so two
|
|
290
|
+
# agents writing different keys of the same task never overwrite each other.
|
|
291
|
+
|
|
292
|
+
async def set_task_property(self, task_id: str, key: str, value: str) -> Task:
|
|
293
|
+
result = await self._transport.perform(
|
|
294
|
+
"set_task_property", {"task_id": task_id, "key": key, "value": value}
|
|
295
|
+
)
|
|
296
|
+
return task_from_wire(result)
|
|
297
|
+
|
|
298
|
+
async def delete_task_property(self, task_id: str, key: str) -> Task:
|
|
299
|
+
result = await self._transport.perform("delete_task_property", {"task_id": task_id, "key": key})
|
|
300
|
+
return task_from_wire(result)
|
|
301
|
+
|
|
302
|
+
async def list_task_property_definitions(self) -> list[TaskPropertyDefinition]:
|
|
303
|
+
"""Return the registered property keys with their labels and render kinds.
|
|
304
|
+
|
|
305
|
+
Read this rather than hardcoding keys — the server can register a new
|
|
306
|
+
property without an SDK release.
|
|
307
|
+
"""
|
|
308
|
+
result = await self._transport.perform("list_task_property_definitions", {})
|
|
309
|
+
if not isinstance(result, list):
|
|
310
|
+
raise ProtocolError("list_task_property_definitions did not return an array")
|
|
311
|
+
return [task_property_definition_from_wire(item) for item in result]
|
|
312
|
+
|
|
286
313
|
# ─── Channel management (BPP semantic actions) ─────────
|
|
287
314
|
|
|
288
315
|
async def list_channels(self) -> list[dict[str, Any]]:
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
+
from collections.abc import Mapping
|
|
6
7
|
from dataclasses import dataclass, field
|
|
7
8
|
from enum import StrEnum
|
|
8
9
|
from typing import Any, Protocol, TypeAlias, runtime_checkable
|
|
@@ -234,6 +235,60 @@ class Task:
|
|
|
234
235
|
created_at: int
|
|
235
236
|
updated_at: int
|
|
236
237
|
heartbeat_prompt: str
|
|
238
|
+
# Registered property keys and their values (PR link, issue link, agent
|
|
239
|
+
# session id). Always a mapping; a task with no properties carries {}.
|
|
240
|
+
properties: Mapping[str, str]
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class TaskPropertyRender(StrEnum):
|
|
244
|
+
"""How a client should present a property value; the value is always a string."""
|
|
245
|
+
|
|
246
|
+
URL = "url"
|
|
247
|
+
IDENTIFIER = "identifier"
|
|
248
|
+
TEXT = "text"
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@dataclass(frozen=True, slots=True)
|
|
252
|
+
class TaskPropertyDefinition:
|
|
253
|
+
"""One entry of the server's task-property registry.
|
|
254
|
+
|
|
255
|
+
Read the registry rather than hardcoding keys: the server can register a new
|
|
256
|
+
property without an SDK release.
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
key: str
|
|
260
|
+
label: str
|
|
261
|
+
render: TaskPropertyRender
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def task_property_definition_from_wire(value: Any) -> TaskPropertyDefinition:
|
|
265
|
+
if not isinstance(value, dict):
|
|
266
|
+
raise TypeError("task property definition must be an object")
|
|
267
|
+
key = _required_non_empty_string(value.get("key"), "key")
|
|
268
|
+
render = value.get("render")
|
|
269
|
+
# An unrecognised render kind means the server registered a presentation
|
|
270
|
+
# this SDK predates. Fall back to text so the value still shows rather than
|
|
271
|
+
# the whole definition being dropped.
|
|
272
|
+
try:
|
|
273
|
+
parsed_render = TaskPropertyRender(str(render))
|
|
274
|
+
except ValueError:
|
|
275
|
+
parsed_render = TaskPropertyRender.TEXT
|
|
276
|
+
return TaskPropertyDefinition(
|
|
277
|
+
key=key,
|
|
278
|
+
label=value.get("label", ""),
|
|
279
|
+
render=parsed_render,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _task_properties_from_wire(value: Any) -> dict[str, str]:
|
|
284
|
+
"""Keep only string values.
|
|
285
|
+
|
|
286
|
+
A non-string value means the server and this SDK disagree about the shape,
|
|
287
|
+
so it is dropped rather than coerced into a plausible-looking repr.
|
|
288
|
+
"""
|
|
289
|
+
if not isinstance(value, dict):
|
|
290
|
+
return {}
|
|
291
|
+
return {key: item for key, item in value.items() if isinstance(key, str) and isinstance(item, str)}
|
|
237
292
|
|
|
238
293
|
|
|
239
294
|
def task_from_wire(value: Any) -> Task:
|
|
@@ -258,6 +313,7 @@ def task_from_wire(value: Any) -> Task:
|
|
|
258
313
|
created_at=value.get("created_at", 0),
|
|
259
314
|
updated_at=value.get("updated_at", 0),
|
|
260
315
|
heartbeat_prompt=value.get("heartbeat_prompt", ""),
|
|
316
|
+
properties=_task_properties_from_wire(value.get("properties")),
|
|
261
317
|
)
|
|
262
318
|
|
|
263
319
|
|
{borgee_plugin_sdk-0.1.2 → borgee_plugin_sdk-0.2.0}/src/borgee_plugin_sdk.egg-info/SOURCES.txt
RENAMED
|
@@ -21,6 +21,7 @@ tests/test_connection_unit.py
|
|
|
21
21
|
tests/test_cursor_store.py
|
|
22
22
|
tests/test_models_and_client.py
|
|
23
23
|
tests/test_task_crud.py
|
|
24
|
+
tests/test_task_properties.py
|
|
24
25
|
tests/test_verify_installed_sdk.py
|
|
25
26
|
tests/test_websocket_integration.py
|
|
26
27
|
tests/test_wire_contract.py
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Tests for task properties on BorgeePluginClient and the property wire decoders."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
import borgee_plugin_sdk as sdk
|
|
8
|
+
from borgee_plugin_sdk.errors import ProtocolError
|
|
9
|
+
from borgee_plugin_sdk.testing import FakeTransport, create_test_client
|
|
10
|
+
|
|
11
|
+
TASK_WIRE: dict = {
|
|
12
|
+
"id": "task-1",
|
|
13
|
+
"channel_id": "ch-1",
|
|
14
|
+
"title": "Write the spec",
|
|
15
|
+
"properties": {"link.pr": "https://example.test/pr/1"},
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _client_and_transport() -> tuple[sdk.BorgeePluginClient, FakeTransport]:
|
|
20
|
+
options = sdk.BorgeePluginOptions("https://example.com", "secret")
|
|
21
|
+
transport = FakeTransport()
|
|
22
|
+
return create_test_client(options, transport), transport
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ── task_from_wire: properties ─────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_task_properties_decode() -> None:
|
|
29
|
+
task = sdk.task_from_wire(TASK_WIRE)
|
|
30
|
+
assert task.properties == {"link.pr": "https://example.test/pr/1"}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_task_without_properties_decodes_to_empty_mapping() -> None:
|
|
34
|
+
"""Callers never have to tell "field absent" from "no properties" apart."""
|
|
35
|
+
task = sdk.task_from_wire({"id": "task-1"})
|
|
36
|
+
assert task.properties == {}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_task_properties_drop_non_string_values() -> None:
|
|
40
|
+
"""A non-string value means server and SDK disagree; dropping beats coercing."""
|
|
41
|
+
wire = {**TASK_WIRE, "properties": {"good": "v", "bad": {"nested": 1}, "also_bad": 7}}
|
|
42
|
+
assert sdk.task_from_wire(wire).properties == {"good": "v"}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ── task_property_definition_from_wire ─────────────────────────────
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_property_definition_decode() -> None:
|
|
49
|
+
definition = sdk.task_property_definition_from_wire(
|
|
50
|
+
{"key": "link.pr", "label": "Pull Request", "render": "url"}
|
|
51
|
+
)
|
|
52
|
+
assert definition.key == "link.pr"
|
|
53
|
+
assert definition.label == "Pull Request"
|
|
54
|
+
assert definition.render is sdk.TaskPropertyRender.URL
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_property_definition_unknown_render_falls_back_to_text() -> None:
|
|
58
|
+
"""An SDK older than the server still shows the value instead of dropping it."""
|
|
59
|
+
definition = sdk.task_property_definition_from_wire({"key": "x", "label": "X", "render": "hologram"})
|
|
60
|
+
assert definition.render is sdk.TaskPropertyRender.TEXT
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_property_definition_rejects_non_dict() -> None:
|
|
64
|
+
with pytest.raises(TypeError, match="task property definition must be an object"):
|
|
65
|
+
sdk.task_property_definition_from_wire("not a dict")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_property_definition_rejects_missing_key() -> None:
|
|
69
|
+
with pytest.raises(TypeError):
|
|
70
|
+
sdk.task_property_definition_from_wire({"label": "X", "render": "url"})
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ── set_task_property ──────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@pytest.mark.asyncio
|
|
77
|
+
async def test_set_task_property() -> None:
|
|
78
|
+
client, transport = _client_and_transport()
|
|
79
|
+
await client.connect()
|
|
80
|
+
|
|
81
|
+
transport.queue_result(TASK_WIRE)
|
|
82
|
+
task = await client.set_task_property("task-1", "link.pr", "https://example.test/pr/1")
|
|
83
|
+
assert task.properties["link.pr"] == "https://example.test/pr/1"
|
|
84
|
+
|
|
85
|
+
action = transport.actions[-1]
|
|
86
|
+
assert action.operation == "set_task_property"
|
|
87
|
+
assert action.payload == {
|
|
88
|
+
"task_id": "task-1",
|
|
89
|
+
"key": "link.pr",
|
|
90
|
+
"value": "https://example.test/pr/1",
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@pytest.mark.asyncio
|
|
95
|
+
async def test_set_task_property_sends_empty_value() -> None:
|
|
96
|
+
"""An empty string is "set this key to empty", not "field omitted"."""
|
|
97
|
+
client, transport = _client_and_transport()
|
|
98
|
+
await client.connect()
|
|
99
|
+
|
|
100
|
+
transport.queue_result(TASK_WIRE)
|
|
101
|
+
await client.set_task_property("task-1", "link.pr", "")
|
|
102
|
+
assert transport.actions[-1].payload["value"] == ""
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# ── delete_task_property ───────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@pytest.mark.asyncio
|
|
109
|
+
async def test_delete_task_property() -> None:
|
|
110
|
+
client, transport = _client_and_transport()
|
|
111
|
+
await client.connect()
|
|
112
|
+
|
|
113
|
+
transport.queue_result({**TASK_WIRE, "properties": {}})
|
|
114
|
+
task = await client.delete_task_property("task-1", "link.pr")
|
|
115
|
+
assert task.properties == {}
|
|
116
|
+
|
|
117
|
+
action = transport.actions[-1]
|
|
118
|
+
assert action.operation == "delete_task_property"
|
|
119
|
+
assert action.payload == {"task_id": "task-1", "key": "link.pr"}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# ── list_task_property_definitions ─────────────────────────────────
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@pytest.mark.asyncio
|
|
126
|
+
async def test_list_task_property_definitions() -> None:
|
|
127
|
+
client, transport = _client_and_transport()
|
|
128
|
+
await client.connect()
|
|
129
|
+
|
|
130
|
+
transport.queue_result(
|
|
131
|
+
[
|
|
132
|
+
{"key": "link.pr", "label": "Pull Request", "render": "url"},
|
|
133
|
+
{"key": "agent.session_id", "label": "Agent Session", "render": "identifier"},
|
|
134
|
+
]
|
|
135
|
+
)
|
|
136
|
+
definitions = await client.list_task_property_definitions()
|
|
137
|
+
assert [d.key for d in definitions] == ["link.pr", "agent.session_id"]
|
|
138
|
+
assert definitions[1].render is sdk.TaskPropertyRender.IDENTIFIER
|
|
139
|
+
assert transport.actions[-1].operation == "list_task_property_definitions"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@pytest.mark.asyncio
|
|
143
|
+
async def test_list_task_property_definitions_rejects_non_array() -> None:
|
|
144
|
+
client, transport = _client_and_transport()
|
|
145
|
+
await client.connect()
|
|
146
|
+
|
|
147
|
+
transport.queue_result({"definitions": []})
|
|
148
|
+
with pytest.raises(ProtocolError, match="did not return an array"):
|
|
149
|
+
await client.list_task_property_definitions()
|
|
@@ -8,7 +8,7 @@ from scripts import verify_installed_sdk as verifier
|
|
|
8
8
|
@pytest.fixture
|
|
9
9
|
def valid_runtime(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
10
10
|
monkeypatch.setattr(verifier, "_python_version", lambda: "3.12")
|
|
11
|
-
monkeypatch.setattr(verifier, "_sdk_distribution_version", lambda: "0.
|
|
11
|
+
monkeypatch.setattr(verifier, "_sdk_distribution_version", lambda: "0.2.0")
|
|
12
12
|
monkeypatch.setattr(verifier, "_websockets_distribution_version", lambda: "15.0.1")
|
|
13
13
|
monkeypatch.setattr(verifier, "_py_typed_exists", lambda: True)
|
|
14
14
|
monkeypatch.setattr(verifier, "_resume_frame_type", lambda: "session.resume_ack")
|
|
@@ -21,7 +21,7 @@ def valid_runtime(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
|
21
21
|
(
|
|
22
22
|
"_sdk_distribution_version",
|
|
23
23
|
"9.9.9",
|
|
24
|
-
"SDK distribution version mismatch: expected 0.
|
|
24
|
+
"SDK distribution version mismatch: expected 0.2.0, found 9.9.9.",
|
|
25
25
|
),
|
|
26
26
|
(
|
|
27
27
|
"_websockets_distribution_version",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{borgee_plugin_sdk-0.1.2 → borgee_plugin_sdk-0.2.0}/src/borgee_plugin_sdk/_wire/frames_generated.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{borgee_plugin_sdk-0.1.2 → borgee_plugin_sdk-0.2.0}/src/borgee_plugin_sdk/testing/__init__.py
RENAMED
|
File without changes
|
{borgee_plugin_sdk-0.1.2 → borgee_plugin_sdk-0.2.0}/src/borgee_plugin_sdk/testing/fake_transport.py
RENAMED
|
File without changes
|
|
File without changes
|
{borgee_plugin_sdk-0.1.2 → borgee_plugin_sdk-0.2.0}/src/borgee_plugin_sdk.egg-info/requires.txt
RENAMED
|
File without changes
|
{borgee_plugin_sdk-0.1.2 → borgee_plugin_sdk-0.2.0}/src/borgee_plugin_sdk.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|