Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
1d32092
Restructured API paths to conform with OGC spec
Sep 24, 2026
d395e1b
Fixed Ninja schema generation issue
Sep 24, 2026
1d7e9d4
Added shared bbox filter schemas and service
Sep 24, 2026
976a50f
Update monitoring sites to use shared bbox filter
Sep 24, 2026
0a00078
Added ExtentQueryParameter schema to API
Sep 24, 2026
537ca70
Added bbox filtering for workspaces, datastreams, and observations
Sep 24, 2026
67a192e
Add shared datetime param schema for OGC API
Sep 24, 2026
e917fbf
Hook up new datetime param to datastreams, observations, and QC histo…
Sep 24, 2026
4d85b09
Make datastream observation statistics read-only
Sep 24, 2026
73eb164
Make datastream's phenomenon_end_time read-only
Sep 25, 2026
95b8338
Add datetime filter for workspaces and monitoring sites
Sep 25, 2026
f986af9
Clamp API limit param instead of returning an error
Sep 25, 2026
3709aad
Fix sortby to allow comma-separated fields
Sep 25, 2026
7308800
Fixed sortby bug in processing apps
Sep 25, 2026
030b52c
Reject unknown query params in OGC API
Sep 25, 2026
79dd2ed
Add self and pagination links to API responses
Sep 25, 2026
8236d54
Create OGC API collection registry
Sep 25, 2026
16006c3
Added base collections endpoints
Sep 25, 2026
e0873ec
Update API links on item responses
Sep 25, 2026
f164c2a
Added landing page and conformance API endpoints
Sep 28, 2026
8ac2d06
Fixed lax validations in OGC API
Sep 28, 2026
0ad8ee9
Updated linked resource and task run responses for consistency with o…
Sep 28, 2026
93a4b29
Merge branch 'release/v2.0.0' into 520-ogc-api
Sep 30, 2026
1dbccab
Removed unused observation signal option
Sep 30, 2026
a030cb7
Added endpoints and client package methods to get linked resources by ID
Sep 30, 2026
e6ea7fc
Fixed paginatedFetch in hydroserver-ts
Sep 30, 2026
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
18 changes: 9 additions & 9 deletions apps/data-management/e2e/specs/sites.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ test.describe('sites and workspaces', () => {
test('Browse metadata controls stay on one line with multiple values', async ({
page,
}) => {
await page.route('**/api/data/monitoring-sites/site-summaries*', async (route) => {
await page.route('**/api/ogc/collections/monitoring-sites/site-summaries*', async (route) => {
const response = await route.fetch()
const summaries = (await response.json()) as Array<{
tags: Record<string, string>
Expand Down Expand Up @@ -629,7 +629,7 @@ test.describe('sites and workspaces', () => {
await expect
.poll(async () => {
const response = await page.request.get(
`${apiBaseUrl}/api/data/monitoring-sites/${fixtures.monitoringSites.public.id}`
`${apiBaseUrl}/api/ogc/collections/monitoring-sites/items/${fixtures.monitoringSites.public.id}`
)
const monitoringSite = await response.json()
return monitoringSite.data.isPrivate
Expand All @@ -638,7 +638,7 @@ test.describe('sites and workspaces', () => {
await page.getByRole('button', { name: 'Close' }).click()

const blockedMonitoringSiteRequest = anonymousPage.waitForResponse((response) =>
response.url().includes(`/api/data/monitoring-sites/${fixtures.monitoringSites.public.id}`)
response.url().includes(`/api/ogc/collections/monitoring-sites/items/${fixtures.monitoringSites.public.id}`)
)
await anonymousPage.goto(`/sites/${fixtures.monitoringSites.public.id}`)
const blockedMonitoringSiteResponse = await blockedMonitoringSiteRequest
Expand All @@ -650,7 +650,7 @@ test.describe('sites and workspaces', () => {
await expect
.poll(async () => {
const response = await page.request.get(
`${apiBaseUrl}/api/data/monitoring-sites/${fixtures.monitoringSites.public.id}`
`${apiBaseUrl}/api/ogc/collections/monitoring-sites/items/${fixtures.monitoringSites.public.id}`
)
const monitoringSite = await response.json()
return monitoringSite.data.isPrivate
Expand All @@ -665,7 +665,7 @@ test.describe('sites and workspaces', () => {
await expect
.poll(async () => {
const response = await page.request.get(
`${apiBaseUrl}/api/data/datastreams/${fixtures.datastreams.public.id}`
`${apiBaseUrl}/api/ogc/collections/datastreams/items/${fixtures.datastreams.public.id}`
)
const datastream = await response.json()
return datastream.data.isPrivate
Expand All @@ -679,7 +679,7 @@ test.describe('sites and workspaces', () => {
await expect
.poll(async () => {
const response = await page.request.get(
`${apiBaseUrl}/api/data/datastreams/${fixtures.datastreams.public.id}`
`${apiBaseUrl}/api/ogc/collections/datastreams/items/${fixtures.datastreams.public.id}`
)
const datastream = await response.json()
return datastream.data.isPrivate
Expand All @@ -688,7 +688,7 @@ test.describe('sites and workspaces', () => {
await expect
.poll(async () => {
const response = await page.request.get(
`${apiBaseUrl}/api/data/datastreams/${fixtures.datastreams.public.id}`
`${apiBaseUrl}/api/ogc/collections/datastreams/items/${fixtures.datastreams.public.id}`
)
const datastream = await response.json()
return datastream.data.isVisible
Expand All @@ -699,7 +699,7 @@ test.describe('sites and workspaces', () => {
await expect
.poll(async () => {
const response = await page.request.get(
`${apiBaseUrl}/api/data/datastreams/${fixtures.datastreams.public.id}`
`${apiBaseUrl}/api/ogc/collections/datastreams/items/${fixtures.datastreams.public.id}`
)
const datastream = await response.json()
return datastream.data.isVisible
Expand All @@ -715,7 +715,7 @@ test.describe('sites and workspaces', () => {
await expect
.poll(async () => {
const response = await page.request.get(
`${apiBaseUrl}/api/data/datastreams/${fixtures.datastreams.public.id}`
`${apiBaseUrl}/api/ogc/collections/datastreams/items/${fixtures.datastreams.public.id}`
)
const datastream = await response.json()
return datastream.data.isVisible
Expand Down
6 changes: 3 additions & 3 deletions apps/data-management/e2e/specs/workspaces.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@ test.describe('workspace management', () => {
page,
}) => {
let failWorkspaceRequests = true
await page.route('**/api/data/workspaces?**', async (route) => {
await page.route('**/api/ogc/collections/workspaces/items?**', async (route) => {
if (failWorkspaceRequests) {
await route.fulfill({
status: 503,
Expand Down Expand Up @@ -427,7 +427,7 @@ test.describe('workspace management', () => {
page,
}) => {
await page.route(
`**/api/data/workspaces/${fixtures.workspaces.private.id}/service-accounts?**`,
`**/api/ogc/collections/workspaces/items/${fixtures.workspaces.private.id}/service-accounts?**`,
(route) =>
route.fulfill({
status: 503,
Expand All @@ -452,7 +452,7 @@ test.describe('workspace management', () => {
test('a slow overview response cannot overwrite the newly selected workspace', async ({
page,
}) => {
await page.route('**/api/data/monitoring-sites/site-summaries?**', async (route) => {
await page.route('**/api/ogc/collections/monitoring-sites/site-summaries?**', async (route) => {
const workspaceId = new URL(route.request().url()).searchParams.get(
'workspace_id'
)
Expand Down
2 changes: 1 addition & 1 deletion apps/data-management/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export default defineConfig({
E2E_TESTING: 'True',
E2E_CONTROL_TOKEN: e2eControlToken,
},
url: `${apiBaseUrl}/api/data/workspaces`,
url: `${apiBaseUrl}/api/ogc/collections/workspaces/items`,
// Isolated per-test scenarios make reusing a developer's local API safe
// and remove repeated migration/startup cost. CI always starts fresh.
reuseExistingServer: !process.env.CI,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,6 @@ describe('useOrchestrationData', () => {
workspace_id: ['workspace-1'],
data_connection_id: ['dc-1'],
sortby: ['name'],
expand_related: true,
})

await data.fetchTasksForGroup('aggregation', 'monitoringSite-1', 'workspace-1')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,7 @@ export function useOrchestrationData() {
workspace_id: [requestedWorkspaceId],
data_connection_id: [groupId],
sortby: ['name'],
expand_related: true,
} as any)
})
if (requestId !== taskRequestId) return
workspaceTasks.value = items as any
} else if (tab === 'aggregation') {
Expand Down
9 changes: 3 additions & 6 deletions apps/data-management/src/store/observations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,7 @@ export const useObservationStore = defineStore('observations', () => {
const fetchedData = (await fetchObservations(
datastream,
beginTime,
endTime,
signal
endTime
)) as ObservationArray
ensureLatest()
observationsRaw.value[id] = parseObservationRows(fetchedData)
Expand All @@ -153,8 +152,7 @@ export const useObservationStore = defineStore('observations', () => {
beginDataPromise = fetchObservations(
datastream,
beginTime,
new Date(storedStart).toISOString(),
signal
new Date(storedStart).toISOString()
) as Promise<ObservationArray>
}

Expand All @@ -164,8 +162,7 @@ export const useObservationStore = defineStore('observations', () => {
endDataPromise = fetchObservations(
datastream,
new Date(storedEnd).toISOString(),
endTime,
signal
endTime
) as Promise<ObservationArray>
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,13 +134,12 @@ describe('fetchObservations', () => {
const data = await fetchObservations(createDatastream(), start, end)

expect(data).toEqual([['2023-01-01T00:00:00Z', 5]])
expect(getObservationsMock).toHaveBeenCalledWith(
'123',
expect.objectContaining({
phenomenon_time_min: start,
phenomenon_time_max: end,
})
)
expect(getObservationsMock).toHaveBeenCalledWith('123', {
sortby: ['phenomenonTime'],
limit: 50_000,
format: 'column',
datetime: `${start}/${end}`,
})
})

it('returns empty for non-ok or invalid payloads', async () => {
Expand Down
7 changes: 2 additions & 5 deletions apps/data-management/src/utils/observationsUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ const toObservationRows = (columnar: Record<string, unknown>): DataArray => {
export const fetchObservations = async (
datastream: Datastream,
startTime: string | null = null,
endTime: string | null = null,
signal?: AbortSignal
endTime: string | null = null
) => {
const { id, phenomenonBeginTime, phenomenonEndTime } = datastream
if (!phenomenonBeginTime || !phenomenonEndTime) return []
Expand All @@ -40,10 +39,8 @@ export const fetchObservations = async (
sortby: ['phenomenonTime'],
limit: 50_000,
format: 'column',
phenomenon_time_min: startTime ?? phenomenonBeginTime,
phenomenon_time_max: endTime ?? phenomenonEndTime,
datetime: `${startTime ?? phenomenonBeginTime}/${endTime ?? phenomenonEndTime}`,
}
if (signal) options.signal = signal

const res = await hs.datastreams.getObservations(id, options)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -568,8 +568,7 @@ const observations = await hs.datastreams.getObservations(
{
format: "row",
sortby: ["phenomenonTime"],
phenomenon_time_min: "2025-01-01T00:00:00Z",
phenomenon_time_max: "2025-12-31T23:59:59Z",
datetime: "2025-01-01T00:00:00Z/2025-12-31T23:59:59Z",
page_size: 1000,
}
);
Expand Down Expand Up @@ -907,8 +906,7 @@ const sourceRes = await hs.datastreams.getObservations(
{
format: "record",
sortby: ["phenomenonTime"],
phenomenon_time_min: rangeStart,
phenomenon_time_max: rangeEnd,
datetime: `${rangeStart}/${rangeEnd}`,
}
);

Expand Down
2 changes: 1 addition & 1 deletion apps/docs/docs/references/hydroserver-apis.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ HydroServer implements the [OGC SensorThings API](https://www.ogc.org/standard/s

The interactive documentation for this API is available at:

https://playground.hydroserver.org/api/data/docs
https://playground.hydroserver.org/api/ogc/docs

### About

Expand Down
14 changes: 6 additions & 8 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 @@ -374,11 +374,11 @@ Result-qualifier creation accepts `name` and `description` by position or by key
| `intended_time_spacing_unit` | `str \| None` | Yes | `"seconds"`, `"minutes"`, `"hours"`, `"days"` |
| `status` | `str \| None` | Yes | |
| `result_type` | `str` | Yes | |
| `value_count` | `int \| None` | Yes | |
| `phenomenon_begin_time` | `datetime \| None` | Yes | |
| `phenomenon_end_time` | `datetime \| None` | Yes | |
| `result_begin_time` | `datetime \| None` | Yes | |
| `result_end_time` | `datetime \| None` | Yes | |
| `value_count` | `int \| None` | No | Maintained from observations |
| `phenomenon_begin_time` | `datetime \| None` | No | Maintained from observations |
| `phenomenon_end_time` | `datetime \| None` | No | Maintained from observations |
| `result_begin_time` | `datetime \| None` | No | Maintained from observations |
| `result_end_time` | `datetime \| None` | No | Maintained from observations |
| `is_private` | `bool` | Yes | |
| `is_visible` | `bool` | Yes | |
| `monitoring_site_id` | `UUID` | Yes | |
Expand Down Expand Up @@ -412,9 +412,7 @@ hs_api.datastreams.create(name, description, monitoring_site, method, observed_p
unit, observation_type, result_type, sampled_medium, no_data_value,
aggregation_statistic, time_aggregation_interval,
time_aggregation_interval_unit, intended_time_spacing=None,
intended_time_spacing_unit=None, status=None, value_count=None,
phenomenon_begin_time=None, phenomenon_end_time=None,
result_begin_time=None, result_end_time=None,
intended_time_spacing_unit=None, status=None,
is_private=False, is_visible=True, uid=None) -> Datastream
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@ stage_datastream = hs_api.datastreams.create(
time_aggregation_interval_unit='minutes',
intended_time_spacing=15,
intended_time_spacing_unit='minutes',
phenomenon_end_time='2025-01-01T00:00:00Z',
status='Ongoing',
is_private=False,
)
Expand All @@ -96,7 +95,7 @@ print(f"{stage_datastream.name}: {stage_datastream.uid}")

## Create a data connection

A data connection describes where your data comes from and how to read it. Our source is the USGS Instantaneous Values API, which returns JSON. We'll define two run-time placeholder variables — `start_date` and `end_date` — that get substituted into the URL each time the task runs. HydroServer automatically sets `start_date` to the timestamp of the most recent observation already in the datastream, so you never re-fetch data you already have.
A data connection describes where your data comes from and how to read it. Our source is the USGS Instantaneous Values API, which returns JSON. We'll define two run-time placeholder variables — `start_date` and `end_date` — that get substituted into the URL each time the task runs. HydroServer automatically sets `start_date` to the timestamp of the most recent observation already in the datastream, so you never re-fetch data you already have. Our datastream is new and has no observations yet, so the first run starts from 1970-01-01 and pulls in all the history USGS has for this gage; later runs pick up from the most recent observation.

```python
usgs_data_connection = hs_api.dataconnections.create(
Expand Down
6 changes: 3 additions & 3 deletions apps/qc-app/docs/API_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ qc-utils service layer lives under `packages/qc-utils/src/services/`.

### Observation read

Paged GET against `/data/datastreams/{id}/observations` with
`format=column`, `sortby=phenomenonTime`, `page_size=50000`, and
`phenomenon_time_min` / `phenomenon_time_max` ISO-8601 bounds. The app
Paged GET against `/api/ogc/collections/observations/items` with
`datastream_id={id}`, `format=column`, `sortby=phenomenonTime`,
`limit=50000`, and a `datetime` interval (`start/end`, RFC 3339). The app
paginates client-side and caches the merged window in
`useObservationStore`.

Expand Down
6 changes: 3 additions & 3 deletions apps/qc-app/e2e/plot-no-intended-spacing.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ async function patchDatastreamFixture(route: Route): Promise<void> {
processingLevels,
units,
}
if (/\/api\/data\/datastreams$/.test(path)) {
if (/\/api\/ogc\/collections\/datastreams\/items$/.test(path)) {
const patched = datastreams.map(withoutIntendedSpacing)
return route.fulfill({
status: 200,
Expand All @@ -102,7 +102,7 @@ async function patchDatastreamFixture(route: Route): Promise<void> {
}),
})
}
const single = path.match(/\/api\/data\/datastreams\/([^/]+)$/)
const single = path.match(/\/api\/ogc\/collections\/datastreams\/items\/([^/]+)$/)
if (single) {
const id = single[1]
const ds = datastreams.find((d) => d.id === id) ?? datastreams[0]!
Expand All @@ -124,7 +124,7 @@ test.describe('plot: datastream without intendedTimeSpacing', () => {
// wins for both the list and single-get endpoints. The sub-path
// `/observations` is left to fall through.
await page.route(
/\/api\/data\/datastreams(\/[^/]+)?(\?.*)?$/,
/\/api\/ogc\/collections\/datastreams\/items(\/[^/]+)?(\?.*)?$/,
patchDatastreamFixture
)
})
Expand Down
Loading
Loading