3131class Settings (BaseSettings ):
3232 # Env names: STREAM_API_KEY, STREAM_API_SECRET, STREAM_BASE_URL, STREAM_TIMEOUT
3333 api_key : str
34- api_secret : str
34+ api_secret : Optional [ str ] = None
3535 base_url : Optional [str ] = None
3636 timeout : float = 6.0
3737
@@ -50,22 +50,70 @@ def __init__(
5050 user_agent : Optional [str ] = None ,
5151 transport = None ,
5252 http_client = None ,
53+ token : Optional [str ] = None ,
5354 ):
55+ """Build a Stream client.
56+
57+ Pass exactly one of ``api_secret`` or ``token``:
58+ - ``api_secret`` enables a server-side client that can mint user tokens
59+ and call protected admin endpoints.
60+ - ``token`` enables a client-side client authenticated as a single
61+ user. Token-only clients cannot mint tokens or call admin endpoints.
62+
63+ Any of ``api_key``, ``api_secret``, ``base_url`` left as ``None`` are
64+ loaded from ``STREAM_*`` env vars; passing ``token`` skips the
65+ ``api_secret`` env fallback.
66+
67+ Args:
68+ api_key: Project API key. Falls back to ``STREAM_API_KEY``.
69+ api_secret: Project API secret. Mutually exclusive with ``token``.
70+ Falls back to ``STREAM_API_SECRET`` only when ``token`` is also
71+ ``None``.
72+ timeout: HTTP request timeout in seconds; must be > 0.
73+ base_url: API base URL. Falls back to ``STREAM_BASE_URL`` then to
74+ the SDK default.
75+ user_agent: Optional custom ``User-Agent`` string.
76+ transport: Optional ``httpx`` transport. Mutually exclusive with
77+ ``http_client``.
78+ http_client: Optional pre-built ``httpx`` client. Mutually
79+ exclusive with ``transport``. When provided, sub-clients
80+ (video/chat/moderation) reuse it instead of opening their own.
81+ token: Pre-minted user JWT. Mutually exclusive with ``api_secret``.
82+
83+ Raises:
84+ ValueError: If both ``transport`` and ``http_client`` are set; if
85+ neither ``api_secret`` nor ``token`` can be resolved; if both
86+ are provided; if either is the empty string; if ``api_key`` is
87+ missing; or if ``timeout`` is not a positive number.
88+ """
5489 if transport is not None and http_client is not None :
5590 raise ValueError ("Cannot specify both 'transport' and 'http_client'" )
5691
57- if None in (api_key , api_secret , timeout , base_url ):
58- s = Settings () # loads from env and optional .env
92+ # Env fallback for anything not explicitly provided. A caller-supplied
93+ # token short-circuits api_secret loading so token-only callers don't
94+ # need STREAM_API_SECRET in env.
95+ if (
96+ api_key is None
97+ or base_url is None
98+ or (api_secret is None and token is None )
99+ ):
100+ s = Settings ()
59101 api_key = api_key or s .api_key
60- api_secret = api_secret or s .api_secret
61102 base_url = base_url or (s .base_url or BASE_URL )
103+ if token is None and api_secret is None :
104+ api_secret = s .api_secret
62105
63- if api_key is None or api_key == "" :
106+ if not api_key :
64107 raise ValueError ("api_key is required" )
65- if api_secret is None or api_secret == "" :
66- raise ValueError ("api_secret is required" )
108+ if api_secret and token :
109+ raise ValueError ("Pass either api_secret or token, not both" )
110+ if api_secret == "" or token == "" :
111+ raise ValueError ("api_secret and token must not be empty strings" )
112+ if api_secret is None and token is None :
113+ raise ValueError ("Either api_secret or token is required" )
114+
67115 self .api_key = api_key
68- self .api_secret = api_secret
116+ self ._api_secret = api_secret or None
69117
70118 if timeout is not None :
71119 if not isinstance (timeout , (int , float )) or timeout <= 0.0 :
@@ -76,7 +124,7 @@ def __init__(
76124 self .user_agent = user_agent
77125 self ._transport = transport
78126 self ._http_client = http_client
79- self .token = self ._create_token ()
127+ self .token = token or self ._create_token ()
80128 super ().__init__ (
81129 self .api_key , self .base_url , self .token , self .timeout , self .user_agent
82130 )
@@ -88,6 +136,25 @@ def __init__(
88136 else :
89137 self ._shared_client = None
90138
139+ @property
140+ def api_secret (self ) -> str :
141+ """
142+ Get api secret if it's set.
143+ Otherwise, raise a ValueError.
144+ """
145+ if self ._api_secret is None :
146+ raise ValueError (
147+ "api_secret is required; this client was initialized with a token"
148+ )
149+ return self ._api_secret
150+
151+ @property
152+ def has_api_secret (self ) -> bool :
153+ """
154+ Check if api secret is set for this client.
155+ """
156+ return self ._api_secret is not None
157+
91158 def _apply_shared_client (self , sub_client ):
92159 """Replace a sub-client's auto-created httpx client with the shared
93160 one built from user-provided transport/http_client config."""
@@ -131,6 +198,20 @@ def create_token(
131198 user_id = user_id , expiration = expiration , iat = int (time .time ()) - 5
132199 )
133200
201+ def clone_for_token (self , token : str ):
202+ """Return a sibling client authenticated with the given user token.
203+
204+ Keeps this client's ``api_key`` and ``base_url``. The clone is
205+ token-only; it cannot mint further tokens.
206+ """
207+ return self .__class__ (
208+ api_key = self .api_key ,
209+ token = token ,
210+ base_url = self .base_url ,
211+ timeout = self .timeout ,
212+ user_agent = self .user_agent ,
213+ )
214+
134215 def create_call_token (
135216 self ,
136217 user_id : str ,
@@ -358,7 +439,8 @@ def from_env(cls, timeout: float = 6.0) -> Stream:
358439 def as_async (self ) -> "AsyncStream" :
359440 return AsyncStream (
360441 api_key = self .api_key ,
361- api_secret = self .api_secret ,
442+ api_secret = self ._api_secret ,
443+ token = None if self .has_api_secret else self .token ,
362444 timeout = self .timeout ,
363445 base_url = self .base_url ,
364446 user_agent = self .user_agent ,
0 commit comments