nanoping-api 11.1.0.dev0__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.
- nanoping_api-11.1.0.dev0/.github/workflows/publish.yml +29 -0
- nanoping_api-11.1.0.dev0/.gitignore +3 -0
- nanoping_api-11.1.0.dev0/PKG-INFO +63 -0
- nanoping_api-11.1.0.dev0/README.md +47 -0
- nanoping_api-11.1.0.dev0/examples/guide.py +194 -0
- nanoping_api-11.1.0.dev0/examples/main.py +37 -0
- nanoping_api-11.1.0.dev0/examples/pipeline_config.py +23 -0
- nanoping_api-11.1.0.dev0/pyproject.toml +31 -0
- nanoping_api-11.1.0.dev0/src/nanoping_api/__init__.py +18 -0
- nanoping_api-11.1.0.dev0/src/nanoping_api/_client.py +154 -0
- nanoping_api-11.1.0.dev0/src/nanoping_api/_generated.py +4182 -0
- nanoping_api-11.1.0.dev0/src/nanoping_api/_runtime.py +685 -0
- nanoping_api-11.1.0.dev0/src/nanoping_api/py.typed +0 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
tags:
|
|
5
|
+
- "v*"
|
|
6
|
+
permissions:
|
|
7
|
+
contents: read
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
name: Publish to PyPI
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
environment: pypi
|
|
13
|
+
permissions:
|
|
14
|
+
id-token: write
|
|
15
|
+
steps:
|
|
16
|
+
- name: Checkout
|
|
17
|
+
uses: actions/checkout@v4
|
|
18
|
+
- name: Setup Python
|
|
19
|
+
uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: "3.12"
|
|
22
|
+
- name: Check that the tag matches the package version
|
|
23
|
+
run: test "v$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')" = "$GITHUB_REF_NAME"
|
|
24
|
+
- name: Build
|
|
25
|
+
run: |
|
|
26
|
+
python -m pip install build
|
|
27
|
+
python -m build
|
|
28
|
+
- name: Publish
|
|
29
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: nanoping-api
|
|
3
|
+
Version: 11.1.0.dev0
|
|
4
|
+
Summary: Typed client for the NanoPing REST API
|
|
5
|
+
Project-URL: Homepage, https://nanoping.com
|
|
6
|
+
Project-URL: Documentation, https://docs.nanoping.com/api/reference
|
|
7
|
+
Project-URL: Source, https://github.com/nanoping-labs/nanoping-api-python
|
|
8
|
+
License-Expression: LicenseRef-Proprietary
|
|
9
|
+
Classifier: Framework :: AsyncIO
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Typing :: Typed
|
|
12
|
+
Requires-Python: >=3.11
|
|
13
|
+
Requires-Dist: httpx>=0.27
|
|
14
|
+
Requires-Dist: websockets>=14
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# NanoPing API for Python
|
|
18
|
+
|
|
19
|
+
A typed client for the REST API of a [NanoPing](https://nanoping.com) node. It
|
|
20
|
+
covers every method of the API, with helpers for flows that take more than one
|
|
21
|
+
call, such as joining a hub. It has a blocking and an asyncio client, and needs
|
|
22
|
+
Python 3.11 or later.
|
|
23
|
+
|
|
24
|
+
This repository is generated from the NanoPing sources on every release, and
|
|
25
|
+
its version matches the NanoPing release it was made from. Changes made here
|
|
26
|
+
are overwritten by the next release.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install nanoping-api
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Use
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from nanoping_api import NanopingClient
|
|
38
|
+
|
|
39
|
+
client = NanopingClient("http://127.0.0.1:10565")
|
|
40
|
+
|
|
41
|
+
local = client.hub_client.get_local_node_id()
|
|
42
|
+
for update in client.hub_client.stream_connection_state():
|
|
43
|
+
print(update.state)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`AsyncNanopingClient` has the same methods for asyncio, and its streams are
|
|
47
|
+
read with `async for`. Requests and responses are dataclasses, and the fields of
|
|
48
|
+
a request are passed as keyword arguments. A failed call raises an `ApiError`,
|
|
49
|
+
whose `code` says why, such as `Code.NOT_FOUND`.
|
|
50
|
+
|
|
51
|
+
## Examples
|
|
52
|
+
|
|
53
|
+
`examples` holds the program the
|
|
54
|
+
[HTTP REST guide](https://docs.nanoping.com/api/http-rest) is made from. It
|
|
55
|
+
joins a node to a hub and manages pipelines on both. Start the two nodes as the
|
|
56
|
+
comment in `examples/main.py` describes, then run
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install nanoping-api
|
|
60
|
+
python examples/main.py --client http://127.0.0.1:10565 --server http://127.0.0.1:10566
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The API is documented at https://docs.nanoping.com/api/reference.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# NanoPing API for Python
|
|
2
|
+
|
|
3
|
+
A typed client for the REST API of a [NanoPing](https://nanoping.com) node. It
|
|
4
|
+
covers every method of the API, with helpers for flows that take more than one
|
|
5
|
+
call, such as joining a hub. It has a blocking and an asyncio client, and needs
|
|
6
|
+
Python 3.11 or later.
|
|
7
|
+
|
|
8
|
+
This repository is generated from the NanoPing sources on every release, and
|
|
9
|
+
its version matches the NanoPing release it was made from. Changes made here
|
|
10
|
+
are overwritten by the next release.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install nanoping-api
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Use
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from nanoping_api import NanopingClient
|
|
22
|
+
|
|
23
|
+
client = NanopingClient("http://127.0.0.1:10565")
|
|
24
|
+
|
|
25
|
+
local = client.hub_client.get_local_node_id()
|
|
26
|
+
for update in client.hub_client.stream_connection_state():
|
|
27
|
+
print(update.state)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`AsyncNanopingClient` has the same methods for asyncio, and its streams are
|
|
31
|
+
read with `async for`. Requests and responses are dataclasses, and the fields of
|
|
32
|
+
a request are passed as keyword arguments. A failed call raises an `ApiError`,
|
|
33
|
+
whose `code` says why, such as `Code.NOT_FOUND`.
|
|
34
|
+
|
|
35
|
+
## Examples
|
|
36
|
+
|
|
37
|
+
`examples` holds the program the
|
|
38
|
+
[HTTP REST guide](https://docs.nanoping.com/api/http-rest) is made from. It
|
|
39
|
+
joins a node to a hub and manages pipelines on both. Start the two nodes as the
|
|
40
|
+
comment in `examples/main.py` describes, then run
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install nanoping-api
|
|
44
|
+
python examples/main.py --client http://127.0.0.1:10565 --server http://127.0.0.1:10566
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The API is documented at https://docs.nanoping.com/api/reference.
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""The code of the HTTP REST guide in the NanoPing docs. Each step of the guide is
|
|
2
|
+
one function, and run runs them in order."""
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from nanoping_api import (
|
|
9
|
+
ApiError,
|
|
10
|
+
AsyncNanopingClient,
|
|
11
|
+
AsyncServerStream,
|
|
12
|
+
Code,
|
|
13
|
+
HubNode,
|
|
14
|
+
Level,
|
|
15
|
+
NodeMetadata,
|
|
16
|
+
NodeMetadataItem,
|
|
17
|
+
NodeService,
|
|
18
|
+
RestartPolicy,
|
|
19
|
+
RestartPolicyOnFailure,
|
|
20
|
+
StreamPipelinesEvent,
|
|
21
|
+
)
|
|
22
|
+
from pipeline_config import PIPELINE_CONFIG
|
|
23
|
+
|
|
24
|
+
Output = Callable[[str], None]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class Addresses:
|
|
29
|
+
"""Addresses of the two nodes the guide uses: a client node that joins the hub,
|
|
30
|
+
and the node hosting the hub server."""
|
|
31
|
+
|
|
32
|
+
client: str
|
|
33
|
+
"""REST API of the client node, e.g. "http://127.0.0.1:10565"."""
|
|
34
|
+
server: str
|
|
35
|
+
"""REST API of the hub server node, e.g. "http://127.0.0.1:10566"."""
|
|
36
|
+
server_http: str
|
|
37
|
+
"""HTTP address of the hub server node, e.g. "http://127.0.0.1:8769"."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def set_node_information(client: AsyncNanopingClient, out: Output) -> None:
|
|
41
|
+
"""Names the client node and gives it metadata that other nodes on the hub can
|
|
42
|
+
filter on."""
|
|
43
|
+
local = await client.hub_client.get_local_node_id()
|
|
44
|
+
|
|
45
|
+
response = await client.hub_client.set_node(
|
|
46
|
+
node_id=local.node_id,
|
|
47
|
+
name="My NanoPing Client",
|
|
48
|
+
metadata=NodeMetadata(
|
|
49
|
+
items={
|
|
50
|
+
"type": NodeMetadataItem(string_value="my-type"),
|
|
51
|
+
"location": NodeMetadataItem(string_value="Denmark, Aalborg"),
|
|
52
|
+
}
|
|
53
|
+
),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
out(f'Named node {response.node.id} "{response.node.name}"')
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def watch_connection_state(client: AsyncNanopingClient, out: Output) -> None:
|
|
60
|
+
"""Prints the connection state of the client node every time it changes, until
|
|
61
|
+
the task is cancelled."""
|
|
62
|
+
async for update in client.hub_client.stream_connection_state():
|
|
63
|
+
out(f"Connection state: {update.state}")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
async def accept_join_requests(server: AsyncNanopingClient) -> None:
|
|
67
|
+
"""Accepts every node that asks to join the hub server, until the task is
|
|
68
|
+
cancelled."""
|
|
69
|
+
await server.answer_join_requests(lambda request: True)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
async def join_the_hub(client: AsyncNanopingClient, server_http_address: str, out: Output) -> None:
|
|
73
|
+
"""Joins the client node to the hub server. The call waits until the hub server
|
|
74
|
+
accepted or rejected the request. A node that already is part of the hub stays
|
|
75
|
+
so."""
|
|
76
|
+
try:
|
|
77
|
+
await client.join_hub(server_http_address)
|
|
78
|
+
out("Joined the hub")
|
|
79
|
+
except ApiError as error:
|
|
80
|
+
if error.code == Code.ALREADY_EXISTS:
|
|
81
|
+
out("Already part of the hub")
|
|
82
|
+
return
|
|
83
|
+
if error.code == Code.UNAUTHENTICATED:
|
|
84
|
+
raise RuntimeError("The hub server rejected the node") from error
|
|
85
|
+
raise
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
async def print_nodes(client: AsyncNanopingClient, out: Output) -> None:
|
|
89
|
+
"""Prints the nodes on the hub: all of them, the ones with metadata "type" set
|
|
90
|
+
to "my-type", and the ones running pipelines."""
|
|
91
|
+
everything = await client.hub_server.get_nodes()
|
|
92
|
+
out("All nodes:")
|
|
93
|
+
for node in everything.nodes:
|
|
94
|
+
out(f" {node.name}")
|
|
95
|
+
|
|
96
|
+
my_type = await client.hub_server.get_nodes(
|
|
97
|
+
metadata_filters={"type": NodeMetadataItem(string_value="my-type")},
|
|
98
|
+
)
|
|
99
|
+
out("Nodes with type my-type:")
|
|
100
|
+
for node in my_type.nodes:
|
|
101
|
+
out(f" {node.name}")
|
|
102
|
+
|
|
103
|
+
out("Nodes running pipelines:")
|
|
104
|
+
for node in await pipeline_nodes(client):
|
|
105
|
+
out(f" {node.name}")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
async def pipeline_nodes(client: AsyncNanopingClient) -> list[HubNode]:
|
|
109
|
+
"""Returns the nodes on the hub that run pipelines."""
|
|
110
|
+
response = await client.hub_server.get_nodes(service_filters=[NodeService.PIPELINES])
|
|
111
|
+
return response.nodes
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
async def watch_pipelines(
|
|
115
|
+
client: AsyncNanopingClient, node: HubNode
|
|
116
|
+
) -> AsyncServerStream[StreamPipelinesEvent]:
|
|
117
|
+
"""Opens a stream of the changes to the pipelines on the node. It returns once
|
|
118
|
+
the stream is ready, so no change made after it returns is missed."""
|
|
119
|
+
return await client.pipelines.stream_pipelines(node_id=node.id)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
async def print_pipeline_events(
|
|
123
|
+
events: AsyncServerStream[StreamPipelinesEvent], node: HubNode, out: Output
|
|
124
|
+
) -> None:
|
|
125
|
+
"""Prints an event every time a pipeline on the node is created, updated or
|
|
126
|
+
deleted, until the task is cancelled."""
|
|
127
|
+
async for event in events:
|
|
128
|
+
if event.create:
|
|
129
|
+
out(f' Event on {node.name}: created "{event.create.pipeline.name}"')
|
|
130
|
+
elif event.update:
|
|
131
|
+
out(f' Event on {node.name}: updated "{event.update.pipeline.name}"')
|
|
132
|
+
elif event.delete:
|
|
133
|
+
out(f' Event on {node.name}: deleted "{event.delete.pipeline.name}"')
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
async def manage_pipeline(client: AsyncNanopingClient, node_id: str, out: Output) -> None:
|
|
137
|
+
"""Creates a pipeline on the node, renames it, reads it back and deletes it
|
|
138
|
+
again."""
|
|
139
|
+
created = await client.pipelines.create_pipeline(
|
|
140
|
+
node_id=node_id,
|
|
141
|
+
name="My First Pipeline",
|
|
142
|
+
json_config=PIPELINE_CONFIG,
|
|
143
|
+
restart_policy=RestartPolicy(on_failure=RestartPolicyOnFailure(max_restarts=3)),
|
|
144
|
+
default_logging_level=Level.DEBUG,
|
|
145
|
+
)
|
|
146
|
+
pipeline = created.pipeline
|
|
147
|
+
out(f' Created "{pipeline.name}"')
|
|
148
|
+
|
|
149
|
+
# An update replaces every setting, so send the current ones along with the
|
|
150
|
+
# new name.
|
|
151
|
+
updated = await client.pipelines.update_pipeline(
|
|
152
|
+
node_id=node_id,
|
|
153
|
+
id=pipeline.id,
|
|
154
|
+
name="My Renamed Pipeline",
|
|
155
|
+
json_config=pipeline.json_config,
|
|
156
|
+
restart_policy=pipeline.restart_policy,
|
|
157
|
+
startup_policy=pipeline.startup_policy,
|
|
158
|
+
default_logging_level=pipeline.default_logging_level,
|
|
159
|
+
instructions_timeout=pipeline.instructions_timeout,
|
|
160
|
+
)
|
|
161
|
+
out(f' Renamed to "{updated.pipeline.name}"')
|
|
162
|
+
|
|
163
|
+
fetched = await client.pipelines.get_pipeline_by_id(node_id=node_id, id=pipeline.id)
|
|
164
|
+
out(f' Read back "{fetched.pipeline.name}"')
|
|
165
|
+
|
|
166
|
+
await client.pipelines.delete_pipeline(node_id=node_id, id=pipeline.id)
|
|
167
|
+
out(f' Deleted "{fetched.pipeline.name}"')
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
async def run(addresses: Addresses, out: Output) -> None:
|
|
171
|
+
"""Connects to both nodes and runs every step of the guide."""
|
|
172
|
+
async with (
|
|
173
|
+
AsyncNanopingClient(addresses.client) as client,
|
|
174
|
+
AsyncNanopingClient(addresses.server) as server,
|
|
175
|
+
):
|
|
176
|
+
background: list[asyncio.Task[None]] = []
|
|
177
|
+
try:
|
|
178
|
+
background.append(asyncio.create_task(watch_connection_state(client, out)))
|
|
179
|
+
await set_node_information(client, out)
|
|
180
|
+
|
|
181
|
+
background.append(asyncio.create_task(accept_join_requests(server)))
|
|
182
|
+
await join_the_hub(client, addresses.server_http, out)
|
|
183
|
+
|
|
184
|
+
await print_nodes(client, out)
|
|
185
|
+
|
|
186
|
+
for node in await pipeline_nodes(client):
|
|
187
|
+
out(f"Managing a pipeline on {node.name}")
|
|
188
|
+
events = await watch_pipelines(client, node)
|
|
189
|
+
background.append(asyncio.create_task(print_pipeline_events(events, node, out)))
|
|
190
|
+
await manage_pipeline(client, node.id, out)
|
|
191
|
+
finally:
|
|
192
|
+
for task in background:
|
|
193
|
+
task.cancel()
|
|
194
|
+
await asyncio.gather(*background, return_exceptions=True)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Runs the HTTP REST guide of the NanoPing docs against two nodes.
|
|
2
|
+
#
|
|
3
|
+
# Start the client node with
|
|
4
|
+
#
|
|
5
|
+
# np up --rest-api 127.0.0.1:10565
|
|
6
|
+
#
|
|
7
|
+
# and the hub server node with `np -c config.yaml up`, where config.yaml is
|
|
8
|
+
#
|
|
9
|
+
# node:
|
|
10
|
+
# name: My Server
|
|
11
|
+
# http:
|
|
12
|
+
# address: 127.0.0.1:8769
|
|
13
|
+
# hubServer:
|
|
14
|
+
# authenticationByRequest:
|
|
15
|
+
# timeout: 5m
|
|
16
|
+
# restApi:
|
|
17
|
+
# address: 127.0.0.1:10566
|
|
18
|
+
# pipelines: {}
|
|
19
|
+
#
|
|
20
|
+
# Then run `python main.py`.
|
|
21
|
+
import argparse
|
|
22
|
+
import asyncio
|
|
23
|
+
import sys
|
|
24
|
+
|
|
25
|
+
from guide import Addresses, run
|
|
26
|
+
|
|
27
|
+
parser = argparse.ArgumentParser()
|
|
28
|
+
parser.add_argument("--client", default="http://127.0.0.1:10565")
|
|
29
|
+
parser.add_argument("--server", default="http://127.0.0.1:10566")
|
|
30
|
+
parser.add_argument("--server-http", default="http://127.0.0.1:8769")
|
|
31
|
+
arguments = parser.parse_args()
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
asyncio.run(run(Addresses(arguments.client, arguments.server, arguments.server_http), print))
|
|
35
|
+
except Exception as error:
|
|
36
|
+
print(error, file=sys.stderr)
|
|
37
|
+
sys.exit(1)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""A pipeline that sends traffic from a traffic source to a traffic sink on the
|
|
2
|
+
same node."""
|
|
3
|
+
|
|
4
|
+
PIPELINE_CONFIG = """{
|
|
5
|
+
"version": 13,
|
|
6
|
+
"plumr_config": {
|
|
7
|
+
"pipeline": {
|
|
8
|
+
"traffic_sink-1": {
|
|
9
|
+
"traffic_sink": {
|
|
10
|
+
"input": "[traffic_sink-1|in:0]-[uniform_traffic_source-1|out:0]",
|
|
11
|
+
"mtu": 1500
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"uniform_traffic_source-1": {
|
|
15
|
+
"uniform_traffic_source": {
|
|
16
|
+
"output": "[traffic_sink-1|in:0]-[uniform_traffic_source-1|out:0]",
|
|
17
|
+
"total_packets": 5000,
|
|
18
|
+
"interval": 25
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}"""
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "nanoping-api"
|
|
7
|
+
version = "11.1.0.dev0"
|
|
8
|
+
description = "Typed client for the NanoPing REST API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "LicenseRef-Proprietary"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"httpx>=0.27",
|
|
14
|
+
"websockets>=14",
|
|
15
|
+
]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Framework :: AsyncIO",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Typing :: Typed",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://nanoping.com"
|
|
24
|
+
Documentation = "https://docs.nanoping.com/api/reference"
|
|
25
|
+
Source = "https://github.com/nanoping-labs/nanoping-api-python"
|
|
26
|
+
|
|
27
|
+
[tool.hatch.build.targets.wheel]
|
|
28
|
+
packages = ["src/nanoping_api"]
|
|
29
|
+
|
|
30
|
+
[tool.mypy]
|
|
31
|
+
strict = true
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""A typed client for the REST API of a NanoPing node."""
|
|
2
|
+
|
|
3
|
+
from ._client import AsyncNanopingClient, NanopingClient
|
|
4
|
+
from ._generated import *
|
|
5
|
+
from ._generated import __all__ as _generated_all
|
|
6
|
+
from ._runtime import ApiError, AsyncBidiStream, AsyncServerStream, BidiStream, Code, ServerStream
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"ApiError",
|
|
10
|
+
"AsyncBidiStream",
|
|
11
|
+
"AsyncNanopingClient",
|
|
12
|
+
"AsyncServerStream",
|
|
13
|
+
"BidiStream",
|
|
14
|
+
"Code",
|
|
15
|
+
"NanopingClient",
|
|
16
|
+
"ServerStream",
|
|
17
|
+
*_generated_all,
|
|
18
|
+
]
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from collections.abc import AsyncIterator, Awaitable, Callable, Iterator, Mapping
|
|
5
|
+
from typing import Self
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from ._generated import (
|
|
10
|
+
AuthenticationByRequestAnswer,
|
|
11
|
+
AuthenticationByRequestRequest,
|
|
12
|
+
Message,
|
|
13
|
+
Options,
|
|
14
|
+
_AsyncServices,
|
|
15
|
+
_Services,
|
|
16
|
+
)
|
|
17
|
+
from ._runtime import AsyncTransport, Transport
|
|
18
|
+
|
|
19
|
+
_EVERY_LINE = Options(lines=2**63 - 1)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class NanopingClient(_Services):
|
|
23
|
+
"""A blocking client for the REST API of a node.
|
|
24
|
+
|
|
25
|
+
`base_url` is the address of the REST API, such as "http://127.0.0.1:10565",
|
|
26
|
+
and `headers` are sent with every request. `timeout` limits each call, in
|
|
27
|
+
seconds, but not the wait for the next message of a stream. By default a call
|
|
28
|
+
waits as long as it takes. Pass `http_client` to send the calls with an
|
|
29
|
+
httpx.Client of your own.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
base_url: str,
|
|
35
|
+
*,
|
|
36
|
+
headers: Mapping[str, str] | None = None,
|
|
37
|
+
timeout: float | None = None,
|
|
38
|
+
http_client: httpx.Client | None = None,
|
|
39
|
+
) -> None:
|
|
40
|
+
self._transport = Transport(base_url, headers, timeout, http_client)
|
|
41
|
+
super().__init__(self._transport)
|
|
42
|
+
|
|
43
|
+
def close(self) -> None:
|
|
44
|
+
"""Closes the connections of the client, unless they belong to `http_client`."""
|
|
45
|
+
self._transport.close()
|
|
46
|
+
|
|
47
|
+
def __enter__(self) -> Self:
|
|
48
|
+
return self
|
|
49
|
+
|
|
50
|
+
def __exit__(self, *_: object) -> None:
|
|
51
|
+
self.close()
|
|
52
|
+
|
|
53
|
+
def join_hub(self, hub_server_address: str) -> None:
|
|
54
|
+
"""Joins the hub server at `hub_server_address` with the node hosting the API.
|
|
55
|
+
|
|
56
|
+
Returns once the hub server approved the node. Raises an ApiError with
|
|
57
|
+
Code.UNAUTHENTICATED when the request was rejected, Code.DEADLINE_EXCEEDED
|
|
58
|
+
when nobody answered in time and Code.ALREADY_EXISTS when the node already
|
|
59
|
+
is part of a hub.
|
|
60
|
+
"""
|
|
61
|
+
self.hub_client.authenticate_by_request(hub_server_address=hub_server_address)
|
|
62
|
+
|
|
63
|
+
def answer_join_requests(self, decide: Callable[[AuthenticationByRequestRequest], bool]) -> None:
|
|
64
|
+
"""Answers the nodes that ask to join the hub server hosted by the node hosting
|
|
65
|
+
the API.
|
|
66
|
+
|
|
67
|
+
`decide` is called for every request and returns whether the node may join.
|
|
68
|
+
Runs until the stream ends or fails.
|
|
69
|
+
"""
|
|
70
|
+
with self.hub_server.authentication_by_requests() as stream:
|
|
71
|
+
for request in stream:
|
|
72
|
+
stream.send(AuthenticationByRequestAnswer(request_id=request.request_id, accept=decide(request)))
|
|
73
|
+
|
|
74
|
+
def run_pipeline(self, node_id: str, pipeline_id: str) -> Iterator[Message]:
|
|
75
|
+
"""Starts a pipeline and follows the log of the new run from its first line.
|
|
76
|
+
|
|
77
|
+
The pipeline starts when the loop does. Yields each log message until the
|
|
78
|
+
log stream ends or the loop stops, which closes the stream.
|
|
79
|
+
"""
|
|
80
|
+
run = self.pipelines.start_pipeline(node_id=node_id, id=pipeline_id)
|
|
81
|
+
with self.pipelines.open_log_stream(node_id=node_id, run_id=run.run_id, options=_EVERY_LINE) as stream:
|
|
82
|
+
for batch in stream:
|
|
83
|
+
yield from batch.messages
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class AsyncNanopingClient(_AsyncServices):
|
|
87
|
+
"""An asyncio client for the REST API of a node.
|
|
88
|
+
|
|
89
|
+
`base_url` is the address of the REST API, such as "http://127.0.0.1:10565",
|
|
90
|
+
and `headers` are sent with every request. `timeout` limits each call, in
|
|
91
|
+
seconds, but not the wait for the next message of a stream. By default a call
|
|
92
|
+
waits as long as it takes. Pass `http_client` to send the calls with an
|
|
93
|
+
httpx.AsyncClient of your own.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def __init__(
|
|
97
|
+
self,
|
|
98
|
+
base_url: str,
|
|
99
|
+
*,
|
|
100
|
+
headers: Mapping[str, str] | None = None,
|
|
101
|
+
timeout: float | None = None,
|
|
102
|
+
http_client: httpx.AsyncClient | None = None,
|
|
103
|
+
) -> None:
|
|
104
|
+
self._transport = AsyncTransport(base_url, headers, timeout, http_client)
|
|
105
|
+
super().__init__(self._transport)
|
|
106
|
+
|
|
107
|
+
async def aclose(self) -> None:
|
|
108
|
+
"""Closes the connections of the client, unless they belong to `http_client`."""
|
|
109
|
+
await self._transport.aclose()
|
|
110
|
+
|
|
111
|
+
async def __aenter__(self) -> Self:
|
|
112
|
+
return self
|
|
113
|
+
|
|
114
|
+
async def __aexit__(self, *_: object) -> None:
|
|
115
|
+
await self.aclose()
|
|
116
|
+
|
|
117
|
+
async def join_hub(self, hub_server_address: str) -> None:
|
|
118
|
+
"""Joins the hub server at `hub_server_address` with the node hosting the API.
|
|
119
|
+
|
|
120
|
+
Returns once the hub server approved the node. Raises an ApiError with
|
|
121
|
+
Code.UNAUTHENTICATED when the request was rejected, Code.DEADLINE_EXCEEDED
|
|
122
|
+
when nobody answered in time and Code.ALREADY_EXISTS when the node already
|
|
123
|
+
is part of a hub.
|
|
124
|
+
"""
|
|
125
|
+
await self.hub_client.authenticate_by_request(hub_server_address=hub_server_address)
|
|
126
|
+
|
|
127
|
+
async def answer_join_requests(
|
|
128
|
+
self, decide: Callable[[AuthenticationByRequestRequest], bool | Awaitable[bool]]
|
|
129
|
+
) -> None:
|
|
130
|
+
"""Answers the nodes that ask to join the hub server hosted by the node hosting
|
|
131
|
+
the API.
|
|
132
|
+
|
|
133
|
+
`decide` is called for every request and returns, or resolves to, whether
|
|
134
|
+
the node may join. Runs until the stream ends or fails, or the task is
|
|
135
|
+
cancelled.
|
|
136
|
+
"""
|
|
137
|
+
async with self.hub_server.authentication_by_requests() as stream:
|
|
138
|
+
async for request in stream:
|
|
139
|
+
accept = decide(request)
|
|
140
|
+
if inspect.isawaitable(accept):
|
|
141
|
+
accept = await accept
|
|
142
|
+
await stream.send(AuthenticationByRequestAnswer(request_id=request.request_id, accept=accept))
|
|
143
|
+
|
|
144
|
+
async def run_pipeline(self, node_id: str, pipeline_id: str) -> AsyncIterator[Message]:
|
|
145
|
+
"""Starts a pipeline and follows the log of the new run from its first line.
|
|
146
|
+
|
|
147
|
+
The pipeline starts when the loop does. Yields each log message until the
|
|
148
|
+
log stream ends or the loop stops, which closes the stream.
|
|
149
|
+
"""
|
|
150
|
+
run = await self.pipelines.start_pipeline(node_id=node_id, id=pipeline_id)
|
|
151
|
+
async with self.pipelines.open_log_stream(node_id=node_id, run_id=run.run_id, options=_EVERY_LINE) as stream:
|
|
152
|
+
async for batch in stream:
|
|
153
|
+
for message in batch.messages:
|
|
154
|
+
yield message
|