vortex-python-sdk 0.0.5__tar.gz → 0.1.0__tar.gz

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.
@@ -7,6 +7,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Changed
11
+
12
+ - **JWT Payload Simplification**: Updated JWT structure to use simplified fields (backward compatible)
13
+ - **NEW (Preferred)**: Use `User` object with `id`, `email`, and `admin_scopes` for cleaner, more maintainable code
14
+ - `id` (string): User's ID in your system
15
+ - `email` (string): User's email address - replaces the `identifiers` array
16
+ - `admin_scopes` (list): List of admin scopes (e.g., `['autojoin']` for autojoin admin privileges)
17
+ - **DEPRECATED (Still Supported)**: `identifiers`, `groups`, and `role` fields maintained for backward compatibility
18
+ - Supports additional properties via Pydantic's `extra="allow"` configuration
19
+ - All existing integrations will continue to work without changes
20
+
21
+ ### Migration Guide (Optional - Backward Compatible)
22
+
23
+ The old format still works, but we recommend migrating to the simpler structure:
24
+
25
+ **New simplified format (recommended):**
26
+
27
+ ```python
28
+ user = {
29
+ "id": "user-123",
30
+ "email": "user@example.com",
31
+ "admin_scopes": ["autojoin"] # optional: grants autojoin admin privileges
32
+ }
33
+ jwt = vortex.generate_jwt(user=user)
34
+ ```
35
+
36
+ **Old format (still supported):**
37
+
38
+ ```python
39
+ jwt = vortex.generate_jwt({
40
+ "user_id": "user-123",
41
+ "identifiers": [{"type": "email", "value": "user@example.com"}],
42
+ "groups": [{"type": "team", "id": "team-1", "name": "Engineering"}],
43
+ "role": "admin"
44
+ })
45
+ ```
46
+
10
47
  ## [0.0.5] - 2025-11-06
11
48
 
12
49
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vortex-python-sdk
3
- Version: 0.0.5
3
+ Version: 0.1.0
4
4
  Summary: Vortex Python SDK for invitation management and JWT generation
5
5
  Author-email: TeamVortexSoftware <support@vortexsoftware.com>
6
6
  License-Expression: MIT
@@ -66,34 +66,32 @@ vortex = Vortex(api_key="your-vortex-api-key", base_url="https://custom-api.exam
66
66
 
67
67
  ```python
68
68
  # Generate JWT for a user
69
- jwt = vortex.generate_jwt({
70
- "user_id": "user-123",
71
- "identifiers": [
72
- {"type": "email", "value": "user@example.com"}
73
- ],
74
- "groups": [
75
- {"type": "team", "id": "team-1", "name": "Engineering"}
76
- ],
77
- "role": "admin"
78
- })
69
+ user = {
70
+ "id": "user-123",
71
+ "email": "user@example.com",
72
+ "admin_scopes": ["autojoin"] # Optional - included as adminScopes array in JWT
73
+ }
79
74
 
75
+ jwt = vortex.generate_jwt(user=user)
80
76
  print(f"JWT: {jwt}")
81
77
 
78
+ # With additional properties
79
+ jwt = vortex.generate_jwt(
80
+ user=user,
81
+ role="admin",
82
+ department="Engineering"
83
+ )
84
+
82
85
  # Or using type-safe models
83
- from vortex_sdk import JwtPayload, IdentifierInput, GroupInput
86
+ from vortex_sdk import User
84
87
 
85
- jwt = vortex.generate_jwt(
86
- JwtPayload(
87
- user_id="user-123",
88
- identifiers=[
89
- IdentifierInput(type="email", value="user@example.com")
90
- ],
91
- groups=[
92
- GroupInput(type="team", id="team-1", name="Engineering")
93
- ],
94
- role="admin"
95
- )
88
+ user = User(
89
+ id="user-123",
90
+ email="user@example.com",
91
+ admin_scopes=["autojoin"]
96
92
  )
93
+
94
+ jwt = vortex.generate_jwt(user=user)
97
95
  ```
98
96
 
99
97
  ### Invitation Management
@@ -28,34 +28,32 @@ vortex = Vortex(api_key="your-vortex-api-key", base_url="https://custom-api.exam
28
28
 
29
29
  ```python
30
30
  # Generate JWT for a user
31
- jwt = vortex.generate_jwt({
32
- "user_id": "user-123",
33
- "identifiers": [
34
- {"type": "email", "value": "user@example.com"}
35
- ],
36
- "groups": [
37
- {"type": "team", "id": "team-1", "name": "Engineering"}
38
- ],
39
- "role": "admin"
40
- })
31
+ user = {
32
+ "id": "user-123",
33
+ "email": "user@example.com",
34
+ "admin_scopes": ["autojoin"] # Optional - included as adminScopes array in JWT
35
+ }
41
36
 
37
+ jwt = vortex.generate_jwt(user=user)
42
38
  print(f"JWT: {jwt}")
43
39
 
40
+ # With additional properties
41
+ jwt = vortex.generate_jwt(
42
+ user=user,
43
+ role="admin",
44
+ department="Engineering"
45
+ )
46
+
44
47
  # Or using type-safe models
45
- from vortex_sdk import JwtPayload, IdentifierInput, GroupInput
48
+ from vortex_sdk import User
46
49
 
47
- jwt = vortex.generate_jwt(
48
- JwtPayload(
49
- user_id="user-123",
50
- identifiers=[
51
- IdentifierInput(type="email", value="user@example.com")
52
- ],
53
- groups=[
54
- GroupInput(type="team", id="team-1", name="Engineering")
55
- ],
56
- role="admin"
57
- )
50
+ user = User(
51
+ id="user-123",
52
+ email="user@example.com",
53
+ admin_scopes=["autojoin"]
58
54
  )
55
+
56
+ jwt = vortex.generate_jwt(user=user)
59
57
  ```
60
58
 
61
59
  ### Invitation Management
@@ -209,4 +207,4 @@ mypy src/
209
207
 
210
208
  ## License
211
209
 
212
- MIT
210
+ MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "vortex-python-sdk"
7
- version = "0.0.5"
7
+ version = "0.1.0"
8
8
  description = "Vortex Python SDK for invitation management and JWT generation"
9
9
  authors = [{name = "TeamVortexSoftware", email = "support@vortexsoftware.com"}]
10
10
  readme = "README.md"
@@ -68,7 +68,7 @@ profile = "black"
68
68
  line_length = 88
69
69
 
70
70
  [tool.mypy]
71
- python_version = "3.8"
71
+ python_version = "3.9"
72
72
  warn_return_any = true
73
73
  warn_unused_configs = true
74
74
  disallow_untyped_defs = true
@@ -81,8 +81,10 @@ warn_no_return = true
81
81
  warn_unreachable = true
82
82
 
83
83
  [tool.ruff]
84
- target-version = "py38"
84
+ target-version = "py39"
85
85
  line-length = 88
86
+
87
+ [tool.ruff.lint]
86
88
  select = [
87
89
  "E", # pycodestyle errors
88
90
  "W", # pycodestyle warnings
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vortex-python-sdk
3
- Version: 0.0.5
3
+ Version: 0.1.0
4
4
  Summary: Vortex Python SDK for invitation management and JWT generation
5
5
  Author-email: TeamVortexSoftware <support@vortexsoftware.com>
6
6
  License-Expression: MIT
@@ -66,34 +66,32 @@ vortex = Vortex(api_key="your-vortex-api-key", base_url="https://custom-api.exam
66
66
 
67
67
  ```python
68
68
  # Generate JWT for a user
69
- jwt = vortex.generate_jwt({
70
- "user_id": "user-123",
71
- "identifiers": [
72
- {"type": "email", "value": "user@example.com"}
73
- ],
74
- "groups": [
75
- {"type": "team", "id": "team-1", "name": "Engineering"}
76
- ],
77
- "role": "admin"
78
- })
69
+ user = {
70
+ "id": "user-123",
71
+ "email": "user@example.com",
72
+ "admin_scopes": ["autojoin"] # Optional - included as adminScopes array in JWT
73
+ }
79
74
 
75
+ jwt = vortex.generate_jwt(user=user)
80
76
  print(f"JWT: {jwt}")
81
77
 
78
+ # With additional properties
79
+ jwt = vortex.generate_jwt(
80
+ user=user,
81
+ role="admin",
82
+ department="Engineering"
83
+ )
84
+
82
85
  # Or using type-safe models
83
- from vortex_sdk import JwtPayload, IdentifierInput, GroupInput
86
+ from vortex_sdk import User
84
87
 
85
- jwt = vortex.generate_jwt(
86
- JwtPayload(
87
- user_id="user-123",
88
- identifiers=[
89
- IdentifierInput(type="email", value="user@example.com")
90
- ],
91
- groups=[
92
- GroupInput(type="team", id="team-1", name="Engineering")
93
- ],
94
- role="admin"
95
- )
88
+ user = User(
89
+ id="user-123",
90
+ email="user@example.com",
91
+ admin_scopes=["autojoin"]
96
92
  )
93
+
94
+ jwt = vortex.generate_jwt(user=user)
97
95
  ```
98
96
 
99
97
  ### Invitation Management
@@ -5,20 +5,26 @@ A Python SDK for Vortex invitation management and JWT generation.
5
5
  """
6
6
 
7
7
  from .types import (
8
+ AcceptInvitationRequest,
8
9
  AcceptInvitationsRequest,
10
+ ApiRequestBody,
9
11
  ApiResponse,
12
+ ApiResponseJson,
10
13
  AuthenticatedUser,
11
14
  CreateInvitationRequest,
12
15
  GroupInput,
13
16
  IdentifierInput,
14
17
  Invitation,
18
+ InvitationAcceptance,
19
+ InvitationGroup,
20
+ InvitationResult,
15
21
  InvitationTarget,
16
22
  JwtPayload,
17
23
  VortexApiError,
18
24
  )
19
25
  from .vortex import Vortex
20
26
 
21
- __version__ = "0.0.5"
27
+ __version__ = "0.0.6"
22
28
  __author__ = "TeamVortexSoftware"
23
29
  __email__ = "support@vortexsoftware.com"
24
30
 
@@ -29,9 +35,15 @@ __all__ = [
29
35
  "IdentifierInput",
30
36
  "GroupInput",
31
37
  "InvitationTarget",
32
- "Invitation",
38
+ "InvitationGroup",
39
+ "InvitationAcceptance",
40
+ "InvitationResult",
41
+ "Invitation", # Alias for InvitationResult
33
42
  "CreateInvitationRequest",
34
- "AcceptInvitationsRequest",
43
+ "AcceptInvitationRequest",
44
+ "AcceptInvitationsRequest", # Alias for AcceptInvitationRequest
35
45
  "ApiResponse",
46
+ "ApiResponseJson",
47
+ "ApiRequestBody",
36
48
  "VortexApiError",
37
49
  ]
@@ -0,0 +1,232 @@
1
+ from typing import Any, Dict, List, Literal, Optional, Union
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class IdentifierInput(BaseModel):
7
+ """Identifier structure for JWT generation"""
8
+
9
+ type: Literal["email", "sms"]
10
+ value: str
11
+
12
+
13
+ class GroupInput(BaseModel):
14
+ """Group structure for JWT generation (input)"""
15
+
16
+ type: str
17
+ id: Optional[str] = None # Legacy field (deprecated, use groupId)
18
+ groupId: Optional[str] = Field(
19
+ None, alias="group_id", serialization_alias="groupId"
20
+ ) # Preferred: Customer's group ID
21
+ name: str
22
+
23
+ class Config:
24
+ populate_by_name = True
25
+
26
+
27
+ class InvitationGroup(BaseModel):
28
+ """
29
+ Invitation group from API responses
30
+ This matches the MemberGroups table structure from the API
31
+ """
32
+
33
+ id: str # Vortex internal UUID
34
+ account_id: str = Field(alias="accountId") # Vortex account ID
35
+ group_id: str = Field(alias="groupId") # Customer's group ID
36
+ type: str # Group type (e.g., "workspace", "team")
37
+ name: str # Group name
38
+ created_at: str = Field(alias="createdAt") # ISO 8601 timestamp
39
+
40
+ class Config:
41
+ # Allow both snake_case (Python) and camelCase (JSON) field names
42
+ populate_by_name = True
43
+ json_schema_extra = {
44
+ "example": {
45
+ "id": "550e8400-e29b-41d4-a716-446655440000",
46
+ "accountId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
47
+ "groupId": "workspace-123",
48
+ "type": "workspace",
49
+ "name": "My Workspace",
50
+ "createdAt": "2025-01-27T12:00:00.000Z",
51
+ }
52
+ }
53
+
54
+
55
+ class User(BaseModel):
56
+ """
57
+ User data for JWT generation
58
+
59
+ Required fields:
60
+ - id: User's ID in their system
61
+ - email: User's email address
62
+
63
+ Optional fields:
64
+ - admin_scopes: List of admin scopes (e.g., ['autojoin'])
65
+
66
+ Additional fields are allowed via extra parameter
67
+ """
68
+ id: str
69
+ email: str
70
+ admin_scopes: Optional[List[str]] = None
71
+
72
+ class Config:
73
+ extra = "allow" # Allow additional fields
74
+
75
+
76
+ class AuthenticatedUser(BaseModel):
77
+ """
78
+ User data for JWT generation (simplified structure)
79
+
80
+ Note: identifiers, groups, and role are maintained for backward compatibility
81
+ but are deprecated in favor of the User object with admin_scopes.
82
+ """
83
+ user_id: str
84
+ user_email: Optional[str] = None
85
+ admin_scopes: Optional[List[str]] = None
86
+
87
+ # Deprecated fields (maintained for backward compatibility)
88
+ identifiers: Optional[List[IdentifierInput]] = None
89
+ groups: Optional[List[GroupInput]] = None
90
+ role: Optional[str] = None
91
+
92
+
93
+ class JwtPayload(BaseModel):
94
+ """
95
+ JWT payload structure (simplified)
96
+
97
+ Required fields:
98
+ - user_id: User's ID in their system
99
+ - user_email: User's email address (preferred)
100
+
101
+ Optional fields:
102
+ - admin_scopes: List of admin scopes (e.g., ['autojoin'] for autojoin admin privileges)
103
+ - attributes: Additional custom attributes
104
+
105
+ Deprecated fields (maintained for backward compatibility):
106
+ - identifiers: Use user_email instead
107
+ - groups: No longer required
108
+ - role: No longer required
109
+
110
+ Additional fields are allowed via [key: string]: any pattern
111
+ """
112
+ user_id: str
113
+ user_email: Optional[str] = None
114
+ admin_scopes: Optional[List[str]] = None
115
+
116
+ # Deprecated fields (maintained for backward compatibility)
117
+ identifiers: Optional[List[IdentifierInput]] = None
118
+ groups: Optional[List[GroupInput]] = None
119
+ role: Optional[str] = None
120
+
121
+ attributes: Optional[Dict[str, Any]] = None
122
+
123
+ class Config:
124
+ extra = "allow" # Allow additional fields [key: string]: any
125
+
126
+
127
+ class InvitationTarget(BaseModel):
128
+ type: Literal["email", "sms"]
129
+ value: str
130
+
131
+
132
+ class InvitationAcceptance(BaseModel):
133
+ """Represents an acceptance of an invitation"""
134
+
135
+ id: str
136
+ account_id: str = Field(alias="accountId")
137
+ project_id: str = Field(alias="projectId")
138
+ accepted_at: str = Field(alias="acceptedAt")
139
+ target: InvitationTarget
140
+
141
+ class Config:
142
+ populate_by_name = True
143
+
144
+
145
+ class InvitationResult(BaseModel):
146
+ """
147
+ Complete invitation result from API responses.
148
+ This is the exact port of the Node.js SDK's InvitationResult type.
149
+ """
150
+
151
+ id: str
152
+ account_id: str = Field(alias="accountId")
153
+ click_throughs: int = Field(alias="clickThroughs")
154
+ configuration_attributes: Optional[Dict[str, Any]] = Field(
155
+ None, alias="configurationAttributes"
156
+ )
157
+ attributes: Optional[Dict[str, Any]] = None
158
+ created_at: str = Field(alias="createdAt")
159
+ deactivated: bool
160
+ delivery_count: int = Field(alias="deliveryCount")
161
+ delivery_types: List[Literal["email", "sms", "share"]] = Field(
162
+ alias="deliveryTypes"
163
+ )
164
+ foreign_creator_id: str = Field(alias="foreignCreatorId")
165
+ invitation_type: Literal["single_use", "multi_use"] = Field(alias="invitationType")
166
+ modified_at: Optional[str] = Field(None, alias="modifiedAt")
167
+ status: Literal[
168
+ "queued",
169
+ "sending",
170
+ "delivered",
171
+ "accepted",
172
+ "shared",
173
+ "unfurled",
174
+ "accepted_elsewhere",
175
+ ]
176
+ target: List[InvitationTarget] = Field(default_factory=list)
177
+ views: int
178
+ widget_configuration_id: str = Field(alias="widgetConfigurationId")
179
+ project_id: str = Field(alias="projectId")
180
+ groups: List[Optional[InvitationGroup]] = Field(default_factory=list)
181
+ accepts: List[InvitationAcceptance] = Field(default_factory=list)
182
+ expired: bool
183
+ expires: Optional[str] = None
184
+
185
+ class Config:
186
+ populate_by_name = True
187
+
188
+
189
+ # Alias for backward compatibility
190
+ Invitation = InvitationResult
191
+
192
+
193
+ class CreateInvitationRequest(BaseModel):
194
+ target: InvitationTarget
195
+ group_type: Optional[str] = None
196
+ group_id: Optional[str] = None
197
+ expires_at: Optional[str] = None
198
+ metadata: Optional[Dict[str, Union[str, int, bool]]] = None
199
+
200
+
201
+ class AcceptInvitationRequest(BaseModel):
202
+ """Request to accept one or more invitations"""
203
+
204
+ invitation_ids: List[str] = Field(alias="invitationIds")
205
+ target: InvitationTarget
206
+
207
+ class Config:
208
+ populate_by_name = True
209
+
210
+
211
+ # Alias for backward compatibility
212
+ AcceptInvitationsRequest = AcceptInvitationRequest
213
+
214
+
215
+ class ApiResponse(BaseModel):
216
+ data: Optional[Dict] = None
217
+ error: Optional[str] = None
218
+ status_code: int = 200
219
+
220
+
221
+ class VortexApiError(Exception):
222
+ def __init__(self, message: str, status_code: int = 500):
223
+ self.message = message
224
+ self.status_code = status_code
225
+ super().__init__(message)
226
+
227
+
228
+ # Type aliases to match Node.js SDK
229
+ ApiResponseJson = Union[
230
+ InvitationResult, Dict[str, List[InvitationResult]], Dict[str, Any]
231
+ ]
232
+ ApiRequestBody = Union[AcceptInvitationRequest, None]
@@ -4,25 +4,22 @@ import hmac
4
4
  import json
5
5
  import time
6
6
  import uuid
7
- from typing import Dict, List, Literal, Optional, Union
8
- from urllib.parse import urlencode
7
+ from typing import Any, Dict, List, Literal, Optional, Union
9
8
 
10
9
  import httpx
11
10
 
12
11
  from .types import (
13
- AcceptInvitationsRequest,
14
- ApiResponse,
15
- CreateInvitationRequest,
16
12
  Invitation,
17
13
  InvitationTarget,
18
- JwtPayload,
14
+ User,
19
15
  VortexApiError,
20
16
  )
21
17
 
22
18
 
23
- def _get_version():
19
+ def _get_version() -> str:
24
20
  """Lazy import of version to avoid circular import"""
25
21
  from . import __version__
22
+
26
23
  return __version__
27
24
 
28
25
 
@@ -42,21 +39,30 @@ class Vortex:
42
39
  self._client = httpx.AsyncClient()
43
40
  self._sync_client = httpx.Client()
44
41
 
45
- def generate_jwt(self, payload: Union[JwtPayload, Dict]) -> str:
42
+ def generate_jwt(self, user: Union[User, Dict], **extra: Any) -> str:
46
43
  """
47
- Generate a JWT token for the given payload matching Node.js SDK implementation
44
+ Generate a JWT token for a user
48
45
 
49
46
  Args:
50
- payload: JWT payload containing user_id, identifiers, groups, and role
47
+ user: User object or dict with 'id', 'email', and optional 'admin_scopes'
48
+ **extra: Additional properties to include in JWT payload
51
49
 
52
50
  Returns:
53
51
  JWT token string
54
52
 
55
53
  Raises:
56
- ValueError: If API key format is invalid
54
+ ValueError: If API key format is invalid or required fields are missing
55
+
56
+ Example:
57
+ user = {'id': 'user-123', 'email': 'user@example.com', 'admin_scopes': ['autojoin']}
58
+ jwt = vortex.generate_jwt(user=user)
59
+
60
+ # With additional properties
61
+ jwt = vortex.generate_jwt(user=user, role='admin', department='Engineering')
57
62
  """
58
- if isinstance(payload, dict):
59
- payload = JwtPayload(**payload)
63
+ # Convert dict to User if needed
64
+ if isinstance(user, dict):
65
+ user = User(**user)
60
66
 
61
67
  # Parse API key (format: VRTX.base64url(uuid).key)
62
68
  parts = self.api_key.split(".")
@@ -80,7 +86,7 @@ class Vortex:
80
86
  uuid_bytes = base64.urlsafe_b64decode(encoded_id_padded)
81
87
  kid = str(uuid.UUID(bytes=uuid_bytes))
82
88
  except Exception as e:
83
- raise ValueError(f"Invalid UUID in API key: {e}")
89
+ raise ValueError(f"Invalid UUID in API key: {e}") from e
84
90
 
85
91
  # Generate timestamps
86
92
  iat = int(time.time())
@@ -97,35 +103,24 @@ class Vortex:
97
103
  "kid": kid,
98
104
  }
99
105
 
100
- # Serialize identifiers
101
- identifiers_list = [
102
- {"type": id.type, "value": id.value} for id in payload.identifiers
103
- ]
104
-
105
- # Serialize groups
106
- groups_list = None
107
- if payload.groups is not None:
108
- groups_list = [
109
- {
110
- k: v
111
- for k, v in group.model_dump(
112
- by_alias=True, exclude_none=True
113
- ).items()
114
- }
115
- for group in payload.groups
116
- ]
117
-
118
- jwt_payload = {
119
- "userId": payload.user_id,
120
- "groups": groups_list,
121
- "role": payload.role,
106
+ # Build JWT payload
107
+ jwt_payload: Dict[str, Any] = {
108
+ "userId": user.id,
109
+ "userEmail": user.email,
122
110
  "expires": expires,
123
- "identifiers": identifiers_list,
124
111
  }
125
112
 
126
- # Add attributes if provided
127
- if hasattr(payload, "attributes") and payload.attributes:
128
- jwt_payload["attributes"] = payload.attributes
113
+ # Add adminScopes if present
114
+ if user.admin_scopes:
115
+ jwt_payload["adminScopes"] = user.admin_scopes
116
+
117
+ # Add any additional properties from user.model_extra
118
+ if hasattr(user, "model_extra") and user.model_extra:
119
+ jwt_payload.update(user.model_extra)
120
+
121
+ # Add any additional properties from **extra
122
+ if extra:
123
+ jwt_payload.update(extra)
129
124
 
130
125
  # Step 3: Base64URL encode (without padding)
131
126
  header_json = json.dumps(header, separators=(",", ":"))
@@ -185,17 +180,21 @@ class Vortex:
185
180
  "error",
186
181
  f"API request failed with status {response.status_code}",
187
182
  )
188
- except:
183
+ except Exception:
189
184
  error_message = (
190
185
  f"API request failed with status {response.status_code}"
191
186
  )
192
187
 
193
188
  raise VortexApiError(error_message, response.status_code)
194
189
 
195
- return response.json()
190
+ # Handle empty responses (e.g., DELETE requests may return 204 or empty 200)
191
+ if response.status_code == 204 or not response.content:
192
+ return {} # type: ignore[return-value]
193
+
194
+ return response.json() # type: ignore[no-any-return]
196
195
 
197
196
  except httpx.RequestError as e:
198
- raise VortexApiError(f"Request failed: {str(e)}")
197
+ raise VortexApiError(f"Request failed: {str(e)}") from e
199
198
 
200
199
  def _vortex_api_request_sync(
201
200
  self,
@@ -238,17 +237,21 @@ class Vortex:
238
237
  "error",
239
238
  f"API request failed with status {response.status_code}",
240
239
  )
241
- except:
240
+ except Exception:
242
241
  error_message = (
243
242
  f"API request failed with status {response.status_code}"
244
243
  )
245
244
 
246
245
  raise VortexApiError(error_message, response.status_code)
247
246
 
248
- return response.json()
247
+ # Handle empty responses (e.g., DELETE requests may return 204 or empty 200)
248
+ if response.status_code == 204 or not response.content:
249
+ return {} # type: ignore[return-value]
250
+
251
+ return response.json() # type: ignore[no-any-return]
249
252
 
250
253
  except httpx.RequestError as e:
251
- raise VortexApiError(f"Request failed: {str(e)}")
254
+ raise VortexApiError(f"Request failed: {str(e)}") from e
252
255
 
253
256
  async def get_invitations_by_target(
254
257
  self,
@@ -268,7 +271,7 @@ class Vortex:
268
271
  params = {"targetType": target_type, "targetValue": target_value}
269
272
 
270
273
  response = await self._vortex_api_request(
271
- "GET", "/invitations/by-target", params=params
274
+ "GET", "/invitations", params=params
272
275
  )
273
276
  return [Invitation(**inv) for inv in response.get("invitations", [])]
274
277
 
@@ -290,7 +293,7 @@ class Vortex:
290
293
  params = {"targetType": target_type, "targetValue": target_value}
291
294
 
292
295
  response = self._vortex_api_request_sync(
293
- "GET", "/invitations/by-target", params=params
296
+ "GET", "/invitations", params=params
294
297
  )
295
298
  return [Invitation(**inv) for inv in response.get("invitations", [])]
296
299
 
@@ -335,10 +338,13 @@ class Vortex:
335
338
  Returns:
336
339
  API response
337
340
  """
341
+ target_obj: InvitationTarget
338
342
  if isinstance(target, dict):
339
- target = InvitationTarget(**target)
343
+ target_obj = InvitationTarget(**target) # type: ignore[arg-type]
344
+ else:
345
+ target_obj = target
340
346
 
341
- data = {"invitationIds": invitation_ids, "target": target.model_dump()}
347
+ data = {"invitationIds": invitation_ids, "target": target_obj.model_dump()}
342
348
 
343
349
  return await self._vortex_api_request("POST", "/invitations/accept", data=data)
344
350
 
@@ -355,10 +361,13 @@ class Vortex:
355
361
  Returns:
356
362
  API response
357
363
  """
364
+ target_obj: InvitationTarget
358
365
  if isinstance(target, dict):
359
- target = InvitationTarget(**target)
366
+ target_obj = InvitationTarget(**target) # type: ignore[arg-type]
367
+ else:
368
+ target_obj = target
360
369
 
361
- data = {"invitationIds": invitation_ids, "target": target.model_dump()}
370
+ data = {"invitationIds": invitation_ids, "target": target_obj.model_dump()}
362
371
 
363
372
  return self._vortex_api_request_sync("POST", "/invitations/accept", data=data)
364
373
 
@@ -482,26 +491,26 @@ class Vortex:
482
491
  )
483
492
  return Invitation(**response)
484
493
 
485
- async def close(self):
494
+ async def close(self) -> None:
486
495
  """Close the HTTP client"""
487
496
  await self._client.aclose()
488
497
 
489
- def close_sync(self):
498
+ def close_sync(self) -> None:
490
499
  """Close the synchronous HTTP client"""
491
500
  self._sync_client.close()
492
501
 
493
- async def __aenter__(self):
502
+ async def __aenter__(self) -> "Vortex":
494
503
  """Async context manager entry"""
495
504
  return self
496
505
 
497
- async def __aexit__(self, exc_type, exc_val, exc_tb):
506
+ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
498
507
  """Async context manager exit"""
499
508
  await self.close()
500
509
 
501
- def __enter__(self):
510
+ def __enter__(self) -> "Vortex":
502
511
  """Context manager entry"""
503
512
  return self
504
513
 
505
- def __exit__(self, exc_type, exc_val, exc_tb):
514
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
506
515
  """Context manager exit"""
507
516
  self.close_sync()
@@ -1,112 +0,0 @@
1
- from typing import Any, Dict, List, Literal, Optional, Union
2
-
3
- from pydantic import BaseModel, Field
4
-
5
-
6
- class IdentifierInput(BaseModel):
7
- """Identifier structure for JWT generation"""
8
-
9
- type: Literal["email", "sms"]
10
- value: str
11
-
12
-
13
- class GroupInput(BaseModel):
14
- """Group structure for JWT generation (input)"""
15
-
16
- type: str
17
- id: Optional[str] = None # Legacy field (deprecated, use groupId)
18
- groupId: Optional[str] = Field(
19
- None, alias="group_id", serialization_alias="groupId"
20
- ) # Preferred: Customer's group ID
21
- name: str
22
-
23
- class Config:
24
- populate_by_name = True
25
-
26
-
27
- class InvitationGroup(BaseModel):
28
- """
29
- Invitation group from API responses
30
- This matches the MemberGroups table structure from the API
31
- """
32
-
33
- id: str # Vortex internal UUID
34
- account_id: str = Field(alias="accountId") # Vortex account ID
35
- group_id: str = Field(alias="groupId") # Customer's group ID
36
- type: str # Group type (e.g., "workspace", "team")
37
- name: str # Group name
38
- created_at: str = Field(alias="createdAt") # ISO 8601 timestamp
39
-
40
- class Config:
41
- # Allow both snake_case (Python) and camelCase (JSON) field names
42
- populate_by_name = True
43
- json_schema_extra = {
44
- "example": {
45
- "id": "550e8400-e29b-41d4-a716-446655440000",
46
- "accountId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
47
- "groupId": "workspace-123",
48
- "type": "workspace",
49
- "name": "My Workspace",
50
- "createdAt": "2025-01-27T12:00:00.000Z",
51
- }
52
- }
53
-
54
-
55
- class AuthenticatedUser(BaseModel):
56
- user_id: str
57
- identifiers: List[IdentifierInput]
58
- groups: Optional[List[GroupInput]] = None
59
- role: Optional[str] = None
60
-
61
-
62
- class JwtPayload(BaseModel):
63
- user_id: str
64
- identifiers: List[IdentifierInput]
65
- groups: Optional[List[GroupInput]] = None
66
- role: Optional[str] = None
67
- attributes: Optional[Dict[str, Any]] = None
68
-
69
-
70
- class InvitationTarget(BaseModel):
71
- type: Literal["email", "username", "phoneNumber"]
72
- value: str
73
-
74
-
75
- class Invitation(BaseModel):
76
- id: str
77
- target: Union[InvitationTarget, List[InvitationTarget]] # API returns list or single
78
- groups: Optional[List[Optional[InvitationGroup]]] = None # Full group information, can contain None
79
- status: str
80
- created_at: Optional[str] = Field(None, alias="createdAt") # API uses camelCase
81
- updated_at: Optional[str] = Field(None, alias="updatedAt")
82
- expires_at: Optional[str] = Field(None, alias="expiresAt")
83
- metadata: Optional[Dict[str, Union[str, int, bool]]] = None
84
-
85
- class Config:
86
- populate_by_name = True
87
-
88
-
89
- class CreateInvitationRequest(BaseModel):
90
- target: InvitationTarget
91
- group_type: Optional[str] = None
92
- group_id: Optional[str] = None
93
- expires_at: Optional[str] = None
94
- metadata: Optional[Dict[str, Union[str, int, bool]]] = None
95
-
96
-
97
- class AcceptInvitationsRequest(BaseModel):
98
- invitation_ids: List[str]
99
- target: InvitationTarget
100
-
101
-
102
- class ApiResponse(BaseModel):
103
- data: Optional[Dict] = None
104
- error: Optional[str] = None
105
- status_code: int = 200
106
-
107
-
108
- class VortexApiError(Exception):
109
- def __init__(self, message: str, status_code: int = 500):
110
- self.message = message
111
- self.status_code = status_code
112
- super().__init__(message)