py-easysync 0.1.0__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.
@@ -0,0 +1,123 @@
1
+ Metadata-Version: 2.4
2
+ Name: py-easysync
3
+ Version: 0.1.0
4
+ Summary: Universal real-time state synchronization for Python.
5
+ Home-page: https://github.com/GalTechDev/easysync
6
+ Author: GalTechDev
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Topic :: System :: Networking
11
+ Classifier: Topic :: Software Development :: Libraries
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ Dynamic: author
15
+ Dynamic: classifier
16
+ Dynamic: description
17
+ Dynamic: description-content-type
18
+ Dynamic: home-page
19
+ Dynamic: requires-python
20
+ Dynamic: summary
21
+
22
+ # EasySync
23
+
24
+ Universal real-time state synchronization for Python.
25
+
26
+ ![Python](https://img.shields.io/badge/Python-3.10+-blue.svg)
27
+ ![License](https://img.shields.io/badge/License-MIT-yellow.svg)
28
+
29
+ Manipulate your Python objects as if the network didn't exist. EasySync intercepts attribute mutations through a transparent proxy and propagates them instantly to all machines connected to the server.
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ pip install easysync
35
+ ```
36
+
37
+ ## Quick Start
38
+
39
+ **Server** (hosts the shared state):
40
+
41
+ ```python
42
+ from easysync import SyncedObject, SyncServer, connect
43
+
44
+ server = SyncServer(port=5000)
45
+ server.start_thread()
46
+
47
+ client = connect("127.0.0.1", 5000)
48
+
49
+ @SyncedObject(client)
50
+ class GameState:
51
+ def __init__(self):
52
+ self.score = 0
53
+ self.players = []
54
+
55
+ state = GameState()
56
+ state.score = 42 # propagated to all clients
57
+ state.players.append("A") # propagated too
58
+ ```
59
+
60
+ **Client** (joins the server):
61
+
62
+ ```python
63
+ from easysync import SyncedObject, connect
64
+
65
+ client = connect("192.168.1.10", 5000)
66
+
67
+ @SyncedObject(client)
68
+ class GameState:
69
+ def __init__(self):
70
+ self.score = 0
71
+ self.players = []
72
+
73
+ state = GameState()
74
+ print(state.score) # 42, updated in real time
75
+ print(state.players) # ['A']
76
+ ```
77
+
78
+ ## Architecture
79
+
80
+ The server uses `asyncio` for non-blocking connection handling, allowing it to support a large number of simultaneous clients without CPU overhead. The client uses a dedicated receive thread to remain compatible with standard application loops (Pygame, Matplotlib, etc.).
81
+
82
+ The wire protocol relies on binary Pickle serialization framed by a 4-byte header (payload size). This ensures measured latency under 20ms.
83
+
84
+ ## Features
85
+
86
+ - **Zero configuration**: a single `@SyncedObject` decorator is all you need.
87
+ - **Transparent proxy**: automatic interception of `__setattr__`, `__setitem__`, `append`, `pop`, etc.
88
+ - **Asyncio server**: built on `asyncio.start_server` for maximum scalability.
89
+ - **Binary serialization**: Pickle + TCP framing protocol, latency < 20ms.
90
+ - **Zero dependencies**: only uses the Python standard library.
91
+ - **Data Science ready**: optimized handling of NumPy, Pandas and Scikit-Learn objects via the copy-and-reassign pattern.
92
+
93
+ ## Examples
94
+
95
+ The `examples/` folder contains several demos:
96
+
97
+ | File | Description |
98
+ |---|---|
99
+ | `pygame_example.py` | Synchronized square between two Pygame windows |
100
+ | `pygame_hanoi.py` | Collaborative Tower of Hanoi |
101
+ | `numpy_matplotlib_example.py` | NumPy data streaming with Matplotlib chart |
102
+ | `pandas_example.py` | Collaborative Pandas spreadsheet |
103
+ | `sklearn_live_training.py` | Live Scikit-Learn training visualization |
104
+ | `federated_learning_example.py` | Distributed federated learning |
105
+ | `genetic_island_example.py` | Distributed genetic algorithm (island model) |
106
+ | `tetris_ai_example.py` | Distributed Tetris AI via genetic algorithm |
107
+
108
+ To run the examples, install the additional dependencies:
109
+
110
+ ```bash
111
+ pip install -r requirements_examples.txt
112
+ ```
113
+
114
+ Then launch a server and one or more clients:
115
+
116
+ ```bash
117
+ python examples/pygame_example.py server # Terminal 1
118
+ python examples/pygame_example.py # Terminal 2
119
+ ```
120
+
121
+ ## License
122
+
123
+ MIT
@@ -0,0 +1,102 @@
1
+ # EasySync
2
+
3
+ Universal real-time state synchronization for Python.
4
+
5
+ ![Python](https://img.shields.io/badge/Python-3.10+-blue.svg)
6
+ ![License](https://img.shields.io/badge/License-MIT-yellow.svg)
7
+
8
+ Manipulate your Python objects as if the network didn't exist. EasySync intercepts attribute mutations through a transparent proxy and propagates them instantly to all machines connected to the server.
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ pip install easysync
14
+ ```
15
+
16
+ ## Quick Start
17
+
18
+ **Server** (hosts the shared state):
19
+
20
+ ```python
21
+ from easysync import SyncedObject, SyncServer, connect
22
+
23
+ server = SyncServer(port=5000)
24
+ server.start_thread()
25
+
26
+ client = connect("127.0.0.1", 5000)
27
+
28
+ @SyncedObject(client)
29
+ class GameState:
30
+ def __init__(self):
31
+ self.score = 0
32
+ self.players = []
33
+
34
+ state = GameState()
35
+ state.score = 42 # propagated to all clients
36
+ state.players.append("A") # propagated too
37
+ ```
38
+
39
+ **Client** (joins the server):
40
+
41
+ ```python
42
+ from easysync import SyncedObject, connect
43
+
44
+ client = connect("192.168.1.10", 5000)
45
+
46
+ @SyncedObject(client)
47
+ class GameState:
48
+ def __init__(self):
49
+ self.score = 0
50
+ self.players = []
51
+
52
+ state = GameState()
53
+ print(state.score) # 42, updated in real time
54
+ print(state.players) # ['A']
55
+ ```
56
+
57
+ ## Architecture
58
+
59
+ The server uses `asyncio` for non-blocking connection handling, allowing it to support a large number of simultaneous clients without CPU overhead. The client uses a dedicated receive thread to remain compatible with standard application loops (Pygame, Matplotlib, etc.).
60
+
61
+ The wire protocol relies on binary Pickle serialization framed by a 4-byte header (payload size). This ensures measured latency under 20ms.
62
+
63
+ ## Features
64
+
65
+ - **Zero configuration**: a single `@SyncedObject` decorator is all you need.
66
+ - **Transparent proxy**: automatic interception of `__setattr__`, `__setitem__`, `append`, `pop`, etc.
67
+ - **Asyncio server**: built on `asyncio.start_server` for maximum scalability.
68
+ - **Binary serialization**: Pickle + TCP framing protocol, latency < 20ms.
69
+ - **Zero dependencies**: only uses the Python standard library.
70
+ - **Data Science ready**: optimized handling of NumPy, Pandas and Scikit-Learn objects via the copy-and-reassign pattern.
71
+
72
+ ## Examples
73
+
74
+ The `examples/` folder contains several demos:
75
+
76
+ | File | Description |
77
+ |---|---|
78
+ | `pygame_example.py` | Synchronized square between two Pygame windows |
79
+ | `pygame_hanoi.py` | Collaborative Tower of Hanoi |
80
+ | `numpy_matplotlib_example.py` | NumPy data streaming with Matplotlib chart |
81
+ | `pandas_example.py` | Collaborative Pandas spreadsheet |
82
+ | `sklearn_live_training.py` | Live Scikit-Learn training visualization |
83
+ | `federated_learning_example.py` | Distributed federated learning |
84
+ | `genetic_island_example.py` | Distributed genetic algorithm (island model) |
85
+ | `tetris_ai_example.py` | Distributed Tetris AI via genetic algorithm |
86
+
87
+ To run the examples, install the additional dependencies:
88
+
89
+ ```bash
90
+ pip install -r requirements_examples.txt
91
+ ```
92
+
93
+ Then launch a server and one or more clients:
94
+
95
+ ```bash
96
+ python examples/pygame_example.py server # Terminal 1
97
+ python examples/pygame_example.py # Terminal 2
98
+ ```
99
+
100
+ ## License
101
+
102
+ MIT
@@ -0,0 +1,10 @@
1
+ from easysync.syncedobject import SyncedObject, SyncedVar, SyncedProxy, connect, get_client
2
+ from easysync.syncserver import SyncServer
3
+ from easysync.syncclient import SyncClient
4
+
5
+ __version__ = "0.1.0"
6
+ __all__ = [
7
+ "SyncedObject", "SyncedVar", "SyncedProxy",
8
+ "SyncServer", "SyncClient",
9
+ "connect", "get_client",
10
+ ]
@@ -0,0 +1,103 @@
1
+ import socket
2
+ import threading
3
+ import pickle
4
+ import struct
5
+
6
+
7
+ class SyncClient:
8
+ """TCP client that connects to a SyncServer to synchronize objects."""
9
+
10
+ def __init__(self, host="localhost", port=5000, auth_payload=None):
11
+ self.host = host
12
+ self.port = port
13
+ self.auth_payload = auth_payload
14
+ self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
15
+ self.connected = False
16
+ self.callbacks = {}
17
+ self.on_sync_request_callback = None
18
+
19
+ def connect(self):
20
+ try:
21
+ self.client_socket.connect((self.host, self.port))
22
+
23
+ self._send_packet({"type": "auth", "payload": self.auth_payload})
24
+ resp = self._recv_packet()
25
+ if not resp:
26
+ print("[EasySync] Connection lost during authentication")
27
+ return
28
+
29
+ if resp.get("type") == "auth_reject":
30
+ print("[EasySync] Authentication rejected by server")
31
+ return
32
+
33
+ self.connected = True
34
+ print(f"[EasySync] Connected to {self.host}:{self.port}")
35
+ self._send_packet({"type": "request_sync"})
36
+
37
+ t = threading.Thread(target=self.receive_loop, daemon=True)
38
+ t.start()
39
+ except Exception as e:
40
+ print(f"[EasySync] Connection failed: {e}")
41
+
42
+ def _send_packet(self, data):
43
+ if self.client_socket:
44
+ raw = pickle.dumps(data)
45
+ self.client_socket.sendall(struct.pack(">I", len(raw)) + raw)
46
+
47
+ def _recv_n_bytes(self, n):
48
+ buf = bytearray()
49
+ while len(buf) < n:
50
+ chunk = self.client_socket.recv(n - len(buf))
51
+ if not chunk:
52
+ return None
53
+ buf.extend(chunk)
54
+ return bytes(buf)
55
+
56
+ def _recv_packet(self):
57
+ header = self._recv_n_bytes(4)
58
+ if not header:
59
+ return None
60
+ length = struct.unpack(">I", header)[0]
61
+ data = self._recv_n_bytes(length)
62
+ if not data:
63
+ return None
64
+ return pickle.loads(data)
65
+
66
+ def send_update(self, object_id, attr_name, value):
67
+ if not self.connected:
68
+ return
69
+ try:
70
+ self._send_packet({
71
+ "type": "update",
72
+ "object_id": object_id,
73
+ "attr_name": attr_name,
74
+ "value": value,
75
+ })
76
+ except Exception as e:
77
+ print(f"[EasySync] Send error: {e}")
78
+
79
+ def register_callback(self, object_id, callback):
80
+ self.callbacks[object_id] = callback
81
+
82
+ def receive_loop(self):
83
+ while self.connected:
84
+ try:
85
+ message = self._recv_packet()
86
+ if message is None:
87
+ print("[EasySync] Disconnected from server")
88
+ self.connected = False
89
+ break
90
+
91
+ msg_type = message.get("type")
92
+ if msg_type == "update":
93
+ oid = message.get("object_id")
94
+ if oid and oid in self.callbacks:
95
+ self.callbacks[oid](message)
96
+ elif msg_type == "request_sync":
97
+ if self.on_sync_request_callback:
98
+ self.on_sync_request_callback()
99
+
100
+ except Exception as e:
101
+ print(f"[EasySync] Network error: {e}")
102
+ self.connected = False
103
+ break
@@ -0,0 +1,303 @@
1
+ import inspect
2
+ import threading
3
+ import time
4
+ import weakref
5
+
6
+ from easysync.syncclient import SyncClient
7
+
8
+ _default_client = None
9
+
10
+
11
+ class SyncedProxy:
12
+ """Transparent proxy that intercepts mutations and triggers the network callback."""
13
+
14
+ def __init__(self, target, callback):
15
+ object.__setattr__(self, "_target", target)
16
+ object.__setattr__(self, "_callback", callback)
17
+
18
+ def __getattribute__(self, name):
19
+ if name in ("_target", "_callback", "_trigger", "__class__",
20
+ "__array__", "__array_struct__", "__array_interface__"):
21
+ if name in ("__array__", "__array_struct__", "__array_interface__"):
22
+ return getattr(object.__getattribute__(self, "_target"), name)
23
+ return object.__getattribute__(self, name)
24
+
25
+ target = object.__getattribute__(self, "_target")
26
+ attr = getattr(target, name)
27
+ callback = object.__getattribute__(self, "_callback")
28
+ return _deep_wrap(attr, callback)
29
+
30
+ def __call__(self, *args, **kwargs):
31
+ target = object.__getattribute__(self, "_target")
32
+ callback = object.__getattribute__(self, "_callback")
33
+ ret = target(*args, **kwargs)
34
+ object.__getattribute__(self, "_trigger")()
35
+ return _deep_wrap(ret, callback)
36
+
37
+ def __setattr__(self, name, value):
38
+ setattr(object.__getattribute__(self, "_target"), name, value)
39
+ object.__getattribute__(self, "_trigger")()
40
+
41
+ def _trigger(self):
42
+ cb = object.__getattribute__(self, "_callback")
43
+ if cb:
44
+ cb()
45
+
46
+ def __getitem__(self, key):
47
+ res = object.__getattribute__(self, "_target")[key]
48
+ return _deep_wrap(res, object.__getattribute__(self, "_callback"))
49
+
50
+ def __setitem__(self, key, value):
51
+ object.__getattribute__(self, "_target")[key] = value
52
+ object.__getattribute__(self, "_trigger")()
53
+
54
+ def __delitem__(self, key):
55
+ del object.__getattribute__(self, "_target")[key]
56
+ object.__getattribute__(self, "_trigger")()
57
+
58
+ def __iter__(self):
59
+ target = object.__getattribute__(self, "_target")
60
+ callback = object.__getattribute__(self, "_callback")
61
+ for item in target:
62
+ yield _deep_wrap(item, callback)
63
+
64
+ def __len__(self):
65
+ return len(object.__getattribute__(self, "_target"))
66
+
67
+ def __repr__(self):
68
+ return repr(object.__getattribute__(self, "_target"))
69
+
70
+ def __str__(self):
71
+ return str(object.__getattribute__(self, "_target"))
72
+
73
+ def __array__(self, *args, **kwargs):
74
+ target = object.__getattribute__(self, "_target")
75
+ if hasattr(target, "__array__"):
76
+ return target.__array__(*args, **kwargs)
77
+ return NotImplemented
78
+
79
+
80
+ def _deep_wrap(value, callback):
81
+ """Recursively wraps an object in a SyncedProxy.
82
+ Immutable types and C-extensions (numpy, pandas) are excluded."""
83
+ import numbers
84
+ if value is None or isinstance(value, (numbers.Number, str, bool, tuple, bytes)):
85
+ return value
86
+ if type(value).__name__ in ("ndarray", "DataFrame", "Series"):
87
+ return value
88
+ if isinstance(value, SyncedProxy):
89
+ object.__setattr__(value, "_callback", callback)
90
+ return value
91
+ return SyncedProxy(value, callback)
92
+
93
+
94
+ def _unproxy(value):
95
+ """Recursively strips proxy layers to get the raw object."""
96
+ if isinstance(value, SyncedProxy):
97
+ return _unproxy(object.__getattribute__(value, "_target"))
98
+ if type(value) is list:
99
+ return [_unproxy(x) for x in value]
100
+ if type(value) is dict:
101
+ return {k: _unproxy(v) for k, v in value.items()}
102
+ return value
103
+
104
+
105
+ # ---------- Global registry and polling loop ----------
106
+
107
+ _master_synced_vars = []
108
+ _master_synced_objects = weakref.WeakSet()
109
+ _master_poller_thread = None
110
+
111
+
112
+ def _master_poller_loop():
113
+ """Single thread that watches for unintercepted local mutations."""
114
+ while True:
115
+ time.sleep(0.05)
116
+ for var in list(_master_synced_vars):
117
+ try:
118
+ if var._updating:
119
+ continue
120
+ current_val = var.namespace.get(var.var_name)
121
+ if current_val != var.last_val:
122
+ var.last_val = current_val
123
+ if var._client:
124
+ var._client.send_update(var.var_name, "value", current_val)
125
+ except Exception:
126
+ pass
127
+
128
+
129
+ def _handle_sync_request():
130
+ """Responds to a resync request from a newly connected client."""
131
+ for var in _master_synced_vars:
132
+ if var._client:
133
+ var._client.send_update(var.var_name, "value", var.last_val)
134
+
135
+ for obj in list(_master_synced_objects):
136
+ try:
137
+ c = object.__getattribute__(obj, "_sync_client")
138
+ oid = object.__getattribute__(obj, "_sync_object_id")
139
+ if c:
140
+ import types
141
+ for attr_name in dir(obj):
142
+ if not attr_name.startswith("_"):
143
+ val = getattr(obj, attr_name)
144
+ if not isinstance(val, types.MethodType):
145
+ c.send_update(oid, attr_name, _unproxy(val))
146
+ except Exception:
147
+ pass
148
+
149
+
150
+ # ---------- Public API ----------
151
+
152
+ def connect(host="localhost", port=5000):
153
+ """Connect to a remote SyncServer and return the client instance."""
154
+ global _default_client
155
+ _default_client = SyncClient(host=host, port=port)
156
+ _default_client.on_sync_request_callback = _handle_sync_request
157
+ _default_client.connect()
158
+ return _default_client
159
+
160
+
161
+ def get_client():
162
+ """Return the default client."""
163
+ return _default_client
164
+
165
+
166
+ def SyncedObject(client=None):
167
+ """Class decorator: synchronizes public attributes over the network."""
168
+ def decorator(cls):
169
+ _object_id = cls.__qualname__
170
+ original_init = cls.__init__ if hasattr(cls, "__init__") else None
171
+
172
+ def new_init(self, *args, _sync_client=None, **kwargs):
173
+ resolved_client = _sync_client or client or _default_client
174
+ object.__setattr__(self, "_sync_client", resolved_client)
175
+ object.__setattr__(self, "_sync_object_id", _object_id)
176
+ object.__setattr__(self, "_sync_updating", False)
177
+
178
+ if resolved_client:
179
+ resolved_client.register_callback(
180
+ _object_id, lambda msg, obj=self: _apply_update(obj, msg)
181
+ )
182
+
183
+ if original_init:
184
+ original_init(self, *args, **kwargs)
185
+
186
+ _master_synced_objects.add(self)
187
+
188
+ def new_setattr(self, name, value):
189
+ if name.startswith("_"):
190
+ object.__setattr__(self, name, value)
191
+ return
192
+
193
+ def trigger_sync():
194
+ try:
195
+ if object.__getattribute__(self, "_sync_updating"):
196
+ return
197
+ except AttributeError:
198
+ pass
199
+ try:
200
+ c = object.__getattribute__(self, "_sync_client")
201
+ oid = object.__getattribute__(self, "_sync_object_id")
202
+ if c:
203
+ unproxied = _unproxy(object.__getattribute__(self, name))
204
+ c.send_update(oid, name, unproxied)
205
+ except AttributeError:
206
+ pass
207
+
208
+ wrapped_value = _deep_wrap(value, trigger_sync)
209
+ object.__setattr__(self, name, wrapped_value)
210
+ trigger_sync()
211
+
212
+ cls.__init__ = new_init
213
+ cls.__setattr__ = new_setattr
214
+ return cls
215
+ return decorator
216
+
217
+
218
+ def _apply_update(obj, message):
219
+ """Apply a network update to the local object."""
220
+ attr_name = message.get("attr_name")
221
+ value = message.get("value")
222
+
223
+ if attr_name:
224
+ if attr_name.startswith("_"):
225
+ return
226
+ if not hasattr(obj, attr_name):
227
+ return
228
+
229
+ object.__setattr__(obj, "_sync_updating", True)
230
+ try:
231
+ setattr(obj, attr_name, value)
232
+ finally:
233
+ object.__setattr__(obj, "_sync_updating", False)
234
+
235
+
236
+ class SyncedVar:
237
+ """Simple network-synchronized variable."""
238
+
239
+ def __init__(self, value=None, client=None):
240
+ global _master_poller_thread
241
+
242
+ self._client = client or _default_client
243
+ self.frame = inspect.currentframe().f_back
244
+ self.namespace = self.frame.f_globals
245
+ self.var_name = None
246
+
247
+ try:
248
+ import dis
249
+ instructions = list(dis.get_instructions(self.frame.f_code))
250
+ for i, inst in enumerate(instructions):
251
+ if inst.offset == self.frame.f_lasti:
252
+ for j in range(i - 1, -1, -1):
253
+ prev_inst = instructions[j]
254
+ if prev_inst.opname in ("LOAD_FAST", "LOAD_NAME", "LOAD_GLOBAL", "LOAD_DEREF"):
255
+ self.var_name = prev_inst.argval
256
+ break
257
+ elif prev_inst.opname.startswith("CALL") or prev_inst.opname.startswith("PRECALL"):
258
+ break
259
+ break
260
+ except Exception:
261
+ pass
262
+
263
+ if not self.var_name:
264
+ raise ValueError("Could not resolve variable name.")
265
+
266
+ self.last_val = self.namespace.get(self.var_name, value)
267
+ self._updating = False
268
+
269
+ if self._client:
270
+ self._client.register_callback(self.var_name, self._apply_net_update)
271
+
272
+ _master_synced_vars.append(self)
273
+
274
+ if _master_poller_thread is None:
275
+ _master_poller_thread = threading.Thread(target=_master_poller_loop, daemon=True)
276
+ _master_poller_thread.start()
277
+
278
+ def _apply_net_update(self, message):
279
+ val = message.get("value")
280
+ if val is not None:
281
+ self._updating = True
282
+ try:
283
+ self.namespace[self.var_name] = val
284
+ self.last_val = val
285
+ finally:
286
+ self._updating = False
287
+
288
+ def get(self):
289
+ return self.last_val
290
+
291
+ def set(self, value):
292
+ self.namespace[self.var_name] = value
293
+
294
+ def set_client(self, client):
295
+ self._client = client
296
+ if self._client:
297
+ self._client.register_callback(self.var_name, self._apply_net_update)
298
+
299
+ def __repr__(self):
300
+ return f"SyncedVar({self.var_name!r}, {self.last_val!r})"
301
+
302
+ def __str__(self):
303
+ return str(self.last_val)
@@ -0,0 +1,119 @@
1
+ import asyncio
2
+ import pickle
3
+ import struct
4
+
5
+
6
+ class SyncServer:
7
+ """Async TCP server that relays state updates between all connected clients.
8
+
9
+ Uses asyncio.start_server for non-blocking connection handling.
10
+ Wire protocol is identical to the legacy threaded version.
11
+ """
12
+
13
+ def __init__(self, host="0.0.0.0", port=5000):
14
+ self.host = host
15
+ self.port = port
16
+ self.clients: list[asyncio.StreamWriter] = []
17
+ self.auth_handler = None
18
+ self._server = None
19
+
20
+ def on_auth(self, func):
21
+ self.auth_handler = func
22
+ return func
23
+
24
+ async def _send_packet(self, writer: asyncio.StreamWriter, data: dict):
25
+ raw = pickle.dumps(data)
26
+ writer.write(struct.pack(">I", len(raw)) + raw)
27
+ await writer.drain()
28
+
29
+ async def _recv_packet(self, reader: asyncio.StreamReader) -> dict | None:
30
+ header = await reader.readexactly(4)
31
+ length = struct.unpack(">I", header)[0]
32
+ raw = await reader.readexactly(length)
33
+ return pickle.loads(raw)
34
+
35
+ async def _broadcast(self, message: dict, sender: asyncio.StreamWriter = None):
36
+ raw = pickle.dumps(message)
37
+ frame = struct.pack(">I", len(raw)) + raw
38
+
39
+ dead = []
40
+ for client in self.clients:
41
+ if client is sender:
42
+ continue
43
+ try:
44
+ client.write(frame)
45
+ await client.drain()
46
+ except (ConnectionError, OSError):
47
+ dead.append(client)
48
+
49
+ for client in dead:
50
+ self.clients.remove(client)
51
+ client.close()
52
+
53
+ async def _handle_client(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
54
+ addr = writer.get_extra_info("peername")
55
+ print(f"[EasySync] New connection: {addr}")
56
+
57
+ try:
58
+ message = await self._recv_packet(reader)
59
+ if not message:
60
+ return
61
+
62
+ if message.get("type") == "auth":
63
+ if self.auth_handler:
64
+ if not self.auth_handler(addr, message.get("payload")):
65
+ await self._send_packet(writer, {"type": "auth_reject"})
66
+ return
67
+ await self._send_packet(writer, {"type": "auth_ok"})
68
+ else:
69
+ if self.auth_handler:
70
+ return
71
+ await self._broadcast(message, sender=writer)
72
+
73
+ self.clients.append(writer)
74
+
75
+ while True:
76
+ message = await self._recv_packet(reader)
77
+ if not message:
78
+ break
79
+ await self._broadcast(message, sender=writer)
80
+
81
+ except (asyncio.IncompleteReadError, ConnectionError, OSError):
82
+ pass
83
+ except Exception as e:
84
+ print(f"[EasySync] Client error {addr}: {e}")
85
+ finally:
86
+ print(f"[EasySync] Disconnected: {addr}")
87
+ if writer in self.clients:
88
+ self.clients.remove(writer)
89
+ writer.close()
90
+
91
+ async def start(self):
92
+ self._server = await asyncio.start_server(
93
+ self._handle_client, self.host, self.port
94
+ )
95
+ print(f"[EasySync] Server started on {self.host}:{self.port}")
96
+ async with self._server:
97
+ await self._server.serve_forever()
98
+
99
+ def start_thread(self):
100
+ """Run the async server in a dedicated thread with its own event loop."""
101
+ import threading
102
+
103
+ def _run():
104
+ loop = asyncio.new_event_loop()
105
+ asyncio.set_event_loop(loop)
106
+ loop.run_until_complete(self.start())
107
+
108
+ t = threading.Thread(target=_run, daemon=True)
109
+ t.start()
110
+
111
+ async def stop(self):
112
+ if self._server:
113
+ self._server.close()
114
+ await self._server.wait_closed()
115
+
116
+
117
+ if __name__ == "__main__":
118
+ server = SyncServer()
119
+ asyncio.run(server.start())
@@ -0,0 +1,123 @@
1
+ Metadata-Version: 2.4
2
+ Name: py-easysync
3
+ Version: 0.1.0
4
+ Summary: Universal real-time state synchronization for Python.
5
+ Home-page: https://github.com/GalTechDev/easysync
6
+ Author: GalTechDev
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Topic :: System :: Networking
11
+ Classifier: Topic :: Software Development :: Libraries
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ Dynamic: author
15
+ Dynamic: classifier
16
+ Dynamic: description
17
+ Dynamic: description-content-type
18
+ Dynamic: home-page
19
+ Dynamic: requires-python
20
+ Dynamic: summary
21
+
22
+ # EasySync
23
+
24
+ Universal real-time state synchronization for Python.
25
+
26
+ ![Python](https://img.shields.io/badge/Python-3.10+-blue.svg)
27
+ ![License](https://img.shields.io/badge/License-MIT-yellow.svg)
28
+
29
+ Manipulate your Python objects as if the network didn't exist. EasySync intercepts attribute mutations through a transparent proxy and propagates them instantly to all machines connected to the server.
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ pip install easysync
35
+ ```
36
+
37
+ ## Quick Start
38
+
39
+ **Server** (hosts the shared state):
40
+
41
+ ```python
42
+ from easysync import SyncedObject, SyncServer, connect
43
+
44
+ server = SyncServer(port=5000)
45
+ server.start_thread()
46
+
47
+ client = connect("127.0.0.1", 5000)
48
+
49
+ @SyncedObject(client)
50
+ class GameState:
51
+ def __init__(self):
52
+ self.score = 0
53
+ self.players = []
54
+
55
+ state = GameState()
56
+ state.score = 42 # propagated to all clients
57
+ state.players.append("A") # propagated too
58
+ ```
59
+
60
+ **Client** (joins the server):
61
+
62
+ ```python
63
+ from easysync import SyncedObject, connect
64
+
65
+ client = connect("192.168.1.10", 5000)
66
+
67
+ @SyncedObject(client)
68
+ class GameState:
69
+ def __init__(self):
70
+ self.score = 0
71
+ self.players = []
72
+
73
+ state = GameState()
74
+ print(state.score) # 42, updated in real time
75
+ print(state.players) # ['A']
76
+ ```
77
+
78
+ ## Architecture
79
+
80
+ The server uses `asyncio` for non-blocking connection handling, allowing it to support a large number of simultaneous clients without CPU overhead. The client uses a dedicated receive thread to remain compatible with standard application loops (Pygame, Matplotlib, etc.).
81
+
82
+ The wire protocol relies on binary Pickle serialization framed by a 4-byte header (payload size). This ensures measured latency under 20ms.
83
+
84
+ ## Features
85
+
86
+ - **Zero configuration**: a single `@SyncedObject` decorator is all you need.
87
+ - **Transparent proxy**: automatic interception of `__setattr__`, `__setitem__`, `append`, `pop`, etc.
88
+ - **Asyncio server**: built on `asyncio.start_server` for maximum scalability.
89
+ - **Binary serialization**: Pickle + TCP framing protocol, latency < 20ms.
90
+ - **Zero dependencies**: only uses the Python standard library.
91
+ - **Data Science ready**: optimized handling of NumPy, Pandas and Scikit-Learn objects via the copy-and-reassign pattern.
92
+
93
+ ## Examples
94
+
95
+ The `examples/` folder contains several demos:
96
+
97
+ | File | Description |
98
+ |---|---|
99
+ | `pygame_example.py` | Synchronized square between two Pygame windows |
100
+ | `pygame_hanoi.py` | Collaborative Tower of Hanoi |
101
+ | `numpy_matplotlib_example.py` | NumPy data streaming with Matplotlib chart |
102
+ | `pandas_example.py` | Collaborative Pandas spreadsheet |
103
+ | `sklearn_live_training.py` | Live Scikit-Learn training visualization |
104
+ | `federated_learning_example.py` | Distributed federated learning |
105
+ | `genetic_island_example.py` | Distributed genetic algorithm (island model) |
106
+ | `tetris_ai_example.py` | Distributed Tetris AI via genetic algorithm |
107
+
108
+ To run the examples, install the additional dependencies:
109
+
110
+ ```bash
111
+ pip install -r requirements_examples.txt
112
+ ```
113
+
114
+ Then launch a server and one or more clients:
115
+
116
+ ```bash
117
+ python examples/pygame_example.py server # Terminal 1
118
+ python examples/pygame_example.py # Terminal 2
119
+ ```
120
+
121
+ ## License
122
+
123
+ MIT
@@ -0,0 +1,10 @@
1
+ README.md
2
+ setup.py
3
+ easysync/__init__.py
4
+ easysync/syncclient.py
5
+ easysync/syncedobject.py
6
+ easysync/syncserver.py
7
+ py_easysync.egg-info/PKG-INFO
8
+ py_easysync.egg-info/SOURCES.txt
9
+ py_easysync.egg-info/dependency_links.txt
10
+ py_easysync.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ easysync
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,20 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="py-easysync",
5
+ version="0.1.0",
6
+ author="GalTechDev",
7
+ description="Universal real-time state synchronization for Python.",
8
+ long_description=open("README.md", encoding="utf-8").read(),
9
+ long_description_content_type="text/markdown",
10
+ url="https://github.com/GalTechDev/easysync",
11
+ packages=find_packages(),
12
+ python_requires=">=3.10",
13
+ classifiers=[
14
+ "Programming Language :: Python :: 3",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Operating System :: OS Independent",
17
+ "Topic :: System :: Networking",
18
+ "Topic :: Software Development :: Libraries",
19
+ ],
20
+ )