easyhttp-python 0.3.3__tar.gz → 0.4.0a6__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.
- {easyhttp_python-0.3.3/easyhttp_python.egg-info → easyhttp_python-0.4.0a6}/PKG-INFO +15 -10
- {easyhttp_python-0.3.3 → easyhttp_python-0.4.0a6}/README.md +7 -11
- {easyhttp_python-0.3.3 → easyhttp_python-0.4.0a6}/README_PY.md +12 -8
- easyhttp_python-0.4.0a6/easyhttp_python/__init__.py +6 -0
- easyhttp_python-0.4.0a6/easyhttp_python/_discovery.py +145 -0
- {easyhttp_python-0.3.3 → easyhttp_python-0.4.0a6}/easyhttp_python/core.py +252 -180
- {easyhttp_python-0.3.3 → easyhttp_python-0.4.0a6}/easyhttp_python/wrapper.py +35 -29
- {easyhttp_python-0.3.3 → easyhttp_python-0.4.0a6/easyhttp_python.egg-info}/PKG-INFO +15 -10
- {easyhttp_python-0.3.3 → easyhttp_python-0.4.0a6}/easyhttp_python.egg-info/SOURCES.txt +1 -0
- {easyhttp_python-0.3.3 → easyhttp_python-0.4.0a6}/easyhttp_python.egg-info/requires.txt +1 -0
- {easyhttp_python-0.3.3 → easyhttp_python-0.4.0a6}/pyproject.toml +4 -3
- easyhttp_python-0.3.3/easyhttp_python/__init__.py +0 -9
- {easyhttp_python-0.3.3 → easyhttp_python-0.4.0a6}/LICENSE +0 -0
- {easyhttp_python-0.3.3 → easyhttp_python-0.4.0a6}/easyhttp_python.egg-info/dependency_links.txt +0 -0
- {easyhttp_python-0.3.3 → easyhttp_python-0.4.0a6}/easyhttp_python.egg-info/top_level.txt +0 -0
- {easyhttp_python-0.3.3 → easyhttp_python-0.4.0a6}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: easyhttp-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0a6
|
|
4
4
|
Summary: Simple HTTP-based P2P framework for IoT
|
|
5
5
|
Author-email: slpuk <yarik6052@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -9,7 +9,7 @@ Project-URL: Documentation, https://github.com/slpuk/easyhttp-python#readme
|
|
|
9
9
|
Project-URL: Repository, https://github.com/slpuk/easyhttp-python
|
|
10
10
|
Project-URL: Issue Tracker, https://github.com/slpuk/easyhttp-python/issues
|
|
11
11
|
Keywords: iot,p2p,http,framework
|
|
12
|
-
Classifier: Development Status ::
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
|
14
14
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
15
15
|
Classifier: Topic :: Communications
|
|
@@ -27,6 +27,7 @@ License-File: LICENSE
|
|
|
27
27
|
Requires-Dist: fastapi>=0.103.2
|
|
28
28
|
Requires-Dist: uvicorn[standard]>=0.22.0
|
|
29
29
|
Requires-Dist: aiohttp>=3.7.0
|
|
30
|
+
Requires-Dist: loggity>=0.5.0a6
|
|
30
31
|
Provides-Extra: dev
|
|
31
32
|
Requires-Dist: pytest>=6.0; extra == "dev"
|
|
32
33
|
Requires-Dist: black; extra == "dev"
|
|
@@ -36,15 +37,15 @@ Dynamic: license-file
|
|
|
36
37
|
# EasyHTTP
|
|
37
38
|
|
|
38
39
|
[](https://github.com/slpuk/easyhttp-python)
|
|
39
|
-

|
|
41
|
+

|
|
41
42
|

|
|
42
43
|

|
|
43
44
|
|
|
44
45
|
> **A lightweight HTTP-based P2P framework for IoT and device-to-device communication**
|
|
45
46
|
|
|
46
47
|
## 🛠️ Changelog
|
|
47
|
-
- Added
|
|
48
|
+
- Added UDP multicast discovery
|
|
48
49
|
- Fixed some bugs
|
|
49
50
|
|
|
50
51
|
## 📖 About
|
|
@@ -58,7 +59,7 @@ Dynamic: license-file
|
|
|
58
59
|
- **🆔 Human-Readable Device IDs** - Base32 identifiers instead of IP addresses
|
|
59
60
|
- **✅ Easy to Use** - Simple API with minimal setup
|
|
60
61
|
- **🚀 Performance** - Asynchronous code and lightweight libraries(FastAPI/aiohttp)
|
|
61
|
-
|
|
62
|
+
- **⚙️ Auto-detect** - Devices automatically find each other
|
|
62
63
|
|
|
63
64
|
## 🏗️ Architecture
|
|
64
65
|
|
|
@@ -127,10 +128,6 @@ def main():
|
|
|
127
128
|
easy.start() # Starting server
|
|
128
129
|
print(f"Device {easy.id} is running on port 5000!")
|
|
129
130
|
|
|
130
|
-
# Adding device
|
|
131
|
-
easy.add("ABC123", "192.168.1.100", 5000)
|
|
132
|
-
print("Added device ABC123")
|
|
133
|
-
|
|
134
131
|
# Monitoring device's status
|
|
135
132
|
try:
|
|
136
133
|
while True:
|
|
@@ -149,4 +146,12 @@ def main():
|
|
|
149
146
|
if __name__ == "__main__":
|
|
150
147
|
main()
|
|
151
148
|
```
|
|
149
|
+
## 📦 Version History
|
|
150
|
+
|
|
151
|
+
| Version | Date | Changes |
|
|
152
|
+
|---------|------|---------|
|
|
153
|
+
| 0.4.0 | 2026-11-03 | UDP Discovery, auto-detect |
|
|
154
|
+
| 0.3.3 | 2026-03-01 | Fixed imports, renamed to easyhttp_python |
|
|
155
|
+
| 0.3.2 | 2026-02-14 | Context managers |
|
|
156
|
+
|
|
152
157
|
**More examples available on [GitHub](https://github.com/slpuk/easyhttp-python)**
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
[EN README](README.md) | [RU README](README_RU.md)
|
|
3
3
|
> **A lightweight HTTP-based P2P framework for IoT and device-to-device communication**
|
|
4
4
|
|
|
5
|
-

|
|
6
|
+

|
|
7
7
|

|
|
8
8
|

|
|
9
9
|
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
> # 0.3.2 (OLD)
|
|
16
16
|
> from easyhttp import ...
|
|
17
17
|
>
|
|
18
|
-
> # 0.3.3 (NEW)
|
|
18
|
+
> # 0.3.3 - newer (NEW)
|
|
19
19
|
> from easyhttp_python import ...
|
|
20
20
|
>```
|
|
21
21
|
|
|
@@ -42,9 +42,6 @@ def main():
|
|
|
42
42
|
with EasyHTTP(debug=True, port=5000) as easy:
|
|
43
43
|
print(f"Device ID: {easy.id}")
|
|
44
44
|
|
|
45
|
-
# Manually add another device
|
|
46
|
-
easy.add("ABC123", "192.168.1.100", 5000)
|
|
47
|
-
|
|
48
45
|
# Ping to check if device is online
|
|
49
46
|
if easy.ping("ABC123"):
|
|
50
47
|
print("Device is online!")
|
|
@@ -77,9 +74,6 @@ async def main():
|
|
|
77
74
|
|
|
78
75
|
print(f"Device ID: {easy.id}")
|
|
79
76
|
|
|
80
|
-
# Manually add another device
|
|
81
|
-
easy.add("ABC123", "192.168.1.100", 5000)
|
|
82
|
-
|
|
83
77
|
# Ping to check if device is online
|
|
84
78
|
if await easy.ping("ABC123"):
|
|
85
79
|
print("Device is online!")
|
|
@@ -111,6 +105,7 @@ if __name__ == "__main__":
|
|
|
111
105
|
- **🆔 Human-Readable Device IDs** - Base32 identifiers instead of IP addresses
|
|
112
106
|
- **✅ Easy to Use** - Simple API with minimal setup
|
|
113
107
|
- **🚀 Performance** - Asynchronous code and lightweight libraries(FastAPI/aiohttp)
|
|
108
|
+
- **⚙️ Auto-detect** - Devices automatically find each other
|
|
114
109
|
|
|
115
110
|
## Project Structure
|
|
116
111
|
```
|
|
@@ -120,8 +115,9 @@ easyhttp-python/
|
|
|
120
115
|
│ └── EasyHTTPAsync.md # Async API reference
|
|
121
116
|
├── easyhttp_python/
|
|
122
117
|
│ ├── __init__.py
|
|
123
|
-
│ ├── core.py
|
|
124
|
-
│
|
|
118
|
+
│ ├── core.py # Main framework file/core
|
|
119
|
+
│ ├── discovery.py # Discovery module
|
|
120
|
+
│ └── wrapper.py # Synchronous wrapper
|
|
125
121
|
├── examples/
|
|
126
122
|
│ ├── async/ # Asynchronous examples
|
|
127
123
|
│ │ ├── basic_ping.py
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# EasyHTTP
|
|
2
2
|
|
|
3
3
|
[](https://github.com/slpuk/easyhttp-python)
|
|
4
|
-

|
|
5
|
+

|
|
6
6
|

|
|
7
7
|

|
|
8
8
|
|
|
9
9
|
> **A lightweight HTTP-based P2P framework for IoT and device-to-device communication**
|
|
10
10
|
|
|
11
11
|
## 🛠️ Changelog
|
|
12
|
-
- Added
|
|
12
|
+
- Added UDP multicast discovery
|
|
13
13
|
- Fixed some bugs
|
|
14
14
|
|
|
15
15
|
## 📖 About
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
- **🆔 Human-Readable Device IDs** - Base32 identifiers instead of IP addresses
|
|
24
24
|
- **✅ Easy to Use** - Simple API with minimal setup
|
|
25
25
|
- **🚀 Performance** - Asynchronous code and lightweight libraries(FastAPI/aiohttp)
|
|
26
|
-
|
|
26
|
+
- **⚙️ Auto-detect** - Devices automatically find each other
|
|
27
27
|
|
|
28
28
|
## 🏗️ Architecture
|
|
29
29
|
|
|
@@ -92,10 +92,6 @@ def main():
|
|
|
92
92
|
easy.start() # Starting server
|
|
93
93
|
print(f"Device {easy.id} is running on port 5000!")
|
|
94
94
|
|
|
95
|
-
# Adding device
|
|
96
|
-
easy.add("ABC123", "192.168.1.100", 5000)
|
|
97
|
-
print("Added device ABC123")
|
|
98
|
-
|
|
99
95
|
# Monitoring device's status
|
|
100
96
|
try:
|
|
101
97
|
while True:
|
|
@@ -114,4 +110,12 @@ def main():
|
|
|
114
110
|
if __name__ == "__main__":
|
|
115
111
|
main()
|
|
116
112
|
```
|
|
113
|
+
## 📦 Version History
|
|
114
|
+
|
|
115
|
+
| Version | Date | Changes |
|
|
116
|
+
|---------|------|---------|
|
|
117
|
+
| 0.4.0 | 2026-11-03 | UDP Discovery, auto-detect |
|
|
118
|
+
| 0.3.3 | 2026-03-01 | Fixed imports, renamed to easyhttp_python |
|
|
119
|
+
| 0.3.2 | 2026-02-14 | Context managers |
|
|
120
|
+
|
|
117
121
|
**More examples available on [GitHub](https://github.com/slpuk/easyhttp-python)**
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""UDP multicast discovery module for EasyHTTP."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import socket
|
|
5
|
+
import struct
|
|
6
|
+
import json
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from .core import EasyHTTPAsync
|
|
11
|
+
|
|
12
|
+
from loggity import Logger, Colors, LoggerConfig
|
|
13
|
+
log_config = LoggerConfig(
|
|
14
|
+
colored = True,
|
|
15
|
+
timestamps = False,
|
|
16
|
+
timeformat = None,
|
|
17
|
+
file = None
|
|
18
|
+
)
|
|
19
|
+
log = Logger(config = log_config)
|
|
20
|
+
|
|
21
|
+
class Discovery:
|
|
22
|
+
"""Manages UDP multicast discovery for EasyHTTP devices."""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
parent: "EasyHTTPAsync",
|
|
27
|
+
multicast_group: str = "224.0.0.106",
|
|
28
|
+
multicast_port: int = 37020,
|
|
29
|
+
):
|
|
30
|
+
self.parent = parent
|
|
31
|
+
self.version = self.parent.__version__
|
|
32
|
+
self.multicast_group = multicast_group
|
|
33
|
+
self.multicast_port = multicast_port
|
|
34
|
+
self.discovery_task: asyncio.Task | None = None
|
|
35
|
+
self.enabled = False
|
|
36
|
+
|
|
37
|
+
async def start(self):
|
|
38
|
+
"""Start discovery listener and broadcaster."""
|
|
39
|
+
self.enabled = True
|
|
40
|
+
self.discovery_task = asyncio.create_task(self._discovery_loop())
|
|
41
|
+
|
|
42
|
+
async def stop(self):
|
|
43
|
+
"""Stop discovery tasks."""
|
|
44
|
+
if self.discovery_task:
|
|
45
|
+
self.discovery_task.cancel()
|
|
46
|
+
try:
|
|
47
|
+
await self.discovery_task
|
|
48
|
+
except asyncio.CancelledError:
|
|
49
|
+
pass
|
|
50
|
+
self.discovery_task = None
|
|
51
|
+
|
|
52
|
+
async def _discovery_loop(self):
|
|
53
|
+
"""Run listener and broadcaster concurrently."""
|
|
54
|
+
listener = asyncio.create_task(self._listen_multicast())
|
|
55
|
+
broadcaster = asyncio.create_task(self._broadcast_presence())
|
|
56
|
+
await asyncio.gather(listener, broadcaster)
|
|
57
|
+
|
|
58
|
+
async def _listen_multicast(self):
|
|
59
|
+
"""Listen for DISCOVERY messages from other devices."""
|
|
60
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
|
|
61
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
62
|
+
sock.bind(("", self.multicast_port))
|
|
63
|
+
|
|
64
|
+
mreq = struct.pack(
|
|
65
|
+
"4sl", socket.inet_aton(self.multicast_group), socket.INADDR_ANY
|
|
66
|
+
)
|
|
67
|
+
sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
|
|
68
|
+
sock.setblocking(False)
|
|
69
|
+
|
|
70
|
+
loop = asyncio.get_event_loop()
|
|
71
|
+
while True:
|
|
72
|
+
try:
|
|
73
|
+
data, addr = await loop.sock_recvfrom(sock, 1024)
|
|
74
|
+
await self._handle_discovery_message(data, addr)
|
|
75
|
+
except asyncio.CancelledError:
|
|
76
|
+
break
|
|
77
|
+
except Exception as e:
|
|
78
|
+
if self.parent.debug:
|
|
79
|
+
log.custom("DISCOVERY", Colors.RED, e)
|
|
80
|
+
|
|
81
|
+
async def _broadcast_presence(self):
|
|
82
|
+
"""Periodically broadcast DISCOVERY messages."""
|
|
83
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
|
|
84
|
+
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2)
|
|
85
|
+
|
|
86
|
+
while True:
|
|
87
|
+
try:
|
|
88
|
+
packet = {
|
|
89
|
+
"version": self.version,
|
|
90
|
+
"type": self.parent.commands.DISCOVERY.value,
|
|
91
|
+
"id": self.parent.id,
|
|
92
|
+
"port": self.parent.port,
|
|
93
|
+
}
|
|
94
|
+
sock.sendto(
|
|
95
|
+
json.dumps(packet).encode(),
|
|
96
|
+
(self.multicast_group, self.multicast_port),
|
|
97
|
+
)
|
|
98
|
+
await asyncio.sleep(30)
|
|
99
|
+
except asyncio.CancelledError:
|
|
100
|
+
break
|
|
101
|
+
except Exception as e:
|
|
102
|
+
if self.parent.debug:
|
|
103
|
+
log.custom("DISCOVERY", Colors.RED, e)
|
|
104
|
+
|
|
105
|
+
async def _handle_discovery_message(self, data: bytes, addr: tuple):
|
|
106
|
+
"""Process incoming discovery messages."""
|
|
107
|
+
try:
|
|
108
|
+
message = json.loads(data.decode())
|
|
109
|
+
cmd_type = message.get("type")
|
|
110
|
+
|
|
111
|
+
# Received DISCOVERY -> send DISCOVERY_ACK
|
|
112
|
+
if cmd_type == self.parent.commands.DISCOVERY.value:
|
|
113
|
+
device_id = message.get("id")
|
|
114
|
+
device_port = message.get("port")
|
|
115
|
+
|
|
116
|
+
if device_id and device_id != self.parent.id:
|
|
117
|
+
ack_packet = {
|
|
118
|
+
"version": self.version,
|
|
119
|
+
"type": self.parent.commands.DISCOVERY_ACK.value,
|
|
120
|
+
"id": self.parent.id,
|
|
121
|
+
"port": self.parent.port,
|
|
122
|
+
}
|
|
123
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
124
|
+
sock.sendto(
|
|
125
|
+
json.dumps(ack_packet).encode(),
|
|
126
|
+
(addr[0], self.multicast_port),
|
|
127
|
+
)
|
|
128
|
+
if self.parent.debug:
|
|
129
|
+
log.custom("DISCOVERY", Colors.GREEN, f"Responded to {device_id} at {addr[0]}")
|
|
130
|
+
|
|
131
|
+
# Received DISCOVERY_ACK -> add device
|
|
132
|
+
elif cmd_type == self.parent.commands.DISCOVERY_ACK.value:
|
|
133
|
+
device_id = message.get("id")
|
|
134
|
+
device_port = message.get("port")
|
|
135
|
+
|
|
136
|
+
if device_id and device_id != self.parent.id:
|
|
137
|
+
if device_id not in self.parent.devices:
|
|
138
|
+
self.parent.add(device_id, addr[0], device_port)
|
|
139
|
+
if self.parent.debug:
|
|
140
|
+
log.custom("DISCOVERY", Colors.GREEN, f"Found device {device_id} at {addr[0]}")
|
|
141
|
+
asyncio.create_task(self.parent.ping(device_id))
|
|
142
|
+
|
|
143
|
+
except Exception as e:
|
|
144
|
+
if self.parent.debug:
|
|
145
|
+
log.custom("DISCOVERY", Colors.RED, e)
|
|
@@ -5,6 +5,7 @@ import secrets
|
|
|
5
5
|
import time
|
|
6
6
|
import logging
|
|
7
7
|
import json
|
|
8
|
+
import struct
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
from enum import Enum, auto
|
|
10
11
|
from typing import Optional, Union, Dict, Any, Callable
|
|
@@ -17,23 +18,44 @@ import uvicorn
|
|
|
17
18
|
from fastapi import FastAPI, Request
|
|
18
19
|
from fastapi.responses import JSONResponse
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
# EasyHTTP modules
|
|
22
|
+
from ._discovery import Discovery
|
|
23
|
+
|
|
24
|
+
# Initializing logger
|
|
25
|
+
from loggity import Logger, Colors, LoggerConfig
|
|
26
|
+
log_config = LoggerConfig(
|
|
27
|
+
colored = True,
|
|
28
|
+
timestamps = False,
|
|
29
|
+
timeformat = None,
|
|
30
|
+
file = None
|
|
31
|
+
)
|
|
32
|
+
log = Logger(config = log_config)
|
|
21
33
|
|
|
22
34
|
class EasyHTTPAsync:
|
|
23
35
|
"""Simple asynchronous HTTP-based core of P2P framework for IoT."""
|
|
24
|
-
|
|
36
|
+
__version__ = "0.4.0-alpha.6"
|
|
37
|
+
|
|
25
38
|
class commands(Enum):
|
|
26
39
|
"""Enumeration of available command types."""
|
|
27
40
|
|
|
28
|
-
PING = auto()
|
|
29
|
-
PONG = auto()
|
|
41
|
+
PING = auto() # Ping another device
|
|
42
|
+
PONG = auto() # Anwser for PING
|
|
30
43
|
FETCH = auto() # Request data from another device
|
|
31
|
-
DATA = auto()
|
|
32
|
-
PUSH = auto()
|
|
33
|
-
ACK = auto()
|
|
34
|
-
NACK = auto()
|
|
35
|
-
|
|
36
|
-
|
|
44
|
+
DATA = auto() # Response containing data
|
|
45
|
+
PUSH = auto() # Send data to another device
|
|
46
|
+
ACK = auto() # Acknowledge successful command
|
|
47
|
+
NACK = auto() # Indicate an error occurred
|
|
48
|
+
# New for discovery
|
|
49
|
+
DISCOVERY = auto() # Broadcast discovery request
|
|
50
|
+
DISCOVERY_ACK = auto() # Response to discovery
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
debug: bool = False,
|
|
55
|
+
port: int = 5000,
|
|
56
|
+
config_file=None,
|
|
57
|
+
enable_discovery: bool = True,
|
|
58
|
+
):
|
|
37
59
|
"""Initialize the EasyHTTPAsync instance.
|
|
38
60
|
|
|
39
61
|
Args:
|
|
@@ -43,12 +65,17 @@ class EasyHTTPAsync:
|
|
|
43
65
|
|
|
44
66
|
self.debug = debug
|
|
45
67
|
self.port = port
|
|
68
|
+
self.enable_discovery = enable_discovery
|
|
69
|
+
|
|
70
|
+
if self.enable_discovery:
|
|
71
|
+
self.discovery = Discovery(self)
|
|
46
72
|
|
|
47
73
|
if config_file:
|
|
48
74
|
self.config_file = config_file
|
|
49
75
|
else:
|
|
50
76
|
import sys
|
|
51
|
-
|
|
77
|
+
|
|
78
|
+
if getattr(sys, "frozen", False):
|
|
52
79
|
base_dir = os.path.dirname(sys.executable)
|
|
53
80
|
else:
|
|
54
81
|
base_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
|
|
@@ -56,23 +83,24 @@ class EasyHTTPAsync:
|
|
|
56
83
|
|
|
57
84
|
self.id = None
|
|
58
85
|
self.callbacks = {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
86
|
+
"on_ping": None,
|
|
87
|
+
"on_pong": None,
|
|
88
|
+
"on_fetch": None,
|
|
89
|
+
"on_data": None,
|
|
90
|
+
"on_push": None,
|
|
64
91
|
}
|
|
65
92
|
self.devices = {}
|
|
66
93
|
self.app = FastAPI(title="EasyHTTP API", docs_url=None, redoc_url=None)
|
|
67
|
-
self.app.post(
|
|
94
|
+
self.app.post("/easyhttp/api")(self.api_handler)
|
|
68
95
|
self.server_task = None
|
|
96
|
+
|
|
69
97
|
self._load_config()
|
|
70
98
|
|
|
71
99
|
async def __aenter__(self):
|
|
72
100
|
"""Enter the async context manager."""
|
|
73
101
|
await self.start()
|
|
74
102
|
return self
|
|
75
|
-
|
|
103
|
+
|
|
76
104
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
77
105
|
"""Exit the async context manager."""
|
|
78
106
|
await self.stop()
|
|
@@ -80,37 +108,33 @@ class EasyHTTPAsync:
|
|
|
80
108
|
def _load_config(self):
|
|
81
109
|
try:
|
|
82
110
|
if os.path.exists(self.config_file):
|
|
83
|
-
with open(self.config_file,
|
|
111
|
+
with open(self.config_file, "r") as f:
|
|
84
112
|
data = json.load(f)
|
|
85
|
-
self.id = data.get(
|
|
86
|
-
|
|
113
|
+
self.id = data.get("device_id")
|
|
114
|
+
|
|
87
115
|
if self.debug and self.id:
|
|
88
|
-
|
|
116
|
+
log.info(f"Loaded ID: {self.id} from {self.config_file}")
|
|
89
117
|
except Exception as e:
|
|
90
118
|
if self.debug:
|
|
91
|
-
|
|
92
|
-
|
|
119
|
+
log.error(f"Error loading config: {e}")
|
|
120
|
+
|
|
93
121
|
def _save_config(self):
|
|
94
122
|
try:
|
|
95
|
-
config = {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
'version': __version__
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
with open(self.config_file, 'w') as f:
|
|
123
|
+
config = {"device_id": self.id, "port": self.port, "version": self.__version__}
|
|
124
|
+
|
|
125
|
+
with open(self.config_file, "w") as f:
|
|
102
126
|
json.dump(config, f, indent=2)
|
|
103
|
-
|
|
127
|
+
|
|
104
128
|
if self.debug:
|
|
105
|
-
|
|
129
|
+
log.info(f"Saved ID to {self.config_file}")
|
|
106
130
|
except Exception as e:
|
|
107
131
|
if self.debug:
|
|
108
|
-
|
|
132
|
+
log.error(f"Error saving config: {e}")
|
|
109
133
|
|
|
110
134
|
def _get_local_ip(self):
|
|
111
135
|
try:
|
|
112
136
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
113
|
-
s.connect((
|
|
137
|
+
s.connect(("8.8.8.8", 80))
|
|
114
138
|
local_ip = s.getsockname()[0]
|
|
115
139
|
s.close()
|
|
116
140
|
return local_ip
|
|
@@ -131,7 +155,7 @@ class EasyHTTPAsync:
|
|
|
131
155
|
"""
|
|
132
156
|
|
|
133
157
|
alphabet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"
|
|
134
|
-
self.id =
|
|
158
|
+
self.id = "".join(secrets.choice(alphabet) for _ in range(length))
|
|
135
159
|
self._save_config()
|
|
136
160
|
|
|
137
161
|
def on(self, event: str, callback_func: Callable) -> None:
|
|
@@ -164,18 +188,18 @@ class EasyHTTPAsync:
|
|
|
164
188
|
|
|
165
189
|
if len(device_id) != 6:
|
|
166
190
|
raise ValueError("Device ID must be 6 characters")
|
|
167
|
-
|
|
191
|
+
|
|
168
192
|
if device_id not in self.devices:
|
|
169
193
|
self.devices[device_id] = {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
194
|
+
"ip": device_ip,
|
|
195
|
+
"port": int(device_port),
|
|
196
|
+
"last_seen": time.time(),
|
|
197
|
+
"added_manually": True,
|
|
174
198
|
}
|
|
175
199
|
if self.debug:
|
|
176
|
-
|
|
200
|
+
log.debug(f"Added device {device_id}: {device_ip}:{device_port}")
|
|
177
201
|
else:
|
|
178
|
-
|
|
202
|
+
log.debug("Device already exists")
|
|
179
203
|
|
|
180
204
|
async def start(self) -> None:
|
|
181
205
|
"""Start the HTTP server and generate a device ID if not set."""
|
|
@@ -185,35 +209,45 @@ class EasyHTTPAsync:
|
|
|
185
209
|
|
|
186
210
|
try:
|
|
187
211
|
config = uvicorn.Config(
|
|
188
|
-
self.app,
|
|
189
|
-
host="0.0.0.0",
|
|
212
|
+
self.app,
|
|
213
|
+
host="0.0.0.0",
|
|
190
214
|
port=self.port,
|
|
191
215
|
log_level="warning",
|
|
192
|
-
lifespan="off"
|
|
216
|
+
lifespan="off",
|
|
193
217
|
)
|
|
194
|
-
|
|
218
|
+
|
|
195
219
|
server = uvicorn.Server(config)
|
|
196
220
|
self.server_task = asyncio.create_task(server.serve())
|
|
197
221
|
|
|
198
|
-
logging.getLogger(
|
|
199
|
-
logging.getLogger(
|
|
200
|
-
logging.getLogger(
|
|
222
|
+
logging.getLogger("werkzeug").disabled = True
|
|
223
|
+
logging.getLogger("uvicorn.error").propagate = False
|
|
224
|
+
logging.getLogger("uvicorn.access").propagate = False
|
|
225
|
+
|
|
226
|
+
# Starting discovery
|
|
227
|
+
if self.enable_discovery:
|
|
228
|
+
await self.discovery.start()
|
|
201
229
|
|
|
202
230
|
await asyncio.sleep(2) # Give server time to start
|
|
203
231
|
|
|
204
232
|
if self.debug:
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
233
|
+
log.info(f"\033[1;32mEasyHTTP \033[37m{self.__version__}\033[0m has been started!")
|
|
234
|
+
log.info(f"Device's ID: {self.id}")
|
|
235
|
+
log.info(f"EasyHTTP starting on port {self.port}")
|
|
236
|
+
log.info(f"API running on \033[1mhttp://{self._get_local_ip()}:{self.port}/easyhttp/api\033[0m")
|
|
237
|
+
if self.enable_discovery:
|
|
238
|
+
log.info(f"Discovery enabled on {self.discovery.multicast_group}:{self.discovery.multicast_port}")
|
|
239
|
+
|
|
210
240
|
except Exception as e:
|
|
211
|
-
|
|
241
|
+
log.error(f"Failed to start server: {e}")
|
|
212
242
|
raise
|
|
213
243
|
|
|
214
244
|
async def stop(self) -> None:
|
|
215
245
|
"""Gracefully stop the HTTP server and cancel the server task."""
|
|
216
246
|
|
|
247
|
+
# Stopping discovery
|
|
248
|
+
if hasattr(self, 'discovery') and self.discovery:
|
|
249
|
+
await self.discovery.stop()
|
|
250
|
+
|
|
217
251
|
if self.server_task:
|
|
218
252
|
self.server_task.cancel()
|
|
219
253
|
try:
|
|
@@ -221,7 +255,34 @@ class EasyHTTPAsync:
|
|
|
221
255
|
except asyncio.CancelledError:
|
|
222
256
|
pass
|
|
223
257
|
|
|
224
|
-
async def
|
|
258
|
+
async def start_discovery(self):
|
|
259
|
+
"""Manually start discovery service."""
|
|
260
|
+
if self.debug:
|
|
261
|
+
log.debug("Enabled discovery")
|
|
262
|
+
self.enable_discovery = True
|
|
263
|
+
await self.discovery.start()
|
|
264
|
+
|
|
265
|
+
async def stop_discovery(self):
|
|
266
|
+
"""Manually stop discovery service."""
|
|
267
|
+
if self.debug:
|
|
268
|
+
log.debug("Disabled discovery")
|
|
269
|
+
self.enable_discovery = False
|
|
270
|
+
await self.discovery.stop()
|
|
271
|
+
|
|
272
|
+
def get_discovered(self) -> list:
|
|
273
|
+
"""Return list of auto-discovered device IDs."""
|
|
274
|
+
return [
|
|
275
|
+
did
|
|
276
|
+
for did, info in self.devices.items()
|
|
277
|
+
if not info.get("added_manually", False)
|
|
278
|
+
]
|
|
279
|
+
|
|
280
|
+
async def send(
|
|
281
|
+
self,
|
|
282
|
+
device_id: str,
|
|
283
|
+
command_type: Union[int, "commands"],
|
|
284
|
+
data: Optional[Any] = None,
|
|
285
|
+
) -> Optional[dict]:
|
|
225
286
|
"""Send a JSON-formatted command to another device.
|
|
226
287
|
|
|
227
288
|
Args:
|
|
@@ -239,35 +300,41 @@ class EasyHTTPAsync:
|
|
|
239
300
|
|
|
240
301
|
if device_id not in self.devices:
|
|
241
302
|
if self.debug:
|
|
242
|
-
|
|
303
|
+
log.error(f"Device {device_id} not found in devices cache")
|
|
243
304
|
return None
|
|
244
|
-
|
|
305
|
+
|
|
245
306
|
packet = {
|
|
246
|
-
"version": __version__,
|
|
247
|
-
"type":
|
|
307
|
+
"version": self.__version__,
|
|
308
|
+
"type": (
|
|
309
|
+
command_type
|
|
310
|
+
if isinstance(command_type, self.commands)
|
|
311
|
+
else command_type
|
|
312
|
+
),
|
|
248
313
|
"header": {
|
|
249
|
-
"sender_id": self.id,
|
|
314
|
+
"sender_id": self.id,
|
|
250
315
|
"sender_port": self.port,
|
|
251
|
-
"recipient_id": device_id,
|
|
252
|
-
"timestamp": int(time.time())
|
|
253
|
-
}
|
|
316
|
+
"recipient_id": device_id,
|
|
317
|
+
"timestamp": int(time.time()),
|
|
318
|
+
},
|
|
254
319
|
}
|
|
255
|
-
|
|
320
|
+
|
|
256
321
|
if data:
|
|
257
|
-
packet[
|
|
258
|
-
|
|
322
|
+
packet["data"] = data
|
|
323
|
+
|
|
259
324
|
recipient_url = f"http://{self.devices[device_id]['ip']}:{self.devices[device_id]['port']}/easyhttp/api"
|
|
260
|
-
|
|
325
|
+
|
|
261
326
|
try:
|
|
262
327
|
async with aiohttp.ClientSession() as session:
|
|
263
|
-
async with session.post(
|
|
328
|
+
async with session.post(
|
|
329
|
+
recipient_url, json=packet, timeout=3
|
|
330
|
+
) as response:
|
|
264
331
|
if response.status == 200:
|
|
265
332
|
return await response.json()
|
|
266
333
|
return None
|
|
267
|
-
|
|
334
|
+
|
|
268
335
|
except Exception as e:
|
|
269
336
|
if self.debug:
|
|
270
|
-
|
|
337
|
+
log.error(f"Failed to send to {device_id}: {e}")
|
|
271
338
|
return None
|
|
272
339
|
|
|
273
340
|
async def ping(self, device_id: str) -> bool:
|
|
@@ -281,26 +348,28 @@ class EasyHTTPAsync:
|
|
|
281
348
|
"""
|
|
282
349
|
|
|
283
350
|
response = await self.send(device_id, self.commands.PING.value)
|
|
284
|
-
|
|
285
|
-
if response and response.get(
|
|
351
|
+
|
|
352
|
+
if response and response.get("type") == self.commands.PONG.value:
|
|
286
353
|
if self.debug:
|
|
287
|
-
|
|
354
|
+
log.custom("PING", Colors.GREEN, f"{device_id} is online")
|
|
288
355
|
if device_id in self.devices:
|
|
289
|
-
self.devices[device_id][
|
|
356
|
+
self.devices[device_id]["last_seen"] = time.time()
|
|
290
357
|
return True
|
|
291
358
|
else:
|
|
292
359
|
if self.debug:
|
|
293
|
-
|
|
360
|
+
log.custom("PING", Colors.RED, f"{device_id} is offline or not responding")
|
|
294
361
|
return False
|
|
295
362
|
|
|
296
|
-
async def fetch(
|
|
363
|
+
async def fetch(
|
|
364
|
+
self, device_id: str, query: Optional[Any] = None
|
|
365
|
+
) -> Optional[dict]:
|
|
297
366
|
"""Send a FETCH request to another device and return the response.
|
|
298
367
|
|
|
299
368
|
Args:
|
|
300
369
|
device_id: ID of the target device.
|
|
301
370
|
query: Query data to send with the FETCH request.
|
|
302
371
|
|
|
303
|
-
Returns:
|
|
372
|
+
Returns:
|
|
304
373
|
Response data from the device, or None if failed.
|
|
305
374
|
The dict typically contains 'type', 'header', and 'data' fields.
|
|
306
375
|
"""
|
|
@@ -324,16 +393,16 @@ class EasyHTTPAsync:
|
|
|
324
393
|
|
|
325
394
|
if data is not None and not isinstance(data, (dict, list, str)):
|
|
326
395
|
raise TypeError("Data must be JSON-serializable (dict, list, str)")
|
|
327
|
-
|
|
396
|
+
|
|
328
397
|
response = await self.send(device_id, self.commands.PUSH, data)
|
|
329
398
|
|
|
330
|
-
if response and response.get(
|
|
399
|
+
if response and response.get("type") == self.commands.ACK.value:
|
|
331
400
|
if self.debug:
|
|
332
|
-
|
|
401
|
+
log.custom("PUSH", Colors.GREEN, f"Successfully wrote to {device_id}")
|
|
333
402
|
return True
|
|
334
403
|
else:
|
|
335
404
|
if self.debug:
|
|
336
|
-
|
|
405
|
+
log.custom("PUSH", Colors.RED, f"Error writing to {device_id}")
|
|
337
406
|
return False
|
|
338
407
|
|
|
339
408
|
async def api_handler(self, request: Request) -> JSONResponse:
|
|
@@ -345,173 +414,176 @@ class EasyHTTPAsync:
|
|
|
345
414
|
Returns:
|
|
346
415
|
JSONResponse: Response to the client.
|
|
347
416
|
"""
|
|
348
|
-
|
|
417
|
+
|
|
349
418
|
try:
|
|
350
419
|
data = await request.json()
|
|
351
420
|
except:
|
|
352
421
|
return JSONResponse({"error": "Invalid JSON data"}, status_code=400)
|
|
353
|
-
|
|
422
|
+
|
|
354
423
|
if not data:
|
|
355
424
|
return JSONResponse({"error": "No JSON data"}, status_code=400)
|
|
356
|
-
|
|
357
|
-
command_type = data.get(
|
|
358
|
-
header = data.get(
|
|
359
|
-
sender_id = header.get(
|
|
360
|
-
|
|
425
|
+
|
|
426
|
+
command_type = data.get("type")
|
|
427
|
+
header = data.get("header", {})
|
|
428
|
+
sender_id = header.get("sender_id")
|
|
429
|
+
|
|
361
430
|
client_ip = request.client.host if request.client else "0.0.0.0"
|
|
362
431
|
|
|
363
432
|
if sender_id and sender_id != self.id and sender_id not in self.devices:
|
|
364
433
|
self.devices[sender_id] = {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
434
|
+
"ip": client_ip,
|
|
435
|
+
"port": header.get("sender_port", self.port),
|
|
436
|
+
"last_seen": int(time.time()),
|
|
368
437
|
}
|
|
369
438
|
|
|
370
439
|
# Handle PING response
|
|
371
440
|
if command_type == self.commands.PING.value:
|
|
372
|
-
if self.callbacks[
|
|
373
|
-
callback = self.callbacks[
|
|
441
|
+
if self.callbacks["on_ping"]:
|
|
442
|
+
callback = self.callbacks["on_ping"]
|
|
374
443
|
if asyncio.iscoroutinefunction(callback):
|
|
375
444
|
await callback(
|
|
376
|
-
sender_id=sender_id,
|
|
377
|
-
timestamp=header.get('timestamp')
|
|
445
|
+
sender_id=sender_id, timestamp=header.get("timestamp")
|
|
378
446
|
)
|
|
379
447
|
else:
|
|
380
|
-
callback(
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
"timestamp": int(time.time())
|
|
448
|
+
callback(sender_id=sender_id, timestamp=header.get("timestamp"))
|
|
449
|
+
|
|
450
|
+
return JSONResponse(
|
|
451
|
+
{
|
|
452
|
+
"version": self.__version__,
|
|
453
|
+
"type": self.commands.PONG.value,
|
|
454
|
+
"header": {
|
|
455
|
+
"sender_id": self.id,
|
|
456
|
+
"sender_port": self.port,
|
|
457
|
+
"recipient_id": sender_id,
|
|
458
|
+
"timestamp": int(time.time()),
|
|
459
|
+
},
|
|
393
460
|
}
|
|
394
|
-
|
|
461
|
+
)
|
|
395
462
|
|
|
396
463
|
# Handle PONG answer
|
|
397
464
|
elif command_type == self.commands.PONG.value:
|
|
398
|
-
if self.callbacks[
|
|
399
|
-
callback = self.callbacks[
|
|
465
|
+
if self.callbacks["on_pong"]:
|
|
466
|
+
callback = self.callbacks["on_pong"]
|
|
400
467
|
if asyncio.iscoroutinefunction(callback):
|
|
401
468
|
await callback(
|
|
402
|
-
sender_id=sender_id,
|
|
403
|
-
timestamp=header.get('timestamp')
|
|
469
|
+
sender_id=sender_id, timestamp=header.get("timestamp")
|
|
404
470
|
)
|
|
405
471
|
else:
|
|
406
|
-
callback(
|
|
407
|
-
sender_id=sender_id,
|
|
408
|
-
timestamp=header.get('timestamp')
|
|
409
|
-
)
|
|
472
|
+
callback(sender_id=sender_id, timestamp=header.get("timestamp"))
|
|
410
473
|
|
|
411
474
|
if self.debug:
|
|
412
|
-
|
|
475
|
+
log.custom("PONG", Colors.GREEN, f"Received from {sender_id}")
|
|
413
476
|
if sender_id in self.devices:
|
|
414
|
-
self.devices[sender_id][
|
|
477
|
+
self.devices[sender_id]["last_seen"] = time.time()
|
|
415
478
|
return JSONResponse({"status": "pong_received"})
|
|
416
479
|
|
|
417
480
|
# Handle FETCH response
|
|
418
481
|
elif command_type == self.commands.FETCH.value:
|
|
419
|
-
if self.callbacks[
|
|
420
|
-
callback = self.callbacks[
|
|
482
|
+
if self.callbacks["on_fetch"]:
|
|
483
|
+
callback = self.callbacks["on_fetch"]
|
|
421
484
|
if asyncio.iscoroutinefunction(callback):
|
|
422
|
-
response_data = await callback
|
|
485
|
+
response_data = await callback(
|
|
423
486
|
sender_id=sender_id,
|
|
424
|
-
query=data.get(
|
|
425
|
-
timestamp=header.get(
|
|
487
|
+
query=data.get("data"),
|
|
488
|
+
timestamp=header.get("timestamp"),
|
|
426
489
|
)
|
|
427
490
|
else:
|
|
428
|
-
response_data = callback
|
|
491
|
+
response_data = callback(
|
|
429
492
|
sender_id=sender_id,
|
|
430
|
-
query=data.get(
|
|
431
|
-
timestamp=header.get(
|
|
493
|
+
query=data.get("data"),
|
|
494
|
+
timestamp=header.get("timestamp"),
|
|
432
495
|
)
|
|
433
496
|
if response_data:
|
|
434
|
-
return JSONResponse(
|
|
435
|
-
|
|
436
|
-
|
|
497
|
+
return JSONResponse(
|
|
498
|
+
{
|
|
499
|
+
"version": self.__version__,
|
|
500
|
+
"type": self.commands.DATA.value,
|
|
501
|
+
"header": {
|
|
502
|
+
"sender_id": self.id,
|
|
503
|
+
"sender_port": self.port,
|
|
504
|
+
"recipient_id": sender_id,
|
|
505
|
+
"timestamp": int(time.time()),
|
|
506
|
+
},
|
|
507
|
+
"data": response_data,
|
|
508
|
+
}
|
|
509
|
+
)
|
|
510
|
+
return JSONResponse({"status": "fetch_handled"})
|
|
511
|
+
|
|
512
|
+
# Handle PUSH response
|
|
513
|
+
elif command_type == self.commands.PUSH.value:
|
|
514
|
+
if not self.callbacks["on_push"]:
|
|
515
|
+
return JSONResponse(
|
|
516
|
+
{
|
|
517
|
+
"version": self.__version__,
|
|
518
|
+
"type": self.commands.NACK.value,
|
|
437
519
|
"header": {
|
|
438
520
|
"sender_id": self.id,
|
|
439
521
|
"sender_port": self.port,
|
|
440
522
|
"recipient_id": sender_id,
|
|
441
|
-
"timestamp": int(time.time())
|
|
523
|
+
"timestamp": int(time.time()),
|
|
442
524
|
},
|
|
443
|
-
"data": response_data
|
|
444
|
-
})
|
|
445
|
-
return JSONResponse({"status": "fetch_handled"})
|
|
446
|
-
|
|
447
|
-
# Handle PUSH response
|
|
448
|
-
elif command_type == self.commands.PUSH.value:
|
|
449
|
-
if not self.callbacks['on_push']:
|
|
450
|
-
return JSONResponse({
|
|
451
|
-
"version": __version__,
|
|
452
|
-
"type": self.commands.NACK.value,
|
|
453
|
-
"header": {
|
|
454
|
-
"sender_id": self.id,
|
|
455
|
-
"sender_port": self.port,
|
|
456
|
-
"recipient_id": sender_id,
|
|
457
|
-
"timestamp": int(time.time())
|
|
458
525
|
},
|
|
459
|
-
|
|
526
|
+
status_code=400,
|
|
527
|
+
)
|
|
460
528
|
|
|
461
|
-
callback = self.callbacks[
|
|
529
|
+
callback = self.callbacks["on_push"]
|
|
462
530
|
if asyncio.iscoroutinefunction(callback):
|
|
463
531
|
success = await callback(
|
|
464
532
|
sender_id=sender_id,
|
|
465
|
-
data=data.get(
|
|
466
|
-
timestamp=header.get(
|
|
533
|
+
data=data.get("data"),
|
|
534
|
+
timestamp=header.get("timestamp"),
|
|
467
535
|
)
|
|
468
536
|
else:
|
|
469
537
|
success = callback(
|
|
470
538
|
sender_id=sender_id,
|
|
471
|
-
data=data.get(
|
|
472
|
-
timestamp=header.get(
|
|
539
|
+
data=data.get("data"),
|
|
540
|
+
timestamp=header.get("timestamp"),
|
|
473
541
|
)
|
|
474
|
-
|
|
542
|
+
|
|
475
543
|
if success:
|
|
476
|
-
return JSONResponse(
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
"
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
544
|
+
return JSONResponse(
|
|
545
|
+
{
|
|
546
|
+
"version": self.__version__,
|
|
547
|
+
"type": self.commands.ACK.value,
|
|
548
|
+
"header": {
|
|
549
|
+
"sender_id": self.id,
|
|
550
|
+
"sender_port": self.port,
|
|
551
|
+
"recipient_id": sender_id,
|
|
552
|
+
"timestamp": int(time.time()),
|
|
553
|
+
},
|
|
484
554
|
}
|
|
485
|
-
|
|
555
|
+
)
|
|
486
556
|
else:
|
|
487
|
-
return JSONResponse(
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
"
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
557
|
+
return JSONResponse(
|
|
558
|
+
{
|
|
559
|
+
"version": self.__version__,
|
|
560
|
+
"type": self.commands.NACK.value,
|
|
561
|
+
"header": {
|
|
562
|
+
"sender_id": self.id,
|
|
563
|
+
"sender_port": self.port,
|
|
564
|
+
"recipient_id": sender_id,
|
|
565
|
+
"timestamp": int(time.time()),
|
|
566
|
+
},
|
|
495
567
|
}
|
|
496
|
-
|
|
568
|
+
)
|
|
497
569
|
|
|
498
570
|
# Handle DATA
|
|
499
571
|
elif command_type == self.commands.DATA.value:
|
|
500
|
-
if self.callbacks[
|
|
501
|
-
callback = self.callbacks[
|
|
572
|
+
if self.callbacks["on_data"]:
|
|
573
|
+
callback = self.callbacks["on_data"]
|
|
502
574
|
if asyncio.iscoroutinefunction(callback):
|
|
503
575
|
await callback(
|
|
504
576
|
sender_id=sender_id,
|
|
505
|
-
data=data.get(
|
|
506
|
-
timestamp=header.get(
|
|
577
|
+
data=data.get("data"),
|
|
578
|
+
timestamp=header.get("timestamp"),
|
|
507
579
|
)
|
|
508
580
|
else:
|
|
509
581
|
callback(
|
|
510
582
|
sender_id=sender_id,
|
|
511
|
-
data=data.get(
|
|
512
|
-
timestamp=header.get(
|
|
583
|
+
data=data.get("data"),
|
|
584
|
+
timestamp=header.get("timestamp"),
|
|
513
585
|
)
|
|
514
586
|
return JSONResponse({"status": "data_received"})
|
|
515
|
-
|
|
587
|
+
|
|
516
588
|
# Handle unknown command types
|
|
517
589
|
return JSONResponse({"error": "Unknown command type"}, status_code=400)
|
|
@@ -2,12 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
from typing import Optional, Any, Callable
|
|
5
|
-
from .core import EasyHTTPAsync
|
|
5
|
+
from .core import EasyHTTPAsync
|
|
6
6
|
|
|
7
7
|
class EasyHTTP:
|
|
8
8
|
"""Simple HTTP-based P2P framework with asynchronous core for IoT."""
|
|
9
|
-
|
|
10
|
-
def __init__(
|
|
9
|
+
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
debug: bool = False,
|
|
13
|
+
port: int = 5000,
|
|
14
|
+
config_file: Optional[str] = None,
|
|
15
|
+
enable_discovery: bool = True,
|
|
16
|
+
):
|
|
11
17
|
"""Initialize the EasyHTTP instance.
|
|
12
18
|
|
|
13
19
|
Args:
|
|
@@ -15,19 +21,23 @@ class EasyHTTP:
|
|
|
15
21
|
port: Port to run the HTTP server on. Defaults to 5000.
|
|
16
22
|
"""
|
|
17
23
|
|
|
18
|
-
self._core = EasyHTTPAsync(
|
|
24
|
+
self._core = EasyHTTPAsync(
|
|
25
|
+
debug=debug,
|
|
26
|
+
port=port,
|
|
27
|
+
config_file=config_file,
|
|
28
|
+
enable_discovery=enable_discovery,
|
|
29
|
+
)
|
|
19
30
|
self._loop = None
|
|
20
31
|
self._running = False
|
|
21
|
-
|
|
22
32
|
self.commands = self._core.commands
|
|
23
|
-
self.__version__ = __version__
|
|
24
|
-
|
|
33
|
+
self.__version__ = self._core.__version__
|
|
34
|
+
|
|
25
35
|
def _ensure_loop(self):
|
|
26
36
|
"""Ensure event loop is running."""
|
|
27
37
|
if not self._loop:
|
|
28
38
|
self._loop = asyncio.new_event_loop()
|
|
29
39
|
asyncio.set_event_loop(self._loop)
|
|
30
|
-
|
|
40
|
+
|
|
31
41
|
def on(self, event: str, callback_func: Callable) -> None:
|
|
32
42
|
"""Register a callback function for a specific event.
|
|
33
43
|
|
|
@@ -39,7 +49,7 @@ class EasyHTTP:
|
|
|
39
49
|
ValueError: If the event is unknown.
|
|
40
50
|
"""
|
|
41
51
|
self._core.on(event, callback_func)
|
|
42
|
-
|
|
52
|
+
|
|
43
53
|
def add(self, device_id: str, device_ip: str, device_port: int) -> None:
|
|
44
54
|
"""Manually add a device to the local devices cache.
|
|
45
55
|
|
|
@@ -58,14 +68,16 @@ class EasyHTTP:
|
|
|
58
68
|
self._ensure_loop()
|
|
59
69
|
self._loop.run_until_complete(self._core.start())
|
|
60
70
|
self._running = True
|
|
61
|
-
|
|
71
|
+
|
|
62
72
|
def stop(self) -> None:
|
|
63
73
|
"""Gracefully stop the HTTP server and cancel the server task."""
|
|
64
74
|
if self._running:
|
|
65
75
|
self._loop.run_until_complete(self._core.stop())
|
|
66
76
|
self._running = False
|
|
67
|
-
|
|
68
|
-
def send(
|
|
77
|
+
|
|
78
|
+
def send(
|
|
79
|
+
self, device_id: str, command_type: Any, data: Optional[Any] = None
|
|
80
|
+
) -> Optional[dict]:
|
|
69
81
|
"""Send a JSON-formatted command to another device.
|
|
70
82
|
|
|
71
83
|
Args:
|
|
@@ -85,7 +97,7 @@ class EasyHTTP:
|
|
|
85
97
|
return self._loop.run_until_complete(
|
|
86
98
|
self._core.send(device_id, command_type, data)
|
|
87
99
|
)
|
|
88
|
-
|
|
100
|
+
|
|
89
101
|
def ping(self, device_id: str) -> bool:
|
|
90
102
|
"""Send a PING request to a device and check if it's online.
|
|
91
103
|
|
|
@@ -95,10 +107,8 @@ class EasyHTTP:
|
|
|
95
107
|
Returns:
|
|
96
108
|
True if device responded with PONG, False otherwise.
|
|
97
109
|
"""
|
|
98
|
-
return self._loop.run_until_complete(
|
|
99
|
-
|
|
100
|
-
)
|
|
101
|
-
|
|
110
|
+
return self._loop.run_until_complete(self._core.ping(device_id))
|
|
111
|
+
|
|
102
112
|
def fetch(self, device_id: str, query: Optional[Any] = None) -> Optional[dict]:
|
|
103
113
|
"""Send a FETCH request to another device and return the response.
|
|
104
114
|
|
|
@@ -106,14 +116,12 @@ class EasyHTTP:
|
|
|
106
116
|
device_id: ID of the target device.
|
|
107
117
|
query: Query data to send with the FETCH request.
|
|
108
118
|
|
|
109
|
-
Returns:
|
|
119
|
+
Returns:
|
|
110
120
|
Response data from the device, or None if failed.
|
|
111
121
|
The dict typically contains 'type', 'header', and 'data' fields.
|
|
112
122
|
"""
|
|
113
|
-
return self._loop.run_until_complete(
|
|
114
|
-
|
|
115
|
-
)
|
|
116
|
-
|
|
123
|
+
return self._loop.run_until_complete(self._core.fetch(device_id, query))
|
|
124
|
+
|
|
117
125
|
def push(self, device_id: str, data: Optional[Any] = None) -> bool:
|
|
118
126
|
"""Send data to another device using PUSH command.
|
|
119
127
|
|
|
@@ -127,27 +135,25 @@ class EasyHTTP:
|
|
|
127
135
|
Raises:
|
|
128
136
|
TypeError: If data is not JSON-serializable.
|
|
129
137
|
"""
|
|
130
|
-
return self._loop.run_until_complete(
|
|
131
|
-
self._core.push(device_id, data)
|
|
132
|
-
)
|
|
138
|
+
return self._loop.run_until_complete(self._core.push(device_id, data))
|
|
133
139
|
|
|
134
140
|
# Context manager support
|
|
135
141
|
def __enter__(self):
|
|
136
142
|
"""Enter the sync context manager."""
|
|
137
143
|
self.start()
|
|
138
144
|
return self
|
|
139
|
-
|
|
145
|
+
|
|
140
146
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
141
147
|
"""Exit the sync context manager."""
|
|
142
148
|
self.stop()
|
|
143
|
-
|
|
149
|
+
|
|
144
150
|
# Property accessors
|
|
145
151
|
@property
|
|
146
152
|
def id(self) -> str:
|
|
147
153
|
"""Get device ID."""
|
|
148
154
|
return self._core.id
|
|
149
|
-
|
|
155
|
+
|
|
150
156
|
@property
|
|
151
157
|
def devices(self) -> dict:
|
|
152
158
|
"""Get devices cache."""
|
|
153
|
-
return self._core.devices
|
|
159
|
+
return self._core.devices
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: easyhttp-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0a6
|
|
4
4
|
Summary: Simple HTTP-based P2P framework for IoT
|
|
5
5
|
Author-email: slpuk <yarik6052@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -9,7 +9,7 @@ Project-URL: Documentation, https://github.com/slpuk/easyhttp-python#readme
|
|
|
9
9
|
Project-URL: Repository, https://github.com/slpuk/easyhttp-python
|
|
10
10
|
Project-URL: Issue Tracker, https://github.com/slpuk/easyhttp-python/issues
|
|
11
11
|
Keywords: iot,p2p,http,framework
|
|
12
|
-
Classifier: Development Status ::
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
|
14
14
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
15
15
|
Classifier: Topic :: Communications
|
|
@@ -27,6 +27,7 @@ License-File: LICENSE
|
|
|
27
27
|
Requires-Dist: fastapi>=0.103.2
|
|
28
28
|
Requires-Dist: uvicorn[standard]>=0.22.0
|
|
29
29
|
Requires-Dist: aiohttp>=3.7.0
|
|
30
|
+
Requires-Dist: loggity>=0.5.0a6
|
|
30
31
|
Provides-Extra: dev
|
|
31
32
|
Requires-Dist: pytest>=6.0; extra == "dev"
|
|
32
33
|
Requires-Dist: black; extra == "dev"
|
|
@@ -36,15 +37,15 @@ Dynamic: license-file
|
|
|
36
37
|
# EasyHTTP
|
|
37
38
|
|
|
38
39
|
[](https://github.com/slpuk/easyhttp-python)
|
|
39
|
-

|
|
41
|
+

|
|
41
42
|

|
|
42
43
|

|
|
43
44
|
|
|
44
45
|
> **A lightweight HTTP-based P2P framework for IoT and device-to-device communication**
|
|
45
46
|
|
|
46
47
|
## 🛠️ Changelog
|
|
47
|
-
- Added
|
|
48
|
+
- Added UDP multicast discovery
|
|
48
49
|
- Fixed some bugs
|
|
49
50
|
|
|
50
51
|
## 📖 About
|
|
@@ -58,7 +59,7 @@ Dynamic: license-file
|
|
|
58
59
|
- **🆔 Human-Readable Device IDs** - Base32 identifiers instead of IP addresses
|
|
59
60
|
- **✅ Easy to Use** - Simple API with minimal setup
|
|
60
61
|
- **🚀 Performance** - Asynchronous code and lightweight libraries(FastAPI/aiohttp)
|
|
61
|
-
|
|
62
|
+
- **⚙️ Auto-detect** - Devices automatically find each other
|
|
62
63
|
|
|
63
64
|
## 🏗️ Architecture
|
|
64
65
|
|
|
@@ -127,10 +128,6 @@ def main():
|
|
|
127
128
|
easy.start() # Starting server
|
|
128
129
|
print(f"Device {easy.id} is running on port 5000!")
|
|
129
130
|
|
|
130
|
-
# Adding device
|
|
131
|
-
easy.add("ABC123", "192.168.1.100", 5000)
|
|
132
|
-
print("Added device ABC123")
|
|
133
|
-
|
|
134
131
|
# Monitoring device's status
|
|
135
132
|
try:
|
|
136
133
|
while True:
|
|
@@ -149,4 +146,12 @@ def main():
|
|
|
149
146
|
if __name__ == "__main__":
|
|
150
147
|
main()
|
|
151
148
|
```
|
|
149
|
+
## 📦 Version History
|
|
150
|
+
|
|
151
|
+
| Version | Date | Changes |
|
|
152
|
+
|---------|------|---------|
|
|
153
|
+
| 0.4.0 | 2026-11-03 | UDP Discovery, auto-detect |
|
|
154
|
+
| 0.3.3 | 2026-03-01 | Fixed imports, renamed to easyhttp_python |
|
|
155
|
+
| 0.3.2 | 2026-02-14 | Context managers |
|
|
156
|
+
|
|
152
157
|
**More examples available on [GitHub](https://github.com/slpuk/easyhttp-python)**
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "easyhttp-python"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0-alpha.6"
|
|
8
8
|
authors = [
|
|
9
9
|
{name = "slpuk", email = "yarik6052@gmail.com"},
|
|
10
10
|
]
|
|
@@ -14,7 +14,7 @@ requires-python = ">=3.7"
|
|
|
14
14
|
license = {text = "MIT"}
|
|
15
15
|
keywords = ["iot", "p2p", "http", "framework"]
|
|
16
16
|
classifiers = [
|
|
17
|
-
"Development Status ::
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
18
|
"Intended Audience :: Developers",
|
|
19
19
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
20
20
|
"Topic :: Communications",
|
|
@@ -31,7 +31,8 @@ classifiers = [
|
|
|
31
31
|
dependencies = [
|
|
32
32
|
"fastapi>=0.103.2",
|
|
33
33
|
"uvicorn[standard]>=0.22.0",
|
|
34
|
-
"aiohttp>=3.7.0"
|
|
34
|
+
"aiohttp>=3.7.0",
|
|
35
|
+
"loggity>=0.5.0a6"
|
|
35
36
|
]
|
|
36
37
|
|
|
37
38
|
[project.urls]
|
|
File without changes
|
{easyhttp_python-0.3.3 → easyhttp_python-0.4.0a6}/easyhttp_python.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|