Skip to content

Commit 37e3b29

Browse files
committed
AttachmentCollection.download method introduced, model updates
1 parent b2d4802 commit 37e3b29

File tree

18 files changed

+188
-45
lines changed

18 files changed

+188
-45
lines changed

.github/workflows/python-app.yml

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,3 @@ jobs:
6767
run: |
6868
echo "${{env.office365_python_sdk_securevars}}"
6969
pytest
70-
71-
# - name: Lint with flake8
72-
# run: |
73-
# # stop the build if there are Python syntax errors or undefined names
74-
# flake8 office365 --count --select=E9,F63,F7,F82 --show-source --statistics
75-
# # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
76-
# flake8 office365 --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,28 @@
1+
"""
2+
Demonstrates how to download list item attachments
3+
"""
14
import os
25
import tempfile
36

7+
from office365.sharepoint.attachments.attachment import Attachment
48
from office365.sharepoint.client_context import ClientContext
59
from tests import test_client_credentials, test_team_site_url
610

7-
ctx = ClientContext(test_team_site_url).with_credentials(test_client_credentials)
811

9-
download_path = tempfile.mkdtemp()
12+
def print_progress(attachment_file):
13+
# type: (Attachment) -> None
14+
print(
15+
"{0} has been downloaded".format(attachment_file.server_relative_url)
16+
)
17+
18+
19+
ctx = ClientContext(test_team_site_url).with_credentials(test_client_credentials)
1020

1121
list_title = "Company Tasks"
1222
source_list = ctx.web.lists.get_by_title(list_title)
13-
items = source_list.items
14-
ctx.load(items, ["ID", "UniqueId", "FileRef", "LinkFilename", "Title", "Attachments"])
15-
ctx.execute_query()
23+
items = source_list.items.get().execute_query()
1624
for item in items:
17-
if item.properties[
18-
"Attachments"
19-
]: # 1. determine whether ListItem contains attachments
20-
# 2. Explicitly load attachments for ListItem
21-
attachment_files = item.attachment_files.get().execute_query()
22-
# 3. Enumerate and save attachments
23-
for attachment_file in attachment_files:
24-
download_file_name = os.path.join(
25-
download_path, os.path.basename(attachment_file.file_name)
26-
)
27-
with open(download_file_name, "wb") as fh:
28-
attachment_file.download(fh).execute_query()
29-
print(
30-
f"{attachment_file.server_relative_url} has been downloaded into {download_file_name}"
31-
)
25+
zip_path = os.path.join(tempfile.mkdtemp(), "attachments_{0}.zip".format(item.id))
26+
with open(zip_path, "wb") as f:
27+
item.attachment_files.download(f, print_progress).execute_query()
28+
print("{0} attachments has been downloaded...".format(zip_path))
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""
2+
Demonstrates how to download list item attachments
3+
"""
4+
import os
5+
import tempfile
6+
7+
from office365.sharepoint.client_context import ClientContext
8+
from tests import test_client_credentials, test_team_site_url
9+
10+
download_path = tempfile.mkdtemp()
11+
list_title = "Company Tasks"
12+
13+
ctx = ClientContext(test_team_site_url).with_credentials(test_client_credentials)
14+
source_list = ctx.web.lists.get_by_title(list_title)
15+
items = source_list.items.get().execute_query()
16+
for item in items:
17+
attachment_files = item.attachment_files.get().execute_query()
18+
for attachment_file in attachment_files:
19+
download_file_name = os.path.join(
20+
download_path, os.path.basename(attachment_file.file_name)
21+
)
22+
with open(download_file_name, "wb") as fh:
23+
attachment_file.download(fh).execute_query()
24+
print(
25+
f"{attachment_file.server_relative_url} has been downloaded into {download_file_name}"
26+
)
27+

office365/directory/identitygovernance/appconsent/request.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1+
from typing import Optional
2+
3+
from office365.directory.identitygovernance.appconsent.request_scope import (
4+
AppConsentRequestScope,
5+
)
16
from office365.directory.identitygovernance.userconsent.request_collection import (
27
UserConsentRequestCollection,
38
)
49
from office365.entity import Entity
10+
from office365.runtime.client_value_collection import ClientValueCollection
511
from office365.runtime.paths.resource_path import ResourcePath
612

713

@@ -15,6 +21,32 @@ class AppConsentRequest(Entity):
1521
the admin consent workflow is enabled.
1622
"""
1723

24+
def __str__(self):
25+
return self.app_display_name or self.entity_type_name
26+
27+
def __repr__(self):
28+
return self.app_id or self.entity_type_name
29+
30+
@property
31+
def app_display_name(self):
32+
# type: () -> Optional[str]
33+
"""Display name of the application object on which this extension property is defined. Read-only"""
34+
return self.properties.get("appDisplayName", None)
35+
36+
@property
37+
def app_id(self):
38+
# type: () -> Optional[str]
39+
"""The identifier of the application"""
40+
return self.properties.get("appId", None)
41+
42+
@property
43+
def pending_scopes(self):
44+
# type: () -> ClientValueCollection[AppConsentRequestScope]
45+
"""A list of pending scopes waiting for approval. Required."""
46+
return self.properties.get(
47+
"pendingScopes", ClientValueCollection(AppConsentRequestScope)
48+
)
49+
1850
@property
1951
def user_consent_requests(self):
2052
"""A list of pending user consent requests."""
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from office365.runtime.client_value import ClientValue
2+
3+
4+
class AppConsentRequestScope(ClientValue):
5+
"""The appConsentRequestScope details the dynamic permission scopes for which access is being requested."""
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from office365.entity import Entity
2+
3+
4+
class PrivilegedAccessGroup(Entity):
5+
"""The entry point for all resources related to Privileged Identity Management (PIM) for groups."""
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
1+
from office365.directory.identitygovernance.privilegedaccess.group import (
2+
PrivilegedAccessGroup,
3+
)
14
from office365.entity import Entity
5+
from office365.runtime.paths.resource_path import ResourcePath
26

37

48
class PrivilegedAccessRoot(Entity):
59
"""Represents the entry point for resources related to Privileged Identity Management (PIM)."""
10+
11+
@property
12+
def group(self):
13+
"""A list of pending user consent requests."""
14+
return self.properties.get(
15+
"group",
16+
PrivilegedAccessGroup(
17+
self.context, ResourcePath("group", self.resource_path)
18+
),
19+
)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from office365.entity import Entity
2+
3+
4+
class PrivilegedAccessScheduleInstance(Entity):
5+
"""An abstract type that exposes properties relating to the instances of membership and ownership assignments
6+
and eligibilities to groups that are governed by PIM"""
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from office365.entity import Entity
2+
3+
4+
class AdminConsentRequestPolicy(Entity):
5+
"""
6+
Represents the policy for enabling or disabling the Microsoft Entra admin consent workflow.
7+
The admin consent workflow allows users to request access for apps that they wish to use and that require admin
8+
authorization before users can use the apps to access organizational data.
9+
There is a single adminConsentRequestPolicy per tenant.
10+
"""

office365/directory/policies/root.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from office365.directory.policies.admin_consent_request import AdminConsentRequestPolicy
12
from office365.directory.policies.app_management import AppManagementPolicy
23
from office365.directory.policies.authentication_flows import AuthenticationFlowsPolicy
34
from office365.directory.policies.authentication_methods import (
@@ -20,6 +21,19 @@
2021
class PolicyRoot(Entity):
2122
"""Resource type exposing navigation properties for the policies singleton."""
2223

24+
@property
25+
def admin_consent_request_policy(self):
26+
"""
27+
The policy by which consent requests are created and managed for the entire tenant.
28+
"""
29+
return self.properties.get(
30+
"adminConsentRequestPolicy",
31+
AdminConsentRequestPolicy(
32+
self.context,
33+
ResourcePath("adminConsentRequestPolicy", self.resource_path),
34+
),
35+
)
36+
2337
@property
2438
def authentication_methods_policy(self):
2539
"""
@@ -151,6 +165,7 @@ def conditional_access_policies(self):
151165
def get_property(self, name, default_value=None):
152166
if default_value is None:
153167
property_mapping = {
168+
"adminConsentRequestPolicy": self.admin_consent_request_policy,
154169
"authenticationStrengthPolicies": self.authentication_strength_policies,
155170
"authenticationFlowsPolicy": self.authentication_flows_policy,
156171
"appManagementPolicies": self.app_management_policies,

office365/directory/synchronization/status.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11
from office365.directory.synchronization.progress import SynchronizationProgress
22
from office365.directory.synchronization.quarantine import SynchronizationQuarantine
3-
from office365.directory.synchronization.task_execution import SynchronizationTaskExecution
3+
from office365.directory.synchronization.task_execution import (
4+
SynchronizationTaskExecution,
5+
)
46
from office365.runtime.client_value import ClientValue
57
from office365.runtime.client_value_collection import ClientValueCollection
68

79

810
class SynchronizationStatus(ClientValue):
911
"""Represents the current status of the synchronizationJob."""
1012

11-
def __init__(self, progress=None, quarantine=SynchronizationQuarantine(),
12-
last_execution=SynchronizationTaskExecution(),
13-
last_successful_execution=SynchronizationTaskExecution(),
14-
last_successful_execution_with_exports=SynchronizationTaskExecution()):
13+
def __init__(
14+
self,
15+
progress=None,
16+
quarantine=SynchronizationQuarantine(),
17+
last_execution=SynchronizationTaskExecution(),
18+
last_successful_execution=SynchronizationTaskExecution(),
19+
last_successful_execution_with_exports=SynchronizationTaskExecution(),
20+
):
1521
"""
1622
:param list[SynchronizationProgress] progress: Details of the progress of a job toward completion.
1723
:param SynchronizationQuarantine quarantine:

office365/graph_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ def with_certificate(
127127
"thumbprint": thumbprint,
128128
"private_key": private_key,
129129
},
130-
token_cache=token_cache # Default cache is in memory only.
130+
token_cache=token_cache, # Default cache is in memory only.
131131
# You can learn how to use SerializableTokenCache from
132132
# https://msal-python.readthedocs.io/en/latest/#msal.SerializableTokenCache
133133
)

office365/onedrive/driveitems/driveItem.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -618,9 +618,9 @@ def invite(
618618
DriveRecipient, [DriveRecipient.from_email(r) for r in recipients]
619619
),
620620
"message": message,
621-
"expirationDateTime": expiration_datetime.isoformat() + "Z"
622-
if expiration_datetime
623-
else None,
621+
"expirationDateTime": (
622+
expiration_datetime.isoformat() + "Z" if expiration_datetime else None
623+
),
624624
"password": password,
625625
"retainInheritedPermissions": retain_inherited_permissions,
626626
}

office365/runtime/auth/providers/saml_token_provider.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -281,9 +281,9 @@ def _get_authentication_cookie(self, security_token, federated=False):
281281
if not federated or self._browser_mode:
282282
headers = {"Content-Type": "application/x-www-form-urlencoded"}
283283
if self._browser_mode:
284-
headers[
285-
"User-Agent"
286-
] = "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)"
284+
headers["User-Agent"] = (
285+
"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)"
286+
)
287287
session.post(
288288
self._sts_profile.signin_page_url, data=security_token, headers=headers
289289
)

office365/sharepoint/attachments/attachment.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import AnyStr, Optional
22

33
from office365.runtime.client_result import ClientResult
4+
from office365.runtime.queries.function import FunctionQuery
45
from office365.runtime.queries.service_operation import ServiceOperationQuery
56
from office365.sharepoint.entity import Entity
67
from office365.sharepoint.internal.queries.upload_file import create_upload_file_query
@@ -42,6 +43,14 @@ def _download_file_by_url():
4243
self.ensure_property("ServerRelativeUrl", _download_file_by_url)
4344
return self
4445

46+
def get_content(self):
47+
# type: () -> ClientResult[AnyStr]
48+
"""Gets the raw contents of attachment"""
49+
return_type = ClientResult(self.context)
50+
qry = FunctionQuery(self, "$value", None, return_type)
51+
self.context.add_query(qry)
52+
return return_type
53+
4554
def recycle_object(self):
4655
"""Move this attachment to site recycle bin."""
4756
qry = ServiceOperationQuery(self, "RecycleObject")

office365/sharepoint/attachments/collection.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import os
2-
from typing import IO, AnyStr
2+
from functools import partial
3+
from typing import IO, AnyStr, Optional, Callable
34

5+
from typing_extensions import Self
6+
7+
from office365.runtime.client_result import ClientResult
48
from office365.runtime.paths.service_operation import ServiceOperationPath
59
from office365.runtime.queries.service_operation import ServiceOperationQuery
610
from office365.sharepoint.attachments.attachment import Attachment
@@ -74,6 +78,27 @@ def _delete_all(return_type):
7478
self.get().after_execute(_delete_all)
7579
return self
7680

81+
def download(self, download_file, file_downloaded=None):
82+
# type: (IO, Optional[Callable[[], None]]) -> Self
83+
"""Downloads attachments as a zip file"""
84+
import zipfile
85+
86+
def _file_downloaded(attachment_file, result):
87+
# type: (Attachment, ClientResult[AnyStr]) -> None
88+
with zipfile.ZipFile(download_file.name, "a", zipfile.ZIP_DEFLATED) as zf:
89+
zf.writestr(attachment_file.file_name, result.value)
90+
if callable(file_downloaded):
91+
file_downloaded(attachment_file)
92+
93+
def _download(return_type):
94+
for attachment_file in return_type:
95+
attachment_file.get_content().after_execute(
96+
partial(_file_downloaded, attachment_file)
97+
)
98+
99+
self.get().after_execute(_download)
100+
return self
101+
77102
def upload(self, file, use_path=True):
78103
# type: (IO, bool) -> Attachment
79104
"""

office365/sharepoint/request.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,10 @@ def with_credentials(self, credentials, environment="commercial"):
3030
:param str environment: The Office 365 Cloud Environment endpoint used for authentication
3131
defaults to 'commercial'.
3232
"""
33-
self._auth_context.with_credentials(
34-
credentials, environment=environment
35-
)
33+
self._auth_context.with_credentials(credentials, environment=environment)
3634
return self
3735

3836
def _authenticate_request(self, request):
3937
# type: (RequestOptions) -> None
4038
"""Authenticate request"""
4139
self._auth_context.authenticate_request(request)
42-

office365/sharepoint/sharing/permission_collection.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ def __init__(
2929
only if the caller is an Auditor.
3030
:param int total_number_of_principals:
3131
"""
32-
self.appConsentPrincipals = ClientValueCollection(PrincipalInfo, app_consent_principals)
32+
self.appConsentPrincipals = ClientValueCollection(
33+
PrincipalInfo, app_consent_principals
34+
)
3335
self.hasInheritedLinks = has_inherited_links
3436
self.links = ClientValueCollection(LinkInfo, links)
3537
self.principals = ClientValueCollection(PrincipalInfo, principals)

0 commit comments

Comments
 (0)