langbly 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,31 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ publish:
12
+ runs-on: ubuntu-latest
13
+ environment: pypi
14
+ permissions:
15
+ id-token: write
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - name: Set up Python
20
+ uses: actions/setup-python@v5
21
+ with:
22
+ python-version: "3.12"
23
+
24
+ - name: Install build dependencies
25
+ run: pip install build
26
+
27
+ - name: Build package
28
+ run: python -m build
29
+
30
+ - name: Publish to PyPI
31
+ uses: pypa/gh-action-pypi-publish@release/v1
langbly-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Langbly
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.
langbly-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,111 @@
1
+ Metadata-Version: 2.4
2
+ Name: langbly
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for the Langbly translation API
5
+ Project-URL: Homepage, https://langbly.com
6
+ Project-URL: Documentation, https://langbly.com/docs
7
+ Project-URL: Repository, https://github.com/Langbly/langbly-python
8
+ Project-URL: Issues, https://github.com/Langbly/langbly-python/issues
9
+ Author-email: Jasper de Winter <jasper@langbly.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: api,google-translate,i18n,langbly,localization,translation
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.8
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Topic :: Software Development :: Libraries
23
+ Classifier: Topic :: Text Processing :: Linguistic
24
+ Requires-Python: >=3.8
25
+ Requires-Dist: httpx>=0.24.0
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
28
+ Requires-Dist: pytest>=7.0; extra == 'dev'
29
+ Requires-Dist: respx>=0.20; extra == 'dev'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # langbly-python
33
+
34
+ Official Python SDK for the [Langbly](https://langbly.com) translation API — a drop-in replacement for Google Translate v2.
35
+
36
+ ## Installation
37
+
38
+ ```bash
39
+ pip install langbly
40
+ ```
41
+
42
+ ## Quick Start
43
+
44
+ ```python
45
+ from langbly import Langbly
46
+
47
+ client = Langbly(api_key="your-api-key")
48
+
49
+ # Translate text
50
+ result = client.translate("Hello world", target="nl")
51
+ print(result.text) # "Hallo wereld"
52
+
53
+ # Batch translate
54
+ results = client.translate(["Hello", "Goodbye"], target="nl")
55
+ for r in results:
56
+ print(r.text)
57
+
58
+ # Detect language
59
+ detection = client.detect("Bonjour le monde")
60
+ print(detection.language) # "fr"
61
+ ```
62
+
63
+ ## Google Translate Migration
64
+
65
+ If you're using `google-cloud-translate`, switching is simple:
66
+
67
+ ```python
68
+ # Before (Google)
69
+ from google.cloud import translate_v2 as translate
70
+ client = translate.Client()
71
+ result = client.translate("Hello", target_language="nl")
72
+
73
+ # After (Langbly)
74
+ from langbly import Langbly
75
+ client = Langbly(api_key="your-key")
76
+ result = client.translate("Hello", target="nl")
77
+ ```
78
+
79
+ ## API Reference
80
+
81
+ ### `Langbly(api_key, base_url=None)`
82
+
83
+ Create a client instance.
84
+
85
+ - `api_key` (str): Your Langbly API key
86
+ - `base_url` (str, optional): Override the API URL (default: `https://api.langbly.com`)
87
+
88
+ ### `client.translate(text, target, source=None, format=None)`
89
+
90
+ Translate text.
91
+
92
+ - `text` (str | list[str]): Text(s) to translate
93
+ - `target` (str): Target language code (e.g., "nl", "de", "fr")
94
+ - `source` (str, optional): Source language code (auto-detected if omitted)
95
+ - `format` (str, optional): "text" or "html"
96
+
97
+ ### `client.detect(text)`
98
+
99
+ Detect the language of text.
100
+
101
+ - `text` (str): Text to analyze
102
+
103
+ ### `client.languages(target=None)`
104
+
105
+ List supported languages.
106
+
107
+ - `target` (str, optional): Language code to return names in
108
+
109
+ ## License
110
+
111
+ MIT
@@ -0,0 +1,80 @@
1
+ # langbly-python
2
+
3
+ Official Python SDK for the [Langbly](https://langbly.com) translation API — a drop-in replacement for Google Translate v2.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install langbly
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```python
14
+ from langbly import Langbly
15
+
16
+ client = Langbly(api_key="your-api-key")
17
+
18
+ # Translate text
19
+ result = client.translate("Hello world", target="nl")
20
+ print(result.text) # "Hallo wereld"
21
+
22
+ # Batch translate
23
+ results = client.translate(["Hello", "Goodbye"], target="nl")
24
+ for r in results:
25
+ print(r.text)
26
+
27
+ # Detect language
28
+ detection = client.detect("Bonjour le monde")
29
+ print(detection.language) # "fr"
30
+ ```
31
+
32
+ ## Google Translate Migration
33
+
34
+ If you're using `google-cloud-translate`, switching is simple:
35
+
36
+ ```python
37
+ # Before (Google)
38
+ from google.cloud import translate_v2 as translate
39
+ client = translate.Client()
40
+ result = client.translate("Hello", target_language="nl")
41
+
42
+ # After (Langbly)
43
+ from langbly import Langbly
44
+ client = Langbly(api_key="your-key")
45
+ result = client.translate("Hello", target="nl")
46
+ ```
47
+
48
+ ## API Reference
49
+
50
+ ### `Langbly(api_key, base_url=None)`
51
+
52
+ Create a client instance.
53
+
54
+ - `api_key` (str): Your Langbly API key
55
+ - `base_url` (str, optional): Override the API URL (default: `https://api.langbly.com`)
56
+
57
+ ### `client.translate(text, target, source=None, format=None)`
58
+
59
+ Translate text.
60
+
61
+ - `text` (str | list[str]): Text(s) to translate
62
+ - `target` (str): Target language code (e.g., "nl", "de", "fr")
63
+ - `source` (str, optional): Source language code (auto-detected if omitted)
64
+ - `format` (str, optional): "text" or "html"
65
+
66
+ ### `client.detect(text)`
67
+
68
+ Detect the language of text.
69
+
70
+ - `text` (str): Text to analyze
71
+
72
+ ### `client.languages(target=None)`
73
+
74
+ List supported languages.
75
+
76
+ - `target` (str, optional): Language code to return names in
77
+
78
+ ## License
79
+
80
+ MIT
@@ -0,0 +1,44 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "langbly"
7
+ version = "0.1.0"
8
+ description = "Official Python SDK for the Langbly translation API"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.8"
12
+ authors = [
13
+ { name = "Jasper de Winter", email = "jasper@langbly.com" },
14
+ ]
15
+ keywords = ["translation", "api", "langbly", "google-translate", "i18n", "localization"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.8",
22
+ "Programming Language :: Python :: 3.9",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Topic :: Software Development :: Libraries",
27
+ "Topic :: Text Processing :: Linguistic",
28
+ ]
29
+ dependencies = [
30
+ "httpx>=0.24.0",
31
+ ]
32
+
33
+ [project.optional-dependencies]
34
+ dev = [
35
+ "pytest>=7.0",
36
+ "pytest-asyncio>=0.21",
37
+ "respx>=0.20",
38
+ ]
39
+
40
+ [project.urls]
41
+ Homepage = "https://langbly.com"
42
+ Documentation = "https://langbly.com/docs"
43
+ Repository = "https://github.com/Langbly/langbly-python"
44
+ Issues = "https://github.com/Langbly/langbly-python/issues"
@@ -0,0 +1,22 @@
1
+ """Langbly — Official Python SDK for the Langbly translation API."""
2
+
3
+ from .client import (
4
+ AuthenticationError,
5
+ Detection,
6
+ Langbly,
7
+ LangblyError,
8
+ Language,
9
+ RateLimitError,
10
+ Translation,
11
+ )
12
+
13
+ __all__ = [
14
+ "AuthenticationError",
15
+ "Detection",
16
+ "Langbly",
17
+ "LangblyError",
18
+ "Language",
19
+ "RateLimitError",
20
+ "Translation",
21
+ ]
22
+ __version__ = "0.1.0"
@@ -0,0 +1,304 @@
1
+ """Langbly API client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from dataclasses import dataclass
7
+ from typing import List, Optional, Union
8
+
9
+ import httpx
10
+
11
+
12
+ @dataclass
13
+ class Translation:
14
+ """A single translation result."""
15
+
16
+ text: str
17
+ source: str
18
+ model: Optional[str] = None
19
+
20
+
21
+ @dataclass
22
+ class Detection:
23
+ """A language detection result."""
24
+
25
+ language: str
26
+ confidence: float
27
+
28
+
29
+ @dataclass
30
+ class Language:
31
+ """A supported language."""
32
+
33
+ code: str
34
+ name: Optional[str] = None
35
+
36
+
37
+ class LangblyError(Exception):
38
+ """Base exception for Langbly API errors."""
39
+
40
+ def __init__(self, message: str, status_code: int = 0, code: str = ""):
41
+ super().__init__(message)
42
+ self.status_code = status_code
43
+ self.code = code
44
+
45
+
46
+ class RateLimitError(LangblyError):
47
+ """Raised when the API returns 429 Too Many Requests."""
48
+
49
+ def __init__(self, message: str, retry_after: Optional[float] = None):
50
+ super().__init__(message, status_code=429, code="RATE_LIMITED")
51
+ self.retry_after = retry_after
52
+
53
+
54
+ class AuthenticationError(LangblyError):
55
+ """Raised when the API key is invalid or missing."""
56
+
57
+ def __init__(self, message: str):
58
+ super().__init__(message, status_code=401, code="UNAUTHENTICATED")
59
+
60
+
61
+ _RETRIABLE_STATUS_CODES = frozenset({429, 500, 502, 503, 504})
62
+
63
+
64
+ class Langbly:
65
+ """Client for the Langbly translation API.
66
+
67
+ A drop-in replacement for Google Translate v2 — powered by LLMs.
68
+
69
+ Args:
70
+ api_key: Your Langbly API key.
71
+ base_url: Override the API base URL (default: https://api.langbly.com).
72
+ timeout: Request timeout in seconds (default: 30).
73
+ max_retries: Number of retries for transient errors (default: 2).
74
+ """
75
+
76
+ def __init__(
77
+ self,
78
+ api_key: str,
79
+ base_url: str = "https://api.langbly.com",
80
+ timeout: float = 30.0,
81
+ max_retries: int = 2,
82
+ ):
83
+ if not api_key:
84
+ raise ValueError("api_key is required")
85
+
86
+ self._api_key = api_key
87
+ self._base_url = base_url.rstrip("/")
88
+ self._max_retries = max_retries
89
+ self._client = httpx.Client(
90
+ base_url=self._base_url,
91
+ headers={
92
+ "Authorization": f"Bearer {api_key}",
93
+ "User-Agent": "langbly-python/0.1.0",
94
+ },
95
+ timeout=timeout,
96
+ )
97
+
98
+ def translate(
99
+ self,
100
+ text: Union[str, List[str]],
101
+ target: str,
102
+ source: Optional[str] = None,
103
+ format: Optional[str] = None,
104
+ ) -> Union[Translation, List[Translation]]:
105
+ """Translate text to the target language.
106
+
107
+ Args:
108
+ text: A string or list of strings to translate.
109
+ target: Target language code (e.g., "nl", "de", "fr").
110
+ source: Source language code. Auto-detected if omitted.
111
+ format: "text" or "html". Default: "text".
112
+
113
+ Returns:
114
+ A Translation object, or a list if input was a list.
115
+
116
+ Raises:
117
+ LangblyError: On API error.
118
+ RateLimitError: When rate limited (429).
119
+ AuthenticationError: When API key is invalid (401).
120
+ """
121
+ q = [text] if isinstance(text, str) else text
122
+
123
+ body: dict = {"q": q, "target": target}
124
+ if source:
125
+ body["source"] = source
126
+ if format:
127
+ body["format"] = format
128
+
129
+ data = self._post("/language/translate/v2", body)
130
+
131
+ translations = []
132
+ for item in data["data"]["translations"]:
133
+ translations.append(
134
+ Translation(
135
+ text=item["translatedText"],
136
+ source=item.get("detectedSourceLanguage", source or ""),
137
+ model=item.get("model"),
138
+ )
139
+ )
140
+
141
+ if isinstance(text, str):
142
+ return translations[0]
143
+ return translations
144
+
145
+ def detect(self, text: str) -> Detection:
146
+ """Detect the language of text.
147
+
148
+ Args:
149
+ text: The text to analyze.
150
+
151
+ Returns:
152
+ A Detection object with language code and confidence.
153
+
154
+ Raises:
155
+ LangblyError: On API error.
156
+ """
157
+ body = {"q": text}
158
+ data = self._post("/language/translate/v2/detect", body)
159
+
160
+ det = data["data"]["detections"][0][0]
161
+ return Detection(
162
+ language=det["language"],
163
+ confidence=det.get("confidence", 0.0),
164
+ )
165
+
166
+ def languages(self, target: Optional[str] = None) -> List[Language]:
167
+ """List supported languages.
168
+
169
+ Args:
170
+ target: If set, return language names in this language.
171
+
172
+ Returns:
173
+ A list of Language objects.
174
+
175
+ Raises:
176
+ LangblyError: On API error.
177
+ """
178
+ params: dict = {}
179
+ if target:
180
+ params["target"] = target
181
+
182
+ resp = self._request("GET", "/language/translate/v2/languages", params=params)
183
+ data = resp.json()
184
+
185
+ return [
186
+ Language(code=lang["language"], name=lang.get("name"))
187
+ for lang in data["data"]["languages"]
188
+ ]
189
+
190
+ def _post(self, path: str, body: dict) -> dict:
191
+ resp = self._request("POST", path, json=body)
192
+ return resp.json()
193
+
194
+ def _request(
195
+ self,
196
+ method: str,
197
+ path: str,
198
+ **kwargs,
199
+ ) -> httpx.Response:
200
+ """Make an HTTP request with automatic retries for transient errors."""
201
+ last_exc: Optional[Exception] = None
202
+
203
+ for attempt in range(self._max_retries + 1):
204
+ try:
205
+ resp = self._client.request(method, path, **kwargs)
206
+ except httpx.TimeoutException as exc:
207
+ last_exc = exc
208
+ if attempt < self._max_retries:
209
+ time.sleep(self._backoff_delay(attempt))
210
+ continue
211
+ raise LangblyError(
212
+ f"Request timed out after {self._max_retries + 1} attempts",
213
+ status_code=0,
214
+ code="TIMEOUT",
215
+ ) from exc
216
+ except httpx.ConnectError as exc:
217
+ last_exc = exc
218
+ if attempt < self._max_retries:
219
+ time.sleep(self._backoff_delay(attempt))
220
+ continue
221
+ raise LangblyError(
222
+ f"Connection failed after {self._max_retries + 1} attempts",
223
+ status_code=0,
224
+ code="CONNECTION_ERROR",
225
+ ) from exc
226
+
227
+ if resp.is_success:
228
+ return resp
229
+
230
+ # Don't retry client errors (except 429)
231
+ if resp.status_code not in _RETRIABLE_STATUS_CODES:
232
+ self._raise_for_status(resp)
233
+
234
+ # Retriable error
235
+ if attempt < self._max_retries:
236
+ delay = self._get_retry_delay(resp, attempt)
237
+ time.sleep(delay)
238
+ continue
239
+
240
+ # Final attempt failed
241
+ self._raise_for_status(resp)
242
+
243
+ # Should not reach here, but just in case
244
+ if last_exc:
245
+ raise last_exc
246
+ raise LangblyError("Request failed")
247
+
248
+ def _raise_for_status(self, resp: httpx.Response) -> None:
249
+ """Parse error response and raise appropriate exception."""
250
+ try:
251
+ err = resp.json()
252
+ msg = err.get("error", {}).get("message", resp.text)
253
+ code = err.get("error", {}).get("status", "")
254
+ except Exception:
255
+ msg = resp.text or resp.reason_phrase
256
+ code = ""
257
+
258
+ if resp.status_code == 401:
259
+ raise AuthenticationError(msg)
260
+ if resp.status_code == 429:
261
+ retry_after = self._parse_retry_after(resp)
262
+ raise RateLimitError(msg, retry_after=retry_after)
263
+
264
+ raise LangblyError(msg, status_code=resp.status_code, code=code)
265
+
266
+ @staticmethod
267
+ def _parse_retry_after(resp: httpx.Response) -> Optional[float]:
268
+ """Parse Retry-After header if present."""
269
+ header = resp.headers.get("retry-after")
270
+ if header is None:
271
+ return None
272
+ try:
273
+ return float(header)
274
+ except (ValueError, TypeError):
275
+ return None
276
+
277
+ @staticmethod
278
+ def _get_retry_delay(resp: httpx.Response, attempt: int) -> float:
279
+ """Calculate retry delay, respecting Retry-After header."""
280
+ retry_after = resp.headers.get("retry-after")
281
+ if retry_after:
282
+ try:
283
+ return min(float(retry_after), 30.0)
284
+ except (ValueError, TypeError):
285
+ pass
286
+ return min(0.5 * (2**attempt), 10.0)
287
+
288
+ @staticmethod
289
+ def _backoff_delay(attempt: int) -> float:
290
+ """Exponential backoff delay for connection/timeout errors."""
291
+ return min(0.5 * (2**attempt), 10.0)
292
+
293
+ def close(self) -> None:
294
+ """Close the underlying HTTP client."""
295
+ self._client.close()
296
+
297
+ def __enter__(self):
298
+ return self
299
+
300
+ def __exit__(self, *args):
301
+ self.close()
302
+
303
+ def __repr__(self) -> str:
304
+ return f"Langbly(base_url={self._base_url!r})"