shift-chat-server-client 0.2.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.
- shift_chat_server_client-0.2.0/PKG-INFO +12 -0
- shift_chat_server_client-0.2.0/README.md +117 -0
- shift_chat_server_client-0.2.0/pyproject.toml +20 -0
- shift_chat_server_client-0.2.0/setup.cfg +4 -0
- shift_chat_server_client-0.2.0/setup.py +23 -0
- shift_chat_server_client-0.2.0/src/shift_chat/__init__.py +9 -0
- shift_chat_server_client-0.2.0/src/shift_chat/client.py +228 -0
- shift_chat_server_client-0.2.0/src/shift_chat/exceptions.py +11 -0
- shift_chat_server_client-0.2.0/src/shift_chat_server_client.egg-info/PKG-INFO +12 -0
- shift_chat_server_client-0.2.0/src/shift_chat_server_client.egg-info/SOURCES.txt +11 -0
- shift_chat_server_client-0.2.0/src/shift_chat_server_client.egg-info/dependency_links.txt +1 -0
- shift_chat_server_client-0.2.0/src/shift_chat_server_client.egg-info/requires.txt +4 -0
- shift_chat_server_client-0.2.0/src/shift_chat_server_client.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: shift-chat-server-client
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: A Python client for the Shift Chat Server
|
|
5
|
+
Author: Lucas
|
|
6
|
+
Author-email: Lucas <lucas@example.com>
|
|
7
|
+
Requires-Python: >=3.7
|
|
8
|
+
Requires-Dist: requests>=2.28.0
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
11
|
+
Dynamic: author
|
|
12
|
+
Dynamic: requires-python
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Shift Chat Server Client
|
|
2
|
+
|
|
3
|
+
Python client for interacting with the Shift Chat Server.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install .
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Initialization
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from shift_chat.client import ShiftChatClient
|
|
17
|
+
|
|
18
|
+
# Initialize the client with your credentials
|
|
19
|
+
client = ShiftChatClient(
|
|
20
|
+
base_url="http://localhost:3000",
|
|
21
|
+
client_id="your-client-id",
|
|
22
|
+
client_secret="your-client-secret"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# Authenticate (optional, methods will auto-authenticate if needed)
|
|
26
|
+
client.authenticate()
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Managing Users
|
|
30
|
+
|
|
31
|
+
You can create or update users using the `set_user` method. You must provide either an email address OR a phone number (or both).
|
|
32
|
+
|
|
33
|
+
**Create user with Email:**
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
user = client.set_user(
|
|
37
|
+
first_name="John",
|
|
38
|
+
last_name="Doe",
|
|
39
|
+
password="securePassword123",
|
|
40
|
+
email="john.doe@example.com"
|
|
41
|
+
)
|
|
42
|
+
print(f"Created user: {user['id']}")
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Create user with Phone Number:**
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
user = client.set_user(
|
|
49
|
+
first_name="Jane",
|
|
50
|
+
last_name="Smith",
|
|
51
|
+
password="securePassword123",
|
|
52
|
+
phone_number="+15550123456"
|
|
53
|
+
)
|
|
54
|
+
print(f"Created user: {user['id']}")
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**Create user with Both:**
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
user = client.set_user(
|
|
61
|
+
first_name="Alice",
|
|
62
|
+
last_name="Wonder",
|
|
63
|
+
password="securePassword123",
|
|
64
|
+
email="alice@example.com",
|
|
65
|
+
phone_number="+15550198765"
|
|
66
|
+
)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Managing Workspaces and Channels
|
|
70
|
+
|
|
71
|
+
You can add users to workspaces and channels using their IDs.
|
|
72
|
+
|
|
73
|
+
**Add User to Workspace:**
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
client.add_user_to_workspace(
|
|
77
|
+
user_id="user-uuid-123",
|
|
78
|
+
workspace_id="workspace-uuid-456"
|
|
79
|
+
)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Add User to Channel:**
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
client.add_user_to_channel(
|
|
86
|
+
user_id="user-uuid-123",
|
|
87
|
+
channel_id="channel-uuid-789"
|
|
88
|
+
)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Get Workspaces with Channels (Formatted for Mobile):**
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
# Returns workspaces with nested channels and channel members
|
|
95
|
+
data = user_client.get_workspaces_with_channels()
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Acting on Behalf of a User
|
|
99
|
+
|
|
100
|
+
Once a user exists, you can perform actions on their behalf using `authenticate_user`.
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
# Get a user-specific client
|
|
104
|
+
user_client = client.authenticate_user(user_id="user-uuid-123")
|
|
105
|
+
|
|
106
|
+
# List workspaces the user belongs to
|
|
107
|
+
workspaces = user_client.get_workspaces()
|
|
108
|
+
for ws in workspaces:
|
|
109
|
+
print(f"Workspace: {ws['name']}")
|
|
110
|
+
|
|
111
|
+
# Create a channel
|
|
112
|
+
channel = user_client.set_channel(
|
|
113
|
+
workspace_id="workspace-uuid-123",
|
|
114
|
+
name="general-discussion",
|
|
115
|
+
is_public=True
|
|
116
|
+
)
|
|
117
|
+
```
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "shift-chat-server-client"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "A Python client for the Shift Chat Server"
|
|
9
|
+
authors = [
|
|
10
|
+
{ name = "Lucas", email = "lucas@example.com" },
|
|
11
|
+
]
|
|
12
|
+
dependencies = [
|
|
13
|
+
"requests>=2.28.0",
|
|
14
|
+
]
|
|
15
|
+
requires-python = ">=3.7"
|
|
16
|
+
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
dev = [
|
|
19
|
+
"pytest>=7.0.0",
|
|
20
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="shift-chat-server-client",
|
|
5
|
+
version="0.2.0",
|
|
6
|
+
description="A Python client for the Shift Chat Server",
|
|
7
|
+
author="Lucas",
|
|
8
|
+
author_email="lucas@example.com",
|
|
9
|
+
packages=find_packages(where="src"),
|
|
10
|
+
package_dir={"": "src"},
|
|
11
|
+
install_requires=[
|
|
12
|
+
"requests>=2.28.0",
|
|
13
|
+
"requests-toolbelt>=0.10.1", # useful for multipart uploads if we add file upload later
|
|
14
|
+
],
|
|
15
|
+
extras_require={
|
|
16
|
+
"dev": [
|
|
17
|
+
"pytest>=7.0.0",
|
|
18
|
+
"flake8>=6.0.0",
|
|
19
|
+
"black>=23.0.0",
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
python_requires=">=3.7",
|
|
23
|
+
)
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from typing import Optional, Dict, List
|
|
3
|
+
from .exceptions import AuthenticationError, APIError
|
|
4
|
+
|
|
5
|
+
class ShiftChatClient:
|
|
6
|
+
def __init__(self, base_url: str, client_id: str, client_secret: str):
|
|
7
|
+
self.base_url = base_url.rstrip('/')
|
|
8
|
+
self.client_id = client_id
|
|
9
|
+
self.client_secret = client_secret
|
|
10
|
+
self._access_token: Optional[str] = None
|
|
11
|
+
self._session = requests.Session()
|
|
12
|
+
|
|
13
|
+
def authenticate(self) -> str:
|
|
14
|
+
"""Authenticate as a client and get an access token."""
|
|
15
|
+
url = f"{self.base_url}/auth/client/login"
|
|
16
|
+
payload = {
|
|
17
|
+
"clientId": self.client_id,
|
|
18
|
+
"clientSecret": self.client_secret
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
response = self._session.post(url, json=payload)
|
|
23
|
+
response.raise_for_status()
|
|
24
|
+
data = response.json()
|
|
25
|
+
self._access_token = data.get("accessToken")
|
|
26
|
+
if not self._access_token:
|
|
27
|
+
raise AuthenticationError("No access token received")
|
|
28
|
+
return self._access_token
|
|
29
|
+
except requests.exceptions.HTTPError as e:
|
|
30
|
+
raise AuthenticationError(f"Authentication failed: {str(e)}") from e
|
|
31
|
+
|
|
32
|
+
def _get_headers(self, token: Optional[str] = None, impersonate_token: Optional[str] = None) -> Dict[str, str]:
|
|
33
|
+
headers = {
|
|
34
|
+
"Content-Type": "application/json"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
auth_token = impersonate_token or token or self._access_token
|
|
38
|
+
if auth_token:
|
|
39
|
+
headers["Authorization"] = f"Bearer {auth_token}"
|
|
40
|
+
|
|
41
|
+
return headers
|
|
42
|
+
|
|
43
|
+
def authenticate_user(self, user_id: str) -> 'ShiftChatUserClient':
|
|
44
|
+
"""Authenticate a user via client delegation and return a user client."""
|
|
45
|
+
if not self._access_token:
|
|
46
|
+
self.authenticate()
|
|
47
|
+
|
|
48
|
+
url = f"{self.base_url}/auth/client/delegate"
|
|
49
|
+
payload = {"userId": user_id}
|
|
50
|
+
headers = self._get_headers()
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
response = self._session.post(url, json=payload, headers=headers)
|
|
54
|
+
response.raise_for_status()
|
|
55
|
+
data = response.json()
|
|
56
|
+
user_token = data.get("accessToken")
|
|
57
|
+
if not user_token:
|
|
58
|
+
raise AuthenticationError("No user token received")
|
|
59
|
+
|
|
60
|
+
return ShiftChatUserClient(self.base_url, user_token)
|
|
61
|
+
|
|
62
|
+
except requests.exceptions.HTTPError as e:
|
|
63
|
+
raise APIError(f"Failed to authenticate user: {str(e)}") from e
|
|
64
|
+
|
|
65
|
+
def set_user(self,
|
|
66
|
+
first_name: str,
|
|
67
|
+
last_name: str,
|
|
68
|
+
password: str,
|
|
69
|
+
email: Optional[str] = None,
|
|
70
|
+
phone_number: Optional[str] = None) -> Dict:
|
|
71
|
+
"""Create or update a user.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
first_name: User's first name
|
|
75
|
+
last_name: User's last name
|
|
76
|
+
password: User's password
|
|
77
|
+
email: User's email (optional if phone_number is provided)
|
|
78
|
+
phone_number: User's phone number (optional if email is provided)
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Dict: The created or updated user data
|
|
82
|
+
|
|
83
|
+
Raises:
|
|
84
|
+
ValueError: If neither email nor phone_number is provided
|
|
85
|
+
APIError: If the API request fails
|
|
86
|
+
"""
|
|
87
|
+
if not email and not phone_number:
|
|
88
|
+
raise ValueError("At least one of email or phone_number must be provided")
|
|
89
|
+
|
|
90
|
+
url = f"{self.base_url}/users"
|
|
91
|
+
payload = {
|
|
92
|
+
"firstName": first_name,
|
|
93
|
+
"lastName": last_name,
|
|
94
|
+
"password": password
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if email:
|
|
98
|
+
payload["email"] = email
|
|
99
|
+
if phone_number:
|
|
100
|
+
payload["phoneNumber"] = phone_number
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
response = self._session.post(url, json=payload)
|
|
104
|
+
if response.status_code == 201:
|
|
105
|
+
return response.json()
|
|
106
|
+
|
|
107
|
+
# If 409 (Conflict) or similar, we might want to update?
|
|
108
|
+
# But standard public register might not support update.
|
|
109
|
+
# Ideally we'd have a server-to-server endpoint for upserting users.
|
|
110
|
+
|
|
111
|
+
response.raise_for_status()
|
|
112
|
+
return response.json()
|
|
113
|
+
except requests.exceptions.HTTPError as e:
|
|
114
|
+
raise APIError(f"Failed to create user: {str(e)}") from e
|
|
115
|
+
|
|
116
|
+
class ShiftChatUserClient:
|
|
117
|
+
"""Client for performing actions on behalf of a user."""
|
|
118
|
+
def __init__(self, base_url: str, access_token: str):
|
|
119
|
+
self.base_url = base_url
|
|
120
|
+
self._access_token = access_token
|
|
121
|
+
self._session = requests.Session()
|
|
122
|
+
|
|
123
|
+
def _get_headers(self) -> Dict[str, str]:
|
|
124
|
+
return {
|
|
125
|
+
"Content-Type": "application/json",
|
|
126
|
+
"Authorization": f"Bearer {self._access_token}"
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
def add_user_to_workspace(self, user_id: str, workspace_id: str) -> None:
|
|
130
|
+
"""Add a user to a workspace."""
|
|
131
|
+
url = f"{self.base_url}/workspaces/invite"
|
|
132
|
+
payload = {
|
|
133
|
+
"userId": user_id,
|
|
134
|
+
"workspaceId": workspace_id
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
response = self._session.post(url, json=payload, headers=self._get_headers())
|
|
139
|
+
response.raise_for_status()
|
|
140
|
+
except requests.exceptions.HTTPError as e:
|
|
141
|
+
raise APIError(f"Failed to add user {user_id} to workspace {workspace_id}: {str(e)}") from e
|
|
142
|
+
|
|
143
|
+
def add_user_to_channel(self, user_id: str, channel_id: str) -> None:
|
|
144
|
+
"""Add a user to a channel."""
|
|
145
|
+
url = f"{self.base_url}/channels/invite"
|
|
146
|
+
payload = {
|
|
147
|
+
"userId": user_id,
|
|
148
|
+
"channelId": channel_id
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
response = self._session.post(url, json=payload, headers=self._get_headers())
|
|
153
|
+
response.raise_for_status()
|
|
154
|
+
except requests.exceptions.HTTPError as e:
|
|
155
|
+
raise APIError(f"Failed to add user {user_id} to channel {channel_id}: {str(e)}") from e
|
|
156
|
+
|
|
157
|
+
def set_channel(self, workspace_id: str, name: str, is_public: bool = False, type: str = "GROUP") -> Dict:
|
|
158
|
+
"""Create a channel."""
|
|
159
|
+
url = f"{self.base_url}/channels/group"
|
|
160
|
+
payload = {
|
|
161
|
+
"workspaceId": workspace_id,
|
|
162
|
+
"name": name,
|
|
163
|
+
"isPublic": is_public
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
response = self._session.post(url, json=payload, headers=self._get_headers())
|
|
168
|
+
response.raise_for_status()
|
|
169
|
+
return response.json()
|
|
170
|
+
except requests.exceptions.HTTPError as e:
|
|
171
|
+
raise APIError(f"Failed to create channel: {str(e)}") from e
|
|
172
|
+
|
|
173
|
+
def get_workspaces(self) -> List[Dict]:
|
|
174
|
+
"""Get all workspaces the user is a member of."""
|
|
175
|
+
url = f"{self.base_url}/workspaces"
|
|
176
|
+
try:
|
|
177
|
+
response = self._session.get(url, headers=self._get_headers())
|
|
178
|
+
response.raise_for_status()
|
|
179
|
+
return response.json()
|
|
180
|
+
except requests.exceptions.HTTPError as e:
|
|
181
|
+
raise APIError(f"Failed to get workspaces: {str(e)}") from e
|
|
182
|
+
|
|
183
|
+
def get_workspace_channels(self, workspace_id: str) -> List[Dict]:
|
|
184
|
+
"""Get all channels for a user in a specific workspace."""
|
|
185
|
+
url = f"{self.base_url}/channels/user"
|
|
186
|
+
params = {"workspaceId": workspace_id}
|
|
187
|
+
try:
|
|
188
|
+
response = self._session.get(url, headers=self._get_headers(), params=params)
|
|
189
|
+
response.raise_for_status()
|
|
190
|
+
return response.json()
|
|
191
|
+
except requests.exceptions.HTTPError as e:
|
|
192
|
+
raise APIError(f"Failed to get channels for workspace {workspace_id}: {str(e)}") from e
|
|
193
|
+
|
|
194
|
+
def get_workspaces_with_channels(self) -> List[Dict]:
|
|
195
|
+
"""Get all workspaces with their channels and members, formatted for mobile app."""
|
|
196
|
+
workspaces = self.get_workspaces()
|
|
197
|
+
for workspace in workspaces:
|
|
198
|
+
workspace_id = workspace.get("id")
|
|
199
|
+
if workspace_id:
|
|
200
|
+
try:
|
|
201
|
+
# fetch channels for the user in this workspace
|
|
202
|
+
channels = self.get_workspace_channels(workspace_id)
|
|
203
|
+
|
|
204
|
+
formatted_channels = []
|
|
205
|
+
for channel in channels:
|
|
206
|
+
# Extract basic info
|
|
207
|
+
channel_data = {
|
|
208
|
+
"channel": channel.get("name"),
|
|
209
|
+
"id": channel.get("id"),
|
|
210
|
+
"users": []
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
# Extract members if available (server should return them in members field)
|
|
214
|
+
members = channel.get("members", [])
|
|
215
|
+
users_list = []
|
|
216
|
+
for member in members:
|
|
217
|
+
# member structure from Prisma include usually has 'user' object inside
|
|
218
|
+
user_obj = member.get("user")
|
|
219
|
+
if user_obj:
|
|
220
|
+
users_list.append(user_obj)
|
|
221
|
+
|
|
222
|
+
channel_data["users"] = users_list
|
|
223
|
+
formatted_channels.append(channel_data)
|
|
224
|
+
|
|
225
|
+
workspace["channels"] = formatted_channels
|
|
226
|
+
except APIError:
|
|
227
|
+
workspace["channels"] = []
|
|
228
|
+
return workspaces
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
class ShiftChatexception(Exception):
|
|
2
|
+
"""Base exception for Shift Chat Client"""
|
|
3
|
+
pass
|
|
4
|
+
|
|
5
|
+
class AuthenticationError(ShiftChatexception):
|
|
6
|
+
"""Raised when authentication fails"""
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
class APIError(ShiftChatexception):
|
|
10
|
+
"""Raised when API request fails"""
|
|
11
|
+
pass
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: shift-chat-server-client
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: A Python client for the Shift Chat Server
|
|
5
|
+
Author: Lucas
|
|
6
|
+
Author-email: Lucas <lucas@example.com>
|
|
7
|
+
Requires-Python: >=3.7
|
|
8
|
+
Requires-Dist: requests>=2.28.0
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
11
|
+
Dynamic: author
|
|
12
|
+
Dynamic: requires-python
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
setup.py
|
|
4
|
+
src/shift_chat/__init__.py
|
|
5
|
+
src/shift_chat/client.py
|
|
6
|
+
src/shift_chat/exceptions.py
|
|
7
|
+
src/shift_chat_server_client.egg-info/PKG-INFO
|
|
8
|
+
src/shift_chat_server_client.egg-info/SOURCES.txt
|
|
9
|
+
src/shift_chat_server_client.egg-info/dependency_links.txt
|
|
10
|
+
src/shift_chat_server_client.egg-info/requires.txt
|
|
11
|
+
src/shift_chat_server_client.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
shift_chat
|