mrxsim 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.
- mrxsim-1.0.0/LICENSE +21 -0
- mrxsim-1.0.0/PKG-INFO +174 -0
- mrxsim-1.0.0/README.md +137 -0
- mrxsim-1.0.0/mrxsim/__init__.py +34 -0
- mrxsim-1.0.0/mrxsim/client.py +452 -0
- mrxsim-1.0.0/mrxsim/exceptions.py +55 -0
- mrxsim-1.0.0/mrxsim/py.typed +1 -0
- mrxsim-1.0.0/mrxsim.egg-info/PKG-INFO +174 -0
- mrxsim-1.0.0/mrxsim.egg-info/SOURCES.txt +13 -0
- mrxsim-1.0.0/mrxsim.egg-info/dependency_links.txt +1 -0
- mrxsim-1.0.0/mrxsim.egg-info/requires.txt +12 -0
- mrxsim-1.0.0/mrxsim.egg-info/top_level.txt +1 -0
- mrxsim-1.0.0/pyproject.toml +74 -0
- mrxsim-1.0.0/setup.cfg +4 -0
- mrxsim-1.0.0/tests/test_client.py +127 -0
mrxsim-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MRXSIM
|
|
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.
|
mrxsim-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mrxsim
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official Python client for MRXSIM.COM — secure SMS number purchasing & OTP retrieval
|
|
5
|
+
Author-email: MRXSIM <whomrxami@pm.me>
|
|
6
|
+
Maintainer-email: MRXSIM <whomrxami@pm.me>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Project-URL: Homepage, https://mrxsim.com
|
|
9
|
+
Project-URL: Documentation, https://mrxsim.com/docs
|
|
10
|
+
Project-URL: Repository, https://github.com/MRXSIM/MRXSIM-Universal-SMS-Automation
|
|
11
|
+
Project-URL: Bug Tracker, https://github.com/MRXSIM/MRXSIM-Universal-SMS-Automation/issues
|
|
12
|
+
Keywords: mrxsim,sms,otp,virtual-number,telegram,whatsapp,automation,api-client
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: requests<3,>=2.31.0
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
30
|
+
Requires-Dist: build>=1.2.0; extra == "dev"
|
|
31
|
+
Requires-Dist: twine>=5.0.0; extra == "dev"
|
|
32
|
+
Provides-Extra: async
|
|
33
|
+
Requires-Dist: aiohttp<4,>=3.9.0; extra == "async"
|
|
34
|
+
Provides-Extra: cli
|
|
35
|
+
Requires-Dist: colorama<1,>=0.4.6; extra == "cli"
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
# mrxsim
|
|
39
|
+
|
|
40
|
+
Official Python client for **[MRXSIM.COM](https://mrxsim.com)** — purchase virtual numbers and retrieve SMS OTPs for every catalog service (Telegram, WhatsApp, Google, Instagram, PayPal, Snapchat, Other (SMS), and more).
|
|
41
|
+
|
|
42
|
+
> Brand: **MRXSIM** · Domain: **https://mrxsim.com** · Package: **`mrxsim`**
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Security first
|
|
47
|
+
|
|
48
|
+
- **No hardcoded API keys** in library source or examples.
|
|
49
|
+
- Prefer environment variable ``MRXSIM_API_KEY``.
|
|
50
|
+
- Or load a local ``config.json`` that is **gitignored**.
|
|
51
|
+
- Never commit live keys. Revoke immediately if exposed.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Install
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install mrxsim
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
From this repository (editable):
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install -e .
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Quick start (environment variable)
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
export MRXSIM_API_KEY="mrxs_your_key_here" # Linux / macOS
|
|
73
|
+
# setx MRXSIM_API_KEY "mrxs_your_key_here" # Windows (new shell)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from mrxsim import Client
|
|
78
|
+
|
|
79
|
+
with Client(country="england", service="telegram") as client:
|
|
80
|
+
order = client.get_number()
|
|
81
|
+
print(order["phone_number"], order["id"])
|
|
82
|
+
|
|
83
|
+
sms = client.wait_for_sms(order["id"])
|
|
84
|
+
print(sms["sms_code"])
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
One-shot purchase + OTP:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from mrxsim import Client
|
|
91
|
+
|
|
92
|
+
with Client(country="egypt", service="whatsapp") as client:
|
|
93
|
+
result = client.buy_and_wait()
|
|
94
|
+
print(result["phone_number"], result["sms_code"])
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Quick start (config file)
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
cp config.example.json config.json
|
|
103
|
+
# edit config.json → set api_key, country, service
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from mrxsim import Client
|
|
108
|
+
|
|
109
|
+
client = Client.from_config("config.json")
|
|
110
|
+
order = client.get_number()
|
|
111
|
+
sms = client.wait_for_sms(order["id"])
|
|
112
|
+
client.close()
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
``MRXSIM_API_KEY`` overrides ``api_key`` in the file when set.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Get your API key
|
|
120
|
+
|
|
121
|
+
1. Open [https://mrxsim.com](https://mrxsim.com) and create an account.
|
|
122
|
+
2. Top up with **Crypto** (USDT / supported networks).
|
|
123
|
+
3. **Profile → Get API KEY** (shown once at create/regenerate).
|
|
124
|
+
4. Export ``MRXSIM_API_KEY`` or paste into gitignored ``config.json``.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## API surface
|
|
129
|
+
|
|
130
|
+
| Method | Endpoint | Client method |
|
|
131
|
+
|--------|----------|---------------|
|
|
132
|
+
| `POST` | `/api/v1/get_number` | `Client.get_number()` |
|
|
133
|
+
| `GET` | `/api/v1/get_sms?order_id=…` | `Client.get_sms()` / `wait_for_sms()` |
|
|
134
|
+
|
|
135
|
+
Header on every call:
|
|
136
|
+
|
|
137
|
+
```http
|
|
138
|
+
X-API-Key: YOUR_KEY
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Docs: [https://mrxsim.com/docs](https://mrxsim.com/docs)
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Examples
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
export MRXSIM_API_KEY="…"
|
|
149
|
+
python examples/buy_number_example.py
|
|
150
|
+
python examples/get_otp_example.py <order_id>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Development
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
python -m venv .venv
|
|
159
|
+
# Windows: .venv\Scripts\activate
|
|
160
|
+
source .venv/bin/activate
|
|
161
|
+
pip install -e ".[dev]"
|
|
162
|
+
pytest
|
|
163
|
+
python -m build
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Support
|
|
169
|
+
|
|
170
|
+
- Site: [https://mrxsim.com](https://mrxsim.com)
|
|
171
|
+
- Docs: [https://mrxsim.com/docs](https://mrxsim.com/docs)
|
|
172
|
+
- Issues: [GitHub](https://github.com/MRXSIM/MRXSIM-Universal-SMS-Automation/issues)
|
|
173
|
+
|
|
174
|
+
© MRXSIM · Secure SMS Infrastructure
|
mrxsim-1.0.0/README.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# mrxsim
|
|
2
|
+
|
|
3
|
+
Official Python client for **[MRXSIM.COM](https://mrxsim.com)** — purchase virtual numbers and retrieve SMS OTPs for every catalog service (Telegram, WhatsApp, Google, Instagram, PayPal, Snapchat, Other (SMS), and more).
|
|
4
|
+
|
|
5
|
+
> Brand: **MRXSIM** · Domain: **https://mrxsim.com** · Package: **`mrxsim`**
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Security first
|
|
10
|
+
|
|
11
|
+
- **No hardcoded API keys** in library source or examples.
|
|
12
|
+
- Prefer environment variable ``MRXSIM_API_KEY``.
|
|
13
|
+
- Or load a local ``config.json`` that is **gitignored**.
|
|
14
|
+
- Never commit live keys. Revoke immediately if exposed.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install mrxsim
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
From this repository (editable):
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install -e .
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Quick start (environment variable)
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
export MRXSIM_API_KEY="mrxs_your_key_here" # Linux / macOS
|
|
36
|
+
# setx MRXSIM_API_KEY "mrxs_your_key_here" # Windows (new shell)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from mrxsim import Client
|
|
41
|
+
|
|
42
|
+
with Client(country="england", service="telegram") as client:
|
|
43
|
+
order = client.get_number()
|
|
44
|
+
print(order["phone_number"], order["id"])
|
|
45
|
+
|
|
46
|
+
sms = client.wait_for_sms(order["id"])
|
|
47
|
+
print(sms["sms_code"])
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
One-shot purchase + OTP:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from mrxsim import Client
|
|
54
|
+
|
|
55
|
+
with Client(country="egypt", service="whatsapp") as client:
|
|
56
|
+
result = client.buy_and_wait()
|
|
57
|
+
print(result["phone_number"], result["sms_code"])
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Quick start (config file)
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
cp config.example.json config.json
|
|
66
|
+
# edit config.json → set api_key, country, service
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from mrxsim import Client
|
|
71
|
+
|
|
72
|
+
client = Client.from_config("config.json")
|
|
73
|
+
order = client.get_number()
|
|
74
|
+
sms = client.wait_for_sms(order["id"])
|
|
75
|
+
client.close()
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
``MRXSIM_API_KEY`` overrides ``api_key`` in the file when set.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Get your API key
|
|
83
|
+
|
|
84
|
+
1. Open [https://mrxsim.com](https://mrxsim.com) and create an account.
|
|
85
|
+
2. Top up with **Crypto** (USDT / supported networks).
|
|
86
|
+
3. **Profile → Get API KEY** (shown once at create/regenerate).
|
|
87
|
+
4. Export ``MRXSIM_API_KEY`` or paste into gitignored ``config.json``.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## API surface
|
|
92
|
+
|
|
93
|
+
| Method | Endpoint | Client method |
|
|
94
|
+
|--------|----------|---------------|
|
|
95
|
+
| `POST` | `/api/v1/get_number` | `Client.get_number()` |
|
|
96
|
+
| `GET` | `/api/v1/get_sms?order_id=…` | `Client.get_sms()` / `wait_for_sms()` |
|
|
97
|
+
|
|
98
|
+
Header on every call:
|
|
99
|
+
|
|
100
|
+
```http
|
|
101
|
+
X-API-Key: YOUR_KEY
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Docs: [https://mrxsim.com/docs](https://mrxsim.com/docs)
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Examples
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
export MRXSIM_API_KEY="…"
|
|
112
|
+
python examples/buy_number_example.py
|
|
113
|
+
python examples/get_otp_example.py <order_id>
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Development
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
python -m venv .venv
|
|
122
|
+
# Windows: .venv\Scripts\activate
|
|
123
|
+
source .venv/bin/activate
|
|
124
|
+
pip install -e ".[dev]"
|
|
125
|
+
pytest
|
|
126
|
+
python -m build
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Support
|
|
132
|
+
|
|
133
|
+
- Site: [https://mrxsim.com](https://mrxsim.com)
|
|
134
|
+
- Docs: [https://mrxsim.com/docs](https://mrxsim.com/docs)
|
|
135
|
+
- Issues: [GitHub](https://github.com/MRXSIM/MRXSIM-Universal-SMS-Automation/issues)
|
|
136
|
+
|
|
137
|
+
© MRXSIM · Secure SMS Infrastructure
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MRXSIM — Official Python client for https://mrxsim.com
|
|
3
|
+
|
|
4
|
+
Secure SMS number purchasing and OTP retrieval for every MRXSIM catalog service.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from mrxsim.client import Client, MrxsimClient, load_config
|
|
10
|
+
from mrxsim.exceptions import (
|
|
11
|
+
MrxsimAPIError,
|
|
12
|
+
MrxsimAuthError,
|
|
13
|
+
MrxsimConfigError,
|
|
14
|
+
MrxsimError,
|
|
15
|
+
MrxsimOrderError,
|
|
16
|
+
MrxsimRateLimitError,
|
|
17
|
+
MrxsimTimeoutError,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"Client",
|
|
22
|
+
"MrxsimClient",
|
|
23
|
+
"load_config",
|
|
24
|
+
"MrxsimError",
|
|
25
|
+
"MrxsimConfigError",
|
|
26
|
+
"MrxsimAuthError",
|
|
27
|
+
"MrxsimAPIError",
|
|
28
|
+
"MrxsimTimeoutError",
|
|
29
|
+
"MrxsimRateLimitError",
|
|
30
|
+
"MrxsimOrderError",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
__version__ = "1.0.0"
|
|
34
|
+
__author__ = "MRXSIM"
|
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MRXSIM synchronous HTTP client.
|
|
3
|
+
|
|
4
|
+
Authentication is never hardcoded. Provide credentials via:
|
|
5
|
+
|
|
6
|
+
* Environment variable ``MRXSIM_API_KEY``
|
|
7
|
+
* Explicit ``api_key=`` constructor argument
|
|
8
|
+
* A local config file (e.g. ``config.json``) loaded with :meth:`Client.from_config`
|
|
9
|
+
|
|
10
|
+
© MRXSIM · https://mrxsim.com
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import time
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any, Mapping
|
|
20
|
+
from urllib.parse import urljoin
|
|
21
|
+
|
|
22
|
+
import requests
|
|
23
|
+
|
|
24
|
+
from mrxsim.exceptions import (
|
|
25
|
+
MrxsimAPIError,
|
|
26
|
+
MrxsimAuthError,
|
|
27
|
+
MrxsimConfigError,
|
|
28
|
+
MrxsimError,
|
|
29
|
+
MrxsimOrderError,
|
|
30
|
+
MrxsimRateLimitError,
|
|
31
|
+
MrxsimTimeoutError,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
DEFAULT_BASE_URL = "https://mrxsim.com"
|
|
35
|
+
DEFAULT_USER_AGENT = "mrxsim-python/1.0.0 (+https://mrxsim.com)"
|
|
36
|
+
ENV_API_KEY = "MRXSIM_API_KEY"
|
|
37
|
+
ENV_BASE_URL = "MRXSIM_BASE_URL"
|
|
38
|
+
|
|
39
|
+
_TERMINAL_FAIL = frozenset({"CANCELED", "CANCELLED", "EXPIRED", "TIMEOUT", "FAILED"})
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _require_nonempty(value: str | None, field: str) -> str:
|
|
43
|
+
text = (value or "").strip()
|
|
44
|
+
if not text:
|
|
45
|
+
raise MrxsimConfigError(f"Missing required configuration: {field}")
|
|
46
|
+
return text
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _normalize_base_url(url: str) -> str:
|
|
50
|
+
return url.strip().rstrip("/")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def load_config(path: str | Path) -> dict[str, Any]:
|
|
54
|
+
"""
|
|
55
|
+
Load a JSON configuration file.
|
|
56
|
+
|
|
57
|
+
The file must be a JSON object. ``api_key`` may be omitted if
|
|
58
|
+
``MRXSIM_API_KEY`` is set in the environment.
|
|
59
|
+
|
|
60
|
+
Parameters
|
|
61
|
+
----------
|
|
62
|
+
path:
|
|
63
|
+
Filesystem path to the JSON config (e.g. ``config.json``).
|
|
64
|
+
|
|
65
|
+
Returns
|
|
66
|
+
-------
|
|
67
|
+
dict[str, Any]
|
|
68
|
+
Parsed configuration mapping.
|
|
69
|
+
|
|
70
|
+
Raises
|
|
71
|
+
------
|
|
72
|
+
MrxsimConfigError
|
|
73
|
+
If the file is missing or not a valid JSON object.
|
|
74
|
+
"""
|
|
75
|
+
cfg_path = Path(path).expanduser().resolve()
|
|
76
|
+
if not cfg_path.is_file():
|
|
77
|
+
raise MrxsimConfigError(
|
|
78
|
+
f"Config file not found: {cfg_path}. "
|
|
79
|
+
"Copy config.example.json → config.json and set your API key, "
|
|
80
|
+
"or export MRXSIM_API_KEY."
|
|
81
|
+
)
|
|
82
|
+
try:
|
|
83
|
+
with cfg_path.open("r", encoding="utf-8") as fh:
|
|
84
|
+
data = json.load(fh)
|
|
85
|
+
except json.JSONDecodeError as exc:
|
|
86
|
+
raise MrxsimConfigError(f"Invalid JSON in {cfg_path}: {exc}") from exc
|
|
87
|
+
if not isinstance(data, dict):
|
|
88
|
+
raise MrxsimConfigError(f"Config file must be a JSON object: {cfg_path}")
|
|
89
|
+
return data
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class Client:
|
|
93
|
+
"""
|
|
94
|
+
Production client for the MRXSIM.COM public SMS API.
|
|
95
|
+
|
|
96
|
+
Supports every catalog service (Telegram, WhatsApp, Google, Instagram,
|
|
97
|
+
PayPal, Snapchat, Other (SMS), and more).
|
|
98
|
+
|
|
99
|
+
Parameters
|
|
100
|
+
----------
|
|
101
|
+
api_key:
|
|
102
|
+
MRXSIM API key. If omitted, read from ``MRXSIM_API_KEY``.
|
|
103
|
+
base_url:
|
|
104
|
+
API origin. Defaults to ``https://mrxsim.com`` or ``MRXSIM_BASE_URL``.
|
|
105
|
+
country:
|
|
106
|
+
Default catalog country code (e.g. ``england``, ``usa``, ``egypt``).
|
|
107
|
+
service:
|
|
108
|
+
Default service code (e.g. ``telegram``, ``whatsapp``, ``other``).
|
|
109
|
+
operator:
|
|
110
|
+
Default operator stack (``any`` or a specific stack id).
|
|
111
|
+
timeout:
|
|
112
|
+
Default HTTP request timeout in seconds.
|
|
113
|
+
poll_interval:
|
|
114
|
+
Default seconds between OTP polls.
|
|
115
|
+
poll_timeout:
|
|
116
|
+
Default maximum seconds to wait for an OTP.
|
|
117
|
+
session:
|
|
118
|
+
Optional pre-configured :class:`requests.Session`.
|
|
119
|
+
|
|
120
|
+
Raises
|
|
121
|
+
------
|
|
122
|
+
MrxsimConfigError
|
|
123
|
+
If no API key can be resolved.
|
|
124
|
+
MrxsimAuthError
|
|
125
|
+
If the resolved key looks like an unreplaced placeholder.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
def __init__(
|
|
129
|
+
self,
|
|
130
|
+
api_key: str | None = None,
|
|
131
|
+
*,
|
|
132
|
+
base_url: str | None = None,
|
|
133
|
+
country: str | None = None,
|
|
134
|
+
service: str | None = None,
|
|
135
|
+
operator: str = "any",
|
|
136
|
+
timeout: float = 30.0,
|
|
137
|
+
poll_interval: float = 3.0,
|
|
138
|
+
poll_timeout: float = 600.0,
|
|
139
|
+
session: requests.Session | None = None,
|
|
140
|
+
) -> None:
|
|
141
|
+
resolved_key = (api_key or os.environ.get(ENV_API_KEY) or "").strip()
|
|
142
|
+
if not resolved_key:
|
|
143
|
+
raise MrxsimConfigError(
|
|
144
|
+
"API key required. Pass api_key=, set MRXSIM_API_KEY, "
|
|
145
|
+
"or use Client.from_config('config.json'). "
|
|
146
|
+
"Generate a key at https://mrxsim.com"
|
|
147
|
+
)
|
|
148
|
+
if resolved_key.startswith("REPLACE_") or resolved_key in {
|
|
149
|
+
"mrxs_your_key_here",
|
|
150
|
+
"YOUR_KEY",
|
|
151
|
+
"changeme",
|
|
152
|
+
}:
|
|
153
|
+
raise MrxsimAuthError(
|
|
154
|
+
"Placeholder API key detected. Set a real key from https://mrxsim.com"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
env_base = os.environ.get(ENV_BASE_URL)
|
|
158
|
+
self.base_url = _normalize_base_url(
|
|
159
|
+
base_url or env_base or DEFAULT_BASE_URL
|
|
160
|
+
)
|
|
161
|
+
self.country = (country or "").strip().lower() or None
|
|
162
|
+
self.service = (service or "").strip().lower() or None
|
|
163
|
+
self.operator = ((operator or "any").strip().lower() or "any")
|
|
164
|
+
self.timeout = max(5.0, float(timeout))
|
|
165
|
+
self.poll_interval = max(1.0, float(poll_interval))
|
|
166
|
+
self.poll_timeout = max(30.0, float(poll_timeout))
|
|
167
|
+
|
|
168
|
+
self._session = session or requests.Session()
|
|
169
|
+
self._session.headers.update(
|
|
170
|
+
{
|
|
171
|
+
"X-API-Key": resolved_key,
|
|
172
|
+
"Content-Type": "application/json",
|
|
173
|
+
"Accept": "application/json",
|
|
174
|
+
"User-Agent": DEFAULT_USER_AGENT,
|
|
175
|
+
}
|
|
176
|
+
)
|
|
177
|
+
# Never keep a public attribute that echoes the key.
|
|
178
|
+
self._api_key_fingerprint = f"{resolved_key[:6]}…{resolved_key[-4:]}" if len(resolved_key) > 12 else "***"
|
|
179
|
+
|
|
180
|
+
@classmethod
|
|
181
|
+
def from_config(
|
|
182
|
+
cls,
|
|
183
|
+
path: str | Path = "config.json",
|
|
184
|
+
*,
|
|
185
|
+
session: requests.Session | None = None,
|
|
186
|
+
) -> Client:
|
|
187
|
+
"""
|
|
188
|
+
Construct a client from a JSON config file.
|
|
189
|
+
|
|
190
|
+
Environment variable ``MRXSIM_API_KEY`` overrides ``api_key`` in the file
|
|
191
|
+
when set (prefer secrets outside the working tree).
|
|
192
|
+
|
|
193
|
+
Parameters
|
|
194
|
+
----------
|
|
195
|
+
path:
|
|
196
|
+
Path to the JSON config file.
|
|
197
|
+
session:
|
|
198
|
+
Optional shared :class:`requests.Session`.
|
|
199
|
+
"""
|
|
200
|
+
raw = load_config(path)
|
|
201
|
+
api_key = os.environ.get(ENV_API_KEY) or str(raw.get("api_key") or "")
|
|
202
|
+
return cls(
|
|
203
|
+
api_key=api_key or None,
|
|
204
|
+
base_url=str(raw.get("base_url") or DEFAULT_BASE_URL),
|
|
205
|
+
country=str(raw.get("country") or "") or None,
|
|
206
|
+
service=str(raw.get("service") or "") or None,
|
|
207
|
+
operator=str(raw.get("operator") or "any"),
|
|
208
|
+
timeout=float(raw.get("request_timeout_seconds") or 30),
|
|
209
|
+
poll_interval=float(raw.get("poll_interval_seconds") or 3),
|
|
210
|
+
poll_timeout=float(raw.get("poll_timeout_seconds") or 600),
|
|
211
|
+
session=session,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
def close(self) -> None:
|
|
215
|
+
"""Close the underlying HTTP session."""
|
|
216
|
+
self._session.close()
|
|
217
|
+
|
|
218
|
+
def __enter__(self) -> Client:
|
|
219
|
+
return self
|
|
220
|
+
|
|
221
|
+
def __exit__(self, *exc: object) -> None:
|
|
222
|
+
self.close()
|
|
223
|
+
|
|
224
|
+
def _url(self, path: str) -> str:
|
|
225
|
+
return urljoin(f"{self.base_url}/", path.lstrip("/"))
|
|
226
|
+
|
|
227
|
+
def _request(
|
|
228
|
+
self,
|
|
229
|
+
method: str,
|
|
230
|
+
path: str,
|
|
231
|
+
*,
|
|
232
|
+
json_body: Mapping[str, Any] | None = None,
|
|
233
|
+
params: Mapping[str, Any] | None = None,
|
|
234
|
+
timeout: float | None = None,
|
|
235
|
+
) -> dict[str, Any]:
|
|
236
|
+
try:
|
|
237
|
+
response = self._session.request(
|
|
238
|
+
method=method.upper(),
|
|
239
|
+
url=self._url(path),
|
|
240
|
+
json=dict(json_body) if json_body is not None else None,
|
|
241
|
+
params=dict(params) if params is not None else None,
|
|
242
|
+
timeout=timeout if timeout is not None else self.timeout,
|
|
243
|
+
)
|
|
244
|
+
except requests.Timeout as exc:
|
|
245
|
+
raise MrxsimTimeoutError(f"HTTP request timed out: {method} {path}") from exc
|
|
246
|
+
except requests.RequestException as exc:
|
|
247
|
+
raise MrxsimError(f"HTTP transport error: {exc}") from exc
|
|
248
|
+
return self._parse(response)
|
|
249
|
+
|
|
250
|
+
@staticmethod
|
|
251
|
+
def _parse(response: requests.Response) -> dict[str, Any]:
|
|
252
|
+
try:
|
|
253
|
+
body: Any = response.json()
|
|
254
|
+
except ValueError:
|
|
255
|
+
body = {"error": (response.text or "")[:300]}
|
|
256
|
+
|
|
257
|
+
if response.status_code in {401, 403}:
|
|
258
|
+
detail = body.get("detail") if isinstance(body, dict) else body
|
|
259
|
+
raise MrxsimAuthError(
|
|
260
|
+
f"Authentication failed (HTTP {response.status_code}): {detail}",
|
|
261
|
+
details=detail,
|
|
262
|
+
)
|
|
263
|
+
if response.status_code == 429:
|
|
264
|
+
detail = body.get("detail") if isinstance(body, dict) else body
|
|
265
|
+
raise MrxsimRateLimitError(
|
|
266
|
+
f"Rate limited (HTTP 429): {detail}",
|
|
267
|
+
status_code=429,
|
|
268
|
+
details=detail,
|
|
269
|
+
)
|
|
270
|
+
if response.status_code >= 400:
|
|
271
|
+
detail: Any
|
|
272
|
+
if isinstance(body, dict):
|
|
273
|
+
raw_detail = body.get("detail", body)
|
|
274
|
+
if isinstance(raw_detail, dict):
|
|
275
|
+
detail = raw_detail.get("error") or raw_detail
|
|
276
|
+
else:
|
|
277
|
+
detail = raw_detail
|
|
278
|
+
else:
|
|
279
|
+
detail = body
|
|
280
|
+
raise MrxsimAPIError(
|
|
281
|
+
f"HTTP {response.status_code}: {detail}",
|
|
282
|
+
status_code=response.status_code,
|
|
283
|
+
details=detail,
|
|
284
|
+
)
|
|
285
|
+
if not isinstance(body, dict):
|
|
286
|
+
raise MrxsimAPIError(
|
|
287
|
+
"Unexpected API response (expected JSON object)",
|
|
288
|
+
status_code=response.status_code,
|
|
289
|
+
details=body,
|
|
290
|
+
)
|
|
291
|
+
return body
|
|
292
|
+
|
|
293
|
+
def get_number(
|
|
294
|
+
self,
|
|
295
|
+
*,
|
|
296
|
+
country: str | None = None,
|
|
297
|
+
service: str | None = None,
|
|
298
|
+
operator: str | None = None,
|
|
299
|
+
) -> dict[str, Any]:
|
|
300
|
+
"""
|
|
301
|
+
Purchase a virtual number for a country/service pair.
|
|
302
|
+
|
|
303
|
+
Parameters
|
|
304
|
+
----------
|
|
305
|
+
country:
|
|
306
|
+
Catalog country. Falls back to the client default.
|
|
307
|
+
service:
|
|
308
|
+
Catalog service. Falls back to the client default.
|
|
309
|
+
operator:
|
|
310
|
+
Operator stack. Falls back to the client default (``any``).
|
|
311
|
+
|
|
312
|
+
Returns
|
|
313
|
+
-------
|
|
314
|
+
dict[str, Any]
|
|
315
|
+
Order payload including ``id``, ``phone_number``, ``price``, ``status``.
|
|
316
|
+
"""
|
|
317
|
+
resolved_country = _require_nonempty(country or self.country, "country")
|
|
318
|
+
resolved_service = _require_nonempty(service or self.service, "service")
|
|
319
|
+
resolved_operator = (
|
|
320
|
+
(operator if operator is not None else self.operator) or "any"
|
|
321
|
+
).strip().lower() or "any"
|
|
322
|
+
|
|
323
|
+
return self._request(
|
|
324
|
+
"POST",
|
|
325
|
+
"/api/v1/get_number",
|
|
326
|
+
json_body={
|
|
327
|
+
"country": resolved_country.lower(),
|
|
328
|
+
"service": resolved_service.lower(),
|
|
329
|
+
"operator": resolved_operator,
|
|
330
|
+
},
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
def get_sms(self, order_id: str) -> dict[str, Any]:
|
|
334
|
+
"""
|
|
335
|
+
Fetch the current SMS/OTP status for an order.
|
|
336
|
+
|
|
337
|
+
Parameters
|
|
338
|
+
----------
|
|
339
|
+
order_id:
|
|
340
|
+
Order identifier returned by :meth:`get_number`.
|
|
341
|
+
|
|
342
|
+
Returns
|
|
343
|
+
-------
|
|
344
|
+
dict[str, Any]
|
|
345
|
+
Status payload; may include ``sms_code`` when received.
|
|
346
|
+
"""
|
|
347
|
+
oid = _require_nonempty(order_id, "order_id")
|
|
348
|
+
return self._request(
|
|
349
|
+
"GET",
|
|
350
|
+
"/api/v1/get_sms",
|
|
351
|
+
params={"order_id": oid},
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
def wait_for_sms(
|
|
355
|
+
self,
|
|
356
|
+
order_id: str,
|
|
357
|
+
*,
|
|
358
|
+
poll_interval: float | None = None,
|
|
359
|
+
poll_timeout: float | None = None,
|
|
360
|
+
) -> dict[str, Any]:
|
|
361
|
+
"""
|
|
362
|
+
Poll until an OTP is received or the order fails / times out.
|
|
363
|
+
|
|
364
|
+
Parameters
|
|
365
|
+
----------
|
|
366
|
+
order_id:
|
|
367
|
+
Order identifier from :meth:`get_number`.
|
|
368
|
+
poll_interval:
|
|
369
|
+
Seconds between polls (default: client ``poll_interval``).
|
|
370
|
+
poll_timeout:
|
|
371
|
+
Maximum wait in seconds (default: client ``poll_timeout``).
|
|
372
|
+
|
|
373
|
+
Returns
|
|
374
|
+
-------
|
|
375
|
+
dict[str, Any]
|
|
376
|
+
Final SMS payload containing ``sms_code`` when successful.
|
|
377
|
+
|
|
378
|
+
Raises
|
|
379
|
+
------
|
|
380
|
+
MrxsimOrderError
|
|
381
|
+
If the order reaches a terminal failure status.
|
|
382
|
+
MrxsimTimeoutError
|
|
383
|
+
If no OTP arrives before ``poll_timeout``.
|
|
384
|
+
"""
|
|
385
|
+
oid = _require_nonempty(order_id, "order_id")
|
|
386
|
+
interval = max(1.0, float(poll_interval if poll_interval is not None else self.poll_interval))
|
|
387
|
+
deadline = time.monotonic() + max(
|
|
388
|
+
30.0, float(poll_timeout if poll_timeout is not None else self.poll_timeout)
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
while time.monotonic() < deadline:
|
|
392
|
+
time.sleep(interval)
|
|
393
|
+
row = self.get_sms(oid)
|
|
394
|
+
status = str(row.get("status") or "").upper()
|
|
395
|
+
code = str(row.get("sms_code") or "").strip()
|
|
396
|
+
if code or status == "RECEIVED":
|
|
397
|
+
return row
|
|
398
|
+
if status in _TERMINAL_FAIL:
|
|
399
|
+
raise MrxsimOrderError(
|
|
400
|
+
f"Order {oid} ended with status={status}",
|
|
401
|
+
status=status,
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
raise MrxsimTimeoutError(
|
|
405
|
+
f"Timed out waiting for SMS on order {oid}"
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
def buy_and_wait(
|
|
409
|
+
self,
|
|
410
|
+
*,
|
|
411
|
+
country: str | None = None,
|
|
412
|
+
service: str | None = None,
|
|
413
|
+
operator: str | None = None,
|
|
414
|
+
poll_interval: float | None = None,
|
|
415
|
+
poll_timeout: float | None = None,
|
|
416
|
+
) -> dict[str, Any]:
|
|
417
|
+
"""
|
|
418
|
+
Purchase a number and block until the OTP arrives.
|
|
419
|
+
|
|
420
|
+
Returns
|
|
421
|
+
-------
|
|
422
|
+
dict[str, Any]
|
|
423
|
+
Combined result with ``order``, ``phone_number``, ``sms_code``, and ``status``.
|
|
424
|
+
"""
|
|
425
|
+
order = self.get_number(country=country, service=service, operator=operator)
|
|
426
|
+
order_id = str(order.get("id") or "")
|
|
427
|
+
if not order_id:
|
|
428
|
+
raise MrxsimAPIError("Purchase succeeded but no order id was returned", details=order)
|
|
429
|
+
sms = self.wait_for_sms(
|
|
430
|
+
order_id,
|
|
431
|
+
poll_interval=poll_interval,
|
|
432
|
+
poll_timeout=poll_timeout,
|
|
433
|
+
)
|
|
434
|
+
return {
|
|
435
|
+
"order": order,
|
|
436
|
+
"order_id": order_id,
|
|
437
|
+
"phone_number": sms.get("phone_number") or order.get("phone_number"),
|
|
438
|
+
"sms_code": str(sms.get("sms_code") or "").strip(),
|
|
439
|
+
"status": sms.get("status") or order.get("status"),
|
|
440
|
+
"sms": sms,
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
def __repr__(self) -> str:
|
|
444
|
+
return (
|
|
445
|
+
f"Client(base_url={self.base_url!r}, "
|
|
446
|
+
f"country={self.country!r}, service={self.service!r}, "
|
|
447
|
+
f"key={self._api_key_fingerprint!r})"
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
# Backwards-compatible alias used by early automation scripts.
|
|
452
|
+
MrxsimClient = Client
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Custom exception hierarchy for the MRXSIM public API client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MrxsimError(Exception):
|
|
9
|
+
"""Base exception for all MRXSIM client errors."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, message: str, *, details: Any | None = None) -> None:
|
|
12
|
+
super().__init__(message)
|
|
13
|
+
self.message = message
|
|
14
|
+
self.details = details
|
|
15
|
+
|
|
16
|
+
def __str__(self) -> str:
|
|
17
|
+
return self.message
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MrxsimConfigError(MrxsimError):
|
|
21
|
+
"""Raised when configuration is missing, invalid, or insecure."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class MrxsimAuthError(MrxsimError):
|
|
25
|
+
"""Raised when the API key is missing, invalid, or unauthorized (HTTP 401/403)."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class MrxsimAPIError(MrxsimError):
|
|
29
|
+
"""Raised for non-success HTTP responses from the MRXSIM API."""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
message: str,
|
|
34
|
+
*,
|
|
35
|
+
status_code: int | None = None,
|
|
36
|
+
details: Any | None = None,
|
|
37
|
+
) -> None:
|
|
38
|
+
super().__init__(message, details=details)
|
|
39
|
+
self.status_code = status_code
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class MrxsimTimeoutError(MrxsimError):
|
|
43
|
+
"""Raised when an HTTP request or OTP poll exceeds the configured timeout."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class MrxsimRateLimitError(MrxsimAPIError):
|
|
47
|
+
"""Raised when the API returns HTTP 429 (rate limited)."""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class MrxsimOrderError(MrxsimError):
|
|
51
|
+
"""Raised when an order ends in a terminal failure state."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, message: str, *, status: str | None = None) -> None:
|
|
54
|
+
super().__init__(message, details={"status": status} if status else None)
|
|
55
|
+
self.status = status
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Marker for PEP 561 typed package
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mrxsim
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official Python client for MRXSIM.COM — secure SMS number purchasing & OTP retrieval
|
|
5
|
+
Author-email: MRXSIM <whomrxami@pm.me>
|
|
6
|
+
Maintainer-email: MRXSIM <whomrxami@pm.me>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Project-URL: Homepage, https://mrxsim.com
|
|
9
|
+
Project-URL: Documentation, https://mrxsim.com/docs
|
|
10
|
+
Project-URL: Repository, https://github.com/MRXSIM/MRXSIM-Universal-SMS-Automation
|
|
11
|
+
Project-URL: Bug Tracker, https://github.com/MRXSIM/MRXSIM-Universal-SMS-Automation/issues
|
|
12
|
+
Keywords: mrxsim,sms,otp,virtual-number,telegram,whatsapp,automation,api-client
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: requests<3,>=2.31.0
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
30
|
+
Requires-Dist: build>=1.2.0; extra == "dev"
|
|
31
|
+
Requires-Dist: twine>=5.0.0; extra == "dev"
|
|
32
|
+
Provides-Extra: async
|
|
33
|
+
Requires-Dist: aiohttp<4,>=3.9.0; extra == "async"
|
|
34
|
+
Provides-Extra: cli
|
|
35
|
+
Requires-Dist: colorama<1,>=0.4.6; extra == "cli"
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
# mrxsim
|
|
39
|
+
|
|
40
|
+
Official Python client for **[MRXSIM.COM](https://mrxsim.com)** — purchase virtual numbers and retrieve SMS OTPs for every catalog service (Telegram, WhatsApp, Google, Instagram, PayPal, Snapchat, Other (SMS), and more).
|
|
41
|
+
|
|
42
|
+
> Brand: **MRXSIM** · Domain: **https://mrxsim.com** · Package: **`mrxsim`**
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Security first
|
|
47
|
+
|
|
48
|
+
- **No hardcoded API keys** in library source or examples.
|
|
49
|
+
- Prefer environment variable ``MRXSIM_API_KEY``.
|
|
50
|
+
- Or load a local ``config.json`` that is **gitignored**.
|
|
51
|
+
- Never commit live keys. Revoke immediately if exposed.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Install
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install mrxsim
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
From this repository (editable):
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install -e .
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Quick start (environment variable)
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
export MRXSIM_API_KEY="mrxs_your_key_here" # Linux / macOS
|
|
73
|
+
# setx MRXSIM_API_KEY "mrxs_your_key_here" # Windows (new shell)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from mrxsim import Client
|
|
78
|
+
|
|
79
|
+
with Client(country="england", service="telegram") as client:
|
|
80
|
+
order = client.get_number()
|
|
81
|
+
print(order["phone_number"], order["id"])
|
|
82
|
+
|
|
83
|
+
sms = client.wait_for_sms(order["id"])
|
|
84
|
+
print(sms["sms_code"])
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
One-shot purchase + OTP:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from mrxsim import Client
|
|
91
|
+
|
|
92
|
+
with Client(country="egypt", service="whatsapp") as client:
|
|
93
|
+
result = client.buy_and_wait()
|
|
94
|
+
print(result["phone_number"], result["sms_code"])
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Quick start (config file)
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
cp config.example.json config.json
|
|
103
|
+
# edit config.json → set api_key, country, service
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from mrxsim import Client
|
|
108
|
+
|
|
109
|
+
client = Client.from_config("config.json")
|
|
110
|
+
order = client.get_number()
|
|
111
|
+
sms = client.wait_for_sms(order["id"])
|
|
112
|
+
client.close()
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
``MRXSIM_API_KEY`` overrides ``api_key`` in the file when set.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Get your API key
|
|
120
|
+
|
|
121
|
+
1. Open [https://mrxsim.com](https://mrxsim.com) and create an account.
|
|
122
|
+
2. Top up with **Crypto** (USDT / supported networks).
|
|
123
|
+
3. **Profile → Get API KEY** (shown once at create/regenerate).
|
|
124
|
+
4. Export ``MRXSIM_API_KEY`` or paste into gitignored ``config.json``.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## API surface
|
|
129
|
+
|
|
130
|
+
| Method | Endpoint | Client method |
|
|
131
|
+
|--------|----------|---------------|
|
|
132
|
+
| `POST` | `/api/v1/get_number` | `Client.get_number()` |
|
|
133
|
+
| `GET` | `/api/v1/get_sms?order_id=…` | `Client.get_sms()` / `wait_for_sms()` |
|
|
134
|
+
|
|
135
|
+
Header on every call:
|
|
136
|
+
|
|
137
|
+
```http
|
|
138
|
+
X-API-Key: YOUR_KEY
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Docs: [https://mrxsim.com/docs](https://mrxsim.com/docs)
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Examples
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
export MRXSIM_API_KEY="…"
|
|
149
|
+
python examples/buy_number_example.py
|
|
150
|
+
python examples/get_otp_example.py <order_id>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Development
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
python -m venv .venv
|
|
159
|
+
# Windows: .venv\Scripts\activate
|
|
160
|
+
source .venv/bin/activate
|
|
161
|
+
pip install -e ".[dev]"
|
|
162
|
+
pytest
|
|
163
|
+
python -m build
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Support
|
|
169
|
+
|
|
170
|
+
- Site: [https://mrxsim.com](https://mrxsim.com)
|
|
171
|
+
- Docs: [https://mrxsim.com/docs](https://mrxsim.com/docs)
|
|
172
|
+
- Issues: [GitHub](https://github.com/MRXSIM/MRXSIM-Universal-SMS-Automation/issues)
|
|
173
|
+
|
|
174
|
+
© MRXSIM · Secure SMS Infrastructure
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
mrxsim/__init__.py
|
|
5
|
+
mrxsim/client.py
|
|
6
|
+
mrxsim/exceptions.py
|
|
7
|
+
mrxsim/py.typed
|
|
8
|
+
mrxsim.egg-info/PKG-INFO
|
|
9
|
+
mrxsim.egg-info/SOURCES.txt
|
|
10
|
+
mrxsim.egg-info/dependency_links.txt
|
|
11
|
+
mrxsim.egg-info/requires.txt
|
|
12
|
+
mrxsim.egg-info/top_level.txt
|
|
13
|
+
tests/test_client.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mrxsim
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "mrxsim"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Official Python client for MRXSIM.COM — secure SMS number purchasing & OTP retrieval"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "MRXSIM", email = "whomrxami@pm.me" },
|
|
15
|
+
]
|
|
16
|
+
maintainers = [
|
|
17
|
+
{ name = "MRXSIM", email = "whomrxami@pm.me" },
|
|
18
|
+
]
|
|
19
|
+
keywords = [
|
|
20
|
+
"mrxsim",
|
|
21
|
+
"sms",
|
|
22
|
+
"otp",
|
|
23
|
+
"virtual-number",
|
|
24
|
+
"telegram",
|
|
25
|
+
"whatsapp",
|
|
26
|
+
"automation",
|
|
27
|
+
"api-client",
|
|
28
|
+
]
|
|
29
|
+
classifiers = [
|
|
30
|
+
"Development Status :: 5 - Production/Stable",
|
|
31
|
+
"Intended Audience :: Developers",
|
|
32
|
+
"Operating System :: OS Independent",
|
|
33
|
+
"Programming Language :: Python :: 3",
|
|
34
|
+
"Programming Language :: Python :: 3.10",
|
|
35
|
+
"Programming Language :: Python :: 3.11",
|
|
36
|
+
"Programming Language :: Python :: 3.12",
|
|
37
|
+
"Programming Language :: Python :: 3.13",
|
|
38
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
39
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
40
|
+
"Typing :: Typed",
|
|
41
|
+
]
|
|
42
|
+
dependencies = [
|
|
43
|
+
"requests>=2.31.0,<3",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[project.optional-dependencies]
|
|
47
|
+
dev = [
|
|
48
|
+
"pytest>=8.0.0",
|
|
49
|
+
"build>=1.2.0",
|
|
50
|
+
"twine>=5.0.0",
|
|
51
|
+
]
|
|
52
|
+
async = [
|
|
53
|
+
"aiohttp>=3.9.0,<4",
|
|
54
|
+
]
|
|
55
|
+
cli = [
|
|
56
|
+
"colorama>=0.4.6,<1",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
[project.urls]
|
|
60
|
+
Homepage = "https://mrxsim.com"
|
|
61
|
+
Documentation = "https://mrxsim.com/docs"
|
|
62
|
+
Repository = "https://github.com/MRXSIM/MRXSIM-Universal-SMS-Automation"
|
|
63
|
+
"Bug Tracker" = "https://github.com/MRXSIM/MRXSIM-Universal-SMS-Automation/issues"
|
|
64
|
+
|
|
65
|
+
[tool.setuptools.packages.find]
|
|
66
|
+
include = ["mrxsim*"]
|
|
67
|
+
exclude = ["tests*", "examples*"]
|
|
68
|
+
|
|
69
|
+
[tool.setuptools.package-data]
|
|
70
|
+
mrxsim = ["py.typed"]
|
|
71
|
+
|
|
72
|
+
[tool.pytest.ini_options]
|
|
73
|
+
testpaths = ["tests"]
|
|
74
|
+
pythonpath = ["."]
|
mrxsim-1.0.0/setup.cfg
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Basic unit tests for mrxsim (no live network / no real API keys)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from unittest.mock import MagicMock, patch
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from mrxsim import Client, MrxsimAuthError, MrxsimConfigError, __version__
|
|
12
|
+
from mrxsim.exceptions import MrxsimAPIError, MrxsimRateLimitError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_version() -> None:
|
|
16
|
+
assert __version__ == "1.0.0"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_client_requires_api_key(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
20
|
+
monkeypatch.delenv("MRXSIM_API_KEY", raising=False)
|
|
21
|
+
with pytest.raises(MrxsimConfigError):
|
|
22
|
+
Client()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_rejects_placeholder_key(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
26
|
+
monkeypatch.delenv("MRXSIM_API_KEY", raising=False)
|
|
27
|
+
with pytest.raises(MrxsimAuthError):
|
|
28
|
+
Client(api_key="REPLACE_WITH_YOUR_MRXSIM_API_KEY")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_from_config_file(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
32
|
+
monkeypatch.delenv("MRXSIM_API_KEY", raising=False)
|
|
33
|
+
cfg = tmp_path / "config.json"
|
|
34
|
+
cfg.write_text(
|
|
35
|
+
json.dumps(
|
|
36
|
+
{
|
|
37
|
+
"api_key": "mrxs_test_key_abcdef123456",
|
|
38
|
+
"base_url": "https://mrxsim.com",
|
|
39
|
+
"country": "england",
|
|
40
|
+
"service": "telegram",
|
|
41
|
+
"operator": "any",
|
|
42
|
+
"poll_interval_seconds": 3,
|
|
43
|
+
"poll_timeout_seconds": 600,
|
|
44
|
+
"request_timeout_seconds": 30,
|
|
45
|
+
}
|
|
46
|
+
),
|
|
47
|
+
encoding="utf-8",
|
|
48
|
+
)
|
|
49
|
+
client = Client.from_config(cfg)
|
|
50
|
+
assert client.country == "england"
|
|
51
|
+
assert client.service == "telegram"
|
|
52
|
+
assert client.base_url == "https://mrxsim.com"
|
|
53
|
+
client.close()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_env_overrides_config_key(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
57
|
+
cfg = tmp_path / "config.json"
|
|
58
|
+
cfg.write_text(
|
|
59
|
+
json.dumps(
|
|
60
|
+
{
|
|
61
|
+
"api_key": "mrxs_file_key_should_not_win",
|
|
62
|
+
"country": "usa",
|
|
63
|
+
"service": "whatsapp",
|
|
64
|
+
}
|
|
65
|
+
),
|
|
66
|
+
encoding="utf-8",
|
|
67
|
+
)
|
|
68
|
+
monkeypatch.setenv("MRXSIM_API_KEY", "mrxs_env_key_wins_abcdef")
|
|
69
|
+
client = Client.from_config(cfg)
|
|
70
|
+
# Fingerprint should reflect env key prefix, not file key.
|
|
71
|
+
assert "mrxs_e" in repr(client)
|
|
72
|
+
client.close()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_parse_rate_limit() -> None:
|
|
76
|
+
response = MagicMock()
|
|
77
|
+
response.status_code = 429
|
|
78
|
+
response.json.return_value = {"detail": {"error": "slow down"}}
|
|
79
|
+
with pytest.raises(MrxsimRateLimitError):
|
|
80
|
+
Client._parse(response)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_parse_auth_error() -> None:
|
|
84
|
+
response = MagicMock()
|
|
85
|
+
response.status_code = 401
|
|
86
|
+
response.json.return_value = {"detail": "invalid key"}
|
|
87
|
+
with pytest.raises(MrxsimAuthError):
|
|
88
|
+
Client._parse(response)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_get_number_posts_payload(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
92
|
+
monkeypatch.setenv("MRXSIM_API_KEY", "mrxs_unit_test_key_xyz")
|
|
93
|
+
client = Client(country="egypt", service="telegram")
|
|
94
|
+
mock_response = MagicMock()
|
|
95
|
+
mock_response.status_code = 200
|
|
96
|
+
mock_response.json.return_value = {
|
|
97
|
+
"id": "ord_1",
|
|
98
|
+
"phone_number": "+201000000000",
|
|
99
|
+
"price": "0.56",
|
|
100
|
+
"status": "PENDING",
|
|
101
|
+
}
|
|
102
|
+
with patch.object(client._session, "request", return_value=mock_response) as req:
|
|
103
|
+
order = client.get_number()
|
|
104
|
+
assert order["id"] == "ord_1"
|
|
105
|
+
kwargs = req.call_args.kwargs
|
|
106
|
+
assert kwargs["method"] == "POST"
|
|
107
|
+
assert kwargs["json"]["country"] == "egypt"
|
|
108
|
+
assert kwargs["json"]["service"] == "telegram"
|
|
109
|
+
client.close()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_get_number_missing_defaults(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
113
|
+
monkeypatch.setenv("MRXSIM_API_KEY", "mrxs_unit_test_key_xyz")
|
|
114
|
+
client = Client()
|
|
115
|
+
with pytest.raises(MrxsimConfigError):
|
|
116
|
+
client.get_number()
|
|
117
|
+
client.close()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_api_error_body(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
121
|
+
monkeypatch.setenv("MRXSIM_API_KEY", "mrxs_unit_test_key_xyz")
|
|
122
|
+
response = MagicMock()
|
|
123
|
+
response.status_code = 422
|
|
124
|
+
response.json.return_value = {"detail": {"error": "no stock"}}
|
|
125
|
+
with pytest.raises(MrxsimAPIError) as excinfo:
|
|
126
|
+
Client._parse(response)
|
|
127
|
+
assert excinfo.value.status_code == 422
|