urirun 0.3.17__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.
- urirun-0.3.17/PKG-INFO +93 -0
- urirun-0.3.17/README.md +71 -0
- urirun-0.3.17/pyproject.toml +60 -0
- urirun-0.3.17/setup.cfg +4 -0
- urirun-0.3.17/tests/test_adopt_pack.py +75 -0
- urirun-0.3.17/tests/test_agent_command.py +57 -0
- urirun-0.3.17/tests/test_compat.py +99 -0
- urirun-0.3.17/tests/test_connect_catalog.py +165 -0
- urirun-0.3.17/tests/test_connector_scaffold.py +59 -0
- urirun-0.3.17/tests/test_connector_sdk.py +62 -0
- urirun-0.3.17/tests/test_connector_smoke.py +82 -0
- urirun-0.3.17/tests/test_declarative.py +102 -0
- urirun-0.3.17/tests/test_domain_monitor.py +161 -0
- urirun-0.3.17/tests/test_errors.py +290 -0
- urirun-0.3.17/tests/test_host_dashboard.py +96 -0
- urirun-0.3.17/tests/test_host_db.py +112 -0
- urirun-0.3.17/tests/test_mesh.py +63 -0
- urirun-0.3.17/tests/test_minimal_imports.py +90 -0
- urirun-0.3.17/tests/test_planfile_adapter.py +342 -0
- urirun-0.3.17/tests/test_scheduler.py +61 -0
- urirun-0.3.17/tests/test_secrets.py +92 -0
- urirun-0.3.17/tests/test_urihandler.py +169 -0
- urirun-0.3.17/tests/test_v2_mcp.py +46 -0
- urirun-0.3.17/urirun/__init__.py +256 -0
- urirun-0.3.17/urirun/_registry.py +8 -0
- urirun-0.3.17/urirun/_runtime.py +8 -0
- urirun-0.3.17/urirun/_scan.py +8 -0
- urirun-0.3.17/urirun/compat.py +8 -0
- urirun-0.3.17/urirun/connect_catalog.py +5 -0
- urirun-0.3.17/urirun/connector_scaffold.py +5 -0
- urirun-0.3.17/urirun/connector_sdk.py +5 -0
- urirun-0.3.17/urirun/connector_smoke.py +5 -0
- urirun-0.3.17/urirun/connectors/__init__.py +1 -0
- urirun-0.3.17/urirun/connectors/connect_catalog.py +236 -0
- urirun-0.3.17/urirun/connectors/connector_scaffold.py +386 -0
- urirun-0.3.17/urirun/connectors/connector_sdk.py +87 -0
- urirun-0.3.17/urirun/connectors/connector_smoke.py +81 -0
- urirun-0.3.17/urirun/connectors/declarative.py +95 -0
- urirun-0.3.17/urirun/domain_monitor.py +5 -0
- urirun-0.3.17/urirun/errors.py +8 -0
- urirun-0.3.17/urirun/host/__init__.py +1 -0
- urirun-0.3.17/urirun/host/domain_monitor.py +393 -0
- urirun-0.3.17/urirun/host/host_dashboard.py +614 -0
- urirun-0.3.17/urirun/host/host_db.py +475 -0
- urirun-0.3.17/urirun/host/host_integrations.py +382 -0
- urirun-0.3.17/urirun/host/planfile_adapter.py +261 -0
- urirun-0.3.17/urirun/host/scheduler.py +133 -0
- urirun-0.3.17/urirun/host/task_planner.py +344 -0
- urirun-0.3.17/urirun/host_dashboard.py +5 -0
- urirun-0.3.17/urirun/host_db.py +5 -0
- urirun-0.3.17/urirun/host_integrations.py +5 -0
- urirun-0.3.17/urirun/mesh.py +5 -0
- urirun-0.3.17/urirun/node/__init__.py +1 -0
- urirun-0.3.17/urirun/node/mesh.py +1092 -0
- urirun-0.3.17/urirun/planfile_adapter.py +5 -0
- urirun-0.3.17/urirun/runtime/__init__.py +1 -0
- urirun-0.3.17/urirun/runtime/_registry.py +701 -0
- urirun-0.3.17/urirun/runtime/_runtime.py +471 -0
- urirun-0.3.17/urirun/runtime/_scan.py +670 -0
- urirun-0.3.17/urirun/runtime/adopt_pack.py +173 -0
- urirun-0.3.17/urirun/runtime/agent.py +107 -0
- urirun-0.3.17/urirun/runtime/compat.py +199 -0
- urirun-0.3.17/urirun/runtime/errors.py +511 -0
- urirun-0.3.17/urirun/runtime/secrets.py +149 -0
- urirun-0.3.17/urirun/runtime/v1.py +423 -0
- urirun-0.3.17/urirun/runtime/v2.py +1627 -0
- urirun-0.3.17/urirun/runtime/v2_adopt.py +195 -0
- urirun-0.3.17/urirun/runtime/v2_grpc.py +205 -0
- urirun-0.3.17/urirun/runtime/v2_mcp.py +205 -0
- urirun-0.3.17/urirun/runtime/v2_service.py +103 -0
- urirun-0.3.17/urirun/scheduler.py +5 -0
- urirun-0.3.17/urirun/task_planner.py +5 -0
- urirun-0.3.17/urirun/v1.py +8 -0
- urirun-0.3.17/urirun/v2.py +8 -0
- urirun-0.3.17/urirun/v2_adopt.py +8 -0
- urirun-0.3.17/urirun/v2_grpc.py +8 -0
- urirun-0.3.17/urirun/v2_mcp.py +8 -0
- urirun-0.3.17/urirun/v2_service.py +8 -0
- urirun-0.3.17/urirun.egg-info/PKG-INFO +93 -0
- urirun-0.3.17/urirun.egg-info/SOURCES.txt +82 -0
- urirun-0.3.17/urirun.egg-info/dependency_links.txt +1 -0
- urirun-0.3.17/urirun.egg-info/entry_points.txt +4 -0
- urirun-0.3.17/urirun.egg-info/requires.txt +19 -0
- urirun-0.3.17/urirun.egg-info/top_level.txt +1 -0
urirun-0.3.17/PKG-INFO
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: urirun
|
|
3
|
+
Version: 0.3.17
|
|
4
|
+
Summary: Language-agnostic URI to handler adapter
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: jsonschema>=4.18
|
|
9
|
+
Requires-Dist: pydantic>=2
|
|
10
|
+
Provides-Extra: grpc
|
|
11
|
+
Requires-Dist: grpcio>=1.60; extra == "grpc"
|
|
12
|
+
Provides-Extra: planfile
|
|
13
|
+
Requires-Dist: planfile>=0.1.103; extra == "planfile"
|
|
14
|
+
Provides-Extra: llm
|
|
15
|
+
Requires-Dist: litellm>=1.60; extra == "llm"
|
|
16
|
+
Provides-Extra: host
|
|
17
|
+
Requires-Dist: planfile>=0.1.103; extra == "host"
|
|
18
|
+
Requires-Dist: litellm>=1.60; extra == "host"
|
|
19
|
+
Provides-Extra: test
|
|
20
|
+
Requires-Dist: planfile>=0.1.103; extra == "test"
|
|
21
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
22
|
+
|
|
23
|
+
# urirun python adapter
|
|
24
|
+
|
|
25
|
+
Install directly from GitHub:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install "git+https://github.com/if-uri/urirun.git@v0.3.13#subdirectory=adapters/python"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Or install a GitHub Release wheel:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install "https://github.com/if-uri/urirun/releases/download/v0.3.13/urirun-0.3.13-py3-none-any.whl"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
PyPI publishing is not required. The distribution is named `urirun`; the Python
|
|
38
|
+
import package remains `urirun`:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
import urirun
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
After installation the `urirun` CLI is available:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
urirun scan ./project --out .urirun/bindings.v2.json --registry-out .urirun/registry.merged.json
|
|
48
|
+
urirun validate .urirun/bindings.v2.json
|
|
49
|
+
urirun list .urirun/registry.merged.json
|
|
50
|
+
urirun run 'cli://local/git/status' .urirun/registry.merged.json
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`urirun-v1` and `urirun-v2` are also installed as explicit versioned entry
|
|
54
|
+
points for scripts that need a stable major-version command.
|
|
55
|
+
|
|
56
|
+
The optional v2 gRPC transport can be installed with:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install "urirun[grpc] @ git+https://github.com/if-uri/urirun.git@v0.3.13#subdirectory=adapters/python"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
v2 can generate schema-first bindings and a compiled registry from existing
|
|
63
|
+
artifacts:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
urirun scan ./project \
|
|
67
|
+
--out generated/bindings.v2.json \
|
|
68
|
+
--registry-out generated/registry.json
|
|
69
|
+
urirun validate generated/bindings.v2.json
|
|
70
|
+
urirun list generated/registry.json
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Connector packages can generate bindings directly from decorated Python
|
|
74
|
+
functions. The shortest path is to declare the connector once and then attach
|
|
75
|
+
short URI paths to functions:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
import urirun
|
|
79
|
+
|
|
80
|
+
connector = urirun.connector("http-check", scheme="httpcheck")
|
|
81
|
+
|
|
82
|
+
@connector.command("http/query/status")
|
|
83
|
+
def status_command(url: str, expectStatus: int = 200, timeout: float = 10.0):
|
|
84
|
+
return ["urirun-http-check", "status", "{url}", "--expect-status", "{expectStatus}"]
|
|
85
|
+
|
|
86
|
+
def urirun_bindings():
|
|
87
|
+
return connector.bindings()
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
Licensed under Apache-2.0.
|
urirun-0.3.17/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# urirun python adapter
|
|
2
|
+
|
|
3
|
+
Install directly from GitHub:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install "git+https://github.com/if-uri/urirun.git@v0.3.13#subdirectory=adapters/python"
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Or install a GitHub Release wheel:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install "https://github.com/if-uri/urirun/releases/download/v0.3.13/urirun-0.3.13-py3-none-any.whl"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
PyPI publishing is not required. The distribution is named `urirun`; the Python
|
|
16
|
+
import package remains `urirun`:
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
import urirun
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
After installation the `urirun` CLI is available:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
urirun scan ./project --out .urirun/bindings.v2.json --registry-out .urirun/registry.merged.json
|
|
26
|
+
urirun validate .urirun/bindings.v2.json
|
|
27
|
+
urirun list .urirun/registry.merged.json
|
|
28
|
+
urirun run 'cli://local/git/status' .urirun/registry.merged.json
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`urirun-v1` and `urirun-v2` are also installed as explicit versioned entry
|
|
32
|
+
points for scripts that need a stable major-version command.
|
|
33
|
+
|
|
34
|
+
The optional v2 gRPC transport can be installed with:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install "urirun[grpc] @ git+https://github.com/if-uri/urirun.git@v0.3.13#subdirectory=adapters/python"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
v2 can generate schema-first bindings and a compiled registry from existing
|
|
41
|
+
artifacts:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
urirun scan ./project \
|
|
45
|
+
--out generated/bindings.v2.json \
|
|
46
|
+
--registry-out generated/registry.json
|
|
47
|
+
urirun validate generated/bindings.v2.json
|
|
48
|
+
urirun list generated/registry.json
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Connector packages can generate bindings directly from decorated Python
|
|
52
|
+
functions. The shortest path is to declare the connector once and then attach
|
|
53
|
+
short URI paths to functions:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
import urirun
|
|
57
|
+
|
|
58
|
+
connector = urirun.connector("http-check", scheme="httpcheck")
|
|
59
|
+
|
|
60
|
+
@connector.command("http/query/status")
|
|
61
|
+
def status_command(url: str, expectStatus: int = 200, timeout: float = 10.0):
|
|
62
|
+
return ["urirun-http-check", "status", "{url}", "--expect-status", "{expectStatus}"]
|
|
63
|
+
|
|
64
|
+
def urirun_bindings():
|
|
65
|
+
return connector.bindings()
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
## License
|
|
70
|
+
|
|
71
|
+
Licensed under Apache-2.0.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "urirun"
|
|
7
|
+
version = "0.3.17"
|
|
8
|
+
description = "Language-agnostic URI to handler adapter"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "Apache-2.0"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"jsonschema>=4.18",
|
|
14
|
+
"pydantic>=2",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
grpc = ["grpcio>=1.60"]
|
|
19
|
+
planfile = ["planfile>=0.1.103"]
|
|
20
|
+
llm = ["litellm>=1.60"]
|
|
21
|
+
host = ["planfile>=0.1.103", "litellm>=1.60"]
|
|
22
|
+
test = ["planfile>=0.1.103", "pytest>=7"]
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
urirun = "urirun.v2:main"
|
|
26
|
+
urirun-v1 = "urirun.v1:main"
|
|
27
|
+
urirun-v2 = "urirun.v2:main"
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.packages.find]
|
|
30
|
+
where = ["."]
|
|
31
|
+
include = ["urirun*"]
|
|
32
|
+
|
|
33
|
+
[tool.pfix]
|
|
34
|
+
# Self-healing Python configuration
|
|
35
|
+
model = "openrouter/qwen/qwen3-coder-next"
|
|
36
|
+
auto_apply = false
|
|
37
|
+
auto_install_deps = true
|
|
38
|
+
auto_restart = false
|
|
39
|
+
max_retries = 3
|
|
40
|
+
create_backups = false
|
|
41
|
+
git_auto_commit = false
|
|
42
|
+
|
|
43
|
+
[tool.pfix.runtime_todo]
|
|
44
|
+
enabled = true
|
|
45
|
+
todo_file = "TODO.md"
|
|
46
|
+
min_severity = "low"
|
|
47
|
+
deduplicate = true
|
|
48
|
+
|
|
49
|
+
[tool.costs]
|
|
50
|
+
# AI Cost tracking configuration
|
|
51
|
+
badge = true
|
|
52
|
+
update_readme = true
|
|
53
|
+
readme_path = "README.md"
|
|
54
|
+
default_model = "openrouter/qwen/qwen3-coder-next"
|
|
55
|
+
analysis_mode = "byok"
|
|
56
|
+
full_history = true
|
|
57
|
+
max_commits = 500
|
|
58
|
+
|
|
59
|
+
# Cost thresholds for badge colors (USD)
|
|
60
|
+
badge_color_thresholds = { low = 1.0, medium = 5.0, high = 10.0, critical = 50.0 }
|
urirun-0.3.17/setup.cfg
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Adopting a capability-pack manifest as bindings.v2 (least-invasive URI adoption)."""
|
|
2
|
+
import json
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
import unittest
|
|
6
|
+
|
|
7
|
+
from urirun import v2
|
|
8
|
+
from urirun.runtime import adopt_pack
|
|
9
|
+
|
|
10
|
+
MANIFEST = {
|
|
11
|
+
"id": "demopack",
|
|
12
|
+
"version": 1,
|
|
13
|
+
"scheme": "demo",
|
|
14
|
+
"uri_patterns": [
|
|
15
|
+
{"pattern": "demo://host/status/query/info", "kind": "query",
|
|
16
|
+
"operation": "demo.status.info", "side_effects": False, "approval": "not_required"},
|
|
17
|
+
{"pattern": "demo://host/task/command/run", "kind": "command",
|
|
18
|
+
"operation": "demo.task.run", "side_effects": True, "approval": "required"},
|
|
19
|
+
],
|
|
20
|
+
"handlers": {"python": {
|
|
21
|
+
"demo.status.info": "python://demopack.handlers:status_info",
|
|
22
|
+
"demo.task.run": "python://demopack.handlers:task_run",
|
|
23
|
+
}},
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AdoptPackTests(unittest.TestCase):
|
|
28
|
+
def test_manifest_maps_to_bindings(self):
|
|
29
|
+
bindings = {b["uri"]: b for b in adopt_pack.manifest_bindings(MANIFEST)}
|
|
30
|
+
self.assertEqual(set(bindings), {"demo://host/status/query/info", "demo://host/task/command/run"})
|
|
31
|
+
q = bindings["demo://host/status/query/info"]
|
|
32
|
+
self.assertEqual(q["adapter"], "local-function")
|
|
33
|
+
self.assertEqual(q["ref"], "demopack.handlers:status_info")
|
|
34
|
+
self.assertEqual(q["meta"]["uriKind"], "query")
|
|
35
|
+
self.assertNotIn("policy", q) # no side effects / approval
|
|
36
|
+
|
|
37
|
+
def test_side_effects_and_approval_become_policy(self):
|
|
38
|
+
cmd = {b["uri"]: b for b in adopt_pack.manifest_bindings(MANIFEST)}["demo://host/task/command/run"]
|
|
39
|
+
self.assertEqual(cmd["policy"], {"approval": "required", "sideEffects": True})
|
|
40
|
+
|
|
41
|
+
def test_document_validates_and_compiles(self):
|
|
42
|
+
# write a JSON manifest so no YAML dependency is needed
|
|
43
|
+
import tempfile, os
|
|
44
|
+
fd, path = tempfile.mkstemp(suffix=".json")
|
|
45
|
+
os.write(fd, json.dumps(MANIFEST).encode())
|
|
46
|
+
os.close(fd)
|
|
47
|
+
try:
|
|
48
|
+
doc = adopt_pack.adopt(path)
|
|
49
|
+
self.assertEqual(doc["version"], v2.VERSION)
|
|
50
|
+
self.assertEqual(len(doc["bindings"]), 2)
|
|
51
|
+
registry = v2.compile_registry(doc) # must not raise
|
|
52
|
+
self.assertTrue(registry.get("routes") or registry.get("tree") or registry)
|
|
53
|
+
finally:
|
|
54
|
+
os.unlink(path)
|
|
55
|
+
|
|
56
|
+
def test_hydrated_route_executes(self):
|
|
57
|
+
from urirun import _registry as reg, _runtime as rt
|
|
58
|
+
import tempfile, os
|
|
59
|
+
fd, path = tempfile.mkstemp(suffix=".json")
|
|
60
|
+
os.write(fd, json.dumps(MANIFEST).encode())
|
|
61
|
+
os.close(fd)
|
|
62
|
+
try:
|
|
63
|
+
registry = v2.compile_registry(adopt_pack.adopt(path))
|
|
64
|
+
hydrated = reg.hydrate_registry(registry, {
|
|
65
|
+
"demopack.handlers:status_info": lambda t, a, p, d: {"ok": True, "where": t},
|
|
66
|
+
})
|
|
67
|
+
env = rt.run("demo://host/status/query/info", hydrated, mode="execute",
|
|
68
|
+
policy={"execute": {"allow": ["demo://*"]}})
|
|
69
|
+
self.assertTrue(env.get("ok"))
|
|
70
|
+
finally:
|
|
71
|
+
os.unlink(path)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
if __name__ == "__main__":
|
|
75
|
+
unittest.main()
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Tests for `urirun agent` (action space + planner loop)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import urirun
|
|
8
|
+
from urirun.runtime import agent
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _registry():
|
|
12
|
+
emit_json = [sys.executable, "-c", "print('{\"ok\": true, \"v\": 1}')"]
|
|
13
|
+
doc = {
|
|
14
|
+
"version": "urirun.bindings.v2",
|
|
15
|
+
"bindings": {
|
|
16
|
+
"demo://host/thing/query/read": {
|
|
17
|
+
"adapter": "argv-template", "kind": "command", "argv": emit_json,
|
|
18
|
+
"inputSchema": {"type": "object", "additionalProperties": False, "properties": {}},
|
|
19
|
+
"meta": {"connector": "demo", "label": "read"}, "uri": "demo://host/thing/query/read",
|
|
20
|
+
},
|
|
21
|
+
"demo://host/thing/command/write": {
|
|
22
|
+
"adapter": "argv-template", "kind": "command", "argv": emit_json,
|
|
23
|
+
"inputSchema": {"type": "object", "additionalProperties": False, "properties": {}},
|
|
24
|
+
"meta": {"connector": "demo", "label": "write"}, "uri": "demo://host/thing/command/write",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
return urirun.compile_registry(doc)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_action_space_marks_query_and_command():
|
|
32
|
+
space = {r["uri"]: r for r in agent.action_space(_registry())}
|
|
33
|
+
assert space["demo://host/thing/query/read"]["kind"] == "query"
|
|
34
|
+
assert space["demo://host/thing/command/write"]["kind"] == "command"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_run_plan_runs_query_and_gates_command():
|
|
38
|
+
registry = _registry()
|
|
39
|
+
steps = [
|
|
40
|
+
{"uri": "demo://host/thing/query/read", "payload": {}},
|
|
41
|
+
{"uri": "demo://host/thing/command/write", "payload": {}},
|
|
42
|
+
]
|
|
43
|
+
trace = agent.run_plan(registry, steps, allow_commands=False)
|
|
44
|
+
read, write = trace
|
|
45
|
+
assert read["ran"] is True and read["ok"] is True and read["data"]["v"] == 1
|
|
46
|
+
assert write["ran"] is False # command gated
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_run_plan_allows_command_with_permission():
|
|
50
|
+
registry = _registry()
|
|
51
|
+
trace = agent.run_plan(registry, [{"uri": "demo://host/thing/command/write", "payload": {}}], allow_commands=True)
|
|
52
|
+
assert trace[0]["ran"] is True and trace[0]["ok"] is True
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_load_planner_resolves_module_function():
|
|
56
|
+
fn = agent._load_planner("urirun.runtime.agent:action_space")
|
|
57
|
+
assert fn is agent.action_space
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Author: Tom Sapletta · https://tom.sapletta.com
|
|
2
|
+
# Part of the ifURI solution.
|
|
3
|
+
|
|
4
|
+
import contextlib
|
|
5
|
+
import io
|
|
6
|
+
import json
|
|
7
|
+
import unittest
|
|
8
|
+
from unittest.mock import patch
|
|
9
|
+
|
|
10
|
+
import urirun
|
|
11
|
+
from urirun import compat
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _healthy_importable(name):
|
|
15
|
+
# backend host/node layers stay in core; namecheap was extracted (removed),
|
|
16
|
+
# its connector replacement is installed.
|
|
17
|
+
if name == "urirun.namecheap_dns":
|
|
18
|
+
return False
|
|
19
|
+
if name == "urirun_connector_namecheap_dns":
|
|
20
|
+
return True
|
|
21
|
+
return bool(name and (name.startswith("urirun.host.") or name.startswith("urirun.node.")))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CompatReportTests(unittest.TestCase):
|
|
25
|
+
def test_backend_layer_is_kept(self):
|
|
26
|
+
with patch.object(compat, "_entry_point_names", return_value={"namecheap-dns"}), \
|
|
27
|
+
patch.object(compat, "_importable", side_effect=_healthy_importable):
|
|
28
|
+
data = compat.report()
|
|
29
|
+
|
|
30
|
+
host_db = next(m for m in data["modules"] if m["module"] == "urirun.host.host_db")
|
|
31
|
+
self.assertEqual(host_db["owner"], "backend")
|
|
32
|
+
self.assertEqual(host_db["layer"], "host")
|
|
33
|
+
self.assertEqual(host_db["reusedBy"], "urirun-connector-sqlite-context")
|
|
34
|
+
self.assertTrue(host_db["currentImportable"])
|
|
35
|
+
self.assertEqual(host_db["status"], "kept")
|
|
36
|
+
|
|
37
|
+
def test_namecheap_is_extracted(self):
|
|
38
|
+
with patch.object(compat, "_entry_point_names", return_value={"namecheap-dns"}), \
|
|
39
|
+
patch.object(compat, "_importable", side_effect=_healthy_importable):
|
|
40
|
+
data = compat.report()
|
|
41
|
+
|
|
42
|
+
nc = next(m for m in data["modules"] if m["module"] == "urirun.namecheap_dns")
|
|
43
|
+
self.assertEqual(nc["owner"], "extracted")
|
|
44
|
+
self.assertFalse(nc["currentImportable"]) # removed from core
|
|
45
|
+
self.assertTrue(nc["replacementInstalled"])
|
|
46
|
+
self.assertEqual(nc["status"], "extracted")
|
|
47
|
+
self.assertTrue(data["ok"])
|
|
48
|
+
|
|
49
|
+
def test_top_level_api_exposes_compat_report(self):
|
|
50
|
+
data = urirun.compat_report()
|
|
51
|
+
self.assertTrue(data["ok"])
|
|
52
|
+
self.assertTrue(any(m["module"] == "urirun.host.host_integrations" for m in data["modules"]))
|
|
53
|
+
|
|
54
|
+
def test_cli_list_json_reports_node_layer(self):
|
|
55
|
+
with patch.object(compat, "_entry_point_names", return_value={"namecheap-dns"}), \
|
|
56
|
+
patch.object(compat, "_importable", side_effect=_healthy_importable):
|
|
57
|
+
buffer = io.StringIO()
|
|
58
|
+
with contextlib.redirect_stdout(buffer):
|
|
59
|
+
code = compat.main(["list", "--json"])
|
|
60
|
+
|
|
61
|
+
self.assertEqual(code, 0)
|
|
62
|
+
data = json.loads(buffer.getvalue())
|
|
63
|
+
self.assertTrue(data["ok"])
|
|
64
|
+
self.assertGreater(data["backendLayers"], 0)
|
|
65
|
+
self.assertEqual(data["extracted"], 1)
|
|
66
|
+
mesh = next(m for m in data["modules"] if m["module"] == "urirun.node.mesh")
|
|
67
|
+
self.assertEqual(mesh["layer"], "node")
|
|
68
|
+
|
|
69
|
+
def test_cli_check_ok_when_layers_present_and_namecheap_extracted(self):
|
|
70
|
+
with patch.object(compat, "_entry_point_names", return_value={"namecheap-dns"}), \
|
|
71
|
+
patch.object(compat, "_importable", side_effect=_healthy_importable), \
|
|
72
|
+
contextlib.redirect_stdout(io.StringIO()):
|
|
73
|
+
code = compat.main(["check"])
|
|
74
|
+
|
|
75
|
+
self.assertEqual(code, 0)
|
|
76
|
+
|
|
77
|
+
def test_cli_check_nonzero_when_namecheap_replacement_missing(self):
|
|
78
|
+
def importable(name):
|
|
79
|
+
# backend present, but the namecheap connector is NOT installed
|
|
80
|
+
return bool(name and (name.startswith("urirun.host.") or name.startswith("urirun.node.")))
|
|
81
|
+
|
|
82
|
+
with patch.object(compat, "_entry_point_names", return_value=set()), \
|
|
83
|
+
patch.object(compat, "_importable", side_effect=importable), \
|
|
84
|
+
contextlib.redirect_stdout(io.StringIO()):
|
|
85
|
+
code = compat.main(["check"])
|
|
86
|
+
|
|
87
|
+
self.assertEqual(code, 1)
|
|
88
|
+
|
|
89
|
+
def test_cli_check_nonzero_when_backend_layer_missing(self):
|
|
90
|
+
with patch.object(compat, "_entry_point_names", return_value={"namecheap-dns"}), \
|
|
91
|
+
patch.object(compat, "_importable", return_value=False), \
|
|
92
|
+
contextlib.redirect_stdout(io.StringIO()):
|
|
93
|
+
code = compat.main(["check"])
|
|
94
|
+
|
|
95
|
+
self.assertEqual(code, 1)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
if __name__ == "__main__":
|
|
99
|
+
unittest.main()
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Tests for the connect.ifuri.com catalog client (no network)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from urirun import connect_catalog
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
CATALOG = {
|
|
14
|
+
"version": "ifuri.connectors.v1",
|
|
15
|
+
"defaultPipSpec": "urirun @ git+https://example/urirun",
|
|
16
|
+
"connectors": [
|
|
17
|
+
{
|
|
18
|
+
"id": "planfile",
|
|
19
|
+
"name": "Planfile Tasks",
|
|
20
|
+
"status": "available",
|
|
21
|
+
"category": "Planning",
|
|
22
|
+
"summary": "Tasks via task:// URIs.",
|
|
23
|
+
"uriSchemes": ["task", "planfile"],
|
|
24
|
+
"routes": ["task://host/tickets/query/list"],
|
|
25
|
+
"install": {"mode": "urirun-extra", "pipSpec": "urirun-connector-planfile @ git+https://example/planfile"},
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"id": "sqlite-context",
|
|
29
|
+
"name": "SQLite Context",
|
|
30
|
+
"status": "available",
|
|
31
|
+
"category": "Data",
|
|
32
|
+
"uriSchemes": ["data"],
|
|
33
|
+
"install": {"mode": "bundled"},
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"id": "mqtt",
|
|
37
|
+
"name": "MQTT",
|
|
38
|
+
"status": "planned",
|
|
39
|
+
"category": "IoT",
|
|
40
|
+
"install": {"mode": "planned", "pipSpec": "urirun-connectors-mqtt"},
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _args(**kw):
|
|
47
|
+
kw.setdefault("catalog", "https://connect.ifuri.com")
|
|
48
|
+
return argparse.Namespace(**kw)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_resolve_install_buckets():
|
|
52
|
+
plan = connect_catalog.resolve_install(CATALOG, ["planfile", "sqlite-context", "mqtt", "ghost"])
|
|
53
|
+
assert [item["pipSpec"] for item in plan["pipSpecs"]] == ["urirun-connector-planfile @ git+https://example/planfile"]
|
|
54
|
+
assert plan["bundled"] == ["sqlite-context"]
|
|
55
|
+
assert [s["id"] for s in plan["skipped"]] == ["mqtt"]
|
|
56
|
+
assert plan["unknown"] == ["ghost"]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_pip_install_command_uses_current_interpreter():
|
|
60
|
+
command = connect_catalog.pip_install_command(["pkg-a", "pkg-b"])
|
|
61
|
+
assert command[1:] == ["-m", "pip", "install", "pkg-a", "pkg-b"]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_install_dry_run_does_not_run_pip(monkeypatch, capsys):
|
|
65
|
+
monkeypatch.setattr(connect_catalog, "fetch_catalog", lambda base="": CATALOG)
|
|
66
|
+
ran = {"called": False}
|
|
67
|
+
monkeypatch.setattr(connect_catalog.subprocess, "run", lambda *a, **k: ran.__setitem__("called", True))
|
|
68
|
+
|
|
69
|
+
rc = connect_catalog.connectors_command(_args(connectors_command="install", ids=["planfile"], execute=False, json=False))
|
|
70
|
+
|
|
71
|
+
assert rc == 0
|
|
72
|
+
assert ran["called"] is False
|
|
73
|
+
out = capsys.readouterr().out
|
|
74
|
+
assert "dry-run" in out
|
|
75
|
+
assert "urirun-connector-planfile" in out
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_install_execute_invokes_pip(monkeypatch):
|
|
79
|
+
monkeypatch.setattr(connect_catalog, "fetch_catalog", lambda base="": CATALOG)
|
|
80
|
+
calls = {}
|
|
81
|
+
|
|
82
|
+
class _Result:
|
|
83
|
+
returncode = 0
|
|
84
|
+
|
|
85
|
+
def fake_run(command, *a, **k):
|
|
86
|
+
calls["command"] = command
|
|
87
|
+
return _Result()
|
|
88
|
+
|
|
89
|
+
monkeypatch.setattr(connect_catalog.subprocess, "run", fake_run)
|
|
90
|
+
|
|
91
|
+
rc = connect_catalog.connectors_command(_args(connectors_command="install", ids=["planfile"], execute=True, json=False))
|
|
92
|
+
|
|
93
|
+
assert rc == 0
|
|
94
|
+
assert calls["command"][1:4] == ["-m", "pip", "install"]
|
|
95
|
+
assert "urirun-connector-planfile @ git+https://example/planfile" in calls["command"]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_install_unknown_only_returns_error(monkeypatch):
|
|
99
|
+
monkeypatch.setattr(connect_catalog, "fetch_catalog", lambda base="": CATALOG)
|
|
100
|
+
rc = connect_catalog.connectors_command(_args(connectors_command="install", ids=["ghost"], execute=False, json=False))
|
|
101
|
+
assert rc == 1
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_list_available_filter(monkeypatch, capsys):
|
|
105
|
+
monkeypatch.setattr(connect_catalog, "fetch_catalog", lambda base="": CATALOG)
|
|
106
|
+
rc = connect_catalog.connectors_command(_args(connectors_command="list", available=True, json=False))
|
|
107
|
+
assert rc == 0
|
|
108
|
+
out = capsys.readouterr().out
|
|
109
|
+
assert "planfile" in out
|
|
110
|
+
assert "mqtt" not in out # planned filtered out
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_show_json(monkeypatch, capsys):
|
|
114
|
+
monkeypatch.setattr(connect_catalog, "fetch_connector", lambda cid, base="": {"connector": CATALOG["connectors"][0], "installCommand": "curl ... | bash"})
|
|
115
|
+
rc = connect_catalog.connectors_command(_args(connectors_command="show", id="planfile", json=True))
|
|
116
|
+
assert rc == 0
|
|
117
|
+
assert '"id": "planfile"' in capsys.readouterr().out
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_diff_manifest_in_sync():
|
|
121
|
+
entry = CATALOG["connectors"][0]
|
|
122
|
+
assert connect_catalog.diff_manifest(entry, entry) == []
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_diff_manifest_detects_route_and_pipspec_drift():
|
|
126
|
+
local = json.loads(json.dumps(CATALOG["connectors"][0]))
|
|
127
|
+
local["routes"] = local["routes"] + ["task://host/extra/query/new"]
|
|
128
|
+
local["install"]["pipSpec"] = "urirun-connector-planfile @ git+https://example/planfile@v9"
|
|
129
|
+
diffs = connect_catalog.diff_manifest(local, CATALOG["connectors"][0])
|
|
130
|
+
fields = {d["field"] for d in diffs}
|
|
131
|
+
assert "routes" in fields
|
|
132
|
+
assert "install.pipSpec" in fields
|
|
133
|
+
routes_diff = next(d for d in diffs if d["field"] == "routes")
|
|
134
|
+
assert routes_diff["onlyLocal"] == ["task://host/extra/query/new"]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_check_in_sync(monkeypatch, tmp_path, capsys):
|
|
138
|
+
manifest = tmp_path / "connector.manifest.json"
|
|
139
|
+
manifest.write_text(json.dumps(CATALOG["connectors"][0]), encoding="utf-8")
|
|
140
|
+
monkeypatch.setattr(connect_catalog, "fetch_connector", lambda cid, base="": {"connector": CATALOG["connectors"][0]})
|
|
141
|
+
rc = connect_catalog.connectors_command(_args(connectors_command="check", manifest=str(manifest), json=False))
|
|
142
|
+
assert rc == 0
|
|
143
|
+
assert "in sync" in capsys.readouterr().out
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_check_drift_returns_1(monkeypatch, tmp_path, capsys):
|
|
147
|
+
drifted = json.loads(json.dumps(CATALOG["connectors"][0]))
|
|
148
|
+
drifted["install"]["pipSpec"] = "urirun-connector-planfile @ git+https://example/planfile@vOLD"
|
|
149
|
+
manifest = tmp_path / "connector.manifest.json"
|
|
150
|
+
manifest.write_text(json.dumps(drifted), encoding="utf-8")
|
|
151
|
+
monkeypatch.setattr(connect_catalog, "fetch_connector", lambda cid, base="": {"connector": CATALOG["connectors"][0]})
|
|
152
|
+
rc = connect_catalog.connectors_command(_args(connectors_command="check", manifest=str(manifest), json=False))
|
|
153
|
+
assert rc == 1
|
|
154
|
+
assert "mismatch" in capsys.readouterr().err
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def test_catalog_network_error_returns_1(monkeypatch):
|
|
158
|
+
import urllib.error
|
|
159
|
+
|
|
160
|
+
def boom(base="", timeout=10.0):
|
|
161
|
+
raise urllib.error.URLError("offline")
|
|
162
|
+
|
|
163
|
+
monkeypatch.setattr(connect_catalog, "fetch_catalog", boom)
|
|
164
|
+
rc = connect_catalog.connectors_command(_args(connectors_command="list", available=False, json=False))
|
|
165
|
+
assert rc == 1
|