33from __future__ import annotations
44
55import os
6- from typing import Any , Mapping
7- from typing_extensions import Self , override
6+ from typing import Any , Dict , Mapping , cast
7+ from typing_extensions import Self , Literal , override
88
99import httpx
1010
2121)
2222from ._utils import is_given , get_async_library
2323from ._version import __version__
24- from .resources import devices , releases , deployments , config_instances
24+ from .resources import devices , releases , webhooks , deployments , config_instances
2525from ._streaming import Stream as Stream , AsyncStream as AsyncStream
2626from ._exceptions import MiruError , APIStatusError
2727from ._base_client import (
3030 AsyncAPIClient ,
3131)
3232
33- __all__ = ["Timeout" , "Transport" , "ProxiesTypes" , "RequestOptions" , "Miru" , "AsyncMiru" , "Client" , "AsyncClient" ]
33+ __all__ = [
34+ "ENVIRONMENTS" ,
35+ "Timeout" ,
36+ "Transport" ,
37+ "ProxiesTypes" ,
38+ "RequestOptions" ,
39+ "Miru" ,
40+ "AsyncMiru" ,
41+ "Client" ,
42+ "AsyncClient" ,
43+ ]
44+
45+ ENVIRONMENTS : Dict [str , str ] = {
46+ "production" : "https://configs.api.miruml.com/v1" ,
47+ "staging" : "https://configs.dev.api.miruml.com/v1" ,
48+ "local" : "http://localhost:8080/v1" ,
49+ }
3450
3551
3652class Miru (SyncAPIClient ):
3753 config_instances : config_instances .ConfigInstancesResource
3854 deployments : deployments .DeploymentsResource
3955 devices : devices .DevicesResource
4056 releases : releases .ReleasesResource
57+ webhooks : webhooks .WebhooksResource
4158 with_raw_response : MiruWithRawResponse
4259 with_streaming_response : MiruWithStreamedResponse
4360
@@ -46,13 +63,16 @@ class Miru(SyncAPIClient):
4663 host : str
4764 version : str
4865
66+ _environment : Literal ["production" , "staging" , "local" ] | NotGiven
67+
4968 def __init__ (
5069 self ,
5170 * ,
5271 api_key : str | None = None ,
5372 host : str | None = None ,
5473 version : str | None = None ,
55- base_url : str | httpx .URL | None = None ,
74+ environment : Literal ["production" , "staging" , "local" ] | NotGiven = not_given ,
75+ base_url : str | httpx .URL | None | NotGiven = not_given ,
5676 timeout : float | Timeout | None | NotGiven = not_given ,
5777 max_retries : int = DEFAULT_MAX_RETRIES ,
5878 default_headers : Mapping [str , str ] | None = None ,
@@ -94,10 +114,31 @@ def __init__(
94114 version = os .environ .get ("MIRU_SERVER_VERSION" ) or "v1"
95115 self .version = version
96116
97- if base_url is None :
98- base_url = os .environ .get ("MIRU_BASE_URL" )
99- if base_url is None :
100- base_url = f"https://{ host } /{ version } "
117+ self ._environment = environment
118+
119+ base_url_env = os .environ .get ("MIRU_BASE_URL" )
120+ if is_given (base_url ) and base_url is not None :
121+ # cast required because mypy doesn't understand the type narrowing
122+ base_url = cast ("str | httpx.URL" , base_url ) # pyright: ignore[reportUnnecessaryCast]
123+ elif is_given (environment ):
124+ if base_url_env and base_url is not None :
125+ raise ValueError (
126+ "Ambiguous URL; The `MIRU_BASE_URL` env var and the `environment` argument are given. If you want to use the environment, you must pass base_url=None" ,
127+ )
128+
129+ try :
130+ base_url = ENVIRONMENTS [environment ]
131+ except KeyError as exc :
132+ raise ValueError (f"Unknown environment: { environment } " ) from exc
133+ elif base_url_env is not None :
134+ base_url = base_url_env
135+ else :
136+ self ._environment = environment = "production"
137+
138+ try :
139+ base_url = ENVIRONMENTS [environment ]
140+ except KeyError as exc :
141+ raise ValueError (f"Unknown environment: { environment } " ) from exc
101142
102143 super ().__init__ (
103144 version = __version__ ,
@@ -114,6 +155,7 @@ def __init__(
114155 self .deployments = deployments .DeploymentsResource (self )
115156 self .devices = devices .DevicesResource (self )
116157 self .releases = releases .ReleasesResource (self )
158+ self .webhooks = webhooks .WebhooksResource (self )
117159 self .with_raw_response = MiruWithRawResponse (self )
118160 self .with_streaming_response = MiruWithStreamedResponse (self )
119161
@@ -143,6 +185,7 @@ def copy(
143185 api_key : str | None = None ,
144186 host : str | None = None ,
145187 version : str | None = None ,
188+ environment : Literal ["production" , "staging" , "local" ] | None = None ,
146189 base_url : str | httpx .URL | None = None ,
147190 timeout : float | Timeout | None | NotGiven = not_given ,
148191 http_client : httpx .Client | None = None ,
@@ -180,6 +223,7 @@ def copy(
180223 host = host or self .host ,
181224 version = version or self .version ,
182225 base_url = base_url or self .base_url ,
226+ environment = environment or self ._environment ,
183227 timeout = self .timeout if isinstance (timeout , NotGiven ) else timeout ,
184228 http_client = http_client ,
185229 max_retries = max_retries if is_given (max_retries ) else self .max_retries ,
@@ -231,6 +275,7 @@ class AsyncMiru(AsyncAPIClient):
231275 deployments : deployments .AsyncDeploymentsResource
232276 devices : devices .AsyncDevicesResource
233277 releases : releases .AsyncReleasesResource
278+ webhooks : webhooks .AsyncWebhooksResource
234279 with_raw_response : AsyncMiruWithRawResponse
235280 with_streaming_response : AsyncMiruWithStreamedResponse
236281
@@ -239,13 +284,16 @@ class AsyncMiru(AsyncAPIClient):
239284 host : str
240285 version : str
241286
287+ _environment : Literal ["production" , "staging" , "local" ] | NotGiven
288+
242289 def __init__ (
243290 self ,
244291 * ,
245292 api_key : str | None = None ,
246293 host : str | None = None ,
247294 version : str | None = None ,
248- base_url : str | httpx .URL | None = None ,
295+ environment : Literal ["production" , "staging" , "local" ] | NotGiven = not_given ,
296+ base_url : str | httpx .URL | None | NotGiven = not_given ,
249297 timeout : float | Timeout | None | NotGiven = not_given ,
250298 max_retries : int = DEFAULT_MAX_RETRIES ,
251299 default_headers : Mapping [str , str ] | None = None ,
@@ -287,10 +335,31 @@ def __init__(
287335 version = os .environ .get ("MIRU_SERVER_VERSION" ) or "v1"
288336 self .version = version
289337
290- if base_url is None :
291- base_url = os .environ .get ("MIRU_BASE_URL" )
292- if base_url is None :
293- base_url = f"https://{ host } /{ version } "
338+ self ._environment = environment
339+
340+ base_url_env = os .environ .get ("MIRU_BASE_URL" )
341+ if is_given (base_url ) and base_url is not None :
342+ # cast required because mypy doesn't understand the type narrowing
343+ base_url = cast ("str | httpx.URL" , base_url ) # pyright: ignore[reportUnnecessaryCast]
344+ elif is_given (environment ):
345+ if base_url_env and base_url is not None :
346+ raise ValueError (
347+ "Ambiguous URL; The `MIRU_BASE_URL` env var and the `environment` argument are given. If you want to use the environment, you must pass base_url=None" ,
348+ )
349+
350+ try :
351+ base_url = ENVIRONMENTS [environment ]
352+ except KeyError as exc :
353+ raise ValueError (f"Unknown environment: { environment } " ) from exc
354+ elif base_url_env is not None :
355+ base_url = base_url_env
356+ else :
357+ self ._environment = environment = "production"
358+
359+ try :
360+ base_url = ENVIRONMENTS [environment ]
361+ except KeyError as exc :
362+ raise ValueError (f"Unknown environment: { environment } " ) from exc
294363
295364 super ().__init__ (
296365 version = __version__ ,
@@ -307,6 +376,7 @@ def __init__(
307376 self .deployments = deployments .AsyncDeploymentsResource (self )
308377 self .devices = devices .AsyncDevicesResource (self )
309378 self .releases = releases .AsyncReleasesResource (self )
379+ self .webhooks = webhooks .AsyncWebhooksResource (self )
310380 self .with_raw_response = AsyncMiruWithRawResponse (self )
311381 self .with_streaming_response = AsyncMiruWithStreamedResponse (self )
312382
@@ -336,6 +406,7 @@ def copy(
336406 api_key : str | None = None ,
337407 host : str | None = None ,
338408 version : str | None = None ,
409+ environment : Literal ["production" , "staging" , "local" ] | None = None ,
339410 base_url : str | httpx .URL | None = None ,
340411 timeout : float | Timeout | None | NotGiven = not_given ,
341412 http_client : httpx .AsyncClient | None = None ,
@@ -373,6 +444,7 @@ def copy(
373444 host = host or self .host ,
374445 version = version or self .version ,
375446 base_url = base_url or self .base_url ,
447+ environment = environment or self ._environment ,
376448 timeout = self .timeout if isinstance (timeout , NotGiven ) else timeout ,
377449 http_client = http_client ,
378450 max_retries = max_retries if is_given (max_retries ) else self .max_retries ,
0 commit comments