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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: easyhttp-python
3
- Version: 0.3.3
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 :: 4 - Beta
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
  [![GitHub](https://img.shields.io/badge/github-%23121011.svg?style=for-the-badge&logo=github&logoColor=white)](https://github.com/slpuk/easyhttp-python)
39
- ![Protocol Version](https://img.shields.io/badge/version-0.3.3-blue?style=for-the-badge)
40
- ![Development Status](https://img.shields.io/badge/status-beta-orange?style=for-the-badge)
40
+ ![Protocol Version](https://img.shields.io/badge/version-0.4.0-blue?style=for-the-badge)
41
+ ![Development Status](https://img.shields.io/badge/status-alpha-red?style=for-the-badge)
41
42
  ![License](https://img.shields.io/badge/license-MIT-green?style=for-the-badge)
42
43
  ![Python](https://img.shields.io/badge/python-3.7+-blue?style=for-the-badge&logo=python&logoColor=white)
43
44
 
44
45
  > **A lightweight HTTP-based P2P framework for IoT and device-to-device communication**
45
46
 
46
47
  ## 🛠️ Changelog
47
- - Added context managers support
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
- ![Protocol Version](https://img.shields.io/badge/version-0.3.3-blue?style=for-the-badge)
6
- ![Development Status](https://img.shields.io/badge/status-beta-orange?style=for-the-badge)
5
+ ![Protocol Version](https://img.shields.io/badge/version-0.4.0-blue?style=for-the-badge)
6
+ ![Development Status](https://img.shields.io/badge/status-alpha-red?style=for-the-badge)
7
7
  ![License](https://img.shields.io/badge/license-MIT-green?style=for-the-badge)
8
8
  ![Python](https://img.shields.io/badge/python-3.7+-blue?style=for-the-badge&logo=python&logoColor=white)
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 # Main framework file/core
124
- └── wrapper.py # Synchronous wrapper
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
  [![GitHub](https://img.shields.io/badge/github-%23121011.svg?style=for-the-badge&logo=github&logoColor=white)](https://github.com/slpuk/easyhttp-python)
4
- ![Protocol Version](https://img.shields.io/badge/version-0.3.3-blue?style=for-the-badge)
5
- ![Development Status](https://img.shields.io/badge/status-beta-orange?style=for-the-badge)
4
+ ![Protocol Version](https://img.shields.io/badge/version-0.4.0-blue?style=for-the-badge)
5
+ ![Development Status](https://img.shields.io/badge/status-alpha-red?style=for-the-badge)
6
6
  ![License](https://img.shields.io/badge/license-MIT-green?style=for-the-badge)
7
7
  ![Python](https://img.shields.io/badge/python-3.7+-blue?style=for-the-badge&logo=python&logoColor=white)
8
8
 
9
9
  > **A lightweight HTTP-based P2P framework for IoT and device-to-device communication**
10
10
 
11
11
  ## 🛠️ Changelog
12
- - Added context managers support
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,6 @@
1
+ from .core import EasyHTTPAsync
2
+ from .wrapper import EasyHTTP
3
+
4
+ __version__ = EasyHTTPAsync.__version__
5
+ __author__ = "slpuk"
6
+ __all__ = ["EasyHTTPAsync", "EasyHTTP"]
@@ -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
- __version__ = "0.3.3"
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() # Ping another device
29
- PONG = auto() # Anwser for PING
41
+ PING = auto() # Ping another device
42
+ PONG = auto() # Anwser for PING
30
43
  FETCH = auto() # Request data from another device
31
- DATA = auto() # Response containing data
32
- PUSH = auto() # Send data to another device
33
- ACK = auto() # Acknowledge successful command
34
- NACK = auto() # Indicate an error occurred
35
-
36
- def __init__(self, debug: bool = False, port: int = 5000, config_file=None):
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
- if getattr(sys, 'frozen', False):
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
- 'on_ping': None,
60
- 'on_pong': None,
61
- 'on_fetch': None,
62
- 'on_data': None,
63
- 'on_push': None
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('/easyhttp/api')(self.api_handler)
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, 'r') as f:
111
+ with open(self.config_file, "r") as f:
84
112
  data = json.load(f)
85
- self.id = data.get('device_id')
86
-
113
+ self.id = data.get("device_id")
114
+
87
115
  if self.debug and self.id:
88
- print(f"\033[32mINFO\033[0m:\t Loaded ID: {self.id} from {self.config_file}")
116
+ log.info(f"Loaded ID: {self.id} from {self.config_file}")
89
117
  except Exception as e:
90
118
  if self.debug:
91
- print(f"\033[31mERROR\033[0m:\t Error loading config: {e}")
92
-
119
+ log.error(f"Error loading config: {e}")
120
+
93
121
  def _save_config(self):
94
122
  try:
95
- config = {
96
- 'device_id': self.id,
97
- 'port': self.port,
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
- print(f"\033[32mINFO\033[0m:\t Saved ID to {self.config_file}")
129
+ log.info(f"Saved ID to {self.config_file}")
106
130
  except Exception as e:
107
131
  if self.debug:
108
- print(f"\033[31mERROR\033[0m:\t Error saving config: {e}")
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(('8.8.8.8', 80))
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 = ''.join(secrets.choice(alphabet) for _ in range(length))
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
- 'ip': device_ip,
171
- 'port': int(device_port),
172
- 'last_seen': time.time(),
173
- 'added_manually': True
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
- print(f"DEBUG:\t Added device {device_id}: {device_ip}:{device_port}")
200
+ log.debug(f"Added device {device_id}: {device_ip}:{device_port}")
177
201
  else:
178
- print(f"DEBUG:\t Device already exists")
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('werkzeug').disabled = True
199
- logging.getLogger('uvicorn.error').propagate = False
200
- logging.getLogger('uvicorn.access').propagate = False
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
- print(f"\033[32mINFO\033[0m:\t \033[1;32mEasyHTTP \033[37m{__version__}\033[0m has been started!")
206
- print(f"\033[32mINFO\033[0m:\t Device's ID: {self.id}")
207
- print(f"\033[32mINFO\033[0m:\t EasyHTTP starting on port {self.port}")
208
- print(f"\033[32mINFO\033[0m:\t API running on \033[1mhttp://{self._get_local_ip()}:{self.port}/easyhttp/api\033[0m")
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
- print(f"\033[31mERROR\033[0m:\t Failed to start server: {e}")
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 send(self, device_id: str, command_type: Union[int, 'commands'], data: Optional[Any] = None) -> Optional[dict]:
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
- print(f"\033[31mERROR\033[0m:\t Device {device_id} not found in devices cache")
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": command_type if isinstance(command_type, self.commands) else command_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['data'] = data
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(recipient_url, json=packet, timeout=3) as response:
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
- print(f'\033[31mERROR\033[0m:\t Failed to send to {device_id}: {e}')
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('type') == self.commands.PONG.value:
351
+
352
+ if response and response.get("type") == self.commands.PONG.value:
286
353
  if self.debug:
287
- print(f"\033[32mPING\033[0m:\t {device_id} is online")
354
+ log.custom("PING", Colors.GREEN, f"{device_id} is online")
288
355
  if device_id in self.devices:
289
- self.devices[device_id]['last_seen'] = time.time()
356
+ self.devices[device_id]["last_seen"] = time.time()
290
357
  return True
291
358
  else:
292
359
  if self.debug:
293
- print(f"\033[31mPING\033[0m:\t {device_id} is offline or not responding")
360
+ log.custom("PING", Colors.RED, f"{device_id} is offline or not responding")
294
361
  return False
295
362
 
296
- async def fetch(self, device_id: str, query: Optional[Any] = None) -> Optional[dict]:
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('type') == self.commands.ACK.value:
399
+ if response and response.get("type") == self.commands.ACK.value:
331
400
  if self.debug:
332
- print(f"\033[32mPUSH\033[0m:\t Successfully wrote to {device_id}")
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
- print(f"\033[31mPUSH\033[0m:\t Error writing to {device_id}")
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('type')
358
- header = data.get('header', {})
359
- sender_id = header.get('sender_id')
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
- 'ip': client_ip,
366
- 'port': header.get('sender_port', self.port),
367
- 'last_seen': int(time.time())
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['on_ping']:
373
- callback = self.callbacks['on_ping']
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
- sender_id=sender_id,
382
- timestamp=header.get('timestamp')
383
- )
384
-
385
- return JSONResponse({
386
- "version": __version__,
387
- "type": self.commands.PONG.value,
388
- "header": {
389
- "sender_id": self.id,
390
- "sender_port": self.port,
391
- "recipient_id": sender_id,
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['on_pong']:
399
- callback = self.callbacks['on_pong']
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
- print(f"\033[32mPONG\033[0m:\t Received from {sender_id}")
475
+ log.custom("PONG", Colors.GREEN, f"Received from {sender_id}")
413
476
  if sender_id in self.devices:
414
- self.devices[sender_id]['last_seen'] = time.time()
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['on_fetch']:
420
- callback = self.callbacks['on_fetch']
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('data'),
425
- timestamp=header.get('timestamp')
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('data'),
431
- timestamp=header.get('timestamp')
493
+ query=data.get("data"),
494
+ timestamp=header.get("timestamp"),
432
495
  )
433
496
  if response_data:
434
- return JSONResponse({
435
- "version": __version__,
436
- "type": self.commands.DATA.value,
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
- }, status_code=400)
526
+ status_code=400,
527
+ )
460
528
 
461
- callback = self.callbacks['on_push']
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('data'),
466
- timestamp=header.get('timestamp')
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('data'),
472
- timestamp=header.get('timestamp')
539
+ data=data.get("data"),
540
+ timestamp=header.get("timestamp"),
473
541
  )
474
-
542
+
475
543
  if success:
476
- return JSONResponse({
477
- "version": __version__,
478
- "type": self.commands.ACK.value,
479
- "header": {
480
- "sender_id": self.id,
481
- "sender_port": self.port,
482
- "recipient_id": sender_id,
483
- "timestamp": int(time.time())
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
- "version": __version__,
489
- "type": self.commands.NACK.value,
490
- "header": {
491
- "sender_id": self.id,
492
- "sender_port": self.port,
493
- "recipient_id": sender_id,
494
- "timestamp": int(time.time())
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['on_data']:
501
- callback = self.callbacks['on_data']
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('data'),
506
- timestamp=header.get('timestamp')
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('data'),
512
- timestamp=header.get('timestamp')
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, __version__
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__(self, debug: bool = False, port: int = 5000, config_file: Optional[str] = None):
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(debug=debug, port=port, config_file=config_file)
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(self, device_id: str, command_type: Any, data: Optional[Any] = None) -> Optional[dict]:
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
- self._core.ping(device_id)
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
- self._core.fetch(device_id, query)
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.3
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 :: 4 - Beta
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
  [![GitHub](https://img.shields.io/badge/github-%23121011.svg?style=for-the-badge&logo=github&logoColor=white)](https://github.com/slpuk/easyhttp-python)
39
- ![Protocol Version](https://img.shields.io/badge/version-0.3.3-blue?style=for-the-badge)
40
- ![Development Status](https://img.shields.io/badge/status-beta-orange?style=for-the-badge)
40
+ ![Protocol Version](https://img.shields.io/badge/version-0.4.0-blue?style=for-the-badge)
41
+ ![Development Status](https://img.shields.io/badge/status-alpha-red?style=for-the-badge)
41
42
  ![License](https://img.shields.io/badge/license-MIT-green?style=for-the-badge)
42
43
  ![Python](https://img.shields.io/badge/python-3.7+-blue?style=for-the-badge&logo=python&logoColor=white)
43
44
 
44
45
  > **A lightweight HTTP-based P2P framework for IoT and device-to-device communication**
45
46
 
46
47
  ## 🛠️ Changelog
47
- - Added context managers support
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)**
@@ -3,6 +3,7 @@ README.md
3
3
  README_PY.md
4
4
  pyproject.toml
5
5
  easyhttp_python/__init__.py
6
+ easyhttp_python/_discovery.py
6
7
  easyhttp_python/core.py
7
8
  easyhttp_python/wrapper.py
8
9
  easyhttp_python.egg-info/PKG-INFO
@@ -1,6 +1,7 @@
1
1
  fastapi>=0.103.2
2
2
  uvicorn[standard]>=0.22.0
3
3
  aiohttp>=3.7.0
4
+ loggity>=0.5.0a6
4
5
 
5
6
  [dev]
6
7
  pytest>=6.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "easyhttp-python"
7
- version = "0.3.3"
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 :: 4 - Beta",
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]
@@ -1,9 +0,0 @@
1
- from .core import EasyHTTPAsync
2
- from .wrapper import EasyHTTP
3
-
4
- __version__ = "0.3.3"
5
- __author__ = "slpuk"
6
- __all__ = [
7
- "EasyHTTPAsync",
8
- "EasyHTTP"
9
- ]