diff --git a/slack_sdk/audit_logs/v1/logs.py b/slack_sdk/audit_logs/v1/logs.py index 062287e95..82d241891 100644 --- a/slack_sdk/audit_logs/v1/logs.py +++ b/slack_sdk/audit_logs/v1/logs.py @@ -6,6 +6,7 @@ class User: name: Optional[str] email: Optional[str] team: Optional[str] + unknown_fields: Dict[str, Any] def __init__( self, @@ -14,20 +15,29 @@ def __init__( name: Optional[str] = None, email: Optional[str] = None, team: Optional[str] = None, + **kwargs, ) -> None: self.id = id self.name = name self.email = email self.team = team + self.unknown_fields = kwargs class Actor: type: Optional[str] user: Optional[User] + unknown_fields: Dict[str, Any] - def __init__(self, type: Optional[str] = None, user: Optional[User] = None) -> None: + def __init__( + self, + type: Optional[str] = None, + user: Optional[User] = None, + **kwargs, + ) -> None: self.type = type self.user = User(**user) if isinstance(user, dict) else user + self.unknown_fields = kwargs class Location: @@ -35,6 +45,7 @@ class Location: id: Optional[str] name: Optional[str] domain: Optional[str] + unknown_fields: Dict[str, Any] def __init__( self, @@ -43,11 +54,13 @@ def __init__( id: Optional[str] = None, name: Optional[str] = None, domain: Optional[str] = None, + **kwargs, ) -> None: self.type = type self.id = id self.name = name self.domain = domain + self.unknown_fields = kwargs class Context: @@ -55,6 +68,7 @@ class Context: ua: Optional[str] ip_address: Optional[str] session_id: Optional[str] + unknown_fields: Dict[str, Any] def __init__( self, @@ -63,11 +77,13 @@ def __init__( ua: Optional[str] = None, ip_address: Optional[str] = None, session_id: Optional[str] = None, + **kwargs, ) -> None: self.location = Location(**location) if isinstance(location, dict) else location self.ua = ua self.ip_address = ip_address self.session_id = session_id + self.unknown_fields = kwargs class Details: @@ -96,6 +112,7 @@ class Details: app_previously_resolved: Optional[bool] admin_app_id: Optional[str] bot_id: Optional[str] + unknown_fields: Dict[str, Any] def __init__( self, @@ -125,6 +142,7 @@ def __init__( app_previously_resolved: Optional[bool] = None, admin_app_id: Optional[str] = None, bot_id: Optional[str] = None, + **kwargs, ) -> None: self.name = name self.new_value = new_value @@ -151,6 +169,7 @@ def __init__( self.app_previously_resolved = app_previously_resolved self.admin_app_id = admin_app_id self.bot_id = bot_id + self.unknown_fields = kwargs class App: @@ -160,6 +179,7 @@ class App: is_directory_approved: Optional[bool] is_workflow_app: Optional[bool] scopes: Optional[List[str]] + unknown_fields: Dict[str, Any] def __init__( self, @@ -170,6 +190,7 @@ def __init__( is_directory_approved: Optional[bool] = None, is_workflow_app: Optional[bool] = None, scopes: Optional[List[str]] = None, + **kwargs, ) -> None: self.id = id self.name = name @@ -177,6 +198,7 @@ def __init__( self.is_directory_approved = is_directory_approved self.is_workflow_app = is_workflow_app self.scopes = scopes + self.unknown_fields = kwargs class Channel: @@ -187,6 +209,7 @@ class Channel: is_org_shared: Optional[bool] teams_shared_with: Optional[List[str]] original_connected_channel_id: Optional[str] + unknown_fields: Dict[str, Any] def __init__( self, @@ -198,6 +221,7 @@ def __init__( is_org_shared: Optional[bool] = None, teams_shared_with: Optional[List[str]] = None, original_connected_channel_id: Optional[str] = None, + **kwargs, ) -> None: self.id = id self.privacy = privacy @@ -206,6 +230,7 @@ def __init__( self.is_org_shared = is_org_shared self.teams_shared_with = teams_shared_with self.original_connected_channel_id = original_connected_channel_id + self.unknown_fields = kwargs class File: @@ -213,6 +238,7 @@ class File: name: Optional[str] filetype: Optional[str] title: Optional[str] + unknown_fields: Dict[str, Any] def __init__( self, @@ -221,11 +247,13 @@ def __init__( name: Optional[str] = None, filetype: Optional[str] = None, title: Optional[str] = None, + **kwargs, ) -> None: self.id = id self.name = name self.filetype = filetype self.title = title + self.unknown_fields = kwargs class Entity: @@ -236,6 +264,7 @@ class Entity: channel: Optional[Channel] file: Optional[File] app: Optional[App] + unknown_fields: Dict[str, Any] def __init__( self, @@ -247,6 +276,7 @@ def __init__( channel: Optional[Union[Channel, dict]] = None, file: Optional[Union[File, dict]] = None, app: Optional[Union[App, dict]] = None, + **kwargs, ) -> None: self.type = type self.user = User(**user) if isinstance(user, dict) else user @@ -259,6 +289,7 @@ def __init__( self.channel = Channel(**channel) if isinstance(channel, dict) else channel self.file = File(**file) if isinstance(file, dict) else file self.app = App(**app) if isinstance(app, dict) else app + self.unknown_fields = kwargs class Entry: @@ -269,6 +300,7 @@ class Entry: entity: Optional[Entity] context: Optional[Context] details: Optional[Details] + unknown_fields: Dict[str, Any] def __init__( self, @@ -280,6 +312,7 @@ def __init__( entity: Optional[Entity] = None, context: Optional[Context] = None, details: Optional[Details] = None, + **kwargs, ) -> None: self.id = id self.date_create = date_create @@ -288,13 +321,21 @@ def __init__( self.entity = Entity(**entity) if isinstance(entity, dict) else entity self.context = Context(**context) if isinstance(context, dict) else context self.details = Details(**details) if isinstance(details, dict) else details + self.unknown_fields = kwargs class ResponseMetadata: next_cursor: Optional[str] + unknown_fields: Dict[str, Any] - def __init__(self, *, next_cursor: Optional[str] = None) -> None: + def __init__( + self, + *, + next_cursor: Optional[str] = None, + **kwargs, + ) -> None: self.next_cursor = next_cursor + self.unknown_fields = kwargs class LogsResponse: @@ -304,6 +345,7 @@ class LogsResponse: error: Optional[str] needed: Optional[str] provided: Optional[str] + unknown_fields: Dict[str, Any] def __init__( self, @@ -314,6 +356,7 @@ def __init__( error: Optional[str] = None, needed: Optional[str] = None, provided: Optional[str] = None, + **kwargs, ) -> None: self.entries = [Entry(**e) if isinstance(e, dict) else e for e in entries] self.response_metadata = ( @@ -325,3 +368,4 @@ def __init__( self.error = error self.needed = needed self.provided = provided + self.unknown_fields = kwargs diff --git a/slack_sdk/scim/v1/group.py b/slack_sdk/scim/v1/group.py index cd3001a77..d26fd5b74 100644 --- a/slack_sdk/scim/v1/group.py +++ b/slack_sdk/scim/v1/group.py @@ -1,4 +1,4 @@ -from typing import Optional, List, Union +from typing import Optional, List, Union, Dict, Any from .default_arg import DefaultArg, NotGiven from .internal_utils import _to_dict_without_not_given, _is_iterable @@ -7,15 +7,18 @@ class GroupMember: display: Union[Optional[str], DefaultArg] value: Union[Optional[str], DefaultArg] + unknown_fields: Dict[str, Any] def __init__( self, *, display: Union[Optional[str], DefaultArg] = NotGiven, value: Union[Optional[str], DefaultArg] = NotGiven, + **kwargs, ) -> None: self.display = display self.value = value + self.unknown_fields = kwargs def to_dict(self): return _to_dict_without_not_given(self) @@ -24,15 +27,18 @@ def to_dict(self): class GroupMeta: created: Union[Optional[str], DefaultArg] location: Union[Optional[str], DefaultArg] + unknown_fields: Dict[str, Any] def __init__( self, *, created: Union[Optional[str], DefaultArg] = NotGiven, location: Union[Optional[str], DefaultArg] = NotGiven, + **kwargs, ) -> None: self.created = created self.location = location + self.unknown_fields = kwargs def to_dict(self): return _to_dict_without_not_given(self) @@ -44,6 +50,7 @@ class Group: members: Union[Optional[List[GroupMember]], DefaultArg] meta: Union[Optional[GroupMeta], DefaultArg] schemas: Union[Optional[List[str]], DefaultArg] + unknown_fields: Dict[str, Any] def __init__( self, @@ -53,6 +60,7 @@ def __init__( members: Union[Optional[List[GroupMember]], DefaultArg] = NotGiven, meta: Union[Optional[GroupMeta], DefaultArg] = NotGiven, schemas: Union[Optional[List[str]], DefaultArg] = NotGiven, + **kwargs, ) -> None: self.display_name = display_name self.id = id @@ -65,6 +73,7 @@ def __init__( GroupMeta(**meta) if meta is not None and isinstance(meta, dict) else meta ) self.schemas = schemas + self.unknown_fields = kwargs def to_dict(self): return _to_dict_without_not_given(self) diff --git a/slack_sdk/scim/v1/internal_utils.py b/slack_sdk/scim/v1/internal_utils.py index d508446ad..8d2e7e367 100644 --- a/slack_sdk/scim/v1/internal_utils.py +++ b/slack_sdk/scim/v1/internal_utils.py @@ -30,7 +30,7 @@ def _to_dict_without_not_given(obj: Any) -> dict: dict_value = {} given_dict = obj if isinstance(obj, dict) else vars(obj) for key, value in given_dict.items(): - if key == "_additional_fields": + if key == "unknown_fields": if value is not None: converted = _to_dict_without_not_given(value) dict_value.update(converted) diff --git a/slack_sdk/scim/v1/types.py b/slack_sdk/scim/v1/types.py index b65a6cd2f..db9c7445d 100644 --- a/slack_sdk/scim/v1/types.py +++ b/slack_sdk/scim/v1/types.py @@ -1,4 +1,4 @@ -from typing import Optional, Union +from typing import Optional, Union, Dict, Any from .default_arg import DefaultArg, NotGiven from .internal_utils import _to_dict_without_not_given @@ -8,6 +8,7 @@ class TypeAndValue: primary: Union[Optional[bool], DefaultArg] type: Union[Optional[str], DefaultArg] value: Union[Optional[str], DefaultArg] + unknown_fields: Dict[str, Any] def __init__( self, @@ -15,10 +16,12 @@ def __init__( primary: Union[Optional[bool], DefaultArg] = NotGiven, type: Union[Optional[str], DefaultArg] = NotGiven, value: Union[Optional[str], DefaultArg] = NotGiven, + **kwargs, ) -> None: self.primary = primary self.type = type self.value = value + self.unknown_fields = kwargs def to_dict(self) -> dict: return _to_dict_without_not_given(self) diff --git a/slack_sdk/scim/v1/user.py b/slack_sdk/scim/v1/user.py index cbf2e9255..f9a7c15de 100644 --- a/slack_sdk/scim/v1/user.py +++ b/slack_sdk/scim/v1/user.py @@ -12,6 +12,7 @@ class UserAddress: primary: Union[Optional[bool], DefaultArg] region: Union[Optional[str], DefaultArg] street_address: Union[Optional[str], DefaultArg] + unknown_fields: Dict[str, Any] def __init__( self, @@ -22,6 +23,7 @@ def __init__( primary: Union[Optional[bool], DefaultArg] = NotGiven, region: Union[Optional[str], DefaultArg] = NotGiven, street_address: Union[Optional[str], DefaultArg] = NotGiven, + **kwargs, ) -> None: self.country = country self.locality = locality @@ -29,6 +31,7 @@ def __init__( self.primary = primary self.region = region self.street_address = street_address + self.unknown_fields = kwargs def to_dict(self) -> dict: return _to_dict_without_not_given(self) @@ -49,15 +52,18 @@ class UserRole(TypeAndValue): class UserGroup: display: Union[Optional[str], DefaultArg] value: Union[Optional[str], DefaultArg] + unknown_fields: Dict[str, Any] def __init__( self, *, display: Union[Optional[str], DefaultArg] = NotGiven, value: Union[Optional[str], DefaultArg] = NotGiven, + **kwargs, ) -> None: self.display = display self.value = value + self.unknown_fields = kwargs def to_dict(self) -> dict: return _to_dict_without_not_given(self) @@ -66,14 +72,17 @@ def to_dict(self) -> dict: class UserMeta: created: Union[Optional[str], DefaultArg] location: Union[Optional[str], DefaultArg] + unknown_fields: Dict[str, Any] def __init__( self, created: Union[Optional[str], DefaultArg] = NotGiven, location: Union[Optional[str], DefaultArg] = NotGiven, + **kwargs, ) -> None: self.created = created self.location = location + self.unknown_fields = kwargs def to_dict(self) -> dict: return _to_dict_without_not_given(self) @@ -82,14 +91,17 @@ def to_dict(self) -> dict: class UserName: family_name: Union[Optional[str], DefaultArg] given_name: Union[Optional[str], DefaultArg] + unknown_fields: Dict[str, Any] def __init__( self, family_name: Union[Optional[str], DefaultArg] = NotGiven, given_name: Union[Optional[str], DefaultArg] = NotGiven, + **kwargs, ) -> None: self.family_name = family_name self.given_name = given_name + self.unknown_fields = kwargs def to_dict(self) -> dict: return _to_dict_without_not_given(self) @@ -98,14 +110,17 @@ def to_dict(self) -> dict: class UserPhoto: type: Union[Optional[str], DefaultArg] value: Union[Optional[str], DefaultArg] + unknown_fields: Dict[str, Any] def __init__( self, type: Union[Optional[str], DefaultArg] = NotGiven, value: Union[Optional[str], DefaultArg] = NotGiven, + **kwargs, ) -> None: self.type = type self.value = value + self.unknown_fields = kwargs def to_dict(self) -> dict: return _to_dict_without_not_given(self) @@ -130,7 +145,7 @@ class User: timezone: Union[Optional[str], DefaultArg] title: Union[Optional[str], DefaultArg] user_name: Union[Optional[str], DefaultArg] - additional_fields: Union[Dict[str, Any], DefaultArg] + unknown_fields: Dict[str, Any] def __init__( self, @@ -165,7 +180,7 @@ def __init__( timezone: Union[Optional[str], DefaultArg] = NotGiven, title: Union[Optional[str], DefaultArg] = NotGiven, user_name: Union[Optional[str], DefaultArg] = NotGiven, - additional_fields: Union[Dict[str, Any], DefaultArg] = NotGiven, + **kwargs, ) -> None: self.active = active self.addresses = ( @@ -217,7 +232,7 @@ def __init__( self.title = title self.user_name = user_name - self._additional_fields = additional_fields + self.unknown_fields = kwargs def to_dict(self): return _to_dict_without_not_given(self) diff --git a/tests/slack_sdk/audit_logs/test_response.py b/tests/slack_sdk/audit_logs/test_response.py new file mode 100644 index 000000000..74489f94f --- /dev/null +++ b/tests/slack_sdk/audit_logs/test_response.py @@ -0,0 +1,108 @@ +import json +import unittest + +from slack_sdk.audit_logs.v1.logs import LogsResponse + +from slack_sdk.audit_logs import AuditLogsClient, AuditLogsResponse +from tests.slack_sdk.audit_logs.mock_web_api_server import ( + cleanup_mock_web_api_server, + setup_mock_web_api_server, +) + + +class TestAuditLogsClient(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_logs(self): + json_data = """{ + "entries": [ + { + "id": "xxx-yyy-zzz-111", + "date_create": 1611221649, + "action": "user_login", + "new_attribute": "this should be just accepted as unknown attribute", + "actor": { + "type": "user", + "new_attribute": "this should be just accepted as unknown attribute", + "user": { + "id": "W111", + "name": "your name", + "email": "foo@example.com", + "team": "E111" + } + }, + "entity": { + "type": "user", + "new_attribute": "this should be just accepted as unknown attribute", + "user": { + "id": "W111", + "name": "your name", + "email": "foo@example.com", + "team": "E111" + } + }, + "context": { + "new_attribute": "this should be just accepted as unknown attribute", + "location": { + "type": "workspace", + "id": "T111", + "new_attribute": "this should be just accepted as unknown attribute", + "name": "WS", + "domain": "foo-bar-baz" + }, + "ua": "UA", + "ip_address": "1.2.3.4", + "session_id": 1656410836837 + } + }, + { + "id": "32c68de4-cbfa-4fcb-9780-25fdd5aacf32", + "date_create": 1611221649, + "action": "user_login", + "actor": { + "type": "user", + "user": { + "id": "W111", + "name": "your name", + "email": "foo@example.com", + "team": "E111" + } + }, + "entity": { + "type": "user", + "user": { + "id": "W111", + "name": "your name", + "email": "foo@example.com", + "team": "E111" + } + }, + "context": { + "location": { + "type": "workspace", + "id": "T111", + "name": "WS", + "domain": "foo-bar-baz" + }, + "ua": "UA", + "ip_address": "1.2.3.4", + "session_id": 1656410836837 + } + } + ], + "response_metadata": { + "next_cursor": "xxx", + "new_attribute": "this should be just accepted as unknown attribute" + }, + "new_attribute": "this should be just accepted as unknown attribute" +} +""" + logs = LogsResponse(**json.loads(json_data)) + self.assertIsNotNone(logs) + self.assertIsNotNone(logs.entries[0].unknown_fields.get("new_attribute")) + self.assertIsNotNone(logs.response_metadata.unknown_fields.get("new_attribute")) + self.assertIsNotNone(logs.unknown_fields.get("new_attribute")) diff --git a/tests/slack_sdk/scim/test_internals.py b/tests/slack_sdk/scim/test_internals.py index dc66a1a0a..d0a27b17b 100644 --- a/tests/slack_sdk/scim/test_internals.py +++ b/tests/slack_sdk/scim/test_internals.py @@ -14,6 +14,5 @@ def tearDown(self): def test_snake_cased(self): response_body = """{"totalResults":441,"itemsPerPage":1,"startIndex":1,"schemas":["urn:scim:schemas:core:1.0"],"Resources":[{"schemas":["urn:scim:schemas:core:1.0"],"id":"W111","externalId":"","meta":{"created":"2020-08-13T04:15:35-07:00","location":"https://api.slack.com/scim/v1/Users/W111"},"userName":"test-app","nickName":"test-app","name":{"givenName":"","familyName":""},"displayName":"","profileUrl":"https://test-test-test.enterprise.slack.com/team/test-app","title":"","timezone":"America/Los_Angeles","active":true,"emails":[{"value":"botuser@slack-bots.com","primary":true}],"photos":[{"value":"https://secure.gravatar.com/avatar/xxx.jpg","type":"photo"}],"groups":[]}]}""" result = _to_snake_cased(json.loads(response_body)) - print(result) self.assertEqual(result["start_index"], 1) self.assertIsNotNone(result["resources"][0]["id"]) diff --git a/tests/slack_sdk/scim/test_response.py b/tests/slack_sdk/scim/test_response.py new file mode 100644 index 000000000..781f45ad3 --- /dev/null +++ b/tests/slack_sdk/scim/test_response.py @@ -0,0 +1,79 @@ +import json +import unittest + +from slack_sdk.scim import SearchUsersResponse, SCIMResponse +from slack_sdk.scim.v1.internal_utils import _to_snake_cased + + +class TEstInternals(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_snake_cased(self): + response_body = """{ + "totalResults": 441, + "itemsPerPage": 1, + "startIndex": 1, + "schemas": [ + "urn:scim:schemas:core:1.0" + ], + "Resources": [ + { + "schemas": [ + "urn:scim:schemas:core:1.0" + ], + "id": "W111", + "externalId": "", + "meta": { + "created": "2020-08-13T04:15:35-07:00", + "location": "https://api.slack.com/scim/v1/Users/W111", + "newAttribute": "this should be just accepted as unknown attribute" + }, + "userName": "test-app", + "nickName": "test-app", + "name": { + "givenName": "", + "familyName": "", + "newAttribute": "this should be just accepted as unknown attribute" + }, + "displayName": "", + "profileUrl": "https://test-test-test.enterprise.slack.com/team/test-app", + "title": "", + "timezone": "America/Los_Angeles", + "active": true, + "emails": [ + { + "value": "botuser@slack-bots.com", + "primary": true, + "newAttribute": "this should be just accepted as unknown attribute" + } + ], + "photos": [ + { + "value": "https://secure.gravatar.com/avatar/xxx.jpg", + "type": "photo", + "newAttribute": "this should be just accepted as unknown attribute" + } + ], + "groups": [], + "newAttribute": "this should be just accepted as unknown attribute" + } + ], + "newAttribute": "this should be just accepted as unknown attribute" +} +""" + response = SearchUsersResponse( + SCIMResponse( + url="https://www.example.com", + status_code=200, + raw_body=response_body, + headers={}, + ) + ) + user = response.users[0] + self.assertIsNotNone(user.unknown_fields.get("new_attribute")) + # the unknown fields need to be also camel-cased + self.assertIsNotNone(user.to_dict().get("newAttribute"))