Skip to content

Commit ebaee15

Browse files
committed
Merge branch '0.3.5' into 'main'
merging 0.3.5 changes to main See merge request osti/elink2/elink2python!4
2 parents 255ba83 + 8cee0f7 commit ebaee15

15 files changed

Lines changed: 662 additions & 268 deletions

CHANGELOG.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Changelog
2+
3+
## 0.3.1
4+
- updated package name to elinkapi (from ostiapi)
5+
- numerous README updates
6+
- fix bad request vs. validation exception errors
7+
8+
## 0.3.2
9+
- fix issues with media API return issues
10+
- allow optional URL and TITLE parameters for POST and PUT on media
11+
- update usage license to BSD-3
12+
- add public github URL locations
13+
14+
## 0.3.3
15+
- fix reserve DOI to properly return a single record response
16+
- dependency updates
17+
18+
## 0.3.4
19+
- changed other_information default typing to list
20+
- update number of dependencies for CVEs
21+
- fix issues with license classifier references
22+
23+
## 0.3.5 - 05/22/2024
24+
- added support for ROR ID to affiliations and organizations, with regular-expression-based validation rules
25+
- added new Person Affiliation class to support ROR ID
26+
- deprecated ValidationException in favor of more general support for BadRequestException consolidation
27+
- cleaned up various error messages and conditions
28+
- removed URL from media upload API, migrating to site_url submission via Record endpoints
29+
- fix bug in POST media uploads for files not opening properly
30+
- added Query support for pagination in response to API query_records endpoint
31+
- add documentation README for new Query pagination
32+
- fix various test cases

README.md

Lines changed: 58 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@
1515
- [Adding Media to Record](#adding-media-to-record)
1616
- [Removing Media from a Record](#removing-media-from-a-record)
1717
- [Compare Two Revision Histories](#compare-two-revision-histories)
18+
- [Searching and pagination](#searching-and-pagination)
1819
- [Method Documentation](#method-documentation)
1920
- [Configuration](#configuration)
2021
- [Records](#records)
2122
- [Revisions](#revisions)
2223
- [Media](#media)
2324
- [Classes](#classes)
2425
- [Record](#record)
26+
- [Query](#query)
2527
- [Organization](#organization)
2628
- [Person](#person)
2729
- [Identifier](#identifier)
@@ -37,7 +39,6 @@
3739
- [BadRequestException](#bad-request-exception)
3840
- [NotFoundException](#not-found-exception)
3941
- [ConflictException](#conflict-exception)
40-
- [ValidationException](#validation-exception)
4142
- [ServerException](#server-exception)
4243

4344
## Introduction<a id="introduction"></a>
@@ -51,17 +52,18 @@ This module is setup to mimic the E-Link 2.0 API Endpoints (API documentation fo
5152
3. Or install them separately: `pip install requests pydantic urllib3==1.26.6`
5253
4. Access the E-Link connector via `from elinkapi import Elink` and creating an instance for use with your API key: `api = Elink(token="Your_API_Token")`
5354
5. API classes are accessible using `from elinkapi import Record`, etc.
54-
6. Exception classes generated by the API are accessible using `from elinkapi import exceptions` then catching appropriate `exceptions.ValidationException` and the like.
55+
6. Exception classes generated by the API are accessible using `from elinkapi import exceptions` then catching appropriate `exceptions.BadRequestException` and the like.
5556

5657
#### Importing the Package from Production PyPI<a id="importing-the-package-from-production-pypi"></a>
5758
1. Install the package: `pip install elinkapi`
5859
2. Access the E-Link connector via `from elinkapi import Elink` and creating an instance for use with your API key: `api = Elink(token="Your_API_Token")`
5960
3. API classes are accessible using `from elinkapi import Record`, etc.
60-
4. Exception classes generated by the API are accessible using `from elinkapi import exceptions` then catching appropriate `exceptions.ValidationException` and the like.
61+
4. Exception classes generated by the API are accessible using `from elinkapi import exceptions` then catching appropriate `exceptions.BadRequestException` and the like.
6162

6263
## Examples<a id="examples"></a>
6364

6465
#### Creating a New Record<a id="creating-a-new-record"></a>
66+
Note: Ensure site_ownership_code is a value to which your user account token has sufficient access to create records.
6567
```python
6668
from elinkapi import Elink, Record, exceptions
6769

@@ -79,15 +81,15 @@ my_record = Record(**my_record_json)
7981
saved_record = None
8082
try:
8183
saved_record = api.post_new_record(my_record, "save")
82-
except exceptions.ValidationException as ve:
84+
except exceptions.BadRequestException as ve:
8385
# ve.message = "Site Code AAAA is not valid."
8486
# ve.errors provides more details:
8587
# [{"status":"400", "detail":"Site Code AAAA is not valid.", "source":{"pointer":"site_ownership_code"}}]
8688
```
8789

8890
#### Seeing Validation Errors on Exception<a id="seeing-validation-errors-on-exception"></a>
8991
```python
90-
from elinkapi import Elink, Record, ValidationException
92+
from elinkapi import Elink, Record, BadRequestException
9193

9294
# Record missing fields, will give 2 validation errors, one for
9395
# each missing field: title and product_type
@@ -123,8 +125,8 @@ try:
123125
# The API will now return an error code on this call
124126
# because "AAAA" is not a valid site_ownership_code
125127
saved_record = api.post_new_record(my_record, "save")
126-
except exceptions.ValidationException as ve:
127-
# E-Link ValidationException provides details of the API response:
128+
except exceptions.BadRequestException as ve:
129+
# E-Link BadRequestException provides details of the API response:
128130
# ve.message = "Site Code AAAA is not valid."
129131
# ve.errors provides more details:
130132
# [{"status":"400", "detail":"Site Code AAAA is not valid.", "source":{"pointer":"site_ownership_code"}}]
@@ -198,6 +200,22 @@ except Exception as e:
198200
# Handle the exception as needed
199201
```
200202

203+
#### Searching and pagination<a id="searching-and-pagination"></a>
204+
```python
205+
from elinkapi import Elink, Query
206+
207+
api = Elink(token = "___Your-API-Token___")
208+
209+
query = api.query_records(title = "science", product_type = "JA")
210+
211+
# see number of results
212+
print (f"Query matched {query.total_rows} records")
213+
214+
# paginate through ALL results using iterator
215+
for page in query:
216+
for record in page.data:
217+
print (f"OSTI ID: {record.osti_id} Title: {record.title}")
218+
```
201219

202220
## Method Documentation<a id="method-documentation"></a>
203221

@@ -250,7 +268,7 @@ Example:
250268
api.query_records(title="science")
251269
```
252270

253-
Returns: List[Records]
271+
Returns: Query object
254272

255273
Params:
256274
- *params* - **dict**: See [here](https://review.osti.gov/elink2api/#tag/records/operation/getRecords) for
@@ -339,18 +357,20 @@ Params:
339357
- *media_file_id* - **int**: ID that uniquely identifies a media file associated with an E-Link 2.0 Record
340358
---
341359
Method:
342-
> post_media(*osti_id*, *file_path*, *params*=None)
360+
> post_media(*osti_id*, *file_path*, *params*=None, *stream*=None)
343361
344362
Returns: MediaInfo
345363

346364
Params:
347365
- *osti_id* - **int**: ID that uniquely identifies an E-Link 2.0 Record
348366
- *file_path* - **str**: Path to the media file that will be attached to the Record
349367
- *params* - **dict**: "title" that can be associated with the media file
350-
"url" that points to media if not sending file (default; {None})
368+
"url" that points to media if not sending file (default: {None})
369+
- *stream* - **bool**: Whether to stream the media file data, which has better performance
370+
for larger files (default: {False})
351371
---
352372
Method:
353-
> put_media(*osti_id*, *media_id*, *file_path*, *params*=None)
373+
> put_media(*osti_id*, *media_id*, *file_path*, *params*=None, *stream*=None)
354374
355375
Returns: MediaInfo
356376

@@ -359,7 +379,9 @@ Params:
359379
- *media_id* - **int**: ID that uniquely identifies a media file associated with an E-Link 2.0 Record
360380
- *file_path* - **str**: Path to the media file that will replace *media_id* Media
361381
- *params* - **dict**: "title" that can be associated with the media file
362-
"url" that points to media if not sending file (default; {None})
382+
"url" that points to media if not sending file (default: {None})
383+
- *stream* - **bool**: Whether to stream the media file data, which has better performance
384+
for larger files (default: {False})
363385
---
364386
Method:
365387
> delete_all_media(*osti_id*, *reason*)
@@ -385,13 +407,24 @@ Each class is a pydantic model that validates the metadata's data types and
385407
enumerated values on instantiation of the class. Each may be imported directly:
386408

387409
```python
388-
from elinkapi import Record, Organization, Person, Identifier, RelatedIdentifier, Geolocation, MediaInfo, MediaFile
410+
from elinkapi import Record, Organization, Person, Query, Identifier, RelatedIdentifier, Geolocation, MediaInfo, MediaFile
389411
from elinkapi import Revision, RevisionComparison
390412
```
391413

392414
### Record<a id="record"></a>
393415
Matches the [Metadata model](https://review.osti.gov/elink2api/#tag/record_model) described in E-Link 2.0's API documentation
394416

417+
### Query<a id="query"></a>
418+
Produced by API query searches, enables pagination and access to total count of rows matching the query. Query is iterable, and may
419+
use Python constructs to paginate all results as desired.
420+
421+
Provides:
422+
- *total_rows* - **int**: Total count of records matching the query
423+
- *data* - **list[Record]**: Records on the current page of query results
424+
- *has_next()* - **boolean**: True if there are more results to be fetched
425+
- *has_previous()* - **boolean**: True if there is a previous page of results
426+
427+
395428
### Organization<a id="organization"></a>
396429
Matches the [Organizations model](https://review.osti.gov/elink2api/#tag/organization_model) described in E-Link 2.0's API documentation
397430

@@ -581,37 +614,33 @@ Generally raised when no API token value is provided when accessing E-Link.
581614

582615
### ForbiddenException<a id="forbidden-exception"></a>
583616

584-
Raised when attempting to query records, post new content to a site, or update records to which the API token has no permission.
617+
Raised when attempting to query records, post new content to a site, or create/update records to which the API token has no permission.
585618

586619
### BadRequestException<a id="bad-request-exception"></a>
587620

588-
Raised when provided query parameters or values are not valid or not understood.
589-
590-
### NotFoundException<a id="not-found-exception"></a>
591-
592-
Raised when OSTI ID or requested resource is not on file.
593-
594-
### ConflictException <a id="conflict-exception"></a>
595-
596-
Raised when attempting to attach duplicate media or URL to a given OSTI ID metadata.
597-
598-
### ValidationException <a id="validation-exception"></a>
599-
600-
Raised on validation errors with submissions of metadata. Additional details are available via the `errors` list, each element containing the following information
621+
Raised when provided query parameters or values are not valid or not understood, or if validation errors occurred during submission of
622+
metadata. Additional details are available via the `errors` list, each element containing the following information
601623
about the various validation issues:
602-
- status: usually 400, indicating a Bad Request error
603624
- detail: an error message indicating the issue
604625
- source: contains a "pointer" to the JSON tag element in error
605626

606627
Example:
607628
```python
608-
[{"status":"400",
629+
[{
609630
"detail":"Site Code BBBB is not valid.",
610631
"source":{
611632
"pointer":"site_ownership_code"
612633
}}]
613634
```
614635

636+
### NotFoundException<a id="not-found-exception"></a>
637+
638+
Raised when OSTI ID or requested resource is not on file.
639+
640+
### ConflictException <a id="conflict-exception"></a>
641+
642+
Raised when attempting to attach duplicate media or URL to a given OSTI ID metadata.
643+
615644
### ServerException <a id="server-exception"></a>
616645

617646
Raised if E-Link back end services or databases have encountered an unrecoverable error during processing.

elinkapi_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"phone": "Optional",
3232
"role": "PRIMARY",
3333
"affiliations": [
34-
"Optional"
34+
{ "name": "Optional" }
3535
]
3636
},
3737
{
@@ -55,7 +55,7 @@
5555
"phone": "Optional",
5656
"contributor_type": "Producer",
5757
"affiliations": [
58-
"Optional"
58+
{ "name": "Optional" }
5959
]
6060
}
6161
],

publishing_steps.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ api = Eink(token='your-token')
5151
```python
5252
from elinkapi import Elink
5353

54-
api = Eink(token='your-token')
54+
api = Elink(token='your-token')
5555
```
5656
3. The pydantic classes can be fetched using `from elinkapi import Record, Organization`
5757
4. Exceptions thrown may be accessed using `from elinkapi import exceptions` then reference each as

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "elinkapi"
7-
version = "0.3.4.1"
7+
version = "0.3.5"
88
authors = [
99
{ name="Jacob Samar", email="samarj@osti.gov" },
1010
{ name="Neal Ensor", email="ensorn@osti.gov" }

requirements.txt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ annotated-types==0.7.0
22
certifi==2024.7.4
33
charset-normalizer==3.3.2
44
idna==3.7
5-
pydantic==2.7.1
6-
pydantic_core==2.18.2
7-
requests==2.32.2
8-
typing_extensions==4.11.0
5+
pydantic==2.8.2
6+
pydantic-core==2.20.1
7+
requests==2.32.3
8+
requests-toolbelt==1.0.0
9+
typing-extensions==4.12.2
910
urllib3==2.2.2

src/elinkapi/__init__.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,24 @@
22

33
from elinkapi.elinkapi import Elink
44
from elinkapi.person import Person
5+
from elinkapi.affiliation import Affiliation
56
from elinkapi.geolocation import Geolocation
67
from elinkapi.identifier import Identifier
78
from elinkapi.organization import Organization
89
from elinkapi.record import Record
10+
from elinkapi.record import AccessLimitation, JournalType, ProductType, PAMSPatentStatus, PAMSProductSubType, PAMSPublicationStatus
911
from elinkapi.media_file import MediaFile
1012
from elinkapi.media_info import MediaInfo
1113
from elinkapi.related_identifier import RelatedIdentifier
1214
from elinkapi.revision_comparison import RevisionComparison
1315
from elinkapi.revision import Revision
16+
from elinkapi.query import Query
1417

1518
from elinkapi.exceptions import (
1619
NotFoundException,
1720
BadRequestException,
1821
UnauthorizedException,
1922
ForbiddenException,
20-
ValidationException,
2123
ServerException,
2224
ConflictException
2325
)
@@ -31,20 +33,28 @@
3133
"ServerException",
3234
"ConflictException",
3335
"ForbiddenException",
34-
"ValidationException",
3536
# connector
3637
"Elink",
3738
# class types
3839
"Record",
3940
"Geolocation",
4041
"Person",
42+
"Affiliation",
4143
"Organization",
4244
"Identifier",
4345
"RelatedIdentifier",
4446
"MediaFile",
4547
"MediaInfo",
4648
"Revision",
4749
"RevisionComparison",
50+
"Query",
51+
# enumerations
52+
"AccessLimitation",
53+
"JournalType",
54+
"ProductType",
55+
"PAMSPatentStatus",
56+
"PAMSProductSubType",
57+
"PAMSPublicationStatus",
4858
# method accessors
4959
"set_api_token",
5060
"set_target_url",

src/elinkapi/affiliation.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from pydantic import BaseModel, ConfigDict, field_validator, model_validator
2+
from .utils import Validation
3+
4+
class Affiliation(BaseModel):
5+
"""
6+
Data model for Person affiliations.
7+
8+
may contain one or both of "name" or "ror_id" values.
9+
"ror_id" is validated against a given pattern for proper format according to ror.org specifications.
10+
"""
11+
model_config = ConfigDict(validate_assignment=True)
12+
13+
name:str = None
14+
ror_id:str = None
15+
16+
@model_validator(mode = 'after')
17+
def name_or_ror(self):
18+
if not self.name and not self.ror_id:
19+
raise ValueError("Either name and/or ROR ID value is required.")
20+
return self
21+
22+
@field_validator("ror_id")
23+
@classmethod
24+
def validate_ror_id(cls, value: str) -> str:
25+
Validation.find_ror_value(value)
26+
return value

0 commit comments

Comments
 (0)