diff --git a/apps/data-management/e2e/specs/sites.spec.ts b/apps/data-management/e2e/specs/sites.spec.ts index d495ad9c..372ab195 100644 --- a/apps/data-management/e2e/specs/sites.spec.ts +++ b/apps/data-management/e2e/specs/sites.spec.ts @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/apps/data-management/e2e/specs/workspaces.spec.ts b/apps/data-management/e2e/specs/workspaces.spec.ts index 1435face..726821ff 100644 --- a/apps/data-management/e2e/specs/workspaces.spec.ts +++ b/apps/data-management/e2e/specs/workspaces.spec.ts @@ -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, @@ -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, @@ -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' ) diff --git a/apps/data-management/playwright.config.ts b/apps/data-management/playwright.config.ts index 15580b1d..29aaf09e 100644 --- a/apps/data-management/playwright.config.ts +++ b/apps/data-management/playwright.config.ts @@ -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, diff --git a/apps/data-management/src/composables/__tests__/useOrchestrationData.spec.ts b/apps/data-management/src/composables/__tests__/useOrchestrationData.spec.ts index 5be21263..a470f98b 100644 --- a/apps/data-management/src/composables/__tests__/useOrchestrationData.spec.ts +++ b/apps/data-management/src/composables/__tests__/useOrchestrationData.spec.ts @@ -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') diff --git a/apps/data-management/src/composables/orchestration/useOrchestrationData.ts b/apps/data-management/src/composables/orchestration/useOrchestrationData.ts index d8c5252b..c9b74fec 100644 --- a/apps/data-management/src/composables/orchestration/useOrchestrationData.ts +++ b/apps/data-management/src/composables/orchestration/useOrchestrationData.ts @@ -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') { diff --git a/apps/data-management/src/store/observations.ts b/apps/data-management/src/store/observations.ts index 4b75f1a6..29b053fa 100644 --- a/apps/data-management/src/store/observations.ts +++ b/apps/data-management/src/store/observations.ts @@ -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) @@ -153,8 +152,7 @@ export const useObservationStore = defineStore('observations', () => { beginDataPromise = fetchObservations( datastream, beginTime, - new Date(storedStart).toISOString(), - signal + new Date(storedStart).toISOString() ) as Promise } @@ -164,8 +162,7 @@ export const useObservationStore = defineStore('observations', () => { endDataPromise = fetchObservations( datastream, new Date(storedEnd).toISOString(), - endTime, - signal + endTime ) as Promise } diff --git a/apps/data-management/src/utils/__tests__/observationsUtils.spec.ts b/apps/data-management/src/utils/__tests__/observationsUtils.spec.ts index 839bfa81..1b282b64 100644 --- a/apps/data-management/src/utils/__tests__/observationsUtils.spec.ts +++ b/apps/data-management/src/utils/__tests__/observationsUtils.spec.ts @@ -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 () => { diff --git a/apps/data-management/src/utils/observationsUtils.ts b/apps/data-management/src/utils/observationsUtils.ts index 557d2971..e3aa695d 100644 --- a/apps/data-management/src/utils/observationsUtils.ts +++ b/apps/data-management/src/utils/observationsUtils.ts @@ -30,8 +30,7 @@ const toObservationRows = (columnar: Record): 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 [] @@ -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) diff --git a/apps/docs/docs/developing-and-contributing/how-to/using-the-typescript-client.md b/apps/docs/docs/developing-and-contributing/how-to/using-the-typescript-client.md index b76f1ce5..30618a3b 100644 --- a/apps/docs/docs/developing-and-contributing/how-to/using-the-typescript-client.md +++ b/apps/docs/docs/developing-and-contributing/how-to/using-the-typescript-client.md @@ -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, } ); @@ -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}`, } ); diff --git a/apps/docs/docs/references/hydroserver-apis.md b/apps/docs/docs/references/hydroserver-apis.md index b7c9271c..f451be91 100644 --- a/apps/docs/docs/references/hydroserver-apis.md +++ b/apps/docs/docs/references/hydroserver-apis.md @@ -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 diff --git a/apps/docs/docs/user-guides/how-to/using-the-python-client.md b/apps/docs/docs/user-guides/how-to/using-the-python-client.md index 44ae5767..6590e95f 100644 --- a/apps/docs/docs/user-guides/how-to/using-the-python-client.md +++ b/apps/docs/docs/user-guides/how-to/using-the-python-client.md @@ -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 | | @@ -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 ``` diff --git a/apps/docs/docs/user-guides/tutorials/getting-started-with-hydroserverpy/part-3-automating-data-ingestion.md b/apps/docs/docs/user-guides/tutorials/getting-started-with-hydroserverpy/part-3-automating-data-ingestion.md index 27da3b0f..76f2bca0 100644 --- a/apps/docs/docs/user-guides/tutorials/getting-started-with-hydroserverpy/part-3-automating-data-ingestion.md +++ b/apps/docs/docs/user-guides/tutorials/getting-started-with-hydroserverpy/part-3-automating-data-ingestion.md @@ -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, ) @@ -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( diff --git a/apps/qc-app/docs/API_REFERENCE.md b/apps/qc-app/docs/API_REFERENCE.md index 0372e2b7..9853d4a3 100644 --- a/apps/qc-app/docs/API_REFERENCE.md +++ b/apps/qc-app/docs/API_REFERENCE.md @@ -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`. diff --git a/apps/qc-app/e2e/plot-no-intended-spacing.spec.ts b/apps/qc-app/e2e/plot-no-intended-spacing.spec.ts index fb62e8d0..59dd86d6 100644 --- a/apps/qc-app/e2e/plot-no-intended-spacing.spec.ts +++ b/apps/qc-app/e2e/plot-no-intended-spacing.spec.ts @@ -89,7 +89,7 @@ async function patchDatastreamFixture(route: Route): Promise { 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, @@ -102,7 +102,7 @@ async function patchDatastreamFixture(route: Route): Promise { }), }) } - 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]! @@ -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 ) }) diff --git a/apps/qc-app/e2e/support/mocks.ts b/apps/qc-app/e2e/support/mocks.ts index 30d02f09..9f1d340f 100644 --- a/apps/qc-app/e2e/support/mocks.ts +++ b/apps/qc-app/e2e/support/mocks.ts @@ -1,7 +1,7 @@ /** * Route-level HydroServer mocks for Playwright. * - * The real QC app talks to a HydroServer instance (`/api/data/*` and + * The real QC app talks to a HydroServer instance (`/api/ogc/*` and * `/api/auth/*`). For e2e specs we don't want to depend on a live * backend: tests are slow, flaky, and can't set up the exact dataset * shape each spec needs. This module registers `page.route()` handlers @@ -12,7 +12,7 @@ * await installMocks(page) * await page.goto('/') * - * The handlers match on path fragments (`/api/data/datastreams/...`) + * The handlers match on path fragments (`/api/ogc/collections/datastreams/items/...`) * regardless of host. */ @@ -178,9 +178,9 @@ export async function installMocks( } // --- Bulk observation create (submit) --- - // Observations moved to a top-level resource: POST /observations/bulk-create + // Observations moved to a top-level resource: POST /collections/observations/bulk-create // with datastreamId in the body, not the path. - const bulkCreate = path === '/api/data/observations/bulk-create' + const bulkCreate = path === '/api/ogc/collections/observations/bulk-create' if (bulkCreate && method === 'POST') { const params = new URL(url).searchParams const body = await safeJson(request) @@ -191,21 +191,19 @@ export async function installMocks( // --- Observations list (columnar) --- // Same restructuring: GET /observations?datastream_id={id}, not nested // under /datastreams/{id}/observations. - const obsList = path === '/api/data/observations' + const obsList = path === '/api/ogc/collections/observations/items' if (obsList && method === 'GET') { const params = new URL(url).searchParams const dsId = params.get('datastream_id') ?? '' const series = observationsById[dsId] ?? observations - // Honour the `phenomenon_time_min` / `phenomenon_time_max` - // params the client always sends. Without this, the app's - // cache-extension logic in `fetchObservationsInRange` (which - // re-fetches the segment outside its cached window every time + // Honour the `datetime` interval the client always sends. Without + // this, the app's cache-extension logic in `fetchObservationsInRange` + // (which re-fetches the segment outside its cached window every time // the range moves) would receive the full fixture series on // each call and stack duplicates into the ObservationRecord — // visible as wrong point counts and a long phantom line // connecting the first and last observations. - const tMin = parseISOorNull(params.get('phenomenon_time_min')) - const tMax = parseISOorNull(params.get('phenomenon_time_max')) + const [tMin, tMax] = parseDatetimeInterval(params.get('datetime')) const sliced = tMin == null && tMax == null ? series : sliceSeries(series, tMin, tMax) // The columnar format spreads its fields at the top level (no `data` @@ -216,26 +214,26 @@ export async function installMocks( } // --- Units --- - const unitGet = path.match(/\/api\/data\/units\/([^/]+)$/) + const unitGet = path.match(/\/api\/ogc\/collections\/units\/items\/([^/]+)$/) if (unitGet && method === 'GET') { const id = unitGet[1] const unit = units.find((u) => u.id === id) ?? units[0] return json(route, unit) } - if (path.endsWith('/api/data/units') && method === 'GET') { + if (path.endsWith('/api/ogc/collections/units/items') && method === 'GET') { return json(route, { data: units, meta: listMeta(units.length) }) } // --- Workspaces --- - if (path.endsWith('/api/data/workspaces') && method === 'GET') { + if (path.endsWith('/api/ogc/collections/workspaces/items') && method === 'GET') { return json(route, { data: workspaces, meta: listMeta(workspaces.length) }) } // --- Monitoring sites / datastreams / processing levels / observed properties --- - if (path.endsWith('/api/data/monitoring-sites') && method === 'GET') { + if (path.endsWith('/api/ogc/collections/monitoring-sites/items') && method === 'GET') { return json(route, { data: monitoringSites, meta: listMeta(monitoringSites.length) }) } - if (path.endsWith('/api/data/datastreams') && method === 'GET') { + if (path.endsWith('/api/ogc/collections/datastreams/items') && method === 'GET') { return json(route, { data: datastreams, meta: listMeta(datastreams.length), @@ -249,21 +247,21 @@ export async function installMocks( }, }) } - if (path.endsWith('/api/data/processing-levels') && method === 'GET') { + if (path.endsWith('/api/ogc/collections/processing-levels/items') && method === 'GET') { return json(route, { data: processingLevels, meta: listMeta(processingLevels.length) }) } - if (path.endsWith('/api/data/observed-properties') && method === 'GET') { + if (path.endsWith('/api/ogc/collections/observed-properties/items') && method === 'GET') { return json(route, { data: observedProperties, meta: listMeta(observedProperties.length) }) } - if (path.endsWith('/api/data/methods') && method === 'GET') { + if (path.endsWith('/api/ogc/collections/methods/items') && method === 'GET') { return json(route, { data: methods, meta: listMeta(methods.length) }) } - if (path.endsWith('/api/data/result-qualifiers') && method === 'GET') { + if (path.endsWith('/api/ogc/collections/result-qualifiers/items') && method === 'GET') { return json(route, { data: resultQualifiers, meta: listMeta(resultQualifiers.length) }) } // --- Single datastream --- - const dsGet = path.match(/\/api\/data\/datastreams\/([^/]+)$/) + const dsGet = path.match(/\/api\/ogc\/collections\/datastreams\/items\/([^/]+)$/) if (dsGet && method === 'GET') { const id = dsGet[1] const ds = datastreams.find((d) => d.id === id) ?? datastreams[0] @@ -301,15 +299,24 @@ async function safeJson(request: ReturnType | any): Promise { + if (!part || part === '..') return null + const t = Date.parse(part) + return Number.isFinite(t) ? t : null + } + if (!value.includes('/')) { + const instant = parseEnd(value) + return [instant, instant] + } + const [start, end] = value.split('/') + return [parseEnd(start), parseEnd(end)] } /** - * Mirror the real backend's `phenomenon_time_min` / `phenomenon_time_max` - * filtering. Bounds are inclusive on both ends, matching how the QC + * Mirror the real backend's `datetime` filtering. Bounds are inclusive on both ends, matching how the QC * app issues its cache-extension queries. */ function sliceSeries( diff --git a/apps/qc-app/src/utils/observations.ts b/apps/qc-app/src/utils/observations.ts index ffab7965..877a8240 100644 --- a/apps/qc-app/src/utils/observations.ts +++ b/apps/qc-app/src/utils/observations.ts @@ -16,8 +16,9 @@ export const fetchObservationsSync = async ( try { const result = await hs.value.datastreams.getObservations(id, { limit: 50_000, - phenomenon_time_min: startTime?.toISOString() ?? phenomenonBeginTime, - phenomenon_time_max: endTime?.toISOString() ?? phenomenonEndTime, + datetime: `${startTime?.toISOString() ?? phenomenonBeginTime}/${ + endTime?.toISOString() ?? phenomenonEndTime + }`, sortby: ['phenomenonTime'], format: 'column', }) diff --git a/django/contracts/openapi/data.openapi.json b/django/contracts/openapi/ogc.openapi.json similarity index 85% rename from django/contracts/openapi/data.openapi.json rename to django/contracts/openapi/ogc.openapi.json index 88a0af2b..07d4cf63 100644 --- a/django/contracts/openapi/data.openapi.json +++ b/django/contracts/openapi/ogc.openapi.json @@ -284,7 +284,6 @@ "properties": { "include": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "role", @@ -293,24 +292,16 @@ ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" }, "limit": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" }, "offset": { "anyOf": [ @@ -328,7 +319,6 @@ }, "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "roleId", @@ -337,7 +327,6 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" }, @@ -352,26 +341,21 @@ "type": "array" }, "sortby": { - "anyOf": [ - { - "items": { - "enum": [ - "id", - "roleId", - "-id", - "-roleId" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" + "items": { + "enum": [ + "id", + "roleId", + "-id", + "-roleId", + "+id", + "+roleId" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" } }, "title": "CollaboratorQueryParameters", @@ -413,6 +397,91 @@ "title": "CollaboratorResponse", "type": "object" }, + "CollectionResponse": { + "description": "A collection's metadata (OGC API - Features Core collection.yaml).", + "properties": { + "description": { + "title": "Description", + "type": "string" + }, + "id": { + "title": "Id", + "type": "string" + }, + "itemType": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Itemtype" + }, + "links": { + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" + }, + "title": { + "title": "Title", + "type": "string" + } + }, + "required": [ + "id", + "title", + "description", + "links" + ], + "title": "CollectionResponse", + "type": "object" + }, + "CollectionsResponse": { + "description": "The collections the API serves (OGC API - Features Core collections.yaml).", + "properties": { + "collections": { + "items": { + "$ref": "#/components/schemas/CollectionResponse" + }, + "title": "Collections", + "type": "array" + }, + "links": { + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" + } + }, + "required": [ + "links", + "collections" + ], + "title": "CollectionsResponse", + "type": "object" + }, + "ConformanceResponse": { + "description": "The conformance classes the API implements (OGC API - Features Core confClasses.yaml).", + "properties": { + "conformsTo": { + "items": { + "type": "string" + }, + "title": "Conformsto", + "type": "array" + } + }, + "required": [ + "conformsTo" + ], + "title": "ConformanceResponse", + "type": "object" + }, "CreatedResponse": { "properties": { "id": { @@ -431,20 +500,17 @@ "properties": { "include": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "workspace" ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" }, "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -464,7 +530,6 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" } @@ -691,33 +756,45 @@ }, "DataConnectionQueryParameters": { "properties": { + "bbox": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], + "type": "array" + }, + "datetime": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, "include": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "workspace" ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" }, "limit": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" }, "offset": { "anyOf": [ @@ -744,7 +821,6 @@ }, "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -764,7 +840,6 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" }, @@ -781,38 +856,39 @@ "title": "Q" }, "sortby": { - "anyOf": [ - { - "items": { - "enum": [ - "id", - "name", - "timestampKey", - "timestampFormat", - "timezoneType", - "timezone", - "workspaceId", - "workspaceName", - "-id", - "-name", - "-timestampKey", - "-timestampFormat", - "-timezoneType", - "-timezone", - "-workspaceId", - "-workspaceName" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" + "items": { + "enum": [ + "id", + "name", + "timestampKey", + "timestampFormat", + "timezoneType", + "timezone", + "workspaceId", + "workspaceName", + "-id", + "-name", + "-timestampKey", + "-timestampFormat", + "-timezoneType", + "-timezone", + "-workspaceId", + "-workspaceName", + "+id", + "+name", + "+timestampKey", + "+timestampFormat", + "+timezoneType", + "+timezone", + "+workspaceId", + "+workspaceName" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" }, "workspace_id": { "default": [], @@ -1135,20 +1211,17 @@ "properties": { "include": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "monitoringSite" ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" }, "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -1162,7 +1235,6 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" } @@ -1249,16 +1321,35 @@ }, "DataProductTaskQueryParameters": { "properties": { + "bbox": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], + "type": "array" + }, + "datetime": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, "include": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "monitoringSite" ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" }, @@ -1290,19 +1381,12 @@ "type": "array" }, "limit": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" }, "monitoring_site_id": { "default": [], @@ -1340,7 +1424,6 @@ }, "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -1354,7 +1437,6 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" }, @@ -1374,47 +1456,57 @@ "default": [], "description": "Filter data product tasks by rating curve ID.", "items": { - "format": "uuid", - "type": "string" + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "const": "null", + "type": "string" + } + ] }, "title": "Rating Curve Id", "type": "array" }, "sortby": { - "anyOf": [ - { - "items": { - "enum": [ - "id", - "name", - "monitoringSiteId", - "monitoringSiteName", - "workspaceId", - "workspaceName", - "latestRunStatus", - "latestRunStartedAt", - "latestRunFinishedAt", - "-id", - "-name", - "-monitoringSiteId", - "-monitoringSiteName", - "-workspaceId", - "-workspaceName", - "-latestRunStatus", - "-latestRunStartedAt", - "-latestRunFinishedAt" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" + "items": { + "enum": [ + "id", + "name", + "monitoringSiteId", + "monitoringSiteName", + "workspaceId", + "workspaceName", + "latestRunStatus", + "latestRunStartedAt", + "latestRunFinishedAt", + "-id", + "-name", + "-monitoringSiteId", + "-monitoringSiteName", + "-workspaceId", + "-workspaceName", + "-latestRunStatus", + "-latestRunStartedAt", + "-latestRunFinishedAt", + "+id", + "+name", + "+monitoringSiteId", + "+monitoringSiteName", + "+workspaceId", + "+workspaceName", + "+latestRunStatus", + "+latestRunStartedAt", + "+latestRunFinishedAt" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" }, "transformation_type": { "default": [], @@ -1513,7 +1605,6 @@ "properties": { "include": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "outputDatastream", @@ -1521,13 +1612,11 @@ ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" }, "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -1547,7 +1636,6 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" } @@ -1861,9 +1949,29 @@ }, "DataProductTransformationQueryParameters": { "properties": { + "bbox": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], + "type": "array" + }, + "datetime": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, "include": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "outputDatastream", @@ -1871,7 +1979,6 @@ ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" }, @@ -1886,19 +1993,12 @@ "type": "array" }, "limit": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" }, "offset": { "anyOf": [ @@ -1926,7 +2026,6 @@ }, "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -1946,33 +2045,28 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" }, "sortby": { - "anyOf": [ - { - "items": { - "enum": [ - "id", - "outputDatastreamId", - "transformationType", - "-id", - "-outputDatastreamId", - "-transformationType" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" + "items": { + "enum": [ + "id", + "outputDatastreamId", + "transformationType", + "-id", + "-outputDatastreamId", + "-transformationType", + "+id", + "+outputDatastreamId", + "+transformationType" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" }, "task_id": { "default": [], @@ -2174,7 +2268,6 @@ "properties": { "include": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "workspace", @@ -2189,13 +2282,11 @@ ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" }, "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -2209,16 +2300,16 @@ "timeAggregationInterval", "status", "resultType", - "valueCount", - "phenomenonBeginTime", - "phenomenonEndTime", - "resultBeginTime", - "resultEndTime", "isPrivate", "isVisible", "timeAggregationIntervalUnit", "intendedTimeSpacing", "intendedTimeSpacingUnit", + "valueCount", + "phenomenonBeginTime", + "phenomenonEndTime", + "resultBeginTime", + "resultEndTime", "monitoringSiteId", "methodId", "observedPropertyId", @@ -2229,7 +2320,6 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" } @@ -2313,71 +2403,11 @@ "title": "Observedpropertyid", "type": "string" }, - "phenomenonBeginTime": { - "anyOf": [ - { - "type": "string" - }, - { - "format": "date-time", - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Phenomenonbegintime" - }, - "phenomenonEndTime": { - "anyOf": [ - { - "type": "string" - }, - { - "format": "date-time", - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Phenomenonendtime" - }, "processingLevelId": { "format": "uuid", "title": "Processinglevelid", "type": "string" }, - "resultBeginTime": { - "anyOf": [ - { - "type": "string" - }, - { - "format": "date-time", - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Resultbegintime" - }, - "resultEndTime": { - "anyOf": [ - { - "type": "string" - }, - { - "format": "date-time", - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Resultendtime" - }, "resultType": { "maxLength": 255, "title": "Resulttype", @@ -2432,18 +2462,6 @@ "format": "uuid", "title": "Unitid", "type": "string" - }, - "valueCount": { - "anyOf": [ - { - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], - "title": "Valuecount" } }, "title": "DatastreamPatchBody", @@ -2539,71 +2557,11 @@ "title": "Observedpropertyid", "type": "string" }, - "phenomenonBeginTime": { - "anyOf": [ - { - "type": "string" - }, - { - "format": "date-time", - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Phenomenonbegintime" - }, - "phenomenonEndTime": { - "anyOf": [ - { - "type": "string" - }, - { - "format": "date-time", - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Phenomenonendtime" - }, "processingLevelId": { "format": "uuid", "title": "Processinglevelid", "type": "string" }, - "resultBeginTime": { - "anyOf": [ - { - "type": "string" - }, - { - "format": "date-time", - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Resultbegintime" - }, - "resultEndTime": { - "anyOf": [ - { - "type": "string" - }, - { - "format": "date-time", - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Resultendtime" - }, "resultType": { "maxLength": 255, "title": "Resulttype", @@ -2652,18 +2610,6 @@ "format": "uuid", "title": "Unitid", "type": "string" - }, - "valueCount": { - "anyOf": [ - { - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], - "title": "Valuecount" } }, "required": [ @@ -2687,9 +2633,29 @@ }, "DatastreamQueryParameters": { "properties": { + "bbox": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], + "type": "array" + }, + "datetime": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, "include": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "workspace", @@ -2704,7 +2670,6 @@ ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" }, @@ -2721,19 +2686,12 @@ "title": "Is Private" }, "limit": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" }, "method_id": { "default": [], @@ -2788,70 +2746,6 @@ "description": "Number of items to skip.", "title": "Offset" }, - "phenomenon_begin_time_max": { - "anyOf": [ - { - "type": "string" - }, - { - "format": "date-time", - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Sets the maximum phenomenon begin time of filtered datastreams.", - "title": "Phenomenon Begin Time Max" - }, - "phenomenon_begin_time_min": { - "anyOf": [ - { - "type": "string" - }, - { - "format": "date-time", - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Sets the minimum phenomenon begin time of filtered datastreams.", - "title": "Phenomenon Begin Time Min" - }, - "phenomenon_end_time_max": { - "anyOf": [ - { - "type": "string" - }, - { - "format": "date-time", - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Sets the maximum phenomenon end time of filtered datastreams.", - "title": "Phenomenon End Time Max" - }, - "phenomenon_end_time_min": { - "anyOf": [ - { - "type": "string" - }, - { - "format": "date-time", - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Sets the minimum phenomenon end time of filtered datastreams.", - "title": "Phenomenon End Time Min" - }, "processing_level_id": { "default": [], "description": "Filter datastreams by processing level ID.", @@ -2864,7 +2758,6 @@ }, "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -2878,16 +2771,16 @@ "timeAggregationInterval", "status", "resultType", - "valueCount", - "phenomenonBeginTime", - "phenomenonEndTime", - "resultBeginTime", - "resultEndTime", "isPrivate", "isVisible", "timeAggregationIntervalUnit", "intendedTimeSpacing", "intendedTimeSpacingUnit", + "valueCount", + "phenomenonBeginTime", + "phenomenonEndTime", + "resultBeginTime", + "resultEndTime", "monitoringSiteId", "methodId", "observedPropertyId", @@ -2898,7 +2791,6 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" }, @@ -2914,70 +2806,6 @@ "description": "Full-text search query. Comma-separated terms are combined with OR; whitespace-separated words within a term are combined with AND.", "title": "Q" }, - "result_begin_time_max": { - "anyOf": [ - { - "type": "string" - }, - { - "format": "date-time", - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Sets the maximum result begin time of filtered datastreams.", - "title": "Result Begin Time Max" - }, - "result_begin_time_min": { - "anyOf": [ - { - "type": "string" - }, - { - "format": "date-time", - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Sets the minimum result begin time of filtered datastreams.", - "title": "Result Begin Time Min" - }, - "result_end_time_max": { - "anyOf": [ - { - "type": "string" - }, - { - "format": "date-time", - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Sets the maximum result end time of filtered datastreams.", - "title": "Result End Time Max" - }, - "result_end_time_min": { - "anyOf": [ - { - "type": "string" - }, - { - "format": "date-time", - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Sets the minimum result end time of filtered datastreams.", - "title": "Result End Time Min" - }, "result_qualifier_id": { "default": [], "description": "Filter datastreams by observation result qualifier ID.", @@ -3007,54 +2835,68 @@ "type": "array" }, "sortby": { - "anyOf": [ - { - "items": { - "enum": [ - "name", - "observationType", - "sampledMedium", - "status", - "resultType", - "isPrivate", - "isVisible", - "aggregationStatistic", - "valueCount", - "phenomenonBeginTime", - "phenomenonEndTime", - "resultBeginTime", - "resultEndTime", - "-name", - "-observationType", - "-sampledMedium", - "-status", - "-resultType", - "-isPrivate", - "-isVisible", - "-aggregationStatistic", - "-valueCount", - "-phenomenonBeginTime", - "-phenomenonEndTime", - "-resultBeginTime", - "-resultEndTime" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" + "items": { + "enum": [ + "name", + "observationType", + "sampledMedium", + "status", + "resultType", + "isPrivate", + "isVisible", + "aggregationStatistic", + "valueCount", + "phenomenonBeginTime", + "phenomenonEndTime", + "resultBeginTime", + "resultEndTime", + "-name", + "-observationType", + "-sampledMedium", + "-status", + "-resultType", + "-isPrivate", + "-isVisible", + "-aggregationStatistic", + "-valueCount", + "-phenomenonBeginTime", + "-phenomenonEndTime", + "-resultBeginTime", + "-resultEndTime", + "+name", + "+observationType", + "+sampledMedium", + "+status", + "+resultType", + "+isPrivate", + "+isVisible", + "+aggregationStatistic", + "+valueCount", + "+phenomenonBeginTime", + "+phenomenonEndTime", + "+resultBeginTime", + "+resultEndTime" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" }, "status": { "default": [], - "description": "Filter monitoring_sites by status.", + "description": "Filter datastreams by status.", "items": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "const": "null", + "type": "string" + } + ] }, "title": "Status", "type": "array" @@ -3498,20 +3340,17 @@ "properties": { "include": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "targetDatastream" ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" }, "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -3520,7 +3359,6 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" } @@ -3582,6 +3420,27 @@ }, "EtlMappingQueryParameters": { "properties": { + "bbox": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], + "type": "array" + }, + "datetime": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, "etl_task_id": { "default": [], "description": "Filter mappings by ETL task ID.", @@ -3594,31 +3453,22 @@ }, "include": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "targetDatastream" ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" }, "limit": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" }, "offset": { "anyOf": [ @@ -3636,7 +3486,6 @@ }, "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -3645,33 +3494,28 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" }, "sortby": { - "anyOf": [ - { - "items": { - "enum": [ - "id", - "sourceIdentifier", - "targetDatastreamId", - "-id", - "-sourceIdentifier", - "-targetDatastreamId" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" + "items": { + "enum": [ + "id", + "sourceIdentifier", + "targetDatastreamId", + "-id", + "-sourceIdentifier", + "-targetDatastreamId", + "+id", + "+sourceIdentifier", + "+targetDatastreamId" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" }, "source_identifier": { "default": [], @@ -3741,20 +3585,17 @@ "properties": { "include": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "dataConnection" ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" }, "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -3769,7 +3610,6 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" } @@ -3867,6 +3707,23 @@ }, "EtlTaskQueryParameters": { "properties": { + "bbox": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], + "type": "array" + }, "data_connection_id": { "default": [], "description": "Filter ETL tasks by data connection ID.", @@ -3877,16 +3734,18 @@ "title": "Data Connection Id", "type": "array" }, + "datetime": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, "include": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "dataConnection" ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" }, @@ -3972,19 +3831,12 @@ "type": "array" }, "limit": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" }, "monitoring_site_id": { "default": [], @@ -4012,7 +3864,6 @@ }, "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -4027,7 +3878,6 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" }, @@ -4044,40 +3894,42 @@ "title": "Q" }, "sortby": { - "anyOf": [ - { - "items": { - "enum": [ - "id", - "name", - "dataConnectionId", - "dataConnectionName", - "workspaceId", - "workspaceName", - "latestRunStatus", - "latestRunStartedAt", - "latestRunFinishedAt", - "-id", - "-name", - "-dataConnectionId", - "-dataConnectionName", - "-workspaceId", - "-workspaceName", - "-latestRunStatus", - "-latestRunStartedAt", - "-latestRunFinishedAt" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" + "items": { + "enum": [ + "id", + "name", + "dataConnectionId", + "dataConnectionName", + "workspaceId", + "workspaceName", + "latestRunStatus", + "latestRunStartedAt", + "latestRunFinishedAt", + "-id", + "-name", + "-dataConnectionId", + "-dataConnectionName", + "-workspaceId", + "-workspaceName", + "-latestRunStatus", + "-latestRunStartedAt", + "-latestRunFinishedAt", + "+id", + "+name", + "+dataConnectionId", + "+dataConnectionName", + "+workspaceId", + "+workspaceName", + "+latestRunStatus", + "+latestRunStartedAt", + "+latestRunFinishedAt" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" }, "workspace_id": { "default": [], @@ -4185,6 +4037,14 @@ } ], "title": "Included" + }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" } }, "required": [ @@ -4212,6 +4072,14 @@ } ], "title": "Included" + }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" } }, "required": [ @@ -4239,6 +4107,14 @@ } ], "title": "Included" + }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" } }, "required": [ @@ -4266,6 +4142,14 @@ } ], "title": "Included" + }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" } }, "required": [ @@ -4293,6 +4177,14 @@ } ], "title": "Included" + }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" } }, "required": [ @@ -4320,6 +4212,14 @@ } ], "title": "Included" + }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" } }, "required": [ @@ -4347,6 +4247,14 @@ } ], "title": "Included" + }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" } }, "required": [ @@ -4374,6 +4282,14 @@ } ], "title": "Included" + }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" } }, "required": [ @@ -4382,6 +4298,41 @@ "title": "ItemResponse[EtlTaskResponse]", "type": "object" }, + "ItemResponse_LinkedResourceGetResponse_": { + "properties": { + "data": { + "$ref": "#/components/schemas/LinkedResourceGetResponse" + }, + "included": { + "anyOf": [ + { + "additionalProperties": { + "items": {}, + "type": "array" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Included" + }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" + } + }, + "required": [ + "data" + ], + "title": "ItemResponse[LinkedResourceGetResponse]", + "type": "object" + }, "ItemResponse_LinkedResourceTypeResponse_": { "properties": { "data": { @@ -4401,6 +4352,14 @@ } ], "title": "Included" + }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" } }, "required": [ @@ -4428,6 +4387,14 @@ } ], "title": "Included" + }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" } }, "required": [ @@ -4455,6 +4422,14 @@ } ], "title": "Included" + }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" } }, "required": [ @@ -4482,6 +4457,14 @@ } ], "title": "Included" + }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" } }, "required": [ @@ -4509,6 +4492,14 @@ } ], "title": "Included" + }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" } }, "required": [ @@ -4536,6 +4527,14 @@ } ], "title": "Included" + }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" } }, "required": [ @@ -4563,6 +4562,14 @@ } ], "title": "Included" + }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" } }, "required": [ @@ -4590,6 +4597,14 @@ } ], "title": "Included" + }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" } }, "required": [ @@ -4617,6 +4632,14 @@ } ], "title": "Included" + }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" } }, "required": [ @@ -4644,6 +4667,14 @@ } ], "title": "Included" + }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" } }, "required": [ @@ -4671,6 +4702,14 @@ } ], "title": "Included" + }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" } }, "required": [ @@ -4698,6 +4737,14 @@ } ], "title": "Included" + }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" } }, "required": [ @@ -4725,6 +4772,14 @@ } ], "title": "Included" + }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" } }, "required": [ @@ -4752,6 +4807,14 @@ } ], "title": "Included" + }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" } }, "required": [ @@ -4779,6 +4842,14 @@ } ], "title": "Included" + }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" } }, "required": [ @@ -4806,6 +4877,14 @@ } ], "title": "Included" + }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" } }, "required": [ @@ -4833,6 +4912,14 @@ } ], "title": "Included" + }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" } }, "required": [ @@ -4860,6 +4947,14 @@ } ], "title": "Included" + }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" } }, "required": [ @@ -4887,6 +4982,14 @@ } ], "title": "Included" + }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" } }, "required": [ @@ -4895,6 +4998,41 @@ "title": "ItemResponse[ServiceAccountResponse]", "type": "object" }, + "ItemResponse_TaskRunResponse_": { + "properties": { + "data": { + "$ref": "#/components/schemas/TaskRunResponse" + }, + "included": { + "anyOf": [ + { + "additionalProperties": { + "items": {}, + "type": "array" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Included" + }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" + } + }, + "required": [ + "data" + ], + "title": "ItemResponse[TaskRunResponse]", + "type": "object" + }, "ItemResponse_UnitResponse_": { "properties": { "data": { @@ -4914,6 +5052,14 @@ } ], "title": "Included" + }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" } }, "required": [ @@ -4941,6 +5087,14 @@ } ], "title": "Included" + }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" } }, "required": [ @@ -4968,6 +5122,14 @@ } ], "title": "Included" + }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" } }, "required": [ @@ -5072,6 +5234,67 @@ "title": "JSONPayloadResponse", "type": "object" }, + "LandingPageResponse": { + "description": "The API's landing page (OGC API - Features Core landingPage.yaml).", + "properties": { + "description": { + "title": "Description", + "type": "string" + }, + "links": { + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" + }, + "title": { + "title": "Title", + "type": "string" + } + }, + "required": [ + "title", + "description", + "links" + ], + "title": "LandingPageResponse", + "type": "object" + }, + "Link": { + "properties": { + "href": { + "title": "Href", + "type": "string" + }, + "rel": { + "title": "Rel", + "type": "string" + }, + "title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Title" + }, + "type": { + "title": "Type", + "type": "string" + } + }, + "required": [ + "href", + "rel", + "type" + ], + "title": "Link", + "type": "object" + }, "LinkedResourceGetResponse": { "properties": { "description": { @@ -5114,32 +5337,13 @@ }, "LinkedResourceQueryParameters": { "properties": { - "include": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Comma-separated list of related resources to include in the response.", - "title": "Include" - }, "limit": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" }, "offset": { "anyOf": [ @@ -5274,7 +5478,6 @@ "properties": { "include": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "workspace", @@ -5282,13 +5485,11 @@ ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" }, "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -5304,7 +5505,6 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" } @@ -5503,6 +5703,23 @@ }, "MethodQueryParameters": { "properties": { + "bbox": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], + "type": "array" + }, "datastream_id": { "default": [], "description": "Filter methods by datastream ID.", @@ -5521,9 +5738,12 @@ "title": "Datastream Id", "type": "array" }, + "datetime": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, "include": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "workspace", @@ -5531,24 +5751,16 @@ ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" }, "limit": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" }, "monitoring_site_id": { "default": [], @@ -5584,7 +5796,6 @@ }, "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -5600,7 +5811,6 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" }, @@ -5620,7 +5830,15 @@ "default": [], "description": "Filter methods by sensor model", "items": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "const": "null", + "type": "string" + } + ] }, "title": "Sensor Model", "type": "array" @@ -5629,42 +5847,50 @@ "default": [], "description": "Filter methods by sensor model manufacturer", "items": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "const": "null", + "type": "string" + } + ] }, "title": "Sensor Model Manufacturer", "type": "array" }, "sortby": { - "anyOf": [ - { - "items": { - "enum": [ - "name", - "code", - "type", - "sensorModel", - "sensorModelManufacturer", - "definition", - "sensorModelDefinition", - "-name", - "-code", - "-type", - "-sensorModel", - "-sensorModelManufacturer", - "-definition", - "-sensorModelDefinition" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" + "items": { + "enum": [ + "name", + "code", + "type", + "sensorModel", + "sensorModelManufacturer", + "definition", + "sensorModelDefinition", + "-name", + "-code", + "-type", + "-sensorModel", + "-sensorModelManufacturer", + "-definition", + "-sensorModelDefinition", + "+name", + "+code", + "+type", + "+sensorModel", + "+sensorModelManufacturer", + "+definition", + "+sensorModelDefinition" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" }, "type": { "default": [], @@ -5895,20 +6121,17 @@ "properties": { "include": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "datastream" ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" }, "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -5922,7 +6145,6 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" } @@ -6079,6 +6301,23 @@ }, "MonitoringRuleQueryParameters": { "properties": { + "bbox": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], + "type": "array" + }, "datastream_id": { "default": [], "description": "Filter rules by datastream ID.", @@ -6089,33 +6328,28 @@ "title": "Datastream Id", "type": "array" }, + "datetime": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, "include": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "datastream" ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" }, "limit": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" }, "offset": { "anyOf": [ @@ -6133,7 +6367,6 @@ }, "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -6147,7 +6380,6 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" }, @@ -6161,30 +6393,27 @@ "type": "array" }, "sortby": { - "anyOf": [ - { - "items": { - "enum": [ - "id", - "ruleType", - "datastreamId", - "lastCheckedAt", - "-id", - "-ruleType", - "-datastreamId", - "-lastCheckedAt" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" + "items": { + "enum": [ + "id", + "ruleType", + "datastreamId", + "lastCheckedAt", + "-id", + "-ruleType", + "-datastreamId", + "-lastCheckedAt", + "+id", + "+ruleType", + "+datastreamId", + "+lastCheckedAt" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" }, "task_id": { "default": [], @@ -6312,7 +6541,6 @@ "properties": { "include": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "workspace", @@ -6320,13 +6548,11 @@ ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" }, "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -6349,7 +6575,6 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" } @@ -6446,12 +6671,20 @@ "MonitoringSiteMarkerQueryParameters": { "properties": { "bbox": { - "default": [], - "description": "Filter markers by bounding box. Format bounding box as {min_lon},{min_lat},{max_lon},{max_lat}", + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", "items": { - "type": "string" + "type": "number" }, - "title": "Bbox", + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], "type": "array" }, "type": { @@ -6845,7 +7078,15 @@ "default": [], "description": "Filter monitoring sites by admin area 1.", "items": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "const": "null", + "type": "string" + } + ] }, "title": "Admin Area 1", "type": "array" @@ -6854,32 +7095,59 @@ "default": [], "description": "Filter monitoring sites by admin area 2.", "items": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "const": "null", + "type": "string" + } + ] }, "title": "Admin Area 2", "type": "array" }, "bbox": { - "default": [], - "description": "Filter monitoring sites by bounding box. Format bounding box as {min_lon},{min_lat},{max_lon},{max_lat}", + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", "items": { - "type": "string" + "type": "number" }, - "title": "Bbox", + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], "type": "array" }, "country": { "default": [], "description": "Filter monitoring sites by country.", "items": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "const": "null", + "type": "string" + } + ] }, "title": "Country", "type": "array" }, + "datetime": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, "include": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "workspace", @@ -6887,7 +7155,6 @@ ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" }, @@ -6904,19 +7171,12 @@ "title": "Is Private" }, "limit": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" }, "offset": { "anyOf": [ @@ -6934,7 +7194,6 @@ }, "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -6957,7 +7216,6 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" }, @@ -6974,44 +7232,48 @@ "title": "Q" }, "sortby": { - "anyOf": [ - { - "items": { - "enum": [ - "name", - "code", - "type", - "isPrivate", - "latitude", - "longitude", - "elevation_m", - "elevationDatum", - "adminArea1", - "adminArea2", - "country", - "-name", - "-code", - "-type", - "-isPrivate", - "-latitude", - "-longitude", - "-elevation_m", - "-elevationDatum", - "-adminArea1", - "-adminArea2", - "-country" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" + "items": { + "enum": [ + "name", + "code", + "type", + "isPrivate", + "latitude", + "longitude", + "elevation_m", + "elevationDatum", + "adminArea1", + "adminArea2", + "country", + "-name", + "-code", + "-type", + "-isPrivate", + "-latitude", + "-longitude", + "-elevation_m", + "-elevationDatum", + "-adminArea1", + "-adminArea2", + "-country", + "+name", + "+code", + "+type", + "+isPrivate", + "+latitude", + "+longitude", + "+elevation_m", + "+elevationDatum", + "+adminArea1", + "+adminArea2", + "+country" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" }, "tag": { "default": [], @@ -7358,20 +7620,17 @@ "properties": { "include": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "monitoringSite" ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" }, "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -7386,7 +7645,6 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" } @@ -7488,6 +7746,23 @@ }, "MonitoringTaskQueryParameters": { "properties": { + "bbox": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], + "type": "array" + }, "datastream_id": { "default": [], "description": "Filter monitoring tasks by datastream ID.", @@ -7498,16 +7773,18 @@ "title": "Datastream Id", "type": "array" }, + "datetime": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, "include": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "monitoringSite" ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" }, @@ -7529,19 +7806,12 @@ "type": "array" }, "limit": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" }, "monitoring_site_id": { "default": [], @@ -7569,7 +7839,6 @@ }, "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -7584,7 +7853,6 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" }, @@ -7610,40 +7878,42 @@ "type": "array" }, "sortby": { - "anyOf": [ - { - "items": { - "enum": [ - "id", - "name", - "monitoringSiteId", - "monitoringSiteName", - "workspaceId", - "workspaceName", - "latestRunStatus", - "latestRunStartedAt", - "latestRunFinishedAt", - "-id", - "-name", - "-monitoringSiteId", - "-monitoringSiteName", - "-workspaceId", - "-workspaceName", - "-latestRunStatus", - "-latestRunStartedAt", - "-latestRunFinishedAt" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" + "items": { + "enum": [ + "id", + "name", + "monitoringSiteId", + "monitoringSiteName", + "workspaceId", + "workspaceName", + "latestRunStatus", + "latestRunStartedAt", + "latestRunFinishedAt", + "-id", + "-name", + "-monitoringSiteId", + "-monitoringSiteName", + "-workspaceId", + "-workspaceName", + "-latestRunStatus", + "-latestRunStartedAt", + "-latestRunFinishedAt", + "+id", + "+name", + "+monitoringSiteId", + "+monitoringSiteName", + "+workspaceId", + "+workspaceName", + "+latestRunStatus", + "+latestRunStartedAt", + "+latestRunFinishedAt" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" }, "workspace_id": { "default": [], @@ -8013,6 +8283,14 @@ ], "title": "Included" }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" + }, "meta": { "$ref": "#/components/schemas/PaginationMeta" } @@ -8028,7 +8306,6 @@ "properties": { "include": { "description": "Comma-separated list of related resources to include in the response. Only applies to format=record.", - "explode": false, "items": { "enum": [ "datastream", @@ -8037,13 +8314,11 @@ ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" }, "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted. Only applies to format=record.", - "explode": false, "items": { "enum": [ "id", @@ -8055,7 +8330,6 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" } @@ -8117,6 +8391,23 @@ }, "ObservationQueryParameters": { "properties": { + "bbox": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], + "type": "array" + }, "datastream_id": { "default": [], "description": "Filter observations by datastream ID.", @@ -8127,6 +8418,10 @@ "title": "Datastream Id", "type": "array" }, + "datetime": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, "format": { "anyOf": [ { @@ -8146,7 +8441,6 @@ }, "include": { "description": "Comma-separated list of related resources to include in the response. Only applies to format=record.", - "explode": false, "items": { "enum": [ "datastream", @@ -8155,24 +8449,16 @@ ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" }, "limit": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" }, "offset": { "anyOf": [ @@ -8188,41 +8474,8 @@ "description": "Number of items to skip.", "title": "Offset" }, - "phenomenon_time_max": { - "anyOf": [ - { - "type": "string" - }, - { - "format": "date-time", - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Sets the maximum phenomenon time of filtered observations.", - "title": "Phenomenon Time Max" - }, - "phenomenon_time_min": { - "anyOf": [ - { - "type": "string" - }, - { - "format": "date-time", - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Sets the minimum phenomenon time of filtered observations.", - "title": "Phenomenon Time Min" - }, "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted. Only applies to format=record.", - "explode": false, "items": { "enum": [ "id", @@ -8234,7 +8487,6 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" }, @@ -8248,26 +8500,21 @@ "type": "array" }, "sortby": { - "anyOf": [ - { - "items": { - "enum": [ - "phenomenonTime", - "datastreamId", - "-phenomenonTime", - "-datastreamId" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" + "items": { + "enum": [ + "phenomenonTime", + "datastreamId", + "-phenomenonTime", + "-datastreamId", + "+phenomenonTime", + "+datastreamId" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" } }, "title": "ObservationQueryParameters", @@ -8368,6 +8615,14 @@ ], "title": "Included" }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" + }, "meta": { "$ref": "#/components/schemas/PaginationMeta" } @@ -8383,7 +8638,6 @@ "properties": { "include": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "workspace", @@ -8391,13 +8645,11 @@ ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" }, "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -8410,7 +8662,6 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" } @@ -8538,6 +8789,23 @@ }, "ObservedPropertyQueryParameters": { "properties": { + "bbox": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], + "type": "array" + }, "datastream_id": { "default": [], "description": "Filter observed properties by datastream ID.", @@ -8556,9 +8824,12 @@ "title": "Datastream Id", "type": "array" }, + "datetime": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, "include": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "workspace", @@ -8566,24 +8837,16 @@ ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" }, "limit": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" }, "monitoring_site_id": { "default": [], @@ -8619,7 +8882,6 @@ }, "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -8632,7 +8894,6 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" }, @@ -8649,28 +8910,24 @@ "title": "Q" }, "sortby": { - "anyOf": [ - { - "items": { - "enum": [ - "name", - "type", - "code", - "-name", - "-type", - "-code" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" + "items": { + "enum": [ + "name", + "type", + "code", + "-name", + "-type", + "-code", + "+name", + "+type", + "+code" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" }, "type": { "default": [], @@ -8884,6 +9141,14 @@ ], "title": "Included" }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" + }, "meta": { "$ref": "#/components/schemas/PaginationMeta" } @@ -8919,6 +9184,14 @@ ], "title": "Included" }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" + }, "meta": { "$ref": "#/components/schemas/PaginationMeta" } @@ -8954,6 +9227,14 @@ ], "title": "Included" }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" + }, "meta": { "$ref": "#/components/schemas/PaginationMeta" } @@ -8989,6 +9270,14 @@ ], "title": "Included" }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" + }, "meta": { "$ref": "#/components/schemas/PaginationMeta" } @@ -9024,6 +9313,14 @@ ], "title": "Included" }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" + }, "meta": { "$ref": "#/components/schemas/PaginationMeta" } @@ -9059,6 +9356,14 @@ ], "title": "Included" }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" + }, "meta": { "$ref": "#/components/schemas/PaginationMeta" } @@ -9094,6 +9399,14 @@ ], "title": "Included" }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" + }, "meta": { "$ref": "#/components/schemas/PaginationMeta" } @@ -9129,6 +9442,14 @@ ], "title": "Included" }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" + }, "meta": { "$ref": "#/components/schemas/PaginationMeta" } @@ -9164,6 +9485,14 @@ ], "title": "Included" }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" + }, "meta": { "$ref": "#/components/schemas/PaginationMeta" } @@ -9175,6 +9504,49 @@ "title": "PaginatedResponse[EtlTaskResponse]", "type": "object" }, + "PaginatedResponse_LinkedResourceGetResponse_": { + "properties": { + "data": { + "items": { + "$ref": "#/components/schemas/LinkedResourceGetResponse" + }, + "title": "Data", + "type": "array" + }, + "included": { + "anyOf": [ + { + "additionalProperties": { + "items": {}, + "type": "array" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Included" + }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" + }, + "meta": { + "$ref": "#/components/schemas/PaginationMeta" + } + }, + "required": [ + "data", + "meta" + ], + "title": "PaginatedResponse[LinkedResourceGetResponse]", + "type": "object" + }, "PaginatedResponse_LinkedResourceTypeResponse_": { "properties": { "data": { @@ -9199,6 +9571,14 @@ ], "title": "Included" }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" + }, "meta": { "$ref": "#/components/schemas/PaginationMeta" } @@ -9234,6 +9614,14 @@ ], "title": "Included" }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" + }, "meta": { "$ref": "#/components/schemas/PaginationMeta" } @@ -9269,6 +9657,14 @@ ], "title": "Included" }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" + }, "meta": { "$ref": "#/components/schemas/PaginationMeta" } @@ -9304,6 +9700,14 @@ ], "title": "Included" }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" + }, "meta": { "$ref": "#/components/schemas/PaginationMeta" } @@ -9339,6 +9743,14 @@ ], "title": "Included" }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" + }, "meta": { "$ref": "#/components/schemas/PaginationMeta" } @@ -9374,6 +9786,14 @@ ], "title": "Included" }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" + }, "meta": { "$ref": "#/components/schemas/PaginationMeta" } @@ -9409,6 +9829,14 @@ ], "title": "Included" }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" + }, "meta": { "$ref": "#/components/schemas/PaginationMeta" } @@ -9444,6 +9872,14 @@ ], "title": "Included" }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" + }, "meta": { "$ref": "#/components/schemas/PaginationMeta" } @@ -9479,6 +9915,14 @@ ], "title": "Included" }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" + }, "meta": { "$ref": "#/components/schemas/PaginationMeta" } @@ -9514,6 +9958,14 @@ ], "title": "Included" }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" + }, "meta": { "$ref": "#/components/schemas/PaginationMeta" } @@ -9549,6 +10001,14 @@ ], "title": "Included" }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" + }, "meta": { "$ref": "#/components/schemas/PaginationMeta" } @@ -9584,6 +10044,14 @@ ], "title": "Included" }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" + }, "meta": { "$ref": "#/components/schemas/PaginationMeta" } @@ -9619,6 +10087,14 @@ ], "title": "Included" }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" + }, "meta": { "$ref": "#/components/schemas/PaginationMeta" } @@ -9654,6 +10130,14 @@ ], "title": "Included" }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" + }, "meta": { "$ref": "#/components/schemas/PaginationMeta" } @@ -9689,6 +10173,14 @@ ], "title": "Included" }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" + }, "meta": { "$ref": "#/components/schemas/PaginationMeta" } @@ -9724,6 +10216,14 @@ ], "title": "Included" }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" + }, "meta": { "$ref": "#/components/schemas/PaginationMeta" } @@ -9759,6 +10259,14 @@ ], "title": "Included" }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" + }, "meta": { "$ref": "#/components/schemas/PaginationMeta" } @@ -9794,6 +10302,14 @@ ], "title": "Included" }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" + }, "meta": { "$ref": "#/components/schemas/PaginationMeta" } @@ -9829,6 +10345,14 @@ ], "title": "Included" }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" + }, "meta": { "$ref": "#/components/schemas/PaginationMeta" } @@ -9864,6 +10388,14 @@ ], "title": "Included" }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" + }, "meta": { "$ref": "#/components/schemas/PaginationMeta" } @@ -9899,6 +10431,14 @@ ], "title": "Included" }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" + }, "meta": { "$ref": "#/components/schemas/PaginationMeta" } @@ -9934,6 +10474,14 @@ ], "title": "Included" }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" + }, "meta": { "$ref": "#/components/schemas/PaginationMeta" } @@ -9969,6 +10517,14 @@ ], "title": "Included" }, + "links": { + "default": [], + "items": { + "$ref": "#/components/schemas/Link" + }, + "title": "Links", + "type": "array" + }, "meta": { "$ref": "#/components/schemas/PaginationMeta" } @@ -10272,20 +10828,17 @@ "properties": { "include": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "workspace" ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" }, "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -10297,7 +10850,6 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" } @@ -10413,6 +10965,23 @@ }, "ProcessingLevelQueryParameters": { "properties": { + "bbox": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], + "type": "array" + }, "datastream_id": { "default": [], "description": "Filter processing levels by datastream ID.", @@ -10431,33 +11000,28 @@ "title": "Datastream Id", "type": "array" }, + "datetime": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, "include": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "workspace" ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" }, "limit": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" }, "monitoring_site_id": { "default": [], @@ -10493,7 +11057,6 @@ }, "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -10505,7 +11068,6 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" }, @@ -10522,26 +11084,21 @@ "title": "Q" }, "sortby": { - "anyOf": [ - { - "items": { - "enum": [ - "code", - "name", - "-code", - "-name" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" + "items": { + "enum": [ + "code", + "name", + "-code", + "-name", + "+code", + "+name" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" }, "workspace_id": { "default": [], @@ -10631,7 +11188,6 @@ "properties": { "include": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "managedDatastream", @@ -10639,13 +11195,11 @@ ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" }, "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -10659,7 +11213,6 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" } @@ -10689,9 +11242,29 @@ }, "QualityControlHistoryQueryParameters": { "properties": { + "bbox": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], + "type": "array" + }, + "datetime": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, "include": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "managedDatastream", @@ -10699,24 +11272,16 @@ ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" }, "limit": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" }, "managed_datastream_id": { "default": [], @@ -10744,7 +11309,6 @@ }, "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -10758,42 +11322,46 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" }, "sortby": { - "anyOf": [ - { - "items": { - "enum": [ - "id", - "createdAt", - "phenomenonTimeStart", - "phenomenonTimeEnd", - "-id", - "-createdAt", - "-phenomenonTimeStart", - "-phenomenonTimeEnd" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" + "items": { + "enum": [ + "id", + "createdAt", + "phenomenonTimeStart", + "phenomenonTimeEnd", + "-id", + "-createdAt", + "-phenomenonTimeStart", + "-phenomenonTimeEnd", + "+id", + "+createdAt", + "+phenomenonTimeStart", + "+phenomenonTimeEnd" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" }, "source_datastream_id": { "default": [], "description": "Filter histories by source datastream ID.", "items": { - "format": "uuid", - "type": "string" + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "const": "null", + "type": "string" + } + ] }, "title": "Source Datastream Id", "type": "array" @@ -10884,7 +11452,6 @@ "properties": { "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -10897,7 +11464,6 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" } @@ -11020,19 +11586,12 @@ "title": "Include" }, "limit": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" }, "offset": { "anyOf": [ @@ -11050,7 +11609,6 @@ }, "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -11063,35 +11621,31 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" }, "sortby": { - "anyOf": [ - { - "items": { - "enum": [ - "id", - "order", - "operationType", - "createdAt", - "-id", - "-order", - "-operationType", - "-createdAt" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" + "items": { + "enum": [ + "id", + "order", + "operationType", + "createdAt", + "-id", + "-order", + "-operationType", + "-createdAt", + "+id", + "+order", + "+operationType", + "+createdAt" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" } }, "title": "QualityControlOperationQueryParameters", @@ -11180,7 +11734,6 @@ "properties": { "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -11198,7 +11751,6 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" } @@ -11302,19 +11854,12 @@ "type": "boolean" }, "limit": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" }, "offset": { "anyOf": [ @@ -11332,7 +11877,6 @@ }, "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -11350,7 +11894,6 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" }, @@ -11387,34 +11930,33 @@ "title": "Range Start" }, "sortby": { - "anyOf": [ - { - "items": { - "enum": [ - "id", - "createdAt", - "phenomenonTimeStart", - "phenomenonTimeEnd", - "status", - "committedAt", - "-id", - "-createdAt", - "-phenomenonTimeStart", - "-phenomenonTimeEnd", - "-status", - "-committedAt" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" + "items": { + "enum": [ + "id", + "createdAt", + "phenomenonTimeStart", + "phenomenonTimeEnd", + "status", + "committedAt", + "-id", + "-createdAt", + "-phenomenonTimeStart", + "-phenomenonTimeEnd", + "-status", + "-committedAt", + "+id", + "+createdAt", + "+phenomenonTimeStart", + "+phenomenonTimeEnd", + "+status", + "+committedAt" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" }, "status": { "anyOf": [ @@ -11538,20 +12080,17 @@ "properties": { "include": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "monitoringSite" ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" }, "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -11563,7 +12102,6 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" } @@ -11690,33 +12228,45 @@ }, "RatingCurveQueryParameters": { "properties": { + "bbox": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], + "type": "array" + }, + "datetime": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, "include": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "monitoringSite" ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" }, "limit": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" }, "monitoring_site_id": { "default": [], @@ -11744,7 +12294,6 @@ }, "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -11756,7 +12305,6 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" }, @@ -11773,36 +12321,36 @@ "title": "Q" }, "sortby": { - "anyOf": [ - { - "items": { - "enum": [ - "id", - "name", - "monitoringSiteId", - "monitoringSiteName", - "workspaceId", - "workspaceName", - "fittingMethod", - "-id", - "-name", - "-monitoringSiteId", - "-monitoringSiteName", - "-workspaceId", - "-workspaceName", - "-fittingMethod" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" + "items": { + "enum": [ + "id", + "name", + "monitoringSiteId", + "monitoringSiteName", + "workspaceId", + "workspaceName", + "fittingMethod", + "-id", + "-name", + "-monitoringSiteId", + "-monitoringSiteName", + "-workspaceId", + "-workspaceName", + "-fittingMethod", + "+id", + "+name", + "+monitoringSiteId", + "+monitoringSiteName", + "+workspaceId", + "+workspaceName", + "+fittingMethod" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" }, "workspace_id": { "default": [], @@ -11886,20 +12434,17 @@ "properties": { "include": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "workspace" ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" }, "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -11909,7 +12454,6 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" } @@ -11990,33 +12534,45 @@ }, "ResultQualifierQueryParameters": { "properties": { + "bbox": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], + "type": "array" + }, + "datetime": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, "include": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "workspace" ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" }, "limit": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" }, "offset": { "anyOf": [ @@ -12034,7 +12590,6 @@ }, "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -12044,7 +12599,6 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" }, @@ -12061,24 +12615,18 @@ "title": "Q" }, "sortby": { - "anyOf": [ - { - "items": { - "enum": [ - "name", - "-name" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" + "items": { + "enum": [ + "name", + "-name", + "+name" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" }, "workspace_id": { "default": [], @@ -12150,7 +12698,6 @@ "properties": { "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -12161,7 +12708,6 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" } @@ -12171,32 +12717,46 @@ }, "RoleQueryParameters": { "properties": { - "include": { - "anyOf": [ + "bbox": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ { - "type": "string" + "maxItems": 4, + "minItems": 4 }, { - "type": "null" + "maxItems": 6, + "minItems": 6 } ], - "description": "Comma-separated list of related resources to include in the response.", - "title": "Include" + "type": "array" }, - "limit": { + "datetime": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, + "include": { "anyOf": [ { - "maximum": 100000, - "minimum": 0, - "type": "integer" + "type": "string" }, { "type": "null" } ], + "description": "Comma-separated list of related resources to include in the response.", + "title": "Include" + }, + "limit": { "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" }, "offset": { "anyOf": [ @@ -12214,7 +12774,6 @@ }, "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -12225,29 +12784,22 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" }, "sortby": { - "anyOf": [ - { - "items": { - "enum": [ - "name", - "-name" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" + "items": { + "enum": [ + "name", + "-name", + "+name" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" }, "workspace_id": { "default": [], @@ -12635,20 +13187,17 @@ "properties": { "include": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "workspace" ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" }, "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -12663,7 +13212,6 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" } @@ -12799,31 +13347,22 @@ "properties": { "include": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "workspace" ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" }, "limit": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" }, "offset": { "anyOf": [ @@ -12841,7 +13380,6 @@ }, "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -12856,37 +13394,34 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" }, "sortby": { - "anyOf": [ - { - "items": { - "enum": [ - "name", - "isActive", - "keyExpiresAt", - "createdAt", - "lastUsedAt", - "-name", - "-isActive", - "-keyExpiresAt", - "-createdAt", - "-lastUsedAt" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" + "items": { + "enum": [ + "name", + "isActive", + "keyExpiresAt", + "createdAt", + "lastUsedAt", + "-name", + "-isActive", + "-keyExpiresAt", + "-createdAt", + "-lastUsedAt", + "+name", + "+isActive", + "+keyExpiresAt", + "+createdAt", + "+lastUsedAt" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" } }, "title": "ServiceAccountQueryParameters", @@ -13038,19 +13573,12 @@ "title": "Include" }, "limit": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" }, "offset": { "anyOf": [ @@ -13068,7 +13596,6 @@ }, "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -13080,7 +13607,6 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" }, @@ -13096,7 +13622,11 @@ "-id", "-status", "-startedAt", - "-finishedAt" + "-finishedAt", + "+id", + "+status", + "+startedAt", + "+finishedAt" ], "type": "string" }, @@ -13274,7 +13804,6 @@ "properties": { "include": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "workspace", @@ -13282,13 +13811,11 @@ ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" }, "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -13300,7 +13827,6 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" } @@ -13405,6 +13931,23 @@ }, "UnitQueryParameters": { "properties": { + "bbox": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], + "type": "array" + }, "datastream_id": { "default": [], "description": "Filter units by datastream ID.", @@ -13423,9 +13966,12 @@ "title": "Datastream Id", "type": "array" }, + "datetime": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, "include": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "workspace", @@ -13433,24 +13979,16 @@ ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" }, "limit": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" }, "monitoring_site_id": { "default": [], @@ -13486,7 +14024,6 @@ }, "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -13498,7 +14035,6 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" }, @@ -13515,28 +14051,24 @@ "title": "Q" }, "sortby": { - "anyOf": [ - { - "items": { - "enum": [ - "name", - "symbol", - "type", - "-name", - "-symbol", - "-type" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" + "items": { + "enum": [ + "name", + "symbol", + "type", + "-name", + "-symbol", + "-type", + "+name", + "+symbol", + "+type" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" }, "type": { "default": [], @@ -14036,7 +14568,6 @@ "properties": { "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -14045,7 +14576,6 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" } @@ -14055,32 +14585,46 @@ }, "VocabularyQueryParameters": { "properties": { - "include": { - "anyOf": [ + "bbox": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ { - "type": "string" + "maxItems": 4, + "minItems": 4 }, { - "type": "null" + "maxItems": 6, + "minItems": 6 } ], - "description": "Comma-separated list of related resources to include in the response.", - "title": "Include" + "type": "array" }, - "limit": { + "datetime": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, + "include": { "anyOf": [ { - "maximum": 100000, - "minimum": 0, - "type": "integer" + "type": "string" }, { "type": "null" } ], + "description": "Comma-separated list of related resources to include in the response.", + "title": "Include" + }, + "limit": { "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" }, "offset": { "anyOf": [ @@ -14098,7 +14642,6 @@ }, "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -14107,7 +14650,6 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" }, @@ -14124,24 +14666,18 @@ "title": "Q" }, "sortby": { - "anyOf": [ - { - "items": { - "enum": [ - "name", - "-name" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" + "items": { + "enum": [ + "name", + "-name", + "+name" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" } }, "title": "VocabularyQueryParameters", @@ -14151,7 +14687,6 @@ "properties": { "include": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "owner", @@ -14160,13 +14695,11 @@ ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" }, "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -14178,7 +14711,6 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" } @@ -14234,9 +14766,29 @@ }, "WorkspaceQueryParameters": { "properties": { + "bbox": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], + "type": "array" + }, + "datetime": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, "include": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "owner", @@ -14245,7 +14797,6 @@ ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" }, @@ -14274,19 +14825,12 @@ "title": "Is Private" }, "limit": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" }, "offset": { "anyOf": [ @@ -14304,7 +14848,6 @@ }, "properties": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -14316,7 +14859,6 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" }, @@ -14333,26 +14875,21 @@ "title": "Q" }, "sortby": { - "anyOf": [ - { - "items": { - "enum": [ - "name", - "isPrivate", - "-name", - "-isPrivate" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" + "items": { + "enum": [ + "name", + "isPrivate", + "-name", + "-isPrivate", + "+name", + "+isPrivate" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" } }, "title": "WorkspaceQueryParameters", @@ -14447,25 +14984,136 @@ } }, "info": { - "description": "", + "description": "Provides access to hydrologic monitoring sites, datastreams, and observations managed in HydroServer.", "title": "HydroServer Data Management API", "version": "2.0.0b3" }, "openapi": "3.1.0", "paths": { - "/api/data/aggregation-statistics": { + "/api/ogc/": { + "get": { + "description": "Get the API's landing page, which links to the API definition, conformance declaration, and\ncollections.", + "operationId": "interfaces_api_views_ogc_get_landing_page", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LandingPageResponse" + } + } + }, + "description": "OK" + } + }, + "security": [ + { + "SessionAuth": [] + }, + { + "OIDCAuth": [] + }, + { + "APIKeyAuth": [] + }, + { + "BasicAuth": [] + } + ], + "summary": "Get Landing Page", + "tags": [ + "Capabilities" + ] + } + }, + "/api/ogc/collections": { + "get": { + "description": "Get the collections the API serves.", + "operationId": "interfaces_api_views_ogc_get_collections", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CollectionsResponse" + } + } + }, + "description": "OK" + } + }, + "security": [ + { + "SessionAuth": [] + }, + { + "OIDCAuth": [] + }, + { + "APIKeyAuth": [] + }, + { + "BasicAuth": [] + } + ], + "summary": "Get Collections", + "tags": [ + "Collections" + ] + } + }, + "/api/ogc/collections/aggregation-statistics/items": { "get": { "description": "Get Aggregation Statistics.", "operationId": "interfaces_api_views_sta_aggregation_statistic_get_aggregation_statistics", "parameters": [ + { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "explode": false, + "in": "query", + "name": "bbox", + "required": false, + "schema": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], + "type": "array" + }, + "style": "form" + }, + { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "explode": false, + "in": "query", + "name": "datetime", + "required": false, + "schema": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, + "style": "form" + }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -14474,10 +15122,10 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", @@ -14518,51 +15166,40 @@ } }, { - "description": "The maximum number of items to return.", + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", "in": "query", "name": "limit", "required": false, "schema": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" } }, { "description": "Select one or more fields to sort the response by.", + "explode": false, "in": "query", "name": "sortby", "required": false, "schema": { - "anyOf": [ - { - "items": { - "enum": [ - "name", - "-name" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" - } + "items": { + "enum": [ + "name", + "-name", + "+name" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" + }, + "style": "form" }, { "description": "Full-text search query. Comma-separated terms are combined with OR; whitespace-separated words within a term are combined with AND.", @@ -14704,7 +15341,7 @@ ] } }, - "/api/data/aggregation-statistics/{aggregation_statistic_id}": { + "/api/ogc/collections/aggregation-statistics/items/{aggregation_statistic_id}": { "delete": { "description": "Delete an Aggregation Statistic.", "operationId": "interfaces_api_views_sta_aggregation_statistic_delete_aggregation_statistic", @@ -14782,12 +15419,12 @@ }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -14796,10 +15433,10 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" } ], "responses": { @@ -14938,19 +15575,56 @@ ] } }, - "/api/data/data-product-rating-curves": { + "/api/ogc/collections/data-product-rating-curves/items": { "get": { "description": "Get rating curves accessible to the authenticated user.", "operationId": "interfaces_api_views_products_rating_curve_get_rating_curves", "parameters": [ + { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "explode": false, + "in": "query", + "name": "bbox", + "required": false, + "schema": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], + "type": "array" + }, + "style": "form" + }, + { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "explode": false, + "in": "query", + "name": "datetime", + "required": false, + "schema": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, + "style": "form" + }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -14962,29 +15636,29 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", + "explode": false, "in": "query", "name": "include", "required": false, "schema": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "monitoringSite" ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" - } + }, + "style": "form" }, { "description": "Number of items to skip.", @@ -15007,63 +15681,58 @@ } }, { - "description": "The maximum number of items to return.", + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", "in": "query", "name": "limit", "required": false, "schema": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" } }, { "description": "Select one or more fields to sort the response by.", + "explode": false, "in": "query", "name": "sortby", "required": false, "schema": { - "anyOf": [ - { - "items": { - "enum": [ - "id", - "name", - "monitoringSiteId", - "monitoringSiteName", - "workspaceId", - "workspaceName", - "fittingMethod", - "-id", - "-name", - "-monitoringSiteId", - "-monitoringSiteName", - "-workspaceId", - "-workspaceName", - "-fittingMethod" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" - } + "items": { + "enum": [ + "id", + "name", + "monitoringSiteId", + "monitoringSiteName", + "workspaceId", + "workspaceName", + "fittingMethod", + "-id", + "-name", + "-monitoringSiteId", + "-monitoringSiteName", + "-workspaceId", + "-workspaceName", + "-fittingMethod", + "+id", + "+name", + "+monitoringSiteId", + "+monitoringSiteName", + "+workspaceId", + "+workspaceName", + "+fittingMethod" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" + }, + "style": "form" }, { "description": "Full-text search query. Comma-separated terms are combined with OR; whitespace-separated words within a term are combined with AND.", @@ -15248,7 +15917,7 @@ ] } }, - "/api/data/data-product-rating-curves/{rating_curve_id}": { + "/api/ogc/collections/data-product-rating-curves/items/{rating_curve_id}": { "delete": { "description": "Delete a rating curve.", "operationId": "interfaces_api_views_products_rating_curve_delete_rating_curve", @@ -15337,12 +16006,12 @@ }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -15354,29 +16023,29 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", + "explode": false, "in": "query", "name": "include", "required": false, "schema": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "monitoringSite" ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" - } + }, + "style": "form" } ], "responses": { @@ -15537,19 +16206,56 @@ ] } }, - "/api/data/data-product-tasks": { + "/api/ogc/collections/data-product-tasks/items": { "get": { "description": "Get data product tasks accessible to the authenticated user.", "operationId": "interfaces_api_views_products_task_get_data_product_tasks", "parameters": [ + { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "explode": false, + "in": "query", + "name": "bbox", + "required": false, + "schema": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], + "type": "array" + }, + "style": "form" + }, + { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "explode": false, + "in": "query", + "name": "datetime", + "required": false, + "schema": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, + "style": "form" + }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -15563,29 +16269,29 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", + "explode": false, "in": "query", "name": "include", "required": false, "schema": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "monitoringSite" ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" - } + }, + "style": "form" }, { "description": "Number of items to skip.", @@ -15608,67 +16314,64 @@ } }, { - "description": "The maximum number of items to return.", + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", "in": "query", "name": "limit", "required": false, "schema": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" } }, { "description": "Select one or more fields to sort the response by.", + "explode": false, "in": "query", "name": "sortby", "required": false, "schema": { - "anyOf": [ - { - "items": { - "enum": [ - "id", - "name", - "monitoringSiteId", - "monitoringSiteName", - "workspaceId", - "workspaceName", - "latestRunStatus", - "latestRunStartedAt", - "latestRunFinishedAt", - "-id", - "-name", - "-monitoringSiteId", - "-monitoringSiteName", - "-workspaceId", - "-workspaceName", - "-latestRunStatus", - "-latestRunStartedAt", - "-latestRunFinishedAt" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" - } + "items": { + "enum": [ + "id", + "name", + "monitoringSiteId", + "monitoringSiteName", + "workspaceId", + "workspaceName", + "latestRunStatus", + "latestRunStartedAt", + "latestRunFinishedAt", + "-id", + "-name", + "-monitoringSiteId", + "-monitoringSiteName", + "-workspaceId", + "-workspaceName", + "-latestRunStatus", + "-latestRunStartedAt", + "-latestRunFinishedAt", + "+id", + "+name", + "+monitoringSiteId", + "+monitoringSiteName", + "+workspaceId", + "+workspaceName", + "+latestRunStatus", + "+latestRunStartedAt", + "+latestRunFinishedAt" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" + }, + "style": "form" }, { "description": "Full-text search query. Comma-separated terms are combined with OR; whitespace-separated words within a term are combined with AND.", @@ -15799,8 +16502,16 @@ "default": [], "description": "Filter data product tasks by rating curve ID.", "items": { - "format": "uuid", - "type": "string" + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "const": "null", + "type": "string" + } + ] }, "title": "Rating Curve Id", "type": "array" @@ -15939,7 +16650,7 @@ ] } }, - "/api/data/data-product-tasks/{task_id}": { + "/api/ogc/collections/data-product-tasks/items/{task_id}": { "delete": { "description": "Delete a data product task.", "operationId": "interfaces_api_views_products_task_delete_data_product_task", @@ -16028,12 +16739,12 @@ }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -16047,29 +16758,29 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", + "explode": false, "in": "query", "name": "include", "required": false, "schema": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "monitoringSite" ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" - } + }, + "style": "form" } ], "responses": { @@ -16230,7 +16941,7 @@ ] } }, - "/api/data/data-product-tasks/{task_id}/runs": { + "/api/ogc/collections/data-product-tasks/items/{task_id}/runs": { "get": { "description": "Get runs for a data product task.", "operationId": "interfaces_api_views_products_task_get_data_product_task_runs", @@ -16247,12 +16958,12 @@ }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -16264,10 +16975,10 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", @@ -16308,28 +17019,22 @@ } }, { - "description": "The maximum number of items to return.", + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", "in": "query", "name": "limit", "required": false, "schema": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" } }, { "description": "Select one or more fields to sort the response by.", + "explode": false, "in": "query", "name": "sortby", "required": false, @@ -16345,13 +17050,18 @@ "-id", "-status", "-startedAt", - "-finishedAt" + "-finishedAt", + "+id", + "+status", + "+startedAt", + "+finishedAt" ], "type": "string" }, "title": "Sortby", "type": "array" - } + }, + "style": "form" }, { "description": "Filters task runs by their status.", @@ -16528,7 +17238,7 @@ ] } }, - "/api/data/data-product-tasks/{task_id}/runs/{run_id}": { + "/api/ogc/collections/data-product-tasks/items/{task_id}/runs/{run_id}": { "get": { "description": "Get a single run for a data product task.", "operationId": "interfaces_api_views_products_task_get_data_product_task_run", @@ -16559,7 +17269,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TaskRunResponse" + "$ref": "#/components/schemas/ItemResponse_TaskRunResponse_" } } }, @@ -16619,7 +17329,7 @@ ] } }, - "/api/data/data-product-tasks/{task_id}/trigger": { + "/api/ogc/collections/data-product-tasks/items/{task_id}/trigger": { "post": { "description": "Trigger an immediate run of a data product task on a Celery worker.", "operationId": "interfaces_api_views_products_task_trigger_data_product_task", @@ -16640,7 +17350,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TaskRunResponse" + "$ref": "#/components/schemas/ItemResponse_TaskRunResponse_" } } }, @@ -16700,19 +17410,56 @@ ] } }, - "/api/data/data-product-transformations": { + "/api/ogc/collections/data-product-transformations/items": { "get": { "description": "Get data product transformations.", "operationId": "interfaces_api_views_products_transformation_get_data_product_transformations", "parameters": [ + { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "explode": false, + "in": "query", + "name": "bbox", + "required": false, + "schema": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], + "type": "array" + }, + "style": "form" + }, + { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "explode": false, + "in": "query", + "name": "datetime", + "required": false, + "schema": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, + "style": "form" + }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -16732,19 +17479,19 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", + "explode": false, "in": "query", "name": "include", "required": false, "schema": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "outputDatastream", @@ -16752,10 +17499,10 @@ ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" - } + }, + "style": "form" }, { "description": "Number of items to skip.", @@ -16778,55 +17525,46 @@ } }, { - "description": "The maximum number of items to return.", + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", "in": "query", "name": "limit", "required": false, "schema": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" } }, { "description": "Select one or more fields to sort the response by.", + "explode": false, "in": "query", "name": "sortby", "required": false, "schema": { - "anyOf": [ - { - "items": { - "enum": [ - "id", - "outputDatastreamId", - "transformationType", - "-id", - "-outputDatastreamId", - "-transformationType" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" - } + "items": { + "enum": [ + "id", + "outputDatastreamId", + "transformationType", + "-id", + "-outputDatastreamId", + "-transformationType", + "+id", + "+outputDatastreamId", + "+transformationType" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" + }, + "style": "form" }, { "description": "Filter transformations by type.", @@ -17062,7 +17800,7 @@ ] } }, - "/api/data/data-product-transformations/{transformation_id}": { + "/api/ogc/collections/data-product-transformations/items/{transformation_id}": { "delete": { "description": "Delete a data product transformation.", "operationId": "interfaces_api_views_products_transformation_delete_data_product_transformation", @@ -17151,12 +17889,12 @@ }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -17176,19 +17914,19 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", + "explode": false, "in": "query", "name": "include", "required": false, "schema": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "outputDatastream", @@ -17196,10 +17934,10 @@ ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" - } + }, + "style": "form" } ], "responses": { @@ -17360,19 +18098,56 @@ ] } }, - "/api/data/datastream-statuses": { + "/api/ogc/collections/datastream-statuses/items": { "get": { "description": "Get Datastream Statuses.", "operationId": "interfaces_api_views_sta_datastream_status_get_datastream_statuses", "parameters": [ + { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "explode": false, + "in": "query", + "name": "bbox", + "required": false, + "schema": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], + "type": "array" + }, + "style": "form" + }, + { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "explode": false, + "in": "query", + "name": "datetime", + "required": false, + "schema": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, + "style": "form" + }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -17381,10 +18156,10 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", @@ -17425,51 +18200,40 @@ } }, { - "description": "The maximum number of items to return.", + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", "in": "query", "name": "limit", "required": false, "schema": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" } }, { "description": "Select one or more fields to sort the response by.", + "explode": false, "in": "query", "name": "sortby", "required": false, "schema": { - "anyOf": [ - { - "items": { - "enum": [ - "name", - "-name" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" - } + "items": { + "enum": [ + "name", + "-name", + "+name" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" + }, + "style": "form" }, { "description": "Full-text search query. Comma-separated terms are combined with OR; whitespace-separated words within a term are combined with AND.", @@ -17611,7 +18375,7 @@ ] } }, - "/api/data/datastream-statuses/{datastream_status_id}": { + "/api/ogc/collections/datastream-statuses/items/{datastream_status_id}": { "delete": { "description": "Delete a Datastream Status.", "operationId": "interfaces_api_views_sta_datastream_status_delete_datastream_status", @@ -17689,12 +18453,12 @@ }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -17703,10 +18467,10 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" } ], "responses": { @@ -17845,19 +18609,56 @@ ] } }, - "/api/data/datastreams": { + "/api/ogc/collections/datastreams/items": { "get": { "description": "Get public Datastreams and Datastreams associated with the authenticated user.", "operationId": "interfaces_api_views_sta_datastream_get_datastreams", "parameters": [ + { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "explode": false, + "in": "query", + "name": "bbox", + "required": false, + "schema": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], + "type": "array" + }, + "style": "form" + }, + { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "explode": false, + "in": "query", + "name": "datetime", + "required": false, + "schema": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, + "style": "form" + }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -17871,16 +18672,16 @@ "timeAggregationInterval", "status", "resultType", - "valueCount", - "phenomenonBeginTime", - "phenomenonEndTime", - "resultBeginTime", - "resultEndTime", "isPrivate", "isVisible", "timeAggregationIntervalUnit", "intendedTimeSpacing", "intendedTimeSpacingUnit", + "valueCount", + "phenomenonBeginTime", + "phenomenonEndTime", + "resultBeginTime", + "resultEndTime", "monitoringSiteId", "methodId", "observedPropertyId", @@ -17891,19 +18692,19 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", + "explode": false, "in": "query", "name": "include", "required": false, "schema": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "workspace", @@ -17918,10 +18719,10 @@ ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" - } + }, + "style": "form" }, { "description": "Number of items to skip.", @@ -17944,75 +18745,76 @@ } }, { - "description": "The maximum number of items to return.", + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", "in": "query", "name": "limit", "required": false, "schema": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" } }, { "description": "Select one or more fields to sort the response by.", + "explode": false, "in": "query", "name": "sortby", "required": false, "schema": { - "anyOf": [ - { - "items": { - "enum": [ - "name", - "observationType", - "sampledMedium", - "status", - "resultType", - "isPrivate", - "isVisible", - "aggregationStatistic", - "valueCount", - "phenomenonBeginTime", - "phenomenonEndTime", - "resultBeginTime", - "resultEndTime", - "-name", - "-observationType", - "-sampledMedium", - "-status", - "-resultType", - "-isPrivate", - "-isVisible", - "-aggregationStatistic", - "-valueCount", - "-phenomenonBeginTime", - "-phenomenonEndTime", - "-resultBeginTime", - "-resultEndTime" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" - } + "items": { + "enum": [ + "name", + "observationType", + "sampledMedium", + "status", + "resultType", + "isPrivate", + "isVisible", + "aggregationStatistic", + "valueCount", + "phenomenonBeginTime", + "phenomenonEndTime", + "resultBeginTime", + "resultEndTime", + "-name", + "-observationType", + "-sampledMedium", + "-status", + "-resultType", + "-isPrivate", + "-isVisible", + "-aggregationStatistic", + "-valueCount", + "-phenomenonBeginTime", + "-phenomenonEndTime", + "-resultBeginTime", + "-resultEndTime", + "+name", + "+observationType", + "+sampledMedium", + "+status", + "+resultType", + "+isPrivate", + "+isVisible", + "+aggregationStatistic", + "+valueCount", + "+phenomenonBeginTime", + "+phenomenonEndTime", + "+resultBeginTime", + "+resultEndTime" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" + }, + "style": "form" }, { "description": "Full-text search query. Comma-separated terms are combined with OR; whitespace-separated words within a term are combined with AND.", @@ -18175,15 +18977,23 @@ } }, { - "description": "Filter monitoring_sites by status.", + "description": "Filter datastreams by status.", "in": "query", "name": "status", "required": false, "schema": { "default": [], - "description": "Filter monitoring_sites by status.", + "description": "Filter datastreams by status.", "items": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "const": "null", + "type": "string" + } + ] }, "title": "Status", "type": "array" @@ -18272,182 +19082,6 @@ "description": "Sets the minimum value count of filtered datastreams.", "title": "Value Count Min" } - }, - { - "description": "Sets the maximum phenomenon begin time of filtered datastreams.", - "in": "query", - "name": "phenomenon_begin_time_max", - "required": false, - "schema": { - "anyOf": [ - { - "type": "string" - }, - { - "format": "date-time", - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Sets the maximum phenomenon begin time of filtered datastreams.", - "title": "Phenomenon Begin Time Max" - } - }, - { - "description": "Sets the minimum phenomenon begin time of filtered datastreams.", - "in": "query", - "name": "phenomenon_begin_time_min", - "required": false, - "schema": { - "anyOf": [ - { - "type": "string" - }, - { - "format": "date-time", - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Sets the minimum phenomenon begin time of filtered datastreams.", - "title": "Phenomenon Begin Time Min" - } - }, - { - "description": "Sets the maximum phenomenon end time of filtered datastreams.", - "in": "query", - "name": "phenomenon_end_time_max", - "required": false, - "schema": { - "anyOf": [ - { - "type": "string" - }, - { - "format": "date-time", - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Sets the maximum phenomenon end time of filtered datastreams.", - "title": "Phenomenon End Time Max" - } - }, - { - "description": "Sets the minimum phenomenon end time of filtered datastreams.", - "in": "query", - "name": "phenomenon_end_time_min", - "required": false, - "schema": { - "anyOf": [ - { - "type": "string" - }, - { - "format": "date-time", - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Sets the minimum phenomenon end time of filtered datastreams.", - "title": "Phenomenon End Time Min" - } - }, - { - "description": "Sets the maximum result begin time of filtered datastreams.", - "in": "query", - "name": "result_begin_time_max", - "required": false, - "schema": { - "anyOf": [ - { - "type": "string" - }, - { - "format": "date-time", - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Sets the maximum result begin time of filtered datastreams.", - "title": "Result Begin Time Max" - } - }, - { - "description": "Sets the minimum result begin time of filtered datastreams.", - "in": "query", - "name": "result_begin_time_min", - "required": false, - "schema": { - "anyOf": [ - { - "type": "string" - }, - { - "format": "date-time", - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Sets the minimum result begin time of filtered datastreams.", - "title": "Result Begin Time Min" - } - }, - { - "description": "Sets the maximum result end time of filtered datastreams.", - "in": "query", - "name": "result_end_time_max", - "required": false, - "schema": { - "anyOf": [ - { - "type": "string" - }, - { - "format": "date-time", - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Sets the maximum result end time of filtered datastreams.", - "title": "Result End Time Max" - } - }, - { - "description": "Sets the minimum result end time of filtered datastreams.", - "in": "query", - "name": "result_end_time_min", - "required": false, - "schema": { - "anyOf": [ - { - "type": "string" - }, - { - "format": "date-time", - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Sets the minimum result end time of filtered datastreams.", - "title": "Result End Time Min" - } } ], "responses": { @@ -18571,162 +19205,7 @@ ] } }, - "/api/data/datastreams/tags/keys": { - "get": { - "description": "Get all existing unique datastream tag keys.", - "operationId": "interfaces_api_views_sta_datastream_get_datastream_tag_keys", - "parameters": [ - { - "in": "query", - "name": "workspace_id", - "required": false, - "schema": { - "anyOf": [ - { - "format": "uuid", - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Workspace Id" - } - }, - { - "in": "query", - "name": "datastream_id", - "required": false, - "schema": { - "anyOf": [ - { - "format": "uuid", - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Datastream Id" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "additionalProperties": { - "items": { - "type": "string" - }, - "type": "array" - }, - "title": "Response", - "type": "object" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/json": { - "schema": { - "title": "Response", - "type": "string" - } - } - }, - "description": "Unauthorized" - } - }, - "security": [ - { - "SessionAuth": [] - }, - { - "OIDCAuth": [] - }, - { - "APIKeyAuth": [] - }, - { - "BasicAuth": [] - } - ], - "summary": "Get Datastream Tag Keys", - "tags": [ - "Datastreams" - ] - } - }, - "/api/data/datastreams/visualization-bootstrap": { - "get": { - "description": "Get the lean metadata required to bootstrap the visualization page.", - "operationId": "interfaces_api_views_sta_datastream_get_datastream_visualization_bootstrap", - "parameters": [ - { - "description": "Filter visualization bootstrap datastreams by workspace ID.", - "in": "query", - "name": "workspace_id", - "required": false, - "schema": { - "default": [], - "description": "Filter visualization bootstrap datastreams by workspace ID.", - "items": { - "format": "uuid", - "type": "string" - }, - "title": "Workspace Id", - "type": "array" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DatastreamVisualizationBootstrapResponse" - } - } - }, - "description": "OK" - }, - "401": { - "content": { - "application/json": { - "schema": { - "title": "Response", - "type": "string" - } - } - }, - "description": "Unauthorized" - } - }, - "security": [ - { - "SessionAuth": [] - }, - { - "OIDCAuth": [] - }, - { - "APIKeyAuth": [] - }, - { - "BasicAuth": [] - } - ], - "summary": "Get Datastream Visualization Bootstrap", - "tags": [ - "Datastreams" - ] - } - }, - "/api/data/datastreams/{datastream_id}": { + "/api/ogc/collections/datastreams/items/{datastream_id}": { "delete": { "description": "Delete a Datastream.", "operationId": "interfaces_api_views_sta_datastream_delete_datastream", @@ -18804,12 +19283,12 @@ }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -18823,16 +19302,16 @@ "timeAggregationInterval", "status", "resultType", - "valueCount", - "phenomenonBeginTime", - "phenomenonEndTime", - "resultBeginTime", - "resultEndTime", "isPrivate", "isVisible", "timeAggregationIntervalUnit", "intendedTimeSpacing", "intendedTimeSpacingUnit", + "valueCount", + "phenomenonBeginTime", + "phenomenonEndTime", + "resultBeginTime", + "resultEndTime", "monitoringSiteId", "methodId", "observedPropertyId", @@ -18843,19 +19322,19 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", + "explode": false, "in": "query", "name": "include", "required": false, "schema": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "workspace", @@ -18870,10 +19349,10 @@ ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" - } + }, + "style": "form" } ], "responses": { @@ -19012,7 +19491,7 @@ ] } }, - "/api/data/datastreams/{datastream_id}/csv": { + "/api/ogc/collections/datastreams/items/{datastream_id}/csv": { "get": { "description": "Get a CSV representation of the Datastream.", "operationId": "interfaces_api_views_sta_datastream_get_datastream_csv", @@ -19075,7 +19554,7 @@ ] } }, - "/api/data/datastreams/{datastream_id}/linked-resources": { + "/api/ogc/collections/datastreams/items/{datastream_id}/linked-resources": { "get": { "description": "Get all linked resources associated with a Datastream.", "operationId": "interfaces_api_views_sta_datastream_get_datastream_linked_resources", @@ -19108,24 +19587,6 @@ "title": "Properties" } }, - { - "description": "Comma-separated list of related resources to include in the response.", - "in": "query", - "name": "include", - "required": false, - "schema": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Comma-separated list of related resources to include in the response.", - "title": "Include" - } - }, { "description": "Number of items to skip.", "in": "query", @@ -19147,24 +19608,17 @@ } }, { - "description": "The maximum number of items to return.", + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", "in": "query", "name": "limit", "required": false, "schema": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" } }, { @@ -19188,11 +19642,7 @@ "content": { "application/json": { "schema": { - "items": { - "$ref": "#/components/schemas/LinkedResourceGetResponse" - }, - "title": "Response", - "type": "array" + "$ref": "#/components/schemas/PaginatedResponse_LinkedResourceGetResponse_" } } }, @@ -19392,7 +19842,7 @@ ] } }, - "/api/data/datastreams/{datastream_id}/linked-resources/{linked_resource_id}": { + "/api/ogc/collections/datastreams/items/{datastream_id}/linked-resources/{linked_resource_id}": { "delete": { "description": "Remove a linked resource from a datastream.", "operationId": "interfaces_api_views_sta_datastream_remove_datastream_linked_resource", @@ -19475,9 +19925,9 @@ "Datastreams" ] }, - "patch": { - "description": "Update a linked resource for a datastream. A linked resource's mode (hosted file vs.\nexternal URL) cannot be changed in place \u2014 delete it and create a new one instead.", - "operationId": "interfaces_api_views_sta_datastream_update_datastream_linked_resource", + "get": { + "description": "Get a linked resource associated with a Datastream.", + "operationId": "interfaces_api_views_sta_datastream_get_datastream_linked_resource", "parameters": [ { "in": "path", @@ -19500,89 +19950,178 @@ } } ], - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "properties": { - "description": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Description" - }, - "file": { - "anyOf": [ - { - "format": "binary", - "type": "string" - }, - { - "type": "null" - } - ], - "title": "File" - }, - "link": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Link" - }, - "name": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Name" - }, - "type": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Type" - } - }, - "title": "MultiPartBodyParams", - "type": "object" - } - } - }, - "required": true - }, "responses": { - "204": { - "description": "No Content" - }, - "400": { + "200": { "content": { "application/json": { "schema": { - "title": "Response", - "type": "string" + "$ref": "#/components/schemas/ItemResponse_LinkedResourceGetResponse_" } } }, - "description": "Bad Request" + "description": "OK" + }, + "401": { + "content": { + "application/json": { + "schema": { + "title": "Response", + "type": "string" + } + } + }, + "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "title": "Response", + "type": "string" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "title": "Response", + "type": "string" + } + } + }, + "description": "Not Found" + } + }, + "security": [ + { + "SessionAuth": [] + }, + { + "OIDCAuth": [] + }, + { + "APIKeyAuth": [] + }, + { + "BasicAuth": [] + } + ], + "summary": "Get Datastream Linked Resource", + "tags": [ + "Datastreams" + ] + }, + "patch": { + "description": "Update a linked resource for a datastream. A linked resource's mode (hosted file vs.\nexternal URL) cannot be changed in place \u2014 delete it and create a new one instead.", + "operationId": "interfaces_api_views_sta_datastream_update_datastream_linked_resource", + "parameters": [ + { + "in": "path", + "name": "datastream_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Datastream Id", + "type": "string" + } + }, + { + "in": "path", + "name": "linked_resource_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Linked Resource Id", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "properties": { + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "file": { + "anyOf": [ + { + "format": "binary", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "File" + }, + "link": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Link" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Type" + } + }, + "title": "MultiPartBodyParams", + "type": "object" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "content": { + "application/json": { + "schema": { + "title": "Response", + "type": "string" + } + } + }, + "description": "Bad Request" }, "401": { "content": { @@ -19649,19 +20188,211 @@ ] } }, - "/api/data/etl-data-connections": { + "/api/ogc/collections/datastreams/tags/keys": { + "get": { + "description": "Get all existing unique datastream tag keys.", + "operationId": "interfaces_api_views_sta_datastream_get_datastream_tag_keys", + "parameters": [ + { + "in": "query", + "name": "workspace_id", + "required": false, + "schema": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Workspace Id" + } + }, + { + "in": "query", + "name": "datastream_id", + "required": false, + "schema": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Datastream Id" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "additionalProperties": { + "items": { + "type": "string" + }, + "type": "array" + }, + "title": "Response", + "type": "object" + } + } + }, + "description": "OK" + }, + "401": { + "content": { + "application/json": { + "schema": { + "title": "Response", + "type": "string" + } + } + }, + "description": "Unauthorized" + } + }, + "security": [ + { + "SessionAuth": [] + }, + { + "OIDCAuth": [] + }, + { + "APIKeyAuth": [] + }, + { + "BasicAuth": [] + } + ], + "summary": "Get Datastream Tag Keys", + "tags": [ + "Datastreams" + ] + } + }, + "/api/ogc/collections/datastreams/visualization-bootstrap": { + "get": { + "description": "Get the lean metadata required to bootstrap the visualization page.", + "operationId": "interfaces_api_views_sta_datastream_get_datastream_visualization_bootstrap", + "parameters": [ + { + "description": "Filter visualization bootstrap datastreams by workspace ID.", + "in": "query", + "name": "workspace_id", + "required": false, + "schema": { + "default": [], + "description": "Filter visualization bootstrap datastreams by workspace ID.", + "items": { + "format": "uuid", + "type": "string" + }, + "title": "Workspace Id", + "type": "array" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DatastreamVisualizationBootstrapResponse" + } + } + }, + "description": "OK" + }, + "401": { + "content": { + "application/json": { + "schema": { + "title": "Response", + "type": "string" + } + } + }, + "description": "Unauthorized" + } + }, + "security": [ + { + "SessionAuth": [] + }, + { + "OIDCAuth": [] + }, + { + "APIKeyAuth": [] + }, + { + "BasicAuth": [] + } + ], + "summary": "Get Datastream Visualization Bootstrap", + "tags": [ + "Datastreams" + ] + } + }, + "/api/ogc/collections/etl-data-connections/items": { "get": { "description": "Get ETL Data Connections associated with the authenticated user.", "operationId": "interfaces_api_views_etl_data_connection_get_data_connections", "parameters": [ + { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "explode": false, + "in": "query", + "name": "bbox", + "required": false, + "schema": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], + "type": "array" + }, + "style": "form" + }, + { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "explode": false, + "in": "query", + "name": "datetime", + "required": false, + "schema": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, + "style": "form" + }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -19681,29 +20412,29 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", + "explode": false, "in": "query", "name": "include", "required": false, "schema": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "workspace" ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" - } + }, + "style": "form" }, { "description": "Number of items to skip.", @@ -19726,65 +20457,61 @@ } }, { - "description": "The maximum number of items to return.", + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", "in": "query", "name": "limit", "required": false, "schema": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" } }, { "description": "Select one or more fields to sort the response by.", + "explode": false, "in": "query", "name": "sortby", "required": false, "schema": { - "anyOf": [ - { - "items": { - "enum": [ - "id", - "name", - "timestampKey", - "timestampFormat", - "timezoneType", - "timezone", - "workspaceId", - "workspaceName", - "-id", - "-name", - "-timestampKey", - "-timestampFormat", - "-timezoneType", - "-timezone", - "-workspaceId", - "-workspaceName" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" - } + "items": { + "enum": [ + "id", + "name", + "timestampKey", + "timestampFormat", + "timezoneType", + "timezone", + "workspaceId", + "workspaceName", + "-id", + "-name", + "-timestampKey", + "-timestampFormat", + "-timezoneType", + "-timezone", + "-workspaceId", + "-workspaceName", + "+id", + "+name", + "+timestampKey", + "+timestampFormat", + "+timezoneType", + "+timezone", + "+workspaceId", + "+workspaceName" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" + }, + "style": "form" }, { "description": "Full-text search query. Comma-separated terms are combined with OR; whitespace-separated words within a term are combined with AND.", @@ -19968,7 +20695,7 @@ ] } }, - "/api/data/etl-data-connections/{data_connection_id}": { + "/api/ogc/collections/etl-data-connections/items/{data_connection_id}": { "delete": { "description": "Delete an ETL Data Connection.", "operationId": "interfaces_api_views_etl_data_connection_delete_data_connection", @@ -20057,12 +20784,12 @@ }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -20082,29 +20809,29 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", + "explode": false, "in": "query", "name": "include", "required": false, "schema": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "workspace" ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" - } + }, + "style": "form" } ], "responses": { @@ -20254,19 +20981,56 @@ ] } }, - "/api/data/etl-mappings": { + "/api/ogc/collections/etl-mappings/items": { "get": { "description": "Get ETL mappings.", "operationId": "interfaces_api_views_etl_mapping_get_etl_mappings", "parameters": [ + { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "explode": false, + "in": "query", + "name": "bbox", + "required": false, + "schema": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], + "type": "array" + }, + "style": "form" + }, + { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "explode": false, + "in": "query", + "name": "datetime", + "required": false, + "schema": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, + "style": "form" + }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -20275,29 +21039,29 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", + "explode": false, "in": "query", "name": "include", "required": false, "schema": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "targetDatastream" ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" - } + }, + "style": "form" }, { "description": "Number of items to skip.", @@ -20320,55 +21084,46 @@ } }, { - "description": "The maximum number of items to return.", + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", "in": "query", "name": "limit", "required": false, "schema": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" } }, { "description": "Select one or more fields to sort the response by.", + "explode": false, "in": "query", "name": "sortby", "required": false, "schema": { - "anyOf": [ - { - "items": { - "enum": [ - "id", - "sourceIdentifier", - "targetDatastreamId", - "-id", - "-sourceIdentifier", - "-targetDatastreamId" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" - } + "items": { + "enum": [ + "id", + "sourceIdentifier", + "targetDatastreamId", + "-id", + "-sourceIdentifier", + "-targetDatastreamId", + "+id", + "+sourceIdentifier", + "+targetDatastreamId" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" + }, + "style": "form" }, { "description": "Filter mappings by source identifier.", @@ -20588,7 +21343,7 @@ ] } }, - "/api/data/etl-mappings/{mapping_id}": { + "/api/ogc/collections/etl-mappings/items/{mapping_id}": { "delete": { "description": "Delete an ETL mapping.", "operationId": "interfaces_api_views_etl_mapping_delete_etl_mapping", @@ -20677,12 +21432,12 @@ }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -20691,29 +21446,29 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", + "explode": false, "in": "query", "name": "include", "required": false, "schema": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "targetDatastream" ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" - } + }, + "style": "form" } ], "responses": { @@ -20874,19 +21629,56 @@ ] } }, - "/api/data/etl-tasks": { + "/api/ogc/collections/etl-tasks/items": { "get": { "description": "Get ETL Tasks accessible to the authenticated user.", "operationId": "interfaces_api_views_etl_task_get_etl_tasks", "parameters": [ + { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "explode": false, + "in": "query", + "name": "bbox", + "required": false, + "schema": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], + "type": "array" + }, + "style": "form" + }, + { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "explode": false, + "in": "query", + "name": "datetime", + "required": false, + "schema": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, + "style": "form" + }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -20901,29 +21693,29 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", + "explode": false, "in": "query", "name": "include", "required": false, "schema": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "dataConnection" ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" - } + }, + "style": "form" }, { "description": "Number of items to skip.", @@ -20946,67 +21738,64 @@ } }, { - "description": "The maximum number of items to return.", + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", "in": "query", "name": "limit", "required": false, "schema": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" } }, { "description": "Select one or more fields to sort the response by.", + "explode": false, "in": "query", "name": "sortby", "required": false, "schema": { - "anyOf": [ - { - "items": { - "enum": [ - "id", - "name", - "dataConnectionId", - "dataConnectionName", - "workspaceId", - "workspaceName", - "latestRunStatus", - "latestRunStartedAt", - "latestRunFinishedAt", - "-id", - "-name", - "-dataConnectionId", - "-dataConnectionName", - "-workspaceId", - "-workspaceName", - "-latestRunStatus", - "-latestRunStartedAt", - "-latestRunFinishedAt" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" - } + "items": { + "enum": [ + "id", + "name", + "dataConnectionId", + "dataConnectionName", + "workspaceId", + "workspaceName", + "latestRunStatus", + "latestRunStartedAt", + "latestRunFinishedAt", + "-id", + "-name", + "-dataConnectionId", + "-dataConnectionName", + "-workspaceId", + "-workspaceName", + "-latestRunStatus", + "-latestRunStartedAt", + "-latestRunFinishedAt", + "+id", + "+name", + "+dataConnectionId", + "+dataConnectionName", + "+workspaceId", + "+workspaceName", + "+latestRunStatus", + "+latestRunStartedAt", + "+latestRunFinishedAt" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" + }, + "style": "form" }, { "description": "Full-text search query. Comma-separated terms are combined with OR; whitespace-separated words within a term are combined with AND.", @@ -21318,7 +22107,7 @@ ] } }, - "/api/data/etl-tasks/{task_id}": { + "/api/ogc/collections/etl-tasks/items/{task_id}": { "delete": { "description": "Delete an ETL Task.", "operationId": "interfaces_api_views_etl_task_delete_etl_task", @@ -21407,12 +22196,12 @@ }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -21427,29 +22216,29 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", + "explode": false, "in": "query", "name": "include", "required": false, "schema": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "dataConnection" ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" - } + }, + "style": "form" } ], "responses": { @@ -21610,7 +22399,7 @@ ] } }, - "/api/data/etl-tasks/{task_id}/runs": { + "/api/ogc/collections/etl-tasks/items/{task_id}/runs": { "get": { "description": "Get runs for an ETL Task.", "operationId": "interfaces_api_views_etl_task_get_etl_task_runs", @@ -21627,12 +22416,12 @@ }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -21644,10 +22433,10 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", @@ -21688,28 +22477,22 @@ } }, { - "description": "The maximum number of items to return.", + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", "in": "query", "name": "limit", "required": false, "schema": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" } }, { "description": "Select one or more fields to sort the response by.", + "explode": false, "in": "query", "name": "sortby", "required": false, @@ -21725,13 +22508,18 @@ "-id", "-status", "-startedAt", - "-finishedAt" + "-finishedAt", + "+id", + "+status", + "+startedAt", + "+finishedAt" ], "type": "string" }, "title": "Sortby", "type": "array" - } + }, + "style": "form" }, { "description": "Filters task runs by their status.", @@ -21908,7 +22696,7 @@ ] } }, - "/api/data/etl-tasks/{task_id}/runs/{run_id}": { + "/api/ogc/collections/etl-tasks/items/{task_id}/runs/{run_id}": { "get": { "description": "Get a single run for an ETL Task.", "operationId": "interfaces_api_views_etl_task_get_etl_task_run", @@ -21939,7 +22727,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TaskRunResponse" + "$ref": "#/components/schemas/ItemResponse_TaskRunResponse_" } } }, @@ -21999,7 +22787,7 @@ ] } }, - "/api/data/etl-tasks/{task_id}/trigger": { + "/api/ogc/collections/etl-tasks/items/{task_id}/trigger": { "post": { "description": "Trigger an immediate run of an ETL Task on a Celery worker.", "operationId": "interfaces_api_views_etl_task_trigger_etl_task", @@ -22020,7 +22808,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TaskRunResponse" + "$ref": "#/components/schemas/ItemResponse_TaskRunResponse_" } } }, @@ -22080,19 +22868,56 @@ ] } }, - "/api/data/linked-resource-types": { + "/api/ogc/collections/linked-resource-types/items": { "get": { "description": "Get Linked Resource Types.", "operationId": "interfaces_api_views_sta_linked_resource_type_get_linked_resource_types", "parameters": [ + { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "explode": false, + "in": "query", + "name": "bbox", + "required": false, + "schema": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], + "type": "array" + }, + "style": "form" + }, + { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "explode": false, + "in": "query", + "name": "datetime", + "required": false, + "schema": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, + "style": "form" + }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -22101,10 +22926,10 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", @@ -22145,51 +22970,40 @@ } }, { - "description": "The maximum number of items to return.", + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", "in": "query", "name": "limit", "required": false, "schema": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" } }, { "description": "Select one or more fields to sort the response by.", + "explode": false, "in": "query", "name": "sortby", "required": false, "schema": { - "anyOf": [ - { - "items": { - "enum": [ - "name", - "-name" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" - } + "items": { + "enum": [ + "name", + "-name", + "+name" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" + }, + "style": "form" }, { "description": "Full-text search query. Comma-separated terms are combined with OR; whitespace-separated words within a term are combined with AND.", @@ -22331,7 +23145,7 @@ ] } }, - "/api/data/linked-resource-types/{linked_resource_type_id}": { + "/api/ogc/collections/linked-resource-types/items/{linked_resource_type_id}": { "delete": { "description": "Delete a Linked Resource Type.", "operationId": "interfaces_api_views_sta_linked_resource_type_delete_linked_resource_type", @@ -22409,12 +23223,12 @@ }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -22423,10 +23237,10 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" } ], "responses": { @@ -22565,19 +23379,56 @@ ] } }, - "/api/data/method-types": { + "/api/ogc/collections/method-types/items": { "get": { "description": "Get Method Types.", "operationId": "interfaces_api_views_sta_method_type_get_method_types", "parameters": [ + { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "explode": false, + "in": "query", + "name": "bbox", + "required": false, + "schema": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], + "type": "array" + }, + "style": "form" + }, + { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "explode": false, + "in": "query", + "name": "datetime", + "required": false, + "schema": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, + "style": "form" + }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -22586,10 +23437,10 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", @@ -22630,51 +23481,40 @@ } }, { - "description": "The maximum number of items to return.", + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", "in": "query", "name": "limit", "required": false, "schema": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" } }, { "description": "Select one or more fields to sort the response by.", + "explode": false, "in": "query", "name": "sortby", "required": false, "schema": { - "anyOf": [ - { - "items": { - "enum": [ - "name", - "-name" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" - } + "items": { + "enum": [ + "name", + "-name", + "+name" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" + }, + "style": "form" }, { "description": "Full-text search query. Comma-separated terms are combined with OR; whitespace-separated words within a term are combined with AND.", @@ -22816,7 +23656,7 @@ ] } }, - "/api/data/method-types/{method_type_id}": { + "/api/ogc/collections/method-types/items/{method_type_id}": { "delete": { "description": "Delete a Method Type.", "operationId": "interfaces_api_views_sta_method_type_delete_method_type", @@ -22894,12 +23734,12 @@ }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -22908,10 +23748,10 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" } ], "responses": { @@ -23050,19 +23890,56 @@ ] } }, - "/api/data/methods": { + "/api/ogc/collections/methods/items": { "get": { "description": "Get public Methods and Methods associated with the authenticated user.", "operationId": "interfaces_api_views_sta_method_get_methods", "parameters": [ + { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "explode": false, + "in": "query", + "name": "bbox", + "required": false, + "schema": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], + "type": "array" + }, + "style": "form" + }, + { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "explode": false, + "in": "query", + "name": "datetime", + "required": false, + "schema": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, + "style": "form" + }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -23078,19 +23955,19 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", + "explode": false, "in": "query", "name": "include", "required": false, "schema": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "workspace", @@ -23098,10 +23975,10 @@ ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" - } + }, + "style": "form" }, { "description": "Number of items to skip.", @@ -23124,63 +24001,58 @@ } }, { - "description": "The maximum number of items to return.", + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", "in": "query", "name": "limit", "required": false, "schema": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" } }, { "description": "Select one or more fields to sort the response by.", + "explode": false, "in": "query", "name": "sortby", "required": false, "schema": { - "anyOf": [ - { - "items": { - "enum": [ - "name", - "code", - "type", - "sensorModel", - "sensorModelManufacturer", - "definition", - "sensorModelDefinition", - "-name", - "-code", - "-type", - "-sensorModel", - "-sensorModelManufacturer", - "-definition", - "-sensorModelDefinition" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" - } + "items": { + "enum": [ + "name", + "code", + "type", + "sensorModel", + "sensorModelManufacturer", + "definition", + "sensorModelDefinition", + "-name", + "-code", + "-type", + "-sensorModel", + "-sensorModelManufacturer", + "-definition", + "-sensorModelDefinition", + "+name", + "+code", + "+type", + "+sensorModel", + "+sensorModelManufacturer", + "+definition", + "+sensorModelDefinition" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" + }, + "style": "form" }, { "description": "Full-text search query. Comma-separated terms are combined with OR; whitespace-separated words within a term are combined with AND.", @@ -23296,7 +24168,15 @@ "default": [], "description": "Filter methods by sensor model", "items": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "const": "null", + "type": "string" + } + ] }, "title": "Sensor Model", "type": "array" @@ -23311,7 +24191,15 @@ "default": [], "description": "Filter methods by sensor model manufacturer", "items": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "const": "null", + "type": "string" + } + ] }, "title": "Sensor Model Manufacturer", "type": "array" @@ -23450,7 +24338,7 @@ ] } }, - "/api/data/methods/{method_id}": { + "/api/ogc/collections/methods/items/{method_id}": { "delete": { "description": "Delete a Method.", "operationId": "interfaces_api_views_sta_method_delete_method", @@ -23539,12 +24427,12 @@ }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -23560,19 +24448,19 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", + "explode": false, "in": "query", "name": "include", "required": false, "schema": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "workspace", @@ -23580,10 +24468,10 @@ ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" - } + }, + "style": "form" } ], "responses": { @@ -23733,19 +24621,56 @@ ] } }, - "/api/data/monitoring-rules": { + "/api/ogc/collections/monitoring-rules/items": { "get": { "description": "Get monitoring rules.", "operationId": "interfaces_api_views_monitoring_rule_get_monitoring_rules", "parameters": [ + { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "explode": false, + "in": "query", + "name": "bbox", + "required": false, + "schema": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], + "type": "array" + }, + "style": "form" + }, + { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "explode": false, + "in": "query", + "name": "datetime", + "required": false, + "schema": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, + "style": "form" + }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -23759,29 +24684,29 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", + "explode": false, "in": "query", "name": "include", "required": false, "schema": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "datastream" ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" - } + }, + "style": "form" }, { "description": "Number of items to skip.", @@ -23804,57 +24729,49 @@ } }, { - "description": "The maximum number of items to return.", + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", "in": "query", "name": "limit", "required": false, "schema": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" } }, { "description": "Select one or more fields to sort the response by.", + "explode": false, "in": "query", "name": "sortby", "required": false, "schema": { - "anyOf": [ - { - "items": { - "enum": [ - "id", - "ruleType", - "datastreamId", - "lastCheckedAt", - "-id", - "-ruleType", - "-datastreamId", - "-lastCheckedAt" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" - } + "items": { + "enum": [ + "id", + "ruleType", + "datastreamId", + "lastCheckedAt", + "-id", + "-ruleType", + "-datastreamId", + "-lastCheckedAt", + "+id", + "+ruleType", + "+datastreamId", + "+lastCheckedAt" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" + }, + "style": "form" }, { "description": "Filter rules by datastream ID.", @@ -24074,7 +24991,7 @@ ] } }, - "/api/data/monitoring-rules/{rule_id}": { + "/api/ogc/collections/monitoring-rules/items/{rule_id}": { "delete": { "description": "Delete a monitoring rule.", "operationId": "interfaces_api_views_monitoring_rule_delete_monitoring_rule", @@ -24163,12 +25080,12 @@ }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -24182,29 +25099,29 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", + "explode": false, "in": "query", "name": "include", "required": false, "schema": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "datastream" ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" - } + }, + "style": "form" } ], "responses": { @@ -24365,19 +25282,56 @@ ] } }, - "/api/data/monitoring-site-types": { + "/api/ogc/collections/monitoring-site-types/items": { "get": { "description": "Get Monitoring Site Types.", "operationId": "interfaces_api_views_sta_monitoring_site_type_get_monitoring_site_types", "parameters": [ + { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "explode": false, + "in": "query", + "name": "bbox", + "required": false, + "schema": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], + "type": "array" + }, + "style": "form" + }, + { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "explode": false, + "in": "query", + "name": "datetime", + "required": false, + "schema": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, + "style": "form" + }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -24386,10 +25340,10 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", @@ -24430,51 +25384,40 @@ } }, { - "description": "The maximum number of items to return.", + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", "in": "query", "name": "limit", "required": false, "schema": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" } }, { "description": "Select one or more fields to sort the response by.", + "explode": false, "in": "query", "name": "sortby", "required": false, "schema": { - "anyOf": [ - { - "items": { - "enum": [ - "name", - "-name" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" - } + "items": { + "enum": [ + "name", + "-name", + "+name" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" + }, + "style": "form" }, { "description": "Full-text search query. Comma-separated terms are combined with OR; whitespace-separated words within a term are combined with AND.", @@ -24616,7 +25559,7 @@ ] } }, - "/api/data/monitoring-site-types/{monitoring_site_type_id}": { + "/api/ogc/collections/monitoring-site-types/items/{monitoring_site_type_id}": { "delete": { "description": "Delete a Monitoring Site Type.", "operationId": "interfaces_api_views_sta_monitoring_site_type_delete_monitoring_site_type", @@ -24694,12 +25637,12 @@ }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -24708,10 +25651,10 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" } ], "responses": { @@ -24850,19 +25793,56 @@ ] } }, - "/api/data/monitoring-sites": { + "/api/ogc/collections/monitoring-sites/items": { "get": { "description": "Get public MonitoringSites and MonitoringSites associated with the authenticated user.", "operationId": "interfaces_api_views_sta_monitoring_site_get_monitoring_sites", "parameters": [ + { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "explode": false, + "in": "query", + "name": "bbox", + "required": false, + "schema": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], + "type": "array" + }, + "style": "form" + }, + { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "explode": false, + "in": "query", + "name": "datetime", + "required": false, + "schema": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, + "style": "form" + }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -24885,19 +25865,19 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", + "explode": false, "in": "query", "name": "include", "required": false, "schema": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "workspace", @@ -24905,10 +25885,10 @@ ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" - } + }, + "style": "form" }, { "description": "Number of items to skip.", @@ -24931,71 +25911,70 @@ } }, { - "description": "The maximum number of items to return.", + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", "in": "query", "name": "limit", "required": false, "schema": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" } }, { "description": "Select one or more fields to sort the response by.", + "explode": false, "in": "query", "name": "sortby", "required": false, "schema": { - "anyOf": [ - { - "items": { - "enum": [ - "name", - "code", - "type", - "isPrivate", - "latitude", - "longitude", - "elevation_m", - "elevationDatum", - "adminArea1", - "adminArea2", - "country", - "-name", - "-code", - "-type", - "-isPrivate", - "-latitude", - "-longitude", - "-elevation_m", - "-elevationDatum", - "-adminArea1", - "-adminArea2", - "-country" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" - } + "items": { + "enum": [ + "name", + "code", + "type", + "isPrivate", + "latitude", + "longitude", + "elevation_m", + "elevationDatum", + "adminArea1", + "adminArea2", + "country", + "-name", + "-code", + "-type", + "-isPrivate", + "-latitude", + "-longitude", + "-elevation_m", + "-elevationDatum", + "-adminArea1", + "-adminArea2", + "-country", + "+name", + "+code", + "+type", + "+isPrivate", + "+latitude", + "+longitude", + "+elevation_m", + "+elevationDatum", + "+adminArea1", + "+adminArea2", + "+country" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" + }, + "style": "form" }, { "description": "Full-text search query. Comma-separated terms are combined with OR; whitespace-separated words within a term are combined with AND.", @@ -25031,21 +26010,6 @@ "type": "array" } }, - { - "description": "Filter monitoring sites by bounding box. Format bounding box as {min_lon},{min_lat},{max_lon},{max_lat}", - "in": "query", - "name": "bbox", - "required": false, - "schema": { - "default": [], - "description": "Filter monitoring sites by bounding box. Format bounding box as {min_lon},{min_lat},{max_lon},{max_lat}", - "items": { - "type": "string" - }, - "title": "Bbox", - "type": "array" - } - }, { "description": "Filter monitoring sites by admin area 1.", "in": "query", @@ -25055,7 +26019,15 @@ "default": [], "description": "Filter monitoring sites by admin area 1.", "items": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "const": "null", + "type": "string" + } + ] }, "title": "Admin Area 1", "type": "array" @@ -25070,7 +26042,15 @@ "default": [], "description": "Filter monitoring sites by admin area 2.", "items": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "const": "null", + "type": "string" + } + ] }, "title": "Admin Area 2", "type": "array" @@ -25085,7 +26065,15 @@ "default": [], "description": "Filter monitoring sites by country.", "items": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "const": "null", + "type": "string" + } + ] }, "title": "Country", "type": "array" @@ -25250,74 +26238,38 @@ ] } }, - "/api/data/monitoring-sites/markers": { - "get": { - "description": "Get lean marker data for public MonitoringSites plus private MonitoringSites visible to the authenticated user.", - "operationId": "interfaces_api_views_sta_monitoring_site_get_monitoring_site_markers", + "/api/ogc/collections/monitoring-sites/items/{monitoring_site_id}": { + "delete": { + "description": "Delete a MonitoringSite.", + "operationId": "interfaces_api_views_sta_monitoring_site_delete_monitoring_site", "parameters": [ { - "description": "Filter markers by workspace ID.", - "in": "query", - "name": "workspace_id", - "required": false, - "schema": { - "default": [], - "description": "Filter markers by workspace ID.", - "items": { - "format": "uuid", - "type": "string" - }, - "title": "Workspace Id", - "type": "array" - } - }, - { - "description": "Filter markers by bounding box. Format bounding box as {min_lon},{min_lat},{max_lon},{max_lat}", - "in": "query", - "name": "bbox", - "required": false, - "schema": { - "default": [], - "description": "Filter markers by bounding box. Format bounding box as {min_lon},{min_lat},{max_lon},{max_lat}", - "items": { - "type": "string" - }, - "title": "Bbox", - "type": "array" - } - }, - { - "description": "Filter markers by monitoring site type.", - "in": "query", - "name": "type", - "required": false, + "in": "path", + "name": "monitoring_site_id", + "required": true, "schema": { - "default": [], - "description": "Filter markers by monitoring site type.", - "items": { - "type": "string" - }, - "title": "Type", - "type": "array" + "format": "uuid", + "title": "Monitoring Site Id", + "type": "string" } } ], "responses": { - "200": { + "204": { + "description": "No Content" + }, + "401": { "content": { "application/json": { "schema": { - "items": { - "$ref": "#/components/schemas/MonitoringSiteMarkerResponse" - }, "title": "Response", - "type": "array" + "type": "string" } } }, - "description": "OK" + "description": "Unauthorized" }, - "401": { + "403": { "content": { "application/json": { "schema": { @@ -25326,7 +26278,7 @@ } } }, - "description": "Unauthorized" + "description": "Forbidden" } }, "security": [ @@ -25343,47 +26295,79 @@ "BasicAuth": [] } ], - "summary": "Get Monitoring Site Markers", + "summary": "Delete Monitoring Site", "tags": [ "Monitoring Sites" ] - } - }, - "/api/data/monitoring-sites/site-summaries": { + }, "get": { - "description": "Get lean site summary data for public MonitoringSites and MonitoringSites associated with the authenticated user.", - "operationId": "interfaces_api_views_sta_monitoring_site_get_monitoring_site_summaries", + "description": "Get a MonitoringSite.", + "operationId": "interfaces_api_views_sta_monitoring_site_get_monitoring_site", "parameters": [ { - "description": "Filter site summaries by workspace ID.", + "in": "path", + "name": "monitoring_site_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Monitoring Site Id", + "type": "string" + } + }, + { + "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", - "name": "workspace_id", + "name": "properties", "required": false, "schema": { - "default": [], - "description": "Filter site summaries by workspace ID.", + "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", "items": { - "format": "uuid", + "enum": [ + "id", + "workspaceId", + "name", + "description", + "code", + "type", + "latitude", + "longitude", + "elevation_m", + "elevationDatum", + "adminArea1", + "adminArea2", + "country", + "dataDisclaimer", + "isPrivate", + "tags", + "linkedResources" + ], "type": "string" }, - "title": "Workspace Id", + "title": "Properties", "type": "array" - } + }, + "style": "form" }, { - "description": "Filter summaries by monitoring site type.", + "description": "Comma-separated list of related resources to include in the response.", + "explode": false, "in": "query", - "name": "type", + "name": "include", "required": false, "schema": { - "default": [], - "description": "Filter summaries by monitoring site type.", + "description": "Comma-separated list of related resources to include in the response.", "items": { + "enum": [ + "workspace", + "type" + ], "type": "string" }, - "title": "Type", + "title": "Include", "type": "array" - } + }, + "style": "form" } ], "responses": { @@ -25391,11 +26375,7 @@ "content": { "application/json": { "schema": { - "items": { - "$ref": "#/components/schemas/MonitoringSiteMapSummaryResponse" - }, - "title": "Response", - "type": "array" + "$ref": "#/components/schemas/ItemResponse_MonitoringSiteResponse_" } } }, @@ -25411,6 +26391,17 @@ } }, "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "title": "Response", + "type": "string" + } + } + }, + "description": "Forbidden" } }, "security": [ @@ -25427,96 +26418,50 @@ "BasicAuth": [] } ], - "summary": "Get Monitoring Site Summaries", - "tags": [ - "Monitoring Sites" - ] - } - }, - "/api/data/monitoring-sites/site-type-icons": { - "get": { - "description": "Get the configured site type icon mappings.", - "operationId": "interfaces_api_views_sta_monitoring_site_get_site_type_icons", - "parameters": [], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/SiteTypeIconResponse" - }, - "title": "Response", - "type": "array" - } - } - }, - "description": "OK" - } - }, - "summary": "Get Site Type Icons", + "summary": "Get Monitoring Site", "tags": [ "Monitoring Sites" ] - } - }, - "/api/data/monitoring-sites/tags/keys": { - "get": { - "description": "Get all existing unique monitoring_site tag keys.", - "operationId": "interfaces_api_views_sta_monitoring_site_get_monitoring_site_tag_keys", + }, + "patch": { + "description": "Update a MonitoringSite.", + "operationId": "interfaces_api_views_sta_monitoring_site_update_monitoring_site", "parameters": [ { - "in": "query", - "name": "workspace_id", - "required": false, - "schema": { - "anyOf": [ - { - "format": "uuid", - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Workspace Id" - } - }, - { - "in": "query", + "in": "path", "name": "monitoring_site_id", - "required": false, + "required": true, "schema": { - "anyOf": [ - { - "format": "uuid", - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Monitoring Site Id" + "format": "uuid", + "title": "Monitoring Site Id", + "type": "string" } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MonitoringSitePatchBody" + } + } + }, + "required": true + }, "responses": { - "200": { + "204": { + "description": "No Content" + }, + "400": { "content": { "application/json": { "schema": { - "additionalProperties": { - "items": { - "type": "string" - }, - "type": "array" - }, "title": "Response", - "type": "object" + "type": "string" } } }, - "description": "OK" + "description": "Bad Request" }, "401": { "content": { @@ -25528,6 +26473,17 @@ } }, "description": "Unauthorized" + }, + "403": { + "content": { + "application/json": { + "schema": { + "title": "Response", + "type": "string" + } + } + }, + "description": "Forbidden" } }, "security": [ @@ -25544,41 +26500,87 @@ "BasicAuth": [] } ], - "summary": "Get Monitoring Site Tag Keys", + "summary": "Update Monitoring Site", "tags": [ "Monitoring Sites" ] } }, - "/api/data/monitoring-sites/task-summaries": { + "/api/ogc/collections/monitoring-sites/items/{monitoring_site_id}/linked-resources": { "get": { - "description": "Get task count summaries for MonitoringSites associated with the authenticated user.", - "operationId": "interfaces_api_views_sta_monitoring_site_get_monitoring_site_task_summaries", + "description": "Get all linked resources associated with a MonitoringSite.", + "operationId": "interfaces_api_views_sta_monitoring_site_get_monitoring_site_linked_resources", "parameters": [ { - "description": "Filter task summaries by workspace ID.", + "in": "path", + "name": "monitoring_site_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Monitoring Site Id", + "type": "string" + } + }, + { + "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", "in": "query", - "name": "workspace_id", + "name": "properties", "required": false, "schema": { - "default": [], - "description": "Filter task summaries by workspace ID.", - "items": { - "format": "uuid", - "type": "string" - }, - "title": "Workspace Id", - "type": "array" + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "title": "Properties" } }, { - "description": "Filter summaries by monitoring site type.", + "description": "Number of items to skip.", + "in": "query", + "name": "offset", + "required": false, + "schema": { + "anyOf": [ + { + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": 0, + "description": "Number of items to skip.", + "title": "Offset" + } + }, + { + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "in": "query", + "name": "limit", + "required": false, + "schema": { + "default": 100, + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" + } + }, + { + "description": "Filter by linked resource type.", "in": "query", "name": "type", "required": false, "schema": { "default": [], - "description": "Filter summaries by monitoring site type.", + "description": "Filter by linked resource type.", "items": { "type": "string" }, @@ -25592,68 +26594,12 @@ "content": { "application/json": { "schema": { - "items": { - "$ref": "#/components/schemas/MonitoringSiteTaskSummaryResponse" - }, - "title": "Response", - "type": "array" + "$ref": "#/components/schemas/PaginatedResponse_LinkedResourceGetResponse_" } } }, "description": "OK" }, - "401": { - "content": { - "application/json": { - "schema": { - "title": "Response", - "type": "string" - } - } - }, - "description": "Unauthorized" - } - }, - "security": [ - { - "SessionAuth": [] - }, - { - "OIDCAuth": [] - }, - { - "APIKeyAuth": [] - }, - { - "BasicAuth": [] - } - ], - "summary": "Get Monitoring Site Task Summaries", - "tags": [ - "Monitoring Sites" - ] - } - }, - "/api/data/monitoring-sites/{monitoring_site_id}": { - "delete": { - "description": "Delete a MonitoringSite.", - "operationId": "interfaces_api_views_sta_monitoring_site_delete_monitoring_site", - "parameters": [ - { - "in": "path", - "name": "monitoring_site_id", - "required": true, - "schema": { - "format": "uuid", - "title": "Monitoring Site Id", - "type": "string" - } - } - ], - "responses": { - "204": { - "description": "No Content" - }, "401": { "content": { "application/json": { @@ -25691,14 +26637,14 @@ "BasicAuth": [] } ], - "summary": "Delete Monitoring Site", + "summary": "Get Monitoring Site Linked Resources", "tags": [ "Monitoring Sites" ] }, - "get": { - "description": "Get a MonitoringSite.", - "operationId": "interfaces_api_views_sta_monitoring_site_get_monitoring_site", + "post": { + "description": "Add a linked resource to a monitoring_site.", + "operationId": "interfaces_api_views_sta_monitoring_site_add_monitoring_site_linked_resource", "parameters": [ { "in": "path", @@ -25709,73 +26655,90 @@ "title": "Monitoring Site Id", "type": "string" } - }, - { - "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "in": "query", - "name": "properties", - "required": false, - "schema": { - "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, - "items": { - "enum": [ - "id", - "workspaceId", + } + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "properties": { + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" + }, + "file": { + "anyOf": [ + { + "format": "binary", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "File" + }, + "link": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Link" + }, + "name": { + "minLength": 1, + "title": "Name", + "type": "string" + }, + "type": { + "minLength": 1, + "title": "Type", + "type": "string" + } + }, + "required": [ "name", - "description", - "code", - "type", - "latitude", - "longitude", - "elevation_m", - "elevationDatum", - "adminArea1", - "adminArea2", - "country", - "dataDisclaimer", - "isPrivate", - "tags", - "linkedResources" - ], - "type": "string" - }, - "style": "form", - "title": "Properties", - "type": "array" - } - }, - { - "description": "Comma-separated list of related resources to include in the response.", - "in": "query", - "name": "include", - "required": false, - "schema": { - "description": "Comma-separated list of related resources to include in the response.", - "explode": false, - "items": { - "enum": [ - "workspace", "type" ], - "type": "string" - }, - "style": "form", - "title": "Include", - "type": "array" + "title": "MultiPartBodyParams", + "type": "object" + } } - } - ], + }, + "required": true + }, "responses": { - "200": { + "201": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ItemResponse_MonitoringSiteResponse_" + "$ref": "#/components/schemas/CreatedResponse" } } }, - "description": "OK" + "description": "Created" + }, + "400": { + "content": { + "application/json": { + "schema": { + "title": "Response", + "type": "string" + } + } + }, + "description": "Bad Request" }, "401": { "content": { @@ -25798,6 +26761,17 @@ } }, "description": "Forbidden" + }, + "413": { + "content": { + "application/json": { + "schema": { + "title": "Response", + "type": "string" + } + } + }, + "description": "Content Too Large" } }, "security": [ @@ -25814,14 +26788,16 @@ "BasicAuth": [] } ], - "summary": "Get Monitoring Site", + "summary": "Add Monitoring Site Linked Resource", "tags": [ "Monitoring Sites" ] - }, - "patch": { - "description": "Update a MonitoringSite.", - "operationId": "interfaces_api_views_sta_monitoring_site_update_monitoring_site", + } + }, + "/api/ogc/collections/monitoring-sites/items/{monitoring_site_id}/linked-resources/{linked_resource_id}": { + "delete": { + "description": "Remove a linked resource from a monitoring_site.", + "operationId": "interfaces_api_views_sta_monitoring_site_remove_monitoring_site_linked_resource", "parameters": [ { "in": "path", @@ -25832,23 +26808,23 @@ "title": "Monitoring Site Id", "type": "string" } + }, + { + "in": "path", + "name": "linked_resource_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Linked Resource Id", + "type": "string" + } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MonitoringSitePatchBody" - } - } - }, - "required": true - }, "responses": { "204": { "description": "No Content" }, - "400": { + "401": { "content": { "application/json": { "schema": { @@ -25857,9 +26833,9 @@ } } }, - "description": "Bad Request" + "description": "Unauthorized" }, - "401": { + "403": { "content": { "application/json": { "schema": { @@ -25868,9 +26844,9 @@ } } }, - "description": "Unauthorized" + "description": "Forbidden" }, - "403": { + "404": { "content": { "application/json": { "schema": { @@ -25879,7 +26855,7 @@ } } }, - "description": "Forbidden" + "description": "Not Found" } }, "security": [ @@ -25896,16 +26872,14 @@ "BasicAuth": [] } ], - "summary": "Update Monitoring Site", + "summary": "Remove Monitoring Site Linked Resource", "tags": [ "Monitoring Sites" ] - } - }, - "/api/data/monitoring-sites/{monitoring_site_id}/linked-resources": { + }, "get": { - "description": "Get all linked resources associated with a MonitoringSite.", - "operationId": "interfaces_api_views_sta_monitoring_site_get_monitoring_site_linked_resources", + "description": "Get a linked resource associated with a MonitoringSite.", + "operationId": "interfaces_api_views_sta_monitoring_site_get_monitoring_site_linked_resource", "parameters": [ { "in": "path", @@ -25918,95 +26892,13 @@ } }, { - "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "in": "query", - "name": "properties", - "required": false, - "schema": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "title": "Properties" - } - }, - { - "description": "Comma-separated list of related resources to include in the response.", - "in": "query", - "name": "include", - "required": false, - "schema": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Comma-separated list of related resources to include in the response.", - "title": "Include" - } - }, - { - "description": "Number of items to skip.", - "in": "query", - "name": "offset", - "required": false, - "schema": { - "anyOf": [ - { - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], - "default": 0, - "description": "Number of items to skip.", - "title": "Offset" - } - }, - { - "description": "The maximum number of items to return.", - "in": "query", - "name": "limit", - "required": false, - "schema": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], - "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" - } - }, - { - "description": "Filter by linked resource type.", - "in": "query", - "name": "type", - "required": false, + "in": "path", + "name": "linked_resource_id", + "required": true, "schema": { - "default": [], - "description": "Filter by linked resource type.", - "items": { - "type": "string" - }, - "title": "Type", - "type": "array" + "format": "uuid", + "title": "Linked Resource Id", + "type": "string" } } ], @@ -26015,11 +26907,7 @@ "content": { "application/json": { "schema": { - "items": { - "$ref": "#/components/schemas/LinkedResourceGetResponse" - }, - "title": "Response", - "type": "array" + "$ref": "#/components/schemas/ItemResponse_LinkedResourceGetResponse_" } } }, @@ -26046,6 +26934,17 @@ } }, "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "title": "Response", + "type": "string" + } + } + }, + "description": "Not Found" } }, "security": [ @@ -26062,14 +26961,14 @@ "BasicAuth": [] } ], - "summary": "Get Monitoring Site Linked Resources", + "summary": "Get Monitoring Site Linked Resource", "tags": [ "Monitoring Sites" ] }, - "post": { - "description": "Add a linked resource to a monitoring_site.", - "operationId": "interfaces_api_views_sta_monitoring_site_add_monitoring_site_linked_resource", + "patch": { + "description": "Update a linked resource for a monitoring_site. A linked resource's mode (hosted file vs.\nexternal URL) cannot be changed in place \u2014 delete it and create a new one instead.", + "operationId": "interfaces_api_views_sta_monitoring_site_update_monitoring_site_linked_resource", "parameters": [ { "in": "path", @@ -26080,6 +26979,16 @@ "title": "Monitoring Site Id", "type": "string" } + }, + { + "in": "path", + "name": "linked_resource_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Linked Resource Id", + "type": "string" + } } ], "requestBody": { @@ -26122,20 +27031,28 @@ "title": "Link" }, "name": { - "minLength": 1, - "title": "Name", - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Name" }, "type": { - "minLength": 1, - "title": "Type", - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Type" } }, - "required": [ - "name", - "type" - ], "title": "MultiPartBodyParams", "type": "object" } @@ -26144,15 +27061,8 @@ "required": true }, "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreatedResponse" - } - } - }, - "description": "Created" + "204": { + "description": "No Content" }, "400": { "content": { @@ -26187,6 +27097,17 @@ }, "description": "Forbidden" }, + "404": { + "content": { + "application/json": { + "schema": { + "title": "Response", + "type": "string" + } + } + }, + "description": "Not Found" + }, "413": { "content": { "application/json": { @@ -26213,41 +27134,88 @@ "BasicAuth": [] } ], - "summary": "Add Monitoring Site Linked Resource", + "summary": "Update Monitoring Site Linked Resource", "tags": [ "Monitoring Sites" ] } }, - "/api/data/monitoring-sites/{monitoring_site_id}/linked-resources/{linked_resource_id}": { - "delete": { - "description": "Remove a linked resource from a monitoring_site.", - "operationId": "interfaces_api_views_sta_monitoring_site_remove_monitoring_site_linked_resource", + "/api/ogc/collections/monitoring-sites/markers": { + "get": { + "description": "Get lean marker data for public MonitoringSites plus private MonitoringSites visible to the authenticated user.", + "operationId": "interfaces_api_views_sta_monitoring_site_get_monitoring_site_markers", "parameters": [ { - "in": "path", - "name": "monitoring_site_id", - "required": true, + "description": "Filter markers by workspace ID.", + "in": "query", + "name": "workspace_id", + "required": false, "schema": { - "format": "uuid", - "title": "Monitoring Site Id", - "type": "string" + "default": [], + "description": "Filter markers by workspace ID.", + "items": { + "format": "uuid", + "type": "string" + }, + "title": "Workspace Id", + "type": "array" } }, { - "in": "path", - "name": "linked_resource_id", - "required": true, + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "explode": false, + "in": "query", + "name": "bbox", + "required": false, "schema": { - "format": "uuid", - "title": "Linked Resource Id", - "type": "string" + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], + "type": "array" + }, + "style": "form" + }, + { + "description": "Filter markers by monitoring site type.", + "in": "query", + "name": "type", + "required": false, + "schema": { + "default": [], + "description": "Filter markers by monitoring site type.", + "items": { + "type": "string" + }, + "title": "Type", + "type": "array" } } ], "responses": { - "204": { - "description": "No Content" + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/MonitoringSiteMarkerResponse" + }, + "title": "Response", + "type": "array" + } + } + }, + "description": "OK" }, "401": { "content": { @@ -26259,19 +27227,81 @@ } }, "description": "Unauthorized" + } + }, + "security": [ + { + "SessionAuth": [] }, - "403": { + { + "OIDCAuth": [] + }, + { + "APIKeyAuth": [] + }, + { + "BasicAuth": [] + } + ], + "summary": "Get Monitoring Site Markers", + "tags": [ + "Monitoring Sites" + ] + } + }, + "/api/ogc/collections/monitoring-sites/site-summaries": { + "get": { + "description": "Get lean site summary data for public MonitoringSites and MonitoringSites associated with the authenticated user.", + "operationId": "interfaces_api_views_sta_monitoring_site_get_monitoring_site_summaries", + "parameters": [ + { + "description": "Filter site summaries by workspace ID.", + "in": "query", + "name": "workspace_id", + "required": false, + "schema": { + "default": [], + "description": "Filter site summaries by workspace ID.", + "items": { + "format": "uuid", + "type": "string" + }, + "title": "Workspace Id", + "type": "array" + } + }, + { + "description": "Filter summaries by monitoring site type.", + "in": "query", + "name": "type", + "required": false, + "schema": { + "default": [], + "description": "Filter summaries by monitoring site type.", + "items": { + "type": "string" + }, + "title": "Type", + "type": "array" + } + } + ], + "responses": { + "200": { "content": { "application/json": { "schema": { + "items": { + "$ref": "#/components/schemas/MonitoringSiteMapSummaryResponse" + }, "title": "Response", - "type": "string" + "type": "array" } } }, - "description": "Forbidden" + "description": "OK" }, - "404": { + "401": { "content": { "application/json": { "schema": { @@ -26280,7 +27310,7 @@ } } }, - "description": "Not Found" + "description": "Unauthorized" } }, "security": [ @@ -26297,119 +27327,96 @@ "BasicAuth": [] } ], - "summary": "Remove Monitoring Site Linked Resource", + "summary": "Get Monitoring Site Summaries", "tags": [ "Monitoring Sites" ] - }, - "patch": { - "description": "Update a linked resource for a monitoring_site. A linked resource's mode (hosted file vs.\nexternal URL) cannot be changed in place \u2014 delete it and create a new one instead.", - "operationId": "interfaces_api_views_sta_monitoring_site_update_monitoring_site_linked_resource", + } + }, + "/api/ogc/collections/monitoring-sites/site-type-icons": { + "get": { + "description": "Get the configured site type icon mappings.", + "operationId": "interfaces_api_views_sta_monitoring_site_get_site_type_icons", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/SiteTypeIconResponse" + }, + "title": "Response", + "type": "array" + } + } + }, + "description": "OK" + } + }, + "summary": "Get Site Type Icons", + "tags": [ + "Monitoring Sites" + ] + } + }, + "/api/ogc/collections/monitoring-sites/tags/keys": { + "get": { + "description": "Get all existing unique monitoring_site tag keys.", + "operationId": "interfaces_api_views_sta_monitoring_site_get_monitoring_site_tag_keys", "parameters": [ { - "in": "path", - "name": "monitoring_site_id", - "required": true, + "in": "query", + "name": "workspace_id", + "required": false, "schema": { - "format": "uuid", - "title": "Monitoring Site Id", - "type": "string" + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Workspace Id" } }, { - "in": "path", - "name": "linked_resource_id", - "required": true, + "in": "query", + "name": "monitoring_site_id", + "required": false, "schema": { - "format": "uuid", - "title": "Linked Resource Id", - "type": "string" + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Monitoring Site Id" } } ], - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "properties": { - "description": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Description" - }, - "file": { - "anyOf": [ - { - "format": "binary", - "type": "string" - }, - { - "type": "null" - } - ], - "title": "File" - }, - "link": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Link" - }, - "name": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Name" - }, - "type": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Type" - } - }, - "title": "MultiPartBodyParams", - "type": "object" - } - } - }, - "required": true - }, "responses": { - "204": { - "description": "No Content" - }, - "400": { + "200": { "content": { "application/json": { "schema": { + "additionalProperties": { + "items": { + "type": "string" + }, + "type": "array" + }, "title": "Response", - "type": "string" + "type": "object" } } }, - "description": "Bad Request" + "description": "OK" }, "401": { "content": { @@ -26421,30 +27428,81 @@ } }, "description": "Unauthorized" + } + }, + "security": [ + { + "SessionAuth": [] }, - "403": { - "content": { - "application/json": { - "schema": { - "title": "Response", - "type": "string" - } - } - }, - "description": "Forbidden" + { + "OIDCAuth": [] }, - "404": { + { + "APIKeyAuth": [] + }, + { + "BasicAuth": [] + } + ], + "summary": "Get Monitoring Site Tag Keys", + "tags": [ + "Monitoring Sites" + ] + } + }, + "/api/ogc/collections/monitoring-sites/task-summaries": { + "get": { + "description": "Get task count summaries for MonitoringSites associated with the authenticated user.", + "operationId": "interfaces_api_views_sta_monitoring_site_get_monitoring_site_task_summaries", + "parameters": [ + { + "description": "Filter task summaries by workspace ID.", + "in": "query", + "name": "workspace_id", + "required": false, + "schema": { + "default": [], + "description": "Filter task summaries by workspace ID.", + "items": { + "format": "uuid", + "type": "string" + }, + "title": "Workspace Id", + "type": "array" + } + }, + { + "description": "Filter summaries by monitoring site type.", + "in": "query", + "name": "type", + "required": false, + "schema": { + "default": [], + "description": "Filter summaries by monitoring site type.", + "items": { + "type": "string" + }, + "title": "Type", + "type": "array" + } + } + ], + "responses": { + "200": { "content": { "application/json": { "schema": { + "items": { + "$ref": "#/components/schemas/MonitoringSiteTaskSummaryResponse" + }, "title": "Response", - "type": "string" + "type": "array" } } }, - "description": "Not Found" + "description": "OK" }, - "413": { + "401": { "content": { "application/json": { "schema": { @@ -26453,7 +27511,7 @@ } } }, - "description": "Content Too Large" + "description": "Unauthorized" } }, "security": [ @@ -26470,25 +27528,62 @@ "BasicAuth": [] } ], - "summary": "Update Monitoring Site Linked Resource", + "summary": "Get Monitoring Site Task Summaries", "tags": [ "Monitoring Sites" ] } }, - "/api/data/monitoring-tasks": { + "/api/ogc/collections/monitoring-tasks/items": { "get": { "description": "Get monitoring tasks accessible to the authenticated user.", "operationId": "interfaces_api_views_monitoring_task_get_monitoring_tasks", "parameters": [ + { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "explode": false, + "in": "query", + "name": "bbox", + "required": false, + "schema": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], + "type": "array" + }, + "style": "form" + }, + { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "explode": false, + "in": "query", + "name": "datetime", + "required": false, + "schema": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, + "style": "form" + }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -26503,29 +27598,29 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", + "explode": false, "in": "query", "name": "include", "required": false, "schema": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "monitoringSite" ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" - } + }, + "style": "form" }, { "description": "Number of items to skip.", @@ -26548,67 +27643,64 @@ } }, { - "description": "The maximum number of items to return.", + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", "in": "query", "name": "limit", "required": false, "schema": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" } }, { "description": "Select one or more fields to sort the response by.", + "explode": false, "in": "query", "name": "sortby", "required": false, "schema": { - "anyOf": [ - { - "items": { - "enum": [ - "id", - "name", - "monitoringSiteId", - "monitoringSiteName", - "workspaceId", - "workspaceName", - "latestRunStatus", - "latestRunStartedAt", - "latestRunFinishedAt", - "-id", - "-name", - "-monitoringSiteId", - "-monitoringSiteName", - "-workspaceId", - "-workspaceName", - "-latestRunStatus", - "-latestRunStartedAt", - "-latestRunFinishedAt" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" - } + "items": { + "enum": [ + "id", + "name", + "monitoringSiteId", + "monitoringSiteName", + "workspaceId", + "workspaceName", + "latestRunStatus", + "latestRunStartedAt", + "latestRunFinishedAt", + "-id", + "-name", + "-monitoringSiteId", + "-monitoringSiteName", + "-workspaceId", + "-workspaceName", + "-latestRunStatus", + "-latestRunStartedAt", + "-latestRunFinishedAt", + "+id", + "+name", + "+monitoringSiteId", + "+monitoringSiteName", + "+workspaceId", + "+workspaceName", + "+latestRunStatus", + "+latestRunStartedAt", + "+latestRunFinishedAt" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" + }, + "style": "form" }, { "description": "Full-text search query. Comma-separated terms are combined with OR; whitespace-separated words within a term are combined with AND.", @@ -26847,7 +27939,7 @@ ] } }, - "/api/data/monitoring-tasks/{task_id}": { + "/api/ogc/collections/monitoring-tasks/items/{task_id}": { "delete": { "description": "Delete a monitoring task.", "operationId": "interfaces_api_views_monitoring_task_delete_monitoring_task", @@ -26936,12 +28028,12 @@ }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -26956,29 +28048,29 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", + "explode": false, "in": "query", "name": "include", "required": false, "schema": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "monitoringSite" ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" - } + }, + "style": "form" } ], "responses": { @@ -27139,7 +28231,7 @@ ] } }, - "/api/data/monitoring-tasks/{task_id}/runs": { + "/api/ogc/collections/monitoring-tasks/items/{task_id}/runs": { "get": { "description": "Get runs for a monitoring task.", "operationId": "interfaces_api_views_monitoring_task_get_monitoring_task_runs", @@ -27156,12 +28248,12 @@ }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -27173,10 +28265,10 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", @@ -27217,28 +28309,22 @@ } }, { - "description": "The maximum number of items to return.", + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", "in": "query", "name": "limit", "required": false, "schema": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" } }, { "description": "Select one or more fields to sort the response by.", + "explode": false, "in": "query", "name": "sortby", "required": false, @@ -27254,13 +28340,18 @@ "-id", "-status", "-startedAt", - "-finishedAt" + "-finishedAt", + "+id", + "+status", + "+startedAt", + "+finishedAt" ], "type": "string" }, "title": "Sortby", "type": "array" - } + }, + "style": "form" }, { "description": "Filters task runs by their status.", @@ -27437,7 +28528,7 @@ ] } }, - "/api/data/monitoring-tasks/{task_id}/runs/{run_id}": { + "/api/ogc/collections/monitoring-tasks/items/{task_id}/runs/{run_id}": { "get": { "description": "Get a single run for a monitoring task.", "operationId": "interfaces_api_views_monitoring_task_get_monitoring_task_run", @@ -27468,7 +28559,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TaskRunResponse" + "$ref": "#/components/schemas/ItemResponse_TaskRunResponse_" } } }, @@ -27528,7 +28619,7 @@ ] } }, - "/api/data/monitoring-tasks/{task_id}/trigger": { + "/api/ogc/collections/monitoring-tasks/items/{task_id}/trigger": { "post": { "description": "Trigger an immediate run of a monitoring task on a Celery worker.", "operationId": "interfaces_api_views_monitoring_task_trigger_monitoring_task", @@ -27549,7 +28640,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TaskRunResponse" + "$ref": "#/components/schemas/ItemResponse_TaskRunResponse_" } } }, @@ -27609,19 +28700,224 @@ ] } }, - "/api/data/observations": { + "/api/ogc/collections/observations/bulk-create": { + "post": { + "description": "Insert Observations.", + "operationId": "interfaces_api_views_sta_observation_insert_observations", + "parameters": [ + { + "description": "Specifies how new observations are added to the datastream. `insert` allows observations at any timestamp. `append` adds only future observations (after the latest existing timestamp). `backfill` adds only historical observations (before the earliest existing timestamp). `replace` deletes all observations in the range of provided observations before inserting new ones.", + "in": "query", + "name": "mode", + "required": false, + "schema": { + "anyOf": [ + { + "enum": [ + "insert", + "append", + "backfill", + "replace" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Specifies how new observations are added to the datastream. `insert` allows observations at any timestamp. `append` adds only future observations (after the latest existing timestamp). `backfill` adds only historical observations (before the earliest existing timestamp). `replace` deletes all observations in the range of provided observations before inserting new ones.", + "title": "Mode" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ObservationBulkPostBody" + }, + { + "$ref": "#/components/schemas/ObservationBulkColumnarPostBody" + } + ], + "title": "Data" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created" + }, + "400": { + "content": { + "application/json": { + "schema": { + "title": "Response", + "type": "string" + } + } + }, + "description": "Bad Request" + }, + "403": { + "content": { + "application/json": { + "schema": { + "title": "Response", + "type": "string" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "title": "Response", + "type": "string" + } + } + }, + "description": "Not Found" + } + }, + "security": [ + { + "SessionAuth": [] + }, + { + "OIDCAuth": [] + }, + { + "APIKeyAuth": [] + }, + { + "BasicAuth": [] + } + ], + "summary": "Insert Observations", + "tags": [ + "Observations" + ] + } + }, + "/api/ogc/collections/observations/bulk-delete": { + "post": { + "description": "Delete Observations between the given phenomenon start and end times.", + "operationId": "interfaces_api_views_sta_observation_delete_observations", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ObservationBulkDeleteBody" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "No Content" + }, + "403": { + "content": { + "application/json": { + "schema": { + "title": "Response", + "type": "string" + } + } + }, + "description": "Forbidden" + }, + "404": { + "content": { + "application/json": { + "schema": { + "title": "Response", + "type": "string" + } + } + }, + "description": "Not Found" + } + }, + "security": [ + { + "SessionAuth": [] + }, + { + "OIDCAuth": [] + }, + { + "APIKeyAuth": [] + }, + { + "BasicAuth": [] + } + ], + "summary": "Delete Observations", + "tags": [ + "Observations" + ] + } + }, + "/api/ogc/collections/observations/items": { "get": { "description": "Get Observations.", "operationId": "interfaces_api_views_sta_observation_get_observations", "parameters": [ + { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "explode": false, + "in": "query", + "name": "bbox", + "required": false, + "schema": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], + "type": "array" + }, + "style": "form" + }, + { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "explode": false, + "in": "query", + "name": "datetime", + "required": false, + "schema": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, + "style": "form" + }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted. Only applies to format=record.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted. Only applies to format=record.", - "explode": false, "items": { "enum": [ "id", @@ -27633,19 +28929,19 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response. Only applies to format=record.", + "explode": false, "in": "query", "name": "include", "required": false, "schema": { "description": "Comma-separated list of related resources to include in the response. Only applies to format=record.", - "explode": false, "items": { "enum": [ "datastream", @@ -27654,10 +28950,10 @@ ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" - } + }, + "style": "form" }, { "description": "Number of items to skip.", @@ -27680,24 +28976,17 @@ } }, { - "description": "The maximum number of items to return.", + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", "in": "query", "name": "limit", "required": false, "schema": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" } }, { @@ -27718,31 +29007,28 @@ }, { "description": "Select one or more fields to sort the response by.", + "explode": false, "in": "query", "name": "sortby", "required": false, "schema": { - "anyOf": [ - { - "items": { - "enum": [ - "phenomenonTime", - "datastreamId", - "-phenomenonTime", - "-datastreamId" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" - } + "items": { + "enum": [ + "phenomenonTime", + "datastreamId", + "-phenomenonTime", + "-datastreamId", + "+phenomenonTime", + "+datastreamId" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" + }, + "style": "form" }, { "description": "Controls the format of the observations response.", @@ -27767,50 +29053,6 @@ "title": "Format" } }, - { - "description": "Sets the maximum phenomenon time of filtered observations.", - "in": "query", - "name": "phenomenon_time_max", - "required": false, - "schema": { - "anyOf": [ - { - "type": "string" - }, - { - "format": "date-time", - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Sets the maximum phenomenon time of filtered observations.", - "title": "Phenomenon Time Max" - } - }, - { - "description": "Sets the minimum phenomenon time of filtered observations.", - "in": "query", - "name": "phenomenon_time_min", - "required": false, - "schema": { - "anyOf": [ - { - "type": "string" - }, - { - "format": "date-time", - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Sets the minimum phenomenon time of filtered observations.", - "title": "Phenomenon Time Min" - } - }, { "description": "Filter observations by result qualifier code.", "in": "query", @@ -27981,175 +29223,7 @@ ] } }, - "/api/data/observations/bulk-create": { - "post": { - "description": "Insert Observations.", - "operationId": "interfaces_api_views_sta_observation_insert_observations", - "parameters": [ - { - "description": "Specifies how new observations are added to the datastream. `insert` allows observations at any timestamp. `append` adds only future observations (after the latest existing timestamp). `backfill` adds only historical observations (before the earliest existing timestamp). `replace` deletes all observations in the range of provided observations before inserting new ones.", - "in": "query", - "name": "mode", - "required": false, - "schema": { - "anyOf": [ - { - "enum": [ - "insert", - "append", - "backfill", - "replace" - ], - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Specifies how new observations are added to the datastream. `insert` allows observations at any timestamp. `append` adds only future observations (after the latest existing timestamp). `backfill` adds only historical observations (before the earliest existing timestamp). `replace` deletes all observations in the range of provided observations before inserting new ones.", - "title": "Mode" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/ObservationBulkPostBody" - }, - { - "$ref": "#/components/schemas/ObservationBulkColumnarPostBody" - } - ], - "title": "Data" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "Created" - }, - "400": { - "content": { - "application/json": { - "schema": { - "title": "Response", - "type": "string" - } - } - }, - "description": "Bad Request" - }, - "403": { - "content": { - "application/json": { - "schema": { - "title": "Response", - "type": "string" - } - } - }, - "description": "Forbidden" - }, - "404": { - "content": { - "application/json": { - "schema": { - "title": "Response", - "type": "string" - } - } - }, - "description": "Not Found" - } - }, - "security": [ - { - "SessionAuth": [] - }, - { - "OIDCAuth": [] - }, - { - "APIKeyAuth": [] - }, - { - "BasicAuth": [] - } - ], - "summary": "Insert Observations", - "tags": [ - "Observations" - ] - } - }, - "/api/data/observations/bulk-delete": { - "post": { - "description": "Delete Observations between the given phenomenon start and end times.", - "operationId": "interfaces_api_views_sta_observation_delete_observations", - "parameters": [], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ObservationBulkDeleteBody" - } - } - }, - "required": true - }, - "responses": { - "204": { - "description": "No Content" - }, - "403": { - "content": { - "application/json": { - "schema": { - "title": "Response", - "type": "string" - } - } - }, - "description": "Forbidden" - }, - "404": { - "content": { - "application/json": { - "schema": { - "title": "Response", - "type": "string" - } - } - }, - "description": "Not Found" - } - }, - "security": [ - { - "SessionAuth": [] - }, - { - "OIDCAuth": [] - }, - { - "APIKeyAuth": [] - }, - { - "BasicAuth": [] - } - ], - "summary": "Delete Observations", - "tags": [ - "Observations" - ] - } - }, - "/api/data/observations/{observation_id}": { + "/api/ogc/collections/observations/items/{observation_id}": { "delete": { "description": "Delete an Observation.", "operationId": "interfaces_api_views_sta_observation_delete_observation", @@ -28238,12 +29312,12 @@ }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted. Only applies to format=record.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted. Only applies to format=record.", - "explode": false, "items": { "enum": [ "id", @@ -28255,19 +29329,19 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response. Only applies to format=record.", + "explode": false, "in": "query", "name": "include", "required": false, "schema": { "description": "Comma-separated list of related resources to include in the response. Only applies to format=record.", - "explode": false, "items": { "enum": [ "datastream", @@ -28276,10 +29350,10 @@ ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" - } + }, + "style": "form" } ], "responses": { @@ -28336,19 +29410,56 @@ ] } }, - "/api/data/observed-properties": { + "/api/ogc/collections/observed-properties/items": { "get": { "description": "Get public Observed Properties and Observed Properties associated with the authenticated user.", "operationId": "interfaces_api_views_sta_observed_property_get_observed_properties", "parameters": [ + { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "explode": false, + "in": "query", + "name": "bbox", + "required": false, + "schema": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], + "type": "array" + }, + "style": "form" + }, + { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "explode": false, + "in": "query", + "name": "datetime", + "required": false, + "schema": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, + "style": "form" + }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -28361,19 +29472,19 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", + "explode": false, "in": "query", "name": "include", "required": false, "schema": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "workspace", @@ -28381,10 +29492,10 @@ ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" - } + }, + "style": "form" }, { "description": "Number of items to skip.", @@ -28407,55 +29518,46 @@ } }, { - "description": "The maximum number of items to return.", + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", "in": "query", "name": "limit", "required": false, "schema": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" } }, { "description": "Select one or more fields to sort the response by.", + "explode": false, "in": "query", "name": "sortby", "required": false, "schema": { - "anyOf": [ - { - "items": { - "enum": [ - "name", - "type", - "code", - "-name", - "-type", - "-code" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" - } + "items": { + "enum": [ + "name", + "type", + "code", + "-name", + "-type", + "-code", + "+name", + "+type", + "+code" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" + }, + "style": "form" }, { "description": "Full-text search query. Comma-separated terms are combined with OR; whitespace-separated words within a term are combined with AND.", @@ -28695,7 +29797,7 @@ ] } }, - "/api/data/observed-properties/{observed_property_id}": { + "/api/ogc/collections/observed-properties/items/{observed_property_id}": { "delete": { "description": "Delete an Observed Property.", "operationId": "interfaces_api_views_sta_observed_property_delete_observed_property", @@ -28784,12 +29886,12 @@ }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -28802,19 +29904,19 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", + "explode": false, "in": "query", "name": "include", "required": false, "schema": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "workspace", @@ -28822,10 +29924,10 @@ ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" - } + }, + "style": "form" } ], "responses": { @@ -28975,19 +30077,56 @@ ] } }, - "/api/data/observed-property-types": { + "/api/ogc/collections/observed-property-types/items": { "get": { "description": "Get Observed Property Types.", "operationId": "interfaces_api_views_sta_observed_property_type_get_observed_property_types", "parameters": [ + { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "explode": false, + "in": "query", + "name": "bbox", + "required": false, + "schema": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], + "type": "array" + }, + "style": "form" + }, + { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "explode": false, + "in": "query", + "name": "datetime", + "required": false, + "schema": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, + "style": "form" + }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -28996,10 +30135,10 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", @@ -29040,51 +30179,40 @@ } }, { - "description": "The maximum number of items to return.", + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", "in": "query", "name": "limit", "required": false, "schema": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" } }, { "description": "Select one or more fields to sort the response by.", + "explode": false, "in": "query", "name": "sortby", "required": false, "schema": { - "anyOf": [ - { - "items": { - "enum": [ - "name", - "-name" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" - } + "items": { + "enum": [ + "name", + "-name", + "+name" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" + }, + "style": "form" }, { "description": "Full-text search query. Comma-separated terms are combined with OR; whitespace-separated words within a term are combined with AND.", @@ -29226,7 +30354,7 @@ ] } }, - "/api/data/observed-property-types/{observed_property_type_id}": { + "/api/ogc/collections/observed-property-types/items/{observed_property_type_id}": { "delete": { "description": "Delete an Observed Property Type.", "operationId": "interfaces_api_views_sta_observed_property_type_delete_observed_property_type", @@ -29304,12 +30432,12 @@ }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -29318,10 +30446,10 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" } ], "responses": { @@ -29460,19 +30588,56 @@ ] } }, - "/api/data/processing-levels": { + "/api/ogc/collections/processing-levels/items": { "get": { "description": "Get public Processing Levels and Processing Levels associated with the authenticated user.", "operationId": "interfaces_api_views_sta_processing_level_get_processing_levels", "parameters": [ + { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "explode": false, + "in": "query", + "name": "bbox", + "required": false, + "schema": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], + "type": "array" + }, + "style": "form" + }, + { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "explode": false, + "in": "query", + "name": "datetime", + "required": false, + "schema": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, + "style": "form" + }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -29484,29 +30649,29 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", + "explode": false, "in": "query", "name": "include", "required": false, "schema": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "workspace" ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" - } + }, + "style": "form" }, { "description": "Number of items to skip.", @@ -29529,53 +30694,43 @@ } }, { - "description": "The maximum number of items to return.", + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", "in": "query", "name": "limit", "required": false, "schema": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" } }, { "description": "Select one or more fields to sort the response by.", + "explode": false, "in": "query", "name": "sortby", "required": false, "schema": { - "anyOf": [ - { - "items": { - "enum": [ - "code", - "name", - "-code", - "-name" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" - } + "items": { + "enum": [ + "code", + "name", + "-code", + "-name", + "+code", + "+name" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" + }, + "style": "form" }, { "description": "Full-text search query. Comma-separated terms are combined with OR; whitespace-separated words within a term are combined with AND.", @@ -29800,7 +30955,7 @@ ] } }, - "/api/data/processing-levels/{processing_level_id}": { + "/api/ogc/collections/processing-levels/items/{processing_level_id}": { "delete": { "description": "Delete a Processing Level.", "operationId": "interfaces_api_views_sta_processing_level_delete_processing_level", @@ -29889,12 +31044,12 @@ }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -29906,29 +31061,29 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", + "explode": false, "in": "query", "name": "include", "required": false, "schema": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "workspace" ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" - } + }, + "style": "form" } ], "responses": { @@ -30078,19 +31233,56 @@ ] } }, - "/api/data/quality-control/histories": { + "/api/ogc/collections/quality-control-histories/items": { "get": { "description": "Get QC histories.", "operationId": "interfaces_api_views_quality_history_get_qc_histories", "parameters": [ + { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "explode": false, + "in": "query", + "name": "bbox", + "required": false, + "schema": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], + "type": "array" + }, + "style": "form" + }, + { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "explode": false, + "in": "query", + "name": "datetime", + "required": false, + "schema": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, + "style": "form" + }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -30104,19 +31296,19 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", + "explode": false, "in": "query", "name": "include", "required": false, "schema": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "managedDatastream", @@ -30124,10 +31316,10 @@ ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" - } + }, + "style": "form" }, { "description": "Number of items to skip.", @@ -30150,57 +31342,49 @@ } }, { - "description": "The maximum number of items to return.", + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", "in": "query", "name": "limit", "required": false, "schema": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" } }, { "description": "Select one or more fields to sort the response by.", + "explode": false, "in": "query", "name": "sortby", "required": false, "schema": { - "anyOf": [ - { - "items": { - "enum": [ - "id", - "createdAt", - "phenomenonTimeStart", - "phenomenonTimeEnd", - "-id", - "-createdAt", - "-phenomenonTimeStart", - "-phenomenonTimeEnd" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" - } + "items": { + "enum": [ + "id", + "createdAt", + "phenomenonTimeStart", + "phenomenonTimeEnd", + "-id", + "-createdAt", + "-phenomenonTimeStart", + "-phenomenonTimeEnd", + "+id", + "+createdAt", + "+phenomenonTimeStart", + "+phenomenonTimeEnd" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" + }, + "style": "form" }, { "description": "Filter histories by managed datastream ID.", @@ -30227,8 +31411,16 @@ "default": [], "description": "Filter histories by source datastream ID.", "items": { - "format": "uuid", - "type": "string" + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "const": "null", + "type": "string" + } + ] }, "title": "Source Datastream Id", "type": "array" @@ -30378,7 +31570,7 @@ ] } }, - "/api/data/quality-control/histories/{history_id}": { + "/api/ogc/collections/quality-control-histories/items/{history_id}": { "delete": { "description": "Delete a QC history and all associated sessions.", "operationId": "interfaces_api_views_quality_history_delete_qc_history", @@ -30467,12 +31659,12 @@ }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -30486,19 +31678,19 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", + "explode": false, "in": "query", "name": "include", "required": false, "schema": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "managedDatastream", @@ -30506,10 +31698,10 @@ ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" - } + }, + "style": "form" } ], "responses": { @@ -30577,7 +31769,7 @@ ] } }, - "/api/data/quality-control/histories/{history_id}/sessions": { + "/api/ogc/collections/quality-control-histories/items/{history_id}/sessions": { "get": { "description": "Get sessions for a QC history. Supports range_start/range_end overlap filtering, ancestor_of, and include_ancestors.", "operationId": "interfaces_api_views_quality_session_get_qc_sessions", @@ -30594,12 +31786,12 @@ }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -30617,10 +31809,10 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", @@ -30661,61 +31853,55 @@ } }, { - "description": "The maximum number of items to return.", + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", "in": "query", "name": "limit", "required": false, "schema": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" } }, { "description": "Select one or more fields to sort the response by.", + "explode": false, "in": "query", "name": "sortby", "required": false, "schema": { - "anyOf": [ - { - "items": { - "enum": [ - "id", - "createdAt", - "phenomenonTimeStart", - "phenomenonTimeEnd", - "status", - "committedAt", - "-id", - "-createdAt", - "-phenomenonTimeStart", - "-phenomenonTimeEnd", - "-status", - "-committedAt" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" - } + "items": { + "enum": [ + "id", + "createdAt", + "phenomenonTimeStart", + "phenomenonTimeEnd", + "status", + "committedAt", + "-id", + "-createdAt", + "-phenomenonTimeStart", + "-phenomenonTimeEnd", + "-status", + "-committedAt", + "+id", + "+createdAt", + "+phenomenonTimeStart", + "+phenomenonTimeEnd", + "+status", + "+committedAt" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" + }, + "style": "form" }, { "in": "query", @@ -30978,7 +32164,7 @@ ] } }, - "/api/data/quality-control/histories/{history_id}/sessions/{session_id}": { + "/api/ogc/collections/quality-control-histories/items/{history_id}/sessions/{session_id}": { "delete": { "description": "Delete an in-progress session.", "operationId": "interfaces_api_views_quality_session_delete_qc_session", @@ -31087,12 +32273,12 @@ }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -31110,10 +32296,10 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" } ], "responses": { @@ -31284,7 +32470,7 @@ ] } }, - "/api/data/quality-control/histories/{history_id}/sessions/{session_id}/commit": { + "/api/ogc/collections/quality-control-histories/items/{history_id}/sessions/{session_id}/commit": { "post": { "description": "Commit an in-progress session after observations have been pushed to the managed datastream.", "operationId": "interfaces_api_views_quality_session_commit_qc_session", @@ -31379,7 +32565,7 @@ ] } }, - "/api/data/quality-control/histories/{history_id}/sessions/{session_id}/operations": { + "/api/ogc/collections/quality-control-histories/items/{history_id}/sessions/{session_id}/operations": { "get": { "description": "Get all operations for a QC session in execution order.", "operationId": "interfaces_api_views_quality_operation_get_qc_operations", @@ -31406,12 +32592,12 @@ }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -31424,10 +32610,10 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", @@ -31468,57 +32654,49 @@ } }, { - "description": "The maximum number of items to return.", + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", "in": "query", "name": "limit", "required": false, "schema": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" } }, { "description": "Select one or more fields to sort the response by.", + "explode": false, "in": "query", "name": "sortby", "required": false, "schema": { - "anyOf": [ - { - "items": { - "enum": [ - "id", - "order", - "operationType", - "createdAt", - "-id", - "-order", - "-operationType", - "-createdAt" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" - } + "items": { + "enum": [ + "id", + "order", + "operationType", + "createdAt", + "-id", + "-order", + "-operationType", + "-createdAt", + "+id", + "+order", + "+operationType", + "+createdAt" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" + }, + "style": "form" } ], "responses": { @@ -31704,7 +32882,7 @@ ] } }, - "/api/data/quality-control/histories/{history_id}/sessions/{session_id}/operations/{operation_id}": { + "/api/ogc/collections/quality-control-histories/items/{history_id}/sessions/{session_id}/operations/{operation_id}": { "delete": { "description": "Delete an operation from an in-progress session.", "operationId": "interfaces_api_views_quality_operation_delete_qc_operation", @@ -31833,12 +33011,12 @@ }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -31851,10 +33029,10 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" } ], "responses": { @@ -32035,19 +33213,56 @@ ] } }, - "/api/data/result-qualifiers": { + "/api/ogc/collections/result-qualifiers/items": { "get": { "description": "Get public Result Qualifiers and Result Qualifiers associated with the authenticated user.", "operationId": "interfaces_api_views_sta_result_qualifier_get_result_qualifiers", "parameters": [ + { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "explode": false, + "in": "query", + "name": "bbox", + "required": false, + "schema": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], + "type": "array" + }, + "style": "form" + }, + { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "explode": false, + "in": "query", + "name": "datetime", + "required": false, + "schema": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, + "style": "form" + }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -32057,29 +33272,29 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", + "explode": false, "in": "query", "name": "include", "required": false, "schema": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "workspace" ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" - } + }, + "style": "form" }, { "description": "Number of items to skip.", @@ -32102,51 +33317,40 @@ } }, { - "description": "The maximum number of items to return.", + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", "in": "query", "name": "limit", "required": false, "schema": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" } }, { "description": "Select one or more fields to sort the response by.", + "explode": false, "in": "query", "name": "sortby", "required": false, "schema": { - "anyOf": [ - { - "items": { - "enum": [ - "name", - "-name" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" - } + "items": { + "enum": [ + "name", + "-name", + "+name" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" + }, + "style": "form" }, { "description": "Full-text search query. Comma-separated terms are combined with OR; whitespace-separated words within a term are combined with AND.", @@ -32312,7 +33516,7 @@ ] } }, - "/api/data/result-qualifiers/{result_qualifier_id}": { + "/api/ogc/collections/result-qualifiers/items/{result_qualifier_id}": { "delete": { "description": "Delete a Result Qualifier.", "operationId": "interfaces_api_views_sta_result_qualifier_delete_result_qualifier", @@ -32390,12 +33594,12 @@ }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -32405,29 +33609,29 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", + "explode": false, "in": "query", "name": "include", "required": false, "schema": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "workspace" ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" - } + }, + "style": "form" } ], "responses": { @@ -32566,19 +33770,56 @@ ] } }, - "/api/data/roles": { + "/api/ogc/collections/roles/items": { "get": { "description": "Get public Roles and Roles associated with the authenticated user.", "operationId": "interfaces_api_views_iam_role_get_roles", "parameters": [ + { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "explode": false, + "in": "query", + "name": "bbox", + "required": false, + "schema": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], + "type": "array" + }, + "style": "form" + }, + { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "explode": false, + "in": "query", + "name": "datetime", + "required": false, + "schema": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, + "style": "form" + }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -32589,10 +33830,10 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", @@ -32633,51 +33874,40 @@ } }, { - "description": "The maximum number of items to return.", + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", "in": "query", "name": "limit", "required": false, "schema": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" } }, { "description": "Select one or more fields to sort the response by.", + "explode": false, "in": "query", "name": "sortby", "required": false, "schema": { - "anyOf": [ - { - "items": { - "enum": [ - "name", - "-name" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" - } + "items": { + "enum": [ + "name", + "-name", + "+name" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" + }, + "style": "form" }, { "description": "Filter roles by workspace ID.", @@ -32747,7 +33977,7 @@ ] } }, - "/api/data/roles/{role_id}": { + "/api/ogc/collections/roles/items/{role_id}": { "get": { "description": "Get a Role.", "operationId": "interfaces_api_views_iam_role_get_role", @@ -32764,12 +33994,12 @@ }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -32780,10 +34010,10 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" } ], "responses": { @@ -32840,19 +34070,56 @@ ] } }, - "/api/data/sampled-mediums": { + "/api/ogc/collections/sampled-mediums/items": { "get": { "description": "Get Sampled Mediums.", "operationId": "interfaces_api_views_sta_sampled_medium_get_sampled_mediums", "parameters": [ + { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "explode": false, + "in": "query", + "name": "bbox", + "required": false, + "schema": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], + "type": "array" + }, + "style": "form" + }, + { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "explode": false, + "in": "query", + "name": "datetime", + "required": false, + "schema": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, + "style": "form" + }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -32861,10 +34128,10 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", @@ -32905,51 +34172,40 @@ } }, { - "description": "The maximum number of items to return.", + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", "in": "query", "name": "limit", "required": false, "schema": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" } }, { "description": "Select one or more fields to sort the response by.", + "explode": false, "in": "query", "name": "sortby", "required": false, "schema": { - "anyOf": [ - { - "items": { - "enum": [ - "name", - "-name" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" - } + "items": { + "enum": [ + "name", + "-name", + "+name" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" + }, + "style": "form" }, { "description": "Full-text search query. Comma-separated terms are combined with OR; whitespace-separated words within a term are combined with AND.", @@ -33091,7 +34347,7 @@ ] } }, - "/api/data/sampled-mediums/{sampled_medium_id}": { + "/api/ogc/collections/sampled-mediums/items/{sampled_medium_id}": { "delete": { "description": "Delete a Sampled Medium.", "operationId": "interfaces_api_views_sta_sampled_medium_delete_sampled_medium", @@ -33169,12 +34425,12 @@ }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -33183,10 +34439,10 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" } ], "responses": { @@ -33325,19 +34581,56 @@ ] } }, - "/api/data/unit-types": { + "/api/ogc/collections/unit-types/items": { "get": { "description": "Get Unit Types.", "operationId": "interfaces_api_views_sta_unit_type_get_unit_types", "parameters": [ + { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "explode": false, + "in": "query", + "name": "bbox", + "required": false, + "schema": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], + "type": "array" + }, + "style": "form" + }, + { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "explode": false, + "in": "query", + "name": "datetime", + "required": false, + "schema": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, + "style": "form" + }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -33346,10 +34639,10 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", @@ -33390,51 +34683,40 @@ } }, { - "description": "The maximum number of items to return.", + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", "in": "query", "name": "limit", "required": false, "schema": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" } }, { "description": "Select one or more fields to sort the response by.", + "explode": false, "in": "query", "name": "sortby", "required": false, "schema": { - "anyOf": [ - { - "items": { - "enum": [ - "name", - "-name" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" - } + "items": { + "enum": [ + "name", + "-name", + "+name" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" + }, + "style": "form" }, { "description": "Full-text search query. Comma-separated terms are combined with OR; whitespace-separated words within a term are combined with AND.", @@ -33576,7 +34858,7 @@ ] } }, - "/api/data/unit-types/{unit_type_id}": { + "/api/ogc/collections/unit-types/items/{unit_type_id}": { "delete": { "description": "Delete a Unit Type.", "operationId": "interfaces_api_views_sta_unit_type_delete_unit_type", @@ -33654,12 +34936,12 @@ }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -33668,10 +34950,10 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" } ], "responses": { @@ -33810,19 +35092,56 @@ ] } }, - "/api/data/units": { + "/api/ogc/collections/units/items": { "get": { "description": "Get public Units and Units associated with the authenticated user.", "operationId": "interfaces_api_views_sta_unit_get_units", "parameters": [ + { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "explode": false, + "in": "query", + "name": "bbox", + "required": false, + "schema": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], + "type": "array" + }, + "style": "form" + }, + { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "explode": false, + "in": "query", + "name": "datetime", + "required": false, + "schema": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, + "style": "form" + }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -33834,19 +35153,19 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", + "explode": false, "in": "query", "name": "include", "required": false, "schema": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "workspace", @@ -33854,10 +35173,10 @@ ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" - } + }, + "style": "form" }, { "description": "Number of items to skip.", @@ -33880,55 +35199,46 @@ } }, { - "description": "The maximum number of items to return.", + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", "in": "query", "name": "limit", "required": false, "schema": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" } }, { "description": "Select one or more fields to sort the response by.", + "explode": false, "in": "query", "name": "sortby", "required": false, "schema": { - "anyOf": [ - { - "items": { - "enum": [ - "name", - "symbol", - "type", - "-name", - "-symbol", - "-type" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" - } + "items": { + "enum": [ + "name", + "symbol", + "type", + "-name", + "-symbol", + "-type", + "+name", + "+symbol", + "+type" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" + }, + "style": "form" }, { "description": "Full-text search query. Comma-separated terms are combined with OR; whitespace-separated words within a term are combined with AND.", @@ -34157,7 +35467,7 @@ ] } }, - "/api/data/units/{unit_id}": { + "/api/ogc/collections/units/items/{unit_id}": { "delete": { "description": "Delete a Unit.", "operationId": "interfaces_api_views_sta_unit_delete_unit", @@ -34246,12 +35556,12 @@ }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -34263,19 +35573,19 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", + "explode": false, "in": "query", "name": "include", "required": false, "schema": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "workspace", @@ -34283,10 +35593,10 @@ ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" - } + }, + "style": "form" } ], "responses": { @@ -34436,19 +35746,56 @@ ] } }, - "/api/data/workspaces": { + "/api/ogc/collections/workspaces/items": { "get": { "description": "Get public workspaces and workspaces associated with the authenticated user.", "operationId": "interfaces_api_views_iam_workspace_get_workspaces", "parameters": [ + { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "explode": false, + "in": "query", + "name": "bbox", + "required": false, + "schema": { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "items": { + "type": "number" + }, + "oneOf": [ + { + "maxItems": 4, + "minItems": 4 + }, + { + "maxItems": 6, + "minItems": 6 + } + ], + "type": "array" + }, + "style": "form" + }, + { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "explode": false, + "in": "query", + "name": "datetime", + "required": false, + "schema": { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime.", + "type": "string" + }, + "style": "form" + }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -34460,19 +35807,19 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", + "explode": false, "in": "query", "name": "include", "required": false, "schema": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "owner", @@ -34481,10 +35828,10 @@ ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" - } + }, + "style": "form" }, { "description": "Number of items to skip.", @@ -34507,53 +35854,43 @@ } }, { - "description": "The maximum number of items to return.", + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", "in": "query", "name": "limit", "required": false, "schema": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" } }, { "description": "Select one or more fields to sort the response by.", + "explode": false, "in": "query", "name": "sortby", "required": false, "schema": { - "anyOf": [ - { - "items": { - "enum": [ - "name", - "isPrivate", - "-name", - "-isPrivate" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" - } + "items": { + "enum": [ + "name", + "isPrivate", + "-name", + "-isPrivate", + "+name", + "+isPrivate" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" + }, + "style": "form" }, { "description": "Full-text search query. Comma-separated terms are combined with OR; whitespace-separated words within a term are combined with AND.", @@ -34731,7 +36068,7 @@ ] } }, - "/api/data/workspaces/{workspace_id}": { + "/api/ogc/collections/workspaces/items/{workspace_id}": { "delete": { "description": "Delete a workspace owned by the authenticated user.", "operationId": "interfaces_api_views_iam_workspace_delete_workspace", @@ -34806,12 +36143,12 @@ }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -34823,19 +36160,19 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", + "explode": false, "in": "query", "name": "include", "required": false, "schema": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "owner", @@ -34844,10 +36181,10 @@ ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" - } + }, + "style": "form" } ], "responses": { @@ -34994,7 +36331,7 @@ ] } }, - "/api/data/workspaces/{workspace_id}/collaborators": { + "/api/ogc/collections/workspaces/items/{workspace_id}/collaborators": { "delete": { "description": "Remove a collaborator from a workspace.", "operationId": "interfaces_api_views_iam_collaborator_remove_collaborator", @@ -35090,12 +36427,12 @@ }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "roleId", @@ -35104,19 +36441,19 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", + "explode": false, "in": "query", "name": "include", "required": false, "schema": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "role", @@ -35125,10 +36462,10 @@ ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" - } + }, + "style": "form" }, { "description": "Number of items to skip.", @@ -35151,24 +36488,17 @@ } }, { - "description": "The maximum number of items to return.", + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", "in": "query", "name": "limit", "required": false, "schema": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" } }, { @@ -35189,31 +36519,28 @@ }, { "description": "Select one or more fields to sort the response by.", + "explode": false, "in": "query", "name": "sortby", "required": false, "schema": { - "anyOf": [ - { - "items": { - "enum": [ - "id", - "roleId", - "-id", - "-roleId" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" - } + "items": { + "enum": [ + "id", + "roleId", + "-id", + "-roleId", + "+id", + "+roleId" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" + }, + "style": "form" } ], "responses": { @@ -35435,7 +36762,7 @@ ] } }, - "/api/data/workspaces/{workspace_id}/service-accounts": { + "/api/ogc/collections/workspaces/items/{workspace_id}/service-accounts": { "get": { "description": "Get service accounts associated with the authenticated user.", "operationId": "interfaces_api_views_iam_service_account_get_service_accounts", @@ -35452,12 +36779,12 @@ }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -35472,29 +36799,29 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", + "explode": false, "in": "query", "name": "include", "required": false, "schema": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "workspace" ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" - } + }, + "style": "form" }, { "description": "Number of items to skip.", @@ -35517,59 +36844,52 @@ } }, { - "description": "The maximum number of items to return.", + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", "in": "query", "name": "limit", "required": false, "schema": { - "anyOf": [ - { - "maximum": 100000, - "minimum": 0, - "type": "integer" - }, - { - "type": "null" - } - ], "default": 100, - "description": "The maximum number of items to return.", - "title": "Limit" + "description": "The maximum number of items to return. (default: 100, maximum: 100000)", + "maximum": 100000, + "minimum": 0, + "title": "Limit", + "type": "integer" } }, { "description": "Select one or more fields to sort the response by.", + "explode": false, "in": "query", "name": "sortby", "required": false, "schema": { - "anyOf": [ - { - "items": { - "enum": [ - "name", - "isActive", - "keyExpiresAt", - "createdAt", - "lastUsedAt", - "-name", - "-isActive", - "-keyExpiresAt", - "-createdAt", - "-lastUsedAt" - ], - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], "default": [], "description": "Select one or more fields to sort the response by.", - "title": "Sortby" - } + "items": { + "enum": [ + "name", + "isActive", + "keyExpiresAt", + "createdAt", + "lastUsedAt", + "-name", + "-isActive", + "-keyExpiresAt", + "-createdAt", + "-lastUsedAt", + "+name", + "+isActive", + "+keyExpiresAt", + "+createdAt", + "+lastUsedAt" + ], + "type": "string" + }, + "title": "Sortby", + "type": "array" + }, + "style": "form" } ], "responses": { @@ -35682,7 +37002,7 @@ ] } }, - "/api/data/workspaces/{workspace_id}/service-accounts/{service_account_id}": { + "/api/ogc/collections/workspaces/items/{workspace_id}/service-accounts/{service_account_id}": { "delete": { "description": "Delete a service account.", "operationId": "interfaces_api_views_iam_service_account_delete_service_account", @@ -35774,12 +37094,12 @@ }, { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", + "explode": false, "in": "query", "name": "properties", "required": false, "schema": { "description": "Comma-separated list of properties to include in the response. All properties are returned if omitted.", - "explode": false, "items": { "enum": [ "id", @@ -35794,29 +37114,29 @@ ], "type": "string" }, - "style": "form", "title": "Properties", "type": "array" - } + }, + "style": "form" }, { "description": "Comma-separated list of related resources to include in the response.", + "explode": false, "in": "query", "name": "include", "required": false, "schema": { "description": "Comma-separated list of related resources to include in the response.", - "explode": false, "items": { "enum": [ "workspace" ], "type": "string" }, - "style": "form", "title": "Include", "type": "array" - } + }, + "style": "form" } ], "responses": { @@ -35942,7 +37262,7 @@ ] } }, - "/api/data/workspaces/{workspace_id}/service-accounts/{service_account_id}/regenerate": { + "/api/ogc/collections/workspaces/items/{workspace_id}/service-accounts/{service_account_id}/regenerate": { "put": { "description": "Regenerate a service account's key using existing settings.", "operationId": "interfaces_api_views_iam_service_account_regenerate_service_account_key", @@ -36027,7 +37347,7 @@ ] } }, - "/api/data/workspaces/{workspace_id}/transfer": { + "/api/ogc/collections/workspaces/items/{workspace_id}/transfer": { "delete": { "description": "Reject a pending workspace transfer.", "operationId": "interfaces_api_views_iam_workspace_reject_workspace_transfer", @@ -36269,6 +37589,101 @@ "Workspaces" ] } + }, + "/api/ogc/collections/{collection_id}": { + "get": { + "description": "Get a collection.", + "operationId": "interfaces_api_views_ogc_get_collection_metadata", + "parameters": [ + { + "in": "path", + "name": "collection_id", + "required": true, + "schema": { + "title": "Collection Id", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CollectionResponse" + } + } + }, + "description": "OK" + }, + "404": { + "content": { + "application/json": { + "schema": { + "title": "Response", + "type": "string" + } + } + }, + "description": "Not Found" + } + }, + "security": [ + { + "SessionAuth": [] + }, + { + "OIDCAuth": [] + }, + { + "APIKeyAuth": [] + }, + { + "BasicAuth": [] + } + ], + "summary": "Get Collection Metadata", + "tags": [ + "Collections" + ] + } + }, + "/api/ogc/conformance": { + "get": { + "description": "Get the OGC API conformance classes the API implements.", + "operationId": "interfaces_api_views_ogc_get_conformance", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConformanceResponse" + } + } + }, + "description": "OK" + } + }, + "security": [ + { + "SessionAuth": [] + }, + { + "OIDCAuth": [] + }, + { + "APIKeyAuth": [] + }, + { + "BasicAuth": [] + } + ], + "summary": "Get Conformance", + "tags": [ + "Capabilities" + ] + } } }, "servers": [] diff --git a/django/interfaces/actions/management/commands/export_openapi_contracts.py b/django/interfaces/actions/management/commands/export_openapi_contracts.py index f4896c0a..7edcd2c8 100644 --- a/django/interfaces/actions/management/commands/export_openapi_contracts.py +++ b/django/interfaces/actions/management/commands/export_openapi_contracts.py @@ -8,7 +8,7 @@ SCHEMA_TARGETS = { - "data.openapi.json": (api, "/api/data/"), + "ogc.openapi.json": (api, "/api/ogc/"), } diff --git a/django/interfaces/api/collections.py b/django/interfaces/api/collections.py new file mode 100644 index 00000000..59f21f95 --- /dev/null +++ b/django/interfaces/api/collections.py @@ -0,0 +1,199 @@ +from dataclasses import dataclass +from typing import Optional + + +@dataclass(frozen=True) +class CollectionDefinition: + """ + An OGC API collection, served at /collections/{id} with its items at /collections/{id}/items. + + The router is an import path rather than a Router, so modules the views import (such as the + response schemas) can use the registry without a circular import. + """ + + id: str + title: str + description: str + router: str + item_type: Optional[str] = None + + +VIEWS = "interfaces.api.views" + +COLLECTIONS: tuple[CollectionDefinition, ...] = ( + CollectionDefinition( + id="workspaces", + title="Workspaces", + description="Workspaces that own monitoring sites, datastreams, and other data, and " + "control who can access them.", + router=f"{VIEWS}.iam.workspace.workspace_router", + ), + CollectionDefinition( + id="roles", + title="Roles", + description="Roles that grant collaborators and service accounts permissions within " + "a workspace.", + router=f"{VIEWS}.iam.role.role_router", + ), + CollectionDefinition( + id="monitoring-sites", + title="Monitoring Sites", + description="Locations where observations are collected, with WGS 84 coordinates.", + router=f"{VIEWS}.sta.monitoring_site.monitoring_site_router", + item_type="feature", + ), + CollectionDefinition( + id="monitoring-site-types", + title="Monitoring Site Types", + description="Controlled vocabulary of monitoring site types.", + router=f"{VIEWS}.sta.monitoring_site_type.monitoring_site_type_router", + ), + CollectionDefinition( + id="linked-resource-types", + title="Linked Resource Types", + description="Controlled vocabulary of types for files and links attached to monitoring " + "sites and datastreams.", + router=f"{VIEWS}.sta.linked_resource_type.linked_resource_type_router", + ), + CollectionDefinition( + id="datastreams", + title="Datastreams", + description="Time series of observations of one observed property at a monitoring site.", + router=f"{VIEWS}.sta.datastream.datastream_router", + item_type="feature", + ), + CollectionDefinition( + id="datastream-statuses", + title="Datastream Statuses", + description="Controlled vocabulary of datastream statuses.", + router=f"{VIEWS}.sta.datastream_status.datastream_status_router", + ), + CollectionDefinition( + id="aggregation-statistics", + title="Aggregation Statistics", + description="Controlled vocabulary of statistics used to aggregate datastream results.", + router=f"{VIEWS}.sta.aggregation_statistic.aggregation_statistic_router", + ), + CollectionDefinition( + id="observations", + title="Observations", + description="Individual timestamped results recorded in datastreams.", + router=f"{VIEWS}.sta.observation.observation_router", + item_type="feature", + ), + CollectionDefinition( + id="observed-properties", + title="Observed Properties", + description="Phenomena that datastreams observe, such as discharge or water temperature.", + router=f"{VIEWS}.sta.observed_property.observed_property_router", + ), + CollectionDefinition( + id="observed-property-types", + title="Observed Property Types", + description="Controlled vocabulary of observed property types.", + router=f"{VIEWS}.sta.observed_property_type.observed_property_type_router", + ), + CollectionDefinition( + id="units", + title="Units", + description="Units of measure for datastream results.", + router=f"{VIEWS}.sta.unit.unit_router", + ), + CollectionDefinition( + id="unit-types", + title="Unit Types", + description="Controlled vocabulary of unit types.", + router=f"{VIEWS}.sta.unit_type.unit_type_router", + ), + CollectionDefinition( + id="methods", + title="Methods", + description="Sensors and procedures used to produce datastream results.", + router=f"{VIEWS}.sta.method.method_router", + ), + CollectionDefinition( + id="method-types", + title="Method Types", + description="Controlled vocabulary of method types.", + router=f"{VIEWS}.sta.method_type.method_type_router", + ), + CollectionDefinition( + id="processing-levels", + title="Processing Levels", + description="Levels of quality control and processing applied to datastream results.", + router=f"{VIEWS}.sta.processing_level.processing_level_router", + ), + CollectionDefinition( + id="result-qualifiers", + title="Result Qualifiers", + description="Codes that qualify individual observation results.", + router=f"{VIEWS}.sta.result_qualifier.result_qualifier_router", + ), + CollectionDefinition( + id="sampled-mediums", + title="Sampled Mediums", + description="Controlled vocabulary of the media that datastreams sample, such as surface " + "water or air.", + router=f"{VIEWS}.sta.sampled_medium.sampled_medium_router", + ), + CollectionDefinition( + id="etl-data-connections", + title="ETL Data Connections", + description="Sources that ETL tasks extract data from.", + router=f"{VIEWS}.etl.data_connection.data_connection_router", + ), + CollectionDefinition( + id="etl-tasks", + title="ETL Tasks", + description="Scheduled jobs that load observations from data connections into datastreams.", + router=f"{VIEWS}.etl.task.etl_task_router", + ), + CollectionDefinition( + id="etl-mappings", + title="ETL Mappings", + description="Mappings from source identifiers in extracted data to target datastreams.", + router=f"{VIEWS}.etl.mapping.etl_mapping_router", + ), + CollectionDefinition( + id="data-product-rating-curves", + title="Data Product Rating Curves", + description="Rating curves used by data product transformations.", + router=f"{VIEWS}.products.rating_curve.rating_curve_router", + ), + CollectionDefinition( + id="data-product-tasks", + title="Data Product Tasks", + description="Scheduled jobs that derive datastreams from other datastreams.", + router=f"{VIEWS}.products.task.data_product_task_router", + ), + CollectionDefinition( + id="data-product-transformations", + title="Data Product Transformations", + description="Transformations that data product tasks apply to input datastreams.", + router=f"{VIEWS}.products.transformation.data_product_transformation_router", + ), + CollectionDefinition( + id="monitoring-tasks", + title="Monitoring Tasks", + description="Scheduled jobs that check datastreams against monitoring rules.", + router=f"{VIEWS}.monitoring.task.monitoring_task_router", + ), + CollectionDefinition( + id="monitoring-rules", + title="Monitoring Rules", + description="Conditions that monitoring tasks check datastreams against.", + router=f"{VIEWS}.monitoring.rule.monitoring_rule_router", + ), + CollectionDefinition( + id="quality-control-histories", + title="Quality Control Histories", + description="Records of quality control sessions applied to datastreams.", + router=f"{VIEWS}.quality.history.qc_history_router", + ), +) + +_COLLECTIONS_BY_ID = {collection.id: collection for collection in COLLECTIONS} + + +def get_collection(collection_id: str) -> Optional[CollectionDefinition]: + return _COLLECTIONS_BY_ID.get(collection_id) diff --git a/django/interfaces/api/http/api.py b/django/interfaces/api/http/api.py new file mode 100644 index 00000000..3a48b419 --- /dev/null +++ b/django/interfaces/api/http/api.py @@ -0,0 +1,35 @@ +from typing import Optional +from ninja import NinjaAPI +from ninja.openapi.schema import OpenAPISchema +from ninja.types import DictStrAny + +PARAMETER_SERIALIZATION_KEYS = ("style", "explode") + + +class HydroServerNinjaAPI(NinjaAPI): + """ + NinjaAPI that moves parameter serialization keywords (style, explode) from a query + parameter's JSON schema onto its OpenAPI Parameter Object, where OpenAPI reads them. + + Ninja copies a parameter's JSON schema into the Parameter Object's schema as-is, so + keywords declared there (e.g., by comma_array_schema) would otherwise be ignored and + clients would fall back to the default explode=true serialization for arrays. + """ + + def get_openapi_schema( + self, + *, + path_prefix: Optional[str] = None, + path_params: Optional[DictStrAny] = None, + ) -> OpenAPISchema: + schema = super().get_openapi_schema(path_prefix=path_prefix, path_params=path_params) + + for path_item in schema.get("paths", {}).values(): + for operation in path_item.values(): + for parameter in operation.get("parameters", []): + parameter_schema = parameter.get("schema", {}) + for key in PARAMETER_SERIALIZATION_KEYS: + if key in parameter_schema: + parameter[key] = parameter_schema.pop(key) + + return schema diff --git a/django/interfaces/api/http/links.py b/django/interfaces/api/http/links.py new file mode 100644 index 00000000..14d8b7bb --- /dev/null +++ b/django/interfaces/api/http/links.py @@ -0,0 +1,110 @@ +import re + +from typing import Optional + +from django.conf import settings +from django.http import HttpRequest +from django.urls import get_script_prefix, reverse +from ninja import Schema +from pydantic import SerializationInfo + +from interfaces.api.collections import get_collection + +JSON_MEDIA_TYPE = "application/json" +COLLECTION_ITEM_PATH = re.compile(r"collections/(?P[^/]+)/items/[^/]+") + + +class Link(Schema): + href: str + rel: str + type: str + title: Optional[str] = None + + +def api_root_path() -> str: + """The OGC API root relative to Django's root, e.g., 'api/ogc/'.""" + + return route_path("ogc:api-root") + + +def route_path(route_name: str) -> str: + """A named route's path relative to Django's root, e.g., 'api/ogc/openapi.json'.""" + + return reverse(route_name).removeprefix(get_script_prefix()) + + +def build_absolute_url(path: str, query_string: str = "") -> str: + """Builds an absolute URL for a path relative to Django's root from PROXY_BASE_URL""" + + url = settings.PROXY_BASE_URL.rstrip("/") + "/" + path.lstrip("/") + + return f"{url}?{query_string}" if query_string else url + + +def build_api_url(path: str = "") -> str: + """Builds an absolute URL for a path relative to the OGC API root, e.g. 'collections/units'.""" + + return build_absolute_url(api_root_path() + path.lstrip("/")) + + +def build_self_link(request: HttpRequest, path: Optional[str] = None) -> Link: + """Builds a link to this response document.""" + + href = ( + build_absolute_url(path) + if path + else build_absolute_url(request.path_info, request.META.get("QUERY_STRING", "")) + ) + + return Link(href=href, rel="self", type=JSON_MEDIA_TYPE) + + +def build_collection_link(request: HttpRequest) -> Optional[Link]: + """Builds a link to the collection that contains the requested item.""" + + path = request.path_info.lstrip("/").removeprefix(api_root_path()) + match = COLLECTION_ITEM_PATH.fullmatch(path) + + if match is None or get_collection(match["collection_id"]) is None: + return None + + return Link( + href=build_api_url(f"collections/{match['collection_id']}"), + rel="collection", + type=JSON_MEDIA_TYPE, + ) + + +def build_page_link(request: HttpRequest, rel: str, offset: int, limit: int) -> Link: + """Builds a pagination link with the specified parameters.""" + + query = request.GET.copy() + query["offset"] = str(offset) + query["limit"] = str(limit) + + return Link( + href=build_absolute_url(request.path_info, query.urlencode()), rel=rel, type=JSON_MEDIA_TYPE + ) + + +def build_page_links(request: HttpRequest, offset: int, limit: int, returned: int) -> list[Link]: + """Returns the self-link plus next and prev links for a page of results.""" + + links = [build_self_link(request)] + + if limit <= 0: + return links + + if returned >= limit: + links.append(build_page_link(request, "next", offset + limit, limit)) + + if offset > 0: + links.append(build_page_link(request, "prev", max(0, offset - limit), limit)) + + return links + + +def get_request(info: SerializationInfo) -> Optional[HttpRequest]: + """Returns the request Ninja passes in the serialization context, if any.""" + + return (info.context or {}).get("request") if info.context else None diff --git a/django/interfaces/api/http/query_params.py b/django/interfaces/api/http/query_params.py new file mode 100644 index 00000000..e4d1a413 --- /dev/null +++ b/django/interfaces/api/http/query_params.py @@ -0,0 +1,62 @@ +from functools import wraps +from typing import Callable + +from django.http import HttpRequest, HttpResponseBase +from ninja.operation import Operation + + +def declared_query_params(operation: Operation) -> frozenset[str]: + """ + Returns the query parameter names an operation declares, as they appear in the request URI + and in the OpenAPI document (aliases such as 'format', not the Python field names). + """ + + names: set[str] = set() + + for model in operation.models: + if getattr(model, "__ninja_param_source__", None) != "query": + continue + + flatten_map = getattr(model, "__ninja_flatten_map__", None) + if flatten_map is None: + raise TypeError( + f"Cannot determine the query parameters of '{operation.view_func.__name__}': " + f"{model.__name__} has no flatten map." + ) + + names.update(flatten_map) + + return frozenset(names) + + +def reject_unknown_query_params(run: Callable[..., HttpResponseBase]) -> Callable[..., HttpResponseBase]: + """ + Ninja view-mode decorator that responds 400 to requests with query parameters the operation + doesn't declare (OGC API - Features Core Req 8, /req/core/query-param-unknown). + + Ninja ignores undeclared query parameters, so without this a misspelled filter such as + '?workspaceId=' silently returns unfiltered results. View-mode decorators wrap the bound + Operation.run, which is how the operation's declared parameters are found. + """ + + operation = getattr(run, "__self__", None) + if not isinstance(operation, Operation): + raise TypeError( + "reject_unknown_query_params must be registered with mode='view' before any other " + "view decorator, so it wraps Operation.run directly." + ) + + allowed = declared_query_params(operation) + + @wraps(run) + def wrapper(request: HttpRequest, **kwargs) -> HttpResponseBase: + unknown = sorted(set(request.GET) - allowed) + + if unknown: + message = f"Unknown query parameter(s): {', '.join(unknown)}." + message += f" Allowed: {', '.join(sorted(allowed))}." if allowed else " This operation accepts none." + return operation.api.create_response(request, {"message": message}, status=400) + + return run(request, **kwargs) + + return wrapper diff --git a/django/interfaces/api/schemas/__init__.py b/django/interfaces/api/schemas/__init__.py index ce502ba6..bab38398 100644 --- a/django/interfaces/api/schemas/__init__.py +++ b/django/interfaces/api/schemas/__init__.py @@ -1,6 +1,8 @@ -from .base import (BaseGetResponse, BasePostBody, BasePatchBody, BaseQueryParameters, CollectionQueryParameters, - PaginationMeta, PaginatedResponse, - CreatedResponse, ItemResponse, split_comma_separated, comma_array_schema) +from .base import (BaseGetResponse, BasePostBody, BasePatchBody, BaseQueryParameters, PaginatedQueryParameters, + CollectionQueryParameters, PaginationMeta, PaginatedResponse, CreatedResponse, ItemResponse, + split_comma_separated, comma_array_schema, split_sortby, sortby_array_schema, QueryBool) +from .extent import (BoundingBox, BoundingBoxQuery, DatetimeQuery, ExtentQueryParameters, TimeInterval, + parse_bbox, parse_datetime) from interfaces.api.schemas.iam.user import UserContactResponse from interfaces.api.schemas.iam.role import (RoleResponse, RoleQueryParameters, RoleItemQueryParameters, RoleSortByFields) diff --git a/django/interfaces/api/schemas/base.py b/django/interfaces/api/schemas/base.py index ffea6261..de526705 100644 --- a/django/interfaces/api/schemas/base.py +++ b/django/interfaces/api/schemas/base.py @@ -1,19 +1,35 @@ import copy import uuid -from typing import Optional, Any, Union, Annotated, Generic, TypeVar, get_args +from typing import Optional, Any, Union, Annotated, Generic, Literal, TypeVar, get_args, get_origin from ninja import Schema, Query from pydantic.alias_generators import to_camel from pydantic import ( + AfterValidator, AliasGenerator, AliasChoices, + BeforeValidator, ConfigDict, + ValidationInfo, field_validator, model_serializer, SerializationInfo, + WithJsonSchema, ) from core.types import Unset +from interfaces.api.http.links import ( + Link, + build_collection_link, + build_page_links, + build_self_link, + get_request, +) + +MAX_LIMIT = 100000 +NULL_QUERY_VALUE = "null" + +T = TypeVar("T") base_alias_generator = AliasGenerator( serialization_alias=lambda field_name: to_camel(field_name), @@ -42,6 +58,53 @@ def split_comma_separated(value: Any) -> Optional[list[str]]: return result +def split_sortby(value: Any) -> Optional[list[str]]: + """ + Splits sortby values like split_comma_separated and drops a leading '+', which means + ascending, the same as no prefix (OGC API - Features Part 8 Req 5). An unencoded '+' arrives + as a space, which the split already trims. + """ + + fields = split_comma_separated(value) + + if fields is None: + return None + + return [ + field[1:] if field.startswith("+") and field[1:2] not in ("+", "-") else field + for field in fields + ] + + +def parse_query_bool(value: Any) -> Any: + """Accepts only the lowercase strings 'true' and 'false' for a boolean query parameter.""" + + if isinstance(value, bool): + return value + + if value == "true": + return True + if value == "false": + return False + + raise ValueError("Boolean query parameters must be 'true' or 'false'") + + +def clamp_limit(value: Optional[int]) -> Optional[int]: + """Caps a page size at MAX_LIMIT.""" + + return value if value is None else min(value, MAX_LIMIT) + + +def accepts_null(annotation: Any) -> bool: + """Returns whether a query parameter's type admits the value 'null'.""" + + if get_origin(annotation) is Literal: + return NULL_QUERY_VALUE in get_args(annotation) + + return any(accepts_null(arg) for arg in get_args(annotation)) + + def comma_array_schema(literal_type: Any) -> dict: """ Generates a JSON schema for an array of strings based on a given Python Literal type. @@ -59,6 +122,158 @@ def comma_array_schema(literal_type: Any) -> dict: } +def sortby_array_schema(literal_type: Any) -> dict: + """ + Generates the JSON schema for a comma-separated sortby parameter. The enum lists each field + in the given Literal type, which holds 'field' and '-field', plus '+field' for ascending. + """ + + schema = comma_array_schema(literal_type) + fields = schema["items"]["enum"] + schema["items"]["enum"] = [ + *fields, + *[f"+{field}" for field in fields if not field.startswith("-")], + ] + + return schema + + +QueryBool = Annotated[ + bool, + BeforeValidator(parse_query_bool), + WithJsonSchema({"type": "boolean"}), +] + +LimitQuery = Annotated[ + Optional[int], + AfterValidator(clamp_limit), + WithJsonSchema({"type": "integer", "minimum": 0, "maximum": MAX_LIMIT}), +] + + +def properties_query(): + return Query( + None, + description="Comma-separated list of properties to include in the response. " + "All properties are returned if omitted.", + ) + + +def offset_query(): + return Query(0, ge=0, description="Number of items to skip.") + + +def limit_query(): + return Query( + 100, + ge=0, + description=f"The maximum number of items to return. (default: 100, maximum: {MAX_LIMIT})", + ) + + +class BaseQueryParameters(Schema): + model_config = ConfigDict(populate_by_name=True, str_strip_whitespace=True) + + @field_validator("*", mode="after") + @classmethod + def convert_null_values(cls, value: Any, info: ValidationInfo) -> Any: + """ + Converts 'null' to None for parameters whose type declares Literal["null"], where it + selects items whose value is null. Matching is case-sensitive, and parameters that + don't declare it treat 'null' as an ordinary value. + """ + + if not accepts_null(cls.model_fields[info.field_name].annotation): + return value + + if value == NULL_QUERY_VALUE: + return None + if isinstance(value, list): + return [None if v == NULL_QUERY_VALUE else v for v in value] + + return value + + +class CollectionQueryParameters(BaseQueryParameters): + properties: Optional[str] = properties_query() + include: Optional[str] = Query( + None, + description="Comma-separated list of related resources to include in the response.", + ) + offset: Optional[int] = offset_query() + limit: LimitQuery = limit_query() + + +class PaginatedQueryParameters(BaseQueryParameters): + """CollectionQueryParameters without include, for collections whose items have no related resources.""" + + properties: Optional[str] = properties_query() + offset: Optional[int] = offset_query() + limit: LimitQuery = limit_query() + + +class BasePostBody(Schema): + @field_validator("*", mode="before") + def empty_str_to_none(cls, value): + if isinstance(value, str) and value == "": + return None + return value + + model_config = ConfigDict( + populate_by_name=True, str_strip_whitespace=True, alias_generator=to_camel + ) + + +class PartialMetaclass(type(Schema)): + """ + Metaclass for creating "partial" schemas. + + Marks all fields as `Unset` by default, necessary for PATCH operations where only provided fields should + be included, or responses where a subset of fields is selected by the client. Copies all inherited model fields and + annotations to avoid mutating base classes. + """ + + def __new__( + cls, name: str, bases: tuple[type, ...], attrs: dict, **kwargs + ) -> "PartialMetaclass": + new_cls = super().__new__(cls, name, bases, attrs, **kwargs) + fields = {k: copy.deepcopy(v) for k, v in new_cls.model_fields.items()} + + for field in fields.values(): + field.default = Unset + + metadata = field.metadata + original_annotation = field.annotation + + if metadata: + constrained_arm = Annotated[original_annotation, *metadata] + field.metadata = [] + else: + constrained_arm = original_annotation + + field.annotation = Union[constrained_arm, type(Unset)] + + # `model_fields` is a read-only view of `__pydantic_fields__` as of newer + # pydantic versions, so the canonical field mapping must be updated directly + # for schema regeneration to pick up the mutated defaults/annotations. + new_cls.__pydantic_fields__ = fields + new_cls.model_rebuild(force=True) # noqa + + return new_cls + + +class BasePatchBody(Schema, metaclass=PartialMetaclass): + @field_validator("*", mode="before") + def empty_str_to_none(cls, value): + if isinstance(value, str) and value == "": + return None + return value + + model_config = ConfigDict( + populate_by_name=True, str_strip_whitespace=True, alias_generator=to_camel + ) + + def parse_requested_properties(info: SerializationInfo) -> Optional[set[str]]: """ Parses and retrieves the requested properties from the serialization context. @@ -94,34 +309,17 @@ def filter_requested_properties(item: Any, requested: set[str]) -> Any: return {k: v for k, v in item.items() if k in requested} -class BaseQueryParameters(Schema): - model_config = ConfigDict(populate_by_name=True, str_strip_whitespace=True) - - @field_validator("*", mode="after") - def convert_null_strings(value: Any) -> Any: # noqa - if isinstance(value, str) and value.lower() == "null": - return None - if isinstance(value, list): - return [ - None if isinstance(v, str) and v.lower() == "null" else v for v in value - ] - return value +def page_links(info: SerializationInfo, meta: "PaginationMeta", returned: int) -> Optional[list[dict]]: + """Serialized self, next and prev links for a page of results, or None without a request.""" + request = get_request(info) + if request is None: + return None -class CollectionQueryParameters(BaseQueryParameters): - properties: Optional[str] = Query( - None, - description="Comma-separated list of properties to include in the response. " - "All properties are returned if omitted.", - ) - include: Optional[str] = Query( - None, - description="Comma-separated list of related resources to include in the response.", - ) - offset: Optional[int] = Query(0, ge=0, description="Number of items to skip.") - limit: Optional[int] = Query( - 100, ge=0, le=100000, description="The maximum number of items to return." - ) + return [ + link.model_dump(exclude_none=True) + for link in build_page_links(request, meta.offset, meta.limit, returned) + ] class BaseGetResponse(Schema): @@ -144,12 +342,10 @@ class PaginationMeta(Schema): model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel) -T = TypeVar("T") - - class ItemResponse(Schema, Generic[T]): included: Optional[dict[str, list[Any]]] = None data: T + links: list[Link] = [] model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel) @@ -163,6 +359,15 @@ def _finalize(self, handler, info: SerializationInfo): if requested is not None: data["data"] = filter_requested_properties(data.get("data"), requested) + if self.links: + data["links"] = [link.model_dump(exclude_none=True) for link in self.links] + return data + + request = get_request(info) + if request is not None: + links = [build_self_link(request), build_collection_link(request)] + data["links"] = [link.model_dump(exclude_none=True) for link in links if link is not None] + return data @@ -170,6 +375,7 @@ class PaginatedResponse(Schema, Generic[T]): included: Optional[dict[str, list[Any]]] = None data: list[T] meta: PaginationMeta + links: list[Link] = [] model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel) @@ -186,66 +392,8 @@ def _finalize(self, handler, info: SerializationInfo): if requested is not None and isinstance(data.get("data"), list): data["data"] = [filter_requested_properties(item, requested) for item in data["data"]] - return data + links = page_links(info, self.meta, len(self.data)) + if links is not None: + data["links"] = links - -class BasePostBody(Schema): - @field_validator("*", mode="before") - def empty_str_to_none(cls, value): - if isinstance(value, str) and value == "": - return None - return value - - model_config = ConfigDict( - populate_by_name=True, str_strip_whitespace=True, alias_generator=to_camel - ) - - -class PartialMetaclass(type(Schema)): - """ - Metaclass for creating "partial" schemas. - - Marks all fields as `Unset` by default, necessary for PATCH operations where only provided fields should - be included, or responses where a subset of fields is selected by the client. Copies all inherited model fields and - annotations to avoid mutating base classes. - """ - - def __new__( - cls, name: str, bases: tuple[type, ...], attrs: dict, **kwargs - ) -> "PartialMetaclass": - new_cls = super().__new__(cls, name, bases, attrs, **kwargs) - fields = {k: copy.deepcopy(v) for k, v in new_cls.model_fields.items()} - - for field in fields.values(): - field.default = Unset - - metadata = field.metadata - original_annotation = field.annotation - - if metadata: - constrained_arm = Annotated[original_annotation, *metadata] - field.metadata = [] - else: - constrained_arm = original_annotation - - field.annotation = Union[constrained_arm, type(Unset)] - - # `model_fields` is a read-only view of `__pydantic_fields__` as of newer - # pydantic versions, so the canonical field mapping must be updated directly - # for schema regeneration to pick up the mutated defaults/annotations. - new_cls.__pydantic_fields__ = fields - new_cls.model_rebuild(force=True) # noqa - - return new_cls - - -class BasePatchBody(Schema, metaclass=PartialMetaclass): - @field_validator("*", mode="before") - def empty_str_to_none(cls, value): - if isinstance(value, str) and value == "": - return None - return value - - model_config = ConfigDict( - populate_by_name=True, str_strip_whitespace=True, alias_generator=to_camel - ) + return data diff --git a/django/interfaces/api/schemas/etl/data_connection.py b/django/interfaces/api/schemas/etl/data_connection.py index 1c4711c2..0595dd32 100644 --- a/django/interfaces/api/schemas/etl/data_connection.py +++ b/django/interfaces/api/schemas/etl/data_connection.py @@ -14,9 +14,12 @@ BasePatchBody, BaseQueryParameters, CollectionQueryParameters, + ExtentQueryParameters, WorkspaceResponse, split_comma_separated, comma_array_schema, + split_sortby, + sortby_array_schema, ) from interfaces.api.schemas.orchestration.schedule import ( ScheduleResponse, @@ -93,10 +96,12 @@ class DataConnectionItemQueryParameters(DataConnectionFilterFields, BaseQueryPar pass -class DataConnectionQueryParameters(DataConnectionFilterFields, CollectionQueryParameters): - sortby: Optional[list[DataConnectionSortByFields]] = Query( - [], description="Select one or more fields to sort the response by." - ) +class DataConnectionQueryParameters(DataConnectionFilterFields, CollectionQueryParameters, ExtentQueryParameters): + sortby: Annotated[ + Optional[list[DataConnectionSortByFields]], + BeforeValidator(split_sortby), + WithJsonSchema(sortby_array_schema(DataConnectionSortByFields)), + ] = Query([], description="Select one or more fields to sort the response by.") q: Optional[str] = Query( None, description="Full-text search query. Comma-separated terms are combined with OR; " diff --git a/django/interfaces/api/schemas/etl/mapping.py b/django/interfaces/api/schemas/etl/mapping.py index 0de2ef2b..ef921258 100644 --- a/django/interfaces/api/schemas/etl/mapping.py +++ b/django/interfaces/api/schemas/etl/mapping.py @@ -11,9 +11,12 @@ BasePatchBody, BaseQueryParameters, CollectionQueryParameters, + ExtentQueryParameters, DatastreamResponse, split_comma_separated, comma_array_schema, + split_sortby, + sortby_array_schema, ) @@ -71,10 +74,12 @@ class EtlMappingItemQueryParameters(EtlMappingFilterFields, BaseQueryParameters) pass -class EtlMappingQueryParameters(EtlMappingFilterFields, CollectionQueryParameters): - sortby: Optional[list[EtlMappingSortByFields]] = Query( - [], description="Select one or more fields to sort the response by." - ) +class EtlMappingQueryParameters(EtlMappingFilterFields, CollectionQueryParameters, ExtentQueryParameters): + sortby: Annotated[ + Optional[list[EtlMappingSortByFields]], + BeforeValidator(split_sortby), + WithJsonSchema(sortby_array_schema(EtlMappingSortByFields)), + ] = Query([], description="Select one or more fields to sort the response by.") source_identifier: list[str] = Query( [], description="Filter mappings by source identifier." ) diff --git a/django/interfaces/api/schemas/etl/task.py b/django/interfaces/api/schemas/etl/task.py index 6586729b..478e28f5 100644 --- a/django/interfaces/api/schemas/etl/task.py +++ b/django/interfaces/api/schemas/etl/task.py @@ -12,9 +12,12 @@ BasePatchBody, BaseQueryParameters, CollectionQueryParameters, + ExtentQueryParameters, DataConnectionResponse, split_comma_separated, comma_array_schema, + split_sortby, + sortby_array_schema, ) from interfaces.api.schemas.orchestration.schedule import ( ScheduleResponse, @@ -89,10 +92,12 @@ class EtlTaskItemQueryParameters(EtlTaskFilterFields, BaseQueryParameters): pass -class EtlTaskQueryParameters(EtlTaskFilterFields, CollectionQueryParameters): - sortby: Optional[list[EtlTaskSortByFields]] = Query( - [], description="Select one or more fields to sort the response by." - ) +class EtlTaskQueryParameters(EtlTaskFilterFields, CollectionQueryParameters, ExtentQueryParameters): + sortby: Annotated[ + Optional[list[EtlTaskSortByFields]], + BeforeValidator(split_sortby), + WithJsonSchema(sortby_array_schema(EtlTaskSortByFields)), + ] = Query([], description="Select one or more fields to sort the response by.") q: Optional[str] = Query( None, description="Full-text search query. Comma-separated terms are combined with OR; " diff --git a/django/interfaces/api/schemas/extent.py b/django/interfaces/api/schemas/extent.py new file mode 100644 index 00000000..dfea1990 --- /dev/null +++ b/django/interfaces/api/schemas/extent.py @@ -0,0 +1,197 @@ +import math +import re + +from dataclasses import dataclass +from datetime import datetime, timezone +from typing import Optional, Any, Annotated +from ninja import Query, Schema +from pydantic import BeforeValidator, WithJsonSchema + + +@dataclass(frozen=True) +class BoundingBox: + """ + A WGS 84 longitude/latitude (CRS84) bounding box, per OGC API - Features - Part 1: Core. + + A west edge greater than the east edge describes a box that crosses the antimeridian. + """ + + west: float + south: float + east: float + north: float + + @property + def crosses_antimeridian(self) -> bool: + return self.west > self.east + + def contains(self, longitude: float, latitude: float) -> bool: + """Returns whether a point lies inside the box, boundaries included.""" + + if not self.south <= latitude <= self.north: + return False + + if self.crosses_antimeridian: + return longitude >= self.west or longitude <= self.east + + return self.west <= longitude <= self.east + + +def parse_bbox(value: Any) -> Optional[BoundingBox]: + """ + Parses an OGC API bbox query parameter value into a BoundingBox. + + Accepts four numbers (min lon, min lat, max lon, max lat) or six numbers + (min lon, min lat, min height, max lon, max lat, max height). HydroServer geometries + are two-dimensional, so the height range of a six-number box is validated but not + used for filtering. + """ + + if value is None or isinstance(value, BoundingBox): + return value + + parts = value.split(",") if isinstance(value, str) else list(value) + + if len(parts) == 1 and not str(parts[0]).strip(): + return None + + try: + numbers = [float(part) for part in parts] + except (TypeError, ValueError): + raise ValueError("Bounding box must contain only numeric values") + + if not all(math.isfinite(number) for number in numbers): + raise ValueError("Bounding box must contain only finite numeric values") + + if len(numbers) == 4: + west, south, east, north = numbers + elif len(numbers) == 6: + west, south, min_height, east, north, max_height = numbers + if min_height > max_height: + raise ValueError("Bounding box minimum height must be less than or equal to maximum height") + else: + raise ValueError( + "Bounding box must have 4 or 6 comma-separated values: " + "min_lon,min_lat,max_lon,max_lat or min_lon,min_lat,min_height,max_lon,max_lat,max_height" + ) + + if not all(-180 <= longitude <= 180 for longitude in (west, east)): + raise ValueError("Bounding box longitudes must be between -180 and 180") + if not all(-90 <= latitude <= 90 for latitude in (south, north)): + raise ValueError("Bounding box latitudes must be between -90 and 90") + if south > north: + raise ValueError("Bounding box minimum latitude must be less than or equal to maximum latitude") + + return BoundingBox(west=west, south=south, east=east, north=north) + + +BBOX_JSON_SCHEMA = { + "description": "Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, " + "or six values including min and max heights (heights are not used for filtering). A min_lon " + "greater than max_lon crosses the antimeridian. Items without a location match any bounding box.", + "type": "array", + "oneOf": [{"minItems": 4, "maxItems": 4}, {"minItems": 6, "maxItems": 6}], + "items": {"type": "number"}, + "style": "form", + "explode": False, +} + +BoundingBoxQuery = Annotated[ + Optional[BoundingBox], + BeforeValidator(parse_bbox), + WithJsonSchema(BBOX_JSON_SCHEMA), +] + + +# RFC 3339 section 5.6 date-time: a full date, "T", a time and a required UTC offset. +RFC3339_DATE_TIME = re.compile( + r"\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(\.\d+)?([Zz]|[+-]\d{2}:\d{2})" +) + + +@dataclass(frozen=True) +class TimeInterval: + """ + An OGC API datetime parameter value, per OGC API - Features - Part 1: Core. An instant has + equal start and end; an interval may leave one end open (None). + """ + + start: Optional[datetime] + end: Optional[datetime] + + +def parse_rfc3339(value: str) -> datetime: + """Parses an RFC 3339 date-time, which requires a UTC offset, into an aware UTC datetime.""" + + if not RFC3339_DATE_TIME.fullmatch(value): + raise ValueError( + f"Invalid date-time '{value}'. Use RFC 3339 with a UTC offset, e.g. 2024-01-01T00:00:00Z" + ) + + try: + parsed = datetime.fromisoformat(value.upper()) + except ValueError: + raise ValueError(f"Invalid date-time '{value}'") + + return parsed.astimezone(timezone.utc) + + +def parse_datetime(value: Any) -> Optional[TimeInterval]: + """ + Parses an OGC API datetime query parameter value into a TimeInterval. + + Accepts an instant (date-time), a bounded interval (start/end) or a half-bounded interval + whose open end is ".." or empty (../end, /end, start/.., start/), per Core Req 26. + """ + + if value is None or isinstance(value, TimeInterval): + return value + + if not isinstance(value, str): + raise ValueError("Datetime must be a string") + + value = value.strip() + + if "/" not in value: + instant = parse_rfc3339(value) + return TimeInterval(start=instant, end=instant) + + parts = value.split("/") + if len(parts) != 2: + raise ValueError("Datetime interval must have exactly one '/' separating its start and end") + + start, end = (None if part in ("", "..") else parse_rfc3339(part) for part in parts) + + if start is None and end is None: + raise ValueError("Datetime interval must have at least one bounded end") + if start is not None and end is not None and start > end: + raise ValueError("Datetime interval start must be earlier than or equal to its end") + + return TimeInterval(start=start, end=end) + + +DATETIME_JSON_SCHEMA = { + "description": "Date-time or interval in RFC 3339 with a UTC offset: an instant " + "(2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with " + "'..' or an empty value for the open end (../end, start/..). Items without a time match any " + "datetime.", + "type": "string", + "style": "form", + "explode": False, +} + +DatetimeQuery = Annotated[ + Optional[TimeInterval], + BeforeValidator(parse_datetime), + WithJsonSchema(DATETIME_JSON_SCHEMA), +] + + +class ExtentQueryParameters(Schema): + """ + Query parameters that filter items by their spatial and temporal extent, shared by every + OGC API collection items endpoint. + """ + + bbox: BoundingBoxQuery = Query(None) + datetime: DatetimeQuery = Query(None) diff --git a/django/interfaces/api/schemas/iam/collaborator.py b/django/interfaces/api/schemas/iam/collaborator.py index ff679cdc..40173128 100644 --- a/django/interfaces/api/schemas/iam/collaborator.py +++ b/django/interfaces/api/schemas/iam/collaborator.py @@ -12,6 +12,8 @@ RoleResponse, split_comma_separated, comma_array_schema, + split_sortby, + sortby_array_schema, ) from interfaces.api.schemas.iam.user import UserContactResponse from interfaces.api.schemas.iam.service_account import ServiceAccountContactResponse @@ -74,9 +76,11 @@ class CollaboratorFilterFields(Schema): class CollaboratorQueryParameters(CollaboratorFilterFields, CollectionQueryParameters): role_id: list[uuid.UUID] = Query([], description="Filter collaborators by role ID.") - sortby: Optional[list[CollaboratorSortByFields]] = Query( - [], description="Select one or more fields to sort the response by." - ) + sortby: Annotated[ + Optional[list[CollaboratorSortByFields]], + BeforeValidator(split_sortby), + WithJsonSchema(sortby_array_schema(CollaboratorSortByFields)), + ] = Query([], description="Select one or more fields to sort the response by.") class CollaboratorResponse(BaseGetResponse): diff --git a/django/interfaces/api/schemas/iam/role.py b/django/interfaces/api/schemas/iam/role.py index 482a9a72..302031a4 100644 --- a/django/interfaces/api/schemas/iam/role.py +++ b/django/interfaces/api/schemas/iam/role.py @@ -11,8 +11,11 @@ BaseGetResponse, BaseQueryParameters, CollectionQueryParameters, + ExtentQueryParameters, split_comma_separated, comma_array_schema, + split_sortby, + sortby_array_schema, ) RESOURCE_TYPES = Literal["*", *resource_types] @@ -57,10 +60,12 @@ class RoleItemQueryParameters(RoleFilterFields, BaseQueryParameters): pass -class RoleQueryParameters(RoleFilterFields, CollectionQueryParameters): - sortby: Optional[list[RoleSortByFields]] = Query( - [], description="Select one or more fields to sort the response by." - ) +class RoleQueryParameters(RoleFilterFields, CollectionQueryParameters, ExtentQueryParameters): + sortby: Annotated[ + Optional[list[RoleSortByFields]], + BeforeValidator(split_sortby), + WithJsonSchema(sortby_array_schema(RoleSortByFields)), + ] = Query([], description="Select one or more fields to sort the response by.") workspace_id: list[uuid.UUID | Literal["null"]] = Query( [], description="Filter roles by workspace ID." ) diff --git a/django/interfaces/api/schemas/iam/service_account.py b/django/interfaces/api/schemas/iam/service_account.py index 9f51cb4e..4c479e7f 100644 --- a/django/interfaces/api/schemas/iam/service_account.py +++ b/django/interfaces/api/schemas/iam/service_account.py @@ -16,6 +16,8 @@ WorkspaceResponse, split_comma_separated, comma_array_schema, + split_sortby, + sortby_array_schema, ) @@ -87,9 +89,11 @@ class ServiceAccountItemQueryParameters(ServiceAccountFilterFields, BaseQueryPar class ServiceAccountQueryParameters(ServiceAccountFilterFields, CollectionQueryParameters): - sortby: Optional[list[ServiceAccountSortByFields]] = Query( - [], description="Select one or more fields to sort the response by." - ) + sortby: Annotated[ + Optional[list[ServiceAccountSortByFields]], + BeforeValidator(split_sortby), + WithJsonSchema(sortby_array_schema(ServiceAccountSortByFields)), + ] = Query([], description="Select one or more fields to sort the response by.") class ServiceAccountResponse(BaseGetResponse, ServiceAccountFields): diff --git a/django/interfaces/api/schemas/iam/workspace.py b/django/interfaces/api/schemas/iam/workspace.py index 5709811b..bbc4271c 100644 --- a/django/interfaces/api/schemas/iam/workspace.py +++ b/django/interfaces/api/schemas/iam/workspace.py @@ -11,10 +11,14 @@ BasePatchBody, BaseQueryParameters, CollectionQueryParameters, + ExtentQueryParameters, UserContactResponse, RoleResponse, split_comma_separated, comma_array_schema, + split_sortby, + sortby_array_schema, + QueryBool, ) @@ -84,20 +88,22 @@ class WorkspaceItemQueryParameters(WorkspaceFilterFields, BaseQueryParameters): pass -class WorkspaceQueryParameters(WorkspaceFilterFields, CollectionQueryParameters): - sortby: Optional[list[WorkspaceSortByFields]] = Query( - [], description="Select one or more fields to sort the response by." - ) +class WorkspaceQueryParameters(WorkspaceFilterFields, CollectionQueryParameters, ExtentQueryParameters): + sortby: Annotated[ + Optional[list[WorkspaceSortByFields]], + BeforeValidator(split_sortby), + WithJsonSchema(sortby_array_schema(WorkspaceSortByFields)), + ] = Query([], description="Select one or more fields to sort the response by.") q: Optional[str] = Query( None, description="Full-text search query. Comma-separated terms are combined with OR; " "whitespace-separated words within a term are combined with AND.", ) - is_associated: Optional[bool] = Query( + is_associated: Optional[QueryBool] = Query( None, description="Whether the workspace is associated with the authenticated user", ) - is_private: Optional[bool] = Query( + is_private: Optional[QueryBool] = Query( None, description="Whether the returned workspaces should be private or public." ) diff --git a/django/interfaces/api/schemas/monitoring/rule.py b/django/interfaces/api/schemas/monitoring/rule.py index c0c2b235..79bcfcd4 100644 --- a/django/interfaces/api/schemas/monitoring/rule.py +++ b/django/interfaces/api/schemas/monitoring/rule.py @@ -11,9 +11,12 @@ BasePatchBody, BaseQueryParameters, CollectionQueryParameters, + ExtentQueryParameters, DatastreamResponse, split_comma_separated, comma_array_schema, + split_sortby, + sortby_array_schema, ) @@ -76,10 +79,12 @@ class MonitoringRuleItemQueryParameters(MonitoringRuleFilterFields, BaseQueryPar pass -class MonitoringRuleQueryParameters(MonitoringRuleFilterFields, CollectionQueryParameters): - sortby: Optional[list[MonitoringRuleSortByFields]] = Query( - [], description="Select one or more fields to sort the response by." - ) +class MonitoringRuleQueryParameters(MonitoringRuleFilterFields, CollectionQueryParameters, ExtentQueryParameters): + sortby: Annotated[ + Optional[list[MonitoringRuleSortByFields]], + BeforeValidator(split_sortby), + WithJsonSchema(sortby_array_schema(MonitoringRuleSortByFields)), + ] = Query([], description="Select one or more fields to sort the response by.") datastream_id: list[uuid.UUID] = Query( [], description="Filter rules by datastream ID." ) diff --git a/django/interfaces/api/schemas/monitoring/task.py b/django/interfaces/api/schemas/monitoring/task.py index 99954848..2a8f46e7 100644 --- a/django/interfaces/api/schemas/monitoring/task.py +++ b/django/interfaces/api/schemas/monitoring/task.py @@ -12,9 +12,12 @@ BasePatchBody, BaseQueryParameters, CollectionQueryParameters, + ExtentQueryParameters, MonitoringSiteResponse, split_comma_separated, comma_array_schema, + split_sortby, + sortby_array_schema, ) from interfaces.api.schemas.orchestration.schedule import ( ScheduleResponse, @@ -91,10 +94,12 @@ class MonitoringTaskItemQueryParameters(MonitoringTaskFilterFields, BaseQueryPar pass -class MonitoringTaskQueryParameters(MonitoringTaskFilterFields, CollectionQueryParameters): - sortby: Optional[list[MonitoringTaskSortByFields]] = Query( - [], description="Select one or more fields to sort the response by." - ) +class MonitoringTaskQueryParameters(MonitoringTaskFilterFields, CollectionQueryParameters, ExtentQueryParameters): + sortby: Annotated[ + Optional[list[MonitoringTaskSortByFields]], + BeforeValidator(split_sortby), + WithJsonSchema(sortby_array_schema(MonitoringTaskSortByFields)), + ] = Query([], description="Select one or more fields to sort the response by.") q: Optional[str] = Query( None, description="Full-text search query. Comma-separated terms are combined with OR; " diff --git a/django/interfaces/api/schemas/ogc.py b/django/interfaces/api/schemas/ogc.py new file mode 100644 index 00000000..9317d652 --- /dev/null +++ b/django/interfaces/api/schemas/ogc.py @@ -0,0 +1,42 @@ +from typing import Optional + +from ninja import Schema +from pydantic import ConfigDict +from pydantic.alias_generators import to_camel + +from interfaces.api.http.links import Link + + +class CollectionResponse(Schema): + """A collection's metadata (OGC API - Features Core collection.yaml).""" + + id: str + title: str + description: str + item_type: Optional[str] = None + links: list[Link] + + model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel) + + +class CollectionsResponse(Schema): + """The collections the API serves (OGC API - Features Core collections.yaml).""" + + links: list[Link] + collections: list[CollectionResponse] + + +class LandingPageResponse(Schema): + """The API's landing page (OGC API - Features Core landingPage.yaml).""" + + title: str + description: str + links: list[Link] + + +class ConformanceResponse(Schema): + """The conformance classes the API implements (OGC API - Features Core confClasses.yaml).""" + + conforms_to: list[str] + + model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel) diff --git a/django/interfaces/api/schemas/orchestration/run.py b/django/interfaces/api/schemas/orchestration/run.py index 807c3e9d..fe0e7ffe 100644 --- a/django/interfaces/api/schemas/orchestration/run.py +++ b/django/interfaces/api/schemas/orchestration/run.py @@ -11,6 +11,8 @@ CollectionQueryParameters, split_comma_separated, comma_array_schema, + split_sortby, + sortby_array_schema, ) @@ -34,9 +36,11 @@ class TaskRunQueryParameters(CollectionQueryParameters): description="Comma-separated list of properties to include in the response. " "All properties are returned if omitted.", ) - sortby: list[TaskRunSortByFields] = Query( - [], description="Select one or more fields to sort the response by." - ) + sortby: Annotated[ + list[TaskRunSortByFields], + BeforeValidator(split_sortby), + WithJsonSchema(sortby_array_schema(TaskRunSortByFields)), + ] = Query([], description="Select one or more fields to sort the response by.") status: list[Literal["PENDING", "STARTED", "SUCCESS", "FAILURE"]] = Query( [], description="Filters task runs by their status." ) diff --git a/django/interfaces/api/schemas/products/rating_curve.py b/django/interfaces/api/schemas/products/rating_curve.py index ee71173c..0386896d 100644 --- a/django/interfaces/api/schemas/products/rating_curve.py +++ b/django/interfaces/api/schemas/products/rating_curve.py @@ -11,9 +11,12 @@ BasePatchBody, BaseQueryParameters, CollectionQueryParameters, + ExtentQueryParameters, MonitoringSiteResponse, split_comma_separated, comma_array_schema, + split_sortby, + sortby_array_schema, ) @@ -81,10 +84,12 @@ class RatingCurveItemQueryParameters(RatingCurveFilterFields, BaseQueryParameter pass -class RatingCurveQueryParameters(RatingCurveFilterFields, CollectionQueryParameters): - sortby: Optional[list[RatingCurveSortByFields]] = Query( - [], description="Select one or more fields to sort the response by." - ) +class RatingCurveQueryParameters(RatingCurveFilterFields, CollectionQueryParameters, ExtentQueryParameters): + sortby: Annotated[ + Optional[list[RatingCurveSortByFields]], + BeforeValidator(split_sortby), + WithJsonSchema(sortby_array_schema(RatingCurveSortByFields)), + ] = Query([], description="Select one or more fields to sort the response by.") q: Optional[str] = Query( None, description="Full-text search query. Comma-separated terms are combined with OR; " diff --git a/django/interfaces/api/schemas/products/task.py b/django/interfaces/api/schemas/products/task.py index 6d5139df..9caf6c7b 100644 --- a/django/interfaces/api/schemas/products/task.py +++ b/django/interfaces/api/schemas/products/task.py @@ -12,9 +12,12 @@ BasePatchBody, BaseQueryParameters, CollectionQueryParameters, + ExtentQueryParameters, MonitoringSiteResponse, split_comma_separated, comma_array_schema, + split_sortby, + sortby_array_schema, ) from interfaces.api.schemas.orchestration.schedule import ( ScheduleResponse, @@ -90,10 +93,12 @@ class DataProductTaskItemQueryParameters(DataProductTaskFilterFields, BaseQueryP pass -class DataProductTaskQueryParameters(DataProductTaskFilterFields, CollectionQueryParameters): - sortby: Optional[list[DataProductTaskSortByFields]] = Query( - [], description="Select one or more fields to sort the response by." - ) +class DataProductTaskQueryParameters(DataProductTaskFilterFields, CollectionQueryParameters, ExtentQueryParameters): + sortby: Annotated[ + Optional[list[DataProductTaskSortByFields]], + BeforeValidator(split_sortby), + WithJsonSchema(sortby_array_schema(DataProductTaskSortByFields)), + ] = Query([], description="Select one or more fields to sort the response by.") q: Optional[str] = Query( None, description="Full-text search query. Comma-separated terms are combined with OR; " @@ -117,7 +122,7 @@ class DataProductTaskQueryParameters(DataProductTaskFilterFields, CollectionQuer input_datastream: list[uuid.UUID] = Query( [], description="Filter data product tasks by input datastream ID.", alias="input_datastream_id" ) - rating_curve: list[uuid.UUID] = Query( + rating_curve: list[uuid.UUID | Literal["null"]] = Query( [], description="Filter data product tasks by rating curve ID.", alias="rating_curve_id" ) diff --git a/django/interfaces/api/schemas/products/transformation.py b/django/interfaces/api/schemas/products/transformation.py index a13eec50..09c1e96f 100644 --- a/django/interfaces/api/schemas/products/transformation.py +++ b/django/interfaces/api/schemas/products/transformation.py @@ -11,9 +11,12 @@ BasePatchBody, BaseQueryParameters, CollectionQueryParameters, + ExtentQueryParameters, DatastreamResponse, split_comma_separated, comma_array_schema, + split_sortby, + sortby_array_schema, ) from interfaces.api.schemas.products.rating_curve import RatingCurveResponse @@ -105,11 +108,13 @@ class DataProductTransformationItemQueryParameters( class DataProductTransformationQueryParameters( - DataProductTransformationFilterFields, CollectionQueryParameters + DataProductTransformationFilterFields, CollectionQueryParameters, ExtentQueryParameters ): - sortby: Optional[list[DataProductTransformationSortByFields]] = Query( - [], description="Select one or more fields to sort the response by." - ) + sortby: Annotated[ + Optional[list[DataProductTransformationSortByFields]], + BeforeValidator(split_sortby), + WithJsonSchema(sortby_array_schema(DataProductTransformationSortByFields)), + ] = Query([], description="Select one or more fields to sort the response by.") transformation_type: list[str] = Query( [], description="Filter transformations by type." ) diff --git a/django/interfaces/api/schemas/quality/history.py b/django/interfaces/api/schemas/quality/history.py index dad50d16..364fd9da 100644 --- a/django/interfaces/api/schemas/quality/history.py +++ b/django/interfaces/api/schemas/quality/history.py @@ -10,9 +10,12 @@ BasePostBody, BaseQueryParameters, CollectionQueryParameters, + ExtentQueryParameters, DatastreamResponse, split_comma_separated, comma_array_schema, + split_sortby, + sortby_array_schema, ) @@ -77,15 +80,17 @@ class QualityControlHistoryItemQueryParameters( class QualityControlHistoryQueryParameters( - QualityControlHistoryFilterFields, CollectionQueryParameters + QualityControlHistoryFilterFields, CollectionQueryParameters, ExtentQueryParameters ): - sortby: Optional[list[QualityControlHistorySortByFields]] = Query( - [], description="Select one or more fields to sort the response by." - ) + sortby: Annotated[ + Optional[list[QualityControlHistorySortByFields]], + BeforeValidator(split_sortby), + WithJsonSchema(sortby_array_schema(QualityControlHistorySortByFields)), + ] = Query([], description="Select one or more fields to sort the response by.") managed_datastream_id: list[uuid.UUID] = Query( [], description="Filter histories by managed datastream ID." ) - source_datastream_id: list[uuid.UUID] = Query( + source_datastream_id: list[uuid.UUID | Literal["null"]] = Query( [], description="Filter histories by source datastream ID." ) diff --git a/django/interfaces/api/schemas/quality/operation.py b/django/interfaces/api/schemas/quality/operation.py index 74ee4d81..e0300a26 100644 --- a/django/interfaces/api/schemas/quality/operation.py +++ b/django/interfaces/api/schemas/quality/operation.py @@ -14,6 +14,8 @@ UserContactResponse, split_comma_separated, comma_array_schema, + split_sortby, + sortby_array_schema, ) from interfaces.api.schemas.iam.collaborator import DELETED_USER_CONTACT @@ -70,9 +72,11 @@ class QualityControlOperationItemQueryParameters(QualityControlOperationFilterFi class QualityControlOperationQueryParameters(QualityControlOperationFilterFields, CollectionQueryParameters): - sortby: Optional[list[QualityControlOperationSortByFields]] = Query( - [], description="Select one or more fields to sort the response by." - ) + sortby: Annotated[ + Optional[list[QualityControlOperationSortByFields]], + BeforeValidator(split_sortby), + WithJsonSchema(sortby_array_schema(QualityControlOperationSortByFields)), + ] = Query([], description="Select one or more fields to sort the response by.") class QualityControlOperationResponse(BaseGetResponse): diff --git a/django/interfaces/api/schemas/quality/session.py b/django/interfaces/api/schemas/quality/session.py index 5dfcfeba..e0ebb2ba 100644 --- a/django/interfaces/api/schemas/quality/session.py +++ b/django/interfaces/api/schemas/quality/session.py @@ -14,6 +14,9 @@ UserContactResponse, split_comma_separated, comma_array_schema, + split_sortby, + sortby_array_schema, + QueryBool, ) from interfaces.api.schemas.iam.collaborator import DELETED_USER_CONTACT @@ -65,14 +68,16 @@ class QualityControlSessionItemQueryParameters(QualityControlSessionFilterFields class QualityControlSessionQueryParameters(QualityControlSessionFilterFields, CollectionQueryParameters): - sortby: Optional[list[QualityControlSessionSortByFields]] = Query( - [], description="Select one or more fields to sort the response by." - ) + sortby: Annotated[ + Optional[list[QualityControlSessionSortByFields]], + BeforeValidator(split_sortby), + WithJsonSchema(sortby_array_schema(QualityControlSessionSortByFields)), + ] = Query([], description="Select one or more fields to sort the response by.") status: Optional[SessionStatus] = None range_start: Optional[ISODatetime] = Query(None, description="Return sessions overlapping with this range start.") range_end: Optional[ISODatetime] = Query(None, description="Return sessions overlapping with this range end.") ancestor_of: Optional[uuid.UUID] = Query(None, description="Return all transitive ancestors of the given session ID.") - include_ancestors: bool = Query(False, description="Also return transitive ancestors of all sessions matched by other filters.") + include_ancestors: QueryBool = Query(False, description="Also return transitive ancestors of all sessions matched by other filters.") class QualityControlSessionResponse(BaseGetResponse): diff --git a/django/interfaces/api/schemas/sta/datastream.py b/django/interfaces/api/schemas/sta/datastream.py index d076e3ca..4e2c2492 100644 --- a/django/interfaces/api/schemas/sta/datastream.py +++ b/django/interfaces/api/schemas/sta/datastream.py @@ -12,6 +12,7 @@ BasePatchBody, BaseQueryParameters, CollectionQueryParameters, + ExtentQueryParameters, WorkspaceResponse, MonitoringSiteResponse, ObservedPropertyResponse, @@ -20,6 +21,9 @@ ProcessingLevelResponse, split_comma_separated, comma_array_schema, + split_sortby, + sortby_array_schema, + QueryBool, ) from interfaces.api.schemas.sta.linked_resource import LinkedResourceGetResponse from interfaces.api.schemas.sta.tags import reject_empty_tag_keys_and_values @@ -37,11 +41,6 @@ class DatastreamFields(Schema): time_aggregation_interval: float status: Optional[str] = Field(None, max_length=255) result_type: str = Field(..., max_length=255) - value_count: Optional[int] = Field(None, ge=0) - phenomenon_begin_time: Optional[ISODatetime] = None - phenomenon_end_time: Optional[ISODatetime] = None - result_begin_time: Optional[ISODatetime] = None - result_end_time: Optional[ISODatetime] = None is_private: bool = False is_visible: bool = True time_aggregation_interval_unit: Literal["seconds", "minutes", "hours", "days"] @@ -51,6 +50,16 @@ class DatastreamFields(Schema): ] = None +class DatastreamStatisticsFields(Schema): + """Observation statistics the server maintains; returned in responses but never writable.""" + + value_count: Optional[int] = Field(None, ge=0) + phenomenon_begin_time: Optional[ISODatetime] = None + phenomenon_end_time: Optional[ISODatetime] = None + result_begin_time: Optional[ISODatetime] = None + result_end_time: Optional[ISODatetime] = None + + class DatastreamRelatedFields(Schema): monitoring_site_id: uuid.UUID method_id: uuid.UUID @@ -134,6 +143,7 @@ class DatastreamRelatedFields(Schema): "id", "workspaceId", *(to_camel(name) for name in DatastreamFields.model_fields), + *(to_camel(name) for name in DatastreamStatisticsFields.model_fields), *(to_camel(name) for name in DatastreamRelatedFields.model_fields), "tags", "linkedResources", @@ -165,10 +175,12 @@ class DatastreamItemQueryParameters(DatastreamFilterFields, BaseQueryParameters) pass -class DatastreamQueryParameters(DatastreamFilterFields, CollectionQueryParameters): - sortby: Optional[list[DatastreamSortByFields]] = Query( - [], description="Select one or more fields to sort the response by." - ) +class DatastreamQueryParameters(DatastreamFilterFields, CollectionQueryParameters, ExtentQueryParameters): + sortby: Annotated[ + Optional[list[DatastreamSortByFields]], + BeforeValidator(split_sortby), + WithJsonSchema(sortby_array_schema(DatastreamSortByFields)), + ] = Query([], description="Select one or more fields to sort the response by.") q: Optional[str] = Query( None, description="Full-text search query. Comma-separated terms are combined with OR; " @@ -199,12 +211,12 @@ class DatastreamQueryParameters(DatastreamFilterFields, CollectionQueryParameter sampled_medium: list[str] = Query( [], description="Filter monitoring_sites by sampled medium." ) - status: list[str] = Query([], description="Filter monitoring_sites by status.") + status: list[str | Literal["null"]] = Query([], description="Filter datastreams by status.") result_type: list[str] = Query([], description="Filter monitoring_sites by result type.") tag: list[str] = Query( [], description="Filter datastreams by tag. Format tag filters as {key}:{value}" ) - is_private: Optional[bool] = Query( + is_private: Optional[QueryBool] = Query( None, description="Controls whether the datastreams should be private or public.", ) @@ -218,46 +230,6 @@ class DatastreamQueryParameters(DatastreamFilterFields, CollectionQueryParameter description="Sets the minimum value count of filtered datastreams.", alias="value_count_min", ) - phenomenon_begin_time__lte: Optional[ISODatetime] = Query( - None, - description="Sets the maximum phenomenon begin time of filtered datastreams.", - alias="phenomenon_begin_time_max", - ) - phenomenon_begin_time__gte: Optional[ISODatetime] = Query( - None, - description="Sets the minimum phenomenon begin time of filtered datastreams.", - alias="phenomenon_begin_time_min", - ) - phenomenon_end_time__lte: Optional[ISODatetime] = Query( - None, - description="Sets the maximum phenomenon end time of filtered datastreams.", - alias="phenomenon_end_time_max", - ) - phenomenon_end_time__gte: Optional[ISODatetime] = Query( - None, - description="Sets the minimum phenomenon end time of filtered datastreams.", - alias="phenomenon_end_time_min", - ) - result_begin_time__lte: Optional[ISODatetime] = Query( - None, - description="Sets the maximum result begin time of filtered datastreams.", - alias="result_begin_time_max", - ) - result_begin_time__gte: Optional[ISODatetime] = Query( - None, - description="Sets the minimum result begin time of filtered datastreams.", - alias="result_begin_time_min", - ) - result_end_time__lte: Optional[ISODatetime] = Query( - None, - description="Sets the maximum result end time of filtered datastreams.", - alias="result_end_time_max", - ) - result_end_time__gte: Optional[ISODatetime] = Query( - None, - description="Sets the minimum result end time of filtered datastreams.", - alias="result_end_time_min", - ) class DatastreamVisualizationBootstrapQueryParameters(BaseQueryParameters): @@ -318,7 +290,7 @@ class DatastreamVisualizationBootstrapResponse(BaseGetResponse): class DatastreamResponse( - BaseGetResponse, DatastreamFields, DatastreamRelatedFields + BaseGetResponse, DatastreamFields, DatastreamStatisticsFields, DatastreamRelatedFields ): id: uuid.UUID workspace_id: uuid.UUID = Field( diff --git a/django/interfaces/api/schemas/sta/linked_resource.py b/django/interfaces/api/schemas/sta/linked_resource.py index 2b8199a9..4145fd5f 100644 --- a/django/interfaces/api/schemas/sta/linked_resource.py +++ b/django/interfaces/api/schemas/sta/linked_resource.py @@ -4,11 +4,11 @@ from interfaces.api.schemas import ( BaseGetResponse, BasePostBody, - CollectionQueryParameters, + PaginatedQueryParameters, ) -class LinkedResourceQueryParameters(CollectionQueryParameters): +class LinkedResourceQueryParameters(PaginatedQueryParameters): type: list[str] = Query([], description="Filter by linked resource type.") diff --git a/django/interfaces/api/schemas/sta/method.py b/django/interfaces/api/schemas/sta/method.py index 7b115e08..fc9da94d 100644 --- a/django/interfaces/api/schemas/sta/method.py +++ b/django/interfaces/api/schemas/sta/method.py @@ -12,9 +12,12 @@ BasePatchBody, BaseQueryParameters, CollectionQueryParameters, + ExtentQueryParameters, WorkspaceResponse, split_comma_separated, comma_array_schema, + split_sortby, + sortby_array_schema, ) from interfaces.api.schemas.sta.vocabulary import VocabularyResponse @@ -84,10 +87,12 @@ class MethodItemQueryParameters(MethodFilterFields, BaseQueryParameters): pass -class MethodQueryParameters(MethodFilterFields, CollectionQueryParameters): - sortby: Optional[list[MethodSortByFields]] = Query( - [], description="Select one or more fields to sort the response by." - ) +class MethodQueryParameters(MethodFilterFields, CollectionQueryParameters, ExtentQueryParameters): + sortby: Annotated[ + Optional[list[MethodSortByFields]], + BeforeValidator(split_sortby), + WithJsonSchema(sortby_array_schema(MethodSortByFields)), + ] = Query([], description="Select one or more fields to sort the response by.") q: Optional[str] = Query( None, description="Full-text search query. Comma-separated terms are combined with OR; " @@ -103,8 +108,8 @@ class MethodQueryParameters(MethodFilterFields, CollectionQueryParameters): [], description="Filter methods by datastream ID.", alias="datastream_id" ) type: list[str] = Query([], description="Filter methods by type") - sensor_model: list[str] = Query([], description="Filter methods by sensor model") - sensor_model_manufacturer: list[str] = Query( + sensor_model: list[str | Literal["null"]] = Query([], description="Filter methods by sensor model") + sensor_model_manufacturer: list[str | Literal["null"]] = Query( [], description="Filter methods by sensor model manufacturer" ) diff --git a/django/interfaces/api/schemas/sta/monitoring_site.py b/django/interfaces/api/schemas/sta/monitoring_site.py index d362485f..6caa53dc 100644 --- a/django/interfaces/api/schemas/sta/monitoring_site.py +++ b/django/interfaces/api/schemas/sta/monitoring_site.py @@ -14,9 +14,14 @@ BasePostBody, BaseQueryParameters, CollectionQueryParameters, + ExtentQueryParameters, + BoundingBoxQuery, WorkspaceResponse, split_comma_separated, comma_array_schema, + split_sortby, + sortby_array_schema, + QueryBool, ) from interfaces.api.schemas.sta.linked_resource import LinkedResourceGetResponse from interfaces.api.schemas.sta.tags import reject_empty_tag_keys_and_values @@ -122,10 +127,12 @@ class MonitoringSiteItemQueryParameters(MonitoringSiteFilterFields, BaseQueryPar pass -class MonitoringSiteQueryParameters(MonitoringSiteFilterFields, CollectionQueryParameters): - sortby: Optional[list[MonitoringSiteSortByFields]] = Query( - [], description="Select one or more fields to sort the response by." - ) +class MonitoringSiteQueryParameters(MonitoringSiteFilterFields, CollectionQueryParameters, ExtentQueryParameters): + sortby: Annotated[ + Optional[list[MonitoringSiteSortByFields]], + BeforeValidator(split_sortby), + WithJsonSchema(sortby_array_schema(MonitoringSiteSortByFields)), + ] = Query([], description="Select one or more fields to sort the response by.") q: Optional[str] = Query( None, description="Full-text search query. Comma-separated terms are combined with OR; " @@ -134,22 +141,18 @@ class MonitoringSiteQueryParameters(MonitoringSiteFilterFields, CollectionQueryP workspace_id: list[uuid.UUID] = Query( [], description="Filter monitoring sites by workspace ID." ) - bbox: list[str] = Query( - [], - description="Filter monitoring sites by bounding box. Format bounding box as {min_lon},{min_lat},{max_lon},{max_lat}", - ) - admin_area_1: list[str] = Query( + admin_area_1: list[str | Literal["null"]] = Query( [], description="Filter monitoring sites by admin area 1." ) - admin_area_2: list[str] = Query( + admin_area_2: list[str | Literal["null"]] = Query( [], description="Filter monitoring sites by admin area 2." ) - country: list[str] = Query([], description="Filter monitoring sites by country.") + country: list[str | Literal["null"]] = Query([], description="Filter monitoring sites by country.") type: list[str] = Query([], description="Filter monitoring sites by type.") tag: list[str] = Query( [], description="Filter monitoring sites by tag. Format tag filters as {key}:{value}" ) - is_private: Optional[bool] = Query( + is_private: Optional[QueryBool] = Query( None, description="Controls whether the returned monitoring sites should be private or public.", ) @@ -159,10 +162,7 @@ class MonitoringSiteMarkerQueryParameters(BaseQueryParameters): workspace_id: list[uuid.UUID] = Query( [], description="Filter markers by workspace ID." ) - bbox: list[str] = Query( - [], - description="Filter markers by bounding box. Format bounding box as {min_lon},{min_lat},{max_lon},{max_lat}", - ) + bbox: BoundingBoxQuery = Query(None) type: list[str] = Query([], description="Filter markers by monitoring site type.") diff --git a/django/interfaces/api/schemas/sta/observation.py b/django/interfaces/api/schemas/sta/observation.py index cba0c8bc..6e87823a 100644 --- a/django/interfaces/api/schemas/sta/observation.py +++ b/django/interfaces/api/schemas/sta/observation.py @@ -13,12 +13,17 @@ BasePostBody, BaseQueryParameters, CollectionQueryParameters, + ExtentQueryParameters, PaginationMeta, WorkspaceResponse, DatastreamResponse, split_comma_separated, comma_array_schema, + split_sortby, + sortby_array_schema, ) +from interfaces.api.schemas.base import page_links +from interfaces.api.http.links import Link from interfaces.api.schemas.sta.result_qualifier import ResultQualifierResponse @@ -88,28 +93,20 @@ class ObservationItemQueryParameters(ObservationFilterFields, BaseQueryParameter pass -class ObservationQueryParameters(ObservationFilterFields, CollectionQueryParameters): +class ObservationQueryParameters(ObservationFilterFields, CollectionQueryParameters, ExtentQueryParameters): datastream_id: list[uuid.UUID] = Query( [], description="Filter observations by datastream ID." ) - sortby: Optional[list[ObservationSortByFields]] = Query( - [], description="Select one or more fields to sort the response by." - ) + sortby: Annotated[ + Optional[list[ObservationSortByFields]], + BeforeValidator(split_sortby), + WithJsonSchema(sortby_array_schema(ObservationSortByFields)), + ] = Query([], description="Select one or more fields to sort the response by.") response_format: Optional[Literal["record", "row", "column"]] = Query( None, description="Controls the format of the observations response.", alias="format", ) - phenomenon_time__lte: Optional[ISODatetime] = Query( - None, - description="Sets the maximum phenomenon time of filtered observations.", - alias="phenomenon_time_max", - ) - phenomenon_time__gte: Optional[ISODatetime] = Query( - None, - description="Sets the minimum phenomenon time of filtered observations.", - alias="phenomenon_time_min", - ) result_qualifier_codes: list[str] = Query( [], description="Filter observations by result qualifier code.", @@ -135,6 +132,7 @@ class ObservationFormatResponse(Schema, Generic[T]): data: T meta: PaginationMeta included: Optional[dict[str, list[Any]]] = None + links: list[Link] = [] model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel) @@ -147,6 +145,10 @@ def _finalize(self, handler, info: SerializationInfo): if not data.get("included"): data.pop("included", None) + links = page_links(info, self.meta, self.data.row_count) + if links is not None: + data["links"] = links + return data @@ -154,12 +156,20 @@ class ObservationRowData(BaseGetResponse): fields: list[Literal["phenomenonTime", "result", "resultQualifierCodes"]] rows: list[list] + @property + def row_count(self) -> int: + return len(self.rows) + class ObservationColumnarData(BaseGetResponse): phenomenon_time: list result: list result_qualifier_codes: list + @property + def row_count(self) -> int: + return len(self.phenomenon_time) + class ObservationRowResponse(ObservationFormatResponse[ObservationRowData]): pass diff --git a/django/interfaces/api/schemas/sta/observed_property.py b/django/interfaces/api/schemas/sta/observed_property.py index 1ac805fd..4dfda4b3 100644 --- a/django/interfaces/api/schemas/sta/observed_property.py +++ b/django/interfaces/api/schemas/sta/observed_property.py @@ -12,9 +12,12 @@ BasePatchBody, BaseQueryParameters, CollectionQueryParameters, + ExtentQueryParameters, WorkspaceResponse, split_comma_separated, comma_array_schema, + split_sortby, + sortby_array_schema, ) from interfaces.api.schemas.sta.vocabulary import VocabularyResponse @@ -81,10 +84,12 @@ class ObservedPropertyItemQueryParameters(ObservedPropertyFilterFields, BaseQuer pass -class ObservedPropertyQueryParameters(ObservedPropertyFilterFields, CollectionQueryParameters): - sortby: Optional[list[ObservedPropertySortByFields]] = Query( - [], description="Select one or more fields to sort the response by." - ) +class ObservedPropertyQueryParameters(ObservedPropertyFilterFields, CollectionQueryParameters, ExtentQueryParameters): + sortby: Annotated[ + Optional[list[ObservedPropertySortByFields]], + BeforeValidator(split_sortby), + WithJsonSchema(sortby_array_schema(ObservedPropertySortByFields)), + ] = Query([], description="Select one or more fields to sort the response by.") q: Optional[str] = Query( None, description="Full-text search query. Comma-separated terms are combined with OR; " diff --git a/django/interfaces/api/schemas/sta/processing_level.py b/django/interfaces/api/schemas/sta/processing_level.py index 29d5cf13..00b3ef0e 100644 --- a/django/interfaces/api/schemas/sta/processing_level.py +++ b/django/interfaces/api/schemas/sta/processing_level.py @@ -11,9 +11,12 @@ BasePatchBody, BaseQueryParameters, CollectionQueryParameters, + ExtentQueryParameters, WorkspaceResponse, split_comma_separated, comma_array_schema, + split_sortby, + sortby_array_schema, ) @@ -68,10 +71,12 @@ class ProcessingLevelItemQueryParameters(ProcessingLevelFilterFields, BaseQueryP pass -class ProcessingLevelQueryParameters(ProcessingLevelFilterFields, CollectionQueryParameters): - sortby: Optional[list[ProcessingLevelSortByFields]] = Query( - [], description="Select one or more fields to sort the response by." - ) +class ProcessingLevelQueryParameters(ProcessingLevelFilterFields, CollectionQueryParameters, ExtentQueryParameters): + sortby: Annotated[ + Optional[list[ProcessingLevelSortByFields]], + BeforeValidator(split_sortby), + WithJsonSchema(sortby_array_schema(ProcessingLevelSortByFields)), + ] = Query([], description="Select one or more fields to sort the response by.") q: Optional[str] = Query( None, description="Full-text search query. Comma-separated terms are combined with OR; " diff --git a/django/interfaces/api/schemas/sta/result_qualifier.py b/django/interfaces/api/schemas/sta/result_qualifier.py index ca498ce4..cfb3c812 100644 --- a/django/interfaces/api/schemas/sta/result_qualifier.py +++ b/django/interfaces/api/schemas/sta/result_qualifier.py @@ -11,9 +11,12 @@ BasePatchBody, BaseQueryParameters, CollectionQueryParameters, + ExtentQueryParameters, WorkspaceResponse, split_comma_separated, comma_array_schema, + split_sortby, + sortby_array_schema, ) @@ -71,11 +74,13 @@ class ResultQualifierItemQueryParameters( class ResultQualifierQueryParameters( - ResultQualifierFilterFields, CollectionQueryParameters + ResultQualifierFilterFields, CollectionQueryParameters, ExtentQueryParameters ): - sortby: Optional[list[ResultQualifierSortByFields]] = Query( - [], description="Select one or more fields to sort the response by." - ) + sortby: Annotated[ + Optional[list[ResultQualifierSortByFields]], + BeforeValidator(split_sortby), + WithJsonSchema(sortby_array_schema(ResultQualifierSortByFields)), + ] = Query([], description="Select one or more fields to sort the response by.") q: Optional[str] = Query( None, description="Full-text search query. Comma-separated terms are combined with OR; " diff --git a/django/interfaces/api/schemas/sta/unit.py b/django/interfaces/api/schemas/sta/unit.py index 6d590e31..7a476c0e 100644 --- a/django/interfaces/api/schemas/sta/unit.py +++ b/django/interfaces/api/schemas/sta/unit.py @@ -12,9 +12,12 @@ BasePatchBody, BaseQueryParameters, CollectionQueryParameters, + ExtentQueryParameters, WorkspaceResponse, split_comma_separated, comma_array_schema, + split_sortby, + sortby_array_schema, ) from interfaces.api.schemas.sta.vocabulary import VocabularyResponse @@ -76,10 +79,12 @@ class UnitItemQueryParameters(UnitFilterFields, BaseQueryParameters): pass -class UnitQueryParameters(UnitFilterFields, CollectionQueryParameters): - sortby: Optional[list[UnitSortByFields]] = Query( - [], description="Select one or more fields to sort the response by." - ) +class UnitQueryParameters(UnitFilterFields, CollectionQueryParameters, ExtentQueryParameters): + sortby: Annotated[ + Optional[list[UnitSortByFields]], + BeforeValidator(split_sortby), + WithJsonSchema(sortby_array_schema(UnitSortByFields)), + ] = Query([], description="Select one or more fields to sort the response by.") q: Optional[str] = Query( None, description="Full-text search query. Comma-separated terms are combined with OR; " diff --git a/django/interfaces/api/schemas/sta/vocabulary.py b/django/interfaces/api/schemas/sta/vocabulary.py index 530c40df..e2ac47ed 100644 --- a/django/interfaces/api/schemas/sta/vocabulary.py +++ b/django/interfaces/api/schemas/sta/vocabulary.py @@ -11,8 +11,11 @@ BasePatchBody, BaseQueryParameters, CollectionQueryParameters, + ExtentQueryParameters, split_comma_separated, comma_array_schema, + split_sortby, + sortby_array_schema, ) @@ -52,11 +55,13 @@ class VocabularyItemQueryParameters( class VocabularyQueryParameters( - VocabularyFilterFields, CollectionQueryParameters + VocabularyFilterFields, CollectionQueryParameters, ExtentQueryParameters ): - sortby: Optional[list[VocabularySortByFields]] = Query( - [], description="Select one or more fields to sort the response by." - ) + sortby: Annotated[ + Optional[list[VocabularySortByFields]], + BeforeValidator(split_sortby), + WithJsonSchema(sortby_array_schema(VocabularySortByFields)), + ] = Query([], description="Select one or more fields to sort the response by.") q: Optional[str] = Query( None, description="Full-text search query. Comma-separated terms are combined with OR; " diff --git a/django/interfaces/api/service.py b/django/interfaces/api/service.py index f2281532..37068e5e 100644 --- a/django/interfaces/api/service.py +++ b/django/interfaces/api/service.py @@ -8,12 +8,14 @@ from django.contrib.postgres.search import SearchQuery, SearchRank from django.core.exceptions import EmptyResultSet from django.db import connection -from django.db.models import QuerySet, Model, Q, F +from django.db.models import QuerySet, Model, Q, F, Exists, OuterRef from core.iam.models import Workspace, ServiceAccount from core.iam.permissions.anonymous import AnonymousPrincipal +from core.sta.models import Datastream from interfaces.api.http.errors import BadRequestError, NotFoundError -from interfaces.api.schemas.base import PaginationMeta +from interfaces.api.schemas.base import PaginationMeta, clamp_limit +from interfaces.api.schemas.extent import BoundingBox, TimeInterval User = get_user_model() @@ -81,6 +83,138 @@ def apply_filters(queryset: QuerySet, field_name: str, values: Optional[Any]): else: return queryset.filter(**{field_name: values}) + @staticmethod + def bbox_q( + bbox: BoundingBox, + latitude_field: str = "latitude", + longitude_field: str = "longitude", + ) -> Q: + """ + Builds a Q object matching points inside a bounding box, boundaries included. A box + that crosses the antimeridian matches longitudes east of its west edge or west of its + east edge. + """ + + latitude_q = Q( + **{f"{latitude_field}__gte": bbox.south, f"{latitude_field}__lte": bbox.north} + ) + + if bbox.crosses_antimeridian: + longitude_q = Q(**{f"{longitude_field}__gte": bbox.west}) | Q( + **{f"{longitude_field}__lte": bbox.east} + ) + else: + longitude_q = Q( + **{f"{longitude_field}__gte": bbox.west, f"{longitude_field}__lte": bbox.east} + ) + + return latitude_q & longitude_q + + @classmethod + def apply_bbox( + cls, + queryset: QuerySet, + bbox: Optional[BoundingBox], + latitude_field: str = "latitude", + longitude_field: str = "longitude", + ) -> QuerySet: + """Filters a queryset to points inside a bounding box. A missing box leaves it unfiltered.""" + + if bbox is None: + return queryset + + return queryset.filter(cls.bbox_q(bbox, latitude_field, longitude_field)) + + @staticmethod + def datetime_instant_q(interval: TimeInterval, field: str) -> Q: + """ + Builds a Q object matching items whose instant lies inside a datetime interval, bounds + included. Items without an instant match any interval (OGC API - Features Core Req 26C). + """ + + q = Q() + if interval.start is not None: + q &= Q(**{f"{field}__gte": interval.start}) + if interval.end is not None: + q &= Q(**{f"{field}__lte": interval.end}) + + return q | Q(**{f"{field}__isnull": True}) + + @staticmethod + def datetime_interval_q(interval: TimeInterval, start_field: str, end_field: str) -> Q: + """ + Builds a Q object matching items whose [start, end] interval intersects a datetime + interval, bounds included (OGC API - Features Core Req 26). A missing start or end on + an item is treated as open, so items without an interval match any datetime (Req 26C). + """ + + q = Q() + if interval.end is not None: + q &= Q(**{f"{start_field}__lte": interval.end}) | Q(**{f"{start_field}__isnull": True}) + if interval.start is not None: + q &= Q(**{f"{end_field}__gte": interval.start}) | Q(**{f"{end_field}__isnull": True}) + + return q + + @classmethod + def apply_datetime_instant( + cls, queryset: QuerySet, interval: Optional[TimeInterval], field: str + ) -> QuerySet: + """Filters a queryset to items whose instant lies inside a datetime interval.""" + + if interval is None: + return queryset + + return queryset.filter(cls.datetime_instant_q(interval, field)) + + @classmethod + def apply_datetime_interval( + cls, + queryset: QuerySet, + interval: Optional[TimeInterval], + start_field: str, + end_field: str, + ) -> QuerySet: + """Filters a queryset to items whose interval intersects a datetime interval.""" + + if interval is None: + return queryset + + return queryset.filter(cls.datetime_interval_q(interval, start_field, end_field)) + + @classmethod + def apply_visible_datastream_datetime( + cls, + principal: User | ServiceAccount | AnonymousPrincipal, + queryset, + interval: Optional[TimeInterval], + datastream_field: str, + ): + """ + Filters items by the observed time of the datastreams the principal can view, where + datastream_field links a datastream to the item's pk. An item matches when one of those + datastreams overlaps the interval, or when none of them has observations, since an item + without a time matches any datetime (OGC API - Features Core Req 26C). Datastreams the + principal can't view never affect the result. + """ + + if interval is None: + return queryset + + observed_datastreams = principal.filter_by_permission( + Datastream.objects.filter( + **{datastream_field: OuterRef("pk")}, phenomenon_begin_time__isnull=False + ), + "can_view", + ) + overlapping_datastreams = cls.apply_datetime_interval( + observed_datastreams, interval, "phenomenon_begin_time", "phenomenon_end_time" + ) + + return queryset.filter( + Exists(overlapping_datastreams) | ~Exists(observed_datastreams) + ) + @staticmethod def apply_sorting( queryset: QuerySet, @@ -298,8 +432,8 @@ def apply_pagination( raise BadRequestError("Offset must be >= 0.") if limit < 0: raise BadRequestError("Limit must be >= 0.") - if limit > 100000: - raise BadRequestError("Limit must be <= 100000.") + + limit = clamp_limit(limit) if count is None: count = queryset.count() @@ -337,6 +471,20 @@ def resolve_count(cls, queryset: QuerySet, threshold: int = 10_000) -> int: return estimated + @staticmethod + def get_linked_resource_by_id( + linked_resource_model: Type[Model], + parent_field: str, + parent: Model, + linked_resource_id: uuid.UUID, + ): + try: + return linked_resource_model.objects.get( + **{parent_field: parent}, id=linked_resource_id + ) + except linked_resource_model.DoesNotExist: + raise NotFoundError("Linked resource does not exist") + @staticmethod def create_linked_resource( linked_resource_model: Type[Model], diff --git a/django/interfaces/api/services/etl/task.py b/django/interfaces/api/services/etl/task.py index 55826273..1d6ca336 100644 --- a/django/interfaces/api/services/etl/task.py +++ b/django/interfaces/api/services/etl/task.py @@ -4,6 +4,7 @@ from django.db import transaction from django.db.models import Count from django.contrib.auth import get_user_model +from pydantic.alias_generators import to_snake from core.types import Unset from core.iam.models import ServiceAccount @@ -103,7 +104,7 @@ def list( "latest_run_finished_at_min", "latest_run_finished_at_max", ] if any(field in filtering for field in latest_run_fields) or any( - term.lstrip("-") in self.latest_run_filter_fields for term in sortby + to_snake(term.lstrip("-")) in self.latest_run_filter_fields for term in sortby ): queryset = self.annotate_latest_run(queryset, fields=self.latest_run_filter_fields) diff --git a/django/interfaces/api/services/iam/workspace.py b/django/interfaces/api/services/iam/workspace.py index 89e1741e..c1c369c6 100644 --- a/django/interfaces/api/services/iam/workspace.py +++ b/django/interfaces/api/services/iam/workspace.py @@ -2,14 +2,17 @@ from typing import Optional, get_args from django.contrib.auth import get_user_model -from django.db.models import Q +from django.db.models import Exists, OuterRef, Q from django.db.utils import IntegrityError from core.iam.models import Workspace, ServiceAccount +from core.sta.models import MonitoringSite from core.iam.permissions.anonymous import AnonymousPrincipal from interfaces.api.service import APIService from interfaces.api.http.errors import BadRequestError, ConflictError, PermissionDeniedError from interfaces.api.schemas import ( + BoundingBox, + TimeInterval, WorkspacePostBody, WorkspacePatchBody, WorkspaceTransferBody, @@ -49,6 +52,31 @@ def attach_role_and_transfer_fields( return workspace + @classmethod + def apply_visible_site_bbox( + cls, + principal: User | ServiceAccount | AnonymousPrincipal, + queryset, + bbox: Optional[BoundingBox], + ): + """ + Filters workspaces by the monitoring sites the principal can view. A workspace matches + when one of those sites lies inside the box, or when the principal can view none of its + sites, since a workspace without a visible location matches any box (OGC API - Features + Core Req 24C). Sites the principal can't view never affect the result. + """ + + if bbox is None: + return queryset + + visible_sites = principal.filter_by_permission( + MonitoringSite.objects.filter(workspace_id=OuterRef("pk")), "can_view" + ) + + return queryset.filter( + Exists(cls.apply_bbox(visible_sites, bbox)) | ~Exists(visible_sites) + ) + def list( self, principal: User | ServiceAccount | AnonymousPrincipal, @@ -57,6 +85,8 @@ def list( sortby: Optional[list[str]] = None, filtering: Optional[dict] = None, include: Optional[list[str]] = None, + bbox: Optional[BoundingBox] = None, + datetime_interval: Optional[TimeInterval] = None, ): requested_includes = self.resolve_include_set(include) queryset = Workspace.objects @@ -90,6 +120,10 @@ def list( else: queryset = self.apply_filters(queryset, field, filtering[field]) + queryset = self.apply_visible_site_bbox(principal, queryset, bbox) + queryset = self.apply_visible_datastream_datetime( + principal, queryset, datetime_interval, "monitoring_site__workspace_id" + ) queryset, has_search = self.apply_search(queryset, filtering.get("q")) queryset = self.apply_sorting( queryset, diff --git a/django/interfaces/api/services/monitoring/task.py b/django/interfaces/api/services/monitoring/task.py index 5eb569c8..0a761db1 100644 --- a/django/interfaces/api/services/monitoring/task.py +++ b/django/interfaces/api/services/monitoring/task.py @@ -4,6 +4,7 @@ from django.db import transaction from django.db.models import Count from django.contrib.auth import get_user_model +from pydantic.alias_generators import to_snake from core.types import Unset from core.iam.models import ServiceAccount @@ -136,7 +137,7 @@ def list( sortby = sortby or [] if "latest_run_status" in filtering or any( - term.lstrip("-") in self.latest_run_filter_fields for term in sortby + to_snake(term.lstrip("-")) in self.latest_run_filter_fields for term in sortby ): queryset = self.annotate_latest_run(queryset, fields=self.latest_run_filter_fields) diff --git a/django/interfaces/api/services/products/task.py b/django/interfaces/api/services/products/task.py index d4e65fe4..dd772cab 100644 --- a/django/interfaces/api/services/products/task.py +++ b/django/interfaces/api/services/products/task.py @@ -4,6 +4,7 @@ from typing import Optional, Literal, get_args from django.db import transaction from django.contrib.auth import get_user_model +from pydantic.alias_generators import to_snake from core.types import Unset from core.iam.models import ServiceAccount @@ -133,7 +134,7 @@ def list( sortby = sortby or [] if "latest_run_status" in filtering or any( - term.lstrip("-") in self.latest_run_filter_fields for term in sortby + to_snake(term.lstrip("-")) in self.latest_run_filter_fields for term in sortby ): queryset = self.annotate_latest_run(queryset, fields=self.latest_run_filter_fields) diff --git a/django/interfaces/api/services/quality/history.py b/django/interfaces/api/services/quality/history.py index bbe87526..9910fada 100644 --- a/django/interfaces/api/services/quality/history.py +++ b/django/interfaces/api/services/quality/history.py @@ -10,6 +10,7 @@ from processing.quality.models import QCHistory from interfaces.api.http.errors import ConflictError, PermissionDeniedError, NotFoundError from interfaces.api.service import APIService +from interfaces.api.schemas import TimeInterval from interfaces.api.schemas.quality.history import ( QualityControlHistorySortByFields, QualityControlHistoryResponse, @@ -85,6 +86,7 @@ def list( sortby: Optional[list[str]] = None, filtering: Optional[dict] = None, include: Optional[list[str]] = None, + datetime_interval: Optional[TimeInterval] = None, ): requested_includes = self.resolve_include_set(include) queryset = QCHistory.objects @@ -93,6 +95,10 @@ def list( if field in filtering: queryset = self.apply_filters(queryset, field, filtering[field]) + queryset = self.apply_datetime_interval( + queryset, datetime_interval, "phenomenon_time_start", "phenomenon_time_end" + ) + queryset = self.apply_sorting( queryset, sortby, diff --git a/django/interfaces/api/services/sta/datastream.py b/django/interfaces/api/services/sta/datastream.py index 2dd20573..dacaec28 100644 --- a/django/interfaces/api/services/sta/datastream.py +++ b/django/interfaces/api/services/sta/datastream.py @@ -18,6 +18,8 @@ from interfaces.api.http.errors import BadRequestError, ConflictError, NotFoundError, PermissionDeniedError from interfaces.api.service import APIService from interfaces.api.schemas import ( + BoundingBox, + TimeInterval, DatastreamPostBody, DatastreamPatchBody, LinkedResourcePostBody, @@ -101,6 +103,14 @@ def apply_tag_filter(queryset, tags: list[str]): return queryset + @classmethod + def apply_site_bbox(cls, queryset, bbox: Optional[BoundingBox]): + """Filters datastreams to those whose monitoring site lies inside a bounding box.""" + + return cls.apply_bbox( + queryset, bbox, "monitoring_site__latitude", "monitoring_site__longitude" + ) + def list( self, principal: User | ServiceAccount | AnonymousPrincipal, @@ -109,6 +119,8 @@ def list( sortby: Optional[list[str]] = None, filtering: Optional[dict] = None, include: Optional[list[str]] = None, + bbox: Optional[BoundingBox] = None, + datetime_interval: Optional[TimeInterval] = None, ): requested_includes = self.resolve_include_set(include) queryset = Datastream.objects @@ -128,14 +140,6 @@ def list( "is_private", "value_count__lte", "value_count__gte", - "phenomenon_begin_time__lte", - "phenomenon_begin_time__gte", - "phenomenon_end_time__lte", - "phenomenon_end_time__gte", - "result_begin_time__lte", - "result_begin_time__gte", - "result_end_time__lte", - "result_end_time__gte", ]: if field in filtering: if field == "is_private": @@ -153,6 +157,10 @@ def list( else: queryset = self.apply_filters(queryset, field, filtering[field]) + queryset = self.apply_site_bbox(queryset, bbox) + queryset = self.apply_datetime_interval( + queryset, datetime_interval, "phenomenon_begin_time", "phenomenon_end_time" + ) queryset = self.apply_tag_filter(queryset, filtering.get("tag")) queryset, has_search = self.apply_search(queryset, filtering.get("q")) queryset = self.apply_sorting( @@ -428,6 +436,8 @@ def get_linked_resources( self, principal: User | ServiceAccount | AnonymousPrincipal, uid: uuid.UUID, + offset: Optional[int] = None, + limit: Optional[int] = None, filtering: Optional[dict] = None, ): datastream = self.get_datastream_for_action( @@ -439,7 +449,27 @@ def get_linked_resources( if filtering.get("type"): queryset = self.apply_filters(queryset, "type", filtering["type"]) - return queryset.all() + queryset = self.apply_sorting(queryset, [], [], default_sortby=("name",)) + queryset, meta = self.apply_pagination(queryset, offset, limit) + + return {"data": list(queryset), "meta": meta} + + def get_linked_resource( + self, + principal: User | ServiceAccount | AnonymousPrincipal, + uid: uuid.UUID, + linked_resource_id: uuid.UUID, + ): + datastream = self.get_datastream_for_action( + principal=principal, uid=uid, action="view" + ) + + return self.get_linked_resource_by_id( + linked_resource_model=DatastreamLinkedResource, + parent_field="datastream", + parent=datastream, + linked_resource_id=linked_resource_id, + ) def add_linked_resource( self, diff --git a/django/interfaces/api/services/sta/monitoring_site.py b/django/interfaces/api/services/sta/monitoring_site.py index 70ad8997..2847a3ae 100644 --- a/django/interfaces/api/services/sta/monitoring_site.py +++ b/django/interfaces/api/services/sta/monitoring_site.py @@ -4,7 +4,7 @@ from typing import Optional, Literal, get_args from django.contrib.auth import get_user_model from django.db import IntegrityError -from django.db.models import Count, QuerySet, Q, FloatField, Subquery, OuterRef, IntegerField +from django.db.models import Count, QuerySet, FloatField, Subquery, OuterRef, IntegerField from django.db.models.functions import Cast, Coalesce from django.utils import timezone @@ -21,6 +21,8 @@ MonitoringSiteLinkedResource, ) from interfaces.api.schemas import ( + BoundingBox, + TimeInterval, MonitoringSiteResponse, MonitoringSitePostBody, MonitoringSitePatchBody, @@ -90,10 +92,6 @@ def _include_query_hints( return select_paths, [] - @classmethod - def apply_bbox_filter(cls, queryset, bbox: Optional[list[str]]): - return cls.apply_marker_bbox_filter(queryset, bbox) - @staticmethod def apply_tag_filter(queryset, tags: list[str]): if not tags: @@ -108,57 +106,6 @@ def apply_tag_filter(queryset, tags: list[str]): return queryset - @staticmethod - def parse_bbox_filters(bbox: Optional[list[str]]) -> list[tuple[float, float, float, float]]: - parsed_bbox_filters: list[tuple[float, float, float, float]] = [] - - if not bbox: - return parsed_bbox_filters - - for bbox_str in bbox: - try: - parts = [float(x) for x in bbox_str.split(",")] - except ValueError: - raise BadRequestError("Bounding box must contain only numeric values") - - if len(parts) != 4: - raise BadRequestError( - "Bounding box must have exactly 4 comma-separated values: min_lon,min_lat,max_lon,max_lat" - ) - - min_lon, min_lat, max_lon, max_lat = parts - - if min_lon > max_lon or min_lat > max_lat: - raise BadRequestError( - "Invalid bounding box coordinates: min must be less than or equal to max" - ) - - parsed_bbox_filters.append((min_lon, min_lat, max_lon, max_lat)) - - return parsed_bbox_filters - - @classmethod - def apply_marker_bbox_filter( - cls, - queryset: QuerySet, - bbox: Optional[list[str]], - ) -> QuerySet: - parsed_bbox_filters = cls.parse_bbox_filters(bbox) - - if not parsed_bbox_filters: - return queryset - - bbox_filter = Q() - for min_lon, min_lat, max_lon, max_lat in parsed_bbox_filters: - bbox_filter |= Q( - longitude__gte=min_lon, - longitude__lte=max_lon, - latitude__gte=min_lat, - latitude__lte=max_lat, - ) - - return queryset.filter(bbox_filter) - @staticmethod def apply_marker_filters(queryset: QuerySet, filtering: Optional[dict] = None) -> QuerySet: filtering = filtering or {} @@ -241,7 +188,10 @@ def serialize_site_summary_rows(site_rows) -> list[dict]: @classmethod def filter_cached_markers( - cls, markers: list[dict], filtering: Optional[dict] = None + cls, + markers: list[dict], + filtering: Optional[dict] = None, + bbox: Optional[BoundingBox] = None, ) -> list[dict]: filtering = filtering or {} filtered_markers = markers @@ -262,21 +212,18 @@ def filter_cached_markers( if marker["type"] in site_types ] - parsed_bbox_filters = cls.parse_bbox_filters(filtering.get("bbox")) - if parsed_bbox_filters: + if bbox is not None: filtered_markers = [ marker for marker in filtered_markers - if any( - min_lon <= marker["longitude"] <= max_lon - and min_lat <= marker["latitude"] <= max_lat - for min_lon, min_lat, max_lon, max_lat in parsed_bbox_filters - ) + if bbox.contains(marker["longitude"], marker["latitude"]) ] return filtered_markers - def get_public_markers(self, filtering: Optional[dict] = None) -> list[dict]: + def get_public_markers( + self, filtering: Optional[dict] = None, bbox: Optional[BoundingBox] = None + ) -> list[dict]: public_markers = get_public_monitoring_site_markers_cache() if public_markers is None: @@ -286,12 +233,13 @@ def get_public_markers(self, filtering: Optional[dict] = None) -> list[dict]: public_markers = self.serialize_marker_rows(public_marker_queryset) set_public_monitoring_site_markers_cache(public_markers) - return self.filter_cached_markers(public_markers, filtering=filtering) + return self.filter_cached_markers(public_markers, filtering=filtering, bbox=bbox) def get_private_markers( self, principal: User | ServiceAccount | AnonymousPrincipal, filtering: Optional[dict] = None, + bbox: Optional[BoundingBox] = None, ) -> list[dict]: if not principal.is_authenticated: return [] @@ -303,10 +251,7 @@ def get_private_markers( private_marker_queryset, filtering=filtering, ) - private_marker_queryset = self.apply_marker_bbox_filter( - private_marker_queryset, - filtering.get("bbox") if filtering else None, - ) + private_marker_queryset = self.apply_bbox(private_marker_queryset, bbox) return self.serialize_marker_rows( self.get_marker_values(private_marker_queryset.order_by("id").distinct()) @@ -316,9 +261,12 @@ def list_markers( self, principal: User | ServiceAccount | AnonymousPrincipal, filtering: Optional[dict] = None, + bbox: Optional[BoundingBox] = None, ): - public_markers = self.get_public_markers(filtering=filtering) - private_markers = self.get_private_markers(principal=principal, filtering=filtering) + public_markers = self.get_public_markers(filtering=filtering, bbox=bbox) + private_markers = self.get_private_markers( + principal=principal, filtering=filtering, bbox=bbox + ) markers = public_markers + private_markers markers.sort(key=lambda marker: marker["id"]) @@ -399,6 +347,8 @@ def list( sortby: Optional[list[str]] = None, filtering: Optional[dict] = None, include: Optional[list[str]] = None, + bbox: Optional[BoundingBox] = None, + datetime_interval: Optional[TimeInterval] = None, ): requested_includes = self.resolve_include_set(include) queryset = MonitoringSite.objects @@ -422,7 +372,10 @@ def list( else: queryset = self.apply_filters(queryset, field, filtering[field]) - queryset = self.apply_bbox_filter(queryset, filtering.get("bbox")) + queryset = self.apply_bbox(queryset, bbox) + queryset = self.apply_visible_datastream_datetime( + principal, queryset, datetime_interval, "monitoring_site_id" + ) queryset = self.apply_tag_filter(queryset, filtering.get("tag")) queryset, has_search = self.apply_search(queryset, filtering.get("q")) queryset = self.apply_sorting( @@ -568,6 +521,8 @@ def get_linked_resources( self, principal: User | ServiceAccount | AnonymousPrincipal, uid: uuid.UUID, + offset: Optional[int] = None, + limit: Optional[int] = None, filtering: Optional[dict] = None, ): monitoring_site = self.get_monitoring_site_for_action( @@ -579,7 +534,27 @@ def get_linked_resources( if filtering.get("type"): queryset = self.apply_filters(queryset, "type", filtering["type"]) - return queryset.all() + queryset = self.apply_sorting(queryset, [], [], default_sortby=("name",)) + queryset, meta = self.apply_pagination(queryset, offset, limit) + + return {"data": list(queryset), "meta": meta} + + def get_linked_resource( + self, + principal: User | ServiceAccount | AnonymousPrincipal, + uid: uuid.UUID, + linked_resource_id: uuid.UUID, + ): + monitoring_site = self.get_monitoring_site_for_action( + principal=principal, uid=uid, action="view" + ) + + return self.get_linked_resource_by_id( + linked_resource_model=MonitoringSiteLinkedResource, + parent_field="monitoring_site", + parent=monitoring_site, + linked_resource_id=linked_resource_id, + ) def add_linked_resource( self, diff --git a/django/interfaces/api/services/sta/observation.py b/django/interfaces/api/services/sta/observation.py index 1fb87131..c54f85b1 100644 --- a/django/interfaces/api/services/sta/observation.py +++ b/django/interfaces/api/services/sta/observation.py @@ -17,6 +17,7 @@ from interfaces.api.service import APIService from interfaces.api.services.sta.datastream import DatastreamAPIService from interfaces.api.http.errors import BadRequestError, ConflictError, PermissionDeniedError, NotFoundError +from interfaces.api.schemas import BoundingBox, TimeInterval from interfaces.api.schemas.sta.observation import ( ObservationFields, ObservationSortByFields, @@ -122,10 +123,12 @@ def _validate_result_qualifier_codes( def sum_datastream_value_count( principal: User | ServiceAccount | AnonymousPrincipal, datastream_ids: list[uuid.UUID], + bbox: Optional[BoundingBox] = None, ) -> int: """ Exact observation count for the given datastreams (or every datastream the - principal can view if none are given), via Datastream's maintained value_count + principal can view if none are given), optionally limited to datastreams whose + monitoring site lies inside a bounding box, via Datastream's maintained value_count rather than a COUNT(*) over Observation. """ @@ -134,6 +137,7 @@ def sum_datastream_value_count( if datastream_ids else Datastream.objects ) + queryset = DatastreamAPIService.apply_site_bbox(queryset, bbox) queryset = principal.filter_by_permission(queryset, "can_view") return queryset.aggregate(total=Sum("value_count"))["total"] or 0 @@ -148,6 +152,8 @@ def list( filtering: Optional[dict] = None, response_format: Optional[str] = None, include: Optional[list[str]] = None, + bbox: Optional[BoundingBox] = None, + datetime_interval: Optional[TimeInterval] = None, ): requested_includes = self.resolve_include_set(include) queryset = Observation.objects @@ -159,9 +165,10 @@ def list( "format=row and format=column require exactly one datastream_id filter value" ) - for field in ["datastream_id", "phenomenon_time__lte", "phenomenon_time__gte"]: - if field in filtering: - queryset = self.apply_filters(queryset, field, filtering[field]) + if "datastream_id" in filtering: + queryset = self.apply_filters(queryset, "datastream_id", filtering["datastream_id"]) + + queryset = self.apply_datetime_instant(queryset, datetime_interval, "phenomenon_time") if filtering.get("result_qualifier_codes"): code_filter = Q() @@ -169,16 +176,19 @@ def list( code_filter |= Q(result_qualifiers__contains=[code]) queryset = queryset.filter(code_filter) + if bbox is not None: + queryset = queryset.filter( + datastream_id__in=DatastreamAPIService.apply_site_bbox( + Datastream.objects, bbox + ).values("id") + ) + queryset = principal.filter_by_permission(queryset, "can_view") count = ( self.resolve_count(queryset) - if bool( - filtering.get("phenomenon_time__lte") - or filtering.get("phenomenon_time__gte") - or filtering.get("result_qualifier_codes") - ) - else self.sum_datastream_value_count(principal, datastream_ids) + if datetime_interval is not None or filtering.get("result_qualifier_codes") + else self.sum_datastream_value_count(principal, datastream_ids, bbox) ) # TODO: Can't really fix this until PostgreSQL 18 UUID v7 support diff --git a/django/interfaces/api/urls.py b/django/interfaces/api/urls.py index 2b31a088..66f00d7f 100644 --- a/django/interfaces/api/urls.py +++ b/django/interfaces/api/urls.py @@ -1,4 +1,3 @@ -from ninja import NinjaAPI from ninja.throttling import AnonRateThrottle, AuthRateThrottle from django.conf import settings from django.urls import path, include @@ -6,95 +5,44 @@ from hydroserver import __version__ from interfaces.api.http import handlers +from interfaces.api.http.api import HydroServerNinjaAPI +from interfaces.api.http.query_params import reject_unknown_query_params from interfaces.api.http.renderer import ORJSONRenderer -from interfaces.api.views import ( - workspace_router, - role_router, - monitoring_site_router, - monitoring_site_type_router, - linked_resource_type_router, - observed_property_router, - observed_property_type_router, - processing_level_router, - result_qualifier_router, - sampled_medium_router, - aggregation_statistic_router, - datastream_status_router, - method_router, - method_type_router, - unit_router, - unit_type_router, - datastream_router, - observation_router, - data_connection_router, - etl_task_router, - etl_mapping_router, - rating_curve_router, - data_product_transformation_router, - data_product_task_router, - monitoring_task_router, - monitoring_rule_router, - qc_history_router, - qc_session_router, - qc_operation_router, -) +from interfaces.api.collections import COLLECTIONS +from interfaces.api.views import qc_history_router, qc_session_router, qc_operation_router +from interfaces.api.views.ogc import API_DESCRIPTION, API_TITLE, ogc_router rate_limits = settings.API_RATE_LIMITS or {} throttle_classes = {"anonymous": AnonRateThrottle, "authenticated": AuthRateThrottle} -api = NinjaAPI( - title="HydroServer Data Management API", +api = HydroServerNinjaAPI( + title=API_TITLE, + description=API_DESCRIPTION, version=__version__, - urls_namespace="data", + urls_namespace="ogc", docs_decorator=ensure_csrf_cookie, renderer=ORJSONRenderer(), throttle=[cls(rate_limits[k]) for k, cls in throttle_classes.items() if rate_limits.get(k)], ) handlers.register(api) +api.add_decorator(reject_unknown_query_params, mode="view") -api.add_router("workspaces", workspace_router) -api.add_router("roles", role_router) - -api.add_router("monitoring-sites", monitoring_site_router) -api.add_router("monitoring-site-types", monitoring_site_type_router) -api.add_router("linked-resource-types", linked_resource_type_router) -api.add_router("datastreams", datastream_router) -api.add_router("datastream-statuses", datastream_status_router) -api.add_router("aggregation-statistics", aggregation_statistic_router) -api.add_router("observations", observation_router) -api.add_router("observed-properties", observed_property_router) -api.add_router("observed-property-types", observed_property_type_router) -api.add_router("units", unit_router) -api.add_router("unit-types", unit_type_router) -api.add_router("methods", method_router) -api.add_router("method-types", method_type_router) -api.add_router("processing-levels", processing_level_router) -api.add_router("result-qualifiers", result_qualifier_router) -api.add_router("sampled-mediums", sampled_medium_router) - -api.add_router("etl-data-connections", data_connection_router) -api.add_router("etl-tasks", etl_task_router) -api.add_router("etl-mappings", etl_mapping_router) - -api.add_router("data-product-rating-curves", rating_curve_router) -api.add_router("data-product-tasks", data_product_task_router) -api.add_router("data-product-transformations", data_product_transformation_router) +api.add_router("", ogc_router) -api.add_router("monitoring-tasks", monitoring_task_router) -api.add_router("monitoring-rules", monitoring_rule_router) +for collection in COLLECTIONS: + api.add_router(f"collections/{collection.id}", collection.router) -api.add_router("quality-control/histories", qc_history_router) qc_history_router.add_router( - "/{history_id}/sessions", qc_session_router, tags=["Quality Control Sessions"] + "/items/{history_id}/sessions", qc_session_router, tags=["Quality Control Sessions"] ) qc_session_router.add_router( "/{session_id}/operations", qc_operation_router, tags=["Quality Control Operations"] ) urlpatterns = [ - path("data/", api.urls), + path("ogc/", api.urls), path("sensorthings/", include("sensorthings.versions.v1_1.urls")), ] diff --git a/django/interfaces/api/views/etl/data_connection.py b/django/interfaces/api/views/etl/data_connection.py index cc66f48f..107ead4e 100644 --- a/django/interfaces/api/views/etl/data_connection.py +++ b/django/interfaces/api/views/etl/data_connection.py @@ -22,7 +22,7 @@ @data_connection_router.get( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 200: PaginatedResponse[DataConnectionResponse], @@ -49,7 +49,7 @@ def get_data_connections( @data_connection_router.post( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 201: CreatedResponse, @@ -87,7 +87,7 @@ def create_data_connection( @data_connection_router.get( - "/{data_connection_id}", + "/items/{data_connection_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 200: ItemResponse[DataConnectionResponse], @@ -114,7 +114,7 @@ def get_data_connection( @data_connection_router.patch( - "/{data_connection_id}", + "/items/{data_connection_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, @@ -155,7 +155,7 @@ def update_data_connection( @data_connection_router.delete( - "/{data_connection_id}", + "/items/{data_connection_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, diff --git a/django/interfaces/api/views/etl/mapping.py b/django/interfaces/api/views/etl/mapping.py index db4c981e..ac0501e7 100644 --- a/django/interfaces/api/views/etl/mapping.py +++ b/django/interfaces/api/views/etl/mapping.py @@ -19,7 +19,7 @@ @etl_mapping_router.get( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 200: PaginatedResponse[EtlMappingResponse], @@ -46,7 +46,7 @@ def get_etl_mappings( @etl_mapping_router.post( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 201: CreatedResponse, @@ -70,7 +70,7 @@ def create_etl_mapping( @etl_mapping_router.get( - "/{mapping_id}", + "/items/{mapping_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 200: ItemResponse[EtlMappingResponse], @@ -95,7 +95,7 @@ def get_etl_mapping( @etl_mapping_router.patch( - "/{mapping_id}", + "/items/{mapping_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, @@ -123,7 +123,7 @@ def update_etl_mapping( @etl_mapping_router.delete( - "/{mapping_id}", + "/items/{mapping_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, diff --git a/django/interfaces/api/views/etl/task.py b/django/interfaces/api/views/etl/task.py index 88faa972..3bdabef0 100644 --- a/django/interfaces/api/views/etl/task.py +++ b/django/interfaces/api/views/etl/task.py @@ -1,7 +1,9 @@ import uuid +from django.http import HttpResponse from ninja import Router, Path, Query +from interfaces.api.http.links import build_self_link from interfaces.api.http.request import HydroServerHttpRequest from interfaces.auth.security import session_auth, oidc_auth, apikey_auth, basic_auth from processing.orchestration.models import TaskRun @@ -22,7 +24,7 @@ @etl_task_router.get( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 200: PaginatedResponse[EtlTaskResponse], @@ -50,7 +52,7 @@ def get_etl_tasks( @etl_task_router.post( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 201: CreatedResponse, @@ -73,7 +75,7 @@ def create_etl_task( @etl_task_router.get( - "/{task_id}", + "/items/{task_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 200: ItemResponse[EtlTaskResponse], @@ -98,7 +100,7 @@ def get_etl_task( @etl_task_router.patch( - "/{task_id}", + "/items/{task_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, @@ -124,7 +126,7 @@ def update_etl_task( @etl_task_router.delete( - "/{task_id}", + "/items/{task_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, @@ -151,10 +153,10 @@ def delete_etl_task( @etl_task_router.post( - "/{task_id}/trigger", + "/items/{task_id}/trigger", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ - 202: TaskRunResponse, + 202: ItemResponse[TaskRunResponse], 401: str, 403: str, 404: str, @@ -163,6 +165,7 @@ def delete_etl_task( ) def trigger_etl_task( request: HydroServerHttpRequest, + response: HttpResponse, task_id: Path[uuid.UUID], ): """ @@ -176,11 +179,14 @@ def trigger_etl_task( run = TaskRun.objects.create(task=etl_task, status="PENDING") run_etl_task.apply_async(kwargs={"task_id": str(etl_task.id), "run_id": str(run.id)}) - return 202, run + run_link = build_self_link(request, request.path_info.removesuffix("/trigger") + f"/runs/{run.id}") + response["Location"] = run_link.href + + return 202, {"data": run, "links": [run_link]} @etl_task_router.get( - "/{task_id}/runs", + "/items/{task_id}/runs", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 200: PaginatedResponse[TaskRunResponse], @@ -222,10 +228,10 @@ def get_etl_task_runs( @etl_task_router.get( - "/{task_id}/runs/{run_id}", + "/items/{task_id}/runs/{run_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ - 200: TaskRunResponse, + 200: ItemResponse[TaskRunResponse], 401: str, 403: str, 404: str, @@ -247,4 +253,4 @@ def get_etl_task_run( principal=request.principal, ) - return 200, run + return 200, {"data": run} diff --git a/django/interfaces/api/views/iam/role.py b/django/interfaces/api/views/iam/role.py index 62af2be4..46a89e28 100644 --- a/django/interfaces/api/views/iam/role.py +++ b/django/interfaces/api/views/iam/role.py @@ -18,7 +18,7 @@ @role_router.get( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth], response={ 200: PaginatedResponse[RoleResponse], @@ -44,7 +44,7 @@ def get_roles( @role_router.get( - "/{role_id}", + "/items/{role_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth], response={ 200: ItemResponse[RoleResponse], diff --git a/django/interfaces/api/views/iam/workspace.py b/django/interfaces/api/views/iam/workspace.py index 2a2133fe..f27094f7 100644 --- a/django/interfaces/api/views/iam/workspace.py +++ b/django/interfaces/api/views/iam/workspace.py @@ -25,7 +25,7 @@ @workspace_router.get( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth], response={ 200: PaginatedResponse[WorkspaceResponse], @@ -49,11 +49,13 @@ def get_workspaces( sortby=query.sortby, filtering=query.dict(exclude_unset=True), include=query.include, + bbox=query.bbox, + datetime_interval=query.datetime, ) @workspace_router.post( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 201: CreatedResponse, @@ -75,7 +77,7 @@ def create_workspace( @workspace_router.get( - "/{workspace_id}", + "/items/{workspace_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth], response={ 200: ItemResponse[WorkspaceResponse], @@ -101,7 +103,7 @@ def get_workspace( @workspace_router.patch( - "/{workspace_id}", + "/items/{workspace_id}", auth=[session_auth, oidc_auth, basic_auth], response={ 204: None, @@ -131,7 +133,7 @@ def update_workspace( @workspace_router.delete( - "/{workspace_id}", + "/items/{workspace_id}", auth=[session_auth, oidc_auth, basic_auth], response={ 204: None, @@ -150,7 +152,7 @@ def delete_workspace(request: HydroServerHttpRequest, workspace_id: Path[uuid.UU @workspace_router.post( - "/{workspace_id}/transfer", + "/items/{workspace_id}/transfer", auth=[session_auth, oidc_auth, basic_auth], response={ 201: str, @@ -176,7 +178,7 @@ def transfer_workspace( @workspace_router.put( - "/{workspace_id}/transfer", + "/items/{workspace_id}/transfer", auth=[session_auth, oidc_auth, basic_auth], response={ 200: str, @@ -200,7 +202,7 @@ def accept_workspace_transfer( @workspace_router.delete( - "/{workspace_id}/transfer", + "/items/{workspace_id}/transfer", auth=[session_auth, oidc_auth, basic_auth], response={ 200: str, @@ -224,8 +226,8 @@ def reject_workspace_transfer( workspace_router.add_router( - "{workspace_id}/collaborators", collaborator_router, tags=["Collaborators"] + "items/{workspace_id}/collaborators", collaborator_router, tags=["Collaborators"] ) workspace_router.add_router( - "{workspace_id}/service-accounts", service_account_router, tags=["Service Accounts"] + "items/{workspace_id}/service-accounts", service_account_router, tags=["Service Accounts"] ) diff --git a/django/interfaces/api/views/monitoring/rule.py b/django/interfaces/api/views/monitoring/rule.py index 2a4f3971..032ff305 100644 --- a/django/interfaces/api/views/monitoring/rule.py +++ b/django/interfaces/api/views/monitoring/rule.py @@ -19,7 +19,7 @@ @monitoring_rule_router.get( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 200: PaginatedResponse[MonitoringRuleResponse], @@ -48,7 +48,7 @@ def get_monitoring_rules( @monitoring_rule_router.post( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 201: CreatedResponse, @@ -74,7 +74,7 @@ def create_monitoring_rule( @monitoring_rule_router.get( - "/{rule_id}", + "/items/{rule_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 200: ItemResponse[MonitoringRuleResponse], @@ -101,7 +101,7 @@ def get_monitoring_rule( @monitoring_rule_router.patch( - "/{rule_id}", + "/items/{rule_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, @@ -131,7 +131,7 @@ def update_monitoring_rule( @monitoring_rule_router.delete( - "/{rule_id}", + "/items/{rule_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, diff --git a/django/interfaces/api/views/monitoring/task.py b/django/interfaces/api/views/monitoring/task.py index 2d5f50f2..a831497a 100644 --- a/django/interfaces/api/views/monitoring/task.py +++ b/django/interfaces/api/views/monitoring/task.py @@ -1,7 +1,9 @@ import uuid +from django.http import HttpResponse from ninja import Router, Path, Query +from interfaces.api.http.links import build_self_link from interfaces.api.http.request import HydroServerHttpRequest from interfaces.auth.security import session_auth, oidc_auth, apikey_auth, basic_auth from processing.orchestration.models import TaskRun @@ -22,7 +24,7 @@ @monitoring_task_router.get( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 200: PaginatedResponse[MonitoringTaskResponse], @@ -50,7 +52,7 @@ def get_monitoring_tasks( @monitoring_task_router.post( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 201: CreatedResponse, @@ -73,7 +75,7 @@ def create_monitoring_task( @monitoring_task_router.get( - "/{task_id}", + "/items/{task_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 200: ItemResponse[MonitoringTaskResponse], @@ -98,7 +100,7 @@ def get_monitoring_task( @monitoring_task_router.patch( - "/{task_id}", + "/items/{task_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, @@ -124,7 +126,7 @@ def update_monitoring_task( @monitoring_task_router.delete( - "/{task_id}", + "/items/{task_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, @@ -151,10 +153,10 @@ def delete_monitoring_task( @monitoring_task_router.post( - "/{task_id}/trigger", + "/items/{task_id}/trigger", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ - 202: TaskRunResponse, + 202: ItemResponse[TaskRunResponse], 401: str, 403: str, 404: str, @@ -163,6 +165,7 @@ def delete_monitoring_task( ) def trigger_monitoring_task( request: HydroServerHttpRequest, + response: HttpResponse, task_id: Path[uuid.UUID], ): """ @@ -176,11 +179,14 @@ def trigger_monitoring_task( run = TaskRun.objects.create(task=task, status="PENDING") run_monitoring_task.apply_async(kwargs={"task_id": str(task.id), "run_id": str(run.id)}) - return 202, run + run_link = build_self_link(request, request.path_info.removesuffix("/trigger") + f"/runs/{run.id}") + response["Location"] = run_link.href + + return 202, {"data": run, "links": [run_link]} @monitoring_task_router.get( - "/{task_id}/runs", + "/items/{task_id}/runs", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 200: PaginatedResponse[TaskRunResponse], @@ -222,10 +228,10 @@ def get_monitoring_task_runs( @monitoring_task_router.get( - "/{task_id}/runs/{run_id}", + "/items/{task_id}/runs/{run_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ - 200: TaskRunResponse, + 200: ItemResponse[TaskRunResponse], 401: str, 403: str, 404: str, @@ -247,4 +253,4 @@ def get_monitoring_task_run( principal=request.principal, ) - return 200, run + return 200, {"data": run} diff --git a/django/interfaces/api/views/ogc.py b/django/interfaces/api/views/ogc.py new file mode 100644 index 00000000..71d7d392 --- /dev/null +++ b/django/interfaces/api/views/ogc.py @@ -0,0 +1,159 @@ +from ninja import Router, Path + +from interfaces.auth.security import session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth +from interfaces.api.collections import COLLECTIONS, CollectionDefinition, get_collection +from interfaces.api.http.errors import NotFoundError +from interfaces.api.http.links import ( + JSON_MEDIA_TYPE, + Link, + build_absolute_url, + build_api_url, + build_self_link, + route_path, +) +from interfaces.api.http.request import HydroServerHttpRequest +from interfaces.api.schemas.ogc import ( + CollectionResponse, + CollectionsResponse, + ConformanceResponse, + LandingPageResponse, +) + +API_TITLE = "HydroServer Data Management API" +API_DESCRIPTION = ( + "Provides access to hydrologic monitoring sites, datastreams, and observations managed in HydroServer." +) + +CONFORMANCE_CLASSES = ( + "http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/core", + "http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/json", + "http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/landing-page", + "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/core", +) + +ogc_router = Router(tags=["Collections"]) + + +@ogc_router.get( + "", + auth=[session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth], + response={200: LandingPageResponse}, + by_alias=True, + exclude_none=True, + tags=["Capabilities"], +) +def get_landing_page(request: HydroServerHttpRequest): + """ + Get the API's landing page, which links to the API definition, conformance declaration, and + collections. + """ + + return 200, LandingPageResponse( + title=API_TITLE, + description=API_DESCRIPTION, + links=[ + build_self_link(request), + Link( + href=build_absolute_url(route_path("ogc:openapi-json")), + rel="service-desc", + type=JSON_MEDIA_TYPE, + title="The API definition", + ), + Link( + href=build_absolute_url(route_path("ogc:openapi-view")), + rel="service-doc", + type="text/html", + title="The API documentation", + ), + Link( + href=build_api_url("conformance"), + rel="conformance", + type=JSON_MEDIA_TYPE, + title="OGC API conformance classes implemented by this server", + ), + Link( + href=build_api_url("conformance"), + rel="http://www.opengis.net/def/rel/ogc/1.0/conformance", + type=JSON_MEDIA_TYPE, + title="OGC API conformance classes implemented by this server", + ), + Link( + href=build_api_url("collections"), + rel="data", + type=JSON_MEDIA_TYPE, + title="Information about the collections", + ), + ], + ) + + +@ogc_router.get( + "/conformance", + auth=[session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth], + response={200: ConformanceResponse}, + by_alias=True, + tags=["Capabilities"], +) +def get_conformance(request: HydroServerHttpRequest): + """ + Get the OGC API conformance classes the API implements. + """ + + return 200, ConformanceResponse(conforms_to=list(CONFORMANCE_CLASSES)) + + +def build_collection(collection: CollectionDefinition) -> CollectionResponse: + """ + Builds a collection's metadata. Both /collections and /collections/{collectionId} use it, so + the two stay identical (OGC API - Features Core Req 19). + """ + + collection_url = build_api_url(f"collections/{collection.id}") + + return CollectionResponse( + id=collection.id, + title=collection.title, + description=collection.description, + item_type=collection.item_type, + links=[ + Link(href=collection_url, rel="self", type=JSON_MEDIA_TYPE), + Link(href=f"{collection_url}/items", rel="items", type=JSON_MEDIA_TYPE), + ], + ) + + +@ogc_router.get( + "/collections", + auth=[session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth], + response={200: CollectionsResponse}, + by_alias=True, + exclude_none=True, +) +def get_collections(request: HydroServerHttpRequest): + """ + Get the collections the API serves. + """ + + return 200, CollectionsResponse( + links=[build_self_link(request)], + collections=[build_collection(collection) for collection in COLLECTIONS], + ) + + +@ogc_router.get( + "/collections/{collection_id}", + auth=[session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth], + response={200: CollectionResponse, 404: str}, + by_alias=True, + exclude_none=True, +) +def get_collection_metadata(request: HydroServerHttpRequest, collection_id: Path[str]): + """ + Get a collection. + """ + + collection = get_collection(collection_id) + if collection is None: + raise NotFoundError(f"Collection '{collection_id}' does not exist") + + return 200, build_collection(collection) diff --git a/django/interfaces/api/views/products/rating_curve.py b/django/interfaces/api/views/products/rating_curve.py index 8fd15dc4..e10ec023 100644 --- a/django/interfaces/api/views/products/rating_curve.py +++ b/django/interfaces/api/views/products/rating_curve.py @@ -19,7 +19,7 @@ @rating_curve_router.get( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 200: PaginatedResponse[RatingCurveResponse], @@ -46,7 +46,7 @@ def get_rating_curves( @rating_curve_router.post( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 201: CreatedResponse, @@ -72,7 +72,7 @@ def create_rating_curve( @rating_curve_router.get( - "/{rating_curve_id}", + "/items/{rating_curve_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 200: ItemResponse[RatingCurveResponse], @@ -99,7 +99,7 @@ def get_rating_curve( @rating_curve_router.patch( - "/{rating_curve_id}", + "/items/{rating_curve_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, @@ -129,7 +129,7 @@ def update_rating_curve( @rating_curve_router.delete( - "/{rating_curve_id}", + "/items/{rating_curve_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, diff --git a/django/interfaces/api/views/products/task.py b/django/interfaces/api/views/products/task.py index 4e99a0a1..5c6e2dbe 100644 --- a/django/interfaces/api/views/products/task.py +++ b/django/interfaces/api/views/products/task.py @@ -1,7 +1,9 @@ import uuid +from django.http import HttpResponse from ninja import Router, Path, Query +from interfaces.api.http.links import build_self_link from interfaces.api.http.request import HydroServerHttpRequest from interfaces.auth.security import session_auth, oidc_auth, apikey_auth, basic_auth from processing.orchestration.models import TaskRun @@ -22,7 +24,7 @@ @data_product_task_router.get( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 200: PaginatedResponse[DataProductTaskResponse], @@ -50,7 +52,7 @@ def get_data_product_tasks( @data_product_task_router.post( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 201: CreatedResponse, @@ -73,7 +75,7 @@ def create_data_product_task( @data_product_task_router.get( - "/{task_id}", + "/items/{task_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 200: ItemResponse[DataProductTaskResponse], @@ -98,7 +100,7 @@ def get_data_product_task( @data_product_task_router.patch( - "/{task_id}", + "/items/{task_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, @@ -126,7 +128,7 @@ def update_data_product_task( @data_product_task_router.delete( - "/{task_id}", + "/items/{task_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, @@ -153,10 +155,10 @@ def delete_data_product_task( @data_product_task_router.post( - "/{task_id}/trigger", + "/items/{task_id}/trigger", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ - 202: TaskRunResponse, + 202: ItemResponse[TaskRunResponse], 401: str, 403: str, 404: str, @@ -165,6 +167,7 @@ def delete_data_product_task( ) def trigger_data_product_task( request: HydroServerHttpRequest, + response: HttpResponse, task_id: Path[uuid.UUID], ): """ @@ -178,11 +181,14 @@ def trigger_data_product_task( run = TaskRun.objects.create(task=task, status="PENDING") run_data_product_task.apply_async(kwargs={"task_id": str(task.id), "run_id": str(run.id)}) - return 202, run + run_link = build_self_link(request, request.path_info.removesuffix("/trigger") + f"/runs/{run.id}") + response["Location"] = run_link.href + + return 202, {"data": run, "links": [run_link]} @data_product_task_router.get( - "/{task_id}/runs", + "/items/{task_id}/runs", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 200: PaginatedResponse[TaskRunResponse], @@ -224,10 +230,10 @@ def get_data_product_task_runs( @data_product_task_router.get( - "/{task_id}/runs/{run_id}", + "/items/{task_id}/runs/{run_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ - 200: TaskRunResponse, + 200: ItemResponse[TaskRunResponse], 401: str, 403: str, 404: str, @@ -249,4 +255,4 @@ def get_data_product_task_run( principal=request.principal, ) - return 200, run + return 200, {"data": run} diff --git a/django/interfaces/api/views/products/transformation.py b/django/interfaces/api/views/products/transformation.py index 5477599f..8b004a7c 100644 --- a/django/interfaces/api/views/products/transformation.py +++ b/django/interfaces/api/views/products/transformation.py @@ -19,7 +19,7 @@ @data_product_transformation_router.get( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 200: PaginatedResponse[DataProductTransformationResponse], @@ -46,7 +46,7 @@ def get_data_product_transformations( @data_product_transformation_router.post( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 201: CreatedResponse, @@ -70,7 +70,7 @@ def create_data_product_transformation( @data_product_transformation_router.get( - "/{transformation_id}", + "/items/{transformation_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 200: ItemResponse[DataProductTransformationResponse], @@ -95,7 +95,7 @@ def get_data_product_transformation( @data_product_transformation_router.patch( - "/{transformation_id}", + "/items/{transformation_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, @@ -123,7 +123,7 @@ def update_data_product_transformation( @data_product_transformation_router.delete( - "/{transformation_id}", + "/items/{transformation_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, diff --git a/django/interfaces/api/views/quality/history.py b/django/interfaces/api/views/quality/history.py index 138e9cc1..8f974e5a 100644 --- a/django/interfaces/api/views/quality/history.py +++ b/django/interfaces/api/views/quality/history.py @@ -18,7 +18,7 @@ @qc_history_router.get( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 200: PaginatedResponse[QualityControlHistoryResponse], @@ -40,11 +40,12 @@ def get_qc_histories( sortby=query.sortby, filtering=query.dict(exclude_unset=True), include=query.include, + datetime_interval=query.datetime, ) @qc_history_router.post( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={201: CreatedResponse, 400: str, 401: str, 403: str, 404: str}, by_alias=True, @@ -59,7 +60,7 @@ def create_qc_history( @qc_history_router.get( - "/{history_id}", + "/items/{history_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 200: ItemResponse[QualityControlHistoryResponse], @@ -82,7 +83,7 @@ def get_qc_history( @qc_history_router.delete( - "/{history_id}", + "/items/{history_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={204: None, 401: str, 403: str, 404: str}, by_alias=True, diff --git a/django/interfaces/api/views/sta/aggregation_statistic.py b/django/interfaces/api/views/sta/aggregation_statistic.py index 86c1349a..3a0205ff 100644 --- a/django/interfaces/api/views/sta/aggregation_statistic.py +++ b/django/interfaces/api/views/sta/aggregation_statistic.py @@ -22,7 +22,7 @@ @aggregation_statistic_router.get( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth], response={ 200: PaginatedResponse[AggregationStatisticResponse], @@ -49,7 +49,7 @@ def get_aggregation_statistics( @aggregation_statistic_router.post( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 201: CreatedResponse, @@ -74,7 +74,7 @@ def create_aggregation_statistic( @aggregation_statistic_router.get( - "/{aggregation_statistic_id}", + "/items/{aggregation_statistic_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth], response={ 200: ItemResponse[AggregationStatisticResponse], @@ -101,7 +101,7 @@ def get_aggregation_statistic( @aggregation_statistic_router.patch( - "/{aggregation_statistic_id}", + "/items/{aggregation_statistic_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, @@ -129,7 +129,7 @@ def update_aggregation_statistic( @aggregation_statistic_router.delete( - "/{aggregation_statistic_id}", + "/items/{aggregation_statistic_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, diff --git a/django/interfaces/api/views/sta/datastream.py b/django/interfaces/api/views/sta/datastream.py index 1933e672..df54ae9d 100644 --- a/django/interfaces/api/views/sta/datastream.py +++ b/django/interfaces/api/views/sta/datastream.py @@ -29,7 +29,7 @@ @datastream_router.get( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth], response={ 200: PaginatedResponse[DatastreamResponse], @@ -52,6 +52,8 @@ def get_datastreams( sortby=query.sortby, filtering=query.dict(exclude_unset=True), include=query.include, + bbox=query.bbox, + datetime_interval=query.datetime, ) @@ -79,7 +81,7 @@ def get_datastream_visualization_bootstrap( @datastream_router.post( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 201: CreatedResponse, @@ -126,7 +128,7 @@ def get_datastream_tag_keys( @datastream_router.get( - "/{datastream_id}", + "/items/{datastream_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth], response={ 200: ItemResponse[DatastreamResponse], @@ -150,7 +152,7 @@ def get_datastream( @datastream_router.patch( - "/{datastream_id}", + "/items/{datastream_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, @@ -180,7 +182,7 @@ def update_datastream( @datastream_router.delete( - "/{datastream_id}", + "/items/{datastream_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, @@ -201,10 +203,10 @@ def delete_datastream(request: HydroServerHttpRequest, datastream_id: Path[uuid. @datastream_router.get( - "/{datastream_id}/linked-resources", + "/items/{datastream_id}/linked-resources", auth=[session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth], response={ - 200: list[LinkedResourceGetResponse], + 200: PaginatedResponse[LinkedResourceGetResponse], 401: str, 403: str, }, @@ -222,12 +224,14 @@ def get_datastream_linked_resources( return 200, datastream_service.get_linked_resources( principal=request.principal, uid=datastream_id, + offset=query.offset, + limit=query.limit, filtering=query.dict(exclude_unset=True), ) @datastream_router.post( - "/{datastream_id}/linked-resources", + "/items/{datastream_id}/linked-resources", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 201: CreatedResponse, @@ -264,8 +268,37 @@ def add_datastream_linked_resource( ) +@datastream_router.get( + "/items/{datastream_id}/linked-resources/{linked_resource_id}", + auth=[session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth], + response={ + 200: ItemResponse[LinkedResourceGetResponse], + 401: str, + 403: str, + 404: str, + }, + by_alias=True, +) +def get_datastream_linked_resource( + request: HydroServerHttpRequest, + datastream_id: Path[uuid.UUID], + linked_resource_id: Path[uuid.UUID], +): + """ + Get a linked resource associated with a Datastream. + """ + + return 200, { + "data": datastream_service.get_linked_resource( + principal=request.principal, + uid=datastream_id, + linked_resource_id=linked_resource_id, + ) + } + + @datastream_router.patch( - "/{datastream_id}/linked-resources/{linked_resource_id}", + "/items/{datastream_id}/linked-resources/{linked_resource_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, @@ -307,7 +340,7 @@ def update_datastream_linked_resource( @datastream_router.delete( - "/{datastream_id}/linked-resources/{linked_resource_id}", + "/items/{datastream_id}/linked-resources/{linked_resource_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, @@ -334,7 +367,7 @@ def remove_datastream_linked_resource( @datastream_router.get( - "/{datastream_id}/csv", + "/items/{datastream_id}/csv", auth=[session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth], response={200: None, 403: str, 404: str}, ) diff --git a/django/interfaces/api/views/sta/datastream_status.py b/django/interfaces/api/views/sta/datastream_status.py index e7d463f7..67db7dbc 100644 --- a/django/interfaces/api/views/sta/datastream_status.py +++ b/django/interfaces/api/views/sta/datastream_status.py @@ -22,7 +22,7 @@ @datastream_status_router.get( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth], response={ 200: PaginatedResponse[DatastreamStatusResponse], @@ -49,7 +49,7 @@ def get_datastream_statuses( @datastream_status_router.post( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 201: CreatedResponse, @@ -74,7 +74,7 @@ def create_datastream_status( @datastream_status_router.get( - "/{datastream_status_id}", + "/items/{datastream_status_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth], response={ 200: ItemResponse[DatastreamStatusResponse], @@ -101,7 +101,7 @@ def get_datastream_status( @datastream_status_router.patch( - "/{datastream_status_id}", + "/items/{datastream_status_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, @@ -129,7 +129,7 @@ def update_datastream_status( @datastream_status_router.delete( - "/{datastream_status_id}", + "/items/{datastream_status_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, diff --git a/django/interfaces/api/views/sta/linked_resource_type.py b/django/interfaces/api/views/sta/linked_resource_type.py index e2438fb2..8cd0744d 100644 --- a/django/interfaces/api/views/sta/linked_resource_type.py +++ b/django/interfaces/api/views/sta/linked_resource_type.py @@ -22,7 +22,7 @@ @linked_resource_type_router.get( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth], response={ 200: PaginatedResponse[LinkedResourceTypeResponse], @@ -49,7 +49,7 @@ def get_linked_resource_types( @linked_resource_type_router.post( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 201: CreatedResponse, @@ -74,7 +74,7 @@ def create_linked_resource_type( @linked_resource_type_router.get( - "/{linked_resource_type_id}", + "/items/{linked_resource_type_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth], response={ 200: ItemResponse[LinkedResourceTypeResponse], @@ -101,7 +101,7 @@ def get_linked_resource_type( @linked_resource_type_router.patch( - "/{linked_resource_type_id}", + "/items/{linked_resource_type_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, @@ -129,7 +129,7 @@ def update_linked_resource_type( @linked_resource_type_router.delete( - "/{linked_resource_type_id}", + "/items/{linked_resource_type_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, diff --git a/django/interfaces/api/views/sta/method.py b/django/interfaces/api/views/sta/method.py index bc9b9d97..547542b4 100644 --- a/django/interfaces/api/views/sta/method.py +++ b/django/interfaces/api/views/sta/method.py @@ -22,7 +22,7 @@ @method_router.get( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth], response={ 200: PaginatedResponse[MethodResponse], @@ -50,7 +50,7 @@ def get_methods( @method_router.post( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 201: CreatedResponse, @@ -76,7 +76,7 @@ def create_method( @method_router.get( - "/{method_id}", + "/items/{method_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth], response={ 200: ItemResponse[MethodResponse], @@ -104,7 +104,7 @@ def get_method( @method_router.patch( - "/{method_id}", + "/items/{method_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, @@ -134,7 +134,7 @@ def update_method( @method_router.delete( - "/{method_id}", + "/items/{method_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, diff --git a/django/interfaces/api/views/sta/method_type.py b/django/interfaces/api/views/sta/method_type.py index 66894cea..1232167e 100644 --- a/django/interfaces/api/views/sta/method_type.py +++ b/django/interfaces/api/views/sta/method_type.py @@ -22,7 +22,7 @@ @method_type_router.get( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth], response={ 200: PaginatedResponse[MethodTypeResponse], @@ -49,7 +49,7 @@ def get_method_types( @method_type_router.post( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 201: CreatedResponse, @@ -74,7 +74,7 @@ def create_method_type( @method_type_router.get( - "/{method_type_id}", + "/items/{method_type_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth], response={ 200: ItemResponse[MethodTypeResponse], @@ -101,7 +101,7 @@ def get_method_type( @method_type_router.patch( - "/{method_type_id}", + "/items/{method_type_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, @@ -129,7 +129,7 @@ def update_method_type( @method_type_router.delete( - "/{method_type_id}", + "/items/{method_type_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, diff --git a/django/interfaces/api/views/sta/monitoring_site.py b/django/interfaces/api/views/sta/monitoring_site.py index c0549ed3..8d17decc 100644 --- a/django/interfaces/api/views/sta/monitoring_site.py +++ b/django/interfaces/api/views/sta/monitoring_site.py @@ -35,7 +35,7 @@ @monitoring_site_router.get( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth], response={ 200: PaginatedResponse[MonitoringSiteResponse], @@ -56,6 +56,8 @@ def get_monitoring_sites( offset=query.offset, limit=query.limit, sortby=query.sortby, + bbox=query.bbox, + datetime_interval=query.datetime, filtering=query.dict(exclude_unset=True), include=query.include, ) @@ -80,6 +82,7 @@ def get_monitoring_site_markers( return 200, monitoring_site_service.list_markers( principal=request.principal, + bbox=query.bbox, filtering=query.dict(exclude_unset=True), ) @@ -132,7 +135,7 @@ def get_monitoring_site_task_summaries( @monitoring_site_router.post( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 201: CreatedResponse, @@ -191,7 +194,7 @@ def get_site_type_icons(request: HydroServerHttpRequest): @monitoring_site_router.get( - "/{monitoring_site_id}", + "/items/{monitoring_site_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth], response={ 200: ItemResponse[MonitoringSiteResponse], @@ -215,7 +218,7 @@ def get_monitoring_site( @monitoring_site_router.patch( - "/{monitoring_site_id}", + "/items/{monitoring_site_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, @@ -245,7 +248,7 @@ def update_monitoring_site( @monitoring_site_router.delete( - "/{monitoring_site_id}", + "/items/{monitoring_site_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, @@ -264,10 +267,10 @@ def delete_monitoring_site(request: HydroServerHttpRequest, monitoring_site_id: @monitoring_site_router.get( - "/{monitoring_site_id}/linked-resources", + "/items/{monitoring_site_id}/linked-resources", auth=[session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth], response={ - 200: list[LinkedResourceGetResponse], + 200: PaginatedResponse[LinkedResourceGetResponse], 401: str, 403: str, }, @@ -285,12 +288,14 @@ def get_monitoring_site_linked_resources( return 200, monitoring_site_service.get_linked_resources( principal=request.principal, uid=monitoring_site_id, + offset=query.offset, + limit=query.limit, filtering=query.dict(exclude_unset=True), ) @monitoring_site_router.post( - "/{monitoring_site_id}/linked-resources", + "/items/{monitoring_site_id}/linked-resources", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 201: CreatedResponse, @@ -327,8 +332,37 @@ def add_monitoring_site_linked_resource( ) +@monitoring_site_router.get( + "/items/{monitoring_site_id}/linked-resources/{linked_resource_id}", + auth=[session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth], + response={ + 200: ItemResponse[LinkedResourceGetResponse], + 401: str, + 403: str, + 404: str, + }, + by_alias=True, +) +def get_monitoring_site_linked_resource( + request: HydroServerHttpRequest, + monitoring_site_id: Path[uuid.UUID], + linked_resource_id: Path[uuid.UUID], +): + """ + Get a linked resource associated with a MonitoringSite. + """ + + return 200, { + "data": monitoring_site_service.get_linked_resource( + principal=request.principal, + uid=monitoring_site_id, + linked_resource_id=linked_resource_id, + ) + } + + @monitoring_site_router.patch( - "/{monitoring_site_id}/linked-resources/{linked_resource_id}", + "/items/{monitoring_site_id}/linked-resources/{linked_resource_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, @@ -370,7 +404,7 @@ def update_monitoring_site_linked_resource( @monitoring_site_router.delete( - "/{monitoring_site_id}/linked-resources/{linked_resource_id}", + "/items/{monitoring_site_id}/linked-resources/{linked_resource_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, diff --git a/django/interfaces/api/views/sta/monitoring_site_type.py b/django/interfaces/api/views/sta/monitoring_site_type.py index e0634dd2..2e770bc6 100644 --- a/django/interfaces/api/views/sta/monitoring_site_type.py +++ b/django/interfaces/api/views/sta/monitoring_site_type.py @@ -22,7 +22,7 @@ @monitoring_site_type_router.get( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth], response={ 200: PaginatedResponse[MonitoringSiteTypeResponse], @@ -49,7 +49,7 @@ def get_monitoring_site_types( @monitoring_site_type_router.post( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 201: CreatedResponse, @@ -74,7 +74,7 @@ def create_monitoring_site_type( @monitoring_site_type_router.get( - "/{monitoring_site_type_id}", + "/items/{monitoring_site_type_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth], response={ 200: ItemResponse[MonitoringSiteTypeResponse], @@ -101,7 +101,7 @@ def get_monitoring_site_type( @monitoring_site_type_router.patch( - "/{monitoring_site_type_id}", + "/items/{monitoring_site_type_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, @@ -129,7 +129,7 @@ def update_monitoring_site_type( @monitoring_site_type_router.delete( - "/{monitoring_site_type_id}", + "/items/{monitoring_site_type_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, diff --git a/django/interfaces/api/views/sta/observation.py b/django/interfaces/api/views/sta/observation.py index d96d11f2..6e38bdc3 100644 --- a/django/interfaces/api/views/sta/observation.py +++ b/django/interfaces/api/views/sta/observation.py @@ -28,7 +28,7 @@ @observation_router.get( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth], response={ 200: PaginatedResponse[ObservationResponse] @@ -58,11 +58,13 @@ def get_observations( filtering=query.dict(exclude_unset=True), response_format=query.response_format, include=query.include, + bbox=query.bbox, + datetime_interval=query.datetime, ) @observation_router.post( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 201: CreatedResponse, @@ -131,7 +133,7 @@ def delete_observations( @observation_router.get( - "/{observation_id}", + "/items/{observation_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth], response={ 200: ItemResponse[ObservationResponse], @@ -157,7 +159,7 @@ def get_observation( @observation_router.delete( - "/{observation_id}", + "/items/{observation_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, diff --git a/django/interfaces/api/views/sta/observed_property.py b/django/interfaces/api/views/sta/observed_property.py index 2102a526..7480c57e 100644 --- a/django/interfaces/api/views/sta/observed_property.py +++ b/django/interfaces/api/views/sta/observed_property.py @@ -22,7 +22,7 @@ @observed_property_router.get( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth], response={ 200: PaginatedResponse[ObservedPropertyResponse], @@ -50,7 +50,7 @@ def get_observed_properties( @observed_property_router.post( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 201: CreatedResponse, @@ -76,7 +76,7 @@ def create_observed_property( @observed_property_router.get( - "/{observed_property_id}", + "/items/{observed_property_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth], response={ 200: ItemResponse[ObservedPropertyResponse], @@ -104,7 +104,7 @@ def get_observed_property( @observed_property_router.patch( - "/{observed_property_id}", + "/items/{observed_property_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, @@ -134,7 +134,7 @@ def update_observed_property( @observed_property_router.delete( - "/{observed_property_id}", + "/items/{observed_property_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, diff --git a/django/interfaces/api/views/sta/observed_property_type.py b/django/interfaces/api/views/sta/observed_property_type.py index e686f4dc..d4425e55 100644 --- a/django/interfaces/api/views/sta/observed_property_type.py +++ b/django/interfaces/api/views/sta/observed_property_type.py @@ -22,7 +22,7 @@ @observed_property_type_router.get( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth], response={ 200: PaginatedResponse[ObservedPropertyTypeResponse], @@ -49,7 +49,7 @@ def get_observed_property_types( @observed_property_type_router.post( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 201: CreatedResponse, @@ -74,7 +74,7 @@ def create_observed_property_type( @observed_property_type_router.get( - "/{observed_property_type_id}", + "/items/{observed_property_type_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth], response={ 200: ItemResponse[ObservedPropertyTypeResponse], @@ -101,7 +101,7 @@ def get_observed_property_type( @observed_property_type_router.patch( - "/{observed_property_type_id}", + "/items/{observed_property_type_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, @@ -129,7 +129,7 @@ def update_observed_property_type( @observed_property_type_router.delete( - "/{observed_property_type_id}", + "/items/{observed_property_type_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, diff --git a/django/interfaces/api/views/sta/processing_level.py b/django/interfaces/api/views/sta/processing_level.py index 388e3f34..b15c0797 100644 --- a/django/interfaces/api/views/sta/processing_level.py +++ b/django/interfaces/api/views/sta/processing_level.py @@ -22,7 +22,7 @@ @processing_level_router.get( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth], response={ 200: PaginatedResponse[ProcessingLevelResponse], @@ -50,7 +50,7 @@ def get_processing_levels( @processing_level_router.post( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 201: CreatedResponse, @@ -75,7 +75,7 @@ def create_processing_level( @processing_level_router.get( - "/{processing_level_id}", + "/items/{processing_level_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth], response={ 200: ItemResponse[ProcessingLevelResponse], @@ -103,7 +103,7 @@ def get_processing_level( @processing_level_router.patch( - "/{processing_level_id}", + "/items/{processing_level_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, @@ -133,7 +133,7 @@ def update_processing_level( @processing_level_router.delete( - "/{processing_level_id}", + "/items/{processing_level_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, diff --git a/django/interfaces/api/views/sta/result_qualifier.py b/django/interfaces/api/views/sta/result_qualifier.py index 88874554..771cc771 100644 --- a/django/interfaces/api/views/sta/result_qualifier.py +++ b/django/interfaces/api/views/sta/result_qualifier.py @@ -24,7 +24,7 @@ @result_qualifier_router.get( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth], response={ 200: PaginatedResponse[ResultQualifierResponse], @@ -52,7 +52,7 @@ def get_result_qualifiers( @result_qualifier_router.post( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 201: CreatedResponse, @@ -77,7 +77,7 @@ def create_result_qualifier( @result_qualifier_router.get( - "/{result_qualifier_id}", + "/items/{result_qualifier_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth], response={ 200: ItemResponse[ResultQualifierResponse], @@ -105,7 +105,7 @@ def get_result_qualifier( @result_qualifier_router.patch( - "/{result_qualifier_id}", + "/items/{result_qualifier_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, @@ -134,7 +134,7 @@ def update_result_qualifier( @result_qualifier_router.delete( - "/{result_qualifier_id}", + "/items/{result_qualifier_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, diff --git a/django/interfaces/api/views/sta/sampled_medium.py b/django/interfaces/api/views/sta/sampled_medium.py index dce84f5a..949cfbb9 100644 --- a/django/interfaces/api/views/sta/sampled_medium.py +++ b/django/interfaces/api/views/sta/sampled_medium.py @@ -22,7 +22,7 @@ @sampled_medium_router.get( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth], response={ 200: PaginatedResponse[SampledMediumResponse], @@ -49,7 +49,7 @@ def get_sampled_mediums( @sampled_medium_router.post( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 201: CreatedResponse, @@ -74,7 +74,7 @@ def create_sampled_medium( @sampled_medium_router.get( - "/{sampled_medium_id}", + "/items/{sampled_medium_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth], response={ 200: ItemResponse[SampledMediumResponse], @@ -101,7 +101,7 @@ def get_sampled_medium( @sampled_medium_router.patch( - "/{sampled_medium_id}", + "/items/{sampled_medium_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, @@ -129,7 +129,7 @@ def update_sampled_medium( @sampled_medium_router.delete( - "/{sampled_medium_id}", + "/items/{sampled_medium_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, diff --git a/django/interfaces/api/views/sta/unit.py b/django/interfaces/api/views/sta/unit.py index 58504b76..3e7e71ef 100644 --- a/django/interfaces/api/views/sta/unit.py +++ b/django/interfaces/api/views/sta/unit.py @@ -22,7 +22,7 @@ @unit_router.get( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth], response={ 200: PaginatedResponse[UnitResponse], @@ -50,7 +50,7 @@ def get_units( @unit_router.post( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 201: CreatedResponse, @@ -75,7 +75,7 @@ def create_unit( @unit_router.get( - "/{unit_id}", + "/items/{unit_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth], response={ 200: ItemResponse[UnitResponse], @@ -103,7 +103,7 @@ def get_unit( @unit_router.patch( - "/{unit_id}", + "/items/{unit_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, @@ -133,7 +133,7 @@ def update_unit( @unit_router.delete( - "/{unit_id}", + "/items/{unit_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, diff --git a/django/interfaces/api/views/sta/unit_type.py b/django/interfaces/api/views/sta/unit_type.py index df921ce3..bf712a62 100644 --- a/django/interfaces/api/views/sta/unit_type.py +++ b/django/interfaces/api/views/sta/unit_type.py @@ -22,7 +22,7 @@ @unit_type_router.get( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth], response={ 200: PaginatedResponse[UnitTypeResponse], @@ -49,7 +49,7 @@ def get_unit_types( @unit_type_router.post( - "", + "/items", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 201: CreatedResponse, @@ -74,7 +74,7 @@ def create_unit_type( @unit_type_router.get( - "/{unit_type_id}", + "/items/{unit_type_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth, anonymous_auth], response={ 200: ItemResponse[UnitTypeResponse], @@ -101,7 +101,7 @@ def get_unit_type( @unit_type_router.patch( - "/{unit_type_id}", + "/items/{unit_type_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, @@ -129,7 +129,7 @@ def update_unit_type( @unit_type_router.delete( - "/{unit_type_id}", + "/items/{unit_type_id}", auth=[session_auth, oidc_auth, apikey_auth, basic_auth], response={ 204: None, diff --git a/django/interfaces/sensorthings/adapter/datastream.py b/django/interfaces/sensorthings/adapter/datastream.py index ed8e794e..6a8545e8 100644 --- a/django/interfaces/sensorthings/adapter/datastream.py +++ b/django/interfaces/sensorthings/adapter/datastream.py @@ -151,30 +151,12 @@ def create_datastreams(self, payload, context=None): raise HttpError(403, "This operation is not permitted.") def update_datastreams(self, payload, context=None): - for datastream_id, dto in payload.items(): - datastream = datastream_service.get_datastream_for_action( + for datastream_id in payload: + datastream_service.get_datastream_for_action( principal=context.principal if context else None, uid=datastream_id, action="edit", ) - if dto.phenomenon_time is not None: - parts = str(dto.phenomenon_time).split("/") - datastream.phenomenon_begin_time = parts[0] - datastream.phenomenon_end_time = parts[-1] - else: - datastream.phenomenon_begin_time = None - datastream.phenomenon_end_time = None - - if dto.result_time is not None: - parts = str(dto.result_time).split("/") - datastream.result_begin_time = parts[0] - datastream.result_end_time = parts[-1] - else: - datastream.result_begin_time = None - datastream.result_end_time = None - - datastream.save() - def delete_datastreams(self, entity_ids, context=None): raise HttpError(403, "This operation is not permitted.") diff --git a/django/tests/hydroserver/test_oidc_auth.py b/django/tests/hydroserver/test_oidc_auth.py index a93f5b20..6d4cc657 100644 --- a/django/tests/hydroserver/test_oidc_auth.py +++ b/django/tests/hydroserver/test_oidc_auth.py @@ -45,7 +45,7 @@ def _access_token(oidc_client, user, value, scopes=None, expires_at=None): def _bearer_request(token_value=None): - request = RequestFactory().get("/api/data/workspaces") + request = RequestFactory().get("/api/ogc/collections/workspaces/items") if token_value is not None: request.META["HTTP_AUTHORIZATION"] = f"Bearer {token_value}" return request diff --git a/django/tests/interfaces/api/test_aggregation_statistic_views.py b/django/tests/interfaces/api/test_aggregation_statistic_views.py index 5b91771b..52cf58f5 100644 --- a/django/tests/interfaces/api/test_aggregation_statistic_views.py +++ b/django/tests/interfaces/api/test_aggregation_statistic_views.py @@ -5,7 +5,7 @@ pytestmark = pytest.mark.django_db -AGGREGATION_STATISTICS_URL = "/api/data/aggregation-statistics" +AGGREGATION_STATISTICS_URL = "/api/ogc/collections/aggregation-statistics/items" def _detail_url(aggregation_statistic_id): diff --git a/django/tests/interfaces/api/test_collaborator_views.py b/django/tests/interfaces/api/test_collaborator_views.py index 76ce21b9..7c38144c 100644 --- a/django/tests/interfaces/api/test_collaborator_views.py +++ b/django/tests/interfaces/api/test_collaborator_views.py @@ -15,7 +15,7 @@ def _collaborators_url(workspace_id): - return f"/api/data/workspaces/{workspace_id}/collaborators" + return f"/api/ogc/collections/workspaces/items/{workspace_id}/collaborators" def _collaborator_with_permission(workspace, resource_type, **permissions): diff --git a/django/tests/interfaces/api/test_collection_extent.py b/django/tests/interfaces/api/test_collection_extent.py new file mode 100644 index 00000000..517e26bb --- /dev/null +++ b/django/tests/interfaces/api/test_collection_extent.py @@ -0,0 +1,142 @@ +import pytest + +from tests.core.iam.factories import UserFactory, WorkspaceFactory +from tests.core.sta.factories import ( + AggregationStatisticFactory, + DatastreamStatusFactory, + LinkedResourceTypeFactory, + MethodFactory, + MethodTypeFactory, + MonitoringSiteTypeFactory, + ObservedPropertyFactory, + ObservedPropertyTypeFactory, + ProcessingLevelFactory, + ResultQualifierFactory, + SampledMediumFactory, + UnitFactory, + UnitTypeFactory, +) + +pytestmark = pytest.mark.django_db + +# Collections whose items have no geometry: bbox is validated, and every item matches +# (OGC API - Features - Part 1: Core, Req 24C). +NON_SPATIAL_COLLECTIONS = [ + "observed-properties", + "units", + "methods", + "processing-levels", + "result-qualifiers", + "monitoring-site-types", + "linked-resource-types", + "observed-property-types", + "unit-types", + "method-types", + "sampled-mediums", + "aggregation-statistics", + "datastream-statuses", + "roles", + "etl-data-connections", + "etl-mappings", + "etl-tasks", + "data-product-rating-curves", + "data-product-tasks", + "data-product-transformations", + "monitoring-rules", + "monitoring-tasks", + "quality-control-histories", +] + +TEMPORAL_COLLECTIONS = ["observations", "datastreams", "monitoring-sites", "workspaces", "quality-control-histories"] +NON_TEMPORAL_COLLECTIONS = [c for c in NON_SPATIAL_COLLECTIONS if c not in TEMPORAL_COLLECTIONS] +ALL_COLLECTIONS = NON_SPATIAL_COLLECTIONS + ["monitoring-sites", "datastreams", "observations", "workspaces"] + +ITEM_FACTORIES = { + "observed-properties": lambda workspace: ObservedPropertyFactory(workspace=workspace), + "units": lambda workspace: UnitFactory(workspace=workspace), + "methods": lambda workspace: MethodFactory(workspace=workspace), + "processing-levels": lambda workspace: ProcessingLevelFactory(workspace=workspace), + "result-qualifiers": lambda workspace: ResultQualifierFactory(workspace=workspace), + "monitoring-site-types": lambda workspace: MonitoringSiteTypeFactory(), + "linked-resource-types": lambda workspace: LinkedResourceTypeFactory(), + "observed-property-types": lambda workspace: ObservedPropertyTypeFactory(), + "unit-types": lambda workspace: UnitTypeFactory(), + "method-types": lambda workspace: MethodTypeFactory(), + "sampled-mediums": lambda workspace: SampledMediumFactory(), + "aggregation-statistics": lambda workspace: AggregationStatisticFactory(), + "datastream-statuses": lambda workspace: DatastreamStatusFactory(), +} + + +def _items_url(collection): + return f"/api/ogc/collections/{collection}/items" + + +def _without_links(response): + """The response body without its links, whose self href echoes the request's query string.""" + + body = response.json() + body.pop("links", None) + return body + + +@pytest.fixture +def owner_workspace(client): + owner = UserFactory() + workspace = WorkspaceFactory(owner=owner) + client.force_login(owner) + return workspace + + +@pytest.mark.parametrize("collection", NON_SPATIAL_COLLECTIONS) +@pytest.mark.parametrize("bbox", ["-112,40,-111", "-181,40,-111,41", "-112,41,-111,40", "not,a,valid,bbox"]) +def test_non_spatial_collection_returns_400_for_invalid_bbox(client, owner_workspace, collection, bbox): + response = client.get(_items_url(collection), {"bbox": bbox}) + + assert response.status_code == 400 + + +@pytest.mark.parametrize("collection", NON_SPATIAL_COLLECTIONS) +def test_non_spatial_collection_ignores_a_valid_bbox(client, owner_workspace, collection): + unfiltered = client.get(_items_url(collection)) + filtered = client.get(_items_url(collection), {"bbox": "170,-20,-170,-10"}) + + assert filtered.status_code == 200 + assert _without_links(filtered) == _without_links(unfiltered) + + +@pytest.mark.parametrize("collection", ITEM_FACTORIES) +def test_non_spatial_collection_returns_items_for_any_bbox(client, owner_workspace, collection): + item = ITEM_FACTORIES[collection](owner_workspace) + + response = client.get(_items_url(collection), {"bbox": "0,0,0.001,0.001"}) + + assert response.status_code == 200 + assert str(item.id) in [i["id"] for i in response.json()["data"]] + + +@pytest.mark.parametrize("collection", ALL_COLLECTIONS) +@pytest.mark.parametrize("value", ["2024-01-01", "2024-01-01T00:00:00", "../..", "2024-02-01T00:00:00Z/2024-01-01T00:00:00Z"]) +def test_collection_returns_400_for_invalid_datetime(client, owner_workspace, collection, value): + response = client.get(_items_url(collection), {"datetime": value}) + + assert response.status_code == 400 + + +@pytest.mark.parametrize("collection", NON_TEMPORAL_COLLECTIONS) +def test_non_temporal_collection_ignores_a_valid_datetime(client, owner_workspace, collection): + unfiltered = client.get(_items_url(collection)) + filtered = client.get(_items_url(collection), {"datetime": "1900-01-01T00:00:00Z/1900-01-02T00:00:00Z"}) + + assert filtered.status_code == 200 + assert _without_links(filtered) == _without_links(unfiltered) + + +@pytest.mark.parametrize("collection", ITEM_FACTORIES) +def test_non_temporal_collection_returns_items_for_any_datetime(client, owner_workspace, collection): + item = ITEM_FACTORIES[collection](owner_workspace) + + response = client.get(_items_url(collection), {"datetime": "1900-01-01T00:00:00Z"}) + + assert response.status_code == 200 + assert str(item.id) in [i["id"] for i in response.json()["data"]] diff --git a/django/tests/interfaces/api/test_collection_registry.py b/django/tests/interfaces/api/test_collection_registry.py new file mode 100644 index 00000000..9ea6141e --- /dev/null +++ b/django/tests/interfaces/api/test_collection_registry.py @@ -0,0 +1,53 @@ +import re + +import pytest +from django.utils.module_loading import import_string +from ninja import Router + +from interfaces.api.collections import COLLECTIONS, get_collection +from interfaces.api.urls import api + +COLLECTION_ITEMS_PATH = re.compile(r"/api/ogc/collections/(?P[^/]+)/items") + + +def test_collection_ids_are_unique(): + ids = [collection.id for collection in COLLECTIONS] + + assert len(ids) == len(set(ids)) + + +@pytest.mark.parametrize("collection", COLLECTIONS, ids=lambda collection: collection.id) +def test_collection_id_is_a_url_segment(collection): + assert re.fullmatch(r"[a-z]+(-[a-z]+)*", collection.id) + + +@pytest.mark.parametrize("collection", COLLECTIONS, ids=lambda collection: collection.id) +def test_collection_router_imports_to_a_router(collection): + assert isinstance(import_string(collection.router), Router) + + +@pytest.mark.parametrize("collection", COLLECTIONS, ids=lambda collection: collection.id) +def test_collection_title_matches_its_router_tag(collection): + assert import_string(collection.router).tags == [collection.title] + + +@pytest.mark.parametrize("collection", COLLECTIONS, ids=lambda collection: collection.id) +def test_collection_has_a_description(collection): + assert collection.description.strip() + + +def test_every_collection_items_path_is_registered(): + paths = api.get_openapi_schema(path_prefix="/api/ogc/")["paths"] + mounted = { + match["id"] for path in paths if (match := COLLECTION_ITEMS_PATH.fullmatch(path)) + } + + assert mounted == {collection.id for collection in COLLECTIONS} + + +def test_get_collection_returns_the_registered_collection(): + assert get_collection("monitoring-sites").title == "Monitoring Sites" + + +def test_get_collection_returns_none_for_an_unknown_id(): + assert get_collection("not-a-collection") is None diff --git a/django/tests/interfaces/api/test_collections_views.py b/django/tests/interfaces/api/test_collections_views.py new file mode 100644 index 00000000..8a18b735 --- /dev/null +++ b/django/tests/interfaces/api/test_collections_views.py @@ -0,0 +1,140 @@ +import pytest + +from django.test import override_settings + +from interfaces.api.collections import COLLECTIONS +from interfaces.api.urls import api +from tests.core.iam.factories import UserFactory + +pytestmark = [pytest.mark.django_db, pytest.mark.usefixtures("proxy_base_url")] + +# /collections lists every collection and /collections/{collectionId} describes one, each with +# self and items links (OGC API - Features Core Req 11-15, 18-19). Links are built from +# PROXY_BASE_URL, not the request's Host, which the test client sends as "testserver". + +BASE_URL = "https://hydroserver.example.org" +COLLECTIONS_PATH = "/api/ogc/collections" +COLLECTION_IDS = [collection.id for collection in COLLECTIONS] + + +@pytest.fixture +def proxy_base_url(): + with override_settings(PROXY_BASE_URL=BASE_URL): + yield + + +def _links(body): + return {link["rel"]: link for link in body["links"]} + + +def test_get_collections_links_to_itself(client): + response = client.get(COLLECTIONS_PATH) + + assert response.status_code == 200 + assert response.json()["links"] == [ + {"href": f"{BASE_URL}{COLLECTIONS_PATH}", "rel": "self", "type": "application/json"} + ] + + +def test_get_collections_lists_every_registered_collection_in_order(client): + response = client.get(COLLECTIONS_PATH) + + assert [collection["id"] for collection in response.json()["collections"]] == COLLECTION_IDS + + +@pytest.mark.parametrize("collection", COLLECTIONS, ids=lambda collection: collection.id) +def test_collection_describes_the_registered_collection(client, collection): + response = client.get(f"{COLLECTIONS_PATH}/{collection.id}") + + assert response.status_code == 200 + body = response.json() + assert body["id"] == collection.id + assert body["title"] == collection.title + assert body["description"] == collection.description + assert body.get("itemType") == collection.item_type + assert _links(body) == { + "self": { + "href": f"{BASE_URL}{COLLECTIONS_PATH}/{collection.id}", + "rel": "self", + "type": "application/json", + }, + "items": { + "href": f"{BASE_URL}{COLLECTIONS_PATH}/{collection.id}/items", + "rel": "items", + "type": "application/json", + }, + } + + +@pytest.mark.parametrize("collection_id", COLLECTION_IDS) +def test_collection_matches_its_entry_in_collections(client, collection_id): + listed = { + collection["id"]: collection for collection in client.get(COLLECTIONS_PATH).json()["collections"] + } + + response = client.get(f"{COLLECTIONS_PATH}/{collection_id}") + + assert response.json() == listed[collection_id] + + +def test_collection_without_an_item_type_omits_it(client): + response = client.get(f"{COLLECTIONS_PATH}/units") + + assert "itemType" not in response.json() + + +def test_feature_collection_declares_its_item_type(client): + response = client.get(f"{COLLECTIONS_PATH}/monitoring-sites") + + assert response.json()["itemType"] == "feature" + + +@pytest.mark.parametrize("collection_id", COLLECTION_IDS) +def test_collection_items_link_resolves(client, collection_id): + client.force_login(UserFactory()) + items_href = _links(client.get(f"{COLLECTIONS_PATH}/{collection_id}").json())["items"]["href"] + + response = client.get(items_href.removeprefix(BASE_URL)) + + assert response.status_code == 200 + + +def test_get_unknown_collection_returns_404(client): + response = client.get(f"{COLLECTIONS_PATH}/not-a-collection") + + assert response.status_code == 404 + assert response.json() == {"message": "Collection 'not-a-collection' does not exist"} + + +@pytest.mark.parametrize("path", [COLLECTIONS_PATH, f"{COLLECTIONS_PATH}/units"]) +def test_collections_endpoints_reject_unknown_query_parameters(client, path): + response = client.get(path, {"foo": "bar"}) + + assert response.status_code == 400 + + +@pytest.mark.parametrize("path", [COLLECTIONS_PATH, f"{COLLECTIONS_PATH}/units"]) +def test_collections_endpoints_are_public(client, path): + response = client.get(path) + + assert response.status_code == 200 + + +@pytest.mark.parametrize("path", [COLLECTIONS_PATH, f"{COLLECTIONS_PATH}/{{collection_id}}"]) +def test_collections_endpoints_are_documented_under_collections(path): + paths = api.get_openapi_schema(path_prefix="/api/ogc/")["paths"] + + assert paths[path]["get"]["tags"] == ["Collections"] + + +def test_collection_metadata_is_documented_once_for_all_collections(): + paths = api.get_openapi_schema(path_prefix="/api/ogc/")["paths"] + + assert not [path for path in paths if path.removeprefix(f"{COLLECTIONS_PATH}/") in COLLECTION_IDS] + + +def test_openapi_operation_ids_are_unique(): + paths = api.get_openapi_schema(path_prefix="/api/ogc/")["paths"] + operation_ids = [operation["operationId"] for path in paths.values() for operation in path.values()] + + assert len(operation_ids) == len(set(operation_ids)) diff --git a/django/tests/interfaces/api/test_common_core.py b/django/tests/interfaces/api/test_common_core.py new file mode 100644 index 00000000..ae3254b4 --- /dev/null +++ b/django/tests/interfaces/api/test_common_core.py @@ -0,0 +1,143 @@ +import pytest + +from tests.core.iam.factories import UserFactory +from tests.core.sta.factories import MonitoringSiteFactory, UnitFactory + +pytestmark = pytest.mark.django_db + +# Query parameter encoding rules from OGC API - Common - Part 1: Core (conformance class +# http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/core). The comma-delimited list parameters +# are properties, include, sortby and bbox; parameters repeated as name=value pairs are exempt. + +UNITS_PATH = "/api/ogc/collections/units/items" +SITES_PATH = "/api/ogc/collections/monitoring-sites/items" + + +@pytest.fixture +def units(): + return [UnitFactory(global_=True, name=name, symbol=symbol) for name, symbol in [("B", "b"), ("A", "a")]] + + +@pytest.fixture +def site(): + return MonitoringSiteFactory(latitude=40.5, longitude=-111.5) + + +# --- Req 6 tolerance: an escaped comma separates list items ------------------------------------- +# +# Req 6 reads %2C as part of an item, but Python requests and browser URLSearchParams always +# percent-encode commas, so standard clients send lists as a%2Cb. No valid value of these +# parameters contains a comma, so the API treats %2C as a separator, as allowed by +# /per/core/query-param-value-tolerance. + + +def test_escaped_comma_separates_properties(client, units): + response = client.get(f"{UNITS_PATH}?properties=name%2Csymbol") + + assert response.status_code == 200 + assert all(set(unit) == {"name", "symbol"} for unit in response.json()["data"]) + + +def test_escaped_comma_separates_sortby_fields(client, units): + response = client.get(f"{UNITS_PATH}?sortby=name%2C-symbol") + + assert response.status_code == 200 + assert [unit["name"] for unit in response.json()["data"]] == ["A", "B"] + + +def test_escaped_comma_separates_include_relations(client, units): + response = client.get(f"{UNITS_PATH}?include=workspace%2Ctype") + + assert response.status_code == 200 + + +def test_escaped_comma_separates_bbox_values(client, site): + response = client.get(f"{SITES_PATH}?bbox=-112%2C40%2C-111%2C41") + + assert response.status_code == 200 + assert [s["id"] for s in response.json()["data"]] == [str(site.id)] + + +def test_unescaped_comma_separates_list_items(client, units): + response = client.get(f"{UNITS_PATH}?properties=name,symbol") + + assert response.status_code == 200 + assert all(set(unit) == {"name", "symbol"} for unit in response.json()["data"]) + + +def test_repeated_list_parameters_combine_their_items(client, units): + response = client.get(f"{UNITS_PATH}?properties=name&properties=symbol") + + assert response.status_code == 200 + assert all(set(unit) == {"name", "symbol"} for unit in response.json()["data"]) + + +def test_unescaped_comma_separates_bbox_values(client, site): + response = client.get(f"{SITES_PATH}?bbox=-112,40,-111,41") + + assert response.status_code == 200 + assert [s["id"] for s in response.json()["data"]] == [str(site.id)] + + +# --- Req 7: an empty value is an empty list ------------------------------------------------ + + +@pytest.mark.parametrize( + "path, parameter", + [(UNITS_PATH, "properties"), (UNITS_PATH, "include"), (UNITS_PATH, "sortby"), (SITES_PATH, "bbox")], +) +def test_empty_list_parameter_behaves_as_if_omitted(client, units, site, path, parameter): + omitted = client.get(path) + + response = client.get(f"{path}?{parameter}=") + + assert response.status_code == 200 + assert response.json()["data"] == omitted.json()["data"] + + +# --- Req 8: booleans --------------------------------------------------------------------- + + +@pytest.mark.parametrize("value, expected", [("true", True), ("false", False)]) +def test_boolean_values(client, value, expected): + owner = UserFactory() + MonitoringSiteFactory(workspace__owner=owner, workspace__is_private=True, is_private=True) + MonitoringSiteFactory(workspace__owner=owner, is_private=False) + client.force_login(owner) + + response = client.get(SITES_PATH, {"is_private": value}) + + assert response.status_code == 200 + assert [s["isPrivate"] for s in response.json()["data"]] == [expected] + + +# --- Reqs 9-11: integers, decimals and doubles ------------------------------------------------ + + +def test_integer_with_leading_zeros(client, units): + response = client.get(UNITS_PATH, {"limit": "001"}) + + assert response.status_code == 200 + assert response.json()["meta"]["limit"] == 1 + + +@pytest.mark.parametrize( + "bbox", + ["-112.500,040.00,-111.0,41", "-1.12e2,4.0e1,-1.11e2,41", "-112,40,-111,41.250"], +) +def test_bbox_accepts_decimal_and_exponent_forms(client, site, bbox): + response = client.get(SITES_PATH, {"bbox": bbox}) + + assert response.status_code == 200 + assert [s["id"] for s in response.json()["data"]] == [str(site.id)] + + +# --- Tolerance kept for sortby: an unencoded '+' arrives as a space -------------------------------- + + +def test_sortby_unencoded_plus_still_sorts_ascending(client, units): + response = client.get(f"{UNITS_PATH}?sortby=+name") + + assert response.status_code == 200 + names = [unit["name"] for unit in response.json()["data"]] + assert names == sorted(names) diff --git a/django/tests/interfaces/api/test_data_connection_views.py b/django/tests/interfaces/api/test_data_connection_views.py index d6f67754..6463414d 100644 --- a/django/tests/interfaces/api/test_data_connection_views.py +++ b/django/tests/interfaces/api/test_data_connection_views.py @@ -13,7 +13,7 @@ pytestmark = pytest.mark.django_db -DATA_CONNECTIONS_URL = "/api/data/etl-data-connections" +DATA_CONNECTIONS_URL = "/api/ogc/collections/etl-data-connections/items" def _detail_url(data_connection_id): diff --git a/django/tests/interfaces/api/test_data_product_task_views.py b/django/tests/interfaces/api/test_data_product_task_views.py index bc268fe8..b505f6a0 100644 --- a/django/tests/interfaces/api/test_data_product_task_views.py +++ b/django/tests/interfaces/api/test_data_product_task_views.py @@ -1,8 +1,11 @@ +from datetime import timedelta from unittest.mock import patch import pytest +from django.conf import settings from django.db import connection from django.test.utils import CaptureQueriesContext +from django.utils import timezone from processing.orchestration.models import TaskRun from tests.core.iam.factories import ( @@ -22,7 +25,7 @@ pytestmark = pytest.mark.django_db -DATA_PRODUCT_TASKS_URL = "/api/data/data-product-tasks" +DATA_PRODUCT_TASKS_URL = "/api/ogc/collections/data-product-tasks/items" def _detail_url(task_id): @@ -344,7 +347,7 @@ def test_trigger_data_product_task_creates_pending_run_for_workspace_owner(clien response = client.post(f"{_detail_url(task.id)}/trigger") assert response.status_code == 202 - assert response.json()["status"] == "PENDING" + assert response.json()["data"]["status"] == "PENDING" mock_apply_async.assert_called_once() assert TaskRun.objects.filter(task=task, status="PENDING").exists() @@ -378,6 +381,41 @@ def test_get_data_product_task_runs_returns_runs_for_workspace_owner(client): assert str(run.id) in [r["id"] for r in response.json()["data"]] +def test_trigger_data_product_task_returns_the_run_with_links_to_it(client): + owner = UserFactory() + workspace = WorkspaceFactory(owner=owner) + task = _make_data_product_task(workspace) + client.force_login(owner) + + with patch("processing.products.tasks.run_data_product_task.apply_async"): + response = client.post(f"{_detail_url(task.id)}/trigger") + + run = TaskRun.objects.get(task=task) + run_url = f"{settings.PROXY_BASE_URL.rstrip('/')}/api/ogc/collections/data-product-tasks/items/{task.id}/runs/{run.id}" + assert response.status_code == 202 + assert response["Location"] == run_url + assert response.json()["data"]["id"] == str(run.id) + assert response.json()["links"] == [{"href": run_url, "rel": "self", "type": "application/json"}] + + +def test_get_data_product_task_run_returns_a_self_link_and_no_collection_link(client): + owner = UserFactory() + workspace = WorkspaceFactory(owner=owner) + task = _make_data_product_task(workspace) + run = TaskRun.objects.create(task=task, status="SUCCESS") + client.force_login(owner) + + response = client.get(f"{_detail_url(task.id)}/runs/{run.id}") + + assert response.json()["links"] == [ + { + "href": f"{settings.PROXY_BASE_URL.rstrip('/')}/api/ogc/collections/data-product-tasks/items/{task.id}/runs/{run.id}", + "rel": "self", + "type": "application/json", + } + ] + + def test_get_data_product_task_run_returns_200_for_workspace_owner(client): owner = UserFactory() workspace = WorkspaceFactory(owner=owner) @@ -388,7 +426,7 @@ def test_get_data_product_task_run_returns_200_for_workspace_owner(client): response = client.get(f"{_detail_url(task.id)}/runs/{run.id}") assert response.status_code == 200 - assert response.json()["id"] == str(run.id) + assert response.json()["data"]["id"] == str(run.id) def test_get_data_product_task_runs_accepts_properties_filter(client): @@ -427,3 +465,31 @@ def test_get_data_product_task_runs_ignores_unsupported_include(client): response = client.get(f"{_detail_url(task.id)}/runs", {"include": "bogus"}) assert response.status_code == 200 + + +@pytest.mark.parametrize( + "sortby, expected", + [ + ("latestRunStartedAt", ["older", "newer"]), + ("-latestRunStartedAt", ["newer", "older"]), + ("latestRunFinishedAt", ["older", "newer"]), + ("latestRunStatus", ["newer", "older"]), + ], +) +def test_get_data_product_tasks_sorts_by_latest_run_fields(client, sortby, expected): + owner = UserFactory() + workspace = WorkspaceFactory(owner=owner) + now = timezone.now() + tasks = {"older": _make_data_product_task(workspace), "newer": _make_data_product_task(workspace)} + TaskRun.objects.create( + task=tasks["older"], status="SUCCESS", started_at=now - timedelta(hours=2), + finished_at=now - timedelta(hours=1), + ) + TaskRun.objects.create(task=tasks["newer"], status="FAILURE", started_at=now, finished_at=now) + client.force_login(owner) + + response = client.get(DATA_PRODUCT_TASKS_URL, {"sortby": sortby}) + + assert response.status_code == 200 + ids = [task["id"] for task in response.json()["data"]] + assert ids == [str(tasks[name].id) for name in expected] diff --git a/django/tests/interfaces/api/test_data_product_transformation_views.py b/django/tests/interfaces/api/test_data_product_transformation_views.py index c7c865a4..9764e5c1 100644 --- a/django/tests/interfaces/api/test_data_product_transformation_views.py +++ b/django/tests/interfaces/api/test_data_product_transformation_views.py @@ -20,7 +20,7 @@ pytestmark = pytest.mark.django_db -TRANSFORMATIONS_URL = "/api/data/data-product-transformations" +TRANSFORMATIONS_URL = "/api/ogc/collections/data-product-transformations/items" TRANSFORMATION_FIELDS = { "id", diff --git a/django/tests/interfaces/api/test_datastream_status_views.py b/django/tests/interfaces/api/test_datastream_status_views.py index 6d6f174b..8b4ce8cd 100644 --- a/django/tests/interfaces/api/test_datastream_status_views.py +++ b/django/tests/interfaces/api/test_datastream_status_views.py @@ -5,7 +5,7 @@ pytestmark = pytest.mark.django_db -DATASTREAM_STATUSES_URL = "/api/data/datastream-statuses" +DATASTREAM_STATUSES_URL = "/api/ogc/collections/datastream-statuses/items" def _detail_url(datastream_status_id): diff --git a/django/tests/interfaces/api/test_datastream_views.py b/django/tests/interfaces/api/test_datastream_views.py index e09f3ec1..b738843a 100644 --- a/django/tests/interfaces/api/test_datastream_views.py +++ b/django/tests/interfaces/api/test_datastream_views.py @@ -1,9 +1,12 @@ import uuid +from datetime import timedelta import pytest +from django.utils import timezone from django.db import connection from django.test.utils import CaptureQueriesContext +from core.sta.models import Datastream, DatastreamLinkedResource from tests.core.iam.factories import ( CollaboratorFactory, PermissionFactory, @@ -26,7 +29,8 @@ pytestmark = pytest.mark.django_db -DATASTREAMS_URL = "/api/data/datastreams" +DATASTREAMS_COLLECTION_URL = "/api/ogc/collections/datastreams" +DATASTREAMS_URL = f"{DATASTREAMS_COLLECTION_URL}/items" def _detail_url(datastream_id): @@ -692,7 +696,7 @@ def test_get_datastream_visualization_bootstrap_returns_public_datastream(client ), ) - response = client.get(f"{DATASTREAMS_URL}/visualization-bootstrap") + response = client.get(f"{DATASTREAMS_COLLECTION_URL}/visualization-bootstrap") assert response.status_code == 200 body = response.json() @@ -715,7 +719,7 @@ def test_get_datastream_tag_keys_returns_keys_for_workspace_owner(client): datastream.save() client.force_login(owner) - response = client.get(f"{DATASTREAMS_URL}/tags/keys") + response = client.get(f"{DATASTREAMS_COLLECTION_URL}/tags/keys") assert response.status_code == 200 assert response.json()["season"] == ["summer"] @@ -745,6 +749,98 @@ def _linked_resources_url(datastream_id): return f"{_detail_url(datastream_id)}/linked-resources" +def _make_datastream_linked_resources(owner, names): + workspace = WorkspaceFactory(owner=owner) + parent = _make_datastream(workspace) + for name in names: + DatastreamLinkedResource.objects.create(datastream=parent, name=name, type="Report", url=f"https://example.com/{name}") + return parent + + +def test_get_datastream_linked_resources_pages_by_name(client): + owner = UserFactory() + parent = _make_datastream_linked_resources(owner, ["C", "A", "B"]) + client.force_login(owner) + + first = client.get(_linked_resources_url(parent.id), {"limit": 2}).json() + second = client.get(_linked_resources_url(parent.id), {"limit": 2, "offset": 2}).json() + + assert [item["name"] for item in first["data"]] == ["A", "B"] + assert first["meta"] == {"limit": 2, "offset": 0, "totalCount": 3} + assert "next" in [link["rel"] for link in first["links"]] + assert [item["name"] for item in second["data"]] == ["C"] + + +def test_get_datastream_linked_resources_selects_properties(client): + owner = UserFactory() + parent = _make_datastream_linked_resources(owner, ["A"]) + client.force_login(owner) + + response = client.get(_linked_resources_url(parent.id), {"properties": "name"}) + + assert response.json()["data"] == [{"name": "A"}] + + +def test_get_datastream_linked_resources_rejects_include(client): + owner = UserFactory() + parent = _make_datastream_linked_resources(owner, ["A"]) + client.force_login(owner) + + response = client.get(_linked_resources_url(parent.id), {"include": "datastream"}) + + assert response.status_code == 400 + + +def test_get_datastream_linked_resource_returns_item(client): + owner = UserFactory() + parent = _make_datastream_linked_resources(owner, ["A", "B"]) + linked_resource = DatastreamLinkedResource.objects.get(datastream=parent, name="B") + client.force_login(owner) + + response = client.get(f"{_linked_resources_url(parent.id)}/{linked_resource.id}") + + assert response.status_code == 200 + data = response.json()["data"] + assert data["id"] == str(linked_resource.id) + assert data["name"] == "B" + assert data["type"] == "Report" + assert data["link"] == "https://example.com/B" + + +def test_get_datastream_linked_resource_returns_404_for_unknown_id(client): + owner = UserFactory() + parent = _make_datastream_linked_resources(owner, ["A"]) + client.force_login(owner) + + response = client.get(f"{_linked_resources_url(parent.id)}/{uuid.uuid4()}") + + assert response.status_code == 404 + + +def test_get_datastream_linked_resource_returns_404_for_other_parents_resource(client): + owner = UserFactory() + parent = _make_datastream_linked_resources(owner, ["A"]) + other_parent = _make_datastream_linked_resources(owner, ["B"]) + other_resource = DatastreamLinkedResource.objects.get(datastream=other_parent) + client.force_login(owner) + + response = client.get(f"{_linked_resources_url(parent.id)}/{other_resource.id}") + + assert response.status_code == 404 + + +def test_get_datastream_linked_resource_returns_404_for_private_parent_when_outsider(client): + parent = _make_datastream(WorkspaceFactory(), private=True) + linked_resource = DatastreamLinkedResource.objects.create( + datastream=parent, name="A", type="Report", url="https://example.com/A" + ) + client.force_login(UserFactory()) + + response = client.get(f"{_linked_resources_url(parent.id)}/{linked_resource.id}") + + assert response.status_code == 404 + + def test_add_datastream_linked_resource_succeeds_with_link(client): owner = UserFactory() workspace = WorkspaceFactory(owner=owner) @@ -762,7 +858,7 @@ def test_add_datastream_linked_resource_succeeds_with_link(client): assert response.status_code == 201 assert set(response.json().keys()) == {"id"} - linked_resources = client.get(_linked_resources_url(datastream.id)).json() + linked_resources = client.get(_linked_resources_url(datastream.id)).json()["data"] assert linked_resources[0]["name"] == "Datastream Report" @@ -929,3 +1025,169 @@ def test_get_datastreams_properties_filters_response_fields(client): assert response.status_code == 200 row = response.json()["data"][0] assert set(row.keys()) == {"id", "name"} + + +# --- bbox -------------------------------------------------------------------------------- + + +def _datastream_at(longitude, latitude): + return DatastreamFactory(monitoring_site=MonitoringSiteFactory(longitude=longitude, latitude=latitude)) + + +def _datastream_ids(response): + return {datastream["id"] for datastream in response.json()["data"]} + + +def test_get_datastreams_filters_by_monitoring_site_location(client): + inside = _datastream_at(-111.5, 40.5) + _datastream_at(-100, 40.5) + + response = client.get(DATASTREAMS_URL, {"bbox": "-112,40,-111,41"}) + + assert response.status_code == 200 + assert _datastream_ids(response) == {str(inside.id)} + + +def test_get_datastreams_filters_by_a_bbox_crossing_the_antimeridian(client): + east = _datastream_at(175, -15) + west = _datastream_at(-175, -15) + _datastream_at(0, -15) + + response = client.get(DATASTREAMS_URL, {"bbox": "170,-20,-170,-10"}) + + assert response.status_code == 200 + assert _datastream_ids(response) == {str(east.id), str(west.id)} + + +def test_get_datastreams_combines_bbox_with_other_filters(client): + inside = _datastream_at(-111.5, 40.5) + _datastream_at(-111.5, 40.5) + + response = client.get( + DATASTREAMS_URL, {"bbox": "-112,40,-111,41", "monitoring_site_id": str(inside.monitoring_site_id)} + ) + + assert response.status_code == 200 + assert _datastream_ids(response) == {str(inside.id)} + + +def test_get_datastreams_returns_400_for_invalid_bbox(client): + response = client.get(DATASTREAMS_URL, {"bbox": "-112,40,-111,91"}) + + assert response.status_code == 400 + + +# --- datetime ---------------------------------------------------------------------------- + + +def _datastream_spanning(begin, end): + return DatastreamFactory(phenomenon_begin_time=begin, phenomenon_end_time=end) + + +def test_get_datastreams_filters_by_overlapping_phenomenon_time(client): + jan = timezone.now().replace(microsecond=0) - timedelta(days=60) + feb = jan + timedelta(days=30) + before = _datastream_spanning(jan - timedelta(days=20), jan - timedelta(days=10)) + overlaps_start = _datastream_spanning(jan - timedelta(days=5), jan + timedelta(days=5)) + inside = _datastream_spanning(jan + timedelta(days=1), jan + timedelta(days=2)) + touches_end = _datastream_spanning(feb, feb + timedelta(days=5)) + after = _datastream_spanning(feb + timedelta(days=1), feb + timedelta(days=5)) + + response = client.get(DATASTREAMS_URL, {"datetime": f"{jan.isoformat()}/{feb.isoformat()}"}) + + assert response.status_code == 200 + ids = _datastream_ids(response) + assert {str(overlaps_start.id), str(inside.id), str(touches_end.id)} <= ids + assert not ids & {str(before.id), str(after.id)} + + +def test_get_datastreams_datetime_matches_datastreams_without_observations(client): + empty = _datastream_spanning(None, None) + old = _datastream_spanning(timezone.now() - timedelta(days=20), timezone.now() - timedelta(days=10)) + + response = client.get(DATASTREAMS_URL, {"datetime": timezone.now().replace(microsecond=0).isoformat()}) + + assert response.status_code == 200 + assert str(empty.id) in _datastream_ids(response) + assert str(old.id) not in _datastream_ids(response) + + +def test_get_datastreams_returns_400_for_invalid_datetime(client): + response = client.get(DATASTREAMS_URL, {"datetime": "../.."}) + + assert response.status_code == 400 + + +# --- server-maintained statistics ------------------------------------------------------- + +STATISTICS = { + "valueCount": 42, + "phenomenonBeginTime": "2020-01-01T00:00:00Z", + "phenomenonEndTime": "2020-12-31T00:00:00Z", + "resultBeginTime": "2020-01-01T00:00:00Z", + "resultEndTime": "2020-12-31T00:00:00Z", +} + + +def test_create_datastream_ignores_server_maintained_statistics(client): + owner = UserFactory() + workspace = WorkspaceFactory(owner=owner) + body = _datastream_body( + MonitoringSiteFactory(workspace=workspace), + MethodFactory(workspace=workspace), + ObservedPropertyFactory(workspace=workspace), + ProcessingLevelFactory(workspace=workspace), + UnitFactory(workspace=workspace), + **STATISTICS, + ) + client.force_login(owner) + + response = client.post(DATASTREAMS_URL, data=body, content_type="application/json") + + assert response.status_code == 201 + datastream = Datastream.objects.get(pk=response.json()["id"]) + assert datastream.value_count in (None, 0) + assert datastream.phenomenon_begin_time is None + assert datastream.phenomenon_end_time is None + assert datastream.result_begin_time is None + + +def test_update_datastream_ignores_server_maintained_statistics(client): + owner = UserFactory() + workspace = WorkspaceFactory(owner=owner) + begin = timezone.now().replace(microsecond=0) - timedelta(days=2) + datastream = DatastreamFactory( + monitoring_site=MonitoringSiteFactory(workspace=workspace), + value_count=3, + phenomenon_begin_time=begin, + phenomenon_end_time=begin + timedelta(days=1), + ) + client.force_login(owner) + + response = client.patch(_detail_url(datastream.id), data=STATISTICS, content_type="application/json") + + assert response.status_code == 204 + datastream.refresh_from_db() + assert datastream.value_count == 3 + assert datastream.phenomenon_begin_time == begin + assert datastream.phenomenon_end_time == begin + timedelta(days=1) + + +def test_get_datastream_returns_server_maintained_statistics(client): + begin = timezone.now().replace(microsecond=0) - timedelta(days=2) + datastream = DatastreamFactory(value_count=3, phenomenon_begin_time=begin, phenomenon_end_time=begin) + + response = client.get(_detail_url(datastream.id), {"properties": "valueCount,phenomenonBeginTime"}) + + assert response.status_code == 200 + assert response.json()["data"] == {"valueCount": 3, "phenomenonBeginTime": begin.isoformat()} + + +def test_datastream_request_bodies_do_not_declare_server_maintained_statistics(): + from interfaces.api.urls import api + + schemas = api.get_openapi_schema(path_prefix="/api/ogc/")["components"]["schemas"] + + for body in ("DatastreamPostBody", "DatastreamPatchBody"): + assert not set(schemas[body]["properties"]) & set(STATISTICS) + assert set(STATISTICS) <= set(schemas["DatastreamResponse"]["properties"]) diff --git a/django/tests/interfaces/api/test_etl_mapping_views.py b/django/tests/interfaces/api/test_etl_mapping_views.py index 0c17df76..0b85bf86 100644 --- a/django/tests/interfaces/api/test_etl_mapping_views.py +++ b/django/tests/interfaces/api/test_etl_mapping_views.py @@ -15,7 +15,7 @@ pytestmark = pytest.mark.django_db -MAPPINGS_URL = "/api/data/etl-mappings" +MAPPINGS_URL = "/api/ogc/collections/etl-mappings/items" MAPPING_FIELDS = {"id", "sourceIdentifier", "targetDatastreamId", "etlTaskId"} diff --git a/django/tests/interfaces/api/test_etl_task_views.py b/django/tests/interfaces/api/test_etl_task_views.py index d5a0ef23..13ccf17a 100644 --- a/django/tests/interfaces/api/test_etl_task_views.py +++ b/django/tests/interfaces/api/test_etl_task_views.py @@ -2,6 +2,7 @@ from unittest.mock import patch import pytest +from django.conf import settings from django.db import connection from django.test.utils import CaptureQueriesContext from django.utils import timezone @@ -18,7 +19,7 @@ pytestmark = pytest.mark.django_db -ETL_TASKS_URL = "/api/data/etl-tasks" +ETL_TASKS_URL = "/api/ogc/collections/etl-tasks/items" def _detail_url(task_id): @@ -352,7 +353,7 @@ def test_trigger_etl_task_creates_pending_run_for_workspace_owner(client): response = client.post(f"{_detail_url(task.id)}/trigger") assert response.status_code == 202 - assert response.json()["status"] == "PENDING" + assert response.json()["data"]["status"] == "PENDING" mock_apply_async.assert_called_once() assert TaskRun.objects.filter(task=task, status="PENDING").exists() @@ -386,6 +387,41 @@ def test_get_etl_task_runs_returns_runs_for_workspace_owner(client): assert str(run.id) in [r["id"] for r in response.json()["data"]] +def test_trigger_etl_task_returns_the_run_with_links_to_it(client): + owner = UserFactory() + workspace = WorkspaceFactory(owner=owner) + task = _make_etl_task(workspace) + client.force_login(owner) + + with patch("processing.etl.tasks.run_etl_task.apply_async"): + response = client.post(f"{_detail_url(task.id)}/trigger") + + run = TaskRun.objects.get(task=task) + run_url = f"{settings.PROXY_BASE_URL.rstrip('/')}/api/ogc/collections/etl-tasks/items/{task.id}/runs/{run.id}" + assert response.status_code == 202 + assert response["Location"] == run_url + assert response.json()["data"]["id"] == str(run.id) + assert response.json()["links"] == [{"href": run_url, "rel": "self", "type": "application/json"}] + + +def test_get_etl_task_run_returns_a_self_link_and_no_collection_link(client): + owner = UserFactory() + workspace = WorkspaceFactory(owner=owner) + task = _make_etl_task(workspace) + run = TaskRun.objects.create(task=task, status="SUCCESS") + client.force_login(owner) + + response = client.get(f"{_detail_url(task.id)}/runs/{run.id}") + + assert response.json()["links"] == [ + { + "href": f"{settings.PROXY_BASE_URL.rstrip('/')}/api/ogc/collections/etl-tasks/items/{task.id}/runs/{run.id}", + "rel": "self", + "type": "application/json", + } + ] + + def test_get_etl_task_run_returns_200_for_workspace_owner(client): owner = UserFactory() workspace = WorkspaceFactory(owner=owner) @@ -396,7 +432,7 @@ def test_get_etl_task_run_returns_200_for_workspace_owner(client): response = client.get(f"{_detail_url(task.id)}/runs/{run.id}") assert response.status_code == 200 - assert response.json()["id"] == str(run.id) + assert response.json()["data"]["id"] == str(run.id) def test_get_etl_task_runs_sortby_started_at_ascending(client): @@ -462,3 +498,31 @@ def test_get_etl_task_runs_ignores_unsupported_include(client): response = client.get(f"{_detail_url(task.id)}/runs", {"include": "bogus"}) assert response.status_code == 200 + + +@pytest.mark.parametrize( + "sortby, expected", + [ + ("latestRunStartedAt", ["older", "newer"]), + ("-latestRunStartedAt", ["newer", "older"]), + ("latestRunFinishedAt", ["older", "newer"]), + ("latestRunStatus", ["newer", "older"]), + ], +) +def test_get_etl_tasks_sorts_by_latest_run_fields(client, sortby, expected): + owner = UserFactory() + workspace = WorkspaceFactory(owner=owner) + now = timezone.now() + tasks = {"older": _make_etl_task(workspace), "newer": _make_etl_task(workspace)} + TaskRun.objects.create( + task=tasks["older"], status="SUCCESS", started_at=now - timedelta(hours=2), + finished_at=now - timedelta(hours=1), + ) + TaskRun.objects.create(task=tasks["newer"], status="FAILURE", started_at=now, finished_at=now) + client.force_login(owner) + + response = client.get(ETL_TASKS_URL, {"sortby": sortby}) + + assert response.status_code == 200 + ids = [task["id"] for task in response.json()["data"]] + assert ids == [str(tasks[name].id) for name in expected] diff --git a/django/tests/interfaces/api/test_extent.py b/django/tests/interfaces/api/test_extent.py new file mode 100644 index 00000000..1b2efb2d --- /dev/null +++ b/django/tests/interfaces/api/test_extent.py @@ -0,0 +1,435 @@ +from datetime import datetime, timedelta, timezone + +import pytest +from ninja import Query +from ninja.testing import TestClient +from pydantic import ValidationError + +from core.sta.models import Datastream, MonitoringSite, Observation +from interfaces.api.http import handlers +from interfaces.api.http.api import HydroServerNinjaAPI +from interfaces.api.schemas import ( + BaseQueryParameters, + BoundingBox, + BoundingBoxQuery, + ExtentQueryParameters, + TimeInterval, + parse_bbox, + parse_datetime, +) +from interfaces.api.service import APIService +from interfaces.api.urls import api +from tests.core.sta.factories import DatastreamFactory, MonitoringSiteFactory, ObservationFactory + + +class _BBoxQueryParameters(BaseQueryParameters): + bbox: BoundingBoxQuery = Query(None, description="Filter by bounding box.") + + +_bbox_api = HydroServerNinjaAPI(urls_namespace="bbox-test") +handlers.register(_bbox_api) + + +@_bbox_api.get("/items") +def _list_items(request, query: Query[_BBoxQueryParameters]): + bbox = query.bbox + return None if bbox is None else [bbox.west, bbox.south, bbox.east, bbox.north] + + +class _ExtentQueryParameters(BaseQueryParameters, ExtentQueryParameters): + pass + + +@_bbox_api.get("/extent-items") +def _list_extent_items(request, query: Query[_ExtentQueryParameters]): + interval = query.datetime + if interval is None: + return None + return [None if t is None else t.isoformat() for t in (interval.start, interval.end)] + + +_bbox_client = TestClient(_bbox_api) + + +# --- parse_bbox --------------------------------------------------------------------------- + + +def test_parse_bbox_parses_four_numbers(): + assert parse_bbox("-112.5,40,-111,41.25") == BoundingBox(west=-112.5, south=40, east=-111, north=41.25) + + +def test_parse_bbox_parses_six_numbers_and_drops_the_height_range(): + assert parse_bbox("-112,40,-10,-111,41,2500") == BoundingBox(west=-112, south=40, east=-111, north=41) + + +def test_parse_bbox_tolerates_whitespace_around_values(): + assert parse_bbox(" -112 , 40 , -111 , 41 ") == BoundingBox(west=-112, south=40, east=-111, north=41) + + +def test_parse_bbox_accepts_a_sequence_of_values(): + assert parse_bbox([-112, 40, -111, 41]) == BoundingBox(west=-112, south=40, east=-111, north=41) + + +def test_parse_bbox_accepts_a_box_that_crosses_the_antimeridian(): + bbox = parse_bbox("170,-20,-170,-10") + + assert bbox == BoundingBox(west=170, south=-20, east=-170, north=-10) + assert bbox.crosses_antimeridian + + +@pytest.mark.parametrize("value", [None, "", " "]) +def test_parse_bbox_returns_none_for_missing_values(value): + assert parse_bbox(value) is None + + +@pytest.mark.parametrize("value", ["null", "NULL"]) +def test_parse_bbox_rejects_null(value): + with pytest.raises(ValueError, match="only numeric values"): + parse_bbox(value) + + +def test_parse_bbox_passes_through_an_existing_bounding_box(): + bbox = BoundingBox(west=-112, south=40, east=-111, north=41) + + assert parse_bbox(bbox) is bbox + + +@pytest.mark.parametrize( + "value, message", + [ + ("-112,40,-111,north", "only numeric values"), + ("-112,40,-111,nan", "finite"), + ("-112,40,-111,inf", "finite"), + ("-112,40,-111", "4 or 6"), + ("-112,40,0,-111,41", "4 or 6"), + ("-112,40,0,-111,41,1,2", "4 or 6"), + ("-181,40,-111,41", "longitudes"), + ("-112,40,181,41", "longitudes"), + ("-112,-91,-111,41", "latitudes"), + ("-112,40,-111,91", "latitudes"), + ("-112,41,-111,40", "minimum latitude"), + ("-112,40,100,-111,41,0", "minimum height"), + ], +) +def test_parse_bbox_rejects_invalid_values(value, message): + with pytest.raises(ValueError, match=message): + parse_bbox(value) + + +# --- BoundingBox.contains ----------------------------------------------------------------- + + +@pytest.mark.parametrize( + "longitude, latitude, expected", + [ + (-111.5, 40.5, True), + (-112, 40, True), + (-111, 41, True), + (-110.9, 40.5, False), + (-111.5, 41.1, False), + ], +) +def test_contains_includes_boundaries(longitude, latitude, expected): + bbox = BoundingBox(west=-112, south=40, east=-111, north=41) + + assert bbox.contains(longitude, latitude) is expected + + +@pytest.mark.parametrize( + "longitude, expected", + [(175, True), (180, True), (-180, True), (-175, True), (0, False), (165, False), (-165, False)], +) +def test_contains_handles_boxes_that_cross_the_antimeridian(longitude, expected): + bbox = BoundingBox(west=170, south=-20, east=-170, north=-10) + + assert bbox.contains(longitude, -15) is expected + + +# --- BoundingBoxQuery --------------------------------------------------------------------- + + +def test_bounding_box_query_parses_the_query_parameter(): + response = _bbox_client.get("/items", query_params={"bbox": "-112,40,-111,41"}) + + assert response.status_code == 200 + assert response.json() == [-112, 40, -111, 41] + + +def test_bounding_box_query_is_optional(): + response = _bbox_client.get("/items") + + assert response.status_code == 200 + assert response.json() is None + + +@pytest.mark.parametrize("value", ["-112,40,-111", "-112,41,-111,40", "not,a,valid,bbox"]) +def test_bounding_box_query_returns_400_for_invalid_values(value): + response = _bbox_client.get("/items", query_params={"bbox": value}) + + assert response.status_code == 400 + + +def test_bounding_box_query_is_declared_per_ogc_core_requirement_23(): + parameters = _bbox_api.get_openapi_schema(path_prefix="")["paths"]["/items"]["get"]["parameters"] + bbox = next(parameter for parameter in parameters if parameter["name"] == "bbox") + + assert bbox["style"] == "form" + assert bbox["explode"] is False + assert bbox["schema"]["type"] == "array" + assert bbox["schema"]["items"] == {"type": "number"} + assert bbox["schema"]["oneOf"] == [{"minItems": 4, "maxItems": 4}, {"minItems": 6, "maxItems": 6}] + + +def test_bounding_box_query_rejects_invalid_values_during_model_validation(): + with pytest.raises(ValidationError): + _BBoxQueryParameters.model_validate({"bbox": "-112,40,-111"}) + + +def test_extent_parameters_are_declared_on_every_collection_items_list_and_no_nested_list(): + paths = api.get_openapi_schema(path_prefix="/api/ogc/")["paths"] + + def declares(path, name): + return any(p["name"] == name for p in paths[path]["get"].get("parameters", [])) + + items_lists = [path for path in paths if path.endswith("/items") and "get" in paths[path]] + nested_lists = [ + path + for path in paths + if "/items/{" in path and not path.rsplit("/", 1)[-1].startswith("{") and "get" in paths[path] + and not path.endswith("/csv") + ] + markers = "/api/ogc/collections/monitoring-sites/markers" + + assert len(items_lists) == 27 + for name in ("bbox", "datetime"): + assert [path for path in items_lists if not declares(path, name)] == [] + assert nested_lists and [path for path in nested_lists if declares(path, name)] == [] + assert declares(markers, "bbox") and not declares(markers, "datetime") + + +# --- APIService.apply_bbox ---------------------------------------------------------------- + + +@pytest.mark.django_db +def test_apply_bbox_returns_the_queryset_unfiltered_without_a_box(): + MonitoringSiteFactory.create_batch(2) + + assert APIService.apply_bbox(MonitoringSite.objects.all(), None).count() == 2 + + +@pytest.mark.django_db +def test_apply_bbox_matches_points_inside_the_box_including_boundaries(): + inside = MonitoringSiteFactory(longitude=-111.5, latitude=40.5) + on_corner = MonitoringSiteFactory(longitude=-112, latitude=40) + MonitoringSiteFactory(longitude=-110, latitude=40.5) + MonitoringSiteFactory(longitude=-111.5, latitude=42) + + bbox = BoundingBox(west=-112, south=40, east=-111, north=41) + matched = set(APIService.apply_bbox(MonitoringSite.objects.all(), bbox).values_list("id", flat=True)) + + assert matched == {inside.id, on_corner.id} + + +@pytest.mark.django_db +def test_apply_bbox_matches_both_sides_of_the_antimeridian(): + east_of_west_edge = MonitoringSiteFactory(longitude=175, latitude=-15) + west_of_east_edge = MonitoringSiteFactory(longitude=-175, latitude=-15) + MonitoringSiteFactory(longitude=0, latitude=-15) + MonitoringSiteFactory(longitude=175, latitude=-30) + + bbox = BoundingBox(west=170, south=-20, east=-170, north=-10) + matched = set(APIService.apply_bbox(MonitoringSite.objects.all(), bbox).values_list("id", flat=True)) + + assert matched == {east_of_west_edge.id, west_of_east_edge.id} + + +@pytest.mark.django_db +def test_apply_bbox_supports_related_field_paths(): + inside = DatastreamFactory(monitoring_site=MonitoringSiteFactory(longitude=-111.5, latitude=40.5)) + DatastreamFactory(monitoring_site=MonitoringSiteFactory(longitude=-100, latitude=40.5)) + + bbox = BoundingBox(west=-112, south=40, east=-111, north=41) + queryset = APIService.apply_bbox( + Datastream.objects.all(), bbox, "monitoring_site__latitude", "monitoring_site__longitude" + ) + + assert list(queryset.values_list("id", flat=True)) == [inside.id] + + +# --- parse_datetime ----------------------------------------------------------------------- + +JAN_1 = datetime(2024, 1, 1, tzinfo=timezone.utc) +FEB_1 = datetime(2024, 2, 1, tzinfo=timezone.utc) + + +def test_parse_datetime_parses_an_instant(): + assert parse_datetime("2024-01-01T00:00:00Z") == TimeInterval(start=JAN_1, end=JAN_1) + + +def test_parse_datetime_converts_offsets_to_utc(): + interval = parse_datetime("2024-01-01T02:00:00+02:00") + + assert interval.start == JAN_1 + assert interval.start.tzinfo == timezone.utc + + +@pytest.mark.parametrize("value", ["2024-01-01t00:00:00z", "2024-01-01T00:00:00.000Z"]) +def test_parse_datetime_accepts_lowercase_separators_and_fractional_seconds(value): + assert parse_datetime(value) == TimeInterval(start=JAN_1, end=JAN_1) + + +def test_parse_datetime_parses_a_bounded_interval(): + assert parse_datetime("2024-01-01T00:00:00Z/2024-02-01T00:00:00Z") == TimeInterval(start=JAN_1, end=FEB_1) + + +@pytest.mark.parametrize("value", ["../2024-02-01T00:00:00Z", "/2024-02-01T00:00:00Z"]) +def test_parse_datetime_parses_an_open_start(value): + assert parse_datetime(value) == TimeInterval(start=None, end=FEB_1) + + +@pytest.mark.parametrize("value", ["2024-01-01T00:00:00Z/..", "2024-01-01T00:00:00Z/"]) +def test_parse_datetime_parses_an_open_end(value): + assert parse_datetime(value) == TimeInterval(start=JAN_1, end=None) + + +def test_parse_datetime_returns_none_for_missing_values(): + assert parse_datetime(None) is None + + +@pytest.mark.parametrize("value", ["null", "NULL"]) +def test_parse_datetime_rejects_null(value): + with pytest.raises(ValueError, match="RFC 3339"): + parse_datetime(value) + + +@pytest.mark.parametrize( + "value, message", + [ + ("2024-01-01", "RFC 3339"), + ("2024-01-01T00:00:00", "RFC 3339"), + ("2024-01-01 00:00:00Z", "RFC 3339"), + ("2024-01-01T00:00:00 00:00", "RFC 3339"), + ("2024-13-01T00:00:00Z", "Invalid date-time"), + ("yesterday", "RFC 3339"), + ("../..", "at least one bounded end"), + ("/", "at least one bounded end"), + ("2024-01-01T00:00:00Z/2024-02-01T00:00:00Z/..", "exactly one '/'"), + ("2024-02-01T00:00:00Z/2024-01-01T00:00:00Z", "earlier than or equal"), + ], +) +def test_parse_datetime_rejects_invalid_values(value, message): + with pytest.raises(ValueError, match=message): + parse_datetime(value) + + +# --- DatetimeQuery ------------------------------------------------------------------------ + + +def test_datetime_query_parses_the_query_parameter(): + response = _bbox_client.get("/extent-items", query_params={"datetime": "../2024-02-01T00:00:00Z"}) + + assert response.status_code == 200 + assert response.json() == [None, "2024-02-01T00:00:00+00:00"] + + +@pytest.mark.parametrize("value", ["2024-01-01", "../..", "2024-02-01T00:00:00Z/2024-01-01T00:00:00Z"]) +def test_datetime_query_returns_400_for_invalid_values(value): + response = _bbox_client.get("/extent-items", query_params={"datetime": value}) + + assert response.status_code == 400 + + +def test_datetime_query_is_declared_per_ogc_core_requirement_25(): + parameters = _bbox_api.get_openapi_schema(path_prefix="")["paths"]["/extent-items"]["get"]["parameters"] + parameter = next(parameter for parameter in parameters if parameter["name"] == "datetime") + + assert parameter["style"] == "form" + assert parameter["explode"] is False + assert parameter["schema"]["type"] == "string" + + +# --- APIService datetime filters ---------------------------------------------------------- + + +def _ids(queryset): + return set(queryset.values_list("id", flat=True)) + + +@pytest.mark.django_db +def test_apply_datetime_instant_matches_instants_inside_the_interval_including_bounds(): + datastream = DatastreamFactory() + on_start = ObservationFactory(datastream=datastream, phenomenon_time=JAN_1) + inside = ObservationFactory(datastream=datastream, phenomenon_time=JAN_1 + timedelta(days=5)) + on_end = ObservationFactory(datastream=datastream, phenomenon_time=FEB_1) + ObservationFactory(datastream=datastream, phenomenon_time=FEB_1 + timedelta(seconds=1)) + + interval = TimeInterval(start=JAN_1, end=FEB_1) + matched = _ids(APIService.apply_datetime_instant(Observation.objects.all(), interval, "phenomenon_time")) + + assert matched == {on_start.id, inside.id, on_end.id} + + +@pytest.mark.django_db +def test_apply_datetime_instant_supports_open_ends_and_instants(): + datastream = DatastreamFactory() + early = ObservationFactory(datastream=datastream, phenomenon_time=JAN_1) + late = ObservationFactory(datastream=datastream, phenomenon_time=FEB_1) + + def matched(interval): + return _ids(APIService.apply_datetime_instant(Observation.objects.all(), interval, "phenomenon_time")) + + assert matched(TimeInterval(start=None, end=JAN_1)) == {early.id} + assert matched(TimeInterval(start=FEB_1, end=None)) == {late.id} + assert matched(TimeInterval(start=FEB_1, end=FEB_1)) == {late.id} + assert matched(None) == {early.id, late.id} + + +@pytest.mark.django_db +def test_apply_datetime_interval_matches_overlapping_intervals_including_bounds(): + before = DatastreamFactory(phenomenon_begin_time=JAN_1 - timedelta(days=10), phenomenon_end_time=JAN_1 - timedelta(days=1)) + touches_start = DatastreamFactory(phenomenon_begin_time=JAN_1 - timedelta(days=10), phenomenon_end_time=JAN_1) + inside = DatastreamFactory(phenomenon_begin_time=JAN_1 + timedelta(days=1), phenomenon_end_time=JAN_1 + timedelta(days=2)) + spans = DatastreamFactory(phenomenon_begin_time=JAN_1 - timedelta(days=10), phenomenon_end_time=FEB_1 + timedelta(days=10)) + touches_end = DatastreamFactory(phenomenon_begin_time=FEB_1, phenomenon_end_time=FEB_1 + timedelta(days=10)) + after = DatastreamFactory(phenomenon_begin_time=FEB_1 + timedelta(days=1), phenomenon_end_time=FEB_1 + timedelta(days=10)) + + interval = TimeInterval(start=JAN_1, end=FEB_1) + matched = _ids( + APIService.apply_datetime_interval( + Datastream.objects.all(), interval, "phenomenon_begin_time", "phenomenon_end_time" + ) + ) + + assert matched == {touches_start.id, inside.id, spans.id, touches_end.id} + assert not matched & {before.id, after.id} + + +@pytest.mark.django_db +def test_apply_datetime_interval_matches_items_without_an_interval(): + no_data = DatastreamFactory(phenomenon_begin_time=None, phenomenon_end_time=None) + DatastreamFactory(phenomenon_begin_time=FEB_1 + timedelta(days=1), phenomenon_end_time=FEB_1 + timedelta(days=10)) + + interval = TimeInterval(start=JAN_1, end=JAN_1) + matched = _ids( + APIService.apply_datetime_interval( + Datastream.objects.all(), interval, "phenomenon_begin_time", "phenomenon_end_time" + ) + ) + + assert matched == {no_data.id} + + +@pytest.mark.django_db +def test_apply_datetime_interval_supports_open_ended_requests(): + early = DatastreamFactory(phenomenon_begin_time=JAN_1 - timedelta(days=10), phenomenon_end_time=JAN_1 - timedelta(days=1)) + late = DatastreamFactory(phenomenon_begin_time=FEB_1, phenomenon_end_time=FEB_1 + timedelta(days=10)) + + def matched(interval): + return _ids( + APIService.apply_datetime_interval( + Datastream.objects.all(), interval, "phenomenon_begin_time", "phenomenon_end_time" + ) + ) + + assert matched(TimeInterval(start=JAN_1, end=None)) == {late.id} + assert matched(TimeInterval(start=None, end=JAN_1)) == {early.id} diff --git a/django/tests/interfaces/api/test_landing_views.py b/django/tests/interfaces/api/test_landing_views.py new file mode 100644 index 00000000..09b243ee --- /dev/null +++ b/django/tests/interfaces/api/test_landing_views.py @@ -0,0 +1,125 @@ +import pytest + +from django.test import override_settings +from django.urls import reverse + +from interfaces.api.urls import api + +pytestmark = [pytest.mark.django_db, pytest.mark.usefixtures("proxy_base_url")] + +# The landing page links to the API definition, documentation, conformance declaration and +# collections, and /conformance lists the classes the API implements (OGC API - Features Core +# Req 1-6; OGC API - Common Core landing-page and json classes). Links are built from +# PROXY_BASE_URL, not the request's Host, which the test client sends as "testserver". + +BASE_URL = "https://hydroserver.example.org" +LANDING_PATH = "/api/ogc/" +CONFORMANCE_PATH = "/api/ogc/conformance" +COMMON_CORE = "http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/core" +COMMON_JSON = "http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/json" +COMMON_LANDING_PAGE = "http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/landing-page" +OGC_CONFORMANCE_REL = "http://www.opengis.net/def/rel/ogc/1.0/conformance" +FEATURES_CORE = "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/core" + + +@pytest.fixture +def proxy_base_url(): + with override_settings(PROXY_BASE_URL=BASE_URL): + yield + + +def _links(response): + return {link["rel"]: link for link in response.json()["links"]} + + +def test_landing_page_describes_the_api(client): + response = client.get(LANDING_PATH) + + assert response.status_code == 200 + assert response.json()["title"] == api.title + assert response.json()["description"] == api.description + + +def test_landing_page_links(client): + links = _links(client.get(LANDING_PATH)) + + assert {rel: (link["href"], link["type"]) for rel, link in links.items()} == { + "self": (f"{BASE_URL}/api/ogc/", "application/json"), + "service-desc": (f"{BASE_URL}/api/ogc/openapi.json", "application/json"), + "service-doc": (f"{BASE_URL}/api/ogc/docs", "text/html"), + "conformance": (f"{BASE_URL}/api/ogc/conformance", "application/json"), + OGC_CONFORMANCE_REL: (f"{BASE_URL}/api/ogc/conformance", "application/json"), + "data": (f"{BASE_URL}/api/ogc/collections", "application/json"), + } + + +@pytest.mark.parametrize( + "rel", ["self", "service-desc", "service-doc", "conformance", OGC_CONFORMANCE_REL, "data"] +) +def test_landing_page_link_returns_its_declared_media_type(client, rel): + link = _links(client.get(LANDING_PATH))[rel] + + response = client.get(link["href"].removeprefix(BASE_URL), HTTP_ACCEPT=link["type"]) + + assert response.status_code == 200 + assert response["Content-Type"].split(";")[0] == link["type"] + + +def test_conformance_declares_the_implemented_classes(client): + response = client.get(CONFORMANCE_PATH) + + assert response.status_code == 200 + assert response.json() == { + "conformsTo": [COMMON_CORE, COMMON_JSON, COMMON_LANDING_PAGE, FEATURES_CORE] + } + + +@pytest.mark.parametrize("path", [LANDING_PATH, CONFORMANCE_PATH]) +def test_capabilities_endpoints_return_json(client, path): + response = client.get(path, HTTP_ACCEPT="application/json") + + assert response.status_code == 200 + assert response["Content-Type"].split(";")[0] == "application/json" + + +def test_landing_page_matches_the_common_landing_page_schema(client): + body = client.get(LANDING_PATH).json() + + assert isinstance(body["links"], list) + assert all(isinstance(link["href"], str) and isinstance(link["rel"], str) for link in body["links"]) + + +def test_conformance_matches_the_common_conformance_schema(client): + body = client.get(CONFORMANCE_PATH).json() + + assert isinstance(body["conformsTo"], list) + assert all(isinstance(uri, str) for uri in body["conformsTo"]) + + +@pytest.mark.parametrize("path", [LANDING_PATH, CONFORMANCE_PATH]) +def test_capabilities_endpoints_are_public(client, path): + assert client.get(path).status_code == 200 + + +@pytest.mark.parametrize("path", [LANDING_PATH, CONFORMANCE_PATH]) +def test_capabilities_endpoints_reject_unknown_query_parameters(client, path): + assert client.get(path, {"foo": "bar"}).status_code == 400 + + +def test_api_root_route_name_still_resolves_to_the_landing_page(): + assert reverse("ogc:api-root") == LANDING_PATH + + +def test_openapi_document_keeps_the_api_path_prefix(): + paths = api.get_openapi_schema(path_prefix=LANDING_PATH)["paths"] + + assert "get" in paths[LANDING_PATH] + assert "get" in paths[CONFORMANCE_PATH] + assert "/api/ogc/collections/units/items" in paths + + +@pytest.mark.parametrize("path", [LANDING_PATH, CONFORMANCE_PATH]) +def test_capabilities_endpoints_are_documented_under_capabilities(path): + paths = api.get_openapi_schema(path_prefix=LANDING_PATH)["paths"] + + assert paths[path]["get"]["tags"] == ["Capabilities"] diff --git a/django/tests/interfaces/api/test_limit.py b/django/tests/interfaces/api/test_limit.py new file mode 100644 index 00000000..c91ad985 --- /dev/null +++ b/django/tests/interfaces/api/test_limit.py @@ -0,0 +1,100 @@ +import pytest + +from interfaces.api.http.errors import BadRequestError +from interfaces.api.schemas import base +from interfaces.api.service import APIService +from interfaces.api.urls import api +from processing.orchestration.models import TaskRun +from core.iam.models import Workspace +from tests.core.iam.factories import UserFactory, WorkspaceFactory +from tests.core.sta.factories import DatastreamFactory, MonitoringSiteFactory, ObservationFactory +from tests.processing.etl.factories import DataConnectionFactory, EtlTaskFactory, PayloadFactory + +pytestmark = pytest.mark.django_db + +# A limit above the maximum isn't an error; the maximum is used instead (OGC API - Features +# Core Req 22C). Tests that count returned items lower the maximum so they don't need 100k rows. + +MONITORING_SITES_URL = "/api/ogc/collections/monitoring-sites/items" + + +@pytest.fixture +def max_limit_of_two(monkeypatch): + monkeypatch.setattr(base, "MAX_LIMIT", 2) + + +def test_get_items_uses_the_maximum_when_limit_is_larger(client): + response = client.get(MONITORING_SITES_URL, {"limit": 200000}) + + assert response.status_code == 200 + assert response.json()["meta"]["limit"] == 100000 + + +def test_get_items_returns_at_most_the_maximum_number_of_items(client, max_limit_of_two): + MonitoringSiteFactory.create_batch(3) + + response = client.get(MONITORING_SITES_URL, {"limit": 5}) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 2 + assert response.json()["meta"]["limit"] == 2 + + +def test_get_observations_row_format_uses_the_maximum(client, max_limit_of_two): + datastream = DatastreamFactory() + ObservationFactory.create_batch(3, datastream=datastream) + + response = client.get( + "/api/ogc/collections/observations/items", + {"datastream_id": str(datastream.id), "format": "row", "limit": 5}, + ) + + assert response.status_code == 200 + assert len(response.json()["data"]["rows"]) == 2 + + +def test_get_task_runs_uses_the_maximum(client, max_limit_of_two): + owner = UserFactory() + data_connection = DataConnectionFactory(workspace=WorkspaceFactory(owner=owner)) + PayloadFactory(data_connection=data_connection) + task = EtlTaskFactory(data_connection=data_connection) + for _ in range(3): + TaskRun.objects.create(task=task, status="SUCCESS") + client.force_login(owner) + + response = client.get(f"/api/ogc/collections/etl-tasks/items/{task.id}/runs", {"limit": 5}) + + assert response.status_code == 200 + assert len(response.json()["data"]) == 2 + assert response.json()["meta"]["limit"] == 2 + + +def test_get_items_returns_400_for_a_negative_limit(client): + response = client.get(MONITORING_SITES_URL, {"limit": -1}) + + assert response.status_code == 400 + + +def test_limit_declares_the_maximum_in_the_openapi_document(): + parameters = api.get_openapi_schema(path_prefix="/api/ogc/")["paths"][MONITORING_SITES_URL]["get"][ + "parameters" + ] + limit = next(p for p in parameters if p["name"] == "limit") + + assert limit["schema"]["type"] == "integer" + assert limit["schema"]["minimum"] == 0 + assert limit["schema"]["maximum"] == 100000 + + +def test_apply_pagination_uses_the_maximum_when_limit_is_larger(max_limit_of_two): + WorkspaceFactory.create_batch(3) + + page, meta = APIService.apply_pagination(Workspace.objects.order_by("pk"), limit=5) + + assert len(page) == 2 + assert meta.limit == 2 + + +def test_apply_pagination_rejects_a_negative_limit(): + with pytest.raises(BadRequestError): + APIService.apply_pagination(Workspace.objects.all(), limit=-1) diff --git a/django/tests/interfaces/api/test_linked_resource_type_views.py b/django/tests/interfaces/api/test_linked_resource_type_views.py index d1017f24..ac1aa864 100644 --- a/django/tests/interfaces/api/test_linked_resource_type_views.py +++ b/django/tests/interfaces/api/test_linked_resource_type_views.py @@ -5,7 +5,7 @@ pytestmark = pytest.mark.django_db -LINKED_RESOURCE_TYPES_URL = "/api/data/linked-resource-types" +LINKED_RESOURCE_TYPES_URL = "/api/ogc/collections/linked-resource-types/items" def _detail_url(linked_resource_type_id): diff --git a/django/tests/interfaces/api/test_links.py b/django/tests/interfaces/api/test_links.py new file mode 100644 index 00000000..1f14a010 --- /dev/null +++ b/django/tests/interfaces/api/test_links.py @@ -0,0 +1,237 @@ +import re +import uuid + +from urllib.parse import parse_qs, urlsplit + +import pytest + +from django.test import override_settings + +from interfaces.api.collections import COLLECTIONS +from interfaces.api.http.links import build_collection_link +from interfaces.api.schemas import base +from interfaces.api.urls import api +from tests.core.iam.factories import UserFactory +from tests.core.sta.factories import DatastreamFactory, MonitoringSiteFactory, ObservationFactory, UnitFactory + +pytestmark = [pytest.mark.django_db, pytest.mark.usefixtures("proxy_base_url")] + +# List and item responses link to themselves (OGC API - Features Core Req 28 and 35) and pages +# link to the next and previous pages (Core Recs 17-19, Permission 7). Links are built from +# PROXY_BASE_URL, not the request's Host, which the test client sends as "testserver". + +BASE_URL = "https://hydroserver.example.org" +UNITS_PATH = "/api/ogc/collections/units/items" +OBSERVATIONS_PATH = "/api/ogc/collections/observations/items" + + +@pytest.fixture +def proxy_base_url(): + with override_settings(PROXY_BASE_URL=f"{BASE_URL}/"): + yield + + +@pytest.fixture +def max_limit_of_two(monkeypatch): + monkeypatch.setattr(base, "MAX_LIMIT", 2) + + +def _links(response): + return {link["rel"]: link for link in response.json()["links"]} + + +def _query(href): + return parse_qs(urlsplit(href).query) + + +def test_list_self_link_is_the_request_url_on_proxy_base_url(client): + response = client.get(f"{UNITS_PATH}?limit=5&sortby=-name") + + assert response.status_code == 200 + assert _links(response)["self"] == { + "href": f"{BASE_URL}{UNITS_PATH}?limit=5&sortby=-name", + "rel": "self", + "type": "application/json", + } + + +def test_list_self_link_without_a_query_string_has_no_question_mark(client): + response = client.get(UNITS_PATH) + + assert _links(response)["self"]["href"] == f"{BASE_URL}{UNITS_PATH}" + + +def test_item_links_to_itself_and_its_collection(client): + unit = UnitFactory(global_=True) + + response = client.get(f"{UNITS_PATH}/{unit.id}", {"properties": "name"}) + + assert response.status_code == 200 + assert response.json()["links"] == [ + { + "href": f"{BASE_URL}{UNITS_PATH}/{unit.id}?properties=name", + "rel": "self", + "type": "application/json", + }, + { + "href": f"{BASE_URL}/api/ogc/collections/units", + "rel": "collection", + "type": "application/json", + }, + ] + + +def test_item_collection_link_resolves_to_its_collection(client): + site = MonitoringSiteFactory() + + response = client.get(f"/api/ogc/collections/monitoring-sites/items/{site.id}") + collection_href = _links(response)["collection"]["href"] + + assert client.get(collection_href.removeprefix(BASE_URL)).json()["id"] == "monitoring-sites" + + +@pytest.mark.parametrize("collection", COLLECTIONS, ids=lambda collection: collection.id) +def test_collection_link_for_an_item_of_every_collection(rf, collection): + request = rf.get(f"/api/ogc/collections/{collection.id}/items/{uuid.uuid4()}") + + assert build_collection_link(request).href == f"{BASE_URL}/api/ogc/collections/{collection.id}" + + +@pytest.mark.parametrize( + "path", + [ + f"/api/ogc/collections/quality-control-histories/items/{uuid.uuid4()}/sessions/{uuid.uuid4()}", + f"/api/ogc/collections/etl-tasks/items/{uuid.uuid4()}/runs/{uuid.uuid4()}", + f"/api/ogc/collections/workspaces/items/{uuid.uuid4()}/service-accounts/{uuid.uuid4()}", + f"/api/ogc/collections/not-a-collection/items/{uuid.uuid4()}", + "/api/ogc/collections/units/items", + ], +) +def test_no_collection_link_outside_a_collection_item(rf, path): + assert build_collection_link(rf.get(path)) is None + + +def test_full_first_page_links_next_but_not_prev(client): + UnitFactory.create_batch(3, global_=True) + + links = _links(client.get(UNITS_PATH, {"limit": 2})) + + assert set(links) == {"self", "next"} + assert _query(links["next"]["href"]) == {"limit": ["2"], "offset": ["2"]} + assert links["next"]["type"] == "application/json" + + +def test_middle_page_links_next_and_prev(client): + UnitFactory.create_batch(5, global_=True) + + links = _links(client.get(UNITS_PATH, {"limit": 2, "offset": 2})) + + assert set(links) == {"self", "next", "prev"} + assert _query(links["next"]["href"])["offset"] == ["4"] + assert _query(links["prev"]["href"])["offset"] == ["0"] + + +def test_partial_last_page_links_prev_but_not_next(client): + UnitFactory.create_batch(3, global_=True) + + links = _links(client.get(UNITS_PATH, {"limit": 2, "offset": 2})) + + assert set(links) == {"self", "prev"} + + +def test_prev_link_does_not_go_below_offset_zero(client): + UnitFactory.create_batch(3, global_=True) + + links = _links(client.get(UNITS_PATH, {"limit": 2, "offset": 1})) + + assert _query(links["prev"]["href"]) == {"limit": ["2"], "offset": ["0"]} + + +def test_zero_limit_links_only_self(client): + UnitFactory(global_=True) + + links = _links(client.get(UNITS_PATH, {"limit": 0})) + + assert set(links) == {"self"} + + +def test_page_links_use_the_limit_that_was_applied(client, max_limit_of_two): + UnitFactory.create_batch(3, global_=True) + + links = _links(client.get(UNITS_PATH, {"limit": 50})) + + assert _query(links["self"]["href"])["limit"] == ["50"] + assert _query(links["next"]["href"])["limit"] == ["2"] + + +def test_page_links_keep_the_other_query_parameters(client): + UnitFactory.create_batch(3, global_=True) + + response = client.get( + f"{UNITS_PATH}?limit=2&properties=name&properties=symbol&sortby=-name,%2Bsymbol" + "&datetime=2024-01-01T00%3A00%3A00Z%2F.." + ) + + assert response.status_code == 200 + assert _query(_links(response)["next"]["href"]) == { + "limit": ["2"], + "offset": ["2"], + "properties": ["name", "symbol"], + "sortby": ["-name,+symbol"], + "datetime": ["2024-01-01T00:00:00Z/.."], + } + + +def test_following_next_links_returns_every_item_once(client): + units = UnitFactory.create_batch(5, global_=True) + seen = [] + url = f"{UNITS_PATH}?limit=2" + + while url: + response = client.get(url) + seen.extend(item["id"] for item in response.json()["data"]) + next_link = _links(response).get("next") + url = next_link["href"].removeprefix(BASE_URL) if next_link else None + + assert sorted(seen) == sorted(str(unit.id) for unit in units) + + +@pytest.mark.parametrize("response_format, key", [("row", "rows"), ("column", "phenomenonTime")]) +def test_observation_formats_link_next_from_their_row_count(client, response_format, key): + datastream = DatastreamFactory() + ObservationFactory.create_batch(3, datastream=datastream) + + response = client.get( + OBSERVATIONS_PATH, + {"datastream_id": str(datastream.id), "format": response_format, "limit": 2}, + ) + + assert response.status_code == 200 + assert len(response.json()["data"][key]) == 2 + assert set(_links(response)) == {"self", "next"} + + +def _collection_items_paths(): + paths = api.get_openapi_schema(path_prefix="/api/ogc/")["paths"] + pattern = re.compile(r"/api/ogc/collections/[a-z-]+/items") + + return sorted(path for path in paths if pattern.fullmatch(path) and "get" in paths[path]) + + +@pytest.mark.parametrize("path", _collection_items_paths()) +def test_every_collection_links_to_itself(client, path): + client.force_login(UserFactory()) + + response = client.get(path) + + assert response.status_code == 200 + links = response.json()["links"] + assert _links(response)["self"]["href"] == f"{BASE_URL}{path}" + assert all({"href", "rel", "type"} <= set(link) for link in links) + + +def test_links_are_documented_in_the_openapi_document(): + schemas = api.get_openapi_schema(path_prefix="/api/ogc/")["components"]["schemas"] + + assert schemas["Link"]["required"] == ["href", "rel", "type"] + assert "links" in schemas["PaginatedResponse_UnitResponse_"]["properties"] diff --git a/django/tests/interfaces/api/test_method_type_views.py b/django/tests/interfaces/api/test_method_type_views.py index a4225849..1afb73d5 100644 --- a/django/tests/interfaces/api/test_method_type_views.py +++ b/django/tests/interfaces/api/test_method_type_views.py @@ -5,7 +5,7 @@ pytestmark = pytest.mark.django_db -METHOD_TYPES_URL = "/api/data/method-types" +METHOD_TYPES_URL = "/api/ogc/collections/method-types/items" def _detail_url(method_type_id): diff --git a/django/tests/interfaces/api/test_method_views.py b/django/tests/interfaces/api/test_method_views.py index 705448fe..5fd80150 100644 --- a/django/tests/interfaces/api/test_method_views.py +++ b/django/tests/interfaces/api/test_method_views.py @@ -14,7 +14,7 @@ pytestmark = pytest.mark.django_db -METHODS_URL = "/api/data/methods" +METHODS_URL = "/api/ogc/collections/methods/items" def _detail_url(method_id): diff --git a/django/tests/interfaces/api/test_monitoring_rule_views.py b/django/tests/interfaces/api/test_monitoring_rule_views.py index 6a144f91..3a48b897 100644 --- a/django/tests/interfaces/api/test_monitoring_rule_views.py +++ b/django/tests/interfaces/api/test_monitoring_rule_views.py @@ -15,7 +15,7 @@ pytestmark = pytest.mark.django_db -RULES_URL = "/api/data/monitoring-rules" +RULES_URL = "/api/ogc/collections/monitoring-rules/items" MONITORING_RULE_FIELDS = { "id", diff --git a/django/tests/interfaces/api/test_monitoring_site_type_views.py b/django/tests/interfaces/api/test_monitoring_site_type_views.py index 50d822b4..dffbbd4f 100644 --- a/django/tests/interfaces/api/test_monitoring_site_type_views.py +++ b/django/tests/interfaces/api/test_monitoring_site_type_views.py @@ -5,7 +5,7 @@ pytestmark = pytest.mark.django_db -MONITORING_SITE_TYPES_URL = "/api/data/monitoring-site-types" +MONITORING_SITE_TYPES_URL = "/api/ogc/collections/monitoring-site-types/items" def _detail_url(monitoring_site_type_id): diff --git a/django/tests/interfaces/api/test_monitoring_site_views.py b/django/tests/interfaces/api/test_monitoring_site_views.py index cb63b77c..cc413a2c 100644 --- a/django/tests/interfaces/api/test_monitoring_site_views.py +++ b/django/tests/interfaces/api/test_monitoring_site_views.py @@ -1,9 +1,12 @@ +import uuid + import pytest from django.core.files.uploadedfile import SimpleUploadedFile from django.db import connection from django.test.client import BOUNDARY, MULTIPART_CONTENT, encode_multipart from django.test.utils import CaptureQueriesContext +from core.sta.models import MonitoringSiteLinkedResource from tests.core.iam.factories import ( CollaboratorFactory, PermissionFactory, @@ -11,11 +14,12 @@ UserFactory, WorkspaceFactory, ) -from tests.core.sta.factories import MonitoringSiteFactory, MonitoringSiteTypeFactory +from tests.core.sta.factories import DatastreamFactory, MonitoringSiteFactory, MonitoringSiteTypeFactory pytestmark = pytest.mark.django_db -MONITORING_SITES_URL = "/api/data/monitoring-sites" +MONITORING_SITES_COLLECTION_URL = "/api/ogc/collections/monitoring-sites" +MONITORING_SITES_URL = f"{MONITORING_SITES_COLLECTION_URL}/items" def _detail_url(monitoring_site_id): @@ -89,6 +93,177 @@ def test_get_monitoring_sites_returns_400_for_malformed_bbox(client): assert response.status_code == 400 +@pytest.mark.parametrize( + "bbox", + ["-181,40,-111,41", "-112,-91,-111,41", "-112,41,-111,40", "-112,40,-111", "-112,40,100,-111,41,0"], +) +def test_get_monitoring_sites_returns_400_for_invalid_bbox(client, bbox): + response = client.get(MONITORING_SITES_URL, {"bbox": bbox}) + + assert response.status_code == 400 + + +def _site_ids(response): + return {site["id"] for site in response.json()["data"]} + + +def test_get_monitoring_sites_filters_by_bbox(client): + inside = MonitoringSiteFactory(longitude=-111.5, latitude=40.5) + MonitoringSiteFactory(longitude=-100, latitude=40.5) + + response = client.get(MONITORING_SITES_URL, {"bbox": "-112,40,-111,41"}) + + assert response.status_code == 200 + assert _site_ids(response) == {str(inside.id)} + + +def test_get_monitoring_sites_accepts_a_six_value_bbox_and_ignores_heights(client): + inside = MonitoringSiteFactory(longitude=-111.5, latitude=40.5, elevation_m=5000) + MonitoringSiteFactory(longitude=-100, latitude=40.5) + + response = client.get(MONITORING_SITES_URL, {"bbox": "-112,40,0,-111,41,10"}) + + assert response.status_code == 200 + assert _site_ids(response) == {str(inside.id)} + + +def test_get_monitoring_sites_filters_by_a_bbox_crossing_the_antimeridian(client): + east = MonitoringSiteFactory(longitude=175, latitude=-15) + west = MonitoringSiteFactory(longitude=-175, latitude=-15) + MonitoringSiteFactory(longitude=0, latitude=-15) + + response = client.get(MONITORING_SITES_URL, {"bbox": "170,-20,-170,-10"}) + + assert response.status_code == 200 + assert _site_ids(response) == {str(east.id), str(west.id)} + + +# --- datetime ---------------------------------------------------------------------------- +# +# A site's time is the observed time of each of its datastreams the requester can view. It +# matches a datetime when one of those datastreams overlaps it, or when none of them has +# observations (no time matches any datetime, per OGC API - Features Core Req 26C). + + +def _observed_datastream(monitoring_site, begin, end, **kwargs): + return DatastreamFactory( + monitoring_site=monitoring_site, + phenomenon_begin_time=begin, + phenomenon_end_time=end, + **kwargs, + ) + + +def test_get_monitoring_sites_datetime_matches_sites_with_an_overlapping_datastream(client): + overlapping = MonitoringSiteFactory() + _observed_datastream(overlapping, "2024-01-01T00:00:00Z", "2024-03-01T00:00:00Z") + touching = MonitoringSiteFactory() + _observed_datastream(touching, "2024-03-01T00:00:00Z", "2024-04-01T00:00:00Z") + before = MonitoringSiteFactory() + _observed_datastream(before, "2023-01-01T00:00:00Z", "2023-02-01T00:00:00Z") + + response = client.get( + MONITORING_SITES_URL, {"datetime": "2024-02-01T00:00:00Z/2024-03-01T00:00:00Z"} + ) + + assert response.status_code == 200 + assert _site_ids(response) == {str(overlapping.id), str(touching.id)} + + +def test_get_monitoring_sites_datetime_requires_one_datastream_to_overlap(client): + # Neither datastream has observations in March, even though the site's overall span does. + site = MonitoringSiteFactory() + _observed_datastream(site, "2024-01-01T00:00:00Z", "2024-02-01T00:00:00Z") + _observed_datastream(site, "2024-06-01T00:00:00Z", "2024-07-01T00:00:00Z") + + march = client.get(MONITORING_SITES_URL, {"datetime": "2024-03-01T00:00:00Z/2024-03-31T00:00:00Z"}) + june = client.get(MONITORING_SITES_URL, {"datetime": "2024-06-15T00:00:00Z"}) + + assert str(site.id) not in _site_ids(march) + assert str(site.id) in _site_ids(june) + + +@pytest.mark.parametrize( + "datetime_value, expected", + [ + ("2024-06-01T00:00:00Z/..", {"later"}), + ("../2024-01-31T00:00:00Z", {"earlier"}), + ("2024-06-01T00:00:00Z/", {"later"}), + ], +) +def test_get_monitoring_sites_datetime_accepts_half_bounded_intervals(client, datetime_value, expected): + sites = { + "earlier": MonitoringSiteFactory(), + "later": MonitoringSiteFactory(), + } + _observed_datastream(sites["earlier"], "2024-01-01T00:00:00Z", "2024-02-01T00:00:00Z") + _observed_datastream(sites["later"], "2024-06-01T00:00:00Z", "2024-07-01T00:00:00Z") + + response = client.get(MONITORING_SITES_URL, {"datetime": datetime_value}) + + assert response.status_code == 200 + assert _site_ids(response) == {str(sites[name].id) for name in expected} + + +def test_get_monitoring_sites_datetime_matches_sites_without_observed_datastreams(client): + without_datastreams = MonitoringSiteFactory() + without_observations = MonitoringSiteFactory() + DatastreamFactory(monitoring_site=without_observations) + + response = client.get(MONITORING_SITES_URL, {"datetime": "2024-02-01T00:00:00Z"}) + + assert _site_ids(response) == {str(without_datastreams.id), str(without_observations.id)} + + +def test_get_monitoring_sites_datetime_ignores_private_datastreams_the_requester_cannot_view(client): + # A private datastream that overlaps must not make the site match... + hidden_overlap = MonitoringSiteFactory() + _observed_datastream(hidden_overlap, "2023-01-01T00:00:00Z", "2023-02-01T00:00:00Z") + _observed_datastream(hidden_overlap, "2024-01-01T00:00:00Z", "2024-03-01T00:00:00Z", private=True) + # ...and a site whose only observed datastream is private has no visible time, so it matches. + only_private = MonitoringSiteFactory() + _observed_datastream(only_private, "2023-01-01T00:00:00Z", "2023-02-01T00:00:00Z", private=True) + + response = client.get(MONITORING_SITES_URL, {"datetime": "2024-02-01T00:00:00Z"}) + + assert str(hidden_overlap.id) not in _site_ids(response) + assert str(only_private.id) in _site_ids(response) + + +def test_get_monitoring_sites_datetime_uses_private_datastreams_the_owner_can_view(client): + owner = UserFactory() + site = MonitoringSiteFactory(workspace=WorkspaceFactory(owner=owner)) + _observed_datastream(site, "2024-01-01T00:00:00Z", "2024-03-01T00:00:00Z", private=True) + client.force_login(owner) + + overlapping = client.get(MONITORING_SITES_URL, {"datetime": "2024-02-01T00:00:00Z"}) + elsewhere = client.get(MONITORING_SITES_URL, {"datetime": "2020-01-01T00:00:00Z"}) + + assert str(site.id) in _site_ids(overlapping) + assert str(site.id) not in _site_ids(elsewhere) + + +def test_get_monitoring_sites_datetime_combines_with_bbox(client): + inside = MonitoringSiteFactory(longitude=-111.5, latitude=40.5) + _observed_datastream(inside, "2024-01-01T00:00:00Z", "2024-03-01T00:00:00Z") + inside_elsewhen = MonitoringSiteFactory(longitude=-111.5, latitude=40.5) + _observed_datastream(inside_elsewhen, "2020-01-01T00:00:00Z", "2020-03-01T00:00:00Z") + outside = MonitoringSiteFactory(longitude=-100, latitude=40.5) + _observed_datastream(outside, "2024-01-01T00:00:00Z", "2024-03-01T00:00:00Z") + + response = client.get( + MONITORING_SITES_URL, {"bbox": "-112,40,-111,41", "datetime": "2024-02-01T00:00:00Z"} + ) + + assert _site_ids(response) == {str(inside.id)} + + +def test_get_monitoring_sites_returns_400_for_invalid_datetime(client): + response = client.get(MONITORING_SITES_URL, {"datetime": "2024-02-01"}) + + assert response.status_code == 400 + + def test_get_monitoring_sites_returns_400_for_malformed_tag(client): response = client.get(MONITORING_SITES_URL, {"tag": "no-colon-in-here"}) @@ -277,7 +452,7 @@ def test_create_monitoring_site_returns_403_without_create_permission(client): def test_get_site_type_icons_returns_configured_icon_mappings(client): - response = client.get(f"{MONITORING_SITES_URL}/site-type-icons") + response = client.get(f"{MONITORING_SITES_COLLECTION_URL}/site-type-icons") assert response.status_code == 200 icons = {entry["icon"] for entry in response.json()} @@ -682,23 +857,53 @@ def test_get_monitoring_site_markers_returns_marker_for_public_monitoring_site(c workspace = WorkspaceFactory() monitoring_site = MonitoringSiteFactory(workspace=workspace) - response = client.get(f"{MONITORING_SITES_URL}/markers") + response = client.get(f"{MONITORING_SITES_COLLECTION_URL}/markers") assert response.status_code == 200 assert str(monitoring_site.id) in [m["id"] for m in response.json()] def test_get_monitoring_site_markers_returns_400_for_malformed_bbox(client): - response = client.get(f"{MONITORING_SITES_URL}/markers", {"bbox": "not,a,valid,bbox"}) + response = client.get(f"{MONITORING_SITES_COLLECTION_URL}/markers", {"bbox": "not,a,valid,bbox"}) + + assert response.status_code == 400 + + +def test_get_monitoring_site_markers_returns_400_for_out_of_range_bbox(client): + response = client.get(f"{MONITORING_SITES_COLLECTION_URL}/markers", {"bbox": "-112,40,-111,91"}) assert response.status_code == 400 +def test_get_monitoring_site_markers_filters_public_markers_by_a_bbox_crossing_the_antimeridian(client): + east = MonitoringSiteFactory(longitude=175, latitude=-15) + west = MonitoringSiteFactory(longitude=-175, latitude=-15) + MonitoringSiteFactory(longitude=0, latitude=-15) + + response = client.get(f"{MONITORING_SITES_COLLECTION_URL}/markers", {"bbox": "170,-20,-170,-10"}) + + assert response.status_code == 200 + assert {m["id"] for m in response.json()} == {str(east.id), str(west.id)} + + +def test_get_monitoring_site_markers_filters_private_markers_by_bbox(client): + owner = UserFactory() + workspace = WorkspaceFactory(owner=owner) + inside = MonitoringSiteFactory(workspace=workspace, private=True, longitude=-111.5, latitude=40.5) + MonitoringSiteFactory(workspace=workspace, private=True, longitude=-100, latitude=40.5) + client.force_login(owner) + + response = client.get(f"{MONITORING_SITES_COLLECTION_URL}/markers", {"bbox": "-112,40,-111,41"}) + + assert response.status_code == 200 + assert {m["id"] for m in response.json()} == {str(inside.id)} + + def test_get_monitoring_site_site_summaries_returns_summary_for_public_monitoring_site(client): workspace = WorkspaceFactory() monitoring_site = MonitoringSiteFactory(workspace=workspace) - response = client.get(f"{MONITORING_SITES_URL}/site-summaries") + response = client.get(f"{MONITORING_SITES_COLLECTION_URL}/site-summaries") assert response.status_code == 200 assert str(monitoring_site.id) in [s["id"] for s in response.json()] @@ -710,7 +915,7 @@ def test_get_monitoring_site_task_summaries_returns_summary_for_workspace_owner( monitoring_site = MonitoringSiteFactory(workspace=workspace) client.force_login(owner) - response = client.get(f"{MONITORING_SITES_URL}/task-summaries") + response = client.get(f"{MONITORING_SITES_COLLECTION_URL}/task-summaries") assert response.status_code == 200 summary = next(s for s in response.json() if s["id"] == str(monitoring_site.id)) @@ -726,7 +931,7 @@ def test_get_monitoring_site_tag_keys_returns_keys_for_workspace_owner(client): monitoring_site.save() client.force_login(owner) - response = client.get(f"{MONITORING_SITES_URL}/tags/keys") + response = client.get(f"{MONITORING_SITES_COLLECTION_URL}/tags/keys") assert response.status_code == 200 assert response.json()["season"] == ["summer"] @@ -739,6 +944,98 @@ def _linked_resources_url(monitoring_site_id): return f"{_detail_url(monitoring_site_id)}/linked-resources" +def _make_monitoring_site_linked_resources(owner, names): + workspace = WorkspaceFactory(owner=owner) + parent = MonitoringSiteFactory(workspace=workspace) + for name in names: + MonitoringSiteLinkedResource.objects.create(monitoring_site=parent, name=name, type="Report", url=f"https://example.com/{name}") + return parent + + +def test_get_monitoring_site_linked_resources_pages_by_name(client): + owner = UserFactory() + parent = _make_monitoring_site_linked_resources(owner, ["C", "A", "B"]) + client.force_login(owner) + + first = client.get(_linked_resources_url(parent.id), {"limit": 2}).json() + second = client.get(_linked_resources_url(parent.id), {"limit": 2, "offset": 2}).json() + + assert [item["name"] for item in first["data"]] == ["A", "B"] + assert first["meta"] == {"limit": 2, "offset": 0, "totalCount": 3} + assert "next" in [link["rel"] for link in first["links"]] + assert [item["name"] for item in second["data"]] == ["C"] + + +def test_get_monitoring_site_linked_resources_selects_properties(client): + owner = UserFactory() + parent = _make_monitoring_site_linked_resources(owner, ["A"]) + client.force_login(owner) + + response = client.get(_linked_resources_url(parent.id), {"properties": "name"}) + + assert response.json()["data"] == [{"name": "A"}] + + +def test_get_monitoring_site_linked_resources_rejects_include(client): + owner = UserFactory() + parent = _make_monitoring_site_linked_resources(owner, ["A"]) + client.force_login(owner) + + response = client.get(_linked_resources_url(parent.id), {"include": "monitoringSite"}) + + assert response.status_code == 400 + + +def test_get_monitoring_site_linked_resource_returns_item(client): + owner = UserFactory() + parent = _make_monitoring_site_linked_resources(owner, ["A", "B"]) + linked_resource = MonitoringSiteLinkedResource.objects.get(monitoring_site=parent, name="B") + client.force_login(owner) + + response = client.get(f"{_linked_resources_url(parent.id)}/{linked_resource.id}") + + assert response.status_code == 200 + data = response.json()["data"] + assert data["id"] == str(linked_resource.id) + assert data["name"] == "B" + assert data["type"] == "Report" + assert data["link"] == "https://example.com/B" + + +def test_get_monitoring_site_linked_resource_returns_404_for_unknown_id(client): + owner = UserFactory() + parent = _make_monitoring_site_linked_resources(owner, ["A"]) + client.force_login(owner) + + response = client.get(f"{_linked_resources_url(parent.id)}/{uuid.uuid4()}") + + assert response.status_code == 404 + + +def test_get_monitoring_site_linked_resource_returns_404_for_other_parents_resource(client): + owner = UserFactory() + parent = _make_monitoring_site_linked_resources(owner, ["A"]) + other_parent = _make_monitoring_site_linked_resources(owner, ["B"]) + other_resource = MonitoringSiteLinkedResource.objects.get(monitoring_site=other_parent) + client.force_login(owner) + + response = client.get(f"{_linked_resources_url(parent.id)}/{other_resource.id}") + + assert response.status_code == 404 + + +def test_get_monitoring_site_linked_resource_returns_404_for_private_parent_when_outsider(client): + parent = MonitoringSiteFactory(workspace=WorkspaceFactory(), private=True) + linked_resource = MonitoringSiteLinkedResource.objects.create( + monitoring_site=parent, name="A", type="Report", url="https://example.com/A" + ) + client.force_login(UserFactory()) + + response = client.get(f"{_linked_resources_url(parent.id)}/{linked_resource.id}") + + assert response.status_code == 404 + + def test_add_monitoring_site_linked_resource_succeeds_with_link(client): owner = UserFactory() workspace = WorkspaceFactory(owner=owner) @@ -756,7 +1053,7 @@ def test_add_monitoring_site_linked_resource_succeeds_with_link(client): assert response.status_code == 201 assert set(response.json().keys()) == {"id"} - linked_resources = client.get(_linked_resources_url(monitoring_site.id)).json() + linked_resources = client.get(_linked_resources_url(monitoring_site.id)).json()["data"] assert linked_resources[0]["name"] == "Site Report" @@ -811,7 +1108,7 @@ def test_update_monitoring_site_linked_resource_succeeds_for_name(client): assert response.status_code == 204 assert not response.content - linked_resources = client.get(_linked_resources_url(monitoring_site.id)).json() + linked_resources = client.get(_linked_resources_url(monitoring_site.id)).json()["data"] assert linked_resources[0]["name"] == "Updated Report" diff --git a/django/tests/interfaces/api/test_monitoring_task_views.py b/django/tests/interfaces/api/test_monitoring_task_views.py index dd08fffc..f15d4d8e 100644 --- a/django/tests/interfaces/api/test_monitoring_task_views.py +++ b/django/tests/interfaces/api/test_monitoring_task_views.py @@ -1,8 +1,11 @@ +from datetime import timedelta from unittest.mock import patch import pytest +from django.conf import settings from django.db import connection from django.test.utils import CaptureQueriesContext +from django.utils import timezone from processing.orchestration.models import TaskRun from tests.core.iam.factories import ( @@ -17,7 +20,7 @@ pytestmark = pytest.mark.django_db -MONITORING_TASKS_URL = "/api/data/monitoring-tasks" +MONITORING_TASKS_URL = "/api/ogc/collections/monitoring-tasks/items" def _detail_url(task_id): @@ -309,7 +312,7 @@ def test_trigger_monitoring_task_creates_pending_run_for_workspace_owner(client) response = client.post(f"{_detail_url(task.id)}/trigger") assert response.status_code == 202 - assert response.json()["status"] == "PENDING" + assert response.json()["data"]["status"] == "PENDING" mock_apply_async.assert_called_once() assert TaskRun.objects.filter(task=task, status="PENDING").exists() @@ -343,6 +346,41 @@ def test_get_monitoring_task_runs_returns_runs_for_workspace_owner(client): assert str(run.id) in [r["id"] for r in response.json()["data"]] +def test_trigger_monitoring_task_returns_the_run_with_links_to_it(client): + owner = UserFactory() + workspace = WorkspaceFactory(owner=owner) + task = _make_monitoring_task(workspace) + client.force_login(owner) + + with patch("processing.monitoring.tasks.run_monitoring_task.apply_async"): + response = client.post(f"{_detail_url(task.id)}/trigger") + + run = TaskRun.objects.get(task=task) + run_url = f"{settings.PROXY_BASE_URL.rstrip('/')}/api/ogc/collections/monitoring-tasks/items/{task.id}/runs/{run.id}" + assert response.status_code == 202 + assert response["Location"] == run_url + assert response.json()["data"]["id"] == str(run.id) + assert response.json()["links"] == [{"href": run_url, "rel": "self", "type": "application/json"}] + + +def test_get_monitoring_task_run_returns_a_self_link_and_no_collection_link(client): + owner = UserFactory() + workspace = WorkspaceFactory(owner=owner) + task = _make_monitoring_task(workspace) + run = TaskRun.objects.create(task=task, status="SUCCESS") + client.force_login(owner) + + response = client.get(f"{_detail_url(task.id)}/runs/{run.id}") + + assert response.json()["links"] == [ + { + "href": f"{settings.PROXY_BASE_URL.rstrip('/')}/api/ogc/collections/monitoring-tasks/items/{task.id}/runs/{run.id}", + "rel": "self", + "type": "application/json", + } + ] + + def test_get_monitoring_task_run_returns_200_for_workspace_owner(client): owner = UserFactory() workspace = WorkspaceFactory(owner=owner) @@ -353,7 +391,7 @@ def test_get_monitoring_task_run_returns_200_for_workspace_owner(client): response = client.get(f"{_detail_url(task.id)}/runs/{run.id}") assert response.status_code == 200 - assert response.json()["id"] == str(run.id) + assert response.json()["data"]["id"] == str(run.id) def test_get_monitoring_task_runs_accepts_properties_filter(client): @@ -392,3 +430,31 @@ def test_get_monitoring_task_runs_ignores_unsupported_include(client): response = client.get(f"{_detail_url(task.id)}/runs", {"include": "bogus"}) assert response.status_code == 200 + + +@pytest.mark.parametrize( + "sortby, expected", + [ + ("latestRunStartedAt", ["older", "newer"]), + ("-latestRunStartedAt", ["newer", "older"]), + ("latestRunFinishedAt", ["older", "newer"]), + ("latestRunStatus", ["newer", "older"]), + ], +) +def test_get_monitoring_tasks_sorts_by_latest_run_fields(client, sortby, expected): + owner = UserFactory() + workspace = WorkspaceFactory(owner=owner) + now = timezone.now() + tasks = {"older": _make_monitoring_task(workspace), "newer": _make_monitoring_task(workspace)} + TaskRun.objects.create( + task=tasks["older"], status="SUCCESS", started_at=now - timedelta(hours=2), + finished_at=now - timedelta(hours=1), + ) + TaskRun.objects.create(task=tasks["newer"], status="FAILURE", started_at=now, finished_at=now) + client.force_login(owner) + + response = client.get(MONITORING_TASKS_URL, {"sortby": sortby}) + + assert response.status_code == 200 + ids = [task["id"] for task in response.json()["data"]] + assert ids == [str(tasks[name].id) for name in expected] diff --git a/django/tests/interfaces/api/test_observation_views.py b/django/tests/interfaces/api/test_observation_views.py index d98f058b..45f79588 100644 --- a/django/tests/interfaces/api/test_observation_views.py +++ b/django/tests/interfaces/api/test_observation_views.py @@ -29,16 +29,16 @@ def _observations_url(datastream_id=None, **params): if datastream_id is not None: query["datastream_id"] = datastream_id if query: - return f"/api/data/observations?{urlencode(query, doseq=True)}" - return "/api/data/observations" + return f"/api/ogc/collections/observations/items?{urlencode(query, doseq=True)}" + return "/api/ogc/collections/observations/items" def _detail_url(observation_id): - return f"/api/data/observations/{observation_id}" + return f"/api/ogc/collections/observations/items/{observation_id}" -_BULK_CREATE_URL = "/api/data/observations/bulk-create" -_BULK_DELETE_URL = "/api/data/observations/bulk-delete" +_BULK_CREATE_URL = "/api/ogc/collections/observations/bulk-create" +_BULK_DELETE_URL = "/api/ogc/collections/observations/bulk-delete" def _with_datastream(datastream_id, body): @@ -152,7 +152,7 @@ def test_get_observations_with_multiple_datastream_ids_unions_results(client): client.force_login(owner) response = client.get( - f"/api/data/observations?{urlencode({'datastream_id': [str(datastream_a.id), str(datastream_b.id)]}, doseq=True)}" + f"/api/ogc/collections/observations/items?{urlencode({'datastream_id': [str(datastream_a.id), str(datastream_b.id)]}, doseq=True)}" ) assert response.status_code == 200 @@ -198,7 +198,7 @@ def test_get_observations_column_format_returns_400_with_multiple_datastream_ids datastream_b = _make_datastream(workspace) response = client.get( - f"/api/data/observations?{urlencode({'datastream_id': [str(datastream_a.id), str(datastream_b.id)], 'format': 'column'}, doseq=True)}" + f"/api/ogc/collections/observations/items?{urlencode({'datastream_id': [str(datastream_a.id), str(datastream_b.id)], 'format': 'column'}, doseq=True)}" ) assert response.status_code == 400 @@ -261,7 +261,7 @@ def test_get_observations_total_count_exact_for_phenomenon_time_filter(client): response = client.get( _observations_url( - datastream.id, phenomenon_time_min=_iso(keep_time - timedelta(minutes=1)) + datastream.id, datetime=f"{_iso(keep_time - timedelta(minutes=1))}/.." ) ) @@ -285,7 +285,7 @@ def test_get_observations_default_order_groups_by_datastream_then_time(client): client.force_login(owner) response = client.get( - f"/api/data/observations?{urlencode({'datastream_id': [str(datastream_a.id), str(datastream_b.id)]}, doseq=True)}" + f"/api/ogc/collections/observations/items?{urlencode({'datastream_id': [str(datastream_a.id), str(datastream_b.id)]}, doseq=True)}" ) assert response.status_code == 200 @@ -978,3 +978,117 @@ def test_delete_observations_returns_400_without_datastream_id(client): ) assert response.status_code == 400 + + +# --- bbox -------------------------------------------------------------------------------- + + +def _datastream_with_observations(longitude, latitude, count=2): + datastream = DatastreamFactory( + monitoring_site=MonitoringSiteFactory(longitude=longitude, latitude=latitude), + value_count=count, + ) + for minutes in range(count): + ObservationFactory(datastream=datastream, phenomenon_time=timezone.now() - timedelta(minutes=minutes)) + return datastream + + +def test_get_observations_filters_by_monitoring_site_location(client): + inside = _datastream_with_observations(-111.5, 40.5) + _datastream_with_observations(-100, 40.5) + + response = client.get(_observations_url(bbox="-112,40,-111,41")) + + assert response.status_code == 200 + body = response.json() + assert {o["datastreamId"] for o in body["data"]} == {str(inside.id)} + assert len(body["data"]) == 2 + assert body["meta"]["totalCount"] == 2 + + +def test_get_observations_bbox_count_uses_matched_datastreams_with_a_time_filter(client): + inside = _datastream_with_observations(-111.5, 40.5) + _datastream_with_observations(-100, 40.5) + + response = client.get( + _observations_url( + bbox="-112,40,-111,41", + datetime=f"{(timezone.now() - timedelta(days=1)).isoformat()}/..", + ) + ) + + assert response.status_code == 200 + assert {o["datastreamId"] for o in response.json()["data"]} == {str(inside.id)} + assert response.json()["meta"]["totalCount"] == 2 + + +def test_get_observations_bbox_combines_with_datastream_id(client): + inside = _datastream_with_observations(-111.5, 40.5) + outside = _datastream_with_observations(-100, 40.5) + + response = client.get( + _observations_url(datastream_id=[str(inside.id), str(outside.id)], bbox="-112,40,-111,41") + ) + + assert response.status_code == 200 + assert {o["datastreamId"] for o in response.json()["data"]} == {str(inside.id)} + assert response.json()["meta"]["totalCount"] == 2 + + +def test_get_observations_row_format_is_empty_when_the_datastream_is_outside_the_bbox(client): + outside = _datastream_with_observations(-100, 40.5) + + response = client.get(_observations_url(datastream_id=str(outside.id), format="row", bbox="-112,40,-111,41")) + + assert response.status_code == 200 + assert response.json()["data"]["rows"] == [] + assert response.json()["meta"]["totalCount"] == 0 + + +def test_get_observations_returns_400_for_invalid_bbox(client): + response = client.get(_observations_url(bbox="-112,40,-111")) + + assert response.status_code == 400 + + +# --- datetime ---------------------------------------------------------------------------- + + +def _observations_at(datastream, *times): + return [ObservationFactory(datastream=datastream, phenomenon_time=t) for t in times] + + +def test_get_observations_filters_by_datetime_interval_including_bounds(client): + datastream = DatastreamFactory(value_count=4) + start = timezone.now().replace(microsecond=0) - timedelta(days=10) + end = start + timedelta(days=5) + before, on_start, on_end, after = _observations_at( + datastream, start - timedelta(seconds=1), start, end, end + timedelta(seconds=1) + ) + + response = client.get(_observations_url(datastream.id, datetime=f"{_iso(start)}/{_iso(end)}")) + + assert response.status_code == 200 + body = response.json() + assert {o["id"] for o in body["data"]} == {str(on_start.id), str(on_end.id)} + assert body["meta"]["totalCount"] == 2 + + +def test_get_observations_filters_by_datetime_instant_and_open_ends(client): + datastream = DatastreamFactory(value_count=2) + early_time = timezone.now().replace(microsecond=0) - timedelta(days=10) + late_time = early_time + timedelta(days=5) + early, late = _observations_at(datastream, early_time, late_time) + + def ids(value): + return {o["id"] for o in client.get(_observations_url(datastream.id, datetime=value)).json()["data"]} + + assert ids(_iso(late_time)) == {str(late.id)} + assert ids(f"../{_iso(early_time)}") == {str(early.id)} + assert ids(f"{_iso(late_time)}/") == {str(late.id)} + + +def test_get_observations_returns_400_for_invalid_datetime(client): + response = client.get(_observations_url(datetime="2024-01-01")) + + assert response.status_code == 400 diff --git a/django/tests/interfaces/api/test_observed_property_type_views.py b/django/tests/interfaces/api/test_observed_property_type_views.py index 79b00f82..fb5f6ba4 100644 --- a/django/tests/interfaces/api/test_observed_property_type_views.py +++ b/django/tests/interfaces/api/test_observed_property_type_views.py @@ -5,7 +5,7 @@ pytestmark = pytest.mark.django_db -OBSERVED_PROPERTY_TYPES_URL = "/api/data/observed-property-types" +OBSERVED_PROPERTY_TYPES_URL = "/api/ogc/collections/observed-property-types/items" def _detail_url(observed_property_type_id): diff --git a/django/tests/interfaces/api/test_observed_property_views.py b/django/tests/interfaces/api/test_observed_property_views.py index 82ff4eb6..aa4f9f8d 100644 --- a/django/tests/interfaces/api/test_observed_property_views.py +++ b/django/tests/interfaces/api/test_observed_property_views.py @@ -18,7 +18,7 @@ pytestmark = pytest.mark.django_db -OBSERVED_PROPERTIES_URL = "/api/data/observed-properties" +OBSERVED_PROPERTIES_URL = "/api/ogc/collections/observed-properties/items" def _detail_url(observed_property_id): diff --git a/django/tests/interfaces/api/test_processing_level_views.py b/django/tests/interfaces/api/test_processing_level_views.py index e5570378..588f782b 100644 --- a/django/tests/interfaces/api/test_processing_level_views.py +++ b/django/tests/interfaces/api/test_processing_level_views.py @@ -14,7 +14,7 @@ pytestmark = pytest.mark.django_db -PROCESSING_LEVELS_URL = "/api/data/processing-levels" +PROCESSING_LEVELS_URL = "/api/ogc/collections/processing-levels/items" def _detail_url(processing_level_id): diff --git a/django/tests/interfaces/api/test_qc_history_views.py b/django/tests/interfaces/api/test_qc_history_views.py index a7cff45b..50e64ebf 100644 --- a/django/tests/interfaces/api/test_qc_history_views.py +++ b/django/tests/interfaces/api/test_qc_history_views.py @@ -1,4 +1,7 @@ +from datetime import timedelta + import pytest +from django.utils import timezone from django.db import connection from django.test.utils import CaptureQueriesContext @@ -14,7 +17,7 @@ pytestmark = pytest.mark.django_db -QC_HISTORIES_URL = "/api/data/quality-control/histories" +QC_HISTORIES_URL = "/api/ogc/collections/quality-control-histories/items" def _detail_url(history_id): @@ -306,3 +309,35 @@ def test_get_qc_histories_properties_filters_response_fields(client): assert response.status_code == 200 row = response.json()["data"][0] assert set(row.keys()) == {"id", "createdAt"} + + +# --- datetime ---------------------------------------------------------------------------- + + +def test_get_qc_histories_filters_by_overlapping_phenomenon_time(client): + owner = UserFactory() + workspace = WorkspaceFactory(owner=owner) + now = timezone.now().replace(microsecond=0) + overlapping = _make_history( + workspace, phenomenon_time_start=now - timedelta(days=10), phenomenon_time_end=now - timedelta(days=5) + ) + uncommitted = _make_history(workspace, phenomenon_time_start=None, phenomenon_time_end=None) + earlier = _make_history( + workspace, phenomenon_time_start=now - timedelta(days=40), phenomenon_time_end=now - timedelta(days=30) + ) + client.force_login(owner) + + response = client.get(QC_HISTORIES_URL, {"datetime": f"{(now - timedelta(days=7)).isoformat()}/.."}) + + assert response.status_code == 200 + ids = {h["id"] for h in response.json()["data"]} + assert ids == {str(overlapping.id), str(uncommitted.id)} + assert str(earlier.id) not in ids + + +def test_get_qc_histories_returns_400_for_invalid_datetime(client): + client.force_login(UserFactory()) + + response = client.get(QC_HISTORIES_URL, {"datetime": "2024-01-01T00:00:00"}) + + assert response.status_code == 400 diff --git a/django/tests/interfaces/api/test_qc_operation_views.py b/django/tests/interfaces/api/test_qc_operation_views.py index 929490fa..6382eb87 100644 --- a/django/tests/interfaces/api/test_qc_operation_views.py +++ b/django/tests/interfaces/api/test_qc_operation_views.py @@ -16,7 +16,7 @@ pytestmark = pytest.mark.django_db -QC_HISTORIES_URL = "/api/data/quality-control/histories" +QC_HISTORIES_URL = "/api/ogc/collections/quality-control-histories/items" def _operations_url(history_id, session_id): diff --git a/django/tests/interfaces/api/test_qc_session_views.py b/django/tests/interfaces/api/test_qc_session_views.py index 2db3aaaa..beab4f5f 100644 --- a/django/tests/interfaces/api/test_qc_session_views.py +++ b/django/tests/interfaces/api/test_qc_session_views.py @@ -15,7 +15,7 @@ pytestmark = pytest.mark.django_db -QC_HISTORIES_URL = "/api/data/quality-control/histories" +QC_HISTORIES_URL = "/api/ogc/collections/quality-control-histories/items" def _sessions_url(history_id): diff --git a/django/tests/interfaces/api/test_query_null.py b/django/tests/interfaces/api/test_query_null.py new file mode 100644 index 00000000..ae37b905 --- /dev/null +++ b/django/tests/interfaces/api/test_query_null.py @@ -0,0 +1,245 @@ +import uuid + +import pytest + +from interfaces.api.schemas.base import accepts_null +from interfaces.api.urls import api +from tests.core.iam.factories import RoleFactory, UserFactory, WorkspaceFactory +from tests.core.sta.factories import ( + DatastreamFactory, + MethodFactory, + MonitoringSiteFactory, + UnitFactory, +) + +pytestmark = pytest.mark.django_db + +# 'null' is only a valid query value where the parameter's type declares Literal["null"], and +# there it selects items whose value is null. Everywhere else it's an invalid value, or an +# ordinary string for free-text parameters. Booleans accept only 'true' and 'false' (OGC API - +# Common - Part 1 /req/core/query-param-value-boolean and /req/core/query-param-capitalization). + +COLLECTIONS_URL = "/api/ogc/collections" + +NULLABLE_PARAMETERS = { + ("data-product-tasks", "latest_run_status"), + ("data-product-tasks", "rating_curve_id"), + ("datastreams", "status"), + ("etl-tasks", "latest_run_status"), + ("methods", "datastream_id"), + ("methods", "monitoring_site_id"), + ("methods", "sensor_model"), + ("methods", "sensor_model_manufacturer"), + ("methods", "workspace_id"), + ("monitoring-sites", "admin_area_1"), + ("monitoring-sites", "admin_area_2"), + ("monitoring-sites", "country"), + ("monitoring-tasks", "latest_run_status"), + ("observed-properties", "datastream_id"), + ("observed-properties", "monitoring_site_id"), + ("observed-properties", "workspace_id"), + ("processing-levels", "datastream_id"), + ("processing-levels", "monitoring_site_id"), + ("processing-levels", "workspace_id"), + ("quality-control-histories", "source_datastream_id"), + ("result-qualifiers", "workspace_id"), + ("roles", "workspace_id"), + ("units", "datastream_id"), + ("units", "monitoring_site_id"), + ("units", "workspace_id"), +} + + +def _nullable_parameters(): + """Yields (collection id, parameter name) for every query parameter that accepts 'null'.""" + + for bound_router in api._get_bound_routers(): + prefix = bound_router.prefix.strip("/") + if not prefix.startswith("collections/"): + continue + + for path_view in bound_router.path_operations.values(): + for operation in path_view.operations: + for model in operation.models: + if getattr(model, "__ninja_param_source__", None) != "query": + continue + + for alias, field_path in model.__ninja_flatten_map__.items(): + schema = model + for name in field_path[:-1]: + schema = schema.model_fields[name].annotation + # The flatten map ends with the parameter's alias, not its field name. + field = next( + field + for name, field in schema.model_fields.items() + if (field.alias or name) == field_path[-1] + ) + if accepts_null(field.annotation): + yield prefix.removeprefix("collections/"), alias + + +def test_only_nullable_columns_accept_null(): + assert set(_nullable_parameters()) == NULLABLE_PARAMETERS + + +def test_null_selects_items_without_a_value(client): + global_role = RoleFactory(global_role=True) + RoleFactory() + + response = client.get(f"{COLLECTIONS_URL}/roles/items", {"workspace_id": "null"}) + + assert response.status_code == 200 + ids = {item["id"] for item in response.json()["data"]} + assert str(global_role.id) in ids + assert all(item["workspaceId"] is None for item in response.json()["data"]) + + +def test_null_combines_with_other_values(client): + user = UserFactory() + workspace = WorkspaceFactory(owner=user) + workspace_unit = UnitFactory(workspace=workspace) + global_unit = UnitFactory(workspace=None) + other_unit = UnitFactory() + client.force_login(user) + + response = client.get( + f"{COLLECTIONS_URL}/units/items?workspace_id={workspace.id}&workspace_id=null&limit=1000" + ) + + assert response.status_code == 200 + ids = {item["id"] for item in response.json()["data"]} + assert {str(workspace_unit.id), str(global_unit.id)} <= ids + assert str(other_unit.id) not in ids + + +@pytest.mark.parametrize( + "collection, parameter, field", + [ + ("monitoring-sites", "country", "country"), + ("monitoring-sites", "admin_area_1", "admin_area_1"), + ("methods", "sensor_model", "sensor_model"), + ], +) +def test_newly_nullable_string_filters_select_null_values(client, collection, parameter, field): + factory = MonitoringSiteFactory if collection == "monitoring-sites" else MethodFactory + null_item = factory(**{field: None}) + valued_item = factory(**{field: "US"}) + client.force_login(UserFactory(superuser=True)) + + response = client.get(f"{COLLECTIONS_URL}/{collection}/items", {parameter: "null", "limit": 1000}) + + assert response.status_code == 200 + ids = {item["id"] for item in response.json()["data"]} + assert str(null_item.id) in ids + assert str(valued_item.id) not in ids + + +def test_datastream_status_null_selects_datastreams_without_a_status(client): + null_datastream = DatastreamFactory(status=None) + valued_datastream = DatastreamFactory(status="Ongoing") + + response = client.get(f"{COLLECTIONS_URL}/datastreams/items", {"status": "null", "limit": 1000}) + + assert response.status_code == 200 + ids = {item["id"] for item in response.json()["data"]} + assert str(null_datastream.id) in ids + assert str(valued_datastream.id) not in ids + + +def test_null_matching_is_case_sensitive_for_string_filters(client): + null_site = MonitoringSiteFactory(admin_area_1=None) + literal_site = MonitoringSiteFactory(admin_area_1="NULL") + + response = client.get( + f"{COLLECTIONS_URL}/monitoring-sites/items", {"admin_area_1": "NULL", "limit": 1000} + ) + + assert response.status_code == 200 + ids = {item["id"] for item in response.json()["data"]} + assert str(literal_site.id) in ids + assert str(null_site.id) not in ids + + +@pytest.mark.parametrize("value", ["NULL", "Null"]) +def test_null_matching_is_case_sensitive_for_typed_filters(client, value): + response = client.get(f"{COLLECTIONS_URL}/roles/items", {"workspace_id": value}) + + assert response.status_code == 400 + + +@pytest.mark.parametrize( + "collection, parameter", + [ + ("units", "limit"), + ("units", "offset"), + ("units", "bbox"), + ("units", "datetime"), + ("units", "sortby"), + ("units", "properties"), + ("datastreams", "monitoring_site_id"), + ("datastreams", "is_private"), + ("workspaces", "is_associated"), + ("etl-tasks", "workspace_id"), + ], +) +def test_non_nullable_parameters_reject_null(client, collection, parameter): + client.force_login(UserFactory()) + + response = client.get(f"{COLLECTIONS_URL}/{collection}/items", {parameter: "null"}) + + assert response.status_code == 400 + + +def test_free_text_parameters_treat_null_as_a_string(client): + site = MonitoringSiteFactory(type="null") + MonitoringSiteFactory(type="Stream") + + response = client.get(f"{COLLECTIONS_URL}/monitoring-sites/items", {"type": "null", "limit": 1000}) + + assert response.status_code == 200 + assert [item["id"] for item in response.json()["data"]] == [str(site.id)] + + +@pytest.mark.parametrize("value, expected", [("true", True), ("false", False)]) +def test_boolean_parameters_accept_lowercase_true_and_false(client, value, expected): + # A site is only private-filtered as private when its workspace is private too. + private_site = MonitoringSiteFactory(private=True, workspace=WorkspaceFactory(private=True)) + public_site = MonitoringSiteFactory() + client.force_login(UserFactory(superuser=True)) + + response = client.get(f"{COLLECTIONS_URL}/monitoring-sites/items", {"is_private": value, "limit": 1000}) + + assert response.status_code == 200 + ids = {item["id"] for item in response.json()["data"]} + assert (str(private_site.id) in ids) is expected + assert (str(public_site.id) in ids) is not expected + + +@pytest.mark.parametrize("value", ["True", "FALSE", "1", "0", "yes", "no", "t", "f", "on", ""]) +@pytest.mark.parametrize( + "path, parameter", + [ + ("workspaces/items", "is_private"), + ("workspaces/items", "is_associated"), + ("datastreams/items", "is_private"), + ("monitoring-sites/items", "is_private"), + ], +) +def test_boolean_parameters_reject_other_spellings(client, path, parameter, value): + response = client.get(f"{COLLECTIONS_URL}/{path}", {parameter: value}) + + assert response.status_code == 400 + assert response.json()["message"] == "Value error, Boolean query parameters must be 'true' or 'false'" + + +@pytest.mark.parametrize("value", ["True", "1", "null"]) +def test_include_ancestors_rejects_other_spellings(client, value): + client.force_login(UserFactory()) + history_id = uuid.uuid4() + + response = client.get( + f"{COLLECTIONS_URL}/quality-control-histories/items/{history_id}/sessions", + {"include_ancestors": value}, + ) + + assert response.status_code == 400 diff --git a/django/tests/interfaces/api/test_query_params.py b/django/tests/interfaces/api/test_query_params.py new file mode 100644 index 00000000..1def47ce --- /dev/null +++ b/django/tests/interfaces/api/test_query_params.py @@ -0,0 +1,127 @@ +import re +import uuid + +import pytest + +from interfaces.api.http.query_params import declared_query_params +from interfaces.api.urls import api +from tests.core.sta.factories import DatastreamFactory, ObservationFactory + +pytestmark = pytest.mark.django_db + +# Query parameters an operation doesn't declare get a 400 (OGC API - Features Core Req 8). + +API_PREFIX = "/api/ogc/" +UNITS_URL = "/api/ogc/collections/units/items" + + +def _operations(): + """Yields (method, OpenAPI-style path, operation) for every operation in the API.""" + + for bound_router in api._get_bound_routers(): + for path, path_view in bound_router.path_operations.items(): + route = "/".join(part.strip("/") for part in (bound_router.prefix, path) if part.strip("/")) + for operation in path_view.operations: + for method in operation.methods: + yield method, f"{API_PREFIX}{route}", operation + + +OPERATIONS = list(_operations()) + + +@pytest.mark.parametrize( + "method, path, operation", + OPERATIONS, + ids=[f"{method} {path.removeprefix(API_PREFIX)}" for method, path, _ in OPERATIONS], +) +def test_every_operation_rejects_an_unknown_query_parameter(client, method, path, operation): + url = re.sub(r"\{[^}]+\}", str(uuid.uuid4()), path) + + response = client.generic(method, f"{url}?notAParameter=1") + + assert response.status_code == 400 + assert "notAParameter" in response.json()["message"] + + +def test_declared_query_parameters_match_the_openapi_document(): + paths = api.get_openapi_schema(path_prefix=API_PREFIX)["paths"] + + for method, path, operation in OPERATIONS: + documented = { + parameter["name"] + for parameter in paths[path][method.lower()].get("parameters", []) + if parameter["in"] == "query" + } + + assert declared_query_params(operation) == documented, f"{method} {path}" + + +def test_unknown_parameter_message_lists_the_allowed_parameters(client): + response = client.get(UNITS_URL, {"sortBy": "name"}) + + assert response.status_code == 400 + assert response.json()["message"].startswith("Unknown query parameter(s): sortBy. Allowed: ") + assert "sortby" in response.json()["message"] + + +def test_parameter_names_are_case_sensitive(client): + response = client.get(UNITS_URL, {"Limit": 5}) + + assert response.status_code == 400 + + +def test_unknown_parameter_without_a_value_is_rejected(client): + response = client.get(f"{UNITS_URL}?foo") + + assert response.status_code == 400 + + +def test_all_unknown_parameters_are_reported(client): + response = client.get(UNITS_URL, {"foo": 1, "bar": 2, "limit": 5}) + + assert response.status_code == 400 + assert "bar, foo." in response.json()["message"] + + +def test_declared_parameters_are_accepted(client): + response = client.get(UNITS_URL, {"limit": 5, "offset": 0, "sortby": "-name,+symbol", "properties": "name"}) + + assert response.status_code == 200 + + +def test_repeated_declared_parameters_are_accepted(client): + response = client.get(f"{UNITS_URL}?properties=name&properties=symbol") + + assert response.status_code == 200 + + +def test_aliased_parameters_are_accepted(client): + datastream = DatastreamFactory() + ObservationFactory(datastream=datastream) + + response = client.get( + "/api/ogc/collections/observations/items", + {"datastream_id": str(datastream.id), "format": "row", "result_qualifier_code": "A"}, + ) + + assert response.status_code == 200 + + +def test_invalid_values_of_declared_parameters_keep_their_validation_error(client): + response = client.get(UNITS_URL, {"limit": "abc"}) + + assert response.status_code == 400 + assert response.json()["message"] == "Input should be a valid integer, unable to parse string as an integer" + + +def test_operation_without_query_parameters_rejects_any(client): + response = client.get("/api/ogc/collections/monitoring-sites/site-type-icons", {"foo": 1}) + + assert response.status_code == 400 + assert response.json()["message"] == "Unknown query parameter(s): foo. This operation accepts none." + + +def test_operation_without_query_parameters_accepts_none(client): + response = client.get("/api/ogc/collections/monitoring-sites/site-type-icons") + + assert response.status_code == 200 diff --git a/django/tests/interfaces/api/test_rating_curve_views.py b/django/tests/interfaces/api/test_rating_curve_views.py index f575b5e1..c2cc74b7 100644 --- a/django/tests/interfaces/api/test_rating_curve_views.py +++ b/django/tests/interfaces/api/test_rating_curve_views.py @@ -15,7 +15,7 @@ pytestmark = pytest.mark.django_db -RATING_CURVES_URL = "/api/data/data-product-rating-curves" +RATING_CURVES_URL = "/api/ogc/collections/data-product-rating-curves/items" RATING_CURVE_FIELDS = { "id", diff --git a/django/tests/interfaces/api/test_result_qualifier_views.py b/django/tests/interfaces/api/test_result_qualifier_views.py index 00209fa6..8b568448 100644 --- a/django/tests/interfaces/api/test_result_qualifier_views.py +++ b/django/tests/interfaces/api/test_result_qualifier_views.py @@ -14,7 +14,7 @@ pytestmark = pytest.mark.django_db -RESULT_QUALIFIERS_URL = "/api/data/result-qualifiers" +RESULT_QUALIFIERS_URL = "/api/ogc/collections/result-qualifiers/items" def _detail_url(result_qualifier_id): diff --git a/django/tests/interfaces/api/test_role_views.py b/django/tests/interfaces/api/test_role_views.py index 5f06f9fc..23333ff1 100644 --- a/django/tests/interfaces/api/test_role_views.py +++ b/django/tests/interfaces/api/test_role_views.py @@ -9,7 +9,7 @@ pytestmark = pytest.mark.django_db -ROLES_URL = "/api/data/roles" +ROLES_URL = "/api/ogc/collections/roles/items" def _detail_url(role_id): diff --git a/django/tests/interfaces/api/test_sampled_medium_views.py b/django/tests/interfaces/api/test_sampled_medium_views.py index a792fb59..7c0bbe58 100644 --- a/django/tests/interfaces/api/test_sampled_medium_views.py +++ b/django/tests/interfaces/api/test_sampled_medium_views.py @@ -5,7 +5,7 @@ pytestmark = pytest.mark.django_db -SAMPLED_MEDIUMS_URL = "/api/data/sampled-mediums" +SAMPLED_MEDIUMS_URL = "/api/ogc/collections/sampled-mediums/items" def _detail_url(sampled_medium_id): diff --git a/django/tests/interfaces/api/test_service_account_views.py b/django/tests/interfaces/api/test_service_account_views.py index 84f1c5a5..2fe9ba03 100644 --- a/django/tests/interfaces/api/test_service_account_views.py +++ b/django/tests/interfaces/api/test_service_account_views.py @@ -30,7 +30,7 @@ def _service_accounts_url(workspace_id): - return f"/api/data/workspaces/{workspace_id}/service-accounts" + return f"/api/ogc/collections/workspaces/items/{workspace_id}/service-accounts" def _detail_url(workspace_id, service_account_id): diff --git a/django/tests/interfaces/api/test_sortby.py b/django/tests/interfaces/api/test_sortby.py new file mode 100644 index 00000000..a43003d7 --- /dev/null +++ b/django/tests/interfaces/api/test_sortby.py @@ -0,0 +1,117 @@ +from datetime import timedelta + +import pytest +from django.utils import timezone + +from interfaces.api.urls import api +from processing.orchestration.models import TaskRun +from tests.core.iam.factories import UserFactory, WorkspaceFactory +from tests.core.sta.factories import MonitoringSiteFactory +from tests.processing.etl.factories import DataConnectionFactory, EtlTaskFactory, PayloadFactory + +pytestmark = pytest.mark.django_db + +# sortby takes comma-separated fields, each with an optional '+' (ascending) or '-' (descending) +# prefix (OGC API - Features Part 8 Req 5). The repeated form (sortby=a&sortby=b) still works. + +MONITORING_SITES_URL = "/api/ogc/collections/monitoring-sites/items" + + +def _names(response): + return [site["name"] for site in response.json()["data"]] + + +@pytest.fixture +def sites(): + MonitoringSiteFactory(name="b", code="1") + MonitoringSiteFactory(name="a", code="1") + MonitoringSiteFactory(name="a", code="2") + + +@pytest.mark.parametrize( + "query", + [ + "sortby=name,-code", + "sortby=name&sortby=-code", + "sortby=%2Bname,-code", + "sortby=%2Bname&sortby=-code", + ], +) +def test_get_items_sorts_by_comma_separated_or_repeated_fields(client, sites, query): + response = client.get(f"{MONITORING_SITES_URL}?{query}") + + assert response.status_code == 200 + assert [(s["name"], s["code"]) for s in response.json()["data"]] == [("a", "2"), ("a", "1"), ("b", "1")] + + +def test_get_items_treats_an_unencoded_plus_as_ascending(client, sites): + # An unencoded '+' in a query string decodes to a space. + response = client.get(f"{MONITORING_SITES_URL}?sortby=+name") + + assert response.status_code == 200 + assert _names(response) == ["a", "a", "b"] + + +@pytest.mark.parametrize( + "sortby", ["name,%2Bname", "name,-name", "bogus", "%2B-name", "%2B%2Bname", "%2B"] +) +def test_get_items_returns_400_for_invalid_sortby(client, sortby): + response = client.get(f"{MONITORING_SITES_URL}?sortby={sortby}") + + assert response.status_code == 400 + + +def _etl_task(owner): + data_connection = DataConnectionFactory(workspace=WorkspaceFactory(owner=owner)) + PayloadFactory(data_connection=data_connection) + return EtlTaskFactory(data_connection=data_connection) + + +def test_get_task_runs_accepts_a_plus_prefix(client): + owner = UserFactory() + task = _etl_task(owner) + now = timezone.now() + older = TaskRun.objects.create(task=task, status="SUCCESS", started_at=now - timedelta(hours=1)) + newer = TaskRun.objects.create(task=task, status="SUCCESS", started_at=now) + client.force_login(owner) + + response = client.get(f"/api/ogc/collections/etl-tasks/items/{task.id}/runs?sortby=%2BstartedAt") + + assert response.status_code == 200 + assert [run["id"] for run in response.json()["data"]] == [str(older.id), str(newer.id)] + + +def test_get_etl_tasks_accepts_a_plus_prefix_on_latest_run_fields(client): + owner = UserFactory() + now = timezone.now() + older = _etl_task(owner) + TaskRun.objects.create(task=older, status="SUCCESS", started_at=now - timedelta(hours=1)) + newer = _etl_task(owner) + TaskRun.objects.create(task=newer, status="SUCCESS", started_at=now) + client.force_login(owner) + + response = client.get("/api/ogc/collections/etl-tasks/items?sortby=%2BlatestRunStartedAt") + + assert response.status_code == 200 + ids = [task["id"] for task in response.json()["data"]] + assert ids.index(str(older.id)) < ids.index(str(newer.id)) + + +def test_every_sortby_parameter_is_comma_separated_with_plus_and_minus_prefixes(): + paths = api.get_openapi_schema(path_prefix="/api/ogc/")["paths"] + sortby_parameters = [ + parameter + for path_item in paths.values() + for operation in path_item.values() + for parameter in operation.get("parameters", []) + if parameter["name"] == "sortby" + ] + + assert sortby_parameters + for parameter in sortby_parameters: + assert parameter["style"] == "form" + assert parameter["explode"] is False + assert parameter["schema"]["type"] == "array" + values = set(parameter["schema"]["items"]["enum"]) + ascending = {value for value in values if value[0] not in "+-"} + assert values == ascending | {f"+{f}" for f in ascending} | {f"-{f}" for f in ascending} diff --git a/django/tests/interfaces/api/test_unit_type_views.py b/django/tests/interfaces/api/test_unit_type_views.py index 6df13639..b668f96f 100644 --- a/django/tests/interfaces/api/test_unit_type_views.py +++ b/django/tests/interfaces/api/test_unit_type_views.py @@ -5,7 +5,7 @@ pytestmark = pytest.mark.django_db -UNIT_TYPES_URL = "/api/data/unit-types" +UNIT_TYPES_URL = "/api/ogc/collections/unit-types/items" def _detail_url(unit_type_id): diff --git a/django/tests/interfaces/api/test_unit_views.py b/django/tests/interfaces/api/test_unit_views.py index 66be0448..4eaed4ab 100644 --- a/django/tests/interfaces/api/test_unit_views.py +++ b/django/tests/interfaces/api/test_unit_views.py @@ -14,7 +14,7 @@ pytestmark = pytest.mark.django_db -UNITS_URL = "/api/data/units" +UNITS_URL = "/api/ogc/collections/units/items" def _detail_url(unit_id): diff --git a/django/tests/interfaces/api/test_workspace_metadata.py b/django/tests/interfaces/api/test_workspace_metadata.py index 4d7c8c37..afe0095a 100644 --- a/django/tests/interfaces/api/test_workspace_metadata.py +++ b/django/tests/interfaces/api/test_workspace_metadata.py @@ -25,7 +25,7 @@ def owner_workspace(client): @pytest.mark.parametrize("resource,fields", METADATA) def test_metadata_optional_fields_and_limits(client, owner_workspace, resource, fields): - url = f"/api/data/{resource}" + url = f"/api/ogc/collections/{resource}/items" body = {**fields, "name": "N" * 255, "workspaceId": str(owner_workspace.id)} response = client.post(url, data=body, content_type="application/json") assert response.status_code == 201, response.content @@ -76,11 +76,11 @@ def test_metadata_optional_fields_and_limits(client, owner_workspace, resource, @pytest.mark.parametrize("resource,fields", METADATA) def test_metadata_requires_name_and_description(client, owner_workspace, resource, fields): body = {**fields, "workspaceId": str(owner_workspace.id)} - assert client.post(f"/api/data/{resource}", data=body, content_type="application/json").status_code == 400 + assert client.post(f"/api/ogc/collections/{resource}/items", data=body, content_type="application/json").status_code == 400 if resource != "units": body["name"] = "Name" del body["description"] - assert client.post(f"/api/data/{resource}", data=body, content_type="application/json").status_code == 400 + assert client.post(f"/api/ogc/collections/{resource}/items", data=body, content_type="application/json").status_code == 400 @pytest.mark.parametrize("global_", [False, True]) @@ -106,9 +106,9 @@ def test_observation_codes_use_qualifier_names(client, owner_workspace, global_) "datastreamId": str(datastream.id), "phenomenonTime": "2026-01-01T00:00:00Z", "result": 1.0, "resultQualifierCodes": [qualifier.name], } - response = client.post("/api/data/observations", data=body, content_type="application/json") + response = client.post("/api/ogc/collections/observations/items", data=body, content_type="application/json") assert response.status_code == 201, response.content - response = client.get("/api/data/observations", {"datastream_id": str(datastream.id), "result_qualifier_code": qualifier.name}) + response = client.get("/api/ogc/collections/observations/items", {"datastream_id": str(datastream.id), "result_qualifier_code": qualifier.name}) assert response.status_code == 200 assert response.json()["data"][0]["resultQualifierCodes"] == [qualifier.name] @@ -118,7 +118,7 @@ def test_visualization_allows_observed_property_without_code(client, owner_works monitoring_site__workspace=owner_workspace, observed_property__code=None, processing_level__code=None, ) - response = client.get("/api/data/datastreams/visualization-bootstrap") + response = client.get("/api/ogc/collections/datastreams/visualization-bootstrap") assert response.status_code == 200, response.content properties = response.json()["observedProperties"] assert next(p for p in properties if p["id"] == str(datastream.observed_property_id))["code"] is None @@ -132,6 +132,6 @@ def test_bulk_observation_codes_use_qualifier_names(client, owner_workspace): "fields": ["phenomenonTime", "result", "resultQualifierCodes"], "data": [["2026-01-01T00:00:00Z", 1.0, [qualifier.name]]], } - response = client.post("/api/data/observations/bulk-create?mode=insert", data=body, content_type="application/json") + response = client.post("/api/ogc/collections/observations/bulk-create?mode=insert", data=body, content_type="application/json") assert response.status_code == 201, response.content assert datastream.observation_set.get().result_qualifiers == [qualifier.name] diff --git a/django/tests/interfaces/api/test_workspace_views.py b/django/tests/interfaces/api/test_workspace_views.py index 765a4b93..cf8cbfad 100644 --- a/django/tests/interfaces/api/test_workspace_views.py +++ b/django/tests/interfaces/api/test_workspace_views.py @@ -10,10 +10,11 @@ UserFactory, WorkspaceFactory, ) +from tests.core.sta.factories import DatastreamFactory, MonitoringSiteFactory pytestmark = pytest.mark.django_db -WORKSPACES_URL = "/api/data/workspaces" +WORKSPACES_URL = "/api/ogc/collections/workspaces/items" WORKSPACE_FIELDS = { "id", @@ -405,7 +406,7 @@ def test_get_workspaces_include_pending_transfer_to_sideloads_for_recipient(clie response = client.get( WORKSPACES_URL, - {"is_associated": True, "include": "pendingTransferTo"}, + {"is_associated": "true", "include": "pendingTransferTo"}, ) assert response.status_code == 200 @@ -424,7 +425,7 @@ def test_get_workspaces_include_collaborator_role_sideloads_for_collaborator(cli response = client.get( WORKSPACES_URL, - {"is_associated": True, "include": "collaboratorRole"}, + {"is_associated": "true", "include": "collaboratorRole"}, ) assert response.status_code == 200 @@ -635,3 +636,175 @@ def test_reject_workspace_transfer_returns_400_when_none_pending(client): response = client.delete(_transfer_url(workspace.id)) assert response.status_code == 400 + + +# --- bbox -------------------------------------------------------------------------------- +# +# A workspace's location is the set of its monitoring sites the requester can view. It matches +# a bbox when one of those sites lies inside it, or when the requester can view none of its +# sites (no location matches any bbox, per OGC API - Features Core Req 24C). + +INSIDE_BBOX = "-112,40,-111,41" + + +def _site(workspace, longitude, latitude, **kwargs): + return MonitoringSiteFactory(workspace=workspace, longitude=longitude, latitude=latitude, **kwargs) + + +def _workspace_ids(response): + return {workspace["id"] for workspace in response.json()["data"]} + + +def test_get_workspaces_bbox_matches_workspaces_with_a_visible_site_inside(client): + inside = WorkspaceFactory() + _site(inside, -111.5, 40.5) + outside = WorkspaceFactory() + _site(outside, -100, 40.5) + + response = client.get(WORKSPACES_URL, {"bbox": INSIDE_BBOX}) + + assert response.status_code == 200 + assert str(inside.id) in _workspace_ids(response) + assert str(outside.id) not in _workspace_ids(response) + + +def test_get_workspaces_bbox_matches_workspaces_without_sites(client): + empty = WorkspaceFactory() + + response = client.get(WORKSPACES_URL, {"bbox": INSIDE_BBOX}) + + assert str(empty.id) in _workspace_ids(response) + + +def test_get_workspaces_bbox_ignores_private_sites_the_requester_cannot_view(client): + # A private site inside the box must not make the workspace match... + hidden_inside = WorkspaceFactory() + _site(hidden_inside, -100, 40.5) + _site(hidden_inside, -111.5, 40.5, private=True) + # ...and a workspace whose only site is private has no visible location, so it matches. + only_private = WorkspaceFactory() + _site(only_private, -100, 40.5, private=True) + + response = client.get(WORKSPACES_URL, {"bbox": INSIDE_BBOX}) + + assert str(hidden_inside.id) not in _workspace_ids(response) + assert str(only_private.id) in _workspace_ids(response) + + +def test_get_workspaces_bbox_uses_private_sites_the_owner_can_view(client): + owner = UserFactory() + workspace = WorkspaceFactory(owner=owner, private=True) + _site(workspace, -100, 40.5) + _site(workspace, -111.5, 40.5, private=True) + client.force_login(owner) + + inside = client.get(WORKSPACES_URL, {"bbox": INSIDE_BBOX}) + elsewhere = client.get(WORKSPACES_URL, {"bbox": "0,0,1,1"}) + + assert str(workspace.id) in _workspace_ids(inside) + assert str(workspace.id) not in _workspace_ids(elsewhere) + + +def test_get_workspaces_bbox_uses_private_sites_a_collaborator_can_view(client): + workspace = WorkspaceFactory(private=True) + _site(workspace, -111.5, 40.5, private=True) + role = RoleFactory(workspace=workspace) + PermissionFactory(role=role, resource_type="Workspace", can_view=True) + PermissionFactory(role=role, resource_type="MonitoringSite", can_view=True) + collaborator = CollaboratorFactory(workspace=workspace, role=role) + client.force_login(collaborator.user) + + inside = client.get(WORKSPACES_URL, {"bbox": INSIDE_BBOX}) + elsewhere = client.get(WORKSPACES_URL, {"bbox": "0,0,1,1"}) + + assert str(workspace.id) in _workspace_ids(inside) + assert str(workspace.id) not in _workspace_ids(elsewhere) + + +def test_get_workspaces_returns_400_for_invalid_bbox(client): + response = client.get(WORKSPACES_URL, {"bbox": "-112,40,-111"}) + + assert response.status_code == 400 + + +# --- datetime ---------------------------------------------------------------------------- +# +# A workspace's time is the observed time of each datastream the requester can view across its +# sites. It matches a datetime when one of those datastreams overlaps it, or when none of them +# has observations (no time matches any datetime, per OGC API - Features Core Req 26C). + + +def _observed_datastream(workspace, begin, end, **kwargs): + return DatastreamFactory( + monitoring_site=MonitoringSiteFactory(workspace=workspace), + phenomenon_begin_time=begin, + phenomenon_end_time=end, + **kwargs, + ) + + +def test_get_workspaces_datetime_matches_workspaces_with_an_overlapping_datastream_on_any_site(client): + overlapping = WorkspaceFactory() + _observed_datastream(overlapping, "2023-01-01T00:00:00Z", "2023-02-01T00:00:00Z") + _observed_datastream(overlapping, "2024-01-01T00:00:00Z", "2024-03-01T00:00:00Z") + before = WorkspaceFactory() + _observed_datastream(before, "2023-01-01T00:00:00Z", "2023-02-01T00:00:00Z") + + response = client.get(WORKSPACES_URL, {"datetime": "2024-02-01T00:00:00Z/.."}) + + assert response.status_code == 200 + assert str(overlapping.id) in _workspace_ids(response) + assert str(before.id) not in _workspace_ids(response) + + +def test_get_workspaces_datetime_requires_one_datastream_to_overlap(client): + workspace = WorkspaceFactory() + _observed_datastream(workspace, "2024-01-01T00:00:00Z", "2024-02-01T00:00:00Z") + _observed_datastream(workspace, "2024-06-01T00:00:00Z", "2024-07-01T00:00:00Z") + + response = client.get(WORKSPACES_URL, {"datetime": "2024-03-01T00:00:00Z/2024-03-31T00:00:00Z"}) + + assert str(workspace.id) not in _workspace_ids(response) + + +def test_get_workspaces_datetime_matches_workspaces_without_observed_datastreams(client): + empty = WorkspaceFactory() + without_observations = WorkspaceFactory() + DatastreamFactory(monitoring_site=MonitoringSiteFactory(workspace=without_observations)) + + response = client.get(WORKSPACES_URL, {"datetime": "2024-02-01T00:00:00Z"}) + + assert str(empty.id) in _workspace_ids(response) + assert str(without_observations.id) in _workspace_ids(response) + + +def test_get_workspaces_datetime_ignores_private_datastreams_the_requester_cannot_view(client): + hidden_overlap = WorkspaceFactory() + _observed_datastream(hidden_overlap, "2023-01-01T00:00:00Z", "2023-02-01T00:00:00Z") + _observed_datastream(hidden_overlap, "2024-01-01T00:00:00Z", "2024-03-01T00:00:00Z", private=True) + only_private = WorkspaceFactory() + _observed_datastream(only_private, "2023-01-01T00:00:00Z", "2023-02-01T00:00:00Z", private=True) + + response = client.get(WORKSPACES_URL, {"datetime": "2024-02-01T00:00:00Z"}) + + assert str(hidden_overlap.id) not in _workspace_ids(response) + assert str(only_private.id) in _workspace_ids(response) + + +def test_get_workspaces_datetime_uses_private_datastreams_the_owner_can_view(client): + owner = UserFactory() + workspace = WorkspaceFactory(owner=owner, private=True) + _observed_datastream(workspace, "2024-01-01T00:00:00Z", "2024-03-01T00:00:00Z", private=True) + client.force_login(owner) + + overlapping = client.get(WORKSPACES_URL, {"datetime": "2024-02-01T00:00:00Z"}) + elsewhere = client.get(WORKSPACES_URL, {"datetime": "2020-01-01T00:00:00Z"}) + + assert str(workspace.id) in _workspace_ids(overlapping) + assert str(workspace.id) not in _workspace_ids(elsewhere) + + +def test_get_workspaces_returns_400_for_invalid_datetime(client): + response = client.get(WORKSPACES_URL, {"datetime": "2024-02-01T00:00:00"}) + + assert response.status_code == 400 diff --git a/django/tests/interfaces/sensorthings/test_st_datastreams.py b/django/tests/interfaces/sensorthings/test_st_datastreams.py index a2820e78..cbcdb1b7 100644 --- a/django/tests/interfaces/sensorthings/test_st_datastreams.py +++ b/django/tests/interfaces/sensorthings/test_st_datastreams.py @@ -1,6 +1,11 @@ +import json +from datetime import timedelta + import pytest +from django.utils import timezone -from tests.core.sta.factories import DatastreamFactory +from tests.core.iam.factories import UserFactory, WorkspaceFactory +from tests.core.sta.factories import DatastreamFactory, MonitoringSiteFactory pytestmark = pytest.mark.django_db diff --git a/django/tests/interfaces/web/test_site_type_icons.py b/django/tests/interfaces/web/test_site_type_icons.py index 59bb5e86..e5fc262c 100644 --- a/django/tests/interfaces/web/test_site_type_icons.py +++ b/django/tests/interfaces/web/test_site_type_icons.py @@ -71,7 +71,7 @@ def test_django_and_data_management_app_icon_lists_stay_in_sync(): @pytest.mark.django_db def test_default_site_type_icon_mappings_are_available(client): - response = client.get("/api/data/monitoring-sites/site-type-icons") + response = client.get("/api/ogc/collections/monitoring-sites/site-type-icons") assert response.status_code == 200 mappings = response.json() diff --git a/packages/hydroserver-ts/package.json b/packages/hydroserver-ts/package.json index ba8bf36d..86287cfb 100644 --- a/packages/hydroserver-ts/package.json +++ b/packages/hydroserver-ts/package.json @@ -27,7 +27,7 @@ "test": "vitest", "coverage": "vitest run --coverage --environment jsdom", "gen:schemas": "DEBUG=True DEPLOYMENT_BACKEND=dev PROXY_BASE_URL=http://127.0.0.1:8000 python3.14 ../../django/manage.py export_openapi_contracts", - "gen:types": "npx --yes openapi-typescript@7.13.0 ../../django/contracts/openapi/data.openapi.json --output ./src/generated/data.types.d.ts", + "gen:types": "npx --yes openapi-typescript@7.13.0 ../../django/contracts/openapi/ogc.openapi.json --output ./src/generated/ogc.types.d.ts", "gen:contracts": "tsx ./scripts/generate-contracts.data.ts", "gen": "npm run gen:schemas && npm run gen:types && npm run gen:contracts", "check:contract": "npm run gen && git diff --exit-code -- ../../django/contracts/openapi ./src/generated" diff --git a/packages/hydroserver-ts/scripts/generate-contracts.data.ts b/packages/hydroserver-ts/scripts/generate-contracts.data.ts index ce0bb92c..74005e2f 100644 --- a/packages/hydroserver-ts/scripts/generate-contracts.data.ts +++ b/packages/hydroserver-ts/scripts/generate-contracts.data.ts @@ -1,88 +1,71 @@ /* eslint-disable no-console */ import path from 'node:path' import { generateContracts } from './generate-contracts.shared' -import { DATA_OPENAPI_FILE } from './openapi-paths' +import { OGC_OPENAPI_FILE } from './openapi-paths' -const SCHEMA_FILE = DATA_OPENAPI_FILE +const SCHEMA_FILE = OGC_OPENAPI_FILE const OUT_DIR = path.resolve('src/generated/contracts') +/** An OGC API collection: list/create at /collections/{id}/items, items at /items/{itemId}. */ +const collection = (collectionId: string, resource = collectionId) => ({ + resource, + pathSuffix: `/collections/${collectionId}/items`, + route: `collections/${collectionId}/items`, +}) + +const QC_HISTORY_ITEM = 'collections/quality-control-histories/items/{history_id}' + const resources = [ - 'workspaces', - 'monitoring-sites', - 'monitoring-site-types', - 'linked-resource-types', - 'datastreams', - 'datastream-statuses', - 'aggregation-statistics', - 'units', - 'unit-types', - 'methods', - 'method-types', - 'observed-properties', - 'observed-property-types', - 'processing-levels', - 'result-qualifiers', - 'sampled-mediums', - 'observations', + collection('workspaces'), + collection('monitoring-sites'), + collection('monitoring-site-types'), + collection('linked-resource-types'), + collection('datastreams'), + collection('datastream-statuses'), + collection('aggregation-statistics'), + collection('units'), + collection('unit-types'), + collection('methods'), + collection('method-types'), + collection('observed-properties'), + collection('observed-property-types'), + collection('processing-levels'), + collection('result-qualifiers'), + collection('sampled-mediums'), + collection('observations'), // ETL - { - resource: 'data-connections', - pathSuffix: '/etl-data-connections', - route: 'etl-data-connections', - }, - { - resource: 'tasks', - pathSuffix: '/etl-tasks', - route: 'etl-tasks', - }, + collection('etl-data-connections', 'data-connections'), + collection('etl-tasks', 'tasks'), 'runs', - 'etl-mappings', + collection('etl-mappings'), // Monitoring - { - resource: 'monitoring-tasks', - pathSuffix: '/monitoring-tasks', - route: 'monitoring-tasks', - }, - 'monitoring-rules', + collection('monitoring-tasks'), + collection('monitoring-rules'), // Products - { - resource: 'data-product-tasks', - pathSuffix: '/data-product-tasks', - route: 'data-product-tasks', - }, - { - resource: 'rating-curves', - pathSuffix: '/data-product-rating-curves', - route: 'data-product-rating-curves', - }, - 'data-product-transformations', + collection('data-product-tasks'), + collection('data-product-rating-curves', 'rating-curves'), + collection('data-product-transformations'), // Quality control - { - resource: 'quality-control-histories', - pathSuffix: '/quality-control/histories', - route: 'quality-control/histories', - }, + collection('quality-control-histories'), { resource: 'quality-control-sessions', - pathSuffix: '/quality-control/histories/{history_id}/sessions', - route: 'quality-control/histories/{history_id}/sessions', + pathSuffix: `/${QC_HISTORY_ITEM}/sessions`, + route: `${QC_HISTORY_ITEM}/sessions`, }, { resource: 'quality-control-operations', - pathSuffix: - '/quality-control/histories/{history_id}/sessions/{session_id}/operations', - route: - 'quality-control/histories/{history_id}/sessions/{session_id}/operations', + pathSuffix: `/${QC_HISTORY_ITEM}/sessions/{session_id}/operations`, + route: `${QC_HISTORY_ITEM}/sessions/{session_id}/operations`, }, ] generateContracts({ schemaFile: SCHEMA_FILE, outDir: OUT_DIR, - typesImportPath: '../data.types', + typesImportPath: '../ogc.types', explicitResources: resources, }) diff --git a/packages/hydroserver-ts/scripts/generate-services.ts b/packages/hydroserver-ts/scripts/generate-services.ts index 1a4a073c..8d09b4e5 100644 --- a/packages/hydroserver-ts/scripts/generate-services.ts +++ b/packages/hydroserver-ts/scripts/generate-services.ts @@ -1,7 +1,7 @@ /* eslint-disable no-console */ import fs from 'node:fs' import path from 'node:path' -import { DATA_OPENAPI_FILE } from './openapi-paths' +import { OGC_OPENAPI_FILE } from './openapi-paths' type OAS = { servers?: any[] @@ -10,7 +10,7 @@ type OAS = { } const ROOT = process.cwd() -const SCHEMA_FILE = DATA_OPENAPI_FILE +const SCHEMA_FILE = OGC_OPENAPI_FILE const OUT_DIR = path.resolve('src/generated/services') fs.mkdirSync(OUT_DIR, { recursive: true }) @@ -40,7 +40,7 @@ const toCamel = (s: string) => .replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : '')) .replace(/^(.)/, (m) => m.toLowerCase()) -/** Return "/api/data/" path root if present */ +/** Return "/api/ogc/" path root if present */ function findCollectionRoot(resource: string) { const key = Object.keys(spec.paths).find((p) => p.endsWith(`/${resource}`)) return key || null @@ -205,7 +205,7 @@ const header = `/* AUTO-GENERATED. DO NOT EDIT. import type { ApiResponse } from '../../api/responseInterceptor' import { apiMethods } from '../../api/apiMethods' import type { HydroServer } from '../../api/HydroServer' -import type * as Data from '../data.types' +import type * as Data from '../ogc.types' import { ${resources .map((r) => `${toPascal(singularizeKebab(r))}Contract`) .join(', ')} } from '../contracts' diff --git a/packages/hydroserver-ts/scripts/openapi-paths.ts b/packages/hydroserver-ts/scripts/openapi-paths.ts index da8ba7c5..ddb95a19 100644 --- a/packages/hydroserver-ts/scripts/openapi-paths.ts +++ b/packages/hydroserver-ts/scripts/openapi-paths.ts @@ -1,4 +1,4 @@ import path from 'node:path' export const OPENAPI_DIR = path.resolve('../../django/contracts/openapi') -export const DATA_OPENAPI_FILE = path.join(OPENAPI_DIR, 'data.openapi.json') +export const OGC_OPENAPI_FILE = path.join(OPENAPI_DIR, 'ogc.openapi.json') diff --git a/packages/hydroserver-ts/src/api/HydroServer.ts b/packages/hydroserver-ts/src/api/HydroServer.ts index c2473f45..4051e72d 100644 --- a/packages/hydroserver-ts/src/api/HydroServer.ts +++ b/packages/hydroserver-ts/src/api/HydroServer.ts @@ -85,7 +85,7 @@ export class HydroServer { const { host, oidc } = opts this.host = host.trim().replace(/\/+$/, '') this.resolvedHost = this.host || globalThis.location?.origin || '' - this.baseRoute = `${this.host}/api/data` + this.baseRoute = `${this.host}/api/ogc` this.oidc = oidc ? { clientId: oidc.clientId, diff --git a/packages/hydroserver-ts/src/api/__tests__/apiMethods.spec.ts b/packages/hydroserver-ts/src/api/__tests__/apiMethods.spec.ts index 2382cb17..675b8b37 100644 --- a/packages/hydroserver-ts/src/api/__tests__/apiMethods.spec.ts +++ b/packages/hydroserver-ts/src/api/__tests__/apiMethods.spec.ts @@ -42,7 +42,7 @@ describe('paginatedFetch', () => { const response = await apiMethods.paginatedFetch< { id: string; ownerEmail: string }[] - >('https://hydro.example.com/api/data/workspaces?limit=1') + >('https://hydro.example.com/api/ogc/collections/workspaces/items?limit=1') expect(response.ok).toBe(true) if (!response.ok) return @@ -86,7 +86,7 @@ describe('paginatedFetch', () => { const response = await apiMethods.paginatedFetch<{ fields: string[] rows: unknown[][] - }>('https://hydro.example.com/api/data/observations?format=row&limit=1') + }>('https://hydro.example.com/api/ogc/collections/observations/items?format=row&limit=1') expect(response.ok).toBe(true) if (!response.ok) return @@ -109,7 +109,7 @@ describe('paginatedFetch', () => { const response = await apiMethods.paginatedFetch< { id: string; ownerEmail: string }[] - >('https://hydro.example.com/api/data/workspaces') + >('https://hydro.example.com/api/ogc/collections/workspaces/items') expect(response.ok).toBe(true) if (!response.ok) return @@ -139,7 +139,7 @@ describe('paginatedFetch', () => { vi.stubGlobal('fetch', fetchMock) const response = await apiMethods.paginatedFetch<{ id: string }[]>( - 'https://hydro.example.com/api/data/observations?limit=2' + 'https://hydro.example.com/api/ogc/collections/observations/items?limit=2' ) expect(response.ok).toBe(true) @@ -167,7 +167,76 @@ describe('paginatedFetch', () => { vi.stubGlobal('fetch', fetchMock) const response = await apiMethods.paginatedFetch<{ id: string }[]>( - 'https://hydro.example.com/api/data/observations?limit=2' + 'https://hydro.example.com/api/ogc/collections/observations/items?limit=2' + ) + + expect(response.ok).toBe(true) + if (!response.ok) return + expect(response.data.map((item) => item.id)).toEqual(['1', '2', '3']) + }) + + describe('when the server clamps the requested limit', () => { + // 5 records; the client asks for limit=4 but the server caps pages at 2. + const mockClampedServer = (meta: (offset: number) => Record) => { + const records = ['1', '2', '3', '4', '5'].map((id) => ({ id })) + const fetchMock = vi.fn().mockImplementation(async (input: string | URL) => { + const url = new URL(String(input)) + const offset = Number(url.searchParams.get('offset') ?? '0') + const limit = Math.min(Number(url.searchParams.get('limit')), 2) + return jsonResponse({ + data: records.slice(offset, offset + limit), + meta: meta(offset), + }) + }) + vi.stubGlobal('fetch', fetchMock) + return fetchMock + } + + const requestedOffsets = (fetchMock: ReturnType) => + fetchMock.mock.calls.map(([input]) => + Number(new URL(String(input)).searchParams.get('offset')) + ) + + it('pages by the returned meta.limit instead of the requested limit', async () => { + const fetchMock = mockClampedServer((offset) => ({ offset, limit: 2, totalCount: 5 })) + + const response = await apiMethods.paginatedFetch<{ id: string }[]>( + 'https://hydro.example.com/api/ogc/collections/observations/items?limit=4' + ) + + expect(response.ok).toBe(true) + if (!response.ok) return + expect(requestedOffsets(fetchMock)).toEqual([0, 2, 4]) + expect(response.data.map((item) => item.id)).toEqual(['1', '2', '3', '4', '5']) + expect(response.meta?.totalCount).toBe(5) + }) + + it('pages by the returned meta.limit when totalCount is missing', async () => { + mockClampedServer((offset) => ({ offset, limit: 2 })) + + const response = await apiMethods.paginatedFetch<{ id: string }[]>( + 'https://hydro.example.com/api/ogc/collections/observations/items?limit=4' + ) + + expect(response.ok).toBe(true) + if (!response.ok) return + expect(response.data.map((item) => item.id)).toEqual(['1', '2', '3', '4', '5']) + }) + }) + + it('falls back to the requested limit when the response has no meta.limit', async () => { + const pages: Record = { + '0': [{ id: '1' }, { id: '2' }], + '2': [{ id: '3' }], + } + const fetchMock = vi.fn().mockImplementation(async (input: string | URL) => { + const offset = new URL(String(input)).searchParams.get('offset') ?? '0' + return jsonResponse({ data: pages[offset] ?? [], meta: { offset: Number(offset), totalCount: 3 } }) + }) + vi.stubGlobal('fetch', fetchMock) + + const response = await apiMethods.paginatedFetch<{ id: string }[]>( + 'https://hydro.example.com/api/ogc/collections/observations/items?limit=2' ) expect(response.ok).toBe(true) diff --git a/packages/hydroserver-ts/src/api/__tests__/data-connection.service.spec.ts b/packages/hydroserver-ts/src/api/__tests__/data-connection.service.spec.ts index 72493a52..7de3e3bf 100644 --- a/packages/hydroserver-ts/src/api/__tests__/data-connection.service.spec.ts +++ b/packages/hydroserver-ts/src/api/__tests__/data-connection.service.spec.ts @@ -39,7 +39,7 @@ describe('DataConnectionService', () => { ) expect(fetchMock.mock.calls[0][0]).toBe( - 'https://hydro.example.com/api/data/etl-data-connections' + 'https://hydro.example.com/api/ogc/collections/etl-data-connections/items' ) expect(fetchMock.mock.calls[0][1].method).toBe('POST') expect(JSON.parse(fetchMock.mock.calls[0][1].body)).toMatchObject({ @@ -68,7 +68,7 @@ describe('DataConnectionService', () => { }) expect(fetchMock.mock.calls[0][0]).toBe( - 'https://hydro.example.com/api/data/etl-data-connections/data-connection-1' + 'https://hydro.example.com/api/ogc/collections/etl-data-connections/items/data-connection-1' ) expect(fetchMock.mock.calls[0][1].method).toBe('PATCH') expect(JSON.parse(fetchMock.mock.calls[0][1].body)).toMatchObject({ diff --git a/packages/hydroserver-ts/src/api/__tests__/data-product-transformation.service.spec.ts b/packages/hydroserver-ts/src/api/__tests__/data-product-transformation.service.spec.ts index 657eca18..7864d754 100644 --- a/packages/hydroserver-ts/src/api/__tests__/data-product-transformation.service.spec.ts +++ b/packages/hydroserver-ts/src/api/__tests__/data-product-transformation.service.spec.ts @@ -50,14 +50,14 @@ describe('DataProductTransformationService', () => { }) expect(fetchMock.mock.calls[0][0]).toBe( - 'https://hydro.example.com/api/data/data-product-transformations' + 'https://hydro.example.com/api/ogc/collections/data-product-transformations/items' ) expect(fetchMock.mock.calls[0][1].method).toBe('POST') const sentBody = JSON.parse(fetchMock.mock.calls[0][1].body) expect(sentBody.taskId).toBe('task-1') expect(fetchMock.mock.calls[1][0]).toBe( - 'https://hydro.example.com/api/data/data-product-transformations/transformation-1' + 'https://hydro.example.com/api/ogc/collections/data-product-transformations/items/transformation-1' ) expect(response.ok).toBe(true) @@ -95,7 +95,7 @@ describe('DataProductTransformationService', () => { }) expect(fetchMock.mock.calls[0][0]).toBe( - 'https://hydro.example.com/api/data/data-product-transformations/transformation-1' + 'https://hydro.example.com/api/ogc/collections/data-product-transformations/items/transformation-1' ) expect(fetchMock.mock.calls[0][1].method).toBe('PATCH') expect(response.ok).toBe(true) diff --git a/packages/hydroserver-ts/src/api/__tests__/etl-mapping.service.spec.ts b/packages/hydroserver-ts/src/api/__tests__/etl-mapping.service.spec.ts index f79cacf2..423be2c2 100644 --- a/packages/hydroserver-ts/src/api/__tests__/etl-mapping.service.spec.ts +++ b/packages/hydroserver-ts/src/api/__tests__/etl-mapping.service.spec.ts @@ -38,14 +38,14 @@ describe('EtlMappingService', () => { }) expect(fetchMock.mock.calls[0][0]).toBe( - 'https://hydro.example.com/api/data/etl-mappings' + 'https://hydro.example.com/api/ogc/collections/etl-mappings/items' ) expect(fetchMock.mock.calls[0][1].method).toBe('POST') const sentBody = JSON.parse(fetchMock.mock.calls[0][1].body) expect(sentBody.etlTaskId).toBe('task-1') expect(fetchMock.mock.calls[1][0]).toBe( - 'https://hydro.example.com/api/data/etl-mappings/mapping-1' + 'https://hydro.example.com/api/ogc/collections/etl-mappings/items/mapping-1' ) expect(response.ok).toBe(true) @@ -77,7 +77,7 @@ describe('EtlMappingService', () => { }) expect(fetchMock.mock.calls[0][0]).toBe( - 'https://hydro.example.com/api/data/etl-mappings/mapping-1' + 'https://hydro.example.com/api/ogc/collections/etl-mappings/items/mapping-1' ) expect(fetchMock.mock.calls[0][1].method).toBe('PATCH') expect(response.ok).toBe(true) diff --git a/packages/hydroserver-ts/src/api/__tests__/monitoring-rule.service.spec.ts b/packages/hydroserver-ts/src/api/__tests__/monitoring-rule.service.spec.ts index c80484ea..b39dcfbe 100644 --- a/packages/hydroserver-ts/src/api/__tests__/monitoring-rule.service.spec.ts +++ b/packages/hydroserver-ts/src/api/__tests__/monitoring-rule.service.spec.ts @@ -42,14 +42,14 @@ describe('MonitoringRuleService', () => { }) expect(fetchMock.mock.calls[0][0]).toBe( - 'https://hydro.example.com/api/data/monitoring-rules' + 'https://hydro.example.com/api/ogc/collections/monitoring-rules/items' ) expect(fetchMock.mock.calls[0][1].method).toBe('POST') const sentBody = JSON.parse(fetchMock.mock.calls[0][1].body) expect(sentBody.taskId).toBe('task-1') expect(fetchMock.mock.calls[1][0]).toBe( - 'https://hydro.example.com/api/data/monitoring-rules/rule-1' + 'https://hydro.example.com/api/ogc/collections/monitoring-rules/items/rule-1' ) expect(response.ok).toBe(true) @@ -83,7 +83,7 @@ describe('MonitoringRuleService', () => { }) expect(fetchMock.mock.calls[0][0]).toBe( - 'https://hydro.example.com/api/data/monitoring-rules/rule-1' + 'https://hydro.example.com/api/ogc/collections/monitoring-rules/items/rule-1' ) expect(fetchMock.mock.calls[0][1].method).toBe('PATCH') expect(response.ok).toBe(true) diff --git a/packages/hydroserver-ts/src/api/__tests__/monitoring-site-datastream.service.spec.ts b/packages/hydroserver-ts/src/api/__tests__/monitoring-site-datastream.service.spec.ts index be57cb65..c0844884 100644 --- a/packages/hydroserver-ts/src/api/__tests__/monitoring-site-datastream.service.spec.ts +++ b/packages/hydroserver-ts/src/api/__tests__/monitoring-site-datastream.service.spec.ts @@ -15,6 +15,34 @@ describe('MonitoringSiteService', () => { const client = new HydroServer({ host: 'https://hydro.example.com' }) + describe('getLinkedResources', () => { + it('fetches every page of a site\'s linked resources', async () => { + const items = Array.from({ length: 250 }, (_, i) => ({ + id: `lr-${i}`, + name: `Resource ${i}`, + type: 'Report', + link: `https://example.com/${i}`, + })) + const fetchMock = vi.fn(async (input: any) => { + const params = new URL(String(input)).searchParams + const offset = Number(params.get('offset') ?? 0) + const limit = Number(params.get('limit') ?? 100) + return jsonResponse({ + data: items.slice(offset, offset + limit), + meta: { offset, limit, totalCount: items.length }, + }) + }) + vi.stubGlobal('fetch', fetchMock) + + const res = await client.monitoringSites.getLinkedResources('site-1') + + expect(fetchMock.mock.calls.length).toBeGreaterThan(1) + expect(res.ok && res.data.map((item: { id: string }) => item.id)).toEqual( + items.map((item) => item.id) + ) + }) + }) + describe('listMarkers', () => { it('fetches monitoringSite markers and returns them in data', async () => { const payload = [ @@ -36,7 +64,7 @@ describe('MonitoringSiteService', () => { expect(res.ok).toBe(true) expect(res.data).toEqual(payload) const [url] = (fetch as any).mock.calls[0] - expect(url).toMatch(/\/api\/data\/monitoring-sites\/markers$/) + expect(url).toMatch(/\/api\/ogc\/collections\/monitoring-sites\/markers$/) }) it('returns ok:false on a failed request', async () => { @@ -139,14 +167,16 @@ describe('MonitoringSiteService', () => { }) describe('linked resources', () => { - it('createLinkedResource posts, then re-fetches the list to find the new entry by id', async () => { + it('createLinkedResource posts, then fetches the new entry by id', async () => { const fetchMock = vi.fn().mockImplementation(async (input: string | URL, init?: RequestInit) => { if (init?.method === 'POST') { return jsonResponse({ id: 'linked-1' }) } - return jsonResponse([ - { id: 'linked-1', name: 'Site Report', type: 'Report', link: 'https://example.com/report.pdf' }, - ]) + return jsonResponse({ + data: { id: 'linked-1', name: 'Site Report', type: 'Report', link: 'https://example.com/report.pdf' }, + included: null, + links: [], + }) }) vi.stubGlobal('fetch', fetchMock) @@ -156,19 +186,22 @@ describe('MonitoringSiteService', () => { ) expect(fetchMock).toHaveBeenCalledTimes(2) + expect(String(fetchMock.mock.calls[1][0])).toMatch(/\/linked-resources\/linked-1$/) expect(res.ok).toBe(true) if (!res.ok) return expect(res.data).toMatchObject({ id: 'linked-1', name: 'Site Report' }) }) - it('updateLinkedResource patches, then re-fetches the list to find the updated entry by id', async () => { + it('updateLinkedResource patches, then fetches the updated entry by id', async () => { const fetchMock = vi.fn().mockImplementation(async (input: string | URL, init?: RequestInit) => { if (init?.method === 'PATCH') { return new Response(null, { status: 204, headers: { 'Content-Length': '0' } }) } - return jsonResponse([ - { id: 'linked-1', name: 'Updated Report', type: 'Report', link: 'https://example.com/report.pdf' }, - ]) + return jsonResponse({ + data: { id: 'linked-1', name: 'Updated Report', type: 'Report', link: 'https://example.com/report.pdf' }, + included: null, + links: [], + }) }) vi.stubGlobal('fetch', fetchMock) @@ -179,15 +212,16 @@ describe('MonitoringSiteService', () => { ) expect(fetchMock).toHaveBeenCalledTimes(2) + expect(String(fetchMock.mock.calls[1][0])).toMatch(/\/linked-resources\/linked-1$/) expect(res.ok).toBe(true) if (!res.ok) return expect(res.data).toMatchObject({ id: 'linked-1', name: 'Updated Report' }) }) - it('createLinkedResource returns ok:false when the new entry is missing from the refetched list', async () => { + it('createLinkedResource returns ok:false when the new entry cannot be fetched', async () => { const fetchMock = vi.fn().mockImplementation(async (input: string | URL, init?: RequestInit) => { if (init?.method === 'POST') return jsonResponse({ id: 'linked-1' }) - return jsonResponse([]) + return jsonResponse({ message: 'Linked resource does not exist' }, 404) }) vi.stubGlobal('fetch', fetchMock) @@ -208,7 +242,7 @@ describe('MonitoringSiteService', () => { expect(res.ok).toBe(true) const [url] = (fetch as any).mock.calls[0] - expect(url).toMatch(/\/api\/data\/monitoring-sites\/site-summaries$/) + expect(url).toMatch(/\/api\/ogc\/collections\/monitoring-sites\/site-summaries$/) }) it('passes workspace_id as a query param and returns summaries', async () => { @@ -235,7 +269,7 @@ describe('MonitoringSiteService', () => { const [url] = (fetch as any).mock.calls[0] const parsed = new URL(url) - expect(parsed.pathname).toBe('/api/data/monitoring-sites/site-summaries') + expect(parsed.pathname).toBe('/api/ogc/collections/monitoring-sites/site-summaries') expect(parsed.searchParams.get('workspace_id')).toBe('workspace id') }) @@ -258,33 +292,38 @@ describe('DatastreamService', () => { const client = new HydroServer({ host: 'https://hydro.example.com' }) describe('linked resources', () => { - it('createLinkedResource posts, then re-fetches the list to find the new entry by id', async () => { + it('createLinkedResource posts, then fetches the new entry by id', async () => { const fetchMock = vi.fn().mockImplementation(async (input: string | URL, init?: RequestInit) => { if (init?.method === 'POST') { return jsonResponse({ id: 'linked-1' }) } - return jsonResponse([ - { id: 'linked-1', name: 'Datastream Report', type: 'Report', link: 'https://example.com/report.pdf' }, - ]) + return jsonResponse({ + data: { id: 'linked-1', name: 'Datastream Report', type: 'Report', link: 'https://example.com/report.pdf' }, + included: null, + links: [], + }) }) vi.stubGlobal('fetch', fetchMock) const res = await client.datastreams.createLinkedResource('ds-1', new FormData()) expect(fetchMock).toHaveBeenCalledTimes(2) + expect(String(fetchMock.mock.calls[1][0])).toMatch(/\/linked-resources\/linked-1$/) expect(res.ok).toBe(true) if (!res.ok) return expect(res.data).toMatchObject({ id: 'linked-1', name: 'Datastream Report' }) }) - it('updateLinkedResource patches, then re-fetches the list to find the updated entry by id', async () => { + it('updateLinkedResource patches, then fetches the updated entry by id', async () => { const fetchMock = vi.fn().mockImplementation(async (input: string | URL, init?: RequestInit) => { if (init?.method === 'PATCH') { return new Response(null, { status: 204, headers: { 'Content-Length': '0' } }) } - return jsonResponse([ - { id: 'linked-1', name: 'Updated Report', type: 'Report', link: 'https://example.com/report.pdf' }, - ]) + return jsonResponse({ + data: { id: 'linked-1', name: 'Updated Report', type: 'Report', link: 'https://example.com/report.pdf' }, + included: null, + links: [], + }) }) vi.stubGlobal('fetch', fetchMock) @@ -295,6 +334,7 @@ describe('DatastreamService', () => { ) expect(fetchMock).toHaveBeenCalledTimes(2) + expect(String(fetchMock.mock.calls[1][0])).toMatch(/\/linked-resources\/linked-1$/) expect(res.ok).toBe(true) if (!res.ok) return expect(res.data).toMatchObject({ id: 'linked-1', name: 'Updated Report' }) @@ -323,7 +363,7 @@ describe('DatastreamService', () => { expect(res.ok).toBe(true) const [url] = (fetch as any).mock.calls[0] - expect(url).toMatch(/\/api\/data\/datastreams\/visualization-bootstrap$/) + expect(url).toMatch(/\/api\/ogc\/collections\/datastreams\/visualization-bootstrap$/) expect(res.data.monitoringSites[0]).toBeInstanceOf(MonitoringSite) expect(res.data.datastreams[0]).toBeInstanceOf(Datastream) diff --git a/packages/hydroserver-ts/src/api/__tests__/quality-control.service.spec.ts b/packages/hydroserver-ts/src/api/__tests__/quality-control.service.spec.ts index 5b3cfd3d..4843eec4 100644 --- a/packages/hydroserver-ts/src/api/__tests__/quality-control.service.spec.ts +++ b/packages/hydroserver-ts/src/api/__tests__/quality-control.service.spec.ts @@ -39,7 +39,7 @@ describe('QualityControl services', () => { const url = new URL(fetchMock.mock.calls[0][0]) expect(url.href).toBe( - 'https://hydro.example.com/api/data/quality-control/histories?managed_datastream_id=managed-1&offset=0&limit=200' + 'https://hydro.example.com/api/ogc/collections/quality-control-histories/items?managed_datastream_id=managed-1&offset=0&limit=200' ) }) @@ -63,7 +63,7 @@ describe('QualityControl services', () => { }) expect(fetchMock.mock.calls[0][0]).toBe( - 'https://hydro.example.com/api/data/quality-control/histories/history-1/sessions' + 'https://hydro.example.com/api/ogc/collections/quality-control-histories/items/history-1/sessions' ) expect(fetchMock.mock.calls[0][1].method).toBe('POST') expect(JSON.parse(fetchMock.mock.calls[0][1].body)).toEqual({ @@ -102,7 +102,7 @@ describe('QualityControl services', () => { expect(fetchMock).toHaveBeenCalledTimes(2) expect(fetchMock.mock.calls[0][0]).toBe( - 'https://hydro.example.com/api/data/quality-control/histories/history-1/sessions/session-1/commit' + 'https://hydro.example.com/api/ogc/collections/quality-control-histories/items/history-1/sessions/session-1/commit' ) expect(response.ok).toBe(true) if (!response.ok) return @@ -134,7 +134,7 @@ describe('QualityControl services', () => { ]) expect(fetchMock.mock.calls[0][0]).toBe( - 'https://hydro.example.com/api/data/quality-control/histories/history-1/sessions/session-1/operations' + 'https://hydro.example.com/api/ogc/collections/quality-control-histories/items/history-1/sessions/session-1/operations' ) expect(fetchMock.mock.calls[0][1].method).toBe('POST') expect(JSON.parse(fetchMock.mock.calls[0][1].body)).toEqual([ diff --git a/packages/hydroserver-ts/src/api/__tests__/rating-curve.service.spec.ts b/packages/hydroserver-ts/src/api/__tests__/rating-curve.service.spec.ts index a87cb62b..70da99b8 100644 --- a/packages/hydroserver-ts/src/api/__tests__/rating-curve.service.spec.ts +++ b/packages/hydroserver-ts/src/api/__tests__/rating-curve.service.spec.ts @@ -43,7 +43,7 @@ describe('RatingCurveService', () => { expect(response).toHaveLength(1) const url = new URL(fetchMock.mock.calls[0][0]) expect(url.href).toBe( - 'https://hydro.example.com/api/data/data-product-rating-curves?sortby=name&monitoring_site_id=monitoringSite-1&offset=0&limit=200' + 'https://hydro.example.com/api/ogc/collections/data-product-rating-curves/items?sortby=name&monitoring_site_id=monitoringSite-1&offset=0&limit=200' ) }) @@ -77,7 +77,7 @@ describe('RatingCurveService', () => { }) expect(fetchMock.mock.calls[0][0]).toBe( - 'https://hydro.example.com/api/data/data-product-rating-curves' + 'https://hydro.example.com/api/ogc/collections/data-product-rating-curves/items' ) expect(fetchMock.mock.calls[0][1].method).toBe('POST') expect(JSON.parse(fetchMock.mock.calls[0][1].body)).toEqual({ @@ -88,7 +88,7 @@ describe('RatingCurveService', () => { points: [[1, 2]], }) expect(String(fetchMock.mock.calls[1][0])).toBe( - 'https://hydro.example.com/api/data/data-product-rating-curves/rating-curve-1' + 'https://hydro.example.com/api/ogc/collections/data-product-rating-curves/items/rating-curve-1' ) expect(response.ok).toBe(true) if (!response.ok) return @@ -127,7 +127,7 @@ describe('RatingCurveService', () => { }) expect(fetchMock.mock.calls[0][0]).toBe( - 'https://hydro.example.com/api/data/data-product-rating-curves/rating-curve-1' + 'https://hydro.example.com/api/ogc/collections/data-product-rating-curves/items/rating-curve-1' ) expect(fetchMock.mock.calls[0][1].method).toBe('PATCH') expect(JSON.parse(fetchMock.mock.calls[0][1].body)).toEqual({ @@ -137,7 +137,7 @@ describe('RatingCurveService', () => { points: [[2, 3]], }) expect(String(fetchMock.mock.calls[1][0])).toBe( - 'https://hydro.example.com/api/data/data-product-rating-curves/rating-curve-1' + 'https://hydro.example.com/api/ogc/collections/data-product-rating-curves/items/rating-curve-1' ) expect(response.ok).toBe(true) if (!response.ok) return @@ -155,7 +155,7 @@ describe('RatingCurveService', () => { await client.ratingCurves.delete('rating-curve-1') expect(fetchMock.mock.calls[0][0]).toBe( - 'https://hydro.example.com/api/data/data-product-rating-curves/rating-curve-1' + 'https://hydro.example.com/api/ogc/collections/data-product-rating-curves/items/rating-curve-1' ) expect(fetchMock.mock.calls[0][1].method).toBe('DELETE') }) diff --git a/packages/hydroserver-ts/src/api/__tests__/task.service.spec.ts b/packages/hydroserver-ts/src/api/__tests__/task.service.spec.ts index 5c8f19a3..e64b3c1c 100644 --- a/packages/hydroserver-ts/src/api/__tests__/task.service.spec.ts +++ b/packages/hydroserver-ts/src/api/__tests__/task.service.spec.ts @@ -15,6 +15,28 @@ describe('TaskService', () => { vi.restoreAllMocks() }) + it('unwraps the item envelope from triggered and fetched runs', async () => { + const envelope = { + data: { id: 'run-1', status: 'PENDING' }, + included: null, + links: [ + { + href: 'https://hydro.example.com/api/ogc/collections/etl-tasks/items/task-1/runs/run-1', + rel: 'self', + type: 'application/json', + }, + ], + } + vi.stubGlobal('fetch', vi.fn().mockImplementation(async () => jsonResponse(envelope))) + + const client = new HydroServer({ host: 'https://hydro.example.com' }) + const triggered = await client.tasks.runTask('task-1') + const fetched = await client.tasks.getTaskRun('task-1', 'run-1') + + expect(triggered.ok && triggered.data).toEqual({ id: 'run-1', status: 'PENDING' }) + expect(fetched.ok && fetched.data).toEqual({ id: 'run-1', status: 'PENDING' }) + }) + it('forwards task run payloads without normalization', async () => { const rawResult = { message: 'Loaded 12 total observation(s) into 1 datastream(s).', @@ -124,7 +146,7 @@ describe('TaskService', () => { }) expect(fetchMock.mock.calls[0][0]).toBe( - 'https://hydro.example.com/api/data/monitoring-tasks' + 'https://hydro.example.com/api/ogc/collections/monitoring-tasks/items' ) expect(fetchMock.mock.calls[0][1].method).toBe('POST') expect(JSON.parse(fetchMock.mock.calls[0][1].body)).toEqual({ diff --git a/packages/hydroserver-ts/src/api/__tests__/workspace.service.spec.ts b/packages/hydroserver-ts/src/api/__tests__/workspace.service.spec.ts index b99a932f..97183c5e 100644 --- a/packages/hydroserver-ts/src/api/__tests__/workspace.service.spec.ts +++ b/packages/hydroserver-ts/src/api/__tests__/workspace.service.spec.ts @@ -7,6 +7,26 @@ describe('WorkspaceService', () => { vi.restoreAllMocks() }) + it('serializes boolean and null query values as lowercase true, false and null', async () => { + const fetchMock = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ data: [], meta: { offset: 0, limit: 100, totalCount: 0 } }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }) + ) + vi.stubGlobal('fetch', fetchMock) + + const client = new HydroServer({ host: 'https://hydro.example.com' }) + await client.workspaces.list({ is_associated: true, is_private: false }) + await client.units.list({ workspace_id: ['workspace-1', 'null'] } as any) + + const workspaceParams = new URL(String(fetchMock.mock.calls[0][0])).searchParams + expect(workspaceParams.get('is_associated')).toBe('true') + expect(workspaceParams.get('is_private')).toBe('false') + const unitParams = new URL(String(fetchMock.mock.calls[1][0])).searchParams + expect(unitParams.getAll('workspace_id')).toEqual(['workspace-1', 'null']) + }) + it('uses the service-account endpoint for the workspace management table', async () => { const fetchMock = vi.fn().mockResolvedValue( new Response(JSON.stringify({ data: [], meta: { offset: 0, limit: 200, totalCount: 0 } }), { @@ -20,7 +40,7 @@ describe('WorkspaceService', () => { await client.workspaces.getServiceAccounts('workspace-1') expect(String(fetchMock.mock.calls[0][0])).toBe( - 'https://hydro.example.com/api/data/workspaces/workspace-1/service-accounts?offset=0&limit=200' + 'https://hydro.example.com/api/ogc/collections/workspaces/items/workspace-1/service-accounts?offset=0&limit=200' ) }) @@ -69,8 +89,8 @@ describe('WorkspaceService', () => { expect(response.data.every((c) => c.role?.id === 'role-1')).toBe(true) expect(fetchMock).toHaveBeenCalledTimes(2) expect(fetchMock.mock.calls.map(([url]) => String(url))).toEqual([ - 'https://hydro.example.com/api/data/workspaces/workspace-1/collaborators?include=role%2Cuser%2CserviceAccount&offset=0&limit=200', - 'https://hydro.example.com/api/data/workspaces/workspace-1/collaborators?include=role%2Cuser%2CserviceAccount&offset=200&limit=200', + 'https://hydro.example.com/api/ogc/collections/workspaces/items/workspace-1/collaborators?include=role%2Cuser%2CserviceAccount&offset=0&limit=200', + 'https://hydro.example.com/api/ogc/collections/workspaces/items/workspace-1/collaborators?include=role%2Cuser%2CserviceAccount&offset=200&limit=200', ]) }) @@ -139,8 +159,8 @@ describe('WorkspaceService', () => { expect(response.data).toEqual([{ id: 'account-1' }, { id: 'account-2' }]) expect(fetchMock).toHaveBeenCalledTimes(2) expect(fetchMock.mock.calls.map(([url]) => String(url))).toEqual([ - 'https://hydro.example.com/api/data/workspaces/workspace-1/service-accounts?offset=0&limit=200', - 'https://hydro.example.com/api/data/workspaces/workspace-1/service-accounts?offset=200&limit=200', + 'https://hydro.example.com/api/ogc/collections/workspaces/items/workspace-1/service-accounts?offset=0&limit=200', + 'https://hydro.example.com/api/ogc/collections/workspaces/items/workspace-1/service-accounts?offset=200&limit=200', ]) }) @@ -175,7 +195,7 @@ describe('WorkspaceService', () => { const response = await client.workspaces.get('workspace-1') expect(String(fetchMock.mock.calls[0][0])).toBe( - 'https://hydro.example.com/api/data/workspaces/workspace-1?include=owner%2CpendingTransferTo%2CcollaboratorRole' + 'https://hydro.example.com/api/ogc/collections/workspaces/items/workspace-1?include=owner%2CpendingTransferTo%2CcollaboratorRole' ) expect(response.ok).toBe(true) if (!response.ok) return @@ -355,7 +375,7 @@ describe('WorkspaceService', () => { expect(fetchMock).toHaveBeenCalledTimes(1) expect(String(fetchMock.mock.calls[0][0])).toBe( - 'https://hydro.example.com/api/data/workspaces/workspace-1/service-accounts/account-1/regenerate' + 'https://hydro.example.com/api/ogc/collections/workspaces/items/workspace-1/service-accounts/account-1/regenerate' ) expect(response.ok).toBe(true) if (!response.ok) return diff --git a/packages/hydroserver-ts/src/api/apiMethods.ts b/packages/hydroserver-ts/src/api/apiMethods.ts index 22399c5d..38ce2321 100644 --- a/packages/hydroserver-ts/src/api/apiMethods.ts +++ b/packages/hydroserver-ts/src/api/apiMethods.ts @@ -209,6 +209,10 @@ export const apiMethods = { ) } + const metaLimit = Number(firstPageMeta?.limit) + const pageSize = + Number.isInteger(metaLimit) && metaLimit > 0 ? metaLimit : limitParam + const totalCount = typeof firstPageMeta?.totalCount === 'number' ? firstPageMeta.totalCount @@ -216,7 +220,7 @@ export const apiMethods = { const offsets: number[] = [] if (totalCount !== undefined) { - for (let offset = limitParam; offset < totalCount; offset += limitParam) { + for (let offset = pageSize; offset < totalCount; offset += pageSize) { offsets.push(offset) } } @@ -233,12 +237,12 @@ export const apiMethods = { lastRowCount = pageRowCount(page.data) } - let nextOffset = limitParam * (1 + offsets.length) + let nextOffset = pageSize * (1 + offsets.length) const MAX_EXTRA_PAGES = 1000 let extraPages = 0 while ( - limitParam > 0 && - lastRowCount === limitParam && + pageSize > 0 && + lastRowCount === pageSize && extraPages < MAX_EXTRA_PAGES ) { const page = await fetchPage(nextOffset) @@ -246,7 +250,7 @@ export const apiMethods = { mergedIncluded = mergeIncluded(mergedIncluded, page.included) if (!mergePageData(page)) break lastRowCount = pageRowCount(page.data) - nextOffset += limitParam + nextOffset += pageSize extraPages += 1 } diff --git a/packages/hydroserver-ts/src/api/services/base.ts b/packages/hydroserver-ts/src/api/services/base.ts index f8b04512..255f57b5 100644 --- a/packages/hydroserver-ts/src/api/services/base.ts +++ b/packages/hydroserver-ts/src/api/services/base.ts @@ -47,10 +47,15 @@ export abstract class HydroServerBaseService< this._ModelCtor = ctor.Model } - /** Not all resources are at api/data/ so provide this function to allow a service to override */ + /** Not all resources are at api/ogc/ so provide this function to allow a service to override */ protected getBaseUrl(): string { return this._client.baseRoute } + + /** Collection root (the route without its trailing `/items`), for collection-level endpoints such as tag keys. */ + protected get _collectionRoute(): string { + return this._route.replace(/\/items$/, '') + } async list( params: Partial> & { fetch_all?: boolean diff --git a/packages/hydroserver-ts/src/api/services/data-product-transformation.types.ts b/packages/hydroserver-ts/src/api/services/data-product-transformation.types.ts index bf547800..5fa1014f 100644 --- a/packages/hydroserver-ts/src/api/services/data-product-transformation.types.ts +++ b/packages/hydroserver-ts/src/api/services/data-product-transformation.types.ts @@ -1,4 +1,4 @@ -import type * as Data from '../../generated/data.types' +import type * as Data from '../../generated/ogc.types' export type DataProductTransformation = Data.components['schemas']['DataProductTransformationResponse'] diff --git a/packages/hydroserver-ts/src/api/services/datastream.service.ts b/packages/hydroserver-ts/src/api/services/datastream.service.ts index 6b492ad4..a8b845ee 100644 --- a/packages/hydroserver-ts/src/api/services/datastream.service.ts +++ b/packages/hydroserver-ts/src/api/services/datastream.service.ts @@ -4,7 +4,7 @@ import { DatastreamContract as C, ObservationContract, } from '../../generated/contracts' -import type * as Data from '../../generated/data.types' +import type * as Data from '../../generated/ogc.types' import type { ApiResponse } from '../responseInterceptor' import { Datastream as M, @@ -16,7 +16,10 @@ import { ProcessingLevel, Unit, } from '../../types' -import { normalizeLinkCollection } from './link-normalization' +import { + normalizeLinkCollection, + normalizeLinkRecord, +} from './link-normalization' const DATASTREAM_EXPAND_INCLUDE = 'workspace,monitoringSite,method,observedProperty,processingLevel,unit' as const @@ -190,7 +193,7 @@ export class DatastreamService extends HydroServerBaseService { /* ----------------------- Sub-resources: Tags ----------------------- */ getTagKeys(params: { workspace_id?: string; datastream_id?: string }) { - const url = this.withQuery(`${this._route}/tags/keys`, params) + const url = this.withQuery(`${this._collectionRoute}/tags/keys`, params) return apiMethods.fetch(url) } @@ -230,7 +233,7 @@ export class DatastreamService extends HydroServerBaseService { const url = `${this._route}/${datastreamId}/linked-resources` const res = await apiMethods.post<{ id: string }>(url, data) if (!res.ok) return res - return this.findLinkedResource(datastreamId, res.data.id) + return this.getLinkedResource(datastreamId, res.data.id) } async updateLinkedResource( @@ -241,24 +244,17 @@ export class DatastreamService extends HydroServerBaseService { const url = `${this._route}/${datastreamId}/linked-resources/${linkedResourceId}` const res = await apiMethods.patch(url, data) if (!res.ok) return res - return this.findLinkedResource(datastreamId, linkedResourceId) + return this.getLinkedResource(datastreamId, linkedResourceId) } - private async findLinkedResource( + async getLinkedResource( datastreamId: string, linkedResourceId: string ): Promise> { - const res = await this.getLinkedResources(datastreamId) + const url = `${this._route}/${datastreamId}/linked-resources/${linkedResourceId}` + const res = await apiMethods.fetch(url) if (!res.ok) return res - const found = res.data.find((r) => r.id === linkedResourceId) - if (!found) { - return { - ok: false, - status: 404, - message: 'Linked resource not found after save.', - } - } - return { ...res, data: found } + return { ...res, data: normalizeLinkRecord(res.data, this._client.host) } } deleteLinkedResource(datastreamId: string, linkedResourceId: string) { @@ -281,7 +277,7 @@ export class DatastreamService extends HydroServerBaseService { datastreamId: string, params: ObservationContract.QueryParameters ) { - const url = this.withQuery(`${this._client.baseRoute}/observations`, { + const url = this.withQuery(`${this._client.baseRoute}/collections/observations/items`, { ...params, datastream_id: datastreamId, }) @@ -289,7 +285,7 @@ export class DatastreamService extends HydroServerBaseService { } createObservation(datastreamId: string, body: ObservationPostBody) { - const url = `${this._client.baseRoute}/observations` + const url = `${this._client.baseRoute}/collections/observations/items` return apiMethods.post(url, { ...body, datastreamId, @@ -302,14 +298,14 @@ export class DatastreamService extends HydroServerBaseService { params?: ObservationBulkPostQueryParameters ) { const url = this.withQuery( - `${this._client.baseRoute}/observations/bulk-create`, + `${this._client.baseRoute}/collections/observations/bulk-create`, params ) return apiMethods.post(url, { ...body, datastreamId }) } deleteObservations(datastreamId: string, body?: ObservationBulkDeleteBody) { - const url = `${this._client.baseRoute}/observations/bulk-delete` + const url = `${this._client.baseRoute}/collections/observations/bulk-delete` return apiMethods.post(url, { ...(body || { phenomenonTimeStart: null, phenomenonTimeEnd: null }), datastreamId, @@ -317,14 +313,14 @@ export class DatastreamService extends HydroServerBaseService { } getObservation(_datastreamId: string, observationId: string) { - const url = `${this._client.baseRoute}/observations/${encodeURIComponent( + const url = `${this._client.baseRoute}/collections/observations/items/${encodeURIComponent( observationId )}` return apiMethods.fetch(url) } deleteObservation(_datastreamId: string, observationId: string) { - const url = `${this._client.baseRoute}/observations/${observationId}` + const url = `${this._client.baseRoute}/collections/observations/items/${observationId}` return apiMethods.delete(url) } @@ -332,7 +328,7 @@ export class DatastreamService extends HydroServerBaseService { ApiResponse > { const res = await apiMethods.fetch( - `${this._route}/visualization-bootstrap` + `${this._collectionRoute}/visualization-bootstrap` ) if (!res.ok) return res diff --git a/packages/hydroserver-ts/src/api/services/etl-mapping.types.ts b/packages/hydroserver-ts/src/api/services/etl-mapping.types.ts index 40814763..ed390067 100644 --- a/packages/hydroserver-ts/src/api/services/etl-mapping.types.ts +++ b/packages/hydroserver-ts/src/api/services/etl-mapping.types.ts @@ -1,4 +1,4 @@ -import type * as Data from '../../generated/data.types' +import type * as Data from '../../generated/ogc.types' export type EtlMapping = Data.components['schemas']['EtlMappingResponse'] diff --git a/packages/hydroserver-ts/src/api/services/link-normalization.ts b/packages/hydroserver-ts/src/api/services/link-normalization.ts index d88f378f..9382b793 100644 --- a/packages/hydroserver-ts/src/api/services/link-normalization.ts +++ b/packages/hydroserver-ts/src/api/services/link-normalization.ts @@ -3,7 +3,7 @@ type LinkLike = { } function pathShouldUseApiOrigin(pathname: string) { - return pathname.startsWith('/media/') || pathname.startsWith('/api/data/') + return pathname.startsWith('/media/') || pathname.startsWith('/api/ogc/') } export function preferredApiOrigin(host?: string | null) { diff --git a/packages/hydroserver-ts/src/api/services/monitoring-site.service.ts b/packages/hydroserver-ts/src/api/services/monitoring-site.service.ts index c0e78227..d3184411 100644 --- a/packages/hydroserver-ts/src/api/services/monitoring-site.service.ts +++ b/packages/hydroserver-ts/src/api/services/monitoring-site.service.ts @@ -1,7 +1,7 @@ import { apiMethods } from '../apiMethods' import { HydroServerBaseService } from './base' import { MonitoringSiteContract as C } from '../../generated/contracts' -import type * as Data from '../../generated/data.types' +import type * as Data from '../../generated/ogc.types' import { MonitoringSite, PostHydroShareArchive, @@ -12,7 +12,10 @@ import { MonitoringSiteTaskSummary, } from '../../types' import { ApiResponse } from '../responseInterceptor' -import { normalizeLinkCollection } from './link-normalization' +import { + normalizeLinkCollection, + normalizeLinkRecord, +} from './link-normalization' type LinkedResourceResponse = Data.components['schemas']['LinkedResourceGetResponse'] @@ -22,14 +25,14 @@ export class MonitoringSiteService extends HydroServerBaseService> { - return apiMethods.fetch(`${this._route}/markers`) + return apiMethods.fetch(`${this._collectionRoute}/markers`) } listSiteSummaries( workspaceId?: string ): Promise> { return apiMethods.fetch( - this.withQuery(`${this._route}/site-summaries`, { + this.withQuery(`${this._collectionRoute}/site-summaries`, { workspace_id: workspaceId, }) ) @@ -40,7 +43,7 @@ export class MonitoringSiteService extends HydroServerBaseService> { return apiMethods.fetch( - this.withQuery(`${this._route}/task-summaries`, params) + this.withQuery(`${this._collectionRoute}/task-summaries`, params) ) } @@ -50,11 +53,11 @@ export class MonitoringSiteService extends HydroServerBaseService> => this.patchAndRefetch(id, { isPrivate }) getSiteTypeIcons = () => - apiMethods.fetch(`${this._route}/site-type-icons`) + apiMethods.fetch(`${this._collectionRoute}/site-type-icons`) /* ----------------------- Sub-resources: Tags ----------------------- */ getTagKeys(params: { workspace_id?: string; monitoring_site_id?: string }) { - const url = this.withQuery(`${this._route}/tags/keys`, params) + const url = this.withQuery(`${this._collectionRoute}/tags/keys`, params) return apiMethods.fetch>(url) } @@ -94,7 +97,7 @@ export class MonitoringSiteService extends HydroServerBaseService(url, data) if (!res.ok) return res - return this.findLinkedResource(monitoringSiteId, res.data.id) + return this.getLinkedResource(monitoringSiteId, res.data.id) } async updateLinkedResource( @@ -105,24 +108,17 @@ export class MonitoringSiteService extends HydroServerBaseService(url, data) if (!res.ok) return res - return this.findLinkedResource(monitoringSiteId, linkedResourceId) + return this.getLinkedResource(monitoringSiteId, linkedResourceId) } - private async findLinkedResource( + async getLinkedResource( monitoringSiteId: string, linkedResourceId: string ): Promise> { - const res = await this.getLinkedResources(monitoringSiteId) + const url = `${this._route}/${monitoringSiteId}/linked-resources/${linkedResourceId}` + const res = await apiMethods.fetch(url) if (!res.ok) return res - const found = res.data.find((r) => r.id === linkedResourceId) - if (!found) { - return { - ok: false, - status: 404, - message: 'Linked resource not found after save.', - } - } - return { ...res, data: found } + return { ...res, data: normalizeLinkRecord(res.data, this._client.host) } } deleteLinkedResource(monitoringSiteId: string, linkedResourceId: string) { diff --git a/packages/hydroserver-ts/src/api/services/quality-control.service.ts b/packages/hydroserver-ts/src/api/services/quality-control.service.ts index 179dca2a..11871ea2 100644 --- a/packages/hydroserver-ts/src/api/services/quality-control.service.ts +++ b/packages/hydroserver-ts/src/api/services/quality-control.service.ts @@ -1,7 +1,7 @@ import { apiMethods } from '../apiMethods' import type { HydroServer } from '../HydroServer' import type { ApiResponse } from '../responseInterceptor' -import type * as Data from '../../generated/data.types' +import type * as Data from '../../generated/ogc.types' import { QualityControlHistoryContract, QualityControlOperationContract, @@ -20,7 +20,7 @@ export class QualityControlHistoryService { private readonly _route: string constructor(client: HydroServer) { - this._route = `${client.baseRoute}/quality-control/histories` + this._route = `${client.baseRoute}/collections/quality-control-histories/items` } async list( @@ -91,7 +91,7 @@ export class QualityControlSessionService { private readonly _route: string constructor(client: HydroServer) { - this._route = `${client.baseRoute}/quality-control/histories` + this._route = `${client.baseRoute}/collections/quality-control-histories/items` } async list( @@ -195,7 +195,7 @@ export class QualityControlOperationService { private readonly _route: string constructor(client: HydroServer) { - this._route = `${client.baseRoute}/quality-control/histories` + this._route = `${client.baseRoute}/collections/quality-control-histories/items` } async list( diff --git a/packages/hydroserver-ts/src/api/services/rating-curve-preview.service.ts b/packages/hydroserver-ts/src/api/services/rating-curve-preview.service.ts index 60ed94d1..ea45db82 100644 --- a/packages/hydroserver-ts/src/api/services/rating-curve-preview.service.ts +++ b/packages/hydroserver-ts/src/api/services/rating-curve-preview.service.ts @@ -183,7 +183,7 @@ export class RatingCurvePreviewService { } function isMonitoringSiteLinkDownloadPath(pathname: string) { - return /^\/api\/data\/monitoring-sites\/[^/]+\/links\/[^/]+\/download\/?$/.test( + return /^\/api\/ogc\/collections\/monitoring-sites\/items\/[^/]+\/links\/[^/]+\/download\/?$/.test( pathname ) } diff --git a/packages/hydroserver-ts/src/api/services/user.service.ts b/packages/hydroserver-ts/src/api/services/user.service.ts index aea91829..f271efd2 100644 --- a/packages/hydroserver-ts/src/api/services/user.service.ts +++ b/packages/hydroserver-ts/src/api/services/user.service.ts @@ -1,6 +1,6 @@ import { apiMethods } from '../apiMethods' import { type HydroServer } from '../HydroServer' -import type * as Data from '../../generated/data.types' +import type * as Data from '../../generated/ogc.types' import { User, Workspace } from '../../types' import { ApiResponse } from '../responseInterceptor' diff --git a/packages/hydroserver-ts/src/api/services/workspace.service.ts b/packages/hydroserver-ts/src/api/services/workspace.service.ts index b2897962..d45322d2 100644 --- a/packages/hydroserver-ts/src/api/services/workspace.service.ts +++ b/packages/hydroserver-ts/src/api/services/workspace.service.ts @@ -8,7 +8,7 @@ import { UserInfo, Workspace as M, } from '../../types' -import type * as Data from '../../generated/data.types' +import type * as Data from '../../generated/ogc.types' import { ApiResponse } from '../responseInterceptor' type RoleQueryParameters = NonNullable< @@ -271,10 +271,10 @@ export class WorkspaceService extends HydroServerBaseService { ) getRoles = (params?: RoleQueryParameters) => { - const url = this.withQuery(`${this._client.baseRoute}/roles`, params) + const url = this.withQuery(`${this._client.baseRoute}/collections/roles/items`, params) return apiMethods.paginatedFetch(url) } getRole = (id: string) => - apiMethods.fetch(`${this._client.baseRoute}/roles/${id}`) + apiMethods.fetch(`${this._client.baseRoute}/collections/roles/items/${id}`) } diff --git a/packages/hydroserver-ts/src/generated/contracts/aggregation-statistics.contract.ts b/packages/hydroserver-ts/src/generated/contracts/aggregation-statistics.contract.ts index 095643fc..c4c2a04a 100644 --- a/packages/hydroserver-ts/src/generated/contracts/aggregation-statistics.contract.ts +++ b/packages/hydroserver-ts/src/generated/contracts/aggregation-statistics.contract.ts @@ -1,9 +1,9 @@ /* AUTO-GENERATED. DO NOT EDIT. - Generated from ../../django/contracts/openapi/data.openapi.json */ -import type * as Data from '../data.types' + Generated from ../../django/contracts/openapi/ogc.openapi.json */ +import type * as Data from '../ogc.types' export namespace AggregationStatisticContract { - export const route = 'aggregation-statistics' as const + export const route = 'collections/aggregation-statistics/items' as const export type QueryParameters = ([Data.operations['interfaces_api_views_sta_aggregation_statistic_get_aggregation_statistics']['parameters']['query']] extends [never] ? {} : NonNullable) export type SummaryResponse = Data.components['schemas']['AggregationStatisticResponse'] export type DetailResponse = Data.components['schemas']['ItemResponse_AggregationStatisticResponse_'] diff --git a/packages/hydroserver-ts/src/generated/contracts/data-connections.contract.ts b/packages/hydroserver-ts/src/generated/contracts/data-connections.contract.ts index db21ac16..76ff094a 100644 --- a/packages/hydroserver-ts/src/generated/contracts/data-connections.contract.ts +++ b/packages/hydroserver-ts/src/generated/contracts/data-connections.contract.ts @@ -1,9 +1,9 @@ /* AUTO-GENERATED. DO NOT EDIT. - Generated from ../../django/contracts/openapi/data.openapi.json */ -import type * as Data from '../data.types' + Generated from ../../django/contracts/openapi/ogc.openapi.json */ +import type * as Data from '../ogc.types' export namespace DataConnectionContract { - export const route = 'etl-data-connections' as const + export const route = 'collections/etl-data-connections/items' as const export type QueryParameters = ([Data.operations['interfaces_api_views_etl_data_connection_get_data_connections']['parameters']['query']] extends [never] ? {} : NonNullable) export type SummaryResponse = Data.components['schemas']['DataConnectionResponse'] export type DetailResponse = Data.components['schemas']['ItemResponse_DataConnectionResponse_'] diff --git a/packages/hydroserver-ts/src/generated/contracts/data-product-tasks.contract.ts b/packages/hydroserver-ts/src/generated/contracts/data-product-tasks.contract.ts index 2e4cf868..e5b1fe87 100644 --- a/packages/hydroserver-ts/src/generated/contracts/data-product-tasks.contract.ts +++ b/packages/hydroserver-ts/src/generated/contracts/data-product-tasks.contract.ts @@ -1,9 +1,9 @@ /* AUTO-GENERATED. DO NOT EDIT. - Generated from ../../django/contracts/openapi/data.openapi.json */ -import type * as Data from '../data.types' + Generated from ../../django/contracts/openapi/ogc.openapi.json */ +import type * as Data from '../ogc.types' export namespace DataProductTaskContract { - export const route = 'data-product-tasks' as const + export const route = 'collections/data-product-tasks/items' as const export type QueryParameters = ([Data.operations['interfaces_api_views_products_task_get_data_product_tasks']['parameters']['query']] extends [never] ? {} : NonNullable) export type SummaryResponse = Data.components['schemas']['DataProductTaskResponse'] export type DetailResponse = Data.components['schemas']['ItemResponse_DataProductTaskResponse_'] diff --git a/packages/hydroserver-ts/src/generated/contracts/data-product-transformations.contract.ts b/packages/hydroserver-ts/src/generated/contracts/data-product-transformations.contract.ts index 19048e09..676f3f62 100644 --- a/packages/hydroserver-ts/src/generated/contracts/data-product-transformations.contract.ts +++ b/packages/hydroserver-ts/src/generated/contracts/data-product-transformations.contract.ts @@ -1,9 +1,9 @@ /* AUTO-GENERATED. DO NOT EDIT. - Generated from ../../django/contracts/openapi/data.openapi.json */ -import type * as Data from '../data.types' + Generated from ../../django/contracts/openapi/ogc.openapi.json */ +import type * as Data from '../ogc.types' export namespace DataProductTransformationContract { - export const route = 'data-product-transformations' as const + export const route = 'collections/data-product-transformations/items' as const export type QueryParameters = ([Data.operations['interfaces_api_views_products_transformation_get_data_product_transformations']['parameters']['query']] extends [never] ? {} : NonNullable) export type SummaryResponse = Data.components['schemas']['DataProductTransformationResponse'] export type DetailResponse = Data.components['schemas']['ItemResponse_DataProductTransformationResponse_'] diff --git a/packages/hydroserver-ts/src/generated/contracts/datastream-statuses.contract.ts b/packages/hydroserver-ts/src/generated/contracts/datastream-statuses.contract.ts index 9b6d3bb4..be57d82f 100644 --- a/packages/hydroserver-ts/src/generated/contracts/datastream-statuses.contract.ts +++ b/packages/hydroserver-ts/src/generated/contracts/datastream-statuses.contract.ts @@ -1,9 +1,9 @@ /* AUTO-GENERATED. DO NOT EDIT. - Generated from ../../django/contracts/openapi/data.openapi.json */ -import type * as Data from '../data.types' + Generated from ../../django/contracts/openapi/ogc.openapi.json */ +import type * as Data from '../ogc.types' export namespace DatastreamStatusContract { - export const route = 'datastream-statuses' as const + export const route = 'collections/datastream-statuses/items' as const export type QueryParameters = ([Data.operations['interfaces_api_views_sta_datastream_status_get_datastream_statuses']['parameters']['query']] extends [never] ? {} : NonNullable) export type SummaryResponse = Data.components['schemas']['DatastreamStatusResponse'] export type DetailResponse = Data.components['schemas']['ItemResponse_DatastreamStatusResponse_'] diff --git a/packages/hydroserver-ts/src/generated/contracts/datastreams.contract.ts b/packages/hydroserver-ts/src/generated/contracts/datastreams.contract.ts index b0e7efe7..4d481a7c 100644 --- a/packages/hydroserver-ts/src/generated/contracts/datastreams.contract.ts +++ b/packages/hydroserver-ts/src/generated/contracts/datastreams.contract.ts @@ -1,16 +1,16 @@ /* AUTO-GENERATED. DO NOT EDIT. - Generated from ../../django/contracts/openapi/data.openapi.json */ -import type * as Data from '../data.types' + Generated from ../../django/contracts/openapi/ogc.openapi.json */ +import type * as Data from '../ogc.types' export namespace DatastreamContract { - export const route = 'datastreams' as const + export const route = 'collections/datastreams/items' as const export type QueryParameters = ([Data.operations['interfaces_api_views_sta_datastream_get_datastreams']['parameters']['query']] extends [never] ? {} : NonNullable) export type SummaryResponse = Data.components['schemas']['DatastreamResponse'] export type DetailResponse = Data.components['schemas']['ItemResponse_DatastreamResponse_'] export type PostBody = Data.components['schemas']['DatastreamPostBody'] export type PatchBody = Data.components['schemas']['DatastreamPatchBody'] export type DeleteBody = never - export const writableKeys = ["aggregationStatistic","description","intendedTimeSpacing","intendedTimeSpacingUnit","isPrivate","isVisible","methodId","monitoringSiteId","name","noDataValue","observationType","observedPropertyId","phenomenonBeginTime","phenomenonEndTime","processingLevelId","resultBeginTime","resultEndTime","resultType","sampledMedium","status","tags","timeAggregationInterval","timeAggregationIntervalUnit","unitId","valueCount"] as const + export const writableKeys = ["aggregationStatistic","description","intendedTimeSpacing","intendedTimeSpacingUnit","isPrivate","isVisible","methodId","monitoringSiteId","name","noDataValue","observationType","observedPropertyId","processingLevelId","resultType","sampledMedium","status","tags","timeAggregationInterval","timeAggregationIntervalUnit","unitId"] as const export declare const __types: { SummaryResponse: SummaryResponse DetailResponse: DetailResponse diff --git a/packages/hydroserver-ts/src/generated/contracts/etl-mappings.contract.ts b/packages/hydroserver-ts/src/generated/contracts/etl-mappings.contract.ts index e58ecc6a..56fa6e59 100644 --- a/packages/hydroserver-ts/src/generated/contracts/etl-mappings.contract.ts +++ b/packages/hydroserver-ts/src/generated/contracts/etl-mappings.contract.ts @@ -1,9 +1,9 @@ /* AUTO-GENERATED. DO NOT EDIT. - Generated from ../../django/contracts/openapi/data.openapi.json */ -import type * as Data from '../data.types' + Generated from ../../django/contracts/openapi/ogc.openapi.json */ +import type * as Data from '../ogc.types' export namespace EtlMappingContract { - export const route = 'etl-mappings' as const + export const route = 'collections/etl-mappings/items' as const export type QueryParameters = ([Data.operations['interfaces_api_views_etl_mapping_get_etl_mappings']['parameters']['query']] extends [never] ? {} : NonNullable) export type SummaryResponse = Data.components['schemas']['EtlMappingResponse'] export type DetailResponse = Data.components['schemas']['ItemResponse_EtlMappingResponse_'] diff --git a/packages/hydroserver-ts/src/generated/contracts/linked-resource-types.contract.ts b/packages/hydroserver-ts/src/generated/contracts/linked-resource-types.contract.ts index d2468bdf..cfab9c09 100644 --- a/packages/hydroserver-ts/src/generated/contracts/linked-resource-types.contract.ts +++ b/packages/hydroserver-ts/src/generated/contracts/linked-resource-types.contract.ts @@ -1,9 +1,9 @@ /* AUTO-GENERATED. DO NOT EDIT. - Generated from ../../django/contracts/openapi/data.openapi.json */ -import type * as Data from '../data.types' + Generated from ../../django/contracts/openapi/ogc.openapi.json */ +import type * as Data from '../ogc.types' export namespace LinkedResourceTypeContract { - export const route = 'linked-resource-types' as const + export const route = 'collections/linked-resource-types/items' as const export type QueryParameters = ([Data.operations['interfaces_api_views_sta_linked_resource_type_get_linked_resource_types']['parameters']['query']] extends [never] ? {} : NonNullable) export type SummaryResponse = Data.components['schemas']['LinkedResourceTypeResponse'] export type DetailResponse = Data.components['schemas']['ItemResponse_LinkedResourceTypeResponse_'] diff --git a/packages/hydroserver-ts/src/generated/contracts/method-types.contract.ts b/packages/hydroserver-ts/src/generated/contracts/method-types.contract.ts index bbd14acf..3e0a14f2 100644 --- a/packages/hydroserver-ts/src/generated/contracts/method-types.contract.ts +++ b/packages/hydroserver-ts/src/generated/contracts/method-types.contract.ts @@ -1,9 +1,9 @@ /* AUTO-GENERATED. DO NOT EDIT. - Generated from ../../django/contracts/openapi/data.openapi.json */ -import type * as Data from '../data.types' + Generated from ../../django/contracts/openapi/ogc.openapi.json */ +import type * as Data from '../ogc.types' export namespace MethodTypeContract { - export const route = 'method-types' as const + export const route = 'collections/method-types/items' as const export type QueryParameters = ([Data.operations['interfaces_api_views_sta_method_type_get_method_types']['parameters']['query']] extends [never] ? {} : NonNullable) export type SummaryResponse = Data.components['schemas']['MethodTypeResponse'] export type DetailResponse = Data.components['schemas']['ItemResponse_MethodTypeResponse_'] diff --git a/packages/hydroserver-ts/src/generated/contracts/methods.contract.ts b/packages/hydroserver-ts/src/generated/contracts/methods.contract.ts index 3370f9a0..3c128d59 100644 --- a/packages/hydroserver-ts/src/generated/contracts/methods.contract.ts +++ b/packages/hydroserver-ts/src/generated/contracts/methods.contract.ts @@ -1,9 +1,9 @@ /* AUTO-GENERATED. DO NOT EDIT. - Generated from ../../django/contracts/openapi/data.openapi.json */ -import type * as Data from '../data.types' + Generated from ../../django/contracts/openapi/ogc.openapi.json */ +import type * as Data from '../ogc.types' export namespace MethodContract { - export const route = 'methods' as const + export const route = 'collections/methods/items' as const export type QueryParameters = ([Data.operations['interfaces_api_views_sta_method_get_methods']['parameters']['query']] extends [never] ? {} : NonNullable) export type SummaryResponse = Data.components['schemas']['MethodResponse'] export type DetailResponse = Data.components['schemas']['ItemResponse_MethodResponse_'] diff --git a/packages/hydroserver-ts/src/generated/contracts/monitoring-rules.contract.ts b/packages/hydroserver-ts/src/generated/contracts/monitoring-rules.contract.ts index 6e8cffa8..9637f8c8 100644 --- a/packages/hydroserver-ts/src/generated/contracts/monitoring-rules.contract.ts +++ b/packages/hydroserver-ts/src/generated/contracts/monitoring-rules.contract.ts @@ -1,9 +1,9 @@ /* AUTO-GENERATED. DO NOT EDIT. - Generated from ../../django/contracts/openapi/data.openapi.json */ -import type * as Data from '../data.types' + Generated from ../../django/contracts/openapi/ogc.openapi.json */ +import type * as Data from '../ogc.types' export namespace MonitoringRuleContract { - export const route = 'monitoring-rules' as const + export const route = 'collections/monitoring-rules/items' as const export type QueryParameters = ([Data.operations['interfaces_api_views_monitoring_rule_get_monitoring_rules']['parameters']['query']] extends [never] ? {} : NonNullable) export type SummaryResponse = Data.components['schemas']['MonitoringRuleResponse'] export type DetailResponse = Data.components['schemas']['ItemResponse_MonitoringRuleResponse_'] diff --git a/packages/hydroserver-ts/src/generated/contracts/monitoring-site-types.contract.ts b/packages/hydroserver-ts/src/generated/contracts/monitoring-site-types.contract.ts index 518686e0..5ef9c984 100644 --- a/packages/hydroserver-ts/src/generated/contracts/monitoring-site-types.contract.ts +++ b/packages/hydroserver-ts/src/generated/contracts/monitoring-site-types.contract.ts @@ -1,9 +1,9 @@ /* AUTO-GENERATED. DO NOT EDIT. - Generated from ../../django/contracts/openapi/data.openapi.json */ -import type * as Data from '../data.types' + Generated from ../../django/contracts/openapi/ogc.openapi.json */ +import type * as Data from '../ogc.types' export namespace MonitoringSiteTypeContract { - export const route = 'monitoring-site-types' as const + export const route = 'collections/monitoring-site-types/items' as const export type QueryParameters = ([Data.operations['interfaces_api_views_sta_monitoring_site_type_get_monitoring_site_types']['parameters']['query']] extends [never] ? {} : NonNullable) export type SummaryResponse = Data.components['schemas']['MonitoringSiteTypeResponse'] export type DetailResponse = Data.components['schemas']['ItemResponse_MonitoringSiteTypeResponse_'] diff --git a/packages/hydroserver-ts/src/generated/contracts/monitoring-sites.contract.ts b/packages/hydroserver-ts/src/generated/contracts/monitoring-sites.contract.ts index 716d13ed..b8f29f55 100644 --- a/packages/hydroserver-ts/src/generated/contracts/monitoring-sites.contract.ts +++ b/packages/hydroserver-ts/src/generated/contracts/monitoring-sites.contract.ts @@ -1,9 +1,9 @@ /* AUTO-GENERATED. DO NOT EDIT. - Generated from ../../django/contracts/openapi/data.openapi.json */ -import type * as Data from '../data.types' + Generated from ../../django/contracts/openapi/ogc.openapi.json */ +import type * as Data from '../ogc.types' export namespace MonitoringSiteContract { - export const route = 'monitoring-sites' as const + export const route = 'collections/monitoring-sites/items' as const export type QueryParameters = ([Data.operations['interfaces_api_views_sta_monitoring_site_get_monitoring_sites']['parameters']['query']] extends [never] ? {} : NonNullable) export type SummaryResponse = Data.components['schemas']['MonitoringSiteResponse'] export type DetailResponse = Data.components['schemas']['ItemResponse_MonitoringSiteResponse_'] diff --git a/packages/hydroserver-ts/src/generated/contracts/monitoring-tasks.contract.ts b/packages/hydroserver-ts/src/generated/contracts/monitoring-tasks.contract.ts index aeab264b..41af9a76 100644 --- a/packages/hydroserver-ts/src/generated/contracts/monitoring-tasks.contract.ts +++ b/packages/hydroserver-ts/src/generated/contracts/monitoring-tasks.contract.ts @@ -1,9 +1,9 @@ /* AUTO-GENERATED. DO NOT EDIT. - Generated from ../../django/contracts/openapi/data.openapi.json */ -import type * as Data from '../data.types' + Generated from ../../django/contracts/openapi/ogc.openapi.json */ +import type * as Data from '../ogc.types' export namespace MonitoringTaskContract { - export const route = 'monitoring-tasks' as const + export const route = 'collections/monitoring-tasks/items' as const export type QueryParameters = ([Data.operations['interfaces_api_views_monitoring_task_get_monitoring_tasks']['parameters']['query']] extends [never] ? {} : NonNullable) export type SummaryResponse = Data.components['schemas']['MonitoringTaskResponse'] export type DetailResponse = Data.components['schemas']['ItemResponse_MonitoringTaskResponse_'] diff --git a/packages/hydroserver-ts/src/generated/contracts/observations.contract.ts b/packages/hydroserver-ts/src/generated/contracts/observations.contract.ts index d7a2e7bf..3ab68fa8 100644 --- a/packages/hydroserver-ts/src/generated/contracts/observations.contract.ts +++ b/packages/hydroserver-ts/src/generated/contracts/observations.contract.ts @@ -1,9 +1,9 @@ /* AUTO-GENERATED. DO NOT EDIT. - Generated from ../../django/contracts/openapi/data.openapi.json */ -import type * as Data from '../data.types' + Generated from ../../django/contracts/openapi/ogc.openapi.json */ +import type * as Data from '../ogc.types' export namespace ObservationContract { - export const route = 'observations' as const + export const route = 'collections/observations/items' as const export type QueryParameters = ([Data.operations['interfaces_api_views_sta_observation_get_observations']['parameters']['query']] extends [never] ? {} : NonNullable) export type SummaryResponse = never export type DetailResponse = Data.components['schemas']['ItemResponse_ObservationResponse_'] diff --git a/packages/hydroserver-ts/src/generated/contracts/observed-properties.contract.ts b/packages/hydroserver-ts/src/generated/contracts/observed-properties.contract.ts index 1a31c75b..d5400377 100644 --- a/packages/hydroserver-ts/src/generated/contracts/observed-properties.contract.ts +++ b/packages/hydroserver-ts/src/generated/contracts/observed-properties.contract.ts @@ -1,9 +1,9 @@ /* AUTO-GENERATED. DO NOT EDIT. - Generated from ../../django/contracts/openapi/data.openapi.json */ -import type * as Data from '../data.types' + Generated from ../../django/contracts/openapi/ogc.openapi.json */ +import type * as Data from '../ogc.types' export namespace ObservedPropertyContract { - export const route = 'observed-properties' as const + export const route = 'collections/observed-properties/items' as const export type QueryParameters = ([Data.operations['interfaces_api_views_sta_observed_property_get_observed_properties']['parameters']['query']] extends [never] ? {} : NonNullable) export type SummaryResponse = Data.components['schemas']['ObservedPropertyResponse'] export type DetailResponse = Data.components['schemas']['ItemResponse_ObservedPropertyResponse_'] diff --git a/packages/hydroserver-ts/src/generated/contracts/observed-property-types.contract.ts b/packages/hydroserver-ts/src/generated/contracts/observed-property-types.contract.ts index 468ed54a..6468dee2 100644 --- a/packages/hydroserver-ts/src/generated/contracts/observed-property-types.contract.ts +++ b/packages/hydroserver-ts/src/generated/contracts/observed-property-types.contract.ts @@ -1,9 +1,9 @@ /* AUTO-GENERATED. DO NOT EDIT. - Generated from ../../django/contracts/openapi/data.openapi.json */ -import type * as Data from '../data.types' + Generated from ../../django/contracts/openapi/ogc.openapi.json */ +import type * as Data from '../ogc.types' export namespace ObservedPropertyTypeContract { - export const route = 'observed-property-types' as const + export const route = 'collections/observed-property-types/items' as const export type QueryParameters = ([Data.operations['interfaces_api_views_sta_observed_property_type_get_observed_property_types']['parameters']['query']] extends [never] ? {} : NonNullable) export type SummaryResponse = Data.components['schemas']['ObservedPropertyTypeResponse'] export type DetailResponse = Data.components['schemas']['ItemResponse_ObservedPropertyTypeResponse_'] diff --git a/packages/hydroserver-ts/src/generated/contracts/processing-levels.contract.ts b/packages/hydroserver-ts/src/generated/contracts/processing-levels.contract.ts index f2d7f789..5715d4ab 100644 --- a/packages/hydroserver-ts/src/generated/contracts/processing-levels.contract.ts +++ b/packages/hydroserver-ts/src/generated/contracts/processing-levels.contract.ts @@ -1,9 +1,9 @@ /* AUTO-GENERATED. DO NOT EDIT. - Generated from ../../django/contracts/openapi/data.openapi.json */ -import type * as Data from '../data.types' + Generated from ../../django/contracts/openapi/ogc.openapi.json */ +import type * as Data from '../ogc.types' export namespace ProcessingLevelContract { - export const route = 'processing-levels' as const + export const route = 'collections/processing-levels/items' as const export type QueryParameters = ([Data.operations['interfaces_api_views_sta_processing_level_get_processing_levels']['parameters']['query']] extends [never] ? {} : NonNullable) export type SummaryResponse = Data.components['schemas']['ProcessingLevelResponse'] export type DetailResponse = Data.components['schemas']['ItemResponse_ProcessingLevelResponse_'] diff --git a/packages/hydroserver-ts/src/generated/contracts/quality-control-histories.contract.ts b/packages/hydroserver-ts/src/generated/contracts/quality-control-histories.contract.ts index 14777a2d..2e46bfc9 100644 --- a/packages/hydroserver-ts/src/generated/contracts/quality-control-histories.contract.ts +++ b/packages/hydroserver-ts/src/generated/contracts/quality-control-histories.contract.ts @@ -1,9 +1,9 @@ /* AUTO-GENERATED. DO NOT EDIT. - Generated from ../../django/contracts/openapi/data.openapi.json */ -import type * as Data from '../data.types' + Generated from ../../django/contracts/openapi/ogc.openapi.json */ +import type * as Data from '../ogc.types' export namespace QualityControlHistoryContract { - export const route = 'quality-control/histories' as const + export const route = 'collections/quality-control-histories/items' as const export type QueryParameters = ([Data.operations['interfaces_api_views_quality_history_get_qc_histories']['parameters']['query']] extends [never] ? {} : NonNullable) export type SummaryResponse = Data.components['schemas']['QualityControlHistoryResponse'] export type DetailResponse = Data.components['schemas']['ItemResponse_QualityControlHistoryResponse_'] diff --git a/packages/hydroserver-ts/src/generated/contracts/quality-control-operations.contract.ts b/packages/hydroserver-ts/src/generated/contracts/quality-control-operations.contract.ts index 27b0042d..fa5346dd 100644 --- a/packages/hydroserver-ts/src/generated/contracts/quality-control-operations.contract.ts +++ b/packages/hydroserver-ts/src/generated/contracts/quality-control-operations.contract.ts @@ -1,9 +1,9 @@ /* AUTO-GENERATED. DO NOT EDIT. - Generated from ../../django/contracts/openapi/data.openapi.json */ -import type * as Data from '../data.types' + Generated from ../../django/contracts/openapi/ogc.openapi.json */ +import type * as Data from '../ogc.types' export namespace QualityControlOperationContract { - export const route = 'quality-control/histories/{history_id}/sessions/{session_id}/operations' as const + export const route = 'collections/quality-control-histories/items/{history_id}/sessions/{session_id}/operations' as const export type QueryParameters = ([Data.operations['interfaces_api_views_quality_operation_get_qc_operations']['parameters']['query']] extends [never] ? {} : NonNullable) export type SummaryResponse = Data.components['schemas']['QualityControlOperationResponse'] export type DetailResponse = Data.components['schemas']['ItemResponse_QualityControlOperationResponse_'] diff --git a/packages/hydroserver-ts/src/generated/contracts/quality-control-sessions.contract.ts b/packages/hydroserver-ts/src/generated/contracts/quality-control-sessions.contract.ts index aad6d543..b1876791 100644 --- a/packages/hydroserver-ts/src/generated/contracts/quality-control-sessions.contract.ts +++ b/packages/hydroserver-ts/src/generated/contracts/quality-control-sessions.contract.ts @@ -1,9 +1,9 @@ /* AUTO-GENERATED. DO NOT EDIT. - Generated from ../../django/contracts/openapi/data.openapi.json */ -import type * as Data from '../data.types' + Generated from ../../django/contracts/openapi/ogc.openapi.json */ +import type * as Data from '../ogc.types' export namespace QualityControlSessionContract { - export const route = 'quality-control/histories/{history_id}/sessions' as const + export const route = 'collections/quality-control-histories/items/{history_id}/sessions' as const export type QueryParameters = ([Data.operations['interfaces_api_views_quality_session_get_qc_sessions']['parameters']['query']] extends [never] ? {} : NonNullable) export type SummaryResponse = Data.components['schemas']['QualityControlSessionResponse'] export type DetailResponse = Data.components['schemas']['ItemResponse_QualityControlSessionResponse_'] diff --git a/packages/hydroserver-ts/src/generated/contracts/rating-curves.contract.ts b/packages/hydroserver-ts/src/generated/contracts/rating-curves.contract.ts index 9dfe665e..29f48ffa 100644 --- a/packages/hydroserver-ts/src/generated/contracts/rating-curves.contract.ts +++ b/packages/hydroserver-ts/src/generated/contracts/rating-curves.contract.ts @@ -1,9 +1,9 @@ /* AUTO-GENERATED. DO NOT EDIT. - Generated from ../../django/contracts/openapi/data.openapi.json */ -import type * as Data from '../data.types' + Generated from ../../django/contracts/openapi/ogc.openapi.json */ +import type * as Data from '../ogc.types' export namespace RatingCurveContract { - export const route = 'data-product-rating-curves' as const + export const route = 'collections/data-product-rating-curves/items' as const export type QueryParameters = ([Data.operations['interfaces_api_views_products_rating_curve_get_rating_curves']['parameters']['query']] extends [never] ? {} : NonNullable) export type SummaryResponse = Data.components['schemas']['RatingCurveResponse'] export type DetailResponse = Data.components['schemas']['ItemResponse_RatingCurveResponse_'] diff --git a/packages/hydroserver-ts/src/generated/contracts/result-qualifiers.contract.ts b/packages/hydroserver-ts/src/generated/contracts/result-qualifiers.contract.ts index b995d654..6f939f2f 100644 --- a/packages/hydroserver-ts/src/generated/contracts/result-qualifiers.contract.ts +++ b/packages/hydroserver-ts/src/generated/contracts/result-qualifiers.contract.ts @@ -1,9 +1,9 @@ /* AUTO-GENERATED. DO NOT EDIT. - Generated from ../../django/contracts/openapi/data.openapi.json */ -import type * as Data from '../data.types' + Generated from ../../django/contracts/openapi/ogc.openapi.json */ +import type * as Data from '../ogc.types' export namespace ResultQualifierContract { - export const route = 'result-qualifiers' as const + export const route = 'collections/result-qualifiers/items' as const export type QueryParameters = ([Data.operations['interfaces_api_views_sta_result_qualifier_get_result_qualifiers']['parameters']['query']] extends [never] ? {} : NonNullable) export type SummaryResponse = Data.components['schemas']['ResultQualifierResponse'] export type DetailResponse = Data.components['schemas']['ItemResponse_ResultQualifierResponse_'] diff --git a/packages/hydroserver-ts/src/generated/contracts/runs.contract.ts b/packages/hydroserver-ts/src/generated/contracts/runs.contract.ts index 4a1f4a2d..3fdd2cbf 100644 --- a/packages/hydroserver-ts/src/generated/contracts/runs.contract.ts +++ b/packages/hydroserver-ts/src/generated/contracts/runs.contract.ts @@ -1,12 +1,12 @@ /* AUTO-GENERATED. DO NOT EDIT. - Generated from ../../django/contracts/openapi/data.openapi.json */ -import type * as Data from '../data.types' + Generated from ../../django/contracts/openapi/ogc.openapi.json */ +import type * as Data from '../ogc.types' export namespace RunContract { export const route = 'runs' as const export type QueryParameters = ([Data.operations['interfaces_api_views_products_task_get_data_product_task_runs']['parameters']['query']] extends [never] ? {} : NonNullable) export type SummaryResponse = Data.components['schemas']['TaskRunResponse'] - export type DetailResponse = Data.components['schemas']['TaskRunResponse'] + export type DetailResponse = Data.components['schemas']['ItemResponse_TaskRunResponse_'] export type PostBody = never export type PatchBody = never export type DeleteBody = never diff --git a/packages/hydroserver-ts/src/generated/contracts/sampled-mediums.contract.ts b/packages/hydroserver-ts/src/generated/contracts/sampled-mediums.contract.ts index a2e62b5b..c3e56543 100644 --- a/packages/hydroserver-ts/src/generated/contracts/sampled-mediums.contract.ts +++ b/packages/hydroserver-ts/src/generated/contracts/sampled-mediums.contract.ts @@ -1,9 +1,9 @@ /* AUTO-GENERATED. DO NOT EDIT. - Generated from ../../django/contracts/openapi/data.openapi.json */ -import type * as Data from '../data.types' + Generated from ../../django/contracts/openapi/ogc.openapi.json */ +import type * as Data from '../ogc.types' export namespace SampledMediumContract { - export const route = 'sampled-mediums' as const + export const route = 'collections/sampled-mediums/items' as const export type QueryParameters = ([Data.operations['interfaces_api_views_sta_sampled_medium_get_sampled_mediums']['parameters']['query']] extends [never] ? {} : NonNullable) export type SummaryResponse = Data.components['schemas']['SampledMediumResponse'] export type DetailResponse = Data.components['schemas']['ItemResponse_SampledMediumResponse_'] diff --git a/packages/hydroserver-ts/src/generated/contracts/tasks.contract.ts b/packages/hydroserver-ts/src/generated/contracts/tasks.contract.ts index 81c2279f..f1be3762 100644 --- a/packages/hydroserver-ts/src/generated/contracts/tasks.contract.ts +++ b/packages/hydroserver-ts/src/generated/contracts/tasks.contract.ts @@ -1,9 +1,9 @@ /* AUTO-GENERATED. DO NOT EDIT. - Generated from ../../django/contracts/openapi/data.openapi.json */ -import type * as Data from '../data.types' + Generated from ../../django/contracts/openapi/ogc.openapi.json */ +import type * as Data from '../ogc.types' export namespace TaskContract { - export const route = 'etl-tasks' as const + export const route = 'collections/etl-tasks/items' as const export type QueryParameters = ([Data.operations['interfaces_api_views_etl_task_get_etl_tasks']['parameters']['query']] extends [never] ? {} : NonNullable) export type SummaryResponse = Data.components['schemas']['EtlTaskResponse'] export type DetailResponse = Data.components['schemas']['ItemResponse_EtlTaskResponse_'] diff --git a/packages/hydroserver-ts/src/generated/contracts/unit-types.contract.ts b/packages/hydroserver-ts/src/generated/contracts/unit-types.contract.ts index f99a1a1f..f71040d1 100644 --- a/packages/hydroserver-ts/src/generated/contracts/unit-types.contract.ts +++ b/packages/hydroserver-ts/src/generated/contracts/unit-types.contract.ts @@ -1,9 +1,9 @@ /* AUTO-GENERATED. DO NOT EDIT. - Generated from ../../django/contracts/openapi/data.openapi.json */ -import type * as Data from '../data.types' + Generated from ../../django/contracts/openapi/ogc.openapi.json */ +import type * as Data from '../ogc.types' export namespace UnitTypeContract { - export const route = 'unit-types' as const + export const route = 'collections/unit-types/items' as const export type QueryParameters = ([Data.operations['interfaces_api_views_sta_unit_type_get_unit_types']['parameters']['query']] extends [never] ? {} : NonNullable) export type SummaryResponse = Data.components['schemas']['UnitTypeResponse'] export type DetailResponse = Data.components['schemas']['ItemResponse_UnitTypeResponse_'] diff --git a/packages/hydroserver-ts/src/generated/contracts/units.contract.ts b/packages/hydroserver-ts/src/generated/contracts/units.contract.ts index 16070bd4..ea4da748 100644 --- a/packages/hydroserver-ts/src/generated/contracts/units.contract.ts +++ b/packages/hydroserver-ts/src/generated/contracts/units.contract.ts @@ -1,9 +1,9 @@ /* AUTO-GENERATED. DO NOT EDIT. - Generated from ../../django/contracts/openapi/data.openapi.json */ -import type * as Data from '../data.types' + Generated from ../../django/contracts/openapi/ogc.openapi.json */ +import type * as Data from '../ogc.types' export namespace UnitContract { - export const route = 'units' as const + export const route = 'collections/units/items' as const export type QueryParameters = ([Data.operations['interfaces_api_views_sta_unit_get_units']['parameters']['query']] extends [never] ? {} : NonNullable) export type SummaryResponse = Data.components['schemas']['UnitResponse'] export type DetailResponse = Data.components['schemas']['ItemResponse_UnitResponse_'] diff --git a/packages/hydroserver-ts/src/generated/contracts/workspaces.contract.ts b/packages/hydroserver-ts/src/generated/contracts/workspaces.contract.ts index 816e8702..c6f8d398 100644 --- a/packages/hydroserver-ts/src/generated/contracts/workspaces.contract.ts +++ b/packages/hydroserver-ts/src/generated/contracts/workspaces.contract.ts @@ -1,9 +1,9 @@ /* AUTO-GENERATED. DO NOT EDIT. - Generated from ../../django/contracts/openapi/data.openapi.json */ -import type * as Data from '../data.types' + Generated from ../../django/contracts/openapi/ogc.openapi.json */ +import type * as Data from '../ogc.types' export namespace WorkspaceContract { - export const route = 'workspaces' as const + export const route = 'collections/workspaces/items' as const export type QueryParameters = ([Data.operations['interfaces_api_views_iam_workspace_get_workspaces']['parameters']['query']] extends [never] ? {} : NonNullable) export type SummaryResponse = Data.components['schemas']['WorkspaceResponse'] export type DetailResponse = Data.components['schemas']['ItemResponse_WorkspaceResponse_'] diff --git a/packages/hydroserver-ts/src/generated/data.types.d.ts b/packages/hydroserver-ts/src/generated/ogc.types.d.ts similarity index 88% rename from packages/hydroserver-ts/src/generated/data.types.d.ts rename to packages/hydroserver-ts/src/generated/ogc.types.d.ts index 4cc43147..38f336d3 100644 --- a/packages/hydroserver-ts/src/generated/data.types.d.ts +++ b/packages/hydroserver-ts/src/generated/ogc.types.d.ts @@ -4,7 +4,48 @@ */ export interface paths { - "/api/data/aggregation-statistics": { + "/api/ogc/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Landing Page + * @description Get the API's landing page, which links to the API definition, conformance declaration, and + * collections. + */ + get: operations["interfaces_api_views_ogc_get_landing_page"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/ogc/collections": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Collections + * @description Get the collections the API serves. + */ + get: operations["interfaces_api_views_ogc_get_collections"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/ogc/collections/aggregation-statistics/items": { parameters: { query?: never; header?: never; @@ -28,7 +69,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/aggregation-statistics/{aggregation_statistic_id}": { + "/api/ogc/collections/aggregation-statistics/items/{aggregation_statistic_id}": { parameters: { query?: never; header?: never; @@ -56,7 +97,7 @@ export interface paths { patch: operations["interfaces_api_views_sta_aggregation_statistic_update_aggregation_statistic"]; trace?: never; }; - "/api/data/data-product-rating-curves": { + "/api/ogc/collections/data-product-rating-curves/items": { parameters: { query?: never; header?: never; @@ -80,7 +121,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/data-product-rating-curves/{rating_curve_id}": { + "/api/ogc/collections/data-product-rating-curves/items/{rating_curve_id}": { parameters: { query?: never; header?: never; @@ -108,7 +149,7 @@ export interface paths { patch: operations["interfaces_api_views_products_rating_curve_update_rating_curve"]; trace?: never; }; - "/api/data/data-product-tasks": { + "/api/ogc/collections/data-product-tasks/items": { parameters: { query?: never; header?: never; @@ -132,7 +173,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/data-product-tasks/{task_id}": { + "/api/ogc/collections/data-product-tasks/items/{task_id}": { parameters: { query?: never; header?: never; @@ -160,7 +201,7 @@ export interface paths { patch: operations["interfaces_api_views_products_task_update_data_product_task"]; trace?: never; }; - "/api/data/data-product-tasks/{task_id}/runs": { + "/api/ogc/collections/data-product-tasks/items/{task_id}/runs": { parameters: { query?: never; header?: never; @@ -180,7 +221,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/data-product-tasks/{task_id}/runs/{run_id}": { + "/api/ogc/collections/data-product-tasks/items/{task_id}/runs/{run_id}": { parameters: { query?: never; header?: never; @@ -200,7 +241,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/data-product-tasks/{task_id}/trigger": { + "/api/ogc/collections/data-product-tasks/items/{task_id}/trigger": { parameters: { query?: never; header?: never; @@ -220,7 +261,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/data-product-transformations": { + "/api/ogc/collections/data-product-transformations/items": { parameters: { query?: never; header?: never; @@ -244,7 +285,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/data-product-transformations/{transformation_id}": { + "/api/ogc/collections/data-product-transformations/items/{transformation_id}": { parameters: { query?: never; header?: never; @@ -272,7 +313,7 @@ export interface paths { patch: operations["interfaces_api_views_products_transformation_update_data_product_transformation"]; trace?: never; }; - "/api/data/datastream-statuses": { + "/api/ogc/collections/datastream-statuses/items": { parameters: { query?: never; header?: never; @@ -296,7 +337,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/datastream-statuses/{datastream_status_id}": { + "/api/ogc/collections/datastream-statuses/items/{datastream_status_id}": { parameters: { query?: never; header?: never; @@ -324,7 +365,7 @@ export interface paths { patch: operations["interfaces_api_views_sta_datastream_status_update_datastream_status"]; trace?: never; }; - "/api/data/datastreams": { + "/api/ogc/collections/datastreams/items": { parameters: { query?: never; header?: never; @@ -348,47 +389,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/datastreams/tags/keys": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Datastream Tag Keys - * @description Get all existing unique datastream tag keys. - */ - get: operations["interfaces_api_views_sta_datastream_get_datastream_tag_keys"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/data/datastreams/visualization-bootstrap": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Datastream Visualization Bootstrap - * @description Get the lean metadata required to bootstrap the visualization page. - */ - get: operations["interfaces_api_views_sta_datastream_get_datastream_visualization_bootstrap"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/data/datastreams/{datastream_id}": { + "/api/ogc/collections/datastreams/items/{datastream_id}": { parameters: { query?: never; header?: never; @@ -416,7 +417,7 @@ export interface paths { patch: operations["interfaces_api_views_sta_datastream_update_datastream"]; trace?: never; }; - "/api/data/datastreams/{datastream_id}/csv": { + "/api/ogc/collections/datastreams/items/{datastream_id}/csv": { parameters: { query?: never; header?: never; @@ -436,7 +437,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/datastreams/{datastream_id}/linked-resources": { + "/api/ogc/collections/datastreams/items/{datastream_id}/linked-resources": { parameters: { query?: never; header?: never; @@ -460,14 +461,18 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/datastreams/{datastream_id}/linked-resources/{linked_resource_id}": { + "/api/ogc/collections/datastreams/items/{datastream_id}/linked-resources/{linked_resource_id}": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; + /** + * Get Datastream Linked Resource + * @description Get a linked resource associated with a Datastream. + */ + get: operations["interfaces_api_views_sta_datastream_get_datastream_linked_resource"]; put?: never; post?: never; /** @@ -485,7 +490,47 @@ export interface paths { patch: operations["interfaces_api_views_sta_datastream_update_datastream_linked_resource"]; trace?: never; }; - "/api/data/etl-data-connections": { + "/api/ogc/collections/datastreams/tags/keys": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Datastream Tag Keys + * @description Get all existing unique datastream tag keys. + */ + get: operations["interfaces_api_views_sta_datastream_get_datastream_tag_keys"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/ogc/collections/datastreams/visualization-bootstrap": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Datastream Visualization Bootstrap + * @description Get the lean metadata required to bootstrap the visualization page. + */ + get: operations["interfaces_api_views_sta_datastream_get_datastream_visualization_bootstrap"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/ogc/collections/etl-data-connections/items": { parameters: { query?: never; header?: never; @@ -509,7 +554,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/etl-data-connections/{data_connection_id}": { + "/api/ogc/collections/etl-data-connections/items/{data_connection_id}": { parameters: { query?: never; header?: never; @@ -537,7 +582,7 @@ export interface paths { patch: operations["interfaces_api_views_etl_data_connection_update_data_connection"]; trace?: never; }; - "/api/data/etl-mappings": { + "/api/ogc/collections/etl-mappings/items": { parameters: { query?: never; header?: never; @@ -561,7 +606,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/etl-mappings/{mapping_id}": { + "/api/ogc/collections/etl-mappings/items/{mapping_id}": { parameters: { query?: never; header?: never; @@ -589,7 +634,7 @@ export interface paths { patch: operations["interfaces_api_views_etl_mapping_update_etl_mapping"]; trace?: never; }; - "/api/data/etl-tasks": { + "/api/ogc/collections/etl-tasks/items": { parameters: { query?: never; header?: never; @@ -613,7 +658,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/etl-tasks/{task_id}": { + "/api/ogc/collections/etl-tasks/items/{task_id}": { parameters: { query?: never; header?: never; @@ -641,7 +686,7 @@ export interface paths { patch: operations["interfaces_api_views_etl_task_update_etl_task"]; trace?: never; }; - "/api/data/etl-tasks/{task_id}/runs": { + "/api/ogc/collections/etl-tasks/items/{task_id}/runs": { parameters: { query?: never; header?: never; @@ -661,7 +706,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/etl-tasks/{task_id}/runs/{run_id}": { + "/api/ogc/collections/etl-tasks/items/{task_id}/runs/{run_id}": { parameters: { query?: never; header?: never; @@ -681,7 +726,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/etl-tasks/{task_id}/trigger": { + "/api/ogc/collections/etl-tasks/items/{task_id}/trigger": { parameters: { query?: never; header?: never; @@ -701,7 +746,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/linked-resource-types": { + "/api/ogc/collections/linked-resource-types/items": { parameters: { query?: never; header?: never; @@ -725,7 +770,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/linked-resource-types/{linked_resource_type_id}": { + "/api/ogc/collections/linked-resource-types/items/{linked_resource_type_id}": { parameters: { query?: never; header?: never; @@ -753,7 +798,7 @@ export interface paths { patch: operations["interfaces_api_views_sta_linked_resource_type_update_linked_resource_type"]; trace?: never; }; - "/api/data/method-types": { + "/api/ogc/collections/method-types/items": { parameters: { query?: never; header?: never; @@ -777,7 +822,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/method-types/{method_type_id}": { + "/api/ogc/collections/method-types/items/{method_type_id}": { parameters: { query?: never; header?: never; @@ -805,7 +850,7 @@ export interface paths { patch: operations["interfaces_api_views_sta_method_type_update_method_type"]; trace?: never; }; - "/api/data/methods": { + "/api/ogc/collections/methods/items": { parameters: { query?: never; header?: never; @@ -829,7 +874,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/methods/{method_id}": { + "/api/ogc/collections/methods/items/{method_id}": { parameters: { query?: never; header?: never; @@ -857,7 +902,7 @@ export interface paths { patch: operations["interfaces_api_views_sta_method_update_method"]; trace?: never; }; - "/api/data/monitoring-rules": { + "/api/ogc/collections/monitoring-rules/items": { parameters: { query?: never; header?: never; @@ -881,7 +926,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/monitoring-rules/{rule_id}": { + "/api/ogc/collections/monitoring-rules/items/{rule_id}": { parameters: { query?: never; header?: never; @@ -909,7 +954,7 @@ export interface paths { patch: operations["interfaces_api_views_monitoring_rule_update_monitoring_rule"]; trace?: never; }; - "/api/data/monitoring-site-types": { + "/api/ogc/collections/monitoring-site-types/items": { parameters: { query?: never; header?: never; @@ -933,7 +978,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/monitoring-site-types/{monitoring_site_type_id}": { + "/api/ogc/collections/monitoring-site-types/items/{monitoring_site_type_id}": { parameters: { query?: never; header?: never; @@ -961,7 +1006,7 @@ export interface paths { patch: operations["interfaces_api_views_sta_monitoring_site_type_update_monitoring_site_type"]; trace?: never; }; - "/api/data/monitoring-sites": { + "/api/ogc/collections/monitoring-sites/items": { parameters: { query?: never; header?: never; @@ -985,7 +1030,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/monitoring-sites/markers": { + "/api/ogc/collections/monitoring-sites/items/{monitoring_site_id}": { parameters: { query?: never; header?: never; @@ -993,19 +1038,27 @@ export interface paths { cookie?: never; }; /** - * Get Monitoring Site Markers - * @description Get lean marker data for public MonitoringSites plus private MonitoringSites visible to the authenticated user. + * Get Monitoring Site + * @description Get a MonitoringSite. */ - get: operations["interfaces_api_views_sta_monitoring_site_get_monitoring_site_markers"]; + get: operations["interfaces_api_views_sta_monitoring_site_get_monitoring_site"]; put?: never; post?: never; - delete?: never; + /** + * Delete Monitoring Site + * @description Delete a MonitoringSite. + */ + delete: operations["interfaces_api_views_sta_monitoring_site_delete_monitoring_site"]; options?: never; head?: never; - patch?: never; + /** + * Update Monitoring Site + * @description Update a MonitoringSite. + */ + patch: operations["interfaces_api_views_sta_monitoring_site_update_monitoring_site"]; trace?: never; }; - "/api/data/monitoring-sites/site-summaries": { + "/api/ogc/collections/monitoring-sites/items/{monitoring_site_id}/linked-resources": { parameters: { query?: never; header?: never; @@ -1013,19 +1066,23 @@ export interface paths { cookie?: never; }; /** - * Get Monitoring Site Summaries - * @description Get lean site summary data for public MonitoringSites and MonitoringSites associated with the authenticated user. + * Get Monitoring Site Linked Resources + * @description Get all linked resources associated with a MonitoringSite. */ - get: operations["interfaces_api_views_sta_monitoring_site_get_monitoring_site_summaries"]; + get: operations["interfaces_api_views_sta_monitoring_site_get_monitoring_site_linked_resources"]; put?: never; - post?: never; + /** + * Add Monitoring Site Linked Resource + * @description Add a linked resource to a monitoring_site. + */ + post: operations["interfaces_api_views_sta_monitoring_site_add_monitoring_site_linked_resource"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/api/data/monitoring-sites/site-type-icons": { + "/api/ogc/collections/monitoring-sites/items/{monitoring_site_id}/linked-resources/{linked_resource_id}": { parameters: { query?: never; header?: never; @@ -1033,19 +1090,28 @@ export interface paths { cookie?: never; }; /** - * Get Site Type Icons - * @description Get the configured site type icon mappings. + * Get Monitoring Site Linked Resource + * @description Get a linked resource associated with a MonitoringSite. */ - get: operations["interfaces_api_views_sta_monitoring_site_get_site_type_icons"]; + get: operations["interfaces_api_views_sta_monitoring_site_get_monitoring_site_linked_resource"]; put?: never; post?: never; - delete?: never; + /** + * Remove Monitoring Site Linked Resource + * @description Remove a linked resource from a monitoring_site. + */ + delete: operations["interfaces_api_views_sta_monitoring_site_remove_monitoring_site_linked_resource"]; options?: never; head?: never; - patch?: never; + /** + * Update Monitoring Site Linked Resource + * @description Update a linked resource for a monitoring_site. A linked resource's mode (hosted file vs. + * external URL) cannot be changed in place — delete it and create a new one instead. + */ + patch: operations["interfaces_api_views_sta_monitoring_site_update_monitoring_site_linked_resource"]; trace?: never; }; - "/api/data/monitoring-sites/tags/keys": { + "/api/ogc/collections/monitoring-sites/markers": { parameters: { query?: never; header?: never; @@ -1053,10 +1119,10 @@ export interface paths { cookie?: never; }; /** - * Get Monitoring Site Tag Keys - * @description Get all existing unique monitoring_site tag keys. + * Get Monitoring Site Markers + * @description Get lean marker data for public MonitoringSites plus private MonitoringSites visible to the authenticated user. */ - get: operations["interfaces_api_views_sta_monitoring_site_get_monitoring_site_tag_keys"]; + get: operations["interfaces_api_views_sta_monitoring_site_get_monitoring_site_markers"]; put?: never; post?: never; delete?: never; @@ -1065,7 +1131,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/monitoring-sites/task-summaries": { + "/api/ogc/collections/monitoring-sites/site-summaries": { parameters: { query?: never; header?: never; @@ -1073,10 +1139,10 @@ export interface paths { cookie?: never; }; /** - * Get Monitoring Site Task Summaries - * @description Get task count summaries for MonitoringSites associated with the authenticated user. + * Get Monitoring Site Summaries + * @description Get lean site summary data for public MonitoringSites and MonitoringSites associated with the authenticated user. */ - get: operations["interfaces_api_views_sta_monitoring_site_get_monitoring_site_task_summaries"]; + get: operations["interfaces_api_views_sta_monitoring_site_get_monitoring_site_summaries"]; put?: never; post?: never; delete?: never; @@ -1085,7 +1151,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/monitoring-sites/{monitoring_site_id}": { + "/api/ogc/collections/monitoring-sites/site-type-icons": { parameters: { query?: never; header?: never; @@ -1093,27 +1159,19 @@ export interface paths { cookie?: never; }; /** - * Get Monitoring Site - * @description Get a MonitoringSite. + * Get Site Type Icons + * @description Get the configured site type icon mappings. */ - get: operations["interfaces_api_views_sta_monitoring_site_get_monitoring_site"]; + get: operations["interfaces_api_views_sta_monitoring_site_get_site_type_icons"]; put?: never; post?: never; - /** - * Delete Monitoring Site - * @description Delete a MonitoringSite. - */ - delete: operations["interfaces_api_views_sta_monitoring_site_delete_monitoring_site"]; + delete?: never; options?: never; head?: never; - /** - * Update Monitoring Site - * @description Update a MonitoringSite. - */ - patch: operations["interfaces_api_views_sta_monitoring_site_update_monitoring_site"]; + patch?: never; trace?: never; }; - "/api/data/monitoring-sites/{monitoring_site_id}/linked-resources": { + "/api/ogc/collections/monitoring-sites/tags/keys": { parameters: { query?: never; header?: never; @@ -1121,48 +1179,39 @@ export interface paths { cookie?: never; }; /** - * Get Monitoring Site Linked Resources - * @description Get all linked resources associated with a MonitoringSite. + * Get Monitoring Site Tag Keys + * @description Get all existing unique monitoring_site tag keys. */ - get: operations["interfaces_api_views_sta_monitoring_site_get_monitoring_site_linked_resources"]; + get: operations["interfaces_api_views_sta_monitoring_site_get_monitoring_site_tag_keys"]; put?: never; - /** - * Add Monitoring Site Linked Resource - * @description Add a linked resource to a monitoring_site. - */ - post: operations["interfaces_api_views_sta_monitoring_site_add_monitoring_site_linked_resource"]; + post?: never; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/api/data/monitoring-sites/{monitoring_site_id}/linked-resources/{linked_resource_id}": { + "/api/ogc/collections/monitoring-sites/task-summaries": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; - put?: never; - post?: never; /** - * Remove Monitoring Site Linked Resource - * @description Remove a linked resource from a monitoring_site. + * Get Monitoring Site Task Summaries + * @description Get task count summaries for MonitoringSites associated with the authenticated user. */ - delete: operations["interfaces_api_views_sta_monitoring_site_remove_monitoring_site_linked_resource"]; + get: operations["interfaces_api_views_sta_monitoring_site_get_monitoring_site_task_summaries"]; + put?: never; + post?: never; + delete?: never; options?: never; head?: never; - /** - * Update Monitoring Site Linked Resource - * @description Update a linked resource for a monitoring_site. A linked resource's mode (hosted file vs. - * external URL) cannot be changed in place — delete it and create a new one instead. - */ - patch: operations["interfaces_api_views_sta_monitoring_site_update_monitoring_site_linked_resource"]; + patch?: never; trace?: never; }; - "/api/data/monitoring-tasks": { + "/api/ogc/collections/monitoring-tasks/items": { parameters: { query?: never; header?: never; @@ -1186,7 +1235,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/monitoring-tasks/{task_id}": { + "/api/ogc/collections/monitoring-tasks/items/{task_id}": { parameters: { query?: never; header?: never; @@ -1214,7 +1263,7 @@ export interface paths { patch: operations["interfaces_api_views_monitoring_task_update_monitoring_task"]; trace?: never; }; - "/api/data/monitoring-tasks/{task_id}/runs": { + "/api/ogc/collections/monitoring-tasks/items/{task_id}/runs": { parameters: { query?: never; header?: never; @@ -1234,7 +1283,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/monitoring-tasks/{task_id}/runs/{run_id}": { + "/api/ogc/collections/monitoring-tasks/items/{task_id}/runs/{run_id}": { parameters: { query?: never; header?: never; @@ -1254,7 +1303,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/monitoring-tasks/{task_id}/trigger": { + "/api/ogc/collections/monitoring-tasks/items/{task_id}/trigger": { parameters: { query?: never; header?: never; @@ -1274,31 +1323,27 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/observations": { + "/api/ogc/collections/observations/bulk-create": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** - * Get Observations - * @description Get Observations. - */ - get: operations["interfaces_api_views_sta_observation_get_observations"]; + get?: never; put?: never; /** - * Create Observation - * @description Create a new Observation. + * Insert Observations + * @description Insert Observations. */ - post: operations["interfaces_api_views_sta_observation_create_observation"]; + post: operations["interfaces_api_views_sta_observation_insert_observations"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/api/data/observations/bulk-create": { + "/api/ogc/collections/observations/bulk-delete": { parameters: { query?: never; header?: never; @@ -1308,37 +1353,41 @@ export interface paths { get?: never; put?: never; /** - * Insert Observations - * @description Insert Observations. + * Delete Observations + * @description Delete Observations between the given phenomenon start and end times. */ - post: operations["interfaces_api_views_sta_observation_insert_observations"]; + post: operations["interfaces_api_views_sta_observation_delete_observations"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/api/data/observations/bulk-delete": { + "/api/ogc/collections/observations/items": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get?: never; + /** + * Get Observations + * @description Get Observations. + */ + get: operations["interfaces_api_views_sta_observation_get_observations"]; put?: never; /** - * Delete Observations - * @description Delete Observations between the given phenomenon start and end times. + * Create Observation + * @description Create a new Observation. */ - post: operations["interfaces_api_views_sta_observation_delete_observations"]; + post: operations["interfaces_api_views_sta_observation_create_observation"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/api/data/observations/{observation_id}": { + "/api/ogc/collections/observations/items/{observation_id}": { parameters: { query?: never; header?: never; @@ -1362,7 +1411,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/observed-properties": { + "/api/ogc/collections/observed-properties/items": { parameters: { query?: never; header?: never; @@ -1386,7 +1435,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/observed-properties/{observed_property_id}": { + "/api/ogc/collections/observed-properties/items/{observed_property_id}": { parameters: { query?: never; header?: never; @@ -1414,7 +1463,7 @@ export interface paths { patch: operations["interfaces_api_views_sta_observed_property_update_observed_property"]; trace?: never; }; - "/api/data/observed-property-types": { + "/api/ogc/collections/observed-property-types/items": { parameters: { query?: never; header?: never; @@ -1438,7 +1487,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/observed-property-types/{observed_property_type_id}": { + "/api/ogc/collections/observed-property-types/items/{observed_property_type_id}": { parameters: { query?: never; header?: never; @@ -1466,7 +1515,7 @@ export interface paths { patch: operations["interfaces_api_views_sta_observed_property_type_update_observed_property_type"]; trace?: never; }; - "/api/data/processing-levels": { + "/api/ogc/collections/processing-levels/items": { parameters: { query?: never; header?: never; @@ -1490,7 +1539,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/processing-levels/{processing_level_id}": { + "/api/ogc/collections/processing-levels/items/{processing_level_id}": { parameters: { query?: never; header?: never; @@ -1518,7 +1567,7 @@ export interface paths { patch: operations["interfaces_api_views_sta_processing_level_update_processing_level"]; trace?: never; }; - "/api/data/quality-control/histories": { + "/api/ogc/collections/quality-control-histories/items": { parameters: { query?: never; header?: never; @@ -1542,7 +1591,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/quality-control/histories/{history_id}": { + "/api/ogc/collections/quality-control-histories/items/{history_id}": { parameters: { query?: never; header?: never; @@ -1566,7 +1615,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/quality-control/histories/{history_id}/sessions": { + "/api/ogc/collections/quality-control-histories/items/{history_id}/sessions": { parameters: { query?: never; header?: never; @@ -1590,7 +1639,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/quality-control/histories/{history_id}/sessions/{session_id}": { + "/api/ogc/collections/quality-control-histories/items/{history_id}/sessions/{session_id}": { parameters: { query?: never; header?: never; @@ -1618,7 +1667,7 @@ export interface paths { patch: operations["interfaces_api_views_quality_session_update_qc_session"]; trace?: never; }; - "/api/data/quality-control/histories/{history_id}/sessions/{session_id}/commit": { + "/api/ogc/collections/quality-control-histories/items/{history_id}/sessions/{session_id}/commit": { parameters: { query?: never; header?: never; @@ -1638,7 +1687,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/quality-control/histories/{history_id}/sessions/{session_id}/operations": { + "/api/ogc/collections/quality-control-histories/items/{history_id}/sessions/{session_id}/operations": { parameters: { query?: never; header?: never; @@ -1662,7 +1711,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/quality-control/histories/{history_id}/sessions/{session_id}/operations/{operation_id}": { + "/api/ogc/collections/quality-control-histories/items/{history_id}/sessions/{session_id}/operations/{operation_id}": { parameters: { query?: never; header?: never; @@ -1690,7 +1739,7 @@ export interface paths { patch: operations["interfaces_api_views_quality_operation_update_qc_operation"]; trace?: never; }; - "/api/data/result-qualifiers": { + "/api/ogc/collections/result-qualifiers/items": { parameters: { query?: never; header?: never; @@ -1714,7 +1763,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/result-qualifiers/{result_qualifier_id}": { + "/api/ogc/collections/result-qualifiers/items/{result_qualifier_id}": { parameters: { query?: never; header?: never; @@ -1742,7 +1791,7 @@ export interface paths { patch: operations["interfaces_api_views_sta_result_qualifier_update_result_qualifier"]; trace?: never; }; - "/api/data/roles": { + "/api/ogc/collections/roles/items": { parameters: { query?: never; header?: never; @@ -1762,7 +1811,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/roles/{role_id}": { + "/api/ogc/collections/roles/items/{role_id}": { parameters: { query?: never; header?: never; @@ -1782,7 +1831,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/sampled-mediums": { + "/api/ogc/collections/sampled-mediums/items": { parameters: { query?: never; header?: never; @@ -1806,7 +1855,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/sampled-mediums/{sampled_medium_id}": { + "/api/ogc/collections/sampled-mediums/items/{sampled_medium_id}": { parameters: { query?: never; header?: never; @@ -1834,7 +1883,7 @@ export interface paths { patch: operations["interfaces_api_views_sta_sampled_medium_update_sampled_medium"]; trace?: never; }; - "/api/data/unit-types": { + "/api/ogc/collections/unit-types/items": { parameters: { query?: never; header?: never; @@ -1858,7 +1907,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/unit-types/{unit_type_id}": { + "/api/ogc/collections/unit-types/items/{unit_type_id}": { parameters: { query?: never; header?: never; @@ -1886,7 +1935,7 @@ export interface paths { patch: operations["interfaces_api_views_sta_unit_type_update_unit_type"]; trace?: never; }; - "/api/data/units": { + "/api/ogc/collections/units/items": { parameters: { query?: never; header?: never; @@ -1910,7 +1959,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/units/{unit_id}": { + "/api/ogc/collections/units/items/{unit_id}": { parameters: { query?: never; header?: never; @@ -1938,7 +1987,7 @@ export interface paths { patch: operations["interfaces_api_views_sta_unit_update_unit"]; trace?: never; }; - "/api/data/workspaces": { + "/api/ogc/collections/workspaces/items": { parameters: { query?: never; header?: never; @@ -1962,7 +2011,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/workspaces/{workspace_id}": { + "/api/ogc/collections/workspaces/items/{workspace_id}": { parameters: { query?: never; header?: never; @@ -1990,7 +2039,7 @@ export interface paths { patch: operations["interfaces_api_views_iam_workspace_update_workspace"]; trace?: never; }; - "/api/data/workspaces/{workspace_id}/collaborators": { + "/api/ogc/collections/workspaces/items/{workspace_id}/collaborators": { parameters: { query?: never; header?: never; @@ -2022,7 +2071,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/workspaces/{workspace_id}/service-accounts": { + "/api/ogc/collections/workspaces/items/{workspace_id}/service-accounts": { parameters: { query?: never; header?: never; @@ -2046,7 +2095,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/workspaces/{workspace_id}/service-accounts/{service_account_id}": { + "/api/ogc/collections/workspaces/items/{workspace_id}/service-accounts/{service_account_id}": { parameters: { query?: never; header?: never; @@ -2074,7 +2123,7 @@ export interface paths { patch: operations["interfaces_api_views_iam_service_account_update_service_account"]; trace?: never; }; - "/api/data/workspaces/{workspace_id}/service-accounts/{service_account_id}/regenerate": { + "/api/ogc/collections/workspaces/items/{workspace_id}/service-accounts/{service_account_id}/regenerate": { parameters: { query?: never; header?: never; @@ -2094,7 +2143,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/data/workspaces/{workspace_id}/transfer": { + "/api/ogc/collections/workspaces/items/{workspace_id}/transfer": { parameters: { query?: never; header?: never; @@ -2122,14 +2171,54 @@ export interface paths { patch?: never; trace?: never; }; -} -export type webhooks = Record; -export interface components { - schemas: { - /** AggregationStatisticPatchBody */ - AggregationStatisticPatchBody: { - /** Description */ - description?: string | null; + "/api/ogc/collections/{collection_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Collection Metadata + * @description Get a collection. + */ + get: operations["interfaces_api_views_ogc_get_collection_metadata"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/ogc/conformance": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get Conformance + * @description Get the OGC API conformance classes the API implements. + */ + get: operations["interfaces_api_views_ogc_get_conformance"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + /** AggregationStatisticPatchBody */ + AggregationStatisticPatchBody: { + /** Description */ + description?: string | null; /** Name */ name?: string; }; @@ -2224,10 +2313,10 @@ export interface components { include?: ("role" | "user" | "serviceAccount")[]; /** * Limit - * @description The maximum number of items to return. + * @description The maximum number of items to return. (default: 100, maximum: 100000) * @default 100 */ - limit: number | null; + limit: number; /** * Offset * @description Number of items to skip. @@ -2250,7 +2339,7 @@ export interface components { * @description Select one or more fields to sort the response by. * @default [] */ - sortby: ("id" | "roleId" | "-id" | "-roleId")[] | null; + sortby: ("id" | "roleId" | "-id" | "-roleId" | "+id" | "+roleId")[]; }; /** CollaboratorResponse */ CollaboratorResponse: { @@ -2264,6 +2353,40 @@ export interface components { /** Useremail */ userEmail?: string | null; }; + /** + * CollectionResponse + * @description A collection's metadata (OGC API - Features Core collection.yaml). + */ + CollectionResponse: { + /** Description */ + description: string; + /** Id */ + id: string; + /** Itemtype */ + itemType?: string | null; + /** Links */ + links: components["schemas"]["Link"][]; + /** Title */ + title: string; + }; + /** + * CollectionsResponse + * @description The collections the API serves (OGC API - Features Core collections.yaml). + */ + CollectionsResponse: { + /** Collections */ + collections: components["schemas"]["CollectionResponse"][]; + /** Links */ + links: components["schemas"]["Link"][]; + }; + /** + * ConformanceResponse + * @description The conformance classes the API implements (OGC API - Features Core confClasses.yaml). + */ + ConformanceResponse: { + /** Conformsto */ + conformsTo: string[]; + }; /** CreatedResponse */ CreatedResponse: { /** @@ -2342,6 +2465,10 @@ export interface components { }; /** DataConnectionQueryParameters */ DataConnectionQueryParameters: { + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; /** * Include * @description Comma-separated list of related resources to include in the response. @@ -2349,10 +2476,10 @@ export interface components { include?: "workspace"[]; /** * Limit - * @description The maximum number of items to return. + * @description The maximum number of items to return. (default: 100, maximum: 100000) * @default 100 */ - limit: number | null; + limit: number; /** * Offset * @description Number of items to skip. @@ -2380,7 +2507,7 @@ export interface components { * @description Select one or more fields to sort the response by. * @default [] */ - sortby: ("id" | "name" | "timestampKey" | "timestampFormat" | "timezoneType" | "timezone" | "workspaceId" | "workspaceName" | "-id" | "-name" | "-timestampKey" | "-timestampFormat" | "-timezoneType" | "-timezone" | "-workspaceId" | "-workspaceName")[] | null; + sortby: ("id" | "name" | "timestampKey" | "timestampFormat" | "timezoneType" | "timezone" | "workspaceId" | "workspaceName" | "-id" | "-name" | "-timestampKey" | "-timestampFormat" | "-timezoneType" | "-timezone" | "-workspaceId" | "-workspaceName" | "+id" | "+name" | "+timestampKey" | "+timestampFormat" | "+timezoneType" | "+timezone" | "+workspaceId" | "+workspaceName")[]; /** * Workspace Id * @description Filter data connections by workspace ID. @@ -2504,6 +2631,10 @@ export interface components { }; /** DataProductTaskQueryParameters */ DataProductTaskQueryParameters: { + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; /** * Include * @description Comma-separated list of related resources to include in the response. @@ -2523,10 +2654,10 @@ export interface components { latest_run_status: (string | "null")[]; /** * Limit - * @description The maximum number of items to return. + * @description The maximum number of items to return. (default: 100, maximum: 100000) * @default 100 */ - limit: number | null; + limit: number; /** * Monitoring Site Id * @description Filter data product tasks by monitoring_site ID. @@ -2560,13 +2691,13 @@ export interface components { * @description Filter data product tasks by rating curve ID. * @default [] */ - rating_curve_id: string[]; + rating_curve_id: (string | "null")[]; /** * Sortby * @description Select one or more fields to sort the response by. * @default [] */ - sortby: ("id" | "name" | "monitoringSiteId" | "monitoringSiteName" | "workspaceId" | "workspaceName" | "latestRunStatus" | "latestRunStartedAt" | "latestRunFinishedAt" | "-id" | "-name" | "-monitoringSiteId" | "-monitoringSiteName" | "-workspaceId" | "-workspaceName" | "-latestRunStatus" | "-latestRunStartedAt" | "-latestRunFinishedAt")[] | null; + sortby: ("id" | "name" | "monitoringSiteId" | "monitoringSiteName" | "workspaceId" | "workspaceName" | "latestRunStatus" | "latestRunStartedAt" | "latestRunFinishedAt" | "-id" | "-name" | "-monitoringSiteId" | "-monitoringSiteName" | "-workspaceId" | "-workspaceName" | "-latestRunStatus" | "-latestRunStartedAt" | "-latestRunFinishedAt" | "+id" | "+name" | "+monitoringSiteId" | "+monitoringSiteName" | "+workspaceId" | "+workspaceName" | "+latestRunStatus" | "+latestRunStartedAt" | "+latestRunFinishedAt")[]; /** * Transformation Type * @description Filter data product tasks by transformation type. @@ -2705,6 +2836,10 @@ export interface components { }; /** DataProductTransformationQueryParameters */ DataProductTransformationQueryParameters: { + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; /** * Include * @description Comma-separated list of related resources to include in the response. @@ -2718,10 +2853,10 @@ export interface components { input_datastream_id: string[]; /** * Limit - * @description The maximum number of items to return. + * @description The maximum number of items to return. (default: 100, maximum: 100000) * @default 100 */ - limit: number | null; + limit: number; /** * Offset * @description Number of items to skip. @@ -2744,7 +2879,7 @@ export interface components { * @description Select one or more fields to sort the response by. * @default [] */ - sortby: ("id" | "outputDatastreamId" | "transformationType" | "-id" | "-outputDatastreamId" | "-transformationType")[] | null; + sortby: ("id" | "outputDatastreamId" | "transformationType" | "-id" | "-outputDatastreamId" | "-transformationType" | "+id" | "+outputDatastreamId" | "+transformationType")[]; /** * Task Id * @description Filter transformations by data product task ID. @@ -2829,7 +2964,7 @@ export interface components { * Properties * @description Comma-separated list of properties to include in the response. All properties are returned if omitted. */ - properties?: ("id" | "workspaceId" | "name" | "description" | "observationType" | "sampledMedium" | "noDataValue" | "aggregationStatistic" | "timeAggregationInterval" | "status" | "resultType" | "valueCount" | "phenomenonBeginTime" | "phenomenonEndTime" | "resultBeginTime" | "resultEndTime" | "isPrivate" | "isVisible" | "timeAggregationIntervalUnit" | "intendedTimeSpacing" | "intendedTimeSpacingUnit" | "monitoringSiteId" | "methodId" | "observedPropertyId" | "processingLevelId" | "unitId" | "tags" | "linkedResources")[]; + properties?: ("id" | "workspaceId" | "name" | "description" | "observationType" | "sampledMedium" | "noDataValue" | "aggregationStatistic" | "timeAggregationInterval" | "status" | "resultType" | "isPrivate" | "isVisible" | "timeAggregationIntervalUnit" | "intendedTimeSpacing" | "intendedTimeSpacingUnit" | "valueCount" | "phenomenonBeginTime" | "phenomenonEndTime" | "resultBeginTime" | "resultEndTime" | "monitoringSiteId" | "methodId" | "observedPropertyId" | "processingLevelId" | "unitId" | "tags" | "linkedResources")[]; }; /** DatastreamPatchBody */ DatastreamPatchBody: { @@ -2866,19 +3001,11 @@ export interface components { * Format: uuid */ observedPropertyId?: string; - /** Phenomenonbegintime */ - phenomenonBeginTime?: string | null; - /** Phenomenonendtime */ - phenomenonEndTime?: string | null; /** * Processinglevelid * Format: uuid */ processingLevelId?: string; - /** Resultbegintime */ - resultBeginTime?: string | null; - /** Resultendtime */ - resultEndTime?: string | null; /** Resulttype */ resultType?: string; /** Sampledmedium */ @@ -2901,8 +3028,6 @@ export interface components { * Format: uuid */ unitId?: string; - /** Valuecount */ - valueCount?: number | null; }; /** DatastreamPostBody */ DatastreamPostBody: { @@ -2947,19 +3072,11 @@ export interface components { * Format: uuid */ observedPropertyId: string; - /** Phenomenonbegintime */ - phenomenonBeginTime?: string | null; - /** Phenomenonendtime */ - phenomenonEndTime?: string | null; /** * Processinglevelid * Format: uuid */ processingLevelId: string; - /** Resultbegintime */ - resultBeginTime?: string | null; - /** Resultendtime */ - resultEndTime?: string | null; /** Resulttype */ resultType: string; /** Sampledmedium */ @@ -2985,11 +3102,13 @@ export interface components { * Format: uuid */ unitId: string; - /** Valuecount */ - valueCount?: number | null; }; /** DatastreamQueryParameters */ DatastreamQueryParameters: { + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; /** * Include * @description Comma-separated list of related resources to include in the response. @@ -3002,10 +3121,10 @@ export interface components { is_private?: boolean | null; /** * Limit - * @description The maximum number of items to return. + * @description The maximum number of items to return. (default: 100, maximum: 100000) * @default 100 */ - limit: number | null; + limit: number; /** * Method Id * @description Filter datastreams by method ID. @@ -3036,26 +3155,6 @@ export interface components { * @default 0 */ offset: number | null; - /** - * Phenomenon Begin Time Max - * @description Sets the maximum phenomenon begin time of filtered datastreams. - */ - phenomenon_begin_time_max?: string | null; - /** - * Phenomenon Begin Time Min - * @description Sets the minimum phenomenon begin time of filtered datastreams. - */ - phenomenon_begin_time_min?: string | null; - /** - * Phenomenon End Time Max - * @description Sets the maximum phenomenon end time of filtered datastreams. - */ - phenomenon_end_time_max?: string | null; - /** - * Phenomenon End Time Min - * @description Sets the minimum phenomenon end time of filtered datastreams. - */ - phenomenon_end_time_min?: string | null; /** * Processing Level Id * @description Filter datastreams by processing level ID. @@ -3066,32 +3165,12 @@ export interface components { * Properties * @description Comma-separated list of properties to include in the response. All properties are returned if omitted. */ - properties?: ("id" | "workspaceId" | "name" | "description" | "observationType" | "sampledMedium" | "noDataValue" | "aggregationStatistic" | "timeAggregationInterval" | "status" | "resultType" | "valueCount" | "phenomenonBeginTime" | "phenomenonEndTime" | "resultBeginTime" | "resultEndTime" | "isPrivate" | "isVisible" | "timeAggregationIntervalUnit" | "intendedTimeSpacing" | "intendedTimeSpacingUnit" | "monitoringSiteId" | "methodId" | "observedPropertyId" | "processingLevelId" | "unitId" | "tags" | "linkedResources")[]; + properties?: ("id" | "workspaceId" | "name" | "description" | "observationType" | "sampledMedium" | "noDataValue" | "aggregationStatistic" | "timeAggregationInterval" | "status" | "resultType" | "isPrivate" | "isVisible" | "timeAggregationIntervalUnit" | "intendedTimeSpacing" | "intendedTimeSpacingUnit" | "valueCount" | "phenomenonBeginTime" | "phenomenonEndTime" | "resultBeginTime" | "resultEndTime" | "monitoringSiteId" | "methodId" | "observedPropertyId" | "processingLevelId" | "unitId" | "tags" | "linkedResources")[]; /** * Q * @description Full-text search query. Comma-separated terms are combined with OR; whitespace-separated words within a term are combined with AND. */ q?: string | null; - /** - * Result Begin Time Max - * @description Sets the maximum result begin time of filtered datastreams. - */ - result_begin_time_max?: string | null; - /** - * Result Begin Time Min - * @description Sets the minimum result begin time of filtered datastreams. - */ - result_begin_time_min?: string | null; - /** - * Result End Time Max - * @description Sets the maximum result end time of filtered datastreams. - */ - result_end_time_max?: string | null; - /** - * Result End Time Min - * @description Sets the minimum result end time of filtered datastreams. - */ - result_end_time_min?: string | null; /** * Result Qualifier Id * @description Filter datastreams by observation result qualifier ID. @@ -3115,13 +3194,13 @@ export interface components { * @description Select one or more fields to sort the response by. * @default [] */ - sortby: ("name" | "observationType" | "sampledMedium" | "status" | "resultType" | "isPrivate" | "isVisible" | "aggregationStatistic" | "valueCount" | "phenomenonBeginTime" | "phenomenonEndTime" | "resultBeginTime" | "resultEndTime" | "-name" | "-observationType" | "-sampledMedium" | "-status" | "-resultType" | "-isPrivate" | "-isVisible" | "-aggregationStatistic" | "-valueCount" | "-phenomenonBeginTime" | "-phenomenonEndTime" | "-resultBeginTime" | "-resultEndTime")[] | null; + sortby: ("name" | "observationType" | "sampledMedium" | "status" | "resultType" | "isPrivate" | "isVisible" | "aggregationStatistic" | "valueCount" | "phenomenonBeginTime" | "phenomenonEndTime" | "resultBeginTime" | "resultEndTime" | "-name" | "-observationType" | "-sampledMedium" | "-status" | "-resultType" | "-isPrivate" | "-isVisible" | "-aggregationStatistic" | "-valueCount" | "-phenomenonBeginTime" | "-phenomenonEndTime" | "-resultBeginTime" | "-resultEndTime" | "+name" | "+observationType" | "+sampledMedium" | "+status" | "+resultType" | "+isPrivate" | "+isVisible" | "+aggregationStatistic" | "+valueCount" | "+phenomenonBeginTime" | "+phenomenonEndTime" | "+resultBeginTime" | "+resultEndTime")[]; /** * Status - * @description Filter monitoring_sites by status. + * @description Filter datastreams by status. * @default [] */ - status: string[]; + status: (string | "null")[]; /** * Tag * @description Filter datastreams by tag. Format tag filters as {key}:{value} @@ -3335,6 +3414,10 @@ export interface components { }; /** EtlMappingQueryParameters */ EtlMappingQueryParameters: { + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; /** * Etl Task Id * @description Filter mappings by ETL task ID. @@ -3348,10 +3431,10 @@ export interface components { include?: "targetDatastream"[]; /** * Limit - * @description The maximum number of items to return. + * @description The maximum number of items to return. (default: 100, maximum: 100000) * @default 100 */ - limit: number | null; + limit: number; /** * Offset * @description Number of items to skip. @@ -3368,7 +3451,7 @@ export interface components { * @description Select one or more fields to sort the response by. * @default [] */ - sortby: ("id" | "sourceIdentifier" | "targetDatastreamId" | "-id" | "-sourceIdentifier" | "-targetDatastreamId")[] | null; + sortby: ("id" | "sourceIdentifier" | "targetDatastreamId" | "-id" | "-sourceIdentifier" | "-targetDatastreamId" | "+id" | "+sourceIdentifier" | "+targetDatastreamId")[]; /** * Source Identifier * @description Filter mappings by source identifier. @@ -3461,12 +3544,16 @@ export interface components { }; /** EtlTaskQueryParameters */ EtlTaskQueryParameters: { + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); /** * Data Connection Id * @description Filter ETL tasks by data connection ID. * @default [] */ data_connection_id: string[]; + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; /** * Include * @description Comma-separated list of related resources to include in the response. @@ -3500,10 +3587,10 @@ export interface components { latest_run_status: (string | "null")[]; /** * Limit - * @description The maximum number of items to return. + * @description The maximum number of items to return. (default: 100, maximum: 100000) * @default 100 */ - limit: number | null; + limit: number; /** * Monitoring Site Id * @description Filter ETL tasks by monitoring_site ID. @@ -3531,7 +3618,7 @@ export interface components { * @description Select one or more fields to sort the response by. * @default [] */ - sortby: ("id" | "name" | "dataConnectionId" | "dataConnectionName" | "workspaceId" | "workspaceName" | "latestRunStatus" | "latestRunStartedAt" | "latestRunFinishedAt" | "-id" | "-name" | "-dataConnectionId" | "-dataConnectionName" | "-workspaceId" | "-workspaceName" | "-latestRunStatus" | "-latestRunStartedAt" | "-latestRunFinishedAt")[] | null; + sortby: ("id" | "name" | "dataConnectionId" | "dataConnectionName" | "workspaceId" | "workspaceName" | "latestRunStatus" | "latestRunStartedAt" | "latestRunFinishedAt" | "-id" | "-name" | "-dataConnectionId" | "-dataConnectionName" | "-workspaceId" | "-workspaceName" | "-latestRunStatus" | "-latestRunStartedAt" | "-latestRunFinishedAt" | "+id" | "+name" | "+dataConnectionId" | "+dataConnectionName" | "+workspaceId" | "+workspaceName" | "+latestRunStatus" | "+latestRunStartedAt" | "+latestRunFinishedAt")[]; /** * Workspace Id * @description Filter ETL tasks by workspace ID. @@ -3582,6 +3669,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; }; /** ItemResponse[DataConnectionResponse] */ ItemResponse_DataConnectionResponse_: { @@ -3590,6 +3682,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; }; /** ItemResponse[DataProductTaskResponse] */ ItemResponse_DataProductTaskResponse_: { @@ -3598,6 +3695,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; }; /** ItemResponse[DataProductTransformationResponse] */ ItemResponse_DataProductTransformationResponse_: { @@ -3606,6 +3708,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; }; /** ItemResponse[DatastreamResponse] */ ItemResponse_DatastreamResponse_: { @@ -3614,6 +3721,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; }; /** ItemResponse[DatastreamStatusResponse] */ ItemResponse_DatastreamStatusResponse_: { @@ -3622,6 +3734,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; }; /** ItemResponse[EtlMappingResponse] */ ItemResponse_EtlMappingResponse_: { @@ -3630,6 +3747,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; }; /** ItemResponse[EtlTaskResponse] */ ItemResponse_EtlTaskResponse_: { @@ -3638,6 +3760,24 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; + }; + /** ItemResponse[LinkedResourceGetResponse] */ + ItemResponse_LinkedResourceGetResponse_: { + data: components["schemas"]["LinkedResourceGetResponse"]; + /** Included */ + included?: { + [key: string]: unknown[]; + } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; }; /** ItemResponse[LinkedResourceTypeResponse] */ ItemResponse_LinkedResourceTypeResponse_: { @@ -3646,6 +3786,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; }; /** ItemResponse[MethodResponse] */ ItemResponse_MethodResponse_: { @@ -3654,6 +3799,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; }; /** ItemResponse[MethodTypeResponse] */ ItemResponse_MethodTypeResponse_: { @@ -3662,6 +3812,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; }; /** ItemResponse[MonitoringRuleResponse] */ ItemResponse_MonitoringRuleResponse_: { @@ -3670,6 +3825,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; }; /** ItemResponse[MonitoringSiteResponse] */ ItemResponse_MonitoringSiteResponse_: { @@ -3678,6 +3838,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; }; /** ItemResponse[MonitoringSiteTypeResponse] */ ItemResponse_MonitoringSiteTypeResponse_: { @@ -3686,6 +3851,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; }; /** ItemResponse[MonitoringTaskResponse] */ ItemResponse_MonitoringTaskResponse_: { @@ -3694,6 +3864,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; }; /** ItemResponse[ObservationResponse] */ ItemResponse_ObservationResponse_: { @@ -3702,6 +3877,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; }; /** ItemResponse[ObservedPropertyResponse] */ ItemResponse_ObservedPropertyResponse_: { @@ -3710,6 +3890,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; }; /** ItemResponse[ObservedPropertyTypeResponse] */ ItemResponse_ObservedPropertyTypeResponse_: { @@ -3718,6 +3903,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; }; /** ItemResponse[ProcessingLevelResponse] */ ItemResponse_ProcessingLevelResponse_: { @@ -3726,6 +3916,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; }; /** ItemResponse[QualityControlHistoryResponse] */ ItemResponse_QualityControlHistoryResponse_: { @@ -3734,6 +3929,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; }; /** ItemResponse[QualityControlOperationResponse] */ ItemResponse_QualityControlOperationResponse_: { @@ -3742,6 +3942,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; }; /** ItemResponse[QualityControlSessionResponse] */ ItemResponse_QualityControlSessionResponse_: { @@ -3750,6 +3955,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; }; /** ItemResponse[RatingCurveResponse] */ ItemResponse_RatingCurveResponse_: { @@ -3758,6 +3968,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; }; /** ItemResponse[ResultQualifierResponse] */ ItemResponse_ResultQualifierResponse_: { @@ -3766,6 +3981,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; }; /** ItemResponse[RoleResponse] */ ItemResponse_RoleResponse_: { @@ -3774,6 +3994,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; }; /** ItemResponse[SampledMediumResponse] */ ItemResponse_SampledMediumResponse_: { @@ -3782,6 +4007,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; }; /** ItemResponse[ServiceAccountResponse] */ ItemResponse_ServiceAccountResponse_: { @@ -3790,6 +4020,24 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; + }; + /** ItemResponse[TaskRunResponse] */ + ItemResponse_TaskRunResponse_: { + data: components["schemas"]["TaskRunResponse"]; + /** Included */ + included?: { + [key: string]: unknown[]; + } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; }; /** ItemResponse[UnitResponse] */ ItemResponse_UnitResponse_: { @@ -3798,6 +4046,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; }; /** ItemResponse[UnitTypeResponse] */ ItemResponse_UnitTypeResponse_: { @@ -3806,6 +4059,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; }; /** ItemResponse[WorkspaceResponse] */ ItemResponse_WorkspaceResponse_: { @@ -3814,6 +4072,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; }; /** JSONPayloadPostBody */ JSONPayloadPostBody: { @@ -3845,6 +4108,29 @@ export interface components { */ type: "JSON"; }; + /** + * LandingPageResponse + * @description The API's landing page (OGC API - Features Core landingPage.yaml). + */ + LandingPageResponse: { + /** Description */ + description: string; + /** Links */ + links: components["schemas"]["Link"][]; + /** Title */ + title: string; + }; + /** Link */ + Link: { + /** Href */ + href: string; + /** Rel */ + rel: string; + /** Title */ + title?: string | null; + /** Type */ + type: string; + }; /** LinkedResourceGetResponse */ LinkedResourceGetResponse: { /** Description */ @@ -3863,17 +4149,12 @@ export interface components { }; /** LinkedResourceQueryParameters */ LinkedResourceQueryParameters: { - /** - * Include - * @description Comma-separated list of related resources to include in the response. - */ - include?: string | null; /** * Limit - * @description The maximum number of items to return. + * @description The maximum number of items to return. (default: 100, maximum: 100000) * @default 100 */ - limit: number | null; + limit: number; /** * Offset * @description Number of items to skip. @@ -3977,12 +4258,16 @@ export interface components { }; /** MethodQueryParameters */ MethodQueryParameters: { + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); /** * Datastream Id * @description Filter methods by datastream ID. * @default [] */ datastream_id: (string | "null")[]; + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; /** * Include * @description Comma-separated list of related resources to include in the response. @@ -3990,10 +4275,10 @@ export interface components { include?: ("workspace" | "type")[]; /** * Limit - * @description The maximum number of items to return. + * @description The maximum number of items to return. (default: 100, maximum: 100000) * @default 100 */ - limit: number | null; + limit: number; /** * Monitoring Site Id * @description Filter methods by monitoring_site ID. @@ -4021,19 +4306,19 @@ export interface components { * @description Filter methods by sensor model * @default [] */ - sensor_model: string[]; + sensor_model: (string | "null")[]; /** * Sensor Model Manufacturer * @description Filter methods by sensor model manufacturer * @default [] */ - sensor_model_manufacturer: string[]; + sensor_model_manufacturer: (string | "null")[]; /** * Sortby * @description Select one or more fields to sort the response by. * @default [] */ - sortby: ("name" | "code" | "type" | "sensorModel" | "sensorModelManufacturer" | "definition" | "sensorModelDefinition" | "-name" | "-code" | "-type" | "-sensorModel" | "-sensorModelManufacturer" | "-definition" | "-sensorModelDefinition")[] | null; + sortby: ("name" | "code" | "type" | "sensorModel" | "sensorModelManufacturer" | "definition" | "sensorModelDefinition" | "-name" | "-code" | "-type" | "-sensorModel" | "-sensorModelManufacturer" | "-definition" | "-sensorModelDefinition" | "+name" | "+code" | "+type" | "+sensorModel" | "+sensorModelManufacturer" | "+definition" | "+sensorModelDefinition")[]; /** * Type * @description Filter methods by type @@ -4155,12 +4440,16 @@ export interface components { }; /** MonitoringRuleQueryParameters */ MonitoringRuleQueryParameters: { + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); /** * Datastream Id * @description Filter rules by datastream ID. * @default [] */ datastream_id: string[]; + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; /** * Include * @description Comma-separated list of related resources to include in the response. @@ -4168,10 +4457,10 @@ export interface components { include?: "datastream"[]; /** * Limit - * @description The maximum number of items to return. + * @description The maximum number of items to return. (default: 100, maximum: 100000) * @default 100 */ - limit: number | null; + limit: number; /** * Offset * @description Number of items to skip. @@ -4194,7 +4483,7 @@ export interface components { * @description Select one or more fields to sort the response by. * @default [] */ - sortby: ("id" | "ruleType" | "datastreamId" | "lastCheckedAt" | "-id" | "-ruleType" | "-datastreamId" | "-lastCheckedAt")[] | null; + sortby: ("id" | "ruleType" | "datastreamId" | "lastCheckedAt" | "-id" | "-ruleType" | "-datastreamId" | "-lastCheckedAt" | "+id" | "+ruleType" | "+datastreamId" | "+lastCheckedAt")[]; /** * Task Id * @description Filter rules by monitoring task ID. @@ -4300,12 +4589,8 @@ export interface components { }; /** MonitoringSiteMarkerQueryParameters */ MonitoringSiteMarkerQueryParameters: { - /** - * Bbox - * @description Filter markers by bounding box. Format bounding box as {min_lon},{min_lat},{max_lon},{max_lat} - * @default [] - */ - bbox: string[]; + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); /** * Type * @description Filter markers by monitoring site type. @@ -4425,25 +4710,23 @@ export interface components { * @description Filter monitoring sites by admin area 1. * @default [] */ - admin_area_1: string[]; + admin_area_1: (string | "null")[]; /** * Admin Area 2 * @description Filter monitoring sites by admin area 2. * @default [] */ - admin_area_2: string[]; - /** - * Bbox - * @description Filter monitoring sites by bounding box. Format bounding box as {min_lon},{min_lat},{max_lon},{max_lat} - * @default [] - */ - bbox: string[]; + admin_area_2: (string | "null")[]; + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); /** * Country * @description Filter monitoring sites by country. * @default [] */ - country: string[]; + country: (string | "null")[]; + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; /** * Include * @description Comma-separated list of related resources to include in the response. @@ -4456,10 +4739,10 @@ export interface components { is_private?: boolean | null; /** * Limit - * @description The maximum number of items to return. + * @description The maximum number of items to return. (default: 100, maximum: 100000) * @default 100 */ - limit: number | null; + limit: number; /** * Offset * @description Number of items to skip. @@ -4481,7 +4764,7 @@ export interface components { * @description Select one or more fields to sort the response by. * @default [] */ - sortby: ("name" | "code" | "type" | "isPrivate" | "latitude" | "longitude" | "elevation_m" | "elevationDatum" | "adminArea1" | "adminArea2" | "country" | "-name" | "-code" | "-type" | "-isPrivate" | "-latitude" | "-longitude" | "-elevation_m" | "-elevationDatum" | "-adminArea1" | "-adminArea2" | "-country")[] | null; + sortby: ("name" | "code" | "type" | "isPrivate" | "latitude" | "longitude" | "elevation_m" | "elevationDatum" | "adminArea1" | "adminArea2" | "country" | "-name" | "-code" | "-type" | "-isPrivate" | "-latitude" | "-longitude" | "-elevation_m" | "-elevationDatum" | "-adminArea1" | "-adminArea2" | "-country" | "+name" | "+code" | "+type" | "+isPrivate" | "+latitude" | "+longitude" | "+elevation_m" | "+elevationDatum" | "+adminArea1" | "+adminArea2" | "+country")[]; /** * Tag * @description Filter monitoring sites by tag. Format tag filters as {key}:{value} @@ -4673,12 +4956,16 @@ export interface components { }; /** MonitoringTaskQueryParameters */ MonitoringTaskQueryParameters: { + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); /** * Datastream Id * @description Filter monitoring tasks by datastream ID. * @default [] */ datastream_id: string[]; + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; /** * Include * @description Comma-separated list of related resources to include in the response. @@ -4692,10 +4979,10 @@ export interface components { latest_run_status: (string | "null")[]; /** * Limit - * @description The maximum number of items to return. + * @description The maximum number of items to return. (default: 100, maximum: 100000) * @default 100 */ - limit: number | null; + limit: number; /** * Monitoring Site Id * @description Filter monitoring tasks by monitoring_site ID. @@ -4729,7 +5016,7 @@ export interface components { * @description Select one or more fields to sort the response by. * @default [] */ - sortby: ("id" | "name" | "monitoringSiteId" | "monitoringSiteName" | "workspaceId" | "workspaceName" | "latestRunStatus" | "latestRunStartedAt" | "latestRunFinishedAt" | "-id" | "-name" | "-monitoringSiteId" | "-monitoringSiteName" | "-workspaceId" | "-workspaceName" | "-latestRunStatus" | "-latestRunStartedAt" | "-latestRunFinishedAt")[] | null; + sortby: ("id" | "name" | "monitoringSiteId" | "monitoringSiteName" | "workspaceId" | "workspaceName" | "latestRunStatus" | "latestRunStartedAt" | "latestRunFinishedAt" | "-id" | "-name" | "-monitoringSiteId" | "-monitoringSiteName" | "-workspaceId" | "-workspaceName" | "-latestRunStatus" | "-latestRunStartedAt" | "-latestRunFinishedAt" | "+id" | "+name" | "+monitoringSiteId" | "+monitoringSiteName" | "+workspaceId" | "+workspaceName" | "+latestRunStatus" | "+latestRunStartedAt" | "+latestRunFinishedAt")[]; /** * Workspace Id * @description Filter monitoring tasks by workspace ID. @@ -4857,6 +5144,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; meta: components["schemas"]["PaginationMeta"]; }; /** ObservationItemQueryParameters */ @@ -4893,12 +5185,16 @@ export interface components { }; /** ObservationQueryParameters */ ObservationQueryParameters: { + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); /** * Datastream Id * @description Filter observations by datastream ID. * @default [] */ datastream_id: string[]; + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; /** * Format * @description Controls the format of the observations response. @@ -4911,26 +5207,16 @@ export interface components { include?: ("datastream" | "workspace" | "resultQualifiers")[]; /** * Limit - * @description The maximum number of items to return. + * @description The maximum number of items to return. (default: 100, maximum: 100000) * @default 100 */ - limit: number | null; + limit: number; /** * Offset * @description Number of items to skip. * @default 0 */ offset: number | null; - /** - * Phenomenon Time Max - * @description Sets the maximum phenomenon time of filtered observations. - */ - phenomenon_time_max?: string | null; - /** - * Phenomenon Time Min - * @description Sets the minimum phenomenon time of filtered observations. - */ - phenomenon_time_min?: string | null; /** * Properties * @description Comma-separated list of properties to include in the response. All properties are returned if omitted. Only applies to format=record. @@ -4947,7 +5233,7 @@ export interface components { * @description Select one or more fields to sort the response by. * @default [] */ - sortby: ("phenomenonTime" | "datastreamId" | "-phenomenonTime" | "-datastreamId")[] | null; + sortby: ("phenomenonTime" | "datastreamId" | "-phenomenonTime" | "-datastreamId" | "+phenomenonTime" | "+datastreamId")[]; }; /** ObservationResponse */ ObservationResponse: { @@ -4993,6 +5279,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; meta: components["schemas"]["PaginationMeta"]; }; /** ObservedPropertyItemQueryParameters */ @@ -5040,12 +5331,16 @@ export interface components { }; /** ObservedPropertyQueryParameters */ ObservedPropertyQueryParameters: { + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); /** * Datastream Id * @description Filter observed properties by datastream ID. * @default [] */ datastream_id: (string | "null")[]; + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; /** * Include * @description Comma-separated list of related resources to include in the response. @@ -5053,10 +5348,10 @@ export interface components { include?: ("workspace" | "type")[]; /** * Limit - * @description The maximum number of items to return. + * @description The maximum number of items to return. (default: 100, maximum: 100000) * @default 100 */ - limit: number | null; + limit: number; /** * Monitoring Site Id * @description Filter observed properties by monitoring_site ID. @@ -5084,7 +5379,7 @@ export interface components { * @description Select one or more fields to sort the response by. * @default [] */ - sortby: ("name" | "type" | "code" | "-name" | "-type" | "-code")[] | null; + sortby: ("name" | "type" | "code" | "-name" | "-type" | "-code" | "+name" | "+type" | "+code")[]; /** * Type * @description Filter observed properties by type @@ -5154,6 +5449,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; meta: components["schemas"]["PaginationMeta"]; }; /** PaginatedResponse[CollaboratorResponse] */ @@ -5164,6 +5464,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; meta: components["schemas"]["PaginationMeta"]; }; /** PaginatedResponse[DataConnectionResponse] */ @@ -5174,6 +5479,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; meta: components["schemas"]["PaginationMeta"]; }; /** PaginatedResponse[DataProductTaskResponse] */ @@ -5184,6 +5494,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; meta: components["schemas"]["PaginationMeta"]; }; /** PaginatedResponse[DataProductTransformationResponse] */ @@ -5194,6 +5509,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; meta: components["schemas"]["PaginationMeta"]; }; /** PaginatedResponse[DatastreamResponse] */ @@ -5204,6 +5524,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; meta: components["schemas"]["PaginationMeta"]; }; /** PaginatedResponse[DatastreamStatusResponse] */ @@ -5214,7 +5539,12 @@ export interface components { included?: { [key: string]: unknown[]; } | null; - meta: components["schemas"]["PaginationMeta"]; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; + meta: components["schemas"]["PaginationMeta"]; }; /** PaginatedResponse[EtlMappingResponse] */ PaginatedResponse_EtlMappingResponse_: { @@ -5224,6 +5554,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; meta: components["schemas"]["PaginationMeta"]; }; /** PaginatedResponse[EtlTaskResponse] */ @@ -5234,6 +5569,26 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; + meta: components["schemas"]["PaginationMeta"]; + }; + /** PaginatedResponse[LinkedResourceGetResponse] */ + PaginatedResponse_LinkedResourceGetResponse_: { + /** Data */ + data: components["schemas"]["LinkedResourceGetResponse"][]; + /** Included */ + included?: { + [key: string]: unknown[]; + } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; meta: components["schemas"]["PaginationMeta"]; }; /** PaginatedResponse[LinkedResourceTypeResponse] */ @@ -5244,6 +5599,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; meta: components["schemas"]["PaginationMeta"]; }; /** PaginatedResponse[MethodResponse] */ @@ -5254,6 +5614,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; meta: components["schemas"]["PaginationMeta"]; }; /** PaginatedResponse[MethodTypeResponse] */ @@ -5264,6 +5629,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; meta: components["schemas"]["PaginationMeta"]; }; /** PaginatedResponse[MonitoringRuleResponse] */ @@ -5274,6 +5644,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; meta: components["schemas"]["PaginationMeta"]; }; /** PaginatedResponse[MonitoringSiteResponse] */ @@ -5284,6 +5659,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; meta: components["schemas"]["PaginationMeta"]; }; /** PaginatedResponse[MonitoringSiteTypeResponse] */ @@ -5294,6 +5674,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; meta: components["schemas"]["PaginationMeta"]; }; /** PaginatedResponse[MonitoringTaskResponse] */ @@ -5304,6 +5689,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; meta: components["schemas"]["PaginationMeta"]; }; /** PaginatedResponse[ObservationResponse] */ @@ -5314,6 +5704,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; meta: components["schemas"]["PaginationMeta"]; }; /** PaginatedResponse[ObservedPropertyResponse] */ @@ -5324,6 +5719,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; meta: components["schemas"]["PaginationMeta"]; }; /** PaginatedResponse[ObservedPropertyTypeResponse] */ @@ -5334,6 +5734,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; meta: components["schemas"]["PaginationMeta"]; }; /** PaginatedResponse[ProcessingLevelResponse] */ @@ -5344,6 +5749,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; meta: components["schemas"]["PaginationMeta"]; }; /** PaginatedResponse[QualityControlHistoryResponse] */ @@ -5354,6 +5764,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; meta: components["schemas"]["PaginationMeta"]; }; /** PaginatedResponse[QualityControlOperationResponse] */ @@ -5364,6 +5779,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; meta: components["schemas"]["PaginationMeta"]; }; /** PaginatedResponse[QualityControlSessionResponse] */ @@ -5374,6 +5794,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; meta: components["schemas"]["PaginationMeta"]; }; /** PaginatedResponse[RatingCurveResponse] */ @@ -5384,6 +5809,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; meta: components["schemas"]["PaginationMeta"]; }; /** PaginatedResponse[ResultQualifierResponse] */ @@ -5394,6 +5824,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; meta: components["schemas"]["PaginationMeta"]; }; /** PaginatedResponse[RoleResponse] */ @@ -5404,6 +5839,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; meta: components["schemas"]["PaginationMeta"]; }; /** PaginatedResponse[SampledMediumResponse] */ @@ -5414,6 +5854,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; meta: components["schemas"]["PaginationMeta"]; }; /** PaginatedResponse[ServiceAccountResponse] */ @@ -5424,6 +5869,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; meta: components["schemas"]["PaginationMeta"]; }; /** PaginatedResponse[TaskRunResponse] */ @@ -5434,6 +5884,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; meta: components["schemas"]["PaginationMeta"]; }; /** PaginatedResponse[UnitResponse] */ @@ -5444,6 +5899,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; meta: components["schemas"]["PaginationMeta"]; }; /** PaginatedResponse[UnitTypeResponse] */ @@ -5454,6 +5914,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; meta: components["schemas"]["PaginationMeta"]; }; /** PaginatedResponse[WorkspaceResponse] */ @@ -5464,6 +5929,11 @@ export interface components { included?: { [key: string]: unknown[]; } | null; + /** + * Links + * @default [] + */ + links: components["schemas"]["Link"][]; meta: components["schemas"]["PaginationMeta"]; }; /** PaginationMeta */ @@ -5584,12 +6054,16 @@ export interface components { }; /** ProcessingLevelQueryParameters */ ProcessingLevelQueryParameters: { + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); /** * Datastream Id * @description Filter processing levels by datastream ID. * @default [] */ datastream_id: (string | "null")[]; + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; /** * Include * @description Comma-separated list of related resources to include in the response. @@ -5597,10 +6071,10 @@ export interface components { include?: "workspace"[]; /** * Limit - * @description The maximum number of items to return. + * @description The maximum number of items to return. (default: 100, maximum: 100000) * @default 100 */ - limit: number | null; + limit: number; /** * Monitoring Site Id * @description Filter processing levels by monitoring_site ID. @@ -5628,7 +6102,7 @@ export interface components { * @description Select one or more fields to sort the response by. * @default [] */ - sortby: ("code" | "name" | "-code" | "-name")[] | null; + sortby: ("code" | "name" | "-code" | "-name" | "+code" | "+name")[]; /** * Workspace Id * @description Filter processing levels by workspace ID. @@ -5682,6 +6156,10 @@ export interface components { }; /** QualityControlHistoryQueryParameters */ QualityControlHistoryQueryParameters: { + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; /** * Include * @description Comma-separated list of related resources to include in the response. @@ -5689,10 +6167,10 @@ export interface components { include?: ("managedDatastream" | "sourceDatastream")[]; /** * Limit - * @description The maximum number of items to return. + * @description The maximum number of items to return. (default: 100, maximum: 100000) * @default 100 */ - limit: number | null; + limit: number; /** * Managed Datastream Id * @description Filter histories by managed datastream ID. @@ -5715,13 +6193,13 @@ export interface components { * @description Select one or more fields to sort the response by. * @default [] */ - sortby: ("id" | "createdAt" | "phenomenonTimeStart" | "phenomenonTimeEnd" | "-id" | "-createdAt" | "-phenomenonTimeStart" | "-phenomenonTimeEnd")[] | null; + sortby: ("id" | "createdAt" | "phenomenonTimeStart" | "phenomenonTimeEnd" | "-id" | "-createdAt" | "-phenomenonTimeStart" | "-phenomenonTimeEnd" | "+id" | "+createdAt" | "+phenomenonTimeStart" | "+phenomenonTimeEnd")[]; /** * Source Datastream Id * @description Filter histories by source datastream ID. * @default [] */ - source_datastream_id: string[]; + source_datastream_id: (string | "null")[]; }; /** QualityControlHistoryResponse */ QualityControlHistoryResponse: { @@ -5798,10 +6276,10 @@ export interface components { include?: string | null; /** * Limit - * @description The maximum number of items to return. + * @description The maximum number of items to return. (default: 100, maximum: 100000) * @default 100 */ - limit: number | null; + limit: number; /** * Offset * @description Number of items to skip. @@ -5818,7 +6296,7 @@ export interface components { * @description Select one or more fields to sort the response by. * @default [] */ - sortby: ("id" | "order" | "operationType" | "createdAt" | "-id" | "-order" | "-operationType" | "-createdAt")[] | null; + sortby: ("id" | "order" | "operationType" | "createdAt" | "-id" | "-order" | "-operationType" | "-createdAt" | "+id" | "+order" | "+operationType" | "+createdAt")[]; }; /** QualityControlOperationResponse */ QualityControlOperationResponse: { @@ -5889,10 +6367,10 @@ export interface components { include_ancestors: boolean; /** * Limit - * @description The maximum number of items to return. + * @description The maximum number of items to return. (default: 100, maximum: 100000) * @default 100 */ - limit: number | null; + limit: number; /** * Offset * @description Number of items to skip. @@ -5919,7 +6397,7 @@ export interface components { * @description Select one or more fields to sort the response by. * @default [] */ - sortby: ("id" | "createdAt" | "phenomenonTimeStart" | "phenomenonTimeEnd" | "status" | "committedAt" | "-id" | "-createdAt" | "-phenomenonTimeStart" | "-phenomenonTimeEnd" | "-status" | "-committedAt")[] | null; + sortby: ("id" | "createdAt" | "phenomenonTimeStart" | "phenomenonTimeEnd" | "status" | "committedAt" | "-id" | "-createdAt" | "-phenomenonTimeStart" | "-phenomenonTimeEnd" | "-status" | "-committedAt" | "+id" | "+createdAt" | "+phenomenonTimeStart" | "+phenomenonTimeEnd" | "+status" | "+committedAt")[]; /** Status */ status?: ("in_progress" | "committed") | null; }; @@ -6026,6 +6504,10 @@ export interface components { }; /** RatingCurveQueryParameters */ RatingCurveQueryParameters: { + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; /** * Include * @description Comma-separated list of related resources to include in the response. @@ -6033,10 +6515,10 @@ export interface components { include?: "monitoringSite"[]; /** * Limit - * @description The maximum number of items to return. + * @description The maximum number of items to return. (default: 100, maximum: 100000) * @default 100 */ - limit: number | null; + limit: number; /** * Monitoring Site Id * @description Filter rating curves by monitoring site ID. @@ -6064,7 +6546,7 @@ export interface components { * @description Select one or more fields to sort the response by. * @default [] */ - sortby: ("id" | "name" | "monitoringSiteId" | "monitoringSiteName" | "workspaceId" | "workspaceName" | "fittingMethod" | "-id" | "-name" | "-monitoringSiteId" | "-monitoringSiteName" | "-workspaceId" | "-workspaceName" | "-fittingMethod")[] | null; + sortby: ("id" | "name" | "monitoringSiteId" | "monitoringSiteName" | "workspaceId" | "workspaceName" | "fittingMethod" | "-id" | "-name" | "-monitoringSiteId" | "-monitoringSiteName" | "-workspaceId" | "-workspaceName" | "-fittingMethod" | "+id" | "+name" | "+monitoringSiteId" | "+monitoringSiteName" | "+workspaceId" | "+workspaceName" | "+fittingMethod")[]; /** * Workspace Id * @description Filter rating curves by workspace ID. @@ -6135,6 +6617,10 @@ export interface components { }; /** ResultQualifierQueryParameters */ ResultQualifierQueryParameters: { + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; /** * Include * @description Comma-separated list of related resources to include in the response. @@ -6142,10 +6628,10 @@ export interface components { include?: "workspace"[]; /** * Limit - * @description The maximum number of items to return. + * @description The maximum number of items to return. (default: 100, maximum: 100000) * @default 100 */ - limit: number | null; + limit: number; /** * Offset * @description Number of items to skip. @@ -6167,7 +6653,7 @@ export interface components { * @description Select one or more fields to sort the response by. * @default [] */ - sortby: ("name" | "-name")[] | null; + sortby: ("name" | "-name" | "+name")[]; /** * Workspace Id * @description Filter terms by workspace ID. @@ -6199,6 +6685,10 @@ export interface components { }; /** RoleQueryParameters */ RoleQueryParameters: { + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; /** * Include * @description Comma-separated list of related resources to include in the response. @@ -6206,10 +6696,10 @@ export interface components { include?: string | null; /** * Limit - * @description The maximum number of items to return. + * @description The maximum number of items to return. (default: 100, maximum: 100000) * @default 100 */ - limit: number | null; + limit: number; /** * Offset * @description Number of items to skip. @@ -6226,7 +6716,7 @@ export interface components { * @description Select one or more fields to sort the response by. * @default [] */ - sortby: ("name" | "-name")[] | null; + sortby: ("name" | "-name" | "+name")[]; /** * Workspace Id * @description Filter roles by workspace ID. @@ -6385,10 +6875,10 @@ export interface components { include?: "workspace"[]; /** * Limit - * @description The maximum number of items to return. + * @description The maximum number of items to return. (default: 100, maximum: 100000) * @default 100 */ - limit: number | null; + limit: number; /** * Offset * @description Number of items to skip. @@ -6405,7 +6895,7 @@ export interface components { * @description Select one or more fields to sort the response by. * @default [] */ - sortby: ("name" | "isActive" | "keyExpiresAt" | "createdAt" | "lastUsedAt" | "-name" | "-isActive" | "-keyExpiresAt" | "-createdAt" | "-lastUsedAt")[] | null; + sortby: ("name" | "isActive" | "keyExpiresAt" | "createdAt" | "lastUsedAt" | "-name" | "-isActive" | "-keyExpiresAt" | "-createdAt" | "-lastUsedAt" | "+name" | "+isActive" | "+keyExpiresAt" | "+createdAt" | "+lastUsedAt")[]; }; /** ServiceAccountResponse */ ServiceAccountResponse: { @@ -6463,10 +6953,10 @@ export interface components { include?: string | null; /** * Limit - * @description The maximum number of items to return. + * @description The maximum number of items to return. (default: 100, maximum: 100000) * @default 100 */ - limit: number | null; + limit: number; /** * Offset * @description Number of items to skip. @@ -6483,7 +6973,7 @@ export interface components { * @description Select one or more fields to sort the response by. * @default [] */ - sortby: ("id" | "status" | "startedAt" | "finishedAt" | "-id" | "-status" | "-startedAt" | "-finishedAt")[]; + sortby: ("id" | "status" | "startedAt" | "finishedAt" | "-id" | "-status" | "-startedAt" | "-finishedAt" | "+id" | "+status" | "+startedAt" | "+finishedAt")[]; /** * Started At Max * @description Filters for task runs started on or before this date and time. @@ -6588,12 +7078,16 @@ export interface components { }; /** UnitQueryParameters */ UnitQueryParameters: { + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); /** * Datastream Id * @description Filter units by datastream ID. * @default [] */ datastream_id: (string | "null")[]; + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; /** * Include * @description Comma-separated list of related resources to include in the response. @@ -6601,10 +7095,10 @@ export interface components { include?: ("workspace" | "type")[]; /** * Limit - * @description The maximum number of items to return. + * @description The maximum number of items to return. (default: 100, maximum: 100000) * @default 100 */ - limit: number | null; + limit: number; /** * Monitoring Site Id * @description Filter units by monitoring_site ID. @@ -6632,7 +7126,7 @@ export interface components { * @description Select one or more fields to sort the response by. * @default [] */ - sortby: ("name" | "symbol" | "type" | "-name" | "-symbol" | "-type")[] | null; + sortby: ("name" | "symbol" | "type" | "-name" | "-symbol" | "-type" | "+name" | "+symbol" | "+type")[]; /** * Type * @description Filter units by type @@ -6823,6 +7317,10 @@ export interface components { }; /** VocabularyQueryParameters */ VocabularyQueryParameters: { + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; /** * Include * @description Comma-separated list of related resources to include in the response. @@ -6830,10 +7328,10 @@ export interface components { include?: string | null; /** * Limit - * @description The maximum number of items to return. + * @description The maximum number of items to return. (default: 100, maximum: 100000) * @default 100 */ - limit: number | null; + limit: number; /** * Offset * @description Number of items to skip. @@ -6855,7 +7353,7 @@ export interface components { * @description Select one or more fields to sort the response by. * @default [] */ - sortby: ("name" | "-name")[] | null; + sortby: ("name" | "-name" | "+name")[]; }; /** WorkspaceItemQueryParameters */ WorkspaceItemQueryParameters: { @@ -6888,6 +7386,10 @@ export interface components { }; /** WorkspaceQueryParameters */ WorkspaceQueryParameters: { + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; /** * Include * @description Comma-separated list of related resources to include in the response. @@ -6905,10 +7407,10 @@ export interface components { is_private?: boolean | null; /** * Limit - * @description The maximum number of items to return. + * @description The maximum number of items to return. (default: 100, maximum: 100000) * @default 100 */ - limit: number | null; + limit: number; /** * Offset * @description Number of items to skip. @@ -6930,7 +7432,7 @@ export interface components { * @description Select one or more fields to sort the response by. * @default [] */ - sortby: ("name" | "isPrivate" | "-name" | "-isPrivate")[] | null; + sortby: ("name" | "isPrivate" | "-name" | "-isPrivate" | "+name" | "+isPrivate")[]; }; /** WorkspaceResponse */ WorkspaceResponse: { @@ -6967,19 +7469,63 @@ export interface components { } export type $defs = Record; export interface operations { + interfaces_api_views_ogc_get_landing_page: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["LandingPageResponse"]; + }; + }; + }; + }; + interfaces_api_views_ogc_get_collections: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CollectionsResponse"]; + }; + }; + }; + }; interfaces_api_views_sta_aggregation_statistic_get_aggregation_statistics: { parameters: { query?: { + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; /** @description Comma-separated list of properties to include in the response. All properties are returned if omitted. */ properties?: ("id" | "name" | "description")[]; /** @description Comma-separated list of related resources to include in the response. */ include?: string | null; /** @description Number of items to skip. */ offset?: number | null; - /** @description The maximum number of items to return. */ - limit?: number | null; + /** @description The maximum number of items to return. (default: 100, maximum: 100000) */ + limit?: number; /** @description Select one or more fields to sort the response by. */ - sortby?: ("name" | "-name")[] | null; + sortby?: ("name" | "-name" | "+name")[]; /** @description Full-text search query. Comma-separated terms are combined with OR; whitespace-separated words within a term are combined with AND. */ q?: string | null; }; @@ -7195,16 +7741,20 @@ export interface operations { interfaces_api_views_products_rating_curve_get_rating_curves: { parameters: { query?: { + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; /** @description Comma-separated list of properties to include in the response. All properties are returned if omitted. */ properties?: ("id" | "monitoringSiteId" | "name" | "description" | "fittingMethod" | "points")[]; /** @description Comma-separated list of related resources to include in the response. */ include?: "monitoringSite"[]; /** @description Number of items to skip. */ offset?: number | null; - /** @description The maximum number of items to return. */ - limit?: number | null; + /** @description The maximum number of items to return. (default: 100, maximum: 100000) */ + limit?: number; /** @description Select one or more fields to sort the response by. */ - sortby?: ("id" | "name" | "monitoringSiteId" | "monitoringSiteName" | "workspaceId" | "workspaceName" | "fittingMethod" | "-id" | "-name" | "-monitoringSiteId" | "-monitoringSiteName" | "-workspaceId" | "-workspaceName" | "-fittingMethod")[] | null; + sortby?: ("id" | "name" | "monitoringSiteId" | "monitoringSiteName" | "workspaceId" | "workspaceName" | "fittingMethod" | "-id" | "-name" | "-monitoringSiteId" | "-monitoringSiteName" | "-workspaceId" | "-workspaceName" | "-fittingMethod" | "+id" | "+name" | "+monitoringSiteId" | "+monitoringSiteName" | "+workspaceId" | "+workspaceName" | "+fittingMethod")[]; /** @description Full-text search query. Comma-separated terms are combined with OR; whitespace-separated words within a term are combined with AND. */ q?: string | null; /** @description Filter rating curves by monitoring site ID. */ @@ -7462,16 +8012,20 @@ export interface operations { interfaces_api_views_products_task_get_data_product_tasks: { parameters: { query?: { + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; /** @description Comma-separated list of properties to include in the response. All properties are returned if omitted. */ properties?: ("id" | "workspaceId" | "monitoringSiteId" | "schedule" | "latestRun" | "transformationTypes" | "name" | "description")[]; /** @description Comma-separated list of related resources to include in the response. */ include?: "monitoringSite"[]; /** @description Number of items to skip. */ offset?: number | null; - /** @description The maximum number of items to return. */ - limit?: number | null; + /** @description The maximum number of items to return. (default: 100, maximum: 100000) */ + limit?: number; /** @description Select one or more fields to sort the response by. */ - sortby?: ("id" | "name" | "monitoringSiteId" | "monitoringSiteName" | "workspaceId" | "workspaceName" | "latestRunStatus" | "latestRunStartedAt" | "latestRunFinishedAt" | "-id" | "-name" | "-monitoringSiteId" | "-monitoringSiteName" | "-workspaceId" | "-workspaceName" | "-latestRunStatus" | "-latestRunStartedAt" | "-latestRunFinishedAt")[] | null; + sortby?: ("id" | "name" | "monitoringSiteId" | "monitoringSiteName" | "workspaceId" | "workspaceName" | "latestRunStatus" | "latestRunStartedAt" | "latestRunFinishedAt" | "-id" | "-name" | "-monitoringSiteId" | "-monitoringSiteName" | "-workspaceId" | "-workspaceName" | "-latestRunStatus" | "-latestRunStartedAt" | "-latestRunFinishedAt" | "+id" | "+name" | "+monitoringSiteId" | "+monitoringSiteName" | "+workspaceId" | "+workspaceName" | "+latestRunStatus" | "+latestRunStartedAt" | "+latestRunFinishedAt")[]; /** @description Full-text search query. Comma-separated terms are combined with OR; whitespace-separated words within a term are combined with AND. */ q?: string | null; /** @description Filter data product tasks by monitoring_site ID. */ @@ -7487,7 +8041,7 @@ export interface operations { /** @description Filter data product tasks by input datastream ID. */ input_datastream_id?: string[]; /** @description Filter data product tasks by rating curve ID. */ - rating_curve_id?: string[]; + rating_curve_id?: (string | "null")[]; }; header?: never; path?: never; @@ -7745,10 +8299,10 @@ export interface operations { include?: string | null; /** @description Number of items to skip. */ offset?: number | null; - /** @description The maximum number of items to return. */ - limit?: number | null; + /** @description The maximum number of items to return. (default: 100, maximum: 100000) */ + limit?: number; /** @description Select one or more fields to sort the response by. */ - sortby?: ("id" | "status" | "startedAt" | "finishedAt" | "-id" | "-status" | "-startedAt" | "-finishedAt")[]; + sortby?: ("id" | "status" | "startedAt" | "finishedAt" | "-id" | "-status" | "-startedAt" | "-finishedAt" | "+id" | "+status" | "+startedAt" | "+finishedAt")[]; /** @description Filters task runs by their status. */ status?: ("PENDING" | "STARTED" | "SUCCESS" | "FAILURE")[]; /** @description Filters for task runs started on or before this date and time. */ @@ -7824,7 +8378,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["TaskRunResponse"]; + "application/json": components["schemas"]["ItemResponse_TaskRunResponse_"]; }; }; /** @description Unauthorized */ @@ -7873,7 +8427,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["TaskRunResponse"]; + "application/json": components["schemas"]["ItemResponse_TaskRunResponse_"]; }; }; /** @description Unauthorized */ @@ -7908,16 +8462,20 @@ export interface operations { interfaces_api_views_products_transformation_get_data_product_transformations: { parameters: { query?: { + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; /** @description Comma-separated list of properties to include in the response. All properties are returned if omitted. */ properties?: ("id" | "transformationType" | "outputDatastreamId" | "inputDatastreams" | "ratingCurveId" | "formula" | "aggregationMethod" | "outputIntervalUnits" | "outputInterval" | "timezoneType" | "timezone" | "minValues" | "stopOnNoData" | "stopOnError")[]; /** @description Comma-separated list of related resources to include in the response. */ include?: ("outputDatastream" | "ratingCurve")[]; /** @description Number of items to skip. */ offset?: number | null; - /** @description The maximum number of items to return. */ - limit?: number | null; + /** @description The maximum number of items to return. (default: 100, maximum: 100000) */ + limit?: number; /** @description Select one or more fields to sort the response by. */ - sortby?: ("id" | "outputDatastreamId" | "transformationType" | "-id" | "-outputDatastreamId" | "-transformationType")[] | null; + sortby?: ("id" | "outputDatastreamId" | "transformationType" | "-id" | "-outputDatastreamId" | "-transformationType" | "+id" | "+outputDatastreamId" | "+transformationType")[]; /** @description Filter transformations by type. */ transformation_type?: string[]; /** @description Filter transformations by output datastream ID. */ @@ -8197,16 +8755,20 @@ export interface operations { interfaces_api_views_sta_datastream_status_get_datastream_statuses: { parameters: { query?: { + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; /** @description Comma-separated list of properties to include in the response. All properties are returned if omitted. */ properties?: ("id" | "name" | "description")[]; /** @description Comma-separated list of related resources to include in the response. */ include?: string | null; /** @description Number of items to skip. */ offset?: number | null; - /** @description The maximum number of items to return. */ - limit?: number | null; + /** @description The maximum number of items to return. (default: 100, maximum: 100000) */ + limit?: number; /** @description Select one or more fields to sort the response by. */ - sortby?: ("name" | "-name")[] | null; + sortby?: ("name" | "-name" | "+name")[]; /** @description Full-text search query. Comma-separated terms are combined with OR; whitespace-separated words within a term are combined with AND. */ q?: string | null; }; @@ -8422,16 +8984,20 @@ export interface operations { interfaces_api_views_sta_datastream_get_datastreams: { parameters: { query?: { + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; /** @description Comma-separated list of properties to include in the response. All properties are returned if omitted. */ - properties?: ("id" | "workspaceId" | "name" | "description" | "observationType" | "sampledMedium" | "noDataValue" | "aggregationStatistic" | "timeAggregationInterval" | "status" | "resultType" | "valueCount" | "phenomenonBeginTime" | "phenomenonEndTime" | "resultBeginTime" | "resultEndTime" | "isPrivate" | "isVisible" | "timeAggregationIntervalUnit" | "intendedTimeSpacing" | "intendedTimeSpacingUnit" | "monitoringSiteId" | "methodId" | "observedPropertyId" | "processingLevelId" | "unitId" | "tags" | "linkedResources")[]; + properties?: ("id" | "workspaceId" | "name" | "description" | "observationType" | "sampledMedium" | "noDataValue" | "aggregationStatistic" | "timeAggregationInterval" | "status" | "resultType" | "isPrivate" | "isVisible" | "timeAggregationIntervalUnit" | "intendedTimeSpacing" | "intendedTimeSpacingUnit" | "valueCount" | "phenomenonBeginTime" | "phenomenonEndTime" | "resultBeginTime" | "resultEndTime" | "monitoringSiteId" | "methodId" | "observedPropertyId" | "processingLevelId" | "unitId" | "tags" | "linkedResources")[]; /** @description Comma-separated list of related resources to include in the response. */ include?: ("workspace" | "monitoringSite" | "method" | "observedProperty" | "processingLevel" | "unit" | "sampledMedium" | "aggregationStatistic" | "status")[]; /** @description Number of items to skip. */ offset?: number | null; - /** @description The maximum number of items to return. */ - limit?: number | null; + /** @description The maximum number of items to return. (default: 100, maximum: 100000) */ + limit?: number; /** @description Select one or more fields to sort the response by. */ - sortby?: ("name" | "observationType" | "sampledMedium" | "status" | "resultType" | "isPrivate" | "isVisible" | "aggregationStatistic" | "valueCount" | "phenomenonBeginTime" | "phenomenonEndTime" | "resultBeginTime" | "resultEndTime" | "-name" | "-observationType" | "-sampledMedium" | "-status" | "-resultType" | "-isPrivate" | "-isVisible" | "-aggregationStatistic" | "-valueCount" | "-phenomenonBeginTime" | "-phenomenonEndTime" | "-resultBeginTime" | "-resultEndTime")[] | null; + sortby?: ("name" | "observationType" | "sampledMedium" | "status" | "resultType" | "isPrivate" | "isVisible" | "aggregationStatistic" | "valueCount" | "phenomenonBeginTime" | "phenomenonEndTime" | "resultBeginTime" | "resultEndTime" | "-name" | "-observationType" | "-sampledMedium" | "-status" | "-resultType" | "-isPrivate" | "-isVisible" | "-aggregationStatistic" | "-valueCount" | "-phenomenonBeginTime" | "-phenomenonEndTime" | "-resultBeginTime" | "-resultEndTime" | "+name" | "+observationType" | "+sampledMedium" | "+status" | "+resultType" | "+isPrivate" | "+isVisible" | "+aggregationStatistic" | "+valueCount" | "+phenomenonBeginTime" | "+phenomenonEndTime" | "+resultBeginTime" | "+resultEndTime")[]; /** @description Full-text search query. Comma-separated terms are combined with OR; whitespace-separated words within a term are combined with AND. */ q?: string | null; /** @description Filter datastreams by workspace ID. */ @@ -8452,8 +9018,8 @@ export interface operations { observation_type?: string[]; /** @description Filter monitoring_sites by sampled medium. */ sampled_medium?: string[]; - /** @description Filter monitoring_sites by status. */ - status?: string[]; + /** @description Filter datastreams by status. */ + status?: (string | "null")[]; /** @description Filter monitoring_sites by result type. */ result_type?: string[]; /** @description Filter datastreams by tag. Format tag filters as {key}:{value} */ @@ -8464,22 +9030,6 @@ export interface operations { value_count_max?: number | null; /** @description Sets the minimum value count of filtered datastreams. */ value_count_min?: number | null; - /** @description Sets the maximum phenomenon begin time of filtered datastreams. */ - phenomenon_begin_time_max?: string | null; - /** @description Sets the minimum phenomenon begin time of filtered datastreams. */ - phenomenon_begin_time_min?: string | null; - /** @description Sets the maximum phenomenon end time of filtered datastreams. */ - phenomenon_end_time_max?: string | null; - /** @description Sets the minimum phenomenon end time of filtered datastreams. */ - phenomenon_end_time_min?: string | null; - /** @description Sets the maximum result begin time of filtered datastreams. */ - result_begin_time_max?: string | null; - /** @description Sets the minimum result begin time of filtered datastreams. */ - result_begin_time_min?: string | null; - /** @description Sets the maximum result end time of filtered datastreams. */ - result_end_time_max?: string | null; - /** @description Sets the minimum result end time of filtered datastreams. */ - result_end_time_min?: string | null; }; header?: never; path?: never; @@ -8558,77 +9108,11 @@ export interface operations { }; }; }; - interfaces_api_views_sta_datastream_get_datastream_tag_keys: { - parameters: { - query?: { - workspace_id?: string | null; - datastream_id?: string | null; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - [key: string]: string[]; - }; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": string; - }; - }; - }; - }; - interfaces_api_views_sta_datastream_get_datastream_visualization_bootstrap: { - parameters: { - query?: { - /** @description Filter visualization bootstrap datastreams by workspace ID. */ - workspace_id?: string[]; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["DatastreamVisualizationBootstrapResponse"]; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": string; - }; - }; - }; - }; interfaces_api_views_sta_datastream_get_datastream: { parameters: { query?: { /** @description Comma-separated list of properties to include in the response. All properties are returned if omitted. */ - properties?: ("id" | "workspaceId" | "name" | "description" | "observationType" | "sampledMedium" | "noDataValue" | "aggregationStatistic" | "timeAggregationInterval" | "status" | "resultType" | "valueCount" | "phenomenonBeginTime" | "phenomenonEndTime" | "resultBeginTime" | "resultEndTime" | "isPrivate" | "isVisible" | "timeAggregationIntervalUnit" | "intendedTimeSpacing" | "intendedTimeSpacingUnit" | "monitoringSiteId" | "methodId" | "observedPropertyId" | "processingLevelId" | "unitId" | "tags" | "linkedResources")[]; + properties?: ("id" | "workspaceId" | "name" | "description" | "observationType" | "sampledMedium" | "noDataValue" | "aggregationStatistic" | "timeAggregationInterval" | "status" | "resultType" | "isPrivate" | "isVisible" | "timeAggregationIntervalUnit" | "intendedTimeSpacing" | "intendedTimeSpacingUnit" | "valueCount" | "phenomenonBeginTime" | "phenomenonEndTime" | "resultBeginTime" | "resultEndTime" | "monitoringSiteId" | "methodId" | "observedPropertyId" | "processingLevelId" | "unitId" | "tags" | "linkedResources")[]; /** @description Comma-separated list of related resources to include in the response. */ include?: ("workspace" | "monitoringSite" | "method" | "observedProperty" | "processingLevel" | "unit" | "sampledMedium" | "aggregationStatistic" | "status")[]; }; @@ -8801,12 +9285,10 @@ export interface operations { query?: { /** @description Comma-separated list of properties to include in the response. All properties are returned if omitted. */ properties?: string | null; - /** @description Comma-separated list of related resources to include in the response. */ - include?: string | null; /** @description Number of items to skip. */ offset?: number | null; - /** @description The maximum number of items to return. */ - limit?: number | null; + /** @description The maximum number of items to return. (default: 100, maximum: 100000) */ + limit?: number; /** @description Filter by linked resource type. */ type?: string[]; }; @@ -8824,7 +9306,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["LinkedResourceGetResponse"][]; + "application/json": components["schemas"]["PaginatedResponse_LinkedResourceGetResponse_"]; }; }; /** @description Unauthorized */ @@ -8920,6 +9402,56 @@ export interface operations { }; }; }; + interfaces_api_views_sta_datastream_get_datastream_linked_resource: { + parameters: { + query?: never; + header?: never; + path: { + datastream_id: string; + linked_resource_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ItemResponse_LinkedResourceGetResponse_"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string; + }; + }; + }; + }; interfaces_api_views_sta_datastream_remove_datastream_linked_resource: { parameters: { query?: never; @@ -9049,19 +9581,89 @@ export interface operations { }; }; }; - interfaces_api_views_etl_data_connection_get_data_connections: { + interfaces_api_views_sta_datastream_get_datastream_tag_keys: { parameters: { query?: { - /** @description Comma-separated list of properties to include in the response. All properties are returned if omitted. */ + workspace_id?: string | null; + datastream_id?: string | null; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: string[]; + }; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string; + }; + }; + }; + }; + interfaces_api_views_sta_datastream_get_datastream_visualization_bootstrap: { + parameters: { + query?: { + /** @description Filter visualization bootstrap datastreams by workspace ID. */ + workspace_id?: string[]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DatastreamVisualizationBootstrapResponse"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string; + }; + }; + }; + }; + interfaces_api_views_etl_data_connection_get_data_connections: { + parameters: { + query?: { + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; + /** @description Comma-separated list of properties to include in the response. All properties are returned if omitted. */ properties?: ("id" | "name" | "description" | "sourceUrl" | "authHeaderName" | "authHeaderValue" | "timezoneType" | "timezone" | "workspaceId" | "payload" | "placeholderVariables" | "notification" | "taskCount" | "taskAttentionCount")[]; /** @description Comma-separated list of related resources to include in the response. */ include?: "workspace"[]; /** @description Number of items to skip. */ offset?: number | null; - /** @description The maximum number of items to return. */ - limit?: number | null; + /** @description The maximum number of items to return. (default: 100, maximum: 100000) */ + limit?: number; /** @description Select one or more fields to sort the response by. */ - sortby?: ("id" | "name" | "timestampKey" | "timestampFormat" | "timezoneType" | "timezone" | "workspaceId" | "workspaceName" | "-id" | "-name" | "-timestampKey" | "-timestampFormat" | "-timezoneType" | "-timezone" | "-workspaceId" | "-workspaceName")[] | null; + sortby?: ("id" | "name" | "timestampKey" | "timestampFormat" | "timezoneType" | "timezone" | "workspaceId" | "workspaceName" | "-id" | "-name" | "-timestampKey" | "-timestampFormat" | "-timezoneType" | "-timezone" | "-workspaceId" | "-workspaceName" | "+id" | "+name" | "+timestampKey" | "+timestampFormat" | "+timezoneType" | "+timezone" | "+workspaceId" | "+workspaceName")[]; /** @description Full-text search query. Comma-separated terms are combined with OR; whitespace-separated words within a term are combined with AND. */ q?: string | null; /** @description Filter data connections by workspace ID. */ @@ -9310,16 +9912,20 @@ export interface operations { interfaces_api_views_etl_mapping_get_etl_mappings: { parameters: { query?: { + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; /** @description Comma-separated list of properties to include in the response. All properties are returned if omitted. */ properties?: ("id" | "sourceIdentifier" | "targetDatastreamId")[]; /** @description Comma-separated list of related resources to include in the response. */ include?: "targetDatastream"[]; /** @description Number of items to skip. */ offset?: number | null; - /** @description The maximum number of items to return. */ - limit?: number | null; + /** @description The maximum number of items to return. (default: 100, maximum: 100000) */ + limit?: number; /** @description Select one or more fields to sort the response by. */ - sortby?: ("id" | "sourceIdentifier" | "targetDatastreamId" | "-id" | "-sourceIdentifier" | "-targetDatastreamId")[] | null; + sortby?: ("id" | "sourceIdentifier" | "targetDatastreamId" | "-id" | "-sourceIdentifier" | "-targetDatastreamId" | "+id" | "+sourceIdentifier" | "+targetDatastreamId")[]; /** @description Filter mappings by source identifier. */ source_identifier?: string[]; /** @description Filter mappings by target datastream ID. */ @@ -9597,16 +10203,20 @@ export interface operations { interfaces_api_views_etl_task_get_etl_tasks: { parameters: { query?: { + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; /** @description Comma-separated list of properties to include in the response. All properties are returned if omitted. */ properties?: ("id" | "workspaceId" | "dataConnectionId" | "schedule" | "latestRun" | "mappingCount" | "name" | "description" | "taskVariables")[]; /** @description Comma-separated list of related resources to include in the response. */ include?: "dataConnection"[]; /** @description Number of items to skip. */ offset?: number | null; - /** @description The maximum number of items to return. */ - limit?: number | null; + /** @description The maximum number of items to return. (default: 100, maximum: 100000) */ + limit?: number; /** @description Select one or more fields to sort the response by. */ - sortby?: ("id" | "name" | "dataConnectionId" | "dataConnectionName" | "workspaceId" | "workspaceName" | "latestRunStatus" | "latestRunStartedAt" | "latestRunFinishedAt" | "-id" | "-name" | "-dataConnectionId" | "-dataConnectionName" | "-workspaceId" | "-workspaceName" | "-latestRunStatus" | "-latestRunStartedAt" | "-latestRunFinishedAt")[] | null; + sortby?: ("id" | "name" | "dataConnectionId" | "dataConnectionName" | "workspaceId" | "workspaceName" | "latestRunStatus" | "latestRunStartedAt" | "latestRunFinishedAt" | "-id" | "-name" | "-dataConnectionId" | "-dataConnectionName" | "-workspaceId" | "-workspaceName" | "-latestRunStatus" | "-latestRunStartedAt" | "-latestRunFinishedAt" | "+id" | "+name" | "+dataConnectionId" | "+dataConnectionName" | "+workspaceId" | "+workspaceName" | "+latestRunStatus" | "+latestRunStartedAt" | "+latestRunFinishedAt")[]; /** @description Full-text search query. Comma-separated terms are combined with OR; whitespace-separated words within a term are combined with AND. */ q?: string | null; /** @description Filter ETL tasks by monitoring_site ID. */ @@ -9882,10 +10492,10 @@ export interface operations { include?: string | null; /** @description Number of items to skip. */ offset?: number | null; - /** @description The maximum number of items to return. */ - limit?: number | null; + /** @description The maximum number of items to return. (default: 100, maximum: 100000) */ + limit?: number; /** @description Select one or more fields to sort the response by. */ - sortby?: ("id" | "status" | "startedAt" | "finishedAt" | "-id" | "-status" | "-startedAt" | "-finishedAt")[]; + sortby?: ("id" | "status" | "startedAt" | "finishedAt" | "-id" | "-status" | "-startedAt" | "-finishedAt" | "+id" | "+status" | "+startedAt" | "+finishedAt")[]; /** @description Filters task runs by their status. */ status?: ("PENDING" | "STARTED" | "SUCCESS" | "FAILURE")[]; /** @description Filters for task runs started on or before this date and time. */ @@ -9961,7 +10571,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["TaskRunResponse"]; + "application/json": components["schemas"]["ItemResponse_TaskRunResponse_"]; }; }; /** @description Unauthorized */ @@ -10010,7 +10620,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["TaskRunResponse"]; + "application/json": components["schemas"]["ItemResponse_TaskRunResponse_"]; }; }; /** @description Unauthorized */ @@ -10045,16 +10655,20 @@ export interface operations { interfaces_api_views_sta_linked_resource_type_get_linked_resource_types: { parameters: { query?: { + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; /** @description Comma-separated list of properties to include in the response. All properties are returned if omitted. */ properties?: ("id" | "name" | "description")[]; /** @description Comma-separated list of related resources to include in the response. */ include?: string | null; /** @description Number of items to skip. */ offset?: number | null; - /** @description The maximum number of items to return. */ - limit?: number | null; + /** @description The maximum number of items to return. (default: 100, maximum: 100000) */ + limit?: number; /** @description Select one or more fields to sort the response by. */ - sortby?: ("name" | "-name")[] | null; + sortby?: ("name" | "-name" | "+name")[]; /** @description Full-text search query. Comma-separated terms are combined with OR; whitespace-separated words within a term are combined with AND. */ q?: string | null; }; @@ -10270,16 +10884,20 @@ export interface operations { interfaces_api_views_sta_method_type_get_method_types: { parameters: { query?: { + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; /** @description Comma-separated list of properties to include in the response. All properties are returned if omitted. */ properties?: ("id" | "name" | "description")[]; /** @description Comma-separated list of related resources to include in the response. */ include?: string | null; /** @description Number of items to skip. */ offset?: number | null; - /** @description The maximum number of items to return. */ - limit?: number | null; + /** @description The maximum number of items to return. (default: 100, maximum: 100000) */ + limit?: number; /** @description Select one or more fields to sort the response by. */ - sortby?: ("name" | "-name")[] | null; + sortby?: ("name" | "-name" | "+name")[]; /** @description Full-text search query. Comma-separated terms are combined with OR; whitespace-separated words within a term are combined with AND. */ q?: string | null; }; @@ -10495,16 +11113,20 @@ export interface operations { interfaces_api_views_sta_method_get_methods: { parameters: { query?: { + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; /** @description Comma-separated list of properties to include in the response. All properties are returned if omitted. */ properties?: ("id" | "workspaceId" | "name" | "code" | "type" | "description" | "definition" | "sensorModel" | "sensorModelManufacturer" | "sensorModelDefinition")[]; /** @description Comma-separated list of related resources to include in the response. */ include?: ("workspace" | "type")[]; /** @description Number of items to skip. */ offset?: number | null; - /** @description The maximum number of items to return. */ - limit?: number | null; + /** @description The maximum number of items to return. (default: 100, maximum: 100000) */ + limit?: number; /** @description Select one or more fields to sort the response by. */ - sortby?: ("name" | "code" | "type" | "sensorModel" | "sensorModelManufacturer" | "definition" | "sensorModelDefinition" | "-name" | "-code" | "-type" | "-sensorModel" | "-sensorModelManufacturer" | "-definition" | "-sensorModelDefinition")[] | null; + sortby?: ("name" | "code" | "type" | "sensorModel" | "sensorModelManufacturer" | "definition" | "sensorModelDefinition" | "-name" | "-code" | "-type" | "-sensorModel" | "-sensorModelManufacturer" | "-definition" | "-sensorModelDefinition" | "+name" | "+code" | "+type" | "+sensorModel" | "+sensorModelManufacturer" | "+definition" | "+sensorModelDefinition")[]; /** @description Full-text search query. Comma-separated terms are combined with OR; whitespace-separated words within a term are combined with AND. */ q?: string | null; /** @description Filter methods by workspace ID. */ @@ -10516,9 +11138,9 @@ export interface operations { /** @description Filter methods by type */ type?: string[]; /** @description Filter methods by sensor model */ - sensor_model?: string[]; + sensor_model?: (string | "null")[]; /** @description Filter methods by sensor model manufacturer */ - sensor_model_manufacturer?: string[]; + sensor_model_manufacturer?: (string | "null")[]; }; header?: never; path?: never; @@ -10761,16 +11383,20 @@ export interface operations { interfaces_api_views_monitoring_rule_get_monitoring_rules: { parameters: { query?: { + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; /** @description Comma-separated list of properties to include in the response. All properties are returned if omitted. */ properties?: ("id" | "datastreamId" | "ruleType" | "lastCheckedAt" | "minValue" | "maxValue" | "windowInterval" | "windowIntervalUnits")[]; /** @description Comma-separated list of related resources to include in the response. */ include?: "datastream"[]; /** @description Number of items to skip. */ offset?: number | null; - /** @description The maximum number of items to return. */ - limit?: number | null; + /** @description The maximum number of items to return. (default: 100, maximum: 100000) */ + limit?: number; /** @description Select one or more fields to sort the response by. */ - sortby?: ("id" | "ruleType" | "datastreamId" | "lastCheckedAt" | "-id" | "-ruleType" | "-datastreamId" | "-lastCheckedAt")[] | null; + sortby?: ("id" | "ruleType" | "datastreamId" | "lastCheckedAt" | "-id" | "-ruleType" | "-datastreamId" | "-lastCheckedAt" | "+id" | "+ruleType" | "+datastreamId" | "+lastCheckedAt")[]; /** @description Filter rules by datastream ID. */ datastream_id?: string[]; /** @description Filter rules by rule type. */ @@ -11048,16 +11674,20 @@ export interface operations { interfaces_api_views_sta_monitoring_site_type_get_monitoring_site_types: { parameters: { query?: { + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; /** @description Comma-separated list of properties to include in the response. All properties are returned if omitted. */ properties?: ("id" | "name" | "description")[]; /** @description Comma-separated list of related resources to include in the response. */ include?: string | null; /** @description Number of items to skip. */ offset?: number | null; - /** @description The maximum number of items to return. */ - limit?: number | null; + /** @description The maximum number of items to return. (default: 100, maximum: 100000) */ + limit?: number; /** @description Select one or more fields to sort the response by. */ - sortby?: ("name" | "-name")[] | null; + sortby?: ("name" | "-name" | "+name")[]; /** @description Full-text search query. Comma-separated terms are combined with OR; whitespace-separated words within a term are combined with AND. */ q?: string | null; }; @@ -11273,28 +11903,30 @@ export interface operations { interfaces_api_views_sta_monitoring_site_get_monitoring_sites: { parameters: { query?: { + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; /** @description Comma-separated list of properties to include in the response. All properties are returned if omitted. */ properties?: ("id" | "workspaceId" | "name" | "description" | "code" | "type" | "latitude" | "longitude" | "elevation_m" | "elevationDatum" | "adminArea1" | "adminArea2" | "country" | "dataDisclaimer" | "isPrivate" | "tags" | "linkedResources")[]; /** @description Comma-separated list of related resources to include in the response. */ include?: ("workspace" | "type")[]; /** @description Number of items to skip. */ offset?: number | null; - /** @description The maximum number of items to return. */ - limit?: number | null; + /** @description The maximum number of items to return. (default: 100, maximum: 100000) */ + limit?: number; /** @description Select one or more fields to sort the response by. */ - sortby?: ("name" | "code" | "type" | "isPrivate" | "latitude" | "longitude" | "elevation_m" | "elevationDatum" | "adminArea1" | "adminArea2" | "country" | "-name" | "-code" | "-type" | "-isPrivate" | "-latitude" | "-longitude" | "-elevation_m" | "-elevationDatum" | "-adminArea1" | "-adminArea2" | "-country")[] | null; + sortby?: ("name" | "code" | "type" | "isPrivate" | "latitude" | "longitude" | "elevation_m" | "elevationDatum" | "adminArea1" | "adminArea2" | "country" | "-name" | "-code" | "-type" | "-isPrivate" | "-latitude" | "-longitude" | "-elevation_m" | "-elevationDatum" | "-adminArea1" | "-adminArea2" | "-country" | "+name" | "+code" | "+type" | "+isPrivate" | "+latitude" | "+longitude" | "+elevation_m" | "+elevationDatum" | "+adminArea1" | "+adminArea2" | "+country")[]; /** @description Full-text search query. Comma-separated terms are combined with OR; whitespace-separated words within a term are combined with AND. */ q?: string | null; /** @description Filter monitoring sites by workspace ID. */ workspace_id?: string[]; - /** @description Filter monitoring sites by bounding box. Format bounding box as {min_lon},{min_lat},{max_lon},{max_lat} */ - bbox?: string[]; /** @description Filter monitoring sites by admin area 1. */ - admin_area_1?: string[]; + admin_area_1?: (string | "null")[]; /** @description Filter monitoring sites by admin area 2. */ - admin_area_2?: string[]; + admin_area_2?: (string | "null")[]; /** @description Filter monitoring sites by country. */ - country?: string[]; + country?: (string | "null")[]; /** @description Filter monitoring sites by type. */ type?: string[]; /** @description Filter monitoring sites by tag. Format tag filters as {key}:{value} */ @@ -11370,18 +12002,18 @@ export interface operations { }; }; }; - interfaces_api_views_sta_monitoring_site_get_monitoring_site_markers: { + interfaces_api_views_sta_monitoring_site_get_monitoring_site: { parameters: { query?: { - /** @description Filter markers by workspace ID. */ - workspace_id?: string[]; - /** @description Filter markers by bounding box. Format bounding box as {min_lon},{min_lat},{max_lon},{max_lat} */ - bbox?: string[]; - /** @description Filter markers by monitoring site type. */ - type?: string[]; + /** @description Comma-separated list of properties to include in the response. All properties are returned if omitted. */ + properties?: ("id" | "workspaceId" | "name" | "description" | "code" | "type" | "latitude" | "longitude" | "elevation_m" | "elevationDatum" | "adminArea1" | "adminArea2" | "country" | "dataDisclaimer" | "isPrivate" | "tags" | "linkedResources")[]; + /** @description Comma-separated list of related resources to include in the response. */ + include?: ("workspace" | "type")[]; }; header?: never; - path?: never; + path: { + monitoring_site_id: string; + }; cookie?: never; }; requestBody?: never; @@ -11392,7 +12024,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["MonitoringSiteMarkerResponse"][]; + "application/json": components["schemas"]["ItemResponse_MonitoringSiteResponse_"]; }; }; /** @description Unauthorized */ @@ -11404,30 +12036,34 @@ export interface operations { "application/json": string; }; }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string; + }; + }; }; }; - interfaces_api_views_sta_monitoring_site_get_monitoring_site_summaries: { + interfaces_api_views_sta_monitoring_site_delete_monitoring_site: { parameters: { - query?: { - /** @description Filter site summaries by workspace ID. */ - workspace_id?: string[]; - /** @description Filter summaries by monitoring site type. */ - type?: string[]; - }; + query?: never; header?: never; - path?: never; + path: { + monitoring_site_id: string; + }; cookie?: never; }; requestBody?: never; responses: { - /** @description OK */ - 200: { + /** @description No Content */ + 204: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["MonitoringSiteMapSummaryResponse"][]; - }; + content?: never; }; /** @description Unauthorized */ 401: { @@ -11438,53 +12074,41 @@ export interface operations { "application/json": string; }; }; - }; - }; - interfaces_api_views_sta_monitoring_site_get_site_type_icons: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { + /** @description Forbidden */ + 403: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["SiteTypeIconResponse"][]; + "application/json": string; }; }; }; }; - interfaces_api_views_sta_monitoring_site_get_monitoring_site_tag_keys: { + interfaces_api_views_sta_monitoring_site_update_monitoring_site: { parameters: { - query?: { - workspace_id?: string | null; - monitoring_site_id?: string | null; - }; + query?: never; header?: never; - path?: never; + path: { + monitoring_site_id: string; + }; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["MonitoringSitePatchBody"]; + }; + }; responses: { - /** @description OK */ - 200: { + /** @description No Content */ + 204: { headers: { [name: string]: unknown; }; - content: { - "application/json": { - [key: string]: string[]; - }; - }; + content?: never; }; - /** @description Unauthorized */ - 401: { + /** @description Bad Request */ + 400: { headers: { [name: string]: unknown; }; @@ -11492,33 +12116,17 @@ export interface operations { "application/json": string; }; }; - }; - }; - interfaces_api_views_sta_monitoring_site_get_monitoring_site_task_summaries: { - parameters: { - query?: { - /** @description Filter task summaries by workspace ID. */ - workspace_id?: string[]; - /** @description Filter summaries by monitoring site type. */ - type?: string[]; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { + /** @description Unauthorized */ + 401: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["MonitoringSiteTaskSummaryResponse"][]; + "application/json": string; }; }; - /** @description Unauthorized */ - 401: { + /** @description Forbidden */ + 403: { headers: { [name: string]: unknown; }; @@ -11528,13 +12136,17 @@ export interface operations { }; }; }; - interfaces_api_views_sta_monitoring_site_get_monitoring_site: { + interfaces_api_views_sta_monitoring_site_get_monitoring_site_linked_resources: { parameters: { query?: { /** @description Comma-separated list of properties to include in the response. All properties are returned if omitted. */ - properties?: ("id" | "workspaceId" | "name" | "description" | "code" | "type" | "latitude" | "longitude" | "elevation_m" | "elevationDatum" | "adminArea1" | "adminArea2" | "country" | "dataDisclaimer" | "isPrivate" | "tags" | "linkedResources")[]; - /** @description Comma-separated list of related resources to include in the response. */ - include?: ("workspace" | "type")[]; + properties?: string | null; + /** @description Number of items to skip. */ + offset?: number | null; + /** @description The maximum number of items to return. (default: 100, maximum: 100000) */ + limit?: number; + /** @description Filter by linked resource type. */ + type?: string[]; }; header?: never; path: { @@ -11550,7 +12162,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ItemResponse_MonitoringSiteResponse_"]; + "application/json": components["schemas"]["PaginatedResponse_LinkedResourceGetResponse_"]; }; }; /** @description Unauthorized */ @@ -11573,7 +12185,7 @@ export interface operations { }; }; }; - interfaces_api_views_sta_monitoring_site_delete_monitoring_site: { + interfaces_api_views_sta_monitoring_site_add_monitoring_site_linked_resource: { parameters: { query?: never; header?: never; @@ -11582,14 +12194,40 @@ export interface operations { }; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "multipart/form-data": { + /** Description */ + description?: string | null; + /** File */ + file?: string | null; + /** Link */ + link?: string | null; + /** Name */ + name: string; + /** Type */ + type: string; + }; + }; + }; responses: { - /** @description No Content */ - 204: { + /** @description Created */ + 201: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["CreatedResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string; + }; }; /** @description Unauthorized */ 401: { @@ -11609,32 +12247,40 @@ export interface operations { "application/json": string; }; }; + /** @description Content Too Large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string; + }; + }; }; }; - interfaces_api_views_sta_monitoring_site_update_monitoring_site: { + interfaces_api_views_sta_monitoring_site_get_monitoring_site_linked_resource: { parameters: { query?: never; header?: never; path: { monitoring_site_id: string; + linked_resource_id: string; }; cookie?: never; }; - requestBody: { - content: { - "application/json": components["schemas"]["MonitoringSitePatchBody"]; - }; - }; + requestBody?: never; responses: { - /** @description No Content */ - 204: { + /** @description OK */ + 200: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["ItemResponse_LinkedResourceGetResponse_"]; + }; }; - /** @description Bad Request */ - 400: { + /** @description Unauthorized */ + 401: { headers: { [name: string]: unknown; }; @@ -11642,8 +12288,8 @@ export interface operations { "application/json": string; }; }; - /** @description Unauthorized */ - 401: { + /** @description Forbidden */ + 403: { headers: { [name: string]: unknown; }; @@ -11651,8 +12297,8 @@ export interface operations { "application/json": string; }; }; - /** @description Forbidden */ - 403: { + /** @description Not Found */ + 404: { headers: { [name: string]: unknown; }; @@ -11662,36 +12308,24 @@ export interface operations { }; }; }; - interfaces_api_views_sta_monitoring_site_get_monitoring_site_linked_resources: { + interfaces_api_views_sta_monitoring_site_remove_monitoring_site_linked_resource: { parameters: { - query?: { - /** @description Comma-separated list of properties to include in the response. All properties are returned if omitted. */ - properties?: string | null; - /** @description Comma-separated list of related resources to include in the response. */ - include?: string | null; - /** @description Number of items to skip. */ - offset?: number | null; - /** @description The maximum number of items to return. */ - limit?: number | null; - /** @description Filter by linked resource type. */ - type?: string[]; - }; + query?: never; header?: never; path: { monitoring_site_id: string; + linked_resource_id: string; }; cookie?: never; }; requestBody?: never; responses: { - /** @description OK */ - 200: { + /** @description No Content */ + 204: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["LinkedResourceGetResponse"][]; - }; + content?: never; }; /** @description Unauthorized */ 401: { @@ -11711,14 +12345,24 @@ export interface operations { "application/json": string; }; }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string; + }; + }; }; }; - interfaces_api_views_sta_monitoring_site_add_monitoring_site_linked_resource: { + interfaces_api_views_sta_monitoring_site_update_monitoring_site_linked_resource: { parameters: { query?: never; header?: never; path: { monitoring_site_id: string; + linked_resource_id: string; }; cookie?: never; }; @@ -11732,21 +12376,19 @@ export interface operations { /** Link */ link?: string | null; /** Name */ - name: string; + name?: string | null; /** Type */ - type: string; + type?: string | null; }; }; }; responses: { - /** @description Created */ - 201: { + /** @description No Content */ + 204: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["CreatedResponse"]; - }; + content?: never; }; /** @description Bad Request */ 400: { @@ -11775,6 +12417,15 @@ export interface operations { "application/json": string; }; }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string; + }; + }; /** @description Content Too Large */ 413: { headers: { @@ -11786,24 +12437,30 @@ export interface operations { }; }; }; - interfaces_api_views_sta_monitoring_site_remove_monitoring_site_linked_resource: { + interfaces_api_views_sta_monitoring_site_get_monitoring_site_markers: { parameters: { - query?: never; - header?: never; - path: { - monitoring_site_id: string; - linked_resource_id: string; + query?: { + /** @description Filter markers by workspace ID. */ + workspace_id?: string[]; + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); + /** @description Filter markers by monitoring site type. */ + type?: string[]; }; + header?: never; + path?: never; cookie?: never; }; requestBody?: never; responses: { - /** @description No Content */ - 204: { + /** @description OK */ + 200: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["MonitoringSiteMarkerResponse"][]; + }; }; /** @description Unauthorized */ 401: { @@ -11814,17 +12471,33 @@ export interface operations { "application/json": string; }; }; - /** @description Forbidden */ - 403: { + }; + }; + interfaces_api_views_sta_monitoring_site_get_monitoring_site_summaries: { + parameters: { + query?: { + /** @description Filter site summaries by workspace ID. */ + workspace_id?: string[]; + /** @description Filter summaries by monitoring site type. */ + type?: string[]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { headers: { [name: string]: unknown; }; content: { - "application/json": string; + "application/json": components["schemas"]["MonitoringSiteMapSummaryResponse"][]; }; - }; - /** @description Not Found */ - 404: { + }; + /** @description Unauthorized */ + 401: { headers: { [name: string]: unknown; }; @@ -11834,47 +12507,47 @@ export interface operations { }; }; }; - interfaces_api_views_sta_monitoring_site_update_monitoring_site_linked_resource: { + interfaces_api_views_sta_monitoring_site_get_site_type_icons: { parameters: { query?: never; header?: never; - path: { - monitoring_site_id: string; - linked_resource_id: string; - }; + path?: never; cookie?: never; }; - requestBody: { - content: { - "multipart/form-data": { - /** Description */ - description?: string | null; - /** File */ - file?: string | null; - /** Link */ - link?: string | null; - /** Name */ - name?: string | null; - /** Type */ - type?: string | null; - }; - }; - }; + requestBody?: never; responses: { - /** @description No Content */ - 204: { + /** @description OK */ + 200: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["SiteTypeIconResponse"][]; + }; }; - /** @description Bad Request */ - 400: { + }; + }; + interfaces_api_views_sta_monitoring_site_get_monitoring_site_tag_keys: { + parameters: { + query?: { + workspace_id?: string | null; + monitoring_site_id?: string | null; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { headers: { [name: string]: unknown; }; content: { - "application/json": string; + "application/json": { + [key: string]: string[]; + }; }; }; /** @description Unauthorized */ @@ -11886,26 +12559,33 @@ export interface operations { "application/json": string; }; }; - /** @description Forbidden */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": string; - }; + }; + }; + interfaces_api_views_sta_monitoring_site_get_monitoring_site_task_summaries: { + parameters: { + query?: { + /** @description Filter task summaries by workspace ID. */ + workspace_id?: string[]; + /** @description Filter summaries by monitoring site type. */ + type?: string[]; }; - /** @description Not Found */ - 404: { + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { headers: { [name: string]: unknown; }; content: { - "application/json": string; + "application/json": components["schemas"]["MonitoringSiteTaskSummaryResponse"][]; }; }; - /** @description Content Too Large */ - 413: { + /** @description Unauthorized */ + 401: { headers: { [name: string]: unknown; }; @@ -11918,16 +12598,20 @@ export interface operations { interfaces_api_views_monitoring_task_get_monitoring_tasks: { parameters: { query?: { + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; /** @description Comma-separated list of properties to include in the response. All properties are returned if omitted. */ properties?: ("id" | "workspaceId" | "monitoringSiteId" | "schedule" | "latestRun" | "ruleTypeCounts" | "recipients" | "name" | "description")[]; /** @description Comma-separated list of related resources to include in the response. */ include?: "monitoringSite"[]; /** @description Number of items to skip. */ offset?: number | null; - /** @description The maximum number of items to return. */ - limit?: number | null; + /** @description The maximum number of items to return. (default: 100, maximum: 100000) */ + limit?: number; /** @description Select one or more fields to sort the response by. */ - sortby?: ("id" | "name" | "monitoringSiteId" | "monitoringSiteName" | "workspaceId" | "workspaceName" | "latestRunStatus" | "latestRunStartedAt" | "latestRunFinishedAt" | "-id" | "-name" | "-monitoringSiteId" | "-monitoringSiteName" | "-workspaceId" | "-workspaceName" | "-latestRunStatus" | "-latestRunStartedAt" | "-latestRunFinishedAt")[] | null; + sortby?: ("id" | "name" | "monitoringSiteId" | "monitoringSiteName" | "workspaceId" | "workspaceName" | "latestRunStatus" | "latestRunStartedAt" | "latestRunFinishedAt" | "-id" | "-name" | "-monitoringSiteId" | "-monitoringSiteName" | "-workspaceId" | "-workspaceName" | "-latestRunStatus" | "-latestRunStartedAt" | "-latestRunFinishedAt" | "+id" | "+name" | "+monitoringSiteId" | "+monitoringSiteName" | "+workspaceId" | "+workspaceName" | "+latestRunStatus" | "+latestRunStartedAt" | "+latestRunFinishedAt")[]; /** @description Full-text search query. Comma-separated terms are combined with OR; whitespace-separated words within a term are combined with AND. */ q?: string | null; /** @description Filter monitoring tasks by monitoring_site ID. */ @@ -12197,10 +12881,10 @@ export interface operations { include?: string | null; /** @description Number of items to skip. */ offset?: number | null; - /** @description The maximum number of items to return. */ - limit?: number | null; + /** @description The maximum number of items to return. (default: 100, maximum: 100000) */ + limit?: number; /** @description Select one or more fields to sort the response by. */ - sortby?: ("id" | "status" | "startedAt" | "finishedAt" | "-id" | "-status" | "-startedAt" | "-finishedAt")[]; + sortby?: ("id" | "status" | "startedAt" | "finishedAt" | "-id" | "-status" | "-startedAt" | "-finishedAt" | "+id" | "+status" | "+startedAt" | "+finishedAt")[]; /** @description Filters task runs by their status. */ status?: ("PENDING" | "STARTED" | "SUCCESS" | "FAILURE")[]; /** @description Filters for task runs started on or before this date and time. */ @@ -12276,7 +12960,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["TaskRunResponse"]; + "application/json": components["schemas"]["ItemResponse_TaskRunResponse_"]; }; }; /** @description Unauthorized */ @@ -12325,7 +13009,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["TaskRunResponse"]; + "application/json": components["schemas"]["ItemResponse_TaskRunResponse_"]; }; }; /** @description Unauthorized */ @@ -12357,44 +13041,28 @@ export interface operations { }; }; }; - interfaces_api_views_sta_observation_get_observations: { + interfaces_api_views_sta_observation_insert_observations: { parameters: { query?: { - /** @description Comma-separated list of properties to include in the response. All properties are returned if omitted. Only applies to format=record. */ - properties?: ("id" | "workspaceId" | "datastreamId" | "phenomenonTime" | "result" | "resultQualifierCodes")[]; - /** @description Comma-separated list of related resources to include in the response. Only applies to format=record. */ - include?: ("datastream" | "workspace" | "resultQualifiers")[]; - /** @description Number of items to skip. */ - offset?: number | null; - /** @description The maximum number of items to return. */ - limit?: number | null; - /** @description Filter observations by datastream ID. */ - datastream_id?: string[]; - /** @description Select one or more fields to sort the response by. */ - sortby?: ("phenomenonTime" | "datastreamId" | "-phenomenonTime" | "-datastreamId")[] | null; - /** @description Controls the format of the observations response. */ - format?: ("record" | "row" | "column") | null; - /** @description Sets the maximum phenomenon time of filtered observations. */ - phenomenon_time_max?: string | null; - /** @description Sets the minimum phenomenon time of filtered observations. */ - phenomenon_time_min?: string | null; - /** @description Filter observations by result qualifier code. */ - result_qualifier_code?: string[]; + /** @description Specifies how new observations are added to the datastream. `insert` allows observations at any timestamp. `append` adds only future observations (after the latest existing timestamp). `backfill` adds only historical observations (before the earliest existing timestamp). `replace` deletes all observations in the range of provided observations before inserting new ones. */ + mode?: ("insert" | "append" | "backfill" | "replace") | null; }; header?: never; path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["ObservationBulkPostBody"] | components["schemas"]["ObservationBulkColumnarPostBody"]; + }; + }; responses: { - /** @description OK */ - 200: { + /** @description Created */ + 201: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["PaginatedResponse_ObservationResponse_"] | components["schemas"]["ObservationRowResponse"] | components["schemas"]["ObservationColumnarResponse"]; - }; + content?: never; }; /** @description Bad Request */ 400: { @@ -12425,7 +13093,7 @@ export interface operations { }; }; }; - interfaces_api_views_sta_observation_create_observation: { + interfaces_api_views_sta_observation_delete_observations: { parameters: { query?: never; header?: never; @@ -12434,30 +13102,19 @@ export interface operations { }; requestBody: { content: { - "application/json": components["schemas"]["ObservationPostBody"]; + "application/json": components["schemas"]["ObservationBulkDeleteBody"]; }; }; responses: { - /** @description Created */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["CreatedResponse"]; - }; - }; - /** @description Bad Request */ - 400: { + /** @description No Content */ + 204: { headers: { [name: string]: unknown; }; - content: { - "application/json": string; - }; + content?: never; }; - /** @description Unauthorized */ - 401: { + /** @description Forbidden */ + 403: { headers: { [name: string]: unknown; }; @@ -12465,8 +13122,8 @@ export interface operations { "application/json": string; }; }; - /** @description Forbidden */ - 403: { + /** @description Not Found */ + 404: { headers: { [name: string]: unknown; }; @@ -12476,28 +13133,44 @@ export interface operations { }; }; }; - interfaces_api_views_sta_observation_insert_observations: { + interfaces_api_views_sta_observation_get_observations: { parameters: { query?: { - /** @description Specifies how new observations are added to the datastream. `insert` allows observations at any timestamp. `append` adds only future observations (after the latest existing timestamp). `backfill` adds only historical observations (before the earliest existing timestamp). `replace` deletes all observations in the range of provided observations before inserting new ones. */ - mode?: ("insert" | "append" | "backfill" | "replace") | null; + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; + /** @description Comma-separated list of properties to include in the response. All properties are returned if omitted. Only applies to format=record. */ + properties?: ("id" | "workspaceId" | "datastreamId" | "phenomenonTime" | "result" | "resultQualifierCodes")[]; + /** @description Comma-separated list of related resources to include in the response. Only applies to format=record. */ + include?: ("datastream" | "workspace" | "resultQualifiers")[]; + /** @description Number of items to skip. */ + offset?: number | null; + /** @description The maximum number of items to return. (default: 100, maximum: 100000) */ + limit?: number; + /** @description Filter observations by datastream ID. */ + datastream_id?: string[]; + /** @description Select one or more fields to sort the response by. */ + sortby?: ("phenomenonTime" | "datastreamId" | "-phenomenonTime" | "-datastreamId" | "+phenomenonTime" | "+datastreamId")[]; + /** @description Controls the format of the observations response. */ + format?: ("record" | "row" | "column") | null; + /** @description Filter observations by result qualifier code. */ + result_qualifier_code?: string[]; }; header?: never; path?: never; cookie?: never; }; - requestBody: { - content: { - "application/json": components["schemas"]["ObservationBulkPostBody"] | components["schemas"]["ObservationBulkColumnarPostBody"]; - }; - }; + requestBody?: never; responses: { - /** @description Created */ - 201: { + /** @description OK */ + 200: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["PaginatedResponse_ObservationResponse_"] | components["schemas"]["ObservationRowResponse"] | components["schemas"]["ObservationColumnarResponse"]; + }; }; /** @description Bad Request */ 400: { @@ -12528,7 +13201,7 @@ export interface operations { }; }; }; - interfaces_api_views_sta_observation_delete_observations: { + interfaces_api_views_sta_observation_create_observation: { parameters: { query?: never; header?: never; @@ -12537,19 +13210,21 @@ export interface operations { }; requestBody: { content: { - "application/json": components["schemas"]["ObservationBulkDeleteBody"]; + "application/json": components["schemas"]["ObservationPostBody"]; }; }; responses: { - /** @description No Content */ - 204: { + /** @description Created */ + 201: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["CreatedResponse"]; + }; }; - /** @description Forbidden */ - 403: { + /** @description Bad Request */ + 400: { headers: { [name: string]: unknown; }; @@ -12557,8 +13232,17 @@ export interface operations { "application/json": string; }; }; - /** @description Not Found */ - 404: { + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string; + }; + }; + /** @description Forbidden */ + 403: { headers: { [name: string]: unknown; }; @@ -12663,16 +13347,20 @@ export interface operations { interfaces_api_views_sta_observed_property_get_observed_properties: { parameters: { query?: { + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; /** @description Comma-separated list of properties to include in the response. All properties are returned if omitted. */ properties?: ("id" | "workspaceId" | "name" | "definition" | "description" | "type" | "code")[]; /** @description Comma-separated list of related resources to include in the response. */ include?: ("workspace" | "type")[]; /** @description Number of items to skip. */ offset?: number | null; - /** @description The maximum number of items to return. */ - limit?: number | null; + /** @description The maximum number of items to return. (default: 100, maximum: 100000) */ + limit?: number; /** @description Select one or more fields to sort the response by. */ - sortby?: ("name" | "type" | "code" | "-name" | "-type" | "-code")[] | null; + sortby?: ("name" | "type" | "code" | "-name" | "-type" | "-code" | "+name" | "+type" | "+code")[]; /** @description Full-text search query. Comma-separated terms are combined with OR; whitespace-separated words within a term are combined with AND. */ q?: string | null; /** @description Filter observed properties by workspace ID. */ @@ -12925,16 +13613,20 @@ export interface operations { interfaces_api_views_sta_observed_property_type_get_observed_property_types: { parameters: { query?: { + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; /** @description Comma-separated list of properties to include in the response. All properties are returned if omitted. */ properties?: ("id" | "name" | "description")[]; /** @description Comma-separated list of related resources to include in the response. */ include?: string | null; /** @description Number of items to skip. */ offset?: number | null; - /** @description The maximum number of items to return. */ - limit?: number | null; + /** @description The maximum number of items to return. (default: 100, maximum: 100000) */ + limit?: number; /** @description Select one or more fields to sort the response by. */ - sortby?: ("name" | "-name")[] | null; + sortby?: ("name" | "-name" | "+name")[]; /** @description Full-text search query. Comma-separated terms are combined with OR; whitespace-separated words within a term are combined with AND. */ q?: string | null; }; @@ -13150,16 +13842,20 @@ export interface operations { interfaces_api_views_sta_processing_level_get_processing_levels: { parameters: { query?: { + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; /** @description Comma-separated list of properties to include in the response. All properties are returned if omitted. */ properties?: ("id" | "workspaceId" | "code" | "name" | "description" | "definition")[]; /** @description Comma-separated list of related resources to include in the response. */ include?: "workspace"[]; /** @description Number of items to skip. */ offset?: number | null; - /** @description The maximum number of items to return. */ - limit?: number | null; + /** @description The maximum number of items to return. (default: 100, maximum: 100000) */ + limit?: number; /** @description Select one or more fields to sort the response by. */ - sortby?: ("code" | "name" | "-code" | "-name")[] | null; + sortby?: ("code" | "name" | "-code" | "-name" | "+code" | "+name")[]; /** @description Full-text search query. Comma-separated terms are combined with OR; whitespace-separated words within a term are combined with AND. */ q?: string | null; /** @description Filter processing levels by workspace ID. */ @@ -13410,20 +14106,24 @@ export interface operations { interfaces_api_views_quality_history_get_qc_histories: { parameters: { query?: { + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; /** @description Comma-separated list of properties to include in the response. All properties are returned if omitted. */ properties?: ("id" | "managedDatastreamId" | "sourceDatastreamId" | "createdAt" | "phenomenonTimeStart" | "phenomenonTimeEnd" | "sourceChecksum" | "managedChecksum")[]; /** @description Comma-separated list of related resources to include in the response. */ include?: ("managedDatastream" | "sourceDatastream")[]; /** @description Number of items to skip. */ offset?: number | null; - /** @description The maximum number of items to return. */ - limit?: number | null; + /** @description The maximum number of items to return. (default: 100, maximum: 100000) */ + limit?: number; /** @description Select one or more fields to sort the response by. */ - sortby?: ("id" | "createdAt" | "phenomenonTimeStart" | "phenomenonTimeEnd" | "-id" | "-createdAt" | "-phenomenonTimeStart" | "-phenomenonTimeEnd")[] | null; + sortby?: ("id" | "createdAt" | "phenomenonTimeStart" | "phenomenonTimeEnd" | "-id" | "-createdAt" | "-phenomenonTimeStart" | "-phenomenonTimeEnd" | "+id" | "+createdAt" | "+phenomenonTimeStart" | "+phenomenonTimeEnd")[]; /** @description Filter histories by managed datastream ID. */ managed_datastream_id?: string[]; /** @description Filter histories by source datastream ID. */ - source_datastream_id?: string[]; + source_datastream_id?: (string | "null")[]; }; header?: never; path?: never; @@ -13630,10 +14330,10 @@ export interface operations { include?: string | null; /** @description Number of items to skip. */ offset?: number | null; - /** @description The maximum number of items to return. */ - limit?: number | null; + /** @description The maximum number of items to return. (default: 100, maximum: 100000) */ + limit?: number; /** @description Select one or more fields to sort the response by. */ - sortby?: ("id" | "createdAt" | "phenomenonTimeStart" | "phenomenonTimeEnd" | "status" | "committedAt" | "-id" | "-createdAt" | "-phenomenonTimeStart" | "-phenomenonTimeEnd" | "-status" | "-committedAt")[] | null; + sortby?: ("id" | "createdAt" | "phenomenonTimeStart" | "phenomenonTimeEnd" | "status" | "committedAt" | "-id" | "-createdAt" | "-phenomenonTimeStart" | "-phenomenonTimeEnd" | "-status" | "-committedAt" | "+id" | "+createdAt" | "+phenomenonTimeStart" | "+phenomenonTimeEnd" | "+status" | "+committedAt")[]; status?: ("in_progress" | "committed") | null; /** @description Return sessions overlapping with this range start. */ range_start?: string | null; @@ -13980,10 +14680,10 @@ export interface operations { include?: string | null; /** @description Number of items to skip. */ offset?: number | null; - /** @description The maximum number of items to return. */ - limit?: number | null; + /** @description The maximum number of items to return. (default: 100, maximum: 100000) */ + limit?: number; /** @description Select one or more fields to sort the response by. */ - sortby?: ("id" | "order" | "operationType" | "createdAt" | "-id" | "-order" | "-operationType" | "-createdAt")[] | null; + sortby?: ("id" | "order" | "operationType" | "createdAt" | "-id" | "-order" | "-operationType" | "-createdAt" | "+id" | "+order" | "+operationType" | "+createdAt")[]; }; header?: never; path: { @@ -14263,16 +14963,20 @@ export interface operations { interfaces_api_views_sta_result_qualifier_get_result_qualifiers: { parameters: { query?: { + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; /** @description Comma-separated list of properties to include in the response. All properties are returned if omitted. */ properties?: ("id" | "workspaceId" | "name" | "description")[]; /** @description Comma-separated list of related resources to include in the response. */ include?: "workspace"[]; /** @description Number of items to skip. */ offset?: number | null; - /** @description The maximum number of items to return. */ - limit?: number | null; + /** @description The maximum number of items to return. (default: 100, maximum: 100000) */ + limit?: number; /** @description Select one or more fields to sort the response by. */ - sortby?: ("name" | "-name")[] | null; + sortby?: ("name" | "-name" | "+name")[]; /** @description Full-text search query. Comma-separated terms are combined with OR; whitespace-separated words within a term are combined with AND. */ q?: string | null; /** @description Filter terms by workspace ID. */ @@ -14492,16 +15196,20 @@ export interface operations { interfaces_api_views_iam_role_get_roles: { parameters: { query?: { + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; /** @description Comma-separated list of properties to include in the response. All properties are returned if omitted. */ properties?: ("id" | "workspaceId" | "name" | "description" | "permissions")[]; /** @description Comma-separated list of related resources to include in the response. */ include?: string | null; /** @description Number of items to skip. */ offset?: number | null; - /** @description The maximum number of items to return. */ - limit?: number | null; + /** @description The maximum number of items to return. (default: 100, maximum: 100000) */ + limit?: number; /** @description Select one or more fields to sort the response by. */ - sortby?: ("name" | "-name")[] | null; + sortby?: ("name" | "-name" | "+name")[]; /** @description Filter roles by workspace ID. */ workspace_id?: (string | "null")[]; }; @@ -14577,16 +15285,20 @@ export interface operations { interfaces_api_views_sta_sampled_medium_get_sampled_mediums: { parameters: { query?: { + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; /** @description Comma-separated list of properties to include in the response. All properties are returned if omitted. */ properties?: ("id" | "name" | "description")[]; /** @description Comma-separated list of related resources to include in the response. */ include?: string | null; /** @description Number of items to skip. */ offset?: number | null; - /** @description The maximum number of items to return. */ - limit?: number | null; + /** @description The maximum number of items to return. (default: 100, maximum: 100000) */ + limit?: number; /** @description Select one or more fields to sort the response by. */ - sortby?: ("name" | "-name")[] | null; + sortby?: ("name" | "-name" | "+name")[]; /** @description Full-text search query. Comma-separated terms are combined with OR; whitespace-separated words within a term are combined with AND. */ q?: string | null; }; @@ -14802,16 +15514,20 @@ export interface operations { interfaces_api_views_sta_unit_type_get_unit_types: { parameters: { query?: { + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; /** @description Comma-separated list of properties to include in the response. All properties are returned if omitted. */ properties?: ("id" | "name" | "description")[]; /** @description Comma-separated list of related resources to include in the response. */ include?: string | null; /** @description Number of items to skip. */ offset?: number | null; - /** @description The maximum number of items to return. */ - limit?: number | null; + /** @description The maximum number of items to return. (default: 100, maximum: 100000) */ + limit?: number; /** @description Select one or more fields to sort the response by. */ - sortby?: ("name" | "-name")[] | null; + sortby?: ("name" | "-name" | "+name")[]; /** @description Full-text search query. Comma-separated terms are combined with OR; whitespace-separated words within a term are combined with AND. */ q?: string | null; }; @@ -15027,16 +15743,20 @@ export interface operations { interfaces_api_views_sta_unit_get_units: { parameters: { query?: { + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; /** @description Comma-separated list of properties to include in the response. All properties are returned if omitted. */ properties?: ("id" | "workspaceId" | "name" | "symbol" | "definition" | "type")[]; /** @description Comma-separated list of related resources to include in the response. */ include?: ("workspace" | "type")[]; /** @description Number of items to skip. */ offset?: number | null; - /** @description The maximum number of items to return. */ - limit?: number | null; + /** @description The maximum number of items to return. (default: 100, maximum: 100000) */ + limit?: number; /** @description Select one or more fields to sort the response by. */ - sortby?: ("name" | "symbol" | "type" | "-name" | "-symbol" | "-type")[] | null; + sortby?: ("name" | "symbol" | "type" | "-name" | "-symbol" | "-type" | "+name" | "+symbol" | "+type")[]; /** @description Full-text search query. Comma-separated terms are combined with OR; whitespace-separated words within a term are combined with AND. */ q?: string | null; /** @description Filter units by workspace ID. */ @@ -15280,16 +16000,20 @@ export interface operations { interfaces_api_views_iam_workspace_get_workspaces: { parameters: { query?: { + /** @description Bounding box in WGS 84 longitude/latitude: min_lon,min_lat,max_lon,max_lat, or six values including min and max heights (heights are not used for filtering). A min_lon greater than max_lon crosses the antimeridian. Items without a location match any bounding box. */ + bbox?: number[] & (unknown | unknown); + /** @description Date-time or interval in RFC 3339 with a UTC offset: an instant (2024-01-01T00:00:00Z), a bounded interval (start/end), or a half-bounded interval with '..' or an empty value for the open end (../end, start/..). Items without a time match any datetime. */ + datetime?: string; /** @description Comma-separated list of properties to include in the response. All properties are returned if omitted. */ properties?: ("id" | "ownerEmail" | "pendingTransferToEmail" | "collaboratorRoleId" | "name" | "isPrivate")[]; /** @description Comma-separated list of related resources to include in the response. */ include?: ("owner" | "pendingTransferTo" | "collaboratorRole")[]; /** @description Number of items to skip. */ offset?: number | null; - /** @description The maximum number of items to return. */ - limit?: number | null; + /** @description The maximum number of items to return. (default: 100, maximum: 100000) */ + limit?: number; /** @description Select one or more fields to sort the response by. */ - sortby?: ("name" | "isPrivate" | "-name" | "-isPrivate")[] | null; + sortby?: ("name" | "isPrivate" | "-name" | "-isPrivate" | "+name" | "+isPrivate")[]; /** @description Full-text search query. Comma-separated terms are combined with OR; whitespace-separated words within a term are combined with AND. */ q?: string | null; /** @description Whether the workspace is associated with the authenticated user */ @@ -15526,12 +16250,12 @@ export interface operations { include?: ("role" | "user" | "serviceAccount")[]; /** @description Number of items to skip. */ offset?: number | null; - /** @description The maximum number of items to return. */ - limit?: number | null; + /** @description The maximum number of items to return. (default: 100, maximum: 100000) */ + limit?: number; /** @description Filter collaborators by role ID. */ role_id?: string[]; /** @description Select one or more fields to sort the response by. */ - sortby?: ("id" | "roleId" | "-id" | "-roleId")[] | null; + sortby?: ("id" | "roleId" | "-id" | "-roleId" | "+id" | "+roleId")[]; }; header?: never; path: { @@ -15734,10 +16458,10 @@ export interface operations { include?: "workspace"[]; /** @description Number of items to skip. */ offset?: number | null; - /** @description The maximum number of items to return. */ - limit?: number | null; + /** @description The maximum number of items to return. (default: 100, maximum: 100000) */ + limit?: number; /** @description Select one or more fields to sort the response by. */ - sortby?: ("name" | "isActive" | "keyExpiresAt" | "createdAt" | "lastUsedAt" | "-name" | "-isActive" | "-keyExpiresAt" | "-createdAt" | "-lastUsedAt")[] | null; + sortby?: ("name" | "isActive" | "keyExpiresAt" | "createdAt" | "lastUsedAt" | "-name" | "-isActive" | "-keyExpiresAt" | "-createdAt" | "-lastUsedAt" | "+name" | "+isActive" | "+keyExpiresAt" | "+createdAt" | "+lastUsedAt")[]; }; header?: never; path: { @@ -16131,4 +16855,55 @@ export interface operations { }; }; }; + interfaces_api_views_ogc_get_collection_metadata: { + parameters: { + query?: never; + header?: never; + path: { + collection_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CollectionResponse"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": string; + }; + }; + }; + }; + interfaces_api_views_ogc_get_conformance: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ConformanceResponse"]; + }; + }; + }; + }; } diff --git a/packages/hydroserverpy/src/hydroserverpy/api/README.md b/packages/hydroserverpy/src/hydroserverpy/api/README.md index 7ee41d76..4676c1f0 100644 --- a/packages/hydroserverpy/src/hydroserverpy/api/README.md +++ b/packages/hydroserverpy/src/hydroserverpy/api/README.md @@ -575,8 +575,6 @@ datastream = hs_api.datastreams.get(uid='00000000-0000-0000-0000-000000000000') #### Example: Create Datastream ```python -from datetime import datetime - ... # Create a new datastream on HydroServer @@ -590,11 +588,6 @@ new_datastream = hs_api.datastreams.create( time_aggregation_interval=1, status='Ongoing', result_type='Timeseries', - value_count=0, - phenomenon_begin_time=datetime(year=2024, month=1, day=1), - phenomenon_end_time=None, - result_begin_time=datetime(year=2024, month=1, day=1), - result_end_time=None, is_visible=True, is_private=False, monitoring_site='00000000-0000-0000-0000-000000000000', diff --git a/packages/hydroserverpy/src/hydroserverpy/api/client.py b/packages/hydroserverpy/src/hydroserverpy/api/client.py index c413d744..16ceff2c 100644 --- a/packages/hydroserverpy/src/hydroserverpy/api/client.py +++ b/packages/hydroserverpy/src/hydroserverpy/api/client.py @@ -26,7 +26,7 @@ class HydroServer: def __init__( self, host: str, - base_route: str = "/api/data", + base_route: str = "/api/ogc", email: Optional[str] = None, password: Optional[str] = None, apikey: Optional[str] = None, @@ -46,9 +46,28 @@ def __init__( self._init_session() + @staticmethod + def _encode_query_value(value): + """ + Encodes booleans as the lowercase 'true' and 'false' the API requires; requests would + otherwise send str(value), i.e. 'True' and 'False'. + """ + + if isinstance(value, bool): + return "true" if value else "false" + if isinstance(value, (list, tuple)): + return [HydroServer._encode_query_value(item) for item in value] + + return value + def request(self, method, path, *args, **kwargs) -> requests.Response: """Sends a request to HydroServer's API.""" + if isinstance(kwargs.get("params"), dict): + kwargs["params"] = { + key: self._encode_query_value(value) for key, value in kwargs["params"].items() + } + for attempt in range(2): try: response = getattr(self._session, method)( diff --git a/packages/hydroserverpy/src/hydroserverpy/api/models/etl/data_connection.py b/packages/hydroserverpy/src/hydroserverpy/api/models/etl/data_connection.py index 823dbde5..bee71f37 100644 --- a/packages/hydroserverpy/src/hydroserverpy/api/models/etl/data_connection.py +++ b/packages/hydroserverpy/src/hydroserverpy/api/models/etl/data_connection.py @@ -109,4 +109,4 @@ def save(self): @classmethod def get_route(cls): - return "etl-data-connections" + return "collections/etl-data-connections/items" diff --git a/packages/hydroserverpy/src/hydroserverpy/api/models/etl/mapping.py b/packages/hydroserverpy/src/hydroserverpy/api/models/etl/mapping.py index 66f893a5..c4d2ca83 100644 --- a/packages/hydroserverpy/src/hydroserverpy/api/models/etl/mapping.py +++ b/packages/hydroserverpy/src/hydroserverpy/api/models/etl/mapping.py @@ -23,4 +23,4 @@ def __init__(self, client: "HydroServer", **data): @classmethod def get_route(cls): - return "etl-mappings" + return "collections/etl-mappings/items" diff --git a/packages/hydroserverpy/src/hydroserverpy/api/models/etl/task.py b/packages/hydroserverpy/src/hydroserverpy/api/models/etl/task.py index cc233c12..763684f2 100644 --- a/packages/hydroserverpy/src/hydroserverpy/api/models/etl/task.py +++ b/packages/hydroserverpy/src/hydroserverpy/api/models/etl/task.py @@ -62,7 +62,7 @@ def save(self): @classmethod def get_route(cls): - return "etl-tasks" + return "collections/etl-tasks/items" @cached_property def mappings(self) -> List[EtlMapping]: diff --git a/packages/hydroserverpy/src/hydroserverpy/api/models/iam/role.py b/packages/hydroserverpy/src/hydroserverpy/api/models/iam/role.py index 0a7426bd..891d2bc4 100644 --- a/packages/hydroserverpy/src/hydroserverpy/api/models/iam/role.py +++ b/packages/hydroserverpy/src/hydroserverpy/api/models/iam/role.py @@ -20,7 +20,7 @@ def __init__(self, client: "HydroServer", **data): @classmethod def get_route(cls): - return "roles" + return "collections/roles/items" @property def workspace(self) -> "Workspace": diff --git a/packages/hydroserverpy/src/hydroserverpy/api/models/iam/workspace.py b/packages/hydroserverpy/src/hydroserverpy/api/models/iam/workspace.py index bc546514..22a30b2c 100644 --- a/packages/hydroserverpy/src/hydroserverpy/api/models/iam/workspace.py +++ b/packages/hydroserverpy/src/hydroserverpy/api/models/iam/workspace.py @@ -55,7 +55,7 @@ def __init__(self, client: "HydroServer", **data): @classmethod def get_route(cls): - return "workspaces" + return "collections/workspaces/items" @property def roles(self) -> List["Role"]: diff --git a/packages/hydroserverpy/src/hydroserverpy/api/models/monitoring/rule.py b/packages/hydroserverpy/src/hydroserverpy/api/models/monitoring/rule.py index a71f32e1..385dcb05 100644 --- a/packages/hydroserverpy/src/hydroserverpy/api/models/monitoring/rule.py +++ b/packages/hydroserverpy/src/hydroserverpy/api/models/monitoring/rule.py @@ -32,4 +32,4 @@ def __init__(self, client: "HydroServer", **data): @classmethod def get_route(cls): - return "monitoring-rules" + return "collections/monitoring-rules/items" diff --git a/packages/hydroserverpy/src/hydroserverpy/api/models/monitoring/task.py b/packages/hydroserverpy/src/hydroserverpy/api/models/monitoring/task.py index 8b206204..df20928a 100644 --- a/packages/hydroserverpy/src/hydroserverpy/api/models/monitoring/task.py +++ b/packages/hydroserverpy/src/hydroserverpy/api/models/monitoring/task.py @@ -63,7 +63,7 @@ def __init__(self, client: "HydroServer", **data): @classmethod def get_route(cls): - return "monitoring-tasks" + return "collections/monitoring-tasks/items" @cached_property def rules(self): diff --git a/packages/hydroserverpy/src/hydroserverpy/api/models/products/rating_curve.py b/packages/hydroserverpy/src/hydroserverpy/api/models/products/rating_curve.py index d747db02..d92f45d3 100644 --- a/packages/hydroserverpy/src/hydroserverpy/api/models/products/rating_curve.py +++ b/packages/hydroserverpy/src/hydroserverpy/api/models/products/rating_curve.py @@ -41,4 +41,4 @@ def save(self): @classmethod def get_route(cls): - return "data-product-rating-curves" + return "collections/data-product-rating-curves/items" diff --git a/packages/hydroserverpy/src/hydroserverpy/api/models/products/task.py b/packages/hydroserverpy/src/hydroserverpy/api/models/products/task.py index 0be63ffc..ba3929c8 100644 --- a/packages/hydroserverpy/src/hydroserverpy/api/models/products/task.py +++ b/packages/hydroserverpy/src/hydroserverpy/api/models/products/task.py @@ -38,7 +38,7 @@ def __init__(self, client: "HydroServer", **data): @classmethod def get_route(cls): - return "data-product-tasks" + return "collections/data-product-tasks/items" @cached_property def rating_curve_transformations(self) -> List["RatingCurveTransformation"]: diff --git a/packages/hydroserverpy/src/hydroserverpy/api/models/sta/datastream.py b/packages/hydroserverpy/src/hydroserverpy/api/models/sta/datastream.py index 5f512a62..29062202 100644 --- a/packages/hydroserverpy/src/hydroserverpy/api/models/sta/datastream.py +++ b/packages/hydroserverpy/src/hydroserverpy/api/models/sta/datastream.py @@ -52,8 +52,7 @@ class Datastream(HydroServerBaseModel): _editable_fields: ClassVar[set[str]] = { "name", "description", "observation_type", "sampled_medium", "no_data_value", "aggregation_statistic", - "time_aggregation_interval", "status", "result_type", "value_count", "phenomenon_begin_time", - "phenomenon_end_time", "result_begin_time", "result_end_time", "is_private", "is_visible", + "time_aggregation_interval", "status", "result_type", "is_private", "is_visible", "time_aggregation_interval_unit", "intended_time_spacing", "intended_time_spacing_unit", "monitoring_site_id", "method_id", "observed_property_id", "processing_level_id", "unit_id" } @@ -70,7 +69,7 @@ def __init__(self, client: "HydroServer", **data): @classmethod def get_route(cls): - return "datastreams" + return "collections/datastreams/items" @property def workspace(self) -> "Workspace": @@ -215,28 +214,13 @@ def delete_observations( phenomenon_time_end=phenomenon_time_end, ) - # TODO: Find a better long-term solution for this issue. def sync_phenomenon_end_time(self): - """Ensures the phenomenon_end_time field matches the actual end time of the observations.""" - - path = f"/{self.client.base_route}/observations" - response = self.client.request( - "get", - path, - params={ - "datastream_id": str(self.uid), - "limit": 1, - "sortby": "-phenomenonTime", - }, - ).json() - - response = response["data"] - if len(response) > 0: - self.phenomenon_end_time = datetime.fromisoformat(response[0]["phenomenonTime"]) - else: - self.phenomenon_end_time = None - - self.save() + """ + Reloads phenomenon_end_time and the datastream's other observation statistics, which + HydroServer maintains whenever observations are written. + """ + + self.refresh() @field_validator("linked_resources", mode="before") def transform_linked_resources(cls, v): diff --git a/packages/hydroserverpy/src/hydroserverpy/api/models/sta/method.py b/packages/hydroserverpy/src/hydroserverpy/api/models/sta/method.py index 3900dc1d..aa5e9726 100644 --- a/packages/hydroserverpy/src/hydroserverpy/api/models/sta/method.py +++ b/packages/hydroserverpy/src/hydroserverpy/api/models/sta/method.py @@ -37,7 +37,7 @@ def __init__(self, client: "HydroServer", **data): @classmethod def get_route(cls): - return "methods" + return "collections/methods/items" @property def workspace(self) -> Optional["Workspace"]: diff --git a/packages/hydroserverpy/src/hydroserverpy/api/models/sta/monitoring_site.py b/packages/hydroserverpy/src/hydroserverpy/api/models/sta/monitoring_site.py index aef7d197..ce192517 100644 --- a/packages/hydroserverpy/src/hydroserverpy/api/models/sta/monitoring_site.py +++ b/packages/hydroserverpy/src/hydroserverpy/api/models/sta/monitoring_site.py @@ -44,7 +44,7 @@ def __init__(self, client: "HydroServer", **data): @classmethod def get_route(cls): - return "monitoring-sites" + return "collections/monitoring-sites/items" @property def workspace(self) -> "Workspace": diff --git a/packages/hydroserverpy/src/hydroserverpy/api/models/sta/observed_property.py b/packages/hydroserverpy/src/hydroserverpy/api/models/sta/observed_property.py index de577775..72c697d7 100644 --- a/packages/hydroserverpy/src/hydroserverpy/api/models/sta/observed_property.py +++ b/packages/hydroserverpy/src/hydroserverpy/api/models/sta/observed_property.py @@ -25,7 +25,7 @@ def __init__(self, client: "HydroServer", **data): @classmethod def get_route(cls): - return "observed-properties" + return "collections/observed-properties/items" @property def workspace(self) -> Optional["Workspace"]: diff --git a/packages/hydroserverpy/src/hydroserverpy/api/models/sta/processing_level.py b/packages/hydroserverpy/src/hydroserverpy/api/models/sta/processing_level.py index eeb26556..ba5af1fc 100644 --- a/packages/hydroserverpy/src/hydroserverpy/api/models/sta/processing_level.py +++ b/packages/hydroserverpy/src/hydroserverpy/api/models/sta/processing_level.py @@ -29,7 +29,7 @@ def __init__(self, client: "HydroServer", **data): @classmethod def get_route(cls): - return "processing-levels" + return "collections/processing-levels/items" @property def workspace(self) -> Optional["Workspace"]: diff --git a/packages/hydroserverpy/src/hydroserverpy/api/models/sta/result_qualifier.py b/packages/hydroserverpy/src/hydroserverpy/api/models/sta/result_qualifier.py index 3e96509f..4e66c030 100644 --- a/packages/hydroserverpy/src/hydroserverpy/api/models/sta/result_qualifier.py +++ b/packages/hydroserverpy/src/hydroserverpy/api/models/sta/result_qualifier.py @@ -22,7 +22,7 @@ def __init__(self, client: "HydroServer", **data): @classmethod def get_route(cls): - return "result-qualifiers" + return "collections/result-qualifiers/items" @property def workspace(self) -> Optional["Workspace"]: diff --git a/packages/hydroserverpy/src/hydroserverpy/api/models/sta/unit.py b/packages/hydroserverpy/src/hydroserverpy/api/models/sta/unit.py index b13c8b4c..a401cd55 100644 --- a/packages/hydroserverpy/src/hydroserverpy/api/models/sta/unit.py +++ b/packages/hydroserverpy/src/hydroserverpy/api/models/sta/unit.py @@ -24,7 +24,7 @@ def __init__(self, client: "HydroServer", **data): @classmethod def get_route(cls): - return "units" + return "collections/units/items" @property def workspace(self) -> Optional["Workspace"]: diff --git a/packages/hydroserverpy/src/hydroserverpy/api/services/etl/task.py b/packages/hydroserverpy/src/hydroserverpy/api/services/etl/task.py index 5d9c7697..474c4b46 100644 --- a/packages/hydroserverpy/src/hydroserverpy/api/services/etl/task.py +++ b/packages/hydroserverpy/src/hydroserverpy/api/services/etl/task.py @@ -118,7 +118,7 @@ def trigger(self, uid: Union[UUID, str]) -> TaskRun: """Trigger an immediate run of an ETL task.""" path = f"/{self.client.base_route}/{self.model.get_route()}/{str(uid)}/trigger" - response = self.client.request("post", path).json() + response = self.client.request("post", path).json()["data"] return TaskRun(**response) @@ -157,4 +157,4 @@ def get_run(self, uid: Union[UUID, str], run_id: Union[UUID, str]) -> TaskRun: path = f"/{self.client.base_route}/{self.model.get_route()}/{str(uid)}/runs/{str(run_id)}" - return TaskRun(**self.client.request("get", path).json()) + return TaskRun(**self.client.request("get", path).json()["data"]) diff --git a/packages/hydroserverpy/src/hydroserverpy/api/services/monitoring/task.py b/packages/hydroserverpy/src/hydroserverpy/api/services/monitoring/task.py index 67b872e2..691f3d70 100644 --- a/packages/hydroserverpy/src/hydroserverpy/api/services/monitoring/task.py +++ b/packages/hydroserverpy/src/hydroserverpy/api/services/monitoring/task.py @@ -114,7 +114,7 @@ def trigger(self, uid: Union[UUID, str]) -> TaskRun: """Trigger an immediate run of a monitoring task.""" path = f"/{self.client.base_route}/{self.model.get_route()}/{str(uid)}/trigger" - response = self.client.request("post", path).json() + response = self.client.request("post", path).json()["data"] return TaskRun(**response) @@ -153,6 +153,6 @@ def get_run(self, uid: Union[UUID, str], run_id: Union[UUID, str]) -> TaskRun: """Fetch a single task run for a monitoring task.""" path = f"/{self.client.base_route}/{self.model.get_route()}/{str(uid)}/runs/{str(run_id)}" - response = self.client.request("get", path).json() + response = self.client.request("get", path).json()["data"] return TaskRun(**response) diff --git a/packages/hydroserverpy/src/hydroserverpy/api/services/products/task.py b/packages/hydroserverpy/src/hydroserverpy/api/services/products/task.py index 65096f5a..7ab45398 100644 --- a/packages/hydroserverpy/src/hydroserverpy/api/services/products/task.py +++ b/packages/hydroserverpy/src/hydroserverpy/api/services/products/task.py @@ -114,7 +114,7 @@ def trigger(self, uid: Union[UUID, str]) -> TaskRun: """Trigger an immediate run of a data product task.""" path = f"/{self.client.base_route}/{self.model.get_route()}/{str(uid)}/trigger" - response = self.client.request("post", path).json() + response = self.client.request("post", path).json()["data"] return TaskRun(**response) @@ -153,4 +153,4 @@ def get_run(self, uid: Union[UUID, str], run_id: Union[UUID, str]) -> TaskRun: path = f"/{self.client.base_route}/{self.model.get_route()}/{str(uid)}/runs/{str(run_id)}" - return TaskRun(**self.client.request("get", path).json()) \ No newline at end of file + return TaskRun(**self.client.request("get", path).json()["data"]) \ No newline at end of file diff --git a/packages/hydroserverpy/src/hydroserverpy/api/services/products/transformation.py b/packages/hydroserverpy/src/hydroserverpy/api/services/products/transformation.py index fba949ce..73ab43cd 100644 --- a/packages/hydroserverpy/src/hydroserverpy/api/services/products/transformation.py +++ b/packages/hydroserverpy/src/hydroserverpy/api/services/products/transformation.py @@ -32,7 +32,7 @@ def default_serializer(obj): raise TypeError(f"Type {type(obj)} not serializable") def _route(self) -> str: - return f"/{self.client.base_route}/data-product-transformations" + return f"/{self.client.base_route}/collections/data-product-transformations/items" def _list( self, diff --git a/packages/hydroserverpy/src/hydroserverpy/api/services/sta/datastream.py b/packages/hydroserverpy/src/hydroserverpy/api/services/sta/datastream.py index f1934645..83a5cbd5 100644 --- a/packages/hydroserverpy/src/hydroserverpy/api/services/sta/datastream.py +++ b/packages/hydroserverpy/src/hydroserverpy/api/services/sta/datastream.py @@ -5,7 +5,7 @@ from datetime import datetime from pydantic.alias_generators import to_camel from hydroserverpy.api.models import Datastream, ObservationCollection -from hydroserverpy.api.utils import normalize_uuid +from hydroserverpy.api.utils import normalize_uuid, build_datetime_interval from ..base import HydroServerBaseService if TYPE_CHECKING: @@ -44,19 +44,15 @@ def list( is_private: bool = ..., value_count_max: int = ..., value_count_min: int = ..., - phenomenon_begin_time_max: datetime = ..., - phenomenon_begin_time_min: datetime = ..., - phenomenon_end_time_max: datetime = ..., - phenomenon_end_time_min: datetime = ..., - result_begin_time_max: datetime = ..., - result_begin_time_min: datetime = ..., - result_end_time_max: datetime = ..., - result_end_time_min: datetime = ..., + phenomenon_time_max: datetime = ..., + phenomenon_time_min: datetime = ..., fetch_all: bool = False, ) -> List["Workspace"]: - """Fetch a collection of HydroServer workspaces.""" + """ + Fetch a collection of HydroServer datastreams. + """ - return super().list( + collection = super().list( offset=offset, limit=limit, sortby=sortby, @@ -74,17 +70,25 @@ def list( is_private=is_private, value_count_max=value_count_max, value_count_min=value_count_min, - phenomenon_begin_time_max=phenomenon_begin_time_max, - phenomenon_begin_time_min=phenomenon_begin_time_min, - phenomenon_end_time_max=phenomenon_end_time_max, - phenomenon_end_time_min=phenomenon_end_time_min, - result_begin_time_max=result_begin_time_max, - result_begin_time_min=result_begin_time_min, - result_end_time_max=result_end_time_max, - result_end_time_min=result_end_time_min, - fetch_all=fetch_all, + datetime=build_datetime_interval(phenomenon_time_min, phenomenon_time_max), + fetch_all=False, ) + collection.filters.pop("datetime", None) + collection.filters.update({ + k: v + for k, v in { + "phenomenon_time_max": phenomenon_time_max, + "phenomenon_time_min": phenomenon_time_min, + }.items() + if v is not ... + }) + + if fetch_all is True: + collection = collection.fetch_all() + + return collection + def create( self, name: str, @@ -106,11 +110,6 @@ def create( Literal["seconds", "minutes", "hours", "days"] ] = None, status: Optional[str] = None, - value_count: Optional[int] = None, - phenomenon_begin_time: Optional[datetime] = None, - phenomenon_end_time: Optional[datetime] = None, - result_begin_time: Optional[datetime] = None, - result_end_time: Optional[datetime] = None, is_private: bool = False, is_visible: bool = True, tags: Optional[Dict[str, str]] = None, @@ -137,11 +136,6 @@ def create( "intendedTimeSpacing": intended_time_spacing, "intendedTimeSpacingUnit": intended_time_spacing_unit, "status": status, - "valueCount": value_count, - "phenomenonBeginTime": phenomenon_begin_time, - "phenomenonEndTime": phenomenon_end_time, - "resultBeginTime": result_begin_time, - "resultEndTime": result_end_time, "isPrivate": is_private, "isVisible": is_visible, "tags": tags or {}, @@ -178,11 +172,6 @@ def update( Literal["seconds", "minutes", "hours", "days"] ] = ..., status: Optional[str] = ..., - value_count: Optional[int] = ..., - phenomenon_begin_time: Optional[datetime] = ..., - phenomenon_end_time: Optional[datetime] = ..., - result_begin_time: Optional[datetime] = ..., - result_end_time: Optional[datetime] = ..., is_private: bool = ..., is_visible: bool = ..., tags: Dict[str, Optional[str]] = ..., @@ -211,11 +200,6 @@ def update( "intendedTimeSpacing": intended_time_spacing, "intendedTimeSpacingUnit": intended_time_spacing_unit, "status": status, - "valueCount": value_count, - "phenomenonBeginTime": phenomenon_begin_time, - "phenomenonEndTime": phenomenon_end_time, - "resultBeginTime": result_begin_time, - "resultEndTime": result_end_time, "isPrivate": is_private, "isVisible": is_visible, "tags": tags, @@ -251,8 +235,7 @@ def get_observations( "offset": offset, "limit": limit, "sortby": ",".join(sortby) if sortby is not ... else sortby, - "phenomenon_time_max": phenomenon_time_max, - "phenomenon_time_min": phenomenon_time_min, + "datetime": build_datetime_interval(phenomenon_time_min, phenomenon_time_max), "result_qualifier_code": result_qualifier_code, "format": "column" } @@ -262,7 +245,7 @@ def get_observations( if v is not ... } - path = f"/{self.client.base_route}/observations" + path = f"/{self.client.base_route}/collections/observations/items" response = self.client.request("get", path, params=params) datastream = self.get(uid=uid) collection = ObservationCollection( @@ -271,8 +254,12 @@ def get_observations( sortby=sortby if sortby is not ... else None, filters={ k: v - for k, v in params.items() - if k not in ["datastream_id", "offset", "limit", "sortby", "format"] + for k, v in { + "phenomenon_time_max": phenomenon_time_max, + "phenomenon_time_min": phenomenon_time_min, + "result_qualifier_code": result_qualifier_code, + }.items() + if v is not ... }, ) if fetch_all is True: @@ -288,7 +275,7 @@ def load_observations( ) -> None: """Load observations to a datastream.""" - path = f"/{self.client.base_route}/observations/bulk-create" + path = f"/{self.client.base_route}/collections/observations/bulk-create" headers = {"Content-type": "application/json"} params = {"mode": mode} body = { @@ -309,7 +296,7 @@ def delete_observations( ) -> None: """Delete observations from a datastream.""" - path = f"/{self.client.base_route}/observations/bulk-delete" + path = f"/{self.client.base_route}/collections/observations/bulk-delete" headers = {"Content-type": "application/json"} body = {"datastreamId": str(uid)} @@ -351,16 +338,36 @@ def add_linked_resource( "post", path, data=data, files={"file": file} if file is not None else None ).json() - return next( - r for r in self.get_linked_resources(uid) if r["id"] == response["id"] - ) + return self.get_linked_resource(uid=uid, linked_resource_id=response["id"]) + + def get_linked_resources( + self, + uid: Union[UUID, str], + offset: int = ..., + limit: int = ..., + type: List[str] = ..., + ) -> List[Dict[str, str]]: + """Get a page of linked resources associated with a HydroServer datastream.""" - def get_linked_resources(self, uid: Union[UUID, str]) -> List[Dict[str, str]]: - """Get all linked resources associated with a HydroServer datastream.""" + params = { + "offset": offset, + "limit": limit, + "type": type, + } + params = {k: v for k, v in params.items() if v is not ...} path = f"/{self.client.base_route}/{self.model.get_route()}/{str(uid)}/linked-resources" - return self.client.request("get", path).json() + return self.client.request("get", path, params=params).json()["data"] + + def get_linked_resource( + self, uid: Union[UUID, str], linked_resource_id: Union[UUID, str] + ) -> Dict[str, str]: + """Get a linked resource associated with a HydroServer datastream.""" + + path = f"/{self.client.base_route}/{self.model.get_route()}/{str(uid)}/linked-resources/{str(linked_resource_id)}" + + return self.client.request("get", path).json()["data"] def update_linked_resource( self, @@ -392,9 +399,7 @@ def update_linked_resource( "patch", path, data=data, files={"file": file} if file is not None else None ) - return next( - r for r in self.get_linked_resources(uid) if r["id"] == str(linked_resource_id) - ) + return self.get_linked_resource(uid=uid, linked_resource_id=linked_resource_id) def delete_linked_resource(self, uid: Union[UUID, str], linked_resource_id: Union[UUID, str]) -> None: """Delete a linked resource from a HydroServer datastream.""" diff --git a/packages/hydroserverpy/src/hydroserverpy/api/services/sta/monitoring_site.py b/packages/hydroserverpy/src/hydroserverpy/api/services/sta/monitoring_site.py index c084ea68..45632e79 100644 --- a/packages/hydroserverpy/src/hydroserverpy/api/services/sta/monitoring_site.py +++ b/packages/hydroserverpy/src/hydroserverpy/api/services/sta/monitoring_site.py @@ -167,16 +167,36 @@ def add_linked_resource( "post", path, data=data, files={"file": file} if file is not None else None ).json() - return next( - r for r in self.get_linked_resources(uid) if r["id"] == response["id"] - ) + return self.get_linked_resource(uid=uid, linked_resource_id=response["id"]) - def get_linked_resources(self, uid: Union[UUID, str]) -> List[Dict[str, str]]: - """Get all linked resources associated with a HydroServer monitoring_site.""" + def get_linked_resources( + self, + uid: Union[UUID, str], + offset: int = ..., + limit: int = ..., + type: List[str] = ..., + ) -> List[Dict[str, str]]: + """Get a page of linked resources associated with a HydroServer monitoring_site.""" + + params = { + "offset": offset, + "limit": limit, + "type": type, + } + params = {k: v for k, v in params.items() if v is not ...} path = f"/{self.client.base_route}/{self.model.get_route()}/{str(uid)}/linked-resources" - return self.client.request("get", path).json() + return self.client.request("get", path, params=params).json()["data"] + + def get_linked_resource( + self, uid: Union[UUID, str], linked_resource_id: Union[UUID, str] + ) -> Dict[str, str]: + """Get a linked resource associated with a HydroServer monitoring_site.""" + + path = f"/{self.client.base_route}/{self.model.get_route()}/{str(uid)}/linked-resources/{str(linked_resource_id)}" + + return self.client.request("get", path).json()["data"] def update_linked_resource( self, @@ -208,9 +228,7 @@ def update_linked_resource( "patch", path, data=data, files={"file": file} if file is not None else None ) - return next( - r for r in self.get_linked_resources(uid) if r["id"] == str(linked_resource_id) - ) + return self.get_linked_resource(uid=uid, linked_resource_id=linked_resource_id) def delete_linked_resource(self, uid: Union[UUID, str], linked_resource_id: Union[UUID, str]) -> None: """Delete a linked resource from a HydroServer monitoring_site.""" diff --git a/packages/hydroserverpy/src/hydroserverpy/api/utils.py b/packages/hydroserverpy/src/hydroserverpy/api/utils.py index 1abc7b7c..c527171d 100644 --- a/packages/hydroserverpy/src/hydroserverpy/api/utils.py +++ b/packages/hydroserverpy/src/hydroserverpy/api/utils.py @@ -1,4 +1,5 @@ from uuid import UUID +from datetime import datetime, timezone from typing import Union, Optional, Any from pydantic.alias_generators import to_camel @@ -20,3 +21,31 @@ def sortby_to_camel(s: str) -> str: if s.startswith('-'): return '-' + to_camel(s[1:]) return to_camel(s) + + +def build_datetime_interval( + start: Optional[Union[datetime, str, Any]], + end: Optional[Union[datetime, str, Any]], +): + """ + Builds an OGC API datetime interval ("start/end", with ".." for an open end) from optional + start and end values. Returns ... when neither is set. Datetimes and ISO 8601 strings without + a UTC offset are treated as UTC. + """ + + def to_rfc3339(value) -> str: + if value is ... or value is None: + return ".." + if isinstance(value, str): + try: + value = datetime.fromisoformat(value) + except ValueError: + return value + if value.tzinfo is None: + value = value.replace(tzinfo=timezone.utc) + return value.isoformat() + + if start in (..., None) and end in (..., None): + return ... + + return f"{to_rfc3339(start)}/{to_rfc3339(end)}" diff --git a/packages/hydroserverpy/tests/api/test_base_service.py b/packages/hydroserverpy/tests/api/test_base_service.py index 0dbccdea..2f916d50 100644 --- a/packages/hydroserverpy/tests/api/test_base_service.py +++ b/packages/hydroserverpy/tests/api/test_base_service.py @@ -32,7 +32,7 @@ def json(self): def make_client(): client = MagicMock() - client.base_route = "/api/data" + client.base_route = "/api/ogc" return client diff --git a/packages/hydroserverpy/tests/api/test_collection.py b/packages/hydroserverpy/tests/api/test_collection.py index 5453e756..5f9b81db 100644 --- a/packages/hydroserverpy/tests/api/test_collection.py +++ b/packages/hydroserverpy/tests/api/test_collection.py @@ -32,7 +32,7 @@ def json(self): def make_client(): client = MagicMock() - client.base_route = "/api/data" + client.base_route = "/api/ogc" return client diff --git a/packages/hydroserverpy/tests/api/test_datastream_datetime.py b/packages/hydroserverpy/tests/api/test_datastream_datetime.py new file mode 100644 index 00000000..809e4642 --- /dev/null +++ b/packages/hydroserverpy/tests/api/test_datastream_datetime.py @@ -0,0 +1,194 @@ +import json +from datetime import datetime, timezone +from unittest.mock import MagicMock + +import pytest + +from hydroserverpy.api.services.sta.datastream import DatastreamService +from hydroserverpy.api.utils import build_datetime_interval + + +class FakeResponse: + def __init__(self, payload, status_code=200): + self._payload = payload + self.status_code = status_code + + def json(self): + return dict(self._payload) + + +def make_service(*payloads): + client = MagicMock() + client.base_route = "/api/ogc" + client.request.side_effect = [FakeResponse(payload) for payload in payloads] + return DatastreamService(client), client + + +def sent_params(client, call=0): + return client.request.call_args_list[call].kwargs["params"] + + +JAN_1 = datetime(2024, 1, 1, tzinfo=timezone.utc) +FEB_1 = datetime(2024, 2, 1, tzinfo=timezone.utc) + + +# --- build_datetime_interval --------------------------------------------------------------- + + +@pytest.mark.parametrize( + "start, end, expected", + [ + (..., ..., ...), + (None, None, ...), + (JAN_1, ..., "2024-01-01T00:00:00+00:00/.."), + (..., JAN_1, "../2024-01-01T00:00:00+00:00"), + (datetime(2024, 1, 1), None, "2024-01-01T00:00:00+00:00/.."), + ("2024-01-01", "2024-02-01T00:00:00Z", "2024-01-01T00:00:00+00:00/2024-02-01T00:00:00+00:00"), + ], +) +def test_build_datetime_interval(start, end, expected): + assert build_datetime_interval(start, end) == expected + + +# --- DatastreamService.list ----------------------------------------------------------------- + + +def test_list_sends_phenomenon_time_bounds_as_a_datetime_interval(): + service, client = make_service({"data": [], "meta": {"offset": 0, "limit": 100, "totalCount": 0}}) + + service.list(phenomenon_time_min=JAN_1) + + params = sent_params(client) + assert params["datetime"] == "2024-01-01T00:00:00+00:00/.." + assert "phenomenon_time_min" not in params + + +def test_list_omits_datetime_without_bounds(): + service, client = make_service({"data": [], "meta": {"offset": 0, "limit": 100, "totalCount": 0}}) + + service.list() + + assert "datetime" not in sent_params(client) + + +def test_list_keeps_the_phenomenon_time_bounds_when_paging(): + page = {"data": [], "meta": {"offset": 0, "limit": 100, "totalCount": 0}} + service, client = make_service(page, page) + + collection = service.list(phenomenon_time_min=JAN_1, phenomenon_time_max=JAN_1) + assert collection.filters["phenomenon_time_min"] == JAN_1 + assert "datetime" not in collection.filters + + collection.next_page() + + assert sent_params(client, 1)["datetime"] == sent_params(client, 0)["datetime"] + assert sent_params(client, 1)["offset"] == 100 + + +def test_list_fetch_all_keeps_the_phenomenon_time_bounds(): + full_page = { + "data": [{"id": f"00000000-0000-0000-0000-00000000000{i}"} for i in range(2)], + "meta": {"offset": 0, "limit": 2, "totalCount": 3}, + } + last_page = {"data": [], "meta": {"offset": 2, "limit": 2, "totalCount": 3}} + service, client = make_service(full_page, last_page) + service.model = MagicMock() + + service.list(limit=2, phenomenon_time_min=JAN_1, fetch_all=True) + + assert client.request.call_count == 2 + assert sent_params(client, 1)["datetime"] == "2024-01-01T00:00:00+00:00/.." + + +# --- DatastreamService.get_observations ----------------------------------------------------- + + +def test_get_observations_sends_a_datetime_interval_and_pages_with_the_same_bounds(): + columnar = { + "data": {"phenomenonTime": [], "result": [], "resultQualifierCodes": []}, + "meta": {"offset": 0, "limit": 1, "totalCount": 0}, + } + service, client = make_service(columnar, columnar) + service.get = MagicMock() + + collection = service.get_observations(uid="ds-1", limit=1, phenomenon_time_min=JAN_1) + assert collection.filters == {"phenomenon_time_min": JAN_1} + + service.get_observations(uid="ds-1", **collection.filters, offset=1, limit=1) + + assert sent_params(client, 0)["datetime"] == "2024-01-01T00:00:00+00:00/.." + assert sent_params(client, 1)["datetime"] == "2024-01-01T00:00:00+00:00/.." + assert "phenomenon_time_min" not in sent_params(client, 0) + + +# --- server-maintained statistics ------------------------------------------------------- + +STATISTIC_KEYS = {"valueCount", "phenomenonBeginTime", "phenomenonEndTime", "resultBeginTime", "resultEndTime"} + + +def _datastream_payload(**overrides): + payload = { + "id": "00000000-0000-0000-0000-000000000001", + "name": "Datastream", + "description": "", + "observationType": "OM_Measurement", + "sampledMedium": "Water", + "noDataValue": -9999, + "aggregationStatistic": "Average", + "timeAggregationInterval": 15, + "timeAggregationIntervalUnit": "minutes", + "resultType": "Time Series Coverage", + "valueCount": 3, + "phenomenonEndTime": "2024-01-01T00:00:00+00:00", + "monitoringSiteId": "00000000-0000-0000-0000-000000000002", + "workspaceId": "00000000-0000-0000-0000-000000000003", + "methodId": "00000000-0000-0000-0000-000000000004", + "observedPropertyId": "00000000-0000-0000-0000-000000000005", + "processingLevelId": "00000000-0000-0000-0000-000000000006", + "unitId": "00000000-0000-0000-0000-000000000007", + "tags": {}, + "linkedResources": {}, + } + payload.update(overrides) + return payload + + +def test_create_does_not_send_server_maintained_statistics(): + service, client = make_service({"id": "00000000-0000-0000-0000-000000000001"}, {"data": _datastream_payload()}) + client.datastreams = service + + service.create( + name="Datastream", description="", monitoring_site="s", method="m", observed_property="o", + processing_level="p", unit="u", observation_type="OM_Measurement", result_type="Time Series Coverage", + sampled_medium="Water", no_data_value=-9999, aggregation_statistic="Average", + time_aggregation_interval=15, time_aggregation_interval_unit="minutes", + ) + + assert not set(json.loads(client.request.call_args_list[0].kwargs["data"])) & STATISTIC_KEYS + + +def test_save_does_not_send_changed_server_maintained_statistics(): + service, client = make_service({"data": _datastream_payload()}) + client.datastreams = service + datastream = service.get("00000000-0000-0000-0000-000000000001") + + datastream.value_count = 99 + datastream.phenomenon_begin_time = JAN_1 + datastream.phenomenon_end_time = FEB_1 + + assert datastream.unsaved_changes == {} + + +def test_sync_phenomenon_end_time_reloads_it_from_hydroserver(): + service, client = make_service( + {"data": _datastream_payload()}, + {"data": _datastream_payload(phenomenonEndTime="2024-02-01T00:00:00+00:00")}, + ) + client.datastreams = service + datastream = service.get("00000000-0000-0000-0000-000000000001") + + datastream.sync_phenomenon_end_time() + + assert datastream.phenomenon_end_time == FEB_1 + assert client.request.call_args_list[1].args[0] == "get" + diff --git a/packages/hydroserverpy/tests/api/test_etl_mapping.py b/packages/hydroserverpy/tests/api/test_etl_mapping.py index f591ae4c..86046d1a 100644 --- a/packages/hydroserverpy/tests/api/test_etl_mapping.py +++ b/packages/hydroserverpy/tests/api/test_etl_mapping.py @@ -18,7 +18,7 @@ def json(self): def make_client(): client = MagicMock() - client.base_route = "/api/data" + client.base_route = "/api/ogc" return client @@ -49,12 +49,12 @@ def test_create_mapping_posts_to_flat_route_with_etl_task_id_in_body(): post_call = client.request.call_args_list[0] assert post_call.args[0] == "post" - assert post_call.args[1] == "//api/data/etl-mappings" + assert post_call.args[1] == "//api/ogc/collections/etl-mappings/items" sent_body = json.loads(post_call.kwargs["data"]) assert sent_body["etlTaskId"] == str(task_id) get_call = client.request.call_args_list[1] - assert get_call.args[1] == f"//api/data/etl-mappings/{mapping_id}" + assert get_call.args[1] == f"//api/ogc/collections/etl-mappings/items/{mapping_id}" assert str(mapping.uid) == mapping_id assert mapping.source_identifier == "sensor_1" @@ -87,7 +87,7 @@ def test_update_mapping_patches_flat_route_and_refetches_after_204(): patch_call = client.request.call_args_list[0] assert patch_call.args[0] == "patch" - assert patch_call.args[1] == f"//api/data/etl-mappings/{mapping_id}" + assert patch_call.args[1] == f"//api/ogc/collections/etl-mappings/items/{mapping_id}" sent_body = json.loads(patch_call.kwargs["data"]) assert sent_body == {"sourceIdentifier": "sensor_2"} diff --git a/packages/hydroserverpy/tests/api/test_linked_resources.py b/packages/hydroserverpy/tests/api/test_linked_resources.py new file mode 100644 index 00000000..e7a6c6f1 --- /dev/null +++ b/packages/hydroserverpy/tests/api/test_linked_resources.py @@ -0,0 +1,118 @@ +import uuid +from unittest.mock import MagicMock + +import pytest + +from hydroserverpy.api.services.sta.datastream import DatastreamService +from hydroserverpy.api.services.sta.monitoring_site import MonitoringSiteService + + +class FakeResponse: + def __init__(self, payload=None, status_code=200): + self._payload = payload + self.status_code = status_code + + def json(self): + if self._payload is None: + raise ValueError("No JSON content (empty response body)") + return self._payload + + +def make_client(): + client = MagicMock() + client.base_route = "/api/ogc" + return client + + +def linked_resource_payload(linked_resource_id, name="Report A"): + return { + "id": str(linked_resource_id), + "name": name, + "description": None, + "type": "Report", + "link": f"https://example.com/{name}", + } + + +@pytest.fixture(params=[DatastreamService, MonitoringSiteService]) +def service(request): + return request.param(make_client()) + + +def test_get_linked_resources_unwraps_data_and_forwards_params(service): + parent_id = uuid.uuid4() + items = [linked_resource_payload(uuid.uuid4())] + service.client.request.return_value = FakeResponse( + {"data": items, "meta": {"limit": 10, "offset": 20, "totalCount": 21}, "links": []} + ) + + result = service.get_linked_resources(parent_id, offset=20, limit=10, type=["Report"]) + + assert result == items + method, path = service.client.request.call_args.args + assert method == "get" + assert path.endswith(f"/{parent_id}/linked-resources") + assert service.client.request.call_args.kwargs["params"] == { + "offset": 20, + "limit": 10, + "type": ["Report"], + } + + +def test_get_linked_resources_omits_unset_params(service): + service.client.request.return_value = FakeResponse({"data": [], "meta": {}, "links": []}) + + service.get_linked_resources(uuid.uuid4()) + + assert service.client.request.call_args.kwargs["params"] == {} + + +def test_get_linked_resource_unwraps_data(service): + parent_id = uuid.uuid4() + linked_resource_id = uuid.uuid4() + item = linked_resource_payload(linked_resource_id) + service.client.request.return_value = FakeResponse({"data": item, "links": []}) + + result = service.get_linked_resource(parent_id, linked_resource_id) + + assert result == item + service.client.request.assert_called_once() + method, path = service.client.request.call_args.args + assert method == "get" + assert path.endswith(f"/{parent_id}/linked-resources/{linked_resource_id}") + + +def test_add_linked_resource_returns_created_item(service): + parent_id = uuid.uuid4() + linked_resource_id = uuid.uuid4() + item = linked_resource_payload(linked_resource_id) + service.client.request.side_effect = [ + FakeResponse({"id": str(linked_resource_id)}, status_code=201), + FakeResponse({"data": item, "links": []}), + ] + + result = service.add_linked_resource( + parent_id, name="Report A", type="Report", url="https://example.com/Report A" + ) + + assert result == item + method, path = service.client.request.call_args.args + assert method == "get" + assert path.endswith(f"/{parent_id}/linked-resources/{linked_resource_id}") + + +def test_update_linked_resource_returns_updated_item(service): + parent_id = uuid.uuid4() + linked_resource_id = uuid.uuid4() + item = linked_resource_payload(linked_resource_id, name="Renamed") + service.client.request.side_effect = [ + FakeResponse(status_code=204), + FakeResponse({"data": item, "links": []}), + ] + + result = service.update_linked_resource(parent_id, linked_resource_id, name="Renamed") + + assert result == item + method, path = service.client.request.call_args.args + assert method == "get" + assert path.endswith(f"/{parent_id}/linked-resources/{linked_resource_id}") diff --git a/packages/hydroserverpy/tests/api/test_metadata.py b/packages/hydroserverpy/tests/api/test_metadata.py index 777e8b00..887f19ca 100644 --- a/packages/hydroserverpy/tests/api/test_metadata.py +++ b/packages/hydroserverpy/tests/api/test_metadata.py @@ -19,7 +19,7 @@ (UnitService, {"type": "Length", "symbol": "m"}), ]) def test_create_metadata_without_optional_fields(service_class, fields): - client = MagicMock(base_route="api/data") + client = MagicMock(base_route="api/ogc") uid = str(uuid4()) payload = {"id": uid, "name": "Name", **fields} if service_class != ResultQualifierService: @@ -55,7 +55,7 @@ def test_create_metadata_without_optional_fields(service_class, fields): {"name": "Raw", "description": "Unprocessed", "code": "0"}), ]) def test_metadata_create_argument_mapping(service_class, args, kwargs, expected): - client = MagicMock(base_route="api/data") + client = MagicMock(base_route="api/ogc") uid, workspace_id = str(uuid4()), str(uuid4()) payload = {"id": uid, "workspaceId": workspace_id, **expected} client.request.side_effect = [ @@ -81,7 +81,7 @@ def test_metadata_create_argument_mapping(service_class, args, kwargs, expected) (ResultQualifierService, (), {"description": "Ice affected"}, "name"), ]) def test_invalid_metadata_create_calls_fail_before_sending_data(service_class, args, kwargs, error): - client = MagicMock(base_route="api/data") + client = MagicMock(base_route="api/ogc") with pytest.raises(TypeError, match=error): service_class(client).create(*args, **kwargs) client.request.assert_not_called() @@ -94,7 +94,7 @@ def test_invalid_metadata_create_calls_fail_before_sending_data(service_class, a {"code": "0", "name": "Raw", "description": "Unprocessed", "definition": "https://example.com/raw"}), ]) def test_metadata_update_preserves_positional_argument_mapping(service_class, args, kwargs, expected): - client = MagicMock(base_route="api/data") + client = MagicMock(base_route="api/ogc") uid = str(uuid4()) payload = {"id": uid, **expected} client.request.side_effect = [ diff --git a/packages/hydroserverpy/tests/api/test_monitoring_rule.py b/packages/hydroserverpy/tests/api/test_monitoring_rule.py index 6f425626..cdd348d7 100644 --- a/packages/hydroserverpy/tests/api/test_monitoring_rule.py +++ b/packages/hydroserverpy/tests/api/test_monitoring_rule.py @@ -18,7 +18,7 @@ def json(self): def make_client(): client = MagicMock() - client.base_route = "/api/data" + client.base_route = "/api/ogc" return client @@ -55,12 +55,12 @@ def test_create_rule_posts_to_flat_route_with_task_id_in_body(): post_call = client.request.call_args_list[0] assert post_call.args[0] == "post" - assert post_call.args[1] == "//api/data/monitoring-rules" + assert post_call.args[1] == "//api/ogc/collections/monitoring-rules/items" sent_body = json.loads(post_call.kwargs["data"]) assert sent_body["taskId"] == str(task_id) get_call = client.request.call_args_list[1] - assert get_call.args[1] == f"//api/data/monitoring-rules/{rule_id}" + assert get_call.args[1] == f"//api/ogc/collections/monitoring-rules/items/{rule_id}" assert str(rule.uid) == rule_id assert rule.task_id == task_id @@ -95,7 +95,7 @@ def test_update_rule_patches_flat_route_and_refetches_after_204(): patch_call = client.request.call_args_list[0] assert patch_call.args[0] == "patch" - assert patch_call.args[1] == f"//api/data/monitoring-rules/{rule_id}" + assert patch_call.args[1] == f"//api/ogc/collections/monitoring-rules/items/{rule_id}" sent_body = json.loads(patch_call.kwargs["data"]) assert sent_body == {"windowInterval": 2} diff --git a/packages/hydroserverpy/tests/api/test_service_account_and_collaborator.py b/packages/hydroserverpy/tests/api/test_service_account_and_collaborator.py index 0fa5f81c..63e8300c 100644 --- a/packages/hydroserverpy/tests/api/test_service_account_and_collaborator.py +++ b/packages/hydroserverpy/tests/api/test_service_account_and_collaborator.py @@ -18,7 +18,7 @@ def json(self): def make_client(): client = MagicMock() - client.base_route = "/api/data" + client.base_route = "/api/ogc" return client diff --git a/packages/hydroserverpy/tests/api/test_transformation.py b/packages/hydroserverpy/tests/api/test_transformation.py index 6b3ab83e..c8eea796 100644 --- a/packages/hydroserverpy/tests/api/test_transformation.py +++ b/packages/hydroserverpy/tests/api/test_transformation.py @@ -18,7 +18,7 @@ def json(self): def make_client(): client = MagicMock() - client.base_route = "/api/data" + client.base_route = "/api/ogc" return client @@ -56,7 +56,7 @@ def test_create_rating_curve_posts_to_flat_route_with_task_id_in_body(): post_call = client.request.call_args_list[0] assert post_call.args[0] == "post" - assert post_call.args[1] == "//api/data/data-product-transformations" + assert post_call.args[1] == "//api/ogc/collections/data-product-transformations/items" sent_body = json.loads(post_call.kwargs["data"]) assert sent_body["taskId"] == str(task_id) @@ -88,7 +88,7 @@ def test_get_rating_curve_hits_the_flat_detail_route_not_a_type_slug(): service.get_rating_curve(uid=transformation_id) args, _ = client.request.call_args - assert args[1] == f"//api/data/data-product-transformations/{transformation_id}" + assert args[1] == f"//api/ogc/collections/data-product-transformations/items/{transformation_id}" def test_list_derivation_filters_by_task_and_type_on_the_flat_route(): @@ -101,7 +101,7 @@ def test_list_derivation_filters_by_task_and_type_on_the_flat_route(): service.list_derivation(task_id=task_id) args, kwargs = client.request.call_args - assert args[1] == "//api/data/data-product-transformations" + assert args[1] == "//api/ogc/collections/data-product-transformations/items" assert kwargs["params"]["transformation_type"] == "derivation" assert kwargs["params"]["task_id"] == str(task_id) @@ -141,5 +141,5 @@ def test_update_aggregation_patches_flat_route_then_refetches_by_id(): patch_call = client.request.call_args_list[0] assert patch_call.args[0] == "patch" - assert patch_call.args[1] == f"//api/data/data-product-transformations/{transformation_id}" + assert patch_call.args[1] == f"//api/ogc/collections/data-product-transformations/items/{transformation_id}" assert str(transformation.input_datastream_id) == input_id diff --git a/packages/hydroserverpy/tests/test_api_client.py b/packages/hydroserverpy/tests/test_api_client.py index a6a7f066..1cdaeabc 100644 --- a/packages/hydroserverpy/tests/test_api_client.py +++ b/packages/hydroserverpy/tests/test_api_client.py @@ -1,3 +1,5 @@ +import json + import pytest import requests @@ -9,6 +11,9 @@ def __init__(self, status_code=200, content=b"{}"): self.status_code = status_code self.content = content + def json(self): + return json.loads(self.content) + def raise_for_status(self): if self.status_code >= 400: raise requests.HTTPError(response=self) @@ -28,6 +33,7 @@ def close(self): self.closed = True def _request(self, method, *args, **kwargs): + self.last_kwargs = kwargs response = self._responses[method].pop(0) if isinstance(response, Exception): raise response @@ -115,9 +121,62 @@ def next_session(): monkeypatch.setattr(client_module.requests, "Session", next_session) - response = hs.request("get", "/api/data/workspaces") + response = hs.request("get", "/api/ogc/collections/workspaces/items") assert response is second_response assert fake_session_factory[0].closed is True assert hs._session is replacement_session assert hs._session.auth == ("user@example.com", "secret") + + +def test_hydroserver_encodes_boolean_query_params_in_lowercase(fake_session_factory): + hs = client_module.HydroServer(host="https://example.com", apikey="hs_test_api_key") + fake_session_factory[0].queue("get", FakeResponse()) + + hs.request( + "get", + "/api/ogc/collections/workspaces/items", + params={"is_private": True, "is_associated": False, "workspace_id": ["a", "null"], "limit": 5}, + ) + + assert fake_session_factory[0].last_kwargs["params"] == { + "is_private": "true", + "is_associated": "false", + "workspace_id": ["a", "null"], + "limit": 5, + } + + +def test_workspace_list_sends_lowercase_booleans(fake_session_factory): + hs = client_module.HydroServer(host="https://example.com", apikey="hs_test_api_key") + fake_session_factory[0].queue( + "get", FakeResponse(content=b'{"data": [], "meta": {"limit": 100, "offset": 0, "totalCount": 0}}') + ) + + hs.workspaces.list(is_private=True, is_associated=False) + + url = requests.Request( + "GET", "https://example.com", params=fake_session_factory[0].last_kwargs["params"] + ).prepare().url + assert "is_private=true" in url + assert "is_associated=false" in url + + +RUN_ID = "01a0e94d-307f-7347-8f40-66fb0327d09e" +RUN_RESPONSE = json.dumps( + {"data": {"id": RUN_ID, "status": "PENDING"}, "included": None, "links": []} +).encode() + + +@pytest.mark.parametrize("service", ["etltasks", "monitoringtasks", "dataproducttasks"]) +def test_task_trigger_and_get_run_unwrap_the_item_envelope(fake_session_factory, service): + hs = client_module.HydroServer(host="https://example.com", apikey="hs_test_api_key") + fake_session_factory[0].queue("post", FakeResponse(status_code=202, content=RUN_RESPONSE)) + fake_session_factory[0].queue("get", FakeResponse(content=RUN_RESPONSE)) + + triggered = getattr(hs, service).trigger("task-1") + fetched = getattr(hs, service).get_run("task-1", RUN_ID) + + assert str(triggered.id) == RUN_ID + assert triggered.status == "PENDING" + assert str(fetched.id) == RUN_ID