diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f0e0796..3db302d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,13 +5,18 @@ All notable changes to this project will be documented in this file. ## [0.4.0 - 2023-10-2x] As the project moves closer to `beta`, final unification changes are being made. -This release contains some breaking changes in `users` API. +This release contains some breaking changes in `users`, `notifications` API. + +### Added + +- `__repr__` method added for most objects(previously it was only present for `FsNode`). ### Changed - `users.get_details` renamed to `get_user` and returns a class instead of a dictionary. #145 - Optional argument `displayname` in `users.create` renamed to `display_name`. -- The `apps.ExAppInfo` class has been rewritten in the same format as all the others. +- The `apps.ExAppInfo` class has been rewritten in the same format as all the others. #146 +- `notifications.Notification` class has been rewritten in the same format as all the others. ### Fixed diff --git a/docs/reference/Users/Notifications.rst b/docs/reference/Users/Notifications.rst index 4b72e797..dcb50dbf 100644 --- a/docs/reference/Users/Notifications.rst +++ b/docs/reference/Users/Notifications.rst @@ -6,6 +6,3 @@ Notifications .. autoclass:: nc_py_api.notifications.Notification :members: - -.. autoclass:: nc_py_api.notifications.NotificationInfo - :members: diff --git a/nc_py_api/activity.py b/nc_py_api/activity.py index 704c6e57..37c5c1dd 100644 --- a/nc_py_api/activity.py +++ b/nc_py_api/activity.py @@ -36,6 +36,9 @@ def priority(self) -> int: """Arrangement priority in ascending order. Values from 0 to 99.""" return self._raw_data["priority"] + def __repr__(self): + return f"<{self.__class__.__name__} id={self.filter_id}, name={self.name}, priority={self.priority}>" + @dataclasses.dataclass class Activity: @@ -122,10 +125,16 @@ def icon(self) -> str: return self._raw_data["icon"] @property - def activity_time(self) -> datetime.datetime: + def time(self) -> datetime.datetime: """Time when the activity occurred.""" return nc_iso_time_to_datetime(self._raw_data["datetime"]) + def __repr__(self): + return ( + f"<{self.__class__.__name__} id={self.activity_id}, app={self.app}, type={self.activity_type}," + f" time={self.time}>" + ) + class _ActivityAPI: """The class provides the Activity Application API.""" diff --git a/nc_py_api/apps.py b/nc_py_api/apps.py index 2a28245e..9f41e724 100644 --- a/nc_py_api/apps.py +++ b/nc_py_api/apps.py @@ -47,6 +47,9 @@ def system(self) -> bool: """Flag indicating if the application is a system application.""" return bool(self._raw_data["system"]) + def __repr__(self): + return f"<{self.__class__.__name__} id={self.app_id}, ver={self.version}>" + class _AppsAPI: """The class provides the application management API on the Nextcloud server.""" diff --git a/nc_py_api/files/__init__.py b/nc_py_api/files/__init__.py index 75849a4c..90f1b869 100644 --- a/nc_py_api/files/__init__.py +++ b/nc_py_api/files/__init__.py @@ -265,6 +265,9 @@ def user_assignable(self) -> bool: """Flag indicating if User can assign this Tag.""" return bool(self._raw_data.get("oc:user-assignable", "false").lower() == "true") + def __repr__(self): + return f"<{self.__class__.__name__} id={self.tag_id}, name={self.display_name}>" + class ShareType(enum.IntEnum): """Type of the object that will receive share.""" diff --git a/nc_py_api/notes.py b/nc_py_api/notes.py index a5621f8c..72c2cc59 100644 --- a/nc_py_api/notes.py +++ b/nc_py_api/notes.py @@ -73,6 +73,9 @@ def last_modified(self) -> datetime.datetime: modified = self._raw_data.get("modified", 0) return datetime.datetime.utcfromtimestamp(modified).replace(tzinfo=datetime.timezone.utc) + def __repr__(self): + return f"<{self.__class__.__name__} id={self.note_id}, title={self.title}, last_modified={self.last_modified}>" + class NotesSettings(typing.TypedDict): """Settings of Notes App.""" diff --git a/nc_py_api/notifications.py b/nc_py_api/notifications.py index da17e90c..b451da9b 100644 --- a/nc_py_api/notifications.py +++ b/nc_py_api/notifications.py @@ -13,53 +13,68 @@ from ._session import NcSessionApp, NcSessionBasic -@dataclasses.dataclass -class NotificationInfo: - """Extra Notification attributes from Nextcloud.""" - - app_name: str - """Application name that generated notification.""" - user_id: str - """User name for which this notification is.""" - time: datetime.datetime - """Time when the notification was created.""" - subject: str - """Subject of the notification.""" - message: str - """Message of the notification.""" - link: str - """Link which will be opened when user clicks on notification.""" - icon: str - """Relative to instance url of the icon image.""" - - def __init__(self, raw_info: dict): - self.app_name = raw_info["app"] - self.user_id = raw_info["user"] - self.time = nc_iso_time_to_datetime(raw_info["datetime"]) - self.subject = raw_info["subject"] - self.message = raw_info["message"] - self.link = raw_info.get("link", "") - self.icon = raw_info.get("icon", "") - - @dataclasses.dataclass class Notification: """Class representing information about Nextcloud notification.""" - notification_id: int - """ID of the notification.""" - object_id: str - """Randomly generated unique object ID""" - object_type: str - """Currently not used.""" - info: NotificationInfo - """Additional extra information for the object""" - - def __init__(self, raw_info: dict): - self.notification_id = raw_info["notification_id"] - self.object_id = raw_info["object_id"] - self.object_type = raw_info["object_type"] - self.info = NotificationInfo(raw_info) + def __init__(self, raw_data: dict): + self._raw_data = raw_data + + @property + def notification_id(self) -> int: + """ID of the notification.""" + return self._raw_data["notification_id"] + + @property + def object_id(self) -> str: + """Randomly generated unique object ID.""" + return self._raw_data["object_id"] + + @property + def object_type(self) -> str: + """Currently not used.""" + return self._raw_data["object_type"] + + @property + def app_name(self) -> str: + """Application name that generated notification.""" + return self._raw_data["app"] + + @property + def user_id(self) -> str: + """User ID of user for which this notification is.""" + return self._raw_data["user"] + + @property + def subject(self) -> str: + """Subject of the notification.""" + return self._raw_data["subject"] + + @property + def message(self) -> str: + """Message of the notification.""" + return self._raw_data["message"] + + @property + def time(self) -> datetime.datetime: + """Time when the notification was created.""" + return nc_iso_time_to_datetime(self._raw_data["datetime"]) + + @property + def link(self) -> str: + """Link, which will be opened when user clicks on notification.""" + return self._raw_data.get("link", "") + + @property + def icon(self) -> str: + """Relative to instance url of the icon image.""" + return self._raw_data.get("icon", "") + + def __repr__(self): + return ( + f"<{self.__class__.__name__} id={self.notification_id}, app_name={self.app_name}, user_id={self.user_id}," + f" time={self.time}>" + ) class _NotificationsAPI: diff --git a/nc_py_api/talk.py b/nc_py_api/talk.py index b1ef4225..71fb50a8 100644 --- a/nc_py_api/talk.py +++ b/nc_py_api/talk.py @@ -1,6 +1,7 @@ """Nextcloud Talk API definitions.""" import dataclasses +import datetime import enum import os import typing @@ -280,6 +281,12 @@ def markdown(self) -> bool: """Whether the message should be rendered as markdown or shown as plain text.""" return self._raw_data.get("markdown", False) + def __repr__(self): + return ( + f"<{self.__class__.__name__} id={self.message_id}, author={self.actor_display_name}," + f" time={datetime.datetime.utcfromtimestamp(self.timestamp).replace(tzinfo=datetime.timezone.utc)}>" + ) + class TalkFileMessage(TalkMessage): """Subclass of Talk Message representing message-containing file.""" @@ -625,6 +632,12 @@ def status_clear_at(self) -> typing.Optional[int]: """ return self._raw_data.get("statusClearAt", None) + def __repr__(self): + return ( + f"<{self.__class__.__name__} id={self.conversation_id}, name={self.display_name}," + f" type={self.conversation_type.name}>" + ) + @dataclasses.dataclass(init=False) class Participant(_TalkUserStatus): @@ -657,7 +670,7 @@ def participant_type(self) -> ParticipantType: @property def last_ping(self) -> int: - """Timestamp of the last ping. Should be used for sorting.""" + """Timestamp of the last ping. Should be used for sorting.""" return self._raw_data["lastPing"] @property @@ -685,6 +698,11 @@ def breakout_token(self) -> str: """Only available with breakout-rooms-v1 capability.""" return self._raw_data.get("roomToken", "") + def __repr__(self): + return ( + f"<{self.__class__.__name__} id={self.attendee_id}, name={self.display_name}, last_ping={self.last_ping}>" + ) + @dataclasses.dataclass class BotInfoBasic: @@ -713,6 +731,9 @@ def state(self) -> int: """One of the Bot states: ``0`` - Disabled, ``1`` - enabled, ``2`` - **No setup**.""" return self._raw_data["state"] + def __repr__(self): + return f"<{self.__class__.__name__} id={self.bot_id}, name={self.bot_name}>" + @dataclasses.dataclass(init=False) class BotInfo(BotInfoBasic): @@ -771,6 +792,9 @@ def option(self) -> int: """The option that was voted for.""" return self._raw_data["optionId"] + def __repr__(self): + return f"<{self.__class__.__name__} actor={self.actor_display_name}, voted_for={self.option}>" + @dataclasses.dataclass class Poll: @@ -856,3 +880,6 @@ def num_voters(self) -> int: def details(self) -> list[PollDetail]: """Detailed list who voted for which option (only available for public closed polls).""" return [PollDetail(i) for i in self._raw_data.get("details", [])] + + def __repr__(self): + return f"<{self.__class__.__name__} id={self.poll_id}, author={self.actor_display_name}>" diff --git a/nc_py_api/talk_bot.py b/nc_py_api/talk_bot.py index 9c274b66..da537dad 100644 --- a/nc_py_api/talk_bot.py +++ b/nc_py_api/talk_bot.py @@ -77,6 +77,9 @@ def conversation_name(self) -> str: """The name of the conversation in which the message was posted.""" return self._raw_data["target"]["name"] + def __repr__(self): + return f"<{self.__class__.__name__} conversation={self.conversation_name}, actor={self.actor_display_name}>" + class TalkBot: """A class that implements the TalkBot functionality.""" diff --git a/nc_py_api/user_status.py b/nc_py_api/user_status.py index 9fe74d56..113c564c 100644 --- a/nc_py_api/user_status.py +++ b/nc_py_api/user_status.py @@ -77,6 +77,9 @@ def status_type(self) -> str: """Status type, on of the: online, away, dnd, invisible, offline.""" return self._raw_data.get("status", "") + def __repr__(self): + return f"<{self.__class__.__name__} user_id={self.user_id}, status_type={self.status_type}>" + @dataclasses.dataclass(init=False) class CurrentUserStatus(UserStatus): @@ -97,6 +100,12 @@ def status_type_defined(self) -> bool: """*True* if :py:attr:`UserStatus.status_type` is set by user, *False* otherwise.""" return self._raw_data["statusIsUserDefined"] + def __repr__(self): + return ( + f"<{self.__class__.__name__} user_id={self.user_id}, status_type={self.status_type}," + f" status_id={self.status_id}>" + ) + class _UserStatusAPI: """Class providing the user status management API on the Nextcloud server.""" diff --git a/nc_py_api/users.py b/nc_py_api/users.py index 64081c3f..f613b7e1 100644 --- a/nc_py_api/users.py +++ b/nc_py_api/users.py @@ -151,6 +151,9 @@ def backend_capabilities(self) -> dict: """By default, only the ``setDisplayName`` and ``setPassword`` keys are available.""" return self._raw_data["backendCapabilities"] + def __repr__(self): + return f"<{self.__class__.__name__} id={self.user_id}, backend={self.backend}, last_login={self.last_login}>" + class _UsersAPI: """The class provides the user API on the Nextcloud server. diff --git a/nc_py_api/users_groups.py b/nc_py_api/users_groups.py index d87fff51..64220917 100644 --- a/nc_py_api/users_groups.py +++ b/nc_py_api/users_groups.py @@ -44,6 +44,9 @@ def can_remove(self) -> bool: """Flag indicating the caller has enough rights to remove users from this group.""" return bool(self._raw_data["canRemove"]) + def __repr__(self): + return f"<{self.__class__.__name__} id={self.group_id}, user_count={self.user_count}, disabled={self.disabled}>" + class _UsersGroupsAPI: """Class providing an API for managing user groups on the Nextcloud server. diff --git a/tests/_talk_bot.py b/tests/_talk_bot.py index 53c58ed3..bf097893 100644 --- a/tests/_talk_bot.py +++ b/tests/_talk_bot.py @@ -41,6 +41,7 @@ def coverage_talk_bot_process_request(message: talk_bot.TalkBotMessage, request: request._url = URL("sample_url") talk_bot_app(request) assert e.value.status_code == 500 + assert str(message).find("conversation=") != -1 @APP.post("/talk_bot_coverage") diff --git a/tests/actual_tests/activity_test.py b/tests/actual_tests/activity_test.py index 7eb44104..4b3c5260 100644 --- a/tests/actual_tests/activity_test.py +++ b/tests/actual_tests/activity_test.py @@ -13,6 +13,7 @@ def test_get_filters(nc_any): assert isinstance(i.icon, str) assert i.name assert isinstance(i.priority, int) + assert str(i).find("name=") != -1 def test_get_activities(nc_any): @@ -37,7 +38,8 @@ def test_get_activities(nc_any): assert isinstance(i.objects, dict) assert isinstance(i.link, str) assert isinstance(i.icon, str) - assert i.activity_time > datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc) + assert i.time > datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc) + assert str(i).find("app=") != -1 r2 = nc_any.activity.get_activities(since=True) if r2: old_activities_id = [i.activity_id for i in r] diff --git a/tests/actual_tests/apps_test.py b/tests/actual_tests/apps_test.py index 761be4a9..aa8a283f 100644 --- a/tests/actual_tests/apps_test.py +++ b/tests/actual_tests/apps_test.py @@ -74,3 +74,4 @@ def test_ex_app_get_list(nc, nc_app): assert isinstance(app.system, bool) if app.app_id == "nc_py_api": assert app.system is True + assert str(app).find("id=") != -1 and str(app).find("ver=") != -1 diff --git a/tests/actual_tests/files_test.py b/tests/actual_tests/files_test.py index 9d988493..866e1dd6 100644 --- a/tests/actual_tests/files_test.py +++ b/tests/actual_tests/files_test.py @@ -597,6 +597,8 @@ def test_create_update_delete_tag(nc_any): assert tag.display_name == "test_nc_py_api2" assert tag.user_visible is False assert tag.user_assignable is False + for i in nc_any.files.list_tags(): + assert str(i).find("name=") != -1 nc_any.files.delete_tag(tag) with pytest.raises(ValueError): nc_any.files.update_tag(tag) diff --git a/tests/actual_tests/notes_test.py b/tests/actual_tests/notes_test.py index 388fa352..49898bbb 100644 --- a/tests/actual_tests/notes_test.py +++ b/tests/actual_tests/notes_test.py @@ -40,6 +40,7 @@ def test_create_delete(nc_client): assert new_note.readonly is False assert new_note.favorite is False assert isinstance(new_note.last_modified, datetime) + assert str(new_note).find("title=") != -1 def test_get_update_note(nc_client): diff --git a/tests/actual_tests/notifications_test.py b/tests/actual_tests/notifications_test.py index 8213b4ff..6f71b0f6 100644 --- a/tests/actual_tests/notifications_test.py +++ b/tests/actual_tests/notifications_test.py @@ -2,7 +2,7 @@ import pytest -from nc_py_api.notifications import Notification, NotificationInfo +from nc_py_api.notifications import Notification def test_available(nc_app): @@ -18,23 +18,23 @@ def test_create(nc_app): obj_id = nc_app.notifications.create("subject0123", "message456") new_notification = nc_app.notifications.by_object_id(obj_id) assert isinstance(new_notification, Notification) - assert isinstance(new_notification.info, NotificationInfo) - assert new_notification.info.subject == "subject0123" - assert new_notification.info.message == "message456" - assert new_notification.info.icon - assert not new_notification.info.link - assert new_notification.info.time > datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc) + assert new_notification.subject == "subject0123" + assert new_notification.message == "message456" + assert new_notification.icon + assert not new_notification.link + assert new_notification.time > datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc) + assert str(new_notification).find("app_name=") != -1 + assert isinstance(new_notification.object_type, str) def test_create_link_icon(nc_app): obj_id = nc_app.notifications.create("1", "", link="https://some.link/gg") new_notification = nc_app.notifications.by_object_id(obj_id) assert isinstance(new_notification, Notification) - assert isinstance(new_notification.info, NotificationInfo) - assert new_notification.info.subject == "1" - assert not new_notification.info.message - assert new_notification.info.icon - assert new_notification.info.link == "https://some.link/gg" + assert new_notification.subject == "1" + assert not new_notification.message + assert new_notification.icon + assert new_notification.link == "https://some.link/gg" def test_delete_all(nc_app): diff --git a/tests/actual_tests/talk_test.py b/tests/actual_tests/talk_test.py index fc062b17..3f38e5cf 100644 --- a/tests/actual_tests/talk_test.py +++ b/tests/actual_tests/talk_test.py @@ -130,10 +130,12 @@ def test_get_conversations_include_status(nc, nc_client): assert isinstance(second_participant.session_ids, list) assert isinstance(second_participant.breakout_token, str) assert second_participant.status_message == "" + assert str(second_participant).find("last_ping=") != -1 participants = nc.talk.list_participants(first_conv, include_status=True) assert len(participants) == 2 second_participant = next(i for i in participants if i.actor_id == environ["TEST_USER_ID"]) assert second_participant.status_message == "my status message" + assert str(conversation).find("type=") != -1 finally: nc.talk.leave_conversation(conversation.token) @@ -196,6 +198,7 @@ def test_message_send_delete_reactions(nc_any): messages = nc_any.talk.receive_messages(conversation) deleted = [i for i in messages if i.system_message == "message_deleted"] assert deleted + assert str(deleted[0]).find("time=") != -1 finally: nc_any.talk.delete_conversation(conversation) @@ -232,7 +235,9 @@ def test_list_bots(nc, nc_app): assert isinstance(registered_bot.url_hash, str) conversation = nc.talk.create_conversation(talk.ConversationType.GROUP, "admin") try: - assert nc.talk.conversation_list_bots(conversation) + conversation_bots = nc.talk.conversation_list_bots(conversation) + assert conversation_bots + assert str(conversation_bots[0]).find("name=") != -1 finally: nc.talk.delete_conversation(conversation.token) @@ -306,6 +311,7 @@ def check_poll(closed: bool): assert poll.voted_self == [] assert poll.votes == [] + assert str(poll).find("author=") != -1 check_poll(False) poll = nc_any.talk.get_poll(poll) check_poll(False) @@ -347,6 +353,7 @@ def test_vote_poll(nc_any): assert poll.details[0].actor_type == "users" assert poll.details[0].option == 1 assert isinstance(poll.details[0].actor_display_name, str) + assert str(poll.details[0]).find("actor=") != -1 finally: nc_any.talk.delete_conversation(conversation) diff --git a/tests/actual_tests/user_status_test.py b/tests/actual_tests/user_status_test.py index 82b8b2d7..8f05f699 100644 --- a/tests/actual_tests/user_status_test.py +++ b/tests/actual_tests/user_status_test.py @@ -31,6 +31,7 @@ def test_get_status(nc, message): assert r1.status_message == message assert r1.status_id is None assert not r1.message_predefined + assert str(r1).find("status_id=") != -1 def test_get_status_non_existent_user(nc): @@ -59,6 +60,7 @@ def test_get_list(nc): for i in r_all: if i.user_id == nc.user: compare_user_statuses(i, r_current) + assert str(i).find("status_type=") != -1 def test_set_status(nc): diff --git a/tests/actual_tests/users_groups_test.py b/tests/actual_tests/users_groups_test.py index 4098abdd..a68fcb88 100644 --- a/tests/actual_tests/users_groups_test.py +++ b/tests/actual_tests/users_groups_test.py @@ -30,6 +30,7 @@ def test_group_get_details(nc, nc_client): assert isinstance(group.user_count, int) assert isinstance(group.can_add, bool) assert isinstance(group.can_remove, bool) + assert str(group).find("user_count=") != -1 def test_get_non_existing_group(nc_client): diff --git a/tests/actual_tests/users_test.py b/tests/actual_tests/users_test.py index eb3ffce4..3905c49c 100644 --- a/tests/actual_tests/users_test.py +++ b/tests/actual_tests/users_test.py @@ -50,6 +50,7 @@ def test_get_user_info(nc): assert isinstance(admin.groups, list) assert isinstance(admin.backend_capabilities, dict) assert admin.display_name == "admin" + assert str(admin).find("last_login=") != -1 def test_get_current_user_wo_user(nc):