slack-objects 0.0.post31__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.
@@ -0,0 +1,261 @@
1
+ from __future__ import annotations
2
+
3
+ """
4
+ slack_objects.workspaces
5
+ =======================
6
+
7
+ Workspaces helper for the `slack-objects` package.
8
+
9
+ Merged/refactored from two legacy implementations:
10
+ - A "single-workspace" helper that fetches attributes via `team.info` (PCbot) :contentReference[oaicite:2]{index=2}
11
+ - A "grid admin" helper that lists workspaces and can list workspace users/admins via admin endpoints :contentReference[oaicite:3]{index=3}
12
+
13
+ Design goals:
14
+ - Factory-friendly: `workspaces = slack.workspaces()` or `ws = slack.workspaces("T123")`
15
+ - Modularity: public methods call wrapper methods; wrappers are the only place that directly call Slack API
16
+ - Usability: supports both "work with one workspace" and "work with many workspaces in a grid"
17
+ """
18
+
19
+ from dataclasses import dataclass, field
20
+ from typing import Any, Dict, List, Optional
21
+
22
+ from .base import SlackObjectBase
23
+ from .config import RateTier
24
+
25
+
26
+ @dataclass
27
+ class Workspaces(SlackObjectBase):
28
+ """
29
+ Workspaces domain helper.
30
+
31
+ Factory-style usage:
32
+ slack = SlackObjectsClient(cfg)
33
+ workspaces = slack.workspaces() # unbound (grid/listing helpers)
34
+ ws = slack.workspaces("T12345678") # bound to workspace_id
35
+
36
+ Notes:
37
+ - `workspace_id` is optional. Methods that require a workspace will enforce it.
38
+ - `workspaces_cache` is an optional cached list of workspaces (from admin.teams.list).
39
+ This enables fast name<->id lookups without re-fetching each time.
40
+ """
41
+ workspace_id: Optional[str] = None
42
+ attributes: Dict[str, Any] = field(default_factory=dict)
43
+
44
+ # Cache of workspaces returned by admin.teams.list (list of dicts with at least id/name)
45
+ workspaces_cache: List[Dict[str, Any]] = field(default_factory=list)
46
+
47
+ # ---------- factory helpers ----------
48
+
49
+ def with_workspace(self, workspace_id: str) -> "Workspaces":
50
+ """Return a new Workspaces instance bound to workspace_id, sharing cfg/client/logger/api."""
51
+ return Workspaces(
52
+ cfg=self.cfg,
53
+ client=self.client,
54
+ logger=self.logger,
55
+ api=self.api,
56
+ workspace_id=workspace_id,
57
+ workspaces_cache=self.workspaces_cache,
58
+ )
59
+
60
+ # ---------- attribute lifecycle ----------
61
+
62
+ def refresh(self, workspace_id: Optional[str] = None) -> Dict[str, Any]:
63
+ """
64
+ Refresh attributes for workspace_id (or self.workspace_id) using team.info.
65
+
66
+ This method is intentionally layered: it calls `get_workspace_info()`.
67
+ """
68
+ if workspace_id:
69
+ self.workspace_id = workspace_id
70
+ if not self.workspace_id:
71
+ raise ValueError("refresh() requires workspace_id (passed or already set)")
72
+
73
+ resp = self.get_workspace_info(self.workspace_id)
74
+ if not resp.get("ok"):
75
+ raise RuntimeError(f"Workspaces.get_workspace_info() failed: {resp}")
76
+
77
+ # `team.info` returns `team` on success in the legacy version :contentReference[oaicite:4]{index=4}
78
+ self.attributes = resp.get("team") or {}
79
+ return self.attributes
80
+
81
+ def _require_workspace_id(self, workspace_id: Optional[str] = None) -> str:
82
+ """Return a workspace_id or raise, used by methods that require one."""
83
+ wid = workspace_id or self.workspace_id
84
+ if not wid:
85
+ raise ValueError("This operation requires a workspace_id (passed or bound).")
86
+ return wid
87
+
88
+ # ============================================================
89
+ # Slack API wrapper layer
90
+ # ============================================================
91
+ # Only these methods should call `self.api.call(...)` directly.
92
+
93
+ def _team_info(self, workspace_id: str) -> Dict[str, Any]:
94
+ """Wrapper for team.info (fetch a workspace's metadata)."""
95
+ return self.api.call(self.client, "team.info", rate_tier=RateTier.TIER_3, team=workspace_id)
96
+
97
+ def _admin_teams_list(self, payload: Dict[str, Any]) -> Dict[str, Any]:
98
+ """Wrapper for admin.teams.list (Grid: list workspaces)."""
99
+ return self.api.call(self.client, "admin.teams.list", rate_tier=RateTier.TIER_3, **payload)
100
+
101
+ def _admin_users_list(self, payload: Dict[str, Any]) -> Dict[str, Any]:
102
+ """Wrapper for admin.users.list (list users in a workspace)."""
103
+ return self.api.call(self.client, "admin.users.list", rate_tier=RateTier.TIER_4, **payload)
104
+
105
+ def _admin_teams_admins_list(self, payload: Dict[str, Any]) -> Dict[str, Any]:
106
+ """Wrapper for admin.teams.admins.list (list admin IDs for a workspace)."""
107
+ return self.api.call(self.client, "admin.teams.admins.list", rate_tier=RateTier.TIER_3, **payload)
108
+
109
+ # ============================================================
110
+ # Public API (calls wrappers above)
111
+ # ============================================================
112
+
113
+ def get_workspace_info(self, workspace_id: str) -> Dict[str, Any]:
114
+ """Public method for team.info."""
115
+ return self._team_info(workspace_id)
116
+
117
+ def list_workspaces(self, *, force_refresh: bool = False) -> List[Dict[str, Any]]:
118
+ """
119
+ Return a list of workspaces in the Enterprise Grid (admin.teams.list), paginated.
120
+
121
+ This replaces the legacy constructor-side fetching of all workspaces :contentReference[oaicite:5]{index=5}.
122
+ Results are cached in `workspaces_cache` unless `force_refresh=True`.
123
+ """
124
+ if self.workspaces_cache and not force_refresh:
125
+ return self.workspaces_cache
126
+
127
+ workspaces: List[Dict[str, Any]] = []
128
+ payload: Dict[str, Any] = {}
129
+
130
+ while True:
131
+ resp = self._admin_teams_list(payload)
132
+ if not resp.get("ok"):
133
+ raise RuntimeError(f"admin.teams.list failed: {resp}")
134
+
135
+ teams = resp.get("teams") or []
136
+ workspaces.extend(teams)
137
+
138
+ # Slack commonly returns cursor pagination via response_metadata.next_cursor
139
+ meta = resp.get("response_metadata") or {}
140
+ cursor = meta.get("next_cursor") or ""
141
+ if cursor:
142
+ payload["cursor"] = cursor
143
+ else:
144
+ break
145
+
146
+ self.workspaces_cache = workspaces
147
+ return workspaces
148
+
149
+ # ----- name/id resolution helpers (from legacy SlackAdmin) -----
150
+
151
+ def get_workspace_name(self, workspace_id: str, *, force_refresh: bool = False) -> str:
152
+ """
153
+ Resolve a workspace ID -> workspace name using the cached list from admin.teams.list.
154
+
155
+ Legacy behavior raised if not found :contentReference[oaicite:6]{index=6}.
156
+ """
157
+ workspaces = self.list_workspaces(force_refresh=force_refresh)
158
+ for ws in workspaces:
159
+ if ws.get("id") == workspace_id:
160
+ name = ws.get("name")
161
+ if name:
162
+ return str(name)
163
+
164
+ raise ValueError(
165
+ f"Could not find a workspace with id '{workspace_id}'. "
166
+ "Check the id/token scopes and ensure you are targeting the correct Grid."
167
+ )
168
+
169
+ def get_workspace_id(self, workspace_name: str, *, force_refresh: bool = False) -> str:
170
+ """
171
+ Resolve a workspace name -> workspace ID using the cached list from admin.teams.list.
172
+
173
+ Legacy behavior raised if not found :contentReference[oaicite:7]{index=7}.
174
+ """
175
+ workspaces = self.list_workspaces(force_refresh=force_refresh)
176
+ target = workspace_name.strip().lower()
177
+
178
+ for ws in workspaces:
179
+ if str(ws.get("name", "")).strip().lower() == target:
180
+ wid = ws.get("id")
181
+ if wid:
182
+ return str(wid)
183
+
184
+ raise ValueError(
185
+ f"Could not find a workspace with name '{workspace_name}'. "
186
+ "Check the name/token scopes and ensure you are targeting the correct Grid."
187
+ )
188
+
189
+ def get_workspace_from_name(self, workspace_name: str, *, force_refresh: bool = False) -> Dict[str, Any]:
190
+ """
191
+ Return the workspace dict that matches the provided name.
192
+
193
+ Legacy behavior raised if not found :contentReference[oaicite:8]{index=8}.
194
+ """
195
+ workspaces = self.list_workspaces(force_refresh=force_refresh)
196
+ target = workspace_name.strip().lower()
197
+
198
+ for ws in workspaces:
199
+ if str(ws.get("name", "")).strip().lower() == target:
200
+ return ws
201
+
202
+ raise ValueError(
203
+ f"Could not find a workspace with name '{workspace_name}'. "
204
+ "Check the name/token scopes and ensure you are targeting the correct Grid."
205
+ )
206
+
207
+ # ----- workspace membership helpers (from legacy SlackAdmin) -----
208
+
209
+ def list_users(self, workspace_id: Optional[str] = None) -> List[Dict[str, Any]]:
210
+ """
211
+ Return a list of users in a workspace via admin.users.list (paginated).
212
+
213
+ This matches the legacy behavior of returning `data['users']` across pages :contentReference[oaicite:9]{index=9}.
214
+ """
215
+ wid = self._require_workspace_id(workspace_id)
216
+
217
+ payload: Dict[str, Any] = {"team_id": wid}
218
+ users: List[Dict[str, Any]] = []
219
+
220
+ while True:
221
+ resp = self._admin_users_list(payload)
222
+ if not resp.get("ok"):
223
+ raise RuntimeError(f"admin.users.list failed: {resp}")
224
+
225
+ users.extend(resp.get("users") or [])
226
+
227
+ meta = resp.get("response_metadata") or {}
228
+ cursor = meta.get("next_cursor") or ""
229
+ if cursor:
230
+ payload["cursor"] = cursor
231
+ else:
232
+ break
233
+
234
+ return users
235
+
236
+ def list_admin_ids(self, workspace_id: Optional[str] = None) -> List[str]:
237
+ """
238
+ Return a list of admin user IDs for a workspace via admin.teams.admins.list (paginated).
239
+
240
+ Legacy version returned list_of_admins (IDs) :contentReference[oaicite:10]{index=10}.
241
+ """
242
+ wid = self._require_workspace_id(workspace_id)
243
+
244
+ payload: Dict[str, Any] = {"team_id": wid}
245
+ admin_ids: List[str] = []
246
+
247
+ while True:
248
+ resp = self._admin_teams_admins_list(payload)
249
+ if not resp.get("ok"):
250
+ raise RuntimeError(f"admin.teams.admins.list failed: {resp}")
251
+
252
+ admin_ids.extend([str(x) for x in (resp.get("admin_ids") or [])])
253
+
254
+ meta = resp.get("response_metadata") or {}
255
+ cursor = meta.get("next_cursor") or ""
256
+ if cursor:
257
+ payload["cursor"] = cursor
258
+ else:
259
+ break
260
+
261
+ return admin_ids
@@ -0,0 +1,201 @@
1
+ Metadata-Version: 2.4
2
+ Name: slack-objects
3
+ Version: 0.0.post31
4
+ Summary: This package defines classes for working with slack objects like users, conversations, messages, etc.
5
+ Author-email: "Marcos E. Mercado" <marcos_elias@hotmail.com>
6
+ Keywords: slack,objects,classes,slack objects,utilities,slack utilities,slack object types,slack types,types
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Requires-Python: >=3.8
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: slack-sdk
14
+ Requires-Dist: PC_Utils
15
+ Dynamic: license-file
16
+
17
+ # slack-objects
18
+
19
+ A focused Python package for working with **Slack objects** commonly used in administration and automation workflows.
20
+
21
+ The following Slack object types will be supported:
22
+
23
+ - **Users**
24
+ - **Conversations**
25
+ - **Messages**
26
+ - **Files**
27
+ - **Workspaces**
28
+ - **IDP_groups**
29
+
30
+
31
+ ---
32
+
33
+ ## Overview
34
+
35
+ `slack-objects` provides lightweight, reusable classes that wrap Slack Web API, Admin API, and SCIM operations in a consistent, object-oriented way. It is designed for:
36
+
37
+ - Slack administration automation
38
+ - Identity and access management flows
39
+ - Internal tooling and bots
40
+ - Auditing and cleanup scripts
41
+
42
+ The package does **not** aim to be a full Slack SDK replacement. Instead, it focuses on common higher-level tasks that typically require multiple API calls and boilerplate logic.
43
+
44
+ ---
45
+
46
+ ## Requirements
47
+
48
+ - Python **3.9+**
49
+ - Slack app with appropriate scopes
50
+ - Tokens provided via environment variables or from Azure KeyVault using PC_Azure package (`python -m pip install PC_Azure`)
51
+
52
+ Typical dependencies:
53
+ - `slack_sdk`
54
+ - `requests`
55
+ - `python-dotenv` (optional)
56
+ - `PC_Azure` (optional)
57
+
58
+ ---
59
+
60
+ ## Installation
61
+
62
+ ```bash
63
+ pip install -r requirements.txt
64
+ ```
65
+
66
+ ## Classes and usage
67
+
68
+ ### `Users`
69
+
70
+ Purpose: actions related to Slack users.
71
+
72
+ Constructor:
73
+ ```python
74
+ Users(global_vars, client, logger, user_id="")
75
+ ```
76
+
77
+ Key methods:
78
+ - `is_contingent_worker()` → bool using name/display name label `[External]`.
79
+ - `is_guest()` → bool if `is_restricted` or `is_ultra_restricted`.
80
+ - `make_multi_channel_guest(token, scim_version='v1')` → `requests.Response` via SCIM (v1/v2).
81
+ - `remove_from_channels(token, client, logger, channel_ids)` → remove user from channels (admin API).
82
+ - `remove_from_workspaces(client, logger, workspace_ids, keep=[])` → remove user from workspaces.
83
+ - `ap_studio_process()` → composite flow: convert to MCG, remove from org-wide channels, remove from other workspaces.
84
+ - `get_userId_from_email(email)` → Slack user ID or empty string.
85
+ - `is_user_authorized(service_name, auth_level='read')` → bool based on IdP group membership.
86
+ - `invite_user(channel_ids, email, team_id, email_password_policy_enabled=False)` → invite a user, returns response string.
87
+
88
+ Example:
89
+ ```python
90
+ u = Users(global_vars, client, logger, user_id="U123")
91
+ if u.is_contingent_worker():
92
+ u.make_multi_channel_guest(token=global_vars.user_token)
93
+ ```
94
+
95
+ ### `Conversations`
96
+
97
+ Purpose: actions related to conversations (e.g., channels).
98
+
99
+ Constructor:
100
+ ```python
101
+ Conversations(global_vars, client, logger, channel_id)
102
+ ```
103
+
104
+ Key methods:
105
+ - `is_private()` → bool.
106
+ - `get_messages(channel_id="", include_all_metadata=False, limit=None, inclusive=True, latest=None, oldest=None)` → list of messages using `conversations.history` with pagination.
107
+
108
+ Example:
109
+ ```python
110
+ ch = Conversations(global_vars, client, logger, channel_id="C123")
111
+ msgs = ch.get_messages(limit=100)
112
+ ```
113
+
114
+ ### `Messages`
115
+
116
+ Purpose: manage Slack messages and blocks.
117
+
118
+ Constructor:
119
+ ```python
120
+ Messages(global_vars, client, logger, channel_id, ts, message=None)
121
+ ```
122
+
123
+ Key methods:
124
+ - `update_message(as_user=True, channel_id="", message_ts="", new_message_blocks=[], new_message_text="", new_message_attachments="")` → update message via `chat.update`.
125
+ - `replace_message_block(blocks=[], block_type="", block_id="", text="", new_block={}, new_block_id="")` → find a block by type or id and replace it, then update message.
126
+
127
+ Example:
128
+ ```python
129
+ msg = Messages(global_vars, client, logger, "C123", "1717000000.000100")
130
+ msg.update_message(new_message_text="Updated content")
131
+ ```
132
+
133
+ ### `Files`
134
+
135
+ Purpose: interact with files in Slack.
136
+
137
+ Constructor:
138
+ ```python
139
+ Files(global_vars, client, logger, file_id="", get_content=False)
140
+ ```
141
+
142
+ Key methods:
143
+ - `get_text_content()` → fetch content for text files via `url_private` (uses bot token).
144
+ - `upload_to_slack(title, channel="", thread_ts="")` → upload the current file content via `files_upload_v2`.
145
+ - `delete_file(file_id="")` → delete a file by id.
146
+ - `list_files(**args)` → simple wrapper around `files.list`.
147
+ - `get_file_source_message(channel: Channels, file_id="", user_id="")` → find the message where a file was shared (looks back ~5 messages).
148
+
149
+ Example:
150
+ ```python
151
+ f = Files(global_vars, client, logger, file_id="F123", get_content=True)
152
+ f.upload_to_slack(title="Processed file", channel="C123")
153
+ ```
154
+
155
+ ### `Workspaces`
156
+
157
+ Purpose: workspace info helper.
158
+
159
+ Constructor:
160
+ ```python
161
+ Workspaces(client, logger, workspace_id)
162
+ ```
163
+
164
+ Obtains attributes via `team.info`.
165
+
166
+ ### `IDP_groups`
167
+
168
+ Purpose: manage IdP (Okta) groups via SCIM.
169
+
170
+ Constructor:
171
+ ```python
172
+ IDP_groups(global_vars)
173
+ ```
174
+
175
+ Key methods:
176
+ - `get_groups()` → list of `{ 'group id', 'group name' }` (paginated).
177
+ - `get_members(group_id)` → list of members with `value` (user id) and `display` (name).
178
+ - `is_member(user_id, group_id)` → bool.
179
+
180
+ Example:
181
+ ```python
182
+ idp = IDP_groups(global_vars)
183
+ if idp.is_member("U123", "GP456"):
184
+ print("authorized")
185
+ ```
186
+
187
+ ## Tokens and rate limits
188
+
189
+ - Methods that call admin or SCIM APIs require the User OAuth token.
190
+ - Standard Web API calls may use the Bot token via `App.client`.
191
+ - Some methods respect internal wait times (e.g., `Tier_2`, `Tier_3`, `Tier_4`) to avoid rate limits. Configure these in `libraries_and_globals.py`.
192
+
193
+ ## Error handling
194
+
195
+ - Methods catch `SlackApiError` and log messages via the provided `logger`.
196
+ - Some methods post audit logs to channels configured in `global_vars`.
197
+
198
+ ## Notes
199
+
200
+ - SCIM version: production uses `v1`, sandbox may use `v2`.
201
+ - `Files.get_text_content()` is designed for `text/*` mimetypes.
@@ -0,0 +1,18 @@
1
+ slack_objects/__init__.py,sha256=wYczk9CK67k2nrq8rtlxVqGMu_93wBNGIf_Ha-GH5mE,525
2
+ slack_objects/_version.py,sha256=SjQ_icTK6fnX32Dc0GVEVLmDoZTtEN7KhTlrIniW_QY,750
3
+ slack_objects/api_caller.py,sha256=Gch_DFpAgQN7Svgoe9Y4F4R2FY4PfiBchXuGbZ0sOgw,1507
4
+ slack_objects/base.py,sha256=mCKi5u1apj1vQTuNwlIcFf-A4Yh_Ag3lTp6kXJVJa3s,1097
5
+ slack_objects/client.py,sha256=K3Ln6SGkgfc9i2IVIvFlcowX8Cgp2E26RnKf7XRr46k,1966
6
+ slack_objects/config.py,sha256=Pe_5SGXNvbZEgKYA6DdsQYpGl1Uod2m41K385QI_ib0,809
7
+ slack_objects/conversations.py,sha256=ySJqUixCKQKkUPd-_6AeFO99uYyNq4qscqpkZ1OzRbs,17837
8
+ slack_objects/files.py,sha256=g6LnP1pWpEKmHeClF6IusABrKY4nSKyZxI0X8NXnEbg,12540
9
+ slack_objects/idp_groups.py,sha256=Jf2Sr6PIO-avgbbt6iJT_g7Qdrzs_Yxz0jd6C8YDTWo,7778
10
+ slack_objects/messages.py,sha256=PsUSqUfX5gtAwl7BKusT1AnTLqKRJgz4bkuXc1W4If4,11978
11
+ slack_objects/rate_limits.py,sha256=cSfG9k04DlpyOQRb8r5IFquKfgbrAEQHX4G9oGd8QcE,1496
12
+ slack_objects/users.py,sha256=BvLYy-Ng82Mv5pnIPS_eF6hYXk-6upeEsMq-9QrTCG8,22315
13
+ slack_objects/workspaces.py,sha256=1yUWIca61fq1OwB6xMJZJFCLh7HNmpku4vhjhvCJg-A,10744
14
+ slack_objects-0.0.post31.dist-info/licenses/LICENSE,sha256=gIj0uwoGs4CwZR0bAP1ZP-F6B5f7VWJDz3Kfpc3c0dI,1090
15
+ slack_objects-0.0.post31.dist-info/METADATA,sha256=gaqROB2GRuwm8FJ6O5GzLOOs7Wddg2HmXg3buGpC0-k,6379
16
+ slack_objects-0.0.post31.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
17
+ slack_objects-0.0.post31.dist-info/top_level.txt,sha256=enSaCqZ69Tu_AzK_F0_NEtvRK-r5OjpMHJsnFh5Z8Wo,14
18
+ slack_objects-0.0.post31.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Marcos Mercado
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ slack_objects