Skip to content

Commit 7d4709c

Browse files
authored
Implement JWT feature in batch sdk (#16)
1 parent 2f71bd5 commit 7d4709c

File tree

7 files changed

+256
-85
lines changed

7 files changed

+256
-85
lines changed

sdk/batch/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,25 @@ async def main():
3636
asyncio.run(main())
3737
```
3838

39+
## JWT Authentication
40+
41+
For enhanced security, use temporary JWT tokens instead of static API keys.
42+
JWTs are short-lived (60 seconds default) and automatically refreshed:
43+
44+
```python
45+
from speechmatics.batch import AsyncClient, JWTAuth
46+
47+
auth = JWTAuth("your-api-key", ttl=60)
48+
49+
async with AsyncClient(auth=auth) as client:
50+
# Tokens are cached and auto-refreshed automatically
51+
result = await client.transcribe("audio.wav")
52+
print(result.transcript_text)
53+
```
54+
55+
Ideal for long-running applications or when minimizing API key exposure.
56+
See the [authentication documentation](https://docs.speechmatics.com/introduction/authentication) for more details.
57+
3958
### Basic Job Workflow
4059

4160
```python

sdk/batch/speechmatics/batch/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
__version__ = "0.0.0"
22

33
from ._async_client import AsyncClient
4+
from ._auth import AuthBase
5+
from ._auth import JWTAuth
6+
from ._auth import StaticKeyAuth
47
from ._exceptions import AuthenticationError
58
from ._exceptions import BatchError
69
from ._exceptions import ConfigurationError
@@ -24,6 +27,9 @@
2427

2528
__all__ = [
2629
"AsyncClient",
30+
"AuthBase",
31+
"JWTAuth",
32+
"StaticKeyAuth",
2733
"ConfigurationError",
2834
"AuthenticationError",
2935
"ConnectionError",

sdk/batch/speechmatics/batch/_async_client.py

Lines changed: 22 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@
1515
from typing import Optional
1616
from typing import Union
1717

18+
from ._auth import AuthBase
19+
from ._auth import StaticKeyAuth
1820
from ._exceptions import AuthenticationError
1921
from ._exceptions import BatchError
20-
from ._exceptions import ConfigurationError
2122
from ._exceptions import JobError
2223
from ._exceptions import TimeoutError
2324
from ._helpers import prepare_audio_file
@@ -48,12 +49,13 @@ class AsyncClient:
4849
4. Proper cleanup and error handling
4950
5051
Args:
51-
api_key: Speechmatics API key for authentication. If not provided,
52-
uses the SPEECHMATICS_API_KEY environment variable.
52+
auth: Authentication instance. If not provided, uses StaticKeyAuth
53+
with api_key parameter or SPEECHMATICS_API_KEY environment variable.
54+
api_key: Speechmatics API key (used only if auth not provided).
5355
url: REST API endpoint URL. If not provided, uses SPEECHMATICS_BATCH_URL
5456
environment variable or defaults to production endpoint.
5557
conn_config: Complete connection configuration object. If provided, overrides
56-
api_key and url parameters.
58+
other parameters.
5759
5860
Raises:
5961
ConfigurationError: If required configuration is missing or invalid.
@@ -65,26 +67,17 @@ class AsyncClient:
6567
... result = await client.wait_for_completion(job.id)
6668
... print(result.transcript)
6769
68-
With custom configuration:
69-
>>> config = ConnectionConfig(
70-
... url="https://asr.api.speechmatics.com/v2",
71-
... api_key="your-key",
72-
... )
73-
>>> async with AsyncClient(conn_config=config) as client:
74-
... # Use client with custom settings
70+
With JWT authentication:
71+
>>> from speechmatics.batch import JWTAuth
72+
>>> auth = JWTAuth("your-api-key", ttl=3600)
73+
>>> async with AsyncClient(auth=auth) as client:
74+
... # Use client with JWT auth
7575
... pass
76-
77-
Manual resource management:
78-
>>> client = AsyncClient(api_key="your-key")
79-
>>> try:
80-
... job = await client.submit_job("audio.wav")
81-
... result = await client.wait_for_completion(job.id)
82-
... finally:
83-
... await client.close()
8476
"""
8577

8678
def __init__(
8779
self,
80+
auth: Optional[AuthBase] = None,
8881
*,
8982
api_key: Optional[str] = None,
9083
url: Optional[str] = None,
@@ -94,30 +87,24 @@ def __init__(
9487
Initialize the AsyncClient.
9588
9689
Args:
90+
auth: Authentication method, it can be StaticKeyAuth or JWTAuth.
91+
If None, creates StaticKeyAuth with the api_key.
9792
api_key: Speechmatics API key. If None, uses SPEECHMATICS_API_KEY env var.
9893
url: REST API endpoint URL. If None, uses SPEECHMATICS_BATCH_URL env var
9994
or defaults to production endpoint.
100-
conn_config: Complete connection configuration. Overrides api_key and url.
95+
conn_config: Complete connection configuration.
10196
10297
Raises:
103-
ConfigurationError: If API key is not provided and not found in environment.
98+
ConfigurationError: If auth is None and API key is not provided/found.
10499
"""
105-
# Set up configuration
106-
if conn_config:
107-
self._conn_config = conn_config
108-
else:
109-
api_key = api_key or os.environ.get("SPEECHMATICS_API_KEY")
110-
if not api_key:
111-
raise ConfigurationError("API key required: provide api_key parameter or set SPEECHMATICS_API_KEY")
112-
113-
final_url = url or os.environ.get("SPEECHMATICS_BATCH_URL") or "https://asr.api.speechmatics.com/v2"
114-
self._conn_config = ConnectionConfig(url=final_url, api_key=api_key)
115-
100+
self._auth = auth or StaticKeyAuth(api_key)
101+
self._url = url or os.environ.get("SPEECHMATICS_BATCH_URL") or "https://asr.api.speechmatics.com/v2"
102+
self._conn_config = conn_config or ConnectionConfig()
116103
self._request_id = str(uuid.uuid4())
117-
self._transport = Transport(self._conn_config, self._request_id)
118-
self._logger = get_logger(__name__)
104+
self._transport = Transport(self._url, self._conn_config, self._auth, self._request_id)
119105

120-
self._logger.debug("AsyncClient initialized (request_id=%s, url=%s)", self._request_id, self._conn_config.url)
106+
self._logger = get_logger(__name__)
107+
self._logger.debug("AsyncClient initialized (request_id=%s, url=%s)", self._request_id, self._url)
121108

122109
async def __aenter__(self) -> AsyncClient:
123110
"""
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import abc
2+
import asyncio
3+
import os
4+
import time
5+
from typing import Literal
6+
from typing import Optional
7+
8+
from ._exceptions import AuthenticationError
9+
10+
11+
class AuthBase(abc.ABC):
12+
"""
13+
Abstract base class for authentication methods.
14+
"""
15+
16+
BASE_URL = "https://mp.speechmatics.com"
17+
18+
@abc.abstractmethod
19+
async def get_auth_headers(self) -> dict[str, str]:
20+
"""
21+
Get authentication headers asynchronously.
22+
23+
Returns:
24+
A dictionary of authentication headers.
25+
"""
26+
raise NotImplementedError
27+
28+
29+
class StaticKeyAuth(AuthBase):
30+
"""
31+
Authentication using a static API key.
32+
33+
This is the traditional authentication method where the same
34+
API key is used for all requests.
35+
36+
Args:
37+
api_key: The Speechmatics API key.
38+
39+
Examples:
40+
>>> auth = StaticKeyAuth("your-api-key")
41+
>>> headers = await auth.get_auth_headers()
42+
>>> print(headers)
43+
{'Authorization': 'Bearer your-api-key'}
44+
"""
45+
46+
def __init__(self, api_key: Optional[str] = None):
47+
self._api_key = api_key or os.environ.get("SPEECHMATICS_API_KEY")
48+
49+
if not self._api_key:
50+
raise ValueError("API key required: provide api_key or set SPEECHMATICS_API_KEY")
51+
52+
async def get_auth_headers(self) -> dict[str, str]:
53+
return {"Authorization": f"Bearer {self._api_key}"}
54+
55+
56+
class JWTAuth(AuthBase):
57+
"""
58+
Authentication using temporary JWT tokens.
59+
60+
Generates short-lived JWTs for enhanced security.
61+
62+
Args:
63+
api_key: The main Speechmatics API key used to generate JWTs.
64+
ttl: Time-to-live for tokens between 60 and 86400 seconds.
65+
For security reasons, we suggest using the shortest TTL possible.
66+
region: Self-Service customers are restricted to "eu".
67+
Enterprise customers can use this to specify which region the temporary key should be enabled in.
68+
client_ref: Optional client reference for JWT token.
69+
This parameter must be used if the temporary keys are exposed to the end-user's client
70+
to prevent a user from accessing the data of a different user.
71+
mp_url: Optional management platform URL override.
72+
request_id: Optional request ID for debugging purposes.
73+
74+
Examples:
75+
>>> auth = JWTAuth("your-api-key")
76+
>>> headers = await auth.get_auth_headers()
77+
>>> print(headers)
78+
{'Authorization': 'Bearer eyJhbGciOiJSUzI1NiIs...'}
79+
"""
80+
81+
def __init__(
82+
self,
83+
api_key: Optional[str] = None,
84+
*,
85+
ttl: int = 60,
86+
region: Literal["eu", "usa", "au"] = "eu",
87+
client_ref: Optional[str] = None,
88+
mp_url: Optional[str] = None,
89+
request_id: Optional[str] = None,
90+
):
91+
self._api_key = api_key or os.environ.get("SPEECHMATICS_API_KEY")
92+
self._ttl = ttl
93+
self._region = region
94+
self._client_ref = client_ref
95+
self._request_id = request_id
96+
self._mp_url = mp_url or os.getenv("SM_MANAGEMENT_PLATFORM_URL", self.BASE_URL)
97+
98+
if not self._api_key:
99+
raise ValueError(
100+
"API key required: please provide api_key or set SPEECHMATICS_API_KEY environment variable"
101+
)
102+
103+
if not 60 <= self._ttl <= 86_400:
104+
raise ValueError("ttl must be between 60 and 86400 seconds")
105+
106+
self._cached_token: Optional[str] = None
107+
self._token_expires_at: float = 0
108+
self._token_lock = asyncio.Lock()
109+
110+
async def get_auth_headers(self) -> dict[str, str]:
111+
"""Get JWT auth headers with caching."""
112+
async with self._token_lock:
113+
current_time = time.time()
114+
if current_time >= self._token_expires_at - 10:
115+
self._cached_token = await self._generate_token()
116+
self._token_expires_at = current_time + self._ttl
117+
118+
return {"Authorization": f"Bearer {self._cached_token}"}
119+
120+
async def _generate_token(self) -> str:
121+
try:
122+
import aiohttp
123+
except ImportError:
124+
raise ImportError(
125+
"aiohttp is required for JWT authentication. Please install it with `pip install 'speechmatics-batch[jwt]'`"
126+
)
127+
128+
endpoint = f"{self._mp_url}/v1/api_keys"
129+
params = {"type": "batch"}
130+
payload = {"ttl": self._ttl, "region": str(self._region)}
131+
132+
if self._client_ref:
133+
payload["client_ref"] = self._client_ref
134+
135+
headers = {
136+
"Authorization": f"Bearer {self._api_key}",
137+
"Content-Type": "application/json",
138+
}
139+
140+
if self._request_id:
141+
headers["X-Request-Id"] = self._request_id
142+
143+
try:
144+
async with aiohttp.ClientSession() as session:
145+
async with session.post(
146+
endpoint,
147+
params=params,
148+
json=payload,
149+
headers=headers,
150+
timeout=aiohttp.ClientTimeout(total=10),
151+
) as response:
152+
if response.status != 201:
153+
text = await response.text()
154+
raise AuthenticationError(f"Failed to generate JWT: HTTP {response.status}: {text}")
155+
156+
data = await response.json()
157+
return str(data["key_value"])
158+
159+
except aiohttp.ClientError as e:
160+
raise AuthenticationError(f"Network error generating JWT: {e}")
161+
except Exception as e:
162+
raise AuthenticationError(f"Unexpected error generating JWT: {e}")

sdk/batch/speechmatics/batch/_helpers.py

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -50,29 +50,12 @@ async def prepare_audio_file(audio_file: Union[str, BinaryIO]) -> AsyncGenerator
5050

5151

5252
def get_version() -> str:
53-
"""
54-
Get SDK version from package metadata or __init__.py file.
55-
56-
Returns:
57-
Version string
58-
"""
5953
try:
6054
return importlib.metadata.version("speechmatics-batch")
6155
except importlib.metadata.PackageNotFoundError:
6256
try:
63-
# Import from the same package
6457
from . import __version__
6558

6659
return __version__
6760
except ImportError:
68-
# Fallback: read __init__.py file directly
69-
try:
70-
init_path = os.path.join(os.path.dirname(__file__), "__init__.py")
71-
with open(init_path, encoding="utf-8") as f:
72-
for line in f:
73-
if line.strip().startswith("__version__"):
74-
# Extract version string from __version__ = "x.x.x"
75-
return line.split("=")[1].strip().strip('"').strip("'")
76-
except (FileNotFoundError, IndexError, AttributeError):
77-
pass
78-
return "0.0.0"
61+
return "0.0.0"

sdk/batch/speechmatics/batch/_models.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -737,17 +737,12 @@ class ConnectionConfig:
737737
"""
738738
Configuration for HTTP connection parameters.
739739
740-
This class defines all connection-related settings including URL,
741-
authentication, and timeouts.
740+
This class defines connection-related settings and timeouts.
742741
743742
Attributes:
744-
url: Base URL for the Speechmatics Batch API.
745-
api_key: Speechmatics API key for authentication.
746743
connect_timeout: Timeout in seconds for connection establishment.
747744
operation_timeout: Default timeout for API operations.
748745
"""
749746

750-
url: str = "https://asr.api.speechmatics.com/v2"
751-
api_key: str = ""
752747
connect_timeout: float = 30.0
753748
operation_timeout: float = 300.0

0 commit comments

Comments
 (0)