diff --git a/README.md b/README.md index 443319e..106ed2d 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ Official Python SDK for [textbee.dev](https://textbee.dev), the open source SMS Zero dependencies, fully typed, works on Python 3.9+. +Writing JavaScript or TypeScript? Use the official [JavaScript SDK](https://github.com/textbee/textbee-js): `npm install @textbee/sdk`. + ## Install ```bash @@ -262,7 +264,7 @@ Textbee(api_key=api_key, base_url="https://sms.example.com/api/v1") Textbee(api_key=api_key, base_url="http://localhost:3001") ``` -If the API sits under a path prefix, give the full path, for example `https://example.com/textbee/api/v1`. Any path other than `/api/v1` is used as given, so a proxy that maps `/sms` onto the API works with `https://example.com/sms`. A value with no scheme gets `https://`. An invalid URL, or one with a username or password, raises `ValueError` instead of falling back to the hosted API, so a typo never sends your key to textbee.dev. +If the API sits under a path prefix, give the prefix: `https://example.com/textbee` reaches `https://example.com/textbee/api/v1`. The SDK adds `/api/v1` unless the path already contains it, the same rule the textbee MCP server uses for `TEXTBEE_BASE_URL`. A value with no scheme gets `https://`. An invalid URL, or one with a username or password, raises `ValueError` instead of falling back to the hosted API, so a typo never sends your key to textbee.dev. ## What is not covered yet diff --git a/src/textbee/_client.py b/src/textbee/_client.py index 71f1f9e..fb1df56 100644 --- a/src/textbee/_client.py +++ b/src/textbee/_client.py @@ -53,13 +53,13 @@ def __init__( ``x-api-key`` header. ``base_url`` points the client at a self-hosted instance instead of - ``https://api.textbee.dev/api/v1``. A bare origin such as - ``https://sms.example.com`` gets ``/api/v1`` added. A URL that contains - ``/api/v1`` is cut there, so a pasted endpoint URL still works. Any - other path is used as given, for proxies that map a prefix onto the - API. A value with no scheme gets ``https://``. An invalid URL raises - ``ValueError`` rather than falling back, so a typo never sends your key - to the public API. + ``https://api.textbee.dev/api/v1``. ``/api/v1`` is added unless the + path already contains it, so ``https://sms.example.com`` and + ``https://example.com/textbee`` both work. A URL that contains + ``/api/v1`` is cut there, so a pasted endpoint URL still works. A value + with no scheme gets ``https://``. An invalid URL raises ``ValueError`` + rather than falling back, so a typo never sends your key to the public + API. ``timeout`` is in seconds and applies to each connect and read, not to the request as a whole. @@ -340,8 +340,8 @@ def _data(payload: dict[str, Any]) -> Any: def _normalize_base_url(raw: str | None) -> str: """Resolve the API root from a base URL. Same rule as textbee-js. - A bare origin gets /api/v1, a path containing /api/v1 is cut there, and - any other path is kept as given for proxies that map a prefix onto the API. + /api/v1 is added unless the path already contains it, and a path that + contains /api/v1 is cut there. """ value = (raw or "").strip() if not value: @@ -383,13 +383,9 @@ def _normalize_base_url(raw: str | None) -> str: cut = index + 2 break - if not segments: - path = ["api", "v1"] - elif cut == -1: - path = segments - else: - # Anything after /api/v1 is a route, not part of the root, so it is dropped. - path = [*segments[: cut - 2], "api", "v1"] + # Anything after /api/v1 is a route, not part of the root, so it is dropped. + root = segments if cut == -1 else segments[: cut - 2] + path = [*root, "api", "v1"] return "/".join([origin, *(quote(segment, safe=_PATH_SAFE) for segment in path)]) diff --git a/src/textbee/_version.py b/src/textbee/_version.py index 3dc1f76..485f44a 100644 --- a/src/textbee/_version.py +++ b/src/textbee/_version.py @@ -1 +1 @@ -__version__ = "0.1.0" +__version__ = "0.1.1" diff --git a/tests/test_client.py b/tests/test_client.py index 3480ec3..0aa7428 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -461,9 +461,9 @@ def test_strips_trailing_slashes_from_a_custom_base_url(self, http: FakeUrlopen) ("https://sms.example.com/api/v1", "https://sms.example.com/api/v1"), ("https://sms.example.com/api/v1/", "https://sms.example.com/api/v1"), ("https://sms.example.com/textbee/api/v1", "https://sms.example.com/textbee/api/v1"), - # A prefix without /api/v1 is kept as given, for proxies that rewrite it. - ("https://sms.example.com/textbee", "https://sms.example.com/textbee"), - ("https://example.com/sms/", "https://example.com/sms"), + # A subpath deployment gets /api/v1 added after its prefix. + ("https://sms.example.com/textbee", "https://sms.example.com/textbee/api/v1"), + ("https://example.com/sms/", "https://example.com/sms/api/v1"), ("https://api.textbee.dev/api/v1/gateway/send-sms", "https://api.textbee.dev/api/v1"), ("api.textbee.dev", "https://api.textbee.dev/api/v1"), ("localhost:3001", "https://localhost:3001/api/v1"), @@ -472,7 +472,7 @@ def test_strips_trailing_slashes_from_a_custom_base_url(self, http: FakeUrlopen) ("http://[::1]:3001", "http://[::1]:3001/api/v1"), ("http://sms.example.com", "http://sms.example.com/api/v1"), ("https://sms.example.com/API/V1", "https://sms.example.com/api/v1"), - ("https://sms.example.com/v1", "https://sms.example.com/v1"), + ("https://sms.example.com/v1", "https://sms.example.com/v1/api/v1"), ("https://example.com/my app/api/v1", "https://example.com/my%20app/api/v1"), ("https://b" + chr(0xFC) + "cher.example", "https://xn--bcher-kva.example/api/v1"), ("HTTPS://SMS.Example.com:443/?q=1#top", "https://sms.example.com/api/v1"),