vortex-python-sdk 0.0.6__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.6
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.6"
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"
@@ -81,7 +81,7 @@ 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
86
 
87
87
  [tool.ruff.lint]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vortex-python-sdk
3
- Version: 0.0.6
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
@@ -52,20 +52,77 @@ class InvitationGroup(BaseModel):
52
52
  }
53
53
 
54
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
+
55
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
+ """
56
83
  user_id: str
57
- identifiers: List[IdentifierInput]
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
58
89
  groups: Optional[List[GroupInput]] = None
59
90
  role: Optional[str] = None
60
91
 
61
92
 
62
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
+ """
63
112
  user_id: str
64
- identifiers: List[IdentifierInput]
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
65
118
  groups: Optional[List[GroupInput]] = None
66
119
  role: Optional[str] = None
120
+
67
121
  attributes: Optional[Dict[str, Any]] = None
68
122
 
123
+ class Config:
124
+ extra = "allow" # Allow additional fields [key: string]: any
125
+
69
126
 
70
127
  class InvitationTarget(BaseModel):
71
128
  type: Literal["email", "sms"]
@@ -116,12 +173,14 @@ class InvitationResult(BaseModel):
116
173
  "unfurled",
117
174
  "accepted_elsewhere",
118
175
  ]
119
- target: List[InvitationTarget]
176
+ target: List[InvitationTarget] = Field(default_factory=list)
120
177
  views: int
121
178
  widget_configuration_id: str = Field(alias="widgetConfigurationId")
122
179
  project_id: str = Field(alias="projectId")
123
180
  groups: List[Optional[InvitationGroup]] = Field(default_factory=list)
124
181
  accepts: List[InvitationAcceptance] = Field(default_factory=list)
182
+ expired: bool
183
+ expires: Optional[str] = None
125
184
 
126
185
  class Config:
127
186
  populate_by_name = True
@@ -11,7 +11,7 @@ import httpx
11
11
  from .types import (
12
12
  Invitation,
13
13
  InvitationTarget,
14
- JwtPayload,
14
+ User,
15
15
  VortexApiError,
16
16
  )
17
17
 
@@ -39,21 +39,30 @@ class Vortex:
39
39
  self._client = httpx.AsyncClient()
40
40
  self._sync_client = httpx.Client()
41
41
 
42
- def generate_jwt(self, payload: Union[JwtPayload, Dict]) -> str:
42
+ def generate_jwt(self, user: Union[User, Dict], **extra: Any) -> str:
43
43
  """
44
- Generate a JWT token for the given payload matching Node.js SDK implementation
44
+ Generate a JWT token for a user
45
45
 
46
46
  Args:
47
- 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
48
49
 
49
50
  Returns:
50
51
  JWT token string
51
52
 
52
53
  Raises:
53
- 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')
54
62
  """
55
- if isinstance(payload, dict):
56
- payload = JwtPayload(**payload)
63
+ # Convert dict to User if needed
64
+ if isinstance(user, dict):
65
+ user = User(**user)
57
66
 
58
67
  # Parse API key (format: VRTX.base64url(uuid).key)
59
68
  parts = self.api_key.split(".")
@@ -94,30 +103,24 @@ class Vortex:
94
103
  "kid": kid,
95
104
  }
96
105
 
97
- # Serialize identifiers
98
- identifiers_list = [
99
- {"type": id.type, "value": id.value} for id in payload.identifiers
100
- ]
101
-
102
- # Serialize groups
103
- groups_list = None
104
- if payload.groups is not None:
105
- groups_list = [
106
- group.model_dump(by_alias=True, exclude_none=True)
107
- for group in payload.groups
108
- ]
109
-
106
+ # Build JWT payload
110
107
  jwt_payload: Dict[str, Any] = {
111
- "userId": payload.user_id,
112
- "groups": groups_list,
113
- "role": payload.role,
108
+ "userId": user.id,
109
+ "userEmail": user.email,
114
110
  "expires": expires,
115
- "identifiers": identifiers_list,
116
111
  }
117
112
 
118
- # Add attributes if provided
119
- if hasattr(payload, "attributes") and payload.attributes:
120
- 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)
121
124
 
122
125
  # Step 3: Base64URL encode (without padding)
123
126
  header_json = json.dumps(header, separators=(",", ":"))
@@ -184,6 +187,10 @@ class Vortex:
184
187
 
185
188
  raise VortexApiError(error_message, response.status_code)
186
189
 
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
+
187
194
  return response.json() # type: ignore[no-any-return]
188
195
 
189
196
  except httpx.RequestError as e:
@@ -237,6 +244,10 @@ class Vortex:
237
244
 
238
245
  raise VortexApiError(error_message, response.status_code)
239
246
 
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
+
240
251
  return response.json() # type: ignore[no-any-return]
241
252
 
242
253
  except httpx.RequestError as e:
@@ -260,7 +271,7 @@ class Vortex:
260
271
  params = {"targetType": target_type, "targetValue": target_value}
261
272
 
262
273
  response = await self._vortex_api_request(
263
- "GET", "/invitations/by-target", params=params
274
+ "GET", "/invitations", params=params
264
275
  )
265
276
  return [Invitation(**inv) for inv in response.get("invitations", [])]
266
277
 
@@ -282,7 +293,7 @@ class Vortex:
282
293
  params = {"targetType": target_type, "targetValue": target_value}
283
294
 
284
295
  response = self._vortex_api_request_sync(
285
- "GET", "/invitations/by-target", params=params
296
+ "GET", "/invitations", params=params
286
297
  )
287
298
  return [Invitation(**inv) for inv in response.get("invitations", [])]
288
299