From 64da6ef1c787cbea6b57d3a8615d1a926fdcafae Mon Sep 17 00:00:00 2001 From: vgrem Date: Sat, 28 Oct 2023 23:01:45 +0300 Subject: [PATCH] typings improvements for Outlook & SharePoint API, new methods for MailFolder type, examples updates --- examples/auth/interactive.py | 2 +- examples/outlook/calendars/__init__.py | 0 examples/outlook/calendars/get_schedule.py | 4 +- examples/outlook/events/__init__.py | 0 .../outlook/messages/create_subscription.py | 4 +- examples/outlook/messages/download.py | 3 +- .../messages/download_with_attachments.py | 10 +- examples/outlook/messages/empty_folder.py | 12 +++ examples/outlook/messages/get_basic_props.py | 3 +- examples/outlook/messages/get_with_body.py | 9 +- examples/outlook/messages/list_all.py | 2 +- examples/outlook/messages/list_attachments.py | 23 +++++ examples/outlook/messages/list_new.py | 1 - examples/outlook/messages/mark_all_as_read.py | 11 +++ examples/outlook/messages/mark_as_read.py | 8 +- examples/outlook/messages/reply.py | 3 +- examples/outlook/messages/send.py | 4 +- .../outlook/messages/send_with_attachment.py | 6 +- examples/sharepoint/apps/list_apps.py | 3 +- .../listitems/attachments/__init__.py | 0 .../sharepoint/listitems/versions/list.py | 18 ++++ examples/sharepoint/search/search_sites.py | 5 + .../sharepoint/users/export_site_users.py | 2 + .../users/get_onedrive_quota_max.py | 9 ++ office365/graph_client.py | 3 +- office365/onedrive/columns/definition.py | 23 +++-- .../onedrive/contenttypes/content_type.py | 7 +- office365/onenote/entity_hierarchy_model.py | 4 + office365/onenote/notebooks/notebook.py | 8 +- .../calendar/meetingtimes/suggestion.py | 3 + .../calendar/meetingtimes/time_slot.py | 3 + office365/outlook/contacts/collection.py | 7 -- office365/outlook/contacts/contact.py | 13 ++- .../outlook/mail/attachments/attachment.py | 62 ++++++------ office365/outlook/mail/folders/folder.py | 68 +++++++++---- office365/outlook/mail/messages/message.py | 85 +++++++--------- office365/runtime/client_object_collection.py | 21 ++-- office365/runtime/client_runtime_context.py | 7 +- office365/runtime/odata/type.py | 3 + office365/runtime/queries/function.py | 2 +- .../sharepoint/contenttypes/content_type.py | 97 ++++++++----------- .../marketplace/app_metadata_collection.py | 4 +- office365/sharepoint/publishing/pages/page.py | 13 +-- .../sharepoint/publishing/pages/service.py | 1 + .../pushnotifications/collection.py | 4 +- .../sharepoint/recyclebin/item_collection.py | 2 +- office365/sharepoint/search/service.py | 14 +-- .../sharing/object_sharing_information.py | 16 ++- .../sharepoint/userprofiles/my_site_links.py | 8 +- .../sharepoint/userprofiles/user_profile.py | 12 ++- office365/sharepoint/webs/web.py | 91 ++++++++--------- tests/outlook/test_messages.py | 8 +- 52 files changed, 407 insertions(+), 324 deletions(-) delete mode 100644 examples/outlook/calendars/__init__.py delete mode 100644 examples/outlook/events/__init__.py create mode 100644 examples/outlook/messages/empty_folder.py create mode 100644 examples/outlook/messages/list_attachments.py create mode 100644 examples/outlook/messages/mark_all_as_read.py delete mode 100644 examples/sharepoint/listitems/attachments/__init__.py create mode 100644 examples/sharepoint/listitems/versions/list.py create mode 100644 examples/sharepoint/users/get_onedrive_quota_max.py diff --git a/examples/auth/interactive.py b/examples/auth/interactive.py index 517c429c4..290a89957 100644 --- a/examples/auth/interactive.py +++ b/examples/auth/interactive.py @@ -15,4 +15,4 @@ client = GraphClient.with_token_interactive(test_tenant, test_client_id) me = client.me.get().execute_query() -print(me.user_principal_name) +print("Welcome, {0}!".format(me.given_name)) diff --git a/examples/outlook/calendars/__init__.py b/examples/outlook/calendars/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/examples/outlook/calendars/get_schedule.py b/examples/outlook/calendars/get_schedule.py index 8a8ee0d63..62681e666 100644 --- a/examples/outlook/calendars/get_schedule.py +++ b/examples/outlook/calendars/get_schedule.py @@ -7,7 +7,6 @@ The following example gets the availability information for user for the specified date, time, and time zone. """ -import json from datetime import datetime, timedelta from office365.graph_client import GraphClient @@ -20,4 +19,5 @@ result = client.me.calendar.get_schedule( [test_user_principal_name], start_time, end_time ).execute_query() -print(json.dumps(result.value.to_json(), indent=4)) +for item in result.value: + print(item.availabilityView) diff --git a/examples/outlook/events/__init__.py b/examples/outlook/events/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/examples/outlook/messages/create_subscription.py b/examples/outlook/messages/create_subscription.py index 8b7360230..3db7794e2 100644 --- a/examples/outlook/messages/create_subscription.py +++ b/examples/outlook/messages/create_subscription.py @@ -1,7 +1,7 @@ """ -Demonstrates how to send a change notification when the user receives a new mail. +Demonstrates how to send a change notification when the user receives a new mail. -https://learn.microsoft.com/en-us/graph/api/subscription-post-subscriptions?view=graph-rest-1.0&tabs=http +https://learn.microsoft.com/en-us/graph/api/subscription-post-subscriptions?view=graph-rest-1.0 """ import datetime diff --git a/examples/outlook/messages/download.py b/examples/outlook/messages/download.py index b060cbef3..f9fb8116c 100644 --- a/examples/outlook/messages/download.py +++ b/examples/outlook/messages/download.py @@ -9,13 +9,12 @@ import tempfile from office365.graph_client import GraphClient -from office365.outlook.mail.messages.message import Message from tests.graph_case import acquire_token_by_username_password client = GraphClient(acquire_token_by_username_password) messages = client.me.messages.select(["id", "subject"]).top(2).get().execute_query() with tempfile.TemporaryDirectory() as local_path: - for message in messages: # type: Message + for message in messages: with open(os.path.join(local_path, message.id + ".eml"), "wb") as local_file: message.download( local_file diff --git a/examples/outlook/messages/download_with_attachments.py b/examples/outlook/messages/download_with_attachments.py index 71cee572a..31e4f5ea3 100644 --- a/examples/outlook/messages/download_with_attachments.py +++ b/examples/outlook/messages/download_with_attachments.py @@ -1,15 +1,13 @@ """ Demonstrates how to download message attachments -https://learn.microsoft.com/en-us/graph/api/attachment-get?view=graph-rest-1.0&tabs=http +https://learn.microsoft.com/en-us/graph/api/attachment-get?view=graph-rest-1.0 """ import os import tempfile from office365.graph_client import GraphClient -from office365.outlook.mail.attachments.attachment import Attachment -from office365.outlook.mail.messages.message import Message from tests import test_user_principal_name from tests.graph_case import acquire_token_by_client_credentials @@ -18,13 +16,13 @@ messages = ( user.messages.filter("hasAttachments eq true") .expand(["attachments"]) - .top(1) + .top(10) .get() .execute_query() ) with tempfile.TemporaryDirectory() as local_path: - for message in messages: # type: Message - for attachment in message.attachments: # type: Attachment + for message in messages: + for attachment in message.attachments: with open(os.path.join(local_path, attachment.name), "wb") as local_file: attachment.download(local_file).execute_query() print("Message attachment downloaded into {0}".format(local_file.name)) diff --git a/examples/outlook/messages/empty_folder.py b/examples/outlook/messages/empty_folder.py new file mode 100644 index 000000000..f57d16278 --- /dev/null +++ b/examples/outlook/messages/empty_folder.py @@ -0,0 +1,12 @@ +""" +Empties the mail folder + +""" +from office365.graph_client import GraphClient +from tests import test_client_id, test_password, test_tenant, test_username + +client = GraphClient.with_username_and_password( + test_tenant, test_client_id, test_username, test_password +) +client.me.mail_folders["Inbox"].empty().execute_query() +print("Inbox has been emptied") diff --git a/examples/outlook/messages/get_basic_props.py b/examples/outlook/messages/get_basic_props.py index eb456d0ca..0e923a346 100644 --- a/examples/outlook/messages/get_basic_props.py +++ b/examples/outlook/messages/get_basic_props.py @@ -4,12 +4,11 @@ """ from office365.graph_client import GraphClient -from office365.outlook.mail.messages.message import Message from tests import test_user_principal_name from tests.graph_case import acquire_token_by_client_credentials client = GraphClient(acquire_token_by_client_credentials) user = client.users[test_user_principal_name] messages = user.messages.select(["id", "subject"]).top(10).get().execute_query() -for message in messages: # type: Message +for message in messages: print(message.subject) diff --git a/examples/outlook/messages/get_with_body.py b/examples/outlook/messages/get_with_body.py index 9e41144eb..68ca49f2b 100644 --- a/examples/outlook/messages/get_with_body.py +++ b/examples/outlook/messages/get_with_body.py @@ -3,14 +3,13 @@ Requires Mail.Read permission at least -https://learn.microsoft.com/en-us/graph/api/message-get?view=graph-rest-1.0&tabs=http +https://learn.microsoft.com/en-us/graph/api/message-get?view=graph-rest-1.0 """ from office365.graph_client import GraphClient -from office365.outlook.mail.messages.message import Message from tests.graph_case import acquire_token_by_username_password client = GraphClient(acquire_token_by_username_password) -messages = client.me.messages.select(["subject", "body"]).top(1).get().execute_query() -for message in messages: # type: Message - print(message.body.content) +messages = client.me.messages.select(["subject", "body"]).top(10).get().execute_query() +for message in messages: + print(message.subject) diff --git a/examples/outlook/messages/list_all.py b/examples/outlook/messages/list_all.py index 40aaf75ad..5364e60cf 100644 --- a/examples/outlook/messages/list_all.py +++ b/examples/outlook/messages/list_all.py @@ -8,6 +8,6 @@ from tests.graph_case import acquire_token_by_username_password client = GraphClient(acquire_token_by_username_password) -messages = client.me.messages.get().execute_query() +messages = client.me.messages.get().top(10).execute_query() for m in messages: print(m.subject) diff --git a/examples/outlook/messages/list_attachments.py b/examples/outlook/messages/list_attachments.py new file mode 100644 index 000000000..af2ead66a --- /dev/null +++ b/examples/outlook/messages/list_attachments.py @@ -0,0 +1,23 @@ +""" +List attachments + +https://learn.microsoft.com/en-us/graph/api/message-list-attachments?view=graph-rest-1.0 +""" + +from office365.graph_client import GraphClient +from tests import test_client_id, test_password, test_tenant, test_username + +client = GraphClient.with_username_and_password( + test_tenant, test_client_id, test_username, test_password +) +messages = ( + client.me.messages.filter("hasAttachments eq true") + .expand(["attachments"]) + .top(10) + .get() + .execute_query() +) + +for message in messages: + for attachment in message.attachments: + print("Message: {0}, Attachment: {1}".format(message.subject, attachment.name)) diff --git a/examples/outlook/messages/list_new.py b/examples/outlook/messages/list_new.py index 38b945544..8d16d8b79 100644 --- a/examples/outlook/messages/list_new.py +++ b/examples/outlook/messages/list_new.py @@ -5,7 +5,6 @@ """ from office365.graph_client import GraphClient -from office365.outlook.mail.messages.message import Message from tests.graph_case import acquire_token_by_username_password client = GraphClient(acquire_token_by_username_password) diff --git a/examples/outlook/messages/mark_all_as_read.py b/examples/outlook/messages/mark_all_as_read.py new file mode 100644 index 000000000..ab7ee1ac3 --- /dev/null +++ b/examples/outlook/messages/mark_all_as_read.py @@ -0,0 +1,11 @@ +""" + +""" +from office365.graph_client import GraphClient +from tests import test_client_id, test_password, test_tenant, test_username + +client = GraphClient.with_username_and_password( + test_tenant, test_client_id, test_username, test_password +) +client.me.mail_folders["Inbox"].mark_all_items_as_unread().execute_query() +print("All messages marked as read") diff --git a/examples/outlook/messages/mark_as_read.py b/examples/outlook/messages/mark_as_read.py index d63a47c43..3df7bf4e0 100644 --- a/examples/outlook/messages/mark_as_read.py +++ b/examples/outlook/messages/mark_as_read.py @@ -1,18 +1,18 @@ """ Mark message as read example -https://learn.microsoft.com/en-us/graph/api/message-update?view=graph-rest-1.0&tabs=http +https://learn.microsoft.com/en-us/graph/api/message-update?view=graph-rest-1.0 """ import sys from office365.graph_client import GraphClient -from office365.outlook.mail.messages.message import Message from tests.graph_case import acquire_token_by_username_password client = GraphClient(acquire_token_by_username_password) messages = client.me.messages.top(1).get().execute_query() if len(messages) == 0: - sys.exit("No messages found") -first_message = messages[0] # type: Message + sys.exit("No messages were found") +first_message = messages[0] first_message.set_property("isRead", True).update().execute_query() +print("Message {0} has been marked as read".format(first_message.subject)) diff --git a/examples/outlook/messages/reply.py b/examples/outlook/messages/reply.py index a0bebe7b6..84f6bfaff 100644 --- a/examples/outlook/messages/reply.py +++ b/examples/outlook/messages/reply.py @@ -7,7 +7,6 @@ import sys from office365.graph_client import GraphClient -from office365.outlook.mail.messages.message import Message from tests.graph_case import acquire_token_by_username_password client = GraphClient(acquire_token_by_username_password) @@ -15,5 +14,5 @@ if len(messages) == 0: sys.exit("No messages were found") -first_message = messages[0] # type: Message +first_message = messages[0] first_message.reply(comment="Fanny, would you join us next time?").execute_query() diff --git a/examples/outlook/messages/send.py b/examples/outlook/messages/send.py index 0894172ed..fc146b3ae 100644 --- a/examples/outlook/messages/send.py +++ b/examples/outlook/messages/send.py @@ -5,12 +5,12 @@ """ from office365.graph_client import GraphClient -from tests import test_user_principal_name +from tests import test_user_principal_name_alt from tests.graph_case import acquire_token_by_username_password client = GraphClient(acquire_token_by_username_password) client.me.send_mail( subject="Meet for lunch?", body="The new cafeteria is open.", - to_recipients=["fannyd@contoso.onmicrosoft.com", test_user_principal_name], + to_recipients=["fannyd@contoso.onmicrosoft.com", test_user_principal_name_alt], ).execute_query() diff --git a/examples/outlook/messages/send_with_attachment.py b/examples/outlook/messages/send_with_attachment.py index 579541065..e4e02b853 100644 --- a/examples/outlook/messages/send_with_attachment.py +++ b/examples/outlook/messages/send_with_attachment.py @@ -1,17 +1,19 @@ """ Create a message with a file attachment and send the message -https://learn.microsoft.com/en-us/graph/api/user-sendmail?view=graph-rest-1.0&tabs=http +https://learn.microsoft.com/en-us/graph/api/user-sendmail?view=graph-rest-1.0 """ from office365.graph_client import GraphClient +from tests import test_user_principal_name_alt from tests.graph_case import acquire_token_by_username_password client = GraphClient(acquire_token_by_username_password) client.me.send_mail( subject="Meet for lunch?", body="The new cafeteria is open.", - to_recipients=["fannyd@contoso.onmicrosoft.com"], + to_recipients=["fannyd@contoso.onmicrosoft.com", test_user_principal_name_alt], ).add_file_attachment( "attachment.txt", "--Some content goes here--", "text/plain" ).execute_query() +print("Message has been sent") diff --git a/examples/sharepoint/apps/list_apps.py b/examples/sharepoint/apps/list_apps.py index f309ebe07..051bd881a 100644 --- a/examples/sharepoint/apps/list_apps.py +++ b/examples/sharepoint/apps/list_apps.py @@ -1,10 +1,9 @@ from office365.sharepoint.client_context import ClientContext -from office365.sharepoint.marketplace.app_metadata import CorporateCatalogAppMetadata from tests import test_admin_credentials, test_admin_site_url admin_client = ClientContext(test_admin_site_url).with_credentials( test_admin_credentials ) apps = admin_client.web.tenant_app_catalog.available_apps.get().execute_query() -for app in apps: # type: CorporateCatalogAppMetadata +for app in apps: print(app.title) diff --git a/examples/sharepoint/listitems/attachments/__init__.py b/examples/sharepoint/listitems/attachments/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/examples/sharepoint/listitems/versions/list.py b/examples/sharepoint/listitems/versions/list.py new file mode 100644 index 000000000..549bfef43 --- /dev/null +++ b/examples/sharepoint/listitems/versions/list.py @@ -0,0 +1,18 @@ +""" +Demonstrates how to retain the history for list items. +""" +from office365.sharepoint.client_context import ClientContext +from tests import test_client_credentials, test_team_site_url + +ctx = ClientContext(test_team_site_url).with_credentials(test_client_credentials) +items = ( + ctx.web.lists.get_by_title("Site Pages") + .items.get() + .expand(["Versions"]) + .top(10) + .execute_query() +) + +for item in items: + for version in item.versions: + print(version) diff --git a/examples/sharepoint/search/search_sites.py b/examples/sharepoint/search/search_sites.py index 4f414f714..05091ea6b 100644 --- a/examples/sharepoint/search/search_sites.py +++ b/examples/sharepoint/search/search_sites.py @@ -1,3 +1,8 @@ +""" +Search SharePoint sites the current user is member of + +""" + from office365.sharepoint.client_context import ClientContext from tests import test_site_url, test_user_credentials diff --git a/examples/sharepoint/users/export_site_users.py b/examples/sharepoint/users/export_site_users.py index 4f8101080..1a785850a 100644 --- a/examples/sharepoint/users/export_site_users.py +++ b/examples/sharepoint/users/export_site_users.py @@ -1,5 +1,7 @@ """ Retrieves site users + + """ from office365.sharepoint.client_context import ClientContext diff --git a/examples/sharepoint/users/get_onedrive_quota_max.py b/examples/sharepoint/users/get_onedrive_quota_max.py new file mode 100644 index 000000000..94cf26c44 --- /dev/null +++ b/examples/sharepoint/users/get_onedrive_quota_max.py @@ -0,0 +1,9 @@ +""" +Get OneDrive quota max for a user +""" +from office365.sharepoint.client_context import ClientContext +from tests import test_password, test_site_url, test_username + +ctx = ClientContext(test_site_url).with_user_credentials(test_username, test_password) +result = ctx.people_manager.get_user_onedrive_quota_max(test_username).execute_query() +print(result.value) diff --git a/office365/graph_client.py b/office365/graph_client.py index 02a1765c7..ce539b18b 100644 --- a/office365/graph_client.py +++ b/office365/graph_client.py @@ -1,4 +1,4 @@ -from typing import Callable, List, Optional +from typing import Any, Callable, List, Optional from office365.booking.solutions.root import SolutionsRoot from office365.communications.cloud_communications import CloudCommunications @@ -120,6 +120,7 @@ def _acquire_token(): def with_client_secret( tenant, client_id, client_secret, scopes=None, token_cache=None ): + # type: (str, str, str, List[str], Any) -> "GraphClient" """ Initializes the confidential client with client secret diff --git a/office365/onedrive/columns/definition.py b/office365/onedrive/columns/definition.py index 1a97bb9a8..a67ba0e0c 100644 --- a/office365/onedrive/columns/definition.py +++ b/office365/onedrive/columns/definition.py @@ -1,3 +1,5 @@ +from typing import Optional + from office365.base_item import BaseItem from office365.onedrive.columns.boolean import BooleanColumn from office365.onedrive.columns.calculated import CalculatedColumn @@ -32,23 +34,21 @@ class ColumnDefinition(BaseItem): @property def display_name(self): - """The user-facing name of the column. - - :rtype: str or None - """ + # type: () -> Optional[str] + """The user-facing name of the column.""" return self.properties.get("displayName", None) @property def enforce_unique_values(self): + # type: () -> Optional[bool] """ If true, no two list items may have the same value for this column. - - :rtype: bool or None """ return self.properties.get("enforceUniqueValues", None) @property def indexed(self): + # type: () -> Optional[bool] """Specifies whether the column values can used for sorting and searching.""" return self.properties.get("indexed", None) @@ -61,6 +61,7 @@ def content_approval_status(self): @property def column_group(self): + # type: () -> Optional[str] """For site columns, the name of the group this column belongs to. Helps organize related columns.""" return self.properties.get("columnGroup", None) @@ -131,16 +132,14 @@ def hyperlink_or_picture(self): @property def hidden(self): - """Specifies whether the column is displayed in the user interface. - :rtype: bool or None - """ + # type: () -> Optional[bool] + """Specifies whether the column is displayed in the user interface.""" return self.properties.get("hidden", None) @property def is_deletable(self): - """Indicates whether this column can be deleted. - :rtype: bool or None - """ + # type: () -> Optional[bool] + """Indicates whether this column can be deleted.""" return self.properties.get("isDeletable", None) @property diff --git a/office365/onedrive/contenttypes/content_type.py b/office365/onedrive/contenttypes/content_type.py index fba269094..7f2480a68 100644 --- a/office365/onedrive/contenttypes/content_type.py +++ b/office365/onedrive/contenttypes/content_type.py @@ -1,3 +1,5 @@ +from typing import Optional + from office365.base_item import BaseItem from office365.entity_collection import EntityCollection from office365.onedrive.columns.column_link import ColumnLink @@ -88,9 +90,9 @@ def document_template(self): @property def name(self): + # type: () -> Optional[str] """ The name of the content type. - :rtype: str or None """ return self.properties.get("name", None) @@ -118,9 +120,9 @@ def propagate_changes(self): @property def read_only(self): + # type: () -> Optional[bool] """ If true, the content type cannot be modified unless this value is first set to false. - :rtype: bool or None """ return self.properties.get("readOnly", None) @@ -163,6 +165,7 @@ def base_types(self): @property def columns(self): + # type: () -> EntityCollection[ColumnDefinition] """The collection of column definitions for this contentType.""" return self.properties.get( "columns", diff --git a/office365/onenote/entity_hierarchy_model.py b/office365/onenote/entity_hierarchy_model.py index 249155bcd..5ce586bfb 100644 --- a/office365/onenote/entity_hierarchy_model.py +++ b/office365/onenote/entity_hierarchy_model.py @@ -1,3 +1,5 @@ +from typing import Optional + from office365.directory.permissions.identity_set import IdentitySet from office365.onenote.entity_schema_object_model import OnenoteEntitySchemaObjectModel @@ -5,11 +7,13 @@ class OnenoteEntityHierarchyModel(OnenoteEntitySchemaObjectModel): @property def display_name(self): + # type: () -> Optional[str] """The name of the section.""" return self.properties.get("displayName", None) @property def created_by(self): + # type: () -> IdentitySet """Identity of the user, device, and application which created the item. Read-only.""" return self.properties.get("createdBy", IdentitySet()) diff --git a/office365/onenote/notebooks/notebook.py b/office365/onenote/notebooks/notebook.py index 96e67b2d8..1a3b10646 100644 --- a/office365/onenote/notebooks/notebook.py +++ b/office365/onenote/notebooks/notebook.py @@ -1,14 +1,20 @@ +from typing import TYPE_CHECKING + from office365.entity_collection import EntityCollection from office365.onenote.entity_hierarchy_model import OnenoteEntityHierarchyModel from office365.onenote.sections.section import OnenoteSection from office365.runtime.paths.resource_path import ResourcePath +if TYPE_CHECKING: + from office365.onenote.sectiongroups.section_group import SectionGroup + class Notebook(OnenoteEntityHierarchyModel): """A OneNote notebook.""" @property def sections(self): + # type: () -> EntityCollection[OnenoteSection] """ Retrieve a list of onenoteSection objects from the specified notebook. """ @@ -23,10 +29,10 @@ def sections(self): @property def section_groups(self): + # type: () -> EntityCollection[SectionGroup] """ Retrieve a list of onenoteSection objects from the specified notebook. """ - from office365.onenote.sectiongroups.section_group import SectionGroup return self.properties.get( "sectionGroups", diff --git a/office365/outlook/calendar/meetingtimes/suggestion.py b/office365/outlook/calendar/meetingtimes/suggestion.py index 5eff57bfd..da7e15732 100644 --- a/office365/outlook/calendar/meetingtimes/suggestion.py +++ b/office365/outlook/calendar/meetingtimes/suggestion.py @@ -33,3 +33,6 @@ def __init__( self.confidence = confidence self.locations = ClientValueCollection(Location, locations) self.meetingTimeSlot = meeting_timeslot + + def __repr__(self): + return repr(self.meetingTimeSlot) diff --git a/office365/outlook/calendar/meetingtimes/time_slot.py b/office365/outlook/calendar/meetingtimes/time_slot.py index 8f85a3d92..bbb4629be 100644 --- a/office365/outlook/calendar/meetingtimes/time_slot.py +++ b/office365/outlook/calendar/meetingtimes/time_slot.py @@ -14,3 +14,6 @@ def __init__(self, start=DateTimeTimeZone(), end=DateTimeTimeZone()): super(TimeSlot, self).__init__() self.start = start self.end = end + + def __repr__(self): + return "({0} - {1})".format(self.start, self.end) diff --git a/office365/outlook/contacts/collection.py b/office365/outlook/contacts/collection.py index 4c5456d0c..53ffced62 100644 --- a/office365/outlook/contacts/collection.py +++ b/office365/outlook/contacts/collection.py @@ -33,10 +33,3 @@ def _create_email_address(address): if business_phone: kwargs["businessPhones"] = StringCollection([business_phone]) return super(ContactCollection, self).add(**kwargs) - - def __getitem__(self, key): - """ - :param int or str key: Contact identifier or index - :rtype: Contact - """ - return super(ContactCollection, self).__getitem__(key) diff --git a/office365/outlook/contacts/contact.py b/office365/outlook/contacts/contact.py index 2319e1ce5..f6e527b98 100644 --- a/office365/outlook/contacts/contact.py +++ b/office365/outlook/contacts/contact.py @@ -1,3 +1,5 @@ +from typing import Optional + from office365.directory.extensions.extended_property import ( MultiValueLegacyExtendedProperty, SingleValueLegacyExtendedProperty, @@ -23,44 +25,44 @@ def business_phones(self): @property def display_name(self): + # type: () -> Optional[str] """ The contact's display name. You can specify the display name in a create or update operation. Note that later updates to other properties may cause an automatically generated value to overwrite the displayName value you have specified. To preserve a pre-existing value, always include it as displayName in an update operation. - :rtype: str or None """ return self.properties.get("displayName", None) @property def manager(self): + # type: () -> Optional[str] """ The name of the contact's manager. - :rtype: str or None """ return self.properties.get("manager", None) @manager.setter def manager(self, value): + # type: (str) -> None """ Sets name of the contact's manager. - :type value: str """ self.set_property("manager", value) @property def mobile_phone(self): + # type: () -> Optional[str] """ The contact's mobile phone number. - :rtype: str or None """ return self.properties.get("mobilePhone", None) @mobile_phone.setter def mobile_phone(self, value): + # type: (str) -> None """ Sets contact's mobile phone number. - :type value: str """ self.set_property("mobilePhone", value) @@ -78,6 +80,7 @@ def email_addresses(self): @property def extensions(self): + # type: () -> EntityCollection[Extension] """The collection of open extensions defined for the contact. Nullable.""" return self.properties.get( "extensions", diff --git a/office365/outlook/mail/attachments/attachment.py b/office365/outlook/mail/attachments/attachment.py index 59c9fcc90..a942931c9 100644 --- a/office365/outlook/mail/attachments/attachment.py +++ b/office365/outlook/mail/attachments/attachment.py @@ -1,3 +1,8 @@ +import datetime +from typing import IO, AnyStr, Optional + +from typing_extensions import Self + from office365.entity import Entity from office365.runtime.client_result import ClientResult from office365.runtime.queries.function import FunctionQuery @@ -6,22 +11,23 @@ class Attachment(Entity): """A file or item (contact, event or message) attached to an event or message.""" - def download(self, file_object): - """Downloads raw contents of a file or item attachment + def __repr__(self): + return self.name or self.entity_type_name - :type file_object: typing.IO - """ + def download(self, file_object): + # type: (IO) -> Self + """Downloads raw contents of a file or item attachment""" def _save_content(return_type): + # type: (ClientResult[AnyStr]) -> None file_object.write(return_type.value) self.get_content().after_execute(_save_content) return self def get_content(self): - """ - Gets the raw contents of a file or item attachment - """ + # type: () -> ClientResult[AnyStr] + """Gets the raw contents of a file or item attachment""" return_type = ClientResult(self.context) qry = FunctionQuery(self, "$value", None, return_type) self.context.add_query(qry) @@ -29,47 +35,41 @@ def get_content(self): @property def name(self): - """ - The attachment's file name. - :rtype: str or None - """ + # type: () -> Optional[str] + """The attachment's file name.""" return self.properties.get("name", None) @name.setter def name(self, value): - """ - Sets the attachment's file name. - :type: value: str - """ + # type: (str) -> None + """Sets the attachment's file name.""" self.set_property("name", value) @property def content_type(self): - """ - :rtype: str or None - """ + # type: () -> Optional[str] return self.properties.get("contentType", None) @content_type.setter def content_type(self, value): - """ - :type: value: str - """ + # type: (str) -> None self.set_property("contentType", value) @property def size(self): - """ - - :rtype: int or None - """ + # type: () -> Optional[int] return self.properties.get("size", None) @property - def last_modified_date_time(self): - """ - The Timestamp type represents date and time information using ISO 8601 format and is always in UTC time. + def last_modified_datetime(self): + # type: () -> Optional[datetime.datetime] + """The Timestamp type represents date and time information using ISO 8601 format and is always in UTC time.""" + return self.properties.get("lastModifiedDateTime", datetime.datetime.min) - :rtype: int or None - """ - return self.properties.get("lastModifiedDateTime", None) + def get_property(self, name, default_value=None): + if default_value is None: + property_mapping = { + "lastModifiedDateTime": self.last_modified_datetime, + } + default_value = property_mapping.get(name, None) + return super(Attachment, self).get_property(name, default_value) diff --git a/office365/outlook/mail/folders/folder.py b/office365/outlook/mail/folders/folder.py index aba166405..bf59d2054 100644 --- a/office365/outlook/mail/folders/folder.py +++ b/office365/outlook/mail/folders/folder.py @@ -1,3 +1,5 @@ +from typing import Optional + from office365.directory.extensions.extended_property import ( MultiValueLegacyExtendedProperty, SingleValueLegacyExtendedProperty, @@ -26,57 +28,84 @@ def copy(self, destination_id): self.context.add_query(qry) return return_type - @property - def child_folder_count(self): + def empty(self, delete_sub_folders=False): """ - The number of immediate child mailFolders in the current mailFolder. - :rtype: int or None + Empties the folder + :param bool delete_sub_folders: true to indicate that subfolders should also be deleted; otherwise, false. """ + + def _empty(col): + # type: (MessageCollection) -> None + if not col.has_next: + [m.delete_object() for m in col] + + self.messages.get_all(page_loaded=_empty) + return self + + def mark_all_items_as_read(self): + """Marks all items in folder as read.""" + + def _mark_all_items_as_read(col): + # type: (MessageCollection) -> None + [m.set_property("isRead", True).update() for m in col.current_page] + + self.messages.get_all(1, page_loaded=_mark_all_items_as_read) + return self + + def mark_all_items_as_unread(self): + """Marks all items in folder as unread.""" + + def _mark_all_items_as_unread(col): + # type: (MessageCollection) -> None + [m.set_property("isRead", False).update() for m in col.current_page] + + self.messages.get_all(page_loaded=_mark_all_items_as_unread) + return self + + @property + def child_folder_count(self): + # type: () -> Optional[int] + """The number of immediate child mailFolders in the current mailFolder.""" return self.properties.get("childFolderCount", None) @property def display_name(self): - """ - The name of the Mail folder - :rtype: str or None - """ + # type: () -> Optional[str] + """The name of the Mail folder""" return self.properties.get("displayName", None) @property def is_hidden(self): + # type: () -> Optional[bool] """ Indicates whether the mailFolder is hidden. This property can be set only when creating the folder. Find more information in Hidden mail folders. - :rtype: bool or None """ return self.properties.get("isHidden", None) @property def parent_folder_id(self): + # type: () -> Optional[str] """ The unique identifier for the mailFolder's parent mailFolder. - :rtype: str or None """ return self.properties.get("parentFolderId", None) @property def total_item_count(self): - """ - The number of items in the mailFolder. - :rtype: int - """ + # type: () -> Optional[int] + """The number of items in the mailFolder.""" return self.properties.get("totalItemCount", None) @property def unread_item_count(self): - """ - The number of items in the mailFolder marked as unread. - :rtype: int - """ + # type: () -> Optional[int] + """The number of items in the mailFolder marked as unread.""" return self.properties.get("unreadItemCount", None) @property def child_folders(self): + # type: () -> EntityCollection[MailFolder] """The collection of child folders in the mailFolder.""" return self.properties.get( "childFolders", @@ -89,6 +118,7 @@ def child_folders(self): @property def message_rules(self): + # type: () -> EntityCollection[MessageRule] """""" return self.properties.get( "messageRules", @@ -112,6 +142,7 @@ def messages(self): @property def multi_value_extended_properties(self): + # type: () -> EntityCollection[MultiValueLegacyExtendedProperty] """The collection of multi-value extended properties defined for the MailFolder.""" return self.properties.get( "multiValueExtendedProperties", @@ -124,6 +155,7 @@ def multi_value_extended_properties(self): @property def single_value_extended_properties(self): + # type: () -> EntityCollection[SingleValueLegacyExtendedProperty] """The collection of single-value extended properties defined for the MailFolder.""" return self.properties.get( "singleValueExtendedProperties", diff --git a/office365/outlook/mail/messages/message.py b/office365/outlook/mail/messages/message.py index 26c48d372..c4ff5d7f6 100644 --- a/office365/outlook/mail/messages/message.py +++ b/office365/outlook/mail/messages/message.py @@ -1,7 +1,9 @@ import os import uuid from datetime import datetime -from typing import Optional +from typing import IO, AnyStr, Optional + +from typing_extensions import Self from office365.directory.extensions.extended_property import ( MultiValueLegacyExtendedProperty, @@ -26,11 +28,8 @@ class Message(OutlookItem): """A message in a mailbox folder.""" def add_extended_property(self, name, value): - """ - Create a single-value extended property for a message - :param str name: A property name - :param str value: - """ + # type: (str, str) -> Self + """Create a single-value extended property for a message""" prop_id = str(uuid.uuid4()) prop_type = "String" prop_value = [ @@ -63,25 +62,20 @@ def create_forward(self, to_recipients=None, message=None, comment=None): return self def download(self, file_object): - """Download MIME content of a message into a file - - :type file_object: typing.IO - """ + # type: (IO) -> Self + """Download MIME content of a message into a file""" def _save_content(return_type): - """ - :type return_type: ClientResult - """ + # type: (ClientResult[AnyStr]) -> None file_object.write(return_type.value) self.get_content().after_execute(_save_content) return self def get_content(self): - """ - Get MIME content of a message - """ - return_type = ClientResult[str](self.context) + # type: () -> ClientResult[AnyStr] + """Get MIME content of a message""" + return_type = ClientResult(self.context) qry = FunctionQuery(self, "$value", None, return_type) self.context.add_query(qry) return return_type @@ -245,43 +239,37 @@ def extensions(self): @property def body(self): - """The body of the message. It can be in HTML or text format. - - :rtype: ItemBody - """ + """The body of the message. It can be in HTML or text format.""" return self.properties.setdefault("body", ItemBody()) @body.setter def body(self, value): - """The body of the message. It can be in HTML or text format. - - :type value: str or ItemBody - """ + # type: (str|ItemBody) -> None + """Sets the body of the message. It can be in HTML or text format.""" if not isinstance(value, ItemBody): value = ItemBody(value) self.set_property("body", value) @property def body_preview(self): + # type: () -> Optional[str] """ - The first 255 characters of the message body. It is in text format. - :rtype: str or None - """ + The first 255 characters of the message body. It is in text format.""" return self.properties.get("bodyPreview", None) @property def conversation_id(self): + # type: () -> Optional[str] """ The ID of the conversation the email belongs to. - :rtype: str or None """ return self.properties.get("conversationId", None) @property def conversation_index(self): + # type: () -> Optional[str] """ Indicates the position of the message within the conversation. - :rtype: str or None """ return self.properties.get("conversationIndex", None) @@ -303,18 +291,18 @@ def sent_from(self): @property def importance(self): + # type: () -> Optional[str] """ The importance of the message. - :rtype: str """ return self.properties.get("importance", None) @property def inference_classification(self): + # type: () -> Optional[str] """ The classification of the message for the user, based on inferred relevance or importance, or on an explicit override. The possible values are: focused or other. - :rtype: str """ return self.properties.get("inferenceClassification", None) @@ -331,41 +319,37 @@ def internet_message_headers(self): @property def internet_message_id(self): - """ - The message ID in the format specified by RFC2822 - :rtype: str - """ + # type: () -> Optional[str] + """The message ID in the format specified by RFC2822""" return self.properties.get("internetMessageId", None) @property def is_delivery_receipt_requested(self): + # type: () -> Optional[bool] """ Indicates whether a read receipt is requested for the message. - :rtype: bool """ return self.properties.get("isDeliveryReceiptRequested", None) @property def is_draft(self): + # type: () -> Optional[bool] """ Indicates whether the message is a draft. A message is a draft if it hasn't been sent yet. - :rtype: bool """ return self.properties.get("isDraft", None) @property def is_read(self): - """ - Indicates whether the message has been read. - :rtype: bool - """ + # type: () -> Optional[bool] + """Indicates whether the message has been read.""" return self.properties.get("isRead", None) @property def is_read_receipt_requested(self): + # type: () -> Optional[bool] """ Indicates whether a read receipt is requested for the message. - :rtype: bool """ return self.properties.get("isReadReceiptRequested", None) @@ -389,10 +373,8 @@ def subject(self): @subject.setter def subject(self, value): - """ - The subject of the message. - :type value: str - """ + # type: (str) -> None + """Sets the subject of the message.""" self.set_property("subject", value) @property @@ -429,13 +411,13 @@ def sender(self): @property def parent_folder_id(self): - """The unique identifier for the message's parent mailFolder. - :rtype: str or None - """ + # type: () -> Optional[str] + """The unique identifier for the message's parent mailFolder.""" return self.properties.get("parentFolderId", None) @property def web_link(self): + # type: () -> Optional[str] """ The URL to open the message in Outlook on the web. @@ -447,12 +429,12 @@ def web_link(self): You will be prompted to login if you are not already logged in with the browser. This URL cannot be accessed from within an iFrame. - :rtype: str or None """ return self.properties.get("webLink", None) @property def multi_value_extended_properties(self): + # type: () -> EntityCollection[MultiValueLegacyExtendedProperty] """The collection of multi-value extended properties defined for the event.""" return self.properties.get( "multiValueExtendedProperties", @@ -465,6 +447,7 @@ def multi_value_extended_properties(self): @property def single_value_extended_properties(self): + # type: () -> EntityCollection[SingleValueLegacyExtendedProperty] """The collection of single-value extended properties defined for the message""" return self.properties.get( "singleValueExtendedProperties", diff --git a/office365/runtime/client_object_collection.py b/office365/runtime/client_object_collection.py index 3f6fb109d..13ac60f87 100644 --- a/office365/runtime/client_object_collection.py +++ b/office365/runtime/client_object_collection.py @@ -156,6 +156,7 @@ def get(self): # type: () -> Self def _loaded(items): + # type: (Self) -> None self._page_loaded.notify(self) self.context.load(self, after_loaded=_loaded) @@ -163,17 +164,13 @@ def _loaded(items): def get_all(self, page_size=None, page_loaded=None): # type: (int, Callable[[Self], None]) -> Self - """ - Gets all the items in a collection, regardless of the size. + """Gets all the items in a collection, regardless of the size.""" - :type self: T - :param int page_size: Page size - :param (T) -> None page_loaded: Page loaded event - """ self.paged(page_size, page_loaded) def _page_loaded(items): - self._page_loaded.notify(self) + # type: (Self) -> None + self._page_loaded.notify(items) if self.has_next: self._get_next(after_loaded=_page_loaded) @@ -184,16 +181,15 @@ def _get_next(self, after_loaded=None): # type: (Optional[EventHandler]) -> Self """ Submit a request to retrieve next collection of items - :param (ClientObjectCollection) -> None after_loaded: Page loaded event """ - def _construct_next_query(request): + def _construct_request(request): # type: (RequestOptions) -> None request.url = self._next_request_url self.context.load( - self, before_loaded=_construct_next_query, after_loaded=after_loaded + self, before_loaded=_construct_request, after_loaded=after_loaded ) return self @@ -254,6 +250,11 @@ def has_next(self): """""" return self._next_request_url is not None + @property + def current_page(self): + # type: () -> List[T] + return self._data[self._current_pos :] + @property def entity_type_name(self): # type: () -> str diff --git a/office365/runtime/client_runtime_context.py b/office365/runtime/client_runtime_context.py index fbfffa052..6094ccf1c 100644 --- a/office365/runtime/client_runtime_context.py +++ b/office365/runtime/client_runtime_context.py @@ -93,7 +93,7 @@ def load( qry = ReadEntityQuery(client_object, properties_to_retrieve) self.add_query(qry) if callable(before_loaded): - self.before_execute(before_loaded) + self.before_query_execute(before_loaded) if callable(after_loaded): self.after_query_execute(after_loaded, client_object) return self @@ -110,10 +110,7 @@ def before_query_execute(self, action, once=True, *args, **kwargs): query = self._queries[-1] def _prepare_request(request): - """ - :type request: office365.runtime.http.request_options.RequestOptions - """ - + # type: (RequestOptions) -> None if self.current_query.id == query.id: if once: self.pending_request().beforeExecute -= _prepare_request diff --git a/office365/runtime/odata/type.py b/office365/runtime/odata/type.py index 72fb477ed..e4f33ee9b 100644 --- a/office365/runtime/odata/type.py +++ b/office365/runtime/odata/type.py @@ -60,6 +60,9 @@ def try_parse_datetime(value): :param str value: Represents date and time with values ranging from 12:00:00 midnight, January 1, 1753 A.D. through 11:59:59 P.M, December 9999 A.D. """ + if value is None: + return None + known_formats = [ "%Y-%m-%dT%H:%M:%SZ", "%Y-%m-%dT%H:%M:%S.%fZ", diff --git a/office365/runtime/queries/function.py b/office365/runtime/queries/function.py index 116886afb..f5a193408 100644 --- a/office365/runtime/queries/function.py +++ b/office365/runtime/queries/function.py @@ -3,7 +3,7 @@ class FunctionQuery(ClientQuery): - """ "Service operation query""" + """Function query""" def __init__( self, binding_type, method_name=None, method_params=None, return_type=None diff --git a/office365/sharepoint/contenttypes/content_type.py b/office365/sharepoint/contenttypes/content_type.py index 8409a9cf1..265e8fe88 100644 --- a/office365/sharepoint/contenttypes/content_type.py +++ b/office365/sharepoint/contenttypes/content_type.py @@ -1,3 +1,5 @@ +from typing import Optional + from office365.runtime.paths.resource_path import ResourcePath from office365.runtime.queries.service_operation import ServiceOperationQuery from office365.runtime.types.collections import StringCollection @@ -43,58 +45,56 @@ def update(self, update_children): @property def client_form_custom_formatter(self): - """ - :rtype: str - """ + # type: () -> Optional[str] return self.properties.get("ClientFormCustomFormatter", None) @property def display_form_client_side_component_id(self): + # type: () -> Optional[str] """ The component ID of an SPFx Form Customizer to connect to this content type for usage with display forms. - :rtype: str """ return self.properties.get("DisplayFormClientSideComponentId", None) @property def display_form_client_side_component_properties(self): + # type: () -> Optional[str] """ The component properties of an SPFx Form Customizer to connect to this content type for usage with display forms - :rtype: str """ return self.properties.get("DisplayFormClientSideComponentProperties", None) @property def display_form_template_name(self): + # type: () -> Optional[str] """ Specifies the name of a custom display form template to use for list items that have been assigned the content type. - :rtype: str or None """ return self.properties.get("DisplayFormTemplateName", None) @property def display_form_url(self): + # type: () -> Optional[str] """ Specifies the URL of a custom display form to use for list items that have been assigned the content type. - :rtype: str or None """ return self.properties.get("DisplayFormUrl", None) @property def edit_form_client_side_component_id(self): + # type: () -> Optional[str] """ The component properties of an SPFx Form Customizer to connect to this content type for usage with edit item forms - :rtype: str or None """ return self.properties.get("EditFormClientSideComponentId", None) @property def edit_form_client_side_component_properties(self): + # type: () -> Optional[str] """ The component ID of an SPFx Form Customizer to connect to this content type for usage with edit item forms - :rtype: str """ return self.properties.get("EditFormClientSideComponentProperties", None) @@ -107,53 +107,45 @@ def id(self): @property def sealed(self): - """Specifies whether the content type can be changed. - :rtype: bool or None - """ + # type: () -> Optional[bool] + """Specifies whether the content type can be changed.""" return self.properties.get("Sealed", None) @property def string_id(self): - """A string representation of the value of the Id - :rtype: str - """ + # type: () -> Optional[str] + """A string representation of the value of the Id""" return self.properties.get("StringId", None) @property def name(self): - """Gets the name of the content type. - :rtype: str or None - """ + # type: () -> Optional[str] + """Gets the name of the content type.""" return self.properties.get("Name", None) @name.setter def name(self, value): - """Sets the name of the content type. - :rtype: str or None - """ + # type: (str) -> None + """Sets the name of the content type.""" self.set_property("Name", value) @property def new_form_client_side_component_properties(self): - """ - The component properties of an SPFx Form Customizer to connect to this content type for usage with new item forms - :rtype: str or None - """ + # type: () -> Optional[str] + """The component properties of an SPFx Form Customizer to connect to this content type for usage with new + item forms""" return self.properties.get("NewFormClientSideComponentProperties", None) @property def description(self): - """Gets the description of the content type. - :rtype: str or None - """ + # type: () -> Optional[str] + """Gets the description of the content type.""" return self.properties.get("Description", None) @description.setter def description(self, value): - """Sets the description of the content type. - - :type value: str - """ + # type: (str) -> None + """Sets the description of the content type.""" self.set_property("Description", value) @property @@ -168,60 +160,54 @@ def description_resource(self): @property def document_template(self): - """ - Specifies the file path to the document template (1) used for a new list item that has been assigned - the content type. - :rtype: str or None + # type: () -> Optional[str] + """Specifies the file path to the document template (1) used for a new list item that has been assigned + the content type. """ return self.properties.get("DocumentTemplate", None) @property def document_template_url(self): - """ - Specifies the URL of the document template assigned to the content type. - :rtype: str or None - """ + # type: () -> Optional[str] + """Specifies the URL of the document template assigned to the content type.""" return self.properties.get("DocumentTemplateUrl", None) @property def edit_form_url(self): + # type: () -> Optional[str] """ Specifies the URL of a custom edit form to use for list items that have been assigned the content type. - :rtype: str or None """ return self.properties.get("EditFormUrl", None) @property def group(self): - """Gets the group of the content type. - :rtype: str or None - """ + # type: () -> Optional[str] + """Gets the group of the content type.""" return self.properties.get("Group", None) @group.setter def group(self, value): - """Sets the group of the content type. - :type value: str or None - """ + # type: (str) -> None + """Sets the group of the content type.""" self.set_property("Group", value) @property def hidden(self): + # type: () -> Optional[bool] """Specifies whether the content type is unavailable for creation or usage directly from a user interface.""" return self.properties.get("Hidden", None) @property def js_link(self): - """Gets or sets the JSLink for the content type custom form template - :rtype: str or None - """ + # type: () -> Optional[str] + """Gets or sets the JSLink for the content type custom form template""" return self.properties.get("JSLink", None) @property def read_only(self): - """Specifies whether changes to the content type properties are denied. - :rtype: bool or None - """ + # type: () -> Optional[bool] + """Specifies whether changes to the content type properties are denied.""" return self.properties.get("ReadOnly", None) @property @@ -236,9 +222,8 @@ def name_resource(self): @property def schema_xml(self): - """Specifies the XML schema that represents the content type. - :rtype: str or None - """ + # type: () -> Optional[str] + """Specifies the XML schema that represents the content type.""" return self.properties.get("SchemaXml", None) @property diff --git a/office365/sharepoint/marketplace/app_metadata_collection.py b/office365/sharepoint/marketplace/app_metadata_collection.py index a8007b1e8..900ce2ff3 100644 --- a/office365/sharepoint/marketplace/app_metadata_collection.py +++ b/office365/sharepoint/marketplace/app_metadata_collection.py @@ -3,7 +3,9 @@ from office365.sharepoint.marketplace.app_metadata import CorporateCatalogAppMetadata -class CorporateCatalogAppMetadataCollection(EntityCollection): +class CorporateCatalogAppMetadataCollection( + EntityCollection[CorporateCatalogAppMetadata] +): """Collection of app metadata.""" def __init__(self, context, resource_path=None): diff --git a/office365/sharepoint/publishing/pages/page.py b/office365/sharepoint/publishing/pages/page.py index 7c2398916..758021dc4 100644 --- a/office365/sharepoint/publishing/pages/page.py +++ b/office365/sharepoint/publishing/pages/page.py @@ -1,3 +1,5 @@ +from typing import Optional + from office365.runtime.client_result import ClientResult from office365.runtime.client_value import ClientValue from office365.runtime.paths.resource_path import ResourcePath @@ -202,15 +204,15 @@ def start_co_auth(self): @property def canvas_content(self): + # type: () -> Optional[str] """ Gets the CanvasContent1 for the current Site Page. - - :rtype: str or None """ return self.properties.get("CanvasContent1", None) @canvas_content.setter def canvas_content(self, value): + # type: (str) -> None """ Sets the CanvasContent1 for the current Site Page. @@ -220,10 +222,9 @@ def canvas_content(self, value): @property def layout_web_parts_content(self): + # type: () -> Optional[str] """ Gets the LayoutWebPartsContent field for the current Site Page. - - :rtype: str or None """ return self.properties.get("LayoutWebpartsContent", None) @@ -231,13 +232,12 @@ def layout_web_parts_content(self): def layout_web_parts_content(self, value): """ Sets the LayoutWebPartsContent field for the current Site Page. - - :rtype: str or None """ self.set_property("LayoutWebpartsContent", value) @property def translations(self): + # type: () -> TranslationStatusCollection return self.properties.get( "Translations", TranslationStatusCollection( @@ -247,4 +247,5 @@ def translations(self): @property def entity_type_name(self): + # type: () -> str return "SP.Publishing.SitePage" diff --git a/office365/sharepoint/publishing/pages/service.py b/office365/sharepoint/publishing/pages/service.py index 816d26568..4a24300a0 100644 --- a/office365/sharepoint/publishing/pages/service.py +++ b/office365/sharepoint/publishing/pages/service.py @@ -22,6 +22,7 @@ def __init__(self, context, resource_path=None): @property def pages(self): + # type: () -> SitePageCollection """Gets the SitePageCollection for the current web.""" return self.properties.get( "pages", diff --git a/office365/sharepoint/pushnotifications/collection.py b/office365/sharepoint/pushnotifications/collection.py index 66c4724b1..5e00af7ae 100644 --- a/office365/sharepoint/pushnotifications/collection.py +++ b/office365/sharepoint/pushnotifications/collection.py @@ -3,7 +3,9 @@ from office365.sharepoint.pushnotifications.subscriber import PushNotificationSubscriber -class PushNotificationSubscriberCollection(EntityCollection): +class PushNotificationSubscriberCollection( + EntityCollection[PushNotificationSubscriber] +): """Specifies the collection of push notification subscribers for the site""" def __init__(self, context, resource_path=None): diff --git a/office365/sharepoint/recyclebin/item_collection.py b/office365/sharepoint/recyclebin/item_collection.py index dca5a7eb7..11623c93b 100644 --- a/office365/sharepoint/recyclebin/item_collection.py +++ b/office365/sharepoint/recyclebin/item_collection.py @@ -5,7 +5,7 @@ from office365.sharepoint.recyclebin.item import RecycleBinItem -class RecycleBinItemCollection(EntityCollection): +class RecycleBinItemCollection(EntityCollection[RecycleBinItem]): """Represents a collection of View resources.""" def __init__(self, context, resource_path=None): diff --git a/office365/sharepoint/search/service.py b/office365/sharepoint/search/service.py index 93a6893bd..31878d0ab 100644 --- a/office365/sharepoint/search/service.py +++ b/office365/sharepoint/search/service.py @@ -133,17 +133,19 @@ def query( "rankingModelId": ranking_model_id, "startRow": start_row, "rowsPerPage": rows_per_page, - "selectProperties": str(StringCollection(select_properties)), - "refinementFilters": str(StringCollection(refinement_filters)), - "refiners": str(StringCollection(refiners)), - "sortList": str(StringCollection([str(s) for s in sort_list])) - if sort_list - else None, "trimDuplicates": trim_duplicates, "rowLimit": row_limit, "enableQueryRules": enable_query_rules, "enableSorting": enable_sorting, } + if refinement_filters: + params["refinementFilters"] = str(StringCollection(refinement_filters)) + if sort_list: + params["sortList"] = str(StringCollection([str(s) for s in sort_list])) + if select_properties: + params["selectProperties"] = str(StringCollection(select_properties)) + if refiners: + params["refiners"] = str(StringCollection(refiners)) params.update(**kwargs) return_type = ClientResult( self.context, SearchResult() diff --git a/office365/sharepoint/sharing/object_sharing_information.py b/office365/sharepoint/sharing/object_sharing_information.py index 40c6f09b6..e53c54dcb 100644 --- a/office365/sharepoint/sharing/object_sharing_information.py +++ b/office365/sharepoint/sharing/object_sharing_information.py @@ -179,65 +179,63 @@ def get_list_item_sharing_information( @property def anonymous_edit_link(self): + # type: () -> Optional[str] """ Provides the URL that allows an anonymous user to edit the securable object. If such a URL is not available, this property will provide an empty string. - - :rtype: str """ return self.properties.get("AnonymousEditLink", None) @property def anonymous_view_link(self): + # type: () -> Optional[str] """ Provides the URL that allows an anonymous user to view the securable object. If such a URL is not available, this property will provide an empty string. - - :rtype: str """ return self.properties.get("AnonymousViewLink", None) @property def can_be_shared(self): + # type: () -> Optional[bool] """ Indicates whether the current securable object can be shared. - :rtype: bool """ return self.properties.get("CanBeShared", None) @property def can_be_unshared(self): + # type: () -> Optional[bool] """ Indicates whether the current securable object can be unshared. - :rtype: bool """ return self.properties.get("CanBeUnshared", None) @property def can_manage_permissions(self): + # type: () -> Optional[bool] """ Specifies whether the current user is allowed to change the permissions of the securable object. - :rtype: bool """ return self.properties.get("CanManagePermissions", None) @property def has_pending_access_requests(self): + # type: () -> Optional[bool] """ Provides information about whether there are any pending access requests for the securable object. This information is only provided if the current user has sufficient permissions to view any pending access requests. If the current user does not have such permissions, this property will return false. - :rtype: bool """ return self.properties.get("HasPendingAccessRequests", None) @property def has_permission_levels(self): + # type: () -> Optional[bool] """ Indicates whether the object sharing information contains permissions information in addition to the identities of the users who have access to the securable object. - :rtype: bool """ return self.properties.get("HasPermissionLevels", None) diff --git a/office365/sharepoint/userprofiles/my_site_links.py b/office365/sharepoint/userprofiles/my_site_links.py index 8cce10c1b..f0b4e5cda 100644 --- a/office365/sharepoint/userprofiles/my_site_links.py +++ b/office365/sharepoint/userprofiles/my_site_links.py @@ -1,3 +1,5 @@ +from typing import Optional + from office365.runtime.queries.service_operation import ServiceOperationQuery from office365.sharepoint.entity import Entity @@ -23,22 +25,20 @@ def get_my_site_links(context): @property def all_documents_link(self): + # type: () -> Optional[str] """ This property value is the URL of the user’s document library on their personal site. This property value is null if the user does not have a personal site or the user does not have a document library in their personal site. - - :rtype: str """ return self.properties.get("AllDocumentsLink", None) @property def all_sites_link(self): + # type: () -> Optional[str] """ This property value is the URL of the user’s followed sites view on their personal site. This property value is null if the user does not have a personal site or social features are not enabled on their personal site. - - :rtype: str """ return self.properties.get("AllSitesLink", None) diff --git a/office365/sharepoint/userprofiles/user_profile.py b/office365/sharepoint/userprofiles/user_profile.py index 75cc96f44..0fb634c66 100644 --- a/office365/sharepoint/userprofiles/user_profile.py +++ b/office365/sharepoint/userprofiles/user_profile.py @@ -1,3 +1,5 @@ +from typing import Optional + from office365.runtime.client_result import ClientResult from office365.runtime.paths.resource_path import ResourcePath from office365.runtime.queries.service_operation import ServiceOperationQuery @@ -12,41 +14,41 @@ class UserProfile(Entity): @property def account_name(self): + # type: () -> Optional[str] """ The account name of the user. - :rtype: str or None """ return self.properties.get("AccountName", None) @property def display_name(self): + # type: () -> Optional[str] """ The title of the user. - :rtype: str or None """ return self.properties.get("DisplayName", None) @property def my_site_host_url(self): + # type: () -> Optional[str] """ Specifies the URL for the personal site of the current user. - :rtype: str or None """ return self.properties.get("MySiteHostUrl", None) @property def public_url(self): + # type: () -> Optional[str] """ Specifies the public URL for the personal site of the current user. - :rtype: str or None """ return self.properties.get("PublicUrl", None) @property def url_to_create_personal_site(self): + # type: () -> Optional[str] """ The UrlToCreatePersonalSite property specifies the URL to allow the current user to create a personal site. - :rtype: str or None """ return self.properties.get("UrlToCreatePersonalSite", None) diff --git a/office365/sharepoint/webs/web.py b/office365/sharepoint/webs/web.py index 7ff5d0322..847a556c9 100644 --- a/office365/sharepoint/webs/web.py +++ b/office365/sharepoint/webs/web.py @@ -1,6 +1,6 @@ # coding=utf-8 import datetime -from typing import Optional +from typing import AnyStr, Optional from office365.runtime.client_result import ClientResult from office365.runtime.client_value_collection import ClientValueCollection @@ -1288,7 +1288,6 @@ def share( :param bool send_email: A flag to determine if an email notification SHOULD be sent (if email is configured). :param str email_subject: The email subject. :param str email_body: The email subject. - :rtype: SharingResult """ return_type = SharingResult(self.context) @@ -1332,11 +1331,7 @@ def _web_resolved(): return return_type def unshare(self): - """ - Removes Sharing permissions on a Web - - :rtype: SharingResult - """ + """Removes Sharing permissions on a Web""" return_type = SharingResult(self.context) def _web_initialized(): @@ -1605,12 +1600,12 @@ def get_file_by_id(self, unique_id): ) def get_list_item(self, str_url): + # type: (str) -> ListItem """ Returns the list item that is associated with the specified server-relative URL. :param str str_url: A string that contains the server-relative URL, for example, "/sites/MySite/Shared Documents/MyDocument.docx". - :return: ListItem """ return ListItem( self.context, @@ -1618,12 +1613,12 @@ def get_list_item(self, str_url): ) def get_list_item_using_path(self, decoded_url): + # type: (str) -> ListItem """ Returns the list item that is associated with the specified server-relative path. :param str decoded_url: A string that contains the server-relative path, for example, "/sites/MySite/Shared Documents/MyDocument.docx" or "Shared Documents/MyDocument.docx". - :return: ListItem """ params = SPResPath.create_relative(self.context.base_url, decoded_url) return ListItem( @@ -1642,6 +1637,7 @@ def get_catalog(self, type_catalog): ) def page_context_info(self, include_odb_settings, emit_navigation_info): + # type: (bool, bool) -> ClientResult[AnyStr] """ Return Page context info for the current list being rendered. @@ -1797,6 +1793,7 @@ def assign_document_id(self, site_prefix, enabled=True): @property def activities(self): + # type: () -> EntityCollection[SPActivityEntity] return self.properties.get( "Activities", EntityCollection( @@ -1818,22 +1815,21 @@ def activity_logger(self): @property def allow_rss_feeds(self): + # type: () -> Optional[bool] """ Gets a Boolean value that specifies whether the site collection allows RSS feeds. - :rtype: str """ return self.properties.get("AllowRssFeeds", None) @property def alternate_css_url(self): - """Gets the URL for an alternate cascading style sheet (CSS) to use in the website. - - :rtype: str - """ + # type: () -> Optional[str] + """Gets the URL for an alternate cascading style sheet (CSS) to use in the website.""" return self.properties.get("AlternateCssUrl", None) @property def app_instance_id(self): + # type: () -> Optional[str] """ Specifies the identifier of the app instance that this site (2) represents. If this site (2) does not represent an app instance, then this MUST specify an empty GUID. @@ -1851,31 +1847,25 @@ def author(self): @property def created(self): - """ - Specifies when the site was created. - """ + # type: () -> datetime.datetime + """Specifies when the site was created""" return self.properties.get("Created", datetime.datetime.min) @property def custom_master_url(self): - """Gets the URL for a custom master page to apply to the Web site - :rtype: str or None - """ + # type: () -> Optional[str] + """Gets the URL for a custom master page to apply to the Web site""" return self.properties.get("CustomMasterUrl", None) @property def custom_site_actions_disabled(self): - """ - :rtype: bool or None - """ + # type: () -> Optional[bool] return self.properties.get("CustomSiteActionsDisabled", None) @property def id(self): - """ - Specifies the site identifier for the site - :rtype: str - """ + # type: () -> Optional[str] + """Specifies the site identifier for the site""" return self.properties.get("Id", None) @property @@ -1888,32 +1878,30 @@ def access_requests_list(self): @property def access_request_list_url(self): - """ - :rtype: str or None - """ + # type: () -> Optional[str] return self.properties.get("AccessRequestListUrl", None) @property def allow_designer_for_current_user(self): + # type: () -> Optional[bool] """ Specifies whether the current user is allowed to use a designer application to customize this site - :rtype: bool or None """ return self.properties.get("AllowDesignerForCurrentUser", None) @property def allow_master_page_editing_for_current_user(self): + # type: () -> Optional[bool] """ Specifies whether the current user is allowed to edit the master page. - :rtype: bool or None """ return self.properties.get("AllowMasterPageEditingForCurrentUser", None) @property def allow_revert_from_template_for_current_user(self): + # type: () -> Optional[bool] """ Specifies whether the current user is allowed to revert the site (2) to a default site template. - :rtype: bool or None """ return self.properties.get("AllowRevertFromTemplateForCurrentUser", None) @@ -1926,6 +1914,7 @@ def effective_base_permissions(self): @property def enable_minimal_download(self): + # type: () -> Optional[bool] """ Specifies whether the site will use the minimal download strategy by default. @@ -1978,6 +1967,7 @@ def lists(self): @property def onedrive_shared_items(self): + # type: () -> EntityCollection[SharedDocumentInfo] """""" return self.properties.get( "OneDriveSharedItems", @@ -2030,6 +2020,7 @@ def parent_web(self): @property def associated_visitor_group(self): + # type: () -> Group """Gets or sets the associated visitor group of the Web site.""" return self.properties.get( "AssociatedVisitorGroup", @@ -2040,6 +2031,7 @@ def associated_visitor_group(self): @property def associated_owner_group(self): + # type: () -> Group """Gets or sets the associated owner group of the Web site.""" return self.properties.get( "AssociatedOwnerGroup", @@ -2050,6 +2042,7 @@ def associated_owner_group(self): @property def associated_member_group(self): + # type: () -> Group """Gets or sets the group of users who have been given contribute permissions to the Web site.""" return self.properties.get( "AssociatedMemberGroup", @@ -2212,10 +2205,8 @@ def mega_menu_enabled(self): @quick_launch_enabled.setter def quick_launch_enabled(self, value): - """Sets a value that specifies whether the Quick Launch area is enabled on the site. - - :type value: bool - """ + # type: (bool) -> None + """Sets a value that specifies whether the Quick Launch area is enabled on the site.""" self.set_property("QuickLaunchEnabled", value) @property @@ -2243,10 +2234,9 @@ def is_multilingual(self): @is_multilingual.setter def is_multilingual(self, val): + # type: (bool) -> None """ Sets whether Multilingual UI is turned on for this web or not. - - :type val: bool """ self.set_property("IsMultilingual", val) @@ -2344,6 +2334,7 @@ def available_fields(self): @property def available_content_types(self): + # type: () -> ContentTypeCollection """ Specifies the collection of all site content types that apply to the current scope, including those of the current site (2), as well as any parent sites. @@ -2367,19 +2358,14 @@ def site_user_info_list(self): @property def title(self): - """Gets the title of the web. - - :rtype: str or None - """ + # type: () -> Optional[str] + """Gets the title of the web.""" return self.properties.get("Title", None) @property def welcome_page(self): - """ - Specifies the URL of the Welcome page for the site - - :rtype: str or None - """ + # type: () -> Optional[str] + """Specifies the URL of the Welcome page for the site""" return self.properties.get("WelcomePage", None) @property @@ -2389,11 +2375,8 @@ def supported_ui_language_ids(self): @property def ui_version(self): - """ - Gets or sets the user interface (UI) version of the Web site. - - :rtype: int or None - """ + # type: () -> Optional[int] + """Gets or sets the user interface (UI) version of the Web site.""" return self.properties.get("UIVersion", None) @property @@ -2415,6 +2398,7 @@ def server_relative_path(self): @property def syndication_enabled(self): + # type: () -> Optional[bool] """Specifies whether the [RSS2.0] feeds are enabled on the site""" return self.properties.get("SyndicationEnabled", None) @@ -2430,6 +2414,7 @@ def title_resource(self): @property def treeview_enabled(self): + # type: () -> Optional[bool] """Specifies whether the tree view is enabled on the site""" return self.properties.get("TreeViewEnabled", None) diff --git a/tests/outlook/test_messages.py b/tests/outlook/test_messages.py index 927d9c61c..f1102f8a6 100644 --- a/tests/outlook/test_messages.py +++ b/tests/outlook/test_messages.py @@ -22,8 +22,7 @@ def test3_send_message(self): message.to_recipients.add(Recipient.from_email(test_user_principal_name)) message.to_recipients.add(Recipient.from_email(test_user_principal_name_alt)) message.body = "The new cafeteria is open." - message.update() - message.send().execute_query() + message.update().send().execute_query() # def test4_create_reply(self): # message = self.__class__.target_message.create_reply().execute_query() @@ -51,12 +50,13 @@ def test_8_create_draft_message_with_attachments(self): content = base64.b64encode( io.BytesIO(b"This is some file content").read() ).decode() + draft = ( self.client.me.messages.add( subject="Check out this attachment", body="The new cafeteria is open." ) - .add_file_attachment("test.txt", "Hello World!") - .add_file_attachment("test2.txt", base64_content=content) + .add_file_attachment("TextAttachment.txt", "Hello World!") + .add_file_attachment("BinaryAttachment.txt", base64_content=content) .execute_query() ) assert (