Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@
>
Apply a mathematical formula to one or more input datastreams to
produce a new output datastream. With more than one input, they're
matched by exact timestamp; if inputs stop lining up, the run stops
there until they're back in sync.
matched by exact timestamp; timestamps not shared by every input
stop the run or are skipped, depending on "Stop on input gaps".
</v-alert>

<v-text-field
Expand Down Expand Up @@ -197,19 +197,23 @@
</div>

<div class="d-flex align-center mb-1">
<div class="text-body-2">Stop on no-data value</div>
<div class="text-body-2">Stop on input gaps</div>
<v-tooltip location="end" max-width="280">
<template #activator="{ props: tp }">
<v-icon v-bind="tp" size="16" color="grey-darken-1" class="ml-1">
{{ mdiInformationOutline }}
</v-icon>
</template>
If an input is set to a no-data value, stop the run there
instead of writing the output's no-data value and continuing.
If an input has no value at a timestamp where the others do, stop
the run there and wait until the missing value arrives. When off,
those timestamps are skipped and the run continues. Runs never go
past the most recent value of the input that is furthest behind.
Inputs holding their no-data value always produce the output's
no-data value.
</v-tooltip>
<v-spacer />
<v-switch
v-model="stopOnNoData"
v-model="stopOnGaps"
:color="DATA_PRODUCT_ACCENT"
density="compact"
hide-details
Expand Down Expand Up @@ -350,7 +354,7 @@ const schedule = ref<TaskSchedule | null>(null)
const outputDatastreamId = ref<string | null>(null)
const inputs = ref<InputRow[]>([makeRow('x')])
const formula = ref('')
const stopOnNoData = ref(true)
const stopOnGaps = ref(false)
const stopOnError = ref(true)

const selectedThingId = computed(() => props.initialThingId ?? null)
Expand Down Expand Up @@ -547,7 +551,7 @@ async function loadExistingTask() {
existingTransformationId.value = t.id
outputDatastreamId.value = (t.outputDatastream as any)?.id ?? null
formula.value = t.formula
stopOnNoData.value = t.stopOnNoData ?? true
stopOnGaps.value = t.stopOnGaps ?? false
stopOnError.value = t.stopOnError ?? true

if (t.inputDatastreams?.length) {
Expand Down Expand Up @@ -617,7 +621,7 @@ async function onCreate(
outputDatastreamId: outputDatastreamId.value!,
inputDatastreams,
formula: formula.value.trim(),
stopOnNoData: stopOnNoData.value,
stopOnGaps: stopOnGaps.value,
stopOnError: stopOnError.value,
}
)
Expand Down Expand Up @@ -658,7 +662,7 @@ async function onUpdate(
outputDatastreamId: outputDatastreamId.value!,
inputDatastreams,
formula: formula.value.trim(),
stopOnNoData: stopOnNoData.value,
stopOnGaps: stopOnGaps.value,
stopOnError: stopOnError.value,
}
)
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/docs/references/data-dictionary.md
Original file line number Diff line number Diff line change
Expand Up @@ -490,7 +490,7 @@ A DataProductTransformation describes how a single derived (output) Datastream i
| M | transformation_type | The type of transformation: `rating_curve`, `derivation`, or `aggregation`. | String |
| O | rating_curve_id | A foreign key identifier for the RatingCurve used when `transformation_type` is `rating_curve`. | UUID |
| O | formula | A formula (expression) string used when `transformation_type` is `derivation`, referencing one or more input variable names. | Text |
| O | stop_on_no_data | For `derivation` transformations, whether a run stops when an input holds its no-data value (vs. filling the output and continuing). | Boolean |
| O | stop_on_gaps | For `derivation` transformations, whether a run stops at timestamps not shared by all inputs (vs. skipping them). Inputs holding their no-data value always produce the output's no-data value. | Boolean |
| O | stop_on_error | For `derivation` transformations, whether a run stops when the formula produces a non-finite result (vs. filling and continuing). | Boolean |
| O | output_interval_units | The interval unit for `aggregation` outputs: `minutes`, `hours`, `days`, `weeks`, or `months`. | String |
| O | output_interval | The positive interval count corresponding to `output_interval_units`. | Integer |
Expand Down
8 changes: 4 additions & 4 deletions apps/docs/docs/user-guides/how-to/using-the-python-client.md
Original file line number Diff line number Diff line change
Expand Up @@ -667,27 +667,27 @@ hs_api.dataproducttransformations.delete_rating_curve(task_id, uid)

#### Derivation Transformations

Combines one or more input datastreams using a formula. Each input datastream is assigned a variable name used in the formula; with a single input, this is equivalent to applying the formula to each observation independently. Multiple inputs are matched by exact timestamp — if they stop lining up, a run stops there until the inputs are back in sync.
Combines one or more input datastreams using a formula. Each input datastream is assigned a variable name used in the formula; with a single input, this is equivalent to applying the formula to each observation independently. Multiple inputs are matched by exact timestamp. Timestamps not shared by every input either stop the run there until the gap is filled or are skipped, depending on `stop_on_gaps`. Runs never go past the most recent value of the input that is furthest behind. If any input holds its no-data value at a timestamp, the output gets its own no-data value there.

| Field | Type | Description |
|---|---|---|
| `id` | `UUID` | |
| `output_datastream_id` | `UUID` | |
| `input_datastream_ids` | `List[UUID]` | |
| `formula` | `str` | Python expression referencing the input variable name(s) |
| `stop_on_no_data` | `bool` | If `True`, stop the run when an input holds its no-data value; if `False`, fill the output with its no-data value and continue |
| `stop_on_gaps` | `bool` | If `True`, stop the run at the first timestamp not shared by all inputs; if `False`, skip those timestamps and continue. Inputs holding their no-data value always produce the output's no-data value |
| `stop_on_error` | `bool` | If `True`, stop the run when the formula produces a non-finite result; if `False`, fill the output with its no-data value and continue |

```python
hs_api.dataproducttransformations.list_derivation(task_id, output_datastream=None, input_datastream=None)
hs_api.dataproducttransformations.get_derivation(task_id, uid)
hs_api.dataproducttransformations.create_derivation(
task_id, output_datastream, input_datastreams, formula,
stop_on_no_data=True, stop_on_error=True, uid=None
stop_on_gaps=False, stop_on_error=True, uid=None
)
hs_api.dataproducttransformations.update_derivation(
task_id, uid, input_datastreams=..., formula=...,
stop_on_no_data=..., stop_on_error=...
stop_on_gaps=..., stop_on_error=...
)
hs_api.dataproducttransformations.delete_derivation(task_id, uid)
```
Expand Down
2 changes: 1 addition & 1 deletion django/contracts/openapi/auth.openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -678,7 +678,7 @@
"info": {
"description": "",
"title": "HydroServer User Authentication API",
"version": "1.12.1"
"version": "1.12.2b1"
},
"openapi": "3.1.0",
"paths": {
Expand Down
24 changes: 12 additions & 12 deletions django/contracts/openapi/data.openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -3739,8 +3739,8 @@
"title": "Stoponerror",
"type": "boolean"
},
"stopOnNoData": {
"title": "Stoponnodata",
"stopOnGaps": {
"title": "Stopongaps",
"type": "boolean"
}
},
Expand Down Expand Up @@ -3775,9 +3775,9 @@
"title": "Stoponerror",
"type": "boolean"
},
"stopOnNoData": {
"default": true,
"title": "Stoponnodata",
"stopOnGaps": {
"default": false,
"title": "Stopongaps",
"type": "boolean"
}
},
Expand Down Expand Up @@ -3814,8 +3814,8 @@
"title": "Stoponerror",
"type": "boolean"
},
"stopOnNoData": {
"title": "Stoponnodata",
"stopOnGaps": {
"title": "Stopongaps",
"type": "boolean"
}
},
Expand All @@ -3824,7 +3824,7 @@
"outputDatastream",
"inputDatastreams",
"formula",
"stopOnNoData",
"stopOnGaps",
"stopOnError"
],
"title": "DerivationTransformationResponse",
Expand Down Expand Up @@ -3858,8 +3858,8 @@
"title": "Stoponerror",
"type": "boolean"
},
"stopOnNoData": {
"title": "Stoponnodata",
"stopOnGaps": {
"title": "Stopongaps",
"type": "boolean"
}
},
Expand All @@ -3868,7 +3868,7 @@
"outputDatastreamId",
"inputDatastreamIds",
"formula",
"stopOnNoData",
"stopOnGaps",
"stopOnError"
],
"title": "DerivationTransformationSummaryResponse",
Expand Down Expand Up @@ -11645,7 +11645,7 @@
"info": {
"description": "",
"title": "HydroServer Data Management API",
"version": "1.12.1"
"version": "1.12.2b1"
},
"openapi": "3.1.0",
"paths": {
Expand Down
2 changes: 1 addition & 1 deletion django/hydroserver/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .celery import app as celery_app

__version__ = "1.12.1"
__version__ = "1.12.2b1"
__all__ = ("celery_app", "__version__",)
8 changes: 4 additions & 4 deletions django/interfaces/api/schemas/products/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ class DerivationTransformationSummaryResponse(BaseGetResponse):
output_datastream_id: uuid.UUID
input_datastream_ids: list[uuid.UUID]
formula: str
stop_on_no_data: bool
stop_on_gaps: bool
stop_on_error: bool

@staticmethod
Expand Down Expand Up @@ -120,7 +120,7 @@ class DerivationTransformationResponse(BaseGetResponse):
output_datastream: DatastreamSummaryResponse
input_datastreams: list[TransformationInputResponse]
formula: str
stop_on_no_data: bool
stop_on_gaps: bool
stop_on_error: bool

@staticmethod
Expand Down Expand Up @@ -164,7 +164,7 @@ class RatingCurveTransformationPostBody(_TransformationPostBodyBase):
class DerivationTransformationPostBody(_TransformationPostBodyBase):
input_datastreams: list[TransformationInputPostBody]
formula: str
stop_on_no_data: bool = True
stop_on_gaps: bool = False
stop_on_error: bool = True


Expand All @@ -190,7 +190,7 @@ class DerivationTransformationPatchBody(BasePatchBody):
output_datastream: uuid.UUID = Field(alias="outputDatastreamId")
input_datastreams: list[TransformationInputPostBody]
formula: str
stop_on_no_data: bool
stop_on_gaps: bool
stop_on_error: bool


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 5.2.2 on 2026-10-19

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('products', '0003_remove_dataproducttransformation_max_gap_interval_and_more'),
]

operations = [
migrations.RenameField(
model_name='dataproducttransformation',
old_name='stop_on_no_data',
new_name='stop_on_gaps',
),
migrations.AlterField(
model_name='dataproducttransformation',
name='stop_on_gaps',
field=models.BooleanField(blank=True, default=False),
),
]
2 changes: 1 addition & 1 deletion django/processing/products/models/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ class DataProductTransformation(models.Model):
blank=True,
)
min_values = models.PositiveIntegerField(null=True, blank=True)
stop_on_no_data = models.BooleanField(default=True, blank=True)
stop_on_gaps = models.BooleanField(default=False, blank=True)
stop_on_error = models.BooleanField(default=True, blank=True)

class Meta:
Expand Down
28 changes: 14 additions & 14 deletions django/processing/products/services/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ def create(
timezone_type: str | None | Unset = Unset,
timezone: str | None | Unset = Unset,
min_values: int | None | Unset = Unset,
stop_on_no_data: bool | Unset = Unset,
stop_on_gaps: bool | Unset = Unset,
stop_on_error: bool | Unset = Unset,
) -> DataProductTransformation:
"""Create a transformation for a task."""
Expand Down Expand Up @@ -208,7 +208,7 @@ def create(
timezone_type=timezone_type,
timezone=timezone,
min_values=min_values,
stop_on_no_data=stop_on_no_data,
stop_on_gaps=stop_on_gaps,
stop_on_error=stop_on_error,
)

Expand All @@ -225,7 +225,7 @@ def create(
timezone_type=timezone_type if timezone_type is not Unset else None,
timezone=timezone if timezone is not Unset else None,
min_values=min_values if min_values is not Unset else None,
stop_on_no_data=stop_on_no_data if stop_on_no_data is not Unset else True,
stop_on_gaps=stop_on_gaps if stop_on_gaps is not Unset else False,
stop_on_error=stop_on_error if stop_on_error is not Unset else True,
)

Expand All @@ -250,7 +250,7 @@ def update(
timezone_type: str | None | Unset = Unset,
timezone: str | None | Unset = Unset,
min_values: int | None | Unset = Unset,
stop_on_no_data: bool | Unset = Unset,
stop_on_gaps: bool | Unset = Unset,
stop_on_error: bool | Unset = Unset,
) -> DataProductTransformation:
"""Update a transformation's parameters and inputs."""
Expand Down Expand Up @@ -300,9 +300,9 @@ def update(
min_values if min_values is not Unset
else (transformation.min_values if transformation_type == "aggregation" else Unset)
),
stop_on_no_data=(
stop_on_no_data if stop_on_no_data is not Unset
else (transformation.stop_on_no_data if transformation_type == "derivation" else Unset)
stop_on_gaps=(
stop_on_gaps if stop_on_gaps is not Unset
else (transformation.stop_on_gaps if transformation_type == "derivation" else Unset)
),
stop_on_error=(
stop_on_error if stop_on_error is not Unset
Expand All @@ -320,7 +320,7 @@ def update(
"timezone_type": timezone_type,
"timezone": timezone,
"min_values": min_values,
"stop_on_no_data": stop_on_no_data,
"stop_on_gaps": stop_on_gaps,
"stop_on_error": stop_on_error,
}
for field, value in editable_fields.items():
Expand Down Expand Up @@ -380,7 +380,7 @@ def validate_transformation(
timezone_type: str | None | Unset = Unset,
timezone: str | None | Unset = Unset,
min_values: int | None | Unset = Unset,
stop_on_no_data: bool | Unset = Unset,
stop_on_gaps: bool | Unset = Unset,
stop_on_error: bool | Unset = Unset,
) -> None:
"""Validate transformation parameters and raise ValueError if invalid."""
Expand Down Expand Up @@ -443,11 +443,11 @@ def validate_transformation(
raise ValueError("rating_curve is required for transformation_type 'rating_curve'.")
if any(v is not Unset for v in (
formula, output_interval_units, output_interval, aggregation_method, min_values,
stop_on_no_data, stop_on_error,
stop_on_gaps, stop_on_error,
)):
raise ValueError(
"formula, output_interval_units, output_interval, aggregation_method, min_values, "
"stop_on_no_data, and stop_on_error must not be set for transformation_type 'rating_curve'."
"stop_on_gaps, and stop_on_error must not be set for transformation_type 'rating_curve'."
)
try:
RatingCurve.objects.get(
Expand Down Expand Up @@ -485,10 +485,10 @@ def validate_transformation(
"transformation_type 'aggregation'."
)
if any(v is not Unset for v in (
rating_curve, formula, stop_on_no_data, stop_on_error,
rating_curve, formula, stop_on_gaps, stop_on_error,
)):
raise ValueError(
"rating_curve, formula, stop_on_no_data, and stop_on_error must not be set "
"rating_curve, formula, stop_on_gaps, and stop_on_error must not be set "
"for transformation_type 'aggregation'."
)

Expand Down Expand Up @@ -623,7 +623,7 @@ def run_derivation(self, transformation: DataProductTransformation) -> int:
result_df = apply_derivation(
inputs=inputs,
formula=transformation.formula,
stop_on_no_data=transformation.stop_on_no_data,
stop_on_gaps=transformation.stop_on_gaps,
stop_on_error=transformation.stop_on_error,
input_no_data_values=input_no_data_values,
output_no_data_value=output_ds.no_data_value,
Expand Down
4 changes: 2 additions & 2 deletions django/tests/fixtures/test_products_data.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@
timezone: null
aggregation_method: null
min_values: null
stop_on_no_data: true
stop_on_gaps: true
stop_on_error: true

- model: products.dataproducttransformationinput
Expand All @@ -143,7 +143,7 @@
timezone: null
aggregation_method: null
min_values: null
stop_on_no_data: true
stop_on_gaps: true
stop_on_error: true

- model: products.dataproducttransformationinput
Expand Down
Loading
Loading