diff --git a/README.md b/README.md index 36daf6a..ebc096b 100644 --- a/README.md +++ b/README.md @@ -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 @@ -158,6 +160,7 @@ print(response) For predefined licenses, pass `licenseAbbreviation=License.` 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=...)`. diff --git a/docs/demo_upload.py b/docs/demo_upload.py index 0a08169..3a3c1ae 100644 --- a/docs/demo_upload.py +++ b/docs/demo_upload.py @@ -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 diff --git a/docs/index.md b/docs/index.md index 2a1db39..05323dc 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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 @@ -174,6 +176,8 @@ print(response) For predefined licenses, pass `licenseAbbreviation=License.` 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=...)`. diff --git a/docs/upload.md b/docs/upload.md index dff4a7f..9b4a2c4 100644 --- a/docs/upload.md +++ b/docs/upload.md @@ -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 @@ -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 @@ -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. @@ -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 diff --git a/src/datacollective/models.py b/src/datacollective/models.py index 656eb9a..47b303a 100644 --- a/src/datacollective/models.py +++ b/src/datacollective/models.py @@ -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." @@ -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): """ @@ -349,6 +375,8 @@ def get(self, key: str, default: Any = None) -> Any: "showContactInfo", "visibility", "exclusivityOptOut", + "isPaid", + "basePriceCents", } SUBMIT_FIELDS = {"agreeToSubmit"} diff --git a/tests/test_models.py b/tests/test_models.py index e6986aa..56f7d90 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -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"})