Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ submission = DatasetSubmission(
ethicalReviewProcess="Describe the ethical review process that was "
"followed for this dataset, including any approvals "
"or considerations related to data collection and usage.",
isPaid=False, # True = the dataset is compensated and requires `basePriceCents`,
# False (default) = the dataset is free to access
exclusivityOptOut=False, # True = This dataset is non-exclusive to Mozilla Data Collective,
# False = Dataset is exclusively hosted in Mozilla Data Collective
agreeToSubmit=True, # True = You confirm that you have the right to submit this dataset and
Expand All @@ -158,6 +160,7 @@ print(response)

For predefined licenses, pass `licenseAbbreviation=License.<VALUE>` and leave `licenseUrl` and `license` unset. For custom licenses, pass a custom string to `license` and optionally include `licenseUrl` and `licenseAbbreviation`.

To publish a compensated dataset, set `isPaid=True` and a `basePriceCents` price in **USD cents** (US Dollars), e.g. `basePriceCents=100_000` for $1,000.00.
> [!TIP]
> To also attach an optional sample of your dataset, pass `sample_file_path="/path/to/dataset-sample.tar.gz"` to `create_submission_with_upload`, or upload it separately with `upload_sample_file(file_path=..., submission_id=...)`.

Expand Down
2 changes: 2 additions & 0 deletions docs/demo_upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
"or considerations related to data collection and usage.",
showContactInfo=False, # Whether to publicly display the contact information above
visibility=Visibility.PUBLIC, # public | private | restricted
isPaid=False, # If True then compensated dataset and requires basePriceCents, If False (default) = free dataset
# basePriceCents=100_000, # Required if isPaid=True. Price in USD cents (in this example: $1,000.00).
exclusivityOptOut=True, # True = dataset is not exclusive to Data Collective (can be found elsewhere),
# False = dataset is exclusively shared in Mozilla Data Collective
agreeToSubmit=True, # True = You confirm that you have the right to submit this dataset and that all information provided in the datasheet is accurate. Required to be true to complete the submission
Expand Down
4 changes: 4 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ submission = DatasetSubmission(
"or considerations related to data collection and usage.",
showContactInfo=False, # Whether to publicly display the contact information above
visibility=Visibility.PUBLIC, # public | private | restricted
isPaid=False, # True = the dataset is compensated and requires `basePriceCents`,
# False (default) = the dataset is free to access
exclusivityOptOut=False, # True = This dataset is non-exclusive to Mozilla Data Collective,
# False = Dataset is exclusively hosted in Mozilla Data Collective
agreeToSubmit=True, # True = You confirm that you have the right to submit this dataset and
Expand All @@ -174,6 +176,8 @@ print(response)

For predefined licenses, pass `licenseAbbreviation=License.<VALUE>` and leave `licenseUrl` and `license` unset. For a custom license, pass a custom string to `license` and optionally include `licenseUrl` and `licenseAbbreviation`.

To publish a compensated dataset instead of a free one, set `isPaid=True` and a `basePriceCents` price in **USD cents** (US Dollars). See [Pricing](upload.md#pricing) for details.

> [!TIP]
> To also attach an optional sample of your dataset, pass `sample_file_path="/path/to/dataset-sample.tar.gz"` to `create_submission_with_upload`, or upload it separately with `upload_sample_file(file_path=..., submission_id=...)`.

Expand Down
46 changes: 46 additions & 0 deletions docs/upload.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ submission = DatasetSubmission(
"or considerations related to data collection and usage.",
showContactInfo=False, # Whether to publicly display the contact information above
visibility=Visibility.PUBLIC, # public | private | restricted
isPaid=False, # True = the dataset is compensated and requires `basePriceCents`,
# False (default) = the dataset is free to access
exclusivityOptOut=False, # True = This dataset is non-exclusive to Mozilla Data Collective,
# False = Dataset is exclusively hosted in Mozilla Data Collective
agreeToSubmit=True, # True = You confirm that you have the right to submit this dataset and
Expand Down Expand Up @@ -121,6 +123,47 @@ See [Uploading a Sample File](#uploading-a-sample-file) for details.
| `Visibility.PRIVATE` | Everyone | Your organization & Approved requesters only |
| `Visibility.RESTRICTED` | Your organization | Your organization (via SDK) |


### Pricing

Datasets are free by default (`isPaid` defaults to `False` on the platform when left unset). To
publish a **compensated** dataset, set `isPaid=True` and provide a price in `basePriceCents`:

```python
from datacollective import DatasetSubmission

submission = DatasetSubmission(
name="Dataset Name",
# ... other metadata fields ...
isPaid=True,
basePriceCents=100_000, # $1,000.00
)
```

> [!IMPORTANT]
> `basePriceCents` is expressed in **USD cents** (US Dollars), not in dollars.
> For example, `basePriceCents=100_000` sets the price to **$1,000.00 USD**.

> [!NOTE]
> The platform validates that the price falls within an acceptable range and rejects the
> submission otherwise.

- `isPaid=True` requires `basePriceCents` to be set.
- `basePriceCents` cannot be set unless `isPaid=True`, since the price would otherwise be
ignored and the dataset would stay uncompensated.

To change the price of an existing submission, pass both fields to `update_submission`:

```python
from datacollective import DatasetSubmission, update_submission

update_submission(
submission_id=submission_id,
submission=DatasetSubmission(isPaid=True, basePriceCents=250_000), # $2,500.00
)
```


## Uploading a Sample File

A **sample file** is a small, representative excerpt of your dataset that reviewers and
Expand Down Expand Up @@ -171,6 +214,7 @@ upload never overwrite each other's resume state.
> sample file right after, before the submission is sent for review. A missing
> `sample_file_path` raises `FileNotFoundError` up front, before anything is uploaded.


## Upload a New File Version to an Approved Dataset

Use `upload_dataset_file` when the dataset already exists on the platform and is already in the **Published / Approved** state.
Expand Down Expand Up @@ -219,6 +263,8 @@ To complete the submission process, the submission **must** include at least all
- `visibility`
- `agreeToSubmit=True`

Pricing (`isPaid` and `basePriceCents`) is optional — see [Pricing](#pricing). Datasets are free unless `isPaid=True`.

A completed file upload must also be attached to the submission before it can be submitted for review. The uploaded archive is linked to the submission automatically when the multipart upload completes (the upload is started with the submission's ID).

## Step-by-Step Upload
Expand Down
28 changes: 28 additions & 0 deletions src/datacollective/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,18 @@ class Dataset(BaseModel):
None,
description="Dataset visibility (e.g., `public`, `private`, `restricted`).",
)
isPaid: bool | None = Field(
None,
description="Whether the dataset is compensated, i.e. has a price. Defaults to `False` on the platform when left unset.",
)
basePriceCents: int | None = Field(
None,
description=(
"Price of the dataset in USD cents (e.g. `100_000` is $1,000.00). Required when "
"`isPaid` is True. The platform validates that the price is within an acceptable "
"range and rejects the submission otherwise."
),
)
# Defined by the API and not user-editable
id: str | None = Field(
None, description="Unique identifier as returned by the API."
Expand Down Expand Up @@ -258,6 +270,20 @@ def _validate_license_details(self) -> DatasetSubmission:
)
return self

@model_validator(mode="after")
def _validate_pricing(self) -> DatasetSubmission:
if self.isPaid and self.basePriceCents is None:
raise ValueError(
"`basePriceCents` is required when `isPaid` is True. The platform only "
"accepts prices within its allowed range, in USD cents"
)
if self.basePriceCents is not None and not self.isPaid:
raise ValueError(
"`isPaid` must be True when providing `basePriceCents`, "
"otherwise the dataset stays uncompensated and the price is ignored"
)
return self


class DatasetDetails(Dataset):
"""
Expand Down Expand Up @@ -349,6 +375,8 @@ def get(self, key: str, default: Any = None) -> Any:
"showContactInfo",
"visibility",
"exclusivityOptOut",
"isPaid",
"basePriceCents",
}
SUBMIT_FIELDS = {"agreeToSubmit"}

Expand Down
30 changes: 30 additions & 0 deletions tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,36 @@ def test_show_contact_info_accepts_boolean() -> None:
assert DatasetSubmission(showContactInfo=False).showContactInfo is False


def test_pricing_defaults_to_unset_free_dataset() -> None:
model = DatasetSubmission(name="Dataset Name")
assert model.isPaid is None
assert model.basePriceCents is None
# Left out of the payload entirely, so the platform default (False) applies
assert "isPaid" not in model.model_dump(exclude_none=True)


def test_paid_dataset_accepts_price_within_platform_bounds() -> None:
model = DatasetSubmission(isPaid=True, basePriceCents=25_000)
assert model.isPaid is True
assert model.basePriceCents == 25_000


def test_paid_dataset_requires_price() -> None:
with pytest.raises(ValidationError) as exc_info:
DatasetSubmission(isPaid=True)
assert "`basePriceCents` is required when `isPaid` is True" in str(exc_info.value)


def test_price_requires_paid_dataset() -> None:
with pytest.raises(ValidationError) as exc_info:
DatasetSubmission(basePriceCents=25_000)
assert "`isPaid` must be True when providing `basePriceCents`" in str(
exc_info.value
)
with pytest.raises(ValidationError):
DatasetSubmission(isPaid=False, basePriceCents=25_000)


def test_dataset_details_requires_id() -> None:
with pytest.raises(ValidationError):
DatasetDetails.model_validate({"filename": "dataset.tar.gz"})
Expand Down
Loading