Skip to content

Commit 352c94e

Browse files
vvgrem@gmail.comvvgrem@gmail.com
vvgrem@gmail.com
authored and
vvgrem@gmail.com
committed
SharePoint API: introduced SearchService API support, bug fixes (#210 and #213)
1 parent 45f90b2 commit 352c94e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+507
-135
lines changed

.travis.yml

-1
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,3 @@ jobs:
3434
tags: true
3535
python: 3.6
3636
repo: "vgrem/Office365-REST-Python-Client"
37-
twine_version: 3.1.1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
<AppPermissionRequests AllowAppOnlyPolicy="true">
2-
<AppPermissionRequest Scope="http://sharepoint/content/sitecollection/web" Right="Read"/>
2+
<AppPermissionRequest Scope="http://sharepoint/content/sitecollection" Right="FullControl" />
3+
<AppPermissionRequest Scope="http://sharepoint/search" Right="QueryAsUserIgnoreAppPrincipal"/>
34
</AppPermissionRequests>

examples/sharepoint/connect_with_app.py

+7-5
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22
from office365.sharepoint.client_context import ClientContext
33
from settings import settings
44

5-
ctx = ClientContext.connect_with_credentials(settings['url'],
6-
ClientCredential(settings['client_credentials']['client_id'],
7-
settings['client_credentials']['client_secret']))
5+
credentials = ClientCredential(settings['client_credentials']['client_id'],
6+
settings['client_credentials']['client_secret'])
7+
ctx = ClientContext(settings['url']).with_credentials(credentials)
8+
if not ctx.authentication_context.acquire_token():
9+
print("Acquire token failed")
10+
811

912
target_web = ctx.web
1013
ctx.load(target_web)
1114
ctx.execute_query()
12-
13-
15+
print(target_web.url)

examples/sharepoint/requirements.txt

Whitespace-only changes.

generator/generate.py

+4-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import ast
2-
1+
from generator.typeBuilder import TypeBuilder
32
from office365.runtime.odata.odata_v3_reader import ODataV3Reader
43
from office365.runtime.odata.odata_v4_reader import ODataV4Reader
54

@@ -10,9 +9,9 @@ def generate_files(model):
109

1110

1211
def generate_type_file(type_schema):
13-
if type_schema['state'] == 'attached':
14-
tree = ast.parse(open(type_schema['file']).read())
15-
print(type_schema['file'])
12+
builder = TypeBuilder(type_schema)
13+
if builder.build():
14+
builder.save()
1615

1716

1817
def generate_sharepoint_model():

generator/metadata/SharePoint.xml

+1-1
Large diffs are not rendered by default.

generator/templates/__init__.py

Whitespace-only changes.

generator/typeBuilder.py

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import ast
2+
import astunparse
3+
4+
5+
class TypeBuilder(object):
6+
7+
def __init__(self, schema):
8+
self._schema = schema
9+
self._node = None
10+
11+
def build(self):
12+
if self._schema['state'] == 'attached':
13+
with open(self._schema['file']) as f:
14+
self._node = ast.parse(f.read())
15+
return True
16+
return False
17+
18+
def save(self):
19+
code = astunparse.unparse(self._node)
20+
with open(self._schema['file']) as f:
21+
f.write(code)

office365/outlookservices/outlook_client.py

+25-4
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,38 @@
1010

1111
class OutlookClient(ClientRuntimeContext):
1212

13-
def __init__(self, ctx_auth):
13+
def __init__(self, auth_context):
1414
"""
1515
Office365 Outlook client context
1616
Status: deprecated, prefer GraphClient instead
1717
18-
:type ctx_auth: AuthenticationContext
18+
:type auth_context: AuthenticationContext
1919
"""
20-
self.__service_root_url = "https://outlook.office365.com/api/v1.0/"
21-
super(OutlookClient, self).__init__(self.__service_root_url, ctx_auth)
20+
self._resource = "https://outlook.office365.com"
21+
self.__service_root_url = "{resource}/api/v1.0/".format(resource=self._resource)
22+
super(OutlookClient, self).__init__(self.__service_root_url, auth_context)
2223
self._pendingRequest = ODataRequest(self, V4JsonFormat("minimal"))
2324
self._pendingRequest.beforeExecute += self._build_specific_query
25+
self._token_parameters = None
26+
27+
@classmethod
28+
def from_tenant(cls, tenant):
29+
return OutlookClient(AuthenticationContext(tenant))
30+
31+
def with_user_credentials(self, client_id, user_name, password):
32+
self._token_parameters = {
33+
"client_id": client_id,
34+
"username": user_name,
35+
"password": password,
36+
"resource": self._resource,
37+
"scope": ("openid", "profile", "offline_access")
38+
}
39+
return self
40+
41+
def authenticate_request(self, request):
42+
if not self._auth_context.is_authenticated:
43+
self._auth_context.acquire_token_password_grant(**self._token_parameters)
44+
super(OutlookClient, self).authenticate_request(request)
2445

2546
def get_pending_request(self):
2647
return self._pendingRequest

office365/runtime/auth/authentication_context.py

+19-8
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class AuthenticationContext(BaseAuthenticationContext):
1212

1313
def __init__(self, url, credentials=None):
1414
"""
15-
Authentication context for SharePoint Online/One Drive
15+
Authentication context for SharePoint Online/OneDrive
1616
1717
:param str url: authority url
1818
:param ClientCredential or UserCredential credentials: credentials
@@ -33,9 +33,9 @@ def set_token(self, token):
3333

3434
def acquire_token(self):
3535
if isinstance(self.credentials, ClientCredential):
36-
self.acquire_token_for_app(self.credentials.clientId, self.credentials.clientSecret)
36+
return self.acquire_token_for_app(self.credentials.clientId, self.credentials.clientSecret)
3737
elif isinstance(self.credentials, UserCredential):
38-
self.acquire_token_for_user(self.credentials.userName, self.credentials.password)
38+
return self.acquire_token_for_user(self.credentials.userName, self.credentials.password)
3939
else:
4040
raise ValueError("Unknown credential type")
4141

@@ -48,19 +48,30 @@ def acquire_token_for_user(self, username, password):
4848
self.provider = SamlTokenProvider(self.url, username, password)
4949
if not self.provider.acquire_token():
5050
raise ValueError('Acquire token failed: {0}'.format(self.provider.error))
51+
return True
5152

5253
def acquire_token_for_app(self, client_id, client_secret):
5354
"""Acquire token via client credentials (SharePoint App Principal)"""
5455
self.provider = ACSTokenProvider(self.url, client_id, client_secret)
5556
if not self.provider.acquire_token():
5657
raise ValueError('Acquire token failed: {0}'.format(self.provider.error))
58+
return True
5759

58-
def acquire_token_password_grant(self, client_credentials, user_credentials):
59-
"""Acquire token via resource owner password credential (ROPC) grant"""
60+
def acquire_token_password_grant(self, client_id, username, password, resource, scope):
61+
"""
62+
Acquire token via resource owner password credential (ROPC) grant
63+
64+
:param str resource: A URI that identifies the resource for which the token is valid.
65+
:param str username: : The username of the user on behalf this application is authenticating.
66+
:param str password: The password of the user named in the username parameter.
67+
:param str client_id: str The OAuth client id of the calling application.
68+
:param list[str] scope:
69+
"""
6070
self.provider = OAuthTokenProvider(self.url)
61-
return self.provider.acquire_token_password_type("https://outlook.office365.com",
62-
client_credentials,
63-
user_credentials)
71+
return self.provider.acquire_token_password_type(resource=resource,
72+
client_id=client_id,
73+
user_credentials=UserCredential(username, password),
74+
scope=scope)
6475

6576
def authenticate_request(self, request_options):
6677
"""Authenticate request
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,28 @@
11
import requests
22
from office365.runtime.auth.base_token_provider import BaseTokenProvider
33
from office365.runtime.auth.tokenResponse import TokenResponse
4+
from office365.runtime.auth.userCredential import UserCredential
45

56

67
class OAuthTokenProvider(BaseTokenProvider):
78
""" OAuth token provider for AAD"""
89

910
def __init__(self, tenant):
10-
self.tenant = tenant
11-
self.authority_url = "https://login.microsoftonline.com/"
11+
self.authority_url = "https://login.microsoftonline.com/{tenant}".format(tenant=tenant)
1212
self.version = "v1.0"
1313
self.error = None
14-
self.token = None # type: TokenResponse
14+
self.token = TokenResponse()
1515

1616
def acquire_token(self, parameters):
1717
try:
18-
url = "https://login.microsoftonline.com/{0}/oauth2/token".format(self.tenant)
19-
response = requests.post(url=url,
18+
token_url = "{authority}/oauth2/token".format(authority=self.authority_url)
19+
response = requests.post(url=token_url,
2020
headers={'Content-Type': 'application/x-www-form-urlencoded'},
2121
data=parameters)
2222
self.token = TokenResponse.from_json(response.content)
2323
return self.token.is_valid
2424
except requests.exceptions.RequestException as e:
25-
self.error = "Error: {}".format(e)
25+
self.error = "Error: {0}".format(e)
2626
return False
2727

2828
def is_authenticated(self):
@@ -31,17 +31,25 @@ def is_authenticated(self):
3131
def get_authorization_header(self):
3232
return '{token_type} {access_token}'.format(token_type=self.token.tokenType, access_token=self.token.accessToken)
3333

34-
def acquire_token_password_type(self, resource, client_credentials, user_credentials):
34+
def acquire_token_password_type(self, resource, client_id, user_credentials, scope):
35+
"""
36+
Gets a token for a given resource via user credentials
37+
38+
:param list[str] scope:
39+
:param str resource:
40+
:param str client_id:
41+
:param UserCredential user_credentials:
42+
:return: bool
43+
"""
3544
token_parameters = {
3645
'grant_type': 'password',
37-
'client_id': client_credentials['client_id'],
38-
'client_secret': client_credentials['client_secret'],
39-
'username': user_credentials['username'],
40-
'password': user_credentials['password'],
41-
'scope': 'user.read openid profile offline_access',
46+
'client_id': client_id,
47+
'username': user_credentials.userName,
48+
'password': user_credentials.password,
49+
'scope': " ".join(scope),
4250
'resource': resource
4351
}
44-
self.acquire_token(token_parameters)
52+
return self.acquire_token(token_parameters)
4553

4654
def get_last_error(self):
4755
return self.error

office365/runtime/auth/providers/saml_token_provider.py

+12-9
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ def acquire_service_token_from_adfs(self, adfs_url, username, password):
120120
logger.error(self.error)
121121
return None
122122
# 2. prepare & submit token request
123-
self.__sts_profile.signInPage = '_vti_bin/idcrl.svc'
123+
self.__sts_profile.signInPage = '_vti_bin/idcrl.svc/'
124124
self.__sts_profile.securityTokenServicePath = 'rst2.srf'
125125
template = self._prepare_request_from_template('RST2.xml', {
126126
'auth_url': self.__sts_profile.authorityUrl,
@@ -198,7 +198,11 @@ def _process_service_token_response(self, response):
198198
return token.text
199199

200200
def _acquire_authentication_cookie(self, security_token, federated=False):
201-
"""Retrieve auth cookie from STS"""
201+
"""Retrieve auth cookie from STS
202+
203+
:type federated: bool
204+
:type security_token: str
205+
"""
202206
logger = self.logger(self._acquire_authentication_cookie.__name__)
203207
session = requests.session()
204208
logger.debug_secrets("session: %s\nsession.post(%s, data=%s)", session, self.__sts_profile.signin_page_url,
@@ -210,13 +214,12 @@ def _acquire_authentication_cookie(self, security_token, federated=False):
210214
headers={'Content-Type': 'application/x-www-form-urlencoded'})
211215
else:
212216
self._auth_cookies['SPOIDCRL'] = None
213-
session.head(self.__sts_profile.signin_page_url,
214-
headers={
215-
'User-Agent': 'Office365 Python Client',
216-
'X-IDCRL_ACCEPTED': 't',
217-
'Authorization': 'BPOSIDCRL {0}'.format(security_token),
218-
'Content-Type': 'application/x-www-form-urlencoded'
219-
})
217+
session.get(self.__sts_profile.signin_page_url,
218+
headers={
219+
'User-Agent': 'Office365 Python Client',
220+
'X-IDCRL_ACCEPTED': 't',
221+
'Authorization': 'BPOSIDCRL {0}'.format(security_token)
222+
})
220223
logger.debug_secrets("session.cookies: %s", session.cookies)
221224
cookies = requests.utils.dict_from_cookiejar(session.cookies)
222225
logger.debug_secrets("cookies: %s", cookies)

office365/runtime/client_value_object.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,18 @@ def __init__(self):
55
super(ClientValueObject, self).__init__()
66

77
def set_property(self, k, v, persist_changes=True):
8-
self.__dict__[k] = v
8+
if hasattr(self, k):
9+
prop_type = getattr(self, k)
10+
if isinstance(prop_type, ClientValueObject):
11+
[prop_type.set_property(k, v, persist_changes) for k, v in v.items()]
12+
setattr(self, k, prop_type)
13+
else:
14+
setattr(self, k, v)
15+
else:
16+
setattr(self, k, v)
917

1018
def get_property(self, k):
11-
return self.__dict__[k]
19+
return getattr(self, k)
1220

1321
def to_json(self):
1422
return dict((k, v) for k, v in vars(self).items() if v is not None)

office365/sharepoint/field.py

+16
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,27 @@
11
from office365.runtime.client_query import DeleteEntityQuery
22
from office365.runtime.resource_path_service_operation import ResourcePathServiceOperation
3+
from office365.runtime.serviceOperationQuery import ServiceOperationQuery
34
from office365.sharepoint.base_entity import BaseEntity
45

56

67
class Field(BaseEntity):
78
"""Represents a field in a SharePoint Web site"""
89

10+
def set_show_in_display_form(self, flag):
11+
"""Sets the value of the ShowInDisplayForm property for this field."""
12+
qry = ServiceOperationQuery(self, "setShowInDisplayForm", [flag])
13+
self.context.add_query(qry)
14+
15+
def set_show_in_edit_form(self, flag):
16+
"""Sets the value of the ShowInEditForm property for this field."""
17+
qry = ServiceOperationQuery(self, "setShowInEditForm", [flag])
18+
self.context.add_query(qry)
19+
20+
def set_show_in_new_form(self, flag):
21+
"""Sets the value of the ShowInNewForm property for this field."""
22+
qry = ServiceOperationQuery(self, "setShowInNewForm", [flag])
23+
self.context.add_query(qry)
24+
925
def delete_object(self):
1026
"""Deletes the field."""
1127
qry = DeleteEntityQuery(self)
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from office365.runtime.client_value_object import ClientValueObject
2+
3+
4+
class FieldLookupValue(ClientValueObject):
5+
6+
def __init__(self, lookup_id=None, lookup_value=None):
7+
"""Specifies the value of a lookup for a field within a list item.
8+
9+
:param int or None lookup_id: Gets or sets the identifier (ID) of the list item that this instance of the lookup
10+
field is referring to.
11+
:param str or None lookup_value: Gets a summary of the list item that this instance
12+
of the lookup field is referring to.
13+
14+
"""
15+
super().__init__()
16+
self.LookupId = lookup_id
17+
self.LookupValue = lookup_value

office365/sharepoint/fieldUrl.py

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from office365.sharepoint.field import Field
2+
3+
4+
class FieldUrl(Field):
5+
"""Specifies a field that contains a URL."""
6+
pass
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from office365.sharepoint.fieldLookupValue import FieldLookupValue
2+
3+
4+
class FieldUserValue(FieldLookupValue):
5+
6+
def __init__(self, user_id):
7+
"""Represents the value of a user field for a list item."""
8+
super().__init__(user_id)
9+
10+
@staticmethod
11+
def from_user(user):
12+
pass
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from office365.runtime.client_value_object import ClientValueObject
2+
3+
4+
class CustomResult(ClientValueObject):
5+
pass

office365/sharepoint/search/query/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from office365.runtime.client_value_object import ClientValueObject
2+
3+
4+
class QuerySuggestionResults(ClientValueObject):
5+
pass
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from office365.runtime.client_value_object import ClientValueObject
2+
3+
4+
class QueryResult(ClientValueObject):
5+
pass

0 commit comments

Comments
 (0)