clitg 0.1.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.
clitg/__init__.py ADDED
@@ -0,0 +1,12 @@
1
+ """clitg package."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("clitg")
7
+ except PackageNotFoundError:
8
+ __version__ = "0.1.0"
9
+
10
+ SCHEMA_VERSION = "0.1"
11
+
12
+ __all__ = ["SCHEMA_VERSION", "__version__"]
clitg/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Run clitg as a module."""
2
+
3
+ from clitg.cli import main
4
+
5
+ main()
clitg/catalog.py ADDED
@@ -0,0 +1,242 @@
1
+ """Generate machine-readable command, schema, and MTProto capability catalogs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ import json
7
+ import re
8
+ from datetime import UTC, datetime
9
+ from pathlib import Path
10
+ from typing import Any, Literal
11
+
12
+ import telethon
13
+ from telethon.tl import alltlobjects
14
+ from telethon.tl.tlobject import TLRequest
15
+
16
+ from clitg import SCHEMA_VERSION, __version__
17
+ from clitg.models import (
18
+ Capability,
19
+ CapabilityCatalog,
20
+ Confirmation,
21
+ Envelope,
22
+ ErrorInfo,
23
+ JsonlRecord,
24
+ LoginState,
25
+ Meta,
26
+ ProfileView,
27
+ )
28
+ from clitg.serialization import to_jsonable
29
+
30
+ HIGH_LEVEL_METHODS: dict[str, str] = {
31
+ "messages.getForumTopics": "topics.list",
32
+ "contacts.getContacts": "contacts.list",
33
+ "messages.deleteMessages": "messages.delete",
34
+ "messages.deleteScheduledMessages": "scheduled.cancel",
35
+ "messages.editMessage": "messages.edit",
36
+ "messages.forwardMessages": "messages.forward",
37
+ "messages.getHistory": "messages.list",
38
+ "messages.getMessages": "messages.get",
39
+ "messages.getScheduledHistory": "scheduled.list",
40
+ "messages.readHistory": "messages.read",
41
+ "messages.search": "messages.search",
42
+ "messages.sendMedia": "messages.send",
43
+ "messages.sendMessage": "messages.send",
44
+ "messages.sendMultiMedia": "messages.send",
45
+ "messages.sendReaction": "messages.react",
46
+ "messages.updatePinnedMessage": "messages.pin",
47
+ }
48
+
49
+ UNSUPPORTED_METHODS: dict[str, str] = {
50
+ "messages.acceptEncryption": "Secret-chat encryption state is not implemented",
51
+ "messages.discardEncryption": "Secret-chat encryption state is not implemented",
52
+ "messages.requestEncryption": "Secret-chat encryption state is not implemented",
53
+ "messages.sendEncrypted": "Secret-chat encryption state is not implemented",
54
+ "messages.sendEncryptedFile": "Secret-chat encryption state is not implemented",
55
+ "messages.sendEncryptedService": "Secret-chat encryption state is not implemented",
56
+ }
57
+
58
+ CRITICAL_PATTERNS = (
59
+ re.compile(r"^account\.(delete|updatePassword|resetPassword|changePhone)"),
60
+ re.compile(r"^auth\."),
61
+ re.compile(r"^channels\.editCreator"),
62
+ )
63
+ DESTRUCTIVE_WORDS = ("delete", "discard", "revoke", "terminate")
64
+ WRITE_WORDS = (
65
+ "accept",
66
+ "add",
67
+ "cancel",
68
+ "create",
69
+ "edit",
70
+ "forward",
71
+ "import",
72
+ "invite",
73
+ "join",
74
+ "leave",
75
+ "mark",
76
+ "read",
77
+ "save",
78
+ "send",
79
+ "set",
80
+ "start",
81
+ "toggle",
82
+ "update",
83
+ "upload",
84
+ )
85
+
86
+
87
+ COMMAND_CATALOG: dict[str, dict[str, Any]] = {
88
+ "auth": {"commands": ["request-code", "verify", "status", "logout"]},
89
+ "capabilities": {"commands": ["list", "get"]},
90
+ "contacts": {"commands": ["list", "search", "resolve"]},
91
+ "dialogs": {"commands": ["list", "get", "search"]},
92
+ "help": {"commands": []},
93
+ "media": {"commands": ["download"]},
94
+ "messages": {
95
+ "commands": [
96
+ "list",
97
+ "get",
98
+ "search",
99
+ "send",
100
+ "reply",
101
+ "forward",
102
+ "edit",
103
+ "delete",
104
+ "read",
105
+ "react",
106
+ "pin",
107
+ "unpin",
108
+ ]
109
+ },
110
+ "polls": {"commands": ["create", "vote", "close"]},
111
+ "profiles": {"commands": ["create", "list", "get", "set-default", "remove"]},
112
+ "raw": {"commands": ["invoke"]},
113
+ "scheduled": {"commands": ["list", "cancel"]},
114
+ "schema": {"commands": ["list", "get", "export"]},
115
+ "state": {"commands": ["get", "prune"]},
116
+ "topics": {"commands": ["list"]},
117
+ "version": {"commands": []},
118
+ }
119
+
120
+ SCHEMA_MODELS = {
121
+ cls.__name__: cls
122
+ for cls in (
123
+ Capability,
124
+ CapabilityCatalog,
125
+ Confirmation,
126
+ Envelope,
127
+ ErrorInfo,
128
+ JsonlRecord,
129
+ LoginState,
130
+ Meta,
131
+ ProfileView,
132
+ )
133
+ }
134
+
135
+
136
+ def method_name(request_class: type[TLRequest]) -> str:
137
+ """Convert a generated Telethon request class to its canonical TL method name."""
138
+
139
+ namespace = request_class.__module__.rsplit(".", maxsplit=1)[-1]
140
+ class_name = request_class.__name__.removesuffix("Request")
141
+ return f"{namespace}.{class_name[0].lower()}{class_name[1:]}"
142
+
143
+
144
+ def risk_for(method: str) -> Literal["read", "write", "destructive", "critical", "unknown"]:
145
+ """Conservatively classify one raw method."""
146
+
147
+ if any(pattern.search(method) for pattern in CRITICAL_PATTERNS):
148
+ return "critical"
149
+ leaf = method.split(".", maxsplit=1)[1]
150
+ lowered = leaf.lower()
151
+ if lowered.startswith(DESTRUCTIVE_WORDS):
152
+ return "destructive"
153
+ if leaf.startswith(("get", "search")):
154
+ return "read"
155
+ if lowered.startswith(WRITE_WORDS):
156
+ return "write"
157
+ return "unknown"
158
+
159
+
160
+ def request_registry() -> dict[str, type[TLRequest]]:
161
+ """Return every request exposed by the installed Telethon layer."""
162
+
163
+ registry: dict[str, type[TLRequest]] = {}
164
+ for candidate in alltlobjects.tlobjects.values():
165
+ if inspect.isclass(candidate) and issubclass(candidate, TLRequest):
166
+ registry[method_name(candidate)] = candidate
167
+ return dict(sorted(registry.items()))
168
+
169
+
170
+ def capability_catalog() -> CapabilityCatalog:
171
+ """Generate the capability manifest from the live Telethon registry."""
172
+
173
+ capabilities: list[Capability] = []
174
+ for method, request_class in request_registry().items():
175
+ reason = UNSUPPORTED_METHODS.get(method)
176
+ if reason:
177
+ status = "unsupported"
178
+ elif method in HIGH_LEVEL_METHODS:
179
+ status = "high-level"
180
+ else:
181
+ status = "raw-only"
182
+ capabilities.append(
183
+ Capability(
184
+ method=method,
185
+ python_class=f"{request_class.__module__}.{request_class.__name__}",
186
+ status=status,
187
+ risk=risk_for(method),
188
+ command=HIGH_LEVEL_METHODS.get(method),
189
+ reason=reason,
190
+ )
191
+ )
192
+ return CapabilityCatalog(
193
+ telethon_version=telethon.__version__,
194
+ telegram_layer=alltlobjects.LAYER,
195
+ generated_at=datetime.now(UTC),
196
+ capabilities=capabilities,
197
+ )
198
+
199
+
200
+ def command_catalog() -> dict[str, Any]:
201
+ """Return the machine-readable top-level command catalog."""
202
+
203
+ return {
204
+ "schema_version": SCHEMA_VERSION,
205
+ "cli_version": __version__,
206
+ "global_options": {
207
+ "profile": "string|null",
208
+ "output": ["json", "jsonl"],
209
+ "timeout_seconds": "positive integer",
210
+ "verbose": "boolean",
211
+ },
212
+ "groups": COMMAND_CATALOG,
213
+ }
214
+
215
+
216
+ def schema_catalog() -> dict[str, Any]:
217
+ """Return JSON Schema documents for stable public models."""
218
+
219
+ return {
220
+ "schema_version": SCHEMA_VERSION,
221
+ "models": {name: model.model_json_schema() for name, model in SCHEMA_MODELS.items()},
222
+ }
223
+
224
+
225
+ def write_catalogs(root: Path) -> None:
226
+ """Write deterministic checked-in catalogs."""
227
+
228
+ root.mkdir(parents=True, exist_ok=True)
229
+ capabilities = capability_catalog().model_dump(mode="json")
230
+ capabilities["generated_at"] = "generated"
231
+ (root / "capabilities.json").write_text(
232
+ json.dumps(to_jsonable(capabilities), indent=2, sort_keys=True) + "\n",
233
+ encoding="utf-8",
234
+ )
235
+ (root / "schemas.json").write_text(
236
+ json.dumps(schema_catalog(), indent=2, sort_keys=True) + "\n",
237
+ encoding="utf-8",
238
+ )
239
+
240
+
241
+ if __name__ == "__main__":
242
+ write_catalogs(Path("schemas"))