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.
- langbly-0.1.0/.github/workflows/publish.yml +31 -0
- langbly-0.1.0/LICENSE +21 -0
- langbly-0.1.0/PKG-INFO +111 -0
- langbly-0.1.0/README.md +80 -0
- langbly-0.1.0/pyproject.toml +44 -0
- langbly-0.1.0/src/langbly/__init__.py +22 -0
- langbly-0.1.0/src/langbly/client.py +304 -0
|
@@ -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
|
langbly-0.1.0/README.md
ADDED
|
@@ -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})"
|