bridgexapi 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ Copyright (c) BridgeXAPI. All rights reserved.
@@ -0,0 +1,43 @@
1
+ Metadata-Version: 2.4
2
+ Name: bridgexapi
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for the BridgeXAPI SMS API
5
+ Author: BridgeXAPI
6
+ Project-URL: Homepage, https://dashboard.bridgexapi.io
7
+ Project-URL: Documentation, https://dashboard.bridgexapi.io
8
+ Project-URL: Source, https://dashboard.bridgexapi.io
9
+ Keywords: sms,api,messaging,bridgexapi,python-sdk
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Topic :: Communications
14
+ Classifier: Topic :: Internet :: WWW/HTTP
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: requests>=2.31.0
19
+ Dynamic: license-file
20
+
21
+ # BridgeXAPI Python SDK
22
+
23
+ Official Python SDK for the BridgeXAPI SMS API.
24
+
25
+ The BridgeXAPI Python SDK provides a simple and secure way to send SMS through the BridgeXAPI infrastructure using one API key, one endpoint, and route-based delivery selection.
26
+
27
+ ## Features
28
+
29
+ - Official Python SDK for BridgeXAPI SMS
30
+ - Simple sync client
31
+ - Local validation before requests are sent
32
+ - Support for route IDs `1` through `5`
33
+ - Typed route constants via `Route`
34
+ - Structured response models
35
+ - Clean Python exceptions
36
+ - Server-side integration ready
37
+
38
+ ## Installation
39
+
40
+ ### Local development
41
+
42
+ ```bash
43
+ pip install -e .
@@ -0,0 +1,23 @@
1
+ # BridgeXAPI Python SDK
2
+
3
+ Official Python SDK for the BridgeXAPI SMS API.
4
+
5
+ The BridgeXAPI Python SDK provides a simple and secure way to send SMS through the BridgeXAPI infrastructure using one API key, one endpoint, and route-based delivery selection.
6
+
7
+ ## Features
8
+
9
+ - Official Python SDK for BridgeXAPI SMS
10
+ - Simple sync client
11
+ - Local validation before requests are sent
12
+ - Support for route IDs `1` through `5`
13
+ - Typed route constants via `Route`
14
+ - Structured response models
15
+ - Clean Python exceptions
16
+ - Server-side integration ready
17
+
18
+ ## Installation
19
+
20
+ ### Local development
21
+
22
+ ```bash
23
+ pip install -e .
@@ -0,0 +1,40 @@
1
+ from .client import BridgeXAPI
2
+ from .version import __version__
3
+ from .routes import Route
4
+ from .models import SMSBatchResponse, SMSMessageStatus
5
+ from .exceptions import (
6
+ BridgeXAPIError,
7
+ ApiRequestError,
8
+ AuthenticationError,
9
+ ValidationError,
10
+ InvalidRouteError,
11
+ UnauthorizedRouteError,
12
+ InsufficientBalanceError,
13
+ VendorSendError,
14
+ InvalidNumberError,
15
+ MixedCountryBatchError,
16
+ MessageTooLongError,
17
+ UnicodeNotAllowedError,
18
+ InvalidCallerIDError,
19
+ )
20
+
21
+ __all__ = [
22
+ "BridgeXAPI",
23
+ "__version__",
24
+ "Route",
25
+ "SMSBatchResponse",
26
+ "SMSMessageStatus",
27
+ "BridgeXAPIError",
28
+ "ApiRequestError",
29
+ "AuthenticationError",
30
+ "ValidationError",
31
+ "InvalidRouteError",
32
+ "UnauthorizedRouteError",
33
+ "InsufficientBalanceError",
34
+ "VendorSendError",
35
+ "InvalidNumberError",
36
+ "MixedCountryBatchError",
37
+ "MessageTooLongError",
38
+ "UnicodeNotAllowedError",
39
+ "InvalidCallerIDError",
40
+ ]
@@ -0,0 +1,133 @@
1
+ from typing import Iterable, Optional, Any
2
+
3
+ import requests
4
+
5
+ from .exceptions import (
6
+ ApiRequestError,
7
+ AuthenticationError,
8
+ InsufficientBalanceError,
9
+ InvalidRouteError,
10
+ UnauthorizedRouteError,
11
+ ValidationError,
12
+ VendorSendError,
13
+ )
14
+ from .models import SMSBatchResponse
15
+ from .validators import (
16
+ validate_route_id,
17
+ validate_caller_id,
18
+ validate_numbers,
19
+ validate_message,
20
+ )
21
+
22
+
23
+ class BridgeXAPI:
24
+ """
25
+ Official sync client for the BridgeXAPI SMS endpoint.
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ api_key: str,
31
+ *,
32
+ base_url: str = "https://hi.bridgexapi.io",
33
+ timeout: float = 30.0,
34
+ session: Optional[requests.Session] = None,
35
+ ) -> None:
36
+ if not api_key or not isinstance(api_key, str):
37
+ raise AuthenticationError("api_key is required.")
38
+
39
+ self.api_key = api_key.strip()
40
+ self.base_url = base_url.rstrip("/")
41
+ self.timeout = timeout
42
+ self.session = session or requests.Session()
43
+
44
+ @property
45
+ def endpoint(self) -> str:
46
+ return f"{self.base_url}/api/v1/send_sms"
47
+
48
+ def send_sms(
49
+ self,
50
+ *,
51
+ route_id: int,
52
+ caller_id: str,
53
+ numbers: Iterable[str],
54
+ message: str,
55
+ ) -> SMSBatchResponse:
56
+ payload = {
57
+ "route_id": validate_route_id(route_id),
58
+ "caller_id": validate_caller_id(caller_id),
59
+ "numbers": validate_numbers(numbers),
60
+ "message": validate_message(message),
61
+ }
62
+
63
+ response = self.session.post(
64
+ self.endpoint,
65
+ headers={
66
+ "Content-Type": "application/json",
67
+ "X-API-KEY": self.api_key,
68
+ },
69
+ json=payload,
70
+ timeout=self.timeout,
71
+ )
72
+
73
+ data = self._decode_json(response)
74
+ self._raise_for_error(response.status_code, data)
75
+
76
+ return SMSBatchResponse.from_dict(data)
77
+
78
+ def send_one(
79
+ self,
80
+ *,
81
+ route_id: int,
82
+ caller_id: str,
83
+ number: str,
84
+ message: str,
85
+ ) -> SMSBatchResponse:
86
+ return self.send_sms(
87
+ route_id=route_id,
88
+ caller_id=caller_id,
89
+ numbers=[number],
90
+ message=message,
91
+ )
92
+
93
+ def _decode_json(self, response: requests.Response) -> dict[str, Any]:
94
+ try:
95
+ return response.json()
96
+ except ValueError as exc:
97
+ raise ApiRequestError(
98
+ f"BridgeXAPI returned a non-JSON response (status {response.status_code})."
99
+ ) from exc
100
+
101
+ def _raise_for_error(self, status_code: int, data: dict[str, Any]) -> None:
102
+ if 200 <= status_code < 300:
103
+ return
104
+
105
+ detail = data.get("detail")
106
+
107
+ if isinstance(detail, list):
108
+ first = detail[0] if detail else {}
109
+ msg = first.get("msg") or "Validation error."
110
+ raise ValidationError(msg)
111
+
112
+ if isinstance(detail, str):
113
+ if "Missing API Key" in detail or "Invalid or inactive API Key" in detail:
114
+ raise AuthenticationError(detail)
115
+
116
+ if "Invalid route_id" in detail:
117
+ raise InvalidRouteError(detail)
118
+
119
+ if "not authorized for the Casino route" in detail:
120
+ raise UnauthorizedRouteError(detail)
121
+
122
+ if "Insufficient balance" in detail:
123
+ raise InsufficientBalanceError(detail)
124
+
125
+ if "Vendor send failed" in detail:
126
+ raise VendorSendError(detail)
127
+
128
+ if "Pricing failed" in detail:
129
+ raise ValidationError(detail)
130
+
131
+ raise ApiRequestError(detail)
132
+
133
+ raise ApiRequestError(f"BridgeXAPI request failed with status {status_code}.")
@@ -0,0 +1,50 @@
1
+ class BridgeXAPIError(Exception):
2
+ """Base exception for all BridgeXAPI SDK errors."""
3
+
4
+
5
+ class ApiRequestError(BridgeXAPIError):
6
+ """Generic API request error."""
7
+
8
+
9
+ class AuthenticationError(ApiRequestError):
10
+ """Missing, invalid, or inactive API key."""
11
+
12
+
13
+ class ValidationError(ApiRequestError):
14
+ """Payload validation failed."""
15
+
16
+
17
+ class InvalidRouteError(ValidationError):
18
+ """Invalid route_id supplied."""
19
+
20
+
21
+ class UnauthorizedRouteError(ApiRequestError):
22
+ """User is not allowed to use the selected route."""
23
+
24
+
25
+ class InsufficientBalanceError(ApiRequestError):
26
+ """User balance is too low for this request."""
27
+
28
+
29
+ class VendorSendError(ApiRequestError):
30
+ """Vendor gateway failed to accept the SMS request."""
31
+
32
+
33
+ class InvalidNumberError(ValidationError):
34
+ """Phone number format is invalid."""
35
+
36
+
37
+ class MixedCountryBatchError(ValidationError):
38
+ """Numbers from multiple countries were provided in one request."""
39
+
40
+
41
+ class MessageTooLongError(ValidationError):
42
+ """Message exceeds the maximum allowed length."""
43
+
44
+
45
+ class UnicodeNotAllowedError(ValidationError):
46
+ """Message contains non-ASCII characters."""
47
+
48
+
49
+ class InvalidCallerIDError(ValidationError):
50
+ """Caller ID is invalid."""
@@ -0,0 +1,60 @@
1
+ from dataclasses import dataclass, asdict
2
+ from typing import Optional, List, Any
3
+
4
+
5
+ @dataclass(slots=True)
6
+ class SMSMessageStatus:
7
+ bx_message_id: Optional[str]
8
+ msisdn: str
9
+ status: str
10
+
11
+ @classmethod
12
+ def from_dict(cls, data: dict) -> "SMSMessageStatus":
13
+ return cls(
14
+ bx_message_id=data.get("bx_message_id"),
15
+ msisdn=str(data.get("msisdn", "")),
16
+ status=str(data.get("status", "")),
17
+ )
18
+
19
+ def to_dict(self) -> dict:
20
+ return asdict(self)
21
+
22
+
23
+ @dataclass(slots=True)
24
+ class SMSBatchResponse:
25
+ status: str
26
+ message: str
27
+ order_id: Optional[int]
28
+ route_id: int
29
+ count: int
30
+ messages: List[SMSMessageStatus]
31
+ cost: Optional[float]
32
+ balance_after: Optional[float]
33
+ raw: dict[str, Any]
34
+
35
+ @classmethod
36
+ def from_dict(cls, data: dict) -> "SMSBatchResponse":
37
+ return cls(
38
+ status=str(data.get("status", "")),
39
+ message=str(data.get("message", "")),
40
+ order_id=data.get("order_id"),
41
+ route_id=int(data.get("route_id", 0)),
42
+ count=int(data.get("count", 0)),
43
+ messages=[SMSMessageStatus.from_dict(x) for x in data.get("messages", [])],
44
+ cost=float(data["cost"]) if data.get("cost") is not None else None,
45
+ balance_after=float(data["balance_after"]) if data.get("balance_after") is not None else None,
46
+ raw=data,
47
+ )
48
+
49
+ def to_dict(self) -> dict:
50
+ return {
51
+ "status": self.status,
52
+ "message": self.message,
53
+ "order_id": self.order_id,
54
+ "route_id": self.route_id,
55
+ "count": self.count,
56
+ "messages": [m.to_dict() for m in self.messages],
57
+ "cost": self.cost,
58
+ "balance_after": self.balance_after,
59
+ "raw": self.raw,
60
+ }
@@ -0,0 +1,19 @@
1
+ from enum import IntEnum
2
+
3
+
4
+ class Route(IntEnum):
5
+ """
6
+ BridgeXAPI route identifiers.
7
+
8
+ Each route represents a different SMS delivery path.
9
+ Route 1 is typically the primary route, while routes 2–4
10
+ are backup or alternative routes depending on availability.
11
+
12
+ Route 5 is restricted to authorized iGaming clients.
13
+ """
14
+
15
+ ROUTE_1 = 1
16
+ ROUTE_2 = 2
17
+ ROUTE_3 = 3
18
+ ROUTE_4 = 4
19
+ CASINO = 5
@@ -0,0 +1,102 @@
1
+ import re
2
+ from typing import Iterable, List
3
+
4
+ from .exceptions import (
5
+ InvalidCallerIDError,
6
+ InvalidNumberError,
7
+ InvalidRouteError,
8
+ MessageTooLongError,
9
+ MixedCountryBatchError,
10
+ UnicodeNotAllowedError,
11
+ )
12
+
13
+ VALID_ROUTES = {1, 2, 3, 4, 5}
14
+ NUMBER_RE = re.compile(r"^\d{10,15}$")
15
+
16
+
17
+ def normalize_number(number: str) -> str:
18
+ """
19
+ Normalize user input into BridgeXAPI format.
20
+ Removes spaces, dashes, parentheses and other non-digits.
21
+ """
22
+ if not isinstance(number, str):
23
+ raise InvalidNumberError("Phone number must be a string.")
24
+ return re.sub(r"\D", "", number.strip())
25
+
26
+
27
+ def validate_route_id(route_id: int) -> int:
28
+ if route_id not in VALID_ROUTES:
29
+ raise InvalidRouteError("route_id must be between 1 and 5.")
30
+ return route_id
31
+
32
+
33
+ def validate_caller_id(caller_id: str) -> str:
34
+ if not isinstance(caller_id, str):
35
+ raise InvalidCallerIDError("caller_id must be a string.")
36
+
37
+ caller_id = caller_id.strip()
38
+
39
+ if not (3 <= len(caller_id) <= 11):
40
+ raise InvalidCallerIDError("caller_id must be between 3 and 11 characters.")
41
+
42
+ return caller_id
43
+
44
+
45
+ def validate_message(message: str) -> str:
46
+ if not isinstance(message, str):
47
+ raise MessageTooLongError("message must be a string.")
48
+
49
+ if len(message) > 158:
50
+ raise MessageTooLongError("Message must contain at most 158 characters.")
51
+
52
+ if not message.isascii():
53
+ raise UnicodeNotAllowedError("Message must contain only ASCII characters.")
54
+
55
+ return message
56
+
57
+
58
+ def validate_numbers(numbers: Iterable[str]) -> List[str]:
59
+ numbers = list(numbers)
60
+
61
+ if not numbers:
62
+ raise InvalidNumberError("numbers must contain at least one phone number.")
63
+
64
+ normalized = []
65
+
66
+ for raw in numbers:
67
+ if not isinstance(raw, str):
68
+ raise InvalidNumberError("Each number must be a string.")
69
+
70
+ if raw.strip().startswith("+"):
71
+ raise InvalidNumberError("Number must not start with '+'.")
72
+
73
+ cleaned = normalize_number(raw)
74
+
75
+ if not NUMBER_RE.fullmatch(cleaned):
76
+ raise InvalidNumberError("Each number must be 10 to 15 digits, digits only.")
77
+
78
+ normalized.append(cleaned)
79
+
80
+ validate_same_country(normalized)
81
+ return normalized
82
+
83
+
84
+ def validate_same_country(numbers: List[str]) -> None:
85
+ """
86
+ BridgeXAPI business rule:
87
+ all numbers in one request must belong to the same country.
88
+
89
+ Current SDK heuristic:
90
+ compare first 3, 2 or 1 digits against the first number.
91
+ """
92
+ if len(numbers) <= 1:
93
+ return
94
+
95
+ ref3, ref2, ref1 = numbers[0][:3], numbers[0][:2], numbers[0][:1]
96
+
97
+ for number in numbers[1:]:
98
+ cur3, cur2, cur1 = number[:3], number[:2], number[:1]
99
+ if not (cur3 == ref3 or cur2 == ref2 or cur1 == ref1):
100
+ raise MixedCountryBatchError(
101
+ "All numbers in one request must belong to the same country."
102
+ )
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,43 @@
1
+ Metadata-Version: 2.4
2
+ Name: bridgexapi
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for the BridgeXAPI SMS API
5
+ Author: BridgeXAPI
6
+ Project-URL: Homepage, https://dashboard.bridgexapi.io
7
+ Project-URL: Documentation, https://dashboard.bridgexapi.io
8
+ Project-URL: Source, https://dashboard.bridgexapi.io
9
+ Keywords: sms,api,messaging,bridgexapi,python-sdk
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Topic :: Communications
14
+ Classifier: Topic :: Internet :: WWW/HTTP
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: requests>=2.31.0
19
+ Dynamic: license-file
20
+
21
+ # BridgeXAPI Python SDK
22
+
23
+ Official Python SDK for the BridgeXAPI SMS API.
24
+
25
+ The BridgeXAPI Python SDK provides a simple and secure way to send SMS through the BridgeXAPI infrastructure using one API key, one endpoint, and route-based delivery selection.
26
+
27
+ ## Features
28
+
29
+ - Official Python SDK for BridgeXAPI SMS
30
+ - Simple sync client
31
+ - Local validation before requests are sent
32
+ - Support for route IDs `1` through `5`
33
+ - Typed route constants via `Route`
34
+ - Structured response models
35
+ - Clean Python exceptions
36
+ - Server-side integration ready
37
+
38
+ ## Installation
39
+
40
+ ### Local development
41
+
42
+ ```bash
43
+ pip install -e .
@@ -0,0 +1,15 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ bridgexapi/__init__.py
5
+ bridgexapi/client.py
6
+ bridgexapi/exceptions.py
7
+ bridgexapi/models.py
8
+ bridgexapi/routes.py
9
+ bridgexapi/validators.py
10
+ bridgexapi/version.py
11
+ bridgexapi.egg-info/PKG-INFO
12
+ bridgexapi.egg-info/SOURCES.txt
13
+ bridgexapi.egg-info/dependency_links.txt
14
+ bridgexapi.egg-info/requires.txt
15
+ bridgexapi.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ requests>=2.31.0
@@ -0,0 +1 @@
1
+ bridgexapi
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "bridgexapi"
7
+ version = "0.1.0"
8
+ description = "Official Python SDK for the BridgeXAPI SMS API"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ authors = [
12
+ { name = "BridgeXAPI" }
13
+ ]
14
+ dependencies = [
15
+ "requests>=2.31.0"
16
+ ]
17
+ keywords = ["sms", "api", "messaging", "bridgexapi", "python-sdk"]
18
+ classifiers = [
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3 :: Only",
21
+ "Operating System :: OS Independent",
22
+ "Topic :: Communications",
23
+ "Topic :: Internet :: WWW/HTTP",
24
+ ]
25
+
26
+ [project.urls]
27
+ Homepage = "https://dashboard.bridgexapi.io"
28
+ Documentation = "https://dashboard.bridgexapi.io"
29
+ Source = "https://dashboard.bridgexapi.io"
30
+
31
+ [tool.setuptools]
32
+ include-package-data = true
33
+
34
+ [tool.setuptools.packages.find]
35
+ include = ["bridgexapi*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+