smsman-client 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
sms_man/__init__.py ADDED
@@ -0,0 +1,12 @@
1
+ from sms_man.client import DEFAULT_REF, SmsMan
2
+ from sms_man.errors import SmsManError
3
+ from sms_man.types import HandlerStatusResult, PhoneNumberOrder, SmsCodeResult
4
+
5
+ __all__ = [
6
+ "DEFAULT_REF",
7
+ "SmsMan",
8
+ "SmsManError",
9
+ "HandlerStatusResult",
10
+ "PhoneNumberOrder",
11
+ "SmsCodeResult",
12
+ ]
sms_man/client.py ADDED
@@ -0,0 +1,349 @@
1
+ from typing import Any
2
+
3
+ import httpx
4
+
5
+ from sms_man.errors import SmsManError
6
+ from sms_man.types import HandlerStatusResult, PhoneNumberOrder, SmsCodeResult
7
+
8
+ DEFAULT_REF = "s_iNhkmN48Jf"
9
+
10
+
11
+ class SmsMan:
12
+ """Client for the SMS-MAN virtual number and SMS activation API.
13
+
14
+ Args:
15
+ token: User API token for SMS-MAN.
16
+ base_url: Base URL for SMS-MAN API. Defaults to "https://api.sms-man.com".
17
+ timeout: HTTP request timeout in seconds. Defaults to 30.0.
18
+
19
+ Examples:
20
+ >>> from sms_man import SmsMan
21
+ >>> client = SmsMan(token="your_api_token")
22
+ >>> balance = client.get_balance()
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ token: str,
28
+ base_url: str = "https://api.sms-man.com",
29
+ timeout: float = 30.0,
30
+ ) -> None:
31
+ if not token:
32
+ raise SmsManError("API token is required")
33
+ self.token = token
34
+ self.base_url = base_url.rstrip("/")
35
+ self.timeout = timeout
36
+ self._client = httpx.Client(base_url=self.base_url, timeout=self.timeout)
37
+
38
+ def close(self) -> None:
39
+ """Closes the underlying HTTP client session."""
40
+ self._client.close()
41
+
42
+ def __enter__(self) -> "SmsMan":
43
+ return self
44
+
45
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
46
+ self.close()
47
+
48
+ def get_balance(self) -> float:
49
+ """Retrieves user account balance.
50
+
51
+ Returns:
52
+ Current balance as a float.
53
+
54
+ Examples:
55
+ >>> balance = client.get_balance()
56
+ >>> print(balance)
57
+ 120.0
58
+ """
59
+ data = self._call_control("get-balance")
60
+ bal = data.get("balance")
61
+ if bal is None:
62
+ raise SmsManError("Missing balance in API response", response_data=data)
63
+ try:
64
+ return float(bal)
65
+ except (ValueError, TypeError) as exc:
66
+ raise SmsManError(f"Invalid balance format: {bal}", response_data=data) from exc
67
+
68
+ def get_limits(
69
+ self,
70
+ country_id: int | None = None,
71
+ application_id: int | None = None,
72
+ ) -> Any:
73
+ """Retrieves available number limits and counts.
74
+
75
+ Args:
76
+ country_id: Optional country identifier.
77
+ application_id: Optional service identifier.
78
+
79
+ Returns:
80
+ Limits response dictionary.
81
+
82
+ Examples:
83
+ >>> limits = client.get_limits(country_id=1, application_id=1)
84
+ """
85
+ params: dict[str, Any] = {}
86
+ if country_id is not None:
87
+ params["country_id"] = country_id
88
+ if application_id is not None:
89
+ params["application_id"] = application_id
90
+ return self._call_control("limits", params)
91
+
92
+ def get_prices(self, country_id: int | None = None) -> Any:
93
+ """Retrieves service pricing catalog.
94
+
95
+ Args:
96
+ country_id: Optional country identifier.
97
+
98
+ Returns:
99
+ Pricing dictionary.
100
+
101
+ Examples:
102
+ >>> prices = client.get_prices(country_id=1)
103
+ """
104
+ params: dict[str, Any] = {}
105
+ if country_id is not None:
106
+ params["country_id"] = country_id
107
+ return self._call_control("get-prices", params)
108
+
109
+ def get_countries(self) -> Any:
110
+ """Retrieves list of supported countries.
111
+
112
+ Returns:
113
+ Countries dictionary.
114
+
115
+ Examples:
116
+ >>> countries = client.get_countries()
117
+ """
118
+ return self._call_control("countries")
119
+
120
+ def get_applications(self) -> Any:
121
+ """Retrieves list of supported applications and services.
122
+
123
+ Returns:
124
+ Applications dictionary.
125
+
126
+ Examples:
127
+ >>> apps = client.get_applications()
128
+ """
129
+ return self._call_control("applications")
130
+
131
+ def get_number(self, country_id: int, application_id: int) -> PhoneNumberOrder:
132
+ """Orders a disposable virtual phone number.
133
+
134
+ Args:
135
+ country_id: Country identifier (or 0 for random).
136
+ application_id: Service identifier.
137
+
138
+ Returns:
139
+ PhoneNumberOrder with requestId and number string.
140
+
141
+ Examples:
142
+ >>> order = client.get_number(country_id=1, application_id=1)
143
+ >>> print(order.number)
144
+ """
145
+ data = self._call_control(
146
+ "get-number",
147
+ {
148
+ "country_id": country_id,
149
+ "application_id": application_id,
150
+ "ref": DEFAULT_REF,
151
+ },
152
+ )
153
+ req_id = data.get("request_id")
154
+ number = data.get("number_p") or data.get("number")
155
+ if not req_id or not number:
156
+ raise SmsManError("Incomplete number data returned by API", response_data=data)
157
+ return PhoneNumberOrder(request_id=int(req_id), number=str(number))
158
+
159
+ def get_sms(self, request_id: int) -> SmsCodeResult:
160
+ """Retrieves the received SMS code for an active order.
161
+
162
+ Args:
163
+ request_id: Order request ID.
164
+
165
+ Returns:
166
+ SmsCodeResult object.
167
+
168
+ Examples:
169
+ >>> res = client.get_sms(12345)
170
+ >>> print(res.sms_code)
171
+ """
172
+ data = self._call_control("get-sms", {"request_id": request_id})
173
+ return SmsCodeResult(
174
+ sms_code=data.get("sms_code"),
175
+ error=data.get("error_msg"),
176
+ )
177
+
178
+ def set_status(self, request_id: int, status: str) -> bool:
179
+ """Sets status of an active number order (e.g. 'ready', 'reject').
180
+
181
+ Args:
182
+ request_id: Order request ID.
183
+ status: Target status string.
184
+
185
+ Returns:
186
+ True if status updated.
187
+
188
+ Examples:
189
+ >>> client.set_status(12345, "reject")
190
+ """
191
+ self._call_control("set-status", {"request_id": request_id, "status": status})
192
+ return True
193
+
194
+ def handler_get_balance(self) -> float:
195
+ """Retrieves account balance via SMS-Activate compatible handler endpoint.
196
+
197
+ Returns:
198
+ Account balance.
199
+
200
+ Examples:
201
+ >>> balance = client.handler_get_balance()
202
+ """
203
+ text = self._call_handler("getBalance")
204
+ if not text.startswith("ACCESS_BALANCE:"):
205
+ raise SmsManError(f"Unexpected balance response: {text}", code=text)
206
+ try:
207
+ return float(text[len("ACCESS_BALANCE:") :])
208
+ except ValueError as exc:
209
+ raise SmsManError(f"Invalid balance format: {text}", code=text) from exc
210
+
211
+ def handler_get_number(
212
+ self,
213
+ country: int | str,
214
+ service: str,
215
+ ) -> PhoneNumberOrder:
216
+ """Orders a number via the handler endpoint.
217
+
218
+ Args:
219
+ country: Country identifier.
220
+ service: Service code (e.g. 'tg', 'vk').
221
+
222
+ Returns:
223
+ PhoneNumberOrder with request ID and phone number.
224
+
225
+ Examples:
226
+ >>> order = client.handler_get_number(1, "tg")
227
+ """
228
+ text = self._call_handler(
229
+ "getNumber",
230
+ {
231
+ "country": country,
232
+ "service": service,
233
+ "ref": DEFAULT_REF,
234
+ },
235
+ )
236
+ if not text.startswith("ACCESS_NUMBER:"):
237
+ raise SmsManError(f"Failed to order number: {text}", code=text)
238
+ parts = text.split(":")
239
+ if len(parts) < 3:
240
+ raise SmsManError(f"Malformed ACCESS_NUMBER response: {text}", code=text)
241
+ return PhoneNumberOrder(request_id=int(parts[1]), number=parts[2])
242
+
243
+ def handler_get_status(self, order_id: int | str) -> HandlerStatusResult:
244
+ """Checks status and verification code via the handler endpoint.
245
+
246
+ Args:
247
+ order_id: Order ID.
248
+
249
+ Returns:
250
+ HandlerStatusResult object.
251
+
252
+ Examples:
253
+ >>> status = client.handler_get_status(12345)
254
+ """
255
+ text = self._call_handler("getStatus", {"id": order_id})
256
+ if text.startswith("STATUS_OK:"):
257
+ return HandlerStatusResult(status="STATUS_OK", code=text[len("STATUS_OK:") :])
258
+ return HandlerStatusResult(status=text)
259
+
260
+ def handler_set_status(self, order_id: int | str, status: int) -> str:
261
+ """Sets status via the handler endpoint.
262
+
263
+ Args:
264
+ order_id: Order ID.
265
+ status: Status code.
266
+
267
+ Returns:
268
+ Status text.
269
+
270
+ Examples:
271
+ >>> client.handler_set_status(12345, 1)
272
+ """
273
+ return self._call_handler("setStatus", {"id": order_id, "status": status})
274
+
275
+ def handler_get_prices(self, country: int | str | None = None) -> Any:
276
+ """Retrieves price catalog via the handler endpoint.
277
+
278
+ Args:
279
+ country: Optional country ID.
280
+
281
+ Returns:
282
+ Prices dictionary or response text.
283
+
284
+ Examples:
285
+ >>> prices = client.handler_get_prices(1)
286
+ """
287
+ params: dict[str, Any] = {}
288
+ if country is not None:
289
+ params["country"] = country
290
+ text = self._call_handler("getPrices", params)
291
+ import json
292
+
293
+ try:
294
+ return json.loads(text)
295
+ except Exception:
296
+ return text
297
+
298
+ def _call_control(self, endpoint: str, params: dict[str, Any] | None = None) -> Any:
299
+ query: dict[str, Any] = {"token": self.token}
300
+ if params:
301
+ query.update(params)
302
+
303
+ try:
304
+ resp = self._client.get(f"/control/{endpoint}", params=query)
305
+ except httpx.RequestError as exc:
306
+ raise SmsManError(f"HTTP request error: {exc}") from exc
307
+
308
+ try:
309
+ data = resp.json()
310
+ except Exception as exc:
311
+ raise SmsManError(f"Invalid JSON: {resp.text}") from exc
312
+
313
+ if isinstance(data, dict) and data.get("success") is False:
314
+ code = data.get("error_code")
315
+ msg = data.get("error_msg") or f"SMS-MAN error: {code}"
316
+ raise SmsManError(msg, code=code, response_data=data)
317
+ return data
318
+
319
+ def _call_handler(self, action: str, params: dict[str, Any] | None = None) -> str:
320
+ query: dict[str, Any] = {
321
+ "action": action,
322
+ "api_key": self.token,
323
+ }
324
+ if params:
325
+ query.update(params)
326
+
327
+ try:
328
+ resp = self._client.get("/stubs/handler_api.php", params=query)
329
+ except httpx.RequestError as exc:
330
+ raise SmsManError(f"HTTP request error: {exc}") from exc
331
+
332
+ text = resp.text.strip()
333
+ error_codes = {
334
+ "BAD_KEY",
335
+ "NO_KEY",
336
+ "ERROR_SQL",
337
+ "BAD_ACTION",
338
+ "BAD_SERVICE",
339
+ "NO_ACTIVATION",
340
+ "BAD_STATUS",
341
+ "NO_BALANCE",
342
+ "NO_NUMBERS",
343
+ "WRONG_SERVICE",
344
+ }
345
+ for err in error_codes:
346
+ if text == err or text.startswith(f"{err}:"):
347
+ raise SmsManError(f"API error: {text}", code=text)
348
+
349
+ return text
sms_man/errors.py ADDED
@@ -0,0 +1,20 @@
1
+ from typing import Any
2
+
3
+
4
+ class SmsManError(Exception):
5
+ """Exception raised by the SMS-MAN API client.
6
+
7
+ Attributes:
8
+ code: Error code string (e.g. wrong_token, no_numbers, BAD_KEY).
9
+ response_data: Raw response payload.
10
+ """
11
+
12
+ def __init__(
13
+ self,
14
+ message: str,
15
+ code: str | None = None,
16
+ response_data: Any = None,
17
+ ) -> None:
18
+ super().__init__(message)
19
+ self.code = code
20
+ self.response_data = response_data
sms_man/types.py ADDED
@@ -0,0 +1,40 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class PhoneNumberOrder:
6
+ """Ordered virtual phone number details.
7
+
8
+ Attributes:
9
+ request_id: Order request ID.
10
+ number: Assigned phone number string.
11
+ """
12
+
13
+ request_id: int
14
+ number: str
15
+
16
+
17
+ @dataclass
18
+ class SmsCodeResult:
19
+ """SMS verification code result.
20
+
21
+ Attributes:
22
+ sms_code: Verification code if received.
23
+ error: Error message string if code not yet arrived.
24
+ """
25
+
26
+ sms_code: str | None = None
27
+ error: str | None = None
28
+
29
+
30
+ @dataclass
31
+ class HandlerStatusResult:
32
+ """Compatible handler status response.
33
+
34
+ Attributes:
35
+ status: Status code string (e.g. STATUS_OK, STATUS_WAIT_CODE).
36
+ code: Verification code if status is STATUS_OK.
37
+ """
38
+
39
+ status: str
40
+ code: str | None = None
@@ -0,0 +1,202 @@
1
+ Metadata-Version: 2.5
2
+ Name: smsman-client
3
+ Version: 0.2.0
4
+ Summary: Python client for the SMS-MAN virtual number and SMS activation API
5
+ Project-URL: Repository, https://github.com/TysikGG/api-kit
6
+ Project-URL: Documentation, https://sms-man.com/site/api
7
+ License-Expression: ISC
8
+ License-File: LICENSE
9
+ Requires-Python: >=3.10
10
+ Requires-Dist: httpx>=0.27
11
+ Description-Content-Type: text/markdown
12
+
13
+ # smsman-client
14
+
15
+ [![PyPI version](https://img.shields.io/pypi/v/smsman-client.svg)](https://pypi.org/project/smsman-client/)
16
+ [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](LICENSE)
17
+ [![Python Version](https://img.shields.io/pypi/pyversions/smsman-client.svg)](https://pypi.org/project/smsman-client/)
18
+
19
+ Modern, fully typed Python client library for the [SMS-MAN](https://sms-man.com/site/api) virtual phone numbers and SMS verification API.
20
+
21
+ Official API documentation: <https://sms-man.com/site/api>
22
+
23
+ ---
24
+
25
+ ## Features
26
+
27
+ - 📱 **Dual Protocol Support** — Support for both the native SMS-MAN Control API (`/control/`) and the SMS-Activate compatible handler protocol (`/stubs/handler_api.php`).
28
+ - 🌍 **Global Coverage** — Rent disposable virtual phone numbers across 300+ countries and hundreds of applications.
29
+ - 🐍 **Modern Pythonic Design** — Type annotations, dataclass models (`PhoneNumberOrder`, `SmsCodeResult`), context manager support (`with SmsMan(...) as client:`), and custom exceptions.
30
+ - 🔄 **Full Activation Lifecycle** — Rent numbers, check status, fetch SMS codes, or reject/cancel.
31
+
32
+ ---
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ pip install smsman-client
38
+ ```
39
+
40
+ Requires Python 3.10 or higher.
41
+
42
+ ---
43
+
44
+ ## Quick Start (Safe Check)
45
+
46
+ The quick check only inspects your account balance. It does not order numbers or consume funds:
47
+
48
+ ```python
49
+ import os
50
+ from sms_man import SmsMan
51
+
52
+ with SmsMan(api_key=os.environ["SMSMAN_API_KEY"]) as client:
53
+ balance = client.get_balance()
54
+ print(f"Current SMS-MAN balance: ${balance:.2f} USD")
55
+ ```
56
+
57
+ ---
58
+
59
+ ## Usage Guide
60
+
61
+ ### 1. Renting a Number and Receiving an SMS Code (Control API)
62
+
63
+ Full activation lifecycle using the native Control API:
64
+
65
+ ```python
66
+ import time
67
+ from sms_man import SmsMan, SmsManError
68
+
69
+ with SmsMan(api_key="your_api_key") as client:
70
+ # 1. Order a number for Telegram (app 1) in USA (country 1)
71
+ order = client.get_number(country_id=1, application_id=1)
72
+ print(f"Received number: +{order.number} (Request ID: {order.request_id})")
73
+
74
+ # 2. Poll for the incoming SMS verification code
75
+ code = None
76
+ for _ in range(30):
77
+ time.sleep(3)
78
+ try:
79
+ res = client.get_sms(order.request_id)
80
+ if res.sms_code:
81
+ code = res.sms_code
82
+ break
83
+ except SmsManError as err:
84
+ if err.code == "wait_code":
85
+ continue
86
+ raise err
87
+
88
+ if code:
89
+ print(f"Verification code received: {code}")
90
+ else:
91
+ print("Timeout reached. Rejecting number to refund balance...")
92
+ client.set_status(order.request_id, "reject")
93
+ ```
94
+
95
+ ### 2. Inspecting Limits, Prices & Applications
96
+
97
+ Check available limits and application IDs before ordering:
98
+
99
+ ```python
100
+ countries = client.get_countries()
101
+ print("Countries:", countries)
102
+
103
+ apps = client.get_applications()
104
+ print("Applications:", apps)
105
+
106
+ limits = client.get_limits(country_id=1, application_id=1)
107
+ print("Available limits:", limits)
108
+ ```
109
+
110
+ ### 3. Using the SMS-Activate Protocol (`handler_api.php`)
111
+
112
+ If your existing workflow uses standard `handler_api.php`:
113
+
114
+ ```python
115
+ # 1. Check balance via handler
116
+ balance = client.handler_get_balance()
117
+ print(f"Handler balance: {balance}")
118
+
119
+ # 2. Order number via handler
120
+ activation = client.handler_get_number(service="tg", country=1)
121
+ print(f"Order ID: {activation.id}, Number: {activation.number}")
122
+
123
+ # 3. Mark ready (status 1)
124
+ client.handler_set_status(activation.id, 1)
125
+
126
+ # 4. Check status
127
+ status = client.handler_get_status(activation.id)
128
+ print(f"Status: {status.status}, Code: {status.code}")
129
+
130
+ # 5. Complete activation (status 6) or cancel (status 8)
131
+ client.handler_set_status(activation.id, 6)
132
+ ```
133
+
134
+ ---
135
+
136
+ ## API Reference
137
+
138
+ ### `SmsMan` Class
139
+
140
+ #### Constructor
141
+
142
+ ```python
143
+ SmsMan(
144
+ api_key: str,
145
+ base_url: str = "https://api.sms-man.com",
146
+ timeout: float = 30.0,
147
+ )
148
+ ```
149
+
150
+ | Parameter | Type | Default | Description |
151
+ | :--- | :--- | :--- | :--- |
152
+ | `api_key` | `str` | **Required** | Your SMS-MAN API key. |
153
+ | `base_url` | `str` | `"https://api.sms-man.com"` | Base URL of SMS-MAN API. |
154
+ | `timeout` | `float` | `30.0` | Request timeout in seconds. |
155
+
156
+ #### Control API Methods (`/control/`)
157
+
158
+ - **`get_balance() -> float`** — Returns current balance in USD.
159
+ - **`get_limits(country_id=None, application_id=None) -> Any`** — Checks number availability and limits.
160
+ - **`get_prices(country_id=None) -> Any`** — Returns pricing for services and countries.
161
+ - **`get_countries() -> Any`** — Lists all supported countries with IDs.
162
+ - **`get_applications() -> Any`** — Lists all supported applications with IDs.
163
+ - **`get_number(country_id: int, application_id: int) -> PhoneNumberOrder`** — Orders a number for the application.
164
+ - **`get_sms(request_id: int) -> SmsCodeResult`** — Checks for received SMS verification code.
165
+ - **`set_status(request_id: int, status: str) -> bool`** — Sets status (`'ready'` or `'reject'`).
166
+
167
+ #### Handler API Methods (`/stubs/handler_api.php`)
168
+
169
+ - **`handler_get_balance() -> float`** — Returns balance via handler API.
170
+ - **`handler_get_number(service, country=None) -> HandlerNumberResult`** — Orders number (`ACCESS_NUMBER`).
171
+ - **`handler_get_status(order_id: int | str) -> HandlerStatusResult`** — Retrieves status and code.
172
+ - **`handler_set_status(order_id: int | str, status: int) -> str`** — Updates activation state (1 = ready, 6 = complete, 8 = cancel).
173
+ - **`handler_get_prices(country=None) -> Any`** — Retrieves prices via handler API.
174
+
175
+ ---
176
+
177
+ ## Error Handling
178
+
179
+ When an API error occurs, `SmsManError` is raised:
180
+
181
+ ```python
182
+ from sms_man import SmsMan, SmsManError
183
+
184
+ try:
185
+ with SmsMan(api_key="invalid_key") as client:
186
+ client.get_balance()
187
+ except SmsManError as exc:
188
+ print(f"SMS-MAN Error: {exc.message} (Code: {exc.code})")
189
+ ```
190
+
191
+ Common error codes:
192
+ - `no_numbers` — No available phone numbers for selected application/country.
193
+ - `balance_empty` — Insufficient funds on account.
194
+ - `wrong_token` — Invalid API key.
195
+ - `wait_code` — SMS code has not arrived yet.
196
+ - `WRONG_ACTIVATION_ID` — Order ID does not exist or expired.
197
+
198
+ ---
199
+
200
+ ## License
201
+
202
+ ISC
@@ -0,0 +1,8 @@
1
+ sms_man/__init__.py,sha256=_lvCfh5od0lIgzhoug-pelEwrMupkv9einDpnogIu5g,304
2
+ sms_man/client.py,sha256=B-idPCShumSh18YWzZ9AS1lKrmh9PiuEEcEuuvZ10aI,10814
3
+ sms_man/errors.py,sha256=WCoBTu6NzWL-SXHlKrgDgQT0O4KuCwfGRM66QtH-v1Q,489
4
+ sms_man/types.py,sha256=3fQYrLBqrzWzVVO3_7nC7EAGZwNDhZT9g3uP2WvCHlc,810
5
+ smsman_client-0.2.0.dist-info/METADATA,sha256=IN3ASgK8DYrgJFHlRh5iRvk8yBr9TG8Nt4C_FllKucc,6488
6
+ smsman_client-0.2.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
7
+ smsman_client-0.2.0.dist-info/licenses/LICENSE,sha256=8S0uYmn_aeljwFudfBoV91HcIo79yab9s0yhMVsOHJ0,751
8
+ smsman_client-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 api-kit contributors
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.