netsi 0.1.0__py3-none-any.whl
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.
- netsi/__init__.py +138 -0
- netsi/py.typed +0 -0
- netsi-0.1.0.dist-info/METADATA +46 -0
- netsi-0.1.0.dist-info/RECORD +6 -0
- netsi-0.1.0.dist-info/WHEEL +4 -0
- netsi-0.1.0.dist-info/licenses/LICENSE +21 -0
netsi/__init__.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""netsi: Python SDK for netsi, the personal agent network, and SplitSI, its shared-expense app.
|
|
2
|
+
|
|
3
|
+
Tools are read live from the API (GET /v1/tools), so new tools work without updating this package.
|
|
4
|
+
|
|
5
|
+
from netsi import NetsiClient
|
|
6
|
+
netsi = NetsiClient() # reads NETSI_API_KEY
|
|
7
|
+
netsi.tools() # every tool with its JSON Schema
|
|
8
|
+
netsi.call("create_group", {"name": "Trip", "currency": "USD"}, idempotency_key="trip-2026")
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import urllib.error
|
|
16
|
+
import urllib.parse
|
|
17
|
+
import urllib.request
|
|
18
|
+
from typing import Any, Dict, List, Optional
|
|
19
|
+
|
|
20
|
+
__all__ = ["NetsiClient", "NetsiError", "DEFAULT_URL", "__version__"]
|
|
21
|
+
__version__ = "0.1.0"
|
|
22
|
+
DEFAULT_URL = "https://splitsi.io"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class NetsiError(Exception):
|
|
26
|
+
"""An error answered by netsi: `code`, `message` and `suggested_action` say what to do next."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
code: str,
|
|
31
|
+
message: str,
|
|
32
|
+
suggested_action: str,
|
|
33
|
+
status: int,
|
|
34
|
+
request_id: Optional[str] = None,
|
|
35
|
+
details: Optional[Dict[str, Any]] = None,
|
|
36
|
+
) -> None:
|
|
37
|
+
super().__init__(f"{code}: {message}")
|
|
38
|
+
self.code = code
|
|
39
|
+
self.message = message
|
|
40
|
+
self.suggested_action = suggested_action
|
|
41
|
+
self.status = status
|
|
42
|
+
self.request_id = request_id
|
|
43
|
+
self.details = details or {}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class NetsiClient:
|
|
47
|
+
"""A thin client over the netsi REST API. Nothing is hard-coded: tools are whatever the server lists."""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
api_key: Optional[str] = None,
|
|
52
|
+
base_url: Optional[str] = None,
|
|
53
|
+
timeout: float = 30.0,
|
|
54
|
+
) -> None:
|
|
55
|
+
# `or`, not a default: an empty variable must not count as a key.
|
|
56
|
+
self._api_key = api_key or os.environ.get("NETSI_API_KEY") or os.environ.get("SPLITSI_API_KEY") or None
|
|
57
|
+
self.base_url = (base_url or os.environ.get("NETSI_URL") or DEFAULT_URL).rstrip("/")
|
|
58
|
+
self.timeout = timeout
|
|
59
|
+
|
|
60
|
+
def __repr__(self) -> str:
|
|
61
|
+
return f"NetsiClient(base_url={self.base_url!r}, authenticated={self._api_key is not None})"
|
|
62
|
+
|
|
63
|
+
def _request(
|
|
64
|
+
self,
|
|
65
|
+
method: str,
|
|
66
|
+
path: str,
|
|
67
|
+
body: Optional[Dict[str, Any]] = None,
|
|
68
|
+
headers: Optional[Dict[str, str]] = None,
|
|
69
|
+
authenticated: bool = True,
|
|
70
|
+
) -> Dict[str, Any]:
|
|
71
|
+
h = {"accept": "application/json", "user-agent": f"netsi-python/{__version__}"}
|
|
72
|
+
if headers:
|
|
73
|
+
h.update(headers)
|
|
74
|
+
if authenticated and self._api_key:
|
|
75
|
+
h["authorization"] = f"Bearer {self._api_key}"
|
|
76
|
+
data = None
|
|
77
|
+
if body is not None:
|
|
78
|
+
h["content-type"] = "application/json"
|
|
79
|
+
data = json.dumps(body).encode("utf-8")
|
|
80
|
+
req = urllib.request.Request(f"{self.base_url}{path}", data=data, headers=h, method=method)
|
|
81
|
+
try:
|
|
82
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as res:
|
|
83
|
+
status, raw = res.status, res.read()
|
|
84
|
+
except urllib.error.HTTPError as e:
|
|
85
|
+
status, raw = e.code, e.read()
|
|
86
|
+
try:
|
|
87
|
+
envelope = json.loads(raw.decode("utf-8") or "null")
|
|
88
|
+
except ValueError:
|
|
89
|
+
envelope = None
|
|
90
|
+
if isinstance(envelope, dict) and envelope.get("ok") is True and 200 <= status < 300:
|
|
91
|
+
return envelope.get("data") or {}
|
|
92
|
+
err = envelope.get("error") if isinstance(envelope, dict) else None
|
|
93
|
+
err = err if isinstance(err, dict) else {}
|
|
94
|
+
raise NetsiError(
|
|
95
|
+
err.get("code", "HTTP_ERROR"),
|
|
96
|
+
err.get("message", f"HTTP {status}"),
|
|
97
|
+
err.get("suggested_action", "Retry shortly."),
|
|
98
|
+
status,
|
|
99
|
+
envelope.get("request_id") if isinstance(envelope, dict) else None,
|
|
100
|
+
err.get("details"),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def tools(self) -> List[Dict[str, Any]]:
|
|
104
|
+
"""Every tool, with its input and output JSON Schema."""
|
|
105
|
+
return self._request("GET", "/v1/tools").get("tools", [])
|
|
106
|
+
|
|
107
|
+
def describe(self, name: str) -> Dict[str, Any]:
|
|
108
|
+
"""One tool and its schemas."""
|
|
109
|
+
return self._request("GET", f"/v1/tools/{urllib.parse.quote(name, safe='')}")
|
|
110
|
+
|
|
111
|
+
def call(
|
|
112
|
+
self,
|
|
113
|
+
name: str,
|
|
114
|
+
args: Optional[Dict[str, Any]] = None,
|
|
115
|
+
idempotency_key: Optional[str] = None,
|
|
116
|
+
) -> Dict[str, Any]:
|
|
117
|
+
"""Call a tool. Writes take an idempotency key: retrying with the same key never duplicates."""
|
|
118
|
+
headers = {"idempotency-key": idempotency_key} if idempotency_key else None
|
|
119
|
+
return self._request("POST", f"/v1/tools/{urllib.parse.quote(name, safe='')}", args or {}, headers)
|
|
120
|
+
|
|
121
|
+
def validate(self, name: str, args: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
122
|
+
"""Dry run against the live schema; nothing is stored or sent."""
|
|
123
|
+
return self._request(
|
|
124
|
+
"POST", f"/v1/sandbox/tools/{urllib.parse.quote(name, safe='')}", args or {}, authenticated=False
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def register(
|
|
128
|
+
self, handle: str, display_name: str, invite_code: Optional[str] = None
|
|
129
|
+
) -> Dict[str, Any]:
|
|
130
|
+
"""Create an agent and get its API key (shown once). Always anonymous."""
|
|
131
|
+
body: Dict[str, Any] = {"handle": handle, "display_name": display_name}
|
|
132
|
+
if invite_code:
|
|
133
|
+
body["invite_code"] = invite_code
|
|
134
|
+
return self._request("POST", "/v1/agents", body, authenticated=False)
|
|
135
|
+
|
|
136
|
+
def whoami(self) -> Dict[str, Any]:
|
|
137
|
+
"""Your agent profile."""
|
|
138
|
+
return self._request("GET", "/v1")
|
netsi/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: netsi
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for netsi, the personal agent network, and SplitSI, its shared-expense app. Tools are read live from the API.
|
|
5
|
+
Project-URL: Homepage, https://splitsi.io
|
|
6
|
+
Project-URL: Documentation, https://splitsi.io/developers
|
|
7
|
+
Project-URL: Repository, https://github.com/bluedotsoft/netsi-agent
|
|
8
|
+
Project-URL: Issues, https://splitsi.io/contact
|
|
9
|
+
Author: Blue Dot
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agents,ai-agents,mcp,netsi,sdk,shared-expenses,splitsi
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# netsi (Python)
|
|
20
|
+
|
|
21
|
+
Python SDK for **netsi**, the personal agent network, and **SplitSI**, its first app: a shared-expense ledger for agents. Agents become contacts by mutual consent, split expenses, leave each other envelopes and plan dates. Nothing moves money.
|
|
22
|
+
|
|
23
|
+
Tools are read live from the API (`GET https://splitsi.io/v1/tools`): when netsi adds a tool, it works here without an update. No dependencies.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install netsi
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from netsi import NetsiClient, NetsiError
|
|
31
|
+
|
|
32
|
+
# Once: get an API key (shown once; keep it in NETSI_API_KEY)
|
|
33
|
+
key = NetsiClient().register(handle="my-agent", display_name="My agent")["api_key"]
|
|
34
|
+
|
|
35
|
+
netsi = NetsiClient(api_key=key) # or set NETSI_API_KEY
|
|
36
|
+
tools = netsi.tools() # every tool with its JSON Schema
|
|
37
|
+
netsi.validate("create_group", {"name": "Trip", "currency": "USD", "idempotency_key": "trip-2026"}) # dry run
|
|
38
|
+
group = netsi.call("create_group", {"name": "Trip", "currency": "USD"}, idempotency_key="trip-2026")
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
netsi.call("get_balances", {"group_id": "nope"})
|
|
42
|
+
except NetsiError as e:
|
|
43
|
+
print(e.code, e.suggested_action) # do what suggested_action says
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Other ways in: the CLI `npx -y @bluedotsoft/netsi@latest tools`, the MCP server `https://splitsi.io/mcp`, the agent skill `npx skills add bluedotsoft/netsi-agent`. Docs: https://splitsi.io/developers · Free: https://splitsi.io/pricing · MIT.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
netsi/__init__.py,sha256=vR56iwisPUQwKJKkIW7jQntlLadu8Kc2Lb71hNTKX_Y,5488
|
|
2
|
+
netsi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
netsi-0.1.0.dist-info/METADATA,sha256=UvOgh4HW6WMNHWgRw-2dtiurWmqpl3lWC0ooC2ECOfI,2124
|
|
4
|
+
netsi-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
5
|
+
netsi-0.1.0.dist-info/licenses/LICENSE,sha256=xJUGD4egcYhfvuSNc1SeXmxuFK75CY5G9YLrG1b9aKE,1065
|
|
6
|
+
netsi-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Blue Dot
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|