vortex-python-sdk 0.0.3__tar.gz → 0.0.6__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,9 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.0.5] - 2025-11-06
11
+
12
+ ### Fixed
13
+
14
+ - **CRITICAL FIX**: Updated api url & auth headers
15
+
10
16
  ## [0.0.3] - 2025-01-31
11
17
 
12
18
  ### Fixed
19
+
13
20
  - **CRITICAL FIX**: JWT generation now matches Node.js SDK implementation exactly
14
21
  - Improved JWT signing algorithm to match Node.js SDK
15
22
  - Added `iat` (issued at) and `expires` timestamp fields to JWT
@@ -18,12 +25,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
18
25
  - Added comprehensive tests verifying JWT output matches Node.js SDK byte-for-byte
19
26
 
20
27
  ### Breaking Changes
28
+
21
29
  - JWT structure changed significantly - tokens from 0.0.2 are incompatible with 0.0.3
22
30
  - JWTs now include `iat` and `expires` fields for proper token lifecycle management
23
31
 
24
32
  ## [0.0.2] - 2025-01-31
25
33
 
26
34
  ### Fixed
35
+
27
36
  - **BREAKING FIX**: JWT payload format now matches TypeScript SDK
28
37
  - `identifiers` changed from `Dict[str, str]` to `List[Dict]` with `type` and `value` fields
29
38
  - `groups` structure now properly includes `type`, `id`/`groupId`, and `name` fields
@@ -32,9 +41,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
32
41
  - Updated documentation with correct JWT generation examples
33
42
 
34
43
  ### Migration Guide
44
+
35
45
  If you're upgrading from 0.0.1, update your JWT generation code:
36
46
 
37
47
  **Before (0.0.1):**
48
+
38
49
  ```python
39
50
  jwt = vortex.generate_jwt({
40
51
  "user_id": "user-123",
@@ -44,6 +55,7 @@ jwt = vortex.generate_jwt({
44
55
  ```
45
56
 
46
57
  **After (0.0.2):**
58
+
47
59
  ```python
48
60
  jwt = vortex.generate_jwt({
49
61
  "user_id": "user-123",
@@ -55,6 +67,7 @@ jwt = vortex.generate_jwt({
55
67
  ## [0.0.1] - 2024-10-10
56
68
 
57
69
  ### Added
70
+
58
71
  - Initial release of Vortex Python SDK
59
72
  - JWT generation with HMAC-SHA256 signing
60
73
  - Complete invitation management API
@@ -65,6 +78,7 @@ jwt = vortex.generate_jwt({
65
78
  - Full compatibility with Node.js SDK API
66
79
 
67
80
  ### Features
81
+
68
82
  - `generate_jwt()` - Generate Vortex JWT tokens
69
83
  - `get_invitations_by_target()` - Get invitations by email/username/phone
70
84
  - `accept_invitations()` - Accept multiple invitations
@@ -74,4 +88,4 @@ jwt = vortex.generate_jwt({
74
88
  - `delete_invitations_by_group()` - Delete all group invitations
75
89
  - `reinvite()` - Reinvite functionality
76
90
  - Both async and sync versions of all methods
77
- - Python 3.8+ support
91
+ - Python 3.8+ support
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vortex-python-sdk
3
- Version: 0.0.3
3
+ Version: 0.0.6
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "vortex-python-sdk"
7
- version = "0.0.3"
7
+ version = "0.0.6"
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
@@ -83,6 +83,8 @@ warn_unreachable = true
83
83
  [tool.ruff]
84
84
  target-version = "py38"
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.3
3
+ Version: 0.0.6
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
@@ -4,21 +4,27 @@ Vortex Python SDK
4
4
  A Python SDK for Vortex invitation management and JWT generation.
5
5
  """
6
6
 
7
- from .vortex import Vortex
8
7
  from .types import (
8
+ AcceptInvitationRequest,
9
+ AcceptInvitationsRequest,
10
+ ApiRequestBody,
11
+ ApiResponse,
12
+ ApiResponseJson,
9
13
  AuthenticatedUser,
10
- JwtPayload,
11
- IdentifierInput,
14
+ CreateInvitationRequest,
12
15
  GroupInput,
13
- InvitationTarget,
16
+ IdentifierInput,
14
17
  Invitation,
15
- CreateInvitationRequest,
16
- AcceptInvitationsRequest,
17
- ApiResponse,
18
- VortexApiError
18
+ InvitationAcceptance,
19
+ InvitationGroup,
20
+ InvitationResult,
21
+ InvitationTarget,
22
+ JwtPayload,
23
+ VortexApiError,
19
24
  )
25
+ from .vortex import Vortex
20
26
 
21
- __version__ = "0.0.3"
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,173 @@
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", "sms"]
72
+ value: str
73
+
74
+
75
+ class InvitationAcceptance(BaseModel):
76
+ """Represents an acceptance of an invitation"""
77
+
78
+ id: str
79
+ account_id: str = Field(alias="accountId")
80
+ project_id: str = Field(alias="projectId")
81
+ accepted_at: str = Field(alias="acceptedAt")
82
+ target: InvitationTarget
83
+
84
+ class Config:
85
+ populate_by_name = True
86
+
87
+
88
+ class InvitationResult(BaseModel):
89
+ """
90
+ Complete invitation result from API responses.
91
+ This is the exact port of the Node.js SDK's InvitationResult type.
92
+ """
93
+
94
+ id: str
95
+ account_id: str = Field(alias="accountId")
96
+ click_throughs: int = Field(alias="clickThroughs")
97
+ configuration_attributes: Optional[Dict[str, Any]] = Field(
98
+ None, alias="configurationAttributes"
99
+ )
100
+ attributes: Optional[Dict[str, Any]] = None
101
+ created_at: str = Field(alias="createdAt")
102
+ deactivated: bool
103
+ delivery_count: int = Field(alias="deliveryCount")
104
+ delivery_types: List[Literal["email", "sms", "share"]] = Field(
105
+ alias="deliveryTypes"
106
+ )
107
+ foreign_creator_id: str = Field(alias="foreignCreatorId")
108
+ invitation_type: Literal["single_use", "multi_use"] = Field(alias="invitationType")
109
+ modified_at: Optional[str] = Field(None, alias="modifiedAt")
110
+ status: Literal[
111
+ "queued",
112
+ "sending",
113
+ "delivered",
114
+ "accepted",
115
+ "shared",
116
+ "unfurled",
117
+ "accepted_elsewhere",
118
+ ]
119
+ target: List[InvitationTarget]
120
+ views: int
121
+ widget_configuration_id: str = Field(alias="widgetConfigurationId")
122
+ project_id: str = Field(alias="projectId")
123
+ groups: List[Optional[InvitationGroup]] = Field(default_factory=list)
124
+ accepts: List[InvitationAcceptance] = Field(default_factory=list)
125
+
126
+ class Config:
127
+ populate_by_name = True
128
+
129
+
130
+ # Alias for backward compatibility
131
+ Invitation = InvitationResult
132
+
133
+
134
+ class CreateInvitationRequest(BaseModel):
135
+ target: InvitationTarget
136
+ group_type: Optional[str] = None
137
+ group_id: Optional[str] = None
138
+ expires_at: Optional[str] = None
139
+ metadata: Optional[Dict[str, Union[str, int, bool]]] = None
140
+
141
+
142
+ class AcceptInvitationRequest(BaseModel):
143
+ """Request to accept one or more invitations"""
144
+
145
+ invitation_ids: List[str] = Field(alias="invitationIds")
146
+ target: InvitationTarget
147
+
148
+ class Config:
149
+ populate_by_name = True
150
+
151
+
152
+ # Alias for backward compatibility
153
+ AcceptInvitationsRequest = AcceptInvitationRequest
154
+
155
+
156
+ class ApiResponse(BaseModel):
157
+ data: Optional[Dict] = None
158
+ error: Optional[str] = None
159
+ status_code: int = 200
160
+
161
+
162
+ class VortexApiError(Exception):
163
+ def __init__(self, message: str, status_code: int = 500):
164
+ self.message = message
165
+ self.status_code = status_code
166
+ super().__init__(message)
167
+
168
+
169
+ # Type aliases to match Node.js SDK
170
+ ApiResponseJson = Union[
171
+ InvitationResult, Dict[str, List[InvitationResult]], Dict[str, Any]
172
+ ]
173
+ ApiRequestBody = Union[AcceptInvitationRequest, None]
@@ -1,34 +1,41 @@
1
- import json
2
- import hmac
3
- import hashlib
4
1
  import base64
2
+ import hashlib
3
+ import hmac
4
+ import json
5
5
  import time
6
6
  import uuid
7
- from typing import Dict, List, Optional, Union, Literal
8
- from urllib.parse import urlencode
7
+ from typing import Any, Dict, List, Literal, Optional, Union
8
+
9
9
  import httpx
10
+
10
11
  from .types import (
11
- JwtPayload,
12
- InvitationTarget,
13
12
  Invitation,
14
- CreateInvitationRequest,
15
- AcceptInvitationsRequest,
16
- ApiResponse,
17
- VortexApiError
13
+ InvitationTarget,
14
+ JwtPayload,
15
+ VortexApiError,
18
16
  )
19
17
 
20
18
 
19
+ def _get_version() -> str:
20
+ """Lazy import of version to avoid circular import"""
21
+ from . import __version__
22
+
23
+ return __version__
24
+
25
+
21
26
  class Vortex:
22
- def __init__(self, api_key: str, base_url: str = "https://api.vortexsoftware.com"):
27
+ def __init__(
28
+ self, api_key: str, base_url: str = "https://api.vortexsoftware.com/api/v1"
29
+ ):
23
30
  """
24
31
  Initialize Vortex client
25
32
 
26
33
  Args:
27
34
  api_key: Your Vortex API key
28
- base_url: Base URL for Vortex API (default: https://api.vortexsoftware.com)
35
+ base_url: Base URL for Vortex API (default: https://api.vortexsoftware.com/api/v1)
29
36
  """
30
37
  self.api_key = api_key
31
- self.base_url = base_url.rstrip('/')
38
+ self.base_url = base_url.rstrip("/")
32
39
  self._client = httpx.AsyncClient()
33
40
  self._sync_client = httpx.Client()
34
41
 
@@ -49,20 +56,20 @@ class Vortex:
49
56
  payload = JwtPayload(**payload)
50
57
 
51
58
  # Parse API key (format: VRTX.base64url(uuid).key)
52
- parts = self.api_key.split('.')
59
+ parts = self.api_key.split(".")
53
60
  if len(parts) != 3:
54
- raise ValueError('Invalid API key format. Expected: VRTX.{encodedId}.{key}')
61
+ raise ValueError("Invalid API key format. Expected: VRTX.{encodedId}.{key}")
55
62
 
56
63
  prefix, encoded_id, key = parts
57
64
 
58
- if prefix != 'VRTX':
59
- raise ValueError('Invalid API key prefix. Expected: VRTX')
65
+ if prefix != "VRTX":
66
+ raise ValueError("Invalid API key prefix. Expected: VRTX")
60
67
 
61
68
  # Decode UUID from base64url
62
69
  # Add padding if needed
63
70
  padding = 4 - len(encoded_id) % 4
64
71
  if padding != 4:
65
- encoded_id_padded = encoded_id + ('=' * padding)
72
+ encoded_id_padded = encoded_id + ("=" * padding)
66
73
  else:
67
74
  encoded_id_padded = encoded_id
68
75
 
@@ -70,75 +77,71 @@ class Vortex:
70
77
  uuid_bytes = base64.urlsafe_b64decode(encoded_id_padded)
71
78
  kid = str(uuid.UUID(bytes=uuid_bytes))
72
79
  except Exception as e:
73
- raise ValueError(f'Invalid UUID in API key: {e}')
80
+ raise ValueError(f"Invalid UUID in API key: {e}") from e
74
81
 
75
82
  # Generate timestamps
76
83
  iat = int(time.time())
77
84
  expires = iat + 3600
78
85
 
79
86
  # Step 1: Derive signing key from API key + UUID
80
- signing_key = hmac.new(
81
- key.encode(),
82
- kid.encode(),
83
- hashlib.sha256
84
- ).digest()
87
+ signing_key = hmac.new(key.encode(), kid.encode(), hashlib.sha256).digest()
85
88
 
86
89
  # Step 2: Build header + payload
87
90
  header = {
88
- 'iat': iat,
89
- 'alg': 'HS256',
90
- 'typ': 'JWT',
91
- 'kid': kid,
91
+ "iat": iat,
92
+ "alg": "HS256",
93
+ "typ": "JWT",
94
+ "kid": kid,
92
95
  }
93
96
 
94
97
  # Serialize identifiers
95
- identifiers_list = [{"type": id.type, "value": id.value} for id in payload.identifiers]
98
+ identifiers_list = [
99
+ {"type": id.type, "value": id.value} for id in payload.identifiers
100
+ ]
96
101
 
97
102
  # Serialize groups
98
103
  groups_list = None
99
104
  if payload.groups is not None:
100
105
  groups_list = [
101
- {k: v for k, v in group.model_dump(by_alias=True, exclude_none=True).items()}
106
+ group.model_dump(by_alias=True, exclude_none=True)
102
107
  for group in payload.groups
103
108
  ]
104
109
 
105
- jwt_payload = {
106
- 'userId': payload.user_id,
107
- 'groups': groups_list,
108
- 'role': payload.role,
109
- 'expires': expires,
110
- 'identifiers': identifiers_list,
110
+ jwt_payload: Dict[str, Any] = {
111
+ "userId": payload.user_id,
112
+ "groups": groups_list,
113
+ "role": payload.role,
114
+ "expires": expires,
115
+ "identifiers": identifiers_list,
111
116
  }
112
117
 
113
118
  # Add attributes if provided
114
- if hasattr(payload, 'attributes') and payload.attributes:
115
- jwt_payload['attributes'] = payload.attributes
119
+ if hasattr(payload, "attributes") and payload.attributes:
120
+ jwt_payload["attributes"] = payload.attributes
116
121
 
117
122
  # Step 3: Base64URL encode (without padding)
118
- header_json = json.dumps(header, separators=(',', ':'))
119
- payload_json = json.dumps(jwt_payload, separators=(',', ':'))
123
+ header_json = json.dumps(header, separators=(",", ":"))
124
+ payload_json = json.dumps(jwt_payload, separators=(",", ":"))
120
125
 
121
- header_b64 = base64.urlsafe_b64encode(header_json.encode()).decode().rstrip('=')
122
- payload_b64 = base64.urlsafe_b64encode(payload_json.encode()).decode().rstrip('=')
126
+ header_b64 = base64.urlsafe_b64encode(header_json.encode()).decode().rstrip("=")
127
+ payload_b64 = (
128
+ base64.urlsafe_b64encode(payload_json.encode()).decode().rstrip("=")
129
+ )
123
130
 
124
131
  # Step 4: Sign
125
- to_sign = f'{header_b64}.{payload_b64}'
126
- signature = hmac.new(
127
- signing_key,
128
- to_sign.encode(),
129
- hashlib.sha256
130
- ).digest()
132
+ to_sign = f"{header_b64}.{payload_b64}"
133
+ signature = hmac.new(signing_key, to_sign.encode(), hashlib.sha256).digest()
131
134
 
132
- signature_b64 = base64.urlsafe_b64encode(signature).decode().rstrip('=')
135
+ signature_b64 = base64.urlsafe_b64encode(signature).decode().rstrip("=")
133
136
 
134
- return f'{to_sign}.{signature_b64}'
137
+ return f"{to_sign}.{signature_b64}"
135
138
 
136
139
  async def _vortex_api_request(
137
140
  self,
138
141
  method: str,
139
142
  endpoint: str,
140
143
  data: Optional[Dict] = None,
141
- params: Optional[Dict] = None
144
+ params: Optional[Dict] = None,
142
145
  ) -> Dict:
143
146
  """
144
147
  Make an API request to Vortex
@@ -157,40 +160,41 @@ class Vortex:
157
160
  """
158
161
  url = f"{self.base_url}{endpoint}"
159
162
  headers = {
160
- "Authorization": f"Bearer {self.api_key}",
163
+ "x-api-key": f"{self.api_key}",
161
164
  "Content-Type": "application/json",
162
- "User-Agent": "vortex-python-sdk/0.0.1"
165
+ "User-Agent": f"vortex-python-sdk/{_get_version()}",
163
166
  }
164
167
 
165
168
  try:
166
169
  response = await self._client.request(
167
- method=method,
168
- url=url,
169
- json=data,
170
- params=params,
171
- headers=headers
170
+ method=method, url=url, json=data, params=params, headers=headers
172
171
  )
173
172
 
174
173
  if response.status_code >= 400:
175
174
  try:
176
175
  error_data = response.json()
177
- error_message = error_data.get('error', f'API request failed with status {response.status_code}')
178
- except:
179
- error_message = f'API request failed with status {response.status_code}'
176
+ error_message = error_data.get(
177
+ "error",
178
+ f"API request failed with status {response.status_code}",
179
+ )
180
+ except Exception:
181
+ error_message = (
182
+ f"API request failed with status {response.status_code}"
183
+ )
180
184
 
181
185
  raise VortexApiError(error_message, response.status_code)
182
186
 
183
- return response.json()
187
+ return response.json() # type: ignore[no-any-return]
184
188
 
185
189
  except httpx.RequestError as e:
186
- raise VortexApiError(f"Request failed: {str(e)}")
190
+ raise VortexApiError(f"Request failed: {str(e)}") from e
187
191
 
188
192
  def _vortex_api_request_sync(
189
193
  self,
190
194
  method: str,
191
195
  endpoint: str,
192
196
  data: Optional[Dict] = None,
193
- params: Optional[Dict] = None
197
+ params: Optional[Dict] = None,
194
198
  ) -> Dict:
195
199
  """
196
200
  Make a synchronous API request to Vortex
@@ -209,38 +213,39 @@ class Vortex:
209
213
  """
210
214
  url = f"{self.base_url}{endpoint}"
211
215
  headers = {
212
- "Authorization": f"Bearer {self.api_key}",
216
+ "x-api-key": f"{self.api_key}",
213
217
  "Content-Type": "application/json",
214
- "User-Agent": "vortex-python-sdk/0.0.1"
218
+ "User-Agent": f"vortex-python-sdk/{_get_version()}",
215
219
  }
216
220
 
217
221
  try:
218
222
  response = self._sync_client.request(
219
- method=method,
220
- url=url,
221
- json=data,
222
- params=params,
223
- headers=headers
223
+ method=method, url=url, json=data, params=params, headers=headers
224
224
  )
225
225
 
226
226
  if response.status_code >= 400:
227
227
  try:
228
228
  error_data = response.json()
229
- error_message = error_data.get('error', f'API request failed with status {response.status_code}')
230
- except:
231
- error_message = f'API request failed with status {response.status_code}'
229
+ error_message = error_data.get(
230
+ "error",
231
+ f"API request failed with status {response.status_code}",
232
+ )
233
+ except Exception:
234
+ error_message = (
235
+ f"API request failed with status {response.status_code}"
236
+ )
232
237
 
233
238
  raise VortexApiError(error_message, response.status_code)
234
239
 
235
- return response.json()
240
+ return response.json() # type: ignore[no-any-return]
236
241
 
237
242
  except httpx.RequestError as e:
238
- raise VortexApiError(f"Request failed: {str(e)}")
243
+ raise VortexApiError(f"Request failed: {str(e)}") from e
239
244
 
240
245
  async def get_invitations_by_target(
241
246
  self,
242
247
  target_type: Literal["email", "username", "phoneNumber"],
243
- target_value: str
248
+ target_value: str,
244
249
  ) -> List[Invitation]:
245
250
  """
246
251
  Get invitations for a specific target
@@ -252,18 +257,17 @@ class Vortex:
252
257
  Returns:
253
258
  List of invitations
254
259
  """
255
- params = {
256
- "targetType": target_type,
257
- "targetValue": target_value
258
- }
260
+ params = {"targetType": target_type, "targetValue": target_value}
259
261
 
260
- response = await self._vortex_api_request("GET", "/invitations/by-target", params=params)
262
+ response = await self._vortex_api_request(
263
+ "GET", "/invitations/by-target", params=params
264
+ )
261
265
  return [Invitation(**inv) for inv in response.get("invitations", [])]
262
266
 
263
267
  def get_invitations_by_target_sync(
264
268
  self,
265
269
  target_type: Literal["email", "username", "phoneNumber"],
266
- target_value: str
270
+ target_value: str,
267
271
  ) -> List[Invitation]:
268
272
  """
269
273
  Get invitations for a specific target (synchronous)
@@ -275,12 +279,11 @@ class Vortex:
275
279
  Returns:
276
280
  List of invitations
277
281
  """
278
- params = {
279
- "targetType": target_type,
280
- "targetValue": target_value
281
- }
282
+ params = {"targetType": target_type, "targetValue": target_value}
282
283
 
283
- response = self._vortex_api_request_sync("GET", "/invitations/by-target", params=params)
284
+ response = self._vortex_api_request_sync(
285
+ "GET", "/invitations/by-target", params=params
286
+ )
284
287
  return [Invitation(**inv) for inv in response.get("invitations", [])]
285
288
 
286
289
  async def get_invitation(self, invitation_id: str) -> Invitation:
@@ -293,7 +296,9 @@ class Vortex:
293
296
  Returns:
294
297
  Invitation object
295
298
  """
296
- response = await self._vortex_api_request("GET", f"/invitations/{invitation_id}")
299
+ response = await self._vortex_api_request(
300
+ "GET", f"/invitations/{invitation_id}"
301
+ )
297
302
  return Invitation(**response)
298
303
 
299
304
  def get_invitation_sync(self, invitation_id: str) -> Invitation:
@@ -310,9 +315,7 @@ class Vortex:
310
315
  return Invitation(**response)
311
316
 
312
317
  async def accept_invitations(
313
- self,
314
- invitation_ids: List[str],
315
- target: Union[InvitationTarget, Dict[str, str]]
318
+ self, invitation_ids: List[str], target: Union[InvitationTarget, Dict[str, str]]
316
319
  ) -> Dict:
317
320
  """
318
321
  Accept multiple invitations
@@ -324,20 +327,18 @@ class Vortex:
324
327
  Returns:
325
328
  API response
326
329
  """
330
+ target_obj: InvitationTarget
327
331
  if isinstance(target, dict):
328
- target = InvitationTarget(**target)
332
+ target_obj = InvitationTarget(**target) # type: ignore[arg-type]
333
+ else:
334
+ target_obj = target
329
335
 
330
- data = {
331
- "invitationIds": invitation_ids,
332
- "target": target.model_dump()
333
- }
336
+ data = {"invitationIds": invitation_ids, "target": target_obj.model_dump()}
334
337
 
335
338
  return await self._vortex_api_request("POST", "/invitations/accept", data=data)
336
339
 
337
340
  def accept_invitations_sync(
338
- self,
339
- invitation_ids: List[str],
340
- target: Union[InvitationTarget, Dict[str, str]]
341
+ self, invitation_ids: List[str], target: Union[InvitationTarget, Dict[str, str]]
341
342
  ) -> Dict:
342
343
  """
343
344
  Accept multiple invitations (synchronous)
@@ -349,13 +350,13 @@ class Vortex:
349
350
  Returns:
350
351
  API response
351
352
  """
353
+ target_obj: InvitationTarget
352
354
  if isinstance(target, dict):
353
- target = InvitationTarget(**target)
355
+ target_obj = InvitationTarget(**target) # type: ignore[arg-type]
356
+ else:
357
+ target_obj = target
354
358
 
355
- data = {
356
- "invitationIds": invitation_ids,
357
- "target": target.model_dump()
358
- }
359
+ data = {"invitationIds": invitation_ids, "target": target_obj.model_dump()}
359
360
 
360
361
  return self._vortex_api_request_sync("POST", "/invitations/accept", data=data)
361
362
 
@@ -384,9 +385,7 @@ class Vortex:
384
385
  return self._vortex_api_request_sync("DELETE", f"/invitations/{invitation_id}")
385
386
 
386
387
  async def get_invitations_by_group(
387
- self,
388
- group_type: str,
389
- group_id: str
388
+ self, group_type: str, group_id: str
390
389
  ) -> List[Invitation]:
391
390
  """
392
391
  Get invitations for a specific group
@@ -398,13 +397,13 @@ class Vortex:
398
397
  Returns:
399
398
  List of invitations
400
399
  """
401
- response = await self._vortex_api_request("GET", f"/invitations/by-group/{group_type}/{group_id}")
400
+ response = await self._vortex_api_request(
401
+ "GET", f"/invitations/by-group/{group_type}/{group_id}"
402
+ )
402
403
  return [Invitation(**inv) for inv in response.get("invitations", [])]
403
404
 
404
405
  def get_invitations_by_group_sync(
405
- self,
406
- group_type: str,
407
- group_id: str
406
+ self, group_type: str, group_id: str
408
407
  ) -> List[Invitation]:
409
408
  """
410
409
  Get invitations for a specific group (synchronous)
@@ -416,14 +415,12 @@ class Vortex:
416
415
  Returns:
417
416
  List of invitations
418
417
  """
419
- response = self._vortex_api_request_sync("GET", f"/invitations/by-group/{group_type}/{group_id}")
418
+ response = self._vortex_api_request_sync(
419
+ "GET", f"/invitations/by-group/{group_type}/{group_id}"
420
+ )
420
421
  return [Invitation(**inv) for inv in response.get("invitations", [])]
421
422
 
422
- async def delete_invitations_by_group(
423
- self,
424
- group_type: str,
425
- group_id: str
426
- ) -> Dict:
423
+ async def delete_invitations_by_group(self, group_type: str, group_id: str) -> Dict:
427
424
  """
428
425
  Delete all invitations for a specific group
429
426
 
@@ -434,13 +431,11 @@ class Vortex:
434
431
  Returns:
435
432
  API response
436
433
  """
437
- return await self._vortex_api_request("DELETE", f"/invitations/by-group/{group_type}/{group_id}")
434
+ return await self._vortex_api_request(
435
+ "DELETE", f"/invitations/by-group/{group_type}/{group_id}"
436
+ )
438
437
 
439
- def delete_invitations_by_group_sync(
440
- self,
441
- group_type: str,
442
- group_id: str
443
- ) -> Dict:
438
+ def delete_invitations_by_group_sync(self, group_type: str, group_id: str) -> Dict:
444
439
  """
445
440
  Delete all invitations for a specific group (synchronous)
446
441
 
@@ -451,7 +446,9 @@ class Vortex:
451
446
  Returns:
452
447
  API response
453
448
  """
454
- return self._vortex_api_request_sync("DELETE", f"/invitations/by-group/{group_type}/{group_id}")
449
+ return self._vortex_api_request_sync(
450
+ "DELETE", f"/invitations/by-group/{group_type}/{group_id}"
451
+ )
455
452
 
456
453
  async def reinvite(self, invitation_id: str) -> Invitation:
457
454
  """
@@ -463,7 +460,9 @@ class Vortex:
463
460
  Returns:
464
461
  Updated invitation object
465
462
  """
466
- response = await self._vortex_api_request("POST", f"/invitations/{invitation_id}/reinvite")
463
+ response = await self._vortex_api_request(
464
+ "POST", f"/invitations/{invitation_id}/reinvite"
465
+ )
467
466
  return Invitation(**response)
468
467
 
469
468
  def reinvite_sync(self, invitation_id: str) -> Invitation:
@@ -476,29 +475,31 @@ class Vortex:
476
475
  Returns:
477
476
  Updated invitation object
478
477
  """
479
- response = self._vortex_api_request_sync("POST", f"/invitations/{invitation_id}/reinvite")
478
+ response = self._vortex_api_request_sync(
479
+ "POST", f"/invitations/{invitation_id}/reinvite"
480
+ )
480
481
  return Invitation(**response)
481
482
 
482
- async def close(self):
483
+ async def close(self) -> None:
483
484
  """Close the HTTP client"""
484
485
  await self._client.aclose()
485
486
 
486
- def close_sync(self):
487
+ def close_sync(self) -> None:
487
488
  """Close the synchronous HTTP client"""
488
489
  self._sync_client.close()
489
490
 
490
- async def __aenter__(self):
491
+ async def __aenter__(self) -> "Vortex":
491
492
  """Async context manager entry"""
492
493
  return self
493
494
 
494
- async def __aexit__(self, exc_type, exc_val, exc_tb):
495
+ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
495
496
  """Async context manager exit"""
496
497
  await self.close()
497
498
 
498
- def __enter__(self):
499
+ def __enter__(self) -> "Vortex":
499
500
  """Context manager entry"""
500
501
  return self
501
502
 
502
- def __exit__(self, exc_type, exc_val, exc_tb):
503
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
503
504
  """Context manager exit"""
504
- self.close_sync()
505
+ self.close_sync()
@@ -1,103 +0,0 @@
1
- from typing import Dict, List, Optional, Union, Literal, Any
2
- from pydantic import BaseModel, Field
3
-
4
-
5
- class IdentifierInput(BaseModel):
6
- """Identifier structure for JWT generation"""
7
- type: Literal["email", "sms"]
8
- value: str
9
-
10
-
11
- class GroupInput(BaseModel):
12
- """Group structure for JWT generation (input)"""
13
- type: str
14
- id: Optional[str] = None # Legacy field (deprecated, use groupId)
15
- groupId: Optional[str] = Field(None, alias="group_id", serialization_alias="groupId") # Preferred: Customer's group ID
16
- name: str
17
-
18
- class Config:
19
- populate_by_name = True
20
-
21
-
22
- class InvitationGroup(BaseModel):
23
- """
24
- Invitation group from API responses
25
- This matches the MemberGroups table structure from the API
26
- """
27
- id: str # Vortex internal UUID
28
- account_id: str # Vortex account ID (camelCase in JSON: accountId)
29
- group_id: str # Customer's group ID (camelCase in JSON: groupId)
30
- type: str # Group type (e.g., "workspace", "team")
31
- name: str # Group name
32
- created_at: str # ISO 8601 timestamp (camelCase in JSON: createdAt)
33
-
34
- class Config:
35
- # Allow both snake_case (Python) and camelCase (JSON) field names
36
- populate_by_name = True
37
- json_schema_extra = {
38
- "example": {
39
- "id": "550e8400-e29b-41d4-a716-446655440000",
40
- "accountId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
41
- "groupId": "workspace-123",
42
- "type": "workspace",
43
- "name": "My Workspace",
44
- "createdAt": "2025-01-27T12:00:00.000Z"
45
- }
46
- }
47
-
48
-
49
- class AuthenticatedUser(BaseModel):
50
- user_id: str
51
- identifiers: List[IdentifierInput]
52
- groups: Optional[List[GroupInput]] = None
53
- role: Optional[str] = None
54
-
55
-
56
- class JwtPayload(BaseModel):
57
- user_id: str
58
- identifiers: List[IdentifierInput]
59
- groups: Optional[List[GroupInput]] = None
60
- role: Optional[str] = None
61
- attributes: Optional[Dict[str, Any]] = None
62
-
63
-
64
- class InvitationTarget(BaseModel):
65
- type: Literal["email", "username", "phoneNumber"]
66
- value: str
67
-
68
-
69
- class Invitation(BaseModel):
70
- id: str
71
- target: InvitationTarget
72
- groups: Optional[List[InvitationGroup]] = None # Full group information
73
- status: str
74
- created_at: str
75
- updated_at: Optional[str] = None
76
- expires_at: Optional[str] = None
77
- metadata: Optional[Dict[str, Union[str, int, bool]]] = None
78
-
79
-
80
- class CreateInvitationRequest(BaseModel):
81
- target: InvitationTarget
82
- group_type: Optional[str] = None
83
- group_id: Optional[str] = None
84
- expires_at: Optional[str] = None
85
- metadata: Optional[Dict[str, Union[str, int, bool]]] = None
86
-
87
-
88
- class AcceptInvitationsRequest(BaseModel):
89
- invitation_ids: List[str]
90
- target: InvitationTarget
91
-
92
-
93
- class ApiResponse(BaseModel):
94
- data: Optional[Dict] = None
95
- error: Optional[str] = None
96
- status_code: int = 200
97
-
98
-
99
- class VortexApiError(Exception):
100
- def __init__(self, message: str, status_code: int = 500):
101
- self.message = message
102
- self.status_code = status_code
103
- super().__init__(message)