amigo_sdk 0.1.1__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.

Potentially problematic release.


This version of amigo_sdk might be problematic. Click here for more details.

amigo_sdk/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.1"
amigo_sdk/auth.py ADDED
@@ -0,0 +1,30 @@
1
+ import httpx
2
+
3
+ from amigo_sdk.config import AmigoConfig
4
+ from amigo_sdk.errors import AuthenticationError
5
+ from amigo_sdk.generated.model import UserSignInWithApiKeyResponse
6
+
7
+
8
+ async def sign_in_with_api_key(
9
+ cfg: AmigoConfig,
10
+ ) -> 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
+ response = await client.post(url, headers=headers)
23
+ response.raise_for_status()
24
+ except httpx.HTTPStatusError as e:
25
+ raise AuthenticationError(f"Sign in with API key failed: {e}") from e
26
+
27
+ try:
28
+ return UserSignInWithApiKeyResponse.model_validate_json(response.text)
29
+ except Exception as e:
30
+ raise AuthenticationError(f"Invalid response format: {e}") from e
amigo_sdk/config.py ADDED
@@ -0,0 +1,48 @@
1
+ from pydantic import Field
2
+ from pydantic_settings import BaseSettings
3
+
4
+
5
+ class AmigoConfig(BaseSettings):
6
+ """
7
+ Configuration for the Amigo API client.
8
+
9
+ Can be configured via three methods (in order of precedence):
10
+ 1. Constructor parameters (highest precedence)
11
+ 2. Environment variables with AMIGO_ prefix
12
+ 3. .env file in the current working directory (lowest precedence)
13
+
14
+ Environment variables:
15
+ - AMIGO_API_KEY
16
+ - AMIGO_API_KEY_ID
17
+ - AMIGO_USER_ID
18
+ - AMIGO_BASE_URL
19
+ - AMIGO_ORGANIZATION_ID
20
+
21
+ Example .env file:
22
+ ```
23
+ AMIGO_API_KEY=your_api_key_here
24
+ AMIGO_API_KEY_ID=your_api_key_id_here
25
+ AMIGO_USER_ID=your_user_id_here
26
+ AMIGO_ORGANIZATION_ID=your_org_id_here
27
+ AMIGO_BASE_URL=https://api.amigo.ai
28
+ ```
29
+ """
30
+
31
+ api_key: str = Field(..., description="API key for authentication")
32
+ api_key_id: str = Field(..., description="API key ID for authentication")
33
+ user_id: str = Field(..., description="User ID for API requests")
34
+ organization_id: str = Field(..., description="Organization ID for API requests")
35
+ base_url: str = Field(
36
+ default="https://api.amigo.ai",
37
+ description="Base URL for the Amigo API",
38
+ )
39
+
40
+ model_config = {
41
+ "env_prefix": "AMIGO_",
42
+ "env_file": ".env",
43
+ "env_file_encoding": "utf-8",
44
+ "case_sensitive": False,
45
+ "validate_assignment": True,
46
+ "frozen": True,
47
+ "extra": "ignore", # Ignore extra fields in .env file
48
+ }
amigo_sdk/errors.py ADDED
@@ -0,0 +1,163 @@
1
+ from typing import Any, Optional
2
+
3
+
4
+ class AmigoError(Exception):
5
+ """
6
+ Base class for Amigo API errors.
7
+ """
8
+
9
+ def __init__(
10
+ self,
11
+ message: str,
12
+ *,
13
+ status_code: Optional[int] = None,
14
+ error_code: Optional[str] = None,
15
+ response_body: Optional[Any] = None,
16
+ ) -> None:
17
+ super().__init__(message)
18
+ self.status_code = status_code
19
+ self.error_code = error_code
20
+ self.response_body = response_body
21
+
22
+ def __str__(self) -> str:
23
+ parts = [super().__str__()]
24
+ if self.status_code:
25
+ parts.append(f"(HTTP {self.status_code})")
26
+ if self.error_code:
27
+ parts.append(f"[{self.error_code}]")
28
+ return " ".join(parts)
29
+
30
+
31
+ # ---- 4xx client errors ------------------------------------------------------
32
+ class BadRequestError(AmigoError): # 400
33
+ pass
34
+
35
+
36
+ class AuthenticationError(AmigoError): # 401
37
+ pass
38
+
39
+
40
+ class PermissionError(AmigoError): # 403
41
+ pass
42
+
43
+
44
+ class NotFoundError(AmigoError): # 404
45
+ pass
46
+
47
+
48
+ class ConflictError(AmigoError): # 409
49
+ pass
50
+
51
+
52
+ class RateLimitError(AmigoError): # 429
53
+ pass
54
+
55
+
56
+ # ---- Validation / semantic errors ------------------------------------------
57
+ class ValidationError(BadRequestError): # 422 or 400 with `errors` list
58
+ def __init__(self, *args, field_errors: Optional[dict[str, str]] = None, **kwargs):
59
+ super().__init__(*args, **kwargs)
60
+ self.field_errors = field_errors or {}
61
+
62
+
63
+ # ---- 5xx server errors ------------------------------------------------------
64
+ class ServerError(AmigoError): # 500
65
+ pass
66
+
67
+
68
+ class ServiceUnavailableError(ServerError): # 503 / maintenance
69
+ pass
70
+
71
+
72
+ # ---- Internal SDK issues ----------------------------------------------------
73
+ class SDKInternalError(AmigoError):
74
+ """JSON decoding failure, Pydantic model mismatch, etc."""
75
+
76
+
77
+ # ---- Status code mapping ----------------------------------------------------
78
+ def get_error_class_for_status_code(status_code: int) -> type[AmigoError]:
79
+ """Map HTTP status codes to appropriate AmigoError classes."""
80
+ error_map = {
81
+ 400: BadRequestError,
82
+ 401: AuthenticationError,
83
+ 403: PermissionError,
84
+ 404: NotFoundError,
85
+ 409: ConflictError,
86
+ 422: ValidationError,
87
+ 429: RateLimitError,
88
+ 500: ServerError,
89
+ 503: ServiceUnavailableError,
90
+ }
91
+
92
+ # Default to appropriate base class for status code ranges
93
+ if status_code in error_map:
94
+ return error_map[status_code]
95
+ elif 400 <= status_code < 500:
96
+ return BadRequestError
97
+ elif 500 <= status_code < 600:
98
+ return ServerError
99
+ else:
100
+ return AmigoError
101
+
102
+
103
+ def raise_for_status(response, message: str = None) -> None:
104
+ """
105
+ Raise an appropriate AmigoError for non-2xx status codes.
106
+
107
+ Args:
108
+ response: httpx.Response object
109
+ message: Optional custom error message
110
+ """
111
+ if response.is_success:
112
+ return
113
+
114
+ status_code = response.status_code
115
+ error_class = get_error_class_for_status_code(status_code)
116
+
117
+ # Try to extract error details from response
118
+ error_code = None
119
+ response_body = None
120
+
121
+ try:
122
+ response_body = response.json()
123
+ # Try to extract error code if it exists in response
124
+ if isinstance(response_body, dict):
125
+ error_code = response_body.get("error_code") or response_body.get("code")
126
+ except Exception:
127
+ # If JSON parsing fails, use text content
128
+ try:
129
+ response_body = response.text
130
+ except Exception:
131
+ response_body = None
132
+
133
+ # Use provided message or generate default
134
+ if not message:
135
+ message = f"HTTP {status_code} error"
136
+ if isinstance(response_body, dict):
137
+ # Prefer common API error fields, including FastAPI's "detail"
138
+ for key in ("message", "error", "detail"):
139
+ api_message = response_body.get(key)
140
+ if api_message:
141
+ message = str(api_message)
142
+ break
143
+ elif isinstance(response_body, str) and response_body.strip():
144
+ # If the server returned a plain-text or JSON string body, surface it
145
+ message = response_body.strip()
146
+
147
+ # Handle ValidationError special case for field errors
148
+ if error_class == ValidationError and isinstance(response_body, dict):
149
+ field_errors = response_body.get("errors") or response_body.get("field_errors")
150
+ raise error_class(
151
+ message,
152
+ status_code=status_code,
153
+ error_code=error_code,
154
+ response_body=response_body,
155
+ field_errors=field_errors,
156
+ )
157
+ else:
158
+ raise error_class(
159
+ message,
160
+ status_code=status_code,
161
+ error_code=error_code,
162
+ response_body=response_body,
163
+ )