Skip to content

Commit 26f8159

Browse files
committed
fix json-patch issues.
fix query.reset() issue provide additional docstrings and clarifications in library methods with examples Signed-off-by: Neal Ensor <[email protected]>
1 parent 1eea566 commit 26f8159

File tree

3 files changed

+111
-26
lines changed

3 files changed

+111
-26
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,4 @@
130130
- Update to requests dependency (>=2.32.4) to address potential vulnerability
131131
- Support patch methods to update metadata through new endpoints
132132
- Added documentation and explanations to a number of library functions
133+
- Accept dict or keyword arguments for Record argument methods

src/elinkapi/elinkapi.py

Lines changed: 109 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@ class Elink:
1717
Defines a set of access points for E-Link API endpoints.
1818
1919
Construct an api access object, defining the desired API target and supplying the user-specific API key token.
20+
Note that Review and Production are entirely separate instances, with Production being the default target if not
21+
specified.
22+
23+
These endpoint targets are:
24+
review: https://review.osti.gov/elink2api/
25+
production: https://www.osti.gov/elink2api/
26+
27+
>>> from elinkapi import Elink
28+
>>> api = Elink(target = "https://review.osti.gov/elink2api/", token=MYUSERTOKEN)
2029
2130
Use this to access functions, including:
2231
@@ -30,6 +39,23 @@ class Elink:
3039
post_media -- add a new media set to the OSTI ID
3140
put_media -- replace an existing media set with new content
3241
42+
Record creation methods (reserve_doi, post_new_record, update_record) may provide a user-supplied Record, a dict
43+
containing required key elements, or as keyword arguments.
44+
>>> api.post_new_record(title="My dataset", product_type="DA")
45+
or
46+
>>> api.post_new_record({ "title": "New Technical Report", "product_type" : "TR" })
47+
or
48+
>>> api.post_new_record(Record(title="Example journal", product_type="JA", journal_name="Science", doi="10.11578/23423"))
49+
50+
Record instances contain a reference function dict() to obtain a dictionary from its content. Supply optional
51+
"exclude_none=True" argument to obtain only elements with values if desired:
52+
>>> myrecord.dict(exclude_none=True)
53+
54+
Individual values in Record instances are directly accessible and may be set in the same way; e.g.,
55+
>>> myrecord.title = "New Title Here"
56+
or
57+
>>> print (myrecord.doi)
58+
3359
"""
3460
def __init__(self, token=None, target=None):
3561
"""
@@ -100,7 +126,10 @@ def record_to_json(self, record):
100126

101127
# Record Methods
102128
def get_single_record(self, osti_id: int):
103-
"""Obtain the metadata JSON for a record at OSTI
129+
"""Obtain the metadata JSON for a record at OSTI.
130+
131+
>>> record = api.get_single_record(2009785)
132+
>>> print (record.title)
104133
105134
Arguments:
106135
osti_id -- ID that uniquely identifies an E-link 2.0 Record
@@ -116,7 +145,14 @@ def get_single_record(self, osti_id: int):
116145
return self._convert_response_to_records(response)[0]
117146

118147
def query_records(self, **kwargs):
119-
"""Query for records using a variety of query params
148+
"""Query for records using a variety of query search parameters.
149+
150+
Example:
151+
>>> query = api.query_records(title="Science", product_type = "JA")
152+
>>> query.total_rows
153+
1738
154+
>>> for record in query:
155+
... print (record.title)
120156
121157
Arguments:
122158
params -- See https://www.osti.gov/elink2api/#tag/records/operation/getRecords for
@@ -138,38 +174,72 @@ def query_records(self, **kwargs):
138174

139175
return Query(response, target=self.target, token=self.token)
140176

141-
def reserve_doi(self, record):
142-
""" Save a Record with minimal validations:
143-
Required:
177+
def reserve_doi(self, r=None, **kwargs):
178+
""" Save a Record with minimal validations.
179+
180+
Required data elements, either via a Record or keyword arguments:
144181
title
145182
site_ownership_code
146183
product_type
147184
185+
Provided as a convenience; functionally equivalent to post_new_record method, with
186+
state="save".
187+
188+
>>> reservation = api.reserve_doi(title="Sample dataset from 2012", product_type = "DA")
189+
>>> print (reservation.doi)
190+
'10.11578/2003824'
191+
148192
Arguments:
149-
record -- Metadata record that you wish to save to E-Link 2.0
193+
record -- Metadata record that you wish to save to E-Link 2.0 (optional)
194+
195+
Keyword Arguments:
196+
if record is not provided, these will be used to construct a new one for DOI reservation.
150197
151198
Returns:
152199
Record - metadata of a single record that has been saved to E-Link 2.0
153200
"""
154-
response = requests.post(self.target+ "records/save", headers={"Authorization": f"Bearer {self.token}"},
155-
json=json.loads(record.model_dump_json(exclude_none=True)))
201+
# perform a minimal new record POST
202+
return self.post_new_record(r=r, **kwargs)
203+
204+
def _convert_record(self, record=None, **kwargs) -> Record:
205+
"""
206+
Accept either a Record or a dict, which we convert to a Record for consistency. Will
207+
take appropriately-named keyword arguments if record is not provided to construct one.
156208
157-
Validation.handle_response(response)
209+
Arguments:
210+
record -- either a dict to convert, or already a Record (optional)
211+
212+
Keyword arguments:
213+
if record not provided, use these to construct one
214+
215+
Returns:
216+
a Record, possibly from the dict if possible.
158217
159-
return self._convert_response_to_records(response)[0]
218+
"""
219+
if record and isinstance(record, dict):
220+
return Record(**record)
221+
elif record and isinstance(record, Record):
222+
return record
223+
else:
224+
return Record(**kwargs)
160225

161-
def post_new_record(self, record, state="save"):
226+
def post_new_record(self, r=None, state="save", **kwargs):
162227
"""Create a new metadata Record with OSTI
163228
164229
Arguments:
165-
record -- Metadata record that you wish to send ("save" or "submit") to E-Link 2.0
230+
record -- Metadata record that you wish to send ("save" or "submit") to E-Link 2.0. May provide as
231+
Record or dict, or as keyword arguments.
166232
167233
Keyword Arguments:
168234
state -- The desired submission state of the record ("save" or "submit") (default: {"save"})
235+
if record not provided, takes rest of keyword arguments to construct a Record
169236
170237
Returns:
171238
Record - metadata of a single record saved (or submitted) to E-Link 2.0
172239
"""
240+
# make a Record from provided arguments
241+
record = self._convert_record(record=r, **kwargs)
242+
# post it as a new record
173243
response = requests.post(f"{self.target}records/{state}",
174244
headers={
175245
"Authorization": f"Bearer {self.token}",
@@ -184,7 +254,9 @@ def post_new_record(self, record, state="save"):
184254

185255
def patch_record(self, osti_id, patch, state="save"):
186256
"""
187-
Update record via partial-patch-json method endpoint
257+
Update record via partial-patch-json method endpoint. Provide only the JSON you wish to alter.
258+
259+
>>> api.patch_record(2008590, { "title": "This title is new", "description": "As is this description" })
188260
189261
Arguments:
190262
osti_id -- the OSTI ID of the record to patch
@@ -209,41 +281,52 @@ def patch_record(self, osti_id, patch, state="save"):
209281

210282
def patch_json(self, osti_id, jsonpatch, state="save"):
211283
"""
212-
Update record via a JSON-patch set of command operations.
213-
214-
:param osti_id: The OSTI ID of the record to patch
215-
:type osti_id: int
284+
Update record via a JSON-patch set of command operations. Note the jsonpatch is intended to be
285+
an array of one or more operations to perform; those including "add", "replace", "copy", "move", or
286+
"remove". See details at https://www.osti.gov/elink2api/#operation/patchRecord.
216287
217-
:param jsonpatch: The JSON or dict containing array of patch operations to perform
218-
:param state: The desired workflow state of the new revision ("save" or "submit") default: "save"
288+
>>> api.patch_json(2007439, [{"op": "add", "path": "/description", "value": "A new description."}])
289+
290+
Arguments:
291+
osti_id -- The OSTI ID of the record to patch
292+
jsonpatch -- JSON or dict containing any operations to perform on the record
219293
220-
:return: a Record of the new revision if successful
294+
Keyword arguments:
295+
state -- Desired workflow state of the new revision ("save" or "submit") default: "save"
221296
297+
Returns:
298+
Record -- the metadata of the new revision with operations performed if successful
222299
"""
223300
response = requests.patch(f"{self.target}/records/{osti_id}/{state}",
224301
headers = {
225302
"Authorization" : f"Bearer {self.token}",
226303
"Content-Type": "application/json-patch+json"
227304
},
228-
data=str(jsonpatch))
305+
data=json.dumps(jsonpatch))
229306

230307
Validation.handle_response(response)
231308

232309
return self._convert_response_to_records(response)[0]
233310

234-
def update_record(self, osti_id, record, state="save"):
235-
"""Update existing records at OSTI by unique OSTI ID
311+
def update_record(self, osti_id, r=None, state="save", **kwargs):
312+
"""Update existing records at OSTI by unique OSTI ID. Note this REPLACES the record entirely;
313+
the provided record details will become the new revision of the record on file; all required
314+
information must therefore be provided as applicable.
236315
237316
Arguments:
238317
osti_id -- ID that uniquely identifies an E-link 2.0 Record
239-
record -- Metadata record that you wish to make the new revision of OSTI ID
318+
record -- Metadata record that you wish to make the new revision of OSTI ID (optional, as Record or dict)
240319
241320
Keyword Arguments:
242321
state -- The desired submission state of the record ("save" or "submit") (default: {"save"})
322+
if record not specified, rest of the keyword arguments are used to construct one
243323
244324
Returns:
245325
Record - Metadata of record updated with the given information, creating a new revision
246326
"""
327+
# get a record
328+
record = self._convert_record(record=r, **kwargs)
329+
# send the UPDATE
247330
response = requests.put(f"{self.target}records/{osti_id}/{state}",
248331
headers={
249332
"Authorization": f"Bearer {self.token}"
@@ -489,7 +572,8 @@ def __post_media_no_stream(self, osti_id, file_path=None, query_params=None):
489572

490573
def put_media(self, osti_id, media_id, file_path=None, title=None, stream=False):
491574
"""Replace a given media set with a new basis file.
492-
This will replace the previous media set.
575+
This will replace the previous media set. Both osti_id and media_id (of the set to replace)
576+
are required.
493577
494578
Arguments:
495579
osti_id -- ID that uniquely identifies an E-link 2.0 Record

src/elinkapi/query.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def reset(self):
8888
raise StopIteration.
8989
"""
9090
if self.first_url:
91-
response = requests.get(f"{self.target}{self.first_url}",
91+
response = requests.get(f"{self._target}{self.first_url}",
9292
headers = { "Authorization" : f"Bearer {self._token}"})
9393
Validation.handle_response(response)
9494
self._load(response)

0 commit comments

Comments
 (0)