slack-objects 0.0.9__tar.gz → 0.1.1__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.
- {slack_objects-0.0.9 → slack_objects-0.1.1}/.gitignore +4 -1
- {slack_objects-0.0.9 → slack_objects-0.1.1}/PKG-INFO +22 -6
- {slack_objects-0.0.9 → slack_objects-0.1.1}/README.md +21 -5
- {slack_objects-0.0.9 → slack_objects-0.1.1}/src/slack_objects/_version.py +3 -3
- {slack_objects-0.0.9 → slack_objects-0.1.1}/src/slack_objects/users.py +45 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/src/slack_objects.egg-info/PKG-INFO +22 -6
- {slack_objects-0.0.9 → slack_objects-0.1.1}/src/slack_objects.egg-info/SOURCES.txt +1 -2
- {slack_objects-0.0.9 → slack_objects-0.1.1}/tests/SCIM/conftest_live.py +5 -5
- {slack_objects-0.0.9 → slack_objects-0.1.1}/tests/SCIM/run_all_scim_users_live_tests.py +1 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/tests/SCIM/test_scim_users_input_validation_live.py +14 -0
- slack_objects-0.1.1/tests/SCIM/test_scim_users_update_email_live.py +160 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/tests/Smoke/users_smoke_test.py +8 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/tests/UnitTests/users_unit_test.py +94 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/tests/Users/conftest_live.py +5 -5
- slack_objects-0.0.9/.vs/VSWorkspaceState.json +0 -8
- slack_objects-0.0.9/.vs/slnx.sqlite +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/.github/copilot-instructions.md +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/.github/workflows/ci.yml +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/.github/workflows/publish-pypi.yml +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/.github/workflows/publish-testpypi.yml +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/LICENSE +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/pyproject.toml +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/pytest.ini +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/setup.cfg +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/src/slack_objects/__init__.py +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/src/slack_objects/api_caller.py +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/src/slack_objects/base.py +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/src/slack_objects/client.py +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/src/slack_objects/config.py +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/src/slack_objects/conversations.py +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/src/slack_objects/files.py +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/src/slack_objects/idp_groups.py +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/src/slack_objects/messages.py +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/src/slack_objects/rate_limits.py +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/src/slack_objects/scim_base.py +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/src/slack_objects/usergroups.py +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/src/slack_objects/workspaces.py +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/src/slack_objects.egg-info/dependency_links.txt +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/src/slack_objects.egg-info/requires.txt +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/src/slack_objects.egg-info/top_level.txt +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/tests/Examples/examples_config.example.json +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/tests/Examples/users_search_by_email_example.py +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/tests/Examples/users_test_AzureKeyVault.py +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/tests/SCIM/test_scim_idp_groups_live.py +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/tests/SCIM/test_scim_users_create_live.py +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/tests/SCIM/test_scim_users_deactivate_live.py +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/tests/SCIM/test_scim_users_make_guest_live.py +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/tests/SCIM/test_scim_users_reactivate_live.py +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/tests/SCIM/test_scim_users_update_attribute_live.py +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/tests/Smoke/_smoke_harness.py +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/tests/Smoke/api_caller_smoke_test.py +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/tests/Smoke/conversations_smoke_test.py +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/tests/Smoke/files_smoke_test.py +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/tests/Smoke/idp_groups_smoke_test.py +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/tests/Smoke/messages_smoke_test.py +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/tests/Smoke/run_all_smoke.py +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/tests/Smoke/security_smoke_test.py +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/tests/Smoke/usergroups_smoke_test.py +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/tests/Smoke/workspaces_smoke_test.py +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/tests/UnitTests/config_unit_test.py +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/tests/UnitTests/conversations_unit_test.py +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/tests/UnitTests/files_unit_test.py +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/tests/UnitTests/idp_groups_unit_test.py +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/tests/UnitTests/messages_unit_test.py +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/tests/UnitTests/rate_limits_unit_test.py +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/tests/UnitTests/usergroups_test.py +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/tests/UnitTests/workspaces_unit_test.py +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/tests/Users/run_all_users_live_tests.py +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/tests/Users/test_users_admin_live.py +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/tests/Users/test_users_classification_live.py +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/tests/Users/test_users_input_validation_live.py +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/tests/Users/test_users_read_live.py +0 -0
- {slack_objects-0.0.9 → slack_objects-0.1.1}/tests/live_test_config.example.json +0 -0
|
@@ -29,4 +29,7 @@ src/slack_objects/_version.py
|
|
|
29
29
|
/tests/Examples/__pycache__
|
|
30
30
|
|
|
31
31
|
# Examples config (contains Key Vault / secret names)
|
|
32
|
-
/tests/Examples/examples_config.json
|
|
32
|
+
/tests/Examples/examples_config.json
|
|
33
|
+
/.vs/slnx.sqlite
|
|
34
|
+
/.vs/VSWorkspaceState.json
|
|
35
|
+
/src/slack_objects/.vs
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: slack-objects
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Opinionated, testable Python wrappers for Slack’s Web, Admin, and SCIM APIs, organized by object domain (users, conversations, messages, files, workspaces, and IdP groups). Designed for automation and administration workflows.
|
|
5
5
|
Author-email: "Marcos E. Mercado" <marcos_elias@hotmail.com>
|
|
6
6
|
Keywords: slack,objects,classes,slack objects,utilities,slack utilities,slack object types,slack types,types
|
|
@@ -23,7 +23,7 @@ Dynamic: license-file
|
|
|
23
23
|
|
|
24
24
|
A focused Python package for working with **Slack objects** commonly used in administration and automation workflows.
|
|
25
25
|
|
|
26
|
-
`slack-objects` provides **opinionated, testable wrappers** around the Slack Web API, Admin API, and
|
|
26
|
+
`slack-objects` provides **opinionated, testable wrappers** around the Slack Web API, Admin API, SCIM API, and Discovery API—favoring object-based access over raw endpoint calls.
|
|
27
27
|
|
|
28
28
|
---
|
|
29
29
|
|
|
@@ -55,7 +55,7 @@ Instead, it focuses on higher-level object operations that typically require:
|
|
|
55
55
|
- multiple API calls
|
|
56
56
|
- pagination
|
|
57
57
|
- rate limiting
|
|
58
|
-
- Admin API or
|
|
58
|
+
- Admin API, SCIM, or Discovery API usage
|
|
59
59
|
- non-trivial orchestration logic
|
|
60
60
|
|
|
61
61
|
---
|
|
@@ -73,7 +73,7 @@ cfg = SlackObjectsConfig(
|
|
|
73
73
|
bot_token="xoxb-...",
|
|
74
74
|
user_token="xoxp-...",
|
|
75
75
|
scim_token="xoxp-...",
|
|
76
|
-
# see SlackObjectsConfig for additional options (scim_base_url, http_timeout_seconds, etc.)
|
|
76
|
+
# see SlackObjectsConfig for additional options (team_id, scim_base_url, http_timeout_seconds, etc.)
|
|
77
77
|
)
|
|
78
78
|
|
|
79
79
|
slack = SlackObjectsClient(cfg)
|
|
@@ -139,6 +139,8 @@ The codebase is designed to be tested **without hitting Slack**.
|
|
|
139
139
|
|
|
140
140
|
## Installation
|
|
141
141
|
|
|
142
|
+
Requires **Python 3.9+**.
|
|
143
|
+
|
|
142
144
|
```bash
|
|
143
145
|
pip install slack-objects
|
|
144
146
|
```
|
|
@@ -154,7 +156,8 @@ cfg = SlackObjectsConfig(
|
|
|
154
156
|
bot_token="xoxb-...",
|
|
155
157
|
user_token="xoxp-...",
|
|
156
158
|
scim_token="xoxp-...",
|
|
157
|
-
|
|
159
|
+
team_id="T0123ABC", # workspace ID; required for org-wide tokens calling workspace-scoped Web APIs
|
|
160
|
+
default_rate_tier=RateTier.TIER_2, # fallback sleep between API calls when no specific tier matches (default)
|
|
158
161
|
)
|
|
159
162
|
```
|
|
160
163
|
|
|
@@ -175,10 +178,23 @@ python -m tests.Smoke.run_all_smoke
|
|
|
175
178
|
|
|
176
179
|
---
|
|
177
180
|
|
|
181
|
+
## Rate Limiting
|
|
182
|
+
All Slack Web/Admin API calls go through `SlackApiCaller`, which:
|
|
183
|
+
|
|
184
|
+
- Sleeps according to the resolved rate tier after every successful call
|
|
185
|
+
- Automatically retries on HTTP **429** (rate-limited) responses up to **5 times**, respecting the `Retry-After` header
|
|
186
|
+
|
|
187
|
+
Rate tiers are resolved in priority order:
|
|
188
|
+
explicit per-call tier → method-specific override → prefix rule → `default_rate_tier` from config.
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
178
192
|
## Notes
|
|
179
193
|
|
|
180
194
|
- SCIM v2 is the default; v1 is supported where applicable
|
|
181
|
-
- `PC_Utils` is
|
|
195
|
+
- `PC_Utils` is a required dependency (used for datetime handling in `set_guest_expiration_date`)
|
|
182
196
|
- This package is intended for automation and administration workflows
|
|
183
197
|
- `resolve_user_id` accepts flexible identifiers (user ID, email, or @username) and verifies existence via Web API + SCIM fallback
|
|
184
198
|
- `is_user_authorized` supports IdP-group-based authorization checks with configurable read/write access levels
|
|
199
|
+
- SCIM user operations are available on `Users`: create, deactivate, reactivate, update attributes, update email, and convert to multi-channel guest
|
|
200
|
+
- Discovery API is used for `Users.get_channels` and `Conversations.get_members` (requires appropriate token scopes)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A focused Python package for working with **Slack objects** commonly used in administration and automation workflows.
|
|
4
4
|
|
|
5
|
-
`slack-objects` provides **opinionated, testable wrappers** around the Slack Web API, Admin API, and
|
|
5
|
+
`slack-objects` provides **opinionated, testable wrappers** around the Slack Web API, Admin API, SCIM API, and Discovery API—favoring object-based access over raw endpoint calls.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -34,7 +34,7 @@ Instead, it focuses on higher-level object operations that typically require:
|
|
|
34
34
|
- multiple API calls
|
|
35
35
|
- pagination
|
|
36
36
|
- rate limiting
|
|
37
|
-
- Admin API or
|
|
37
|
+
- Admin API, SCIM, or Discovery API usage
|
|
38
38
|
- non-trivial orchestration logic
|
|
39
39
|
|
|
40
40
|
---
|
|
@@ -52,7 +52,7 @@ cfg = SlackObjectsConfig(
|
|
|
52
52
|
bot_token="xoxb-...",
|
|
53
53
|
user_token="xoxp-...",
|
|
54
54
|
scim_token="xoxp-...",
|
|
55
|
-
# see SlackObjectsConfig for additional options (scim_base_url, http_timeout_seconds, etc.)
|
|
55
|
+
# see SlackObjectsConfig for additional options (team_id, scim_base_url, http_timeout_seconds, etc.)
|
|
56
56
|
)
|
|
57
57
|
|
|
58
58
|
slack = SlackObjectsClient(cfg)
|
|
@@ -118,6 +118,8 @@ The codebase is designed to be tested **without hitting Slack**.
|
|
|
118
118
|
|
|
119
119
|
## Installation
|
|
120
120
|
|
|
121
|
+
Requires **Python 3.9+**.
|
|
122
|
+
|
|
121
123
|
```bash
|
|
122
124
|
pip install slack-objects
|
|
123
125
|
```
|
|
@@ -133,7 +135,8 @@ cfg = SlackObjectsConfig(
|
|
|
133
135
|
bot_token="xoxb-...",
|
|
134
136
|
user_token="xoxp-...",
|
|
135
137
|
scim_token="xoxp-...",
|
|
136
|
-
|
|
138
|
+
team_id="T0123ABC", # workspace ID; required for org-wide tokens calling workspace-scoped Web APIs
|
|
139
|
+
default_rate_tier=RateTier.TIER_2, # fallback sleep between API calls when no specific tier matches (default)
|
|
137
140
|
)
|
|
138
141
|
```
|
|
139
142
|
|
|
@@ -154,10 +157,23 @@ python -m tests.Smoke.run_all_smoke
|
|
|
154
157
|
|
|
155
158
|
---
|
|
156
159
|
|
|
160
|
+
## Rate Limiting
|
|
161
|
+
All Slack Web/Admin API calls go through `SlackApiCaller`, which:
|
|
162
|
+
|
|
163
|
+
- Sleeps according to the resolved rate tier after every successful call
|
|
164
|
+
- Automatically retries on HTTP **429** (rate-limited) responses up to **5 times**, respecting the `Retry-After` header
|
|
165
|
+
|
|
166
|
+
Rate tiers are resolved in priority order:
|
|
167
|
+
explicit per-call tier → method-specific override → prefix rule → `default_rate_tier` from config.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
157
171
|
## Notes
|
|
158
172
|
|
|
159
173
|
- SCIM v2 is the default; v1 is supported where applicable
|
|
160
|
-
- `PC_Utils` is
|
|
174
|
+
- `PC_Utils` is a required dependency (used for datetime handling in `set_guest_expiration_date`)
|
|
161
175
|
- This package is intended for automation and administration workflows
|
|
162
176
|
- `resolve_user_id` accepts flexible identifiers (user ID, email, or @username) and verifies existence via Web API + SCIM fallback
|
|
163
177
|
- `is_user_authorized` supports IdP-group-based authorization checks with configurable read/write access levels
|
|
178
|
+
- SCIM user operations are available on `Users`: create, deactivate, reactivate, update attributes, update email, and convert to multi-channel guest
|
|
179
|
+
- Discovery API is used for `Users.get_channels` and `Conversations.get_members` (requires appropriate token scopes)
|
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.
|
|
32
|
-
__version_tuple__ = version_tuple = (0,
|
|
31
|
+
__version__ = version = '0.1.1'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 1, 1)
|
|
33
33
|
|
|
34
|
-
__commit_id__ = commit_id = '
|
|
34
|
+
__commit_id__ = commit_id = 'g74ff4e47f'
|
|
@@ -646,6 +646,51 @@ class Users(ScimMixin, SlackObjectBase):
|
|
|
646
646
|
|
|
647
647
|
return self._scim_request(path=f"Users/{user_id}", method="PATCH", payload=payload)
|
|
648
648
|
|
|
649
|
+
def scim_update_email(
|
|
650
|
+
self,
|
|
651
|
+
*,
|
|
652
|
+
user_id: Optional[str] = None,
|
|
653
|
+
new_email: str,
|
|
654
|
+
) -> ScimResponse:
|
|
655
|
+
"""Update a user's primary email address via SCIM PATCH.
|
|
656
|
+
|
|
657
|
+
This is intentionally separate from :meth:`scim_update_user_attribute`
|
|
658
|
+
because email updates require a structurally different payload in both
|
|
659
|
+
SCIM versions (value-filter path in v2, typed email array in v1).
|
|
660
|
+
|
|
661
|
+
Accepts a plain email-address string and builds the version-correct
|
|
662
|
+
payload automatically:
|
|
663
|
+
- v2: value-filter path ``emails[primary eq true].value`` to avoid
|
|
664
|
+
the "more than one primary element" conflict.
|
|
665
|
+
- v1: wraps the address in the standard SCIM email array.
|
|
666
|
+
"""
|
|
667
|
+
uid = user_id or self.user_id
|
|
668
|
+
if not uid:
|
|
669
|
+
raise ValueError("scim_update_email requires user_id (passed or bound)")
|
|
670
|
+
validate_scim_id(uid, "user_id")
|
|
671
|
+
|
|
672
|
+
scim_version = self.cfg.scim_version
|
|
673
|
+
if scim_version == "v2":
|
|
674
|
+
payload = {
|
|
675
|
+
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
|
676
|
+
"Operations": [
|
|
677
|
+
{
|
|
678
|
+
"op": "replace",
|
|
679
|
+
"path": "emails[primary eq true].value",
|
|
680
|
+
"value": new_email,
|
|
681
|
+
}
|
|
682
|
+
],
|
|
683
|
+
}
|
|
684
|
+
elif scim_version == "v1":
|
|
685
|
+
payload = {
|
|
686
|
+
"schemas": ["urn:scim:schemas:core:1.0"],
|
|
687
|
+
"emails": [{"value": new_email, "type": "work", "primary": True}],
|
|
688
|
+
}
|
|
689
|
+
else:
|
|
690
|
+
raise NotImplementedError(f"Invalid SCIM version: {scim_version}")
|
|
691
|
+
|
|
692
|
+
return self._scim_request(path=f"Users/{uid}", method="PATCH", payload=payload)
|
|
693
|
+
|
|
649
694
|
def make_multi_channel_guest(self, user_id: Optional[str] = None) -> ScimResponse:
|
|
650
695
|
"""Convert a user to a multi-channel guest via SCIM PATCH."""
|
|
651
696
|
uid = user_id or self.user_id
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: slack-objects
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Opinionated, testable Python wrappers for Slack’s Web, Admin, and SCIM APIs, organized by object domain (users, conversations, messages, files, workspaces, and IdP groups). Designed for automation and administration workflows.
|
|
5
5
|
Author-email: "Marcos E. Mercado" <marcos_elias@hotmail.com>
|
|
6
6
|
Keywords: slack,objects,classes,slack objects,utilities,slack utilities,slack object types,slack types,types
|
|
@@ -23,7 +23,7 @@ Dynamic: license-file
|
|
|
23
23
|
|
|
24
24
|
A focused Python package for working with **Slack objects** commonly used in administration and automation workflows.
|
|
25
25
|
|
|
26
|
-
`slack-objects` provides **opinionated, testable wrappers** around the Slack Web API, Admin API, and
|
|
26
|
+
`slack-objects` provides **opinionated, testable wrappers** around the Slack Web API, Admin API, SCIM API, and Discovery API—favoring object-based access over raw endpoint calls.
|
|
27
27
|
|
|
28
28
|
---
|
|
29
29
|
|
|
@@ -55,7 +55,7 @@ Instead, it focuses on higher-level object operations that typically require:
|
|
|
55
55
|
- multiple API calls
|
|
56
56
|
- pagination
|
|
57
57
|
- rate limiting
|
|
58
|
-
- Admin API or
|
|
58
|
+
- Admin API, SCIM, or Discovery API usage
|
|
59
59
|
- non-trivial orchestration logic
|
|
60
60
|
|
|
61
61
|
---
|
|
@@ -73,7 +73,7 @@ cfg = SlackObjectsConfig(
|
|
|
73
73
|
bot_token="xoxb-...",
|
|
74
74
|
user_token="xoxp-...",
|
|
75
75
|
scim_token="xoxp-...",
|
|
76
|
-
# see SlackObjectsConfig for additional options (scim_base_url, http_timeout_seconds, etc.)
|
|
76
|
+
# see SlackObjectsConfig for additional options (team_id, scim_base_url, http_timeout_seconds, etc.)
|
|
77
77
|
)
|
|
78
78
|
|
|
79
79
|
slack = SlackObjectsClient(cfg)
|
|
@@ -139,6 +139,8 @@ The codebase is designed to be tested **without hitting Slack**.
|
|
|
139
139
|
|
|
140
140
|
## Installation
|
|
141
141
|
|
|
142
|
+
Requires **Python 3.9+**.
|
|
143
|
+
|
|
142
144
|
```bash
|
|
143
145
|
pip install slack-objects
|
|
144
146
|
```
|
|
@@ -154,7 +156,8 @@ cfg = SlackObjectsConfig(
|
|
|
154
156
|
bot_token="xoxb-...",
|
|
155
157
|
user_token="xoxp-...",
|
|
156
158
|
scim_token="xoxp-...",
|
|
157
|
-
|
|
159
|
+
team_id="T0123ABC", # workspace ID; required for org-wide tokens calling workspace-scoped Web APIs
|
|
160
|
+
default_rate_tier=RateTier.TIER_2, # fallback sleep between API calls when no specific tier matches (default)
|
|
158
161
|
)
|
|
159
162
|
```
|
|
160
163
|
|
|
@@ -175,10 +178,23 @@ python -m tests.Smoke.run_all_smoke
|
|
|
175
178
|
|
|
176
179
|
---
|
|
177
180
|
|
|
181
|
+
## Rate Limiting
|
|
182
|
+
All Slack Web/Admin API calls go through `SlackApiCaller`, which:
|
|
183
|
+
|
|
184
|
+
- Sleeps according to the resolved rate tier after every successful call
|
|
185
|
+
- Automatically retries on HTTP **429** (rate-limited) responses up to **5 times**, respecting the `Retry-After` header
|
|
186
|
+
|
|
187
|
+
Rate tiers are resolved in priority order:
|
|
188
|
+
explicit per-call tier → method-specific override → prefix rule → `default_rate_tier` from config.
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
178
192
|
## Notes
|
|
179
193
|
|
|
180
194
|
- SCIM v2 is the default; v1 is supported where applicable
|
|
181
|
-
- `PC_Utils` is
|
|
195
|
+
- `PC_Utils` is a required dependency (used for datetime handling in `set_guest_expiration_date`)
|
|
182
196
|
- This package is intended for automation and administration workflows
|
|
183
197
|
- `resolve_user_id` accepts flexible identifiers (user ID, email, or @username) and verifies existence via Web API + SCIM fallback
|
|
184
198
|
- `is_user_authorized` supports IdP-group-based authorization checks with configurable read/write access levels
|
|
199
|
+
- SCIM user operations are available on `Users`: create, deactivate, reactivate, update attributes, update email, and convert to multi-channel guest
|
|
200
|
+
- Discovery API is used for `Users.get_channels` and `Conversations.get_members` (requires appropriate token scopes)
|
|
@@ -7,8 +7,6 @@ pytest.ini
|
|
|
7
7
|
.github/workflows/ci.yml
|
|
8
8
|
.github/workflows/publish-pypi.yml
|
|
9
9
|
.github/workflows/publish-testpypi.yml
|
|
10
|
-
.vs/VSWorkspaceState.json
|
|
11
|
-
.vs/slnx.sqlite
|
|
12
10
|
src/slack_objects/__init__.py
|
|
13
11
|
src/slack_objects/_version.py
|
|
14
12
|
src/slack_objects/api_caller.py
|
|
@@ -42,6 +40,7 @@ tests/SCIM/test_scim_users_input_validation_live.py
|
|
|
42
40
|
tests/SCIM/test_scim_users_make_guest_live.py
|
|
43
41
|
tests/SCIM/test_scim_users_reactivate_live.py
|
|
44
42
|
tests/SCIM/test_scim_users_update_attribute_live.py
|
|
43
|
+
tests/SCIM/test_scim_users_update_email_live.py
|
|
45
44
|
tests/Smoke/_smoke_harness.py
|
|
46
45
|
tests/Smoke/api_caller_smoke_test.py
|
|
47
46
|
tests/Smoke/conversations_smoke_test.py
|
|
@@ -7,8 +7,8 @@ Test-user IDs and other identifiers are loaded from a JSON file.
|
|
|
7
7
|
Setup
|
|
8
8
|
-----
|
|
9
9
|
1. Store your Slack tokens in Azure Key Vault:
|
|
10
|
-
Bot-token-
|
|
11
|
-
User-token-
|
|
10
|
+
Bot-token-TEST – xoxb-... bot token with users:read and users:read.email scopes
|
|
11
|
+
User-token-TEST – xoxp-... user token with admin scope (org-level admin + SCIM provisioning)
|
|
12
12
|
|
|
13
13
|
2. Set the ``_KEYVAULT_NAME`` constant in this file to your Azure Key Vault name.
|
|
14
14
|
|
|
@@ -51,9 +51,9 @@ _DEFAULT_CONFIG_PATH = _HERE.parent / "live_test_config.json"
|
|
|
51
51
|
_KEYVAULT_NAME = "Slack-AI-assistant-KV"
|
|
52
52
|
|
|
53
53
|
# Key Vault secret names
|
|
54
|
-
_SECRET_BOT_TOKEN = "Bot-token-
|
|
55
|
-
_SECRET_USER_TOKEN = "User-token-
|
|
56
|
-
_SECRET_SCIM_TOKEN = "User-token-
|
|
54
|
+
_SECRET_BOT_TOKEN = "Bot-token-TEST"
|
|
55
|
+
_SECRET_USER_TOKEN = "User-token-TEST"
|
|
56
|
+
_SECRET_SCIM_TOKEN = "User-token-TEST"
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
# ---------------------------------------------------------------------------
|
|
@@ -25,6 +25,7 @@ _TEST_FILES = [
|
|
|
25
25
|
"test_scim_users_input_validation_live.py", # no API calls
|
|
26
26
|
"test_scim_users_reactivate_live.py", # read-mostly (no-ops on active users)
|
|
27
27
|
"test_scim_users_update_attribute_live.py", # mutating, reversible
|
|
28
|
+
"test_scim_users_update_email_live.py", # mutating, reversible
|
|
28
29
|
"test_scim_users_create_live.py", # self-contained (create + delete)
|
|
29
30
|
"test_scim_users_deactivate_live.py", # mutating, reversible (strips workspaces)
|
|
30
31
|
"test_scim_users_make_guest_live.py", # DESTRUCTIVE (disposable users only)
|
{slack_objects-0.0.9 → slack_objects-0.1.1}/tests/SCIM/test_scim_users_input_validation_live.py
RENAMED
|
@@ -99,4 +99,18 @@ class TestScimInputValidation:
|
|
|
99
99
|
with patch.object(users, "_scim_request", wraps=users._scim_request) as spy:
|
|
100
100
|
with pytest.raises(ValueError):
|
|
101
101
|
users.make_multi_channel_guest(bad_id)
|
|
102
|
+
spy.assert_not_called()
|
|
103
|
+
|
|
104
|
+
@pytest.mark.parametrize("bad_id", [
|
|
105
|
+
"../traversal",
|
|
106
|
+
"",
|
|
107
|
+
"id with spaces",
|
|
108
|
+
])
|
|
109
|
+
def test_update_email_rejects_bad_ids(self, users, bad_id):
|
|
110
|
+
with patch.object(users, "_scim_request", wraps=users._scim_request) as spy:
|
|
111
|
+
with pytest.raises(ValueError):
|
|
112
|
+
users.scim_update_email(
|
|
113
|
+
user_id=bad_id,
|
|
114
|
+
new_email="test@example.com",
|
|
115
|
+
)
|
|
102
116
|
spy.assert_not_called()
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Live integration tests for scim_update_email.
|
|
3
|
+
|
|
4
|
+
MUTATING but REVERSIBLE — every email change is followed by a restore
|
|
5
|
+
to the original email in teardown.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import time
|
|
11
|
+
from typing import Any, Dict, Optional
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
import requests
|
|
15
|
+
|
|
16
|
+
from slack_objects.users import Users
|
|
17
|
+
|
|
18
|
+
from conftest_live import LiveTestContext, build_live_context
|
|
19
|
+
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
# Fixtures
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
_ctx: Optional[LiveTestContext] = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _get_ctx() -> LiveTestContext:
|
|
28
|
+
global _ctx
|
|
29
|
+
if _ctx is None:
|
|
30
|
+
_ctx = build_live_context()
|
|
31
|
+
return _ctx
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@pytest.fixture(scope="module")
|
|
35
|
+
def ctx() -> LiveTestContext:
|
|
36
|
+
return _get_ctx()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.fixture(scope="module")
|
|
40
|
+
def users(ctx: LiveTestContext) -> Users:
|
|
41
|
+
return ctx.slack.users()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
# Helpers
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
_SCIM_PAUSE = 4.0
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _pause():
|
|
52
|
+
time.sleep(_SCIM_PAUSE)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _scim_get_user(users: Users, user_id: str) -> Dict[str, Any]:
|
|
56
|
+
resp = users._scim_request(path=f"Users/{user_id}", method="GET")
|
|
57
|
+
assert resp.ok, f"SCIM GET Users/{user_id} failed: {resp.data}"
|
|
58
|
+
return resp.data
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _get_primary_email(scim_data: Dict[str, Any]) -> str:
|
|
62
|
+
"""Extract the primary email from a SCIM user resource."""
|
|
63
|
+
for email in scim_data.get("emails", []):
|
|
64
|
+
if email.get("primary", False):
|
|
65
|
+
return email.get("value", "")
|
|
66
|
+
return ""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
# Tests
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
class TestScimUpdateEmail:
|
|
74
|
+
"""scim_update_email — identifier × type matrix."""
|
|
75
|
+
|
|
76
|
+
# ----- active member -----
|
|
77
|
+
|
|
78
|
+
def test_update_email_active_member(self, ctx, users):
|
|
79
|
+
"""Update primary email on an active member, verify, then restore."""
|
|
80
|
+
original_data = _scim_get_user(users, ctx.active_member_id)
|
|
81
|
+
original_email = _get_primary_email(original_data)
|
|
82
|
+
assert original_email, "Could not determine original email"
|
|
83
|
+
_pause()
|
|
84
|
+
|
|
85
|
+
new_email = f"scim-test-{int(time.time())}@example.com"
|
|
86
|
+
resp = users.scim_update_email(
|
|
87
|
+
user_id=ctx.active_member_id,
|
|
88
|
+
new_email=new_email,
|
|
89
|
+
)
|
|
90
|
+
assert resp.ok, f"Expected ok: {resp.data}"
|
|
91
|
+
_pause()
|
|
92
|
+
|
|
93
|
+
updated = _scim_get_user(users, ctx.active_member_id)
|
|
94
|
+
assert _get_primary_email(updated) == new_email
|
|
95
|
+
_pause()
|
|
96
|
+
|
|
97
|
+
# Teardown: restore original email
|
|
98
|
+
users.scim_update_email(
|
|
99
|
+
user_id=ctx.active_member_id,
|
|
100
|
+
new_email=original_email,
|
|
101
|
+
)
|
|
102
|
+
_pause()
|
|
103
|
+
|
|
104
|
+
# ----- active admin -----
|
|
105
|
+
|
|
106
|
+
def test_update_email_active_admin(self, ctx, users):
|
|
107
|
+
"""Update primary email on an admin, verify, then restore."""
|
|
108
|
+
original_data = _scim_get_user(users, ctx.active_admin_id)
|
|
109
|
+
original_email = _get_primary_email(original_data)
|
|
110
|
+
assert original_email, "Could not determine original email"
|
|
111
|
+
_pause()
|
|
112
|
+
|
|
113
|
+
new_email = f"scim-admin-test-{int(time.time())}@example.com"
|
|
114
|
+
resp = users.scim_update_email(
|
|
115
|
+
user_id=ctx.active_admin_id,
|
|
116
|
+
new_email=new_email,
|
|
117
|
+
)
|
|
118
|
+
assert resp.ok, f"Expected ok for admin: {resp.data}"
|
|
119
|
+
_pause()
|
|
120
|
+
|
|
121
|
+
# Teardown: restore
|
|
122
|
+
users.scim_update_email(
|
|
123
|
+
user_id=ctx.active_admin_id,
|
|
124
|
+
new_email=original_email,
|
|
125
|
+
)
|
|
126
|
+
_pause()
|
|
127
|
+
|
|
128
|
+
# ----- bound user_id (no explicit user_id) -----
|
|
129
|
+
|
|
130
|
+
def test_update_email_bound_user(self, ctx, users):
|
|
131
|
+
"""Update email using bound user_id (no explicit user_id arg)."""
|
|
132
|
+
bound = users.with_user(ctx.active_member_id)
|
|
133
|
+
original_data = _scim_get_user(bound, ctx.active_member_id)
|
|
134
|
+
original_email = _get_primary_email(original_data)
|
|
135
|
+
assert original_email, "Could not determine original email"
|
|
136
|
+
_pause()
|
|
137
|
+
|
|
138
|
+
new_email = f"scim-bound-test-{int(time.time())}@example.com"
|
|
139
|
+
resp = bound.scim_update_email(new_email=new_email)
|
|
140
|
+
assert resp.ok, f"Expected ok for bound user: {resp.data}"
|
|
141
|
+
_pause()
|
|
142
|
+
|
|
143
|
+
# Teardown: restore
|
|
144
|
+
bound.scim_update_email(new_email=original_email)
|
|
145
|
+
_pause()
|
|
146
|
+
|
|
147
|
+
# ----- deactivated user -----
|
|
148
|
+
|
|
149
|
+
def test_update_email_deactivated_user(self, ctx, users):
|
|
150
|
+
"""Updating email on a deactivated user should fail or be a no-op."""
|
|
151
|
+
try:
|
|
152
|
+
resp = users.scim_update_email(
|
|
153
|
+
user_id=ctx.deactivated_user_id,
|
|
154
|
+
new_email="should-not-apply@example.com",
|
|
155
|
+
)
|
|
156
|
+
except requests.HTTPError as exc:
|
|
157
|
+
assert exc.response.status_code in (400, 403, 404), (
|
|
158
|
+
f"Unexpected status for deactivated user: {exc.response.status_code}"
|
|
159
|
+
)
|
|
160
|
+
_pause()
|
|
@@ -161,6 +161,14 @@ def main() -> None:
|
|
|
161
161
|
"scim_update_user_attribute()",
|
|
162
162
|
lambda: bound.scim_update_user_attribute(user_id="U1", attribute="active", new_value=False),
|
|
163
163
|
),
|
|
164
|
+
CallSpec(
|
|
165
|
+
"scim_update_email(by id)",
|
|
166
|
+
lambda: bound.scim_update_email(user_id="U1", new_email="newemail@example.com"),
|
|
167
|
+
),
|
|
168
|
+
CallSpec(
|
|
169
|
+
"scim_update_email(bound)",
|
|
170
|
+
lambda: bound.scim_update_email(new_email="newemail@example.com"),
|
|
171
|
+
),
|
|
164
172
|
CallSpec("make_multi_channel_guest()", lambda: bound.make_multi_channel_guest()),
|
|
165
173
|
|
|
166
174
|
# SCIM search primitives
|
|
@@ -479,3 +479,97 @@ class TestIsActiveWithUserId:
|
|
|
479
479
|
bound.get_user_info = MagicMock()
|
|
480
480
|
assert bound.is_active(user_id="U123") is True
|
|
481
481
|
bound.get_user_info.assert_not_called()
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
485
|
+
# scim_update_email
|
|
486
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
487
|
+
|
|
488
|
+
class TestScimUpdateEmail:
|
|
489
|
+
"""scim_update_email — version-aware SCIM PATCH for primary email."""
|
|
490
|
+
|
|
491
|
+
def test_v2_payload_uses_value_filter_path(self):
|
|
492
|
+
"""v2 uses emails[primary eq true].value to avoid multi-primary conflict."""
|
|
493
|
+
users = _make_users()
|
|
494
|
+
bound = users.with_user("U1")
|
|
495
|
+
bound._scim_request = MagicMock(
|
|
496
|
+
return_value=ScimResponse(ok=True, status_code=200, data={}, text=""),
|
|
497
|
+
)
|
|
498
|
+
bound.scim_update_email(user_id="U1", new_email="new@example.com")
|
|
499
|
+
|
|
500
|
+
bound._scim_request.assert_called_once()
|
|
501
|
+
_, kwargs = bound._scim_request.call_args
|
|
502
|
+
op = kwargs["payload"]["Operations"][0]
|
|
503
|
+
assert op["op"] == "replace"
|
|
504
|
+
assert op["path"] == "emails[primary eq true].value"
|
|
505
|
+
assert op["value"] == "new@example.com"
|
|
506
|
+
|
|
507
|
+
def test_v1_payload_uses_email_array(self):
|
|
508
|
+
"""v1 wraps address in standard SCIM email array with primary+type."""
|
|
509
|
+
users = _make_users()
|
|
510
|
+
bound = users.with_user("U1")
|
|
511
|
+
object.__setattr__(bound.cfg, "scim_version", "v1")
|
|
512
|
+
bound._scim_request = MagicMock(
|
|
513
|
+
return_value=ScimResponse(ok=True, status_code=200, data={}, text=""),
|
|
514
|
+
)
|
|
515
|
+
bound.scim_update_email(user_id="U1", new_email="new@example.com")
|
|
516
|
+
|
|
517
|
+
bound._scim_request.assert_called_once()
|
|
518
|
+
_, kwargs = bound._scim_request.call_args
|
|
519
|
+
emails = kwargs["payload"]["emails"]
|
|
520
|
+
assert len(emails) == 1
|
|
521
|
+
assert emails[0]["value"] == "new@example.com"
|
|
522
|
+
assert emails[0]["primary"] is True
|
|
523
|
+
assert emails[0]["type"] == "work"
|
|
524
|
+
|
|
525
|
+
def test_falls_back_to_bound_user_id(self):
|
|
526
|
+
"""Omitting user_id uses self.user_id."""
|
|
527
|
+
users = _make_users()
|
|
528
|
+
bound = users.with_user("U1")
|
|
529
|
+
bound._scim_request = MagicMock(
|
|
530
|
+
return_value=ScimResponse(ok=True, status_code=200, data={}, text=""),
|
|
531
|
+
)
|
|
532
|
+
bound.scim_update_email(new_email="new@example.com")
|
|
533
|
+
|
|
534
|
+
bound._scim_request.assert_called_once()
|
|
535
|
+
_, kwargs = bound._scim_request.call_args
|
|
536
|
+
assert kwargs["path"] == "Users/U1"
|
|
537
|
+
|
|
538
|
+
def test_explicit_user_id_overrides_bound(self):
|
|
539
|
+
"""Passing user_id explicitly takes precedence over bound user_id."""
|
|
540
|
+
users = _make_users()
|
|
541
|
+
bound = users.with_user("U_IGNORED")
|
|
542
|
+
bound._scim_request = MagicMock(
|
|
543
|
+
return_value=ScimResponse(ok=True, status_code=200, data={}, text=""),
|
|
544
|
+
)
|
|
545
|
+
bound.scim_update_email(user_id="U_OTHER", new_email="new@example.com")
|
|
546
|
+
|
|
547
|
+
bound._scim_request.assert_called_once()
|
|
548
|
+
_, kwargs = bound._scim_request.call_args
|
|
549
|
+
assert kwargs["path"] == "Users/U_OTHER"
|
|
550
|
+
|
|
551
|
+
def test_no_user_id_raises(self):
|
|
552
|
+
"""Unbound instance with no user_id raises ValueError."""
|
|
553
|
+
users = _make_users()
|
|
554
|
+
with pytest.raises(ValueError, match="requires user_id"):
|
|
555
|
+
users.scim_update_email(new_email="x@example.com")
|
|
556
|
+
|
|
557
|
+
def test_invalid_scim_version_raises(self):
|
|
558
|
+
"""Unsupported scim_version raises NotImplementedError."""
|
|
559
|
+
users = _make_users()
|
|
560
|
+
bound = users.with_user("U1")
|
|
561
|
+
object.__setattr__(bound.cfg, "scim_version", "v99")
|
|
562
|
+
with pytest.raises(NotImplementedError, match="Invalid SCIM version"):
|
|
563
|
+
bound.scim_update_email(new_email="x@example.com")
|
|
564
|
+
|
|
565
|
+
def test_invalid_user_id_raises(self):
|
|
566
|
+
"""Path-traversal user_id is rejected by validate_scim_id."""
|
|
567
|
+
users = _make_users()
|
|
568
|
+
with pytest.raises(ValueError):
|
|
569
|
+
users.scim_update_email(user_id="../../bad", new_email="x@example.com")
|
|
570
|
+
|
|
571
|
+
def test_empty_user_id_raises(self):
|
|
572
|
+
"""Empty user_id is rejected by validate_scim_id."""
|
|
573
|
+
users = _make_users()
|
|
574
|
+
with pytest.raises(ValueError):
|
|
575
|
+
users.scim_update_email(user_id="", new_email="x@example.com")
|
|
@@ -7,8 +7,8 @@ Test-user IDs and other identifiers are loaded from a JSON file.
|
|
|
7
7
|
Setup
|
|
8
8
|
-----
|
|
9
9
|
1. Store your Slack tokens in Azure Key Vault:
|
|
10
|
-
Bot-token-
|
|
11
|
-
User-token-
|
|
10
|
+
Bot-token-TEST – xoxb-... bot token with users:read and users:read.email scopes
|
|
11
|
+
User-token-TEST – xoxp-... user token with admin scope
|
|
12
12
|
|
|
13
13
|
2. Set the ``_KEYVAULT_NAME`` constant in this file to your Azure Key Vault name.
|
|
14
14
|
|
|
@@ -48,9 +48,9 @@ _DEFAULT_CONFIG_PATH = _HERE.parent / "live_test_config.json"
|
|
|
48
48
|
_KEYVAULT_NAME = "Slack-AI-assistant-KV"
|
|
49
49
|
|
|
50
50
|
# Key Vault secret names
|
|
51
|
-
_SECRET_BOT_TOKEN = "Bot-token-
|
|
52
|
-
_SECRET_USER_TOKEN = "User-token-
|
|
53
|
-
_SECRET_SCIM_TOKEN = "User-token-
|
|
51
|
+
_SECRET_BOT_TOKEN = "Bot-token-TEST"
|
|
52
|
+
_SECRET_USER_TOKEN = "User-token-TEST"
|
|
53
|
+
_SECRET_SCIM_TOKEN = "User-token-TEST" # There's no separate SCIM token at the moment
|
|
54
54
|
|
|
55
55
|
|
|
56
56
|
# ---------------------------------------------------------------------------
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{slack_objects-0.0.9 → slack_objects-0.1.1}/tests/SCIM/test_scim_users_update_attribute_live.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|