hivemind-http-protocol 0.0.2a2__tar.gz → 0.0.3a1__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.
- hivemind_http_protocol-0.0.3a1/PKG-INFO +24 -0
- hivemind_http_protocol-0.0.3a1/README.md +121 -0
- {hivemind_http_protocol-0.0.2a2 → hivemind_http_protocol-0.0.3a1}/hivemind_http_protocol/__init__.py +1 -3
- hivemind_http_protocol-0.0.3a1/hivemind_http_protocol/version.py +8 -0
- hivemind_http_protocol-0.0.3a1/hivemind_http_protocol.egg-info/PKG-INFO +24 -0
- {hivemind_http_protocol-0.0.2a2 → hivemind_http_protocol-0.0.3a1}/hivemind_http_protocol.egg-info/SOURCES.txt +5 -2
- hivemind_http_protocol-0.0.3a1/hivemind_http_protocol.egg-info/requires.txt +15 -0
- hivemind_http_protocol-0.0.3a1/pyproject.toml +45 -0
- hivemind_http_protocol-0.0.3a1/tests/test_decode_auth.py +61 -0
- hivemind_http_protocol-0.0.3a1/tests/test_handlers.py +547 -0
- hivemind_http_protocol-0.0.3a1/tests/test_protocol_unit.py +138 -0
- hivemind_http_protocol-0.0.2a2/PKG-INFO +0 -20
- hivemind_http_protocol-0.0.2a2/README.md +0 -158
- hivemind_http_protocol-0.0.2a2/hivemind_http_protocol/version.py +0 -6
- hivemind_http_protocol-0.0.2a2/hivemind_http_protocol.egg-info/PKG-INFO +0 -20
- hivemind_http_protocol-0.0.2a2/hivemind_http_protocol.egg-info/requires.txt +0 -4
- hivemind_http_protocol-0.0.2a2/setup.py +0 -55
- {hivemind_http_protocol-0.0.2a2 → hivemind_http_protocol-0.0.3a1}/LICENSE.md +0 -0
- {hivemind_http_protocol-0.0.2a2 → hivemind_http_protocol-0.0.3a1}/hivemind_http_protocol.egg-info/dependency_links.txt +0 -0
- {hivemind_http_protocol-0.0.2a2 → hivemind_http_protocol-0.0.3a1}/hivemind_http_protocol.egg-info/entry_points.txt +0 -0
- {hivemind_http_protocol-0.0.2a2 → hivemind_http_protocol-0.0.3a1}/hivemind_http_protocol.egg-info/top_level.txt +0 -0
- {hivemind_http_protocol-0.0.2a2 → hivemind_http_protocol-0.0.3a1}/setup.cfg +0 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hivemind-http-protocol
|
|
3
|
+
Version: 0.0.3a1
|
|
4
|
+
Summary: http network protocol for hivemind-core
|
|
5
|
+
Author-email: jarbasAi <jarbasai@mailfence.com>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/JarbasHiveMind/hivemind-http-protocol
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
License-File: LICENSE.md
|
|
10
|
+
Requires-Dist: tornado
|
|
11
|
+
Requires-Dist: hivemind-plugin-manager
|
|
12
|
+
Requires-Dist: poorman_handshake>=0.1.0
|
|
13
|
+
Requires-Dist: pyOpenSSL
|
|
14
|
+
Requires-Dist: pybase64
|
|
15
|
+
Requires-Dist: hivemind-bus-client
|
|
16
|
+
Provides-Extra: test
|
|
17
|
+
Requires-Dist: pytest; extra == "test"
|
|
18
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
19
|
+
Requires-Dist: hivemind-core; extra == "test"
|
|
20
|
+
Requires-Dist: hivemind-bus-client; extra == "test"
|
|
21
|
+
Requires-Dist: ovos-bus-client; extra == "test"
|
|
22
|
+
Requires-Dist: ovos-utils; extra == "test"
|
|
23
|
+
Requires-Dist: hivescope; extra == "test"
|
|
24
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# hivemind-http-protocol
|
|
2
|
+
|
|
3
|
+
REST/HTTP transport plugin for [hivemind-core](https://github.com/JarbasHiveMind/HiveMind-core).
|
|
4
|
+
|
|
5
|
+
An alternative to the default WebSocket transport. Clients use HTTP polling (POST to send,
|
|
6
|
+
GET to receive) instead of a persistent WebSocket connection. Suitable for environments where
|
|
7
|
+
long-lived TCP connections are not possible (firewalls, IoT gateways, HTTP-only proxies).
|
|
8
|
+
|
|
9
|
+
## Where it fits
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
hivemind-core
|
|
13
|
+
└── hivemind-plugin-manager (NetworkProtocolFactory loads plugins by entry-point)
|
|
14
|
+
└── hivemind-http-protocol ← this repo
|
|
15
|
+
└── Tornado HTTP server (REST endpoints)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The plugin registers under the `hivemind.network.protocol` entry-point group as
|
|
19
|
+
`hivemind-http-plugin`. It can run alongside the WebSocket transport if both are listed
|
|
20
|
+
in the `network_protocol` config.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install hivemind-http-protocol
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quickstart
|
|
29
|
+
|
|
30
|
+
Add to `~/.config/hivemind-core/server.json`:
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"network_protocol": {
|
|
35
|
+
"module": "hivemind-http-plugin",
|
|
36
|
+
"hivemind-http-plugin": {
|
|
37
|
+
"host": "0.0.0.0",
|
|
38
|
+
"port": 5679
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Start hivemind-core:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
hivemind-core listen
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Running alongside WebSocket
|
|
51
|
+
|
|
52
|
+
Both transports can run at the same time by configuring them both:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"network_protocol": {
|
|
57
|
+
"hivemind-websocket-plugin": {
|
|
58
|
+
"host": "0.0.0.0",
|
|
59
|
+
"port": 5678
|
|
60
|
+
},
|
|
61
|
+
"hivemind-http-plugin": {
|
|
62
|
+
"host": "0.0.0.0",
|
|
63
|
+
"port": 5679
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Python client example
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from hivemind_bus_client.http_client import HiveMindHTTPClient, BinaryDataCallbacks
|
|
73
|
+
from hivemind_bus_client.message import HiveMessage, HiveMessageType
|
|
74
|
+
from ovos_bus_client.message import Message
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class MyBinaryCallbacks(BinaryDataCallbacks):
|
|
78
|
+
def handle_receive_tts(self, bin_data: bytes, utterance: str,
|
|
79
|
+
lang: str, file_name: str):
|
|
80
|
+
print(f"received {len(bin_data)} bytes of TTS for: {utterance}")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
client = HiveMindHTTPClient(
|
|
84
|
+
host="http://localhost",
|
|
85
|
+
port=5679,
|
|
86
|
+
bin_callbacks=MyBinaryCallbacks(),
|
|
87
|
+
)
|
|
88
|
+
client.emit(HiveMessage(HiveMessageType.BUS,
|
|
89
|
+
Message("speak:synth", {"utterance": "hello world"})))
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Configuration reference
|
|
93
|
+
|
|
94
|
+
| Key | Default | Description |
|
|
95
|
+
|---|---|---|
|
|
96
|
+
| `host` | `0.0.0.0` | Bind address. |
|
|
97
|
+
| `port` | `5679` | Listen port. |
|
|
98
|
+
| `ssl` | `false` | Enable TLS. |
|
|
99
|
+
| `cert_dir` | `$XDG_DATA_HOME/hivemind` | Directory for TLS cert/key files. |
|
|
100
|
+
| `cert_name` | `hivemind` | Base filename for cert and key. |
|
|
101
|
+
|
|
102
|
+
## REST API
|
|
103
|
+
|
|
104
|
+
Authentication uses an HTTP `authorization` parameter (not a header) containing
|
|
105
|
+
a Base64-encoded `useragent:access_key` string.
|
|
106
|
+
|
|
107
|
+
| Endpoint | Method | Description |
|
|
108
|
+
|---|---|---|
|
|
109
|
+
| `/connect` | POST | Register a client session. Parameters: `authorization`. |
|
|
110
|
+
| `/disconnect` | POST | Remove a client session. Parameters: `authorization`. |
|
|
111
|
+
| `/send_message` | POST | Send a HiveMessage. Parameters: `authorization`, `message`. |
|
|
112
|
+
| `/get_messages` | GET | Poll for pending text messages. Parameters: `authorization`. |
|
|
113
|
+
| `/get_binary_messages` | GET | Poll for pending binary messages (Base64-encoded). Parameters: `authorization`. |
|
|
114
|
+
|
|
115
|
+
See [docs/api.md](docs/api.md) for full endpoint documentation.
|
|
116
|
+
|
|
117
|
+
## Docs
|
|
118
|
+
|
|
119
|
+
- [docs/api.md](docs/api.md) — REST endpoint reference
|
|
120
|
+
- [docs/architecture.md](docs/architecture.md) — handler lifecycle, polling model, TLS
|
|
121
|
+
- [docs/operations.md](docs/operations.md) — authoring a transport plugin
|
{hivemind_http_protocol-0.0.2a2 → hivemind_http_protocol-0.0.3a1}/hivemind_http_protocol/__init__.py
RENAMED
|
@@ -117,8 +117,7 @@ class HiveMindHttpProtocol(NetworkProtocol):
|
|
|
117
117
|
cert.gmtime_adj_notAfter(10 * 365 * 24 * 60 * 60)
|
|
118
118
|
cert.set_issuer(cert.get_subject())
|
|
119
119
|
cert.set_pubkey(k)
|
|
120
|
-
|
|
121
|
-
cert.sign(k, "sha1")
|
|
120
|
+
cert.sign(k, "sha256")
|
|
122
121
|
|
|
123
122
|
open(cert_path, "wb").write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
|
|
124
123
|
open(key_path, "wb").write(crypto.dump_privatekey(crypto.FILETYPE_PEM, k))
|
|
@@ -178,7 +177,6 @@ class HiveMindHttpHandler(web.RequestHandler):
|
|
|
178
177
|
|
|
179
178
|
client.name = f"{useragent}::{user.client_id}::{user.name}"
|
|
180
179
|
client.crypto_key = user.crypto_key
|
|
181
|
-
client.msg_blacklist = user.message_blacklist or []
|
|
182
180
|
client.skill_blacklist = user.skill_blacklist or []
|
|
183
181
|
client.intent_blacklist = user.intent_blacklist or []
|
|
184
182
|
client.allowed_types = user.allowed_types
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hivemind-http-protocol
|
|
3
|
+
Version: 0.0.3a1
|
|
4
|
+
Summary: http network protocol for hivemind-core
|
|
5
|
+
Author-email: jarbasAi <jarbasai@mailfence.com>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/JarbasHiveMind/hivemind-http-protocol
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
License-File: LICENSE.md
|
|
10
|
+
Requires-Dist: tornado
|
|
11
|
+
Requires-Dist: hivemind-plugin-manager
|
|
12
|
+
Requires-Dist: poorman_handshake>=0.1.0
|
|
13
|
+
Requires-Dist: pyOpenSSL
|
|
14
|
+
Requires-Dist: pybase64
|
|
15
|
+
Requires-Dist: hivemind-bus-client
|
|
16
|
+
Provides-Extra: test
|
|
17
|
+
Requires-Dist: pytest; extra == "test"
|
|
18
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
19
|
+
Requires-Dist: hivemind-core; extra == "test"
|
|
20
|
+
Requires-Dist: hivemind-bus-client; extra == "test"
|
|
21
|
+
Requires-Dist: ovos-bus-client; extra == "test"
|
|
22
|
+
Requires-Dist: ovos-utils; extra == "test"
|
|
23
|
+
Requires-Dist: hivescope; extra == "test"
|
|
24
|
+
Dynamic: license-file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
LICENSE.md
|
|
2
2
|
README.md
|
|
3
|
-
|
|
3
|
+
pyproject.toml
|
|
4
4
|
hivemind_http_protocol/__init__.py
|
|
5
5
|
hivemind_http_protocol/version.py
|
|
6
6
|
hivemind_http_protocol.egg-info/PKG-INFO
|
|
@@ -8,4 +8,7 @@ hivemind_http_protocol.egg-info/SOURCES.txt
|
|
|
8
8
|
hivemind_http_protocol.egg-info/dependency_links.txt
|
|
9
9
|
hivemind_http_protocol.egg-info/entry_points.txt
|
|
10
10
|
hivemind_http_protocol.egg-info/requires.txt
|
|
11
|
-
hivemind_http_protocol.egg-info/top_level.txt
|
|
11
|
+
hivemind_http_protocol.egg-info/top_level.txt
|
|
12
|
+
tests/test_decode_auth.py
|
|
13
|
+
tests/test_handlers.py
|
|
14
|
+
tests/test_protocol_unit.py
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "hivemind-http-protocol"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "http network protocol for hivemind-core"
|
|
9
|
+
license = "Apache-2.0"
|
|
10
|
+
authors = [
|
|
11
|
+
{ name = "jarbasAi", email = "jarbasai@mailfence.com" }
|
|
12
|
+
]
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
|
+
dependencies = [
|
|
15
|
+
"tornado",
|
|
16
|
+
"hivemind-plugin-manager",
|
|
17
|
+
"poorman_handshake>=0.1.0",
|
|
18
|
+
"pyOpenSSL",
|
|
19
|
+
"pybase64",
|
|
20
|
+
"hivemind-bus-client",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
test = [
|
|
25
|
+
"pytest",
|
|
26
|
+
"pytest-cov",
|
|
27
|
+
"hivemind-core",
|
|
28
|
+
"hivemind-bus-client",
|
|
29
|
+
"ovos-bus-client",
|
|
30
|
+
"ovos-utils",
|
|
31
|
+
"hivescope",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://github.com/JarbasHiveMind/hivemind-http-protocol"
|
|
36
|
+
|
|
37
|
+
[project.entry-points."hivemind.network.protocol"]
|
|
38
|
+
"hivemind-http-plugin" = "hivemind_http_protocol:HiveMindHttpProtocol"
|
|
39
|
+
|
|
40
|
+
[tool.setuptools.dynamic]
|
|
41
|
+
version = { attr = "hivemind_http_protocol.version.__version__" }
|
|
42
|
+
|
|
43
|
+
[tool.setuptools.packages.find]
|
|
44
|
+
where = ["."]
|
|
45
|
+
include = ["hivemind_http_protocol*"]
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Unit tests for HiveMindHttpHandler.decode_auth.
|
|
2
|
+
|
|
3
|
+
decode_auth parses a base64-encoded "useragent:key" string passed as the
|
|
4
|
+
`authorization` query parameter. Valid input splits on the first colon;
|
|
5
|
+
everything to the left is the useragent, everything to the right is the
|
|
6
|
+
API key. When the parameter is absent or malformed the method returns
|
|
7
|
+
(None, None) with a 400 status set.
|
|
8
|
+
"""
|
|
9
|
+
import pybase64
|
|
10
|
+
import pytest
|
|
11
|
+
from unittest.mock import patch, MagicMock
|
|
12
|
+
|
|
13
|
+
from hivemind_http_protocol import HiveMindHttpHandler
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _encode(s: str) -> str:
|
|
17
|
+
return pybase64.b64encode(s.encode("utf-8")).decode("ascii")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _make_handler(auth_value: str):
|
|
21
|
+
"""Return a HiveMindHttpHandler instance whose get_argument() returns auth_value."""
|
|
22
|
+
handler = HiveMindHttpHandler.__new__(HiveMindHttpHandler)
|
|
23
|
+
handler.get_argument = MagicMock(return_value=auth_value)
|
|
24
|
+
handler.set_status = MagicMock()
|
|
25
|
+
return handler
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TestDecodeAuthValid:
|
|
29
|
+
def test_simple_useragent_and_key(self):
|
|
30
|
+
h = _make_handler(_encode("myagent:secretkey"))
|
|
31
|
+
useragent, key = h.decode_auth()
|
|
32
|
+
assert useragent == "myagent"
|
|
33
|
+
assert key == "secretkey"
|
|
34
|
+
|
|
35
|
+
def test_key_with_colon_splits_on_first(self):
|
|
36
|
+
# The current implementation splits on all colons and returns
|
|
37
|
+
# a list — the caller is expected to consume index 0 and 1.
|
|
38
|
+
h = _make_handler(_encode("agent:secretkey"))
|
|
39
|
+
result = h.decode_auth()
|
|
40
|
+
assert result[0] == "agent"
|
|
41
|
+
assert result[1] == "secretkey"
|
|
42
|
+
|
|
43
|
+
def test_unicode_values(self):
|
|
44
|
+
h = _make_handler(_encode("ünïcødé:kéy"))
|
|
45
|
+
useragent, key = h.decode_auth()
|
|
46
|
+
assert useragent == "ünïcødé"
|
|
47
|
+
assert key == "kéy"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class TestDecodeAuthMissing:
|
|
51
|
+
def test_empty_auth_returns_none_pair(self):
|
|
52
|
+
h = _make_handler("")
|
|
53
|
+
result = h.decode_auth()
|
|
54
|
+
assert result == (None, None)
|
|
55
|
+
h.set_status.assert_called_once_with(400)
|
|
56
|
+
|
|
57
|
+
def test_none_value_treated_as_missing(self):
|
|
58
|
+
h = _make_handler(None)
|
|
59
|
+
result = h.decode_auth()
|
|
60
|
+
assert result == (None, None)
|
|
61
|
+
h.set_status.assert_called_once_with(400)
|
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
"""Unit tests for HiveMindHttpHandler and all derived handler classes.
|
|
2
|
+
|
|
3
|
+
Covers lines 147-347 (get_client, ConnectHandler, DisconnectHandler,
|
|
4
|
+
SendMessageHandler, GetMessagesHandler, GetBinMessagesHandler).
|
|
5
|
+
|
|
6
|
+
Uses a real MasterNode from hivescope (so HiveMindClientConnection gets a
|
|
7
|
+
valid identity/private-key path) but patches the DB and protocol callbacks
|
|
8
|
+
to keep tests deterministic and side-effect-free.
|
|
9
|
+
"""
|
|
10
|
+
import asyncio
|
|
11
|
+
from collections import defaultdict
|
|
12
|
+
from queue import Queue
|
|
13
|
+
from unittest.mock import MagicMock, patch, call
|
|
14
|
+
|
|
15
|
+
import pybase64
|
|
16
|
+
import pytest
|
|
17
|
+
|
|
18
|
+
from hivemind_http_protocol import (
|
|
19
|
+
HiveMindHttpHandler,
|
|
20
|
+
ConnectHandler,
|
|
21
|
+
DisconnectHandler,
|
|
22
|
+
SendMessageHandler,
|
|
23
|
+
GetMessagesHandler,
|
|
24
|
+
GetBinMessagesHandler,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# Module-level fixtures
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
@pytest.fixture(scope="module")
|
|
33
|
+
def master():
|
|
34
|
+
"""One MasterNode reused across all tests in this module."""
|
|
35
|
+
hivescope = pytest.importorskip("hivescope")
|
|
36
|
+
from hivescope.node import MasterNode
|
|
37
|
+
return MasterNode.create("HH", require_crypto=False, handshake_enabled=True)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _encode(s: str) -> str:
|
|
41
|
+
return pybase64.b64encode(s.encode("utf-8")).decode("ascii")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _make_user(
|
|
45
|
+
*,
|
|
46
|
+
client_id=42,
|
|
47
|
+
name="testclient",
|
|
48
|
+
crypto_key="cryptokey",
|
|
49
|
+
skill_blacklist=None,
|
|
50
|
+
intent_blacklist=None,
|
|
51
|
+
allowed_types=None,
|
|
52
|
+
can_propagate=True,
|
|
53
|
+
can_escalate=True,
|
|
54
|
+
is_admin=False,
|
|
55
|
+
password=None,
|
|
56
|
+
):
|
|
57
|
+
u = MagicMock()
|
|
58
|
+
u.client_id = client_id
|
|
59
|
+
u.name = name
|
|
60
|
+
u.crypto_key = crypto_key
|
|
61
|
+
u.skill_blacklist = skill_blacklist
|
|
62
|
+
u.intent_blacklist = intent_blacklist
|
|
63
|
+
u.allowed_types = allowed_types or []
|
|
64
|
+
u.can_propagate = can_propagate
|
|
65
|
+
u.can_escalate = can_escalate
|
|
66
|
+
u.is_admin = is_admin
|
|
67
|
+
u.password = password
|
|
68
|
+
return u
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _clean_class_state():
|
|
72
|
+
"""Reset class-level dicts to prevent test bleed."""
|
|
73
|
+
HiveMindHttpHandler.clients = {}
|
|
74
|
+
HiveMindHttpHandler.undelivered = defaultdict(Queue)
|
|
75
|
+
HiveMindHttpHandler.undelivered_bin = defaultdict(Queue)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _make_handler(cls, auth_value, proto, *, extra_get_arg=None):
|
|
79
|
+
"""Return a handler instance bypassing Tornado's __init__."""
|
|
80
|
+
_clean_class_state()
|
|
81
|
+
cls.hm_protocol = proto
|
|
82
|
+
|
|
83
|
+
h = cls.__new__(cls)
|
|
84
|
+
h.set_status = MagicMock()
|
|
85
|
+
h.write = MagicMock()
|
|
86
|
+
|
|
87
|
+
def _get_arg(name, default=""):
|
|
88
|
+
if name == "authorization":
|
|
89
|
+
return auth_value
|
|
90
|
+
if extra_get_arg and name in extra_get_arg:
|
|
91
|
+
return extra_get_arg[name]
|
|
92
|
+
return default
|
|
93
|
+
|
|
94
|
+
h.get_argument = MagicMock(side_effect=_get_arg)
|
|
95
|
+
return h
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _run(coro):
|
|
99
|
+
return asyncio.get_event_loop().run_until_complete(coro)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# ---------------------------------------------------------------------------
|
|
103
|
+
# get_client
|
|
104
|
+
# ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
class TestGetClient:
|
|
107
|
+
def test_returns_cached_client(self, master):
|
|
108
|
+
proto = master.hm_protocol
|
|
109
|
+
_clean_class_state()
|
|
110
|
+
HiveMindHttpHandler.hm_protocol = proto
|
|
111
|
+
|
|
112
|
+
existing = MagicMock()
|
|
113
|
+
HiveMindHttpHandler.clients["mykey"] = existing
|
|
114
|
+
|
|
115
|
+
h = HiveMindHttpHandler.__new__(HiveMindHttpHandler)
|
|
116
|
+
result = h.get_client("agent", "mykey", cache=True)
|
|
117
|
+
assert result is existing
|
|
118
|
+
|
|
119
|
+
def test_invalid_key_returns_none(self, master):
|
|
120
|
+
proto = master.hm_protocol
|
|
121
|
+
_clean_class_state()
|
|
122
|
+
HiveMindHttpHandler.hm_protocol = proto
|
|
123
|
+
|
|
124
|
+
with patch.object(proto.db, "get_client_by_api_key", return_value=None):
|
|
125
|
+
with patch.object(proto, "handle_invalid_key_connected") as mock_invalid:
|
|
126
|
+
h = HiveMindHttpHandler.__new__(HiveMindHttpHandler)
|
|
127
|
+
result = h.get_client("agent", "badkey", cache=False)
|
|
128
|
+
assert result is None
|
|
129
|
+
mock_invalid.assert_called_once()
|
|
130
|
+
|
|
131
|
+
def test_valid_key_builds_client(self, master):
|
|
132
|
+
proto = master.hm_protocol
|
|
133
|
+
_clean_class_state()
|
|
134
|
+
HiveMindHttpHandler.hm_protocol = proto
|
|
135
|
+
|
|
136
|
+
user = _make_user()
|
|
137
|
+
with patch.object(proto.db, "get_client_by_api_key", return_value=user):
|
|
138
|
+
h = HiveMindHttpHandler.__new__(HiveMindHttpHandler)
|
|
139
|
+
result = h.get_client("agent", "validkey", cache=False)
|
|
140
|
+
assert result is not None
|
|
141
|
+
assert result.crypto_key == user.crypto_key
|
|
142
|
+
assert result.is_admin == user.is_admin
|
|
143
|
+
assert result.can_propagate == user.can_propagate
|
|
144
|
+
|
|
145
|
+
def test_valid_key_caches_client(self, master):
|
|
146
|
+
proto = master.hm_protocol
|
|
147
|
+
_clean_class_state()
|
|
148
|
+
HiveMindHttpHandler.hm_protocol = proto
|
|
149
|
+
|
|
150
|
+
user = _make_user()
|
|
151
|
+
with patch.object(proto.db, "get_client_by_api_key", return_value=user):
|
|
152
|
+
h = HiveMindHttpHandler.__new__(HiveMindHttpHandler)
|
|
153
|
+
result = h.get_client("agent", "cachekey", cache=True)
|
|
154
|
+
assert HiveMindHttpHandler.clients.get("cachekey") is result
|
|
155
|
+
|
|
156
|
+
def test_no_cache_does_not_store(self, master):
|
|
157
|
+
proto = master.hm_protocol
|
|
158
|
+
_clean_class_state()
|
|
159
|
+
HiveMindHttpHandler.hm_protocol = proto
|
|
160
|
+
|
|
161
|
+
user = _make_user()
|
|
162
|
+
with patch.object(proto.db, "get_client_by_api_key", return_value=user):
|
|
163
|
+
h = HiveMindHttpHandler.__new__(HiveMindHttpHandler)
|
|
164
|
+
h.get_client("agent", "nocachekey", cache=False)
|
|
165
|
+
assert "nocachekey" not in HiveMindHttpHandler.clients
|
|
166
|
+
|
|
167
|
+
def test_user_with_password_sets_handshake(self, master):
|
|
168
|
+
proto = master.hm_protocol
|
|
169
|
+
_clean_class_state()
|
|
170
|
+
HiveMindHttpHandler.hm_protocol = proto
|
|
171
|
+
|
|
172
|
+
user = _make_user(password="hunter2")
|
|
173
|
+
with patch.object(proto.db, "get_client_by_api_key", return_value=user):
|
|
174
|
+
h = HiveMindHttpHandler.__new__(HiveMindHttpHandler)
|
|
175
|
+
result = h.get_client("agent", "pwdkey", cache=False)
|
|
176
|
+
assert result is not None
|
|
177
|
+
assert result.pswd_handshake is not None
|
|
178
|
+
|
|
179
|
+
def test_user_without_password_no_handshake(self, master):
|
|
180
|
+
proto = master.hm_protocol
|
|
181
|
+
_clean_class_state()
|
|
182
|
+
HiveMindHttpHandler.hm_protocol = proto
|
|
183
|
+
|
|
184
|
+
user = _make_user(password=None)
|
|
185
|
+
with patch.object(proto.db, "get_client_by_api_key", return_value=user):
|
|
186
|
+
h = HiveMindHttpHandler.__new__(HiveMindHttpHandler)
|
|
187
|
+
result = h.get_client("agent", "nopwdkey", cache=False)
|
|
188
|
+
assert result is not None
|
|
189
|
+
assert not getattr(result, "pswd_handshake", None)
|
|
190
|
+
|
|
191
|
+
def test_do_send_text_goes_to_undelivered(self, master):
|
|
192
|
+
proto = master.hm_protocol
|
|
193
|
+
_clean_class_state()
|
|
194
|
+
HiveMindHttpHandler.hm_protocol = proto
|
|
195
|
+
|
|
196
|
+
user = _make_user()
|
|
197
|
+
with patch.object(proto.db, "get_client_by_api_key", return_value=user):
|
|
198
|
+
h = HiveMindHttpHandler.__new__(HiveMindHttpHandler)
|
|
199
|
+
client = h.get_client("agent", "sendkey", cache=False)
|
|
200
|
+
client.send_msg("hello", is_bin=False)
|
|
201
|
+
assert not HiveMindHttpHandler.undelivered["sendkey"].empty()
|
|
202
|
+
assert HiveMindHttpHandler.undelivered["sendkey"].get() == "hello"
|
|
203
|
+
|
|
204
|
+
def test_do_send_binary_goes_to_undelivered_bin(self, master):
|
|
205
|
+
proto = master.hm_protocol
|
|
206
|
+
_clean_class_state()
|
|
207
|
+
HiveMindHttpHandler.hm_protocol = proto
|
|
208
|
+
|
|
209
|
+
user = _make_user()
|
|
210
|
+
with patch.object(proto.db, "get_client_by_api_key", return_value=user):
|
|
211
|
+
h = HiveMindHttpHandler.__new__(HiveMindHttpHandler)
|
|
212
|
+
client = h.get_client("agent", "binkey", cache=False)
|
|
213
|
+
client.send_msg(b"\x00\x01", is_bin=True)
|
|
214
|
+
assert not HiveMindHttpHandler.undelivered_bin["binkey"].empty()
|
|
215
|
+
|
|
216
|
+
def test_do_disconnect_removes_client_and_queue(self, master):
|
|
217
|
+
proto = master.hm_protocol
|
|
218
|
+
_clean_class_state()
|
|
219
|
+
HiveMindHttpHandler.hm_protocol = proto
|
|
220
|
+
|
|
221
|
+
user = _make_user()
|
|
222
|
+
with patch.object(proto.db, "get_client_by_api_key", return_value=user):
|
|
223
|
+
h = HiveMindHttpHandler.__new__(HiveMindHttpHandler)
|
|
224
|
+
client = h.get_client("agent", "disckey", cache=True)
|
|
225
|
+
HiveMindHttpHandler.undelivered["disckey"].put("msg")
|
|
226
|
+
client.disconnect()
|
|
227
|
+
assert "disckey" not in HiveMindHttpHandler.clients
|
|
228
|
+
assert "disckey" not in HiveMindHttpHandler.undelivered
|
|
229
|
+
|
|
230
|
+
def test_skill_blacklist_none_defaults_to_empty(self, master):
|
|
231
|
+
proto = master.hm_protocol
|
|
232
|
+
_clean_class_state()
|
|
233
|
+
HiveMindHttpHandler.hm_protocol = proto
|
|
234
|
+
|
|
235
|
+
user = _make_user(skill_blacklist=None)
|
|
236
|
+
with patch.object(proto.db, "get_client_by_api_key", return_value=user):
|
|
237
|
+
h = HiveMindHttpHandler.__new__(HiveMindHttpHandler)
|
|
238
|
+
client = h.get_client("agent", "blkey", cache=False)
|
|
239
|
+
assert client.skill_blacklist == []
|
|
240
|
+
|
|
241
|
+
def test_intent_blacklist_none_defaults_to_empty(self, master):
|
|
242
|
+
proto = master.hm_protocol
|
|
243
|
+
_clean_class_state()
|
|
244
|
+
HiveMindHttpHandler.hm_protocol = proto
|
|
245
|
+
|
|
246
|
+
user = _make_user(intent_blacklist=None)
|
|
247
|
+
with patch.object(proto.db, "get_client_by_api_key", return_value=user):
|
|
248
|
+
h = HiveMindHttpHandler.__new__(HiveMindHttpHandler)
|
|
249
|
+
client = h.get_client("agent", "iblkey", cache=False)
|
|
250
|
+
assert client.intent_blacklist == []
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
# ---------------------------------------------------------------------------
|
|
254
|
+
# ConnectHandler
|
|
255
|
+
# ---------------------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
class TestConnectHandler:
|
|
258
|
+
def test_missing_auth_returns_error(self, master):
|
|
259
|
+
proto = master.hm_protocol
|
|
260
|
+
h = _make_handler(ConnectHandler, "", proto)
|
|
261
|
+
_run(h.post())
|
|
262
|
+
h.write.assert_called_with({"error": "Missing authorization"})
|
|
263
|
+
|
|
264
|
+
def test_invalid_key_does_not_call_handle_new_client(self, master):
|
|
265
|
+
proto = master.hm_protocol
|
|
266
|
+
with patch.object(proto.db, "get_client_by_api_key", return_value=None):
|
|
267
|
+
with patch.object(proto, "handle_invalid_key_connected"):
|
|
268
|
+
with patch.object(proto, "handle_new_client") as mock_new:
|
|
269
|
+
h = _make_handler(ConnectHandler, _encode("agent:badkey"), proto)
|
|
270
|
+
_run(h.post())
|
|
271
|
+
mock_new.assert_not_called()
|
|
272
|
+
|
|
273
|
+
def test_valid_connect_calls_handle_new_client(self, master):
|
|
274
|
+
proto = master.hm_protocol
|
|
275
|
+
user = _make_user()
|
|
276
|
+
with patch.object(proto.db, "get_client_by_api_key", return_value=user):
|
|
277
|
+
with patch.object(proto, "handle_new_client") as mock_new:
|
|
278
|
+
h = _make_handler(ConnectHandler, _encode("agent:validkey"), proto)
|
|
279
|
+
_run(h.post())
|
|
280
|
+
mock_new.assert_called_once()
|
|
281
|
+
h.write.assert_called_with({"status": "Connected"})
|
|
282
|
+
|
|
283
|
+
def test_no_crypto_key_handshake_disabled_require_crypto_triggers_invalid_protocol(self, master):
|
|
284
|
+
proto = master.hm_protocol
|
|
285
|
+
user = _make_user(crypto_key=None)
|
|
286
|
+
with patch.object(proto.db, "get_client_by_api_key", return_value=user):
|
|
287
|
+
with patch.object(proto, "handle_invalid_protocol_version") as mock_ipv:
|
|
288
|
+
with patch.object(proto, "handle_new_client") as mock_new:
|
|
289
|
+
with patch.object(type(proto), "handshake_enabled",
|
|
290
|
+
new_callable=lambda: property(lambda s: False)):
|
|
291
|
+
with patch.object(type(proto), "require_crypto",
|
|
292
|
+
new_callable=lambda: property(lambda s: True)):
|
|
293
|
+
h = _make_handler(ConnectHandler, _encode("agent:key"), proto)
|
|
294
|
+
_run(h.post())
|
|
295
|
+
mock_ipv.assert_called_once()
|
|
296
|
+
mock_new.assert_not_called()
|
|
297
|
+
|
|
298
|
+
def test_connect_exception_returns_500(self, master):
|
|
299
|
+
proto = master.hm_protocol
|
|
300
|
+
with patch.object(proto.db, "sync", side_effect=RuntimeError("boom")):
|
|
301
|
+
h = _make_handler(ConnectHandler, _encode("agent:key"), proto)
|
|
302
|
+
_run(h.post())
|
|
303
|
+
h.set_status.assert_called_with(500)
|
|
304
|
+
h.write.assert_called_with({"error": "Connection failed"})
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# ---------------------------------------------------------------------------
|
|
308
|
+
# DisconnectHandler
|
|
309
|
+
# ---------------------------------------------------------------------------
|
|
310
|
+
|
|
311
|
+
class TestDisconnectHandler:
|
|
312
|
+
def test_missing_auth_returns_error(self, master):
|
|
313
|
+
proto = master.hm_protocol
|
|
314
|
+
h = _make_handler(DisconnectHandler, "", proto)
|
|
315
|
+
_run(h.post())
|
|
316
|
+
h.write.assert_called_with({"error": "Missing authorization"})
|
|
317
|
+
|
|
318
|
+
def test_not_connected_returns_already_disconnected(self, master):
|
|
319
|
+
proto = master.hm_protocol
|
|
320
|
+
h = _make_handler(DisconnectHandler, _encode("agent:key"), proto)
|
|
321
|
+
# clients is empty after _clean_class_state
|
|
322
|
+
_run(h.post())
|
|
323
|
+
h.write.assert_called_with({"error": "Already Disconnected"})
|
|
324
|
+
|
|
325
|
+
def test_connected_client_is_disconnected(self, master):
|
|
326
|
+
proto = master.hm_protocol
|
|
327
|
+
user = _make_user()
|
|
328
|
+
with patch.object(proto.db, "get_client_by_api_key", return_value=user):
|
|
329
|
+
with patch.object(proto, "handle_client_disconnected") as mock_disc:
|
|
330
|
+
h = _make_handler(DisconnectHandler, _encode("agent:disckey"), proto)
|
|
331
|
+
mock_client = MagicMock()
|
|
332
|
+
DisconnectHandler.clients["disckey"] = mock_client
|
|
333
|
+
_run(h.post())
|
|
334
|
+
mock_disc.assert_called_once()
|
|
335
|
+
h.write.assert_called_with({"status": "Disconnected"})
|
|
336
|
+
|
|
337
|
+
def test_disconnect_exception_returns_500(self, master):
|
|
338
|
+
proto = master.hm_protocol
|
|
339
|
+
user = _make_user()
|
|
340
|
+
with patch.object(proto.db, "get_client_by_api_key", return_value=user):
|
|
341
|
+
with patch.object(proto, "handle_client_disconnected",
|
|
342
|
+
side_effect=RuntimeError("fail")):
|
|
343
|
+
h = _make_handler(DisconnectHandler, _encode("agent:errkey"), proto)
|
|
344
|
+
DisconnectHandler.clients["errkey"] = MagicMock()
|
|
345
|
+
_run(h.post())
|
|
346
|
+
h.set_status.assert_called_with(500)
|
|
347
|
+
h.write.assert_called_with({"error": "Disconnection failed"})
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
# ---------------------------------------------------------------------------
|
|
351
|
+
# SendMessageHandler
|
|
352
|
+
# ---------------------------------------------------------------------------
|
|
353
|
+
|
|
354
|
+
class TestSendMessageHandler:
|
|
355
|
+
def test_missing_auth_returns_error(self, master):
|
|
356
|
+
proto = master.hm_protocol
|
|
357
|
+
h = _make_handler(SendMessageHandler, "", proto)
|
|
358
|
+
_run(h.post())
|
|
359
|
+
h.write.assert_called_with({"error": "Missing authorization"})
|
|
360
|
+
|
|
361
|
+
def test_not_connected_returns_error(self, master):
|
|
362
|
+
proto = master.hm_protocol
|
|
363
|
+
h = _make_handler(SendMessageHandler, _encode("agent:key"), proto)
|
|
364
|
+
# clients empty
|
|
365
|
+
_run(h.post())
|
|
366
|
+
h.write.assert_called_with({"error": "Client is not connected"})
|
|
367
|
+
|
|
368
|
+
def test_missing_message_returns_400(self, master):
|
|
369
|
+
proto = master.hm_protocol
|
|
370
|
+
user = _make_user()
|
|
371
|
+
with patch.object(proto.db, "get_client_by_api_key", return_value=user):
|
|
372
|
+
h = _make_handler(SendMessageHandler, _encode("agent:msgkey"), proto,
|
|
373
|
+
extra_get_arg={"message": ""})
|
|
374
|
+
SendMessageHandler.clients["msgkey"] = MagicMock()
|
|
375
|
+
_run(h.post())
|
|
376
|
+
h.set_status.assert_called_with(400)
|
|
377
|
+
h.write.assert_called_with({"error": "Missing message"})
|
|
378
|
+
|
|
379
|
+
def test_valid_message_dispatched(self, master):
|
|
380
|
+
proto = master.hm_protocol
|
|
381
|
+
user = _make_user()
|
|
382
|
+
|
|
383
|
+
from hivemind_bus_client.message import HiveMessage, HiveMessageType
|
|
384
|
+
from ovos_bus_client.message import Message
|
|
385
|
+
bus_msg = Message("test_msg", {})
|
|
386
|
+
hive_msg = HiveMessage(HiveMessageType.BUS, payload=bus_msg)
|
|
387
|
+
|
|
388
|
+
mock_client = MagicMock()
|
|
389
|
+
mock_client.decode.return_value = hive_msg
|
|
390
|
+
|
|
391
|
+
with patch.object(proto.db, "get_client_by_api_key", return_value=user):
|
|
392
|
+
with patch.object(proto, "handle_message") as mock_handle:
|
|
393
|
+
h = _make_handler(SendMessageHandler, _encode("agent:sendmsg"), proto,
|
|
394
|
+
extra_get_arg={"message": "encoded_payload"})
|
|
395
|
+
# inject mock client to avoid re-building from DB
|
|
396
|
+
SendMessageHandler.clients["sendmsg"] = mock_client
|
|
397
|
+
_run(h.post())
|
|
398
|
+
mock_handle.assert_called_once()
|
|
399
|
+
h.write.assert_called_with({"status": "message sent"})
|
|
400
|
+
|
|
401
|
+
def test_b64_audio_message_dispatched(self, master):
|
|
402
|
+
proto = master.hm_protocol
|
|
403
|
+
user = _make_user()
|
|
404
|
+
|
|
405
|
+
from hivemind_bus_client.message import HiveMessage, HiveMessageType
|
|
406
|
+
from ovos_bus_client.message import Message
|
|
407
|
+
bus_msg = Message("recognizer_loop:b64_audio", {})
|
|
408
|
+
hive_msg = HiveMessage(HiveMessageType.BUS, payload=bus_msg)
|
|
409
|
+
|
|
410
|
+
mock_client = MagicMock()
|
|
411
|
+
mock_client.decode.return_value = hive_msg
|
|
412
|
+
|
|
413
|
+
with patch.object(proto.db, "get_client_by_api_key", return_value=user):
|
|
414
|
+
with patch.object(proto, "handle_message") as mock_handle:
|
|
415
|
+
h = _make_handler(SendMessageHandler, _encode("agent:audiokey"), proto,
|
|
416
|
+
extra_get_arg={"message": "audio_encoded"})
|
|
417
|
+
SendMessageHandler.clients["audiokey"] = mock_client
|
|
418
|
+
_run(h.post())
|
|
419
|
+
mock_handle.assert_called_once()
|
|
420
|
+
h.write.assert_called_with({"status": "message sent"})
|
|
421
|
+
|
|
422
|
+
def test_send_exception_returns_500(self, master):
|
|
423
|
+
proto = master.hm_protocol
|
|
424
|
+
with patch.object(proto.db, "sync", side_effect=RuntimeError("boom")):
|
|
425
|
+
h = _make_handler(SendMessageHandler, _encode("agent:errkey"), proto,
|
|
426
|
+
extra_get_arg={"message": "msg"})
|
|
427
|
+
SendMessageHandler.clients["errkey"] = MagicMock()
|
|
428
|
+
_run(h.post())
|
|
429
|
+
h.set_status.assert_called_with(500)
|
|
430
|
+
h.write.assert_called_with({"error": "Message sending failed"})
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
# ---------------------------------------------------------------------------
|
|
434
|
+
# GetMessagesHandler
|
|
435
|
+
# ---------------------------------------------------------------------------
|
|
436
|
+
|
|
437
|
+
class TestGetMessagesHandler:
|
|
438
|
+
def test_missing_auth_returns_error(self, master):
|
|
439
|
+
proto = master.hm_protocol
|
|
440
|
+
h = _make_handler(GetMessagesHandler, "", proto)
|
|
441
|
+
_run(h.get())
|
|
442
|
+
h.write.assert_called_with({"error": "Missing authorization"})
|
|
443
|
+
|
|
444
|
+
def test_not_connected_returns_error(self, master):
|
|
445
|
+
proto = master.hm_protocol
|
|
446
|
+
h = _make_handler(GetMessagesHandler, _encode("agent:key"), proto)
|
|
447
|
+
_run(h.get())
|
|
448
|
+
h.write.assert_called_with({"error": "Client is not connected"})
|
|
449
|
+
|
|
450
|
+
def test_empty_queue_returns_empty_messages(self, master):
|
|
451
|
+
proto = master.hm_protocol
|
|
452
|
+
h = _make_handler(GetMessagesHandler, _encode("agent:emptykey"), proto)
|
|
453
|
+
GetMessagesHandler.clients["emptykey"] = MagicMock()
|
|
454
|
+
_run(h.get())
|
|
455
|
+
h.write.assert_called_with({"status": "messages retrieved", "messages": []})
|
|
456
|
+
|
|
457
|
+
def test_queued_messages_are_returned(self, master):
|
|
458
|
+
proto = master.hm_protocol
|
|
459
|
+
h = _make_handler(GetMessagesHandler, _encode("agent:qkey"), proto)
|
|
460
|
+
GetMessagesHandler.clients["qkey"] = MagicMock()
|
|
461
|
+
HiveMindHttpHandler.undelivered["qkey"].put("msg1")
|
|
462
|
+
HiveMindHttpHandler.undelivered["qkey"].put("msg2")
|
|
463
|
+
_run(h.get())
|
|
464
|
+
h.write.assert_called_with({"status": "messages retrieved", "messages": ["msg1", "msg2"]})
|
|
465
|
+
|
|
466
|
+
def test_get_messages_exception_returns_500(self, master):
|
|
467
|
+
proto = master.hm_protocol
|
|
468
|
+
h = _make_handler(GetMessagesHandler, _encode("agent:exckey"), proto)
|
|
469
|
+
GetMessagesHandler.clients["exckey"] = MagicMock()
|
|
470
|
+
with patch.object(HiveMindHttpHandler.undelivered["exckey"], "empty",
|
|
471
|
+
side_effect=RuntimeError("fail")):
|
|
472
|
+
_run(h.get())
|
|
473
|
+
h.set_status.assert_called_with(500)
|
|
474
|
+
h.write.assert_called_with({"error": "Retrieving messages failed"})
|
|
475
|
+
|
|
476
|
+
def test_get_nowait_exception_breaks_loop(self, master):
|
|
477
|
+
"""Inner except: get_nowait raises while queue reports non-empty."""
|
|
478
|
+
proto = master.hm_protocol
|
|
479
|
+
h = _make_handler(GetMessagesHandler, _encode("agent:getnowaitkey"), proto)
|
|
480
|
+
GetMessagesHandler.clients["getnowaitkey"] = MagicMock()
|
|
481
|
+
|
|
482
|
+
mock_queue = MagicMock()
|
|
483
|
+
mock_queue.empty.return_value = False
|
|
484
|
+
mock_queue.get_nowait.side_effect = RuntimeError("get_nowait fail")
|
|
485
|
+
HiveMindHttpHandler.undelivered["getnowaitkey"] = mock_queue
|
|
486
|
+
|
|
487
|
+
_run(h.get())
|
|
488
|
+
# The inner except breaks; we still write the (empty) messages list
|
|
489
|
+
h.write.assert_called_with({"status": "messages retrieved", "messages": []})
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
# ---------------------------------------------------------------------------
|
|
493
|
+
# GetBinMessagesHandler
|
|
494
|
+
# ---------------------------------------------------------------------------
|
|
495
|
+
|
|
496
|
+
class TestGetBinMessagesHandler:
|
|
497
|
+
def test_missing_auth_returns_error(self, master):
|
|
498
|
+
proto = master.hm_protocol
|
|
499
|
+
h = _make_handler(GetBinMessagesHandler, "", proto)
|
|
500
|
+
_run(h.get())
|
|
501
|
+
h.write.assert_called_with({"error": "Missing authorization"})
|
|
502
|
+
|
|
503
|
+
def test_not_connected_returns_error(self, master):
|
|
504
|
+
proto = master.hm_protocol
|
|
505
|
+
h = _make_handler(GetBinMessagesHandler, _encode("agent:key"), proto)
|
|
506
|
+
_run(h.get())
|
|
507
|
+
h.write.assert_called_with({"error": "Client is not connected"})
|
|
508
|
+
|
|
509
|
+
def test_empty_queue_returns_empty_messages(self, master):
|
|
510
|
+
proto = master.hm_protocol
|
|
511
|
+
h = _make_handler(GetBinMessagesHandler, _encode("agent:binempty"), proto)
|
|
512
|
+
GetBinMessagesHandler.clients["binempty"] = MagicMock()
|
|
513
|
+
_run(h.get())
|
|
514
|
+
h.write.assert_called_with({"status": "messages retrieved", "b64_messages": []})
|
|
515
|
+
|
|
516
|
+
def test_queued_bin_messages_are_returned(self, master):
|
|
517
|
+
proto = master.hm_protocol
|
|
518
|
+
h = _make_handler(GetBinMessagesHandler, _encode("agent:binq"), proto)
|
|
519
|
+
GetBinMessagesHandler.clients["binq"] = MagicMock()
|
|
520
|
+
HiveMindHttpHandler.undelivered_bin["binq"].put("b64data1")
|
|
521
|
+
HiveMindHttpHandler.undelivered_bin["binq"].put("b64data2")
|
|
522
|
+
_run(h.get())
|
|
523
|
+
h.write.assert_called_with({"status": "messages retrieved", "b64_messages": ["b64data1", "b64data2"]})
|
|
524
|
+
|
|
525
|
+
def test_get_bin_messages_exception_returns_500(self, master):
|
|
526
|
+
proto = master.hm_protocol
|
|
527
|
+
h = _make_handler(GetBinMessagesHandler, _encode("agent:binexc"), proto)
|
|
528
|
+
GetBinMessagesHandler.clients["binexc"] = MagicMock()
|
|
529
|
+
with patch.object(HiveMindHttpHandler.undelivered_bin["binexc"], "empty",
|
|
530
|
+
side_effect=RuntimeError("fail")):
|
|
531
|
+
_run(h.get())
|
|
532
|
+
h.set_status.assert_called_with(500)
|
|
533
|
+
h.write.assert_called_with({"error": "Retrieving messages failed"})
|
|
534
|
+
|
|
535
|
+
def test_get_nowait_exception_breaks_loop(self, master):
|
|
536
|
+
"""Inner except: get_nowait raises while queue reports non-empty."""
|
|
537
|
+
proto = master.hm_protocol
|
|
538
|
+
h = _make_handler(GetBinMessagesHandler, _encode("agent:binnowait"), proto)
|
|
539
|
+
GetBinMessagesHandler.clients["binnowait"] = MagicMock()
|
|
540
|
+
|
|
541
|
+
mock_queue = MagicMock()
|
|
542
|
+
mock_queue.empty.return_value = False
|
|
543
|
+
mock_queue.get_nowait.side_effect = RuntimeError("get_nowait fail")
|
|
544
|
+
HiveMindHttpHandler.undelivered_bin["binnowait"] = mock_queue
|
|
545
|
+
|
|
546
|
+
_run(h.get())
|
|
547
|
+
h.write.assert_called_with({"status": "messages retrieved", "b64_messages": []})
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Unit-level coverage for HiveMindHttpProtocol.
|
|
2
|
+
|
|
3
|
+
Targets:
|
|
4
|
+
- version.py module loading.
|
|
5
|
+
- create_self_signed_cert() certificate / key generation.
|
|
6
|
+
- run() lifecycle (plain and SSL paths).
|
|
7
|
+
"""
|
|
8
|
+
import os
|
|
9
|
+
import socket
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
|
|
16
|
+
from hivemind_http_protocol import HiveMindHttpProtocol
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# --- version.py -----------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
def test_version_module_exposes_constants_and_string():
|
|
22
|
+
from hivemind_http_protocol import version as v
|
|
23
|
+
assert isinstance(v.VERSION_MAJOR, int)
|
|
24
|
+
assert isinstance(v.VERSION_MINOR, int)
|
|
25
|
+
assert isinstance(v.VERSION_BUILD, int)
|
|
26
|
+
assert isinstance(v.VERSION_ALPHA, int)
|
|
27
|
+
assert isinstance(v.__version__, str)
|
|
28
|
+
assert v.__version__.startswith(
|
|
29
|
+
f"{v.VERSION_MAJOR}.{v.VERSION_MINOR}.{v.VERSION_BUILD}"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# --- self-signed cert generation ------------------------------------------
|
|
34
|
+
|
|
35
|
+
def test_create_self_signed_cert_writes_files(tmp_path):
|
|
36
|
+
cert, key = HiveMindHttpProtocol.create_self_signed_cert(
|
|
37
|
+
cert_dir=str(tmp_path), name="http-test"
|
|
38
|
+
)
|
|
39
|
+
assert Path(cert).exists()
|
|
40
|
+
assert Path(key).exists()
|
|
41
|
+
assert Path(cert).read_bytes().startswith(b"-----BEGIN CERTIFICATE-----")
|
|
42
|
+
assert b"PRIVATE KEY" in Path(key).read_bytes()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_create_self_signed_cert_idempotent(tmp_path):
|
|
46
|
+
c1, k1 = HiveMindHttpProtocol.create_self_signed_cert(
|
|
47
|
+
cert_dir=str(tmp_path), name="http-test"
|
|
48
|
+
)
|
|
49
|
+
mtime = os.path.getmtime(c1)
|
|
50
|
+
time.sleep(0.05)
|
|
51
|
+
c2, k2 = HiveMindHttpProtocol.create_self_signed_cert(
|
|
52
|
+
cert_dir=str(tmp_path), name="http-test"
|
|
53
|
+
)
|
|
54
|
+
assert c1 == c2 and k1 == k2
|
|
55
|
+
assert os.path.getmtime(c1) == mtime, "cert should not be rewritten"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# --- run() lifecycle -------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
def _free_port() -> int:
|
|
61
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
62
|
+
s.bind(("127.0.0.1", 0))
|
|
63
|
+
return s.getsockname()[1]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _make_mock_hm_protocol():
|
|
67
|
+
from unittest.mock import MagicMock
|
|
68
|
+
m = MagicMock()
|
|
69
|
+
m.handshake_enabled = True
|
|
70
|
+
m.require_crypto = False
|
|
71
|
+
m.clients = {}
|
|
72
|
+
return m
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _spawn_proto(proto, *, timeout=5.0):
|
|
76
|
+
"""Start proto.run() in a daemon thread; return when the port is reachable.
|
|
77
|
+
|
|
78
|
+
The thread is intentionally not stopped here — tests that only need to
|
|
79
|
+
check that the server binds can call this and let the daemon thread die
|
|
80
|
+
when the process exits. Returns True when the port accepted a connection
|
|
81
|
+
within *timeout* seconds, False otherwise.
|
|
82
|
+
"""
|
|
83
|
+
port = int(proto.config["port"])
|
|
84
|
+
host = proto.config.get("host", "127.0.0.1")
|
|
85
|
+
ready = threading.Event()
|
|
86
|
+
|
|
87
|
+
def _run():
|
|
88
|
+
import asyncio
|
|
89
|
+
from tornado.platform.asyncio import AnyThreadEventLoopPolicy
|
|
90
|
+
asyncio.set_event_loop_policy(AnyThreadEventLoopPolicy())
|
|
91
|
+
proto.run()
|
|
92
|
+
|
|
93
|
+
t = threading.Thread(target=_run, daemon=True)
|
|
94
|
+
t.start()
|
|
95
|
+
|
|
96
|
+
deadline = time.monotonic() + timeout
|
|
97
|
+
while time.monotonic() < deadline:
|
|
98
|
+
try:
|
|
99
|
+
s = socket.socket()
|
|
100
|
+
s.settimeout(0.3)
|
|
101
|
+
s.connect((host, port))
|
|
102
|
+
s.close()
|
|
103
|
+
return True
|
|
104
|
+
except OSError:
|
|
105
|
+
time.sleep(0.05)
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_run_starts_and_serves_on_plain_http():
|
|
110
|
+
"""run() actually binds a port and the HTTP server accepts connections."""
|
|
111
|
+
pytest.importorskip("hivescope")
|
|
112
|
+
from hivescope.node import MasterNode
|
|
113
|
+
master = MasterNode.create("MH", require_crypto=False, handshake_enabled=True)
|
|
114
|
+
port = _free_port()
|
|
115
|
+
proto = HiveMindHttpProtocol(
|
|
116
|
+
config={"host": "127.0.0.1", "port": port, "ssl": False},
|
|
117
|
+
hm_protocol=master.hm_protocol,
|
|
118
|
+
)
|
|
119
|
+
assert _spawn_proto(proto), "HTTP server never became reachable"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_run_ssl_generates_cert(tmp_path):
|
|
123
|
+
"""SSL branch: missing cert/key are generated then the server starts."""
|
|
124
|
+
pytest.importorskip("hivescope")
|
|
125
|
+
from hivescope.node import MasterNode
|
|
126
|
+
master = MasterNode.create("MH2", require_crypto=False, handshake_enabled=True)
|
|
127
|
+
port = _free_port()
|
|
128
|
+
proto = HiveMindHttpProtocol(
|
|
129
|
+
config={
|
|
130
|
+
"host": "127.0.0.1", "port": port, "ssl": True,
|
|
131
|
+
"cert_dir": str(tmp_path), "cert_name": "gen-http",
|
|
132
|
+
},
|
|
133
|
+
hm_protocol=master.hm_protocol,
|
|
134
|
+
)
|
|
135
|
+
_spawn_proto(proto, timeout=5.0)
|
|
136
|
+
# Cert files must exist regardless of whether the SSL port was reachable.
|
|
137
|
+
assert (tmp_path / "gen-http.crt").exists()
|
|
138
|
+
assert (tmp_path / "gen-http.key").exists()
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: hivemind-http-protocol
|
|
3
|
-
Version: 0.0.2a2
|
|
4
|
-
Summary: http network protocol for hivemind-core
|
|
5
|
-
Home-page: https://github.com/JarbasHiveMind/hivemind-http-protocol
|
|
6
|
-
Author: jarbasAi
|
|
7
|
-
Author-email: jarbasai@mailfence.com
|
|
8
|
-
License: Apache-2.0
|
|
9
|
-
License-File: LICENSE.md
|
|
10
|
-
Requires-Dist: tornado
|
|
11
|
-
Requires-Dist: hivemind-plugin-manager
|
|
12
|
-
Requires-Dist: poorman_handshake>=0.1.0
|
|
13
|
-
Requires-Dist: pyOpenSSL
|
|
14
|
-
Dynamic: author
|
|
15
|
-
Dynamic: author-email
|
|
16
|
-
Dynamic: home-page
|
|
17
|
-
Dynamic: license
|
|
18
|
-
Dynamic: license-file
|
|
19
|
-
Dynamic: requires-dist
|
|
20
|
-
Dynamic: summary
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
# HiveMind HTTP Protocol
|
|
2
|
-
|
|
3
|
-
The HiveMind HTTP Protocol provides an alternative REST-based implementation for message exchange in the HiveMind ecosystem.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
## Configuration
|
|
9
|
-
|
|
10
|
-
This plugin integrates with the `hivemind-core` framework. It is not a standalone project, and its behavior is controlled by the `hivemind-core` configuration.
|
|
11
|
-
|
|
12
|
-
To enable and configure the HiveMind HTTP Protocol, update the `network_protocol` entry in the `hivemind-core` configuration file. Below is an example configuration:
|
|
13
|
-
|
|
14
|
-
```json
|
|
15
|
-
"network_protocol": {
|
|
16
|
-
"hivemind-websocket-plugin": {
|
|
17
|
-
"host": "0.0.0.0",
|
|
18
|
-
"port": 5678
|
|
19
|
-
},
|
|
20
|
-
"hivemind-http-plugin": {
|
|
21
|
-
"host": "0.0.0.0",
|
|
22
|
-
"port": 5679
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
---
|
|
28
|
-
## Client Library
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
```python
|
|
32
|
-
from hivemind_bus_client.http_client import HiveMindHTTPClient, BinaryDataCallbacks
|
|
33
|
-
from hivemind_bus_client.message import HiveMessage, HiveMessageType
|
|
34
|
-
from ovos_bus_client.message import Message
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
class BinaryDataHandler(BinaryDataCallbacks):
|
|
38
|
-
def handle_receive_tts(self, bin_data: bytes,
|
|
39
|
-
utterance: str,
|
|
40
|
-
lang: str,
|
|
41
|
-
file_name: str):
|
|
42
|
-
# we can play it or save to file or whatever
|
|
43
|
-
print(f"got {len(bin_data)} bytes of TTS audio")
|
|
44
|
-
print(f"utterance: {utterance}", f"lang: {lang}", f"file_name: {file_name}")
|
|
45
|
-
# got 33836 bytes of TTS audio
|
|
46
|
-
# utterance: hello world lang: en-US file_name: 5eb63bbbe01eeed093cb22bb8f5acdc3.wav
|
|
47
|
-
|
|
48
|
-
# not passing key etc so it uses hivemind identity file for details
|
|
49
|
-
client = HiveMindHTTPClient(host="http://localhost", port=5679,
|
|
50
|
-
bin_callbacks=BinaryDataHandler())
|
|
51
|
-
|
|
52
|
-
client.emit(HiveMessage(HiveMessageType.BUS,
|
|
53
|
-
Message("speak:synth", {"utterance": "hello world"})))
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
---
|
|
57
|
-
|
|
58
|
-
## REST API Documentation
|
|
59
|
-
|
|
60
|
-
### Authentication
|
|
61
|
-
|
|
62
|
-
Authentication is handled via an HTTP `authorization` parameter in the request. The value should be a Base64-encoded string in the format `useragent:access_key`.
|
|
63
|
-
|
|
64
|
-
### Endpoints
|
|
65
|
-
|
|
66
|
-
#### 1. Connect to the Server
|
|
67
|
-
|
|
68
|
-
**Endpoint:** `/connect`
|
|
69
|
-
**Method:** `POST`
|
|
70
|
-
|
|
71
|
-
**Request Parameters:**
|
|
72
|
-
- `authorization` (string, mandatory): Base64-encoded `useragent:access_key`.
|
|
73
|
-
|
|
74
|
-
**Response:**
|
|
75
|
-
- `200 OK`: `{ "status": "Connected" }`
|
|
76
|
-
- `400 Bad Request`: `{ "error": "Missing authorization" }`
|
|
77
|
-
- `500 Internal Server Error`: `{ "error": "Connection failed" }`
|
|
78
|
-
|
|
79
|
-
---
|
|
80
|
-
|
|
81
|
-
#### 2. Disconnect from the Server
|
|
82
|
-
|
|
83
|
-
**Endpoint:** `/disconnect`
|
|
84
|
-
**Method:** `POST`
|
|
85
|
-
|
|
86
|
-
**Request Parameters:**
|
|
87
|
-
- `authorization` (string, mandatory): Base64-encoded `useragent:access_key`.
|
|
88
|
-
|
|
89
|
-
**Response:**
|
|
90
|
-
- `200 OK`: `{ "status": "Disconnected" }`
|
|
91
|
-
- `400 Bad Request`: `{ "error": "Missing authorization" }`
|
|
92
|
-
- `500 Internal Server Error`: `{ "error": "Disconnection failed" }`
|
|
93
|
-
|
|
94
|
-
---
|
|
95
|
-
|
|
96
|
-
#### 3. Send a Message
|
|
97
|
-
|
|
98
|
-
**Endpoint:** `/send_message`
|
|
99
|
-
**Method:** `POST`
|
|
100
|
-
|
|
101
|
-
**Request Parameters:**
|
|
102
|
-
- `authorization` (string, mandatory): Base64-encoded `useragent:access_key`.
|
|
103
|
-
- `message` (string, mandatory): Encoded message payload.
|
|
104
|
-
|
|
105
|
-
**Response:**
|
|
106
|
-
- `200 OK`: `{ "status": "message sent" }`
|
|
107
|
-
- `400 Bad Request`: `{ "error": "Missing message" }`
|
|
108
|
-
- `500 Internal Server Error`: `{ "error": "Message sending failed" }`
|
|
109
|
-
|
|
110
|
-
---
|
|
111
|
-
|
|
112
|
-
#### 4. Retrieve Messages
|
|
113
|
-
|
|
114
|
-
**Endpoint:** `/get_messages`
|
|
115
|
-
**Method:** `GET`
|
|
116
|
-
|
|
117
|
-
**Request Parameters:**
|
|
118
|
-
- `authorization` (string, mandatory): Base64-encoded `useragent:access_key`.
|
|
119
|
-
|
|
120
|
-
**Response:**
|
|
121
|
-
- `200 OK`: `{ "messages": ["message1", "message2"] }`
|
|
122
|
-
- `400 Bad Request`: `{ "error": "Missing authorization" }`
|
|
123
|
-
- `500 Internal Server Error`: `{ "error": "Failed to retrieve messages" }`
|
|
124
|
-
|
|
125
|
-
---
|
|
126
|
-
|
|
127
|
-
#### 5. Retrieve Binary Messages
|
|
128
|
-
|
|
129
|
-
**Endpoint:** `/get_binary_messages`
|
|
130
|
-
**Method:** `GET`
|
|
131
|
-
|
|
132
|
-
**Request Parameters:**
|
|
133
|
-
- `authorization` (string, mandatory): Base64-encoded `useragent:access_key`.
|
|
134
|
-
|
|
135
|
-
**Response:**
|
|
136
|
-
- `200 OK`: `{ "messages": ["Base64Message1", "Base64Message2"] }`
|
|
137
|
-
- `400 Bad Request`: `{ "error": "Missing authorization" }`
|
|
138
|
-
- `500 Internal Server Error`: `{ "error": "Failed to retrieve messages" }`
|
|
139
|
-
|
|
140
|
-
---
|
|
141
|
-
|
|
142
|
-
## Notes
|
|
143
|
-
|
|
144
|
-
- The `connect` and `disconnect` endpoints enable state management in scenarios where persistent connections are not feasible
|
|
145
|
-
- Binary messages are Base64-encoded to ensure compatibility with REST APIs, which are text-based protocols.
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
---
|
|
149
|
-
|
|
150
|
-
## Contributing
|
|
151
|
-
|
|
152
|
-
Contributions are welcome! Please submit a pull request or open an issue for bug reports or feature requests.
|
|
153
|
-
|
|
154
|
-
---
|
|
155
|
-
|
|
156
|
-
## License
|
|
157
|
-
|
|
158
|
-
This project is licensed under the Apache 2.0 License. See the `LICENSE` file for more details.
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: hivemind-http-protocol
|
|
3
|
-
Version: 0.0.2a2
|
|
4
|
-
Summary: http network protocol for hivemind-core
|
|
5
|
-
Home-page: https://github.com/JarbasHiveMind/hivemind-http-protocol
|
|
6
|
-
Author: jarbasAi
|
|
7
|
-
Author-email: jarbasai@mailfence.com
|
|
8
|
-
License: Apache-2.0
|
|
9
|
-
License-File: LICENSE.md
|
|
10
|
-
Requires-Dist: tornado
|
|
11
|
-
Requires-Dist: hivemind-plugin-manager
|
|
12
|
-
Requires-Dist: poorman_handshake>=0.1.0
|
|
13
|
-
Requires-Dist: pyOpenSSL
|
|
14
|
-
Dynamic: author
|
|
15
|
-
Dynamic: author-email
|
|
16
|
-
Dynamic: home-page
|
|
17
|
-
Dynamic: license
|
|
18
|
-
Dynamic: license-file
|
|
19
|
-
Dynamic: requires-dist
|
|
20
|
-
Dynamic: summary
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
from setuptools import setup
|
|
3
|
-
|
|
4
|
-
BASEDIR = os.path.abspath(os.path.dirname(__file__))
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def get_version():
|
|
8
|
-
""" Find the version of the package"""
|
|
9
|
-
version_file = os.path.join(BASEDIR, 'hivemind_http_protocol', 'version.py')
|
|
10
|
-
major, minor, build, alpha = (None, None, None, None)
|
|
11
|
-
with open(version_file) as f:
|
|
12
|
-
for line in f:
|
|
13
|
-
if 'VERSION_MAJOR' in line:
|
|
14
|
-
major = line.split('=')[1].strip()
|
|
15
|
-
elif 'VERSION_MINOR' in line:
|
|
16
|
-
minor = line.split('=')[1].strip()
|
|
17
|
-
elif 'VERSION_BUILD' in line:
|
|
18
|
-
build = line.split('=')[1].strip()
|
|
19
|
-
elif 'VERSION_ALPHA' in line:
|
|
20
|
-
alpha = line.split('=')[1].strip()
|
|
21
|
-
|
|
22
|
-
if ((major and minor and build and alpha) or
|
|
23
|
-
'# END_VERSION_BLOCK' in line):
|
|
24
|
-
break
|
|
25
|
-
version = f"{major}.{minor}.{build}"
|
|
26
|
-
if int(alpha) > 0:
|
|
27
|
-
version += f"a{alpha}"
|
|
28
|
-
return version
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def required(requirements_file):
|
|
32
|
-
""" Read requirements file and remove comments and empty lines. """
|
|
33
|
-
with open(os.path.join(BASEDIR, requirements_file), 'r') as f:
|
|
34
|
-
requirements = f.read().splitlines()
|
|
35
|
-
if 'MYCROFT_LOOSE_REQUIREMENTS' in os.environ:
|
|
36
|
-
print('USING LOOSE REQUIREMENTS!')
|
|
37
|
-
requirements = [r.replace('==', '>=').replace('~=', '>=') for r in requirements]
|
|
38
|
-
return [pkg for pkg in requirements
|
|
39
|
-
if pkg.strip() and not pkg.startswith("#")]
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
PLUGIN_ENTRY_POINT = 'hivemind-http-plugin=hivemind_http_protocol:HiveMindHttpProtocol'
|
|
43
|
-
|
|
44
|
-
setup(
|
|
45
|
-
name='hivemind-http-protocol',
|
|
46
|
-
version=get_version(),
|
|
47
|
-
packages=['hivemind_http_protocol'],
|
|
48
|
-
url='https://github.com/JarbasHiveMind/hivemind-http-protocol',
|
|
49
|
-
license='Apache-2.0',
|
|
50
|
-
author='jarbasAi',
|
|
51
|
-
install_requires=required("requirements.txt"),
|
|
52
|
-
entry_points={'hivemind.network.protocol': PLUGIN_ENTRY_POINT},
|
|
53
|
-
author_email='jarbasai@mailfence.com',
|
|
54
|
-
description='http network protocol for hivemind-core'
|
|
55
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|