amigo_sdk 0.8.0__py3-none-any.whl → 0.9.0__py3-none-any.whl

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.
amigo_sdk/__init__.py CHANGED
@@ -1 +1,8 @@
1
- __version__ = "0.8.0"
1
+ __version__ = "0.9.0"
2
+ from .sdk_client import AmigoClient, AsyncAmigoClient
3
+
4
+ __all__ = [
5
+ "__version__",
6
+ "AmigoClient",
7
+ "AsyncAmigoClient",
8
+ ]
@@ -0,0 +1,70 @@
1
+ import datetime as dt
2
+ import random
3
+ from email.utils import parsedate_to_datetime
4
+ from typing import Optional
5
+
6
+ DEFAULT_RETRYABLE_STATUS: set[int] = {429, 500, 502, 503, 504}
7
+
8
+
9
+ def parse_retry_after_seconds(retry_after: Optional[str]) -> float | None:
10
+ """Parse Retry-After header into seconds.
11
+
12
+ Supports both numeric seconds and HTTP-date formats. Returns None when
13
+ header is missing or invalid.
14
+ """
15
+ if not retry_after:
16
+ return None
17
+ # Numeric seconds
18
+ try:
19
+ seconds = float(retry_after)
20
+ return max(0.0, seconds)
21
+ except Exception:
22
+ pass
23
+ # HTTP-date format
24
+ try:
25
+ target_dt = parsedate_to_datetime(retry_after)
26
+ if target_dt is None:
27
+ return None
28
+ if target_dt.tzinfo is None:
29
+ target_dt = target_dt.replace(tzinfo=dt.UTC)
30
+ now = dt.datetime.now(dt.UTC)
31
+ delta_seconds = (target_dt - now).total_seconds()
32
+ return max(0.0, delta_seconds)
33
+ except Exception:
34
+ return None
35
+
36
+
37
+ def is_retryable_response(
38
+ method: str,
39
+ status_code: int,
40
+ headers: dict,
41
+ retry_on_methods: set[str],
42
+ retry_on_status: set[int],
43
+ ) -> bool:
44
+ """Determine if the response is retryable under our policy.
45
+
46
+ Special case: allow POST retry only on 429 when Retry-After is present.
47
+ """
48
+ method_upper = method.upper()
49
+ if method_upper == "POST" and status_code == 429 and headers.get("Retry-After"):
50
+ return True
51
+ return method_upper in retry_on_methods and status_code in retry_on_status
52
+
53
+
54
+ def compute_retry_delay_seconds(
55
+ attempt: int,
56
+ backoff_base: float,
57
+ max_delay_seconds: float,
58
+ retry_after_header: Optional[str],
59
+ ) -> float:
60
+ """Compute delay for a given retry attempt.
61
+
62
+ If Retry-After is present, honor it (clamped by max). Otherwise, use
63
+ exponential backoff with full jitter.
64
+ """
65
+ ra_seconds = parse_retry_after_seconds(retry_after_header)
66
+ if ra_seconds is not None:
67
+ return min(max_delay_seconds, ra_seconds)
68
+ window = backoff_base * (2 ** (attempt - 1))
69
+ window = min(window, max_delay_seconds)
70
+ return random.uniform(0.0, window)
amigo_sdk/auth.py CHANGED
@@ -5,26 +5,44 @@ from amigo_sdk.errors import AuthenticationError
5
5
  from amigo_sdk.generated.model import UserSignInWithApiKeyResponse
6
6
 
7
7
 
8
- async def sign_in_with_api_key(
9
- cfg: AmigoConfig,
8
+ def _signin_url_headers(cfg: AmigoConfig) -> tuple[str, dict[str, str]]:
9
+ url = f"{cfg.base_url}/v1/{cfg.organization_id}/user/signin_with_api_key"
10
+ headers = {
11
+ "x-api-key": cfg.api_key,
12
+ "x-api-key-id": cfg.api_key_id,
13
+ "x-user-id": cfg.user_id,
14
+ }
15
+ return url, headers
16
+
17
+
18
+ def _parse_signin_response_text(
19
+ response: httpx.Response,
10
20
  ) -> UserSignInWithApiKeyResponse:
11
- """
12
- Sign in with API key.
13
- """
14
- async with httpx.AsyncClient() as client:
15
- url = f"{cfg.base_url}/v1/{cfg.organization_id}/user/signin_with_api_key"
16
- headers = {
17
- "x-api-key": cfg.api_key,
18
- "x-api-key-id": cfg.api_key_id,
19
- "x-user-id": cfg.user_id,
20
- }
21
+ try:
22
+ return UserSignInWithApiKeyResponse.model_validate_json(response.text)
23
+ except Exception as e:
24
+ raise AuthenticationError(f"Invalid response format: {e}") from e
25
+
26
+
27
+ def sign_in_with_api_key(cfg: AmigoConfig) -> UserSignInWithApiKeyResponse:
28
+ """Sign in with API key (sync)."""
29
+ url, headers = _signin_url_headers(cfg)
30
+ with httpx.Client() as client:
21
31
  try:
22
- response = await client.post(url, headers=headers)
32
+ response = client.post(url, headers=headers)
23
33
  response.raise_for_status()
24
34
  except httpx.HTTPStatusError as e:
25
35
  raise AuthenticationError(f"Sign in with API key failed: {e}") from e
36
+ return _parse_signin_response_text(response)
26
37
 
38
+
39
+ async def sign_in_with_api_key_async(cfg: AmigoConfig) -> UserSignInWithApiKeyResponse:
40
+ """Sign in with API key (async)."""
41
+ url, headers = _signin_url_headers(cfg)
42
+ async with httpx.AsyncClient() as client:
27
43
  try:
28
- return UserSignInWithApiKeyResponse.model_validate_json(response.text)
29
- except Exception as e:
30
- raise AuthenticationError(f"Invalid response format: {e}") from e
44
+ response = await client.post(url, headers=headers)
45
+ response.raise_for_status()
46
+ except httpx.HTTPStatusError as e:
47
+ raise AuthenticationError(f"Sign in with API key failed: {e}") from e
48
+ return _parse_signin_response_text(response)
@@ -1,6 +1,6 @@
1
1
  # generated by datamodel-codegen:
2
2
  # filename: <stdin>
3
- # timestamp: 2025-08-21T18:39:42+00:00
3
+ # timestamp: 2025-08-25T20:27:06+00:00
4
4
 
5
5
  from __future__ import annotations
6
6
 
@@ -8330,8 +8330,6 @@ class LLMConfigOutput(BaseModel):
8330
8330
 
8331
8331
  class LLMLoadBalancingSetType(Enum):
8332
8332
  o4_mini_2025_04_16 = 'o4-mini-2025-04-16'
8333
- gpt_4_1_2025_04_14 = 'gpt-4.1-2025-04-14'
8334
- gpt_4_1_mini_2025_04_14 = 'gpt-4.1-mini-2025-04-14'
8335
8333
  gpt_5_2025_08_07 = 'gpt-5-2025-08-07'
8336
8334
  gpt_5_mini_2025_08_07 = 'gpt-5-mini-2025-08-07'
8337
8335
  gpt_5_nano_2025_08_07 = 'gpt-5-nano-2025-08-07'
@@ -8341,8 +8339,6 @@ class LLMLoadBalancingSetType(Enum):
8341
8339
 
8342
8340
  class LLMType(Enum):
8343
8341
  openai_o4_mini_2025_04_16 = 'openai_o4-mini-2025-04-16'
8344
- openai_gpt_4_1_2025_04_14 = 'openai_gpt-4.1-2025-04-14'
8345
- openai_gpt_4_1_mini_2025_04_14 = 'openai_gpt-4.1-mini-2025-04-14'
8346
8342
  openai_gpt_5_2025_08_07 = 'openai_gpt-5-2025-08-07'
8347
8343
  openai_gpt_5_mini_2025_08_07 = 'openai_gpt-5-mini-2025-08-07'
8348
8344
  openai_gpt_5_nano_2025_08_07 = 'openai_gpt-5-nano-2025-08-07'
@@ -11646,7 +11642,7 @@ class GetConversationMessagesParametersQuery(BaseModel):
11646
11642
  [], description='The IDs of the messages to retrieve.', title='Id'
11647
11643
  )
11648
11644
  message_type: Optional[List[MessageType]] = Field(
11649
- ['agent-message', 'user-message', 'external-event'],
11645
+ ['agent-message', 'external-event', 'user-message'],
11650
11646
  description='The type of messages to retrieve.',
11651
11647
  title='Message Type',
11652
11648
  )
amigo_sdk/http_client.py CHANGED
@@ -1,13 +1,15 @@
1
1
  import asyncio
2
2
  import datetime as dt
3
3
  import random
4
- from collections.abc import AsyncIterator
4
+ import time
5
+ from collections.abc import AsyncIterator, Iterator
6
+ from dataclasses import dataclass
5
7
  from email.utils import parsedate_to_datetime
6
8
  from typing import Any, Optional
7
9
 
8
10
  import httpx
9
11
 
10
- from amigo_sdk.auth import sign_in_with_api_key
12
+ from amigo_sdk.auth import sign_in_with_api_key, sign_in_with_api_key_async
11
13
  from amigo_sdk.config import AmigoConfig
12
14
  from amigo_sdk.errors import (
13
15
  AuthenticationError,
@@ -16,48 +18,33 @@ from amigo_sdk.errors import (
16
18
  )
17
19
  from amigo_sdk.generated.model import UserSignInWithApiKeyResponse
18
20
 
21
+ # -----------------------------
22
+ # Shared helpers and structures
23
+ # -----------------------------
19
24
 
20
- class AmigoHttpClient:
21
- def __init__(
22
- self,
23
- cfg: AmigoConfig,
24
- *,
25
- retry_max_attempts: int = 3,
26
- retry_backoff_base: float = 0.25,
27
- retry_max_delay_seconds: float = 30.0,
28
- retry_on_status: set[int] | None = None,
29
- retry_on_methods: set[str] | None = None,
30
- **httpx_kwargs: Any,
31
- ) -> None:
32
- self._cfg = cfg
33
- self._token: Optional[UserSignInWithApiKeyResponse] = None
34
- self._client = httpx.AsyncClient(
35
- base_url=cfg.base_url,
36
- **httpx_kwargs,
37
- )
38
- # Retry configuration
39
- self._retry_max_attempts = max(1, retry_max_attempts)
40
- self._retry_backoff_base = retry_backoff_base
41
- self._retry_max_delay_seconds = max(0.0, retry_max_delay_seconds)
42
- self._retry_on_status = retry_on_status or {408, 429, 500, 502, 503, 504}
43
- # Default to GET only; POST is handled specially for 429 + Retry-After
44
- self._retry_on_methods = {m.upper() for m in (retry_on_methods or {"GET"})}
45
25
 
46
- def _is_retryable_method(self, method: str) -> bool:
47
- return method.upper() in self._retry_on_methods
26
+ @dataclass
27
+ class _RetryConfig:
28
+ max_attempts: int
29
+ backoff_base: float
30
+ max_delay_seconds: float
31
+ on_status: set[int]
32
+ on_methods: set[str]
33
+
34
+ def is_retryable_method(self, method: str) -> bool:
35
+ return method.upper() in self.on_methods
48
36
 
49
- def _is_retryable_response(self, method: str, resp: httpx.Response) -> bool:
37
+ def is_retryable_response(self, method: str, resp: httpx.Response) -> bool:
50
38
  status = resp.status_code
51
- # Allow POST retry only for 429 when Retry-After header is present
52
39
  if (
53
40
  method.upper() == "POST"
54
41
  and status == 429
55
42
  and resp.headers.get("Retry-After")
56
43
  ):
57
44
  return True
58
- return self._is_retryable_method(method) and status in self._retry_on_status
45
+ return self.is_retryable_method(method) and status in self.on_status
59
46
 
60
- def _parse_retry_after_seconds(self, resp: httpx.Response) -> float | None:
47
+ def parse_retry_after_seconds(self, resp: httpx.Response) -> float | None:
61
48
  retry_after = resp.headers.get("Retry-After")
62
49
  if not retry_after:
63
50
  return None
@@ -76,28 +63,94 @@ class AmigoHttpClient:
76
63
  target_dt = target_dt.replace(tzinfo=dt.UTC)
77
64
  now = dt.datetime.now(dt.UTC)
78
65
  delta_seconds = (target_dt - now).total_seconds()
66
+ # Round to milliseconds to avoid borderline off-by-epsilon in tests
67
+ delta_seconds = round(delta_seconds, 3)
79
68
  return max(0.0, delta_seconds)
80
69
  except Exception:
81
70
  return None
82
71
 
83
- def _retry_delay_seconds(self, attempt: int, resp: httpx.Response | None) -> float:
72
+ def retry_delay_seconds(self, attempt: int, resp: httpx.Response | None) -> float:
84
73
  # Honor Retry-After when present (numeric or HTTP-date), clamped by max delay
85
74
  if resp is not None:
86
- ra_seconds = self._parse_retry_after_seconds(resp)
75
+ ra_seconds = self.parse_retry_after_seconds(resp)
87
76
  if ra_seconds is not None:
88
- return min(self._retry_max_delay_seconds, ra_seconds)
77
+ return min(self.max_delay_seconds, ra_seconds)
89
78
  # Exponential backoff with full jitter: U(0, min(cap, base * 2^(attempt-1)))
90
- window = self._retry_backoff_base * (2 ** (attempt - 1))
91
- window = min(window, self._retry_max_delay_seconds)
79
+ window = self.backoff_base * (2 ** (attempt - 1))
80
+ window = min(window, self.max_delay_seconds)
92
81
  return random.uniform(0.0, window)
93
82
 
83
+
84
+ def _should_refresh_token(token: Optional[UserSignInWithApiKeyResponse]) -> bool:
85
+ if not token:
86
+ return True
87
+ return dt.datetime.now(dt.UTC) > token.expires_at - dt.timedelta(minutes=5)
88
+
89
+
90
+ async def _raise_status_with_body_async(resp: httpx.Response) -> None:
91
+ if 200 <= resp.status_code < 300:
92
+ return
93
+ try:
94
+ await resp.aread()
95
+ except Exception:
96
+ pass
97
+ if hasattr(resp, "is_success"):
98
+ raise_for_status(resp)
99
+ error_class = get_error_class_for_status_code(getattr(resp, "status_code", 0))
100
+ raise error_class(
101
+ f"HTTP {getattr(resp, 'status_code', 'unknown')} error",
102
+ status_code=getattr(resp, "status_code", None),
103
+ )
104
+
105
+
106
+ def _raise_status_with_body_sync(resp: httpx.Response) -> None:
107
+ if 200 <= resp.status_code < 300:
108
+ return
109
+ try:
110
+ _ = resp.text
111
+ except Exception:
112
+ pass
113
+ if hasattr(resp, "is_success"):
114
+ raise_for_status(resp)
115
+ error_class = get_error_class_for_status_code(getattr(resp, "status_code", 0))
116
+ raise error_class(
117
+ f"HTTP {getattr(resp, 'status_code', 'unknown')} error",
118
+ status_code=getattr(resp, "status_code", None),
119
+ )
120
+
121
+
122
+ class AmigoAsyncHttpClient:
123
+ def __init__(
124
+ self,
125
+ cfg: AmigoConfig,
126
+ *,
127
+ retry_max_attempts: int = 3,
128
+ retry_backoff_base: float = 0.25,
129
+ retry_max_delay_seconds: float = 30.0,
130
+ retry_on_status: set[int] | None = None,
131
+ retry_on_methods: set[str] | None = None,
132
+ **httpx_kwargs: Any,
133
+ ) -> None:
134
+ self._cfg = cfg
135
+ self._token: Optional[UserSignInWithApiKeyResponse] = None
136
+ self._client = httpx.AsyncClient(
137
+ base_url=cfg.base_url,
138
+ **httpx_kwargs,
139
+ )
140
+ # Retry configuration
141
+ self._retry_cfg = _RetryConfig(
142
+ max(1, retry_max_attempts),
143
+ retry_backoff_base,
144
+ max(0.0, retry_max_delay_seconds),
145
+ retry_on_status or {408, 429, 500, 502, 503, 504},
146
+ {m.upper() for m in (retry_on_methods or {"GET"})},
147
+ )
148
+
94
149
  async def _ensure_token(self) -> str:
95
150
  """Fetch or refresh bearer token ~5 min before expiry."""
96
- if not self._token or dt.datetime.now(
97
- dt.UTC
98
- ) > self._token.expires_at - dt.timedelta(minutes=5):
151
+ if _should_refresh_token(self._token):
99
152
  try:
100
- self._token = await sign_in_with_api_key(self._cfg)
153
+ self._token = await sign_in_with_api_key_async(self._cfg)
101
154
  except Exception as e:
102
155
  raise AuthenticationError(
103
156
  "API-key exchange failed",
@@ -127,20 +180,20 @@ class AmigoHttpClient:
127
180
  except (httpx.TimeoutException, httpx.TransportError):
128
181
  # Retry only if method is allowed (e.g., GET); POST not retried for transport/timeouts
129
182
  if (
130
- not self._is_retryable_method(method)
131
- or attempt >= self._retry_max_attempts
183
+ not self._retry_cfg.is_retryable_method(method)
184
+ or attempt >= self._retry_cfg.max_attempts
132
185
  ):
133
186
  raise
134
- await asyncio.sleep(self._retry_delay_seconds(attempt, None))
187
+ await asyncio.sleep(self._retry_cfg.retry_delay_seconds(attempt, None))
135
188
  attempt += 1
136
189
  continue
137
190
 
138
191
  # Retry on configured HTTP status codes
139
192
  if (
140
- self._is_retryable_response(method, resp)
141
- and attempt < self._retry_max_attempts
193
+ self._retry_cfg.is_retryable_response(method, resp)
194
+ and attempt < self._retry_cfg.max_attempts
142
195
  ):
143
- await asyncio.sleep(self._retry_delay_seconds(attempt, resp))
196
+ await asyncio.sleep(self._retry_cfg.retry_delay_seconds(attempt, resp))
144
197
  attempt += 1
145
198
  continue
146
199
 
@@ -166,29 +219,8 @@ class AmigoHttpClient:
166
219
  headers["Authorization"] = f"Bearer {await self._ensure_token()}"
167
220
  headers.setdefault("Accept", "application/x-ndjson")
168
221
 
169
- async def _raise_status_with_body(resp: httpx.Response) -> None:
170
- """Ensure response body is buffered, then raise mapped error with details."""
171
- if 200 <= resp.status_code < 300:
172
- return
173
- # Fully buffer the body so raise_for_status() can extract JSON/text safely
174
- try:
175
- await resp.aread()
176
- except Exception:
177
- pass
178
- # If this is a real httpx.Response, use our rich raise_for_status
179
- if hasattr(resp, "is_success"):
180
- raise_for_status(resp)
181
- # Otherwise, fall back to lightweight error mapping used in tests' mock responses
182
- error_class = get_error_class_for_status_code(
183
- getattr(resp, "status_code", 0)
184
- )
185
- raise error_class(
186
- f"HTTP {getattr(resp, 'status_code', 'unknown')} error",
187
- status_code=getattr(resp, "status_code", None),
188
- )
189
-
190
222
  async def _yield_from_response(resp: httpx.Response) -> AsyncIterator[str]:
191
- await _raise_status_with_body(resp)
223
+ await _raise_status_with_body_async(resp)
192
224
  if abort_event and abort_event.is_set():
193
225
  return
194
226
  async for line in resp.aiter_lines():
@@ -221,8 +253,127 @@ class AmigoHttpClient:
221
253
  await self._client.aclose()
222
254
 
223
255
  # async-context-manager sugar
224
- async def __aenter__(self): # → async with AmigoHTTPClient(...) as http:
256
+ async def __aenter__(self): # → async with AmigoAsyncHttpClient(...) as http:
225
257
  return self
226
258
 
227
259
  async def __aexit__(self, *_):
228
260
  await self.aclose()
261
+
262
+
263
+ class AmigoHttpClient:
264
+ def __init__(
265
+ self,
266
+ cfg: AmigoConfig,
267
+ *,
268
+ retry_max_attempts: int = 3,
269
+ retry_backoff_base: float = 0.25,
270
+ retry_max_delay_seconds: float = 30.0,
271
+ retry_on_status: set[int] | None = None,
272
+ retry_on_methods: set[str] | None = None,
273
+ **httpx_kwargs: Any,
274
+ ) -> None:
275
+ self._cfg = cfg
276
+ self._token: Optional[UserSignInWithApiKeyResponse] = None
277
+ self._client = httpx.Client(base_url=cfg.base_url, **httpx_kwargs)
278
+ # Retry configuration
279
+ self._retry_cfg = _RetryConfig(
280
+ max(1, retry_max_attempts),
281
+ retry_backoff_base,
282
+ max(0.0, retry_max_delay_seconds),
283
+ retry_on_status or {408, 429, 500, 502, 503, 504},
284
+ {m.upper() for m in (retry_on_methods or {"GET"})},
285
+ )
286
+
287
+ def _ensure_token(self) -> str:
288
+ if _should_refresh_token(self._token):
289
+ try:
290
+ self._token = sign_in_with_api_key(self._cfg)
291
+ except Exception as e:
292
+ raise AuthenticationError("API-key exchange failed") from e
293
+ return self._token.id_token
294
+
295
+ def request(self, method: str, path: str, **kwargs) -> httpx.Response:
296
+ kwargs.setdefault("headers", {})
297
+ attempt = 1
298
+
299
+ while True:
300
+ kwargs["headers"]["Authorization"] = f"Bearer {self._ensure_token()}"
301
+
302
+ resp: httpx.Response | None = None
303
+ try:
304
+ resp = self._client.request(method, path, **kwargs)
305
+ if resp.status_code == 401:
306
+ self._token = None
307
+ kwargs["headers"]["Authorization"] = (
308
+ f"Bearer {self._ensure_token()}"
309
+ )
310
+ resp = self._client.request(method, path, **kwargs)
311
+
312
+ except (httpx.TimeoutException, httpx.TransportError):
313
+ if (
314
+ not self._retry_cfg.is_retryable_method(method)
315
+ ) or attempt >= self._retry_cfg.max_attempts:
316
+ raise
317
+ time.sleep(self._retry_cfg.retry_delay_seconds(attempt, None))
318
+ attempt += 1
319
+ continue
320
+
321
+ if (
322
+ self._retry_cfg.is_retryable_response(method, resp)
323
+ and attempt < self._retry_cfg.max_attempts
324
+ ):
325
+ time.sleep(self._retry_cfg.retry_delay_seconds(attempt, resp))
326
+ attempt += 1
327
+ continue
328
+
329
+ raise_for_status(resp)
330
+ return resp
331
+
332
+ def stream_lines(
333
+ self,
334
+ method: str,
335
+ path: str,
336
+ abort_flag: list[bool] | None = None,
337
+ **kwargs,
338
+ ) -> Iterator[str]:
339
+ kwargs.setdefault("headers", {})
340
+ headers = kwargs["headers"]
341
+ headers["Authorization"] = f"Bearer {self._ensure_token()}"
342
+ headers.setdefault("Accept", "application/x-ndjson")
343
+
344
+ def _yield_from_response(resp: httpx.Response) -> Iterator[str]:
345
+ _raise_status_with_body_sync(resp)
346
+ if abort_flag and abort_flag[0]:
347
+ return
348
+ for line in resp.iter_lines():
349
+ if abort_flag and abort_flag[0]:
350
+ return
351
+ line_stripped = (line or "").strip()
352
+ if not line_stripped:
353
+ continue
354
+ yield line_stripped
355
+
356
+ if abort_flag and abort_flag[0]:
357
+ return iter(())
358
+ with self._client.stream(method, path, **kwargs) as resp:
359
+ if resp.status_code == 401:
360
+ self._token = None
361
+ headers["Authorization"] = f"Bearer {self._ensure_token()}"
362
+ if abort_flag and abort_flag[0]:
363
+ return iter(())
364
+ with self._client.stream(method, path, **kwargs) as retry_resp:
365
+ for ln in _yield_from_response(retry_resp):
366
+ yield ln
367
+ return
368
+
369
+ for ln in _yield_from_response(resp):
370
+ yield ln
371
+
372
+ def aclose(self) -> None:
373
+ self._client.close()
374
+
375
+ def __enter__(self):
376
+ return self
377
+
378
+ def __exit__(self, *_):
379
+ self.aclose()
@@ -1,5 +1,5 @@
1
1
  import asyncio
2
- from collections.abc import AsyncGenerator
2
+ from collections.abc import AsyncGenerator, Iterator
3
3
  from datetime import datetime
4
4
  from typing import Any, Literal
5
5
 
@@ -21,7 +21,7 @@ from amigo_sdk.generated.model import (
21
21
  GetConversationsParametersQuery,
22
22
  InteractWithConversationParametersQuery,
23
23
  )
24
- from amigo_sdk.http_client import AmigoHttpClient
24
+ from amigo_sdk.http_client import AmigoAsyncHttpClient, AmigoHttpClient
25
25
 
26
26
 
27
27
  class GetMessageSourceResponse(BaseModel):
@@ -35,10 +35,10 @@ class GetMessageSourceResponse(BaseModel):
35
35
  content_type: Literal["audio/mpeg", "audio/wav"]
36
36
 
37
37
 
38
- class ConversationResource:
38
+ class AsyncConversationResource:
39
39
  """Conversation resource for Amigo API operations."""
40
40
 
41
- def __init__(self, http_client: AmigoHttpClient, organization_id: str) -> None:
41
+ def __init__(self, http_client: AmigoAsyncHttpClient, organization_id: str) -> None:
42
42
  self._http = http_client
43
43
  self._organization_id = organization_id
44
44
 
@@ -206,3 +206,155 @@ class ConversationResource:
206
206
  return ConversationGenerateConversationStarterResponse.model_validate_json(
207
207
  response.text
208
208
  )
209
+
210
+
211
+ class ConversationResource:
212
+ """Conversation resource for synchronous operations."""
213
+
214
+ def __init__(self, http_client: AmigoHttpClient, organization_id: str) -> None:
215
+ self._http = http_client
216
+ self._organization_id = organization_id
217
+
218
+ def create_conversation(
219
+ self,
220
+ body: ConversationCreateConversationRequest,
221
+ params: CreateConversationParametersQuery,
222
+ abort_flag: list[bool] | None = None,
223
+ ) -> Iterator[ConversationCreateConversationResponse]:
224
+ def _iter():
225
+ for line in self._http.stream_lines(
226
+ "POST",
227
+ f"/v1/{self._organization_id}/conversation/",
228
+ params=params.model_dump(mode="json", exclude_none=True),
229
+ json=body.model_dump(mode="json", exclude_none=True),
230
+ headers={"Accept": "application/x-ndjson"},
231
+ abort_flag=abort_flag,
232
+ ):
233
+ yield ConversationCreateConversationResponse.model_validate_json(line)
234
+
235
+ return _iter()
236
+
237
+ def interact_with_conversation(
238
+ self,
239
+ conversation_id: str,
240
+ params: InteractWithConversationParametersQuery,
241
+ abort_flag: list[bool] | None = None,
242
+ *,
243
+ text_message: str | None = None,
244
+ audio_bytes: bytes | None = None,
245
+ audio_content_type: Literal["audio/mpeg", "audio/wav"] | None = None,
246
+ ) -> Iterator[ConversationInteractWithConversationResponse]:
247
+ def _iter():
248
+ request_kwargs: dict[str, Any] = {
249
+ "params": params.model_dump(mode="json", exclude_none=True),
250
+ "headers": {"Accept": "application/x-ndjson"},
251
+ "abort_flag": abort_flag,
252
+ }
253
+ req_format = getattr(params, "request_format", None)
254
+ if req_format == Format.text:
255
+ if text_message is None:
256
+ raise ValueError(
257
+ "text_message is required when request_format is 'text'"
258
+ )
259
+ text_bytes = text_message.encode("utf-8")
260
+ request_kwargs["files"] = {
261
+ "recorded_message": (
262
+ "message.txt",
263
+ text_bytes,
264
+ "text/plain; charset=utf-8",
265
+ )
266
+ }
267
+ elif req_format == Format.voice:
268
+ if audio_bytes is None or audio_content_type is None:
269
+ raise ValueError(
270
+ "audio_bytes and audio_content_type are required when request_format is 'voice'"
271
+ )
272
+ request_kwargs["content"] = audio_bytes
273
+ request_kwargs.setdefault("headers", {})
274
+ request_kwargs["headers"]["Content-Type"] = audio_content_type
275
+ else:
276
+ raise ValueError("Unsupported or missing request_format in params")
277
+
278
+ for line in self._http.stream_lines(
279
+ "POST",
280
+ f"/v1/{self._organization_id}/conversation/{conversation_id}/interact",
281
+ **request_kwargs,
282
+ ):
283
+ yield ConversationInteractWithConversationResponse.model_validate_json(
284
+ line
285
+ )
286
+
287
+ return _iter()
288
+
289
+ def finish_conversation(self, conversation_id: str) -> None:
290
+ self._http.request(
291
+ "POST",
292
+ f"/v1/{self._organization_id}/conversation/{conversation_id}/finish/",
293
+ )
294
+
295
+ def get_conversations(
296
+ self, params: GetConversationsParametersQuery
297
+ ) -> ConversationGetConversationsResponse:
298
+ response = self._http.request(
299
+ "GET",
300
+ f"/v1/{self._organization_id}/conversation/",
301
+ params=params.model_dump(mode="json", exclude_none=True),
302
+ )
303
+ return ConversationGetConversationsResponse.model_validate_json(response.text)
304
+
305
+ def get_conversation_messages(
306
+ self, conversation_id: str, params: GetConversationMessagesParametersQuery
307
+ ) -> ConversationGetConversationMessagesResponse:
308
+ response = self._http.request(
309
+ "GET",
310
+ f"/v1/{self._organization_id}/conversation/{conversation_id}/messages/",
311
+ params=params.model_dump(
312
+ mode="json", exclude_none=True, exclude_defaults=True
313
+ ),
314
+ )
315
+ return ConversationGetConversationMessagesResponse.model_validate_json(
316
+ response.text
317
+ )
318
+
319
+ def recommend_responses_for_interaction(
320
+ self, conversation_id: str, interaction_id: str
321
+ ) -> ConversationRecommendResponsesForInteractionResponse:
322
+ response = self._http.request(
323
+ "GET",
324
+ f"/v1/{self._organization_id}/conversation/{conversation_id}/interaction/{interaction_id}/recommend_responses",
325
+ )
326
+ return ConversationRecommendResponsesForInteractionResponse.model_validate_json(
327
+ response.text
328
+ )
329
+
330
+ def get_interaction_insights(
331
+ self, conversation_id: str, interaction_id: str
332
+ ) -> ConversationGetInteractionInsightsResponse:
333
+ response = self._http.request(
334
+ "GET",
335
+ f"/v1/{self._organization_id}/conversation/{conversation_id}/interaction/{interaction_id}/insights",
336
+ )
337
+ return ConversationGetInteractionInsightsResponse.model_validate_json(
338
+ response.text
339
+ )
340
+
341
+ def get_message_source(
342
+ self, conversation_id: str, message_id: str
343
+ ) -> GetMessageSourceResponse:
344
+ response = self._http.request(
345
+ "GET",
346
+ f"/v1/{self._organization_id}/conversation/{conversation_id}/messages/{message_id}/source",
347
+ )
348
+ return GetMessageSourceResponse.model_validate_json(response.text)
349
+
350
+ def generate_conversation_starters(
351
+ self, body: ConversationGenerateConversationStarterRequest
352
+ ) -> ConversationGenerateConversationStarterResponse:
353
+ response = self._http.request(
354
+ "POST",
355
+ f"/v1/{self._organization_id}/conversation/conversation_starter",
356
+ json=body.model_dump(mode="json", exclude_none=True),
357
+ )
358
+ return ConversationGenerateConversationStarterResponse.model_validate_json(
359
+ response.text
360
+ )
@@ -1,13 +1,13 @@
1
1
  from amigo_sdk.generated.model import (
2
2
  OrganizationGetOrganizationResponse,
3
3
  )
4
- from amigo_sdk.http_client import AmigoHttpClient
4
+ from amigo_sdk.http_client import AmigoAsyncHttpClient, AmigoHttpClient
5
5
 
6
6
 
7
- class OrganizationResource:
7
+ class AsyncOrganizationResource:
8
8
  """Organization resource for Amigo API operations."""
9
9
 
10
- def __init__(self, http_client: AmigoHttpClient, organization_id: str) -> None:
10
+ def __init__(self, http_client: AmigoAsyncHttpClient, organization_id: str) -> None:
11
11
  self._http = http_client
12
12
  self._organization_id = organization_id
13
13
 
@@ -20,3 +20,15 @@ class OrganizationResource:
20
20
  )
21
21
 
22
22
  return OrganizationGetOrganizationResponse.model_validate_json(response.text)
23
+
24
+
25
+ class OrganizationResource:
26
+ def __init__(self, http_client: AmigoHttpClient, organization_id: str) -> None:
27
+ self._http = http_client
28
+ self._organization_id = organization_id
29
+
30
+ def get(self) -> OrganizationGetOrganizationResponse:
31
+ response = self._http.request(
32
+ "GET", f"/v1/{self._organization_id}/organization/"
33
+ )
34
+ return OrganizationGetOrganizationResponse.model_validate_json(response.text)
@@ -4,15 +4,13 @@ from amigo_sdk.generated.model import (
4
4
  GetServicesParametersQuery,
5
5
  ServiceGetServicesResponse,
6
6
  )
7
- from amigo_sdk.http_client import AmigoHttpClient
7
+ from amigo_sdk.http_client import AmigoAsyncHttpClient, AmigoHttpClient
8
8
 
9
9
 
10
- class ServiceResource:
10
+ class AsyncServiceResource:
11
11
  """Service resource for Amigo API operations."""
12
12
 
13
- def __init__(
14
- self, http_client: AmigoHttpClient, organization_id: str
15
- ) -> ServiceGetServicesResponse:
13
+ def __init__(self, http_client: AmigoAsyncHttpClient, organization_id: str) -> None:
16
14
  self._http = http_client
17
15
  self._organization_id = organization_id
18
16
 
@@ -28,3 +26,21 @@ class ServiceResource:
28
26
  else None,
29
27
  )
30
28
  return ServiceGetServicesResponse.model_validate_json(response.text)
29
+
30
+
31
+ class ServiceResource:
32
+ def __init__(self, http_client: AmigoHttpClient, organization_id: str) -> None:
33
+ self._http = http_client
34
+ self._organization_id = organization_id
35
+
36
+ def get_services(
37
+ self, params: Optional[GetServicesParametersQuery] = None
38
+ ) -> ServiceGetServicesResponse:
39
+ response = self._http.request(
40
+ "GET",
41
+ f"/v1/{self._organization_id}/service/",
42
+ params=params.model_dump(mode="json", exclude_none=True)
43
+ if params
44
+ else None,
45
+ )
46
+ return ServiceGetServicesResponse.model_validate_json(response.text)
@@ -7,13 +7,13 @@ from amigo_sdk.generated.model import (
7
7
  UserGetUsersResponse,
8
8
  UserUpdateUserInfoRequest,
9
9
  )
10
- from amigo_sdk.http_client import AmigoHttpClient
10
+ from amigo_sdk.http_client import AmigoAsyncHttpClient, AmigoHttpClient
11
11
 
12
12
 
13
- class UserResource:
13
+ class AsyncUserResource:
14
14
  """User resource for Amigo API operations."""
15
15
 
16
- def __init__(self, http_client: AmigoHttpClient, organization_id: str) -> None:
16
+ def __init__(self, http_client: AmigoAsyncHttpClient, organization_id: str) -> None:
17
17
  self._http = http_client
18
18
  self._organization_id = organization_id
19
19
 
@@ -55,3 +55,43 @@ class UserResource:
55
55
  f"/v1/{self._organization_id}/user/{user_id}/user",
56
56
  json=body.model_dump(mode="json", exclude_none=True),
57
57
  )
58
+
59
+
60
+ class UserResource:
61
+ """User resource (synchronous)."""
62
+
63
+ def __init__(self, http_client: AmigoHttpClient, organization_id: str) -> None:
64
+ self._http = http_client
65
+ self._organization_id = organization_id
66
+
67
+ def get_users(
68
+ self, params: Optional[GetUsersParametersQuery] = None
69
+ ) -> UserGetUsersResponse:
70
+ response = self._http.request(
71
+ "GET",
72
+ f"/v1/{self._organization_id}/user/",
73
+ params=params.model_dump(mode="json", exclude_none=True)
74
+ if params
75
+ else None,
76
+ )
77
+ return UserGetUsersResponse.model_validate_json(response.text)
78
+
79
+ def create_user(
80
+ self, body: UserCreateInvitedUserRequest
81
+ ) -> UserCreateInvitedUserResponse:
82
+ response = self._http.request(
83
+ "POST",
84
+ f"/v1/{self._organization_id}/user/invite",
85
+ json=body.model_dump(mode="json", exclude_none=True),
86
+ )
87
+ return UserCreateInvitedUserResponse.model_validate_json(response.text)
88
+
89
+ def delete_user(self, user_id: str) -> None:
90
+ self._http.request("DELETE", f"/v1/{self._organization_id}/user/{user_id}")
91
+
92
+ def update_user(self, user_id: str, body: UserUpdateUserInfoRequest) -> None:
93
+ self._http.request(
94
+ "POST",
95
+ f"/v1/{self._organization_id}/user/{user_id}/user",
96
+ json=body.model_dump(mode="json", exclude_none=True),
97
+ )
amigo_sdk/sdk_client.py CHANGED
@@ -1,17 +1,21 @@
1
1
  from typing import Any, Optional
2
2
 
3
3
  from amigo_sdk.config import AmigoConfig
4
- from amigo_sdk.http_client import AmigoHttpClient
5
- from amigo_sdk.resources.conversation import ConversationResource
6
- from amigo_sdk.resources.organization import OrganizationResource
7
- from amigo_sdk.resources.service import ServiceResource
8
- from amigo_sdk.resources.user import UserResource
4
+ from amigo_sdk.http_client import AmigoAsyncHttpClient, AmigoHttpClient
5
+ from amigo_sdk.resources.conversation import (
6
+ AsyncConversationResource,
7
+ ConversationResource,
8
+ )
9
+ from amigo_sdk.resources.organization import (
10
+ AsyncOrganizationResource,
11
+ OrganizationResource,
12
+ )
13
+ from amigo_sdk.resources.service import AsyncServiceResource, ServiceResource
14
+ from amigo_sdk.resources.user import AsyncUserResource, UserResource
9
15
 
10
16
 
11
- class AmigoClient:
12
- """
13
- Amigo API client
14
- """
17
+ class AsyncAmigoClient:
18
+ """Amigo API client (asynchronous)."""
15
19
 
16
20
  def __init__(
17
21
  self,
@@ -62,11 +66,15 @@ class AmigoClient:
62
66
  ) from e
63
67
 
64
68
  # Initialize HTTP client and resources
65
- self._http = AmigoHttpClient(self._cfg, **httpx_kwargs)
66
- self._organization = OrganizationResource(self._http, self._cfg.organization_id)
67
- self._service = ServiceResource(self._http, self._cfg.organization_id)
68
- self._conversation = ConversationResource(self._http, self._cfg.organization_id)
69
- self._users = UserResource(self._http, self._cfg.organization_id)
69
+ self._http = AmigoAsyncHttpClient(self._cfg, **httpx_kwargs)
70
+ self._organization = AsyncOrganizationResource(
71
+ self._http, self._cfg.organization_id
72
+ )
73
+ self._service = AsyncServiceResource(self._http, self._cfg.organization_id)
74
+ self._conversation = AsyncConversationResource(
75
+ self._http, self._cfg.organization_id
76
+ )
77
+ self._users = AsyncUserResource(self._http, self._cfg.organization_id)
70
78
 
71
79
  @property
72
80
  def config(self) -> AmigoConfig:
@@ -74,22 +82,22 @@ class AmigoClient:
74
82
  return self._cfg
75
83
 
76
84
  @property
77
- def organization(self) -> OrganizationResource:
85
+ def organization(self) -> AsyncOrganizationResource:
78
86
  """Access organization resource."""
79
87
  return self._organization
80
88
 
81
89
  @property
82
- def service(self) -> ServiceResource:
90
+ def service(self) -> AsyncServiceResource:
83
91
  """Access service resource."""
84
92
  return self._service
85
93
 
86
94
  @property
87
- def conversation(self) -> ConversationResource:
95
+ def conversation(self) -> AsyncConversationResource:
88
96
  """Access conversation resource."""
89
97
  return self._conversation
90
98
 
91
99
  @property
92
- def users(self) -> UserResource:
100
+ def users(self) -> AsyncUserResource:
93
101
  """Access user resource."""
94
102
  return self._users
95
103
 
@@ -103,3 +111,77 @@ class AmigoClient:
103
111
 
104
112
  async def __aexit__(self, *_):
105
113
  await self.aclose()
114
+
115
+
116
+ class AmigoClient:
117
+ """Amigo API client (synchronous)."""
118
+
119
+ def __init__(
120
+ self,
121
+ *,
122
+ api_key: Optional[str] = None,
123
+ api_key_id: Optional[str] = None,
124
+ user_id: Optional[str] = None,
125
+ organization_id: Optional[str] = None,
126
+ base_url: Optional[str] = None,
127
+ config: Optional[AmigoConfig] = None,
128
+ **httpx_kwargs: Any,
129
+ ):
130
+ if config:
131
+ self._cfg = config
132
+ else:
133
+ cfg_dict: dict[str, Any] = {
134
+ k: v
135
+ for k, v in [
136
+ ("api_key", api_key),
137
+ ("api_key_id", api_key_id),
138
+ ("user_id", user_id),
139
+ ("organization_id", organization_id),
140
+ ("base_url", base_url),
141
+ ]
142
+ if v is not None
143
+ }
144
+
145
+ try:
146
+ self._cfg = AmigoConfig(**cfg_dict)
147
+ except Exception as e:
148
+ raise ValueError(
149
+ "AmigoClient configuration incomplete. "
150
+ "Provide api_key, api_key_id, user_id, organization_id, base_url "
151
+ "either as kwargs or environment variables."
152
+ ) from e
153
+
154
+ self._http = AmigoHttpClient(self._cfg, **httpx_kwargs)
155
+ self._organization = OrganizationResource(self._http, self._cfg.organization_id)
156
+ self._service = ServiceResource(self._http, self._cfg.organization_id)
157
+ self._conversation = ConversationResource(self._http, self._cfg.organization_id)
158
+ self._users = UserResource(self._http, self._cfg.organization_id)
159
+
160
+ @property
161
+ def config(self) -> AmigoConfig:
162
+ return self._cfg
163
+
164
+ @property
165
+ def organization(self) -> OrganizationResource:
166
+ return self._organization
167
+
168
+ @property
169
+ def service(self) -> ServiceResource:
170
+ return self._service
171
+
172
+ @property
173
+ def conversation(self) -> ConversationResource:
174
+ return self._conversation
175
+
176
+ @property
177
+ def users(self) -> UserResource:
178
+ return self._users
179
+
180
+ def aclose(self) -> None:
181
+ self._http.aclose()
182
+
183
+ def __enter__(self):
184
+ return self
185
+
186
+ def __exit__(self, *_):
187
+ self.aclose()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: amigo_sdk
3
- Version: 0.8.0
3
+ Version: 0.9.0
4
4
  Summary: Amigo AI Python SDK
5
5
  Author: Amigo AI
6
6
  License-File: LICENSE
@@ -46,34 +46,23 @@ amigo_sdk
46
46
 
47
47
  This SDK auto-generates its types from the latest [Amigo OpenAPI schema](https://api.amigo.ai/v1/openapi.json). As a result, only the latest published SDK version is guaranteed to match the current API. If you pin to an older version, it may not include the newest endpoints or fields.
48
48
 
49
- ## Quick Start
49
+ ## Quick Start (sync)
50
50
 
51
51
  ```python
52
- import asyncio
53
52
  from amigo_sdk import AmigoClient
54
53
  from amigo_sdk.generated.model import GetConversationsParametersQuery
55
54
 
56
- # Initialize the client
57
- client = AmigoClient(
55
+ # Initialize and use the client synchronously
56
+ with AmigoClient(
58
57
  api_key="your-api-key",
59
58
  api_key_id="your-api-key-id",
60
59
  user_id="user-id",
61
- org_id="your-organization-id",
62
- )
63
-
64
- # List recent conversations
65
- async def example():
66
- try:
67
- async with client:
68
- conversations = await client.conversation.get_conversations(
69
- GetConversationsParametersQuery(limit=10, sort_by=["-created_at"])
70
- )
71
- print("Conversations:", conversations)
72
- except Exception as error:
73
- print(error)
74
-
75
- # Run the example
76
- asyncio.run(example())
60
+ organization_id="your-organization-id",
61
+ ) as client:
62
+ conversations = client.conversation.get_conversations(
63
+ GetConversationsParametersQuery(limit=10, sort_by=["-created_at"])
64
+ )
65
+ print("Conversations:", conversations)
77
66
  ```
78
67
 
79
68
  ## Examples
@@ -84,13 +73,13 @@ For more SDK usage examples see checkout the [examples/](examples/README.md) fol
84
73
 
85
74
  The SDK requires the following configuration parameters:
86
75
 
87
- | Parameter | Type | Required | Description |
88
- | ------------ | ---- | -------- | -------------------------------------------------------------- |
89
- | `api_key` | str | ✅ | API key from Amigo dashboard |
90
- | `api_key_id` | str | ✅ | API key ID from Amigo dashboard |
91
- | `user_id` | str | ✅ | User ID on whose behalf the request is made |
92
- | `org_id` | str | ✅ | Your organization ID |
93
- | `base_url` | str | ❌ | Base URL of the Amigo API (defaults to `https://api.amigo.ai`) |
76
+ | Parameter | Type | Required | Description |
77
+ | ----------------- | ---- | -------- | -------------------------------------------------------------- |
78
+ | `api_key` | str | ✅ | API key from Amigo dashboard |
79
+ | `api_key_id` | str | ✅ | API key ID from Amigo dashboard |
80
+ | `user_id` | str | ✅ | User ID on whose behalf the request is made |
81
+ | `organization_id` | str | ✅ | Your organization ID |
82
+ | `base_url` | str | ❌ | Base URL of the Amigo API (defaults to `https://api.amigo.ai`) |
94
83
 
95
84
  ### Environment Variables
96
85
 
@@ -100,7 +89,7 @@ You can also configure the SDK using environment variables:
100
89
  export AMIGO_API_KEY="your-api-key"
101
90
  export AMIGO_API_KEY_ID="your-api-key-id"
102
91
  export AMIGO_USER_ID="user-id"
103
- export AMIGO_ORG_ID="your-organization-id"
92
+ export AMIGO_ORGANIZATION_ID="your-organization-id"
104
93
  export AMIGO_BASE_URL="https://api.amigo.ai" # optional
105
94
  ```
106
95
 
@@ -110,7 +99,8 @@ Then initialize the client without parameters:
110
99
  from amigo_sdk import AmigoClient
111
100
 
112
101
  # Automatically loads from environment variables
113
- client = AmigoClient()
102
+ with AmigoClient() as client:
103
+ ...
114
104
  ```
115
105
 
116
106
  ### Using .env Files
@@ -153,25 +143,23 @@ from amigo_sdk.errors import (
153
143
  AuthenticationError,
154
144
  NotFoundError,
155
145
  BadRequestError,
156
- ValidationError
146
+ ValidationError,
157
147
  )
158
148
 
159
- async def example_with_error_handling():
160
- client = AmigoClient()
161
-
162
- try:
163
- async with client:
164
- result = await client.organizations.get_organization("org-id")
165
- except AuthenticationError as error:
166
- print("Authentication failed:", error)
167
- except NotFoundError as error:
168
- print("Resource not found:", error)
169
- except BadRequestError as error:
170
- print("Bad request:", error)
171
- except ValidationError as error:
172
- print("Validation error:", error)
173
- except Exception as error:
174
- print("Unexpected error:", error)
149
+ try:
150
+ with AmigoClient() as client:
151
+ org = client.organization.get()
152
+ print("Organization:", org)
153
+ except AuthenticationError as error:
154
+ print("Authentication failed:", error)
155
+ except NotFoundError as error:
156
+ print("Resource not found:", error)
157
+ except BadRequestError as error:
158
+ print("Bad request:", error)
159
+ except ValidationError as error:
160
+ print("Validation error:", error)
161
+ except Exception as error:
162
+ print("Unexpected error:", error)
175
163
  ```
176
164
 
177
165
  ## Development
@@ -0,0 +1,17 @@
1
+ amigo_sdk/__init__.py,sha256=kCOODDDBTcYl4OwHYOLi80txgJizsTwkNb49c88xU0M,153
2
+ amigo_sdk/_retry_utils.py,sha256=kFjw9Wqye6MB5-B4rjLxsbSNcfYBIztcollIoncd1hY,2142
3
+ amigo_sdk/auth.py,sha256=WaM9PcEcnaC6CzNsgRKueHkdSAxNbRylzpR_3Q6guQ0,1765
4
+ amigo_sdk/config.py,sha256=0eZIo-hcJ8ODftKAr-mwB-FGJxGO5PT5T4dRpyWPqAg,1491
5
+ amigo_sdk/errors.py,sha256=RkRyF5eAASd8fIOS6YvL9rLDvLAYWqHfpHSCR7jqvl4,4840
6
+ amigo_sdk/http_client.py,sha256=z8h8FKHRxGzULRz_C60mL5PfYSAv8e_RHUndVo0vHrM,13452
7
+ amigo_sdk/sdk_client.py,sha256=Kr9M9o66pOLu0T2VDvqdYMmPZzgKJyTELu7BSPgGrYQ,6152
8
+ amigo_sdk/generated/model.py,sha256=jYhncFPqXUiwOSmV6OJT82TpkmjG5LZH_gFRFr7mTEs,427161
9
+ amigo_sdk/resources/conversation.py,sha256=pGM2vtUsem8ClVfzZne1qqKZdM4o7aWaRdAYXXKEMFw,14784
10
+ amigo_sdk/resources/organization.py,sha256=yX4UlOHNegRzFW4gCJrCxjiLCAGnGegasjviR1yad_Q,1211
11
+ amigo_sdk/resources/service.py,sha256=SiwEHXCQk4r1b_tGv47M08VuB7RALDHJQzWlpuD937g,1571
12
+ amigo_sdk/resources/user.py,sha256=i4t5aVzBI37KwAtLKSDWTMwf4D4KQdSDoUiblFe1u7o,3529
13
+ amigo_sdk-0.9.0.dist-info/METADATA,sha256=KOQJZyfLcA-ZbG8w22B24yP4XUvUDiUbeUct_4gWMyE,5982
14
+ amigo_sdk-0.9.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
15
+ amigo_sdk-0.9.0.dist-info/entry_points.txt,sha256=ivKZ8S9W6SH796zUDHeM-qHodrwmkmUItophi-jJWK0,82
16
+ amigo_sdk-0.9.0.dist-info/licenses/LICENSE,sha256=tx3FiTVbGxwBUOxQbNh05AAQlC2jd5hGvNpIkSfVbCo,1062
17
+ amigo_sdk-0.9.0.dist-info/RECORD,,
@@ -1,16 +0,0 @@
1
- amigo_sdk/__init__.py,sha256=iPlYCcIzuzW7T2HKDkmYlMkRI51dBLfNRxPPiWrfw9U,22
2
- amigo_sdk/auth.py,sha256=Kvwk4P2HJaVUIn2Lwj7l6_xTm0IOVxMlthiXthm-V2Y,1029
3
- amigo_sdk/config.py,sha256=0eZIo-hcJ8ODftKAr-mwB-FGJxGO5PT5T4dRpyWPqAg,1491
4
- amigo_sdk/errors.py,sha256=RkRyF5eAASd8fIOS6YvL9rLDvLAYWqHfpHSCR7jqvl4,4840
5
- amigo_sdk/http_client.py,sha256=XDtcQnAXf4E46Kjl6tKVVFgrMgozjCbOcDf21aK_c4s,8843
6
- amigo_sdk/sdk_client.py,sha256=yLK8f0ARxLOlJX_cM-Y1GW-zCgJWsL2LIrWMxQGaMhY,3685
7
- amigo_sdk/generated/model.py,sha256=JSpAiUZ_1vrAp7pG3EcOdlXpFAG8w0hd0qKDo_orw9E,427393
8
- amigo_sdk/resources/conversation.py,sha256=gRkZwqLGOscqoCFcagwU-NfTeMuvLsuEqN_uLyNu1NI,8514
9
- amigo_sdk/resources/organization.py,sha256=HNwUgeggeEklvcwFS7Of6nGDawN-_Uvd9NsXtcYg65o,726
10
- amigo_sdk/resources/service.py,sha256=DrKaLnKAglcCeZJQEw50hAOLWtW_InnOu551TxgwF60,947
11
- amigo_sdk/resources/user.py,sha256=Y66Eb5kH3sYWpdx1E9PP8slban8jpUHmig3YEXHaD_Y,2066
12
- amigo_sdk-0.8.0.dist-info/METADATA,sha256=UIgZqBJj4VYy8lbcqRIg_CtEaGCzafi0s2KlAXGHz88,6215
13
- amigo_sdk-0.8.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
14
- amigo_sdk-0.8.0.dist-info/entry_points.txt,sha256=ivKZ8S9W6SH796zUDHeM-qHodrwmkmUItophi-jJWK0,82
15
- amigo_sdk-0.8.0.dist-info/licenses/LICENSE,sha256=tx3FiTVbGxwBUOxQbNh05AAQlC2jd5hGvNpIkSfVbCo,1062
16
- amigo_sdk-0.8.0.dist-info/RECORD,,