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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,9 @@
1
+ from .client import ShiftChatClient
2
+ from .exceptions import ShiftChatexception, AuthenticationError, APIError
3
+
4
+ __all__ = [
5
+ "ShiftChatClient",
6
+ "ShiftChatexception",
7
+ "AuthenticationError",
8
+ "APIError",
9
+ ]
@@ -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,4 @@
1
+ requests>=2.28.0
2
+
3
+ [dev]
4
+ pytest>=7.0.0