TgrEzLi 0.1.2__py3-none-any.whl → 0.4.0__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.

Potentially problematic release.


This version of TgrEzLi might be problematic. Click here for more details.

TgrEzLi/crypto.py ADDED
@@ -0,0 +1,39 @@
1
+ """Encrypted credential storage (PyCypherLib / PyCypher)."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+
6
+ from PyCypher import Cy
7
+
8
+
9
+ class CredentialManager:
10
+ """Store and load Telegram token + chat dict encrypted with a password (PyCypherLib)."""
11
+
12
+ def __init__(self, filename: str = "tgrdata.cy") -> None:
13
+ self.filename = filename
14
+
15
+ def signup(self, token: str, chat_dict: dict[str, str], password: str) -> None:
16
+ """Create encrypted file with token and chat IDs. Fails if file already exists."""
17
+
18
+ if os.path.exists(self.filename):
19
+ raise FileExistsError("Encrypted file already exists.")
20
+ lines = [token] + [f"{name}:{cid}" for name, cid in chat_dict.items()]
21
+ Cy().encLines(self.filename).Lines(lines).P(password)
22
+
23
+ def login(self, password: str) -> tuple[str, dict[str, str]]:
24
+ """Read encrypted file and return (token, chat_dict)."""
25
+ if not os.path.exists(self.filename):
26
+ raise FileNotFoundError("Encrypted file not found.")
27
+ try:
28
+ lines = Cy().decLines(self.filename).P(password)
29
+ except Exception as e:
30
+ raise ValueError("Invalid password or corrupted encrypted file.") from e
31
+ token = lines[0]
32
+ chat_dict: dict[str, str] = {}
33
+ for element in lines[1:]:
34
+ name, cid = element.split(":", 1)
35
+ chat_dict[name] = cid
36
+ return token, chat_dict
37
+
38
+
39
+ __all__ = ["CredentialManager"]
TgrEzLi/defaults.py ADDED
@@ -0,0 +1,16 @@
1
+ """Default constants for TgrEzLi."""
2
+
3
+ # API server
4
+ DEFAULT_API_HOST = "localhost"
5
+ DEFAULT_API_PORT = 9999
6
+ MAX_REQUEST_BODY_BYTES = 1 * 1024 * 1024 # 1 MiB
7
+
8
+ # Logging
9
+ DEFAULT_LOG_FILE = "TgrEzLi.log"
10
+ DEFAULT_LOGGER_NAME = "TgrEzLi"
11
+
12
+ # Credentials (crypto)
13
+ DEFAULT_CREDENTIAL_FILE = "tgrdata.cy"
14
+
15
+ # Banner
16
+ DEFAULT_BANNER_FILLER = "█"
TgrEzLi/handlers.py ADDED
@@ -0,0 +1,70 @@
1
+ """Handler registry and user-callback invocation (threaded)."""
2
+ from __future__ import annotations
3
+
4
+ import threading
5
+ import traceback
6
+ from typing import Any, Callable
7
+
8
+ from TgrEzLi.models import clear_context, set_context
9
+ from TgrEzLi.types import TgArgsData, TgCbData, TgCmdData, TgMsgData
10
+
11
+
12
+ def call_user_function(
13
+ logger: Any,
14
+ func: Callable[[], None],
15
+ *,
16
+ TgMsg: TgMsgData | None = None,
17
+ TgCmd: TgCmdData | None = None,
18
+ TgCb: TgCbData | None = None,
19
+ TgArgs: TgArgsData | None = None,
20
+ ) -> None:
21
+ """Run the user handler in a background thread with thread-local context set."""
22
+ def worker() -> None:
23
+ try:
24
+ set_context(TgMsg=TgMsg, TgCmd=TgCmd, TgCb=TgCb, TgArgs=TgArgs)
25
+ func()
26
+ except Exception as e: # noqa: BLE001
27
+ logger.error("Error in user handler: %s", e)
28
+ logger.debug(traceback.format_exc())
29
+ finally:
30
+ clear_context()
31
+
32
+ thread = threading.Thread(target=worker, daemon=True)
33
+ thread.start()
34
+
35
+
36
+ class HandlerRegistry:
37
+ """Registry for message, command, callback, and API route handlers."""
38
+
39
+ __slots__ = ("message_handlers", "command_handlers", "callback_handlers", "api_routes")
40
+
41
+ def __init__(self) -> None:
42
+ self.message_handlers: list[tuple[set[str], Callable[[], None]]] = []
43
+ self.command_handlers: dict[str, list[tuple[set[str], Callable[[], None]]]] = {}
44
+ self.callback_handlers: list[tuple[set[str], Callable[[], None]]] = []
45
+ self.api_routes: dict[str, dict[str, Any]] = {} # path -> {"args": [...], "func": ...}
46
+
47
+ def register_message_handler(self, func: Callable[[], None], chats: set[str]) -> None:
48
+ name = getattr(func, "__name__", None)
49
+ if name is not None:
50
+ self.message_handlers[:] = [(c, f) for c, f in self.message_handlers if getattr(f, "__name__", None) != name]
51
+ self.message_handlers.append((chats, func))
52
+
53
+ def register_command_handler(self, command: str, func: Callable[[], None], chats: set[str]) -> None:
54
+ name = getattr(func, "__name__", None)
55
+ lst = self.command_handlers.setdefault(command, [])
56
+ if name is not None:
57
+ lst[:] = [(c, f) for c, f in lst if getattr(f, "__name__", None) != name]
58
+ lst.append((chats, func))
59
+
60
+ def register_callback_handler(self, func: Callable[[], None], chats: set[str]) -> None:
61
+ name = getattr(func, "__name__", None)
62
+ if name is not None:
63
+ self.callback_handlers[:] = [(c, f) for c, f in self.callback_handlers if getattr(f, "__name__", None) != name]
64
+ self.callback_handlers.append((chats, func))
65
+
66
+ def register_api_route(self, endpoint: str, args_list: list[str], func: Callable[[], None]) -> None:
67
+ self.api_routes[endpoint] = {"args": args_list, "func": func}
68
+
69
+
70
+ __all__ = ["call_user_function", "HandlerRegistry"]
TgrEzLi/logger.py ADDED
@@ -0,0 +1,65 @@
1
+ """Logger for TgrEzLi: ezlog for console, optional file append when save_log is True."""
2
+ from __future__ import annotations
3
+
4
+ from datetime import datetime
5
+
6
+ from ezlog import EzLog
7
+
8
+ from TgrEzLi.defaults import DEFAULT_LOG_FILE
9
+
10
+
11
+ def _format_msg(msg: str, *args: object) -> str:
12
+ return msg % args if args else msg
13
+
14
+
15
+ class TgrezliLogger:
16
+ """Adapter: EzLog for console output and optional file append (save_log)."""
17
+
18
+ def __init__(
19
+ self,
20
+ save_log: bool = True,
21
+ log_file: str | None = DEFAULT_LOG_FILE,
22
+ ) -> None:
23
+ self._ez = EzLog()
24
+ self._save_log = save_log
25
+ self._log_file = log_file or None
26
+
27
+ def set_save_log(self, flag: bool) -> None:
28
+ """Enable or disable writing log lines to the file."""
29
+ self._save_log = flag
30
+
31
+ def _write_file(self, level: str, msg: str) -> None:
32
+ if not self._save_log or not self._log_file:
33
+ return
34
+ try:
35
+ line = f"[{datetime.now():%Y-%m-%d %H:%M:%S}] [{level}] TgrEzLi - {msg}\n"
36
+ with open(self._log_file, "a", encoding="utf-8") as f:
37
+ f.write(line)
38
+ except OSError:
39
+ pass
40
+
41
+ def info(self, msg: str, *args: object) -> None:
42
+ formatted = _format_msg(msg, *args)
43
+ self._ez.info(formatted)
44
+ self._write_file("INFO", formatted)
45
+
46
+ def debug(self, msg: str, *args: object) -> None:
47
+ formatted = _format_msg(msg, *args)
48
+ self._ez.d(formatted)
49
+ self._write_file("DEBUG", formatted)
50
+
51
+ def error(self, msg: str, *args: object) -> None:
52
+ formatted = _format_msg(msg, *args)
53
+ self._ez.error(formatted)
54
+ self._write_file("ERROR", formatted)
55
+
56
+
57
+ def get_default_logger(
58
+ save_log: bool = True,
59
+ log_file: str | None = DEFAULT_LOG_FILE,
60
+ ) -> TgrezliLogger:
61
+ """Return a logger that uses ezlog for console and optionally appends to a file."""
62
+ return TgrezliLogger(save_log=save_log, log_file=log_file)
63
+
64
+
65
+ __all__ = ["TgrezliLogger", "get_default_logger"]
TgrEzLi/models.py ADDED
@@ -0,0 +1,82 @@
1
+ """Data payloads and thread-local proxies for handler context (TgMsg, TgCmd, TgCb, TgArgs)."""
2
+ from __future__ import annotations
3
+
4
+ import threading
5
+ from typing import Any
6
+
7
+ from TgrEzLi.types import TgArgsData, TgCbData, TgCmdData, TgMsgData
8
+
9
+ # Thread-local storage for current handler payload
10
+ _local: threading.local = threading.local()
11
+
12
+
13
+ def _get_proxy(attr: str, label: str) -> Any:
14
+ obj = getattr(_local, attr, None)
15
+ if obj is None:
16
+ raise AttributeError(f"{label} is not available in this context (use inside a handler).")
17
+ return obj
18
+
19
+
20
+ class TgMsgProxy:
21
+ """Proxy to current message payload; use only inside onMessage handlers."""
22
+
23
+ def __getattr__(self, name: str) -> Any:
24
+ return getattr(_get_proxy("TgMsg", "TgMsg"), name)
25
+
26
+
27
+ class TgCmdProxy:
28
+ """Proxy to current command payload; use only inside onCommand handlers."""
29
+
30
+ def __getattr__(self, name: str) -> Any:
31
+ return getattr(_get_proxy("TgCmd", "TgCmd"), name)
32
+
33
+
34
+ class TgCbProxy:
35
+ """Proxy to current callback payload; use only inside onCallback handlers."""
36
+
37
+ def __getattr__(self, name: str) -> Any:
38
+ return getattr(_get_proxy("TgCb", "TgCb"), name)
39
+
40
+
41
+ class TgArgsProxy:
42
+ """Proxy to current API request body; use only inside onApiReq handlers."""
43
+
44
+ def __getattr__(self, name: str) -> Any:
45
+ return getattr(_get_proxy("TgArgs", "TgArgs"), name)
46
+
47
+
48
+ def set_context(TgMsg: TgMsgData | None = None, TgCmd: TgCmdData | None = None,
49
+ TgCb: TgCbData | None = None, TgArgs: TgArgsData | None = None) -> None:
50
+ """Set thread-local context (used by handlers module)."""
51
+ _local.TgMsg = TgMsg
52
+ _local.TgCmd = TgCmd
53
+ _local.TgCb = TgCb
54
+ _local.TgArgs = TgArgs
55
+
56
+
57
+ def clear_context() -> None:
58
+ """Clear thread-local context."""
59
+ _local.TgMsg = None
60
+ _local.TgCmd = None
61
+ _local.TgCb = None
62
+ _local.TgArgs = None
63
+
64
+
65
+ # Public proxies (singletons)
66
+ TgMsg = TgMsgProxy()
67
+ TgCmd = TgCmdProxy()
68
+ TgCb = TgCbProxy()
69
+ TgArgs = TgArgsProxy()
70
+
71
+ __all__ = [
72
+ "TgMsgData",
73
+ "TgCmdData",
74
+ "TgCbData",
75
+ "TgArgsData",
76
+ "TgMsg",
77
+ "TgCmd",
78
+ "TgCb",
79
+ "TgArgs",
80
+ "set_context",
81
+ "clear_context",
82
+ ]
TgrEzLi/requests.py ADDED
@@ -0,0 +1,49 @@
1
+ """HTTP client for sending requests to the embedded API server (TReq)."""
2
+ from __future__ import annotations
3
+
4
+ import requests
5
+
6
+
7
+ class TgrezliRequestError(Exception):
8
+ """Raised when a TReq request fails (network or HTTP error)."""
9
+
10
+
11
+ class TReq:
12
+ """Fluent client for POST requests to the TgrEzLi API server."""
13
+
14
+ def __init__(self, endpoint: str, host: str = "localhost", port: int = 9999, timeout_sec: float = 30.0) -> None:
15
+ self.endpoint = endpoint if endpoint.startswith("/") else f"/{endpoint}"
16
+ self._host = host
17
+ self._port = port
18
+ self._timeout = timeout_sec
19
+ self._body: dict = {}
20
+
21
+ def host(self, host: str) -> TReq:
22
+ self._host = host
23
+ return self
24
+
25
+ def port(self, port: int) -> TReq:
26
+ self._port = port
27
+ return self
28
+
29
+ def timeout(self, seconds: float) -> TReq:
30
+ self._timeout = seconds
31
+ return self
32
+
33
+ def arg(self, name: str, value: object) -> TReq:
34
+ self._body[name] = value
35
+ return self
36
+
37
+ def body(self, body_dict: dict) -> TReq:
38
+ self._body = dict(body_dict)
39
+ return self
40
+
41
+ def send(self) -> requests.Response:
42
+ url = f"http://{self._host}:{self._port}{self.endpoint}"
43
+ try:
44
+ return requests.post(url, json=self._body, timeout=self._timeout)
45
+ except requests.RequestException as e:
46
+ raise TgrezliRequestError(f"Request to {url} failed: {e}") from e
47
+
48
+
49
+ __all__ = ["TReq", "TgrezliRequestError"]
TgrEzLi/types.py ADDED
@@ -0,0 +1,119 @@
1
+ """Type definitions and data structures for TgrEzLi."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+ from typing import Any
6
+
7
+ # --- Config ---
8
+
9
+
10
+ @dataclass(frozen=False)
11
+ class TgrezliConfig:
12
+ """Runtime configuration for TEL (log, API server)."""
13
+
14
+ save_log: bool = True
15
+ log_file: str = "TgrEzLi.log"
16
+ api_host: str = "localhost"
17
+ api_port: int = 9999
18
+
19
+
20
+ # --- Handler payloads (internal; passed from core to user via thread-local) ---
21
+
22
+
23
+ @dataclass
24
+ class TgMsgData:
25
+ """Payload for onMessage handlers."""
26
+
27
+ text: str
28
+ msg_id: int | None
29
+ chat_id: str
30
+ user_id: int | None
31
+ user_name: str | None
32
+ timestamp: Any
33
+ raw_update: Any
34
+
35
+ @property
36
+ def msgId(self) -> int | None:
37
+ return self.msg_id
38
+
39
+ @property
40
+ def chatId(self) -> str:
41
+ return self.chat_id
42
+
43
+ @property
44
+ def userId(self) -> int | None:
45
+ return self.user_id
46
+
47
+ @property
48
+ def userName(self) -> str | None:
49
+ return self.user_name
50
+
51
+
52
+ @dataclass
53
+ class TgCmdData:
54
+ """Payload for onCommand handlers."""
55
+
56
+ command: str
57
+ args: str
58
+ msg_id: int | None
59
+ chat_id: str
60
+ user_id: int | None
61
+ user_name: str | None
62
+ timestamp: Any
63
+ raw_update: Any
64
+
65
+ @property
66
+ def msgId(self) -> int | None:
67
+ return self.msg_id
68
+
69
+ @property
70
+ def chatId(self) -> str:
71
+ return self.chat_id
72
+
73
+ @property
74
+ def userId(self) -> int | None:
75
+ return self.user_id
76
+
77
+ @property
78
+ def userName(self) -> str | None:
79
+ return self.user_name
80
+
81
+
82
+ @dataclass
83
+ class TgCbData:
84
+ """Payload for onCallback handlers."""
85
+
86
+ text: str | None
87
+ value: str
88
+ msg_id: int | None
89
+ chat_id: str
90
+ user_id: int | None
91
+ user_name: str | None
92
+ timestamp: Any
93
+ raw_update: Any
94
+
95
+ @property
96
+ def msgId(self) -> int | None:
97
+ return self.msg_id
98
+
99
+ @property
100
+ def chatId(self) -> str:
101
+ return self.chat_id
102
+
103
+ @property
104
+ def userId(self) -> int | None:
105
+ return self.user_id
106
+
107
+ @property
108
+ def userName(self) -> str | None:
109
+ return self.user_name
110
+
111
+
112
+ @dataclass
113
+ class TgArgsData:
114
+ """Payload for onApiReq handlers (POST body)."""
115
+
116
+ _data: dict[str, Any]
117
+
118
+ def get(self, key: str, default: Any = None) -> Any:
119
+ return self._data.get(key, default)