sharepoint-v1-api 0.2.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1101 @@
1
+ import sys
2
+ import os
3
+ import getpass
4
+ import json
5
+ import requests
6
+ from requests_ntlm import HttpNtlmAuth
7
+ from typing import List
8
+
9
+ from .SharePointUser import SharePointUser
10
+ from .SharePointUserList import SharePointUserList
11
+ from .SharePointListItem import SharePointListItem, SharepointSiteCase
12
+ from .SharePointList import SharePointList, CasesList, TimeRegistrationList
13
+ from .SharePointLists import SharePointLists
14
+
15
+
16
+ class SharePointAPI:
17
+ """High‑level client for interacting with SharePoint sites.
18
+
19
+ Provides methods for authentication, list management, file operations,
20
+ and time‑registration handling. All public interactions should be performed
21
+ through an instance created via :meth:`_compact_init`.
22
+ """
23
+
24
+ @classmethod
25
+ def _compact_init(cls, credentials: dict):
26
+ """
27
+ Initialise a :class:`SharePointAPI` instance from a credentials dictionary.
28
+
29
+ Parameters
30
+ ----------
31
+ credentials : dict
32
+ Mapping containing the keys ``username``, ``password`` and ``sharepoint_url``.
33
+ An optional ``proxies`` key may be supplied to route HTTP requests through a proxy.
34
+
35
+ Returns
36
+ -------
37
+ SharePointAPI
38
+ A fully‑initialised ``SharePointAPI`` object ready for use.
39
+
40
+ Notes
41
+ -----
42
+ This method creates a new instance without invoking ``__init__`` directly,
43
+ then calls ``__init__`` with the extracted values. The stored credentials
44
+ are later used for NTLM authentication in all API calls.
45
+ """
46
+
47
+ username = credentials['username']
48
+ password = credentials['password']
49
+ sharepoint_url = credentials['sharepoint_url']
50
+ proxies = {
51
+ } if 'proxies' not in credentials else credentials['proxies']
52
+
53
+ # Create a new instance without calling __init__ directly.
54
+ sharepoint_api = object.__new__(SharePointAPI)
55
+ # Initialise the instance with the provided credentials.
56
+ sharepoint_api.__init__(username, password, sharepoint_url, proxies)
57
+
58
+ return sharepoint_api
59
+
60
+ def __init__(self, username: str, password: str, sharepoint_url: str, proxies: dict):
61
+ """
62
+ Initialise the SharePointAPI client with credentials.
63
+
64
+ Stores the username, password and SharePoint URL for NTLM authentication.
65
+ """
66
+ # Store credentials for NTLM authentication
67
+ self.username = username
68
+ self.password = password
69
+ self.sharepoint_url = sharepoint_url
70
+ self.proxies = proxies
71
+
72
+ def _handle_response(self, response: requests.Response, success_codes: List[int]) -> requests.Response:
73
+ """
74
+ Centralised HTTP response handling.
75
+
76
+ Parameters
77
+ ----------
78
+ response : requests.Response
79
+ The response object returned by ``requests``.
80
+ success_codes : List[int]
81
+ HTTP status codes that are considered successful for the caller.
82
+
83
+ Returns
84
+ -------
85
+ requests.Response
86
+ The original response if it is successful.
87
+
88
+ Raises
89
+ ------
90
+ PermissionError
91
+ Raised for HTTP 401 Unauthorized responses.
92
+ FileNotFoundError
93
+ Raised for HTTP 404 Not Found responses.
94
+ ValueError
95
+ Raised for HTTP 400 Bad Request responses.
96
+ ConnectionError
97
+ Raised for any other unexpected status codes.
98
+ """
99
+ status = response.status_code
100
+
101
+ if status == 401:
102
+ print('Request failed (401 Unauthorized):')
103
+ print(f'URL: {response.request.url}')
104
+ print(response.text)
105
+ raise PermissionError(
106
+ 'Unauthorized (401) – authentication failed.')
107
+
108
+ if status == 404:
109
+ print('Request failed (404 Not Found):')
110
+ print(f'URL: {response.request.url}')
111
+ try:
112
+ print(response.json()['error']['message']['value'])
113
+ except Exception:
114
+ print('No detailed error message')
115
+ raise FileNotFoundError(
116
+ 'Resource not found (404) – see printed details.')
117
+
118
+ if status == 400:
119
+ print('Request failed (400 Bad Request):')
120
+ print(response.text)
121
+ raise ValueError('Bad request (400) – see printed details.')
122
+
123
+ if status not in success_codes:
124
+ print(f'Request failed (status {status}):')
125
+ try:
126
+ error_msg = response.json().get('error', {}).get('message', {}).get('value')
127
+ if error_msg:
128
+ print(f'Error message: {error_msg}')
129
+ except Exception:
130
+ print(response.text)
131
+ raise ConnectionError(
132
+ f'Unexpected status code {status} – see printed details.')
133
+
134
+ return response
135
+
136
+ # NOTE: All API calls use NTLM authentication via ``requests_ntlm.HttpNtlmAuth``
137
+ # with the username and password supplied during ``SharePointAPI`` construction.
138
+ def _api_post_call(self, url: str, post_data: dict, form_digest_value: str | None = None, merge: bool = False) -> requests.Response:
139
+ """
140
+ Perform a POST request against the SharePoint REST API.
141
+
142
+ Enhanced error handling:
143
+ * Network‑level errors (timeouts, DNS failures, etc.) are caught and re‑raised as
144
+ ``ConnectionError`` with a helpful message.
145
+ * HTTP error codes are reported with status, URL and response body when available.
146
+ * The method always returns a ``requests.Response`` on success (status 200, 201 or 204).
147
+
148
+ Parameters
149
+ ----------
150
+ url : str
151
+ The full endpoint URL.
152
+ post_data : dict
153
+ JSON‑serialisable payload to send.
154
+ form_digest_value : str | None, optional
155
+ FormDigest required for POST/PUT/MERGE operations.
156
+ merge : bool, optional
157
+ If ``True`` the ``X-HTTP-Method: MERGE`` header is added.
158
+
159
+ Returns
160
+ -------
161
+ requests.Response
162
+ The successful response object.
163
+ """
164
+ # Build headers
165
+ headers = {
166
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:71.0) Gecko/20100101 Firefox/71.0',
167
+ 'accept': 'application/json;odata=verbose',
168
+ 'Content-type': 'application/json; charset=utf-8',
169
+ 'Cache-Control': 'no-cache',
170
+ 'Connection': 'keep-alive',
171
+ 'Accept-Encoding': 'gzip, deflate, br',
172
+ 'Content-Length': str(len(f'{post_data}')),
173
+ 'If-Match': '*'
174
+ }
175
+ if form_digest_value is not None:
176
+ headers['X-RequestDigest'] = f'{form_digest_value}'
177
+ if merge:
178
+ headers['X-HTTP-Method'] = 'MERGE'
179
+
180
+ try:
181
+ response = requests.post(
182
+ url,
183
+ auth=HttpNtlmAuth(self.username, self.password),
184
+ headers=headers,
185
+ json=post_data,
186
+ proxies=self.proxies,
187
+ timeout=30,
188
+ )
189
+ except requests.exceptions.RequestException as exc:
190
+ print(f'Network error during POST to {url}: {exc}')
191
+ raise ConnectionError(
192
+ f'Network error during POST request: {exc}') from exc
193
+
194
+ # Centralised response handling
195
+ return self._handle_response(response, [200, 201, 204])
196
+
197
+ # NOTE: All API calls use NTLM authentication via ``requests_ntlm.HttpNtlmAuth``
198
+ # with the username and password supplied during ``SharePointAPI`` construction.
199
+ def _api_put_call(self, url: str, put_data: dict, form_digest_value: str | None = None, merge: bool = False) -> requests.Response:
200
+ """
201
+ Perform a PUT request against the SharePoint REST API.
202
+
203
+ Enhanced error handling:
204
+ * Network‑level errors (timeouts, DNS failures, etc.) are caught and re‑raised as
205
+ ``ConnectionError`` with a helpful message.
206
+ * HTTP error codes are reported with status, URL and response body when available.
207
+ * The method always returns a ``requests.Response`` on success (status 200, 201 or 204).
208
+
209
+ Parameters
210
+ ----------
211
+ url : str
212
+ The full endpoint URL.
213
+ put_data : dict
214
+ JSON‑serialisable payload to send.
215
+ form_digest_value : str | None, optional
216
+ FormDigest required for POST/PUT/MERGE operations.
217
+ merge : bool, optional
218
+ If ``True`` the ``X-HTTP-Method: MERGE`` header is added.
219
+
220
+ Returns
221
+ -------
222
+ requests.Response
223
+ The successful response object.
224
+ """
225
+ # Build headers
226
+ headers = {
227
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:71.0) Gecko/20100101 Firefox/71.0',
228
+ 'accept': 'application/json;odata=verbose',
229
+ 'Content-type': 'application/json; charset=utf-8',
230
+ 'Cache-Control': 'no-cache',
231
+ 'Connection': 'keep-alive',
232
+ 'Accept-Encoding': 'gzip, deflate, br',
233
+ 'Content-Length': str(len(f'{put_data}')),
234
+ 'If-Match': '*'
235
+ }
236
+ if form_digest_value is not None:
237
+ headers['X-RequestDigest'] = f'{form_digest_value}'
238
+ if merge:
239
+ headers['X-HTTP-Method'] = 'MERGE'
240
+
241
+ try:
242
+ response = requests.put(
243
+ url,
244
+ auth=HttpNtlmAuth(self.username, self.password),
245
+ headers=headers,
246
+ json=put_data,
247
+ proxies=self.proxies,
248
+ timeout=30,
249
+ )
250
+ except requests.exceptions.RequestException as exc:
251
+ print(f'Network error during PUT to {url}: {exc}')
252
+ raise ConnectionError(
253
+ f'Network error during PUT request: {exc}') from exc
254
+
255
+ # Centralised response handling
256
+ return self._handle_response(response, [200, 201, 204])
257
+
258
+ def _api_attachment_call(self, url: str, post_data: bytes | None = None, form_digest_value: str | None = None,
259
+ overwrite: bool = False, x_http_method: str | None = None) -> requests.Response:
260
+ """
261
+ Perform an attachment‑related request (POST, PUT, DELETE) against the SharePoint REST API.
262
+
263
+ Enhanced error handling:
264
+ * Network‑level errors (timeouts, DNS failures, etc.) are caught and re‑raised as
265
+ ``ConnectionError`` with a helpful message.
266
+ * HTTP error codes are reported with status, URL and response body when available.
267
+ * The method always returns a ``requests.Response`` on success (status 200 or 204).
268
+
269
+ Parameters
270
+ ----------
271
+ url : str
272
+ The full endpoint URL.
273
+ post_data : bytes | None, optional
274
+ Binary payload for the request (e.g., file content). If ``None`` a simple POST/DELETE is performed.
275
+ form_digest_value : str | None, optional
276
+ FormDigest required for POST/PUT/DELETE operations.
277
+ overwrite : bool, optional
278
+ If ``True`` the ``X-HTTP-Method: PUT`` header is added (used for overwriting files).
279
+ x_http_method : str | None, optional
280
+ Explicit HTTP method override (e.g., ``'DELETE'`` or ``'PUT'``). Takes precedence over ``overwrite``.
281
+ """
282
+ # Build base headers
283
+ headers = {
284
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:71.0) Gecko/20100101 Firefox/71.0',
285
+ 'accept': 'application/json;odata=verbose',
286
+ 'X-RequestDigest': f'{form_digest_value}'
287
+ }
288
+
289
+ # Determine effective X‑HTTP‑Method
290
+ if overwrite:
291
+ headers['X-HTTP-Method'] = "PUT"
292
+ if x_http_method:
293
+ method = x_http_method.lower()
294
+ if method == 'delete':
295
+ headers['X-HTTP-Method'] = "DELETE"
296
+ elif method == 'put':
297
+ headers['X-HTTP-Method'] = "PUT"
298
+ else:
299
+ print(f'X-HTTP-Method \"{x_http_method}\" is not implemented')
300
+ raise ConnectionError(
301
+ f'Unsupported X-HTTP-Method: {x_http_method}')
302
+
303
+ # Add Content‑Length when payload present
304
+ if post_data is not None:
305
+ headers['Content-Length'] = str(len(post_data))
306
+
307
+ try:
308
+ response = requests.post(
309
+ url,
310
+ auth=HttpNtlmAuth(self.username, self.password),
311
+ data=post_data,
312
+ headers=headers,
313
+ proxies=self.proxies,
314
+ timeout=30,
315
+ )
316
+ except requests.exceptions.RequestException as exc:
317
+ print(f'Network error during attachment request to {url}: {exc}')
318
+ raise ConnectionError(
319
+ f'Network error during attachment request: {exc}') from exc
320
+
321
+ # Centralised response handling (attachment calls consider 200 and 204 as success)
322
+ return self._handle_response(response, [200, 204])
323
+
324
+ # NOTE: All API calls use NTLM authentication via ``requests_ntlm.HttpNtlmAuth``
325
+ # with the username and password supplied during ``SharePointAPI`` construction.
326
+ def _api_get_call(self, url, *args, **kwargs) -> requests.Response:
327
+ """
328
+ Perform a GET request against the SharePoint REST API.
329
+
330
+ Enhanced error handling:
331
+ * Network‑level errors (timeouts, DNS failures, etc.) are caught and re‑raised as
332
+ ``ConnectionError`` with a helpful message.
333
+ * HTTP error codes are reported with status, URL and response body when available.
334
+ * The method always returns a ``requests.Response`` on success (status 200).
335
+
336
+ Parameters
337
+ ----------
338
+ url : str
339
+ The full endpoint URL.
340
+ """
341
+ # Build headers
342
+ headers = {
343
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:71.0) Gecko/20100101 Firefox/71.0',
344
+ 'accept': 'application/json;odata=verbose',
345
+ 'Content-type': 'application/json; charset=utf-8',
346
+ 'Cache-Control': 'no-cache',
347
+ 'Connection': 'keep-alive',
348
+ 'Accept-Encoding': 'gzip, deflate, br'
349
+ }
350
+
351
+ try:
352
+ response = requests.get(
353
+ url,
354
+ auth=HttpNtlmAuth(self.username, self.password),
355
+ headers=headers,
356
+ proxies=self.proxies,
357
+ timeout=30,
358
+ *args,
359
+ **kwargs
360
+ )
361
+ except requests.exceptions.RequestException as exc:
362
+ print(f'Network error during GET to {url}: {exc}')
363
+ raise ConnectionError(
364
+ f'Network error during GET request: {exc}') from exc
365
+
366
+ # Centralised response handling (GET expects 200)
367
+ return self._handle_response(response, [200])
368
+
369
+ def get_users(self, sharepoint_site, filters=None, select_fields=None):
370
+ '''
371
+ Returns a list of users from a given sharepoint_site.
372
+ Optional ``filters`` can be provided to filter the users using OData syntax.
373
+ Optional ``select_fields`` (list of strings) can be provided to limit the returned fields
374
+ via the ``$select`` query option.
375
+ '''
376
+ # Build query arguments
377
+ arguments = []
378
+
379
+ if filters is not None:
380
+ if not isinstance(filters, list):
381
+ filter_string = self.py2sp_conditional(filters)
382
+ else:
383
+ filter_string = self.py2sp_conditional(' and '.join(filters))
384
+ arguments.append(f'$filter={filter_string}')
385
+
386
+ if select_fields is not None:
387
+ if isinstance(select_fields, list):
388
+ select_string = ','.join(select_fields)
389
+ else:
390
+ select_string = str(select_fields)
391
+ arguments.append(f'$select={select_string}')
392
+
393
+ # Construct final URL
394
+ base_url = f'{self.sharepoint_url}/cases/{sharepoint_site}/_api/Web/siteusers'
395
+ if arguments:
396
+ url = f'{base_url}?{"&".join(arguments)}'
397
+ else:
398
+ url = base_url
399
+
400
+ r = self._api_get_call(url)
401
+
402
+ users = []
403
+
404
+ for user_settings in r.json()["d"]["results"]:
405
+ users.append(SharePointUser(user_settings))
406
+
407
+ return SharePointUserList(sharepoint_site, users)
408
+
409
+ def get_group_users(self, sharepoint_site, group_name: str, filters=None, select_fields=None) -> SharePointUserList:
410
+ """
411
+ Retrieve users that are members of a SharePoint group, with optional OData filters
412
+ and field selection.
413
+
414
+ Parameters
415
+ ----------
416
+ sharepoint_site : str
417
+ Identifier of the SharePoint site (e.g., 'mySite').
418
+ group_name : str
419
+ Exact name of the SharePoint group.
420
+ filters : str or list, optional
421
+ OData filter expression(s) to limit the returned users.
422
+ select_fields : list or str, optional
423
+ Fields to include in the response via the ``$select`` query option.
424
+
425
+ Returns
426
+ -------
427
+ SharePointUserList
428
+ A list‑like container with :class:`SharePointUser` objects for each member.
429
+ """
430
+ # Build the request URL for the group's users.
431
+ # SharePoint REST endpoint: /_api/web/sitegroups/GetByName('<group_name>')/users
432
+ base_url = (
433
+ f"{self.sharepoint_url}/cases/{sharepoint_site}"
434
+ f"/_api/web/sitegroups/GetByName('{group_name}')/users"
435
+ )
436
+
437
+ # Build query arguments (filters / select)
438
+ arguments = []
439
+
440
+ if filters is not None:
441
+ if not isinstance(filters, list):
442
+ filter_string = self.py2sp_conditional(filters)
443
+ else:
444
+ filter_string = self.py2sp_conditional(' and '.join(filters))
445
+ arguments.append(f"$filter={filter_string}")
446
+
447
+ if select_fields is not None:
448
+ if isinstance(select_fields, list):
449
+ select_string = ','.join(select_fields)
450
+ else:
451
+ select_string = str(select_fields)
452
+ arguments.append(f"$select={select_string}")
453
+
454
+ # Construct final URL
455
+ if arguments:
456
+ url = f"{base_url}?{'&'.join(arguments)}"
457
+ else:
458
+ url = base_url
459
+
460
+ # Perform the GET request.
461
+ r = self._api_get_call(url)
462
+
463
+ # Parse the JSON payload – users are under ``d.results``.
464
+ users = [
465
+ SharePointUser(user_settings)
466
+ for user_settings in r.json()["d"]["results"]
467
+ ]
468
+
469
+ # Return a ``SharePointUserList`` (consistent with other user‑retrieval methods).
470
+ return SharePointUserList(sharepoint_site, users)
471
+
472
+ def get_user(self, sharepoint_site, user_id, select_fields=None):
473
+ '''
474
+ Returns a single user from a given sharepoint_site.
475
+ If ``user_id`` is ``None`` an empty :class:`SharePointUser` instance is returned.
476
+ Optional ``select_fields`` (list of strings) can be provided to limit the fields
477
+ returned via the ``$select`` OData query option.
478
+ '''
479
+ if user_id is None:
480
+ return SharePointUser()
481
+
482
+ # Build the request URL, adding $select if needed.
483
+ base_url = f'{self.sharepoint_url}/cases/{sharepoint_site}/_api/Web/getUserById({user_id})'
484
+ if select_fields is not None:
485
+ if isinstance(select_fields, list):
486
+ select_string = ','.join(select_fields)
487
+ else:
488
+ select_string = str(select_fields)
489
+ url = f'{base_url}?$select={select_string}'
490
+ else:
491
+ url = base_url
492
+
493
+ try:
494
+ r = self._api_get_call(url)
495
+ except ConnectionError:
496
+ # If the user does not exist, return an empty user object for graceful handling.
497
+ print(
498
+ f"User with ID {user_id} does not exist in sharepoint_site {sharepoint_site}")
499
+ return SharePointUser()
500
+
501
+ # The endpoint returns the user object directly under the ``d`` key.
502
+ user_settings = r.json()["d"]
503
+ return SharePointUser(user_settings)
504
+
505
+ # SP Lists
506
+
507
+ def get_lists(self, sharepoint_site):
508
+ '''
509
+ Returns a list of lists from a given sharepoint_site
510
+ '''
511
+ r = self._api_get_call(
512
+ f'{self.sharepoint_url}/cases/{sharepoint_site}/_api/Web/Lists')
513
+
514
+ _lists = []
515
+ for list_props in r.json()["d"]["results"]:
516
+ _lists.append(SharePointList(self, sharepoint_site, list_props))
517
+
518
+ return SharePointLists(_lists)
519
+
520
+ def get_list(self, sharepoint_site, sp_list, filters=None, top=1000, view_path=None, select_fields=None, SPListType: SharePointList = SharePointList) -> SharePointList:
521
+ '''
522
+ Returns a list from a given sharepoint_site using its guid
523
+
524
+ Returns a subset of items from a list
525
+
526
+ sharepoint_site:
527
+ guid: the guid of the list to retrieve items from
528
+
529
+ Optional ``select_fields`` (list of strings) can be provided to limit the fields
530
+ returned for each list item via the ``$select`` OData query option.
531
+ '''
532
+
533
+ # Uses either guid or SharePointList
534
+ if isinstance(sp_list, SharePointList):
535
+ guid = sp_list.guid
536
+ r = self._api_get_call(
537
+ f'{self.sharepoint_url}/cases/{sharepoint_site}/_api/Web/Lists(guid\'{guid}\')')
538
+ sp_list = SPListType(self, sharepoint_site, r.json()["d"])
539
+
540
+ elif isinstance(sp_list, str):
541
+ guid = sp_list
542
+ r = self._api_get_call(
543
+ f'{self.sharepoint_url}/cases/{sharepoint_site}/_api/Web/Lists(guid\'{guid}\')')
544
+ sp_list = SPListType(self, sharepoint_site, r.json()["d"])
545
+ else:
546
+ raise TypeError(
547
+ 'Invalid sp_list argument; expected SharePointList or str')
548
+
549
+ arguments = []
550
+
551
+ if filters is not None:
552
+ if not isinstance(filters, list):
553
+ filter_string = self.py2sp_conditional(filters)
554
+ # print(filters)
555
+ # raise('invalid search filters')
556
+ else:
557
+
558
+ filter_string = self.py2sp_conditional(' and '.join(filters))
559
+ arguments.append(f'$filter={filter_string}')
560
+
561
+ if view_path is not None:
562
+ # Shows top x items
563
+ arguments.append(f'$ViewPath={view_path}')
564
+
565
+ if top is not None:
566
+ # Shows top x items
567
+ arguments.append(f'$top={top}')
568
+
569
+ if select_fields is not None:
570
+ if isinstance(select_fields, list):
571
+ select_string = ','.join(select_fields)
572
+ else:
573
+ select_string = str(select_fields)
574
+ arguments.append(f'$select={select_string}')
575
+
576
+ r = self._api_get_call(
577
+ f'{self.sharepoint_url}/cases/{sharepoint_site}/_api/Web/Lists(guid\'{guid}\')/items?{"&".join(arguments)}')
578
+
579
+ items = [SPListType.SPItem(self, sharepoint_site, guid, item_settings)
580
+ for item_settings in r.json()["d"]["results"]]
581
+ sp_list.append_items(items)
582
+ return sp_list
583
+
584
+ def get_list_by_name(self, sharepoint_site, sp_list_name: str, filters=None, top=1000, view_path=None, select_fields=None, SPListType: SharePointList = SharePointList) -> SharePointList:
585
+ '''
586
+ Returns a list from a given sharepoint_site filtering by list name
587
+
588
+ Returns a subset of items from a list
589
+
590
+ sharepoint_site: The sharepoint_site containing the list
591
+ sp_list_name: the name of the list
592
+ filters: query filters
593
+ top: Maximum items to query from the list
594
+
595
+ Optional ``select_fields`` (list of strings) can be provided to limit the fields
596
+ returned for each list item via the ``$select`` OData query option.
597
+ '''
598
+ # Retrieve the list directly by its title using SharePoint REST API v1
599
+ try:
600
+ r = self._api_get_call(
601
+ f"{self.sharepoint_url}/cases/{sharepoint_site}/_api/Web/Lists/GetByTitle('{sp_list_name}')")
602
+ except ConnectionError:
603
+ # The list was not found – provide a clear error message
604
+ msg = f"List '{sp_list_name}' does not exist in sharepoint_site {sharepoint_site}"
605
+ print(msg)
606
+ raise ConnectionError(msg)
607
+
608
+ sp_list = SPListType(self, sharepoint_site, r.json()["d"])
609
+
610
+ guid = sp_list.guid
611
+
612
+ arguments = []
613
+
614
+ if filters is not None:
615
+ if not isinstance(filters, list):
616
+ filter_string = self.py2sp_conditional(filters)
617
+ else:
618
+ filter_string = self.py2sp_conditional(' and '.join(filters))
619
+ arguments.append(f"$filter={filter_string}")
620
+
621
+ if view_path is not None:
622
+ arguments.append(f"$ViewPath={view_path}")
623
+
624
+ if top is not None:
625
+ arguments.append(f"$top={top}")
626
+
627
+ if select_fields is not None:
628
+ if isinstance(select_fields, list):
629
+ select_string = ','.join(select_fields)
630
+ else:
631
+ select_string = str(select_fields)
632
+ arguments.append(f"$select={select_string}")
633
+
634
+ r = self._api_get_call(
635
+ f"{self.sharepoint_url}/cases/{sharepoint_site}/_api/Web/Lists(guid'{guid}')/items?{'&'.join(arguments)}")
636
+ items = [SPListType.SPItem(self, sharepoint_site, guid, item_settings)
637
+ for item_settings in r.json()["d"]["results"]]
638
+ sp_list.append_items(items)
639
+ return sp_list
640
+
641
+ def get_list_from_json(self, file_name, SPListType: SharePointList = SharePointList) -> SharePointList:
642
+ '''
643
+ Returns a list from a sharepoint_site based on a json file.
644
+
645
+ file_name: the json file to load the list from
646
+
647
+ '''
648
+ try:
649
+ with open(file_name, 'r') as fp:
650
+ data_dict = json.load(fp)
651
+
652
+ sharepoint_site = data_dict['sharepoint_site']
653
+ guid = data_dict['GUID']
654
+
655
+ cases = []
656
+ for case in data_dict['cases']:
657
+ settings = case["settings"]
658
+ versions = None if "versions" not in case else case["versions"]
659
+ cases.append(SPListType.SPItem(
660
+ self, sharepoint_site, guid, settings, versions))
661
+
662
+ return SPListType(self, sharepoint_site, data_dict['Settings'], cases)
663
+ except FileNotFoundError as err:
664
+ print(f"File '{file_name}' was not found")
665
+ raise err
666
+ except Exception as err:
667
+ raise err
668
+
669
+ # SP Item
670
+
671
+ def get_item(self, sharepoint_site, sp_list, item_id, select_fields=None) -> SharePointListItem:
672
+ '''
673
+ Returns a single list item from a given sharepoint_site.
674
+ Optional ``select_fields`` (list of strings) can be provided to limit the fields
675
+ returned via the ``$select`` OData query option.
676
+ '''
677
+
678
+ if isinstance(sp_list, SharePointList):
679
+ guid = sp_list.guid
680
+
681
+ elif isinstance(sp_list, str):
682
+ guid = sp_list
683
+ else:
684
+ raise TypeError(
685
+ 'Invalid sp_list argument; expected SharePointList or str')
686
+
687
+ # Build the request URL, adding $select if needed.
688
+ if select_fields is not None:
689
+ if isinstance(select_fields, list):
690
+ select_string = ','.join(select_fields)
691
+ else:
692
+ select_string = str(select_fields)
693
+ url = f'{self.sharepoint_url}/cases/{sharepoint_site}/_api/Web/Lists(guid\'{guid}\')/items({item_id})?$select={select_string}'
694
+ else:
695
+ url = f'{self.sharepoint_url}/cases/{sharepoint_site}/_api/Web/Lists(guid\'{guid}\')/items({item_id})'
696
+
697
+ r = self._api_get_call(url)
698
+
699
+ settings = r.json()["d"]
700
+ return SharePointListItem(self, sharepoint_site, guid, settings)
701
+
702
+ def create_item(self, sharepoint_site, sp_list, data) -> SharePointListItem:
703
+ # Uses either guid or SharePointList
704
+ if isinstance(sp_list, SharePointList):
705
+ guid = sp_list.guid
706
+ elif isinstance(sp_list, str):
707
+ guid = sp_list
708
+ r = self._api_get_call(
709
+ f'{self.sharepoint_url}/cases/{sharepoint_site}/_api/Web/Lists(guid\'{guid}\')')
710
+ sp_list = SharePointList(self,
711
+ sharepoint_site, r.json()["d"]["results"][0])
712
+
713
+ r = self._api_post_call(
714
+ f'{self.sharepoint_url}/cases/{sharepoint_site}/_api/contextinfo', {})
715
+
716
+ form_digest_value = r.json(
717
+ )["d"]["GetContextWebInformation"]["FormDigestValue"]
718
+
719
+ r = self._api_post_call(
720
+ f'{self.sharepoint_url}/cases/{sharepoint_site}/_api/Web/Lists(guid\'{guid}\')/items', data, form_digest_value)
721
+
722
+ settings = r.json()["d"]
723
+ return SharePointListItem(self, sharepoint_site, guid, settings)
724
+
725
+ def update_item(self, sharepoint_site, sp_list, item_id, data) -> None:
726
+ '''
727
+ Update a sharepoint item
728
+
729
+ sharepoint_site: The sharepoint_site containing the item
730
+ sp_list: The list containing the item
731
+ item_id: The id of the item
732
+ data: Data to push to the item
733
+ '''
734
+ if isinstance(sp_list, SharePointList):
735
+ guid = sp_list.guid
736
+ elif isinstance(sp_list, str):
737
+ guid = sp_list
738
+ else:
739
+ raise TypeError(
740
+ 'Only "SharePointList" and "str" types are allowed')
741
+
742
+ r = self._api_post_call(
743
+ f'{self.sharepoint_url}/cases/{sharepoint_site}/_api/contextinfo', {})
744
+
745
+ form_digest_value = r.json(
746
+ )["d"]["GetContextWebInformation"]["FormDigestValue"]
747
+
748
+ r = self._api_post_call(
749
+ f'{self.sharepoint_url}/cases/{sharepoint_site}/_api/Web/Lists(guid\'{guid}\')/items({item_id})', data, form_digest_value, merge=True)
750
+
751
+ return r
752
+
753
+ def attach_file(self, sharepoint_site, sp_list, item, file_name, file_content) -> dict:
754
+ # Uses either guid or SharePointList
755
+ if isinstance(sp_list, SharePointList):
756
+ guid = sp_list.guid
757
+ elif isinstance(sp_list, str):
758
+ guid = sp_list
759
+ else:
760
+ raise TypeError(
761
+ 'Only "SharePointList" and "str" types are allowed')
762
+
763
+ r = self._api_post_call(
764
+ f'{self.sharepoint_url}/cases/{sharepoint_site}/_api/contextinfo', {})
765
+ form_digest_value = r.json(
766
+ )["d"]["GetContextWebInformation"]["FormDigestValue"]
767
+
768
+ r = self._api_attachment_call(
769
+ f"{self.sharepoint_url}/cases/{sharepoint_site}/_api/Web/Lists(guid\'{guid}\')/items({item.Id})/AttachmentFiles/ add(FileName='{file_name}')", file_content, form_digest_value)
770
+
771
+ return r.json()
772
+
773
+ def get_item_versions(self, sharepoint_site, sp_list, item_id, select_fields=[]) -> list:
774
+ '''
775
+ Returns a list of users from a given sharepoint_site
776
+ '''
777
+
778
+ if isinstance(sp_list, SharePointList):
779
+ guid = sp_list.guid
780
+
781
+ elif isinstance(sp_list, str):
782
+ guid = sp_list
783
+ else:
784
+ raise TypeError(
785
+ 'Invalid sp_list argument; expected SharePointList or str')
786
+
787
+ if select_fields:
788
+ r = self._api_get_call(
789
+ f'{self.sharepoint_url}/cases/{sharepoint_site}/_api/Web/Lists(guid\'{guid}\')/items({item_id})/versions?$select={",".join(select_fields)}')
790
+ else:
791
+ r = self._api_get_call(
792
+ f'{self.sharepoint_url}/cases/{sharepoint_site}/_api/Web/Lists(guid\'{guid}\')/items({item_id})/Versions')
793
+
794
+ versions = r.json()["d"]['results']
795
+ return versions
796
+
797
+ # Cases
798
+
799
+ def get_cases_list_from_json(self, file_name) -> CasesList:
800
+ '''
801
+ Returns a cases list from a sharepoint_site based on a json file.
802
+
803
+ file_name: the json file to load the list from
804
+
805
+ '''
806
+
807
+ return self.get_list_from_json(file_name, SPListType=CasesList)
808
+
809
+ def get_cases_list(self, sharepoint_site, sp_list, filters=None, top=1000, view_path=None, select_fields=None) -> CasesList:
810
+ '''
811
+ Returns a cases list from a given sharepoint_site using its guid
812
+
813
+ Returns a subset of items from a list
814
+
815
+ sharepoint_site: The sharepoint_site containing the list
816
+ sp_list: the guid of the list to retrieve items from
817
+ filters: query filters
818
+ top: Maximum items to query from the list
819
+ '''
820
+
821
+ return self.get_list(sharepoint_site, sp_list, filters, top, view_path, select_fields, SPListType=CasesList)
822
+
823
+ def get_cases_list_by_name(self, sharepoint_site, sp_list_name: str = 'Cases', filters=None, top=1000, view_path=None, select_fields=None) -> CasesList:
824
+ '''
825
+ Returns a cases list from a given sharepoint_site filtering by list name
826
+
827
+ sharepoint_site: The sharepoint_site containing the list
828
+ sp_list_name: the name of the list
829
+ filters: query filters
830
+ top: Maximum items to query from the list
831
+ '''
832
+ return self.get_list_by_name(sharepoint_site, sp_list_name, filters, top, view_path, select_fields, SPListType=CasesList)
833
+
834
+ def get_case(self, sharepoint_site, sp_list, item_id) -> SharePointListItem:
835
+ '''
836
+ Returns a list of users from a given sharepoint_site
837
+ '''
838
+
839
+ if isinstance(sp_list, SharePointList):
840
+ guid = sp_list.guid
841
+
842
+ elif isinstance(sp_list, str):
843
+ guid = sp_list
844
+ else:
845
+ raise TypeError(
846
+ 'Invalid sp_list argument; expected SharePointList or str')
847
+
848
+ r = self._api_get_call(
849
+ f'{self.sharepoint_url}/cases/{sharepoint_site}/_api/Web/Lists(guid\'{guid}\')/items({item_id})')
850
+
851
+ settings = r.json()["d"]
852
+ return SharepointSiteCase(self, sharepoint_site, guid, settings)
853
+
854
+ # Time Registration
855
+
856
+ def get_time_registration_list_from_json(self, file_name) -> TimeRegistrationList:
857
+ '''
858
+ Returns a Time Registration list from a sharepoint_site based on a json file.
859
+
860
+ file_name: the json file to load the list from
861
+
862
+ '''
863
+
864
+ return self.get_list_from_json(file_name, SPListType=TimeRegistrationList)
865
+
866
+ def get_time_registration_list(self, sharepoint_site, sp_list, filters=None, top=1000, view_path=None, select_fields=None) -> TimeRegistrationList:
867
+ '''
868
+ Returns a Time Registration list from a given sharepoint_site using its guid
869
+
870
+ Returns a subset of items from a list
871
+
872
+ sharepoint_site: The sharepoint_site containing the list
873
+ sp_list: the guid of the list to retrieve items from
874
+ filters: query filters
875
+ top: Maximum items to query from the list
876
+ '''
877
+
878
+ return self.get_list(sharepoint_site, sp_list, filters, top, view_path, select_fields, SPListType=TimeRegistrationList)
879
+
880
+ def get_time_registration_list_by_name(self, sharepoint_site, sp_list_name: str, filters=None, top=1000, view_path=None, select_fields=None) -> TimeRegistrationList:
881
+ '''
882
+ Returns a Time Registration list from a given sharepoint_site filtering by list name
883
+
884
+ sharepoint_site: The sharepoint_site containing the list
885
+ sp_list_name: the name of the list
886
+ filters: query filters
887
+ top: Maximum items to query from the list
888
+ '''
889
+ return self.get_list_by_name(sharepoint_site, sp_list_name, filters, top, view_path, select_fields, SPListType=TimeRegistrationList)
890
+
891
+ # SP Files
892
+
893
+ def folder_exists(self, sharepoint_site, folder, in_doc_lib=True):
894
+ """
895
+ Check whether a folder exists in the given SharePoint site.
896
+
897
+ Parameters
898
+ ----------
899
+ sharepoint_site : str
900
+ The SharePoint site identifier.
901
+ folder : str
902
+ The folder name (relative to the document library if ``in_doc_lib`` is True).
903
+ in_doc_lib : bool, optional
904
+ If True, the folder path is prefixed with ``DocumentLibrary``. Defaults to True.
905
+
906
+ Returns
907
+ -------
908
+ bool
909
+ ``True`` if the folder exists, ``False`` otherwise.
910
+ """
911
+ if in_doc_lib:
912
+ folder = os.path.join("DocumentLibrary", folder)
913
+
914
+ r = self._api_get_call(
915
+ f"{self.sharepoint_url}/cases/{sharepoint_site}/_api/Web/GetFolderByServerRelativeUrl('/cases/{sharepoint_site}/{folder}')/ListItemAllFields")
916
+
917
+ return False if ("ListItemAllFields" in r.json()["d"] and r.json()["d"]["ListItemAllFields"] is None) else True
918
+
919
+ def create_new_folder(self, sharepoint_site, folder, new_folder, in_doc_lib=True):
920
+ '''
921
+ Creates a new folder
922
+ '''
923
+
924
+ if in_doc_lib:
925
+ folder = os.path.join("DocumentLibrary", folder)
926
+
927
+ r = self._api_post_call(
928
+ f'{self.sharepoint_url}/cases/{sharepoint_site}/_api/contextinfo', {})
929
+ form_digest_value = r.json(
930
+ )["d"]["GetContextWebInformation"]["FormDigestValue"]
931
+
932
+ r = self._api_post_call(f"{self.sharepoint_url}/cases/{sharepoint_site}/_api/web/folders",
933
+ form_digest_value=form_digest_value,
934
+ post_data={
935
+ "ServerRelativeUrl": f'/cases/{sharepoint_site}/{folder}/{new_folder}',
936
+ }
937
+ )
938
+
939
+ return r.status_code
940
+
941
+ def get_files(self, sharepoint_site, folder, in_doc_lib=True):
942
+ '''
943
+ Returns a list of files from a folder in a given sharepoint_site
944
+ '''
945
+
946
+ if in_doc_lib:
947
+ folder = os.path.join("DocumentLibrary", folder)
948
+
949
+ r = self._api_get_call(
950
+ f"{self.sharepoint_url}/cases/{sharepoint_site}/_api/Web/GetFolderByServerRelativeUrl('/cases/{sharepoint_site}/{folder}')/Files")
951
+
952
+ return r.json()["d"]["results"]
953
+
954
+ def get_file_content(self, sharepoint_site, folder, file, in_doc_lib=True):
955
+ '''
956
+ Downloads and saves a file from a folder in a given sharepoint_site
957
+ '''
958
+
959
+ if in_doc_lib:
960
+ folder = os.path.join("DocumentLibrary", folder)
961
+
962
+ r = self._api_get_call(
963
+ f"{self.sharepoint_url}/cases/{sharepoint_site}/_api/Web/GetFolderByServerRelativeUrl('/cases/{sharepoint_site}/{folder}')/Files('{file}')/$value")
964
+ return r._content
965
+
966
+ def download_file(self, sharepoint_site, folder, file, out_file=None, in_doc_lib=True):
967
+ '''
968
+ Downloads and saves a file from a folder in a given sharepoint_site
969
+ '''
970
+
971
+ if in_doc_lib:
972
+ folder = os.path.join("DocumentLibrary", folder)
973
+
974
+ if not out_file:
975
+ out_file = file
976
+
977
+ file_content = self.get_file_content(
978
+ sharepoint_site, folder, file, in_doc_lib)
979
+
980
+ with open(out_file, 'wb') as f:
981
+ f.write(file_content)
982
+
983
+ def upload_file_content(self, sharepoint_site, folder, file, file_content, overwrite=False, in_doc_lib=True):
984
+ '''
985
+ Uploads a file to a folder in a given sharepoint_site
986
+ '''
987
+
988
+ if in_doc_lib:
989
+ folder = os.path.join("DocumentLibrary", folder)
990
+
991
+ r = self._api_post_call(
992
+ f'{self.sharepoint_url}/cases/{sharepoint_site}/_api/contextinfo', {})
993
+ form_digest_value = r.json(
994
+ )["d"]["GetContextWebInformation"]["FormDigestValue"]
995
+
996
+ r = self._api_attachment_call(
997
+ f"{self.sharepoint_url}/cases/{sharepoint_site}/_api/Web/GetFolderByServerRelativeUrl('/cases/{sharepoint_site}/{folder}')/Files/add(url='{file}',overwrite={str(overwrite).lower()})",
998
+ file_content,
999
+ form_digest_value)
1000
+ return r.json()
1001
+
1002
+ def upload_file(self, sharepoint_site, folder, file, in_file=None, overwrite=False, in_doc_lib=True):
1003
+ '''
1004
+ Uploads a file to a folder in a given sharepoint_site
1005
+ '''
1006
+
1007
+ if in_doc_lib:
1008
+ folder = os.path.join("DocumentLibrary", folder)
1009
+
1010
+ if not in_file:
1011
+ in_file = file
1012
+ with open(in_file, 'rb') as f:
1013
+ file_content = f.read()
1014
+
1015
+ return self.upload_file_content(sharepoint_site, folder, file, file_content, overwrite, False)
1016
+
1017
+ def delete_file(self, sharepoint_site, folder, file, in_doc_lib=True):
1018
+ '''
1019
+ Uploads a file to a folder in a given sharepoint_site
1020
+ '''
1021
+
1022
+ if in_doc_lib:
1023
+ folder = os.path.join("DocumentLibrary", folder)
1024
+
1025
+ r = self._api_post_call(
1026
+ f'{self.sharepoint_url}/cases/{sharepoint_site}/_api/contextinfo', {})
1027
+ form_digest_value = r.json(
1028
+ )["d"]["GetContextWebInformation"]["FormDigestValue"]
1029
+
1030
+ r = self._api_attachment_call(
1031
+ url=f"{self.sharepoint_url}/cases/{sharepoint_site}/_api/Web/GetFolderByServerRelativeUrl('/cases/{sharepoint_site}/{folder}')/Files('{file}')",
1032
+ post_data=None,
1033
+ form_digest_value=form_digest_value,
1034
+ x_http_method='DELETE')
1035
+
1036
+ def copy_file(self, sharepoint_site, folder, file, out_folder=None, out_file=None, overwrite=False, in_doc_lib=True):
1037
+ """
1038
+ Copy a file within a SharePoint site.
1039
+
1040
+ Parameters
1041
+ ----------
1042
+ sharepoint_site : str
1043
+ The SharePoint site identifier.
1044
+ folder : str
1045
+ Source folder (relative to the document library if ``in_doc_lib`` is True).
1046
+ file : str
1047
+ Name of the file to copy.
1048
+ out_folder : str, optional
1049
+ Destination folder. If omitted, defaults to the source folder.
1050
+ out_file : str, optional
1051
+ Destination file name. If omitted, defaults to ``'copy of ' + file``.
1052
+ overwrite : bool, optional
1053
+ Overwrite the destination file if it already exists. Defaults to ``False``.
1054
+ in_doc_lib : bool, optional
1055
+ Whether the operation is within the document library. Defaults to ``True``.
1056
+
1057
+ Returns
1058
+ -------
1059
+ None
1060
+ The method performs the copy operation via SharePoint REST API.
1061
+ """
1062
+ if in_doc_lib:
1063
+ folder = os.path.join("DocumentLibrary", folder)
1064
+
1065
+ if not out_file:
1066
+ out_file = 'copy of '+file
1067
+
1068
+ if not out_folder:
1069
+ out_folder = folder
1070
+
1071
+ r = self._api_post_call(
1072
+ f'{self.sharepoint_url}/cases/{sharepoint_site}/_api/contextinfo', {})
1073
+ form_digest_value = r.json(
1074
+ )["d"]["GetContextWebInformation"]["FormDigestValue"]
1075
+
1076
+ in_path = f"/cases/{sharepoint_site}/{folder}"
1077
+ out_path = f"/cases/{sharepoint_site}/{out_folder}/{out_file}"
1078
+
1079
+ r = self._api_attachment_call(
1080
+ f"{self.sharepoint_url}/cases/{sharepoint_site}/_api/Web/GetFolderByServerRelativeUrl('{in_path}')/Files('{file}')/copyto(strnewurl='{out_path}',boverwrite={str(overwrite).lower()})",
1081
+ None,
1082
+ form_digest_value)
1083
+
1084
+ # STATIC METHODS
1085
+
1086
+ @staticmethod
1087
+ def py2sp_conditional(conditional: str):
1088
+ """
1089
+ Convert a Python conditional expression to SharePoint OData query syntax.
1090
+
1091
+ Parameters
1092
+ ----------
1093
+ conditional : str
1094
+ A conditional expression using Python comparison operators.
1095
+
1096
+ Returns
1097
+ -------
1098
+ str
1099
+ The expression with operators replaced by their OData equivalents.
1100
+ """
1101
+ return conditional.replace('==', 'eq').replace('!=', 'ne').replace('>=', 'ge').replace('<=', 'le').replace('>', 'gt').replace('<', 'lt')