From cdb85f354df9a346c6f42a249942906da9275ac2 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Fri, 6 Mar 2026 02:50:53 +0100 Subject: [PATCH 01/11] Make class __init__ methods simpler and more readable --- dspace_rest_client/models.py | 341 +++++++++++++++-------------------- 1 file changed, 144 insertions(+), 197 deletions(-) diff --git a/dspace_rest_client/models.py b/dspace_rest_client/models.py index af7df4e..b8e2a98 100644 --- a/dspace_rest_client/models.py +++ b/dspace_rest_client/models.py @@ -19,45 +19,38 @@ class HALResource: """ Base class to represent HAL+JSON API resources """ - links = {} - type = None - embedded = dict() def __init__(self, api_resource=None): """ Default constructor @param api_resource: optional API resource (JSON) from a GET response or successful POST can populate instance """ + self.links = {} + self.embedded = {} + self.type = None + if api_resource is not None: - if 'type' in api_resource: - self.type = api_resource['type'] - if '_links' in api_resource: - self.links = api_resource['_links'].copy() - if '_embedded' in api_resource: - self.embedded = api_resource['_embedded'].copy() - else: - self.links = {'self': {'href': None}} + self.links = api_resource.get('_links', {}).copy() + self.embedded = api_resource.get('_embedded', {}).copy() + self.type = api_resource.get('type') + else: + self.links = {'self': {'href': None}} class AddressableHALResource(HALResource): - id = None def __init__(self, api_resource=None): super().__init__(api_resource) + self.id = None + if api_resource is not None: - if 'id' in api_resource: - self.id = api_resource['id'] + self.id = api_resource.get('id') def as_dict(self): return {'id': self.id} -class ExternalDataObject(HALResource): +class ExternalDataObject(AddressableHALResource): """ Generic External Data Object as configured in DSpace's external data providers framework """ - id = None - display = None - value = None - externalSource = None - metadata = {} def __init__(self, api_resource=None): """ @@ -65,20 +58,16 @@ def __init__(self, api_resource=None): @param api_resource: optional API resource (JSON) from a GET response or successful POST can populate instance """ super().__init__(api_resource) - + self.display = None + self.value = None + self.externalSource = None self.metadata = {} if api_resource is not None: - if 'id' in api_resource: - self.id = api_resource['id'] - if 'display' in api_resource: - self.display = api_resource['display'] - if 'value' in api_resource: - self.value = api_resource['value'] - if 'externalSource' in api_resource: - self.externalSource = api_resource['externalSource'] - if 'metadata' in api_resource: - self.metadata = api_resource['metadata'].copy() + self.display = api_resource.get('display') + self.value = api_resource.get('value') + self.externalSource = api_resource.get('externalSource') + self.metadata = api_resource.get('metadata').copy() def get_metadata_values(self, field): """ @@ -86,10 +75,7 @@ def get_metadata_values(self, field): @param field: DSpace field, eg. dc.creator @return: list of strings """ - values = [] - if field in self.metadata: - values = self.metadata[field] - return values + return self.metadata.get(field, []) class DSpaceObject(HALResource): @@ -99,13 +85,6 @@ class DSpaceObject(HALResource): operations are included in the dict returned by asDict(). Implements toJSON() as well. This class can be used on its own but is generally expected to be extended by other types: Item, Bitstream, etc. """ - uuid = None - name = None - handle = None - metadata = {} - lastModified = None - type = None - parent = None def __init__(self, api_resource=None, dso=None): """ @@ -113,31 +92,29 @@ def __init__(self, api_resource=None, dso=None): @param api_resource: optional API resource (JSON) from a GET response or successful POST can populate instance """ super().__init__(api_resource) + self.uuid = None + self.name = None + self.handle = None + self.lastModified = None + self.parent = None self.type = None self.metadata = {} if dso is not None: api_resource = dso.as_dict() self.links = dso.links.copy() + if api_resource is not None: - if 'id' in api_resource: - self.id = api_resource['id'] - if 'uuid' in api_resource: - self.uuid = api_resource['uuid'] - if 'type' in api_resource: - self.type = api_resource['type'] - if 'name' in api_resource: - self.name = api_resource['name'] - if 'handle' in api_resource: - self.handle = api_resource['handle'] - if 'metadata' in api_resource: - self.metadata = api_resource['metadata'].copy() - if 'lastModified' in api_resource: - self.lastModified = api_resource['lastModified'] + self.id = api_resource.get('id') + self.uuid = api_resource.get('uuid') + self.type = api_resource.get('type') + self.name = api_resource.get('name') + self.handle = api_resource.get('handle') + self.metadata = api_resource.get('metadata', {}).copy() + self.lastModified = api_resource.get('lastModified') # Python interprets _ prefix as private so for now, renaming this and handling it separately # alternatively - each item could implement getters, or a public method to return links - if '_links' in api_resource: - self.links = api_resource['_links'].copy() + self.links = api_resource.get('_links', {}).copy() def add_metadata(self, field, value, language=None, authority=None, confidence=-1, place=None): """ @@ -155,17 +132,14 @@ def add_metadata(self, field, value, language=None, authority=None, confidence=- """ if field is None or value is None: return - if field in self.metadata: - values = self.metadata[field] - # Ensure we don't accidentally duplicate place value. If this place already exists, the user - # should use a patch operation or we should allow another way to re-order / re-calc place? - # For now, we'll just set place to none if it matches an existing place - for v in values: - if v['place'] == place: - place = None - break - else: - values = [] + values = self.metadata.get(field, []) + # Ensure we don't accidentally duplicate place value. If this place already exists, the user + # should use a patch operation or we should allow another way to re-order / re-calc place? + # For now, we'll just set place to none if it matches an existing place + for v in values: + if v['place'] == place: + place = None + break values.append({"value": value, "language": language, "authority": authority, "confidence": confidence, "place": place}) self.metadata[field] = values @@ -218,17 +192,17 @@ class Item(SimpleDSpaceObject): """ Extends DSpaceObject to implement specific attributes and functions for items """ - type = 'item' - inArchive = False - discoverable = False - withdrawn = False - metadata = {} def __init__(self, api_resource=None, dso=None): """ Default constructor. Call DSpaceObject init then set item-specific attributes @param api_resource: API result object to use as initial data """ + self.type = 'item' + self.inArchive = False + self.discoverable = False + self.withdrawn = False + self.metadata = {} if dso is not None: api_resource = dso.as_dict() super().__init__(dso=dso) @@ -237,9 +211,9 @@ def __init__(self, api_resource=None, dso=None): if api_resource is not None: self.type = 'item' - self.inArchive = api_resource['inArchive'] if 'inArchive' in api_resource else True - self.discoverable = api_resource['discoverable'] if 'discoverable' in api_resource else False - self.withdrawn = api_resource['withdrawn'] if 'withdrawn' in api_resource else False + self.inArchive = api_resource.get('inArchive', True) + self.discoverable = api_resource.get('discoverable', False) + self.withdrawn = api_resource.get('withdrawn', False) def get_metadata_values(self, field): """ @@ -247,10 +221,7 @@ def get_metadata_values(self, field): @param field: DSpace field, eg. dc.creator @return: list of strings """ - values = [] - if field in self.metadata: - values = self.metadata[field] - return values + return self.metadata.get(field, []) def as_dict(self): """ @@ -274,7 +245,6 @@ class Community(SimpleDSpaceObject): """ Extends DSpaceObject to implement specific attributes and functions for communities """ - type = 'community' def __init__(self, api_resource=None): """ @@ -299,7 +269,6 @@ class Collection(SimpleDSpaceObject): """ Extends DSpaceObject to implement specific attributes and functions for collections """ - type = 'collection' def __init__(self, api_resource=None): """ @@ -323,7 +292,6 @@ class Bundle(DSpaceObject): """ Extends DSpaceObject to implement specific attributes and functions for bundles """ - type = 'bundle' def __init__(self, api_resource=None): """ @@ -347,15 +315,6 @@ class Bitstream(DSpaceObject): """ Extends DSpaceObject to implement specific attributes and functions for bundles """ - type = 'bitstream' - # Bitstream has a few extra fields specific to file storage - bundleName = None - sizeBytes = None - checkSum = { - 'checkSumAlgorithm': 'MD5', - 'value': None - } - sequenceId = None def __init__(self, api_resource=None): """ @@ -364,14 +323,20 @@ def __init__(self, api_resource=None): """ super().__init__(api_resource) self.type = 'bitstream' - if 'bundleName' in api_resource: - self.bundleName = api_resource['bundleName'] - if 'sizeBytes' in api_resource: - self.sizeBytes = api_resource['sizeBytes'] - if 'checkSum' in api_resource: - self.checkSum = api_resource['checkSum'] - if 'sequenceId' in api_resource: - self.sequenceId = api_resource['sequenceId'] + # Bitstream has a few extra fields specific to file storage + self.bundleName = None + self.sizeBytes = None + self.checkSum = { + 'checkSumAlgorithm': 'MD5', + 'value': None + } + self.sequenceId = None + + if api_resource is not None: + self.bundleName = api_resource.get('bundleName') + self.sizeBytes = api_resource.get('sizeBytes') + self.checkSum = api_resource.get('checkSum', self.checkSum) + self.sequenceId = api_resource.get('sequenceId') def as_dict(self): """ @@ -399,28 +364,24 @@ class BitstreamFormat(AddressableHALResource): "type": "bitstreamformat" } """ - shortDescription = None - description = None - mimetype = None - supportLevel = None - internal = False - extensions = [] - type = 'bitstreamformat' def __init__(self, api_resource): super(BitstreamFormat, self).__init__(api_resource) - if 'shortDescription' in api_resource: - self.shortDescription = api_resource['shortDescription'] - if 'description' in api_resource: - self.description = api_resource['description'] - if 'mimetype' in api_resource: - self.mimetype = api_resource['mimetype'] - if 'supportLevel' in api_resource: - self.supportLevel = api_resource['supportLevel'] - if 'internal' in api_resource: - self.internal = api_resource['internal'] - if 'extensions' in api_resource: - self.extensions = api_resource['extensions'].copy() + self.shortDescription = None + self.description = None + self.mimetype = None + self.supportLevel = None + self.internal = False + self.extensions = [] + self.type = 'bitstreamformat' + + if api_resource is not None: + self.shortDescription = api_resource.get('shortDescription') + self.description = api_resource.get('description') + self.mimetype = api_resource.get('mimetype') + self.supportLevel = api_resource.get('supportLevel') + self.internal = api_resource.get('internal') + self.extensions = api_resource.get('extensions', {}).copy() def as_dict(self): parent_dict = super(BitstreamFormat, self).as_dict() @@ -439,9 +400,6 @@ class Group(DSpaceObject): """ Extends DSpaceObject to implement specific attributes and methods for groups (aka. EPersonGroups) """ - type = 'group' - name = None - permanent = False def __init__(self, api_resource=None): """ @@ -449,11 +407,13 @@ def __init__(self, api_resource=None): @param api_resource: API result object to use as initial data """ super().__init__(api_resource) + self.name = None + self.permanent = False self.type = 'group' - if 'name' in api_resource: - self.name = api_resource['name'] - if 'permanent' in api_resource: - self.permanent = api_resource['permanent'] + + if api_resource is not None: + self.name = api_resource.get('name') + self.permanent = api_resource.get('permanent') def as_dict(self): """ @@ -469,14 +429,6 @@ class User(SimpleDSpaceObject): """ Extends DSpaceObject to implement specific attributes and methods for users (aka. EPersons) """ - type = 'user' - name = None - netid = None - lastActive = None - canLogIn = False - email = None - requireCertificate = False - selfRegistered = False def __init__(self, api_resource=None): """ @@ -484,21 +436,23 @@ def __init__(self, api_resource=None): @param api_resource: API result object to use as initial data """ super().__init__(api_resource) + self.name = None + self.netid = None + self.lastActive = None + self.canLogIn = False + self.email = None + self.requireCertificate = False + self.selfRegistered = False self.type = 'user' - if 'name' in api_resource: - self.name = api_resource['name'] - if 'netid' in api_resource: - self.netid = api_resource['netid'] - if 'lastActive' in api_resource: - self.lastActive = api_resource['lastActive'] - if 'canLogIn' in api_resource: - self.canLogIn = api_resource['canLogIn'] - if 'email' in api_resource: - self.email = api_resource['email'] - if 'requireCertificate' in api_resource: - self.requireCertificate = api_resource['requireCertificate'] - if 'selfRegistered' in api_resource: - self.selfRegistered = api_resource['selfRegistered'] + + if api_resource is not None: + self.name = api_resource.get('name') + self.netid = api_resource.get('netid') + self.lastActive = api_resource.get('lastActive') + self.canLogIn = api_resource.get('canLogIn') + self.email = api_resource.get('email') + self.requireCertificate = api_resource.get('requireCertificate') + self.selfRegistered = api_resource.get('selfRegistered') def as_dict(self): """ @@ -512,21 +466,19 @@ def as_dict(self): return {**dso_dict, **user_dict} class InProgressSubmission(AddressableHALResource): - lastModified = None - step = None - sections = {} - type = None def __init__(self, api_resource): super().__init__(api_resource) - if 'lastModified' in api_resource: - self.lastModified = api_resource['lastModified'] - if 'step' in api_resource: - self.step = api_resource['lastModified'] - if 'sections' in api_resource: - self.sections = api_resource['sections'].copy() - if 'type' in api_resource: - self.lastModified = api_resource['lastModified'] + self.lastModified = None + self.step = None + self.sections = {} + self.type = None + + if api_resource is not None: + self.lastModified = api_resource.get('lastModified') + self.step = api_resource.get('lastModified') + self.sections = api_resource.get('sections', {}).copy() + self.lastModified = api_resource.get('lastModified') def as_dict(self): parent_dict = super().as_dict() @@ -554,10 +506,11 @@ class EntityType(AddressableHALResource): """ def __init__(self, api_resource): super().__init__(api_resource) - if 'label' in api_resource: - self.label = api_resource['label'] - if 'type' in api_resource: - self.label = api_resource['type'] + self.label = None + self.type = 'entitytype' + + if api_resource is not None: + self.label = api_resource.get('label') class RelationshipType(AddressableHALResource): """ @@ -617,24 +570,22 @@ class SearchResult(HALResource): } }, "facets"... (TODO) """ - query = None - scope = None - appliedFilters = [] - type = None def __init__(self, api_resource): super().__init__(api_resource) - if 'lastModified' in api_resource: - self.lastModified = api_resource['lastModified'] - if 'step' in api_resource: - self.step = api_resource['step'] - if 'sections' in api_resource: - self.sections = api_resource['sections'].copy() - if 'type' in api_resource and self.type is not None: - self.type = api_resource['type'] + self.query = None + self.scope = None + self.appliedFilters = [] + self.type = None + + if api_resource is not None: + self.lastModified = api_resource.get('lastModified') + self.step = api_resource.get('step') + self.sections = api_resource.get('sections', {}).copy() + self.type = api_resource.get('type') def as_dict(self): - parent_dict = super().as_dict() + parent_dict = super().__dict__ dict = { 'lastModified': self.lastModified, 'step': self.step, @@ -648,28 +599,24 @@ class ResourcePolicy(AddressableHALResource): A resource policy to control access and authorization to DSpace objects See: https://github.com/DSpace/RestContract/blob/main/resourcepolicies.md """ - type = 'resourcepolicy' - name = None - description = None - policyType = None - action = None - startDate = None - endDate = None def __init__(self, api_resource): super().__init__(api_resource) - if 'name' in api_resource: - self.name = api_resource['name'] - if 'description' in api_resource: - self.description = api_resource['description'] - if 'policyType' in api_resource: - self.policyType = api_resource['policyType'] - if 'action' in api_resource: - self.action = api_resource['action'] - if 'startDate' in api_resource: - self.startDate = api_resource['startDate'] - if 'endDate' in api_resource: - self.endDate = api_resource['endDate'] + self.type = 'resourcepolicy' + self.name = None + self.description = None + self.policyType = None + self.action = None + self.startDate = None + self.endDate = None + + if api_resource is not None: + self.name = api_resource.get('name') + self.description = api_resource.get('description') + self.policyType = api_resource.get('policyType') + self.action = api_resource.get('action') + self.startDate = api_resource.get('startDate') + self.endDate = api_resource.get('endDate') def as_dict(self): hal_dict = super().as_dict() From 240b0fccb37852eb199694f0eb7c27df16195eaf Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Fri, 6 Mar 2026 03:17:28 +0100 Subject: [PATCH 02/11] Move 'type' up to inherited class var from HALResource down --- dspace_rest_client/models.py | 119 +++++++++++++---------------------- 1 file changed, 43 insertions(+), 76 deletions(-) diff --git a/dspace_rest_client/models.py b/dspace_rest_client/models.py index b8e2a98..f4f9c71 100644 --- a/dspace_rest_client/models.py +++ b/dspace_rest_client/models.py @@ -19,6 +19,7 @@ class HALResource: """ Base class to represent HAL+JSON API resources """ + type = None def __init__(self, api_resource=None): """ @@ -27,15 +28,16 @@ def __init__(self, api_resource=None): """ self.links = {} self.embedded = {} - self.type = None if api_resource is not None: self.links = api_resource.get('_links', {}).copy() self.embedded = api_resource.get('_embedded', {}).copy() - self.type = api_resource.get('type') else: self.links = {'self': {'href': None}} + def as_dict(self): + return {'type': self.type} + class AddressableHALResource(HALResource): def __init__(self, api_resource=None): super().__init__(api_resource) @@ -45,12 +47,17 @@ def __init__(self, api_resource=None): self.id = api_resource.get('id') def as_dict(self): - return {'id': self.id} + parent_dict = super().as_dict() + this_dict = {'id': self.id} + return {**parent_dict, **this_dict} class ExternalDataObject(AddressableHALResource): """ Generic External Data Object as configured in DSpace's external data providers framework + TODO: this is also known as externalSourceEntry? Should the class name be modified or aliased? + Or should we draw a subtle distinction between the two even if they share the same model """ + type = "externalSourceEntry" def __init__(self, api_resource=None): """ @@ -77,8 +84,17 @@ def get_metadata_values(self, field): """ return self.metadata.get(field, []) + def as_dict(self): + parent_dict = super().as_dict() + edo_dict = { + 'display': self.display, + 'value': self.value, + 'externalSource': self.externalSource, + 'metadata': self.metadata, + } + return {**parent_dict, **edo_dict} -class DSpaceObject(HALResource): +class DSpaceObject(AddressableHALResource): """ Base class to represent DSpaceObject API resources The variables here are present in an _embedded response and the ones required for POST / PUT / PATCH @@ -97,7 +113,6 @@ def __init__(self, api_resource=None, dso=None): self.handle = None self.lastModified = None self.parent = None - self.type = None self.metadata = {} if dso is not None: @@ -107,7 +122,6 @@ def __init__(self, api_resource=None, dso=None): if api_resource is not None: self.id = api_resource.get('id') self.uuid = api_resource.get('uuid') - self.type = api_resource.get('type') self.name = api_resource.get('name') self.handle = api_resource.get('handle') self.metadata = api_resource.get('metadata', {}).copy() @@ -165,14 +179,15 @@ def as_dict(self): Return custom dict of this DSpaceObject with specific attributes included (no _links, etc.) @return: dict of this DSpaceObject for API use """ - return { + parent_dict = super().as_dict() + dso_dict = { 'uuid': self.uuid, 'name': self.name, 'handle': self.handle, 'metadata': self.metadata, - 'lastModified': self.lastModified, - 'type': self.type, + 'lastModified': self.lastModified } + return {**parent_dict, **dso_dict} def to_json(self): return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=None) @@ -192,13 +207,13 @@ class Item(SimpleDSpaceObject): """ Extends DSpaceObject to implement specific attributes and functions for items """ + type = "item" def __init__(self, api_resource=None, dso=None): """ Default constructor. Call DSpaceObject init then set item-specific attributes @param api_resource: API result object to use as initial data """ - self.type = 'item' self.inArchive = False self.discoverable = False self.withdrawn = False @@ -210,7 +225,6 @@ def __init__(self, api_resource=None, dso=None): super().__init__(api_resource) if api_resource is not None: - self.type = 'item' self.inArchive = api_resource.get('inArchive', True) self.discoverable = api_resource.get('discoverable', False) self.withdrawn = api_resource.get('withdrawn', False) @@ -245,6 +259,7 @@ class Community(SimpleDSpaceObject): """ Extends DSpaceObject to implement specific attributes and functions for communities """ + type = 'community' def __init__(self, api_resource=None): """ @@ -252,7 +267,6 @@ def __init__(self, api_resource=None): @param api_resource: API result object to use as initial data """ super().__init__(api_resource) - self.type = 'community' def as_dict(self): """ @@ -269,6 +283,7 @@ class Collection(SimpleDSpaceObject): """ Extends DSpaceObject to implement specific attributes and functions for collections """ + type = "collection" def __init__(self, api_resource=None): """ @@ -276,7 +291,6 @@ def __init__(self, api_resource=None): @param api_resource: API result object to use as initial data """ super().__init__(api_resource) - self.type = 'collection' def as_dict(self): dso_dict = super().as_dict() @@ -292,6 +306,7 @@ class Bundle(DSpaceObject): """ Extends DSpaceObject to implement specific attributes and functions for bundles """ + type = "collection" def __init__(self, api_resource=None): """ @@ -299,7 +314,6 @@ def __init__(self, api_resource=None): @param api_resource: API result object to use as initial data """ super().__init__(api_resource) - self.type = 'bundle' def as_dict(self): """ @@ -315,6 +329,7 @@ class Bitstream(DSpaceObject): """ Extends DSpaceObject to implement specific attributes and functions for bundles """ + type = "bitstream" def __init__(self, api_resource=None): """ @@ -322,7 +337,6 @@ def __init__(self, api_resource=None): @param api_resource: API result object to use as initial data """ super().__init__(api_resource) - self.type = 'bitstream' # Bitstream has a few extra fields specific to file storage self.bundleName = None self.sizeBytes = None @@ -364,6 +378,7 @@ class BitstreamFormat(AddressableHALResource): "type": "bitstreamformat" } """ + type = "bitstreamformat" def __init__(self, api_resource): super(BitstreamFormat, self).__init__(api_resource) @@ -373,7 +388,6 @@ def __init__(self, api_resource): self.supportLevel = None self.internal = False self.extensions = [] - self.type = 'bitstreamformat' if api_resource is not None: self.shortDescription = api_resource.get('shortDescription') @@ -391,8 +405,7 @@ def as_dict(self): 'mimetype': self.mimetype, 'supportLevel': self.supportLevel, 'internal': self.internal, - 'extensions': self.extensions, - 'type': self.type + 'extensions': self.extensions } return {**parent_dict, **dict} @@ -400,6 +413,7 @@ class Group(DSpaceObject): """ Extends DSpaceObject to implement specific attributes and methods for groups (aka. EPersonGroups) """ + type = 'group' def __init__(self, api_resource=None): """ @@ -409,7 +423,6 @@ def __init__(self, api_resource=None): super().__init__(api_resource) self.name = None self.permanent = False - self.type = 'group' if api_resource is not None: self.name = api_resource.get('name') @@ -429,6 +442,7 @@ class User(SimpleDSpaceObject): """ Extends DSpaceObject to implement specific attributes and methods for users (aka. EPersons) """ + type = "eperson" def __init__(self, api_resource=None): """ @@ -443,7 +457,6 @@ def __init__(self, api_resource=None): self.email = None self.requireCertificate = False self.selfRegistered = False - self.type = 'user' if api_resource is not None: self.name = api_resource.get('name') @@ -472,7 +485,6 @@ def __init__(self, api_resource): self.lastModified = None self.step = None self.sections = {} - self.type = None if api_resource is not None: self.lastModified = api_resource.get('lastModified') @@ -486,11 +498,11 @@ def as_dict(self): 'lastModified': self.lastModified, 'step': self.step, 'sections': self.sections, - 'type': self.type } return {**parent_dict, **dict} class WorkspaceItem(InProgressSubmission): + type = 'workspaceitem' def __init__(self, api_resource): super().__init__(api_resource) @@ -504,10 +516,11 @@ class EntityType(AddressableHALResource): used in entities and relationships. For example, Publication, Person, Project and Journal are all common entity types used in DSpace 7+ """ + type = "entitytype" + def __init__(self, api_resource): super().__init__(api_resource) self.label = None - self.type = 'entitytype' if api_resource is not None: self.label = api_resource.get('label') @@ -516,73 +529,27 @@ class RelationshipType(AddressableHALResource): """ TODO: RelationshipType """ + type = "relationshiptype" + def __init__(self, api_resource): super().__init__(api_resource) class SearchResult(HALResource): """ - { - "query":"my query", - "scope":"9076bd16-e69a-48d6-9e41-0238cb40d863", - "appliedFilters": [ - { - "filter" : "title", - "operator" : "notcontains", - "value" : "abcd", - "label" : "abcd" - }, - { - "filter" : "author", - "operator" : "authority", - "value" : "1234", - "label" : "Smith, Donald" - } - ], - "sort" : { - "by" : "dc.date.issued", - "order" : "asc" - }, - "_embedded" : { - "searchResults": { - "_embedded": { - "objects" : [...], - }, - - "_links": { - "first": { - "href": "/api/discover/search/objects?query=my+query&scope=9076bd16-e69a-48d6-9e41-0238cb40d863&f.title=abcd,notcontains&f.author=1234,authority&page=0&size=5" - }, - "self": { - "href": "/api/discover/search/objects?query=my+query&scope=9076bd16-e69a-48d6-9e41-0238cb40d863&f.title=abcd,notcontains&f.author=1234,authority&page=0&size=5" - }, - "next": { - "href": "/api/discover/search/objects?query=my+query&scope=9076bd16-e69a-48d6-9e41-0238cb40d863&f.title=abcd,notcontains&f.author=1234,authority&page=1&size=5" - }, - "last": { - "href": "/api/discover/search/objects?query=my+query&scope=9076bd16-e69a-48d6-9e41-0238cb40d863&f.title=abcd,notcontains&f.author=1234,authority&page=2&size=5" - } - }, - "page": { - "number": 0, - "size": 20, - "totalElements": 12, - "totalPages": 3 - } - }, "facets"... (TODO) + Discover search result """ + type = "discover" def __init__(self, api_resource): super().__init__(api_resource) self.query = None self.scope = None self.appliedFilters = [] - self.type = None if api_resource is not None: self.lastModified = api_resource.get('lastModified') self.step = api_resource.get('step') self.sections = api_resource.get('sections', {}).copy() - self.type = api_resource.get('type') def as_dict(self): parent_dict = super().__dict__ @@ -590,7 +557,6 @@ def as_dict(self): 'lastModified': self.lastModified, 'step': self.step, 'sections': self.sections, - 'type': self.type } return {**parent_dict, **dict} @@ -599,10 +565,10 @@ class ResourcePolicy(AddressableHALResource): A resource policy to control access and authorization to DSpace objects See: https://github.com/DSpace/RestContract/blob/main/resourcepolicies.md """ + type = "resourcepolicy" def __init__(self, api_resource): super().__init__(api_resource) - self.type = 'resourcepolicy' self.name = None self.description = None self.policyType = None @@ -620,7 +586,8 @@ def __init__(self, api_resource): def as_dict(self): hal_dict = super().as_dict() - rp_dict = {'name': self.name, 'description': self.description, 'policyType': self.policyType, 'action': self.action, 'startDate': self.startDate, 'endDate': self.endDate} + rp_dict = {'name': self.name, 'description': self.description, 'policyType': self.policyType, + 'action': self.action, 'startDate': self.startDate, 'endDate': self.endDate} return {**hal_dict, **rp_dict} From 9f4eed8caa9da2e07bddf1f5f3e7536a64c918f1 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Fri, 6 Mar 2026 03:18:17 +0100 Subject: [PATCH 03/11] Fix bad api resource get for InProgressSubmission --- dspace_rest_client/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace_rest_client/models.py b/dspace_rest_client/models.py index f4f9c71..da7b740 100644 --- a/dspace_rest_client/models.py +++ b/dspace_rest_client/models.py @@ -488,7 +488,7 @@ def __init__(self, api_resource): if api_resource is not None: self.lastModified = api_resource.get('lastModified') - self.step = api_resource.get('lastModified') + self.step = api_resource.get('step') self.sections = api_resource.get('sections', {}).copy() self.lastModified = api_resource.get('lastModified') From 4f9cdcb3bc12166cfdfc33978fb0821af8e31712 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Fri, 6 Mar 2026 03:27:46 +0100 Subject: [PATCH 04/11] fix type in bundle type, fix SearchResult dict handling --- dspace_rest_client/models.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/dspace_rest_client/models.py b/dspace_rest_client/models.py index da7b740..9e2da95 100644 --- a/dspace_rest_client/models.py +++ b/dspace_rest_client/models.py @@ -306,7 +306,7 @@ class Bundle(DSpaceObject): """ Extends DSpaceObject to implement specific attributes and functions for bundles """ - type = "collection" + type = "bundle" def __init__(self, api_resource=None): """ @@ -547,18 +547,19 @@ def __init__(self, api_resource): self.appliedFilters = [] if api_resource is not None: - self.lastModified = api_resource.get('lastModified') - self.step = api_resource.get('step') - self.sections = api_resource.get('sections', {}).copy() + self.query = api_resource.get('query') + self.scope = api_resource.get('scope') + self.appliedFilters = api_resource.get('appliedFilters', []).copy() def as_dict(self): - parent_dict = super().__dict__ - dict = { - 'lastModified': self.lastModified, - 'step': self.step, - 'sections': self.sections, + parent_dict = super().as_dict() + this_dict = { + 'query': self.query, + 'scope': self.scope, + 'appliedFilters': self.appliedFilters } - return {**parent_dict, **dict} + + return {**parent_dict, **this_dict} class ResourcePolicy(AddressableHALResource): """ From b208f11f2632a84f1bbbc58caccf6d63119e3386 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Fri, 6 Mar 2026 03:29:11 +0100 Subject: [PATCH 05/11] move get_metadata_values from item->DSpaceObject --- dspace_rest_client/models.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/dspace_rest_client/models.py b/dspace_rest_client/models.py index 9e2da95..aae803a 100644 --- a/dspace_rest_client/models.py +++ b/dspace_rest_client/models.py @@ -130,6 +130,14 @@ def __init__(self, api_resource=None, dso=None): # alternatively - each item could implement getters, or a public method to return links self.links = api_resource.get('_links', {}).copy() + def get_metadata_values(self, field): + """ + Return metadata values as simple list of strings + @param field: DSpace field, eg. dc.creator + @return: list of strings + """ + return self.metadata.get(field, []) + def add_metadata(self, field, value, language=None, authority=None, confidence=-1, place=None): """ Add metadata to a DSO. This is performed on the local object only, it is not an API operation (see patch) @@ -229,13 +237,7 @@ def __init__(self, api_resource=None, dso=None): self.discoverable = api_resource.get('discoverable', False) self.withdrawn = api_resource.get('withdrawn', False) - def get_metadata_values(self, field): - """ - Return metadata values as simple list of strings - @param field: DSpace field, eg. dc.creator - @return: list of strings - """ - return self.metadata.get(field, []) + def as_dict(self): """ From 0cf23dcc9d6f01f3ca5bf1e6e8249283faf83f91 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Fri, 6 Mar 2026 03:36:36 +0100 Subject: [PATCH 06/11] fix base as_dict return type, collection as_dict docstring --- dspace_rest_client/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dspace_rest_client/models.py b/dspace_rest_client/models.py index aae803a..861a1d9 100644 --- a/dspace_rest_client/models.py +++ b/dspace_rest_client/models.py @@ -10,6 +10,7 @@ @author Kim Shepherd """ import json +from typing import Any __all__ = ['DSpaceObject', 'HALResource', 'ExternalDataObject', 'SimpleDSpaceObject', 'Community', 'Collection', 'Item', 'Bundle', 'Bitstream', 'BitstreamFormat', 'User', 'Group'] @@ -35,7 +36,7 @@ def __init__(self, api_resource=None): else: self.links = {'self': {'href': None}} - def as_dict(self): + def as_dict(self) -> dict[str, Any]: return {'type': self.type} class AddressableHALResource(HALResource): @@ -295,11 +296,11 @@ def __init__(self, api_resource=None): super().__init__(api_resource) def as_dict(self): - dso_dict = super().as_dict() """ Return a dict representation of this Collection, based on super with collection-specific attributes added @return: dict of Item for API use """ + dso_dict = super().as_dict() collection_dict = {} return {**dso_dict, **collection_dict} @@ -492,7 +493,6 @@ def __init__(self, api_resource): self.lastModified = api_resource.get('lastModified') self.step = api_resource.get('step') self.sections = api_resource.get('sections', {}).copy() - self.lastModified = api_resource.get('lastModified') def as_dict(self): parent_dict = super().as_dict() From 15916c9863aa306e80b07d2706b5a3a0f93e65d1 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Fri, 6 Mar 2026 03:53:23 +0100 Subject: [PATCH 07/11] Fix None checks before accessing r_json / other response objects --- dspace_rest_client/client.py | 41 ++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/dspace_rest_client/client.py b/dspace_rest_client/client.py index 724bef6..bb30552 100644 --- a/dspace_rest_client/client.py +++ b/dspace_rest_client/client.py @@ -92,7 +92,6 @@ class DSpaceClient: """ # Set up basic environment, variables - session = None API_ENDPOINT = "http://localhost:8080/server/api" SOLR_ENDPOINT = "http://localhost:8983/solr" SOLR_AUTH = None @@ -112,7 +111,6 @@ class DSpaceClient: SOLR_AUTH = os.environ["SOLR_AUTH"] if "USER_AGENT" in os.environ: USER_AGENT = os.environ["USER_AGENT"] - verbose = False ITER_PAGE_SIZE = 20 PROXY_DICT = dict(http=os.environ["PROXY_URL"],https=os.environ["PROXY_URL"]) if "PROXY_URL" in os.environ else dict() @@ -284,7 +282,7 @@ def authenticate(self, retry=False): # Update headers with new bearer token if present if "Authorization" in r.headers: self.session.headers.update( - {"Authorization": r.headers.get("Authorization")} + {"Authorization": r.headers["Authorization"]} ) # Get and check authentication status @@ -294,7 +292,7 @@ def authenticate(self, retry=False): ) if r.status_code == 200: r_json = parse_json(r) - if "authenticated" in r_json and r_json["authenticated"] is True: + if r_json is not None and "authenticated" in r_json and r_json["authenticated"] is True: logging.info("Authenticated successfully as %s", self.USERNAME) return r_json["authenticated"] @@ -503,6 +501,8 @@ def search_objects( r_json = self.fetch_resource(url=url, params={**params, **filters}) + if r_json is None: + return dsos # instead lots of 'does this key exist, etc etc' checks, just go for it and wrap in a try? try: results = r_json["_embedded"]["searchResult"]["_embedded"]["objects"] @@ -611,8 +611,10 @@ def create_dso(self, url, params, data, embeds=None): if r.status_code == 201: # 201 Created - success! new_dso = parse_json(r) + if new_dso is None: + return r logging.info( - "%s %s created successfully!", new_dso["type"], new_dso["uuid"] + "%s %s created successfully!", new_dso.get("type"), new_dso.get("uuid") ) else: logging.error( @@ -702,7 +704,7 @@ def delete_dso(self, dso=None, url=None, params=None): ) return None except ValueError as e: - logging.error("Error deleting DSO %s: %s", dso.uuid, e) + logging.error("Error deleting DSO %s: %s", url, e) return None # PAGINATION @@ -739,7 +741,7 @@ def get_bundles( try: if single_result: bundles.append(Bundle(r_json)) - if not single_result: + if not single_result and r_json is not None: resources = r_json["_embedded"]["bundles"] for resource in resources: bundles.append(Bundle(resource)) @@ -825,7 +827,7 @@ def get_bitstreams( params["sort"] = sort r_json = self.fetch_resource(url, params=params) - if "_embedded" in r_json: + if r_json is not None and "_embedded" in r_json: if "bitstreams" in r_json["_embedded"]: bitstreams = [] for bitstream_resource in r_json["_embedded"]["bitstreams"]: @@ -891,6 +893,10 @@ def create_bitstream( # TODO: Better error detection and handling for file reading if metadata is None: metadata = {} + if bundle is None: + logging.error("Cannot create bitstream without bundle") + return None + url = f"{self.API_ENDPOINT}/core/bundles/{bundle.uuid}/bitstreams" try: @@ -923,7 +929,7 @@ def create_bitstream( # we should enhance self.api_post to be able to send files and use our decorators if r.status_code == 403: r_json = parse_json(r) - if "message" in r_json and "CSRF token" in r_json["message"]: + if r_json is not None and "message" in r_json and "CSRF token" in r_json["message"]: if retry: logging.error("Already retried... something must be wrong") else: @@ -1000,11 +1006,11 @@ def get_communities( r_json = self.fetch_resource(url, params) # Empty list communities = [] - if "_embedded" in r_json: + if r_json is not None and "_embedded" in r_json: if "communities" in r_json["_embedded"]: for community_resource in r_json["_embedded"]["communities"]: communities.append(Community(community_resource)) - elif "uuid" in r_json: + elif r_json is not None and "uuid" in r_json: # This is a single communities communities.append(Community(r_json)) # Return list (populated or empty) @@ -1089,12 +1095,12 @@ def get_collections( r_json = self.fetch_resource(url, params=params) # Empty list collections = [] - if "_embedded" in r_json: + if r_json is not None and "_embedded" in r_json: # This is a list of collections if "collections" in r_json["_embedded"]: for collection_resource in r_json["_embedded"]["collections"]: collections.append(Collection(collection_resource)) - elif "uuid" in r_json: + elif r_json is not None and "uuid" in r_json: # This is a single collection collections.append(Collection(r_json)) @@ -1167,12 +1173,12 @@ def get_items(self, embeds=None): r_json = self.fetch_resource(url, params=parse_params(embeds=embeds)) # Empty list items = [] - if "_embedded" in r_json: + if r_json is not None and "_embedded" in r_json: # This is a list of items if "items" in r_json["_embedded"]: for item_resource in r_json["_embedded"]["items"]: items.append(Item(item_resource)) - elif "uuid" in r_json: + elif r_json is not None and "uuid" in r_json: # This is a single item items.append(Item(r_json)) @@ -1355,7 +1361,7 @@ def get_users(self, page=0, size=20, sort=None, embeds=None): params["sort"] = sort r = self.api_get(url, params=params) r_json = parse_json(response=r) - if "_embedded" in r_json: + if r_json is not None and "_embedded" in r_json: if "epersons" in r_json["_embedded"]: for user_resource in r_json["_embedded"]["epersons"]: users.append(User(user_resource)) @@ -1544,6 +1550,9 @@ def create_resource_policy(self, resource_policy, parent=None, eperson=None, gro if r.status_code == 200 or r.status_code == 201: # 200 OK or 201 Created means Created - success! (201 is used now, 200 perhaps in teh past?) new_policy = parse_json(r) + if new_policy is None: + logging.error("Response containing new resource policy is empty or invalid") + return None logging.info("%s %s created successfully!", new_policy["type"], new_policy["id"]) return ResourcePolicy(api_resource=new_policy) From 4eed6f9f2342877069bae02f805ac35b3e98730f Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Fri, 6 Mar 2026 04:12:56 +0100 Subject: [PATCH 08/11] Swap self/do_paginate params for better static analysis/linting --- dspace_rest_client/client.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/dspace_rest_client/client.py b/dspace_rest_client/client.py index bb30552..7415637 100644 --- a/dspace_rest_client/client.py +++ b/dspace_rest_client/client.py @@ -121,6 +121,7 @@ class PatchOperation: REPLACE = "replace" MOVE = "move" + @staticmethod def paginated(embed_name, item_constructor, embedding=lambda x: x): """ @param embed_name: The key under '_embedded' in the JSON response that contains the @@ -151,7 +152,7 @@ def do_paginate(url, params): else: url = None - return fun(do_paginate, self, *args, **kwargs) + return fun(self, do_paginate, *args, **kwargs) return decorated return decorator @@ -523,8 +524,8 @@ def search_objects( embedding=lambda x: x["_embedded"]["searchResult"], ) def search_objects_iter( - do_paginate, self, + do_paginate, query=None, scope=None, filters=None, @@ -751,7 +752,7 @@ def get_bundles( return bundles @paginated("bundles", Bundle) - def get_bundles_iter(do_paginate, self, parent, sort=None, embeds=None): + def get_bundles_iter(self, do_paginate, parent, sort=None, embeds=None): """ Get bundles for an item, automatically handling pagination by requesting the next page when all items from one page have been consumed @param parent: python Item object, from which the UUID will be referenced in the URL. @@ -836,7 +837,7 @@ def get_bitstreams( return bitstreams @paginated("bitstreams", Bitstream) - def get_bitstreams_iter(do_paginate, self, bundle, sort=None, embeds=None): + def get_bitstreams_iter(self, do_paginate, bundle, sort=None, embeds=None): """ Get all bitstreams for a specific bundle, automatically handling pagination by requesting the next page when all items from one page have been consumed @param bundle: A python Bundle object to parse for bitstream links to retrieve @@ -900,7 +901,7 @@ def create_bitstream( url = f"{self.API_ENDPOINT}/core/bundles/{bundle.uuid}/bitstreams" try: - with smart_open.open(path, "rb") as file_obj: + with smart_open.open(path, "rb") as file_obj: # type: ignore[attr-defined] file = (name, file_obj.read(), mime) files = {"file": file} properties = {"name": name, "metadata": metadata, "bundleName": bundle.name} @@ -1017,7 +1018,7 @@ def get_communities( return communities @paginated("communities", Community) - def get_communities_iter(do_paginate, self, sort=None, top=False, embeds=None): + def get_communities_iter(self, do_paginate, sort=None, top=False, embeds=None): """ Get communities as an iterator, automatically handling pagination by requesting the next page when all items from one page have been consumed @param top: whether to restrict search to top communities (default: false) @@ -1108,7 +1109,7 @@ def get_collections( return collections @paginated("collections", Collection) - def get_collections_iter(do_paginate, self, community=None, sort=None, embeds=None): + def get_collections_iter(self, do_paginate, community=None, sort=None, embeds=None): """ Get collections as an iterator, automatically handling pagination by requesting the next page when all items from one page have been consumed @param community: Community object. If present, collections for a community @@ -1368,7 +1369,7 @@ def get_users(self, page=0, size=20, sort=None, embeds=None): return users @paginated("epersons", User) - def get_users_iter(do_paginate, self, sort=None, embeds=None): + def get_users_iter(self, do_paginate, sort=None, embeds=None): """ Get an iterator of users (epersons) in the DSpace instance, automatically handling pagination by requesting the next page when all items from one page have been consumed @param sort: Optional sort parameter @@ -1383,7 +1384,7 @@ def get_users_iter(do_paginate, self, sort=None, embeds=None): return do_paginate(url, params) @paginated("groups", Group) - def search_groups_by_metadata_iter(do_paginate, self, query, embeds=None): + def search_groups_by_metadata_iter(self, do_paginate, query, embeds=None): """ Search for groups by metadata @param query: Search query (UUID or group name) @@ -1501,7 +1502,7 @@ def resolve_identifier_to_dso(self, identifier=None): logging.error(f"Error resolving identifier {identifier} to DSO: {r.status_code}") @paginated("resourcepolicies", ResourcePolicy) - def get_resource_policies_iter(do_paginate, self, parent=None, action=None, embeds=None): + def get_resource_policies_iter(self, do_paginate, parent=None, action=None, embeds=None): """ Get resource policies (as an iterator) for a given parent object and action @param parent: UUID of an object to which the policy applies From 49b6678204c90b2e7f21e71f7d20dcf3c1bdff0f Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Fri, 6 Mar 2026 04:14:52 +0100 Subject: [PATCH 09/11] cast FileTypeProxy to avoid spurious static analysis errs it does support __enter__ and __exit__ but at runtime --- dspace_rest_client/client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dspace_rest_client/client.py b/dspace_rest_client/client.py index 7415637..46b9955 100644 --- a/dspace_rest_client/client.py +++ b/dspace_rest_client/client.py @@ -26,6 +26,7 @@ from requests import Request import pysolr import smart_open +from typing import cast, IO from .models import ( SimpleDSpaceObject, @@ -901,7 +902,7 @@ def create_bitstream( url = f"{self.API_ENDPOINT}/core/bundles/{bundle.uuid}/bitstreams" try: - with smart_open.open(path, "rb") as file_obj: # type: ignore[attr-defined] + with cast(IO[bytes], smart_open.open(path, "rb")) as file_obj: file = (name, file_obj.read(), mime) files = {"file": file} properties = {"name": name, "metadata": metadata, "bundleName": bundle.name} From 8f30b62083592bab86d4c7c2ded3e9f103513320 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Thu, 12 Mar 2026 04:39:47 +0100 Subject: [PATCH 10/11] Add javadoc and rest contract URLs for each data model --- dspace_rest_client/models.py | 113 ++++++++++++++++++++++++++--------- 1 file changed, 86 insertions(+), 27 deletions(-) diff --git a/dspace_rest_client/models.py b/dspace_rest_client/models.py index 861a1d9..f7eced3 100644 --- a/dspace_rest_client/models.py +++ b/dspace_rest_client/models.py @@ -101,6 +101,9 @@ class DSpaceObject(AddressableHALResource): The variables here are present in an _embedded response and the ones required for POST / PUT / PATCH operations are included in the dict returned by asDict(). Implements toJSON() as well. This class can be used on its own but is generally expected to be extended by other types: Item, Bitstream, etc. + + Java API model: https://javadoc.io/doc/org.dspace/dspace-api/9.0/org/dspace/content/DSpaceObject.html + Java REST API model: https://javadoc.io/doc/org.dspace/dspace-server-webapp/9.0/org/dspace/app/rest/model/DSpaceObjectRest.html """ def __init__(self, api_resource=None, dso=None): @@ -209,12 +212,19 @@ class SimpleDSpaceObject(DSpaceObject): """ Objects that share similar simple API methods eg. PUT update for full metadata replacement, can have handles, etc. By default this is Item, Community, Collection classes + + Java API model: https://javadoc.io/doc/org.dspace/dspace-api/9.0/org/dspace/content/DSpaceObject.html + Java REST API model: https://javadoc.io/doc/org.dspace/dspace-server-webapp/9.0/org/dspace/app/rest/model/DSpaceObjectRest.html """ class Item(SimpleDSpaceObject): """ - Extends DSpaceObject to implement specific attributes and functions for items + Extends DSpaceObject to implement specific attributes and functions for items. + + Java API model: https://javadoc.io/doc/org.dspace/dspace-api/9.0/org/dspace/content/Item.html + Java REST API model: https://javadoc.io/doc/org.dspace/dspace-server-webapp/9.0/org/dspace/app/rest/model/ItemRest.html + REST endpoint contract: https://github.com/DSpace/RestContract/blob/dspace-9.0/items.md """ type = "item" @@ -260,7 +270,11 @@ def from_dso(cls, dso: DSpaceObject): class Community(SimpleDSpaceObject): """ - Extends DSpaceObject to implement specific attributes and functions for communities + Extends DSpaceObject to implement specific attributes and functions for communities. + + Java API model: https://javadoc.io/doc/org.dspace/dspace-api/9.0/org/dspace/content/Community.html + Java REST API model: https://javadoc.io/doc/org.dspace/dspace-server-webapp/9.0/org/dspace/app/rest/model/CommunityRest.html + REST endpoint contract: https://github.com/DSpace/RestContract/blob/dspace-9.0/communities.md """ type = 'community' @@ -284,7 +298,11 @@ def as_dict(self): class Collection(SimpleDSpaceObject): """ - Extends DSpaceObject to implement specific attributes and functions for collections + Extends DSpaceObject to implement specific attributes and functions for collections. + + Java API model: https://javadoc.io/doc/org.dspace/dspace-api/9.0/org/dspace/content/Collection.html + Java REST API model: https://javadoc.io/doc/org.dspace/dspace-server-webapp/9.0/org/dspace/app/rest/model/CollectionRest.html + REST endpoint contract: https://github.com/DSpace/RestContract/blob/dspace-9.0/collections.md """ type = "collection" @@ -307,7 +325,11 @@ def as_dict(self): class Bundle(DSpaceObject): """ - Extends DSpaceObject to implement specific attributes and functions for bundles + Extends DSpaceObject to implement specific attributes and functions for bundles. + + Java API model: https://javadoc.io/doc/org.dspace/dspace-api/9.0/org/dspace/content/Bundle.html + Java REST API model: https://javadoc.io/doc/org.dspace/dspace-server-webapp/9.0/org/dspace/app/rest/model/BundleRest.html + REST endpoint contract: https://github.com/DSpace/RestContract/blob/dspace-9.0/bundles.md """ type = "bundle" @@ -330,7 +352,11 @@ def as_dict(self): class Bitstream(DSpaceObject): """ - Extends DSpaceObject to implement specific attributes and functions for bundles + Extends DSpaceObject to implement specific attributes and functions for bitstreams. + + Java API model: https://javadoc.io/doc/org.dspace/dspace-api/9.0/org/dspace/content/Bitstream.html + Java REST API model: https://javadoc.io/doc/org.dspace/dspace-server-webapp/9.0/org/dspace/app/rest/model/BitstreamRest.html + REST endpoint contract: https://github.com/DSpace/RestContract/blob/dspace-9.0/bitstreams.md """ type = "bitstream" @@ -367,19 +393,11 @@ def as_dict(self): class BitstreamFormat(AddressableHALResource): """ - Bitstream format: https://github.com/DSpace/RestContract/blob/main/bitstreamformats.md - example: - { - "shortDescription": "XML", - "description": "Extensible Markup Language", - "mimetype": "text/xml", - "supportLevel": "KNOWN", - "internal": false, - "extensions": [ - "xml" - ], - "type": "bitstreamformat" - } + Represents format / MIME metadata for a bitstream. + + Java API model: https://javadoc.io/doc/org.dspace/dspace-api/9.0/org/dspace/content/BitstreamFormat.html + Java REST API model: https://javadoc.io/doc/org.dspace/dspace-server-webapp/9.0/org/dspace/app/rest/model/BitstreamFormatRest.html + REST endpoint contract: https://github.com/DSpace/RestContract/blob/dspace-9.0/bitstreamformats.md """ type = "bitstreamformat" @@ -414,7 +432,11 @@ def as_dict(self): class Group(DSpaceObject): """ - Extends DSpaceObject to implement specific attributes and methods for groups (aka. EPersonGroups) + Extends DSpaceObject to implement specific attributes and methods for groups (aka. EPersonGroups). + + Java API model: https://javadoc.io/doc/org.dspace/dspace-api/9.0/org/dspace/eperson/Group.html + Java REST API model: https://javadoc.io/doc/org.dspace/dspace-server-webapp/9.0/org/dspace/app/rest/model/GroupRest.html + REST endpoint contract: https://github.com/DSpace/RestContract/blob/dspace-9.0/epersongroups.md """ type = 'group' @@ -443,7 +465,13 @@ def as_dict(self): class User(SimpleDSpaceObject): """ - Extends DSpaceObject to implement specific attributes and methods for users (aka. EPersons) + Extends DSpaceObject to implement specific attributes and methods for users (aka. EPersons). + This is one class that is deliberately named differently to the base implementation, out of + protest ;) But perhaps it needs to be aliased or aligned with DSpace API for usability... + + Java API model: https://javadoc.io/doc/org.dspace/dspace-api/9.0/org/dspace/eperson/EPerson.html + Java REST API model: https://javadoc.io/doc/org.dspace/dspace-server-webapp/9.0/org/dspace/app/rest/model/EPersonRest.html + REST endpoint contract: https://github.com/DSpace/RestContract/blob/dspace-9.0/epersons.md """ type = "eperson" @@ -482,6 +510,15 @@ def as_dict(self): return {**dso_dict, **user_dict} class InProgressSubmission(AddressableHALResource): + """ + Extends AddressableHALResource to implement an 'in-progress' item (i.e. workspace or workflow item). + + Java API model: https://javadoc.io/doc/org.dspace/dspace-api/9.0/org/dspace/content/InProgressSubmission.html + Java REST API model (workspace): https://javadoc.io/doc/org.dspace/dspace-server-webapp/9.0/org/dspace/app/rest/model/WorkspaceItemRest.html + Java REST API model (workflow): https://javadoc.io/doc/org.dspace/dspace-server-webapp/9.0/org/dspace/app/rest/model/WorkflowItemRest.html + REST endpoint contract (workspace): https://github.com/DSpace/RestContract/blob/dspace-9.0/workspaceitems.md + REST endpoint contract (workflow): https://github.com/DSpace/RestContract/blob/dspace-9.0/workflowitems.md + """ def __init__(self, api_resource): super().__init__(api_resource) @@ -504,6 +541,13 @@ def as_dict(self): return {**parent_dict, **dict} class WorkspaceItem(InProgressSubmission): + """ + Extends InProgressSubmission to implement a WorkspaceItem. + + Java API model: https://javadoc.io/doc/org.dspace/dspace-api/9.0/org/dspace/content/WorkspaceItem.html + Java REST API model: https://javadoc.io/doc/org.dspace/dspace-server-webapp/9.0/org/dspace/app/rest/model/WorkspaceItemRest.html + REST endpoint contract: https://github.com/DSpace/RestContract/blob/dspace-9.0/workspaceitems.md + """ type = 'workspaceitem' def __init__(self, api_resource): @@ -514,9 +558,12 @@ def as_dict(self): class EntityType(AddressableHALResource): """ - Extends Addressable HAL Resource to model an entity type (aka item type) - used in entities and relationships. For example, Publication, Person, Project and Journal - are all common entity types used in DSpace 7+ + Extends Addressable HAL Resource to model an entity type (aka item type) used in entities and relationships. + For example, Publication, Person, Project and Journal are all common entity types used in DSpace 7+ + + Java API model: https://javadoc.io/doc/org.dspace/dspace-api/9.0/org/dspace/content/EntityType.html + Java REST API model: https://javadoc.io/doc/org.dspace/dspace-server-webapp/9.0/org/dspace/app/rest/model/EntityTypeRest.html + REST endpoint contract: https://github.com/DSpace/RestContract/blob/dspace-9.0/entitytypes.md """ type = "entitytype" @@ -529,7 +576,13 @@ def __init__(self, api_resource): class RelationshipType(AddressableHALResource): """ - TODO: RelationshipType + TODO! Not yet implemented + Extends Addressable HAL Resource to model a relationship type. + For example, isAuthorOfPublication. + + Java API model: https://javadoc.io/doc/org.dspace/dspace-api/9.0/org/dspace/content/RelationshipType.html + Java REST API model: https://javadoc.io/doc/org.dspace/dspace-server-webapp/9.0/org/dspace/app/rest/model/RelationshipTypeRest.html + REST endpoint contract: https://github.com/DSpace/RestContract/blob/dspace-9.0/relationshiptypes.md """ type = "relationshiptype" @@ -538,7 +591,11 @@ def __init__(self, api_resource): class SearchResult(HALResource): """ - Discover search result + A 'Discover' search result, which can embed any kind of addressable object managed by DSpace. + + Java API model: https://javadoc.io/doc/org.dspace/dspace-api/9.0/org/dspace/discover/DiscoverResult.html + Java REST API model: https://javadoc.io/doc/org.dspace/dspace-server-webapp/9.0/org/dspace/app/rest/model/SearchResultRest.html + REST endpoint contract: https://github.com/DSpace/RestContract/blob/dspace-9.0/search-endpoint.md """ type = "discover" @@ -565,8 +622,10 @@ def as_dict(self): class ResourcePolicy(AddressableHALResource): """ - A resource policy to control access and authorization to DSpace objects - See: https://github.com/DSpace/RestContract/blob/main/resourcepolicies.md + A resource policy to control access and authorization to DSpace objects. + Java API model: https://javadoc.io/doc/org.dspace/dspace-api/9.0/org/dspace/authorize/ResourcePolicy.html + Java REST API model: https://javadoc.io/doc/org.dspace/dspace-server-webapp/9.0/org/dspace/app/rest/model/ResourcePolicyRest.html + REST endpoint contract: https://github.com/DSpace/RestContract/blob/dspace-9.0/resourcepolicies.md """ type = "resourcepolicy" From 74a7038daa517a4b02deae9b289555358efe4a19 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Thu, 12 Mar 2026 04:48:40 +0100 Subject: [PATCH 11/11] More inline doc, add newer classes to __all__ list --- dspace_rest_client/models.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/dspace_rest_client/models.py b/dspace_rest_client/models.py index f7eced3..7674d79 100644 --- a/dspace_rest_client/models.py +++ b/dspace_rest_client/models.py @@ -13,7 +13,8 @@ from typing import Any __all__ = ['DSpaceObject', 'HALResource', 'ExternalDataObject', 'SimpleDSpaceObject', 'Community', - 'Collection', 'Item', 'Bundle', 'Bitstream', 'BitstreamFormat', 'User', 'Group'] + 'Collection', 'Item', 'Bundle', 'Bitstream', 'BitstreamFormat', 'User', 'Group', + 'WorkspaceItem', 'InProgressSubmission', 'SearchResult', 'EntityType', 'ResourcePolicy'] class HALResource: @@ -40,6 +41,9 @@ def as_dict(self) -> dict[str, Any]: return {'type': self.type} class AddressableHALResource(HALResource): + """ + Any DSpace resource with an identifier ('id' in serialised JSON) + """ def __init__(self, api_resource=None): super().__init__(api_resource) self.id = None @@ -57,6 +61,10 @@ class ExternalDataObject(AddressableHALResource): Generic External Data Object as configured in DSpace's external data providers framework TODO: this is also known as externalSourceEntry? Should the class name be modified or aliased? Or should we draw a subtle distinction between the two even if they share the same model + + Java API Model: https://javadoc.io/doc/org.dspace/dspace-api/9.0/org/dspace/external/model/ExternalDataObject.html + Java REST API Model: https://javadoc.io/doc/org.dspace/dspace-server-webapp/9.0/org/dspace/app/rest/model/ExternalSourceEntryRest.html + REST endpoint contract: https://github.com/DSpace/RestContract/blob/dspace-9.0/external-authority-sources.md """ type = "externalSourceEntry" @@ -220,8 +228,6 @@ class SimpleDSpaceObject(DSpaceObject): class Item(SimpleDSpaceObject): """ - Extends DSpaceObject to implement specific attributes and functions for items. - Java API model: https://javadoc.io/doc/org.dspace/dspace-api/9.0/org/dspace/content/Item.html Java REST API model: https://javadoc.io/doc/org.dspace/dspace-server-webapp/9.0/org/dspace/app/rest/model/ItemRest.html REST endpoint contract: https://github.com/DSpace/RestContract/blob/dspace-9.0/items.md @@ -270,8 +276,6 @@ def from_dso(cls, dso: DSpaceObject): class Community(SimpleDSpaceObject): """ - Extends DSpaceObject to implement specific attributes and functions for communities. - Java API model: https://javadoc.io/doc/org.dspace/dspace-api/9.0/org/dspace/content/Community.html Java REST API model: https://javadoc.io/doc/org.dspace/dspace-server-webapp/9.0/org/dspace/app/rest/model/CommunityRest.html REST endpoint contract: https://github.com/DSpace/RestContract/blob/dspace-9.0/communities.md @@ -298,8 +302,6 @@ def as_dict(self): class Collection(SimpleDSpaceObject): """ - Extends DSpaceObject to implement specific attributes and functions for collections. - Java API model: https://javadoc.io/doc/org.dspace/dspace-api/9.0/org/dspace/content/Collection.html Java REST API model: https://javadoc.io/doc/org.dspace/dspace-server-webapp/9.0/org/dspace/app/rest/model/CollectionRest.html REST endpoint contract: https://github.com/DSpace/RestContract/blob/dspace-9.0/collections.md @@ -325,8 +327,6 @@ def as_dict(self): class Bundle(DSpaceObject): """ - Extends DSpaceObject to implement specific attributes and functions for bundles. - Java API model: https://javadoc.io/doc/org.dspace/dspace-api/9.0/org/dspace/content/Bundle.html Java REST API model: https://javadoc.io/doc/org.dspace/dspace-server-webapp/9.0/org/dspace/app/rest/model/BundleRest.html REST endpoint contract: https://github.com/DSpace/RestContract/blob/dspace-9.0/bundles.md @@ -352,8 +352,6 @@ def as_dict(self): class Bitstream(DSpaceObject): """ - Extends DSpaceObject to implement specific attributes and functions for bitstreams. - Java API model: https://javadoc.io/doc/org.dspace/dspace-api/9.0/org/dspace/content/Bitstream.html Java REST API model: https://javadoc.io/doc/org.dspace/dspace-server-webapp/9.0/org/dspace/app/rest/model/BitstreamRest.html REST endpoint contract: https://github.com/DSpace/RestContract/blob/dspace-9.0/bitstreams.md