Skip to content

Commit 8ddb369

Browse files
authored
Fix custom terms (#48)
* add new `CustomLicense` class * handle custom and instance licenses - Also fixes the issue that single-compound fields break the code * ignore linter complain due to pydantic alias * revert `match` syntax * removed unused imports
1 parent 0dddaa4 commit 8ddb369

7 files changed

Lines changed: 159 additions & 16 deletions

File tree

easyDataverse/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
from .dataset import Dataset # noqa: F401
22
from .dataverse import Dataverse # noqa: F401
3+
from .license import CustomLicense, License # noqa: F401
34
import nest_asyncio
45

6+
__all__ = ["Dataset", "Dataverse", "CustomLicense", "License"]
7+
58
nest_asyncio.apply()
69

710
__version__ = "0.4.3"

easyDataverse/dataset.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import json
22
import os
33
from json import dumps
4-
from typing import Dict, List, Optional
4+
from typing import Dict, List, Optional, Union
55

66
import nob
77
import xmltodict
@@ -11,7 +11,7 @@
1111
from dvuploader import File, add_directory
1212

1313
from easyDataverse.base import DataverseBase
14-
from easyDataverse.license import License
14+
from easyDataverse.license import CustomLicense, License
1515
from easyDataverse.uploader import update_dataset, upload_to_dataverse
1616
from easyDataverse.utils import YAMLDumper
1717

@@ -34,7 +34,8 @@ class Dataset(BaseModel):
3434
validate_assignment=True,
3535
)
3636

37-
license: License = Field(
37+
license: Union[License, CustomLicense, None] = Field(
38+
default=None,
3839
description="The license of the dataset.",
3940
)
4041

@@ -182,10 +183,17 @@ def dataverse_dict(self) -> dict:
182183
for block in self.metadatablocks.values():
183184
blocks.update(block.dataverse_dict())
184185

186+
if isinstance(self.license, License):
187+
terms = {"license": self.license.name}
188+
elif isinstance(self.license, CustomLicense):
189+
terms = self.license.model_dump(by_alias=True, exclude={"name"})
190+
else:
191+
terms = {}
192+
185193
return {
186194
"datasetVersion": {
187-
"license": self.license.name,
188195
"metadataBlocks": blocks,
196+
**terms,
189197
}
190198
}
191199

easyDataverse/dataverse.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from urllib import parse
77

88
import httpx
9-
from easyDataverse.license import License
9+
from easyDataverse.license import CustomLicense, License
1010
from rich.panel import Panel
1111
from rich.progress import Progress, SpinnerColumn, TextColumn
1212
from anytree import Node, findall_by_attr
@@ -372,10 +372,22 @@ def load_dataset(
372372

373373
# Fetch and extract data
374374
remote_ds = self._fetch_dataset(pid, version)
375-
dataset.license = self.licenses[remote_ds.data.latestVersion.license.name] # type: ignore
376-
dataset.p_id = remote_ds.data.latestVersion.datasetPersistentId # type: ignore
377-
blocks = remote_ds.data.latestVersion.metadataBlocks # type: ignore
378-
files = remote_ds.data.latestVersion.files # type: ignore
375+
376+
# Get the latest version data
377+
latest_version = remote_ds.data.latestVersion # type: ignore
378+
379+
# Handle license information
380+
if hasattr(latest_version, "license") and latest_version.license:
381+
dataset.license = self.licenses.get(latest_version.license.name)
382+
else:
383+
# Try to create a custom license from available fields
384+
custom_license = CustomLicense(**latest_version)
385+
if custom_license.model_dump(exclude_none=True):
386+
dataset.license = custom_license
387+
388+
dataset.p_id = latest_version.datasetPersistentId # type: ignore
389+
blocks = latest_version.metadataBlocks # type: ignore
390+
files = latest_version.files # type: ignore
379391

380392
# Process metadatablocks and files
381393
self._construct_block_classes(blocks, dataset)
@@ -554,7 +566,7 @@ def _extract_data(self, fields: List, tree: Node):
554566
dvtype = node.typeClass
555567

556568
if dvtype.lower() == "compound":
557-
data[name] = self._process_compound(field.value, tree)
569+
data[name] = self._process_compound(field.value, node)
558570
else:
559571
data[name] = field.value
560572
else:
@@ -571,7 +583,7 @@ def _process_compound(self, compound, tree):
571583
self._extract_data(list(entry.values()), tree) for entry in compound
572584
]
573585

574-
return self._extract_data(compound, tree)
586+
return self._extract_data(compound.values(), tree)
575587

576588
# ! Importers
577589
def dataset_from_json(self, handler: IO) -> Dataset:

easyDataverse/downloader.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ async def _download_file(
168168

169169
return File(
170170
filepath=local_path,
171-
file_id=str(file_id),
171+
file_id=str(file_id), # type: ignore
172172
**file,
173173
)
174174

easyDataverse/license.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
from typing import Optional
12
from urllib import parse
2-
from pydantic import BaseModel, Field
3+
from pydantic import BaseModel, ConfigDict, Field
34
import httpx
45

56

@@ -68,3 +69,64 @@ def fetch_by_name(cls, name: str, server_url: str) -> "License":
6869
return next(filter(lambda x: x["name"] == name, licenses))
6970
except StopIteration:
7071
raise Exception(f"License '{name}' not found at '{server_url}'")
72+
73+
74+
class CustomLicense(BaseModel):
75+
"""
76+
Represents a custom license for a Dataverse dataset.
77+
78+
This class models the custom terms of use information including name, URI, and other metadata
79+
that can be associated with a dataset in Dataverse.
80+
"""
81+
82+
model_config = ConfigDict(
83+
populate_by_name=True,
84+
)
85+
86+
terms_of_use: Optional[str] = Field(
87+
default=None,
88+
description="The terms of use of the dataset.",
89+
alias="termsOfUse",
90+
)
91+
92+
confidentiality_declaration: Optional[str] = Field(
93+
default=None,
94+
description="The confidentiality declaration of the dataset.",
95+
alias="confidentialityDeclaration",
96+
)
97+
98+
special_permissions: Optional[str] = Field(
99+
default=None,
100+
description="Special permissions for the dataset.",
101+
alias="specialPermissions",
102+
)
103+
104+
restrictions: Optional[str] = Field(
105+
default=None,
106+
description="Restrictions applied to the dataset.",
107+
alias="restrictions",
108+
)
109+
110+
citation_requirements: Optional[str] = Field(
111+
default=None,
112+
description="Requirements for citing the dataset.",
113+
alias="citationRequirements",
114+
)
115+
116+
depositor_requirements: Optional[str] = Field(
117+
default=None,
118+
description="Requirements for depositors of the dataset.",
119+
alias="depositorRequirements",
120+
)
121+
122+
conditions: Optional[str] = Field(
123+
default=None,
124+
description="Conditions for using the dataset.",
125+
alias="conditions",
126+
)
127+
128+
disclaimer: Optional[str] = Field(
129+
default=None,
130+
description="Disclaimer for the dataset.",
131+
alias="disclaimer",
132+
)

tests/integration/test_dataset_creation.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from easyDataverse.dataset import Dataset
44

55
from easyDataverse.dataverse import Dataverse
6+
from easyDataverse.license import CustomLicense
67

78

89
class TestDatasetCreation:
@@ -130,6 +131,66 @@ def test_creation_other_license(
130131

131132
assert self.sort_citation(dataset) == minimal_upload_other_license
132133

134+
@pytest.mark.integration
135+
def test_creation_custom_terms_of_use(
136+
self,
137+
credentials,
138+
):
139+
# Arrange
140+
base_url, api_token = credentials
141+
dataverse = Dataverse(
142+
server_url=base_url,
143+
api_token=api_token,
144+
)
145+
146+
# Act
147+
dataset = dataverse.create_dataset()
148+
dataset.license = CustomLicense(
149+
termsOfUse="This is a custom terms of use",
150+
confidentialityDeclaration="This is a custom confidentiality declaration",
151+
specialPermissions="This is a custom special permissions",
152+
restrictions="This is a custom restrictions",
153+
citationRequirements="This is a custom citation requirements",
154+
depositorRequirements="This is a custom depositor requirements",
155+
conditions="This is a custom conditions",
156+
disclaimer="This is a custom disclaimer",
157+
)
158+
159+
dataset.citation.title = "My dataset"
160+
dataset.citation.subject = ["Other"]
161+
dataset.citation.add_author(name="John Doe")
162+
dataset.citation.add_ds_description(
163+
value="This is a description of the dataset",
164+
date="2024",
165+
)
166+
dataset.citation.add_dataset_contact(
167+
name="John Doe",
168+
email="john@doe.com",
169+
)
170+
171+
pid = dataset.upload(dataverse_name="root")
172+
173+
# Re-fetch the dataset
174+
dataset = dataverse.load_dataset(pid)
175+
176+
# Check the terms of use
177+
assert isinstance(dataset.license, CustomLicense)
178+
license = dataset.license
179+
assert license.terms_of_use == "This is a custom terms of use"
180+
assert license.special_permissions == "This is a custom special permissions"
181+
assert license.restrictions == "This is a custom restrictions"
182+
assert license.citation_requirements == "This is a custom citation requirements"
183+
assert license.conditions == "This is a custom conditions"
184+
assert license.disclaimer == "This is a custom disclaimer"
185+
assert (
186+
license.confidentiality_declaration
187+
== "This is a custom confidentiality declaration"
188+
)
189+
assert (
190+
license.depositor_requirements == "This is a custom depositor requirements"
191+
)
192+
193+
@pytest.mark.integration
133194
def test_tab_ingest_disabled(
134195
self,
135196
credentials,

tests/unit/test_base.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,8 @@
66

77

88
class TestBase:
9-
109
@pytest.mark.unit
1110
def test_template(self):
12-
1311
# Arrange
1412
class Child(DataverseBase):
1513
bar: Optional[str] = Field(
@@ -18,7 +16,6 @@ class Child(DataverseBase):
1816
)
1917

2018
class Test(DataverseBase):
21-
2219
foo: Optional[str] = Field(
2320
default=None,
2421
alias="Foo",

0 commit comments

Comments
 (0)