vortex-python-sdk 0.0.1__tar.gz → 0.0.3__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.

Potentially problematic release.


This version of vortex-python-sdk might be problematic. Click here for more details.

@@ -0,0 +1,77 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.0.3] - 2025-01-31
11
+
12
+ ### Fixed
13
+ - **CRITICAL FIX**: JWT generation now matches Node.js SDK implementation exactly
14
+ - Improved JWT signing algorithm to match Node.js SDK
15
+ - Added `iat` (issued at) and `expires` timestamp fields to JWT
16
+ - Added `attributes` field support in JwtPayload for custom user attributes
17
+ - Fixed base64url encoding
18
+ - Added comprehensive tests verifying JWT output matches Node.js SDK byte-for-byte
19
+
20
+ ### Breaking Changes
21
+ - JWT structure changed significantly - tokens from 0.0.2 are incompatible with 0.0.3
22
+ - JWTs now include `iat` and `expires` fields for proper token lifecycle management
23
+
24
+ ## [0.0.2] - 2025-01-31
25
+
26
+ ### Fixed
27
+ - **BREAKING FIX**: JWT payload format now matches TypeScript SDK
28
+ - `identifiers` changed from `Dict[str, str]` to `List[Dict]` with `type` and `value` fields
29
+ - `groups` structure now properly includes `type`, `id`/`groupId`, and `name` fields
30
+ - Added `IdentifierInput` type for type-safe identifier creation
31
+ - Updated `GroupInput` to support both `id` (legacy) and `groupId` (preferred) with proper camelCase serialization
32
+ - Updated documentation with correct JWT generation examples
33
+
34
+ ### Migration Guide
35
+ If you're upgrading from 0.0.1, update your JWT generation code:
36
+
37
+ **Before (0.0.1):**
38
+ ```python
39
+ jwt = vortex.generate_jwt({
40
+ "user_id": "user-123",
41
+ "identifiers": {"email": "user@example.com"}, # Dict
42
+ "groups": ["admin"], # List of strings
43
+ })
44
+ ```
45
+
46
+ **After (0.0.2):**
47
+ ```python
48
+ jwt = vortex.generate_jwt({
49
+ "user_id": "user-123",
50
+ "identifiers": [{"type": "email", "value": "user@example.com"}], # List of dicts
51
+ "groups": [{"type": "team", "id": "team-1", "name": "Engineering"}], # List of objects
52
+ })
53
+ ```
54
+
55
+ ## [0.0.1] - 2024-10-10
56
+
57
+ ### Added
58
+ - Initial release of Vortex Python SDK
59
+ - JWT generation with HMAC-SHA256 signing
60
+ - Complete invitation management API
61
+ - Async and sync HTTP client methods
62
+ - Type safety with Pydantic models
63
+ - Context manager support for resource cleanup
64
+ - Comprehensive error handling with VortexApiError
65
+ - Full compatibility with Node.js SDK API
66
+
67
+ ### Features
68
+ - `generate_jwt()` - Generate Vortex JWT tokens
69
+ - `get_invitations_by_target()` - Get invitations by email/username/phone
70
+ - `accept_invitations()` - Accept multiple invitations
71
+ - `get_invitation()` - Get specific invitation by ID
72
+ - `revoke_invitation()` - Revoke invitation
73
+ - `get_invitations_by_group()` - Get invitations for a group
74
+ - `delete_invitations_by_group()` - Delete all group invitations
75
+ - `reinvite()` - Reinvite functionality
76
+ - Both async and sync versions of all methods
77
+ - Python 3.8+ support
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vortex-python-sdk
3
- Version: 0.0.1
3
+ Version: 0.0.3
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
@@ -55,11 +55,11 @@ pip install vortex-python-sdk
55
55
  ```python
56
56
  from vortex_sdk import Vortex
57
57
 
58
- # Initialize the client
59
- vortex = Vortex(api_key="your-api-key")
58
+ # Initialize the client with your Vortex API key
59
+ vortex = Vortex(api_key="your-vortex-api-key")
60
60
 
61
61
  # Or with custom base URL
62
- vortex = Vortex(api_key="your-api-key", base_url="https://custom-api.example.com")
62
+ vortex = Vortex(api_key="your-vortex-api-key", base_url="https://custom-api.example.com")
63
63
  ```
64
64
 
65
65
  ### JWT Generation
@@ -67,16 +67,33 @@ vortex = Vortex(api_key="your-api-key", base_url="https://custom-api.example.com
67
67
  ```python
68
68
  # Generate JWT for a user
69
69
  jwt = vortex.generate_jwt({
70
- "user_id": "user123",
71
- "identifiers": {
72
- "email": "user@example.com",
73
- "username": "johndoe"
74
- },
75
- "groups": ["admin", "users"],
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
+ ],
76
77
  "role": "admin"
77
78
  })
78
79
 
79
80
  print(f"JWT: {jwt}")
81
+
82
+ # Or using type-safe models
83
+ from vortex_sdk import JwtPayload, IdentifierInput, GroupInput
84
+
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
+ )
96
+ )
80
97
  ```
81
98
 
82
99
  ### Invitation Management
@@ -17,11 +17,11 @@ pip install vortex-python-sdk
17
17
  ```python
18
18
  from vortex_sdk import Vortex
19
19
 
20
- # Initialize the client
21
- vortex = Vortex(api_key="your-api-key")
20
+ # Initialize the client with your Vortex API key
21
+ vortex = Vortex(api_key="your-vortex-api-key")
22
22
 
23
23
  # Or with custom base URL
24
- vortex = Vortex(api_key="your-api-key", base_url="https://custom-api.example.com")
24
+ vortex = Vortex(api_key="your-vortex-api-key", base_url="https://custom-api.example.com")
25
25
  ```
26
26
 
27
27
  ### JWT Generation
@@ -29,16 +29,33 @@ vortex = Vortex(api_key="your-api-key", base_url="https://custom-api.example.com
29
29
  ```python
30
30
  # Generate JWT for a user
31
31
  jwt = vortex.generate_jwt({
32
- "user_id": "user123",
33
- "identifiers": {
34
- "email": "user@example.com",
35
- "username": "johndoe"
36
- },
37
- "groups": ["admin", "users"],
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
+ ],
38
39
  "role": "admin"
39
40
  })
40
41
 
41
42
  print(f"JWT: {jwt}")
43
+
44
+ # Or using type-safe models
45
+ from vortex_sdk import JwtPayload, IdentifierInput, GroupInput
46
+
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
+ )
58
+ )
42
59
  ```
43
60
 
44
61
  ### Invitation Management
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "vortex-python-sdk"
7
- version = "0.0.1"
7
+ version = "0.0.3"
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"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vortex-python-sdk
3
- Version: 0.0.1
3
+ Version: 0.0.3
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
@@ -55,11 +55,11 @@ pip install vortex-python-sdk
55
55
  ```python
56
56
  from vortex_sdk import Vortex
57
57
 
58
- # Initialize the client
59
- vortex = Vortex(api_key="your-api-key")
58
+ # Initialize the client with your Vortex API key
59
+ vortex = Vortex(api_key="your-vortex-api-key")
60
60
 
61
61
  # Or with custom base URL
62
- vortex = Vortex(api_key="your-api-key", base_url="https://custom-api.example.com")
62
+ vortex = Vortex(api_key="your-vortex-api-key", base_url="https://custom-api.example.com")
63
63
  ```
64
64
 
65
65
  ### JWT Generation
@@ -67,16 +67,33 @@ vortex = Vortex(api_key="your-api-key", base_url="https://custom-api.example.com
67
67
  ```python
68
68
  # Generate JWT for a user
69
69
  jwt = vortex.generate_jwt({
70
- "user_id": "user123",
71
- "identifiers": {
72
- "email": "user@example.com",
73
- "username": "johndoe"
74
- },
75
- "groups": ["admin", "users"],
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
+ ],
76
77
  "role": "admin"
77
78
  })
78
79
 
79
80
  print(f"JWT: {jwt}")
81
+
82
+ # Or using type-safe models
83
+ from vortex_sdk import JwtPayload, IdentifierInput, GroupInput
84
+
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
+ )
96
+ )
80
97
  ```
81
98
 
82
99
  ### Invitation Management
@@ -8,6 +8,8 @@ from .vortex import Vortex
8
8
  from .types import (
9
9
  AuthenticatedUser,
10
10
  JwtPayload,
11
+ IdentifierInput,
12
+ GroupInput,
11
13
  InvitationTarget,
12
14
  Invitation,
13
15
  CreateInvitationRequest,
@@ -16,7 +18,7 @@ from .types import (
16
18
  VortexApiError
17
19
  )
18
20
 
19
- __version__ = "0.0.1"
21
+ __version__ = "0.0.3"
20
22
  __author__ = "TeamVortexSoftware"
21
23
  __email__ = "support@vortexsoftware.com"
22
24
 
@@ -24,6 +26,8 @@ __all__ = [
24
26
  "Vortex",
25
27
  "AuthenticatedUser",
26
28
  "JwtPayload",
29
+ "IdentifierInput",
30
+ "GroupInput",
27
31
  "InvitationTarget",
28
32
  "Invitation",
29
33
  "CreateInvitationRequest",
@@ -0,0 +1,103 @@
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)
@@ -2,6 +2,8 @@ import json
2
2
  import hmac
3
3
  import hashlib
4
4
  import base64
5
+ import time
6
+ import uuid
5
7
  from typing import Dict, List, Optional, Union, Literal
6
8
  from urllib.parse import urlencode
7
9
  import httpx
@@ -32,54 +34,104 @@ class Vortex:
32
34
 
33
35
  def generate_jwt(self, payload: Union[JwtPayload, Dict]) -> str:
34
36
  """
35
- Generate a JWT token for the given payload
37
+ Generate a JWT token for the given payload matching Node.js SDK implementation
36
38
 
37
39
  Args:
38
40
  payload: JWT payload containing user_id, identifiers, groups, and role
39
41
 
40
42
  Returns:
41
43
  JWT token string
44
+
45
+ Raises:
46
+ ValueError: If API key format is invalid
42
47
  """
43
48
  if isinstance(payload, dict):
44
49
  payload = JwtPayload(**payload)
45
50
 
46
- # JWT Header
51
+ # Parse API key (format: VRTX.base64url(uuid).key)
52
+ parts = self.api_key.split('.')
53
+ if len(parts) != 3:
54
+ raise ValueError('Invalid API key format. Expected: VRTX.{encodedId}.{key}')
55
+
56
+ prefix, encoded_id, key = parts
57
+
58
+ if prefix != 'VRTX':
59
+ raise ValueError('Invalid API key prefix. Expected: VRTX')
60
+
61
+ # Decode UUID from base64url
62
+ # Add padding if needed
63
+ padding = 4 - len(encoded_id) % 4
64
+ if padding != 4:
65
+ encoded_id_padded = encoded_id + ('=' * padding)
66
+ else:
67
+ encoded_id_padded = encoded_id
68
+
69
+ try:
70
+ uuid_bytes = base64.urlsafe_b64decode(encoded_id_padded)
71
+ kid = str(uuid.UUID(bytes=uuid_bytes))
72
+ except Exception as e:
73
+ raise ValueError(f'Invalid UUID in API key: {e}')
74
+
75
+ # Generate timestamps
76
+ iat = int(time.time())
77
+ expires = iat + 3600
78
+
79
+ # 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()
85
+
86
+ # Step 2: Build header + payload
47
87
  header = {
48
- "alg": "HS256",
49
- "typ": "JWT"
88
+ 'iat': iat,
89
+ 'alg': 'HS256',
90
+ 'typ': 'JWT',
91
+ 'kid': kid,
50
92
  }
51
93
 
52
- # JWT Payload
94
+ # Serialize identifiers
95
+ identifiers_list = [{"type": id.type, "value": id.value} for id in payload.identifiers]
96
+
97
+ # Serialize groups
98
+ groups_list = None
99
+ if payload.groups is not None:
100
+ groups_list = [
101
+ {k: v for k, v in group.model_dump(by_alias=True, exclude_none=True).items()}
102
+ for group in payload.groups
103
+ ]
104
+
53
105
  jwt_payload = {
54
- "userId": payload.user_id,
55
- "identifiers": payload.identifiers,
106
+ 'userId': payload.user_id,
107
+ 'groups': groups_list,
108
+ 'role': payload.role,
109
+ 'expires': expires,
110
+ 'identifiers': identifiers_list,
56
111
  }
57
112
 
58
- if payload.groups is not None:
59
- jwt_payload["groups"] = payload.groups
60
- if payload.role is not None:
61
- jwt_payload["role"] = payload.role
113
+ # Add attributes if provided
114
+ if hasattr(payload, 'attributes') and payload.attributes:
115
+ jwt_payload['attributes'] = payload.attributes
62
116
 
63
- # Encode header and payload
64
- header_encoded = base64.urlsafe_b64encode(
65
- json.dumps(header, separators=(',', ':')).encode()
66
- ).decode().rstrip('=')
117
+ # Step 3: Base64URL encode (without padding)
118
+ header_json = json.dumps(header, separators=(',', ':'))
119
+ payload_json = json.dumps(jwt_payload, separators=(',', ':'))
67
120
 
68
- payload_encoded = base64.urlsafe_b64encode(
69
- json.dumps(jwt_payload, separators=(',', ':')).encode()
70
- ).decode().rstrip('=')
121
+ header_b64 = base64.urlsafe_b64encode(header_json.encode()).decode().rstrip('=')
122
+ payload_b64 = base64.urlsafe_b64encode(payload_json.encode()).decode().rstrip('=')
71
123
 
72
- # Create signature
73
- message = f"{header_encoded}.{payload_encoded}"
124
+ # Step 4: Sign
125
+ to_sign = f'{header_b64}.{payload_b64}'
74
126
  signature = hmac.new(
75
- self.api_key.encode(),
76
- message.encode(),
127
+ signing_key,
128
+ to_sign.encode(),
77
129
  hashlib.sha256
78
130
  ).digest()
79
131
 
80
- signature_encoded = base64.urlsafe_b64encode(signature).decode().rstrip('=')
132
+ signature_b64 = base64.urlsafe_b64encode(signature).decode().rstrip('=')
81
133
 
82
- return f"{message}.{signature_encoded}"
134
+ return f'{to_sign}.{signature_b64}'
83
135
 
84
136
  async def _vortex_api_request(
85
137
  self,
@@ -1,32 +0,0 @@
1
- # Changelog
2
-
3
- All notable changes to this project will be documented in this file.
4
-
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
-
8
- ## [Unreleased]
9
-
10
- ## [0.0.1] - 2024-10-10
11
-
12
- ### Added
13
- - Initial release of Vortex Python SDK
14
- - JWT generation with HMAC-SHA256 signing
15
- - Complete invitation management API
16
- - Async and sync HTTP client methods
17
- - Type safety with Pydantic models
18
- - Context manager support for resource cleanup
19
- - Comprehensive error handling with VortexApiError
20
- - Full compatibility with Node.js SDK API
21
-
22
- ### Features
23
- - `generate_jwt()` - Generate Vortex JWT tokens
24
- - `get_invitations_by_target()` - Get invitations by email/username/phone
25
- - `accept_invitations()` - Accept multiple invitations
26
- - `get_invitation()` - Get specific invitation by ID
27
- - `revoke_invitation()` - Revoke invitation
28
- - `get_invitations_by_group()` - Get invitations for a group
29
- - `delete_invitations_by_group()` - Delete all group invitations
30
- - `reinvite()` - Reinvite functionality
31
- - Both async and sync versions of all methods
32
- - Python 3.8+ support
@@ -1,59 +0,0 @@
1
- from typing import Dict, List, Optional, Union, Literal
2
- from pydantic import BaseModel
3
-
4
-
5
- class AuthenticatedUser(BaseModel):
6
- user_id: str
7
- identifiers: Dict[str, str]
8
- groups: Optional[List[str]] = None
9
- role: Optional[str] = None
10
-
11
-
12
- class JwtPayload(BaseModel):
13
- user_id: str
14
- identifiers: Dict[str, str]
15
- groups: Optional[List[str]] = None
16
- role: Optional[str] = None
17
-
18
-
19
- class InvitationTarget(BaseModel):
20
- type: Literal["email", "username", "phoneNumber"]
21
- value: str
22
-
23
-
24
- class Invitation(BaseModel):
25
- id: str
26
- target: InvitationTarget
27
- group_type: Optional[str] = None
28
- group_id: Optional[str] = None
29
- status: str
30
- created_at: str
31
- updated_at: Optional[str] = None
32
- expires_at: Optional[str] = None
33
- metadata: Optional[Dict[str, Union[str, int, bool]]] = None
34
-
35
-
36
- class CreateInvitationRequest(BaseModel):
37
- target: InvitationTarget
38
- group_type: Optional[str] = None
39
- group_id: Optional[str] = None
40
- expires_at: Optional[str] = None
41
- metadata: Optional[Dict[str, Union[str, int, bool]]] = None
42
-
43
-
44
- class AcceptInvitationsRequest(BaseModel):
45
- invitation_ids: List[str]
46
- target: InvitationTarget
47
-
48
-
49
- class ApiResponse(BaseModel):
50
- data: Optional[Dict] = None
51
- error: Optional[str] = None
52
- status_code: int = 200
53
-
54
-
55
- class VortexApiError(Exception):
56
- def __init__(self, message: str, status_code: int = 500):
57
- self.message = message
58
- self.status_code = status_code
59
- super().__init__(message)