@openhands/extensions 0.9.0 → 0.10.0
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.
- package/.github/workflows/deprecation-check.yml +25 -0
- package/.release-please-manifest.json +1 -1
- package/integrations/README.md +25 -0
- package/integrations/catalog/atlassian.json +13 -0
- package/integrations/catalog/github.json +20 -0
- package/integrations/catalog/microsoft-outlook.json +13 -0
- package/integrations/catalog/microsoft-teams.json +13 -0
- package/integrations/catalog/notion.json +13 -0
- package/integrations/catalog/onedrive.json +13 -0
- package/integrations/catalog/sharepoint.json +13 -0
- package/integrations/catalog/slack.json +14 -0
- package/integrations/catalog/vanta.json +55 -0
- package/integrations/catalog-index.js +69 -67
- package/integrations/catalog.schema.json +70 -3
- package/integrations/index.d.ts +49 -3
- package/package.json +1 -1
- package/pyproject.toml +6 -1
- package/python/openhands_extensions/__init__.py +38 -0
- package/python/openhands_extensions/_version.py +2 -2
- package/python/openhands_extensions/deprecation.py +159 -0
- package/python/openhands_extensions/integration_models.py +233 -0
- package/python/openhands_extensions/integrations.py +59 -4
- package/scripts/check_deprecations.py +488 -0
- package/tests/test_catalog_schema.py +58 -0
- package/tests/test_deprecations.py +136 -0
- package/tests/test_integration_catalog_in_sync.py +118 -7
|
@@ -4,9 +4,12 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
6
|
import subprocess
|
|
7
|
+
from copy import deepcopy
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
|
|
9
10
|
import openhands_extensions
|
|
11
|
+
import pytest
|
|
12
|
+
from pydantic import ValidationError
|
|
10
13
|
|
|
11
14
|
ROOT = Path(__file__).resolve().parents[1]
|
|
12
15
|
CATALOG_DIR = ROOT / "integrations" / "catalog"
|
|
@@ -23,7 +26,17 @@ def _catalog_files() -> dict[str, dict]:
|
|
|
23
26
|
|
|
24
27
|
def _catalog_entries() -> list[dict]:
|
|
25
28
|
entries = list(_catalog_files().values())
|
|
26
|
-
return sorted(
|
|
29
|
+
return sorted(
|
|
30
|
+
entries,
|
|
31
|
+
key=lambda entry: (
|
|
32
|
+
-(
|
|
33
|
+
entry.get("popularityRank")
|
|
34
|
+
if entry.get("popularityRank") is not None
|
|
35
|
+
else -1
|
|
36
|
+
),
|
|
37
|
+
entry["id"],
|
|
38
|
+
),
|
|
39
|
+
)
|
|
27
40
|
|
|
28
41
|
|
|
29
42
|
def _supports_mcp(entry: dict) -> bool:
|
|
@@ -31,8 +44,10 @@ def _supports_mcp(entry: dict) -> bool:
|
|
|
31
44
|
|
|
32
45
|
|
|
33
46
|
def _supports_oauth(entry: dict) -> bool:
|
|
34
|
-
return any(
|
|
35
|
-
|
|
47
|
+
return any(
|
|
48
|
+
option.get("auth", {}).get("strategy") == "oauth2"
|
|
49
|
+
for option in entry["connectionOptions"]
|
|
50
|
+
)
|
|
36
51
|
|
|
37
52
|
|
|
38
53
|
def test_catalog_directory_is_hand_authored_source_of_truth() -> None:
|
|
@@ -84,6 +99,95 @@ def test_python_list_integration_catalog_returns_raw_entries() -> None:
|
|
|
84
99
|
assert "runtimeAvailability" not in entry
|
|
85
100
|
|
|
86
101
|
|
|
102
|
+
def test_python_list_integration_catalog_models_returns_typed_entries() -> None:
|
|
103
|
+
entries = openhands_extensions.list_integration_catalog()
|
|
104
|
+
models = openhands_extensions.list_integration_catalog_models()
|
|
105
|
+
|
|
106
|
+
assert [model.model_dump(exclude_none=True) for model in models] == entries
|
|
107
|
+
assert all(
|
|
108
|
+
isinstance(model, openhands_extensions.IntegrationCatalogEntry)
|
|
109
|
+
for model in models
|
|
110
|
+
)
|
|
111
|
+
assert (
|
|
112
|
+
openhands_extensions.get_integration_catalog_entry_model(entries[0]["id"])
|
|
113
|
+
== models[0]
|
|
114
|
+
)
|
|
115
|
+
assert openhands_extensions.get_integration_catalog_entry_model("missing") is None
|
|
116
|
+
|
|
117
|
+
with pytest.raises(ValidationError):
|
|
118
|
+
openhands_extensions.IntegrationCatalogEntry.model_validate(
|
|
119
|
+
{**entries[0], "unexpected": True}
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_http_catalog_options_require_both_urls() -> None:
|
|
124
|
+
entry = next(
|
|
125
|
+
entry
|
|
126
|
+
for entry in _catalog_entries()
|
|
127
|
+
if any(option["provider"] == "http" for option in entry["connectionOptions"])
|
|
128
|
+
)
|
|
129
|
+
option = next(
|
|
130
|
+
option for option in entry["connectionOptions"] if option["provider"] == "http"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
for field in ("apiBaseUrl", "openApiUrl"):
|
|
134
|
+
invalid = deepcopy(entry)
|
|
135
|
+
invalid_option = next(
|
|
136
|
+
item for item in invalid["connectionOptions"] if item["id"] == option["id"]
|
|
137
|
+
)
|
|
138
|
+
invalid_option["http"].pop(field)
|
|
139
|
+
with pytest.raises(ValidationError, match=field):
|
|
140
|
+
openhands_extensions.IntegrationCatalogEntry.model_validate(invalid)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_catalog_models_require_connectability_and_install_metadata() -> None:
|
|
144
|
+
entry = next(entry for entry in _catalog_entries() if entry["id"] == "slack")
|
|
145
|
+
|
|
146
|
+
missing_docs = deepcopy(entry)
|
|
147
|
+
missing_docs.pop("docsUrl")
|
|
148
|
+
with pytest.raises(ValidationError, match="docsUrl"):
|
|
149
|
+
openhands_extensions.IntegrationCatalogEntry.model_validate(missing_docs)
|
|
150
|
+
|
|
151
|
+
missing_field_type = deepcopy(entry)
|
|
152
|
+
missing_field_type["connectionOptions"][1]["transport"]["envFields"][0].pop("type")
|
|
153
|
+
with pytest.raises(ValidationError, match="type"):
|
|
154
|
+
openhands_extensions.IntegrationCatalogEntry.model_validate(missing_field_type)
|
|
155
|
+
|
|
156
|
+
missing_required_marker = deepcopy(entry)
|
|
157
|
+
missing_required_marker["connectionOptions"][1]["transport"]["envFields"][0].pop(
|
|
158
|
+
"required"
|
|
159
|
+
)
|
|
160
|
+
with pytest.raises(ValidationError, match="required"):
|
|
161
|
+
openhands_extensions.IntegrationCatalogEntry.model_validate(
|
|
162
|
+
missing_required_marker
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
missing_principal_mapping = deepcopy(entry)
|
|
166
|
+
missing_principal_mapping["connectionOptions"][0]["connectionModel"][
|
|
167
|
+
"identityMapping"
|
|
168
|
+
].pop("externalPrincipalIdPath")
|
|
169
|
+
with pytest.raises(ValidationError, match="externalPrincipalIdPath"):
|
|
170
|
+
openhands_extensions.IntegrationCatalogEntry.model_validate(
|
|
171
|
+
missing_principal_mapping
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
null_instead_of_omission = deepcopy(entry)
|
|
175
|
+
null_instead_of_omission["notes"] = None
|
|
176
|
+
with pytest.raises(ValidationError, match="must be omitted"):
|
|
177
|
+
openhands_extensions.IntegrationCatalogEntry.model_validate(
|
|
178
|
+
null_instead_of_omission
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
invalid_identity_path = deepcopy(entry)
|
|
182
|
+
invalid_identity_path["connectionOptions"][0]["connectionModel"]["identityMapping"][
|
|
183
|
+
"externalPrincipalIdPath"
|
|
184
|
+
] = "not/a/path"
|
|
185
|
+
with pytest.raises(ValidationError, match="pattern"):
|
|
186
|
+
openhands_extensions.IntegrationCatalogEntry.model_validate(
|
|
187
|
+
invalid_identity_path
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
87
191
|
def test_logo_metadata_is_serializable_and_language_agnostic() -> None:
|
|
88
192
|
entries = openhands_extensions.list_integration_catalog()
|
|
89
193
|
with_logo = [entry for entry in entries if entry.get("logoUrl")]
|
|
@@ -130,7 +234,9 @@ def test_js_reads_the_same_catalog_as_python() -> None:
|
|
|
130
234
|
"import { getIntegrationCatalogEntry } from './integrations/index.js';\n"
|
|
131
235
|
f"process.stdout.write(JSON.stringify(getIntegrationCatalogEntry({json.dumps(sample_id)})));"
|
|
132
236
|
)
|
|
133
|
-
assert json.loads(sample) == openhands_extensions.get_integration_catalog_entry(
|
|
237
|
+
assert json.loads(sample) == openhands_extensions.get_integration_catalog_entry(
|
|
238
|
+
sample_id
|
|
239
|
+
)
|
|
134
240
|
|
|
135
241
|
|
|
136
242
|
def _js_filter(mcp, oauth) -> list[str]:
|
|
@@ -155,7 +261,9 @@ def test_filter_mcp_only() -> None:
|
|
|
155
261
|
|
|
156
262
|
|
|
157
263
|
def test_filter_oauth_only() -> None:
|
|
158
|
-
py_ids = {
|
|
264
|
+
py_ids = {
|
|
265
|
+
e["id"] for e in openhands_extensions.list_integration_catalog(oauth=True)
|
|
266
|
+
}
|
|
159
267
|
js_ids = set(_js_filter("undefined", "true"))
|
|
160
268
|
all_oauth = {
|
|
161
269
|
e["id"]
|
|
@@ -168,7 +276,8 @@ def test_filter_oauth_only() -> None:
|
|
|
168
276
|
|
|
169
277
|
def test_filter_oauth_not_mcp() -> None:
|
|
170
278
|
py_ids = {
|
|
171
|
-
e["id"]
|
|
279
|
+
e["id"]
|
|
280
|
+
for e in openhands_extensions.list_integration_catalog(oauth=True, mcp=False)
|
|
172
281
|
}
|
|
173
282
|
js_ids = set(_js_filter("false", "true"))
|
|
174
283
|
expected = {
|
|
@@ -197,7 +306,9 @@ def test_accessors_return_independent_copies() -> None:
|
|
|
197
306
|
sample = openhands_extensions.get_integration_catalog_entry(sample_id)
|
|
198
307
|
assert sample is not None
|
|
199
308
|
sample["__mutated"] = True
|
|
200
|
-
assert "__mutated" not in openhands_extensions.get_integration_catalog_entry(
|
|
309
|
+
assert "__mutated" not in openhands_extensions.get_integration_catalog_entry(
|
|
310
|
+
sample_id
|
|
311
|
+
)
|
|
201
312
|
|
|
202
313
|
snapshot = openhands_extensions.INTEGRATION_CATALOG_SNAPSHOT
|
|
203
314
|
snapshot["integrations"][0]["__mutated"] = True
|