mobileproxy-sdk 1.0.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.
- mobileproxy_sdk-1.0.0/LICENSE +21 -0
- mobileproxy_sdk-1.0.0/PKG-INFO +197 -0
- mobileproxy_sdk-1.0.0/README.md +161 -0
- mobileproxy_sdk-1.0.0/mobileproxy/__init__.py +26 -0
- mobileproxy_sdk-1.0.0/mobileproxy/client.py +432 -0
- mobileproxy_sdk-1.0.0/mobileproxy/exceptions.py +23 -0
- mobileproxy_sdk-1.0.0/mobileproxy/http_client.py +118 -0
- mobileproxy_sdk-1.0.0/mobileproxy/py.typed +0 -0
- mobileproxy_sdk-1.0.0/mobileproxy_sdk.egg-info/PKG-INFO +197 -0
- mobileproxy_sdk-1.0.0/mobileproxy_sdk.egg-info/SOURCES.txt +14 -0
- mobileproxy_sdk-1.0.0/mobileproxy_sdk.egg-info/dependency_links.txt +1 -0
- mobileproxy_sdk-1.0.0/mobileproxy_sdk.egg-info/requires.txt +8 -0
- mobileproxy_sdk-1.0.0/mobileproxy_sdk.egg-info/top_level.txt +1 -0
- mobileproxy_sdk-1.0.0/pyproject.toml +47 -0
- mobileproxy_sdk-1.0.0/setup.cfg +4 -0
- mobileproxy_sdk-1.0.0/tests/test_client.py +86 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MOBIROX LLP / MobileProxy.Space
|
|
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,197 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mobileproxy-sdk
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official Python SDK for MobileProxy.Space API — private mobile proxies on real GSM devices
|
|
5
|
+
Author-email: "MobileProxy.Space" <support@mobirox.co.uk>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://mobileproxy.space
|
|
8
|
+
Project-URL: Documentation, https://mobileproxy.space/en/user.html?api
|
|
9
|
+
Project-URL: Repository, https://github.com/mobileproxy/python-sdk
|
|
10
|
+
Project-URL: Issues, https://github.com/mobileproxy/python-sdk/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/mobileproxy/python-sdk/releases
|
|
12
|
+
Keywords: proxy,mobile-proxy,mobileproxy,socks5,http-proxy,scraping,api-client,ip-rotation
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Internet :: Proxy Servers
|
|
25
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
26
|
+
Requires-Python: >=3.8
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Requires-Dist: requests>=2.20.0
|
|
30
|
+
Provides-Extra: async
|
|
31
|
+
Requires-Dist: httpx>=0.24.0; extra == "async"
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
34
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# MobileProxy.Space Python SDK
|
|
38
|
+
|
|
39
|
+
[](https://github.com/mobileproxy/python-sdk/actions/workflows/ci.yml)
|
|
40
|
+
[](https://pypi.org/project/mobileproxy-sdk/)
|
|
41
|
+
[](https://pypi.org/project/mobileproxy-sdk/)
|
|
42
|
+
|
|
43
|
+
Official Python SDK for the [MobileProxy.Space](https://mobileproxy.space) API — private mobile proxies on real GSM devices across 52 countries.
|
|
44
|
+
|
|
45
|
+
## Features
|
|
46
|
+
|
|
47
|
+
- **Full API coverage** — all endpoints wrapped in typed, documented methods
|
|
48
|
+
- **Context manager** support (`with Client(...) as client:`)
|
|
49
|
+
- **Typed exceptions** — `ApiError`, `AuthenticationError`, `RateLimitError`
|
|
50
|
+
- **IP rotation** — dedicated `change_ip()` with no rate limit
|
|
51
|
+
- **Python 3.8+** compatible
|
|
52
|
+
- **Type hints** throughout + `py.typed` marker (PEP 561)
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install mobileproxy-sdk
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Quick Start
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from mobileproxy import Client
|
|
64
|
+
|
|
65
|
+
client = Client("YOUR_API_TOKEN")
|
|
66
|
+
|
|
67
|
+
# Check balance
|
|
68
|
+
balance = client.get_balance()
|
|
69
|
+
print(balance)
|
|
70
|
+
|
|
71
|
+
# List active proxies
|
|
72
|
+
proxies = client.get_my_proxy()
|
|
73
|
+
|
|
74
|
+
# Get current IP
|
|
75
|
+
ip = client.get_proxy_ip(12345)
|
|
76
|
+
|
|
77
|
+
# Rotate IP (no rate limit)
|
|
78
|
+
client.change_ip("your_proxy_key")
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Context Manager
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
with Client("YOUR_API_TOKEN") as client:
|
|
85
|
+
balance = client.get_balance()
|
|
86
|
+
print(balance)
|
|
87
|
+
# session is closed automatically
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## API Methods
|
|
91
|
+
|
|
92
|
+
### Proxy Information
|
|
93
|
+
|
|
94
|
+
| Method | Description |
|
|
95
|
+
|--------|-------------|
|
|
96
|
+
| `get_proxy_ip(proxy_id, *, check_spam=)` | Get current IP address of a proxy |
|
|
97
|
+
| `get_my_proxy(proxy_id=)` | List active proxies (all or specific) |
|
|
98
|
+
| `get_ip_stats()` | IP address statistics by GEO |
|
|
99
|
+
|
|
100
|
+
### Proxy Management
|
|
101
|
+
|
|
102
|
+
| Method | Description |
|
|
103
|
+
|--------|-------------|
|
|
104
|
+
| `change_proxy_credentials(proxy_id, login, password)` | Change proxy login/password |
|
|
105
|
+
| `reboot_proxy(proxy_id)` | Restart the modem |
|
|
106
|
+
| `edit_proxy(proxy_id, *, reboot_time=, ip_auth=, comment=)` | Update proxy settings |
|
|
107
|
+
| `change_ip(proxy_key, *, format=, user_agent=)` | Rotate IP (no rate limit) |
|
|
108
|
+
|
|
109
|
+
### Equipment & GEO
|
|
110
|
+
|
|
111
|
+
| Method | Description |
|
|
112
|
+
|--------|-------------|
|
|
113
|
+
| `change_equipment(proxy_id, **kwargs)` | Switch modem/SIM/operator/city |
|
|
114
|
+
| `get_available_equipment(proxy_id, **kwargs)` | List available equipment by GEO |
|
|
115
|
+
| `get_geo_list(proxy_id, geo_id)` | Available GEOs for a proxy |
|
|
116
|
+
| `get_operators(geo_id)` | Operators for a GEO |
|
|
117
|
+
| `get_countries(only_available=)` | List of countries |
|
|
118
|
+
| `get_cities()` | List of cities |
|
|
119
|
+
|
|
120
|
+
### Blacklist
|
|
121
|
+
|
|
122
|
+
| Method | Description |
|
|
123
|
+
|--------|-------------|
|
|
124
|
+
| `get_black_list(proxy_id)` | Get equipment/operator blacklist |
|
|
125
|
+
| `add_operator_to_black_list(proxy_id, operator_id)` | Block an operator |
|
|
126
|
+
| `remove_operator_from_black_list(proxy_id, operator_id)` | Unblock an operator |
|
|
127
|
+
| `remove_from_black_list(proxy_id, black_list_id, eid)` | Remove equipment from blacklist |
|
|
128
|
+
|
|
129
|
+
### Purchasing & Billing
|
|
130
|
+
|
|
131
|
+
| Method | Description |
|
|
132
|
+
|--------|-------------|
|
|
133
|
+
| `buy_proxy(**kwargs)` | Purchase a proxy |
|
|
134
|
+
| `refund_proxy(proxy_id)` | Request a refund |
|
|
135
|
+
| `get_balance()` | Account balance |
|
|
136
|
+
| `get_prices(country_id)` | Prices for a country |
|
|
137
|
+
| `get_test_proxy(geo_id, operator)` | Get a free 2-hour trial proxy |
|
|
138
|
+
|
|
139
|
+
### Utilities
|
|
140
|
+
|
|
141
|
+
| Method | Description |
|
|
142
|
+
|--------|-------------|
|
|
143
|
+
| `check_equipment_availability(eid)` | Check if equipment is available |
|
|
144
|
+
| `view_url_from_different_ips(url, country_id)` | Anti-cloaking: view URL from another country |
|
|
145
|
+
| `get_task_result(task_id)` | Get async task result |
|
|
146
|
+
|
|
147
|
+
## Error Handling
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from mobileproxy import Client, ApiError, AuthenticationError, RateLimitError
|
|
151
|
+
|
|
152
|
+
client = Client("YOUR_API_TOKEN")
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
client.get_balance()
|
|
156
|
+
except AuthenticationError as e:
|
|
157
|
+
# Invalid API token
|
|
158
|
+
print(e, e.http_code)
|
|
159
|
+
except RateLimitError as e:
|
|
160
|
+
# Too many requests — back off and retry
|
|
161
|
+
# Limit: 3 × (number of active proxies) requests/sec
|
|
162
|
+
print(e)
|
|
163
|
+
except ApiError as e:
|
|
164
|
+
print(e.http_code, e.response_body)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Configuration
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
client = Client(
|
|
171
|
+
"YOUR_API_TOKEN",
|
|
172
|
+
timeout=120, # request timeout in seconds
|
|
173
|
+
base_url="https://mobileproxy.space/api.html", # default
|
|
174
|
+
)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Rate Limits
|
|
178
|
+
|
|
179
|
+
API requests are limited to **3 × (number of active proxies)** per second. For example, 10 proxies = 30 req/s. The `change_ip()` method uses a separate endpoint with **no rate limit**.
|
|
180
|
+
|
|
181
|
+
## Requirements
|
|
182
|
+
|
|
183
|
+
- Python 3.8 or higher
|
|
184
|
+
- `requests` library (installed automatically)
|
|
185
|
+
|
|
186
|
+
## Links
|
|
187
|
+
|
|
188
|
+
- [API Documentation](https://mobileproxy.space/en/user.html?api)
|
|
189
|
+
- [Dashboard](https://mobileproxy.space/en/user.html)
|
|
190
|
+
- [Website](https://mobileproxy.space)
|
|
191
|
+
- [PHP SDK](https://github.com/mobileproxy/php-sdk)
|
|
192
|
+
- [Node.js SDK](https://github.com/mobileproxy/node-sdk)
|
|
193
|
+
- [Chrome Extension](https://chromewebstore.google.com/detail/mobile-proxy-manager/lhbdhjhflkejgkkhlgacbaogbaaollac)
|
|
194
|
+
|
|
195
|
+
## License
|
|
196
|
+
|
|
197
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# MobileProxy.Space Python SDK
|
|
2
|
+
|
|
3
|
+
[](https://github.com/mobileproxy/python-sdk/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/mobileproxy-sdk/)
|
|
5
|
+
[](https://pypi.org/project/mobileproxy-sdk/)
|
|
6
|
+
|
|
7
|
+
Official Python SDK for the [MobileProxy.Space](https://mobileproxy.space) API — private mobile proxies on real GSM devices across 52 countries.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Full API coverage** — all endpoints wrapped in typed, documented methods
|
|
12
|
+
- **Context manager** support (`with Client(...) as client:`)
|
|
13
|
+
- **Typed exceptions** — `ApiError`, `AuthenticationError`, `RateLimitError`
|
|
14
|
+
- **IP rotation** — dedicated `change_ip()` with no rate limit
|
|
15
|
+
- **Python 3.8+** compatible
|
|
16
|
+
- **Type hints** throughout + `py.typed` marker (PEP 561)
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install mobileproxy-sdk
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from mobileproxy import Client
|
|
28
|
+
|
|
29
|
+
client = Client("YOUR_API_TOKEN")
|
|
30
|
+
|
|
31
|
+
# Check balance
|
|
32
|
+
balance = client.get_balance()
|
|
33
|
+
print(balance)
|
|
34
|
+
|
|
35
|
+
# List active proxies
|
|
36
|
+
proxies = client.get_my_proxy()
|
|
37
|
+
|
|
38
|
+
# Get current IP
|
|
39
|
+
ip = client.get_proxy_ip(12345)
|
|
40
|
+
|
|
41
|
+
# Rotate IP (no rate limit)
|
|
42
|
+
client.change_ip("your_proxy_key")
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Context Manager
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
with Client("YOUR_API_TOKEN") as client:
|
|
49
|
+
balance = client.get_balance()
|
|
50
|
+
print(balance)
|
|
51
|
+
# session is closed automatically
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## API Methods
|
|
55
|
+
|
|
56
|
+
### Proxy Information
|
|
57
|
+
|
|
58
|
+
| Method | Description |
|
|
59
|
+
|--------|-------------|
|
|
60
|
+
| `get_proxy_ip(proxy_id, *, check_spam=)` | Get current IP address of a proxy |
|
|
61
|
+
| `get_my_proxy(proxy_id=)` | List active proxies (all or specific) |
|
|
62
|
+
| `get_ip_stats()` | IP address statistics by GEO |
|
|
63
|
+
|
|
64
|
+
### Proxy Management
|
|
65
|
+
|
|
66
|
+
| Method | Description |
|
|
67
|
+
|--------|-------------|
|
|
68
|
+
| `change_proxy_credentials(proxy_id, login, password)` | Change proxy login/password |
|
|
69
|
+
| `reboot_proxy(proxy_id)` | Restart the modem |
|
|
70
|
+
| `edit_proxy(proxy_id, *, reboot_time=, ip_auth=, comment=)` | Update proxy settings |
|
|
71
|
+
| `change_ip(proxy_key, *, format=, user_agent=)` | Rotate IP (no rate limit) |
|
|
72
|
+
|
|
73
|
+
### Equipment & GEO
|
|
74
|
+
|
|
75
|
+
| Method | Description |
|
|
76
|
+
|--------|-------------|
|
|
77
|
+
| `change_equipment(proxy_id, **kwargs)` | Switch modem/SIM/operator/city |
|
|
78
|
+
| `get_available_equipment(proxy_id, **kwargs)` | List available equipment by GEO |
|
|
79
|
+
| `get_geo_list(proxy_id, geo_id)` | Available GEOs for a proxy |
|
|
80
|
+
| `get_operators(geo_id)` | Operators for a GEO |
|
|
81
|
+
| `get_countries(only_available=)` | List of countries |
|
|
82
|
+
| `get_cities()` | List of cities |
|
|
83
|
+
|
|
84
|
+
### Blacklist
|
|
85
|
+
|
|
86
|
+
| Method | Description |
|
|
87
|
+
|--------|-------------|
|
|
88
|
+
| `get_black_list(proxy_id)` | Get equipment/operator blacklist |
|
|
89
|
+
| `add_operator_to_black_list(proxy_id, operator_id)` | Block an operator |
|
|
90
|
+
| `remove_operator_from_black_list(proxy_id, operator_id)` | Unblock an operator |
|
|
91
|
+
| `remove_from_black_list(proxy_id, black_list_id, eid)` | Remove equipment from blacklist |
|
|
92
|
+
|
|
93
|
+
### Purchasing & Billing
|
|
94
|
+
|
|
95
|
+
| Method | Description |
|
|
96
|
+
|--------|-------------|
|
|
97
|
+
| `buy_proxy(**kwargs)` | Purchase a proxy |
|
|
98
|
+
| `refund_proxy(proxy_id)` | Request a refund |
|
|
99
|
+
| `get_balance()` | Account balance |
|
|
100
|
+
| `get_prices(country_id)` | Prices for a country |
|
|
101
|
+
| `get_test_proxy(geo_id, operator)` | Get a free 2-hour trial proxy |
|
|
102
|
+
|
|
103
|
+
### Utilities
|
|
104
|
+
|
|
105
|
+
| Method | Description |
|
|
106
|
+
|--------|-------------|
|
|
107
|
+
| `check_equipment_availability(eid)` | Check if equipment is available |
|
|
108
|
+
| `view_url_from_different_ips(url, country_id)` | Anti-cloaking: view URL from another country |
|
|
109
|
+
| `get_task_result(task_id)` | Get async task result |
|
|
110
|
+
|
|
111
|
+
## Error Handling
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from mobileproxy import Client, ApiError, AuthenticationError, RateLimitError
|
|
115
|
+
|
|
116
|
+
client = Client("YOUR_API_TOKEN")
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
client.get_balance()
|
|
120
|
+
except AuthenticationError as e:
|
|
121
|
+
# Invalid API token
|
|
122
|
+
print(e, e.http_code)
|
|
123
|
+
except RateLimitError as e:
|
|
124
|
+
# Too many requests — back off and retry
|
|
125
|
+
# Limit: 3 × (number of active proxies) requests/sec
|
|
126
|
+
print(e)
|
|
127
|
+
except ApiError as e:
|
|
128
|
+
print(e.http_code, e.response_body)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Configuration
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
client = Client(
|
|
135
|
+
"YOUR_API_TOKEN",
|
|
136
|
+
timeout=120, # request timeout in seconds
|
|
137
|
+
base_url="https://mobileproxy.space/api.html", # default
|
|
138
|
+
)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Rate Limits
|
|
142
|
+
|
|
143
|
+
API requests are limited to **3 × (number of active proxies)** per second. For example, 10 proxies = 30 req/s. The `change_ip()` method uses a separate endpoint with **no rate limit**.
|
|
144
|
+
|
|
145
|
+
## Requirements
|
|
146
|
+
|
|
147
|
+
- Python 3.8 or higher
|
|
148
|
+
- `requests` library (installed automatically)
|
|
149
|
+
|
|
150
|
+
## Links
|
|
151
|
+
|
|
152
|
+
- [API Documentation](https://mobileproxy.space/en/user.html?api)
|
|
153
|
+
- [Dashboard](https://mobileproxy.space/en/user.html)
|
|
154
|
+
- [Website](https://mobileproxy.space)
|
|
155
|
+
- [PHP SDK](https://github.com/mobileproxy/php-sdk)
|
|
156
|
+
- [Node.js SDK](https://github.com/mobileproxy/node-sdk)
|
|
157
|
+
- [Chrome Extension](https://chromewebstore.google.com/detail/mobile-proxy-manager/lhbdhjhflkejgkkhlgacbaogbaaollac)
|
|
158
|
+
|
|
159
|
+
## License
|
|
160
|
+
|
|
161
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""MobileProxy.Space Python SDK.
|
|
2
|
+
|
|
3
|
+
Official Python SDK for interacting with the MobileProxy.Space API.
|
|
4
|
+
Provides private mobile proxies on real GSM devices across 52 countries.
|
|
5
|
+
|
|
6
|
+
Quick start::
|
|
7
|
+
|
|
8
|
+
from mobileproxy import Client
|
|
9
|
+
|
|
10
|
+
client = Client("YOUR_API_TOKEN")
|
|
11
|
+
print(client.get_balance())
|
|
12
|
+
|
|
13
|
+
See https://mobileproxy.space/api.html
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from .client import Client
|
|
17
|
+
from .exceptions import ApiError, AuthenticationError, RateLimitError
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"Client",
|
|
21
|
+
"ApiError",
|
|
22
|
+
"AuthenticationError",
|
|
23
|
+
"RateLimitError",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
__version__ = "1.0.0"
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
"""MobileProxy.Space API Client.
|
|
2
|
+
|
|
3
|
+
Official Python SDK for interacting with the MobileProxy.Space API.
|
|
4
|
+
Provides private mobile proxies on real GSM devices across 52 countries.
|
|
5
|
+
|
|
6
|
+
Example::
|
|
7
|
+
|
|
8
|
+
from mobileproxy import Client
|
|
9
|
+
|
|
10
|
+
client = Client("YOUR_API_TOKEN")
|
|
11
|
+
balance = client.get_balance()
|
|
12
|
+
print(balance)
|
|
13
|
+
|
|
14
|
+
See https://mobileproxy.space/api.html for API documentation.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import Any, Dict, Optional
|
|
20
|
+
|
|
21
|
+
import requests
|
|
22
|
+
|
|
23
|
+
from .exceptions import ApiError
|
|
24
|
+
from .http_client import HttpClient
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Client:
|
|
28
|
+
"""MobileProxy.Space API client.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
api_token: Your API token (find it in your dashboard).
|
|
32
|
+
base_url: API endpoint override.
|
|
33
|
+
timeout: Request timeout in seconds (default: 60).
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
api_token: str,
|
|
39
|
+
*,
|
|
40
|
+
base_url: str = "https://mobileproxy.space/api.html",
|
|
41
|
+
timeout: int = 60,
|
|
42
|
+
):
|
|
43
|
+
if not api_token:
|
|
44
|
+
raise ValueError("API token is required")
|
|
45
|
+
self._http = HttpClient(api_token, base_url=base_url, timeout=timeout)
|
|
46
|
+
|
|
47
|
+
def close(self):
|
|
48
|
+
"""Close the underlying HTTP session."""
|
|
49
|
+
self._http.close()
|
|
50
|
+
|
|
51
|
+
def __enter__(self):
|
|
52
|
+
return self
|
|
53
|
+
|
|
54
|
+
def __exit__(self, *args):
|
|
55
|
+
self.close()
|
|
56
|
+
|
|
57
|
+
# ──────────────────────────────────────────────
|
|
58
|
+
# Proxy Information
|
|
59
|
+
# ──────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
def get_proxy_ip(
|
|
62
|
+
self, proxy_id: int, *, check_spam: Optional[str] = None
|
|
63
|
+
) -> dict:
|
|
64
|
+
"""Get the current IP address of a proxy.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
proxy_id: Proxy ID.
|
|
68
|
+
check_spam: Pass "1" to check IP against spam databases.
|
|
69
|
+
"""
|
|
70
|
+
params: Dict[str, Any] = {"proxy_id": proxy_id}
|
|
71
|
+
if check_spam is not None:
|
|
72
|
+
params["check_spam"] = check_spam
|
|
73
|
+
return self._http.request("proxy_ip", "GET", params)
|
|
74
|
+
|
|
75
|
+
def get_my_proxy(self, proxy_id: Optional[int] = None) -> dict:
|
|
76
|
+
"""Get a list of your active proxies.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
proxy_id: Filter by specific proxy ID, or None for all.
|
|
80
|
+
"""
|
|
81
|
+
params: Dict[str, Any] = {}
|
|
82
|
+
if proxy_id is not None:
|
|
83
|
+
params["proxy_id"] = proxy_id
|
|
84
|
+
return self._http.request("get_my_proxy", "GET", params)
|
|
85
|
+
|
|
86
|
+
def get_ip_stats(self) -> dict:
|
|
87
|
+
"""Get IP address statistics by GEO."""
|
|
88
|
+
return self._http.request("get_ipstat")
|
|
89
|
+
|
|
90
|
+
# ──────────────────────────────────────────────
|
|
91
|
+
# Proxy Management
|
|
92
|
+
# ──────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
def change_proxy_credentials(
|
|
95
|
+
self, proxy_id: int, login: str, password: str
|
|
96
|
+
) -> dict:
|
|
97
|
+
"""Change the login and password of a proxy.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
proxy_id: Proxy ID.
|
|
101
|
+
login: New login.
|
|
102
|
+
password: New password.
|
|
103
|
+
"""
|
|
104
|
+
return self._http.request(
|
|
105
|
+
"change_proxy_login_password",
|
|
106
|
+
"GET",
|
|
107
|
+
{"proxy_id": proxy_id, "proxy_login": login, "proxy_pass": password},
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def reboot_proxy(self, proxy_id: int) -> dict:
|
|
111
|
+
"""Restart a proxy (reboot the modem).
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
proxy_id: Proxy ID.
|
|
115
|
+
"""
|
|
116
|
+
return self._http.request("reboot_proxy", "GET", {"proxy_id": proxy_id})
|
|
117
|
+
|
|
118
|
+
def edit_proxy(
|
|
119
|
+
self,
|
|
120
|
+
proxy_id: int,
|
|
121
|
+
*,
|
|
122
|
+
reboot_time: Optional[int] = None,
|
|
123
|
+
ip_auth: Optional[str] = None,
|
|
124
|
+
comment: Optional[str] = None,
|
|
125
|
+
) -> dict:
|
|
126
|
+
"""Change existing proxy settings.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
proxy_id: Proxy ID.
|
|
130
|
+
reboot_time: Auto-reboot interval in minutes (0 to disable).
|
|
131
|
+
ip_auth: Authorized IP address (empty string to disable).
|
|
132
|
+
comment: Comment / label for this proxy.
|
|
133
|
+
"""
|
|
134
|
+
params: Dict[str, Any] = {"proxy_id": proxy_id}
|
|
135
|
+
if reboot_time is not None:
|
|
136
|
+
params["proxy_reboot_time"] = reboot_time
|
|
137
|
+
if ip_auth is not None:
|
|
138
|
+
params["proxy_ipauth"] = ip_auth
|
|
139
|
+
if comment is not None:
|
|
140
|
+
params["proxy_comment"] = comment
|
|
141
|
+
return self._http.request("edit_proxy", "GET", params)
|
|
142
|
+
|
|
143
|
+
# ──────────────────────────────────────────────
|
|
144
|
+
# Equipment & GEO
|
|
145
|
+
# ──────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
def change_equipment(self, proxy_id: int, **kwargs) -> dict:
|
|
148
|
+
"""Change proxy equipment (modem/SIM).
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
proxy_id: Proxy ID.
|
|
152
|
+
**kwargs: Optional parameters:
|
|
153
|
+
operator (str): Target operator name.
|
|
154
|
+
geo_id (int): GEO ID.
|
|
155
|
+
country_id (int): Country ID.
|
|
156
|
+
city_id (int): City ID.
|
|
157
|
+
eid (int): Equipment ID.
|
|
158
|
+
add_to_black_list: Add current equipment to blacklist.
|
|
159
|
+
check_after_change: Check IP after change.
|
|
160
|
+
check_spam: Check spam after change.
|
|
161
|
+
"""
|
|
162
|
+
params: Dict[str, Any] = {"proxy_id": proxy_id}
|
|
163
|
+
mapping = {
|
|
164
|
+
"operator": "operator",
|
|
165
|
+
"geo_id": "geoid",
|
|
166
|
+
"country_id": "id_country",
|
|
167
|
+
"city_id": "id_city",
|
|
168
|
+
"eid": "eid",
|
|
169
|
+
"add_to_black_list": "add_to_black_list",
|
|
170
|
+
"check_after_change": "check_after_change",
|
|
171
|
+
"check_spam": "check_spam",
|
|
172
|
+
}
|
|
173
|
+
for py_key, api_key in mapping.items():
|
|
174
|
+
if py_key in kwargs and kwargs[py_key] is not None:
|
|
175
|
+
params[api_key] = kwargs[py_key]
|
|
176
|
+
return self._http.request("change_equipment", "GET", params)
|
|
177
|
+
|
|
178
|
+
def get_available_equipment(self, proxy_id: int, **kwargs) -> dict:
|
|
179
|
+
"""Get available equipment grouped by GEO and operator.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
proxy_id: Proxy ID.
|
|
183
|
+
**kwargs: Optional parameters:
|
|
184
|
+
equipments_back_list (int): Exclude equipment IDs.
|
|
185
|
+
operators_back_list (int): Exclude operator IDs.
|
|
186
|
+
show_count_null: Show items with 0 count.
|
|
187
|
+
"""
|
|
188
|
+
params: Dict[str, Any] = {"proxy_id": proxy_id}
|
|
189
|
+
for key in ("equipments_back_list", "operators_back_list", "show_count_null"):
|
|
190
|
+
if key in kwargs and kwargs[key] is not None:
|
|
191
|
+
params[key] = kwargs[key]
|
|
192
|
+
return self._http.request("get_geo_operator_list", "GET", params)
|
|
193
|
+
|
|
194
|
+
def get_geo_list(self, proxy_id: int, geo_id: int) -> dict:
|
|
195
|
+
"""Get list of available GEOs for a proxy.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
proxy_id: Proxy ID.
|
|
199
|
+
geo_id: GEO ID.
|
|
200
|
+
"""
|
|
201
|
+
return self._http.request(
|
|
202
|
+
"get_geo_list", "GET", {"proxy_id": proxy_id, "geoid": geo_id}
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
def get_operators(self, geo_id: int) -> dict:
|
|
206
|
+
"""Get list of operators for a GEO.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
geo_id: GEO ID.
|
|
210
|
+
"""
|
|
211
|
+
return self._http.request("get_operators_list", "GET", {"geoid": geo_id})
|
|
212
|
+
|
|
213
|
+
def get_countries(self, only_available: Optional[str] = None) -> dict:
|
|
214
|
+
"""Get list of countries.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
only_available: Filter to only available countries.
|
|
218
|
+
"""
|
|
219
|
+
params: Dict[str, Any] = {}
|
|
220
|
+
if only_available is not None:
|
|
221
|
+
params["only_avaliable"] = only_available
|
|
222
|
+
return self._http.request("get_id_country", "GET", params)
|
|
223
|
+
|
|
224
|
+
def get_cities(self) -> dict:
|
|
225
|
+
"""Get list of cities."""
|
|
226
|
+
return self._http.request("get_id_city")
|
|
227
|
+
|
|
228
|
+
# ──────────────────────────────────────────────
|
|
229
|
+
# Blacklist Management
|
|
230
|
+
# ──────────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
def get_black_list(self, proxy_id: int) -> dict:
|
|
233
|
+
"""Get the blacklist of equipment and operators for a proxy.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
proxy_id: Proxy ID.
|
|
237
|
+
"""
|
|
238
|
+
return self._http.request("get_black_list", "GET", {"proxy_id": proxy_id})
|
|
239
|
+
|
|
240
|
+
def add_operator_to_black_list(self, proxy_id: int, operator_id: int) -> dict:
|
|
241
|
+
"""Add an operator to the blacklist.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
proxy_id: Proxy ID.
|
|
245
|
+
operator_id: Operator ID.
|
|
246
|
+
"""
|
|
247
|
+
return self._http.request(
|
|
248
|
+
"add_operator_to_black_list",
|
|
249
|
+
"GET",
|
|
250
|
+
{"proxy_id": proxy_id, "operator_id": operator_id},
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
def remove_operator_from_black_list(
|
|
254
|
+
self, proxy_id: int, operator_id: int
|
|
255
|
+
) -> dict:
|
|
256
|
+
"""Remove an operator from the blacklist.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
proxy_id: Proxy ID.
|
|
260
|
+
operator_id: Operator ID.
|
|
261
|
+
"""
|
|
262
|
+
return self._http.request(
|
|
263
|
+
"remove_operator_black_list",
|
|
264
|
+
"GET",
|
|
265
|
+
{"proxy_id": proxy_id, "operator_id": operator_id},
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
def remove_from_black_list(
|
|
269
|
+
self, proxy_id: int, black_list_id: int, eid: int
|
|
270
|
+
) -> dict:
|
|
271
|
+
"""Remove an entry from the equipment blacklist.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
proxy_id: Proxy ID.
|
|
275
|
+
black_list_id: Blacklist entry ID.
|
|
276
|
+
eid: Equipment ID.
|
|
277
|
+
"""
|
|
278
|
+
return self._http.request(
|
|
279
|
+
"remove_black_list",
|
|
280
|
+
"GET",
|
|
281
|
+
{"proxy_id": proxy_id, "black_list_id": black_list_id, "eid": eid},
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
# ──────────────────────────────────────────────
|
|
285
|
+
# Purchasing & Billing
|
|
286
|
+
# ──────────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
def buy_proxy(self, **kwargs) -> dict:
|
|
289
|
+
"""Purchase a proxy.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
**kwargs:
|
|
293
|
+
country_id (int): Country ID (required).
|
|
294
|
+
period (int): Rental period in days (required).
|
|
295
|
+
num (int): Number of proxies (default: 1).
|
|
296
|
+
operator (str): Preferred operator.
|
|
297
|
+
geo_id (int): GEO ID.
|
|
298
|
+
city_id (int): City ID.
|
|
299
|
+
coupon_code (str): Discount coupon code.
|
|
300
|
+
auto_renewal: Enable auto-renewal.
|
|
301
|
+
proxy_id (int): Extend an existing proxy.
|
|
302
|
+
"""
|
|
303
|
+
params: Dict[str, Any] = {}
|
|
304
|
+
mapping = {
|
|
305
|
+
"country_id": "id_country",
|
|
306
|
+
"period": "period",
|
|
307
|
+
"num": "num",
|
|
308
|
+
"operator": "operator",
|
|
309
|
+
"geo_id": "geoid",
|
|
310
|
+
"city_id": "id_city",
|
|
311
|
+
"coupon_code": "coupons_code",
|
|
312
|
+
"auto_renewal": "auto_renewal",
|
|
313
|
+
"proxy_id": "proxy_id",
|
|
314
|
+
}
|
|
315
|
+
for py_key, api_key in mapping.items():
|
|
316
|
+
if py_key in kwargs and kwargs[py_key] is not None:
|
|
317
|
+
params[api_key] = kwargs[py_key]
|
|
318
|
+
return self._http.request("buyproxy", "GET", params)
|
|
319
|
+
|
|
320
|
+
def refund_proxy(self, proxy_id: int) -> dict:
|
|
321
|
+
"""Refund a proxy.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
proxy_id: Proxy ID.
|
|
325
|
+
"""
|
|
326
|
+
return self._http.request("refund_proxy", "GET", {"proxy_id": proxy_id})
|
|
327
|
+
|
|
328
|
+
def get_balance(self) -> dict:
|
|
329
|
+
"""Get your account balance."""
|
|
330
|
+
return self._http.request("get_balance")
|
|
331
|
+
|
|
332
|
+
def get_prices(self, country_id: int) -> dict:
|
|
333
|
+
"""Get proxy prices for a country.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
country_id: Country ID.
|
|
337
|
+
"""
|
|
338
|
+
return self._http.request("get_price", "GET", {"id_country": country_id})
|
|
339
|
+
|
|
340
|
+
def get_test_proxy(self, geo_id: int, operator: str) -> dict:
|
|
341
|
+
"""Get a free 2-hour test proxy.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
geo_id: GEO ID.
|
|
345
|
+
operator: Operator name.
|
|
346
|
+
"""
|
|
347
|
+
return self._http.request(
|
|
348
|
+
"get_test_proxy", "GET", {"geoid": geo_id, "operator": operator}
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
# ──────────────────────────────────────────────
|
|
352
|
+
# Utilities
|
|
353
|
+
# ──────────────────────────────────────────────
|
|
354
|
+
|
|
355
|
+
def check_equipment_availability(self, eid: int) -> dict:
|
|
356
|
+
"""Check if a specific equipment is available.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
eid: Equipment ID.
|
|
360
|
+
"""
|
|
361
|
+
return self._http.request("eid_avaliable", "GET", {"eid": eid})
|
|
362
|
+
|
|
363
|
+
def view_url_from_different_ips(self, url: str, country_id: int) -> dict:
|
|
364
|
+
"""View a URL from different IPs (anti-cloaking tool).
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
url: URL to fetch.
|
|
368
|
+
country_id: Country ID to fetch from.
|
|
369
|
+
"""
|
|
370
|
+
return self._http.request(
|
|
371
|
+
"see_the_url_from_different_IPs",
|
|
372
|
+
"POST",
|
|
373
|
+
{"url": url, "id_country": country_id},
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
def get_task_result(self, task_id: int) -> dict:
|
|
377
|
+
"""Get the result of an async task.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
task_id: Task ID.
|
|
381
|
+
"""
|
|
382
|
+
return self._http.request("tasks", "GET", {"tasks_id": task_id})
|
|
383
|
+
|
|
384
|
+
# ──────────────────────────────────────────────
|
|
385
|
+
# IP Rotation (via changeip.mobileproxy.space)
|
|
386
|
+
# ──────────────────────────────────────────────
|
|
387
|
+
|
|
388
|
+
def change_ip(
|
|
389
|
+
self,
|
|
390
|
+
proxy_key: str,
|
|
391
|
+
*,
|
|
392
|
+
format: str = "json",
|
|
393
|
+
user_agent: str = "MobileProxy-Python-SDK/1.0",
|
|
394
|
+
) -> dict:
|
|
395
|
+
"""Change proxy IP address using the dedicated rotation endpoint.
|
|
396
|
+
|
|
397
|
+
This method uses a separate endpoint with NO rate limit
|
|
398
|
+
(unlike other API methods).
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
proxy_key: Proxy key from your dashboard.
|
|
402
|
+
format: Response format: 'json' or '0' for plain text.
|
|
403
|
+
user_agent: Custom User-Agent header.
|
|
404
|
+
"""
|
|
405
|
+
response = requests.get(
|
|
406
|
+
"https://changeip.mobileproxy.space/",
|
|
407
|
+
params={"proxy_key": proxy_key, "format": format},
|
|
408
|
+
headers={"User-Agent": user_agent},
|
|
409
|
+
timeout=30,
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
if format == "json":
|
|
413
|
+
try:
|
|
414
|
+
return response.json()
|
|
415
|
+
except ValueError:
|
|
416
|
+
return {"response": response.text}
|
|
417
|
+
|
|
418
|
+
return {"response": response.text}
|
|
419
|
+
|
|
420
|
+
# ──────────────────────────────────────────────
|
|
421
|
+
# Debug
|
|
422
|
+
# ──────────────────────────────────────────────
|
|
423
|
+
|
|
424
|
+
@property
|
|
425
|
+
def last_request(self) -> dict:
|
|
426
|
+
"""Get the last HTTP request details (for debugging)."""
|
|
427
|
+
return self._http.last_request
|
|
428
|
+
|
|
429
|
+
@property
|
|
430
|
+
def last_response(self) -> dict:
|
|
431
|
+
"""Get the last HTTP response details (for debugging)."""
|
|
432
|
+
return self._http.last_response
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""MobileProxy.Space API exceptions."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ApiError(Exception):
|
|
5
|
+
"""Base exception for API errors."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, message: str, http_code: int = 0, response_body: dict = None):
|
|
8
|
+
super().__init__(message)
|
|
9
|
+
self.http_code = http_code
|
|
10
|
+
self.response_body = response_body
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AuthenticationError(ApiError):
|
|
14
|
+
"""Raised when the API token is invalid or missing."""
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RateLimitError(ApiError):
|
|
19
|
+
"""Raised when the API rate limit is exceeded.
|
|
20
|
+
|
|
21
|
+
Limit: 3 × (number of active proxies) requests per second.
|
|
22
|
+
"""
|
|
23
|
+
pass
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""HTTP transport layer for MobileProxy.Space API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
from .exceptions import ApiError, AuthenticationError, RateLimitError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class HttpClient:
|
|
13
|
+
"""Low-level HTTP client for the MobileProxy.Space API."""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
api_token: str,
|
|
18
|
+
base_url: str = "https://mobileproxy.space/api.html",
|
|
19
|
+
timeout: int = 60,
|
|
20
|
+
):
|
|
21
|
+
self.api_token = api_token
|
|
22
|
+
self.base_url = base_url
|
|
23
|
+
self.timeout = timeout
|
|
24
|
+
self.session = requests.Session()
|
|
25
|
+
self.session.headers.update(
|
|
26
|
+
{
|
|
27
|
+
"Authorization": f"Bearer {api_token}",
|
|
28
|
+
"Accept": "application/json",
|
|
29
|
+
"User-Agent": "MobileProxy-Python-SDK/1.0",
|
|
30
|
+
}
|
|
31
|
+
)
|
|
32
|
+
self.last_request: Dict[str, Any] = {}
|
|
33
|
+
self.last_response: Dict[str, Any] = {}
|
|
34
|
+
|
|
35
|
+
def request(
|
|
36
|
+
self,
|
|
37
|
+
command: str,
|
|
38
|
+
method: str = "GET",
|
|
39
|
+
params: Optional[Dict[str, Any]] = None,
|
|
40
|
+
) -> dict:
|
|
41
|
+
"""Execute an API request.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
command: API command name.
|
|
45
|
+
method: HTTP method (GET or POST).
|
|
46
|
+
params: Additional query/body parameters.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Parsed JSON response.
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
ApiError: On API-level errors.
|
|
53
|
+
AuthenticationError: On auth failures.
|
|
54
|
+
RateLimitError: When rate limit is exceeded.
|
|
55
|
+
"""
|
|
56
|
+
if params is None:
|
|
57
|
+
params = {}
|
|
58
|
+
params["command"] = command
|
|
59
|
+
|
|
60
|
+
self.last_request = {
|
|
61
|
+
"command": command,
|
|
62
|
+
"method": method,
|
|
63
|
+
"params": params.copy(),
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
if method == "POST":
|
|
68
|
+
response = self.session.post(
|
|
69
|
+
self.base_url,
|
|
70
|
+
params={"command": command},
|
|
71
|
+
data=params,
|
|
72
|
+
timeout=self.timeout,
|
|
73
|
+
)
|
|
74
|
+
else:
|
|
75
|
+
response = self.session.get(
|
|
76
|
+
self.base_url,
|
|
77
|
+
params=params,
|
|
78
|
+
timeout=self.timeout,
|
|
79
|
+
)
|
|
80
|
+
except requests.exceptions.Timeout:
|
|
81
|
+
raise ApiError(f"Request timed out after {self.timeout}s")
|
|
82
|
+
except requests.exceptions.ConnectionError as e:
|
|
83
|
+
raise ApiError(f"Connection error: {e}")
|
|
84
|
+
except requests.exceptions.RequestException as e:
|
|
85
|
+
raise ApiError(f"Request failed: {e}")
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
json_data = response.json()
|
|
89
|
+
except ValueError:
|
|
90
|
+
raise ApiError(
|
|
91
|
+
f"Invalid JSON response: {response.text[:200]}",
|
|
92
|
+
http_code=response.status_code,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
self.last_response = {
|
|
96
|
+
"http_code": response.status_code,
|
|
97
|
+
"headers": dict(response.headers),
|
|
98
|
+
"json": json_data,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if "error" in json_data:
|
|
102
|
+
msg = json_data["error"]
|
|
103
|
+
|
|
104
|
+
if response.status_code == 401 or any(
|
|
105
|
+
w in msg.lower() for w in ("auth", "token")
|
|
106
|
+
):
|
|
107
|
+
raise AuthenticationError(msg, response.status_code, json_data)
|
|
108
|
+
|
|
109
|
+
if response.status_code == 429 or "too many requests" in msg.lower():
|
|
110
|
+
raise RateLimitError(msg, response.status_code, json_data)
|
|
111
|
+
|
|
112
|
+
raise ApiError(msg, response.status_code, json_data)
|
|
113
|
+
|
|
114
|
+
return json_data
|
|
115
|
+
|
|
116
|
+
def close(self):
|
|
117
|
+
"""Close the underlying HTTP session."""
|
|
118
|
+
self.session.close()
|
|
File without changes
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mobileproxy-sdk
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official Python SDK for MobileProxy.Space API — private mobile proxies on real GSM devices
|
|
5
|
+
Author-email: "MobileProxy.Space" <support@mobirox.co.uk>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://mobileproxy.space
|
|
8
|
+
Project-URL: Documentation, https://mobileproxy.space/en/user.html?api
|
|
9
|
+
Project-URL: Repository, https://github.com/mobileproxy/python-sdk
|
|
10
|
+
Project-URL: Issues, https://github.com/mobileproxy/python-sdk/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/mobileproxy/python-sdk/releases
|
|
12
|
+
Keywords: proxy,mobile-proxy,mobileproxy,socks5,http-proxy,scraping,api-client,ip-rotation
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Internet :: Proxy Servers
|
|
25
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
26
|
+
Requires-Python: >=3.8
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Requires-Dist: requests>=2.20.0
|
|
30
|
+
Provides-Extra: async
|
|
31
|
+
Requires-Dist: httpx>=0.24.0; extra == "async"
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
34
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# MobileProxy.Space Python SDK
|
|
38
|
+
|
|
39
|
+
[](https://github.com/mobileproxy/python-sdk/actions/workflows/ci.yml)
|
|
40
|
+
[](https://pypi.org/project/mobileproxy-sdk/)
|
|
41
|
+
[](https://pypi.org/project/mobileproxy-sdk/)
|
|
42
|
+
|
|
43
|
+
Official Python SDK for the [MobileProxy.Space](https://mobileproxy.space) API — private mobile proxies on real GSM devices across 52 countries.
|
|
44
|
+
|
|
45
|
+
## Features
|
|
46
|
+
|
|
47
|
+
- **Full API coverage** — all endpoints wrapped in typed, documented methods
|
|
48
|
+
- **Context manager** support (`with Client(...) as client:`)
|
|
49
|
+
- **Typed exceptions** — `ApiError`, `AuthenticationError`, `RateLimitError`
|
|
50
|
+
- **IP rotation** — dedicated `change_ip()` with no rate limit
|
|
51
|
+
- **Python 3.8+** compatible
|
|
52
|
+
- **Type hints** throughout + `py.typed` marker (PEP 561)
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install mobileproxy-sdk
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Quick Start
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from mobileproxy import Client
|
|
64
|
+
|
|
65
|
+
client = Client("YOUR_API_TOKEN")
|
|
66
|
+
|
|
67
|
+
# Check balance
|
|
68
|
+
balance = client.get_balance()
|
|
69
|
+
print(balance)
|
|
70
|
+
|
|
71
|
+
# List active proxies
|
|
72
|
+
proxies = client.get_my_proxy()
|
|
73
|
+
|
|
74
|
+
# Get current IP
|
|
75
|
+
ip = client.get_proxy_ip(12345)
|
|
76
|
+
|
|
77
|
+
# Rotate IP (no rate limit)
|
|
78
|
+
client.change_ip("your_proxy_key")
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Context Manager
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
with Client("YOUR_API_TOKEN") as client:
|
|
85
|
+
balance = client.get_balance()
|
|
86
|
+
print(balance)
|
|
87
|
+
# session is closed automatically
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## API Methods
|
|
91
|
+
|
|
92
|
+
### Proxy Information
|
|
93
|
+
|
|
94
|
+
| Method | Description |
|
|
95
|
+
|--------|-------------|
|
|
96
|
+
| `get_proxy_ip(proxy_id, *, check_spam=)` | Get current IP address of a proxy |
|
|
97
|
+
| `get_my_proxy(proxy_id=)` | List active proxies (all or specific) |
|
|
98
|
+
| `get_ip_stats()` | IP address statistics by GEO |
|
|
99
|
+
|
|
100
|
+
### Proxy Management
|
|
101
|
+
|
|
102
|
+
| Method | Description |
|
|
103
|
+
|--------|-------------|
|
|
104
|
+
| `change_proxy_credentials(proxy_id, login, password)` | Change proxy login/password |
|
|
105
|
+
| `reboot_proxy(proxy_id)` | Restart the modem |
|
|
106
|
+
| `edit_proxy(proxy_id, *, reboot_time=, ip_auth=, comment=)` | Update proxy settings |
|
|
107
|
+
| `change_ip(proxy_key, *, format=, user_agent=)` | Rotate IP (no rate limit) |
|
|
108
|
+
|
|
109
|
+
### Equipment & GEO
|
|
110
|
+
|
|
111
|
+
| Method | Description |
|
|
112
|
+
|--------|-------------|
|
|
113
|
+
| `change_equipment(proxy_id, **kwargs)` | Switch modem/SIM/operator/city |
|
|
114
|
+
| `get_available_equipment(proxy_id, **kwargs)` | List available equipment by GEO |
|
|
115
|
+
| `get_geo_list(proxy_id, geo_id)` | Available GEOs for a proxy |
|
|
116
|
+
| `get_operators(geo_id)` | Operators for a GEO |
|
|
117
|
+
| `get_countries(only_available=)` | List of countries |
|
|
118
|
+
| `get_cities()` | List of cities |
|
|
119
|
+
|
|
120
|
+
### Blacklist
|
|
121
|
+
|
|
122
|
+
| Method | Description |
|
|
123
|
+
|--------|-------------|
|
|
124
|
+
| `get_black_list(proxy_id)` | Get equipment/operator blacklist |
|
|
125
|
+
| `add_operator_to_black_list(proxy_id, operator_id)` | Block an operator |
|
|
126
|
+
| `remove_operator_from_black_list(proxy_id, operator_id)` | Unblock an operator |
|
|
127
|
+
| `remove_from_black_list(proxy_id, black_list_id, eid)` | Remove equipment from blacklist |
|
|
128
|
+
|
|
129
|
+
### Purchasing & Billing
|
|
130
|
+
|
|
131
|
+
| Method | Description |
|
|
132
|
+
|--------|-------------|
|
|
133
|
+
| `buy_proxy(**kwargs)` | Purchase a proxy |
|
|
134
|
+
| `refund_proxy(proxy_id)` | Request a refund |
|
|
135
|
+
| `get_balance()` | Account balance |
|
|
136
|
+
| `get_prices(country_id)` | Prices for a country |
|
|
137
|
+
| `get_test_proxy(geo_id, operator)` | Get a free 2-hour trial proxy |
|
|
138
|
+
|
|
139
|
+
### Utilities
|
|
140
|
+
|
|
141
|
+
| Method | Description |
|
|
142
|
+
|--------|-------------|
|
|
143
|
+
| `check_equipment_availability(eid)` | Check if equipment is available |
|
|
144
|
+
| `view_url_from_different_ips(url, country_id)` | Anti-cloaking: view URL from another country |
|
|
145
|
+
| `get_task_result(task_id)` | Get async task result |
|
|
146
|
+
|
|
147
|
+
## Error Handling
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from mobileproxy import Client, ApiError, AuthenticationError, RateLimitError
|
|
151
|
+
|
|
152
|
+
client = Client("YOUR_API_TOKEN")
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
client.get_balance()
|
|
156
|
+
except AuthenticationError as e:
|
|
157
|
+
# Invalid API token
|
|
158
|
+
print(e, e.http_code)
|
|
159
|
+
except RateLimitError as e:
|
|
160
|
+
# Too many requests — back off and retry
|
|
161
|
+
# Limit: 3 × (number of active proxies) requests/sec
|
|
162
|
+
print(e)
|
|
163
|
+
except ApiError as e:
|
|
164
|
+
print(e.http_code, e.response_body)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Configuration
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
client = Client(
|
|
171
|
+
"YOUR_API_TOKEN",
|
|
172
|
+
timeout=120, # request timeout in seconds
|
|
173
|
+
base_url="https://mobileproxy.space/api.html", # default
|
|
174
|
+
)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Rate Limits
|
|
178
|
+
|
|
179
|
+
API requests are limited to **3 × (number of active proxies)** per second. For example, 10 proxies = 30 req/s. The `change_ip()` method uses a separate endpoint with **no rate limit**.
|
|
180
|
+
|
|
181
|
+
## Requirements
|
|
182
|
+
|
|
183
|
+
- Python 3.8 or higher
|
|
184
|
+
- `requests` library (installed automatically)
|
|
185
|
+
|
|
186
|
+
## Links
|
|
187
|
+
|
|
188
|
+
- [API Documentation](https://mobileproxy.space/en/user.html?api)
|
|
189
|
+
- [Dashboard](https://mobileproxy.space/en/user.html)
|
|
190
|
+
- [Website](https://mobileproxy.space)
|
|
191
|
+
- [PHP SDK](https://github.com/mobileproxy/php-sdk)
|
|
192
|
+
- [Node.js SDK](https://github.com/mobileproxy/node-sdk)
|
|
193
|
+
- [Chrome Extension](https://chromewebstore.google.com/detail/mobile-proxy-manager/lhbdhjhflkejgkkhlgacbaogbaaollac)
|
|
194
|
+
|
|
195
|
+
## License
|
|
196
|
+
|
|
197
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
mobileproxy/__init__.py
|
|
5
|
+
mobileproxy/client.py
|
|
6
|
+
mobileproxy/exceptions.py
|
|
7
|
+
mobileproxy/http_client.py
|
|
8
|
+
mobileproxy/py.typed
|
|
9
|
+
mobileproxy_sdk.egg-info/PKG-INFO
|
|
10
|
+
mobileproxy_sdk.egg-info/SOURCES.txt
|
|
11
|
+
mobileproxy_sdk.egg-info/dependency_links.txt
|
|
12
|
+
mobileproxy_sdk.egg-info/requires.txt
|
|
13
|
+
mobileproxy_sdk.egg-info/top_level.txt
|
|
14
|
+
tests/test_client.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mobileproxy
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "mobileproxy-sdk"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Official Python SDK for MobileProxy.Space API — private mobile proxies on real GSM devices"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.8"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "MobileProxy.Space", email = "support@mobirox.co.uk"},
|
|
14
|
+
]
|
|
15
|
+
keywords = ["proxy", "mobile-proxy", "mobileproxy", "socks5", "http-proxy", "scraping", "api-client", "ip-rotation"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 5 - Production/Stable",
|
|
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.8",
|
|
23
|
+
"Programming Language :: Python :: 3.9",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Programming Language :: Python :: 3.13",
|
|
28
|
+
"Topic :: Internet :: Proxy Servers",
|
|
29
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
30
|
+
]
|
|
31
|
+
dependencies = [
|
|
32
|
+
"requests>=2.20.0",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
async = ["httpx>=0.24.0"]
|
|
37
|
+
dev = ["pytest>=7.0", "pytest-asyncio>=0.21"]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://mobileproxy.space"
|
|
41
|
+
Documentation = "https://mobileproxy.space/en/user.html?api"
|
|
42
|
+
Repository = "https://github.com/mobileproxy/python-sdk"
|
|
43
|
+
Issues = "https://github.com/mobileproxy/python-sdk/issues"
|
|
44
|
+
Changelog = "https://github.com/mobileproxy/python-sdk/releases"
|
|
45
|
+
|
|
46
|
+
[tool.setuptools.packages.find]
|
|
47
|
+
include = ["mobileproxy*"]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Tests for the MobileProxy.Space Python SDK."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from mobileproxy import Client, ApiError, AuthenticationError, RateLimitError
|
|
6
|
+
from mobileproxy.http_client import HttpClient
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestClient:
|
|
10
|
+
def test_instantiate_with_token(self):
|
|
11
|
+
client = Client("test_token")
|
|
12
|
+
assert isinstance(client, Client)
|
|
13
|
+
|
|
14
|
+
def test_accepts_options(self):
|
|
15
|
+
client = Client("test_token", base_url="https://custom.api/api.html", timeout=120)
|
|
16
|
+
assert isinstance(client, Client)
|
|
17
|
+
|
|
18
|
+
def test_raises_on_empty_token(self):
|
|
19
|
+
with pytest.raises(ValueError, match="API token is required"):
|
|
20
|
+
Client("")
|
|
21
|
+
|
|
22
|
+
def test_context_manager(self):
|
|
23
|
+
with Client("test_token") as client:
|
|
24
|
+
assert isinstance(client, Client)
|
|
25
|
+
|
|
26
|
+
def test_last_request_empty_initially(self):
|
|
27
|
+
client = Client("test_token")
|
|
28
|
+
assert client.last_request == {}
|
|
29
|
+
|
|
30
|
+
def test_last_response_empty_initially(self):
|
|
31
|
+
client = Client("test_token")
|
|
32
|
+
assert client.last_response == {}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TestHttpClient:
|
|
36
|
+
def test_instantiate_with_defaults(self):
|
|
37
|
+
http = HttpClient("test_token")
|
|
38
|
+
assert http.base_url == "https://mobileproxy.space/api.html"
|
|
39
|
+
assert http.timeout == 60
|
|
40
|
+
|
|
41
|
+
def test_custom_base_url_and_timeout(self):
|
|
42
|
+
http = HttpClient("test_token", base_url="https://custom.api/api.html", timeout=5)
|
|
43
|
+
assert http.base_url == "https://custom.api/api.html"
|
|
44
|
+
assert http.timeout == 5
|
|
45
|
+
|
|
46
|
+
def test_session_headers(self):
|
|
47
|
+
http = HttpClient("test_token_123")
|
|
48
|
+
assert http.session.headers["Authorization"] == "Bearer test_token_123"
|
|
49
|
+
assert "MobileProxy-Python-SDK" in http.session.headers["User-Agent"]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class TestExceptions:
|
|
53
|
+
def test_api_error_carries_details(self):
|
|
54
|
+
body = {"error": "Something went wrong"}
|
|
55
|
+
err = ApiError("Test error", http_code=400, response_body=body)
|
|
56
|
+
assert str(err) == "Test error"
|
|
57
|
+
assert err.http_code == 400
|
|
58
|
+
assert err.response_body == body
|
|
59
|
+
|
|
60
|
+
def test_authentication_error_extends_api_error(self):
|
|
61
|
+
err = AuthenticationError("Invalid token", http_code=401)
|
|
62
|
+
assert isinstance(err, ApiError)
|
|
63
|
+
assert err.http_code == 401
|
|
64
|
+
|
|
65
|
+
def test_rate_limit_error_extends_api_error(self):
|
|
66
|
+
err = RateLimitError("Too many requests", http_code=429)
|
|
67
|
+
assert isinstance(err, ApiError)
|
|
68
|
+
assert err.http_code == 429
|
|
69
|
+
|
|
70
|
+
def test_api_error_defaults(self):
|
|
71
|
+
err = ApiError("fail")
|
|
72
|
+
assert err.http_code == 0
|
|
73
|
+
assert err.response_body is None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class TestExports:
|
|
77
|
+
def test_all_classes_importable(self):
|
|
78
|
+
from mobileproxy import Client, ApiError, AuthenticationError, RateLimitError
|
|
79
|
+
assert Client is not None
|
|
80
|
+
assert ApiError is not None
|
|
81
|
+
assert AuthenticationError is not None
|
|
82
|
+
assert RateLimitError is not None
|
|
83
|
+
|
|
84
|
+
def test_version(self):
|
|
85
|
+
import mobileproxy
|
|
86
|
+
assert mobileproxy.__version__ == "1.0.0"
|