Skip to content

Commit

Permalink
Merge branch '0.3.5' into 'main'
Browse files Browse the repository at this point in the history
merging 0.3.5 changes to main

See merge request osti/elink2/elink2python!4
  • Loading branch information
nensor committed Aug 15, 2024
2 parents 255ba83 + 8cee0f7 commit ebaee15
Show file tree
Hide file tree
Showing 15 changed files with 662 additions and 268 deletions.
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Changelog

## 0.3.1
- updated package name to elinkapi (from ostiapi)
- numerous README updates
- fix bad request vs. validation exception errors

## 0.3.2
- fix issues with media API return issues
- allow optional URL and TITLE parameters for POST and PUT on media
- update usage license to BSD-3
- add public github URL locations

## 0.3.3
- fix reserve DOI to properly return a single record response
- dependency updates

## 0.3.4
- changed other_information default typing to list
- update number of dependencies for CVEs
- fix issues with license classifier references

## 0.3.5 - 05/22/2024
- added support for ROR ID to affiliations and organizations, with regular-expression-based validation rules
- added new Person Affiliation class to support ROR ID
- deprecated ValidationException in favor of more general support for BadRequestException consolidation
- cleaned up various error messages and conditions
- removed URL from media upload API, migrating to site_url submission via Record endpoints
- fix bug in POST media uploads for files not opening properly
- added Query support for pagination in response to API query_records endpoint
- add documentation README for new Query pagination
- fix various test cases
87 changes: 58 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@
- [Adding Media to Record](#adding-media-to-record)
- [Removing Media from a Record](#removing-media-from-a-record)
- [Compare Two Revision Histories](#compare-two-revision-histories)
- [Searching and pagination](#searching-and-pagination)
- [Method Documentation](#method-documentation)
- [Configuration](#configuration)
- [Records](#records)
- [Revisions](#revisions)
- [Media](#media)
- [Classes](#classes)
- [Record](#record)
- [Query](#query)
- [Organization](#organization)
- [Person](#person)
- [Identifier](#identifier)
Expand All @@ -37,7 +39,6 @@
- [BadRequestException](#bad-request-exception)
- [NotFoundException](#not-found-exception)
- [ConflictException](#conflict-exception)
- [ValidationException](#validation-exception)
- [ServerException](#server-exception)

## Introduction<a id="introduction"></a>
Expand All @@ -51,17 +52,18 @@ This module is setup to mimic the E-Link 2.0 API Endpoints (API documentation fo
3. Or install them separately: `pip install requests pydantic urllib3==1.26.6`
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")`
5. API classes are accessible using `from elinkapi import Record`, etc.
6. Exception classes generated by the API are accessible using `from elinkapi import exceptions` then catching appropriate `exceptions.ValidationException` and the like.
6. Exception classes generated by the API are accessible using `from elinkapi import exceptions` then catching appropriate `exceptions.BadRequestException` and the like.

#### Importing the Package from Production PyPI<a id="importing-the-package-from-production-pypi"></a>
1. Install the package: `pip install elinkapi`
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")`
3. API classes are accessible using `from elinkapi import Record`, etc.
4. Exception classes generated by the API are accessible using `from elinkapi import exceptions` then catching appropriate `exceptions.ValidationException` and the like.
4. Exception classes generated by the API are accessible using `from elinkapi import exceptions` then catching appropriate `exceptions.BadRequestException` and the like.

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

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

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

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

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

#### Searching and pagination<a id="searching-and-pagination"></a>
```python
from elinkapi import Elink, Query

api = Elink(token = "___Your-API-Token___")

query = api.query_records(title = "science", product_type = "JA")

# see number of results
print (f"Query matched {query.total_rows} records")

# paginate through ALL results using iterator
for page in query:
for record in page.data:
print (f"OSTI ID: {record.osti_id} Title: {record.title}")
```

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

Expand Down Expand Up @@ -250,7 +268,7 @@ Example:
api.query_records(title="science")
```

Returns: List[Records]
Returns: Query object

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

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

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

```python
from elinkapi import Record, Organization, Person, Identifier, RelatedIdentifier, Geolocation, MediaInfo, MediaFile
from elinkapi import Record, Organization, Person, Query, Identifier, RelatedIdentifier, Geolocation, MediaInfo, MediaFile
from elinkapi import Revision, RevisionComparison
```

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

### Query<a id="query"></a>
Produced by API query searches, enables pagination and access to total count of rows matching the query. Query is iterable, and may
use Python constructs to paginate all results as desired.

Provides:
- *total_rows* - **int**: Total count of records matching the query
- *data* - **list[Record]**: Records on the current page of query results
- *has_next()* - **boolean**: True if there are more results to be fetched
- *has_previous()* - **boolean**: True if there is a previous page of results


### Organization<a id="organization"></a>
Matches the [Organizations model](https://review.osti.gov/elink2api/#tag/organization_model) described in E-Link 2.0's API documentation

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

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

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

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

Raised when provided query parameters or values are not valid or not understood.

### NotFoundException<a id="not-found-exception"></a>

Raised when OSTI ID or requested resource is not on file.

### ConflictException <a id="conflict-exception"></a>

Raised when attempting to attach duplicate media or URL to a given OSTI ID metadata.

### ValidationException <a id="validation-exception"></a>

Raised on validation errors with submissions of metadata. Additional details are available via the `errors` list, each element containing the following information
Raised when provided query parameters or values are not valid or not understood, or if validation errors occurred during submission of
metadata. Additional details are available via the `errors` list, each element containing the following information
about the various validation issues:
- status: usually 400, indicating a Bad Request error
- detail: an error message indicating the issue
- source: contains a "pointer" to the JSON tag element in error

Example:
```python
[{"status":"400",
[{
"detail":"Site Code BBBB is not valid.",
"source":{
"pointer":"site_ownership_code"
}}]
```

### NotFoundException<a id="not-found-exception"></a>

Raised when OSTI ID or requested resource is not on file.

### ConflictException <a id="conflict-exception"></a>

Raised when attempting to attach duplicate media or URL to a given OSTI ID metadata.

### ServerException <a id="server-exception"></a>

Raised if E-Link back end services or databases have encountered an unrecoverable error during processing.
4 changes: 2 additions & 2 deletions elinkapi_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"phone": "Optional",
"role": "PRIMARY",
"affiliations": [
"Optional"
{ "name": "Optional" }
]
},
{
Expand All @@ -55,7 +55,7 @@
"phone": "Optional",
"contributor_type": "Producer",
"affiliations": [
"Optional"
{ "name": "Optional" }
]
}
],
Expand Down
2 changes: 1 addition & 1 deletion publishing_steps.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ api = Eink(token='your-token')
```python
from elinkapi import Elink

api = Eink(token='your-token')
api = Elink(token='your-token')
```
3. The pydantic classes can be fetched using `from elinkapi import Record, Organization`
4. Exceptions thrown may be accessed using `from elinkapi import exceptions` then reference each as
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "elinkapi"
version = "0.3.4.1"
version = "0.3.5"
authors = [
{ name="Jacob Samar", email="[email protected]" },
{ name="Neal Ensor", email="[email protected]" }
Expand Down
9 changes: 5 additions & 4 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ annotated-types==0.7.0
certifi==2024.7.4
charset-normalizer==3.3.2
idna==3.7
pydantic==2.7.1
pydantic_core==2.18.2
requests==2.32.2
typing_extensions==4.11.0
pydantic==2.8.2
pydantic-core==2.20.1
requests==2.32.3
requests-toolbelt==1.0.0
typing-extensions==4.12.2
urllib3==2.2.2
14 changes: 12 additions & 2 deletions src/elinkapi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,24 @@

from elinkapi.elinkapi import Elink
from elinkapi.person import Person
from elinkapi.affiliation import Affiliation
from elinkapi.geolocation import Geolocation
from elinkapi.identifier import Identifier
from elinkapi.organization import Organization
from elinkapi.record import Record
from elinkapi.record import AccessLimitation, JournalType, ProductType, PAMSPatentStatus, PAMSProductSubType, PAMSPublicationStatus
from elinkapi.media_file import MediaFile
from elinkapi.media_info import MediaInfo
from elinkapi.related_identifier import RelatedIdentifier
from elinkapi.revision_comparison import RevisionComparison
from elinkapi.revision import Revision
from elinkapi.query import Query

from elinkapi.exceptions import (
NotFoundException,
BadRequestException,
UnauthorizedException,
ForbiddenException,
ValidationException,
ServerException,
ConflictException
)
Expand All @@ -31,20 +33,28 @@
"ServerException",
"ConflictException",
"ForbiddenException",
"ValidationException",
# connector
"Elink",
# class types
"Record",
"Geolocation",
"Person",
"Affiliation",
"Organization",
"Identifier",
"RelatedIdentifier",
"MediaFile",
"MediaInfo",
"Revision",
"RevisionComparison",
"Query",
# enumerations
"AccessLimitation",
"JournalType",
"ProductType",
"PAMSPatentStatus",
"PAMSProductSubType",
"PAMSPublicationStatus",
# method accessors
"set_api_token",
"set_target_url",
Expand Down
26 changes: 26 additions & 0 deletions src/elinkapi/affiliation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from pydantic import BaseModel, ConfigDict, field_validator, model_validator
from .utils import Validation

class Affiliation(BaseModel):
"""
Data model for Person affiliations.
may contain one or both of "name" or "ror_id" values.
"ror_id" is validated against a given pattern for proper format according to ror.org specifications.
"""
model_config = ConfigDict(validate_assignment=True)

name:str = None
ror_id:str = None

@model_validator(mode = 'after')
def name_or_ror(self):
if not self.name and not self.ror_id:
raise ValueError("Either name and/or ROR ID value is required.")
return self

@field_validator("ror_id")
@classmethod
def validate_ror_id(cls, value: str) -> str:
Validation.find_ror_value(value)
return value
Loading

0 comments on commit ebaee15

Please sign in to comment.