vortex-python-sdk 0.0.1__py3-none-any.whl → 0.0.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
@@ -0,0 +1,9 @@
1
+ vortex_python_sdk-0.0.3.dist-info/licenses/LICENSE,sha256=VndlWxbL4-w3YDf2yE5gJscj4zVXF0qlSq0LDtay3lo,1074
2
+ vortex_sdk/__init__.py,sha256=0Q9cyxYBvCQtLsKWJJu-lAjPFp5HCXnIYKtp6xYAYFA,709
3
+ vortex_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ vortex_sdk/types.py,sha256=Si5fXbf2gkcK3FoirJURNBBPLOJM090P5uhlHx6kXZQ,3079
5
+ vortex_sdk/vortex.py,sha256=B7XAT8CAEJLEH5kxYF3KpMo4kAO_LwCBs8-VEBgC3cw,14754
6
+ vortex_python_sdk-0.0.3.dist-info/METADATA,sha256=bACa6c5OMHoIwGBKC2oH8uNKVIdHmlIzzrHYnftqr5w,6237
7
+ vortex_python_sdk-0.0.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
+ vortex_python_sdk-0.0.3.dist-info/top_level.txt,sha256=xFDlEcXIIi_sBhkse0YfMnSdg2IlaYUd0oP2UCDc_Y0,11
9
+ vortex_python_sdk-0.0.3.dist-info/RECORD,,
vortex_sdk/__init__.py CHANGED
@@ -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",
vortex_sdk/types.py CHANGED
@@ -1,19 +1,64 @@
1
- from typing import Dict, List, Optional, Union, Literal
2
- from pydantic import BaseModel
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
+ }
3
47
 
4
48
 
5
49
  class AuthenticatedUser(BaseModel):
6
50
  user_id: str
7
- identifiers: Dict[str, str]
8
- groups: Optional[List[str]] = None
51
+ identifiers: List[IdentifierInput]
52
+ groups: Optional[List[GroupInput]] = None
9
53
  role: Optional[str] = None
10
54
 
11
55
 
12
56
  class JwtPayload(BaseModel):
13
57
  user_id: str
14
- identifiers: Dict[str, str]
15
- groups: Optional[List[str]] = None
58
+ identifiers: List[IdentifierInput]
59
+ groups: Optional[List[GroupInput]] = None
16
60
  role: Optional[str] = None
61
+ attributes: Optional[Dict[str, Any]] = None
17
62
 
18
63
 
19
64
  class InvitationTarget(BaseModel):
@@ -24,8 +69,7 @@ class InvitationTarget(BaseModel):
24
69
  class Invitation(BaseModel):
25
70
  id: str
26
71
  target: InvitationTarget
27
- group_type: Optional[str] = None
28
- group_id: Optional[str] = None
72
+ groups: Optional[List[InvitationGroup]] = None # Full group information
29
73
  status: str
30
74
  created_at: str
31
75
  updated_at: Optional[str] = None
vortex_sdk/vortex.py CHANGED
@@ -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,9 +0,0 @@
1
- vortex_python_sdk-0.0.1.dist-info/licenses/LICENSE,sha256=VndlWxbL4-w3YDf2yE5gJscj4zVXF0qlSq0LDtay3lo,1074
2
- vortex_sdk/__init__.py,sha256=mFLcyV2i5W7xuj25cThFWYBm6S0HpeOGtUIHDenzhBc,631
3
- vortex_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- vortex_sdk/types.py,sha256=whBfV7yVcrmk5K7ex1s4FCs92V9_bj-7KUsCRi5fmgk,1491
5
- vortex_sdk/vortex.py,sha256=_Ov62rzGdLecbID6LVQLMZgDc96xw7jY5qNqsfUjV3o,12963
6
- vortex_python_sdk-0.0.1.dist-info/METADATA,sha256=BH1AkI5SMi_mhRtttqLa5w1o2gk8ymwdzX3emM6U5iQ,5765
7
- vortex_python_sdk-0.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
- vortex_python_sdk-0.0.1.dist-info/top_level.txt,sha256=xFDlEcXIIi_sBhkse0YfMnSdg2IlaYUd0oP2UCDc_Y0,11
9
- vortex_python_sdk-0.0.1.dist-info/RECORD,,