tgdata 0.0.6__tar.gz → 0.0.8__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.
Files changed (36) hide show
  1. {tgdata-0.0.6 → tgdata-0.0.8}/PKG-INFO +83 -2
  2. {tgdata-0.0.6 → tgdata-0.0.8}/README.md +81 -0
  3. {tgdata-0.0.6 → tgdata-0.0.8}/setup.py +3 -1
  4. {tgdata-0.0.6 → tgdata-0.0.8}/tgdata/__init__.py +11 -5
  5. {tgdata-0.0.6 → tgdata-0.0.8}/tgdata/connection_engine.py +36 -0
  6. tgdata-0.0.8/tgdata/discovery_engine.py +579 -0
  7. {tgdata-0.0.6 → tgdata-0.0.8}/tgdata/models.py +29 -0
  8. tgdata-0.0.8/tgdata/smoke_tests/test_11_discover_groups.py +224 -0
  9. {tgdata-0.0.6 → tgdata-0.0.8}/tgdata/tgdata.py +189 -1
  10. {tgdata-0.0.6 → tgdata-0.0.8}/tgdata/utils.py +53 -2
  11. {tgdata-0.0.6 → tgdata-0.0.8}/tgdata.egg-info/PKG-INFO +83 -2
  12. {tgdata-0.0.6 → tgdata-0.0.8}/tgdata.egg-info/SOURCES.txt +2 -0
  13. tgdata-0.0.8/tgdata.egg-info/requires.txt +2 -0
  14. tgdata-0.0.6/tgdata.egg-info/requires.txt +0 -2
  15. {tgdata-0.0.6 → tgdata-0.0.8}/setup.cfg +0 -0
  16. {tgdata-0.0.6 → tgdata-0.0.8}/tgdata/message_engine.py +0 -0
  17. {tgdata-0.0.6 → tgdata-0.0.8}/tgdata/progress.py +0 -0
  18. {tgdata-0.0.6 → tgdata-0.0.8}/tgdata/smoke_tests/__init__.py +0 -0
  19. {tgdata-0.0.6 → tgdata-0.0.8}/tgdata/smoke_tests/my_test.py +0 -0
  20. {tgdata-0.0.6 → tgdata-0.0.8}/tgdata/smoke_tests/test_00_connection.py +0 -0
  21. {tgdata-0.0.6 → tgdata-0.0.8}/tgdata/smoke_tests/test_01_list_group_chats.py +0 -0
  22. {tgdata-0.0.6 → tgdata-0.0.8}/tgdata/smoke_tests/test_02_get_message_count.py +0 -0
  23. {tgdata-0.0.6 → tgdata-0.0.8}/tgdata/smoke_tests/test_03_get_all_messages.py +0 -0
  24. {tgdata-0.0.6 → tgdata-0.0.8}/tgdata/smoke_tests/test_04_get_all_messages_in_batches.py +0 -0
  25. {tgdata-0.0.6 → tgdata-0.0.8}/tgdata/smoke_tests/test_05_with_progress_feature.py +0 -0
  26. {tgdata-0.0.6 → tgdata-0.0.8}/tgdata/smoke_tests/test_06_polling_simple.py +0 -0
  27. {tgdata-0.0.6 → tgdata-0.0.8}/tgdata/smoke_tests/test_07_on_new_message_simple.py +0 -0
  28. {tgdata-0.0.6 → tgdata-0.0.8}/tgdata/smoke_tests/test_07_real_time_event_handler.py +0 -0
  29. {tgdata-0.0.6 → tgdata-0.0.8}/tgdata/smoke_tests/test_08_batch_callback.py +0 -0
  30. {tgdata-0.0.6 → tgdata-0.0.8}/tgdata/smoke_tests/test_08_polling.py +0 -0
  31. {tgdata-0.0.6 → tgdata-0.0.8}/tgdata/smoke_tests/test_10_advanced_features.py +0 -0
  32. {tgdata-0.0.6 → tgdata-0.0.8}/tgdata/smoke_tests/test_debug_min_id.py +0 -0
  33. {tgdata-0.0.6 → tgdata-0.0.8}/tgdata/smoke_tests/test_polling_debug.py +0 -0
  34. {tgdata-0.0.6 → tgdata-0.0.8}/tgdata/smoke_tests/test_username_support.py +0 -0
  35. {tgdata-0.0.6 → tgdata-0.0.8}/tgdata.egg-info/dependency_links.txt +0 -0
  36. {tgdata-0.0.6 → tgdata-0.0.8}/tgdata.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tgdata
3
- Version: 0.0.6
3
+ Version: 0.0.8
4
4
  Summary: A production-grade Python library for extracting and processing Telegram group and channel messages
5
5
  Author: enes kuzucu
6
6
  Classifier: Development Status :: 3 - Alpha
@@ -14,7 +14,7 @@ Classifier: Programming Language :: Python :: 3.10
14
14
  Classifier: Operating System :: OS Independent
15
15
  Requires-Python: >=3.7
16
16
  Description-Content-Type: text/markdown
17
- Requires-Dist: Telethon<2.0,>=1.24
17
+ Requires-Dist: Telethon<2.0,>=1.33
18
18
  Requires-Dist: pandas>=1.0
19
19
  Dynamic: author
20
20
  Dynamic: classifier
@@ -28,6 +28,14 @@ Dynamic: summary
28
28
 
29
29
  A production-grade Python library for extracting and processing Telegram group and channel messages. Designed for ETL pipelines, data analysis, and archival purposes.
30
30
 
31
+ > **New in 0.0.8 — group discovery.** tgdata can now *find* groups and channels, not only read
32
+ > them: Telegram's name search, its "similar channels" recommendations, and links mined from
33
+ > posts, on the same persistent session, paced, flood-safe and budgeted. Four calls —
34
+ > `search_groups`, `similar_groups`, `linked_groups`, `discover_groups` — return candidate rooms
35
+ > in the `list_groups` row shape, already usable by `get_messages`. Quick start below under
36
+ > [Group discovery](#group-discovery--finding-groups-you-are-not-in); the full guide to using it
37
+ > properly is [`devdocs/guides/group_discovery.md`](devdocs/guides/group_discovery.md).
38
+
31
39
  ## Features
32
40
 
33
41
  - 🚀 **Production-Ready**: Built for reliability and scale in ETL pipelines
@@ -42,6 +50,7 @@ A production-grade Python library for extracting and processing Telegram group a
42
50
  - ⏱️ **Polling Support**: Poll for new messages at configurable intervals
43
51
  - 🎯 **Batch Processing**: Handle large groups with configurable batch sizes and delays
44
52
  - 🖼️ **Media Detection & Download**: Flag photos/videos per message (no download needed) and pull the files on demand or during the fetch
53
+ - 🔎 **Group Discovery**: Find rooms you are not in — Telegram's name search, its "similar channels", and links mined from posts — paced, flood-safe, budgeted
45
54
 
46
55
  ## Installation
47
56
 
@@ -110,6 +119,78 @@ Outputs:
110
119
  Python Devs 10012312313
111
120
  ```
112
121
 
122
+ ### Group discovery — finding groups you are not in
123
+
124
+ `list_groups` shows what the account already follows. To find *new* rooms there are three
125
+ routes, all returning the same columns as `list_groups` plus `FoundVia` and `FoundBy`.
126
+ This is the quick start; the rules for running discovery on a real account without earning
127
+ a block — pacing, budgets, resumable runs, reading the output — are in
128
+ [`devdocs/guides/group_discovery.md`](devdocs/guides/group_discovery.md).
129
+
130
+ ```python
131
+ from tgdata import TgData, build_search_queries
132
+
133
+ tg = TgData("config.ini")
134
+
135
+ # 1. Telegram's global search — matches room NAMES and @usernames only, never post text
136
+ df = await tg.search_groups("Анталия аренда")
137
+
138
+ # 2. Telegram's "similar channels" — rooms whose subscribers overlap the seeds', no keyword needed
139
+ df = await tg.similar_groups(["@antalyadaa", "@sprout_antalya"], rounds=2)
140
+
141
+ # 3. t.me links found in a room's recent posts — usernames only (free), or resolved
142
+ df = await tg.linked_groups(group_id, posts=100, resolve=False)
143
+
144
+ # All three on one session, deduplicated by id, in cost order
145
+ df = await tg.discover_groups(
146
+ seeds=["@antalyadaa"],
147
+ queries=build_search_queries(["Анталия", "Antalya"], topics=["чат", "аренда", "новости"],
148
+ prefixes=["Турция"]),
149
+ mine_links=True,
150
+ heartbeat=lambda phase: print(phase), # liveness ticks: search / similar / links / pause / flood-wait Ns
151
+ found_callback=lambda row: rows.append(row), # each room as it is found — persist as you go
152
+ )
153
+ ```
154
+
155
+ **The name-only rule.** Telegram's search matches names and @usernames, never what a room
156
+ posts. A perfect room under an unrelated name is invisible to every query — that is what
157
+ `similar_groups` and `linked_groups` are for. Rooms name themselves "place + topic", so
158
+ `build_search_queries` combines your place spellings with topic words, in both word orders.
159
+
160
+ | Column | Meaning |
161
+ |---|---|
162
+ | `GroupID` … `ParticipantsCount` | the `list_groups` columns; `ParticipantsCount` is present on search and similar-channel results, absent (`NA`) on rooms resolved from links — Telegram's lookup reply does not carry it |
163
+ | `FoundVia` | `search` · `similar` · `link` — the cheapest route that found the room |
164
+ | `FoundBy` | the query, the seed's `@name`, or the source room |
165
+
166
+ Unresolved link rows carry only `Username`/`Identifier`; `GroupID` and the flags are `NA`
167
+ (the columns are nullable, so dtypes never drift). Every row with an id is already usable
168
+ by `get_messages(row['GroupID'])` — Telegram results land in the session cache with their
169
+ access hash, no dialog sync needed.
170
+
171
+ **Pacing and waits.** Every request is followed by `pace` seconds (default 2, the proven
172
+ pace for a personal account). A rate-limit wait up to `max_flood_wait` is obeyed exactly,
173
+ with heartbeat ticks so a watchdog sees life; a longer one raises `DiscoveryInterrupted`,
174
+ which carries `.found` (everything collected so far) and `.retry_after`. A dropped connection
175
+ is waited out up to `max_offline` and the same request retried. Nothing found is ever lost.
176
+
177
+ **The budget.** Resolving a username to a room is the request Telegram punishes hardest —
178
+ a production account lost 8 and 21 hours of lookups to it. So every call has `max_resolve`
179
+ (default 100), `discover_groups` mines links from the first `link_sources` rooms only
180
+ (default 50), `resolve_links` is off by default (mined names come back as text, which costs
181
+ nothing), and a wait on a lookup is skipped or stopped, never waited out. A truncating cap
182
+ logs a warning telling you which parameter lifts it.
183
+
184
+ **Similar channels** work on channels and megagroups, not basic groups; two rounds by
185
+ default (a third drifts off topic); the seeds themselves are never returned. Yield depends
186
+ on the account — a Premium account got 64 recommendations from one seed, a normal account
187
+ reportedly about 10.
188
+
189
+ **Links** are read from the visible text, from hyperlinked text (where channels put nearly
190
+ all of them) and from URL buttons. Accepted: `t.me/name`, `t.me/name/123`, `t.me/s/name`,
191
+ `t.me/boost/name`, `telegram.me/name`. Skipped: invite links (`t.me/+…`), `t.me/c/…`,
192
+ reserved paths, and bots (`…bot`). Discovery reads; it never joins.
193
+
113
194
 
114
195
  ### Get all messages of a chat
115
196
 
@@ -2,6 +2,14 @@
2
2
 
3
3
  A production-grade Python library for extracting and processing Telegram group and channel messages. Designed for ETL pipelines, data analysis, and archival purposes.
4
4
 
5
+ > **New in 0.0.8 — group discovery.** tgdata can now *find* groups and channels, not only read
6
+ > them: Telegram's name search, its "similar channels" recommendations, and links mined from
7
+ > posts, on the same persistent session, paced, flood-safe and budgeted. Four calls —
8
+ > `search_groups`, `similar_groups`, `linked_groups`, `discover_groups` — return candidate rooms
9
+ > in the `list_groups` row shape, already usable by `get_messages`. Quick start below under
10
+ > [Group discovery](#group-discovery--finding-groups-you-are-not-in); the full guide to using it
11
+ > properly is [`devdocs/guides/group_discovery.md`](devdocs/guides/group_discovery.md).
12
+
5
13
  ## Features
6
14
 
7
15
  - 🚀 **Production-Ready**: Built for reliability and scale in ETL pipelines
@@ -16,6 +24,7 @@ A production-grade Python library for extracting and processing Telegram group a
16
24
  - ⏱️ **Polling Support**: Poll for new messages at configurable intervals
17
25
  - 🎯 **Batch Processing**: Handle large groups with configurable batch sizes and delays
18
26
  - 🖼️ **Media Detection & Download**: Flag photos/videos per message (no download needed) and pull the files on demand or during the fetch
27
+ - 🔎 **Group Discovery**: Find rooms you are not in — Telegram's name search, its "similar channels", and links mined from posts — paced, flood-safe, budgeted
19
28
 
20
29
  ## Installation
21
30
 
@@ -84,6 +93,78 @@ Outputs:
84
93
  Python Devs 10012312313
85
94
  ```
86
95
 
96
+ ### Group discovery — finding groups you are not in
97
+
98
+ `list_groups` shows what the account already follows. To find *new* rooms there are three
99
+ routes, all returning the same columns as `list_groups` plus `FoundVia` and `FoundBy`.
100
+ This is the quick start; the rules for running discovery on a real account without earning
101
+ a block — pacing, budgets, resumable runs, reading the output — are in
102
+ [`devdocs/guides/group_discovery.md`](devdocs/guides/group_discovery.md).
103
+
104
+ ```python
105
+ from tgdata import TgData, build_search_queries
106
+
107
+ tg = TgData("config.ini")
108
+
109
+ # 1. Telegram's global search — matches room NAMES and @usernames only, never post text
110
+ df = await tg.search_groups("Анталия аренда")
111
+
112
+ # 2. Telegram's "similar channels" — rooms whose subscribers overlap the seeds', no keyword needed
113
+ df = await tg.similar_groups(["@antalyadaa", "@sprout_antalya"], rounds=2)
114
+
115
+ # 3. t.me links found in a room's recent posts — usernames only (free), or resolved
116
+ df = await tg.linked_groups(group_id, posts=100, resolve=False)
117
+
118
+ # All three on one session, deduplicated by id, in cost order
119
+ df = await tg.discover_groups(
120
+ seeds=["@antalyadaa"],
121
+ queries=build_search_queries(["Анталия", "Antalya"], topics=["чат", "аренда", "новости"],
122
+ prefixes=["Турция"]),
123
+ mine_links=True,
124
+ heartbeat=lambda phase: print(phase), # liveness ticks: search / similar / links / pause / flood-wait Ns
125
+ found_callback=lambda row: rows.append(row), # each room as it is found — persist as you go
126
+ )
127
+ ```
128
+
129
+ **The name-only rule.** Telegram's search matches names and @usernames, never what a room
130
+ posts. A perfect room under an unrelated name is invisible to every query — that is what
131
+ `similar_groups` and `linked_groups` are for. Rooms name themselves "place + topic", so
132
+ `build_search_queries` combines your place spellings with topic words, in both word orders.
133
+
134
+ | Column | Meaning |
135
+ |---|---|
136
+ | `GroupID` … `ParticipantsCount` | the `list_groups` columns; `ParticipantsCount` is present on search and similar-channel results, absent (`NA`) on rooms resolved from links — Telegram's lookup reply does not carry it |
137
+ | `FoundVia` | `search` · `similar` · `link` — the cheapest route that found the room |
138
+ | `FoundBy` | the query, the seed's `@name`, or the source room |
139
+
140
+ Unresolved link rows carry only `Username`/`Identifier`; `GroupID` and the flags are `NA`
141
+ (the columns are nullable, so dtypes never drift). Every row with an id is already usable
142
+ by `get_messages(row['GroupID'])` — Telegram results land in the session cache with their
143
+ access hash, no dialog sync needed.
144
+
145
+ **Pacing and waits.** Every request is followed by `pace` seconds (default 2, the proven
146
+ pace for a personal account). A rate-limit wait up to `max_flood_wait` is obeyed exactly,
147
+ with heartbeat ticks so a watchdog sees life; a longer one raises `DiscoveryInterrupted`,
148
+ which carries `.found` (everything collected so far) and `.retry_after`. A dropped connection
149
+ is waited out up to `max_offline` and the same request retried. Nothing found is ever lost.
150
+
151
+ **The budget.** Resolving a username to a room is the request Telegram punishes hardest —
152
+ a production account lost 8 and 21 hours of lookups to it. So every call has `max_resolve`
153
+ (default 100), `discover_groups` mines links from the first `link_sources` rooms only
154
+ (default 50), `resolve_links` is off by default (mined names come back as text, which costs
155
+ nothing), and a wait on a lookup is skipped or stopped, never waited out. A truncating cap
156
+ logs a warning telling you which parameter lifts it.
157
+
158
+ **Similar channels** work on channels and megagroups, not basic groups; two rounds by
159
+ default (a third drifts off topic); the seeds themselves are never returned. Yield depends
160
+ on the account — a Premium account got 64 recommendations from one seed, a normal account
161
+ reportedly about 10.
162
+
163
+ **Links** are read from the visible text, from hyperlinked text (where channels put nearly
164
+ all of them) and from URL buttons. Accepted: `t.me/name`, `t.me/name/123`, `t.me/s/name`,
165
+ `t.me/boost/name`, `telegram.me/name`. Skipped: invite links (`t.me/+…`), `t.me/c/…`,
166
+ reserved paths, and bots (`…bot`). Discovery reads; it never joins.
167
+
87
168
 
88
169
  ### Get all messages of a chat
89
170
 
@@ -33,7 +33,9 @@ setup(
33
33
  install_requires=[
34
34
  # min_id is exclusive in Telethon 1.x (the off-by-one fix relies on it);
35
35
  # 2.0 is a breaking API rewrite, so cap below it. Verified on 1.40.
36
- 'Telethon>=1.24,<2.0',
36
+ # 1.33 is the first release carrying channels.GetChannelRecommendationsRequest
37
+ # (group discovery, scoped/10) — verified against the 1.33.1 sdist.
38
+ 'Telethon>=1.33,<2.0',
37
39
  # Required, not optional: everything returns DataFrames. Nullable Int64
38
40
  # (used for GroupedId album ids > 2^53) needs pandas >= 1.0.
39
41
  'pandas>=1.0',
@@ -9,6 +9,8 @@ from .tgdata import TgData
9
9
 
10
10
  # Errors
11
11
  from .message_engine import GroupAccessError
12
+ from .connection_engine import AuthRequiredError
13
+ from .discovery_engine import DiscoveryInterrupted
12
14
 
13
15
  # Models
14
16
  from .models import (
@@ -27,13 +29,14 @@ from .utils import (
27
29
  filter_messages_by_content,
28
30
  get_message_statistics,
29
31
  save_profile_photos,
30
- create_metrics_report
32
+ create_metrics_report,
33
+ build_search_queries
31
34
  )
32
35
 
33
36
  # Progress tracking
34
37
  from .progress import ProgressTracker
35
38
 
36
- __version__ = "0.0.6"
39
+ __version__ = "0.0.8"
37
40
 
38
41
  __all__ = [
39
42
  # Main class
@@ -41,13 +44,15 @@ __all__ = [
41
44
 
42
45
  # Errors
43
46
  "GroupAccessError",
47
+ "AuthRequiredError",
48
+ "DiscoveryInterrupted",
44
49
 
45
50
  # Models
46
51
  "MessageData",
47
52
  "GroupInfo",
48
53
  "ConnectionConfig",
49
54
  "RateLimitInfo",
50
-
55
+
51
56
  # Utilities
52
57
  "format_message_for_display",
53
58
  "export_to_json",
@@ -57,7 +62,8 @@ __all__ = [
57
62
  "get_message_statistics",
58
63
  "save_profile_photos",
59
64
  "create_metrics_report",
60
-
65
+ "build_search_queries",
66
+
61
67
  # Progress
62
68
  "ProgressTracker"
63
- ]
69
+ ]
@@ -67,6 +67,14 @@ class ConnectionPool:
67
67
  self.connections.clear()
68
68
 
69
69
 
70
+ class AuthRequiredError(RuntimeError):
71
+ """The session is not authorized and this code path is non-interactive.
72
+
73
+ Raised by ephemeral_client() instead of Telethon's start() prompt —
74
+ a headless process must FAIL LOUDLY here, never sit on input().
75
+ Fix: run an interactive login for this account once, by hand."""
76
+
77
+
70
78
  class ConnectionEngine:
71
79
  """
72
80
  Manages Telegram connections with advanced features.
@@ -189,6 +197,34 @@ class ConnectionEngine:
189
197
  yield client
190
198
  # Intentionally NO disconnect here — the connection is persistent.
191
199
 
200
+ @asynccontextmanager
201
+ async def ephemeral_client(self):
202
+ """USE-AND-CLOSE (2026-08-05): a fresh client on the same resolved
203
+ session, connected WITHOUT start() (never interactive), disconnected
204
+ in finally — for short, human-triggered lookups (identity fetch,
205
+ access probe) that must not park a live transport next to a standing
206
+ loop sharing the session file. The persistent session()/pool path
207
+ above is untouched — it remains correct for the loop's own fetches,
208
+ where ONE process owns the session for its lifetime.
209
+
210
+ Raises AuthRequiredError when the session is not authorized: the
211
+ one situation start() would have prompted for a login code and hung
212
+ a headless process on stdin forever."""
213
+ config = self._load_config()
214
+ client = TelegramClient(config.session_file, config.api_id, config.api_hash)
215
+ try:
216
+ await client.connect()
217
+ if not await client.is_user_authorized():
218
+ raise AuthRequiredError(
219
+ f"session {config.session_file!r} is not authorized — "
220
+ f"run an interactive login for this account once, then retry")
221
+ yield client
222
+ finally:
223
+ try:
224
+ await client.disconnect()
225
+ except Exception: # noqa: BLE001 — teardown must never mask the work
226
+ pass
227
+
192
228
  async def _init_primary_client(self):
193
229
  """Create and authenticate the primary client once (may be interactive)."""
194
230
  config = self._load_config()