Skip to content

Commit 3588f30

Browse files
committed
prepare for PyPi publication
1 parent 050cff8 commit 3588f30

File tree

6 files changed

+1444
-7
lines changed

6 files changed

+1444
-7
lines changed
File renamed without changes.

dspace.py

Lines changed: 202 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# This software is licenced under the BSD 3-Clause licence
22
# available at https://opensource.org/licenses/BSD-3-Clause
3-
# and described in the LICENSE file in the root of this project
3+
# and described in the LICENSE.txt file in the root of this project
44

55
"""
66
DSpace REST API client library. Intended to make interacting with DSpace in Python 3 easier, particularly
@@ -14,8 +14,10 @@
1414
1515
@author Kim Shepherd <kim@shepherd.nz>
1616
"""
17-
17+
import code
1818
import json
19+
import logging
20+
1921
import requests
2022
from requests import Request
2123
import os
@@ -329,11 +331,88 @@ def parse_json(response):
329331
return response_json
330332

331333

334+
class Group(DSpaceObject):
335+
"""
336+
Extends DSpaceObject to implement specific attributes and methods for groups (aka. EPersonGroups)
337+
"""
338+
type = 'group'
339+
name = None
340+
permanent = False
341+
342+
def __init__(self, api_resource=None):
343+
"""
344+
Default constructor. Call DSpaceObject init then set group-specific attributes
345+
@param api_resource: API result object to use as initial data
346+
"""
347+
super(Group, self).__init__(api_resource)
348+
self.type = 'group'
349+
if 'name' in api_resource:
350+
self.name = api_resource['name']
351+
if 'permanent' in api_resource:
352+
self.permanent = api_resource['permanent']
353+
354+
def as_dict(self):
355+
"""
356+
Return a dict representation of this Group, based on super with group-specific attributes added
357+
@return: dict of Group for API use
358+
"""
359+
dso_dict = super(Group, self).as_dict()
360+
group_dict = {'name': self.name, 'permanent': self.permanent}
361+
return {**dso_dict, **group_dict}
362+
363+
364+
class User(SimpleDSpaceObject):
365+
"""
366+
Extends DSpaceObject to implement specific attributes and methods for users (aka. EPersons)
367+
"""
368+
type = 'user'
369+
name = None,
370+
netid = None,
371+
lastActive = None,
372+
canLogIn = False,
373+
email = None,
374+
requireCertificate = False,
375+
selfRegistered = False
376+
377+
def __init__(self, api_resource=None):
378+
"""
379+
Default constructor. Call DSpaceObject init then set user-specific attributes
380+
@param api_resource: API result object to use as initial data
381+
"""
382+
super(User, self).__init__(api_resource)
383+
self.type = 'user'
384+
if 'name' in api_resource:
385+
self.name = api_resource['name']
386+
if 'netid' in api_resource:
387+
self.netid = api_resource['netid']
388+
if 'lastActive' in api_resource:
389+
self.lastActive = api_resource['lastActive']
390+
if 'canLogIn' in api_resource:
391+
self.canLogIn = api_resource['canLogIn']
392+
if 'email' in api_resource:
393+
self.email = api_resource['email']
394+
if 'requireCertificate' in api_resource:
395+
self.requireCertificate = api_resource['requireCertificate']
396+
if 'selfRegistered' in api_resource:
397+
self.selfRegistered = api_resource['selfRegistered']
398+
399+
def as_dict(self):
400+
"""
401+
Return a dict representation of this User, based on super with user-specific attributes added
402+
@return: dict of User for API use
403+
"""
404+
dso_dict = super(User, self).as_dict()
405+
user_dict = {'name': self.name, 'netid': self.netid, 'lastActive': self.lastActive, 'canLogIn': self.canLogIn,
406+
'email': self.email, 'requireCertificate': self.requireCertificate,
407+
'selfRegistered': self.selfRegistered}
408+
return {**dso_dict, **user_dict}
409+
410+
332411
class DSpaceClient:
333412
"""
334413
Main class of the API client itself. This client uses request sessions to connect and authenticate to
335414
the REST API, maintain XSRF tokens, and all GET, POST, PUT, PATCH operations.
336-
Low-level api_get, api_post, api_put, api_patch functions are defined to handle the requests and do
415+
Low-level api_get, api_post, api_put, api_delete, api_patch functions are defined to handle the requests and do
337416
retries / XSRF refreshes where necessary.
338417
Higher level get, create, update, partial_update (patch) functions are implemented for each DSO type
339418
"""
@@ -468,7 +547,7 @@ def api_post(self, url, params, json, retry=False):
468547
def api_put(self, url, params, json, retry=False):
469548
"""
470549
Perform a PUT request. Refresh XSRF token if necessary.
471-
PUTs are tupically used to update objects.
550+
PUTs are typically used to update objects.
472551
@param url: DSpace REST API URL
473552
@param params: Any parameters to include (eg ?parent=abbc-....)
474553
@param json: Data in json-ready form (dict) to send as PUT body (eg. item.as_dict())
@@ -499,6 +578,39 @@ def api_put(self, url, params, json, retry=False):
499578

500579
return r
501580

581+
def api_delete(self, url, params, retry=False):
582+
"""
583+
Perform a DELETE request. Refresh XSRF token if necessary.
584+
DELETES are typically used to update objects.
585+
@param url: DSpace REST API URL
586+
@param params: Any parameters to include (eg ?parent=abbc-....)
587+
@param retry: Has this method already been retried? Used if we need to refresh XSRF.
588+
@return: Response from API
589+
"""
590+
h = {'Content-type': 'application/json'}
591+
r = self.session.delete(url, params=params, headers=h)
592+
if 'DSPACE-XSRF-TOKEN' in r.headers:
593+
t = r.headers['DSPACE-XSRF-TOKEN']
594+
print('Updating token to ' + t)
595+
self.session.headers.update({'X-XSRF-Token': t})
596+
self.session.cookies.update({'X-XSRF-Token': t})
597+
598+
if r.status_code == 403:
599+
# 403 Forbidden
600+
# If we had a CSRF failure, retry the request with the updated token
601+
# After speaking in #dev it seems that these do need occasional refreshes but I suspect
602+
# it's happening too often for me, so check for accidentally triggering it
603+
print(r.text)
604+
r_json = r.json()
605+
if 'message' in r_json and 'CSRF token' in r_json['message']:
606+
if retry:
607+
print('Already retried... something must be wrong')
608+
else:
609+
print("Retrying request with updated CSRF token")
610+
return self.api_delete(url, params=params, retry=True)
611+
612+
return r
613+
502614
def api_patch(self, url, operation, path, value, retry=False):
503615
"""
504616
@param url: DSpace REST API URL
@@ -693,6 +805,42 @@ def update_dso(self, dso, params=None):
693805
print(f'{e}')
694806
return None
695807

808+
def delete_dso(self, dso=None, url=None, params=None):
809+
"""
810+
Delete DSpaceObject. Takes a DSpaceObject and any optional parameters. Will send a PUT update to the remote
811+
object and return the updated object, typed correctly.
812+
:param dso: DSpaceObject from which to parse self link
813+
:param params: Optional parameters
814+
:param url: URI if not deleting from DSO
815+
:return:
816+
817+
"""
818+
if dso is None:
819+
if url is None:
820+
print(f'Need a DSO or a URL to delete')
821+
return None
822+
else:
823+
if not isinstance(dso, SimpleDSpaceObject):
824+
print(f'Only SimpleDSpaceObject types (eg Item, Collection, Community, EPerson) '
825+
f'are supported by generic update_dso PUT.')
826+
return dso
827+
# Get self URI from HAL links
828+
url = dso.links['self']['href']
829+
830+
try:
831+
r = self.api_delete(url, params=params)
832+
if r.status_code == 204:
833+
# 204 No Content - success!
834+
print(f'{url} was deleted sucessfully!')
835+
return r
836+
else:
837+
print(f'update operation failed: {r.status_code}: {r.text} ({url})')
838+
return None
839+
840+
except ValueError as e:
841+
print(f'{e}')
842+
return None
843+
696844
def get_bundles(self, parent=None, uuid=None):
697845
"""
698846
Get bundles for an item
@@ -950,7 +1098,7 @@ def create_item(self, parent, item):
9501098
Create an item beneath the given parent collection
9511099
@param parent: UUID of parent collection to pass as a parameter to create_dso
9521100
@param item: python Item object containing all the data and links expected by the REST API
953-
@return: Item object construcuted from the API response
1101+
@return: Item object constructed from the API response
9541102
"""
9551103
url = f'{self.API_ENDPOINT}/core/items'
9561104
if parent is None:
@@ -1008,3 +1156,52 @@ def add_metadata(self, dso, field, value, language=None, authority=None, confide
10081156
url=url, operation=self.PatchOperation.ADD, path=path, value=patch_value)
10091157

10101158
return dso_type(api_resource=parse_json(r))
1159+
1160+
def create_user(self, user, token=None):
1161+
"""
1162+
Create a user
1163+
@param user: python User object or Python dict containing all the data and links expected by the REST API
1164+
:param token: Token if creating new user (optional) from the link in a registration email
1165+
@return: User object constructed from the API response
1166+
"""
1167+
url = f'{self.API_ENDPOINT}/eperson/epersons'
1168+
data = user
1169+
if isinstance(user, User):
1170+
data = user.as_dict()
1171+
# TODO: Validation. Note, at least here I will just allow a dict instead of the pointless cast<->cast
1172+
# that you see for other DSO types - still figuring out the best way
1173+
params = None
1174+
if token is not None:
1175+
params = {'token': token}
1176+
return User(api_resource=parse_json(self.create_dso(url, params=params, data=data)))
1177+
1178+
def delete_user(self, user):
1179+
if not isinstance(user, User):
1180+
print(f'Must be a valid user')
1181+
return None
1182+
return self.delete_dso(user)
1183+
1184+
def get_users(self):
1185+
url = f'{self.API_ENDPOINT}/eperson/epersons'
1186+
users = list()
1187+
r = self.api_get(url)
1188+
r_json = parse_json(response=r)
1189+
if '_embedded' in r_json:
1190+
if 'epersons' in r_json['_embedded']:
1191+
for user_resource in r_json['_embedded']['epersons']:
1192+
users.append(User(user_resource))
1193+
return users
1194+
1195+
def create_group(self, group):
1196+
"""
1197+
Create a group
1198+
@param group: python Group object or Python dict containing all the data and links expected by the REST API
1199+
@return: User object constructed from the API response
1200+
"""
1201+
url = f'{self.API_ENDPOINT}/eperson/groups'
1202+
data = group
1203+
if isinstance(group, Group):
1204+
data = group.as_dict()
1205+
# TODO: Validation. Note, at least here I will just allow a dict instead of the pointless cast<->cast
1206+
# that you see for other DSO types - still figuring out the best way
1207+
return Group(api_resource=parse_json(self.create_dso(url, params=None, data=data)))

dspace_rest_client/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)