44``ShadeClient`` binds a set of credentials and connection settings to a single
55object, so an application acting on behalf of several merchants can hold one
66client per tenant instead of mutating the global ``shade`` module config.
7- Anything left unset falls back to the global config at construction time.
7+ Anything left unset falls back to the global config, resolved per request so a
8+ client on the defaults follows later changes to ``shade.api_key`` and friends.
89"""
910from __future__ import annotations
1011
1516
1617from .config import Environment , validate_client_settings
1718from .config import config as _config
18- from .errors import AuthenticationError
1919from .http import AsyncHTTPClient , HTTPXTransport , SyncHTTPClient
2020
2121API_KEY_ENV_VAR = "SHADE_API_KEY"
@@ -32,9 +32,9 @@ class ShadeClient:
3232 globex = ShadeClient(api_key="sk_live_globex")
3333
3434 Every parameter falls back to the matching global setting
35- (``shade.api_key``, ``shade.environment``, …) when omitted, and the fallback
36- is resolved once at construction — later changes to the global config do not
37- retroactively alter an existing client .
35+ (``shade.api_key``, ``shade.environment``, …) when omitted. Explicit
36+ arguments are pinned to the instance; omitted ones track the global config,
37+ which is read at request time rather than captured at construction .
3838
3939 Parameters
4040 ----------
@@ -48,13 +48,13 @@ class ShadeClient:
4848 self-hosted backend). Takes precedence over the module-level
4949 ``shade.api_base`` and the URL derived from ``environment``. Trailing
5050 slashes are trimmed.
51- timeout : float, optional
52- Per-request socket timeout in seconds. Defaults to ``shade.timeout ``.
51+ base_url : str
52+ Deprecated. Prefer ``api_base ``.
5353 max_retries : int, optional
5454 Automatic retries on HTTP 429 and transient failures. Defaults to
5555 ``shade.max_retries``. Set to ``0`` to disable auto-retry.
56- base_url : str
57- Deprecated. Prefer ``api_base ``.
56+ timeout : float, optional
57+ Per-request socket timeout in seconds. Defaults to ``shade.timeout ``.
5858 debug : bool
5959 Log requests and responses for this client. The global
6060 ``shade.config.debug`` enables logging regardless of this flag.
@@ -64,8 +64,6 @@ class ShadeClient:
6464
6565 Raises
6666 ------
67- AuthenticationError
68- If no API key is given and no global ``shade.api_key`` is set.
6967 ValueError
7068 If ``timeout`` or ``max_retries`` is out of range, or ``environment``
7169 is not a recognised value.
@@ -76,50 +74,46 @@ def __init__(
7674 api_key : Optional [str ] = None ,
7775 environment : Optional [Environment | str ] = None ,
7876 api_base : Optional [str ] = None ,
79- timeout : Optional [float ] = None ,
80- max_retries : Optional [int ] = None ,
8177 base_url : str = "" ,
78+ max_retries : Optional [int ] = None ,
79+ timeout : Optional [float ] = None ,
8280 debug : bool = False ,
8381 http_client : Optional [httpx .Client ] = None ,
8482 ) -> None :
85- resolved_api_key = api_key or _config .api_key
86- if not resolved_api_key :
87- raise AuthenticationError (
88- "No API key provided. Pass api_key= to ShadeClient, set "
89- f"shade.api_key, or set the { API_KEY_ENV_VAR } environment variable."
90- )
91- self .api_key = resolved_api_key
92-
93- if environment is not None :
94- self .environment = _config .parse_environment (environment )
95- else :
96- self .environment = _config .environment
97-
98- self .max_retries = _config .max_retries if max_retries is None else max_retries
99- self .timeout = _config .timeout if timeout is None else timeout
100- validate_client_settings (self .timeout , self .max_retries )
101-
102- # Resolution order: explicit api_base > module-level shade.api_base
103- # > legacy base_url > environment URL
104- resolved = api_base or _config .api_base or base_url or self .environment .base_url
105- self ._base_url = resolved .rstrip ("/" )
83+ self ._api_key = api_key
84+ self ._environment = (
85+ _config .parse_environment (environment ) if environment is not None else None
86+ )
87+ api_base = api_base or (base_url if base_url else None )
88+ self ._api_base = api_base .rstrip ("/" ) if api_base else None
89+ self ._timeout = timeout
90+ self ._max_retries = max_retries
10691 self .debug = debug
10792
93+ if timeout is not None or max_retries is not None :
94+ validate_client_settings (
95+ timeout if timeout is not None else _config .timeout ,
96+ max_retries if max_retries is not None else _config .max_retries ,
97+ )
98+
10899 self ._http = SyncHTTPClient (
109- base_url = self ._base_url ,
110- api_key = self .api_key ,
111- max_retries = self .max_retries ,
112- timeout = self .timeout ,
100+ base_url = self ._api_base ,
101+ api_key = self ._api_key ,
102+ environment = self ._environment ,
103+ max_retries = self ._max_retries ,
104+ timeout = self ._timeout ,
113105 )
114106 self ._async_http = AsyncHTTPClient (
115- base_url = self ._base_url ,
116- api_key = self .api_key ,
117- max_retries = self .max_retries ,
118- timeout = self .timeout ,
107+ base_url = self ._api_base ,
108+ api_key = self ._api_key ,
109+ environment = self ._environment ,
110+ max_retries = self ._max_retries ,
111+ timeout = self ._timeout ,
119112 )
120113 self ._client = HTTPXTransport (
121- api_key = self .api_key ,
122- base_url = self ._base_url ,
114+ api_key = self ._api_key ,
115+ base_url = self ._api_base ,
116+ environment = self ._environment ,
123117 debug = debug ,
124118 http_client = http_client ,
125119 )
@@ -130,7 +124,8 @@ def from_env(cls, **overrides: Any) -> "ShadeClient":
130124
131125 Either variable may be absent, in which case the usual global-config
132126 fallback applies — so a missing ``SHADE_API_KEY`` with no
133- ``shade.api_key`` set raises :class:`~shade.errors.AuthenticationError`.
127+ ``shade.api_key`` set leaves the client without credentials, and its
128+ requests raise :class:`~shade.errors.AuthenticationError`.
134129
135130 Any keyword argument overrides the corresponding environment variable,
136131 letting callers take the key from the environment while setting the rest
@@ -148,9 +143,50 @@ def from_env(cls, **overrides: Any) -> "ShadeClient":
148143 env_kwargs .update (overrides )
149144 return cls (** env_kwargs )
150145
146+ @property
147+ def api_key (self ) -> Optional [str ]:
148+ return self ._api_key if self ._api_key is not None else _config .api_key
149+
150+ @api_key .setter
151+ def api_key (self , value : Optional [str ]) -> None :
152+ self ._api_key = value
153+ self ._http .api_key = value
154+ self ._async_http .api_key = value
155+ self ._client .api_key = value
156+
157+ @property
158+ def environment (self ) -> Environment :
159+ if self ._environment is not None :
160+ return self ._environment
161+ return _config .environment
162+
163+ @environment .setter
164+ def environment (self , value : str | Environment ) -> None :
165+ parsed = _config .parse_environment (value )
166+ self ._environment = parsed
167+ self ._http .environment = parsed
168+ self ._async_http .environment = parsed
169+ self ._client .environment = parsed
170+
171+ @property
172+ def timeout (self ) -> float :
173+ return self ._timeout if self ._timeout is not None else _config .timeout
174+
175+ @property
176+ def max_retries (self ) -> int :
177+ return self ._max_retries if self ._max_retries is not None else _config .max_retries
178+
179+ @property
180+ def _base_url (self ) -> str :
181+ if self ._api_base :
182+ return self ._api_base
183+ if _config .api_base :
184+ return _config .api_base .rstrip ("/" )
185+ return self .environment .base_url .rstrip ("/" )
186+
151187 @property
152188 def api_base (self ) -> str :
153- """The resolved API base URL this client sends requests to."""
189+ """The API base URL this client currently sends requests to."""
154190 return self ._base_url
155191
156192 def close (self ) -> None :
@@ -187,45 +223,34 @@ def __repr__(self) -> str:
187223 )
188224
189225
190- def _mask_api_key (api_key : str ) -> str :
226+ def _mask_api_key (api_key : Optional [ str ] ) -> str :
191227 """Show only the last four characters of a key, for use in reprs."""
228+ if not api_key :
229+ return "unset"
192230 if len (api_key ) <= 4 :
193231 return "****"
194232 return "*" * (len (api_key ) - 4 ) + api_key [- 4 :]
195233
196234
197235_default_client : Optional [ShadeClient ] = None
198- _default_client_settings : Optional [tuple ] = None
199236
200237
201238def default_client () -> ShadeClient :
202- """Return the shared client built from the global ``shade`` config.
239+ """Return the shared client backed by the global ``shade`` config.
203240
204241 Resources fall back to this when constructed without an explicit
205- ``client=``. The instance is cached, but rebuilt whenever a global setting
206- changes, so assigning ``shade.api_key`` after the first call still takes
207- effect.
208-
209- Raises:
210- AuthenticationError: If no global ``shade.api_key`` has been set.
242+ ``client=``. It pins no settings of its own, so every global change —
243+ including a ``shade.api_key`` assigned after the first call — is picked up
244+ on the next request.
211245 """
212- global _default_client , _default_client_settings
213-
214- settings = (
215- _config .api_key ,
216- _config .environment ,
217- _config .api_base ,
218- _config .timeout ,
219- _config .max_retries ,
220- )
221- if _default_client is None or _default_client_settings != settings :
246+ global _default_client
247+
248+ if _default_client is None :
222249 _default_client = ShadeClient ()
223- _default_client_settings = settings
224250 return _default_client
225251
226252
227253def reset_default_client () -> None :
228254 """Drop the cached global client. Primarily useful in tests."""
229- global _default_client , _default_client_settings
255+ global _default_client
230256 _default_client = None
231- _default_client_settings = None
0 commit comments