easyhttp-python 0.3.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
easyhttp/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ from .core import EasyHTTPAsync
2
+ from .wrapper import EasyHTTP
3
+
4
+ __version__ = "0.3.2"
5
+ __author__ = "slpuk"
6
+ __all__ = [
7
+ "EasyHTTPAsync",
8
+ "EasyHTTP"
9
+ ]
easyhttp/core.py ADDED
@@ -0,0 +1,517 @@
1
+ """EasyHTTP - Simple HTTP-based P2P framework for IoT."""
2
+
3
+ import os
4
+ import secrets
5
+ import time
6
+ import logging
7
+ import json
8
+ from pathlib import Path
9
+ from enum import Enum, auto
10
+ from typing import Optional, Union, Dict, Any, Callable
11
+
12
+ # API libraries
13
+ import aiohttp
14
+ import asyncio
15
+ import socket
16
+ import uvicorn
17
+ from fastapi import FastAPI, Request
18
+ from fastapi.responses import JSONResponse
19
+
20
+ __version__ = "0.3.2"
21
+
22
+ class EasyHTTPAsync:
23
+ """Simple asynchronous HTTP-based core of P2P framework for IoT."""
24
+
25
+ class commands(Enum):
26
+ """Enumeration of available command types."""
27
+
28
+ PING = auto() # Ping another device
29
+ PONG = auto() # Anwser for PING
30
+ 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):
37
+ """Initialize the EasyHTTPAsync instance.
38
+
39
+ Args:
40
+ debug: Enable debug output. Defaults to False.
41
+ port: Port to run the HTTP server on. Defaults to 5000.
42
+ """
43
+
44
+ self.debug = debug
45
+ self.port = port
46
+
47
+ if config_file:
48
+ self.config_file = config_file
49
+ else:
50
+ import sys
51
+ if getattr(sys, 'frozen', False):
52
+ base_dir = os.path.dirname(sys.executable)
53
+ else:
54
+ base_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
55
+ self.config_file = os.path.join(base_dir, "easyhttp_device.json")
56
+
57
+ self.id = None
58
+ self.callbacks = {
59
+ 'on_ping': None,
60
+ 'on_pong': None,
61
+ 'on_fetch': None,
62
+ 'on_data': None,
63
+ 'on_push': None
64
+ }
65
+ self.devices = {}
66
+ self.app = FastAPI(title="EasyHTTP API", docs_url=None, redoc_url=None)
67
+ self.app.post('/easyhttp/api')(self.api_handler)
68
+ self.server_task = None
69
+ self._load_config()
70
+
71
+ async def __aenter__(self):
72
+ """Enter the async context manager."""
73
+ await self.start()
74
+ return self
75
+
76
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
77
+ """Exit the async context manager."""
78
+ await self.stop()
79
+
80
+ def _load_config(self):
81
+ try:
82
+ if os.path.exists(self.config_file):
83
+ with open(self.config_file, 'r') as f:
84
+ data = json.load(f)
85
+ self.id = data.get('device_id')
86
+
87
+ if self.debug and self.id:
88
+ print(f"\033[32mINFO\033[0m:\t Loaded ID: {self.id} from {self.config_file}")
89
+ except Exception as e:
90
+ if self.debug:
91
+ print(f"\033[31mERROR\033[0m:\t Error loading config: {e}")
92
+
93
+ def _save_config(self):
94
+ 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:
102
+ json.dump(config, f, indent=2)
103
+
104
+ if self.debug:
105
+ print(f"\033[32mINFO\033[0m:\t Saved ID to {self.config_file}")
106
+ except Exception as e:
107
+ if self.debug:
108
+ print(f"\033[31mERROR\033[0m:\t Error saving config: {e}")
109
+
110
+ def _get_local_ip(self):
111
+ try:
112
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
113
+ s.connect(('8.8.8.8', 80))
114
+ local_ip = s.getsockname()[0]
115
+ s.close()
116
+ return local_ip
117
+ except:
118
+ try:
119
+ return socket.gethostbyname(socket.gethostname())
120
+ except:
121
+ return "127.0.0.1"
122
+
123
+ def _generate_id(self, length: int = 6) -> str:
124
+ """Generate a unique device ID with a custom alphabet.
125
+
126
+ Args:
127
+ length: Length of the generated ID.
128
+
129
+ Returns:
130
+ The generated unique ID.
131
+ """
132
+
133
+ alphabet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"
134
+ self.id = ''.join(secrets.choice(alphabet) for _ in range(length))
135
+ self._save_config()
136
+
137
+ def on(self, event: str, callback_func: Callable) -> None:
138
+ """Register a callback function for a specific event.
139
+
140
+ Args:
141
+ event: Event name ('on_ping', 'on_fetch', etc.).
142
+ callback_func: Function to call when the event occurs.
143
+
144
+ Raises:
145
+ ValueError: If the event is unknown.
146
+ """
147
+
148
+ if event in self.callbacks:
149
+ self.callbacks[event] = callback_func
150
+ else:
151
+ raise ValueError(f"Unknown event: {event}")
152
+
153
+ def add(self, device_id: str, device_ip: str, device_port: int) -> None:
154
+ """Manually add a device to the local devices cache.
155
+
156
+ Args:
157
+ device_id: 6-character device identifier.
158
+ device_ip: IP address of the device.
159
+ device_port: Port number of the device.
160
+
161
+ Raises:
162
+ ValueError: If device_id is not 6 characters.
163
+ """
164
+
165
+ if len(device_id) != 6:
166
+ raise ValueError("Device ID must be 6 characters")
167
+
168
+ if device_id not in self.devices:
169
+ self.devices[device_id] = {
170
+ 'ip': device_ip,
171
+ 'port': int(device_port),
172
+ 'last_seen': time.time(),
173
+ 'added_manually': True
174
+ }
175
+ if self.debug:
176
+ print(f"DEBUG:\t Added device {device_id}: {device_ip}:{device_port}")
177
+ else:
178
+ print(f"DEBUG:\t Device already exists")
179
+
180
+ async def start(self) -> None:
181
+ """Start the HTTP server and generate a device ID if not set."""
182
+
183
+ if not self.id:
184
+ self._generate_id()
185
+
186
+ try:
187
+ config = uvicorn.Config(
188
+ self.app,
189
+ host="0.0.0.0",
190
+ port=self.port,
191
+ log_level="warning",
192
+ lifespan="off"
193
+ )
194
+
195
+ server = uvicorn.Server(config)
196
+ self.server_task = asyncio.create_task(server.serve())
197
+
198
+ logging.getLogger('werkzeug').disabled = True
199
+ logging.getLogger('uvicorn.error').propagate = False
200
+ logging.getLogger('uvicorn.access').propagate = False
201
+
202
+ await asyncio.sleep(2) # Give server time to start
203
+
204
+ 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
+
210
+ except Exception as e:
211
+ print(f"\033[31mERROR\033[0m:\t Failed to start server: {e}")
212
+ raise
213
+
214
+ async def stop(self) -> None:
215
+ """Gracefully stop the HTTP server and cancel the server task."""
216
+
217
+ if self.server_task:
218
+ self.server_task.cancel()
219
+ try:
220
+ await self.server_task
221
+ except asyncio.CancelledError:
222
+ pass
223
+
224
+ async def send(self, device_id: str, command_type: Union[int, 'commands'], data: Optional[Any] = None) -> Optional[dict]:
225
+ """Send a JSON-formatted command to another device.
226
+
227
+ Args:
228
+ device_id: ID of the target device (must be 6 characters).
229
+ command_type: Command type (commands enum member) or its integer value.
230
+ data: JSON-serializable data to send (dict, list, str, or None).
231
+
232
+ Returns:
233
+ Response JSON dict if successful, None otherwise.
234
+ Response typically contains 'type', 'header', and optionally 'data' fields.
235
+
236
+ Note:
237
+ The device must be added to the devices cache before sending.
238
+ """
239
+
240
+ if device_id not in self.devices:
241
+ if self.debug:
242
+ print(f"\033[31mERROR\033[0m:\t Device {device_id} not found in devices cache")
243
+ return None
244
+
245
+ packet = {
246
+ "version": __version__,
247
+ "type": command_type if isinstance(command_type, self.commands) else command_type,
248
+ "header": {
249
+ "sender_id": self.id,
250
+ "sender_port": self.port,
251
+ "recipient_id": device_id,
252
+ "timestamp": int(time.time())
253
+ }
254
+ }
255
+
256
+ if data:
257
+ packet['data'] = data
258
+
259
+ recipient_url = f"http://{self.devices[device_id]['ip']}:{self.devices[device_id]['port']}/easyhttp/api"
260
+
261
+ try:
262
+ async with aiohttp.ClientSession() as session:
263
+ async with session.post(recipient_url, json=packet, timeout=3) as response:
264
+ if response.status == 200:
265
+ return await response.json()
266
+ return None
267
+
268
+ except Exception as e:
269
+ if self.debug:
270
+ print(f'\033[31mERROR\033[0m:\t Failed to send to {device_id}: {e}')
271
+ return None
272
+
273
+ async def ping(self, device_id: str) -> bool:
274
+ """Send a PING request to a device and check if it's online.
275
+
276
+ Args:
277
+ device_id: ID of the device to ping.
278
+
279
+ Returns:
280
+ True if device responded with PONG, False otherwise.
281
+ """
282
+
283
+ response = await self.send(device_id, self.commands.PING.value)
284
+
285
+ if response and response.get('type') == self.commands.PONG.value:
286
+ if self.debug:
287
+ print(f"\033[32mPING\033[0m:\t {device_id} is online")
288
+ if device_id in self.devices:
289
+ self.devices[device_id]['last_seen'] = time.time()
290
+ return True
291
+ else:
292
+ if self.debug:
293
+ print(f"\033[31mPING\033[0m:\t {device_id} is offline or not responding")
294
+ return False
295
+
296
+ async def fetch(self, device_id: str, query: Optional[Any] = None) -> Optional[dict]:
297
+ """Send a FETCH request to another device and return the response.
298
+
299
+ Args:
300
+ device_id: ID of the target device.
301
+ query: Query data to send with the FETCH request.
302
+
303
+ Returns:
304
+ Response data from the device, or None if failed.
305
+ The dict typically contains 'type', 'header', and 'data' fields.
306
+ """
307
+
308
+ response = await self.send(device_id, self.commands.FETCH.value, query)
309
+ return response
310
+
311
+ async def push(self, device_id: str, data: Optional[Any] = None) -> bool:
312
+ """Send data to another device using PUSH command.
313
+
314
+ Args:
315
+ device_id: ID of the target device.
316
+ data: JSON-serializable data to send.
317
+
318
+ Returns:
319
+ True if data was successfully sent and acknowledged, False otherwise.
320
+
321
+ Raises:
322
+ TypeError: If data is not JSON-serializable.
323
+ """
324
+
325
+ if data is not None and not isinstance(data, (dict, list, str)):
326
+ raise TypeError("Data must be JSON-serializable (dict, list, str)")
327
+
328
+ response = await self.send(device_id, self.commands.PUSH, data)
329
+
330
+ if response and response.get('type') == self.commands.ACK.value:
331
+ if self.debug:
332
+ print(f"\033[32mPUSH\033[0m:\t Successfully wrote to {device_id}")
333
+ return True
334
+ else:
335
+ if self.debug:
336
+ print(f"\033[31mPUSH\033[0m:\t Error writing to {device_id}")
337
+ return False
338
+
339
+ async def api_handler(self, request: Request) -> JSONResponse:
340
+ """Handle incoming API requests and route commands to callbacks.
341
+
342
+ Args:
343
+ request: FastAPI request object.
344
+
345
+ Returns:
346
+ JSONResponse: Response to the client.
347
+ """
348
+
349
+ try:
350
+ data = await request.json()
351
+ except:
352
+ return JSONResponse({"error": "Invalid JSON data"}, status_code=400)
353
+
354
+ if not data:
355
+ 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
+
361
+ client_ip = request.client.host if request.client else "0.0.0.0"
362
+
363
+ if sender_id and sender_id != self.id and sender_id not in self.devices:
364
+ self.devices[sender_id] = {
365
+ 'ip': client_ip,
366
+ 'port': header.get('sender_port', self.port),
367
+ 'last_seen': int(time.time())
368
+ }
369
+
370
+ # Handle PING response
371
+ if command_type == self.commands.PING.value:
372
+ if self.callbacks['on_ping']:
373
+ callback = self.callbacks['on_ping']
374
+ if asyncio.iscoroutinefunction(callback):
375
+ await callback(
376
+ sender_id=sender_id,
377
+ timestamp=header.get('timestamp')
378
+ )
379
+ 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())
393
+ }
394
+ })
395
+
396
+ # Handle PONG answer
397
+ elif command_type == self.commands.PONG.value:
398
+ if self.callbacks['on_pong']:
399
+ callback = self.callbacks['on_pong']
400
+ if asyncio.iscoroutinefunction(callback):
401
+ await callback(
402
+ sender_id=sender_id,
403
+ timestamp=header.get('timestamp')
404
+ )
405
+ else:
406
+ callback(
407
+ sender_id=sender_id,
408
+ timestamp=header.get('timestamp')
409
+ )
410
+
411
+ if self.debug:
412
+ print(f"\033[32mPONG\033[0m:\t Received from {sender_id}")
413
+ if sender_id in self.devices:
414
+ self.devices[sender_id]['last_seen'] = time.time()
415
+ return JSONResponse({"status": "pong_received"})
416
+
417
+ # Handle FETCH response
418
+ elif command_type == self.commands.FETCH.value:
419
+ if self.callbacks['on_fetch']:
420
+ callback = self.callbacks['on_fetch']
421
+ if asyncio.iscoroutinefunction(callback):
422
+ response_data = await callback (
423
+ sender_id=sender_id,
424
+ query=data.get('data'),
425
+ timestamp=header.get('timestamp')
426
+ )
427
+ else:
428
+ response_data = callback (
429
+ sender_id=sender_id,
430
+ query=data.get('data'),
431
+ timestamp=header.get('timestamp')
432
+ )
433
+ if response_data:
434
+ return JSONResponse({
435
+ "version": __version__,
436
+ "type": self.commands.DATA.value,
437
+ "header": {
438
+ "sender_id": self.id,
439
+ "sender_port": self.port,
440
+ "recipient_id": sender_id,
441
+ "timestamp": int(time.time())
442
+ },
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
+ },
459
+ }, status_code=400)
460
+
461
+ callback = self.callbacks['on_push']
462
+ if asyncio.iscoroutinefunction(callback):
463
+ success = await callback(
464
+ sender_id=sender_id,
465
+ data=data.get('data'),
466
+ timestamp=header.get('timestamp')
467
+ )
468
+ else:
469
+ success = callback(
470
+ sender_id=sender_id,
471
+ data=data.get('data'),
472
+ timestamp=header.get('timestamp')
473
+ )
474
+
475
+ 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())
484
+ }
485
+ })
486
+ 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())
495
+ }
496
+ })
497
+
498
+ # Handle DATA
499
+ elif command_type == self.commands.DATA.value:
500
+ if self.callbacks['on_data']:
501
+ callback = self.callbacks['on_data']
502
+ if asyncio.iscoroutinefunction(callback):
503
+ await callback(
504
+ sender_id=sender_id,
505
+ data=data.get('data'),
506
+ timestamp=header.get('timestamp')
507
+ )
508
+ else:
509
+ callback(
510
+ sender_id=sender_id,
511
+ data=data.get('data'),
512
+ timestamp=header.get('timestamp')
513
+ )
514
+ return JSONResponse({"status": "data_received"})
515
+
516
+ # Handle unknown command types
517
+ return JSONResponse({"error": "Unknown command type"}, status_code=400)
easyhttp/wrapper.py ADDED
@@ -0,0 +1,153 @@
1
+ """EasyHTTP - Simple HTTP-based P2P framework for IoT."""
2
+
3
+ import asyncio
4
+ from typing import Optional, Any, Callable
5
+ from .core import EasyHTTPAsync, __version__
6
+
7
+ class EasyHTTP:
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):
11
+ """Initialize the EasyHTTP instance.
12
+
13
+ Args:
14
+ debug: Enable debug output. Defaults to False.
15
+ port: Port to run the HTTP server on. Defaults to 5000.
16
+ """
17
+
18
+ self._core = EasyHTTPAsync(debug=debug, port=port, config_file=config_file)
19
+ self._loop = None
20
+ self._running = False
21
+
22
+ self.commands = self._core.commands
23
+ self.__version__ = __version__
24
+
25
+ def _ensure_loop(self):
26
+ """Ensure event loop is running."""
27
+ if not self._loop:
28
+ self._loop = asyncio.new_event_loop()
29
+ asyncio.set_event_loop(self._loop)
30
+
31
+ def on(self, event: str, callback_func: Callable) -> None:
32
+ """Register a callback function for a specific event.
33
+
34
+ Args:
35
+ event: Event name ('on_ping', 'on_fetch', etc.).
36
+ callback_func: Function to call when the event occurs.
37
+
38
+ Raises:
39
+ ValueError: If the event is unknown.
40
+ """
41
+ self._core.on(event, callback_func)
42
+
43
+ def add(self, device_id: str, device_ip: str, device_port: int) -> None:
44
+ """Manually add a device to the local devices cache.
45
+
46
+ Args:
47
+ device_id: 6-character device identifier.
48
+ device_ip: IP address of the device.
49
+ device_port: Port number of the device.
50
+
51
+ Raises:
52
+ ValueError: If device_id is not 6 characters.
53
+ """
54
+ self._core.add(device_id, device_ip, device_port)
55
+
56
+ def start(self) -> None:
57
+ """Start the HTTP server and generate a device ID if not set."""
58
+ self._ensure_loop()
59
+ self._loop.run_until_complete(self._core.start())
60
+ self._running = True
61
+
62
+ def stop(self) -> None:
63
+ """Gracefully stop the HTTP server and cancel the server task."""
64
+ if self._running:
65
+ self._loop.run_until_complete(self._core.stop())
66
+ self._running = False
67
+
68
+ def send(self, device_id: str, command_type: Any, data: Optional[Any] = None) -> Optional[dict]:
69
+ """Send a JSON-formatted command to another device.
70
+
71
+ Args:
72
+ device_id: ID of the target device (must be 6 characters).
73
+ command_type: Command type (commands enum member) or its integer value.
74
+ data: JSON-serializable data to send (dict, list, str, or None).
75
+
76
+ Returns:
77
+ Response JSON dict if successful, None otherwise.
78
+ Response typically contains 'type', 'header', and optionally 'data' fields.
79
+
80
+ Note:
81
+ The device must be added to the devices cache before sending.
82
+ """
83
+ if not self._loop:
84
+ self._ensure_loop()
85
+ return self._loop.run_until_complete(
86
+ self._core.send(device_id, command_type, data)
87
+ )
88
+
89
+ def ping(self, device_id: str) -> bool:
90
+ """Send a PING request to a device and check if it's online.
91
+
92
+ Args:
93
+ device_id: ID of the device to ping.
94
+
95
+ Returns:
96
+ True if device responded with PONG, False otherwise.
97
+ """
98
+ return self._loop.run_until_complete(
99
+ self._core.ping(device_id)
100
+ )
101
+
102
+ def fetch(self, device_id: str, query: Optional[Any] = None) -> Optional[dict]:
103
+ """Send a FETCH request to another device and return the response.
104
+
105
+ Args:
106
+ device_id: ID of the target device.
107
+ query: Query data to send with the FETCH request.
108
+
109
+ Returns:
110
+ Response data from the device, or None if failed.
111
+ The dict typically contains 'type', 'header', and 'data' fields.
112
+ """
113
+ return self._loop.run_until_complete(
114
+ self._core.fetch(device_id, query)
115
+ )
116
+
117
+ def push(self, device_id: str, data: Optional[Any] = None) -> bool:
118
+ """Send data to another device using PUSH command.
119
+
120
+ Args:
121
+ device_id: ID of the target device.
122
+ data: JSON-serializable data to send.
123
+
124
+ Returns:
125
+ True if data was successfully sent and acknowledged, False otherwise.
126
+
127
+ Raises:
128
+ TypeError: If data is not JSON-serializable.
129
+ """
130
+ return self._loop.run_until_complete(
131
+ self._core.push(device_id, data)
132
+ )
133
+
134
+ # Context manager support
135
+ def __enter__(self):
136
+ """Enter the sync context manager."""
137
+ self.start()
138
+ return self
139
+
140
+ def __exit__(self, exc_type, exc_val, exc_tb):
141
+ """Exit the sync context manager."""
142
+ self.stop()
143
+
144
+ # Property accessors
145
+ @property
146
+ def id(self) -> str:
147
+ """Get device ID."""
148
+ return self._core.id
149
+
150
+ @property
151
+ def devices(self) -> dict:
152
+ """Get devices cache."""
153
+ return self._core.devices
@@ -0,0 +1,152 @@
1
+ Metadata-Version: 2.4
2
+ Name: easyhttp-python
3
+ Version: 0.3.2
4
+ Summary: Simple HTTP-based P2P framework for IoT
5
+ Author-email: slpuk <yarik6052@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/slpuk/easyhttp-python
8
+ Project-URL: Documentation, https://github.com/slpuk/easyhttp-python#readme
9
+ Project-URL: Repository, https://github.com/slpuk/easyhttp-python
10
+ Project-URL: Issue Tracker, https://github.com/slpuk/easyhttp-python/issues
11
+ Keywords: iot,p2p,http,framework
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Classifier: Topic :: Communications
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.7
19
+ Classifier: Programming Language :: Python :: 3.8
20
+ Classifier: Programming Language :: Python :: 3.9
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Requires-Python: >=3.7
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: fastapi>=0.103.2
28
+ Requires-Dist: uvicorn[standard]>=0.22.0
29
+ Requires-Dist: aiohttp>=3.7.0
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest>=6.0; extra == "dev"
32
+ Requires-Dist: black; extra == "dev"
33
+ Requires-Dist: flake8; extra == "dev"
34
+ Dynamic: license-file
35
+
36
+ # EasyHTTP
37
+
38
+ [![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.2-blue?style=for-the-badge)
40
+ ![Development Status](https://img.shields.io/badge/status-beta-orange?style=for-the-badge)
41
+ ![License](https://img.shields.io/badge/license-MIT-green?style=for-the-badge)
42
+ ![Python](https://img.shields.io/badge/python-3.7+-blue?style=for-the-badge&logo=python&logoColor=white)
43
+
44
+ > **A lightweight HTTP-based P2P framework for IoT and device-to-device communication**
45
+
46
+ ## 🛠️ Changelog
47
+ - Added context managers support
48
+ - Fixed some bugs
49
+
50
+ ## 📖 About
51
+
52
+ **EasyHTTP** is a simple yet powerful framework with asynchronous core that enables P2P (peer-to-peer) communication between devices using plain HTTP.
53
+
54
+ ### Key Features:
55
+ - **🔄 P2P Architecture** - No central server required
56
+ - **🧩 Dual API:** `EasyHTTP` (synchronous) and `EasyHTTPAsync` (asynchronous) with the same methods
57
+ - **📡 Event-Driven Communication** - Callback-based architecture
58
+ - **🆔 Human-Readable Device IDs** - Base32 identifiers instead of IP addresses
59
+ - **✅ Easy to Use** - Simple API with minimal setup
60
+ - **🚀 Performance** - Asynchronous code and lightweight libraries(FastAPI/aiohttp)
61
+
62
+
63
+ ## 🏗️ Architecture
64
+
65
+ ### Device Identification
66
+ Instead of using hard-to-remember IP addresses, each device in the EasyHTTP network has a unique 6-character identifier:
67
+
68
+ - **Format**: 6 characters from Base32 alphabet (without ambiguous characters)
69
+ - **Alphabet**: `23456789ABCDEFGHJKLMNPQRSTUVWXYZ`
70
+ - **Examples**: `7H8G2K`, `AB3F9Z`, `X4R7T2`
71
+ - **Generation**: Randomly generated on first boot, stored in device configuration
72
+
73
+ ### Command System
74
+ EasyHTTP uses a simple JSON-based command system:
75
+
76
+ | Command | Value | Description |
77
+ |---------|-------|-------------|
78
+ | `PING` | 1 | Check if another device is reachable |
79
+ | `PONG` | 2 | Response to ping request |
80
+ | `FETCH` | 3 | Request data from a device |
81
+ | `DATA` | 4 | Send data or answer to FETCH |
82
+ | `PUSH` | 5 | Request to write/execute on remote device |
83
+ | `ACK` | 6 | Success/confirmation |
84
+ | `NACK` | 7 | Error/reject |
85
+
86
+ ### Basic Example with Callbacks (synchronous)
87
+ ```python
88
+ import time
89
+ from easyhttp import EasyHTTP
90
+
91
+ # Callback function
92
+ def handle_data(sender_id, data, timestamp):
93
+ # Callback for incoming DATA responses
94
+ print(f"From {sender_id}: {data}")
95
+
96
+ def handle_fetch(sender_id, query, timestamp):
97
+ # Callback for FETCH requests - returns data when someone requests it
98
+ print(f"FETCH request from {sender_id}")
99
+ return {
100
+ "temperature": 23.5,
101
+ "humidity": 45,
102
+ "status": "normal",
103
+ "timestamp": timestamp
104
+ }
105
+
106
+ def handle_push(sender_id, data, timestamp):
107
+ # Callback for PUSH requests - handle control commands
108
+ print(f"Control from {sender_id}: {data}")
109
+ if data and data.get("command") == "led":
110
+ state = data.get("state", "off")
111
+ print(f"[CONTROL] Turning LED {state}")
112
+ # Here you can add real GPIO control
113
+ return True # Successful → ACK
114
+ return False # Error → NACK
115
+
116
+ def main():
117
+ # Initializing EasyHTTP - sync wrapper of EasyHTTPAsync
118
+ easy = EasyHTTP(debug=True, port=5000)
119
+
120
+ # Setting up callback functions
121
+ easy.on('on_ping', handle_ping)
122
+ easy.on('on_pong', handle_pong)
123
+ easy.on('on_fetch', handle_fetch)
124
+ easy.on('on_data', handle_data)
125
+ easy.on('on_push', handle_push)
126
+
127
+ easy.start() # Starting server
128
+ print(f"Device {easy.id} is running on port 5000!")
129
+
130
+ # Adding device
131
+ easy.add("ABC123", "192.168.1.100", 5000)
132
+ print("Added device ABC123")
133
+
134
+ # Monitoring device's status
135
+ try:
136
+ while True:
137
+ if easy.ping("ABC123"):
138
+ print("Device ABC123 is online")
139
+ else:
140
+ print("Device ABC123 is offline")
141
+
142
+ time.sleep(5)
143
+
144
+ except KeyboardInterrupt:
145
+ print("\nStopping device...")
146
+ easy.stop() # Stopping server
147
+
148
+ # Starting main process
149
+ if __name__ == "__main__":
150
+ main()
151
+ ```
152
+ **More examples available on [GitHub](https://github.com/slpuk/easyhttp-python)**
@@ -0,0 +1,8 @@
1
+ easyhttp/__init__.py,sha256=1MUoeXjG1dWEDDM_i8BrrpNwFu4EF3tos16LY6VAytc,163
2
+ easyhttp/core.py,sha256=Dr1mnn6C4qiOVmDtJMtjA4AxyFr4oIg6hWa7yrS0of8,19132
3
+ easyhttp/wrapper.py,sha256=2rA7I5eJR_x31YWfUIxzssqlfzE2egihsmw7PVwg0a0,5281
4
+ easyhttp_python-0.3.2.dist-info/licenses/LICENSE,sha256=nkDB7rjnRVh38AZftJtY1IEwTNYqzt9-1csUHIRZqGI,1083
5
+ easyhttp_python-0.3.2.dist-info/METADATA,sha256=e205jFToQrxJvMbtbqRUr78LxO4ylTefViFPyrTgeuU,5804
6
+ easyhttp_python-0.3.2.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
7
+ easyhttp_python-0.3.2.dist-info/top_level.txt,sha256=hXtOmZbZY_T3tJWgX_q2py0BhG48txcA16eH6ZawTqE,9
8
+ easyhttp_python-0.3.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 slpuk
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ easyhttp