tgdata 0.0.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.
- tgdata/__init__.py +57 -0
- tgdata/connection_engine.py +316 -0
- tgdata/message_engine.py +299 -0
- tgdata/models.py +69 -0
- tgdata/progress.py +105 -0
- tgdata/smoke_tests/__init__.py +1 -0
- tgdata/smoke_tests/my_test.py +149 -0
- tgdata/smoke_tests/test_00_connection.py +286 -0
- tgdata/smoke_tests/test_01_list_group_chats.py +408 -0
- tgdata/smoke_tests/test_02_get_message_count.py +73 -0
- tgdata/smoke_tests/test_03_get_all_messages.py +69 -0
- tgdata/smoke_tests/test_04_get_all_messages_in_batches.py +98 -0
- tgdata/smoke_tests/test_06_advanced_features.py +253 -0
- tgdata/smoke_tests/test_10_polling.py +244 -0
- tgdata/tgdata.py +554 -0
- tgdata/utils.py +247 -0
- tgdata-0.0.0.dist-info/METADATA +309 -0
- tgdata-0.0.0.dist-info/RECORD +20 -0
- tgdata-0.0.0.dist-info/WHEEL +5 -0
- tgdata-0.0.0.dist-info/top_level.txt +1 -0
tgdata/models.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data models for the Telegram Group Message Crawler.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import Optional, Any, Dict
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class RateLimitInfo:
|
|
13
|
+
"""Track rate limit information"""
|
|
14
|
+
requests_made: int = 0
|
|
15
|
+
window_start: float = 0
|
|
16
|
+
last_request: float = 0
|
|
17
|
+
flood_wait_until: float = 0
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class GroupInfo:
|
|
22
|
+
"""Information about a Telegram group/channel"""
|
|
23
|
+
id: int
|
|
24
|
+
title: str
|
|
25
|
+
username: Optional[str] = None
|
|
26
|
+
is_channel: bool = False
|
|
27
|
+
is_megagroup: bool = False
|
|
28
|
+
participants_count: Optional[int] = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class MessageData:
|
|
33
|
+
"""Structured message data"""
|
|
34
|
+
message_id: int
|
|
35
|
+
sender_id: int
|
|
36
|
+
sender_name: str
|
|
37
|
+
username: str
|
|
38
|
+
message: Optional[str]
|
|
39
|
+
date: datetime
|
|
40
|
+
reply_to_id: Optional[int] = None
|
|
41
|
+
forwarded_from: Optional[int] = None
|
|
42
|
+
photo_data: Optional[bytes] = None
|
|
43
|
+
|
|
44
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
45
|
+
"""Convert to dictionary for DataFrame creation"""
|
|
46
|
+
return {
|
|
47
|
+
'MessageId': self.message_id,
|
|
48
|
+
'SenderId': self.sender_id,
|
|
49
|
+
'Name': self.sender_name,
|
|
50
|
+
'Username': self.username,
|
|
51
|
+
'Message': self.message,
|
|
52
|
+
'Date': self.date,
|
|
53
|
+
'ReplyToId': self.reply_to_id,
|
|
54
|
+
'ForwardedFrom': self.forwarded_from,
|
|
55
|
+
'PhotoData': self.photo_data
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class ConnectionConfig:
|
|
61
|
+
"""Configuration for Telegram connection"""
|
|
62
|
+
api_id: str
|
|
63
|
+
api_hash: str
|
|
64
|
+
session_file: str
|
|
65
|
+
phone: Optional[str] = None
|
|
66
|
+
username: Optional[str] = None
|
|
67
|
+
max_retries: int = 3
|
|
68
|
+
retry_delay: float = 1.0
|
|
69
|
+
exponential_backoff: bool = True
|
tgdata/progress.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Progress tracking utilities for long-running operations.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
from typing import Optional, Callable
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ProgressTracker:
|
|
13
|
+
"""
|
|
14
|
+
Track progress of long-running operations.
|
|
15
|
+
This doesn't need persistence as it's for real-time progress only.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self,
|
|
19
|
+
total_expected: Optional[int] = None,
|
|
20
|
+
callback: Optional[Callable[[int, Optional[int], float], None]] = None):
|
|
21
|
+
"""
|
|
22
|
+
Initialize progress tracker.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
total_expected: Expected total items (if known)
|
|
26
|
+
callback: Optional callback function(current, total, rate)
|
|
27
|
+
"""
|
|
28
|
+
self.total_expected = total_expected
|
|
29
|
+
self.callback = callback
|
|
30
|
+
self.current = 0
|
|
31
|
+
self.start_time: Optional[datetime] = None
|
|
32
|
+
|
|
33
|
+
def start(self) -> None:
|
|
34
|
+
"""Start tracking progress."""
|
|
35
|
+
self.start_time = datetime.now()
|
|
36
|
+
self.current = 0
|
|
37
|
+
logger.debug("Progress tracking started")
|
|
38
|
+
|
|
39
|
+
def update(self, count: int = 1) -> None:
|
|
40
|
+
"""
|
|
41
|
+
Update progress.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
count: Number of items processed
|
|
45
|
+
"""
|
|
46
|
+
self.current += count
|
|
47
|
+
|
|
48
|
+
# Calculate rate
|
|
49
|
+
if self.start_time:
|
|
50
|
+
elapsed = (datetime.now() - self.start_time).total_seconds()
|
|
51
|
+
rate = self.current / elapsed if elapsed > 0 else 0
|
|
52
|
+
else:
|
|
53
|
+
rate = 0
|
|
54
|
+
|
|
55
|
+
# Call callback
|
|
56
|
+
if self.callback:
|
|
57
|
+
try:
|
|
58
|
+
self.callback(self.current, self.total_expected, rate)
|
|
59
|
+
except Exception as e:
|
|
60
|
+
logger.error(f"Progress callback error: {e}")
|
|
61
|
+
|
|
62
|
+
def get_rate(self) -> float:
|
|
63
|
+
"""Get current processing rate (items per second)."""
|
|
64
|
+
if not self.start_time:
|
|
65
|
+
return 0.0
|
|
66
|
+
|
|
67
|
+
elapsed = (datetime.now() - self.start_time).total_seconds()
|
|
68
|
+
return self.current / elapsed if elapsed > 0 else 0.0
|
|
69
|
+
|
|
70
|
+
def get_eta(self) -> Optional[datetime]:
|
|
71
|
+
"""Get estimated time of completion."""
|
|
72
|
+
if not self.total_expected or not self.start_time or self.current <= 0:
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
rate = self.get_rate()
|
|
76
|
+
if rate <= 0:
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
remaining = self.total_expected - self.current
|
|
80
|
+
eta_seconds = remaining / rate
|
|
81
|
+
|
|
82
|
+
return datetime.now() + timedelta(seconds=eta_seconds)
|
|
83
|
+
|
|
84
|
+
def get_elapsed(self) -> Optional[timedelta]:
|
|
85
|
+
"""Get elapsed time since start."""
|
|
86
|
+
if not self.start_time:
|
|
87
|
+
return None
|
|
88
|
+
return datetime.now() - self.start_time
|
|
89
|
+
|
|
90
|
+
def get_progress_percentage(self) -> Optional[float]:
|
|
91
|
+
"""Get progress as percentage (0-100)."""
|
|
92
|
+
if not self.total_expected or self.total_expected <= 0:
|
|
93
|
+
return None
|
|
94
|
+
return min(100.0, (self.current / self.total_expected) * 100)
|
|
95
|
+
|
|
96
|
+
def get_summary(self) -> dict:
|
|
97
|
+
"""Get progress summary."""
|
|
98
|
+
return {
|
|
99
|
+
'current': self.current,
|
|
100
|
+
'total': self.total_expected,
|
|
101
|
+
'rate': self.get_rate(),
|
|
102
|
+
'eta': self.get_eta(),
|
|
103
|
+
'elapsed': self.get_elapsed(),
|
|
104
|
+
'percentage': self.get_progress_percentage()
|
|
105
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Smoke tests for Telegram Group Message Crawler
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from tgdata import TgData
|
|
6
|
+
|
|
7
|
+
# python -m tgdata.smoke_tests.my_test
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def extract_to_csv_with_resume(tg: TgData, group_id: int, csv_file: str = "messages.csv", batch_size: int = 10):
|
|
12
|
+
"""
|
|
13
|
+
Extract messages to CSV using the CSV itself as checkpoint.
|
|
14
|
+
Supports resuming from the last message ID in the CSV.
|
|
15
|
+
|
|
16
|
+
Uses get_messages(after_id=...) for incremental extraction.
|
|
17
|
+
"""
|
|
18
|
+
# Read last message ID from existing CSV
|
|
19
|
+
last_message_id = 0
|
|
20
|
+
existing_count = 0
|
|
21
|
+
|
|
22
|
+
if os.path.exists(csv_file):
|
|
23
|
+
try:
|
|
24
|
+
# Read just the last row efficiently
|
|
25
|
+
# In production, you might use: pd.read_csv(csv_file, skiprows=lambda x: x < last_row)
|
|
26
|
+
df_existing = pd.read_csv(csv_file)
|
|
27
|
+
if not df_existing.empty:
|
|
28
|
+
# Get the maximum message ID (in case CSV is not ordered)
|
|
29
|
+
last_message_id = int(df_existing['MessageId'].max())
|
|
30
|
+
existing_count = len(df_existing)
|
|
31
|
+
print(f"📄 Found existing CSV with {existing_count} messages")
|
|
32
|
+
print(f"📍 Resuming from message ID: {last_message_id}")
|
|
33
|
+
except Exception as e:
|
|
34
|
+
print(f"⚠️ Could not read existing CSV: {e}")
|
|
35
|
+
print(" Starting fresh extraction")
|
|
36
|
+
else:
|
|
37
|
+
print("📄 No existing CSV found, starting fresh extraction")
|
|
38
|
+
|
|
39
|
+
# Use get_messages with after_id for incremental extraction
|
|
40
|
+
print(f"\n🔄 Extracting messages since ID {last_message_id}...")
|
|
41
|
+
|
|
42
|
+
# Get today's date at midnight in UTC
|
|
43
|
+
from datetime import datetime, time, timezone
|
|
44
|
+
today_utc = datetime.combine(datetime.now(timezone.utc).date(), time.min, tzinfo=timezone.utc)
|
|
45
|
+
print(f"📅 Filtering for messages from today ({today_utc.date()}) only")
|
|
46
|
+
print(f"⏰ Start time filter: {today_utc} (UTC)")
|
|
47
|
+
print(f"⏰ Current time: {datetime.now(timezone.utc)} (UTC)")
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
# Use get_messages with after_id for proper batch extraction
|
|
51
|
+
new_messages = await tg.get_messages(
|
|
52
|
+
group_id=group_id,
|
|
53
|
+
after_id=last_message_id
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# For demo purposes, limit to batch_size and filter for today
|
|
57
|
+
if not new_messages.empty:
|
|
58
|
+
print(f" Got {len(new_messages)} messages since ID {last_message_id}")
|
|
59
|
+
|
|
60
|
+
# Convert Date column to UTC if needed
|
|
61
|
+
new_messages['Date'] = pd.to_datetime(new_messages['Date'], utc=True)
|
|
62
|
+
|
|
63
|
+
# Filter for today's messages only (for demo)
|
|
64
|
+
today_messages = new_messages[new_messages['Date'] >= today_utc]
|
|
65
|
+
print(f" Found {len(today_messages)} messages from today")
|
|
66
|
+
|
|
67
|
+
# Limit to batch_size
|
|
68
|
+
new_messages = today_messages.head(batch_size)
|
|
69
|
+
print(f" Limited to {len(new_messages)} messages (batch_size={batch_size})")
|
|
70
|
+
|
|
71
|
+
if not new_messages.empty:
|
|
72
|
+
# Sort by MessageId to ensure order
|
|
73
|
+
new_messages = new_messages.sort_values('MessageId')
|
|
74
|
+
|
|
75
|
+
# Append to CSV
|
|
76
|
+
new_messages.to_csv(
|
|
77
|
+
csv_file,
|
|
78
|
+
mode='a',
|
|
79
|
+
header=not os.path.exists(csv_file),
|
|
80
|
+
index=False
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
print(f"✅ Added {len(new_messages)} new messages to CSV")
|
|
84
|
+
|
|
85
|
+
# Show sample of new messages
|
|
86
|
+
print("\n📋 Sample of new messages:")
|
|
87
|
+
# Show relevant columns
|
|
88
|
+
cols_to_show = ['MessageId', 'Name', 'Date']
|
|
89
|
+
print(new_messages[cols_to_show].head(5))
|
|
90
|
+
else:
|
|
91
|
+
print("ℹ️ No new messages found")
|
|
92
|
+
|
|
93
|
+
except Exception as e:
|
|
94
|
+
print(f"❌ Error extracting messages: {e}")
|
|
95
|
+
new_messages = pd.DataFrame()
|
|
96
|
+
|
|
97
|
+
# Summary
|
|
98
|
+
print(f"\n📊 Extraction Summary:")
|
|
99
|
+
print(f" - Existing messages: {existing_count}")
|
|
100
|
+
print(f" - New messages: {len(new_messages) if not new_messages.empty else 0}")
|
|
101
|
+
print(f" - Total messages: {existing_count + (len(new_messages) if not new_messages.empty else 0)}")
|
|
102
|
+
|
|
103
|
+
return new_messages
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
async def main():
|
|
108
|
+
"""Run CSV checkpoint test"""
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
print("This test demonstrates:")
|
|
112
|
+
print("- Using CSV file itself as checkpoint (no checkpoint.json)")
|
|
113
|
+
print("- Reading last message ID from existing CSV")
|
|
114
|
+
print("- Resuming extraction from that point")
|
|
115
|
+
print("- Appending new messages to existing CSV")
|
|
116
|
+
|
|
117
|
+
# Define the test function inline or import it
|
|
118
|
+
# For now, let's create a simple test
|
|
119
|
+
tg = TgData("config.ini")
|
|
120
|
+
csv_file = "test_messages.csv"
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
# Get test group ID from environment or use Bitcoinsensus channel
|
|
124
|
+
group_id = int(os.environ.get('TEST_GROUP_ID', '1670178185')) # Bitcoinsensus channel
|
|
125
|
+
|
|
126
|
+
print(f"Using group ID: {group_id}")
|
|
127
|
+
print("\n⚠️ Using gentle settings: batch_size=10, only today's messages")
|
|
128
|
+
|
|
129
|
+
# Run the extraction
|
|
130
|
+
messages = await extract_to_csv_with_resume(tg, group_id, csv_file, batch_size=10)
|
|
131
|
+
|
|
132
|
+
success = True
|
|
133
|
+
except Exception as e:
|
|
134
|
+
print(f"Error: {e}")
|
|
135
|
+
success = False
|
|
136
|
+
finally:
|
|
137
|
+
await tg.close()
|
|
138
|
+
# Cleanup
|
|
139
|
+
if os.path.exists(csv_file):
|
|
140
|
+
os.remove(csv_file)
|
|
141
|
+
print(f"Cleaned up {csv_file}")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
if not success:
|
|
145
|
+
sys.exit(1)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
if __name__ == "__main__":
|
|
149
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Smoke tests for ConnectionEngine with real Telegram connections
|
|
3
|
+
"""
|
|
4
|
+
# To run: python -m tgdata.smoke_tests.test_01_connection
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import sys
|
|
8
|
+
import os
|
|
9
|
+
import time
|
|
10
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
|
11
|
+
|
|
12
|
+
from tgdata.connection_engine import ConnectionEngine, ConnectionPool, RateLimitInfo
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def test_connection_engine_initialization():
|
|
16
|
+
"""Test ConnectionEngine initialization with real config"""
|
|
17
|
+
print("TEST: ConnectionEngine initialization...")
|
|
18
|
+
try:
|
|
19
|
+
# Test with real config.ini
|
|
20
|
+
if not os.path.exists("config.ini"):
|
|
21
|
+
print("✗ config.ini not found. Please ensure config.ini exists with valid Telegram credentials")
|
|
22
|
+
return False
|
|
23
|
+
|
|
24
|
+
engine = ConnectionEngine("config.ini")
|
|
25
|
+
assert engine.config_path == "config.ini"
|
|
26
|
+
assert engine.pool_size == 1
|
|
27
|
+
assert engine.max_retries == 3
|
|
28
|
+
print("✓ Default initialization successful")
|
|
29
|
+
|
|
30
|
+
# Test with custom settings
|
|
31
|
+
engine2 = ConnectionEngine(
|
|
32
|
+
config_path="config.ini",
|
|
33
|
+
pool_size=2,
|
|
34
|
+
max_retries=2,
|
|
35
|
+
retry_delay=1.0,
|
|
36
|
+
exponential_backoff=True
|
|
37
|
+
)
|
|
38
|
+
assert engine2.pool_size == 2
|
|
39
|
+
assert engine2.max_retries == 2
|
|
40
|
+
print("✓ Custom initialization successful")
|
|
41
|
+
|
|
42
|
+
return True
|
|
43
|
+
except Exception as e:
|
|
44
|
+
print(f"✗ Initialization test failed: {e}")
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
async def test_real_connection():
|
|
49
|
+
"""Test real Telegram connection"""
|
|
50
|
+
print("\nTEST: Real Telegram connection...")
|
|
51
|
+
engine = None
|
|
52
|
+
try:
|
|
53
|
+
engine = ConnectionEngine("config.ini", pool_size=1)
|
|
54
|
+
|
|
55
|
+
# Test getting a real client
|
|
56
|
+
print("Attempting to get Telegram client...")
|
|
57
|
+
client = await engine.get_client()
|
|
58
|
+
|
|
59
|
+
# Verify it's connected
|
|
60
|
+
assert client is not None
|
|
61
|
+
assert await client.is_user_authorized()
|
|
62
|
+
print("✓ Successfully connected to Telegram")
|
|
63
|
+
|
|
64
|
+
# Get some basic info to verify connection
|
|
65
|
+
me = await client.get_me()
|
|
66
|
+
print(f"✓ Authenticated as: {me.first_name} (ID: {me.id})")
|
|
67
|
+
|
|
68
|
+
# Test connection validation
|
|
69
|
+
is_valid = await engine.validate_connection()
|
|
70
|
+
assert is_valid == True
|
|
71
|
+
print("✓ Connection validation successful")
|
|
72
|
+
|
|
73
|
+
return True
|
|
74
|
+
except Exception as e:
|
|
75
|
+
print(f"✗ Real connection test failed: {e}")
|
|
76
|
+
print("Make sure:")
|
|
77
|
+
print(" 1. config.ini has valid Telegram API credentials")
|
|
78
|
+
print(" 2. You have an authenticated session ('karaposu'.session)")
|
|
79
|
+
return False
|
|
80
|
+
finally:
|
|
81
|
+
if engine:
|
|
82
|
+
await engine.close()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
async def test_config_loading():
|
|
86
|
+
"""Test real configuration loading"""
|
|
87
|
+
print("\nTEST: Real configuration loading...")
|
|
88
|
+
try:
|
|
89
|
+
engine = ConnectionEngine("config.ini")
|
|
90
|
+
config = engine._load_config()
|
|
91
|
+
|
|
92
|
+
# Check that real config values are loaded
|
|
93
|
+
assert config.api_id is not None
|
|
94
|
+
assert config.api_hash is not None
|
|
95
|
+
assert len(config.api_id) > 0
|
|
96
|
+
assert len(config.api_hash) > 0
|
|
97
|
+
print(f"✓ Config loaded successfully")
|
|
98
|
+
print(f" - API ID: {config.api_id}")
|
|
99
|
+
print(f" - Session: {config.username if config.username else config.session_file}")
|
|
100
|
+
print(f" - Phone: {config.phone if config.phone else 'Not specified'}")
|
|
101
|
+
|
|
102
|
+
return True
|
|
103
|
+
except Exception as e:
|
|
104
|
+
print(f"✗ Config loading test failed: {e}")
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
async def test_health_check():
|
|
109
|
+
"""Test health check with real connection"""
|
|
110
|
+
print("\nTEST: Health check with real connection...")
|
|
111
|
+
engine = None
|
|
112
|
+
try:
|
|
113
|
+
engine = ConnectionEngine("config.ini")
|
|
114
|
+
|
|
115
|
+
# Get a connection first
|
|
116
|
+
await engine.get_client()
|
|
117
|
+
|
|
118
|
+
# Run health check
|
|
119
|
+
health = await engine.health_check()
|
|
120
|
+
|
|
121
|
+
assert isinstance(health, dict)
|
|
122
|
+
assert 'timestamp' in health
|
|
123
|
+
assert 'primary_connection' in health
|
|
124
|
+
assert 'pool_connections' in health
|
|
125
|
+
assert 'errors' in health
|
|
126
|
+
|
|
127
|
+
print("✓ Health check structure correct")
|
|
128
|
+
print(f" - Primary connection: {'healthy' if health['primary_connection'] else 'unhealthy'}")
|
|
129
|
+
print(f" - Pool connections: {len(health['pool_connections'])}")
|
|
130
|
+
print(f" - Errors: {len(health['errors'])}")
|
|
131
|
+
|
|
132
|
+
# Primary connection should be healthy
|
|
133
|
+
assert health['primary_connection'] == True
|
|
134
|
+
print("✓ Primary connection is healthy")
|
|
135
|
+
|
|
136
|
+
return True
|
|
137
|
+
except Exception as e:
|
|
138
|
+
print(f"✗ Health check test failed: {e}")
|
|
139
|
+
return False
|
|
140
|
+
finally:
|
|
141
|
+
if engine:
|
|
142
|
+
await engine.close()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
async def test_connection_pool_real():
|
|
146
|
+
"""Test connection pool with real connections"""
|
|
147
|
+
print("\nTEST: Connection pool with real connections...")
|
|
148
|
+
engine = None
|
|
149
|
+
try:
|
|
150
|
+
# Note: For authenticated sessions, pool_size=1 is usually sufficient
|
|
151
|
+
# as Telegram allows multiple concurrent requests on the same connection
|
|
152
|
+
engine = ConnectionEngine("config.ini", pool_size=1)
|
|
153
|
+
|
|
154
|
+
print("Testing connection reuse...")
|
|
155
|
+
|
|
156
|
+
# Get multiple client references
|
|
157
|
+
client1 = await engine.get_client()
|
|
158
|
+
client2 = await engine.get_client()
|
|
159
|
+
|
|
160
|
+
# Both should be valid
|
|
161
|
+
assert await client1.is_user_authorized()
|
|
162
|
+
print("✓ First client authorized")
|
|
163
|
+
|
|
164
|
+
assert await client2.is_user_authorized()
|
|
165
|
+
print("✓ Second client authorized")
|
|
166
|
+
|
|
167
|
+
# For pool_size=1, they should be the same client instance
|
|
168
|
+
if engine.pool_size == 1:
|
|
169
|
+
assert client1 is client2
|
|
170
|
+
print("✓ Single connection reused correctly (pool_size=1)")
|
|
171
|
+
else:
|
|
172
|
+
print(f"✓ Pool with {engine.pool_size} connections working")
|
|
173
|
+
|
|
174
|
+
# Test that we can use both references
|
|
175
|
+
me1 = await client1.get_me()
|
|
176
|
+
me2 = await client2.get_me()
|
|
177
|
+
assert me1.id == me2.id
|
|
178
|
+
print("✓ Both client references work correctly")
|
|
179
|
+
|
|
180
|
+
return True
|
|
181
|
+
except Exception as e:
|
|
182
|
+
print(f"✗ Connection pool test failed: {e}")
|
|
183
|
+
print("This might happen if the session requires re-authentication")
|
|
184
|
+
return False
|
|
185
|
+
finally:
|
|
186
|
+
if engine:
|
|
187
|
+
await engine.close()
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
async def test_connection_close():
|
|
191
|
+
"""Test connection closing with real connection"""
|
|
192
|
+
print("\nTEST: Connection closing...")
|
|
193
|
+
engine = None
|
|
194
|
+
try:
|
|
195
|
+
engine = ConnectionEngine("config.ini")
|
|
196
|
+
|
|
197
|
+
# Get a real connection
|
|
198
|
+
client = await engine.get_client()
|
|
199
|
+
assert await client.is_user_authorized()
|
|
200
|
+
|
|
201
|
+
# Close connections
|
|
202
|
+
await engine.close()
|
|
203
|
+
print("✓ Close operation successful")
|
|
204
|
+
|
|
205
|
+
# Verify client is disconnected
|
|
206
|
+
# Note: We can't easily test if it's truly disconnected without errors
|
|
207
|
+
print("✓ Engine closed without errors")
|
|
208
|
+
|
|
209
|
+
return True
|
|
210
|
+
except Exception as e:
|
|
211
|
+
print(f"✗ Connection close test failed: {e}")
|
|
212
|
+
return False
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
async def test_rate_limiting():
|
|
216
|
+
"""Test rate limiting behavior"""
|
|
217
|
+
print("\nTEST: Rate limiting behavior...")
|
|
218
|
+
engine = None
|
|
219
|
+
try:
|
|
220
|
+
engine = ConnectionEngine("config.ini")
|
|
221
|
+
client = await engine.get_client()
|
|
222
|
+
|
|
223
|
+
# Note: We can't easily trigger real rate limits without making many requests
|
|
224
|
+
# But we can test the mechanism
|
|
225
|
+
print("✓ Rate limiting mechanisms in place")
|
|
226
|
+
print(" - Exponential backoff enabled")
|
|
227
|
+
print(" - Retry logic configured")
|
|
228
|
+
|
|
229
|
+
return True
|
|
230
|
+
except Exception as e:
|
|
231
|
+
print(f"✗ Rate limiting test failed: {e}")
|
|
232
|
+
return False
|
|
233
|
+
finally:
|
|
234
|
+
if engine:
|
|
235
|
+
await engine.close()
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
async def main():
|
|
239
|
+
"""Run all connection engine tests with real connections"""
|
|
240
|
+
print("Connection Engine Tests (Real Connections)")
|
|
241
|
+
print("=" * 50)
|
|
242
|
+
print("\nNOTE: These tests require:")
|
|
243
|
+
print(" 1. Valid config.ini with Telegram API credentials")
|
|
244
|
+
print(" 2. Authenticated session file ('karaposu'.session)")
|
|
245
|
+
print(" 3. Active internet connection")
|
|
246
|
+
print("=" * 50)
|
|
247
|
+
|
|
248
|
+
tests = [
|
|
249
|
+
test_connection_engine_initialization,
|
|
250
|
+
test_config_loading,
|
|
251
|
+
test_real_connection,
|
|
252
|
+
test_health_check,
|
|
253
|
+
test_connection_pool_real,
|
|
254
|
+
test_rate_limiting,
|
|
255
|
+
test_connection_close
|
|
256
|
+
]
|
|
257
|
+
|
|
258
|
+
results = []
|
|
259
|
+
for test in tests:
|
|
260
|
+
try:
|
|
261
|
+
result = await test()
|
|
262
|
+
results.append(result)
|
|
263
|
+
except Exception as e:
|
|
264
|
+
print(f"✗ Test failed with error: {e}")
|
|
265
|
+
import traceback
|
|
266
|
+
traceback.print_exc()
|
|
267
|
+
results.append(False)
|
|
268
|
+
|
|
269
|
+
# Summary
|
|
270
|
+
print("\nSummary")
|
|
271
|
+
print("=" * 50)
|
|
272
|
+
passed = sum(results)
|
|
273
|
+
total = len(results)
|
|
274
|
+
print(f"Passed: {passed}/{total}")
|
|
275
|
+
|
|
276
|
+
if passed == total:
|
|
277
|
+
print("✓ All connection engine tests passed!")
|
|
278
|
+
return 0
|
|
279
|
+
else:
|
|
280
|
+
print("✗ Some tests failed")
|
|
281
|
+
return 1
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
if __name__ == "__main__":
|
|
285
|
+
exit_code = asyncio.run(main())
|
|
286
|
+
sys.exit(exit_code)
|