Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More upload functions #35

Merged
merged 9 commits into from
Jan 31, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/source/howto/ingest.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ from pyscicat.model import (
Datablock,
DataFile,
Dataset,
Sample,
Ownable
)

Expand Down Expand Up @@ -61,6 +62,19 @@ Note that we store the provided dataset_id in a variable for later use.

Also note the `sourceFolder`. This is a folder on the file system that SciCat has access to, and will contain the files for this `Dataset`.

Proposals and instruments have to be created by an administrator. A sample with `sampleId="gargleblaster"` can be created like this:
```python
sample = Sample(
sampleId="gargleblaster",
owner="Chamber of Commerce",
description="A legendary drink.",
sampleCharacteristics={"Flavour": "Unknown, but potent"},
isPublished=False,
**ownable.dict()
)
sample_id = client.upload_sample(sample) # sample_id == "gargleblaster"
```

## Upload a Datablock

```python
Expand Down
208 changes: 207 additions & 1 deletion pyscicat/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@
Attachment,
Datablock,
Dataset,
DerivedDataset,
Instrument,
OrigDatablock,
Proposal,
RawDataset,
DerivedDataset,
Sample,
)

logger = logging.getLogger("splash_ingest")
Expand Down Expand Up @@ -413,6 +416,209 @@ def datasets_attachment_create(
upload_attachment = datasets_attachment_create
create_dataset_attachment = datasets_attachment_create

def samples_create(self, sample: Sample) -> str:
"""
Create a new sample.
An error is raised when a sample with the same sampleId already exists.
This function is also accessible as upload_sample.


Parameters
----------
sample : Sample
Sample to upload

Returns
-------
str
ID of the newly created sample

Raises
------
ScicatCommError
Raises if a non-20x message is returned
"""
return self._call_endpoint(
cmd="post",
endpoint="Samples",
data=sample,
operation="samples_create",
).get("sampleId")

upload_sample = samples_create

def update_sample(self, sample: Sample, sampleId: str = None) -> str:
eilmiv marked this conversation as resolved.
Show resolved Hide resolved
"""Updates an existing sample

Parameters
----------
sample : Sample
Sample to update

sampleId
ID of sample being updated. By default, ID is taken from sample parameter.

Returns
-------
str
ID of the sample

Raises
------
ScicatCommError
Raises if a non-20x message is returned

AssertionError
Raises if no ID is provided
"""
if sampleId is None:
assert sample.sampleId is not None, "sampleId should not be None"
sampleId = sample.sampleId
sample.sampleId = None
return self._call_endpoint(
cmd="patch",
endpoint=f"Samples/{quote_plus(sampleId)}",
data=sample,
operation="update_sample",
).get("sampleId")

def instruments_create(self, instrument: Instrument):
"""
Create a new instrument.
Note that in SciCat admin rights are required to upload instruments.
An error is raised when an instrument with the same pid already exists.
This function is also accessible as upload_instrument.


Parameters
----------
instrument : Instrument
Instrument to upload

Returns
-------
str
pid (or unique identifier) of the newly created instrument

Raises
------
ScicatCommError
Raises if a non-20x message is returned
"""
return self._call_endpoint(
cmd="post",
endpoint="Instruments",
data=instrument,
operation="instruments_create",
).get("pid")

upload_instrument = instruments_create

def update_instrument(self, instrument: Instrument, pid: str = None) -> str:
eilmiv marked this conversation as resolved.
Show resolved Hide resolved
"""Updates an existing instrument.
Note that in SciCat admin rights are required to upload instruments.

Parameters
----------
instrument : Instrument
Instrument to update

pid
pid (or unique identifier) of instrument being updated.
By default, pid is taken from instrument parameter.

Returns
-------
str
ID of the instrument

Raises
------
ScicatCommError
Raises if a non-20x message is returned

AssertionError
Raises if no ID is provided
"""
if pid is None:
assert instrument.pid is not None, "pid should not be None"
pid = instrument.pid
instrument.pid = None
return self._call_endpoint(
cmd="patch",
endpoint=f"Instruments/{quote_plus(pid)}",
data=instrument,
operation="update_instrument",
).get("pid")

def proposals_create(self, proposal: Proposal):
"""
Create a new proposal.
Note that in SciCat admin rights are required to upload proposals.
An error is raised when a proposal with the same proposalId already exists.
This function is also accessible as upload_proposal.


Parameters
----------
proposal : Proposal
Proposal to upload

Returns
-------
str
ID of the newly created proposal

Raises
------
ScicatCommError
Raises if a non-20x message is returned
"""
return self._call_endpoint(
cmd="post",
endpoint="Proposals",
data=proposal,
operation="proposals_create",
).get("proposalId")

upload_proposal = proposals_create

def update_proposal(self, proposal: Proposal, proposalId: str = None) -> str:
eilmiv marked this conversation as resolved.
Show resolved Hide resolved
"""Updates an existing proposal.
Note that in SciCat admin rights are required to upload proposals.

Parameters
----------
proposal : Proposal
Proposal to update

proposalId
ID of proposal being updated. By default, this is taken from proposal parameter.

Returns
-------
str
ID of the proposal

Raises
------
ScicatCommError
Raises if a non-20x message is returned

AssertionError
Raises if no ID is provided
"""
if proposalId is None:
assert proposal.proposalId is not None, "proposalId should not be None"
proposalId = proposal.proposalId
proposal.proposalId = None
return self._call_endpoint(
cmd="patch",
endpoint=f"Proposals/{quote_plus(proposalId)}",
data=proposal,
operation="update_proposal",
).get("proposalId")

def datasets_find(
self, skip: int = 0, limit: int = 25, query_fields: Optional[dict] = None
) -> Optional[dict]:
Expand Down
11 changes: 5 additions & 6 deletions pyscicat/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ class Ownable(MongoQueryable):
"""Many objects in SciCat are ownable"""

ownerGroup: str
accessGroups: List[str]
accessGroups: Optional[List[str]]
instrumentGroup: Optional[str]


class User(BaseModel):
Expand All @@ -45,15 +46,14 @@ class Proposal(Ownable):
Defines the purpose of an experiment and links an experiment to principal investigator and main proposer
"""

# TODO: find out which of these are not optional and update
proposalId: Optional[str]
proposalId: str
pi_email: Optional[str]
pi_firstname: Optional[str]
pi_lastname: Optional[str]
email: Optional[str]
email: str
firstname: Optional[str]
lastname: Optional[str]
title: Optional[str]
title: Optional[str] # required in next backend version
abstract: Optional[str]
startTime: Optional[str]
endTime: Optional[str]
Expand All @@ -68,7 +68,6 @@ class Sample(Ownable):
Raw datasets should be linked to such sample definitions.
"""

# TODO: find out which of these are not optional and update
sampleId: Optional[str]
owner: Optional[str]
description: Optional[str]
Expand Down
46 changes: 45 additions & 1 deletion pyscicat/tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@
Attachment,
Datablock,
DataFile,
Instrument,
Proposal,
RawDataset,
Sample,
Ownable,
)

Expand All @@ -28,7 +31,18 @@ def add_mock_requests(mock_request):
local_url + "Users/login",
json={"id": "a_token"},
)
mock_request.post(local_url + "Samples", json={"sampleId": "dataset_id"})

mock_request.post(local_url + "Instruments", json={"pid": "earth"})
mock_request.post(local_url + "Proposals", json={"proposalId": "deepthought"})
mock_request.post(local_url + "Samples", json={"sampleId": "gargleblaster"})
mock_request.patch(local_url + "Instruments/earth", json={"pid": "earth"})
mock_request.patch(
local_url + "Proposals/deepthought", json={"proposalId": "deepthought"}
)
mock_request.patch(
local_url + "Samples/gargleblaster", json={"sampleId": "gargleblaster"}
)

mock_request.post(local_url + "RawDatasets/replaceOrCreate", json={"pid": "42"})
mock_request.patch(
local_url + "Datasets/42",
Expand Down Expand Up @@ -66,6 +80,36 @@ def test_scicat_ingest():
size = get_file_size(thumb_path)
assert size is not None

# Instrument
instrument = Instrument(
pid="earth", name="Earth", customMetadata={"a": "field"}
)
assert scicat.upload_instrument(instrument) == "earth"
assert scicat.instruments_create(instrument) == "earth"
assert scicat.update_instrument(instrument) == "earth"

# Proposal
proposal = Proposal(
proposalId="deepthought",
title="Deepthought",
email="deepthought@viltvodle.com",
**ownable.dict()
)
assert scicat.upload_proposal(proposal) == "deepthought"
assert scicat.proposals_create(proposal) == "deepthought"
assert scicat.update_proposal(proposal) == "deepthought"

# Sample
sample = Sample(
sampleId="gargleblaster",
description="Gargleblaster",
sampleCharacteristics={"a": "field"},
**ownable.dict()
)
assert scicat.upload_sample(sample) == "gargleblaster"
assert scicat.samples_create(sample) == "gargleblaster"
assert scicat.update_sample(sample) == "gargleblaster"

# RawDataset
dataset = RawDataset(
path="/foo/bar",
Expand Down