tinycomp-amadeus 0.1.0__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 TinyComp
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,139 @@
1
+ Metadata-Version: 2.1
2
+ Name: tinycomp-amadeus
3
+ Version: 0.1.0
4
+ Summary: A Python package for compressing images using TinyPNG API
5
+ Home-page: https://github.com/Amadeus9029/tinycomp
6
+ Author: Amadeus9029
7
+ Author-email: 965720890@qq.com
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.6
14
+ Classifier: Programming Language :: Python :: 3.7
15
+ Classifier: Programming Language :: Python :: 3.8
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Requires-Python: >=3.6
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: tinify
21
+ Requires-Dist: requests
22
+ Requires-Dist: tqdm
23
+ Requires-Dist: beautifulsoup4
24
+ Requires-Dist: selenium
25
+ Requires-Dist: fake-useragent
26
+
27
+ # TinyComp
28
+
29
+ TinyComp is a Python package that helps you compress images using the TinyPNG API. It provides both a command-line interface and a Python API for easy integration into your projects.
30
+
31
+ ## Features
32
+
33
+ - Automatic API key management and rotation
34
+ - Automatic API key acquisition when needed
35
+ - Batch image compression
36
+ - Support for multiple image formats (PNG, JPG, JPEG, SVG, GIF)
37
+ - Progress bar for tracking compression status
38
+ - Multi-threaded compression for better performance
39
+ - Automatic handling of API usage limits
40
+ - Automatic API key update through web automation
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ pip install tinycomp
46
+ ```
47
+
48
+ ## Usage
49
+
50
+ ### Command Line Interface
51
+
52
+ #### Compressing Images
53
+
54
+ ```bash
55
+ # Basic compression
56
+ tinycomp compress --source ./images --target ./compressed
57
+
58
+ # With custom API key
59
+ tinycomp compress --source ./images --target ./compressed --api-key YOUR_API_KEY
60
+
61
+ # Set number of threads
62
+ tinycomp compress --source ./images --target ./compressed --threads 4
63
+
64
+ # Enable automatic API key updates when needed
65
+ tinycomp compress --source ./images --target ./compressed --auto-update-key
66
+ ```
67
+
68
+ #### Managing API Keys
69
+
70
+ ```bash
71
+ # Update API key (checks current key first)
72
+ tinycomp update-key
73
+
74
+ # Force update API key even if current one is valid
75
+ tinycomp update-key --force
76
+ ```
77
+
78
+ ### Python API
79
+
80
+ ```python
81
+ from tinycomp import TinyCompressor
82
+
83
+ # Initialize compressor
84
+ compressor = TinyCompressor(api_key="YOUR_API_KEY") # API key is optional
85
+
86
+ # Enable automatic API key updates
87
+ compressor = TinyCompressor(auto_update_key=True)
88
+
89
+ # Compress a single image
90
+ compressor.compress_image("input.png", "output.png")
91
+
92
+ # Compress multiple images
93
+ compressor.compress_directory("./images", "./compressed")
94
+
95
+ # Update API key programmatically
96
+ from tinycomp.api_manager import APIKeyManager
97
+ api_manager = APIKeyManager()
98
+ new_key = api_manager.get_new_api_key()
99
+ ```
100
+
101
+ ## Configuration
102
+
103
+ You can configure TinyComp using environment variables:
104
+
105
+ - `TINYCOMP_API_KEY`: Your TinyPNG API key
106
+ - `TINYCOMP_MAX_THREADS`: Maximum number of compression threads (default: 4)
107
+
108
+ ## Requirements
109
+
110
+ - Python 3.6 or higher
111
+ - Chrome/Chromium browser (for automatic API key updates)
112
+ - ChromeDriver matching your Chrome version
113
+
114
+ ## API Key Management
115
+
116
+ TinyComp includes an automatic API key management system that:
117
+
118
+ 1. Automatically rotates between multiple API keys
119
+ 2. Monitors remaining API usage
120
+ 3. Can automatically obtain new API keys when needed
121
+ 4. Saves API keys for future use
122
+
123
+ The package offers two modes for handling API key depletion:
124
+
125
+ 1. Default mode: Stops compression and notifies when the API key runs out
126
+ 2. Auto-update mode: Automatically obtains new API keys when needed (use `--auto-update-key` flag)
127
+
128
+ The automatic key update feature requires:
129
+ - Chrome/Chromium browser installed
130
+ - ChromeDriver in your system PATH or in the working directory
131
+ - Internet connection for accessing TinyPNG website
132
+
133
+ ## License
134
+
135
+ This project is licensed under the MIT License - see the LICENSE file for details.
136
+
137
+ ## Contributing
138
+
139
+ Contributions are welcome! Please feel free to submit a Pull Request.
@@ -0,0 +1,113 @@
1
+ # TinyComp
2
+
3
+ TinyComp is a Python package that helps you compress images using the TinyPNG API. It provides both a command-line interface and a Python API for easy integration into your projects.
4
+
5
+ ## Features
6
+
7
+ - Automatic API key management and rotation
8
+ - Automatic API key acquisition when needed
9
+ - Batch image compression
10
+ - Support for multiple image formats (PNG, JPG, JPEG, SVG, GIF)
11
+ - Progress bar for tracking compression status
12
+ - Multi-threaded compression for better performance
13
+ - Automatic handling of API usage limits
14
+ - Automatic API key update through web automation
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ pip install tinycomp
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ ### Command Line Interface
25
+
26
+ #### Compressing Images
27
+
28
+ ```bash
29
+ # Basic compression
30
+ tinycomp compress --source ./images --target ./compressed
31
+
32
+ # With custom API key
33
+ tinycomp compress --source ./images --target ./compressed --api-key YOUR_API_KEY
34
+
35
+ # Set number of threads
36
+ tinycomp compress --source ./images --target ./compressed --threads 4
37
+
38
+ # Enable automatic API key updates when needed
39
+ tinycomp compress --source ./images --target ./compressed --auto-update-key
40
+ ```
41
+
42
+ #### Managing API Keys
43
+
44
+ ```bash
45
+ # Update API key (checks current key first)
46
+ tinycomp update-key
47
+
48
+ # Force update API key even if current one is valid
49
+ tinycomp update-key --force
50
+ ```
51
+
52
+ ### Python API
53
+
54
+ ```python
55
+ from tinycomp import TinyCompressor
56
+
57
+ # Initialize compressor
58
+ compressor = TinyCompressor(api_key="YOUR_API_KEY") # API key is optional
59
+
60
+ # Enable automatic API key updates
61
+ compressor = TinyCompressor(auto_update_key=True)
62
+
63
+ # Compress a single image
64
+ compressor.compress_image("input.png", "output.png")
65
+
66
+ # Compress multiple images
67
+ compressor.compress_directory("./images", "./compressed")
68
+
69
+ # Update API key programmatically
70
+ from tinycomp.api_manager import APIKeyManager
71
+ api_manager = APIKeyManager()
72
+ new_key = api_manager.get_new_api_key()
73
+ ```
74
+
75
+ ## Configuration
76
+
77
+ You can configure TinyComp using environment variables:
78
+
79
+ - `TINYCOMP_API_KEY`: Your TinyPNG API key
80
+ - `TINYCOMP_MAX_THREADS`: Maximum number of compression threads (default: 4)
81
+
82
+ ## Requirements
83
+
84
+ - Python 3.6 or higher
85
+ - Chrome/Chromium browser (for automatic API key updates)
86
+ - ChromeDriver matching your Chrome version
87
+
88
+ ## API Key Management
89
+
90
+ TinyComp includes an automatic API key management system that:
91
+
92
+ 1. Automatically rotates between multiple API keys
93
+ 2. Monitors remaining API usage
94
+ 3. Can automatically obtain new API keys when needed
95
+ 4. Saves API keys for future use
96
+
97
+ The package offers two modes for handling API key depletion:
98
+
99
+ 1. Default mode: Stops compression and notifies when the API key runs out
100
+ 2. Auto-update mode: Automatically obtains new API keys when needed (use `--auto-update-key` flag)
101
+
102
+ The automatic key update feature requires:
103
+ - Chrome/Chromium browser installed
104
+ - ChromeDriver in your system PATH or in the working directory
105
+ - Internet connection for accessing TinyPNG website
106
+
107
+ ## License
108
+
109
+ This project is licensed under the MIT License - see the LICENSE file for details.
110
+
111
+ ## Contributing
112
+
113
+ Contributions are welcome! Please feel free to submit a Pull Request.
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,41 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ with open("README.md", "r", encoding="utf-8") as fh:
4
+ long_description = fh.read()
5
+
6
+ setup(
7
+ name="tinycomp-amadeus",
8
+ version="0.1.0",
9
+ author="Amadeus9029",
10
+ author_email="965720890@qq.com",
11
+ description="A Python package for compressing images using TinyPNG API",
12
+ long_description=long_description,
13
+ long_description_content_type="text/markdown",
14
+ url="https://github.com/Amadeus9029/tinycomp",
15
+ packages=find_packages(),
16
+ classifiers=[
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Operating System :: OS Independent",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.6",
23
+ "Programming Language :: Python :: 3.7",
24
+ "Programming Language :: Python :: 3.8",
25
+ "Programming Language :: Python :: 3.9",
26
+ ],
27
+ python_requires=">=3.6",
28
+ install_requires=[
29
+ "tinify",
30
+ "requests",
31
+ "tqdm",
32
+ "beautifulsoup4",
33
+ "selenium",
34
+ "fake-useragent"
35
+ ],
36
+ entry_points={
37
+ 'console_scripts': [
38
+ 'tinycomp=tinycomp.cli:main',
39
+ ],
40
+ },
41
+ )
@@ -0,0 +1,11 @@
1
+ """
2
+ TinyComp - A Python package for compressing images using TinyPNG API
3
+ """
4
+
5
+ from .compressor import TinyCompressor
6
+
7
+ __version__ = "0.1.0"
8
+ __author__ = "Amadeus9029"
9
+ __email__ = "965720890@qq.com"
10
+
11
+ __all__ = ['TinyCompressor']
@@ -0,0 +1,318 @@
1
+ """
2
+ API key management for TinyPNG
3
+ """
4
+
5
+ import os
6
+ import json
7
+ import time
8
+ import random
9
+ import string
10
+ from typing import List, Dict, Optional, Tuple
11
+ import tinify
12
+ from selenium import webdriver
13
+ from selenium.webdriver.chrome.options import Options
14
+ from selenium.webdriver.chrome.service import Service
15
+ from selenium.webdriver.common.by import By
16
+ from selenium.webdriver.support.ui import WebDriverWait
17
+ from selenium.webdriver.support import expected_conditions as EC
18
+ from fake_useragent import UserAgent
19
+
20
+ class APIKeyManager:
21
+ """Manages TinyPNG API keys, including loading, saving, and validation."""
22
+
23
+ def __init__(self, api_key: Optional[str] = None):
24
+ """
25
+ Initialize the API key manager.
26
+
27
+ Args:
28
+ api_key (str, optional): Initial API key to use. If not provided,
29
+ will try to get from environment or saved keys.
30
+ """
31
+ self.api_keys_file = "tinypng_api_keys.json"
32
+ self.current_key = api_key or os.getenv("TINYCOMP_API_KEY")
33
+
34
+ if not self.current_key:
35
+ self.current_key = self._get_valid_api_key()
36
+
37
+ def _load_api_keys(self) -> List[str]:
38
+ """Load saved API keys from file."""
39
+ if os.path.exists(self.api_keys_file):
40
+ try:
41
+ with open(self.api_keys_file, 'r') as f:
42
+ data = json.load(f)
43
+ return data.get("api_keys", [])
44
+ except Exception as e:
45
+ print(f"Error loading API keys file: {str(e)}")
46
+ return []
47
+
48
+ def _save_api_keys(self, api_keys: List[str]) -> None:
49
+ """Save API keys to file."""
50
+ try:
51
+ data = {"api_keys": api_keys}
52
+ with open(self.api_keys_file, 'w') as f:
53
+ json.dump(data, f, indent=2)
54
+ except Exception as e:
55
+ print(f"Error saving API keys to file: {str(e)}")
56
+
57
+ def _get_compression_count(self, api_key: Optional[str] = None) -> Dict[str, any]:
58
+ """
59
+ Get the compression count for an API key.
60
+
61
+ Args:
62
+ api_key (str, optional): API key to check. If None, uses current key.
63
+
64
+ Returns:
65
+ dict: Contains compression count information and status.
66
+ """
67
+ result = {
68
+ 'compression_count': 0,
69
+ 'remaining': 500,
70
+ 'success': False,
71
+ 'error': None
72
+ }
73
+
74
+ # If provided new API key, temporarily set it
75
+ old_key = None
76
+ if api_key:
77
+ old_key = tinify.key
78
+ tinify.key = api_key
79
+
80
+ try:
81
+ # Create a tiny PNG image for validation
82
+ tiny_png = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx\x9cc\x00\x01\x00\x00\x05\x00\x01\r\n-\xb4\x00\x00\x00\x00IEND\xaeB`\x82'
83
+
84
+ # Send request to activate compression_count
85
+ source = tinify.from_buffer(tiny_png)
86
+
87
+ # Get compression count
88
+ compression_count = getattr(tinify, 'compression_count', 0)
89
+ if compression_count is None:
90
+ compression_count = 0
91
+
92
+ # Calculate remaining
93
+ remaining = 500 - compression_count
94
+
95
+ result.update({
96
+ 'compression_count': compression_count,
97
+ 'remaining': remaining,
98
+ 'success': True
99
+ })
100
+
101
+ except tinify.Error as e:
102
+ result['error'] = str(e)
103
+ except Exception as e:
104
+ result['error'] = f"Unknown error: {str(e)}"
105
+
106
+ # Restore original API key
107
+ if old_key:
108
+ tinify.key = old_key
109
+
110
+ return result
111
+
112
+ def _get_valid_api_key(self) -> Optional[str]:
113
+ """Get a valid API key from saved keys or environment."""
114
+ # Load saved API keys
115
+ api_keys = self._load_api_keys()
116
+
117
+ # Check each saved key
118
+ for key in api_keys:
119
+ tinify.key = key
120
+ try:
121
+ result = self._get_compression_count(key)
122
+ if result['success'] and result['remaining'] > 0:
123
+ return key
124
+ except:
125
+ continue
126
+
127
+ return None
128
+
129
+ def _generate_random_name(self) -> str:
130
+ """Generate random name for registration."""
131
+ first_names = ['Zhang', 'Li', 'Wang', 'Liu', 'Chen', 'Yang', 'Huang', 'Zhao', 'Wu', 'Zhou']
132
+ last_names = ['Wei', 'Min', 'Jie', 'Fang', 'Ying', 'Hai', 'Jun', 'Xin', 'Feng', 'Yu']
133
+ return f"{random.choice(first_names)} {random.choice(last_names)}"
134
+
135
+ def _configure_chrome_options(self) -> Options:
136
+ """Configure Chrome options with random fingerprint."""
137
+ chrome_options = Options()
138
+ chrome_options.add_argument('--headless')
139
+ chrome_options.add_argument('--no-sandbox')
140
+ chrome_options.add_argument('--disable-dev-shm-usage')
141
+
142
+ try:
143
+ ua = UserAgent().chrome
144
+ except:
145
+ ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
146
+
147
+ chrome_options.add_argument(f'--user-agent={ua}')
148
+ chrome_options.add_argument('--disable-blink-features=AutomationControlled')
149
+ chrome_options.add_experimental_option('excludeSwitches', ['enable-automation'])
150
+ chrome_options.add_experimental_option('useAutomationExtension', False)
151
+
152
+ return chrome_options
153
+
154
+ def _get_temp_email(self) -> Tuple[Optional[str], Optional[webdriver.Chrome]]:
155
+ """Get temporary email address from temporary email service."""
156
+ print("Getting temporary email...")
157
+
158
+ chrome_options = self._configure_chrome_options()
159
+ driver_path = os.path.join(os.getcwd(), "chromedriver.exe")
160
+ driver = webdriver.Chrome(service=Service(driver_path), options=chrome_options)
161
+
162
+ try:
163
+ driver.get("https://www.nimail.cn/index.html")
164
+
165
+ random_email_btn = WebDriverWait(driver, 10).until(
166
+ EC.element_to_be_clickable((By.XPATH, "//button[contains(text(), '随机邮箱')]"))
167
+ )
168
+ random_email_btn.click()
169
+
170
+ time.sleep(2)
171
+
172
+ email_username_element = WebDriverWait(driver, 10).until(
173
+ EC.presence_of_element_located((By.XPATH, '//*[@id="mailuser"]'))
174
+ )
175
+ email_username = email_username_element.get_attribute("value")
176
+ email = f"{email_username}@nimail.cn"
177
+
178
+ apply_email_btn = WebDriverWait(driver, 10).until(
179
+ EC.element_to_be_clickable((By.XPATH, "//button[contains(text(), '申请邮箱')]"))
180
+ )
181
+ apply_email_btn.click()
182
+
183
+ time.sleep(3)
184
+ print(f"Temporary email activated: {email}")
185
+
186
+ return email, driver
187
+ except Exception as e:
188
+ print(f"Failed to get temporary email: {str(e)}")
189
+ if driver:
190
+ driver.quit()
191
+ return None, None
192
+
193
+ def _request_new_api_key(self, email: str, driver: webdriver.Chrome) -> Optional[str]:
194
+ """Request new TinyPNG API key using temporary email."""
195
+ print(f"Requesting new TinyPNG API key using email: {email}")
196
+
197
+ try:
198
+ original_window = driver.current_window_handle
199
+ driver.execute_script("window.open('https://tinify.com/developers', '_blank');")
200
+ time.sleep(2)
201
+ driver.switch_to.window(driver.window_handles[-1])
202
+
203
+ WebDriverWait(driver, 20).until(
204
+ EC.presence_of_element_located((By.NAME, "name"))
205
+ )
206
+
207
+ name_input = driver.find_element(By.NAME, "name")
208
+ name_input.send_keys(self._generate_random_name())
209
+
210
+ email_input = driver.find_element(By.NAME, "email")
211
+ email_input.send_keys(email)
212
+
213
+ submit_button = driver.find_element(By.CSS_SELECTOR, "button[type='submit']")
214
+ submit_button.click()
215
+
216
+ driver.switch_to.window(original_window)
217
+
218
+ max_attempts = 15
219
+ for attempt in range(max_attempts):
220
+ print(f"Waiting for confirmation email... ({attempt+1}/{max_attempts})")
221
+ time.sleep(10)
222
+
223
+ try:
224
+ tinypng_email = WebDriverWait(driver, 5).until(
225
+ EC.presence_of_element_located((By.XPATH, '//*[@id="inbox"]/tr[2]'))
226
+ )
227
+ tinypng_email.click()
228
+ time.sleep(3)
229
+
230
+ new_window = driver.window_handles[-1]
231
+ driver.switch_to.window(new_window)
232
+
233
+ dashboard_link = WebDriverWait(driver, 5).until(
234
+ EC.element_to_be_clickable((By.XPATH, "//a[contains(text(), 'Visit your dashboard') or contains(@href, 'dashboard')]"))
235
+ )
236
+ dashboard_url = dashboard_link.get_attribute("href")
237
+ driver.execute_script(f"window.open('{dashboard_url}', '_blank');")
238
+ time.sleep(3)
239
+
240
+ driver.switch_to.window(driver.window_handles[-1])
241
+ time.sleep(5)
242
+
243
+ api_key_element = WebDriverWait(driver, 10).until(
244
+ EC.presence_of_element_located((By.XPATH, "/html/body/div[1]/div/main/section/div/div/section/div[2]/div[1]/div/div[3]/strong/p"))
245
+ )
246
+ key_text = api_key_element.text.strip()
247
+
248
+ if key_text and len(key_text) > 20:
249
+ print(f"Successfully obtained new API key")
250
+ return key_text
251
+
252
+ except Exception as e:
253
+ print(f"Attempt {attempt+1} failed: {str(e)}")
254
+ continue
255
+
256
+ print("Timeout waiting for API key")
257
+ return None
258
+
259
+ except Exception as e:
260
+ print(f"Failed to request new API key: {str(e)}")
261
+ return None
262
+ finally:
263
+ if driver:
264
+ driver.quit()
265
+
266
+ def get_new_api_key(self) -> Optional[str]:
267
+ """Get new API key and save it."""
268
+ email, driver = self._get_temp_email()
269
+ if not email or not driver:
270
+ return None
271
+
272
+ try:
273
+ new_key = self._request_new_api_key(email, driver)
274
+ if new_key:
275
+ # Add new key to saved keys
276
+ api_keys = self._load_api_keys()
277
+ if new_key not in api_keys:
278
+ api_keys.append(new_key)
279
+ self._save_api_keys(api_keys)
280
+ return new_key
281
+ return None
282
+ finally:
283
+ if driver:
284
+ driver.quit()
285
+
286
+ def check_and_update_api_key(self) -> bool:
287
+ """
288
+ Check current API key and update if necessary.
289
+
290
+ Returns:
291
+ bool: True if a valid API key is available, False otherwise.
292
+ """
293
+ if not self.current_key:
294
+ self.current_key = self._get_valid_api_key()
295
+ if not self.current_key:
296
+ return False
297
+
298
+ tinify.key = self.current_key
299
+
300
+ # Get API key usage
301
+ result = self._get_compression_count()
302
+
303
+ if result['success']:
304
+ if result['remaining'] <= 50: # If less than 50 compressions remaining
305
+ # Try to get a new key
306
+ new_key = self._get_valid_api_key()
307
+ if new_key:
308
+ self.current_key = new_key
309
+ tinify.key = new_key
310
+ return True
311
+ else:
312
+ # Current key is invalid, try to get a new one
313
+ new_key = self._get_valid_api_key()
314
+ if new_key:
315
+ self.current_key = new_key
316
+ tinify.key = new_key
317
+ return True
318
+ return False
@@ -0,0 +1,149 @@
1
+ """
2
+ Command-line interface for TinyComp
3
+ """
4
+
5
+ import os
6
+ import argparse
7
+ from typing import Optional
8
+ from .compressor import TinyCompressor
9
+ from .api_manager import APIKeyManager
10
+
11
+ def parse_args():
12
+ """Parse command line arguments."""
13
+ parser = argparse.ArgumentParser(
14
+ description="Compress images using TinyPNG API",
15
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter
16
+ )
17
+
18
+ subparsers = parser.add_subparsers(dest='command', help='Commands')
19
+
20
+ # Compress command
21
+ compress_parser = subparsers.add_parser('compress', help='Compress images')
22
+ compress_parser.add_argument(
23
+ "--source",
24
+ "-s",
25
+ required=True,
26
+ help="Source directory or file path"
27
+ )
28
+
29
+ compress_parser.add_argument(
30
+ "--target",
31
+ "-t",
32
+ required=True,
33
+ help="Target directory or file path"
34
+ )
35
+
36
+ compress_parser.add_argument(
37
+ "--api-key",
38
+ "-k",
39
+ help="TinyPNG API key (optional)"
40
+ )
41
+
42
+ compress_parser.add_argument(
43
+ "--threads",
44
+ "-n",
45
+ type=int,
46
+ default=4,
47
+ help="Number of compression threads"
48
+ )
49
+
50
+ compress_parser.add_argument(
51
+ "--skip-existing",
52
+ "-x",
53
+ action="store_true",
54
+ default=True,
55
+ help="Skip existing files in target directory"
56
+ )
57
+
58
+ compress_parser.add_argument(
59
+ "--auto-update-key",
60
+ "-a",
61
+ action="store_true",
62
+ help="Automatically get new API keys when current one runs out"
63
+ )
64
+
65
+ # Update API key command
66
+ update_parser = subparsers.add_parser('update-key', help='Update TinyPNG API key')
67
+ update_parser.add_argument(
68
+ "--force",
69
+ "-f",
70
+ action="store_true",
71
+ help="Force update even if current key is still valid"
72
+ )
73
+
74
+ return parser.parse_args()
75
+
76
+ def compress_images(args):
77
+ """Handle image compression command."""
78
+ # Initialize compressor
79
+ compressor = TinyCompressor(
80
+ api_key=args.api_key,
81
+ max_workers=args.threads,
82
+ auto_update_key=args.auto_update_key
83
+ )
84
+
85
+ # Check if source is a directory or file
86
+ if os.path.isdir(args.source):
87
+ # Compress directory
88
+ print(f"Compressing directory: {args.source}")
89
+ stats = compressor.compress_directory(
90
+ args.source,
91
+ args.target,
92
+ skip_existing=args.skip_existing
93
+ )
94
+
95
+ # Print results
96
+ print("\nCompression completed!")
97
+ print(f"Total files: {stats['total']}")
98
+ print(f"Successfully compressed: {stats['success']}")
99
+ print(f"Failed: {stats['failed']}")
100
+ print(f"Success rate: {stats['percent']:.1f}%")
101
+ if stats.get('keys_used'):
102
+ print(f"API keys used: {stats['keys_used']}")
103
+
104
+ else:
105
+ # Compress single file
106
+ print(f"Compressing file: {args.source}")
107
+ result = compressor.compress_image(args.source, args.target)
108
+
109
+ if result['status'] == 'success':
110
+ print("File compressed successfully!")
111
+ else:
112
+ print(f"Compression failed: {result['message']}")
113
+
114
+ def update_api_key(args):
115
+ """Handle API key update command."""
116
+ api_manager = APIKeyManager()
117
+
118
+ if not args.force:
119
+ # Check if current key is still valid
120
+ if api_manager.current_key:
121
+ result = api_manager._get_compression_count()
122
+ if result['success'] and result['remaining'] > 50:
123
+ print(f"Current API key is still valid (remaining: {result['remaining']} compressions)")
124
+ print("Use --force to update anyway")
125
+ return
126
+
127
+ print("Requesting new API key...")
128
+ new_key = api_manager.get_new_api_key()
129
+
130
+ if new_key:
131
+ print("Successfully obtained and saved new API key")
132
+ print(f"Remaining compressions: {api_manager._get_compression_count(new_key)['remaining']}")
133
+ else:
134
+ print("Failed to obtain new API key")
135
+
136
+ def main():
137
+ """Main entry point for the CLI."""
138
+ args = parse_args()
139
+
140
+ if args.command == 'compress':
141
+ compress_images(args)
142
+ elif args.command == 'update-key':
143
+ update_api_key(args)
144
+ else:
145
+ print("Please specify a command: compress or update-key")
146
+ print("Use -h or --help for more information")
147
+
148
+ if __name__ == "__main__":
149
+ main()
@@ -0,0 +1,195 @@
1
+ """
2
+ TinyCompressor class for handling image compression using TinyPNG API
3
+ """
4
+
5
+ import os
6
+ import json
7
+ import time
8
+ from concurrent.futures import ThreadPoolExecutor, as_completed
9
+ from typing import List, Dict, Optional, Union
10
+ from tqdm import tqdm
11
+ import tinify
12
+ from .api_manager import APIKeyManager
13
+
14
+ class TinyCompressor:
15
+ """
16
+ A class for compressing images using the TinyPNG API.
17
+ """
18
+
19
+ SUPPORTED_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.svg', '.gif']
20
+
21
+ def __init__(self, api_key: Optional[str] = None, max_workers: int = 4, auto_update_key: bool = False):
22
+ """
23
+ Initialize the TinyCompressor.
24
+
25
+ Args:
26
+ api_key (str, optional): TinyPNG API key. If not provided, will try to get from environment
27
+ or manage automatically.
28
+ max_workers (int): Maximum number of concurrent compression threads.
29
+ auto_update_key (bool): Whether to automatically get new API keys when current one runs out.
30
+ """
31
+ self.max_workers = max_workers
32
+ self.auto_update_key = auto_update_key
33
+ self.api_manager = APIKeyManager(api_key)
34
+ self.keys_used = set() # Track used API keys
35
+
36
+ def compress_image(self, source_path: str, target_path: str) -> Dict[str, str]:
37
+ """
38
+ Compress a single image.
39
+
40
+ Args:
41
+ source_path (str): Path to the source image.
42
+ target_path (str): Path where the compressed image will be saved.
43
+
44
+ Returns:
45
+ dict: Compression result containing status and message.
46
+ """
47
+ # Ensure the API key is valid
48
+ if not self.api_manager.check_and_update_api_key():
49
+ if self.auto_update_key:
50
+ print("Current API key is invalid or depleted, requesting new key...")
51
+ new_key = self.api_manager.get_new_api_key()
52
+ if not new_key:
53
+ return {
54
+ 'status': 'failed',
55
+ 'message': 'Failed to obtain new API key'
56
+ }
57
+ self.api_manager.current_key = new_key
58
+ tinify.key = new_key
59
+ else:
60
+ return {
61
+ 'status': 'failed',
62
+ 'message': 'No valid API key available. Use --auto-update-key to automatically get new keys.'
63
+ }
64
+
65
+ # Add current key to used keys set
66
+ if self.api_manager.current_key:
67
+ self.keys_used.add(self.api_manager.current_key)
68
+
69
+ # Create target directory if it doesn't exist
70
+ os.makedirs(os.path.dirname(target_path), exist_ok=True)
71
+
72
+ try:
73
+ # Compress the image
74
+ source = tinify.from_file(source_path)
75
+ source.to_file(target_path)
76
+
77
+ return {
78
+ 'status': 'success',
79
+ 'message': 'Image compressed successfully'
80
+ }
81
+ except tinify.AccountError as e:
82
+ if self.auto_update_key:
83
+ print("API key limit reached, requesting new key...")
84
+ new_key = self.api_manager.get_new_api_key()
85
+ if new_key:
86
+ self.api_manager.current_key = new_key
87
+ tinify.key = new_key
88
+ # Retry compression with new key
89
+ return self.compress_image(source_path, target_path)
90
+ return {
91
+ 'status': 'failed',
92
+ 'message': f'API key error: {str(e)}. Use --auto-update-key to automatically get new keys.'
93
+ }
94
+ except tinify.Error as e:
95
+ return {
96
+ 'status': 'failed',
97
+ 'message': f'Compression failed: {str(e)}'
98
+ }
99
+ except Exception as e:
100
+ return {
101
+ 'status': 'failed',
102
+ 'message': f'Processing error: {str(e)}'
103
+ }
104
+
105
+ def compress_directory(self, source_dir: str, target_dir: str,
106
+ skip_existing: bool = True) -> Dict[str, Union[int, float]]:
107
+ """
108
+ Compress all supported images in a directory.
109
+
110
+ Args:
111
+ source_dir (str): Source directory containing images to compress.
112
+ target_dir (str): Target directory for compressed images.
113
+ skip_existing (bool): Whether to skip files that already exist in target directory.
114
+
115
+ Returns:
116
+ dict: Compression statistics.
117
+ """
118
+ # Get list of files to process
119
+ image_files = self._get_image_files(source_dir)
120
+
121
+ if skip_existing:
122
+ image_files = [f for f in image_files if self._should_compress(f, source_dir, target_dir)]
123
+
124
+ total_files = len(image_files)
125
+ if total_files == 0:
126
+ return {
127
+ 'total': 0,
128
+ 'processed': 0,
129
+ 'success': 0,
130
+ 'failed': 0,
131
+ 'percent': 100.0,
132
+ 'keys_used': 0
133
+ }
134
+
135
+ stats = {
136
+ 'total': total_files,
137
+ 'processed': 0,
138
+ 'success': 0,
139
+ 'failed': 0
140
+ }
141
+
142
+ # Process files with progress bar
143
+ with tqdm(total=total_files, unit="file", desc="Compressing images") as progress_bar:
144
+ with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
145
+ future_to_file = {
146
+ executor.submit(
147
+ self._process_single_file,
148
+ file_path,
149
+ source_dir,
150
+ target_dir
151
+ ): file_path for file_path in image_files
152
+ }
153
+
154
+ for future in as_completed(future_to_file):
155
+ result = future.result()
156
+ stats['processed'] += 1
157
+
158
+ if result['status'] == 'success':
159
+ stats['success'] += 1
160
+ else:
161
+ stats['failed'] += 1
162
+ print(f"\nFailed to compress {future_to_file[future]}: {result['message']}")
163
+
164
+ progress_bar.update(1)
165
+
166
+ stats['percent'] = (stats['success'] / stats['total']) * 100
167
+ stats['keys_used'] = len(self.keys_used)
168
+ return stats
169
+
170
+ def _get_image_files(self, directory: str) -> List[str]:
171
+ """Get all supported image files in the directory."""
172
+ image_files = []
173
+
174
+ for root, _, files in os.walk(directory):
175
+ for name in files:
176
+ file_path = os.path.join(root, name)
177
+ _, file_ext = os.path.splitext(name)
178
+
179
+ if file_ext.lower() in self.SUPPORTED_EXTENSIONS:
180
+ image_files.append(file_path)
181
+
182
+ return image_files
183
+
184
+ def _should_compress(self, file_path: str, source_dir: str, target_dir: str) -> bool:
185
+ """Check if the file should be compressed (skip if target exists)."""
186
+ relative_path = os.path.relpath(file_path, source_dir)
187
+ target_path = os.path.join(target_dir, relative_path)
188
+ return not os.path.exists(target_path)
189
+
190
+ def _process_single_file(self, file_path: str, source_dir: str, target_dir: str) -> Dict[str, str]:
191
+ """Process a single file for compression."""
192
+ relative_path = os.path.relpath(file_path, source_dir)
193
+ target_path = os.path.join(target_dir, relative_path)
194
+
195
+ return self.compress_image(file_path, target_path)
@@ -0,0 +1,3 @@
1
+ """
2
+ Test package for TinyComp
3
+ """
@@ -0,0 +1,101 @@
1
+ """
2
+ Tests for the TinyCompressor class
3
+ """
4
+
5
+ import os
6
+ import unittest
7
+ from unittest.mock import patch, MagicMock
8
+ from tinycomp import TinyCompressor
9
+
10
+ class TestTinyCompressor(unittest.TestCase):
11
+ """Test cases for TinyCompressor class."""
12
+
13
+ def setUp(self):
14
+ """Set up test fixtures."""
15
+ self.compressor = TinyCompressor(api_key="YYKcY8Z99BDJnjvZhgRWJQqqvNFKhlcL")
16
+ self.test_image = "test.png"
17
+ self.test_output = "output.png"
18
+
19
+ @patch('tinycomp.compressor.tinify.from_file')
20
+ def test_compress_image_success(self, mock_from_file):
21
+ """Test successful image compression."""
22
+ # Mock the tinify.from_file() call
23
+ mock_source = MagicMock()
24
+ mock_from_file.return_value = mock_source
25
+
26
+ # Mock successful compression
27
+ result = self.compressor.compress_image(self.test_image, self.test_output)
28
+
29
+ # 验证返回值
30
+ self.assertEqual(result['status'], 'success')
31
+ self.assertEqual(result['message'], 'Image compressed successfully')
32
+
33
+ # 验证调用
34
+ mock_from_file.assert_called_once_with(self.test_image)
35
+ mock_source.to_file.assert_called_once_with(self.test_output)
36
+
37
+ def test_get_image_files(self):
38
+ """Test getting supported image files from directory."""
39
+ # Create temporary test directory with some files
40
+ test_dir = "test_dir"
41
+ os.makedirs(test_dir, exist_ok=True)
42
+
43
+ # Create test files
44
+ test_files = [
45
+ "test1.png",
46
+ "test2.jpg",
47
+ "test3.txt", # Unsupported extension
48
+ "test4.jpeg"
49
+ ]
50
+
51
+ for file in test_files:
52
+ with open(os.path.join(test_dir, file), 'w') as f:
53
+ f.write("test")
54
+
55
+ # Get image files
56
+ image_files = self.compressor._get_image_files(test_dir)
57
+
58
+ # Verify results
59
+ self.assertEqual(len(image_files), 3) # Should find 3 supported images
60
+
61
+ # Clean up
62
+ for file in test_files:
63
+ os.remove(os.path.join(test_dir, file))
64
+ os.rmdir(test_dir)
65
+
66
+ def test_should_compress(self):
67
+ """Test should_compress method."""
68
+ # Create test directories
69
+ source_dir = "test_source"
70
+ target_dir = "test_target"
71
+ os.makedirs(source_dir, exist_ok=True)
72
+ os.makedirs(target_dir, exist_ok=True)
73
+
74
+ # Create test file
75
+ test_file = os.path.join(source_dir, "test.png")
76
+ with open(test_file, 'w') as f:
77
+ f.write("test")
78
+
79
+ # Test when target doesn't exist
80
+ self.assertTrue(
81
+ self.compressor._should_compress(test_file, source_dir, target_dir)
82
+ )
83
+
84
+ # Create target file
85
+ target_file = os.path.join(target_dir, "test.png")
86
+ with open(target_file, 'w') as f:
87
+ f.write("test")
88
+
89
+ # Test when target exists
90
+ self.assertFalse(
91
+ self.compressor._should_compress(test_file, source_dir, target_dir)
92
+ )
93
+
94
+ # Clean up
95
+ os.remove(test_file)
96
+ os.remove(target_file)
97
+ os.rmdir(source_dir)
98
+ os.rmdir(target_dir)
99
+
100
+ if __name__ == '__main__':
101
+ unittest.main()
@@ -0,0 +1,139 @@
1
+ Metadata-Version: 2.1
2
+ Name: tinycomp-amadeus
3
+ Version: 0.1.0
4
+ Summary: A Python package for compressing images using TinyPNG API
5
+ Home-page: https://github.com/Amadeus9029/tinycomp
6
+ Author: Amadeus9029
7
+ Author-email: 965720890@qq.com
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.6
14
+ Classifier: Programming Language :: Python :: 3.7
15
+ Classifier: Programming Language :: Python :: 3.8
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Requires-Python: >=3.6
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: tinify
21
+ Requires-Dist: requests
22
+ Requires-Dist: tqdm
23
+ Requires-Dist: beautifulsoup4
24
+ Requires-Dist: selenium
25
+ Requires-Dist: fake-useragent
26
+
27
+ # TinyComp
28
+
29
+ TinyComp is a Python package that helps you compress images using the TinyPNG API. It provides both a command-line interface and a Python API for easy integration into your projects.
30
+
31
+ ## Features
32
+
33
+ - Automatic API key management and rotation
34
+ - Automatic API key acquisition when needed
35
+ - Batch image compression
36
+ - Support for multiple image formats (PNG, JPG, JPEG, SVG, GIF)
37
+ - Progress bar for tracking compression status
38
+ - Multi-threaded compression for better performance
39
+ - Automatic handling of API usage limits
40
+ - Automatic API key update through web automation
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ pip install tinycomp
46
+ ```
47
+
48
+ ## Usage
49
+
50
+ ### Command Line Interface
51
+
52
+ #### Compressing Images
53
+
54
+ ```bash
55
+ # Basic compression
56
+ tinycomp compress --source ./images --target ./compressed
57
+
58
+ # With custom API key
59
+ tinycomp compress --source ./images --target ./compressed --api-key YOUR_API_KEY
60
+
61
+ # Set number of threads
62
+ tinycomp compress --source ./images --target ./compressed --threads 4
63
+
64
+ # Enable automatic API key updates when needed
65
+ tinycomp compress --source ./images --target ./compressed --auto-update-key
66
+ ```
67
+
68
+ #### Managing API Keys
69
+
70
+ ```bash
71
+ # Update API key (checks current key first)
72
+ tinycomp update-key
73
+
74
+ # Force update API key even if current one is valid
75
+ tinycomp update-key --force
76
+ ```
77
+
78
+ ### Python API
79
+
80
+ ```python
81
+ from tinycomp import TinyCompressor
82
+
83
+ # Initialize compressor
84
+ compressor = TinyCompressor(api_key="YOUR_API_KEY") # API key is optional
85
+
86
+ # Enable automatic API key updates
87
+ compressor = TinyCompressor(auto_update_key=True)
88
+
89
+ # Compress a single image
90
+ compressor.compress_image("input.png", "output.png")
91
+
92
+ # Compress multiple images
93
+ compressor.compress_directory("./images", "./compressed")
94
+
95
+ # Update API key programmatically
96
+ from tinycomp.api_manager import APIKeyManager
97
+ api_manager = APIKeyManager()
98
+ new_key = api_manager.get_new_api_key()
99
+ ```
100
+
101
+ ## Configuration
102
+
103
+ You can configure TinyComp using environment variables:
104
+
105
+ - `TINYCOMP_API_KEY`: Your TinyPNG API key
106
+ - `TINYCOMP_MAX_THREADS`: Maximum number of compression threads (default: 4)
107
+
108
+ ## Requirements
109
+
110
+ - Python 3.6 or higher
111
+ - Chrome/Chromium browser (for automatic API key updates)
112
+ - ChromeDriver matching your Chrome version
113
+
114
+ ## API Key Management
115
+
116
+ TinyComp includes an automatic API key management system that:
117
+
118
+ 1. Automatically rotates between multiple API keys
119
+ 2. Monitors remaining API usage
120
+ 3. Can automatically obtain new API keys when needed
121
+ 4. Saves API keys for future use
122
+
123
+ The package offers two modes for handling API key depletion:
124
+
125
+ 1. Default mode: Stops compression and notifies when the API key runs out
126
+ 2. Auto-update mode: Automatically obtains new API keys when needed (use `--auto-update-key` flag)
127
+
128
+ The automatic key update feature requires:
129
+ - Chrome/Chromium browser installed
130
+ - ChromeDriver in your system PATH or in the working directory
131
+ - Internet connection for accessing TinyPNG website
132
+
133
+ ## License
134
+
135
+ This project is licensed under the MIT License - see the LICENSE file for details.
136
+
137
+ ## Contributing
138
+
139
+ Contributions are welcome! Please feel free to submit a Pull Request.
@@ -0,0 +1,15 @@
1
+ LICENSE
2
+ README.md
3
+ setup.py
4
+ tinycomp/__init__.py
5
+ tinycomp/api_manager.py
6
+ tinycomp/cli.py
7
+ tinycomp/compressor.py
8
+ tinycomp/tests/__init__.py
9
+ tinycomp/tests/test_compressor.py
10
+ tinycomp_amadeus.egg-info/PKG-INFO
11
+ tinycomp_amadeus.egg-info/SOURCES.txt
12
+ tinycomp_amadeus.egg-info/dependency_links.txt
13
+ tinycomp_amadeus.egg-info/entry_points.txt
14
+ tinycomp_amadeus.egg-info/requires.txt
15
+ tinycomp_amadeus.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ tinycomp = tinycomp.cli:main
@@ -0,0 +1,6 @@
1
+ tinify
2
+ requests
3
+ tqdm
4
+ beautifulsoup4
5
+ selenium
6
+ fake-useragent