tlgr-cli 2.0.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.
Files changed (192) hide show
  1. tlgr/__init__.py +3 -0
  2. tlgr/__main__.py +6 -0
  3. tlgr/actions/__init__.py +45 -0
  4. tlgr/actions/forward.py +74 -0
  5. tlgr/actions/reply.py +32 -0
  6. tlgr/cli/__init__.py +259 -0
  7. tlgr/cli/confirm.py +55 -0
  8. tlgr/cli/errors.py +84 -0
  9. tlgr/cli/gen.py +690 -0
  10. tlgr/cli/globals.py +273 -0
  11. tlgr/cli/introspect.py +170 -0
  12. tlgr/cli/params.py +189 -0
  13. tlgr/cli/render.py +418 -0
  14. tlgr/core/__init__.py +0 -0
  15. tlgr/core/accounts.py +384 -0
  16. tlgr/core/config.py +358 -0
  17. tlgr/core/custom_tl.py +170 -0
  18. tlgr/core/errors.py +687 -0
  19. tlgr/core/eventtypes.py +1170 -0
  20. tlgr/core/identity.py +127 -0
  21. tlgr/core/launchd.py +122 -0
  22. tlgr/core/logging.py +194 -0
  23. tlgr/core/media.py +134 -0
  24. tlgr/core/output.py +251 -0
  25. tlgr/core/pagination.py +227 -0
  26. tlgr/core/paths.py +360 -0
  27. tlgr/core/peers.py +427 -0
  28. tlgr/core/process.py +138 -0
  29. tlgr/core/signing.py +38 -0
  30. tlgr/core/systemd.py +96 -0
  31. tlgr/core/telethon_compat.py +295 -0
  32. tlgr/core/text.py +211 -0
  33. tlgr/core/timefmt.py +199 -0
  34. tlgr/core/tl.py +98 -0
  35. tlgr/daemon/__init__.py +0 -0
  36. tlgr/daemon/app.py +869 -0
  37. tlgr/daemon/dispatch.py +446 -0
  38. tlgr/daemon/events.py +723 -0
  39. tlgr/daemon/files.py +431 -0
  40. tlgr/daemon/idle.py +119 -0
  41. tlgr/daemon/jobs.py +68 -0
  42. tlgr/daemon/main.py +161 -0
  43. tlgr/daemon/peercred.py +75 -0
  44. tlgr/daemon/policy.py +113 -0
  45. tlgr/daemon/preauth.py +366 -0
  46. tlgr/daemon/ratelimit.py +391 -0
  47. tlgr/daemon/server.py +24 -0
  48. tlgr/daemon/session.py +648 -0
  49. tlgr/daemon/sessions.py +274 -0
  50. tlgr/daemon/singleton.py +114 -0
  51. tlgr/daemon/stream.py +193 -0
  52. tlgr/daemon/transfers.py +219 -0
  53. tlgr/daemon/webhook.py +390 -0
  54. tlgr/data/catalog_index.json +1 -0
  55. tlgr/data/parity_waivers.toml +90 -0
  56. tlgr/filters/__init__.py +42 -0
  57. tlgr/filters/compose.py +121 -0
  58. tlgr/filters/content.py +85 -0
  59. tlgr/filters/context.py +114 -0
  60. tlgr/filters/message.py +161 -0
  61. tlgr/filters/temporal.py +87 -0
  62. tlgr/filters/user.py +36 -0
  63. tlgr/gateway/__init__.py +1 -0
  64. tlgr/gateway/config.py +161 -0
  65. tlgr/gateway/engine.py +215 -0
  66. tlgr/gateway/event.py +22 -0
  67. tlgr/jobs/__init__.py +0 -0
  68. tlgr/jobs/base.py +81 -0
  69. tlgr/jobs/client.py +37 -0
  70. tlgr/models/__init__.py +1220 -0
  71. tlgr/models/admin.py +744 -0
  72. tlgr/models/auth.py +510 -0
  73. tlgr/models/base.py +81 -0
  74. tlgr/models/bot.py +576 -0
  75. tlgr/models/business.py +265 -0
  76. tlgr/models/call.py +586 -0
  77. tlgr/models/config.py +101 -0
  78. tlgr/models/contact.py +481 -0
  79. tlgr/models/daemon.py +336 -0
  80. tlgr/models/dialog.py +626 -0
  81. tlgr/models/envelope.py +68 -0
  82. tlgr/models/error.py +30 -0
  83. tlgr/models/event.py +79 -0
  84. tlgr/models/export.py +66 -0
  85. tlgr/models/gift.py +275 -0
  86. tlgr/models/inline.py +84 -0
  87. tlgr/models/location.py +115 -0
  88. tlgr/models/media.py +507 -0
  89. tlgr/models/message.py +584 -0
  90. tlgr/models/net.py +232 -0
  91. tlgr/models/notify.py +105 -0
  92. tlgr/models/page.py +32 -0
  93. tlgr/models/payment.py +172 -0
  94. tlgr/models/peer.py +400 -0
  95. tlgr/models/poll.py +119 -0
  96. tlgr/models/premium.py +161 -0
  97. tlgr/models/privacy.py +93 -0
  98. tlgr/models/profile.py +217 -0
  99. tlgr/models/reaction.py +160 -0
  100. tlgr/models/resolve.py +175 -0
  101. tlgr/models/settings.py +103 -0
  102. tlgr/models/stars.py +101 -0
  103. tlgr/models/sticker.py +243 -0
  104. tlgr/models/story.py +467 -0
  105. tlgr/models/sync.py +105 -0
  106. tlgr/models/todo.py +36 -0
  107. tlgr/models/webapp.py +89 -0
  108. tlgr/ops/__init__.py +63 -0
  109. tlgr/ops/_admin.py +313 -0
  110. tlgr/ops/_auth.py +599 -0
  111. tlgr/ops/_bots.py +586 -0
  112. tlgr/ops/_calls.py +535 -0
  113. tlgr/ops/_common.py +160 -0
  114. tlgr/ops/_layer.py +46 -0
  115. tlgr/ops/_media.py +592 -0
  116. tlgr/ops/_params.py +212 -0
  117. tlgr/ops/_rights.py +402 -0
  118. tlgr/ops/_send.py +593 -0
  119. tlgr/ops/_serialize.py +667 -0
  120. tlgr/ops/_settings.py +306 -0
  121. tlgr/ops/_spec.py +167 -0
  122. tlgr/ops/_story.py +743 -0
  123. tlgr/ops/account.py +2604 -0
  124. tlgr/ops/agent.py +937 -0
  125. tlgr/ops/auth.py +1282 -0
  126. tlgr/ops/bot.py +4880 -0
  127. tlgr/ops/business.py +1520 -0
  128. tlgr/ops/call.py +1610 -0
  129. tlgr/ops/chat.py +4025 -0
  130. tlgr/ops/chat_admin.py +929 -0
  131. tlgr/ops/chat_extra.py +1061 -0
  132. tlgr/ops/chat_invite.py +716 -0
  133. tlgr/ops/chat_manage.py +1691 -0
  134. tlgr/ops/chat_member.py +1357 -0
  135. tlgr/ops/chat_stats.py +902 -0
  136. tlgr/ops/chat_topic.py +905 -0
  137. tlgr/ops/conference.py +791 -0
  138. tlgr/ops/config.py +1698 -0
  139. tlgr/ops/contact.py +2330 -0
  140. tlgr/ops/daemon.py +1397 -0
  141. tlgr/ops/draft.py +299 -0
  142. tlgr/ops/emoji.py +343 -0
  143. tlgr/ops/events.py +1327 -0
  144. tlgr/ops/export.py +596 -0
  145. tlgr/ops/folder.py +1322 -0
  146. tlgr/ops/gif.py +522 -0
  147. tlgr/ops/gift.py +1546 -0
  148. tlgr/ops/giveaway.py +541 -0
  149. tlgr/ops/inline.py +773 -0
  150. tlgr/ops/job.py +799 -0
  151. tlgr/ops/location.py +917 -0
  152. tlgr/ops/media.py +4495 -0
  153. tlgr/ops/message.py +3769 -0
  154. tlgr/ops/net.py +536 -0
  155. tlgr/ops/notify.py +840 -0
  156. tlgr/ops/passport.py +464 -0
  157. tlgr/ops/payment.py +907 -0
  158. tlgr/ops/poll.py +1078 -0
  159. tlgr/ops/premium.py +488 -0
  160. tlgr/ops/privacy.py +794 -0
  161. tlgr/ops/profile.py +1481 -0
  162. tlgr/ops/proxy.py +750 -0
  163. tlgr/ops/reaction.py +1475 -0
  164. tlgr/ops/resolve.py +1140 -0
  165. tlgr/ops/search.py +521 -0
  166. tlgr/ops/settings.py +1066 -0
  167. tlgr/ops/stars.py +594 -0
  168. tlgr/ops/sticker.py +1602 -0
  169. tlgr/ops/story.py +3216 -0
  170. tlgr/ops/sync.py +788 -0
  171. tlgr/ops/todo.py +514 -0
  172. tlgr/ops/user.py +1406 -0
  173. tlgr/ops/vc.py +2351 -0
  174. tlgr/ops/webapp.py +717 -0
  175. tlgr/ops/webhook.py +418 -0
  176. tlgr/parity.py +386 -0
  177. tlgr/processors/__init__.py +125 -0
  178. tlgr/processors/regex.py +26 -0
  179. tlgr/processors/text.py +56 -0
  180. tlgr/registry.py +519 -0
  181. tlgr/schema.py +173 -0
  182. tlgr/transport/__init__.py +30 -0
  183. tlgr/transport/autostart.py +293 -0
  184. tlgr/transport/client.py +805 -0
  185. tlgr/transport/ndjson.py +44 -0
  186. tlgr/version.py +31 -0
  187. tlgr_cli-2.0.1.dist-info/METADATA +957 -0
  188. tlgr_cli-2.0.1.dist-info/RECORD +192 -0
  189. tlgr_cli-2.0.1.dist-info/WHEEL +5 -0
  190. tlgr_cli-2.0.1.dist-info/entry_points.txt +2 -0
  191. tlgr_cli-2.0.1.dist-info/licenses/LICENSE +21 -0
  192. tlgr_cli-2.0.1.dist-info/top_level.txt +1 -0
tlgr/cli/render.py ADDED
@@ -0,0 +1,418 @@
1
+ """Output rendering (ARCHITECTURE §9), driven entirely by the spec.
2
+
3
+ Three modes, one rule each:
4
+
5
+ * `--json` prints the envelope verbatim, so an agent parses one shape;
6
+ * `--plain` prints TSV that survives `cut` and `awk`;
7
+ * the default prints a table a person can read, with real formatting — v1
8
+ printed `None`, `True` and raw dict reprs into columns (UX-03).
9
+
10
+ `--results-only` returns `result` verbatim rather than guessing which key was
11
+ the "primary" one, which is how v1 came to print a bare `2` for
12
+ `message delete` (COR-18).
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import contextlib
18
+ import json
19
+ import os
20
+ import re
21
+ import sys
22
+ from collections.abc import Iterable, Sequence
23
+ from typing import Any
24
+
25
+ import click
26
+
27
+ from tlgr.core.timefmt import fmt_local
28
+
29
+ __all__ = [
30
+ "MAX_COLUMN",
31
+ "format_cell",
32
+ "get_path",
33
+ "project",
34
+ "render",
35
+ "render_human",
36
+ "render_json",
37
+ "render_plain",
38
+ "render_stream",
39
+ "results_payload",
40
+ ]
41
+
42
+ MAX_COLUMN = 48
43
+
44
+ _RFC3339 = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$")
45
+
46
+
47
+ def use_color(stream: Any = None) -> bool:
48
+ """Colour only on a TTY, and never when `NO_COLOR` is set."""
49
+ if os.environ.get("NO_COLOR") is not None:
50
+ return False
51
+ stream = stream or sys.stdout
52
+ return bool(getattr(stream, "isatty", lambda: False)())
53
+
54
+
55
+ # ---------------------------------------------------------------------------
56
+ # Projection (--select)
57
+ # ---------------------------------------------------------------------------
58
+
59
+
60
+ def get_path(obj: Any, path: str) -> tuple[Any, bool]:
61
+ """Walk a dot path into nested dicts and lists. Returns (value, found)."""
62
+ current = obj
63
+ for segment in (s.strip() for s in path.split(".") if s.strip()):
64
+ if isinstance(current, dict):
65
+ if segment not in current:
66
+ return None, False
67
+ current = current[segment]
68
+ elif isinstance(current, list):
69
+ try:
70
+ current = current[int(segment)]
71
+ except (ValueError, IndexError):
72
+ return None, False
73
+ else:
74
+ return None, False
75
+ return current, True
76
+
77
+
78
+ def project(data: Any, fields: Sequence[str]) -> Any:
79
+ """Keep only *fields*, recursing element-wise into lists.
80
+
81
+ Missing paths are omitted rather than emitted as null: `--select` is a
82
+ projection, and inventing keys would make the output lie about what the
83
+ operation returned.
84
+ """
85
+ if isinstance(data, list):
86
+ return [project(item, fields) for item in data]
87
+ if not isinstance(data, dict):
88
+ return data
89
+ out: dict[str, Any] = {}
90
+ for path in fields:
91
+ value, found = get_path(data, path)
92
+ if found:
93
+ out[path] = value
94
+ return out
95
+
96
+
97
+ def _split_fields(select: str | None) -> list[str]:
98
+ return [f.strip() for f in (select or "").split(",") if f.strip()]
99
+
100
+
101
+ def results_payload(envelope: dict[str, Any]) -> Any:
102
+ """What `--results-only` prints.
103
+
104
+ For a paginated op that is the `Page[T]` object, so STYLE's contract
105
+ (`{items, has_more, next_cursor, total}`) holds; otherwise it is `result`
106
+ verbatim.
107
+ """
108
+ page = envelope.get("page")
109
+ if page is not None:
110
+ return {"items": envelope.get("result") or [], **page}
111
+ if "error" in envelope:
112
+ return envelope["error"]
113
+ return envelope.get("result")
114
+
115
+
116
+ # ---------------------------------------------------------------------------
117
+ # JSON
118
+ # ---------------------------------------------------------------------------
119
+
120
+
121
+ def render_json(
122
+ envelope: dict[str, Any],
123
+ *,
124
+ results_only: bool = False,
125
+ select: str | None = None,
126
+ stream: Any = None,
127
+ ) -> None:
128
+ """Print the envelope, or the projection of it the flags ask for."""
129
+ fields = _split_fields(select)
130
+ if results_only:
131
+ payload: Any = results_payload(envelope)
132
+ if fields:
133
+ payload = project(payload, fields)
134
+ elif fields:
135
+ # `--select` alone keeps the envelope and projects only `result`;
136
+ # v1 projected the envelope and printed `{}`.
137
+ payload = dict(envelope)
138
+ if "result" in payload:
139
+ payload["result"] = project(payload["result"], fields)
140
+ else:
141
+ payload = envelope
142
+
143
+ out = stream or sys.stdout
144
+ json.dump(payload, out, default=str, ensure_ascii=False)
145
+ out.write("\n")
146
+ out.flush()
147
+
148
+
149
+ # ---------------------------------------------------------------------------
150
+ # Cells
151
+ # ---------------------------------------------------------------------------
152
+
153
+
154
+ def format_cell(value: Any, *, wide: bool = False, width: int = MAX_COLUMN) -> str:
155
+ """One value, formatted for a human table (the §9 table)."""
156
+ if value is None:
157
+ return "-"
158
+ if isinstance(value, bool):
159
+ return "yes" if value else "no"
160
+ if isinstance(value, str):
161
+ text = fmt_local(value) if _RFC3339.match(value) else value
162
+ elif isinstance(value, dict):
163
+ # A dict in a column means the caller asked for the object rather than
164
+ # a dot path into it; summarise instead of printing a repr.
165
+ text = ", ".join(f"{k}={format_cell(v, wide=True)}" for k, v in value.items())
166
+ elif isinstance(value, Iterable) and not isinstance(value, bytes):
167
+ text = ", ".join(format_cell(item, wide=True) for item in value)
168
+ else:
169
+ text = str(value)
170
+
171
+ text = text.replace("\n", "⏎").replace("\r", "")
172
+ if not wide and len(text) > width:
173
+ text = text[: width - 1] + "…"
174
+ return text
175
+
176
+
177
+ def _rows(data: Any) -> list[dict[str, Any]]:
178
+ if data is None:
179
+ return []
180
+ if isinstance(data, list):
181
+ return [row if isinstance(row, dict) else {"value": row} for row in data]
182
+ if isinstance(data, dict):
183
+ return [data]
184
+ return [{"value": data}]
185
+
186
+
187
+ def _default_columns(rows: Sequence[dict[str, Any]]) -> list[str]:
188
+ return list(rows[0].keys()) if rows else ["value"]
189
+
190
+
191
+ def resolve_columns(
192
+ data: Any,
193
+ *,
194
+ spec_columns: Sequence[str] = (),
195
+ override: str | None = None,
196
+ ) -> list[str]:
197
+ if override:
198
+ return [c.strip() for c in override.split(",") if c.strip()]
199
+ if spec_columns:
200
+ return list(spec_columns)
201
+ return _default_columns(_rows(data))
202
+
203
+
204
+ # ---------------------------------------------------------------------------
205
+ # Plain (TSV)
206
+ # ---------------------------------------------------------------------------
207
+
208
+
209
+ def _tsv(value: Any) -> str:
210
+ if value is None:
211
+ return ""
212
+ if isinstance(value, bool):
213
+ return "true" if value else "false"
214
+ if isinstance(value, (list, tuple, dict)):
215
+ text = json.dumps(value, ensure_ascii=False, default=str)
216
+ else:
217
+ text = str(value)
218
+ return text.replace("\t", " ").replace("\n", " ").replace("\r", " ")
219
+
220
+
221
+ def render_plain(
222
+ data: Any,
223
+ columns: Sequence[str],
224
+ *,
225
+ no_header: bool = False,
226
+ stream: Any = None,
227
+ ) -> None:
228
+ out = stream or sys.stdout
229
+ if not no_header:
230
+ out.write("\t".join(columns) + "\n")
231
+ for row in _rows(data):
232
+ out.write("\t".join(_tsv(get_path(row, c)[0]) for c in columns) + "\n")
233
+ out.flush()
234
+
235
+
236
+ # ---------------------------------------------------------------------------
237
+ # Human
238
+ # ---------------------------------------------------------------------------
239
+
240
+
241
+ def render_human(
242
+ data: Any,
243
+ columns: Sequence[str],
244
+ *,
245
+ headers: Sequence[str] = (),
246
+ wide: bool = False,
247
+ no_header: bool = False,
248
+ stream: Any = None,
249
+ ) -> None:
250
+ """Table for lists, key/value for a single object."""
251
+ out = stream or sys.stdout
252
+ rows = _rows(data)
253
+
254
+ if isinstance(data, dict) and not columns:
255
+ _render_object(data, wide=wide, stream=out)
256
+ return
257
+
258
+ if not rows:
259
+ return
260
+
261
+ titles = list(headers) if headers else [c.upper() for c in columns]
262
+ cells = [[format_cell(get_path(row, c)[0], wide=wide) for c in columns] for row in rows]
263
+ widths = [len(t) for t in titles]
264
+ for row_cells in cells:
265
+ for index, value in enumerate(row_cells):
266
+ widths[index] = max(widths[index], len(value))
267
+
268
+ gap = " "
269
+ if not no_header:
270
+ header = gap.join(t.ljust(w) for t, w in zip(titles, widths, strict=True)).rstrip()
271
+ out.write((click.style(header, bold=True) if use_color(out) else header) + "\n")
272
+ for row_cells in cells:
273
+ out.write(
274
+ gap.join(v.ljust(w) for v, w in zip(row_cells, widths, strict=True)).rstrip() + "\n"
275
+ )
276
+ out.flush()
277
+
278
+
279
+ def _render_object(data: dict[str, Any], *, wide: bool, stream: Any) -> None:
280
+ """A single object as a key/value block, with maps of records as tables.
281
+
282
+ A mapping whose values are all objects (the exit-code table, a per-account
283
+ health map) is data with a natural key column; printing it as one long
284
+ `k=v` cell would be technically correct and useless.
285
+ """
286
+ scalars = {k: v for k, v in data.items() if not _is_record_map(v)}
287
+ if scalars:
288
+ width = max(len(k) for k in scalars)
289
+ for key, value in scalars.items():
290
+ stream.write(f"{key.ljust(width)} {format_cell(value, wide=wide)}\n")
291
+
292
+ for key, value in data.items():
293
+ if not _is_record_map(value):
294
+ continue
295
+ inner_columns = sorted({k for record in value.values() for k in record})
296
+ rows = [{"name": name, **record} for name, record in value.items()]
297
+ if scalars:
298
+ stream.write("\n")
299
+ stream.write(f"{key}:\n")
300
+ render_human(rows, ["name", *inner_columns], wide=wide, stream=stream)
301
+ stream.flush()
302
+
303
+
304
+ def _is_record_map(value: Any) -> bool:
305
+ return (
306
+ isinstance(value, dict)
307
+ and bool(value)
308
+ and all(isinstance(item, dict) for item in value.values())
309
+ )
310
+
311
+
312
+ # ---------------------------------------------------------------------------
313
+ # Entry point
314
+ # ---------------------------------------------------------------------------
315
+
316
+
317
+ def render(
318
+ envelope: dict[str, Any],
319
+ *,
320
+ fmt: str = "human",
321
+ results_only: bool = False,
322
+ select: str | None = None,
323
+ spec_columns: Sequence[str] = (),
324
+ headers: Sequence[str] = (),
325
+ columns: str | None = None,
326
+ wide: bool = False,
327
+ no_header: bool = False,
328
+ stream: Any = None,
329
+ ) -> None:
330
+ """Render one response envelope in the mode the flags ask for."""
331
+ if fmt == "json":
332
+ render_json(envelope, results_only=results_only, select=select, stream=stream)
333
+ return
334
+
335
+ data = results_payload(envelope)
336
+ if isinstance(data, dict) and "items" in data and envelope.get("page") is not None:
337
+ data = data["items"]
338
+ fields = _split_fields(select)
339
+ if fields:
340
+ data = project(data, fields)
341
+ spec_columns = fields
342
+
343
+ resolved = resolve_columns(data, spec_columns=spec_columns, override=columns)
344
+ if fmt == "plain":
345
+ render_plain(data, resolved, no_header=no_header, stream=stream)
346
+ return
347
+
348
+ if isinstance(data, dict) and not columns and not fields:
349
+ render_human(data, (), wide=wide, stream=stream)
350
+ return
351
+ render_human(
352
+ data,
353
+ resolved,
354
+ headers=headers if len(headers) == len(resolved) else (),
355
+ wide=wide,
356
+ no_header=no_header,
357
+ stream=stream,
358
+ )
359
+
360
+
361
+ #: Frames that describe the *stream* rather than something that happened. A
362
+ #: consumer filtering on the event type has to be able to tell them apart,
363
+ #: which is why they are named here rather than recognised by their absence.
364
+ CONTROL_FRAMES = frozenset(
365
+ {"meta", "end", "heartbeat", "lag", "gap", "watching", "cursor", "page", "item"}
366
+ )
367
+
368
+
369
+ def render_stream(
370
+ frames: Iterable[dict[str, Any]],
371
+ *,
372
+ fmt: str = "human",
373
+ results_only: bool = False,
374
+ select: str | None = None,
375
+ stream: Any = None,
376
+ ) -> int:
377
+ """Print an NDJSON stream as it arrives, and return the exit code.
378
+
379
+ Flushed per frame, deliberately: a `watch` piped into `jq` that only
380
+ appears once a 4 KB buffer fills is indistinguishable from a daemon that
381
+ is not delivering anything.
382
+
383
+ `--results-only` restores v1's line shape — `{"event_type", "chat_id",
384
+ "data"}` — and drops the control frames, because that is what a script
385
+ written against v1's `tlgr watch` already parses (§12.4).
386
+ """
387
+ out = stream or sys.stdout
388
+ fields = _split_fields(select)
389
+ exit_code = 0
390
+ for frame in frames:
391
+ kind = str(frame.get("type", ""))
392
+ if kind == "end" and not frame.get("ok", True):
393
+ from tlgr.core.errors import EXIT_RETRYABLE
394
+
395
+ body = frame.get("error") or {}
396
+ exit_code = int(body.get("exit_code", EXIT_RETRYABLE))
397
+ click.echo(f"Error: {body.get('message') or 'the stream ended'}", err=True)
398
+ continue
399
+ if results_only:
400
+ if kind in CONTROL_FRAMES:
401
+ continue
402
+ frame = {
403
+ "event_type": kind,
404
+ "chat_id": frame.get("chat_id"),
405
+ "data": frame.get("payload", {}),
406
+ "seq": frame.get("seq"),
407
+ "account": frame.get("account"),
408
+ }
409
+ if fields:
410
+ frame = project(frame, fields)
411
+ _write_frame(frame, out)
412
+ return exit_code
413
+
414
+
415
+ def _write_frame(frame: Any, out: Any) -> None:
416
+ out.write(json.dumps(frame, ensure_ascii=False, default=str) + "\n")
417
+ with contextlib.suppress(AttributeError, ValueError, OSError): # a closed pipe
418
+ out.flush()
tlgr/core/__init__.py ADDED
File without changes