pyurt-pro 3.1.1__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.
pyurt_pro/__init__.py ADDED
@@ -0,0 +1,23 @@
1
+ """
2
+ PyURT Pro – расширенная версия с кластеризацией, очередями, RPC и JWT.
3
+ """
4
+
5
+ from pyurt_pro.core.protocol import ExtendedProtocol, MessageTypes
6
+ from pyurt_pro.cluster.node import ClusterNode, ClusterServer
7
+ from pyurt_pro.mq.broker import RedisBroker
8
+ from pyurt_pro.rpc.server import RPCServer, rpc_method
9
+ from pyurt_pro.auth.jwt import JWTManager
10
+ from pyurt_pro.config import load_config, get_config
11
+
12
+ __all__ = [
13
+ "ExtendedProtocol",
14
+ "MessageTypes",
15
+ "ClusterNode",
16
+ "ClusterServer",
17
+ "RedisBroker",
18
+ "RPCServer",
19
+ "rpc_method",
20
+ "JWTManager",
21
+ "load_config",
22
+ "get_config",
23
+ ]
pyurt_pro/auth/jwt.py ADDED
@@ -0,0 +1,24 @@
1
+ import jwt
2
+ import time
3
+ from typing import Optional
4
+ from pyurt_pro.config import get_config
5
+
6
+ class JWTManager:
7
+ @staticmethod
8
+ def get_secret_key() -> str:
9
+ return get_config()['auth']['secret_key']
10
+
11
+ @classmethod
12
+ def generate_token(cls, user_id: str, expires_in: int = None) -> str:
13
+ if expires_in is None:
14
+ expires_in = get_config()['auth'].get('expires_in', 3600)
15
+ payload = {"user_id": user_id, "exp": time.time() + expires_in}
16
+ return jwt.encode(payload, cls.get_secret_key(), algorithm="HS256")
17
+
18
+ @classmethod
19
+ def verify_token(cls, token: str) -> Optional[str]:
20
+ try:
21
+ payload = jwt.decode(token, cls.get_secret_key(), algorithms=["HS256"])
22
+ return payload.get("user_id")
23
+ except jwt.InvalidTokenError:
24
+ return None
pyurt_pro/cli.py ADDED
@@ -0,0 +1,20 @@
1
+ import sys
2
+ import argparse
3
+ from pyurt_pro.config import load_config
4
+ from pyurt_pro.cluster.node import ClusterNode
5
+
6
+ def main():
7
+ parser = argparse.ArgumentParser(description="PyURT Pro Cluster Node")
8
+ parser.add_argument("-c", "--config", default="config.yaml", help="Path to YAML config")
9
+ args = parser.parse_args()
10
+
11
+ load_config(args.config)
12
+ node = ClusterNode()
13
+ try:
14
+ node.start()
15
+ except KeyboardInterrupt:
16
+ node.stop()
17
+ sys.exit(0)
18
+
19
+ if __name__ == "__main__":
20
+ main()
@@ -0,0 +1,80 @@
1
+ import asyncio
2
+ import json
3
+ import threading
4
+ import time
5
+ import redis
6
+ from typing import Any
7
+ from pyurt import PyURTServer
8
+ from pyurt_pro.core.protocol import ExtendedProtocol, MessageTypes
9
+ from pyurt_pro.config import get_config
10
+
11
+ class ClusterServer(PyURTServer):
12
+ def __init__(self, node: 'ClusterNode'):
13
+ super().__init__()
14
+ self.node = node
15
+
16
+ async def handle_client(self, reader, writer):
17
+ while True:
18
+ try:
19
+ msg_type, payload = await ExtendedProtocol.read(reader)
20
+ if msg_type == 0:
21
+ break
22
+ if msg_type == MessageTypes.CLUSTER_JOIN:
23
+ data = json.loads(payload)
24
+ self.node.peers.add((data['host'], data['port']))
25
+ writer.write(ExtendedProtocol.build(b'OK', MessageTypes.ACK))
26
+ await writer.drain()
27
+ elif msg_type == MessageTypes.CLUSTER_PING:
28
+ writer.write(ExtendedProtocol.build(b'PONG', MessageTypes.ACK))
29
+ await writer.drain()
30
+ else:
31
+ writer.write(ExtendedProtocol.build(b'NACK', MessageTypes.NACK))
32
+ await writer.drain()
33
+ except Exception:
34
+ break
35
+ writer.close()
36
+ await writer.wait_closed()
37
+
38
+ class ClusterNode:
39
+ def __init__(self):
40
+ config = get_config()['cluster']
41
+ self.host = config['host']
42
+ self.port = config['port']
43
+ self.seed_nodes = config['seed_nodes']
44
+ self.redis_url = config['redis_url']
45
+ self.max_size = config['max_size']
46
+ self.redis = redis.from_url(self.redis_url)
47
+ self.peers = set()
48
+ self.running = False
49
+ self.server = None
50
+ self._loop = None
51
+
52
+ def start(self):
53
+ self.running = True
54
+ self._register_node()
55
+ self.server = ClusterServer(self)
56
+ self._loop = asyncio.new_event_loop()
57
+ asyncio.set_event_loop(self._loop)
58
+ self._loop.run_until_complete(self.server.run())
59
+
60
+ def _register_node(self):
61
+ self.redis.sadd("pyurt:cluster:nodes", f"{self.host}:{self.port}")
62
+ self.redis.expire("pyurt:cluster:nodes", 60)
63
+ threading.Thread(target=self._heartbeat_loop, daemon=True).start()
64
+
65
+ def _heartbeat_loop(self):
66
+ while self.running:
67
+ self.redis.sadd("pyurt:cluster:nodes", f"{self.host}:{self.port}")
68
+ self.redis.expire("pyurt:cluster:nodes", 30)
69
+ all_nodes = self.redis.smembers("pyurt:cluster:nodes")
70
+ self.peers = {node.decode() for node in all_nodes if node.decode() != f"{self.host}:{self.port}"}
71
+ time.sleep(10)
72
+
73
+ def broadcast(self, data: Any):
74
+ self.redis.publish("pyurt:cluster:broadcast", json.dumps(data))
75
+
76
+ def stop(self):
77
+ self.running = False
78
+ self.redis.srem("pyurt:cluster:nodes", f"{self.host}:{self.port}")
79
+ if self.server and self._loop:
80
+ asyncio.run_coroutine_threadsafe(self.server.close(), self._loop)
pyurt_pro/config.py ADDED
@@ -0,0 +1,56 @@
1
+ import os
2
+ import yaml
3
+ from typing import Dict, Any
4
+
5
+ DEFAULT_CONFIG = {
6
+ "cluster": {
7
+ "host": "0.0.0.0",
8
+ "port": 8888,
9
+ "seed_nodes": [],
10
+ "redis_url": "redis://localhost:6379/0",
11
+ "max_size": 1048576,
12
+ },
13
+ "mq": {
14
+ "persistent": True,
15
+ "max_messages": 1000,
16
+ },
17
+ "rpc": {
18
+ "enable_ssl": False,
19
+ },
20
+ "auth": {
21
+ "secret_key": "change-me-in-production",
22
+ "expires_in": 3600,
23
+ },
24
+ }
25
+
26
+ _config = None
27
+
28
+ def load_config(config_path: str = None) -> Dict[str, Any]:
29
+ global _config
30
+ if _config is not None:
31
+ return _config
32
+ if config_path is None:
33
+ config_path = os.environ.get("PYURT_PRO_CONFIG", "config.yaml")
34
+ try:
35
+ with open(config_path, 'r') as f:
36
+ user_config = yaml.safe_load(f) or {}
37
+ except FileNotFoundError:
38
+ user_config = {}
39
+ merged = _deep_merge(DEFAULT_CONFIG, user_config)
40
+ _config = merged
41
+ return merged
42
+
43
+ def _deep_merge(base: dict, override: dict) -> dict:
44
+ result = base.copy()
45
+ for key, value in override.items():
46
+ if key in result and isinstance(result[key], dict) and isinstance(value, dict):
47
+ result[key] = _deep_merge(result[key], value)
48
+ else:
49
+ result[key] = value
50
+ return result
51
+
52
+ def get_config() -> Dict[str, Any]:
53
+ global _config
54
+ if _config is None:
55
+ load_config()
56
+ return _config
@@ -0,0 +1,51 @@
1
+ import enum
2
+ import struct
3
+ import zlib
4
+ import asyncio
5
+ from typing import Tuple, Optional
6
+ from pyurt import Protocol
7
+ from pyurt_pro.config import get_config
8
+
9
+ class MessageTypes(enum.IntEnum):
10
+ DATA = 1
11
+ ACK = 2
12
+ NACK = 3
13
+ RPC_REQUEST = 100
14
+ RPC_RESPONSE = 101
15
+ MQ_PUBLISH = 200
16
+ MQ_SUBSCRIBE = 201
17
+ MQ_UNSUBSCRIBE = 202
18
+ MQ_DELIVER = 203
19
+ CLUSTER_JOIN = 300
20
+ CLUSTER_LEAVE = 301
21
+ CLUSTER_SYNC = 302
22
+ CLUSTER_PING = 303
23
+ AUTH_TOKEN = 400
24
+ AUTH_CHALLENGE = 401
25
+ AUTH_RESPONSE = 402
26
+
27
+ class ExtendedProtocol(Protocol):
28
+ HEADER_SIZE = 9
29
+
30
+ @staticmethod
31
+ def build(payload: bytes, msg_type: int = MessageTypes.DATA) -> bytes:
32
+ crc = zlib.crc32(payload) & 0xFFFFFFFF
33
+ header = struct.pack('!BII', msg_type, len(payload), crc)
34
+ return header + payload
35
+
36
+ @staticmethod
37
+ async def read(reader, timeout: float = 30.0) -> Tuple[int, Optional[bytes]]:
38
+ try:
39
+ raw_header = await asyncio.wait_for(reader.readexactly(ExtendedProtocol.HEADER_SIZE), timeout)
40
+ except asyncio.IncompleteReadError:
41
+ return 0, None
42
+ except asyncio.TimeoutError:
43
+ raise ValueError("Timeout reading header")
44
+ msg_type, length, crc = struct.unpack('!BII', raw_header)
45
+ max_size = get_config()['cluster']['max_size']
46
+ if length > max_size:
47
+ raise ValueError(f"Message too large: {length}")
48
+ payload = await asyncio.wait_for(reader.readexactly(length), timeout)
49
+ if zlib.crc32(payload) & 0xFFFFFFFF != crc:
50
+ raise ValueError("CRC mismatch")
51
+ return msg_type, payload
pyurt_pro/mq/broker.py ADDED
@@ -0,0 +1,38 @@
1
+ import json
2
+ import redis
3
+ from typing import Any, Callable
4
+ from pyurt_pro.config import get_config
5
+
6
+ class RedisBroker:
7
+ def __init__(self):
8
+ config = get_config()['mq']
9
+ self.redis_url = get_config()['cluster']['redis_url']
10
+ self.redis = redis.from_url(self.redis_url)
11
+ self.pubsub = self.redis.pubsub()
12
+ self._subscriptions = {}
13
+ self.persistent = config.get('persistent', True)
14
+ self.max_messages = config.get('max_messages', 1000)
15
+
16
+ def publish(self, topic: str, message: Any, persistent: bool = None):
17
+ if persistent is None:
18
+ persistent = self.persistent
19
+ data = {"payload": message, "persistent": persistent}
20
+ self.redis.publish(f"pyurt:mq:{topic}", json.dumps(data))
21
+ if persistent:
22
+ self.redis.rpush(f"pyurt:mq:{topic}:history", json.dumps(message))
23
+ self.redis.ltrim(f"pyurt:mq:{topic}:history", -self.max_messages, -1)
24
+
25
+ def subscribe(self, topic: str, callback: Callable):
26
+ if topic not in self._subscriptions:
27
+ self.pubsub.subscribe(f"pyurt:mq:{topic}")
28
+ self._subscriptions[topic] = callback
29
+ import threading
30
+ def listener():
31
+ for msg in self.pubsub.listen():
32
+ if msg['type'] == 'message':
33
+ data = json.loads(msg['data'])
34
+ callback(data['payload'])
35
+ threading.Thread(target=listener, daemon=True).start()
36
+
37
+ def get_history(self, topic: str, limit: int = 10) -> list:
38
+ return [json.loads(item) for item in self.redis.lrange(f"pyurt:mq:{topic}:history", -limit, -1)]
@@ -0,0 +1,43 @@
1
+ import json
2
+ from typing import Dict, Callable
3
+ from pyurt import PyURTServer
4
+ from pyurt_pro.core.protocol import ExtendedProtocol, MessageTypes
5
+
6
+ class RPCServer(PyURTServer):
7
+ def __init__(self):
8
+ super().__init__()
9
+ self.methods: Dict[str, Callable] = {}
10
+
11
+ def register(self, name: str, func: Callable):
12
+ self.methods[name] = func
13
+
14
+ async def handle_client(self, reader, writer):
15
+ while True:
16
+ try:
17
+ msg_type, payload = await ExtendedProtocol.read(reader, timeout=30.0)
18
+ if msg_type == 0:
19
+ break
20
+ if msg_type == MessageTypes.RPC_REQUEST:
21
+ data = json.loads(payload)
22
+ method_name = data.get("method")
23
+ params = data.get("params", [])
24
+ if method_name in self.methods:
25
+ result = self.methods[method_name](*params)
26
+ response = {"id": data.get("id"), "result": result, "error": None}
27
+ else:
28
+ response = {"id": data.get("id"), "result": None, "error": "Method not found"}
29
+ writer.write(ExtendedProtocol.build(json.dumps(response).encode(), MessageTypes.RPC_RESPONSE))
30
+ await writer.drain()
31
+ else:
32
+ writer.write(ExtendedProtocol.build(b'NACK', MessageTypes.NACK))
33
+ await writer.drain()
34
+ except Exception:
35
+ break
36
+ writer.close()
37
+ await writer.wait_closed()
38
+
39
+ def rpc_method(name: str = None):
40
+ def decorator(func):
41
+ func._rpc_name = name or func.__name__
42
+ return func
43
+ return decorator
@@ -0,0 +1,51 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyurt-pro
3
+ Version: 3.1.1
4
+ Summary: PyURT Pro – расширенный протокол с кластеризацией, очередями, RPC и JWT
5
+ Author: HioDev
6
+ License: MIT
7
+ Keywords: pyurt,cluster,mq,rpc,jwt
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.9
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Description-Content-Type: text/markdown
17
+ Requires-Dist: pyurt>=3.1.0
18
+ Requires-Dist: redis>=4.0.0
19
+ Requires-Dist: pyyaml>=6.0
20
+ Requires-Dist: pyjwt>=2.0.0
21
+
22
+ # PyURT Pro – расширенный протокол
23
+
24
+ Добавляет кластеризацию, очереди сообщений, RPC и JWT-аутентификацию.
25
+
26
+ ## Установка
27
+
28
+ ```bash
29
+ pip install pyurt-pro
30
+ ## Запуск
31
+ # УСТАНОВИТЕ И ЗАПУСТИТЕ redis
32
+ pyurt-pro -c config.yaml # Запуск
33
+
34
+
35
+ ## В коде
36
+ from pyurt_pro import ClusterNode, RedisBroker, RPCServer, rpc_method
37
+
38
+ # Кластер
39
+ node = ClusterNode()
40
+ node.start()
41
+
42
+ # Очереди
43
+ broker = RedisBroker()
44
+ broker.publish("chat", "Hello, world!")
45
+
46
+ # RPC
47
+ @rpc_method("add")
48
+ def add(a, b):
49
+ return a + b
50
+
51
+ ## Для запуска
@@ -0,0 +1,13 @@
1
+ pyurt_pro/__init__.py,sha256=ey8i70sjvdy0UUz91AO2DmtWIHqg-v5r4zQuAv0W-d0,654
2
+ pyurt_pro/cli.py,sha256=gPpzGOk0Q1HrlxpE-0JVZ2faQV-eChe9x7dFqDKUwx4,520
3
+ pyurt_pro/config.py,sha256=p9SxaQiOXfANFknh7hpPHg9VWs9XyLUKS8rKTKbXkxg,1419
4
+ pyurt_pro/auth/jwt.py,sha256=tJVpaVn268oEGejTvAi11sC8eAYNy1oNQiJKhGg28h0,826
5
+ pyurt_pro/cluster/node.py,sha256=jTdX3tleHKzsgjmr7WWnf407Cv-OI9ggGs3EcWkQ8T0,3026
6
+ pyurt_pro/core/protocol.py,sha256=T0cps4tnwQiadpBFslSg2wnC-gFD1ys5HPOLqmMrEtM,1623
7
+ pyurt_pro/mq/broker.py,sha256=M1-wqkmYHNpXnaL_bJnDW3NXHPiUAB6BIu8q2S06b_w,1648
8
+ pyurt_pro/rpc/server.py,sha256=0Gzvyvo2xRDM_7zr_hzd2qAU8wbcrhyLVK01kwfnJs4,1703
9
+ pyurt_pro-3.1.1.dist-info/METADATA,sha256=0rbP3FDVvlFZuj_PBO6kfKPpq8yqaqD3O6bEsjVtoMc,1442
10
+ pyurt_pro-3.1.1.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
11
+ pyurt_pro-3.1.1.dist-info/entry_points.txt,sha256=K1ndqExjHITXrgsDRxUQc8DZKc-esCIMqr3FT5-YYAo,49
12
+ pyurt_pro-3.1.1.dist-info/top_level.txt,sha256=Zbt566bE_5KUXhLoAxf_8LsPT584MayWb_rd0LObh68,10
13
+ pyurt_pro-3.1.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pyurt-pro = pyurt_pro.cli:main
@@ -0,0 +1 @@
1
+ pyurt_pro