vortex-python-sdk 0.0.2__tar.gz → 0.0.5__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.
- {vortex_python_sdk-0.0.2 → vortex_python_sdk-0.0.5}/CHANGELOG.md +29 -1
- {vortex_python_sdk-0.0.2/src/vortex_python_sdk.egg-info → vortex_python_sdk-0.0.5}/PKG-INFO +4 -4
- {vortex_python_sdk-0.0.2 → vortex_python_sdk-0.0.5}/README.md +3 -3
- {vortex_python_sdk-0.0.2 → vortex_python_sdk-0.0.5}/pyproject.toml +1 -1
- {vortex_python_sdk-0.0.2 → vortex_python_sdk-0.0.5/src/vortex_python_sdk.egg-info}/PKG-INFO +4 -4
- {vortex_python_sdk-0.0.2 → vortex_python_sdk-0.0.5}/src/vortex_sdk/__init__.py +10 -10
- {vortex_python_sdk-0.0.2 → vortex_python_sdk-0.0.5}/src/vortex_sdk/types.py +22 -12
- {vortex_python_sdk-0.0.2 → vortex_python_sdk-0.0.5}/src/vortex_sdk/vortex.py +162 -111
- {vortex_python_sdk-0.0.2 → vortex_python_sdk-0.0.5}/LICENSE +0 -0
- {vortex_python_sdk-0.0.2 → vortex_python_sdk-0.0.5}/MANIFEST.in +0 -0
- {vortex_python_sdk-0.0.2 → vortex_python_sdk-0.0.5}/setup.cfg +0 -0
- {vortex_python_sdk-0.0.2 → vortex_python_sdk-0.0.5}/src/vortex_python_sdk.egg-info/SOURCES.txt +0 -0
- {vortex_python_sdk-0.0.2 → vortex_python_sdk-0.0.5}/src/vortex_python_sdk.egg-info/dependency_links.txt +0 -0
- {vortex_python_sdk-0.0.2 → vortex_python_sdk-0.0.5}/src/vortex_python_sdk.egg-info/requires.txt +0 -0
- {vortex_python_sdk-0.0.2 → vortex_python_sdk-0.0.5}/src/vortex_python_sdk.egg-info/top_level.txt +0 -0
- {vortex_python_sdk-0.0.2 → vortex_python_sdk-0.0.5}/src/vortex_sdk/py.typed +0 -0
|
@@ -7,9 +7,32 @@ 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
|
+
|
|
16
|
+
## [0.0.3] - 2025-01-31
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- **CRITICAL FIX**: JWT generation now matches Node.js SDK implementation exactly
|
|
21
|
+
- Improved JWT signing algorithm to match Node.js SDK
|
|
22
|
+
- Added `iat` (issued at) and `expires` timestamp fields to JWT
|
|
23
|
+
- Added `attributes` field support in JwtPayload for custom user attributes
|
|
24
|
+
- Fixed base64url encoding
|
|
25
|
+
- Added comprehensive tests verifying JWT output matches Node.js SDK byte-for-byte
|
|
26
|
+
|
|
27
|
+
### Breaking Changes
|
|
28
|
+
|
|
29
|
+
- JWT structure changed significantly - tokens from 0.0.2 are incompatible with 0.0.3
|
|
30
|
+
- JWTs now include `iat` and `expires` fields for proper token lifecycle management
|
|
31
|
+
|
|
10
32
|
## [0.0.2] - 2025-01-31
|
|
11
33
|
|
|
12
34
|
### Fixed
|
|
35
|
+
|
|
13
36
|
- **BREAKING FIX**: JWT payload format now matches TypeScript SDK
|
|
14
37
|
- `identifiers` changed from `Dict[str, str]` to `List[Dict]` with `type` and `value` fields
|
|
15
38
|
- `groups` structure now properly includes `type`, `id`/`groupId`, and `name` fields
|
|
@@ -18,9 +41,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
18
41
|
- Updated documentation with correct JWT generation examples
|
|
19
42
|
|
|
20
43
|
### Migration Guide
|
|
44
|
+
|
|
21
45
|
If you're upgrading from 0.0.1, update your JWT generation code:
|
|
22
46
|
|
|
23
47
|
**Before (0.0.1):**
|
|
48
|
+
|
|
24
49
|
```python
|
|
25
50
|
jwt = vortex.generate_jwt({
|
|
26
51
|
"user_id": "user-123",
|
|
@@ -30,6 +55,7 @@ jwt = vortex.generate_jwt({
|
|
|
30
55
|
```
|
|
31
56
|
|
|
32
57
|
**After (0.0.2):**
|
|
58
|
+
|
|
33
59
|
```python
|
|
34
60
|
jwt = vortex.generate_jwt({
|
|
35
61
|
"user_id": "user-123",
|
|
@@ -41,6 +67,7 @@ jwt = vortex.generate_jwt({
|
|
|
41
67
|
## [0.0.1] - 2024-10-10
|
|
42
68
|
|
|
43
69
|
### Added
|
|
70
|
+
|
|
44
71
|
- Initial release of Vortex Python SDK
|
|
45
72
|
- JWT generation with HMAC-SHA256 signing
|
|
46
73
|
- Complete invitation management API
|
|
@@ -51,6 +78,7 @@ jwt = vortex.generate_jwt({
|
|
|
51
78
|
- Full compatibility with Node.js SDK API
|
|
52
79
|
|
|
53
80
|
### Features
|
|
81
|
+
|
|
54
82
|
- `generate_jwt()` - Generate Vortex JWT tokens
|
|
55
83
|
- `get_invitations_by_target()` - Get invitations by email/username/phone
|
|
56
84
|
- `accept_invitations()` - Accept multiple invitations
|
|
@@ -60,4 +88,4 @@ jwt = vortex.generate_jwt({
|
|
|
60
88
|
- `delete_invitations_by_group()` - Delete all group invitations
|
|
61
89
|
- `reinvite()` - Reinvite functionality
|
|
62
90
|
- Both async and sync versions of all methods
|
|
63
|
-
- 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
|
+
Version: 0.0.5
|
|
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
|
|
@@ -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
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vortex-python-sdk"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.5"
|
|
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.
|
|
3
|
+
Version: 0.0.5
|
|
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
|
|
@@ -4,21 +4,21 @@ 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
|
+
AcceptInvitationsRequest,
|
|
9
|
+
ApiResponse,
|
|
9
10
|
AuthenticatedUser,
|
|
10
|
-
|
|
11
|
-
IdentifierInput,
|
|
11
|
+
CreateInvitationRequest,
|
|
12
12
|
GroupInput,
|
|
13
|
-
|
|
13
|
+
IdentifierInput,
|
|
14
14
|
Invitation,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
VortexApiError
|
|
15
|
+
InvitationTarget,
|
|
16
|
+
JwtPayload,
|
|
17
|
+
VortexApiError,
|
|
19
18
|
)
|
|
19
|
+
from .vortex import Vortex
|
|
20
20
|
|
|
21
|
-
__version__ = "0.0.
|
|
21
|
+
__version__ = "0.0.5"
|
|
22
22
|
__author__ = "TeamVortexSoftware"
|
|
23
23
|
__email__ = "support@vortexsoftware.com"
|
|
24
24
|
|
|
@@ -34,4 +34,4 @@ __all__ = [
|
|
|
34
34
|
"AcceptInvitationsRequest",
|
|
35
35
|
"ApiResponse",
|
|
36
36
|
"VortexApiError",
|
|
37
|
-
]
|
|
37
|
+
]
|
|
@@ -1,18 +1,23 @@
|
|
|
1
|
-
from typing import Dict, List, Optional, Union
|
|
1
|
+
from typing import Any, Dict, List, Literal, Optional, Union
|
|
2
|
+
|
|
2
3
|
from pydantic import BaseModel, Field
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
class IdentifierInput(BaseModel):
|
|
6
7
|
"""Identifier structure for JWT generation"""
|
|
8
|
+
|
|
7
9
|
type: Literal["email", "sms"]
|
|
8
10
|
value: str
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
class GroupInput(BaseModel):
|
|
12
14
|
"""Group structure for JWT generation (input)"""
|
|
15
|
+
|
|
13
16
|
type: str
|
|
14
17
|
id: Optional[str] = None # Legacy field (deprecated, use groupId)
|
|
15
|
-
groupId: Optional[str] = Field(
|
|
18
|
+
groupId: Optional[str] = Field(
|
|
19
|
+
None, alias="group_id", serialization_alias="groupId"
|
|
20
|
+
) # Preferred: Customer's group ID
|
|
16
21
|
name: str
|
|
17
22
|
|
|
18
23
|
class Config:
|
|
@@ -24,12 +29,13 @@ class InvitationGroup(BaseModel):
|
|
|
24
29
|
Invitation group from API responses
|
|
25
30
|
This matches the MemberGroups table structure from the API
|
|
26
31
|
"""
|
|
32
|
+
|
|
27
33
|
id: str # Vortex internal UUID
|
|
28
|
-
account_id: str # Vortex account ID
|
|
29
|
-
group_id: str # Customer's group ID
|
|
34
|
+
account_id: str = Field(alias="accountId") # Vortex account ID
|
|
35
|
+
group_id: str = Field(alias="groupId") # Customer's group ID
|
|
30
36
|
type: str # Group type (e.g., "workspace", "team")
|
|
31
37
|
name: str # Group name
|
|
32
|
-
created_at: str # ISO 8601 timestamp
|
|
38
|
+
created_at: str = Field(alias="createdAt") # ISO 8601 timestamp
|
|
33
39
|
|
|
34
40
|
class Config:
|
|
35
41
|
# Allow both snake_case (Python) and camelCase (JSON) field names
|
|
@@ -41,7 +47,7 @@ class InvitationGroup(BaseModel):
|
|
|
41
47
|
"groupId": "workspace-123",
|
|
42
48
|
"type": "workspace",
|
|
43
49
|
"name": "My Workspace",
|
|
44
|
-
"createdAt": "2025-01-27T12:00:00.000Z"
|
|
50
|
+
"createdAt": "2025-01-27T12:00:00.000Z",
|
|
45
51
|
}
|
|
46
52
|
}
|
|
47
53
|
|
|
@@ -58,6 +64,7 @@ class JwtPayload(BaseModel):
|
|
|
58
64
|
identifiers: List[IdentifierInput]
|
|
59
65
|
groups: Optional[List[GroupInput]] = None
|
|
60
66
|
role: Optional[str] = None
|
|
67
|
+
attributes: Optional[Dict[str, Any]] = None
|
|
61
68
|
|
|
62
69
|
|
|
63
70
|
class InvitationTarget(BaseModel):
|
|
@@ -67,14 +74,17 @@ class InvitationTarget(BaseModel):
|
|
|
67
74
|
|
|
68
75
|
class Invitation(BaseModel):
|
|
69
76
|
id: str
|
|
70
|
-
target: InvitationTarget
|
|
71
|
-
groups: Optional[List[InvitationGroup]] = None # Full group information
|
|
77
|
+
target: Union[InvitationTarget, List[InvitationTarget]] # API returns list or single
|
|
78
|
+
groups: Optional[List[Optional[InvitationGroup]]] = None # Full group information, can contain None
|
|
72
79
|
status: str
|
|
73
|
-
created_at: str
|
|
74
|
-
updated_at: Optional[str] = None
|
|
75
|
-
expires_at: Optional[str] = None
|
|
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")
|
|
76
83
|
metadata: Optional[Dict[str, Union[str, int, bool]]] = None
|
|
77
84
|
|
|
85
|
+
class Config:
|
|
86
|
+
populate_by_name = True
|
|
87
|
+
|
|
78
88
|
|
|
79
89
|
class CreateInvitationRequest(BaseModel):
|
|
80
90
|
target: InvitationTarget
|
|
@@ -99,4 +109,4 @@ class VortexApiError(Exception):
|
|
|
99
109
|
def __init__(self, message: str, status_code: int = 500):
|
|
100
110
|
self.message = message
|
|
101
111
|
self.status_code = status_code
|
|
102
|
-
super().__init__(message)
|
|
112
|
+
super().__init__(message)
|
|
@@ -1,96 +1,155 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import hmac
|
|
3
|
-
import hashlib
|
|
4
1
|
import base64
|
|
5
|
-
|
|
2
|
+
import hashlib
|
|
3
|
+
import hmac
|
|
4
|
+
import json
|
|
5
|
+
import time
|
|
6
|
+
import uuid
|
|
7
|
+
from typing import Dict, List, Literal, Optional, Union
|
|
6
8
|
from urllib.parse import urlencode
|
|
9
|
+
|
|
7
10
|
import httpx
|
|
11
|
+
|
|
8
12
|
from .types import (
|
|
9
|
-
JwtPayload,
|
|
10
|
-
InvitationTarget,
|
|
11
|
-
Invitation,
|
|
12
|
-
CreateInvitationRequest,
|
|
13
13
|
AcceptInvitationsRequest,
|
|
14
14
|
ApiResponse,
|
|
15
|
-
|
|
15
|
+
CreateInvitationRequest,
|
|
16
|
+
Invitation,
|
|
17
|
+
InvitationTarget,
|
|
18
|
+
JwtPayload,
|
|
19
|
+
VortexApiError,
|
|
16
20
|
)
|
|
17
21
|
|
|
18
22
|
|
|
23
|
+
def _get_version():
|
|
24
|
+
"""Lazy import of version to avoid circular import"""
|
|
25
|
+
from . import __version__
|
|
26
|
+
return __version__
|
|
27
|
+
|
|
28
|
+
|
|
19
29
|
class Vortex:
|
|
20
|
-
def __init__(
|
|
30
|
+
def __init__(
|
|
31
|
+
self, api_key: str, base_url: str = "https://api.vortexsoftware.com/api/v1"
|
|
32
|
+
):
|
|
21
33
|
"""
|
|
22
34
|
Initialize Vortex client
|
|
23
35
|
|
|
24
36
|
Args:
|
|
25
37
|
api_key: Your Vortex API key
|
|
26
|
-
base_url: Base URL for Vortex API (default: https://api.vortexsoftware.com)
|
|
38
|
+
base_url: Base URL for Vortex API (default: https://api.vortexsoftware.com/api/v1)
|
|
27
39
|
"""
|
|
28
40
|
self.api_key = api_key
|
|
29
|
-
self.base_url = base_url.rstrip(
|
|
41
|
+
self.base_url = base_url.rstrip("/")
|
|
30
42
|
self._client = httpx.AsyncClient()
|
|
31
43
|
self._sync_client = httpx.Client()
|
|
32
44
|
|
|
33
45
|
def generate_jwt(self, payload: Union[JwtPayload, Dict]) -> str:
|
|
34
46
|
"""
|
|
35
|
-
Generate a JWT token for the given payload
|
|
47
|
+
Generate a JWT token for the given payload matching Node.js SDK implementation
|
|
36
48
|
|
|
37
49
|
Args:
|
|
38
50
|
payload: JWT payload containing user_id, identifiers, groups, and role
|
|
39
51
|
|
|
40
52
|
Returns:
|
|
41
53
|
JWT token string
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
ValueError: If API key format is invalid
|
|
42
57
|
"""
|
|
43
58
|
if isinstance(payload, dict):
|
|
44
59
|
payload = JwtPayload(**payload)
|
|
45
60
|
|
|
46
|
-
#
|
|
61
|
+
# Parse API key (format: VRTX.base64url(uuid).key)
|
|
62
|
+
parts = self.api_key.split(".")
|
|
63
|
+
if len(parts) != 3:
|
|
64
|
+
raise ValueError("Invalid API key format. Expected: VRTX.{encodedId}.{key}")
|
|
65
|
+
|
|
66
|
+
prefix, encoded_id, key = parts
|
|
67
|
+
|
|
68
|
+
if prefix != "VRTX":
|
|
69
|
+
raise ValueError("Invalid API key prefix. Expected: VRTX")
|
|
70
|
+
|
|
71
|
+
# Decode UUID from base64url
|
|
72
|
+
# Add padding if needed
|
|
73
|
+
padding = 4 - len(encoded_id) % 4
|
|
74
|
+
if padding != 4:
|
|
75
|
+
encoded_id_padded = encoded_id + ("=" * padding)
|
|
76
|
+
else:
|
|
77
|
+
encoded_id_padded = encoded_id
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
uuid_bytes = base64.urlsafe_b64decode(encoded_id_padded)
|
|
81
|
+
kid = str(uuid.UUID(bytes=uuid_bytes))
|
|
82
|
+
except Exception as e:
|
|
83
|
+
raise ValueError(f"Invalid UUID in API key: {e}")
|
|
84
|
+
|
|
85
|
+
# Generate timestamps
|
|
86
|
+
iat = int(time.time())
|
|
87
|
+
expires = iat + 3600
|
|
88
|
+
|
|
89
|
+
# Step 1: Derive signing key from API key + UUID
|
|
90
|
+
signing_key = hmac.new(key.encode(), kid.encode(), hashlib.sha256).digest()
|
|
91
|
+
|
|
92
|
+
# Step 2: Build header + payload
|
|
47
93
|
header = {
|
|
94
|
+
"iat": iat,
|
|
48
95
|
"alg": "HS256",
|
|
49
|
-
"typ": "JWT"
|
|
96
|
+
"typ": "JWT",
|
|
97
|
+
"kid": kid,
|
|
50
98
|
}
|
|
51
99
|
|
|
52
|
-
#
|
|
53
|
-
|
|
54
|
-
"
|
|
55
|
-
|
|
56
|
-
}
|
|
100
|
+
# Serialize identifiers
|
|
101
|
+
identifiers_list = [
|
|
102
|
+
{"type": id.type, "value": id.value} for id in payload.identifiers
|
|
103
|
+
]
|
|
57
104
|
|
|
105
|
+
# Serialize groups
|
|
106
|
+
groups_list = None
|
|
58
107
|
if payload.groups is not None:
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
+
}
|
|
62
115
|
for group in payload.groups
|
|
63
116
|
]
|
|
64
|
-
if payload.role is not None:
|
|
65
|
-
jwt_payload["role"] = payload.role
|
|
66
117
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
118
|
+
jwt_payload = {
|
|
119
|
+
"userId": payload.user_id,
|
|
120
|
+
"groups": groups_list,
|
|
121
|
+
"role": payload.role,
|
|
122
|
+
"expires": expires,
|
|
123
|
+
"identifiers": identifiers_list,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# Add attributes if provided
|
|
127
|
+
if hasattr(payload, "attributes") and payload.attributes:
|
|
128
|
+
jwt_payload["attributes"] = payload.attributes
|
|
129
|
+
|
|
130
|
+
# Step 3: Base64URL encode (without padding)
|
|
131
|
+
header_json = json.dumps(header, separators=(",", ":"))
|
|
132
|
+
payload_json = json.dumps(jwt_payload, separators=(",", ":"))
|
|
71
133
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
134
|
+
header_b64 = base64.urlsafe_b64encode(header_json.encode()).decode().rstrip("=")
|
|
135
|
+
payload_b64 = (
|
|
136
|
+
base64.urlsafe_b64encode(payload_json.encode()).decode().rstrip("=")
|
|
137
|
+
)
|
|
75
138
|
|
|
76
|
-
#
|
|
77
|
-
|
|
78
|
-
signature = hmac.new(
|
|
79
|
-
self.api_key.encode(),
|
|
80
|
-
message.encode(),
|
|
81
|
-
hashlib.sha256
|
|
82
|
-
).digest()
|
|
139
|
+
# Step 4: Sign
|
|
140
|
+
to_sign = f"{header_b64}.{payload_b64}"
|
|
141
|
+
signature = hmac.new(signing_key, to_sign.encode(), hashlib.sha256).digest()
|
|
83
142
|
|
|
84
|
-
|
|
143
|
+
signature_b64 = base64.urlsafe_b64encode(signature).decode().rstrip("=")
|
|
85
144
|
|
|
86
|
-
return f"{
|
|
145
|
+
return f"{to_sign}.{signature_b64}"
|
|
87
146
|
|
|
88
147
|
async def _vortex_api_request(
|
|
89
148
|
self,
|
|
90
149
|
method: str,
|
|
91
150
|
endpoint: str,
|
|
92
151
|
data: Optional[Dict] = None,
|
|
93
|
-
params: Optional[Dict] = None
|
|
152
|
+
params: Optional[Dict] = None,
|
|
94
153
|
) -> Dict:
|
|
95
154
|
"""
|
|
96
155
|
Make an API request to Vortex
|
|
@@ -109,26 +168,27 @@ class Vortex:
|
|
|
109
168
|
"""
|
|
110
169
|
url = f"{self.base_url}{endpoint}"
|
|
111
170
|
headers = {
|
|
112
|
-
"
|
|
171
|
+
"x-api-key": f"{self.api_key}",
|
|
113
172
|
"Content-Type": "application/json",
|
|
114
|
-
"User-Agent": "vortex-python-sdk/
|
|
173
|
+
"User-Agent": f"vortex-python-sdk/{_get_version()}",
|
|
115
174
|
}
|
|
116
175
|
|
|
117
176
|
try:
|
|
118
177
|
response = await self._client.request(
|
|
119
|
-
method=method,
|
|
120
|
-
url=url,
|
|
121
|
-
json=data,
|
|
122
|
-
params=params,
|
|
123
|
-
headers=headers
|
|
178
|
+
method=method, url=url, json=data, params=params, headers=headers
|
|
124
179
|
)
|
|
125
180
|
|
|
126
181
|
if response.status_code >= 400:
|
|
127
182
|
try:
|
|
128
183
|
error_data = response.json()
|
|
129
|
-
error_message = error_data.get(
|
|
184
|
+
error_message = error_data.get(
|
|
185
|
+
"error",
|
|
186
|
+
f"API request failed with status {response.status_code}",
|
|
187
|
+
)
|
|
130
188
|
except:
|
|
131
|
-
error_message =
|
|
189
|
+
error_message = (
|
|
190
|
+
f"API request failed with status {response.status_code}"
|
|
191
|
+
)
|
|
132
192
|
|
|
133
193
|
raise VortexApiError(error_message, response.status_code)
|
|
134
194
|
|
|
@@ -142,7 +202,7 @@ class Vortex:
|
|
|
142
202
|
method: str,
|
|
143
203
|
endpoint: str,
|
|
144
204
|
data: Optional[Dict] = None,
|
|
145
|
-
params: Optional[Dict] = None
|
|
205
|
+
params: Optional[Dict] = None,
|
|
146
206
|
) -> Dict:
|
|
147
207
|
"""
|
|
148
208
|
Make a synchronous API request to Vortex
|
|
@@ -161,26 +221,27 @@ class Vortex:
|
|
|
161
221
|
"""
|
|
162
222
|
url = f"{self.base_url}{endpoint}"
|
|
163
223
|
headers = {
|
|
164
|
-
"
|
|
224
|
+
"x-api-key": f"{self.api_key}",
|
|
165
225
|
"Content-Type": "application/json",
|
|
166
|
-
"User-Agent": "vortex-python-sdk/
|
|
226
|
+
"User-Agent": f"vortex-python-sdk/{_get_version()}",
|
|
167
227
|
}
|
|
168
228
|
|
|
169
229
|
try:
|
|
170
230
|
response = self._sync_client.request(
|
|
171
|
-
method=method,
|
|
172
|
-
url=url,
|
|
173
|
-
json=data,
|
|
174
|
-
params=params,
|
|
175
|
-
headers=headers
|
|
231
|
+
method=method, url=url, json=data, params=params, headers=headers
|
|
176
232
|
)
|
|
177
233
|
|
|
178
234
|
if response.status_code >= 400:
|
|
179
235
|
try:
|
|
180
236
|
error_data = response.json()
|
|
181
|
-
error_message = error_data.get(
|
|
237
|
+
error_message = error_data.get(
|
|
238
|
+
"error",
|
|
239
|
+
f"API request failed with status {response.status_code}",
|
|
240
|
+
)
|
|
182
241
|
except:
|
|
183
|
-
error_message =
|
|
242
|
+
error_message = (
|
|
243
|
+
f"API request failed with status {response.status_code}"
|
|
244
|
+
)
|
|
184
245
|
|
|
185
246
|
raise VortexApiError(error_message, response.status_code)
|
|
186
247
|
|
|
@@ -192,7 +253,7 @@ class Vortex:
|
|
|
192
253
|
async def get_invitations_by_target(
|
|
193
254
|
self,
|
|
194
255
|
target_type: Literal["email", "username", "phoneNumber"],
|
|
195
|
-
target_value: str
|
|
256
|
+
target_value: str,
|
|
196
257
|
) -> List[Invitation]:
|
|
197
258
|
"""
|
|
198
259
|
Get invitations for a specific target
|
|
@@ -204,18 +265,17 @@ class Vortex:
|
|
|
204
265
|
Returns:
|
|
205
266
|
List of invitations
|
|
206
267
|
"""
|
|
207
|
-
params = {
|
|
208
|
-
"targetType": target_type,
|
|
209
|
-
"targetValue": target_value
|
|
210
|
-
}
|
|
268
|
+
params = {"targetType": target_type, "targetValue": target_value}
|
|
211
269
|
|
|
212
|
-
response = await self._vortex_api_request(
|
|
270
|
+
response = await self._vortex_api_request(
|
|
271
|
+
"GET", "/invitations/by-target", params=params
|
|
272
|
+
)
|
|
213
273
|
return [Invitation(**inv) for inv in response.get("invitations", [])]
|
|
214
274
|
|
|
215
275
|
def get_invitations_by_target_sync(
|
|
216
276
|
self,
|
|
217
277
|
target_type: Literal["email", "username", "phoneNumber"],
|
|
218
|
-
target_value: str
|
|
278
|
+
target_value: str,
|
|
219
279
|
) -> List[Invitation]:
|
|
220
280
|
"""
|
|
221
281
|
Get invitations for a specific target (synchronous)
|
|
@@ -227,12 +287,11 @@ class Vortex:
|
|
|
227
287
|
Returns:
|
|
228
288
|
List of invitations
|
|
229
289
|
"""
|
|
230
|
-
params = {
|
|
231
|
-
"targetType": target_type,
|
|
232
|
-
"targetValue": target_value
|
|
233
|
-
}
|
|
290
|
+
params = {"targetType": target_type, "targetValue": target_value}
|
|
234
291
|
|
|
235
|
-
response = self._vortex_api_request_sync(
|
|
292
|
+
response = self._vortex_api_request_sync(
|
|
293
|
+
"GET", "/invitations/by-target", params=params
|
|
294
|
+
)
|
|
236
295
|
return [Invitation(**inv) for inv in response.get("invitations", [])]
|
|
237
296
|
|
|
238
297
|
async def get_invitation(self, invitation_id: str) -> Invitation:
|
|
@@ -245,7 +304,9 @@ class Vortex:
|
|
|
245
304
|
Returns:
|
|
246
305
|
Invitation object
|
|
247
306
|
"""
|
|
248
|
-
response = await self._vortex_api_request(
|
|
307
|
+
response = await self._vortex_api_request(
|
|
308
|
+
"GET", f"/invitations/{invitation_id}"
|
|
309
|
+
)
|
|
249
310
|
return Invitation(**response)
|
|
250
311
|
|
|
251
312
|
def get_invitation_sync(self, invitation_id: str) -> Invitation:
|
|
@@ -262,9 +323,7 @@ class Vortex:
|
|
|
262
323
|
return Invitation(**response)
|
|
263
324
|
|
|
264
325
|
async def accept_invitations(
|
|
265
|
-
self,
|
|
266
|
-
invitation_ids: List[str],
|
|
267
|
-
target: Union[InvitationTarget, Dict[str, str]]
|
|
326
|
+
self, invitation_ids: List[str], target: Union[InvitationTarget, Dict[str, str]]
|
|
268
327
|
) -> Dict:
|
|
269
328
|
"""
|
|
270
329
|
Accept multiple invitations
|
|
@@ -279,17 +338,12 @@ class Vortex:
|
|
|
279
338
|
if isinstance(target, dict):
|
|
280
339
|
target = InvitationTarget(**target)
|
|
281
340
|
|
|
282
|
-
data = {
|
|
283
|
-
"invitationIds": invitation_ids,
|
|
284
|
-
"target": target.model_dump()
|
|
285
|
-
}
|
|
341
|
+
data = {"invitationIds": invitation_ids, "target": target.model_dump()}
|
|
286
342
|
|
|
287
343
|
return await self._vortex_api_request("POST", "/invitations/accept", data=data)
|
|
288
344
|
|
|
289
345
|
def accept_invitations_sync(
|
|
290
|
-
self,
|
|
291
|
-
invitation_ids: List[str],
|
|
292
|
-
target: Union[InvitationTarget, Dict[str, str]]
|
|
346
|
+
self, invitation_ids: List[str], target: Union[InvitationTarget, Dict[str, str]]
|
|
293
347
|
) -> Dict:
|
|
294
348
|
"""
|
|
295
349
|
Accept multiple invitations (synchronous)
|
|
@@ -304,10 +358,7 @@ class Vortex:
|
|
|
304
358
|
if isinstance(target, dict):
|
|
305
359
|
target = InvitationTarget(**target)
|
|
306
360
|
|
|
307
|
-
data = {
|
|
308
|
-
"invitationIds": invitation_ids,
|
|
309
|
-
"target": target.model_dump()
|
|
310
|
-
}
|
|
361
|
+
data = {"invitationIds": invitation_ids, "target": target.model_dump()}
|
|
311
362
|
|
|
312
363
|
return self._vortex_api_request_sync("POST", "/invitations/accept", data=data)
|
|
313
364
|
|
|
@@ -336,9 +387,7 @@ class Vortex:
|
|
|
336
387
|
return self._vortex_api_request_sync("DELETE", f"/invitations/{invitation_id}")
|
|
337
388
|
|
|
338
389
|
async def get_invitations_by_group(
|
|
339
|
-
self,
|
|
340
|
-
group_type: str,
|
|
341
|
-
group_id: str
|
|
390
|
+
self, group_type: str, group_id: str
|
|
342
391
|
) -> List[Invitation]:
|
|
343
392
|
"""
|
|
344
393
|
Get invitations for a specific group
|
|
@@ -350,13 +399,13 @@ class Vortex:
|
|
|
350
399
|
Returns:
|
|
351
400
|
List of invitations
|
|
352
401
|
"""
|
|
353
|
-
response = await self._vortex_api_request(
|
|
402
|
+
response = await self._vortex_api_request(
|
|
403
|
+
"GET", f"/invitations/by-group/{group_type}/{group_id}"
|
|
404
|
+
)
|
|
354
405
|
return [Invitation(**inv) for inv in response.get("invitations", [])]
|
|
355
406
|
|
|
356
407
|
def get_invitations_by_group_sync(
|
|
357
|
-
self,
|
|
358
|
-
group_type: str,
|
|
359
|
-
group_id: str
|
|
408
|
+
self, group_type: str, group_id: str
|
|
360
409
|
) -> List[Invitation]:
|
|
361
410
|
"""
|
|
362
411
|
Get invitations for a specific group (synchronous)
|
|
@@ -368,14 +417,12 @@ class Vortex:
|
|
|
368
417
|
Returns:
|
|
369
418
|
List of invitations
|
|
370
419
|
"""
|
|
371
|
-
response = self._vortex_api_request_sync(
|
|
420
|
+
response = self._vortex_api_request_sync(
|
|
421
|
+
"GET", f"/invitations/by-group/{group_type}/{group_id}"
|
|
422
|
+
)
|
|
372
423
|
return [Invitation(**inv) for inv in response.get("invitations", [])]
|
|
373
424
|
|
|
374
|
-
async def delete_invitations_by_group(
|
|
375
|
-
self,
|
|
376
|
-
group_type: str,
|
|
377
|
-
group_id: str
|
|
378
|
-
) -> Dict:
|
|
425
|
+
async def delete_invitations_by_group(self, group_type: str, group_id: str) -> Dict:
|
|
379
426
|
"""
|
|
380
427
|
Delete all invitations for a specific group
|
|
381
428
|
|
|
@@ -386,13 +433,11 @@ class Vortex:
|
|
|
386
433
|
Returns:
|
|
387
434
|
API response
|
|
388
435
|
"""
|
|
389
|
-
return await self._vortex_api_request(
|
|
436
|
+
return await self._vortex_api_request(
|
|
437
|
+
"DELETE", f"/invitations/by-group/{group_type}/{group_id}"
|
|
438
|
+
)
|
|
390
439
|
|
|
391
|
-
def delete_invitations_by_group_sync(
|
|
392
|
-
self,
|
|
393
|
-
group_type: str,
|
|
394
|
-
group_id: str
|
|
395
|
-
) -> Dict:
|
|
440
|
+
def delete_invitations_by_group_sync(self, group_type: str, group_id: str) -> Dict:
|
|
396
441
|
"""
|
|
397
442
|
Delete all invitations for a specific group (synchronous)
|
|
398
443
|
|
|
@@ -403,7 +448,9 @@ class Vortex:
|
|
|
403
448
|
Returns:
|
|
404
449
|
API response
|
|
405
450
|
"""
|
|
406
|
-
return self._vortex_api_request_sync(
|
|
451
|
+
return self._vortex_api_request_sync(
|
|
452
|
+
"DELETE", f"/invitations/by-group/{group_type}/{group_id}"
|
|
453
|
+
)
|
|
407
454
|
|
|
408
455
|
async def reinvite(self, invitation_id: str) -> Invitation:
|
|
409
456
|
"""
|
|
@@ -415,7 +462,9 @@ class Vortex:
|
|
|
415
462
|
Returns:
|
|
416
463
|
Updated invitation object
|
|
417
464
|
"""
|
|
418
|
-
response = await self._vortex_api_request(
|
|
465
|
+
response = await self._vortex_api_request(
|
|
466
|
+
"POST", f"/invitations/{invitation_id}/reinvite"
|
|
467
|
+
)
|
|
419
468
|
return Invitation(**response)
|
|
420
469
|
|
|
421
470
|
def reinvite_sync(self, invitation_id: str) -> Invitation:
|
|
@@ -428,7 +477,9 @@ class Vortex:
|
|
|
428
477
|
Returns:
|
|
429
478
|
Updated invitation object
|
|
430
479
|
"""
|
|
431
|
-
response = self._vortex_api_request_sync(
|
|
480
|
+
response = self._vortex_api_request_sync(
|
|
481
|
+
"POST", f"/invitations/{invitation_id}/reinvite"
|
|
482
|
+
)
|
|
432
483
|
return Invitation(**response)
|
|
433
484
|
|
|
434
485
|
async def close(self):
|
|
@@ -453,4 +504,4 @@ class Vortex:
|
|
|
453
504
|
|
|
454
505
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
455
506
|
"""Context manager exit"""
|
|
456
|
-
self.close_sync()
|
|
507
|
+
self.close_sync()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{vortex_python_sdk-0.0.2 → vortex_python_sdk-0.0.5}/src/vortex_python_sdk.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{vortex_python_sdk-0.0.2 → vortex_python_sdk-0.0.5}/src/vortex_python_sdk.egg-info/requires.txt
RENAMED
|
File without changes
|
{vortex_python_sdk-0.0.2 → vortex_python_sdk-0.0.5}/src/vortex_python_sdk.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|