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.
@@ -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
+ [![CI](https://github.com/mobileproxy/python-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/mobileproxy/python-sdk/actions/workflows/ci.yml)
40
+ [![PyPI](https://img.shields.io/pypi/v/mobileproxy-sdk)](https://pypi.org/project/mobileproxy-sdk/)
41
+ [![Python](https://img.shields.io/pypi/pyversions/mobileproxy-sdk)](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
+ [![CI](https://github.com/mobileproxy/python-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/mobileproxy/python-sdk/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/mobileproxy-sdk)](https://pypi.org/project/mobileproxy-sdk/)
5
+ [![Python](https://img.shields.io/pypi/pyversions/mobileproxy-sdk)](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
+ [![CI](https://github.com/mobileproxy/python-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/mobileproxy/python-sdk/actions/workflows/ci.yml)
40
+ [![PyPI](https://img.shields.io/pypi/v/mobileproxy-sdk)](https://pypi.org/project/mobileproxy-sdk/)
41
+ [![Python](https://img.shields.io/pypi/pyversions/mobileproxy-sdk)](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,8 @@
1
+ requests>=2.20.0
2
+
3
+ [async]
4
+ httpx>=0.24.0
5
+
6
+ [dev]
7
+ pytest>=7.0
8
+ pytest-asyncio>=0.21
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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"